From aba00c623e2dfe552943ed010b7da4f80e534e6b Mon Sep 17 00:00:00 2001 From: xts-Michi Date: Wed, 6 Aug 2025 13:58:00 +0200 Subject: [PATCH 01/11] feat(optsolvx): add OptSolvXAdapter with model validation and debug logging --- pom.xml | 7 +++ .../org/simulator/optsolvx/OptSolvXDemo.java | 7 +++ .../optsolvx/OptSolvXSolverAdapter.java | 55 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 src/main/java/org/simulator/optsolvx/OptSolvXDemo.java create mode 100644 src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java diff --git a/pom.xml b/pom.xml index e162e1e8..e8061850 100644 --- a/pom.xml +++ b/pom.xml @@ -471,6 +471,13 @@ + + + + org.optsolvx + optsolvx + 0.1.0-SNAPSHOT + diff --git a/src/main/java/org/simulator/optsolvx/OptSolvXDemo.java b/src/main/java/org/simulator/optsolvx/OptSolvXDemo.java new file mode 100644 index 00000000..561e1be4 --- /dev/null +++ b/src/main/java/org/simulator/optsolvx/OptSolvXDemo.java @@ -0,0 +1,7 @@ +package org.simulator.optsolvx; + +public class OptSolvXDemo { + public static void main(String[] args) { + + } +} diff --git a/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java b/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java new file mode 100644 index 00000000..f7c268f5 --- /dev/null +++ b/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java @@ -0,0 +1,55 @@ +package org.simulator.optsolvx; + +import org.optsolvx.model.AbstractLPModel; +import org.optsolvx.solver.LPSolution; +import org.optsolvx.solver.LPSolverAdapter; + +import java.text.MessageFormat; +import java.util.logging.Logger; + +public class OptSolvXSolverAdapter implements LPSolverAdapter { + private static final Logger LOGGER = + Logger.getLogger(OptSolvXSolverAdapter.class.getName()); + + private final LPSolverAdapter backend; + private final boolean debug; + + /** + * @param backend concrete OptSolvX solver (e.g. CommonsMathSolver) + * @param debug enable verbose logging + */ + public OptSolvXSolverAdapter(LPSolverAdapter backend, boolean debug) { + this.backend = backend; + this.debug = debug; + } + + @Override + public LPSolution solve(AbstractLPModel model) { + if (!model.isBuilt()) { + throw new IllegalStateException("Model must be built() before solving."); + } + + if (debug) { + LOGGER.info(MessageFormat.format( + "{0}: solving with {1} (vars={2}, cons={3})", + getClass().getSimpleName(), + backend.getClass().getSimpleName(), + model.getVariables().size(), + model.getConstraints().size() + )); + } + + LPSolution sol = backend.solve(model); + + if (debug) { + LOGGER.info(MessageFormat.format( + "{0}: result feasible={1}, objective={2}", + getClass().getSimpleName(), + sol.isFeasible(), + sol.getObjectiveValue() + )); + } + + return sol; + } +} From be38d5c17990db39650beeb92c96083e3264f5c5 Mon Sep 17 00:00:00 2001 From: xts-Michi Date: Sun, 10 Aug 2025 19:23:27 +0200 Subject: [PATCH 02/11] feat(optsolvx): add working demo --- .../org/simulator/optsolvx/OptSolvXDemo.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/main/java/org/simulator/optsolvx/OptSolvXDemo.java b/src/main/java/org/simulator/optsolvx/OptSolvXDemo.java index 561e1be4..f5ed8676 100644 --- a/src/main/java/org/simulator/optsolvx/OptSolvXDemo.java +++ b/src/main/java/org/simulator/optsolvx/OptSolvXDemo.java @@ -1,7 +1,50 @@ package org.simulator.optsolvx; +import org.optsolvx.model.AbstractLPModel; +import org.optsolvx.model.Constraint; +import org.optsolvx.model.OptimizationDirection; +import org.optsolvx.solver.CommonsMathSolver; +import org.optsolvx.solver.LPSolution; +import org.optsolvx.solver.LPSolverAdapter; + +import java.util.HashMap; +import java.util.Map; + public class OptSolvXDemo { public static void main(String[] args) { + // Build a tiny LP: maximize x + y + AbstractLPModel model = new AbstractLPModel(); + model.addVariable("x", 0.0d, Double.POSITIVE_INFINITY); + model.addVariable("y", 0.0d, Double.POSITIVE_INFINITY); + + // Objective: max x + y (Java 8 style map) + Map objective = new HashMap<>(); + objective.put("x", 1.0d); + objective.put("y", 1.0d); + model.setObjective(objective, OptimizationDirection.MAXIMIZE); + + // Constraints (Java 8 style maps) + Map c1 = new HashMap<>(); + c1.put("x", 1.0d); + c1.put("y", 2.0d); + model.addConstraint("c1", c1, Constraint.Relation.LEQ, 4.0d); + + Map c2 = new HashMap<>(); + c2.put("x", 1.0d); + model.addConstraint("c2", c2, Constraint.Relation.LEQ, 3.0d); + + // Finalize model + model.build(); + + // Solve via adapter, using CommonsMath as backend + LPSolverAdapter backend = new CommonsMathSolver(); + LPSolverAdapter solver = new OptSolvXSolverAdapter(backend, /*debug=*/true); + + LPSolution sol = solver.solve(model); + // Print result (expected optimum: x=3.0, y=0.5, objective=3.5) + System.out.println("Variables: " + sol.getVariableValues()); + System.out.println("Objective: " + sol.getObjectiveValue()); + System.out.println("Feasible: " + sol.isFeasible()); } } From 1c63b823e9ddb48924029d152ddded870ea65ade Mon Sep 17 00:00:00 2001 From: xts-Michi Date: Sun, 10 Aug 2025 19:33:18 +0200 Subject: [PATCH 03/11] test(optsolvx): add JUnit test for OptSolvXSolverAdapter (build check + simple LP) --- .../optsolvx/OptSolvXSolverAdapterTest.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/test/java/org/simulator/optsolvx/OptSolvXSolverAdapterTest.java diff --git a/src/test/java/org/simulator/optsolvx/OptSolvXSolverAdapterTest.java b/src/test/java/org/simulator/optsolvx/OptSolvXSolverAdapterTest.java new file mode 100644 index 00000000..0103872a --- /dev/null +++ b/src/test/java/org/simulator/optsolvx/OptSolvXSolverAdapterTest.java @@ -0,0 +1,49 @@ +package org.simulator.optsolvx; + +import org.junit.Test; +import static org.junit.Assert.*; +import org.optsolvx.model.*; +import org.optsolvx.solver.*; + +import java.util.HashMap; +import java.util.Map; + +public class OptSolvXSolverAdapterTest { + + @Test(expected = IllegalStateException.class) + public void solve_requires_build() { + AbstractLPModel m = new AbstractLPModel(); + m.addVariable("x", 0.0d, 10.0d); + LPSolverAdapter s = new OptSolvXSolverAdapter(new CommonsMathSolver(), false); + s.solve(m); // not built -> must throw + } + + @Test + public void solves_simple_lp() { + AbstractLPModel m = new AbstractLPModel(); + m.addVariable("x", 0.0d, Double.POSITIVE_INFINITY); + m.addVariable("y", 0.0d, Double.POSITIVE_INFINITY); + + Map obj = new HashMap<>(); + obj.put("x", 1.0d); obj.put("y", 1.0d); + m.setObjective(obj, OptimizationDirection.MAXIMIZE); + + Map c1 = new HashMap<>(); + c1.put("x", 1.0d); c1.put("y", 2.0d); + m.addConstraint("c1", c1, Constraint.Relation.LEQ, 4.0d); + + Map c2 = new HashMap<>(); + c2.put("x", 1.0d); + m.addConstraint("c2", c2, Constraint.Relation.LEQ, 3.0d); + + m.build(); + + LPSolverAdapter s = new OptSolvXSolverAdapter(new CommonsMathSolver(), false); + LPSolution sol = s.solve(m); + + assertTrue(sol.isFeasible()); + assertEquals(3.5d, sol.getObjectiveValue(), 1e-6d); + assertEquals(3.0d, sol.getVariableValues().get("x"), 1e-6d); + assertEquals(0.5d, sol.getVariableValues().get("y"), 1e-6d); + } +} From 36e4b99e5c7a94ccbac12da9d49d038d46a28840 Mon Sep 17 00:00:00 2001 From: xts-Michi Date: Sun, 10 Aug 2025 23:02:07 +0200 Subject: [PATCH 04/11] Updated README with OptSolvX User Guide --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index a355d23f..67f1b5a9 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,21 @@ Further examples can be found directly within this repository in the [Examples P * How to run a [dynamic simulation](https://github.com/draeger-lab/SBSCL/blob/master/src/main/java/org/simulator/examples/SimulatorExample.java) * How to run a [stochastic simulation](https://github.com/draeger-lab/SBSCL/blob/master/src/main/java/fern/Start.java) +## Using OptSolvX (LP) in SBSCL + +### Run the OptSolvX demo +- In your IDE, run the main class: `org.simulator.optsolvx.OptSolvXDemo`. +- The demo builds a tiny LP and solves it via OptSolvX (CommonsMath backend just yet). + +> Note: The demo assumes OptSolvX is on the classpath (added as a dependency in SBSCL’s `pom.xml`). + +### Enable debug logs +Construct the adapter with `debug = true`: +```java +LPSolverAdapter solver = + new OptSolvXSolverAdapter(new CommonsMathSolver(), true); +``` + ### Comparison to Similar Libraries From 6ac8cc078b6b3a96da98e0c84fb86cc2523f4b40 Mon Sep 17 00:00:00 2001 From: xts-Michi Date: Sun, 10 Aug 2025 23:02:46 +0200 Subject: [PATCH 05/11] test: added more tests --- .../optsolvx/OptSolvXSolverAdapterTest.java | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/test/java/org/simulator/optsolvx/OptSolvXSolverAdapterTest.java b/src/test/java/org/simulator/optsolvx/OptSolvXSolverAdapterTest.java index 0103872a..c36b9716 100644 --- a/src/test/java/org/simulator/optsolvx/OptSolvXSolverAdapterTest.java +++ b/src/test/java/org/simulator/optsolvx/OptSolvXSolverAdapterTest.java @@ -10,6 +10,12 @@ public class OptSolvXSolverAdapterTest { + @Test(expected = IllegalArgumentException.class) + public void solve_requires_non_null_model() { + LPSolverAdapter s = new OptSolvXSolverAdapter(new CommonsMathSolver(), false); + s.solve(null); // must throw + } + @Test(expected = IllegalStateException.class) public void solve_requires_build() { AbstractLPModel m = new AbstractLPModel(); @@ -19,22 +25,21 @@ public void solve_requires_build() { } @Test - public void solves_simple_lp() { + public void smoke_minimize_with_eq_and_bounds() { + // Minimize 2x + y, s.t. x + y = 5, 0 <= x,y <= 5 AbstractLPModel m = new AbstractLPModel(); - m.addVariable("x", 0.0d, Double.POSITIVE_INFINITY); - m.addVariable("y", 0.0d, Double.POSITIVE_INFINITY); + m.addVariable("x", 0.0d, 5.0d); + m.addVariable("y", 0.0d, 5.0d); Map obj = new HashMap<>(); - obj.put("x", 1.0d); obj.put("y", 1.0d); - m.setObjective(obj, OptimizationDirection.MAXIMIZE); - - Map c1 = new HashMap<>(); - c1.put("x", 1.0d); c1.put("y", 2.0d); - m.addConstraint("c1", c1, Constraint.Relation.LEQ, 4.0d); + obj.put("x", 2.0d); + obj.put("y", 1.0d); + m.setObjective(obj, OptimizationDirection.MINIMIZE); - Map c2 = new HashMap<>(); - c2.put("x", 1.0d); - m.addConstraint("c2", c2, Constraint.Relation.LEQ, 3.0d); + Map eq = new HashMap<>(); + eq.put("x", 1.0d); + eq.put("y", 1.0d); + m.addConstraint("sum", eq, Constraint.Relation.EQ, 5.0d); m.build(); @@ -42,8 +47,9 @@ public void solves_simple_lp() { LPSolution sol = s.solve(m); assertTrue(sol.isFeasible()); - assertEquals(3.5d, sol.getObjectiveValue(), 1e-6d); - assertEquals(3.0d, sol.getVariableValues().get("x"), 1e-6d); - assertEquals(0.5d, sol.getVariableValues().get("y"), 1e-6d); + // Optimum at x=0, y=5 -> objective = 5 + assertEquals(5.0d, sol.getObjectiveValue(), 1e-6d); + assertEquals(0.0d, sol.getVariableValues().get("x"), 1e-6d); + assertEquals(5.0d, sol.getVariableValues().get("y"), 1e-6d); } } From e72f950057f6a2efd1bff343e185f9e8ec919b11 Mon Sep 17 00:00:00 2001 From: xts-Michi Date: Sun, 10 Aug 2025 23:03:54 +0200 Subject: [PATCH 06/11] feat: added better error handling and a timer to track the time while logging --- .../org/simulator/optsolvx/OptSolvXSolverAdapter.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java b/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java index f7c268f5..10e67692 100644 --- a/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java +++ b/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java @@ -25,6 +25,9 @@ public OptSolvXSolverAdapter(LPSolverAdapter backend, boolean debug) { @Override public LPSolution solve(AbstractLPModel model) { + if (model == null) { + throw new IllegalArgumentException("model must not be null"); + } if (!model.isBuilt()) { throw new IllegalStateException("Model must be built() before solving."); } @@ -39,17 +42,19 @@ public LPSolution solve(AbstractLPModel model) { )); } + long t0 = System.nanoTime(); LPSolution sol = backend.solve(model); + long dtMs = (System.nanoTime() - t0) / 1_000_000L; if (debug) { LOGGER.info(MessageFormat.format( - "{0}: result feasible={1}, objective={2}", + "{0}: result feasible={1}, objective={2}, time={3} ms", getClass().getSimpleName(), sol.isFeasible(), - sol.getObjectiveValue() + sol.getObjectiveValue(), + dtMs )); } - return sol; } } From c8edbe43b499ce07adc18473dd2555debc79bb18 Mon Sep 17 00:00:00 2001 From: xts-Michi Date: Tue, 19 Aug 2025 14:06:31 +0200 Subject: [PATCH 07/11] feat: add SBML to OptSolvX bridge skeleton (FbaToOptSolvX) --- .../org/simulator/optsolvx/FbaToOptSolvX.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/org/simulator/optsolvx/FbaToOptSolvX.java diff --git a/src/main/java/org/simulator/optsolvx/FbaToOptSolvX.java b/src/main/java/org/simulator/optsolvx/FbaToOptSolvX.java new file mode 100644 index 00000000..ccd85e47 --- /dev/null +++ b/src/main/java/org/simulator/optsolvx/FbaToOptSolvX.java @@ -0,0 +1,20 @@ +package org.simulator.optsolvx; + +import org.optsolvx.model.AbstractLPModel; +import org.sbml.jsbml.SBMLDocument; + +/** Maps SBML/FBC to an OptSolvX LP model. */ +public final class FbaToOptSolvX { + + private FbaToOptSolvX() {} + + /** Build an OptSolvX model from SBML/FBC. */ + public static AbstractLPModel fromSBML(SBMLDocument doc) { + // TODO: extract reactions -> variables + // TODO: bounds from FBC + // TODO: S·v = 0 constraints + // TODO: objective (coeffs + direction) + // TODO: model.build() + return null; // placeholder + } +} From ae037928d0b4611f2d35ff3d577fea4dd3f3f6d3 Mon Sep 17 00:00:00 2001 From: xts-Michi Date: Wed, 20 Aug 2025 22:01:17 +0200 Subject: [PATCH 08/11] feat(sbscl): harden OptSolvXSolverAdapter (final + null-check + 1-arg ctor) and changed the pom OptSolvX verison to java 8 --- pom.xml | 1 + .../org/simulator/optsolvx/OptSolvXSolverAdapter.java | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e8061850..de26c1f9 100644 --- a/pom.xml +++ b/pom.xml @@ -477,6 +477,7 @@ org.optsolvx optsolvx 0.1.0-SNAPSHOT + jdk8 diff --git a/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java b/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java index 10e67692..b423f51d 100644 --- a/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java +++ b/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java @@ -4,10 +4,11 @@ import org.optsolvx.solver.LPSolution; import org.optsolvx.solver.LPSolverAdapter; +import java.util.Objects; import java.text.MessageFormat; import java.util.logging.Logger; -public class OptSolvXSolverAdapter implements LPSolverAdapter { +public final class OptSolvXSolverAdapter implements LPSolverAdapter { private static final Logger LOGGER = Logger.getLogger(OptSolvXSolverAdapter.class.getName()); @@ -19,10 +20,15 @@ public class OptSolvXSolverAdapter implements LPSolverAdapter { * @param debug enable verbose logging */ public OptSolvXSolverAdapter(LPSolverAdapter backend, boolean debug) { - this.backend = backend; + this.backend = Objects.requireNonNull(backend, "backend must not be null"); this.debug = debug; } + /** Convenience: debug disabled by default. */ + public OptSolvXSolverAdapter(LPSolverAdapter backend) { + this(backend, false); + } + @Override public LPSolution solve(AbstractLPModel model) { if (model == null) { From f2afc173b6c8fc2b5339eb23630ae6aa177cda6b Mon Sep 17 00:00:00 2001 From: xts-Michi Date: Wed, 20 Aug 2025 22:13:30 +0200 Subject: [PATCH 09/11] =?UTF-8?q?feat(sbscl-bridge):=20add=20FbaToOptSolvX?= =?UTF-8?q?=20to=20map=20SBML/FBC=20(reactions,=20bounds,=20S=C2=B7v=3D0,?= =?UTF-8?q?=20objective)=20into=20OptSolvX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/simulator/optsolvx/FbaToOptSolvX.java | 194 +++++++++++++++++- 1 file changed, 184 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/simulator/optsolvx/FbaToOptSolvX.java b/src/main/java/org/simulator/optsolvx/FbaToOptSolvX.java index ccd85e47..300ef4bb 100644 --- a/src/main/java/org/simulator/optsolvx/FbaToOptSolvX.java +++ b/src/main/java/org/simulator/optsolvx/FbaToOptSolvX.java @@ -1,20 +1,194 @@ package org.simulator.optsolvx; import org.optsolvx.model.AbstractLPModel; -import org.sbml.jsbml.SBMLDocument; +import org.optsolvx.model.Constraint; +import org.optsolvx.model.OptimizationDirection; +import org.sbml.jsbml.*; +import org.sbml.jsbml.ext.fbc.*; -/** Maps SBML/FBC to an OptSolvX LP model. */ +import java.text.MessageFormat; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Logger; + +/** + * SBML/FBC -> OptSolvX bridge (minimal functional version). + * Maps reactions to variables (with bounds), builds S·v=0 constraints, and sets the active objective. + * NOTE: This intentionally ignores InitialAssignments and StoichiometryMath for now (TODO). + */ public final class FbaToOptSolvX { + private static final Logger LOGGER = Logger.getLogger(FbaToOptSolvX.class.getName()); - private FbaToOptSolvX() {} + private FbaToOptSolvX() { /* no instances */ } - /** Build an OptSolvX model from SBML/FBC. */ + /** Build an OptSolvX LP model from SBML/FBC (v1 or v2). */ public static AbstractLPModel fromSBML(SBMLDocument doc) { - // TODO: extract reactions -> variables - // TODO: bounds from FBC - // TODO: S·v = 0 constraints - // TODO: objective (coeffs + direction) - // TODO: model.build() - return null; // placeholder + if (doc == null || !doc.isSetModel()) { + throw new IllegalArgumentException("SBMLDocument must contain a Model."); + } + final Model m = doc.getModel(); + + // Prepare LP model + final AbstractLPModel lp = new AbstractLPModel(); + + // Resolve FBC namespaces for level/version + final int level = doc.getLevel(); + final int version = doc.getVersion(); + final String fbcNSv1 = FBCConstants.getNamespaceURI(level, version, 1); + final String fbcNSv2 = FBCConstants.getNamespaceURI(level, version, 2); + + // --- 1) Variables (reactions) with bounds --------------------------------------------- + // Default bounds if FBC has no values: 0 .. +inf (conventional for FBA) + final Map lb = new LinkedHashMap<>(); + final Map ub = new LinkedHashMap<>(); + + for (Reaction r : m.getListOfReactions()) { + String rid = r.getId(); + double lower = 0.0d; + double upper = Double.POSITIVE_INFINITY; + + // FBC v2: bounds via FBCReactionPlugin lower/upper "Parameter" instances + if (r.isSetPlugin(fbcNSv2)) { + FBCReactionPlugin rp = (FBCReactionPlugin) r.getPlugin(fbcNSv2); + Parameter lpi = rp.getLowerFluxBoundInstance(); + Parameter upi = rp.getUpperFluxBoundInstance(); + if (lpi != null) lower = valueOf(lpi); + if (upi != null) upper = valueOf(upi); + } + // Store preliminary bounds; FBC v1 may override below via FluxBounds list + lb.put(rid, lower); + ub.put(rid, upper); + } + + // FBC v1: FluxBounds at model level (override) + FBCModelPlugin mpV1 = (FBCModelPlugin) m.getPlugin(fbcNSv1); + if (mpV1 != null && mpV1.isSetListOfFluxBounds()) { + for (FluxBound fb : mpV1.getListOfFluxBounds()) { + String rid = fb.getReaction(); + if (rid == null) continue; + switch (fb.getOperation()) { // Java 8 compatible switch + case GREATER_EQUAL: + lb.put(rid, fb.getValue()); + break; + case LESS_EQUAL: + ub.put(rid, fb.getValue()); + break; + case EQUAL: + lb.put(rid, fb.getValue()); + ub.put(rid, fb.getValue()); + break; + default: + LOGGER.warning("FBC v1: Unsupported FluxBound operation on " + rid); + } + } + } + + // Add variables to LP + for (Reaction r : m.getListOfReactions()) { + String rid = r.getId(); + double lower = nvl(lb.get(rid), 0.0d); + double upper = nvl(ub.get(rid), Double.POSITIVE_INFINITY); + lp.addVariable(rid, lower, upper); + } + + // --- 2) Mass-balance constraints S·v = 0 (ignore boundary species) --------------------- + for (Species s : m.getListOfSpecies()) { + boolean isBoundary = s.isSetBoundaryCondition() && s.getBoundaryCondition(); + if (isBoundary) continue; + + Map coeffs = new LinkedHashMap<>(); + // For each reaction, accumulate stoichiometry of species s + for (Reaction r : m.getListOfReactions()) { + double sum = 0.0d; + // Reactants contribute negative stoichiometry + for (SpeciesReference sr : r.getListOfReactants()) { + if (s.getId().equals(sr.getSpecies())) { + sum += -stoich(sr); + } + } + // Products contribute positive stoichiometry + for (SpeciesReference sr : r.getListOfProducts()) { + if (s.getId().equals(sr.getSpecies())) { + sum += +stoich(sr); + } + } + if (sum != 0.0d) { + coeffs.put(r.getId(), sum); + } + } + if (!coeffs.isEmpty()) { + lp.addConstraint("mb_" + s.getId(), coeffs, Constraint.Relation.EQ, 0.0d); + } else { + // Optional: many species simply don't appear; keep quiet to avoid noisy logs + } + } + + // --- 3) Objective (active FBC objective) ----------------------------------------------- + Map obj = new LinkedHashMap<>(); + OptimizationDirection dir = OptimizationDirection.MAXIMIZE; // sensible default + + // Prefer FBC v2 at model level; fall back to v1 + FBCModelPlugin mpV2 = (FBCModelPlugin) m.getPlugin(fbcNSv2); + FBCModelPlugin mp = (mpV2 != null) ? mpV2 : mpV1; + + if (mp != null && mp.isSetActiveObjective()) { + Objective o = mp.getActiveObjectiveInstance(); + if (o != null) { + // Direction + Objective.Type t = o.getType(); + if (t == Objective.Type.MINIMIZE) { + dir = OptimizationDirection.MINIMIZE; + } else if (t == Objective.Type.MAXIMIZE) { + dir = OptimizationDirection.MAXIMIZE; + } + // Coefficients + for (FluxObjective fo : o.getListOfFluxObjectives()) { + String rid = fo.getReaction(); + if (rid != null) obj.put(rid, fo.getCoefficient()); + } + } else { + LOGGER.warning("No active FBC Objective instance set; using empty objective."); + } + } else { + LOGGER.warning("FBC Objective not set; using empty objective (objective=0)."); + } + + if (!obj.isEmpty()) { + lp.setObjective(obj, dir); + } else { + // Keep objective empty → objective value will be 0.0; direction still recorded if needed + lp.setObjective(new LinkedHashMap(), dir); + } + + // --- 4) Finalize & Log ----------------------------------------------------------------- + lp.build(); + LOGGER.info(MessageFormat.format( + "FbaToOptSolvX: built LP (vars={0}, cons={1}, objectiveVars={2}, dir={3})", + lp.getVariables().size(), + lp.getConstraints().size(), + obj.size(), + dir + )); + return lp; + } + + // --- Helpers ----------------------------------------------------------------------------- + + /** Get numeric value from a Parameter (defaults to 0.0 if unset). */ + private static double valueOf(Parameter p) { + if (p == null) return 0.0d; + // Prefer explicit value; ignoring units & possible initial assignments for now (TODO) + return p.isSetValue() ? p.getValue() : 0.0d; + } + + /** Null-safe value with default. */ + private static double nvl(Double v, double def) { + return (v != null) ? v.doubleValue() : def; + } + + /** Basic stoichiometry extraction (ignores StoichiometryMath & InitialAssignments for now). */ + private static double stoich(SpeciesReference sr) { + if (sr == null) return 0.0d; + return sr.isSetStoichiometry() ? sr.getStoichiometry() : 1.0d; } } From a64d24c2b84ab903047e3379c4b651fdb9371f9f Mon Sep 17 00:00:00 2001 From: xts-Michi Date: Wed, 20 Aug 2025 22:58:17 +0200 Subject: [PATCH 10/11] feat(sbscl): add OptSolvX bridge skeleton (FbaToOptSolvX) + smoke/objective tests using jdk 8 artifact --- .../optsolvx/BridgeObjectiveTest.java | 79 +++++++++++++++++++ .../simulator/optsolvx/BridgeSmokeTest.java | 79 +++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/test/java/org/simulator/optsolvx/BridgeObjectiveTest.java create mode 100644 src/test/java/org/simulator/optsolvx/BridgeSmokeTest.java diff --git a/src/test/java/org/simulator/optsolvx/BridgeObjectiveTest.java b/src/test/java/org/simulator/optsolvx/BridgeObjectiveTest.java new file mode 100644 index 00000000..a52d6194 --- /dev/null +++ b/src/test/java/org/simulator/optsolvx/BridgeObjectiveTest.java @@ -0,0 +1,79 @@ +package org.simulator.optsolvx; + +import org.junit.Test; +import static org.junit.Assert.*; + +import org.optsolvx.model.AbstractLPModel; +import org.optsolvx.solver.LPSolution; +import org.optsolvx.solver.CommonsMathSolver; +import org.sbml.jsbml.*; +import org.sbml.jsbml.ext.fbc.*; + +public class BridgeObjectiveTest { + + @Test + public void maps_fbc_v2_bounds_and_objective() { + SBMLDocument doc = new SBMLDocument(3, 2); + Model m = doc.createModel("toy_fbc2"); + Compartment c = m.createCompartment("c"); c.setConstant(true); c.setSize(1.0); + + Species x = m.createSpecies("X"); + x.setCompartment("c"); + x.setBoundaryCondition(false); + + Reaction rin = m.createReaction("R_in"); + rin.setReversible(false); + rin.createProduct().setSpecies("X"); + + Reaction rout = m.createReaction("R_out"); + rout.setReversible(false); + rout.createReactant().setSpecies("X"); + + // --- Attach FBC v2 plugin to model and reactions + final String fbcNS = FBCConstants.getNamespaceURI(doc.getLevel(), doc.getVersion(), 2); + FBCModelPlugin fbcModel = (FBCModelPlugin) m.getPlugin(fbcNS); + if (fbcModel == null) { + fbcModel = new FBCModelPlugin(m); + m.addExtension(FBCConstants.shortLabel, fbcModel); + } + + FBCReactionPlugin fbcIn = (FBCReactionPlugin) rin.getPlugin(fbcNS); + if (fbcIn == null) { + fbcIn = new FBCReactionPlugin(rin); + rin.addExtension(FBCConstants.shortLabel, fbcIn); + } + FBCReactionPlugin fbcOut = (FBCReactionPlugin) rout.getPlugin(fbcNS); + if (fbcOut == null) { + fbcOut = new FBCReactionPlugin(rout); + rout.addExtension(FBCConstants.shortLabel, fbcOut); + } + + // Bounds via Parameters referenced by FBC (v2 style) + Parameter lbIn = m.createParameter("LB_IN"); lbIn.setConstant(true); lbIn.setValue(0.0); + Parameter ubIn = m.createParameter("UB_IN"); ubIn.setConstant(true); ubIn.setValue(10.0); + Parameter lbOut= m.createParameter("LB_OUT");lbOut.setConstant(true);lbOut.setValue(0.0); + Parameter ubOut= m.createParameter("UB_OUT");ubOut.setConstant(true);ubOut.setValue(10.0); + + fbcIn.setLowerFluxBound(lbIn.getId()); + fbcIn.setUpperFluxBound(ubIn.getId()); + fbcOut.setLowerFluxBound(lbOut.getId()); + fbcOut.setUpperFluxBound(ubOut.getId()); + + // Objective: maximize v_out + Objective obj = fbcModel.createObjective("obj"); + obj.setType(Objective.Type.MAXIMIZE); + FluxObjective fo = obj.createFluxObjective(); + fo.setReaction(rout.getId()); + fo.setCoefficient(1.0); + fbcModel.setActiveObjective(obj.getId()); + + // --- Bridge & solve + AbstractLPModel lp = FbaToOptSolvX.fromSBML(doc); + LPSolution sol = new OptSolvXSolverAdapter(new CommonsMathSolver()).solve(lp); + + assertTrue(sol.isFeasible()); + assertEquals(10.0, sol.getObjectiveValue(), 1e-6); // max v_out = 10 + assertEquals(10.0, sol.getVariableValues().get("R_in"), 1e-6); + assertEquals(10.0, sol.getVariableValues().get("R_out"), 1e-6); + } +} diff --git a/src/test/java/org/simulator/optsolvx/BridgeSmokeTest.java b/src/test/java/org/simulator/optsolvx/BridgeSmokeTest.java new file mode 100644 index 00000000..970df053 --- /dev/null +++ b/src/test/java/org/simulator/optsolvx/BridgeSmokeTest.java @@ -0,0 +1,79 @@ +package org.simulator.optsolvx; + +import org.junit.Test; +import static org.junit.Assert.*; + +import org.optsolvx.model.AbstractLPModel; +import org.optsolvx.solver.LPSolution; +import org.optsolvx.solver.CommonsMathSolver; +import org.sbml.jsbml.*; +import org.sbml.jsbml.ext.fbc.*; + +public class BridgeSmokeTest { + + @Test + public void maps_fbc_v2_bounds_and_objective() { + SBMLDocument doc = new SBMLDocument(3, 2); + Model m = doc.createModel("toy_fbc2"); + Compartment c = m.createCompartment("c"); c.setConstant(true); c.setSize(1.0); + + Species x = m.createSpecies("X"); + x.setCompartment("c"); + x.setBoundaryCondition(false); + + Reaction rin = m.createReaction("R_in"); + rin.setReversible(false); + rin.createProduct().setSpecies("X"); + + Reaction rout = m.createReaction("R_out"); + rout.setReversible(false); + rout.createReactant().setSpecies("X"); + + // --- Attach FBC v2 plugin to model and reactions + final String fbcNS = FBCConstants.getNamespaceURI(doc.getLevel(), doc.getVersion(), 2); + FBCModelPlugin fbcModel = (FBCModelPlugin) m.getPlugin(fbcNS); + if (fbcModel == null) { + fbcModel = new FBCModelPlugin(m); + m.addExtension(FBCConstants.shortLabel, fbcModel); + } + + FBCReactionPlugin fbcIn = (FBCReactionPlugin) rin.getPlugin(fbcNS); + if (fbcIn == null) { + fbcIn = new FBCReactionPlugin(rin); + rin.addExtension(FBCConstants.shortLabel, fbcIn); + } + FBCReactionPlugin fbcOut = (FBCReactionPlugin) rout.getPlugin(fbcNS); + if (fbcOut == null) { + fbcOut = new FBCReactionPlugin(rout); + rout.addExtension(FBCConstants.shortLabel, fbcOut); + } + + // Bounds via Parameters referenced by FBC (v2 style) + Parameter lbIn = m.createParameter("LB_IN"); lbIn.setConstant(true); lbIn.setValue(0.0); + Parameter ubIn = m.createParameter("UB_IN"); ubIn.setConstant(true); ubIn.setValue(10.0); + Parameter lbOut= m.createParameter("LB_OUT");lbOut.setConstant(true);lbOut.setValue(0.0); + Parameter ubOut= m.createParameter("UB_OUT");ubOut.setConstant(true);ubOut.setValue(10.0); + + fbcIn.setLowerFluxBound(lbIn.getId()); + fbcIn.setUpperFluxBound(ubIn.getId()); + fbcOut.setLowerFluxBound(lbOut.getId()); + fbcOut.setUpperFluxBound(ubOut.getId()); + + // Objective: maximize v_out + Objective obj = fbcModel.createObjective("obj"); + obj.setType(Objective.Type.MAXIMIZE); + FluxObjective fo = obj.createFluxObjective(); + fo.setReaction(rout.getId()); + fo.setCoefficient(1.0); + fbcModel.setActiveObjective(obj.getId()); + + // --- Bridge & solve + AbstractLPModel lp = FbaToOptSolvX.fromSBML(doc); + LPSolution sol = new OptSolvXSolverAdapter(new CommonsMathSolver()).solve(lp); + + assertTrue(sol.isFeasible()); + assertEquals(10.0, sol.getObjectiveValue(), 1e-6); // max v_out = 10 + assertEquals(10.0, sol.getVariableValues().get("R_in"), 1e-6); + assertEquals(10.0, sol.getVariableValues().get("R_out"), 1e-6); + } +} From 6c7e23978ac387cd01a0e1828f2febf7d0e6eaa1 Mon Sep 17 00:00:00 2001 From: xts-Michi Date: Wed, 20 Aug 2025 23:40:31 +0200 Subject: [PATCH 11/11] =?UTF-8?q?docs(README):=20add=20=E2=80=9CUsing=20Op?= =?UTF-8?q?tSolvX=E2=80=9D=20section=20(demo,=20debug=20flag)=20and=20note?= =?UTF-8?q?=20FbaToOptSolvX=20preview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 67f1b5a9..897d214c 100644 --- a/README.md +++ b/README.md @@ -80,21 +80,29 @@ Further examples can be found directly within this repository in the [Examples P * How to run a [dynamic simulation](https://github.com/draeger-lab/SBSCL/blob/master/src/main/java/org/simulator/examples/SimulatorExample.java) * How to run a [stochastic simulation](https://github.com/draeger-lab/SBSCL/blob/master/src/main/java/fern/Start.java) + ## Using OptSolvX (LP) in SBSCL ### Run the OptSolvX demo -- In your IDE, run the main class: `org.simulator.optsolvx.OptSolvXDemo`. -- The demo builds a tiny LP and solves it via OptSolvX (CommonsMath backend just yet). -> Note: The demo assumes OptSolvX is on the classpath (added as a dependency in SBSCL’s `pom.xml`). +* In your IDE, run: `org.simulator.optsolvx.OptSolvXDemo`. +* The demo builds a tiny LP and solves it via OptSolvX (CommonsMath backend for now). +* Make sure the OptSolvX **jdk8** artifact is on the classpath (declared in SBSCL’s `pom.xml`). ### Enable debug logs -Construct the adapter with `debug = true`: + +Create the adapter with `debug = true`: + ```java LPSolverAdapter solver = new OptSolvXSolverAdapter(new CommonsMathSolver(), true); ``` +### (Preview) SBML/FBC → LP + +An experimental entry point is available at `org.simulator.optsolvx.FbaToOptSolvX`. + + ### Comparison to Similar Libraries