Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ Place for licenses, java links and passings. Inspired and copied from SBSCL and
![Code Size](https://img.shields.io/github/languages/code-size/draeger-lab/OptSolvX.svg?style=plastic)
![Downloads of all releases](https://img.shields.io/github/downloads/draeger-lab/OptSolvX/total.svg?style=plastic)

OptSolvX is a flexible Java library for solving linear programming (LP) problems with multiple interchangeable solver backends.
OptSolvX is a flexible Java library for solving linear programming (LP) problems with multiple interchangeable solver
backends.
It provides a clean, test-driven API for building, comparing and extending LP solvers.
OptSolvX is intended for applications in mathematics, research, and systems biology.

Expand All @@ -29,19 +30,17 @@ OptSolvX is intended for applications in mathematics, research, and systems biol
- Clean logging & validation (build checks, bounds, relations)
- Easy to extend with custom backends; demo included


► Status
----------------------------

- LP modeling & solving: maximize/minimize, EQ/LEQ/GEQ constraints, variable bounds, build() workflow
- Backends: Commons Math adapter ready; ojAlgo adapter planned
- Builds: Java 22 by default; optional Java 8 bytecode via compat8 profile (classifier jdk8)


► Installation
----------------------------

Requirements: Maven ≥ 3.9, Java 22 (default).
Requirements: Maven ≥ 3.9, Java 22 (default).

Optional: build an additional Java 8 bytecode artifact via profile compat8.

Expand All @@ -51,19 +50,31 @@ cd OptSolvX
```

Default (Java 22) - installs to local Maven repo

```
mvn clean install
```

Optional: Java 8 bytecode JAR (classifier jdk8)

```
mvn -P compat8 -DskipTests clean package
```

Artifacts

- target/optsolvx-<version>.jar - Java 22 (default)
- target/optsolvx-<version>-jdk8.jar - Java 8 bytecode (compatibility)

► Java Version
----------------------------

OptSolvX requires **Java 22** to build and run.
The build enforces this via the Maven Enforcer plugin.

If a different JDK is active, the build will fail early with a clear message.
Optional: use the `compat8` profile to produce a Java 8 bytecode JAR.


► Testing
----------------------------
Expand Down Expand Up @@ -97,20 +108,23 @@ Run the built-in demo (max x + y with two constraints) using the Commons Math ba
**From IDE:** run `org.optsolvx.solver.SolverDemo`.

**From Maven (CLI):**

```bash
mvn -q exec:java
# If needed:
# mvn -q -DskipTests exec:java -Dexec.mainClass=org.optsolvx.solver.SolverDemo
```

Expected Output:

```bash
Variable values: {x=3.0, y=0.5}
Objective: 3.5
Feasible: true
```

***Optional debug:*** enable verbose model logging in the demo:

```java
model.setDebug(true); // call before model.build()
```
Expand Down
102 changes: 102 additions & 0 deletions docs/figures/OptSolvX Linear Programming Core Model.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,37 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.5.0</version>

<!-- Enables: mvn enforcer:enforce -->
<configuration>
<rules>
<requireJavaVersion>
<version>[22,23)</version>
Copy link
Collaborator

Choose a reason for hiding this comment

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

allow higher Java versions also

Copy link
Collaborator

Choose a reason for hiding this comment

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

No whitespaces in filenames: OptSolvX Linear Programming Core Model.svg

<message>
OptSolvX requires JDK 22 to build. Set JAVA_HOME to 22.x or use toolchains.
</message>
</requireJavaVersion>
</rules>
<fail>true</fail>
</configuration>

<!-- Keeps it bound to 'validate' as well -->
<executions>
<execution>
<id>enforce-java</id>
<phase>validate</phase>
<goals>
<goal>enforce</goal>
</goals>
</execution>
</executions>
</plugin>

</plugins>
</build>

Expand Down
102 changes: 61 additions & 41 deletions src/main/java/org/optsolvx/model/AbstractLPModel.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.optsolvx.model;

import static java.text.MessageFormat.format;

import java.util.*;
import java.util.logging.Logger;

import org.optsolvx.model.OptimizationDirection;

/**
Expand Down Expand Up @@ -37,42 +39,38 @@ public class AbstractLPModel {
// The optimization direction of the model (MAXIMIZE or MINIMIZE). Default ist Maximize.
private OptimizationDirection direction = OptimizationDirection.MAXIMIZE;

// Optional per-model solver preference (e.g., "ojalgo", "commons-math", ...).
private String preferredSolver;

// True after build() is called; no further changes allowed
private boolean built = false;

private void beforeModelChange() {
if (built) {
built = false;
if (debug) LOGGER.warning(
format(
"{0}: Model was changed after build(); 'built' status reset. " +
"Please call build() again before solving.",
getClass().getSimpleName()
)
);
if (debug)
LOGGER.warning(format("{0}: Model was changed after build(); 'built' status reset. " + "Please call build() again before solving.", getClass().getSimpleName()));
}
}

/**
* Adds a new variable to the model.
*
* @param name unique name of the variable
* @param name unique name of the variable
* @param lower lower bound (inclusive)
* @param upper upper bound (inclusive)
* @return index of the variable in the variables list
* @throws IllegalArgumentException if the name already exists
* @throws IllegalStateException if the model is already built
* @throws IllegalStateException if the model is already built
*/
public int addVariable(String name, double lower, double upper) {
beforeModelChange();
if (variableIndices.containsKey(name)) {
if (debug) LOGGER.warning("Duplicate variable name: " + name);
throw new IllegalArgumentException("Variable name already exists: " + name);
}
if (debug) LOGGER.info(format(
"{0}: Added variable: {1} [{2,number,0.####}, {3,number,0.####}]",
getClass().getSimpleName(), name, lower, upper
));
if (debug)
LOGGER.info(format("{0}: Added variable: {1} [{2,number,0.####}, {3,number,0.####}]", getClass().getSimpleName(), name, lower, upper));
Variable var = new Variable(name, lower, upper);
int idx = variables.size();
variables.add(var);
Expand All @@ -83,24 +81,22 @@ public int addVariable(String name, double lower, double upper) {
/**
* Adds a new linear constraint to the model.
*
* @param name unique name of the constraint
* @param name unique name of the constraint
* @param coeffs map of variable name to coefficient in the constraint
* @param rel type of constraint (LEQ, GEQ, EQ)
* @param rhs right-hand side value of the constraint
* @param rel type of constraint (LEQ, GEQ, EQ)
* @param rhs right-hand side value of the constraint
* @return the new Constraint object
* @throws IllegalArgumentException if the name already exists
* @throws IllegalStateException if the model is already built
* @throws IllegalStateException if the model is already built
*/
public Constraint addConstraint(String name, Map<String, Double> coeffs, Constraint.Relation rel, double rhs) {
beforeModelChange();
if (constraintIndices.containsKey(name)) {
if (debug) LOGGER.warning("Duplicate constraint name: " + name);
throw new IllegalArgumentException("Constraint name already exists: " + name);
}
if (debug) LOGGER.info(format(
"{0}: Added constraint {1} ({2}) rhs={3, number,0.####}, vars={4}",
getClass().getSimpleName(), name, rel, rhs, coeffs.keySet()
));
if (debug)
LOGGER.info(format("{0}: Added constraint {1} ({2}) rhs={3, number,0.####}, vars={4}", getClass().getSimpleName(), name, rel, rhs, coeffs.keySet()));
Constraint c = new Constraint(name, coeffs, rel, rhs);
int idx = constraints.size();
constraints.add(c);
Expand All @@ -111,7 +107,7 @@ public Constraint addConstraint(String name, Map<String, Double> coeffs, Constra
/**
* Sets the objective function for the model.
*
* @param coeffs map of variable name to objective coefficient
* @param coeffs map of variable name to objective coefficient
* @param direction the optimization direction (MAXIMIZE or MINIMIZE)
* @throws IllegalStateException if the model is already built
*/
Expand All @@ -122,57 +118,72 @@ public void setObjective(Map<String, Double> coeffs, OptimizationDirection direc
this.direction = direction;
}

/**
* Returns the optional per-model solver preference, or null if not set.
*/
public String getPreferredSolver() {
return preferredSolver;
}

/**
* Sets the optional per-model solver preference (normalized, nullable).
*/
public void setPreferredSolver(String name) {
this.preferredSolver = (name == null ? null : name.trim());
}

/**
* Finalizes the model, assigns indices to variables and constraints.
* After calling build(), no further variables or constraints can be added
* until the model is changed again. If the model is changed after build(),
* the 'built' flag will be reset and build() must be called again before solving.
* <p>
* Logs a summary when the model is finalized.
* </p>
*/
public void build() {
if (built) return;
if (debug) LOGGER.info(format(
"{0}: Building model with {1} variables and {2} constraints.",
getClass().getSimpleName(), variables.size(), constraints.size()
));
if (debug)
LOGGER.info(format("{0}: Building model with {1} variables and {2} constraints.", getClass().getSimpleName(), variables.size(), constraints.size()));
built = true;
if (debug) LOGGER.info(format(
"{0}: Model finalized. No further modifications allowed.",
getClass().getSimpleName()
));
if (debug)
LOGGER.info(format("{0}: Model finalized. No further modifications allowed.", getClass().getSimpleName()));
}

/**
* Returns the internal list of variables.
* Modifications to this list affect the model directly.
*
* @return the variables list
*/
public List<Variable> getVariables() { return variables; }
public List<Variable> getVariables() {
return variables;
}

/**
* Returns the internal list of constraints.
* Modifications to this list affect the model directly.
*
* @return the constraint list
*/
public List<Constraint> getConstraints() { return constraints; }
public List<Constraint> getConstraints() {
return constraints;
}

/**
* Returns the variable object with the specified name.
*
* @param name the variable name
* @return the Variable object
* @throws IllegalArgumentException if not found
*/
public Variable getVariable(String name) {
Integer idx = variableIndices.get(name);
if (idx == null) throw new IllegalArgumentException("No such variable: " + name);
return variables.get(idx);

return variables.get(idx);
}

/**
* Returns the constraint object with the specified name.
*
* @param name the constraint name
* @return the Constraint object
* @throws IllegalArgumentException if not found
Expand All @@ -183,8 +194,6 @@ public Constraint getConstraint(String name) {
return constraints.get(idx);
}



/**
* Returns an unmodifiable map of the objective function coefficients.
*/
Expand All @@ -194,6 +203,7 @@ public Map<String, Double> getObjectiveCoefficients() {

/**
* Returns the current optimization direction (MAXIMIZE or MINIMIZE).
*
* @return the optimization direction
*/
public OptimizationDirection getDirection() {
Expand All @@ -202,13 +212,22 @@ public OptimizationDirection getDirection() {

/**
* Sets the optimization direction for this model.
*
* @param direction the optimization direction (MAXIMIZE or MINIMIZE)
*/
public void setDirection(OptimizationDirection direction) {
beforeModelChange();
this.direction = direction;
}

/**
* Fluent builder: set optimization direction and return this.
*/
public AbstractLPModel direction(OptimizationDirection dir) {
setDirection(dir);
return this;
}

/**
* @return true if build() has been called and the model is finalized
*/
Expand All @@ -218,6 +237,7 @@ public boolean isBuilt() {

/**
* Returns the index of a variable by its name.
*
* @param name the variable name
* @return index of the variable in the model
* @throws IllegalArgumentException if not found
Expand All @@ -230,6 +250,7 @@ public int getVariableIndex(String name) {

/**
* Returns the index of a constraint by its name.
*
* @param name the constraint name
* @return index of the constraint in the model
* @throws IllegalArgumentException if not found
Expand All @@ -252,8 +273,7 @@ public String toString() {
for (Variable v : variables) sb.append(" ").append(v).append("\n");
sb.append("Constraints:\n");
for (Constraint c : constraints) sb.append(" ").append(c).append("\n");
sb.append("Objective: ").append(objectiveCoefficients)
.append(" direction=").append(direction).append("\n");
sb.append("Objective: ").append(objectiveCoefficients).append(" direction=").append(direction).append("\n");
return sb.toString();
}

Expand All @@ -262,7 +282,7 @@ public String toString() {
* Logging is OFF by default.
* Call setDebug(true) before model building to activate.
* Example: model.setDebug(true); // Logging on
* model.setDebug(false); // Logging off
* model.setDebug(false); // Logging off
*
*/
public void setDebug(boolean debug) {
Expand Down
Loading