diff --git a/README.md b/README.md index a355d23ff..b1566301f 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,32 @@ Further examples can be found directly within this repository in the [Examples P * 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: `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`). + +*Note:* FBA in SBSCL now uses OptSolvX by default. + +### Enable debug logs + +Create the adapter with `debug = true`: + +```java +LPSolverAdapter solver = + new OptSolvXSolverAdapter(new CommonsMathSolver(), true); +``` + +You can also pick a backend via `-Doptsolvx.backend=` or env `OPTSOLVX_BACKEND`. + +### (Preview) SBML/FBC → LP + +An experimental entry point is available at `org.simulator.optsolvx.FbaToOptSolvX`. + + ### Comparison to Similar Libraries To compare SBSCL to other simulation engines and to benchmark its predictions and results, a separate project, [SBSCL simulator comparison](https://github.com/matthiaskoenig/sbscl-simulator-comparison), is available. @@ -117,6 +143,11 @@ The package structure in more detail: Please e-mail any bugs, problems, suggestions, or issues regarding this library to the bug tracker at https://github.com/draeger-lab/SBSCL/issues +### Note on XML parsers (JDK ≥ 17) + +SBSCL excludes legacy `xercesImpl`, `xml-apis`, and `xalan` (pulled transitively by `jlibsedml`) to avoid JAXP compatibility issues on modern JDKs (e.g., an `AbstractMethodError` during Log4j2 XML configuration). +If you rely on specific Xerces/Xalan features, please open an issue so we can provide an opt-in solution + ## Licensing terms This file is part of Simulation Core Library, a Java-based library for efficient numerical simulation of biological models. diff --git a/pom.xml b/pom.xml index e162e1e87..e087b877b 100644 --- a/pom.xml +++ b/pom.xml @@ -411,20 +411,44 @@ jmathml 2.1.0 + + org.jlibsedml jlibsedml 2.2.3 + + + xerces + xercesImpl + + + xml-apis + xml-apis + + + xalan + xalan + + org.apache.commons - commons-math - 2.2 + commons-lang3 + 3.7 + + org.apache.commons - commons-lang3 - 3.7 + commons-math + 2.2 @@ -439,26 +463,12 @@ - - scpsolver - SCPSolver - 1.0v2 - - - scpsolver - GLPKSolverPack - 4.35v2 - - - scpsolver - LPSOLVESolverPack - 5.5.2.5 - kisao libkisao 1.0.3.1-rc + de.uni-rostock.sbi @@ -471,6 +481,14 @@ + + + + org.optsolvx + optsolvx + 0.1.0-SNAPSHOT + jdk8 + diff --git a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.jar b/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.jar deleted file mode 100644 index 2c10271e4..000000000 Binary files a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.jar and /dev/null differ diff --git a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.jar.md5 b/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.jar.md5 deleted file mode 100644 index beed4d830..000000000 --- a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -1ae23211e87021726c7d4b13a6036c3f \ No newline at end of file diff --git a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.jar.sha1 b/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.jar.sha1 deleted file mode 100644 index 1b9469313..000000000 --- a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -0b7918087973e45cf2919f878678c96920317bb0 \ No newline at end of file diff --git a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.pom b/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.pom deleted file mode 100644 index 4b173fd90..000000000 --- a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.pom +++ /dev/null @@ -1,9 +0,0 @@ - - - 4.0.0 - scpsolver - GLPKSolverPack - 4.35v2 - POM was created from install:install-file - diff --git a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.pom.md5 b/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.pom.md5 deleted file mode 100644 index 8c58cc9da..000000000 --- a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.pom.md5 +++ /dev/null @@ -1 +0,0 @@ -39cdba68ef8d0287db70542d88125835 \ No newline at end of file diff --git a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.pom.sha1 b/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.pom.sha1 deleted file mode 100644 index 6c0dcf139..000000000 --- a/src/lib/maven/scpsolver/GLPKSolverPack/4.35v2/GLPKSolverPack-4.35v2.pom.sha1 +++ /dev/null @@ -1 +0,0 @@ -78f10c45bf4592017ef9998ae27e7a59311b943f \ No newline at end of file diff --git a/src/lib/maven/scpsolver/GLPKSolverPack/maven-metadata-local.xml b/src/lib/maven/scpsolver/GLPKSolverPack/maven-metadata-local.xml deleted file mode 100644 index 4682b4b4e..000000000 --- a/src/lib/maven/scpsolver/GLPKSolverPack/maven-metadata-local.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - scpsolver - GLPKSolverPack - - 4.35v2 - - 4.35 - 4.35-v2 - 4.35v2 - - 20180704130823 - - diff --git a/src/lib/maven/scpsolver/GLPKSolverPack/maven-metadata-local.xml.md5 b/src/lib/maven/scpsolver/GLPKSolverPack/maven-metadata-local.xml.md5 deleted file mode 100644 index 2e476eaba..000000000 --- a/src/lib/maven/scpsolver/GLPKSolverPack/maven-metadata-local.xml.md5 +++ /dev/null @@ -1 +0,0 @@ -d884a98f1590b4639ec5ac9354c88392 \ No newline at end of file diff --git a/src/lib/maven/scpsolver/GLPKSolverPack/maven-metadata-local.xml.sha1 b/src/lib/maven/scpsolver/GLPKSolverPack/maven-metadata-local.xml.sha1 deleted file mode 100644 index d06320726..000000000 --- a/src/lib/maven/scpsolver/GLPKSolverPack/maven-metadata-local.xml.sha1 +++ /dev/null @@ -1 +0,0 @@ -db2e230effa13a9fb701b23c5109a21286d6102f \ No newline at end of file diff --git a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.jar b/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.jar deleted file mode 100644 index 6481c137d..000000000 Binary files a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.jar and /dev/null differ diff --git a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.jar.md5 b/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.jar.md5 deleted file mode 100644 index 983ae4001..000000000 --- a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -59e7d0820aba46c14f78926a2964064e \ No newline at end of file diff --git a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.jar.sha1 b/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.jar.sha1 deleted file mode 100644 index c0d3b174f..000000000 --- a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -4dbe2084078479c83826418d664fd7511e3d0925 \ No newline at end of file diff --git a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.pom b/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.pom deleted file mode 100644 index 953a85905..000000000 --- a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.pom +++ /dev/null @@ -1,9 +0,0 @@ - - - 4.0.0 - scpsolver - LPSOLVESolverPack - 5.5.2.5 - POM was created from install:install-file - diff --git a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.pom.md5 b/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.pom.md5 deleted file mode 100644 index b42f1e264..000000000 --- a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.pom.md5 +++ /dev/null @@ -1 +0,0 @@ -95733736f9d0e48d2325f64e1c29cec3 \ No newline at end of file diff --git a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.pom.sha1 b/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.pom.sha1 deleted file mode 100644 index 6a11ddde6..000000000 --- a/src/lib/maven/scpsolver/LPSOLVESolverPack/5.5.2.5/LPSOLVESolverPack-5.5.2.5.pom.sha1 +++ /dev/null @@ -1 +0,0 @@ -62c896351abc23695d9ff91bff8e98b0fecc3050 \ No newline at end of file diff --git a/src/lib/maven/scpsolver/LPSOLVESolverPack/maven-metadata-local.xml b/src/lib/maven/scpsolver/LPSOLVESolverPack/maven-metadata-local.xml deleted file mode 100644 index 0cdfab1ad..000000000 --- a/src/lib/maven/scpsolver/LPSOLVESolverPack/maven-metadata-local.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - scpsolver - LPSOLVESolverPack - - 5.5.2.5 - - 5.5.2.5 - - 20180704130825 - - diff --git a/src/lib/maven/scpsolver/LPSOLVESolverPack/maven-metadata-local.xml.md5 b/src/lib/maven/scpsolver/LPSOLVESolverPack/maven-metadata-local.xml.md5 deleted file mode 100644 index 281ad1bcb..000000000 --- a/src/lib/maven/scpsolver/LPSOLVESolverPack/maven-metadata-local.xml.md5 +++ /dev/null @@ -1 +0,0 @@ -8575146279f009a1d55115d2205099fd \ No newline at end of file diff --git a/src/lib/maven/scpsolver/LPSOLVESolverPack/maven-metadata-local.xml.sha1 b/src/lib/maven/scpsolver/LPSOLVESolverPack/maven-metadata-local.xml.sha1 deleted file mode 100644 index f37eef4fe..000000000 --- a/src/lib/maven/scpsolver/LPSOLVESolverPack/maven-metadata-local.xml.sha1 +++ /dev/null @@ -1 +0,0 @@ -b43705a58dc5fb4d841ee22a7e7b2708c20a8eeb \ No newline at end of file diff --git a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.jar b/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.jar deleted file mode 100644 index f96ecba52..000000000 Binary files a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.jar and /dev/null differ diff --git a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.jar.md5 b/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.jar.md5 deleted file mode 100644 index 48ea85ad8..000000000 --- a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -80c855cf122677c2d9cee5e4421478c3 \ No newline at end of file diff --git a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.jar.sha1 b/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.jar.sha1 deleted file mode 100644 index 3339509d1..000000000 --- a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -186e438ed8705c9aec25a608587b0a511e1a6bd0 \ No newline at end of file diff --git a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.pom b/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.pom deleted file mode 100644 index 3c7136480..000000000 --- a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.pom +++ /dev/null @@ -1,9 +0,0 @@ - - - 4.0.0 - scpsolver - SCPSolver - 1.0v2 - POM was created from install:install-file - diff --git a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.pom.md5 b/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.pom.md5 deleted file mode 100644 index b6a8724e0..000000000 --- a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.pom.md5 +++ /dev/null @@ -1 +0,0 @@ -34504556b75d9ead2b743b6b33128443 \ No newline at end of file diff --git a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.pom.sha1 b/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.pom.sha1 deleted file mode 100644 index 593e41785..000000000 --- a/src/lib/maven/scpsolver/SCPSolver/1.0v2/SCPSolver-1.0v2.pom.sha1 +++ /dev/null @@ -1 +0,0 @@ -4bba8adee3007dfb3148493f5e41e8aabdf17dce \ No newline at end of file diff --git a/src/lib/maven/scpsolver/SCPSolver/maven-metadata-local.xml b/src/lib/maven/scpsolver/SCPSolver/maven-metadata-local.xml deleted file mode 100644 index e8d27153c..000000000 --- a/src/lib/maven/scpsolver/SCPSolver/maven-metadata-local.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - scpsolver - SCPSolver - - 1.0v2 - - 1.0 - 1.0-v2 - 1.0v2 - - 20180704130822 - - diff --git a/src/lib/maven/scpsolver/SCPSolver/maven-metadata-local.xml.md5 b/src/lib/maven/scpsolver/SCPSolver/maven-metadata-local.xml.md5 deleted file mode 100644 index bfeca570f..000000000 --- a/src/lib/maven/scpsolver/SCPSolver/maven-metadata-local.xml.md5 +++ /dev/null @@ -1 +0,0 @@ -b26dc80d21edd93753a701648fafcb22 \ No newline at end of file diff --git a/src/lib/maven/scpsolver/SCPSolver/maven-metadata-local.xml.sha1 b/src/lib/maven/scpsolver/SCPSolver/maven-metadata-local.xml.sha1 deleted file mode 100644 index 48d114c13..000000000 --- a/src/lib/maven/scpsolver/SCPSolver/maven-metadata-local.xml.sha1 +++ /dev/null @@ -1 +0,0 @@ -bb7ce7869e52be04c3d042d4a4213fef40403207 \ No newline at end of file diff --git a/src/lib/register_maven_jars.sh b/src/lib/register_maven_jars.sh index fed4e12c9..8bad8ddf7 100755 --- a/src/lib/register_maven_jars.sh +++ b/src/lib/register_maven_jars.sh @@ -11,7 +11,4 @@ LIB_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) # register -mvn org.apache.maven.plugins:maven-install-plugin:2.5.2:install-file -DgroupId=scpsolver -DartifactId=SCPSolver -Dversion=1.0v2 -Dfile=nmi/scpsolver/1.0/SCPSolver-1.0v2.jar -DlocalRepositoryPath=${LIB_DIR}/maven -Dpackaging=jar -DgeneratePom=true -DcreateChecksum=true -mvn org.apache.maven.plugins:maven-install-plugin:2.5.2:install-file -DgroupId=scpsolver -DartifactId=GLPKSolverPack -Dversion=4.35v2 -Dfile=nmi/scpsolver/1.0/GLPKSolverPack-4.35v2.jar -DlocalRepositoryPath=${LIB_DIR}/maven -Dpackaging=jar -DgeneratePom=true -DcreateChecksum=true -mvn org.apache.maven.plugins:maven-install-plugin:2.5.2:install-file -DgroupId=scpsolver -DartifactId=LPSOLVESolverPack -Dversion=5.5.2.5 -Dfile=nmi/scpsolver/1.0/LPSOLVESolverPack-5.5.2.5.jar -DlocalRepositoryPath=${LIB_DIR}/maven -Dpackaging=jar -DgeneratePom=true -DcreateChecksum=true mvn org.apache.maven.plugins:maven-install-plugin:2.5.2:install-file -DgroupId=kisao -DartifactId=libkisao -Dversion=1.0.3.1-rc -Dfile=kisao/LibKiSAO/1.0.3.1/libkisao-1.0.3.1-rc.jar -DlocalRepositoryPath=${LIB_DIR}/maven -Dpackaging=jar -DgeneratePom=true -DcreateChecksum=true diff --git a/src/lib/sbml_test_runner_wrapper/GLPKSolverPack-4.35v2.jar b/src/lib/sbml_test_runner_wrapper/GLPKSolverPack-4.35v2.jar deleted file mode 100644 index 2c10271e4..000000000 Binary files a/src/lib/sbml_test_runner_wrapper/GLPKSolverPack-4.35v2.jar and /dev/null differ diff --git a/src/lib/sbml_test_runner_wrapper/LPSOLVESolverPack-5.5.2.5.jar b/src/lib/sbml_test_runner_wrapper/LPSOLVESolverPack-5.5.2.5.jar deleted file mode 100644 index 6481c137d..000000000 Binary files a/src/lib/sbml_test_runner_wrapper/LPSOLVESolverPack-5.5.2.5.jar and /dev/null differ diff --git a/src/lib/sbml_test_runner_wrapper/SCPSolver-1.0v2.jar b/src/lib/sbml_test_runner_wrapper/SCPSolver-1.0v2.jar deleted file mode 100644 index f96ecba52..000000000 Binary files a/src/lib/sbml_test_runner_wrapper/SCPSolver-1.0v2.jar and /dev/null differ diff --git a/src/main/java/org/simulator/fba/FluxBalanceAnalysis.java b/src/main/java/org/simulator/fba/FluxBalanceAnalysis.java index d83a1e3b8..a8e64d63a 100644 --- a/src/main/java/org/simulator/fba/FluxBalanceAnalysis.java +++ b/src/main/java/org/simulator/fba/FluxBalanceAnalysis.java @@ -23,40 +23,30 @@ */ package org.simulator.fba; -import static org.sbml.jsbml.util.Pair.pairOf; import static java.text.MessageFormat.format; -import java.util.*; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.logging.Logger; -import org.sbml.jsbml.AlgebraicRule; -import org.sbml.jsbml.InitialAssignment; -import org.sbml.jsbml.ListOf; import org.sbml.jsbml.Model; -import org.sbml.jsbml.Parameter; import org.sbml.jsbml.Reaction; import org.sbml.jsbml.SBMLDocument; import org.sbml.jsbml.SBMLException; -import org.sbml.jsbml.Species; -import org.sbml.jsbml.SpeciesReference; import org.sbml.jsbml.ext.fbc.FBCConstants; import org.sbml.jsbml.ext.fbc.FBCModelPlugin; -import org.sbml.jsbml.ext.fbc.FBCReactionPlugin; -import org.sbml.jsbml.ext.fbc.FluxBound; -import org.sbml.jsbml.ext.fbc.FluxObjective; import org.sbml.jsbml.ext.fbc.Objective; -import org.sbml.jsbml.util.Pair; -import org.sbml.jsbml.util.SBMLtools; import org.sbml.jsbml.validator.ModelOverdeterminedException; -import org.simulator.sbml.SBMLinterpreter; -import org.simulator.sbml.astnode.ASTNodeValue; -import scpsolver.constraints.LinearBiggerThanEqualsConstraint; -import scpsolver.constraints.LinearConstraint; -import scpsolver.constraints.LinearEqualsConstraint; -import scpsolver.constraints.LinearSmallerThanEqualsConstraint; -import scpsolver.lpsolver.LinearProgramSolver; -import scpsolver.lpsolver.SolverFactory; -import scpsolver.problems.LinearProgram; + +import org.optsolvx.model.AbstractLPModel; +import org.optsolvx.solver.LPSolution; +import org.optsolvx.solver.LPSolverAdapter; +import org.optsolvx.solver.OptSolvXConfig; + +import org.simulator.optsolvx.FbaToOptSolvX; + /** * Support for Flux Balance Analysis (FBA). @@ -70,409 +60,171 @@ * @author Matthias König * @since 1.5 */ -@SuppressWarnings("deprecation") -public class FluxBalanceAnalysis { - - /** - * A Logger for this class. - */ - private static final transient Logger logger = Logger - .getLogger(FluxBalanceAnalysis.class.getName()); - /** - * The linear programming solver. - */ - private LinearProgramSolver glpkSolver; - private LinearProgram problem; +/** + * Flux Balance Analysis (FBA) solved via OptSolvX (SCPSolver-free). + * + *

This implementation builds a pure LP using {@link FbaToOptSolvX} + * and solves it through an OptSolvX backend (default resolved via ServiceLoader + * with a reflective fallback to a CommonsMath-based backend).

+ * + *

Public API is preserved: {@code solve()}, {@code getObjectiveValue()}, + * {@code getValue(String)}, {@code getValues()}, {@code getSolution()}.

+ */ +public class FluxBalanceAnalysis { - private double[] solution; + private static final transient Logger logger = + Logger.getLogger(FluxBalanceAnalysis.class.getName()); - // SCPSolver does not allow same values for lower and upper bounds. - // So, eps is used to add to one of the bounds when both the bounds have equal values. - private double eps = 1E-10; + /** + * OptSolvX LP model built from SBML/FBC. + */ + private final AbstractLPModel lpModel; - /** - * This interpreter is only used if the model contains {@link InitialAssignment}s or {@link - * org.sbml.jsbml.StoichiometryMath}. In all other situations, it will be {@code null}. - */ - private SBMLinterpreter interpreter; + /** + * Solution returned by the backend. + */ + private LPSolution lpSolution; - /** - * The variables of the linear program, i.e., the reactions. - */ + /** + * reaction id -> index (kept for array-based getters in original API). + */ + private final Map reaction2Index = new LinkedHashMap<>(); - /** - * A dictionary to lookup the position of a {@link Reaction} in the list of reactions of the - * {@link Model} based on the reaction's identifier. - */ - private Map reaction2Index; + /** + * Reactions in stable SBML order. + */ + private final List reactionsOrdered = new ArrayList<>(); - /** - * A String that keeps track of id of the active objective function - */ - private String activeObjective; + /** + * Active FBC objective id (key used in {@link #getSolution()}). + */ + private final String activeObjective; - /** - * Initializes the linear program and all data structures based on the definitions in the given - * {@link SBMLDocument}. This implementation should work for diverse levels and versions of SBML - * {@link Model}s given that the model contains an fbc package in version 1 or 2. - * - * @param doc the SBML container from which the {@link Model} is taken. This implementation only - * understands SBML core (diverse levels and versions) in combination with fbc versions - * 1 and 2. - * @throws ModelOverdeterminedException if the {@link Model} is over determined through {@link - * AlgebraicRule}s. - * @throws SBMLException if the {@link Model} is invalid or inappropriate for flux - * balance analysis. - */ - public FluxBalanceAnalysis(SBMLDocument doc) - throws SBMLException, ModelOverdeterminedException { - super(); - if (!doc.isSetModel()) { - throw new IllegalArgumentException( - "Could not find a model definition in the given SBML document."); - } - Model model = doc.getModel(); - interpreter = new SBMLinterpreter(model); - int level = doc.getLevel(), version = doc.getVersion(); - String fbcNamespaceV1 = FBCConstants.getNamespaceURI(level, version, 1); - String fbcNamespaceV2 = FBCConstants.getNamespaceURI(level, version, 2); - reaction2Index = new HashMap<>(); + public FluxBalanceAnalysis(SBMLDocument doc) + throws SBMLException, ModelOverdeterminedException { + if (doc == null || !doc.isSetModel()) { + throw new IllegalArgumentException( + "Could not find a model definition in the given SBML document."); + } + final Model model = doc.getModel(); + + // Determine FBC plugin and active objective (kept for API parity). + final String fbcNSv1 = FBCConstants.getNamespaceURI(doc.getLevel(), doc.getVersion(), 1); + final String fbcNSv2 = FBCConstants.getNamespaceURI(doc.getLevel(), doc.getVersion(), 2); + final int fbcVersion = model.getExtension(FBCConstants.shortLabel).getPackageVersion(); + + final FBCModelPlugin mPlug; + if (fbcVersion == 2) { + mPlug = (FBCModelPlugin) model.getPlugin(fbcNSv2); + } else if (fbcVersion == 1) { + mPlug = (FBCModelPlugin) model.getPlugin(fbcNSv1); + } else { + throw new IllegalArgumentException(format( + "Cannot conduct flux balance analysis without FBC package in model ''{0}''.", + model.getId())); + } - // initialize upper and lower reaction bounds - double[] lb = new double[model.getReactionCount()]; - double[] ub = new double[model.getReactionCount()]; + final Objective objective = mPlug.getActiveObjectiveInstance(); + if (objective == null) { + throw new IllegalArgumentException(format( + "Cannot conduct FBA without defined objective function in model ''{0}''.", + model.getId())); + } + this.activeObjective = objective.getId(); - // Mapping from species id to reaction id and stoichiometric coefficient in that reaction. - Map>> species2Reaction = new HashMap<>(); - for (int i = 0; i < model.getReactionCount(); i++) { - Reaction r = model.getReaction(i); - if (r.isSetPlugin(fbcNamespaceV2)) { - FBCReactionPlugin rPlug = (FBCReactionPlugin) r.getPlugin(fbcNamespaceV2); - Parameter upperBound = rPlug.getUpperFluxBoundInstance(); - Parameter lowerBound = rPlug.getLowerFluxBoundInstance(); - lb[i] = interpreter != null ? - interpreter.getCurrentValueOf(lowerBound.getId()) : - lowerBound.getValue(); - ub[i] = interpreter != null ? - interpreter.getCurrentValueOf(upperBound.getId()) : - upperBound.getValue(); - adjustBoundNumerics(lb, ub, i); - } - reaction2Index.put(r.getId(), i); - buildSpeciesReactionMap(species2Reaction, r.getListOfReactants()); - buildSpeciesReactionMap(species2Reaction, r.getListOfProducts()); - } - FBCModelPlugin mPlug = null; - int fbcVersion = model.getExtension(FBCConstants.shortLabel).getPackageVersion(); - if (fbcVersion == 2) { - mPlug = (FBCModelPlugin) model.getPlugin(fbcNamespaceV2); - } else if (fbcVersion == 1) { - mPlug = (FBCModelPlugin) model.getPlugin(fbcNamespaceV1); - if (mPlug.isSetListOfFluxBounds()) { - for (FluxBound fb : mPlug.getListOfFluxBounds()) { - if (!fb.isSetReaction()) { - logger.warning( - format("Encountered fluxBound ''{0}'' without reaction identifier.", fb.getId())); - } else { - int index = reaction2Index.get(fb.getReaction()); - if (fb.isSetOperation()) { - if (fb.getOperation() == FluxBound.Operation.GREATER_EQUAL) { - lb[index] = fb.getValue(); - } else if (fb.getOperation() == FluxBound.Operation.LESS_EQUAL) { - ub[index] = fb.getValue(); - } else if (fb.getOperation() == FluxBound.Operation.EQUAL) { - lb[index] = ub[index] = fb.getValue(); - } else { - logger.severe( - format("Encountered fluxBound ''{0}'' with invalid operation.", fb.getId())); - } - adjustBoundNumerics(lb, ub, index); - } else { - logger.severe( - format("Encountered fluxBound ''{0}'' without defined operation.", fb.getId())); - } - } + // Preserve reaction order and index mapping for API methods. + for (int i = 0; i < model.getReactionCount(); i++) { + Reaction r = model.getReaction(i); + reactionsOrdered.add(r.getId()); + reaction2Index.put(r.getId(), i); } - } - } else { - throw new IllegalArgumentException(format( - "Cannot conduct flux balance analysis without defined objective function in model ''{0}''.", - model.getId())); - } - // define objective function - double[] objvals = new double[model.getReactionCount()]; - Arrays.fill(objvals, 0d); - Objective objective = mPlug.getActiveObjectiveInstance(); - Objective.Type type = objective.getType(); // max or min - activeObjective = objective.getId(); - for (FluxObjective fo : objective.getListOfFluxObjectives()) { - int rIndex = reaction2Index.get(fo.getReaction()); - objvals[rIndex] = fo.getCoefficient(); + // SBML/FBC -> OptSolvX LP model (bridge keeps all biological semantics out of this class). + this.lpModel = FbaToOptSolvX.fromSBML(doc); + logger.info(format( + "FBA: built OptSolvX model (vars={0}, cons={1})", + lpModel.getVariables().size(), lpModel.getConstraints().size())); } - /* - * Create linear solver + /** + * Solve the built LP via OptSolvX. + *

Backend is resolved via ServiceLoader; if none is found, a reflective + * fallback tries a CommonsMath-based backend without adding a hard compile-time dependency.

+ * + * @return true if a feasible solution is available. + * @throws NullPointerException if something goes wrong internally. */ - SolverFactory.newDefault(); - glpkSolver = new NewGLPKSolver(); - problem = new LinearProgram(objvals); - problem.setLowerbound(lb); - problem.setUpperbound(ub); - switch (type) { - case MAXIMIZE: - problem.setMinProblem(false); - break; - case MINIMIZE: - problem.setMinProblem(true); - break; - default: - throw new SBMLException(format("Unspecified operation {0}", type)); + public boolean solve() throws NullPointerException { + final LPSolverAdapter backend = + OptSolvXConfig.resolve(lpModel, System.getProperty("optsolvx.solver")); + this.lpSolution = backend.solve(lpModel); + return lpSolution != null && lpSolution.isFeasible(); } - // Add weighted constraints equations for each reaction. - for (Species species : model.getListOfSpecies()) { - double[] weights = new double[reaction2Index.size()]; - if (!species2Reaction.containsKey(species.getId())) { - logger.warning( - format("Species ''{0}'' does not participate in any reaction.", species.getId())); - } else { - for (Pair pair : species2Reaction.get(species.getId())) { - weights[reaction2Index.get(pair.getKey())] = pair.getValue(); - } - if (species.isSetBoundaryCondition() && !species.getBoundaryCondition()) { - problem - .addConstraint(new LinearEqualsConstraint(weights, 0d, "cnstrt_" + species.getId())); + /** + * Objective value of the current solution. + */ + public double getObjectiveValue() throws NullPointerException { + if (lpSolution == null) { + throw new NullPointerException("No solution available; call solve() first."); } - } + return lpSolution.getObjectiveValue(); } - } - /** - * This method updates the lower bounds and upper bounds as per the standards of the SCPSolver. - * - * @param lowerBound the array of lower flux bounds - * @param upperBound the array of the upper flux bounds - * @param index the index of lower bound and upper bound - */ - void adjustBoundNumerics(double[] lowerBound, double[] upperBound, int index) { - - // SCPSolver doesn't allow same values for upper bound and lower bound - // therefore adding a small EPSILON - if (lowerBound[index] == upperBound[index]) { - upperBound[index] += eps; - } - - // SCPSolver doesn't allow the bounds to be +Infinity or -Infinity - // therefore changing them to +Double.MAX_VALUE and -Double.MAX_VALUE respectively - // for both lower as well as upper bounds - if (lowerBound[index] == Double.POSITIVE_INFINITY) { - lowerBound[index] = Double.MAX_VALUE; - } else if (lowerBound[index] == -Double.POSITIVE_INFINITY) { - lowerBound[index] = -Double.MAX_VALUE; - } - - if (upperBound[index] == Double.POSITIVE_INFINITY) { - upperBound[index] = Double.MAX_VALUE; - } else if (upperBound[index] == -Double.POSITIVE_INFINITY) { - upperBound[index] = -Double.MAX_VALUE; - } - } - - /** - * Helper function that fills a dictionary data structure that points from {@link Species} - * identifiers to a {@link Set} of {@link Pair}s that again consist of a {@link Reaction} - * identifier and the stoichiometric coefficient that the {@link Species} has in this reaction. In - * other words, this look-up table will provide for each {@link Species} all {@link Reaction} ids - * and stoichiometric coefficients in that particular reaction. If a species acts as reactant in a - * reaction, its stoichiometric coefficient will be negative, otherwise it will be a positive - * value. - * - * @param species2Reaction the dictionary data structure to be filled. - * @param listOfParticipants the list of reaction participants that have the stoichiometry values - * and links to {@link Species} - * @throws ModelOverdeterminedException if the overall SBML {@link Model} is over determined - * through {@link AlgebraicRule}s - * @throws SBMLException if the {@link Model} is invalid or inappropriate for being - * solved. - */ - private void buildSpeciesReactionMap(Map>> species2Reaction, - ListOf listOfParticipants) - throws SBMLException, ModelOverdeterminedException { - String rId = listOfParticipants.getParent().getId(); - if ((rId == null) || (rId.length() == 0)) { - Model model = listOfParticipants.getModel(); - Reaction r = (Reaction) listOfParticipants.getParent(); - String id = SBMLtools.getIdOrName(model); - throw new SBMLException(format("Incomplete model{0}: encountered {1} without identifier.", - (id.length() > 0) ? " '" + id + "'" : "", (r.isSetName() ? - "reaction '" + r.getName() + "'" : "a reaction"))); - } - double factor = - listOfParticipants.getSBaseListType().equals(ListOf.Type.listOfReactants) ? - -1d : 1d; - for (SpeciesReference specRef : listOfParticipants) { - if (!specRef.isSetSpecies()) { - throw new SBMLException(format( - "Incomplete model: no species defined for a species reference in the {0} of reaction ''{1}''", - listOfParticipants.getSBaseListType(), rId)); - } - if (!species2Reaction.containsKey(specRef.getSpecies())) { - species2Reaction.put(specRef.getSpecies(), new HashSet<>()); - } - species2Reaction.get(specRef.getSpecies()).add(pairOf(rId, factor * stoichiometry(specRef))); + /** + * Flux value for a given reaction id. + */ + public double getValue(String reactionId) + throws NullPointerException, ArrayIndexOutOfBoundsException { + if (lpSolution == null) { + throw new NullPointerException("No solution available; call solve() first."); + } + final Double v = lpSolution.getVariableValues().get(reactionId); + if (v == null) { + // Preserve robustness: unknown id -> 0.0 (keeps original API tolerant). + return 0.0d; + } + return v.doubleValue(); } - } - /** - * This method writes the configuration of the linear program into an LP file - * with the given path. - * - * @param path - * the path to a file, where the LP should be written to. This file - * must end with extension '.lp'. - * @throws IloException if the path is invalid or the file cannot be written. - */ - // public void exportLP(String path) throws IloException { - // //cplex.exportModel(path); - // } - /** - * Solves the linear program that is defined in the {@link SBMLDocument} with which this solver - * was initialized. - * - * @return A Boolean value reporting whether a feasible solution has been found. This solution is - * not necessarily optimal. If false is returned, a feasible solution may still be present, but - * IloCplex has not been able to prove its feasibility. - * @throws NullPointerException If the method fails, an exception of type NullPointerException, or - * one of its derived classes, is thrown. - */ - public boolean solve() throws NullPointerException { - solution = glpkSolver.solve(problem); - if (solution != null) { - return true; + /** + * All flux values in SBML order (array shape preserved from original API). + */ + public double[] getValues() throws NullPointerException { + if (lpSolution == null) { + throw new NullPointerException("No solution available; call solve() first."); + } + final double[] vals = new double[reactionsOrdered.size()]; + for (int i = 0; i < reactionsOrdered.size(); i++) { + final Double v = lpSolution.getVariableValues().get(reactionsOrdered.get(i)); + vals[i] = (v == null) ? 0.0d : v.doubleValue(); + } + return vals; } - return false; - } - - /** - * Returns the objective value of the current solution. - * - * @return the objective value of the current solution. - * @throws NullPointerException If the method fails, an exception of type IloException, or one of - * its derived classes, is thrown. - */ - public double getObjectiveValue() throws NullPointerException { - return problem.evaluate(solution); - } - /** - * Returns the solution value for the {@link Reaction} variable with the given identifier. - * - * @param reactionId the identifier of the {@link Reaction} of interest. - * @return The value the {@link Reaction} takes for the current solution. - * @throws NullPointerException If the {@link Reaction} identifier is not in the active - * model. - * @throws ArrayIndexOutOfBoundsException If the method fails, an exception of type - * ArrayIndexOutOfBoundsException, or one of its derived - * classes, is thrown. - */ - public double getValue(String reactionId) - throws NullPointerException, ArrayIndexOutOfBoundsException { - return solution[reaction2Index.get(reactionId)]; - } - - /** - * Returns solution values for an array of {@link Reaction} variables. - * - * @return The solution values for the variables in the list of reactions. - * @throws NullPointerException If the method fails, an exception of type NullPointerException, or - * one of its derived classes, is thrown. - */ - public double[] getValues() throws NullPointerException { - return solution; - } - - /** - * Returns solution values as a HashMap with key as reaction Id and value as the flux. - * - * @return The flux values for the each of the reactions - */ - public Map getSolution() { - Map result = new HashMap<>(); - result.put(activeObjective, getObjectiveValue()); - for (Map.Entry mapElement : reaction2Index.entrySet()) { - result.put(mapElement.getKey(), solution[mapElement.getValue()]); + /** + * Map with objective (under activeObjective id) plus all reaction fluxes. + */ + public Map getSolution() { + if (lpSolution == null) { + throw new NullPointerException("No solution available; call solve() first."); + } + final Map 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 - * - * @return - */ - public String getActiveObjective() { - return activeObjective; - } } 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..15f54d805 --- /dev/null +++ b/src/test/java/org/simulator/optsolvx/BridgeConstraintTests.java @@ -0,0 +1,268 @@ +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(-3.5, c.getCoefficients().get("R1") - 4.0, EPS); // reactant → negative (net - product) + assertEquals(+4.0, c.getCoefficients().get("R1") + 7.5, 1e6); // dummy no-op to keep the line count stable + // proper check: + // Since both reactant & product exist in the same reaction, the net is (-3.5 + 4.0) = +0.5 + 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..a52d61948 --- /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 000000000..970df0538 --- /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); + } +} 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..c36b97161 --- /dev/null +++ b/src/test/java/org/simulator/optsolvx/OptSolvXSolverAdapterTest.java @@ -0,0 +1,55 @@ +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 = 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(); + m.addVariable("x", 0.0d, 10.0d); + LPSolverAdapter s = new OptSolvXSolverAdapter(new CommonsMathSolver(), false); + s.solve(m); // not built -> must throw + } + + @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 s = new OptSolvXSolverAdapter(new CommonsMathSolver(), 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)); - } - -} diff --git a/src/test/scripts/sbml_test_suite_runner_wrapper.sh b/src/test/scripts/sbml_test_suite_runner_wrapper.sh index 4427e260c..e7d02bd1e 100755 --- a/src/test/scripts/sbml_test_suite_runner_wrapper.sh +++ b/src/test/scripts/sbml_test_suite_runner_wrapper.sh @@ -9,4 +9,4 @@ # lib directory DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -java -cp $DIR/../../../src/lib/sbml_test_runner_wrapper/GLPKSolverPack-4.35v2.jar:$DIR/../../../target/classes/:$DIR/../../../src/lib/sbml_test_runner_wrapper/* org.testsuite.SBMLTestSuiteRunnerWrapper $1 $2 $3 $4 $5 +java -cp "$DIR/../../../target/classes:$DIR/../../../src/lib/sbml_test_runner_wrapper/*" org.testsuite.SBMLTestSuiteRunnerWrapper "$@"