Skip to content

Move export validation from backend errors to frontend warnings#415

Draft
travisformayor wants to merge 3 commits intofinalfrom
pr/export-validation
Draft

Move export validation from backend errors to frontend warnings#415
travisformayor wants to merge 3 commits intofinalfrom
pr/export-validation

Conversation

@travisformayor
Copy link
Copy Markdown
Contributor

Export errors during auto-save created a stream of toast notifications in SynBioSuite.

  • Backend (MxToSBML.java, MxToSBOL.java) skips incomplete elements (missing promoters, disconnected edges, null species) and logs to stderr instead of throwing
  • Frontend ProblemsComponent checks for the same conditions and shows warnings in the Problems dropdown
  • Removed error postMessage from auto-export sending all the toast notifications to SynBioSuite
  • Fixed setInterval leak in ProblemsComponent, timer was never cleared on destroy
  • Fixed SBML parameter names from kr_f_<id> to kr_<id>_f to match iBioSim
  • Reduced SBML auto-export debounce from 2000ms to 1000ms

@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://blue-flower-0c36fd81e-415.westus2.5.azurestaticapps.net

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ProblemsComponent and 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.

Comment on lines 506 to 510
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());
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to 2
import { Component, OnDestroy } from '@angular/core'
import { GraphService } from '../graph.service'
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 874 to 883
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;
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines 1032 to 1055
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;
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +388 to 397
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
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines 468 to +484
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;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 525 to 548
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();
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
.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());
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines 965 to 982
/**
* 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;
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants