Hooking Your Solver to ojAlgo

This post aims to demonstrate how to make a solver usable from ExpressionsBasedModel. To do that we first implement a very simple solver, and then the integration.

There are some very high-level ojAlgo concepts (interfaces) you should know about:

  • Optimisation.Model – a description of an optimisation problem.
  • Optimisation.Solver – a specific algorithm implementation.
  • Optimisation.Integration – translates between a model’s general problem description and a solver’s specific data structure.

There needs to be one Optimisation.Integration per Optimisation.Model / Optimisation.Solver combination. It makes sense to have multiple solver implementations handling different kinds of problems, but only one tool to model them. In ojAlgo the modelling tool is ExpressionsBasedModel.

Now imagine you have some existing 3:d party solver and you want to make that available to (usable from) ojAlgo’s ExpressionsBasedModel. We need to create two classes – one that implements Optimisation.Solver and another that implements Optimisation.Integration. It’s important to distinguish between these two. The integration must be stateless. There will only ever be one instance created, that you register with the model. The solver implements the actual algorithm (delegating to the 3:d party solver). There’s typically a new solver instance created every time you solve a new problem.

Example Code

HookingUpSolver.java
import static org.ojalgo.function.constant.PrimitiveMath.*;

import java.util.ArrayList;
import java.util.List;

import org.ojalgo.OjAlgoUtils;
import org.ojalgo.equation.Equation;
import org.ojalgo.matrix.store.PhysicalStore;
import org.ojalgo.matrix.store.R064Store;
import org.ojalgo.matrix.task.iterative.ConjugateGradientSolver;
import org.ojalgo.netio.BasicLogger;
import org.ojalgo.optimisation.Expression;
import org.ojalgo.optimisation.ExpressionsBasedModel;
import org.ojalgo.optimisation.Optimisation;
import org.ojalgo.optimisation.Variable;
import org.ojalgo.optimisation.convex.ConvexSolver;
import org.ojalgo.optimisation.integer.IntegerSolver;
import org.ojalgo.optimisation.linear.LinearSolver;
import org.ojalgo.structure.Structure1D.IntIndex;
import org.ojalgo.structure.Structure2D.IntRowColumn;

/**
 * An example of how to:
 * <ol>
 * <li>implement a simple solver, and
 * <li>hook that up to be used with {@link ExpressionsBasedModel}.
 * </ol>
 *
 * @see https://www.ojalgo.org/2025/02/hooking-your-solver-to-ojalgo/
 */
public class HookingUpSolver {

    /**
     * The {@link Optimisation.Integration} interface actually declares a few more methods than what we
     * implement here. Since we extend the abstract {@link ExpressionsBasedModel.Integration} class, and use
     * it the standard way, several things are taken care of for us.
     */
    static final class UnconstrainedIntegration extends ExpressionsBasedModel.Integration<UnconstrainedSolver> {

        /**
         * Turn parameter scaling on/off
         */
        private static final boolean ADJUSTED = false;

        /**
         * Build the solver instance.
         */
        @Override
        public HookingUpSolver.UnconstrainedSolver build(final ExpressionsBasedModel model) {

            List<Variable> variables = model.getVariables();

            Expression objective = model.objective();

            /*
             * In this case we know that the objective function is quadratic, and that there are no
             * constraints. We can use this information to build a simple equation system. Essentially we take
             * the partial derivatives of the objective function with respect to each variable, and create a
             * system of linear equations by equating each of them to 0.0.
             */

            List<Equation> equations = new ArrayList<>(variables.size());
            for (int i = 0, limit = variables.size(); i < limit; i++) {
                equations.add(Equation.sparse(i, limit));
            }

            for (IntRowColumn quadraticKey : objective.getQuadraticKeySet()) {
                double quadraticValue = objective.doubleValue(quadraticKey, ADJUSTED);
                equations.get(quadraticKey.row).add(quadraticKey.column, quadraticValue);
                equations.get(quadraticKey.column).add(quadraticKey.row, quadraticValue);
            }

            for (IntIndex linearKey : objective.getLinearKeySet()) {
                double linearValue = objective.doubleValue(linearKey, ADJUSTED);
                equations.get(linearKey.index).setRHS(-linearValue);
            }

            return new UnconstrainedSolver(equations, model.options);
        }

        /**
         * Can the solver, built by this integration, handle the model?
         */
        @Override
        public boolean isCapable(final ExpressionsBasedModel model) {

            /*
             * Verify that the model is unconstrained and that the objective function is quadratic (not just
             * linear). ExpressionsBasedModel cannot model expressions of higher order than 2, and we check
             * that at least one factor is quadratic – that's NOT enough to guarantee that the problem is
             * solvable by this solver, but it's a good start. To be thorough we should check the eigenvalues
             * of the Hessian.
             */
            return model.constraints().count() == 0 && model.objective().isAnyQuadraticFactorNonZero();
        }

    }

    /**
     * Solves unconstrained optimisation problems. A very simple implementation that only works for (some)
     * quadratic problems modelled in {@link ExpressionsBasedModel}.
     * <p>
     * Actually this solver solves an equation system, and doesn't know where that comes from. Transforming
     * the unconstrained optimisation model into an equation system that this solver can solve is done by the
     * {@link UnconstrainedIntegration} class.
     * <p>
     * If this solver should be usable without that integration class (without {@link ExpressionsBasedModel}),
     * it would need some other way to build the equation system from a multivariate function or similar.
     */
    static final class UnconstrainedSolver implements Optimisation.Solver {

        /**
         * Requires the equation system body matrix to be symmetric and positive-definite. So this wont be
         * able to solve anything we throw at it
         */
        private final ConjugateGradientSolver myDelegateSolver = new ConjugateGradientSolver();
        private final List<Equation> myEquations;

        UnconstrainedSolver(final List<Equation> equations, final Optimisation.Options options) {
            super();
            myEquations = equations;
            myDelegateSolver.configurator().debug(options.logger_appender);
        }

        @Override
        public Result solve(final Result kickStarter) {

            PhysicalStore<Double> solution = R064Store.FACTORY.column(kickStarter);

            /*
             * Solve the equation system, updating the solution vector, returning an error/accuracy measure.
             */
            double error = myDelegateSolver.resolve(myEquations, solution);

            /*
             * Is the solution good enough?
             */
            State state = error < HUNDREDTH ? Optimisation.State.OPTIMAL : Optimisation.State.APPROXIMATE;

            /*
             * The objective function value
             */
            double value = this.evaluate(solution);

            return Result.wrap(solution).withState(state).withValue(value);
        }

        /**
         * Evaluate the objective function at the given solution.
         */
        private double evaluate(final PhysicalStore<Double> solution) {

            double tmpVal;
            double retVal = ZERO;

            for (Equation equation : myEquations) {
                tmpVal = equation.dot(solution) * HALF;
                tmpVal -= equation.getRHS();
                retVal += tmpVal * solution.doubleValue(equation.index);
            }

            return retVal;
        }

    }

    /**
     * Integrations absolutely must be stateless, and you typically only ever need one instance.
     */
    static final ExpressionsBasedModel.Integration<UnconstrainedSolver> INTEGRATION = new UnconstrainedIntegration();

    /**
     * Solve a small problem you'll find towards the end of this document:
     * https://www.ece.mcmaster.ca/~xwu/part4.pdf
     * <p>
     * Using the custom solver and integration defined above.
     */
    public static void main(final String[] args) {

        BasicLogger.debug();
        BasicLogger.debug(HookingUpSolver.class);
        BasicLogger.debug(OjAlgoUtils.getTitle());
        BasicLogger.debug(OjAlgoUtils.getDate());
        BasicLogger.debug();

        /*
         * Register the solver integration with ExpressionsBasedModel. This is a one-time operation – you only
         * need to do this once per JVM.
         */
        ExpressionsBasedModel.addIntegration(INTEGRATION);
        /*
         * Provided it's capable to solve the problem, ExpressionsBasedModel will now use the
         * UnconstrainedSolver rather than the default set of solvers. The integration's isCapable method
         * determines if the solver will be used.
         */

        ExpressionsBasedModel model = new ExpressionsBasedModel();

        /*
         * max: 2xy + 2x - x^2 - 2y^2
         */

        Variable x = model.addVariable("x");
        Variable y = model.addVariable("y");

        Expression expression = model.addExpression().weight(1);
        /*
         * Setting a weight on the expression is what turns it into (part of) the objective function. In a
         * more complex case the objective function could be a weighted combination of several expressions.
         * Here we only have one expression but still need to set a non-zero weight.
         */
        expression.set(x, y, 2); // +2xy
        expression.set(x, 2); // +2x
        expression.set(x, x, -1); // -x^2
        expression.set(y, y, -2); // -2y^2

        BasicLogger.debug("Expected result: {}", Optimisation.Result.of(2.0, Optimisation.State.OPTIMAL, 2.0, 1.0));
        BasicLogger.debug();

        /*
         * Turn on debug logging of the solver. With each iteration the current solution is printed as well as
         * a measure of the current error and the iteration number.
         */
        model.options.debug(UnconstrainedSolver.class);

        Optimisation.Result result = model.maximise();

        BasicLogger.debug();
        BasicLogger.debug("Actual result: {}", result);

        /*
         * ojAlgo's buil-in solvers are integrated with ExpressionsBasedModel the same way any other solver
         * is. The main built-in solvers are:
         */
        ExpressionsBasedModel.addIntegration(LinearSolver.INTEGRATION);
        ExpressionsBasedModel.addIntegration(ConvexSolver.INTEGRATION);
        ExpressionsBasedModel.addIntegration(IntegerSolver.INTEGRATION);
        /*
         * To learn how more complex integrations are built, study the source code of those.
         */
        /*
         * The only difference between the built-in solvers and custom ones is that you never have worry about
         * adding/registering built-in solver integrations. Adding them explicitly, like we just did above,
         * works fine but is entirely redundant. You only need to add/register solvers you want to use rather
         * than the built-in ones.
         */

    }

}

Console Output

class HookingUpSolver
ojAlgo
2025-02-08

Expected result: OPTIMAL 2.0 @ { 2.0, 1.0 }

0: NaN – { 0.0, 0.0 }
1: 0.8944271909999159 – { 1.0, 0.0 }
2: 0.0 – { 2.0, 1.0 }

Actual result: OPTIMAL 2.0 @ { 2, 1 }

Interpretation

Not that much to say. The solver output shows the initial solution, as well as the solutions after each iteration. With the second iteration the optimal solution is found.

Available 3:d Party Solver Integrations

There are multiple 3:d party solvers integrated with ojAlgo’s ExpressionsBasedModel. ojAlgo itself has zero dependencies, so each of these are in a separate dependency artifact.

Gurobi

https://www.gurobi.com

  • Top quality commercial solver.
  • Native code with a java interface.
  • Can solve all types of problems you can model with ExpressionsBasedModel.
<!-- https://mvnrepository.com/artifact/org.ojalgo/ojalgo-gurobi -->
<dependency>
    <groupId>org.ojalgo</groupId>
    <artifactId>ojalgo-gurobi</artifactId>
    <version>3.1.0</version>
</dependency>

org.ojalgo.optimisation.solver.gurobi.SolverGurobi#INTEGRATION

CPLEX

https://www.ibm.com/products/ilog-cplex-optimization-studio

  • Top quality commercial solver.
  • Native code with a java interface.
  • Can solve all types of problems you can model with ExpressionsBasedModel.
<!-- https://mvnrepository.com/artifact/org.ojalgo/ojalgo-cplex -->
<dependency>
    <groupId>org.ojalgo</groupId>
    <artifactId>ojalgo-cplex</artifactId>
    <version>3.1.0</version>
</dependency>

org.ojalgo.optimisation.solver.cplex.SolverCPLEX#INTEGRATION

OR-Tools

https://developers.google.com/optimization

  • A collection of open source solvers, and also possible to use some commercial
  • Native code with a java interface.
  • Great support for LP, ok for MIP and I believe some support for QP. In addition to that it contains a constraint programming solver that is supposedly rather good.
<!-- https://mvnrepository.com/artifact/org.ojalgo/ojalgo-ortools -->
<dependency>
    <groupId>org.ojalgo</groupId>
    <artifactId>ojalgo-ortools</artifactId>
    <version>2.0.0</version>
</dependency>

org.ojalgo.optimisation.solver.ortools.SolverORTools#INTEGRATION

MOSEK

https://www.mosek.com

  • Top quality commercial solver.
  • Native code with a java interface.
  • Can solve all types of problems you can model with ExpressionsBasedModel.
<!-- https://mvnrepository.com/artifact/org.ojalgo/ojalgo-mosek -->
<dependency>
    <groupId>org.ojalgo</groupId>
    <artifactId>ojalgo-mosek</artifactId>
    <version>3.2.0</version>
</dependency>

org.ojalgo.optimisation.solver.mosek.SolverMosek#INTEGRATION

Hipparchus

https://www.hipparchus.org

  • Open Source
  • Pure Java
  • Hipparchus started as a fork of Apache Commons Math. It contains both LP and convex (QP) solvers. Not sure about its QP capability, but the built-in ojAlgo solvers should be better than Hipparchus’. For LP they definitely are.
<!-- https://mvnrepository.com/artifact/org.ojalgo/ojalgo-hipparchus -->
<dependency>
    <groupId>org.ojalgo</groupId>
    <artifactId>ojalgo-hipparchus</artifactId>
    <version>2.0.0</version>
</dependency>

org.ojalgo.optimisation.solver.hipparchus.SolverHipparchus#INTEGRATION

Apache Commons Math

https://commons.apache.org/proper/commons-math/

  • Open source
  • Pure Java
  • Apache Commons Math (ACM) contains an LP solver. It offers no benefit over ojAlgo’s LP solver. In addition the ACM project seems to have problems. v3.6.1 is almost a decade old and v4.0.0 still only exists in beta (which is more than 2 years old). As far as I know the developers moved to Hipparchus.
<!-- https://mvnrepository.com/artifact/org.ojalgo/ojalgo-commons-math3 -->
<dependency>
    <groupId>org.ojalgo</groupId>
    <artifactId>ojalgo-commons-math3</artifactId>
    <version>4.0.1</version>
</dependency>

org.ojalgo.optimisation.solver.acm.SolverACM#INTEGRATION

JOptimizer

https://github.com/vincentk/joptimizer (not the original)

  • Open source
  • Pure Java
  • A collection of solvers for convex optimisation problems. In theory JOptimizer should be able to solve some types of problems the standard ojAlgo solvers cannot (quadratically constrained). In practice we find that this library doesn’t work very well, and as far as we know the project is abandoned.
<!-- https://mvnrepository.com/artifact/org.ojalgo/ojalgo-joptimizer -->
<dependency>
    <groupId>org.ojalgo</groupId>
    <artifactId>ojalgo-joptimizer</artifactId>
    <version>3.0.3</version>
</dependency>

org.ojalgo.optimisation.solver.joptimizer.SolverJOptimizer#INTEGRATION


All of these 3:d party solver integrations are available, with source code, from https://mvnrepository.com/artifact/org.ojalgo

Even if the solvers are not of interest to you, the integration code may be. They serve as additional examples of how to write a 3:d party solver integration. Start by looking at the OR-Tools and CPLEX integrations.