diff --git a/pom.xml b/pom.xml index a1b53c06..59106150 100644 --- a/pom.xml +++ b/pom.xml @@ -512,6 +512,12 @@ + + + org.ojalgo + ojalgo + 49.0.0 + diff --git a/src/main/java/org/simulator/fba/OjAlgoLinearProgramSolver.java b/src/main/java/org/simulator/fba/OjAlgoLinearProgramSolver.java new file mode 100644 index 00000000..8de844aa --- /dev/null +++ b/src/main/java/org/simulator/fba/OjAlgoLinearProgramSolver.java @@ -0,0 +1,205 @@ +package org.simulator.fba; + +import java.util.ArrayList; + +import org.ojalgo.optimisation.ExpressionsBasedModel; +import org.ojalgo.optimisation.Optimisation; +import org.ojalgo.optimisation.Variable; +import org.ojalgo.optimisation.Expression; + +import scpsolver.constraints.Constraint; +import scpsolver.constraints.LinearBiggerThanEqualsConstraint; +import scpsolver.constraints.LinearEqualsConstraint; +import scpsolver.constraints.LinearSmallerThanEqualsConstraint; +import scpsolver.lpsolver.LinearProgramSolver; +import scpsolver.problems.LinearProgram; + +/** + * Pure Java implementation of {@link LinearProgramSolver} using ojAlgo as backend. + * + * This solver does not require any native libraries (no GLPK, no JNI). + * + * NOTE: The incremental constraint methods (addLinear*Constraint) are not + * currently supported and will throw UnsupportedOperationException. SBSCL + * only uses {@link #solve(LinearProgram)} with constraints already stored + * in the {@link LinearProgram}. + */ +public class OjAlgoLinearProgramSolver implements LinearProgramSolver { + + /** + * Optional time limit (seconds). Currently stored but not enforced. + */ + private int timeConstraintSeconds = -1; + + @Override + public double[] solve(LinearProgram lp) { + + if (lp == null) { + throw new IllegalArgumentException("LinearProgram must not be null"); + } + + // Build an ojAlgo model + final ExpressionsBasedModel model = new ExpressionsBasedModel(); + + // Objective coefficients + final double[] c = lp.getC(); + final int n = c.length; + + // Variable bounds (may or may not be set) + final boolean hasBounds; + final double[] lower; + final double[] upper; + try { + hasBounds = lp.hasBounds(); + if (hasBounds) { + lower = lp.getLowerbound(); + upper = lp.getUpperbound(); + } else { + lower = null; + upper = null; + } + } catch (NoSuchMethodError e) { + // Very old SCPSolver versions would not have hasBounds(); not expected here. + throw new IllegalStateException("SCPSolver LinearProgram is missing bounds methods", e); + } + + // Integrality and boolean flags (may all be false in typical FBA) + boolean[] integerVars = null; + boolean[] booleanVars = null; + try { + integerVars = lp.getIsinteger(); + } catch (NoSuchMethodError e) { + // ignore, treat as continuous + } + try { + booleanVars = lp.getIsboolean(); + } catch (NoSuchMethodError e) { + // ignore, treat as continuous + } + + // Create variables + final Variable[] vars = new Variable[n]; + for (int i = 0; i < n; i++) { + Variable v = Variable.make("x" + i); + + if (hasBounds && lower != null && upper != null) { + v.lower(lower[i]).upper(upper[i]); + } + + boolean isBool = (booleanVars != null && i < booleanVars.length && booleanVars[i]); + boolean isInt = (integerVars != null && i < integerVars.length && integerVars[i]); + + if (isBool) { + v.lower(0.0).upper(1.0).integer(true); + } else if (isInt) { + v.integer(true); + } + + vars[i] = v; + model.addVariable(v); + } + + // Objective: set variable weights and always minimise. + // For maximisation problems, we negate the coefficients. + final boolean isMin = lp.isMinProblem(); + for (int i = 0; i < n; i++) { + double coeff = c[i]; + if (!isMin) { + coeff = -coeff; + } + if (coeff != 0.0) { + vars[i].weight(coeff); + } + } + + // Constraints from the LinearProgram + final ArrayList constraints = lp.getConstraints(); + int constrIndex = 0; + for (Constraint con : constraints) { + if (!(con instanceof scpsolver.constraints.LinearConstraint)) { + // Ignore non-linear constraints (not expected in FBA) + continue; + } + + scpsolver.constraints.LinearConstraint lc = + (scpsolver.constraints.LinearConstraint) con; + double[] coeff = lc.getC(); + double rhs = lc.getT(); + + Expression expr = model.addExpression("c" + constrIndex++); + for (int j = 0; j < coeff.length && j < n; j++) { + if (coeff[j] != 0.0) { + expr.set(vars[j], coeff[j]); + } + } + + if (con instanceof LinearEqualsConstraint) { + expr.level(rhs); // sum(coeff * x) = rhs + } else if (con instanceof LinearBiggerThanEqualsConstraint) { + expr.lower(rhs); // sum(coeff * x) >= rhs + } else if (con instanceof LinearSmallerThanEqualsConstraint) { + expr.upper(rhs); // sum(coeff * x) <= rhs + } else { + // Fallback: treat as equality + expr.level(rhs); + } + } + + // Solve (minimise or maximise) + Optimisation.Result result; + try { + result = model.minimise(); + } catch (Exception e) { + // Any numerical/solver error: behave like "no solution" + e.printStackTrace(System.err); + return null; + } + + if (result == null) { + // No solution returned by ojAlgo + return null; + } + + // Extract solution vector + double[] solution = new double[n]; + for (int i = 0; i < n; i++) { + solution[i] = result.doubleValue(i); + } + + return solution; + } + + @Override + public void addLinearBiggerThanEqualsConstraint(LinearBiggerThanEqualsConstraint c) { + throw new UnsupportedOperationException( + "Incremental constraint addition is not supported by OjAlgoLinearProgramSolver"); + } + + @Override + public void addLinearSmallerThanEqualsConstraint(LinearSmallerThanEqualsConstraint c) { + throw new UnsupportedOperationException( + "Incremental constraint addition is not supported by OjAlgoLinearProgramSolver"); + } + + @Override + public void addEqualsConstraint(LinearEqualsConstraint c) { + throw new UnsupportedOperationException( + "Incremental constraint addition is not supported by OjAlgoLinearProgramSolver"); + } + + @Override + public String getName() { + return "ojAlgo"; + } + + @Override + public String[] getLibraryNames() { + // No native libraries required + return new String[0]; + } + + @Override + public void setTimeconstraint(int seconds) { + this.timeConstraintSeconds = seconds; + } +} \ No newline at end of file diff --git a/src/test/java/org/sbscl/fba/OjAlgoLinearProgramSolverTest.java b/src/test/java/org/sbscl/fba/OjAlgoLinearProgramSolverTest.java new file mode 100644 index 00000000..1870f5db --- /dev/null +++ b/src/test/java/org/sbscl/fba/OjAlgoLinearProgramSolverTest.java @@ -0,0 +1,47 @@ +package org.sbscl.fba; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.simulator.fba.OjAlgoLinearProgramSolver; + +import scpsolver.constraints.LinearSmallerThanEqualsConstraint; +import scpsolver.problems.LinearProgram; + +/** + * Basic tests for the pure Java {@link OjAlgoLinearProgramSolver}. + */ +public class OjAlgoLinearProgramSolverTest { + + @Test + public void testSimpleMaximisation() { + // Maximise 10*x0 + 6*x1 + 4*x2 + // subject to x0 + x1 + x2 <= 100, x >= 0 + LinearProgram lp = new LinearProgram(new double[] {10.0, 6.0, 4.0}); + + lp.addConstraint(new LinearSmallerThanEqualsConstraint( + new double[] {1.0, 1.0, 1.0}, + 100.0, + "c1")); + + lp.setLowerbound(new double[] {0.0, 0.0, 0.0}); + lp.setUpperbound(new double[] { + Double.MAX_VALUE, + Double.MAX_VALUE, + Double.MAX_VALUE + }); + + OjAlgoLinearProgramSolver solver = new OjAlgoLinearProgramSolver(); + double[] solution = solver.solve(lp); + + assertNotNull(solution, "Solver should return a solution array"); + assertEquals(3, solution.length, "Solution dimension must match number of variables"); + + double objective = lp.evaluate(solution); + + // Best is to put all 100 units into x0: + // 10*100 = 1000, with x1 = x2 = 0 + assertEquals(1000.0, objective, 1e-6, "Objective value should be optimal"); + } +} \ No newline at end of file