Move export validation from backend errors to frontend warnings#415
Move export validation from backend errors to frontend warnings#415travisformayor wants to merge 3 commits intofinalfrom
Conversation
|
Azure Static Web Apps: Your stage site is ready! Visit it here: https://blue-flower-0c36fd81e-415.westus2.5.azurestaticapps.net |
There was a problem hiding this comment.
Pull request overview
Moves export-time validation from backend exceptions/toast notifications into frontend “Problems” warnings to reduce noisy autosave UX, while keeping exports best-effort.
Changes:
- Frontend: add SBML/SBOL export validation warnings in
ProblemsComponentand fix its interval cleanup. - Frontend: stop posting auto-export errors to the embedded parent and reduce SBML debounce to 1000ms.
- Backend: make SBOL/SBML exporters skip incomplete/invalid elements instead of throwing; adjust SBML parameter naming to match iBioSim.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| SBOLCanvasFrontend/src/app/problems/problems.component.ts | Adds new validation checks + clears the validation timer on destroy. |
| SBOLCanvasFrontend/src/app/graph.service.ts | Removes embedded error postMessage calls and reduces SBML debounce interval. |
| SBOLCanvasFrontend/src/app/graph-helpers.ts | Exposes event/interaction dict accessors publicly for UI validation. |
| SBOLCanvasBackend/src/utils/MxToSBOL.java | Wraps SBOL export steps in try/catch to skip invalid elements and warn to stderr. |
| SBOLCanvasBackend/src/utils/MxToSBML.java | Skips invalid SBML constructs instead of throwing; renames parameters; adjusts error handling behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try { | ||
| law.setMath(new FormulaParser(new ByteArrayInputStream(formula.getBytes(StandardCharsets.UTF_8))).parse()); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("Failed to parse repression kinetic law: " + e.getMessage(), e); | ||
| System.err.println("Warning: repression formula parse failed for " + promoterId + ": " + e.getMessage()); | ||
| } |
There was a problem hiding this comment.
If formula parsing fails, the KineticLaw has already been created and attached to the reaction; logging and continuing leaves a kinetic law without math (potentially invalid SBML). Consider removing the kinetic law / reaction (or only attaching it after successful parse) when parsing fails (same applies to the activation formula path).
| import { Component, OnDestroy } from '@angular/core' | ||
| import { GraphService } from '../graph.service' |
There was a problem hiding this comment.
TSLint enables the use-life-cycle-interface rule (codelyzer). Since this component defines ngOnInit(), it should also import and implement OnInit (currently only OnDestroy is implemented), otherwise lint/build may fail.
| KineticLaw law = reaction.createKineticLaw(); | ||
| LocalParameter kd = law.createLocalParameter("kd"); | ||
| kd.setValue(getParam(info.getSimulationData(), SBOLData.PARAM_KD, SystemsBiologyOntology.DEGRADATION, "degradation of '" + speciesId + "'")); | ||
| try { | ||
| law.setMath(new FormulaParser( | ||
| new ByteArrayInputStream(("kd * " + speciesId).getBytes(StandardCharsets.UTF_8))).parse()); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("Failed to parse degradation kinetic law: " + e.getMessage(), e); | ||
| System.err.println("Warning: degradation formula parse failed for " + speciesId + ": " + e.getMessage()); | ||
| return; | ||
| } |
There was a problem hiding this comment.
On formula parse failure this method returns after the Reaction and KineticLaw have already been created, which leaves a degradation reaction in the SBML document with no math set. To avoid generating invalid SBML, consider removing the reaction/kinetic law when parsing fails (or only creating the reaction after the formula parses successfully).
| private double getDefaultValue(URI type, String paramName, String context) { | ||
| String key = null; | ||
| if (SBOLData.roles.containsValue(type)) { | ||
| key = SBOLData.roles.getKey(type); | ||
| } else if (SBOLData.interactions.containsValue(type)) { | ||
| key = SBOLData.interactions.getKey(type); | ||
| } | ||
|
|
||
| if (key == null) { | ||
| throw new IllegalArgumentException( | ||
| "Cannot find default for parameter '" + paramName + "' on " + context + ": unknown type"); | ||
| } | ||
| if (key == null) | ||
| return 0.0; | ||
|
|
||
| LinkedHashMap<String, Object> params = SBOLData.getSimulationConfig().get(key); | ||
| if (params == null) { | ||
| throw new IllegalArgumentException( | ||
| "Cannot find default for parameter '" + paramName + "' on " + context + | ||
| ": no simulation config for type '" + key + "'"); | ||
| } | ||
| if (params == null) | ||
| return 0.0; | ||
|
|
||
| Object val = params.get(paramName); | ||
| if (val == null) { | ||
| throw new IllegalArgumentException( | ||
| "Missing required parameter '" + paramName + "' on " + context + ". Set this value in the Model tab."); | ||
| } | ||
| if (val == null) | ||
| return 0.0; | ||
|
|
||
| if (val instanceof Number) { | ||
| return ((Number) val).doubleValue(); | ||
| } | ||
| throw new IllegalArgumentException( | ||
| "Invalid default value type for parameter '" + paramName + "' on " + context); | ||
| return 0.0; | ||
| } |
There was a problem hiding this comment.
getDefaultValue now returns 0.0 when the type key, simulation config, or parameter default is missing/invalid. This hides configuration problems and can introduce divide-by-zero / incorrect kinetics (e.g., ko_f/ko_r). Consider at least logging a warning with paramName/context, and consider keeping a hard failure for missing internal defaults (as opposed to missing user-provided values).
| if (repressorEdge != null && activatorEdge != null) | ||
| continue; | ||
|
|
||
| // TODO: Unregulated promoters not supported | ||
| if (repressorEdge != null) { | ||
| buildRepressionFormula(reaction, promoterId, promoterInfo, repressorEdge); | ||
| } else if (activatorEdge != null) { | ||
| buildActivationFormula(reaction, promoterId, promoterInfo, activatorEdge); | ||
| } else { | ||
| String promoterName = promoterInfo.getName(); | ||
| if (promoterName == null || promoterName.isEmpty()) { | ||
| promoterName = promoterInfo.getDisplayID(); | ||
| } | ||
| throw new IllegalArgumentException("Promoter '" + promoterName + "' has no regulator."); | ||
| // TODO: Add constitutive (unregulated) promoter formula | ||
| } |
There was a problem hiding this comment.
continue here skips adding a kinetic law for mixed regulation, but the Reaction has already been created earlier in the loop. This leaves a production reaction in the SBML model without kinetics (and the same happens for unregulated promoters in the else block), which is likely invalid/unsimulatable output. Consider delaying createReaction until after regulation checks (or explicitly removing the reaction before continue/when no regulator is present).
| KineticLaw law = reaction.createKineticLaw(); | ||
|
|
||
| Hashtable<String, Object> promoterSimData = promoterInfo.getSimulationData(); | ||
| String promoterContext = "promoter '" + promoterId + "'"; | ||
| double ko = getParam(promoterSimData, SBOLData.PARAM_KO, SequenceOntology.PROMOTER, promoterContext); | ||
| double Ko_f = getParam(promoterSimData, SBOLData.PARAM_KO_F, SequenceOntology.PROMOTER, promoterContext); | ||
| double Ko_r = getParam(promoterSimData, SBOLData.PARAM_KO_R, SequenceOntology.PROMOTER, promoterContext); | ||
| double nr = getParam(promoterSimData, SBOLData.PARAM_NR, SequenceOntology.PROMOTER, promoterContext); | ||
|
|
||
| law.createLocalParameter("ko").setValue(ko); | ||
| law.createLocalParameter("ko_f").setValue(Ko_f); | ||
| law.createLocalParameter("ko_r").setValue(Ko_r); | ||
| law.createLocalParameter("nr").setValue(nr); | ||
|
|
||
| mxCell repCell = (mxCell) repressorEdge.getSource(); | ||
| SpeciesData repData = glyphToSpeciesData.get((String) repCell.getValue()); | ||
| if (repData == null) { | ||
| throw new IllegalArgumentException("Repressor species not found for edge"); | ||
| } | ||
| if (repData == null) return; |
There was a problem hiding this comment.
buildRepressionFormula creates a KineticLaw up front, but then returns early when repData == null. That leaves the reaction with an empty kinetic law (no Math), which can make the exported SBML invalid. Consider checking prerequisites before creating the KineticLaw, or removing the kinetic law / reaction when required inputs are missing.
| KineticLaw law = reaction.createKineticLaw(); | ||
|
|
||
| Hashtable<String, Object> promoterSimData = promoterInfo.getSimulationData(); | ||
| String promoterContext = "promoter '" + promoterId + "'"; | ||
| double kb = getParam(promoterSimData, SBOLData.PARAM_KB, SequenceOntology.PROMOTER, promoterContext); | ||
| double ka = getParam(promoterSimData, SBOLData.PARAM_KA, SequenceOntology.PROMOTER, promoterContext); | ||
| double Ko_f = getParam(promoterSimData, SBOLData.PARAM_KO_F, SequenceOntology.PROMOTER, promoterContext); | ||
| double Ko_r = getParam(promoterSimData, SBOLData.PARAM_KO_R, SequenceOntology.PROMOTER, promoterContext); | ||
| double Kao_f = getParam(promoterSimData, SBOLData.PARAM_KAO_F, SequenceOntology.PROMOTER, promoterContext); | ||
| double Kao_r = getParam(promoterSimData, SBOLData.PARAM_KAO_R, SequenceOntology.PROMOTER, promoterContext); | ||
| double nr = getParam(promoterSimData, SBOLData.PARAM_NR, SequenceOntology.PROMOTER, promoterContext); | ||
|
|
||
| law.createLocalParameter("kb").setValue(kb); | ||
| law.createLocalParameter("ka").setValue(ka); | ||
| law.createLocalParameter("ko_f").setValue(Ko_f); | ||
| law.createLocalParameter("ko_r").setValue(Ko_r); | ||
| law.createLocalParameter("kao_f").setValue(Kao_f); | ||
| law.createLocalParameter("kao_r").setValue(Kao_r); | ||
| law.createLocalParameter("nr").setValue(nr); | ||
|
|
||
| mxCell actCell = (mxCell) activatorEdge.getSource(); | ||
| SpeciesData actData = glyphToSpeciesData.get((String) actCell.getValue()); | ||
| if (actData == null) { | ||
| throw new IllegalArgumentException("Activator species not found for edge"); | ||
| } | ||
| if (actData == null) return; | ||
| String actId = actData.species.getId(); |
There was a problem hiding this comment.
buildActivationFormula creates a KineticLaw before validating that the activator species exists; returning when actData == null leaves the reaction with an empty kinetic law (no Math). Consider validating inputs first, or removing the kinetic law / reaction when an activator species can't be resolved.
| .parse()); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("Failed to parse complex formation kinetic law: " + e.getMessage(), e); | ||
| System.err.println("Warning: complex formation formula parse failed for " + productId + ": " + e.getMessage()); |
There was a problem hiding this comment.
On formula parse failure this returns after the Reaction/KineticLaw have already been created, leaving a complex formation reaction without math. That can make the exported SBML invalid. Consider deleting the reaction (or only creating it after parsing succeeds) when the rate law can't be parsed.
| System.err.println("Warning: complex formation formula parse failed for " + productId + ": " + e.getMessage()); | |
| System.err.println("Warning: complex formation formula parse failed for " + productId + ": " + e.getMessage()); | |
| // Remove the reaction to avoid leaving an invalid reaction without a kinetic law math | |
| model.removeReaction(reaction); |
| /** | ||
| * Extracts a double from a value that may be a Number, a numeric String, | ||
| * or null. Returns defaultVal for null; throws for non-numeric values. | ||
| * or null. Returns defaultVal for null, unparseable strings, or unexpected types. | ||
| */ | ||
| private static double extractDouble(Object val, double defaultVal, String context) { | ||
| if (val == null) return defaultVal; | ||
| if (val instanceof Number) return ((Number) val).doubleValue(); | ||
| private double extractDouble(Object val, double defaultVal, String context) { | ||
| if (val == null) | ||
| return defaultVal; | ||
| if (val instanceof Number) | ||
| return ((Number) val).doubleValue(); | ||
| if (val instanceof String) { | ||
| try { | ||
| return Double.parseDouble((String) val); | ||
| } catch (NumberFormatException e) { | ||
| throw new IllegalArgumentException( | ||
| "Non-numeric value for " + context + ": '" + val + "'", e); | ||
| return defaultVal; | ||
| } | ||
| } | ||
| throw new IllegalArgumentException( | ||
| "Invalid type for " + context + ". Expected number, got: " + val.getClass().getSimpleName()); | ||
| return defaultVal; | ||
| } |
There was a problem hiding this comment.
extractDouble now silently returns defaultVal for unparseable strings/unexpected types and does not use the provided context. This can mask bad simulation parameter values and make exports hard to debug. Consider emitting a warning (including context) when parsing fails or an unexpected type is encountered, or remove the unused context parameter if intentional.
Export errors during auto-save created a stream of toast notifications in SynBioSuite.
MxToSBML.java,MxToSBOL.java) skips incomplete elements (missing promoters, disconnected edges, null species) and logs to stderr instead of throwingProblemsComponentchecks for the same conditions and shows warnings in the Problems dropdownpostMessagefrom auto-export sending all the toast notifications to SynBioSuitesetIntervalleak inProblemsComponent, timer was never cleared on destroykr_f_<id>tokr_<id>_fto match iBioSim