map = new LinkedHashMap<>();
+ map.put(activeObjective, getObjectiveValue());
+ for (String rid : reactionsOrdered) {
+ final Double v = lpSolution.getVariableValues().get(rid);
+ map.put(rid, (v == null) ? 0.0d : v.doubleValue());
+ }
+ return map;
}
- return result;
- }
- /**
- * Determines the stoichiometry value of a given {@link SpeciesReference}. This might involve the
- * evaluation of a {@link org.sbml.jsbml.StoichiometryMath} or needs to lookup the current
- * stoichiometry value if it has been changed by an initial assignment.
- *
- * @param specRef the {@link SpeciesReference} whose stoichiometry value needs to be determined
- * @return a double value indicating the stoichiometry value of the given {@link
- * SpeciesReference}. This value can be directly specified by the element, or needs to be
- * calculated from its {@link org.sbml.jsbml.StoichiometryMath} or through an {@link
- * InitialAssignment}.
- * @throws ModelOverdeterminedException if the model cannot be solved because too many equations
- * over determine its solution space (this can happen if
- * algebraic rules are used in the model).
- * @throws SBMLException if the model has an invalid structure.
- */
- private double stoichiometry(SpeciesReference specRef)
- throws SBMLException, ModelOverdeterminedException {
- if ((interpreter != null) && specRef.isSetId()) {
- // There could be an initial assignment that has changed the value of this speciesReference.
- return interpreter.getCurrentStoichiometry(specRef.getId());
- } else if (specRef.isSetStoichiometry()) {
- return specRef.getStoichiometry();
- } else {
- if (interpreter == null) {
- interpreter = new SBMLinterpreter(specRef.getModel());
- }
- if (specRef.isSetStoichiometryMath()) {
- return ((ASTNodeValue) specRef.getStoichiometryMath().getMath()
- .getUserObject(SBMLinterpreter.TEMP_VALUE))
- .compileDouble(interpreter.getCurrentTime(), 0d);
- } else if (specRef.isSetId()) {
- // Is there an initial assignment?
- interpreter.getCurrentStoichiometry(specRef.getId());
- } else {
- throw new SBMLException(
- "Could not calculate the stoichiometry for a species reference because it was lacking an identifier.");
- }
+ /**
+ * Public accessor for the active FBC objective id (API/test compatibility).
+ */
+ public String getActiveObjective() { // simple O(0) getter; not performance-critical
+ return activeObjective;
}
- return Double.NaN;
- }
-
- /**
- * Gets the value of the EPSILON
- *
- * @return the epsilon value
- */
- public double getEpsilon() {
- return eps;
- }
-
- /**
- * Set the value of the EPSILON specific to a particular FBC instance
- *
- * @param eps
- */
- public void setEpsilon(double eps) {
- this.eps = eps;
- }
/**
* Gets the id of the active objective function
diff --git a/src/main/java/org/simulator/fba/NewGLPKSolver.java b/src/main/java/org/simulator/fba/NewGLPKSolver.java
deleted file mode 100644
index 801f4550e..000000000
--- a/src/main/java/org/simulator/fba/NewGLPKSolver.java
+++ /dev/null
@@ -1,289 +0,0 @@
-package org.simulator.fba;
-
-import org.gnu.glpk.GlpkSolver;
-import scpsolver.constraints.*;
-import scpsolver.lpsolver.GLPKSolver;
-import scpsolver.lpsolver.LinearProgramSolver;
-import scpsolver.problems.LinearProgram;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-
-/**
- * Class for solving the linear programs.
- *
- * This class is added temporarily till issue of freeing the memory of the instance of the
- * GlpkSolver in the solve() method gets resolved in SCPSolver.
- *
- * Currently, while running the SBML Test Suite, it crashes in between due to the memory allocation
- * error for GlpkSolver class.
- *
- * The error: glp_free: memory allocation error Error detected in file env/alloc.c at line 72
- *
- * This happened as the memory of the GlpkSolver instance was not freed. So, this class is the copy
- * of the GLPKSolver class from the SCPSolver. The only change here is that the
- * `solver.deleteProb();` is called in the solve() method after its use is done which deletes the
- * object, i.e., frees the memory.
- *
- * This class will be removed from SBSCL as this issue gets resolved in the SCPSolver.
- */
-public class NewGLPKSolver implements LinearProgramSolver {
-
- GlpkSolver solver;
- int rowcount;
- int timeconstraint = -1;
-
- public NewGLPKSolver() {
- }
-
- /**
- * @return the timeconstraint
- */
- public int getTimeconstraint() {
- return timeconstraint;
- }
-
- /**
- * @param timeconstraint the timeconstraint to set
- */
- public void setTimeconstraint(int timeconstraint) {
- this.timeconstraint = timeconstraint;
- }
-
-
- public double[] solve(LinearProgram lp) {
-
- /* add variables */
-
- try {
- solver = new GlpkSolver();
- } catch (Error e) {
- System.err.println("Can't instantiate solver:");
- System.err.println(" ** " + e.getClass().getName() + ": " + e.getMessage());
- System.err.println(
- " ** java.library.path: " + System.getProperty("java.library.path"));
- System.err.println("Probably you don't have GLPK JNI properly installed.");
- return null;
- }
-
- // solver.setRealParm(GlpkSolver.LPX_K_TMLIM, 100);
- // solver.setIntParm(GlpkSolver.LPX_K_TMLIM, 100);
- solver.enablePrints(false); // turn this to "false" to prevent printouts
- if (lp.isMIP()) {
- solver.setClss(GlpkSolver.LPX_MIP);
- } else {
- solver.setClss(GlpkSolver.LPX_LP);
- }
-
- /* we usually excpect a max problem, but I have to think about that..*/
-
- solver.setObjDir((lp.isMinProblem()) ? GlpkSolver.LPX_MIN : GlpkSolver.LPX_MAX);
-
- /* set columns */
-
- double[] c = lp.getC();
- solver.addCols(c.length);
-
- for (int i = 0; i < c.length; i++) {
- solver.setColName(i + 1, "x" + i);
- solver.setObjCoef(i + 1, c[i]);
- }
-
- if (!lp.hasBounds()) {
- for (int i = 0; i < c.length; i++) {
- solver.setColBnds(i + 1, GlpkSolver.LPX_FR, 0, 0);
- }
- } else {
-
- for (int i = 0; i < c.length; i++) {
- solver.setColBnds(i + 1, GlpkSolver.LPX_DB, lp.getLowerbound()[i],
- lp.getUpperbound()[i]); //TODO
- }
- }
-
- /* add variable types */
-
- boolean[] integers = lp.getIsinteger();
-
- for (int i = 0; i < integers.length; i++) {
- solver.setColKind(i + 1, (integers[i]) ? GlpkSolver.LPX_IV : GlpkSolver.LPX_CV);
- }
-
- boolean[] booleans = lp.getIsboolean();
-
- for (int i = 0; i < booleans.length; i++) {
- if (booleans[i]) {
- solver.setColKind(i + 1, GlpkSolver.LPX_IV);
- solver.setColBnds(i + 1, GlpkSolver.LPX_DB, 0, 1); //TODO
- }
- }
-
- /* add constraints */
-
- transferConstraints(lp);
-
- if (timeconstraint > 0) {
- System.out.println("Setting time constraint to:" + timeconstraint + " seconds");
- solver.setRealParm(GlpkSolver.LPX_K_TMLIM, timeconstraint);
- }
-
- double[] result = null;
-
- int res = solver.simplex();
-
- if (!lp.isMIP()) {
-
- //System.out.println("Maximum: " + solver.getObjVal());
-
- if (res != GlpkSolver.LPX_E_OK ||
- (solver.getStatus() != GlpkSolver.LPX_OPT &&
- solver.getStatus() != GlpkSolver.LPX_FEAS)) {
- System.err.println("simplex() failed");
- } else {
- result = new double[c.length];
- for (int i = 0; i < result.length; i++) {
- result[i] = solver.getColPrim(i + 1);
- }
- }
- } else {
- res = solver.integer();
- // System.out.println("SOLVER STATUS: " + solver.getPrimStat());
- if (res != GlpkSolver.LPX_E_OK ||
- (solver.mipStatus() != GlpkSolver.LPX_I_OPT &&
- solver.mipStatus() != GlpkSolver.LPX_I_FEAS)) {
-
- System.err.println("integer() failed");
- } else {
- // System.out.println("Maximum: " + solver.mipObjVal());
- // System.out.println("MIP STATUS: " + solver.mipStatus());
- result = new double[c.length];
- for (int i = 0; i < result.length; i++) {
- result[i] = solver.mipColVal(i + 1);
- // System.out.println("x" +(i+1) +": " + result[i]);
- }
- }
- }
- solver.deleteProb();
- return result;
- }
-
- public int[] getIntegerSolution() {
- int length = solver.getNumCols();
- int[] result = new int[length];
- for (int i = 1; i <= length; i++) {
- result[i - 1] = (int) solver.mipColVal(i);
- }
- return result;
- }
-
- private void transferConstraints(LinearProgram lp) {
- ArrayList constraints = lp.getConstraints();
- rowcount = 0;
-
- /* add rows */
- solver.addRows(constraints.size());
-
- /* add row/names borders */
- for (Constraint constraint : constraints) {
- ((LinearConstraint) constraint).addToLinearProgramSolver(this);
- }
-
- int nonzeroa = 0;
- for (Constraint constraint : constraints) {
-
- double[] c = ((LinearConstraint) constraint).getC();
- for (int i = 0; i < c.length; i++) {
- if (c[i] != 0.0) {
- nonzeroa++;
- }
- }
- }
-
- int[] ia = new int[nonzeroa + 1];
- int[] ja = new int[nonzeroa + 1];
- double[] ar = new double[nonzeroa + 1];
-
- rowcount = 0;
- nonzeroa = 0;
-
- for (Constraint constraint : constraints) {
-
- rowcount++;
- double[] c = ((LinearConstraint) constraint).getC();
-
- //System.out.print(constraint.getName() + " ");
- for (int i = 0; i < c.length; i++) {
- if (c[i] != 0.0) {
- nonzeroa++;
- ia[nonzeroa] = rowcount;
- ja[nonzeroa] = i + 1;
- ar[nonzeroa] = c[i];
- // System.out.print((i+1) + "(" + c[i] + ") ");
- }
- }
- // System.out.println();
-
- }
- solver.loadMatrix(nonzeroa, ia, ja, ar);
-
-
- }
-
-
- public void addLinearBiggerThanEqualsConstraint(LinearBiggerThanEqualsConstraint c) {
- rowcount++;
- solver.setRowName(rowcount, c.getName());
- solver.setRowBnds(rowcount, GlpkSolver.LPX_LO, c.getT(), 0.0);
- }
-
- public void addLinearSmallerThanEqualsConstraint(LinearSmallerThanEqualsConstraint c) {
- rowcount++;
- solver.setRowName(rowcount, c.getName());
- solver.setRowBnds(rowcount, GlpkSolver.LPX_UP, 0.0, c.getT());
- }
-
- public void addEqualsConstraint(LinearEqualsConstraint c) {
- rowcount++;
- solver.setRowName(rowcount, c.getName());
- solver.setRowBnds(rowcount, GlpkSolver.LPX_FX, c.getT(), c.getT());
-
- }
-
- public String getName() {
- return "GLPK";
- }
-
- public String[] getLibraryNames() {
- return new String[]{"glpkjni"};
- }
-
- public static void main(String[] args) {
- LinearProgram lp = new LinearProgram(new double[]{10.0, 6.0, 4.0});
- lp.addConstraint(new LinearSmallerThanEqualsConstraint(new double[]{1.0, 1.0, 1.0}, 320, "p"));
- lp.addConstraint(new LinearSmallerThanEqualsConstraint(new double[]{10.0, 4.0, 5.0}, 650, "q"));
- lp.addConstraint(new LinearBiggerThanEqualsConstraint(new double[]{2.0, 2.0, 6.0}, 100, "r1"));
-
- lp.setLowerbound(new double[]{30.0, 0.0, 0.0});
-
- //lp.addConstraint(new LinearEqualsConstraint(new double[]{1.0,1.0,1.0}, 100,"t"));
-
- lp.setInteger(0);
- lp.setInteger(1);
- lp.setInteger(2);
-
- LinearProgramSolver solver = new GLPKSolver();
-
- System.out.println(solver.solve(lp)[0]);
- double[] sol = solver.solve(lp);
- ArrayList constraints = lp.getConstraints();
- for (Iterator iterator = constraints.iterator(); iterator.hasNext(); ) {
- Constraint constraint = iterator.next();
- if (constraint.isSatisfiedBy(sol)) {
- System.out.println(constraint.getName() + " satisfied");
- }
- }
-
- }
-
-
-}
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 000000000..2d809058d
--- /dev/null
+++ b/src/main/java/org/simulator/optsolvx/FbaToOptSolvX.java
@@ -0,0 +1,214 @@
+package org.simulator.optsolvx;
+
+import org.optsolvx.model.AbstractLPModel;
+import org.optsolvx.model.Constraint;
+import org.optsolvx.model.OptimizationDirection;
+import org.sbml.jsbml.*;
+import org.sbml.jsbml.ext.fbc.*;
+
+import java.text.MessageFormat;
+import java.util.LinkedHashMap;
+import java.util.List;
+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() { /* no instances */ }
+
+ /** Build an OptSolvX LP model from SBML/FBC (v1 or v2). */
+ public static AbstractLPModel fromSBML(SBMLDocument doc) {
+ 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();
+
+ // Cache SBML lists once to avoid repeated getter calls in hot paths
+ final List reactions = m.getListOfReactions(); // cached reaction list
+ final List speciesList = m.getListOfSpecies(); // cached species list
+
+ // 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(Math.max(16, reactions.size() * 2));
+ final Map ub = new LinkedHashMap(Math.max(16, reactions.size() * 2));
+
+ for (Reaction r : reactions) {
+ 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 : reactions) {
+ 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 (int si = 0; si < speciesList.size(); si++) {
+ final Species s = speciesList.get(si);
+ final String sid = s.getId(); // cache id once per species
+ final boolean isBoundary = s.isSetBoundaryCondition() && s.getBoundaryCondition();
+ if (isBoundary) continue;
+
+ // Pre-size to reduce rehashing; order preserved for readability
+ final Map coeffs = new LinkedHashMap(
+ Math.max(16, reactions.size()));
+
+ // For each reaction, accumulate stoichiometry of species sid
+ for (int ri = 0; ri < reactions.size(); ri++) {
+ final Reaction r = reactions.get(ri);
+ final String rid = r.getId(); // cache id once per reaction
+ double sum = 0.0d;
+
+ // Reactants contribute negative stoichiometry
+ final List reactants = r.getListOfReactants();
+ for (int k = 0; k < reactants.size(); k++) {
+ final SpeciesReference sr = reactants.get(k);
+ if (sid.equals(sr.getSpecies())) {
+ sum -= stoich(sr);
+ }
+ }
+
+ // Products contribute positive stoichiometry
+ final List products = r.getListOfProducts();
+ for (int k = 0; k < products.size(); k++) {
+ final SpeciesReference sr = products.get(k);
+ if (sid.equals(sr.getSpecies())) {
+ sum += stoich(sr);
+ }
+ }
+
+ if (sum != 0.0d) {
+ coeffs.put(rid, sum);
+ }
+ }
+
+ if (!coeffs.isEmpty()) {
+ lp.addConstraint("mb_" + sid, 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;
+ }
+}
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 000000000..ab0afc78e
--- /dev/null
+++ b/src/main/java/org/simulator/optsolvx/OptSolvXDemo.java
@@ -0,0 +1,49 @@
+package org.simulator.optsolvx;
+
+import org.optsolvx.model.AbstractLPModel;
+import org.optsolvx.model.Constraint;
+import org.optsolvx.model.OptimizationDirection;
+import org.optsolvx.solver.LPSolution;
+import org.optsolvx.solver.LPSolverAdapter;
+import org.optsolvx.solver.OptSolvXConfig;
+
+
+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();
+
+ LPSolverAdapter backend = OptSolvXConfig.resolve(model, System.getProperty("optsolvx.solver"));
+
+ LPSolution sol = backend.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());
+ }
+}
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 000000000..b423f51d5
--- /dev/null
+++ b/src/main/java/org/simulator/optsolvx/OptSolvXSolverAdapter.java
@@ -0,0 +1,66 @@
+package org.simulator.optsolvx;
+
+import org.optsolvx.model.AbstractLPModel;
+import org.optsolvx.solver.LPSolution;
+import org.optsolvx.solver.LPSolverAdapter;
+
+import java.util.Objects;
+import java.text.MessageFormat;
+import java.util.logging.Logger;
+
+public final 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 = 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) {
+ throw new IllegalArgumentException("model must not be null");
+ }
+ 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()
+ ));
+ }
+
+ 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}, time={3} ms",
+ getClass().getSimpleName(),
+ sol.isFeasible(),
+ sol.getObjectiveValue(),
+ dtMs
+ ));
+ }
+ return sol;
+ }
+}
diff --git a/src/test/java/org/simulator/optsolvx/BridgeConstraintTests.java b/src/test/java/org/simulator/optsolvx/BridgeConstraintTests.java
new file mode 100644
index 000000000..71fe3133f
--- /dev/null
+++ b/src/test/java/org/simulator/optsolvx/BridgeConstraintTests.java
@@ -0,0 +1,264 @@
+package org.simulator.optsolvx;
+
+import org.junit.Test;
+import org.sbml.jsbml.*;
+import org.sbml.jsbml.ext.fbc.FBCConstants;
+import org.optsolvx.model.AbstractLPModel;
+import org.optsolvx.model.Constraint;
+
+import java.util.*;
+
+import static org.junit.Assert.*;
+
+/**
+ * Constraint-level tests for FbaToOptSolvX:
+ * - Count of S·v=0 constraints
+ * - Correct sign (+/-) and value of stoichiometric coefficients
+ * - Ignoring boundary species
+ * - No constraints when net stoichiometry is zero
+ * Note: We keep objective empty in these tests; warnings in logs are expected.
+ */
+public class BridgeConstraintTests {
+
+ private static final double EPS = 1e-9;
+
+ // ---- Tests ----
+
+ /**
+ * One species, one reaction, unit stoichiometry: expect 1 constraint with -1.0 for reactant.
+ */
+ @Test
+ public void unitStoich_reactant_negativeSign_and_count() {
+ AbstractLPModel lp = buildLP("A", false, mapOf(
+ // R1: A as reactant (1.0), no product
+ entry("R1", stoich(1.0, 0.0))));
+ assertEquals("exactly one mass-balance constraint expected", 1, lp.getConstraints().size());
+ Constraint c = findMb(lp, "A");
+ assertNotNull("mb_A must exist", c);
+ assertEquals(-1.0, c.getCoefficients().get("R1"), EPS);
+ }
+
+ /**
+ * Reactant -3.5 and product +4.0: coefficients must equal -3.5 and +4.0.
+ */
+ @Test
+ public void nonUnitStoich_signs_and_values() {
+ AbstractLPModel lp = buildLP("S", false, mapOf(entry("R1", stoich(3.5, 4.0)) // reactant 3.5, product 4.0
+ ));
+ Constraint c = findMb(lp, "S");
+ assertNotNull(c);
+ assertEquals(+0.5, c.getCoefficients().get("R1"), EPS);
+ }
+
+ /**
+ * Species appears on both sides with equal amounts: net zero → no constraint should be added.
+ */
+ @Test
+ public void species_on_both_sides_netZero_should_not_create_constraint() {
+ AbstractLPModel lp = buildLP("X", false, mapOf(entry("R_eq", stoich(1.0, 1.0)) // -1 + 1 = 0
+ ));
+ assertNull("No mb_X expected if all coefficients are zero", findMb(lp, "X"));
+ assertEquals(0, lp.getConstraints().size());
+ }
+
+ /**
+ * Two reactions: coefficients must accumulate and filter zeros only.
+ */
+ @Test
+ public void summing_across_multiple_reactions_and_filter_zero_entries() {
+ AbstractLPModel lp = buildLP("M", false, mapOf(entry("R1", stoich(1.0, 0.0)), // contributes -1.0
+ entry("R2", stoich(0.0, 2.0)) // contributes +2.0
+ ));
+ Constraint c = findMb(lp, "M");
+ assertNotNull(c);
+ assertEquals(-1.0, c.getCoefficients().get("R1"), EPS);
+ assertEquals(+2.0, c.getCoefficients().get("R2"), EPS);
+ assertEquals("only non-zero entries must be present", 2, c.getCoefficients().size());
+ }
+
+ /**
+ * Boundary species must be ignored entirely (no constraint).
+ */
+ @Test
+ public void boundary_species_are_ignored() {
+ AbstractLPModel lp = buildLP("BND", true, mapOf(entry("R1", stoich(1.0, 0.0))));
+ assertEquals(0, lp.getConstraints().size());
+ assertNull(findMb(lp, "BND"));
+ }
+
+ /**
+ * ignore species with no participation: Z appears in no reaction → no mb_Z.
+ */
+ @Test
+ public void ignore_species_with_no_participation() {
+ AbstractLPModel lp = buildLP("Z", false, mapOf(
+ // Reaction exists but Z is neither reactant nor product (0/0) → no entries
+ entry("R_unused", stoich(0.0, 0.0))));
+ assertEquals(0, lp.getConstraints().size());
+ assertNull(findMb(lp, "Z"));
+ }
+
+ /**
+ * multiple species (A,B) with different patterns → one constraint per species, with expected sizes.
+ */
+ @Test
+ public void multiple_species_constraint_counts_and_per_species_entries() {
+ // A participates in R1 (reactant 1.0) and R2 (product 3.0) → 2 entries in mb_A
+ Map A = mapOf(entry("R1", stoich(1.0, 0.0)), entry("R2", stoich(0.0, 3.0)));
+ // B participates only in R1 (product 2.0) → 1 entry in mb_B
+ Map B = mapOf(entry("R1", stoich(0.0, 2.0)));
+
+ AbstractLPModel lp = buildLP_twoSpecies("A", false, "B", false, A, B);
+
+ assertEquals("exactly two mass-balance constraints (one per species)", 2, lp.getConstraints().size());
+
+ Constraint cA = findMb(lp, "A");
+ Constraint cB = findMb(lp, "B");
+ assertNotNull(cA);
+ assertNotNull(cB);
+
+ assertEquals(2, cA.getCoefficients().size()); // R1, R2
+ assertEquals(1, cB.getCoefficients().size()); // R1 only
+ }
+
+ // ---- Helpers & builders ----
+
+ /**
+ * Build an LP via FbaToOptSolvX for a single species across multiple reactions.
+ */
+ private static AbstractLPModel buildLP(String speciesId, boolean boundary, Map reactions) {
+ SBMLDocument doc = new SBMLDocument(3, 1);
+ Model m = doc.createModel("m");
+
+ // Compartment (required by SBML for species)
+ Compartment c = m.createCompartment("c");
+ c.setConstant(true);
+ c.setSize(1.0);
+ c.setSpatialDimensions(3);
+
+ // Species
+ Species s = m.createSpecies(speciesId);
+ s.setCompartment("c");
+ s.setBoundaryCondition(boundary);
+
+ // Reactions: for each entry, array[0]=reactant stoich, array[1]=product stoich
+ for (Map.Entry e : reactions.entrySet()) {
+ String rid = e.getKey();
+ double[] st = e.getValue();
+ Reaction r = m.createReaction(rid);
+ r.setReversible(false);
+
+ if (st[0] > 0.0) {
+ SpeciesReference sr = r.createReactant();
+ sr.setSpecies(speciesId);
+ sr.setStoichiometry(st[0]);
+ }
+ if (st[1] > 0.0) {
+ SpeciesReference sr = r.createProduct();
+ sr.setSpecies(speciesId);
+ sr.setStoichiometry(st[1]);
+ }
+ }
+
+ // Ensure FBC v2 namespace is resolvable (avoid "result ignored" inspection)
+ final String fbcNs = FBCConstants.getNamespaceURI(doc.getLevel(), doc.getVersion(), 2);
+ assertNotNull("FBC v2 namespace must be resolvable", fbcNs);
+
+ return FbaToOptSolvX.fromSBML(doc);
+ }
+
+ /**
+ * Build an LP for two species (A,B) sharing reactions.
+ */
+ private static AbstractLPModel buildLP_twoSpecies(String aId, boolean aBoundary, String bId, boolean bBoundary, Map aStoich, Map bStoich) {
+
+ SBMLDocument doc = new SBMLDocument(3, 1);
+ Model m = doc.createModel("m");
+
+ Compartment c = m.createCompartment("c");
+ c.setConstant(true);
+ c.setSize(1.0);
+ c.setSpatialDimensions(3);
+
+ Species A = m.createSpecies(aId);
+ A.setCompartment("c");
+ A.setBoundaryCondition(aBoundary);
+ Species B = m.createSpecies(bId);
+ B.setCompartment("c");
+ B.setBoundaryCondition(bBoundary);
+
+ // union of reaction ids used by A or B
+ Set rids = new LinkedHashSet<>();
+ rids.addAll(aStoich.keySet());
+ rids.addAll(bStoich.keySet());
+
+ for (String rid : rids) {
+ Reaction r = m.createReaction(rid);
+ r.setReversible(false);
+
+ double[] a = aStoich.get(rid);
+ if (a != null) {
+ if (a[0] > 0.0) {
+ SpeciesReference sr = r.createReactant();
+ sr.setSpecies(aId);
+ sr.setStoichiometry(a[0]);
+ }
+ if (a[1] > 0.0) {
+ SpeciesReference sr = r.createProduct();
+ sr.setSpecies(aId);
+ sr.setStoichiometry(a[1]);
+ }
+ }
+ double[] b = bStoich.get(rid);
+ if (b != null) {
+ if (b[0] > 0.0) {
+ SpeciesReference sr = r.createReactant();
+ sr.setSpecies(bId);
+ sr.setStoichiometry(b[0]);
+ }
+ if (b[1] > 0.0) {
+ SpeciesReference sr = r.createProduct();
+ sr.setSpecies(bId);
+ sr.setStoichiometry(b[1]);
+ }
+ }
+ }
+
+ FBCConstants.getNamespaceURI(doc.getLevel(), doc.getVersion(), 2);
+
+ return FbaToOptSolvX.fromSBML(doc);
+ }
+
+ /**
+ * Find the mass-balance constraint for a species ("mb_").
+ */
+ private static Constraint findMb(AbstractLPModel lp, String sid) {
+ String name = "mb_" + sid;
+ for (Constraint c : lp.getConstraints()) {
+ if (name.equals(c.getName())) return c;
+ }
+ return null;
+ }
+
+ // Tiny inlined helpers to keep Java 8 compatibility (no Map.of in SBSCL).
+ private static Map mapOf(Map.Entry e1) {
+ Map m = new LinkedHashMap<>();
+ m.put(e1.getKey(), e1.getValue());
+ return m;
+ }
+
+ private static Map mapOf(Map.Entry e1, Map.Entry e2) {
+ Map m = new LinkedHashMap<>();
+ m.put(e1.getKey(), e1.getValue());
+ m.put(e2.getKey(), e2.getValue());
+ return m;
+ }
+
+ private static Map.Entry entry(String k, double[] v) {
+ return new AbstractMap.SimpleImmutableEntry<>(k, v);
+ }
+
+ private static double[] stoich(double reactant, double product) {
+ return new double[]{reactant, product};
+ }
+}
\ No newline at end of file
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 000000000..10e7e6171
--- /dev/null
+++ b/src/test/java/org/simulator/optsolvx/BridgeObjectiveTest.java
@@ -0,0 +1,97 @@
+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.LPSolverAdapter;
+import org.optsolvx.solver.OptSolvXConfig;
+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);
+ LPSolverAdapter backend = OptSolvXConfig.resolve(lp, System.getProperty("optsolvx.solver"));
+ assertNotNull("backend must not be null", backend);
+
+ String wanted = System.getProperty("optsolvx.solver");
+ if (wanted != null) {
+ String w = wanted.trim().toLowerCase();
+ String actual = backend.getClass().getName().toLowerCase();
+
+ if (w.equals("ojalgo") || w.equals("oj")) {
+ assertTrue("Expected OjAlgo backend but got: " + backend.getClass().getName(),
+ actual.contains("ojalgo"));
+ } else if (w.equals("commons-math") || w.equals("commonsmath") || w.equals("cm")) {
+ assertTrue("Expected CommonsMath backend but got: " + backend.getClass().getName(),
+ actual.contains("commonsmath"));
+ }
+ }
+
+ LPSolution sol = new OptSolvXSolverAdapter(backend).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/OptSolvXConfigSelectionTest.java b/src/test/java/org/simulator/optsolvx/OptSolvXConfigSelectionTest.java
new file mode 100644
index 000000000..377f9a7fe
--- /dev/null
+++ b/src/test/java/org/simulator/optsolvx/OptSolvXConfigSelectionTest.java
@@ -0,0 +1,42 @@
+package org.simulator.optsolvx;
+
+import org.junit.Test;
+import org.optsolvx.model.AbstractLPModel;
+import org.optsolvx.solver.LPSolverAdapter;
+import org.optsolvx.solver.OptSolvXConfig;
+
+import static org.junit.Assert.*;
+
+public class OptSolvXConfigSelectionTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void unknown_solver_name_throws_exception() {
+ AbstractLPModel lp = new AbstractLPModel();
+ lp.addVariable("x", 0.0d, 1.0d);
+ lp.build();
+
+ OptSolvXConfig.resolve(lp, "does-not-exist");
+ }
+
+ @Test
+ public void resolves_oj_alias_to_ojalgo() {
+ AbstractLPModel lp = new AbstractLPModel();
+ lp.addVariable("x", 0.0d, 1.0d);
+ lp.build();
+
+ LPSolverAdapter backend = OptSolvXConfig.resolve(lp, "oj");
+ assertNotNull(backend);
+ assertTrue(backend.getClass().getName().toLowerCase().contains("ojalgo"));
+ }
+
+ @Test
+ public void default_solver_is_commons_math_when_no_name_given() {
+ AbstractLPModel lp = new AbstractLPModel();
+ lp.addVariable("x", 0.0d, 1.0d);
+ lp.build();
+
+ LPSolverAdapter backend = OptSolvXConfig.resolve(lp, null);
+ assertNotNull(backend);
+ assertTrue(backend.getClass().getName().toLowerCase().contains("commonsmath"));
+ }
+}
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 000000000..9abd7477a
--- /dev/null
+++ b/src/test/java/org/simulator/optsolvx/OptSolvXSolverAdapterTest.java
@@ -0,0 +1,65 @@
+package org.simulator.optsolvx;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.optsolvx.model.*;
+import org.optsolvx.solver.*;
+import org.optsolvx.solver.OptSolvXConfig;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class OptSolvXSolverAdapterTest {
+
+ private static final LPSolverAdapter NOOP_BACKEND = new LPSolverAdapter() {
+ @Override
+ public LPSolution solve(AbstractLPModel model) {
+ throw new AssertionError("Backend must not be called for these tests.");
+ }
+ };
+
+ @Test(expected = IllegalArgumentException.class)
+ public void solve_requires_non_null_model() {
+ LPSolverAdapter s = new OptSolvXSolverAdapter(NOOP_BACKEND, false);
+ s.solve(null);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void solve_requires_build() {
+ AbstractLPModel m = new AbstractLPModel();
+ m.addVariable("x", 0.0d, 10.0d);
+
+ LPSolverAdapter s = new OptSolvXSolverAdapter(NOOP_BACKEND, false);
+ s.solve(m);
+ }
+
+ @Test
+ 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, 5.0d);
+ m.addVariable("y", 0.0d, 5.0d);
+
+ Map obj = new HashMap<>();
+ obj.put("x", 2.0d);
+ obj.put("y", 1.0d);
+ m.setObjective(obj, OptimizationDirection.MINIMIZE);
+
+ 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();
+
+ LPSolverAdapter backend = OptSolvXConfig.resolve(m, System.getProperty("optsolvx.solver"));
+ LPSolverAdapter s = new OptSolvXSolverAdapter(backend, false);
+ LPSolution sol = s.solve(m);
+
+ assertTrue(sol.isFeasible());
+ // 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);
+ }
+}
diff --git a/src/test/java/org/simulator/oven/SCPSolverIssue.java b/src/test/java/org/simulator/oven/SCPSolverIssue.java
deleted file mode 100644
index 14cffaecb..000000000
--- a/src/test/java/org/simulator/oven/SCPSolverIssue.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.simulator.oven;
-
-import java.util.Arrays;
-import scpsolver.constraints.LinearBiggerThanEqualsConstraint;
-import scpsolver.constraints.LinearSmallerThanEqualsConstraint;
-import scpsolver.lpsolver.LinearProgramSolver;
-import scpsolver.lpsolver.SolverFactory;
-import scpsolver.problems.LinearProgram;
-
-public class SCPSolverIssue {
-
- public static void main(String[] args) {
- LinearProgram lp = new LinearProgram(new double[]{5.0, 10.0});
- lp.addConstraint(new LinearBiggerThanEqualsConstraint(new double[]{3.0, 1.0}, 8.0, "c1"));
- lp.addConstraint(new LinearBiggerThanEqualsConstraint(new double[]{0.0, 4.0}, 4.0, "c2"));
- lp.addConstraint(new LinearSmallerThanEqualsConstraint(new double[]{2.0, 0.0}, 2.0, "c3"));
- lp.setMinProblem(true);
-
- LinearProgramSolver solver = SolverFactory.newDefault();
- double[] sol = solver.solve(lp);
- System.out.println(Arrays.toString(sol));
-
- LinearProgramSolver solver2 = SolverFactory.newDefault();
- double[] sol2 = solver2.solve(lp);
- System.out.println(Arrays.toString(sol2));
- }
-
-}