Optimisation Cookbook

Complete, runnable models for the problem shapes that come up most often in real systems. Each recipe is a single Java file with a main method, depends on nothing but ojAlgo, and is formulated idiomatically with ExpressionsBasedModel — copy it, replace the data with yours, and run it. The code is maintained with the other example code and kept current with each release. Under each recipe is the result it prints, after the standard header every example starts with (verified with ojAlgo 57.3.1).

If you are new to ojAlgo, add the Maven dependency and start with production planning. If you already have a problem, find its shape below.

Find your problem

If you need to… Shape Type
decide how much of each product to make with limited resources Production planning LP
mix ingredients to meet a specification at minimum cost Blending LP
pick the most valuable subset of items within a budget Knapsack MIP
match workers to tasks, jobs to machines, one to one Assignment MIP
fit items into as few containers, trucks or servers as possible Bin packing MIP
choose the cheapest set of options that covers every requirement Set covering MIP
staff every hour of the day from a set of shift patterns Shift scheduling MIP
decide which sites to open and which customers each serves Facility location MIP
allocate money across assets for minimum risk at a target return Portfolio optimisation QP

Routing problems (visiting a set of locations in the best order) are covered separately in Model and Solve the Traveling Salesman Problem.

Rules that apply to every model

  1. Check the state before reading the solution. result.getState().isOptimal() for a proven optimum; isFeasible() if you have set a time limit and accept a good solution that is not proven optimal. After an INFEASIBLE or UNBOUNDED result the variables have no values — getValue() returns null.
  2. Read integer and binary values by rounding, never with ==. A binary at 0.9999999999 is a 1. Use getValue().doubleValue() > 0.5 for binaries and Math.round(...) for integer counts.
  3. Write every constraint as bounds on an expression. newExpression(name) followed by lower(...), upper(...) or level(...) for equality, then set(variable, coefficient) for each term. Give each expression a unique name — creating a second expression with a name already in use replaces the first, and that constraint silently disappears. Build names from the loop indices, as the recipes do. Ratios and percentages become linear rows against a total, as in blending.
  4. The objective is the weights. weight(...) on a variable or on an expression puts it in the objective; then call minimise() or maximise(). Quadratic terms go on an expression, as in portfolio optimisation.
  5. Avoid big-M where a tighter link exists. Tie a yes/no decision to the quantities it enables through the row that already limits them, and add the per-pair link when it is cheap to do so. Both bin packing and facility location show this. A weak formulation is the most common reason a MIP is slow.
  6. Break symmetry between identical resources. If bins, trucks or machines are interchangeable, order them. Otherwise the solver proves the same thing once per permutation.
  7. Set limits in production. model.options.time_abort (milliseconds) and a MIP gap via model.options.integer(IntegerStrategy.DEFAULT.withGapTolerance(...)), as in facility location.
  8. When something is wrong, write the model out. model.writeTo(Path.of("model.lp")) writes it in the CPLEX LP format (the format follows the file name; .mps also works) so you can read what you actually built, or run the identical model in another solver. Write it after calling minimise() or maximise() — the direction is recorded when the model is solved, so a file written before a maximise() call says Minimize. Name your variables and expressions; those names appear in the file.

A model that is correct but too slow is usually a formulation problem first and a solver problem second. See MIP Strategy Configuration for tuning the built-in integer solver, and Using the Optimisation Service for running the same model on native solvers without changing it.

Production planning (LP)

You make several products that compete for the same limited resources — machine time, material, labour — and want the most profitable mix. One continuous variable per product, one constraint per resource. This is the shape behind most "how much of each" questions.

ProductionPlanning.java
import org.ojalgo.OjAlgoUtils;
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;

/**
 * Production planning (LP): decide how much of each product to make to maximise profit, subject to limited
 * machine hours and raw material.
 *
 * @see https://www.ojalgo.org/optimisation-cookbook/#production-planning
 */
public class ProductionPlanning {

    public static void main(final String[] args) {

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

        String[] products = { "Chairs", "Tables", "Shelves" };
        double[] profit = { 45, 80, 30 }; // per unit
        double[] maxDemand = { 100, 40, 60 }; // units that can be sold

        String[] resources = { "Machine hours", "Timber (m)", "Finishing hours" };
        double[] capacity = { 400, 600, 150 };
        double[][] usage = { // usage[resource][product] per unit
                { 2, 5, 1 }, //
                { 4, 7, 3 }, //
                { 1, 2, 0.5 } };

        ExpressionsBasedModel model = new ExpressionsBasedModel();

        // One variable per product: quantity to make. The weight is its objective coefficient.
        Variable[] make = new Variable[products.length];
        for (int p = 0; p < products.length; p++) {
            make[p] = model.newVariable(products[p]).lower(0).upper(maxDemand[p]).weight(profit[p]);
        }

        // One constraint per resource: total usage <= capacity.
        for (int r = 0; r < resources.length; r++) {
            Expression limit = model.newExpression(resources[r]).upper(capacity[r]);
            for (int p = 0; p < products.length; p++) {
                limit.set(make[p], usage[r][p]);
            }
        }

        Optimisation.Result result = model.maximise();

        // Always check the state before using the values.
        if (!result.getState().isOptimal()) {
            throw new IllegalStateException("No optimal plan: " + result.getState());
        }

        BasicLogger.debug("Profit: {}", result.getValue());
        for (Variable v : make) {
            BasicLogger.debug("{}: {}", v.getName(), v.getValue().toPlainString());
        }
    }
}

Output:

Profit: 6700.0
Chairs: 100
Tables: 20
Shelves: 20

Blending (LP)

You mix ingredients into a product that must meet specifications — feed, fuel, alloys, fertiliser, a financial product — at minimum cost. The one idea to take away: a percentage requirement such as "at least 18% protein" is not a ratio constraint, it is a linear constraint on quantities against the batch size.

Blending.java
import org.ojalgo.OjAlgoUtils;
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;

/**
 * Blending (LP): mix raw ingredients into 1000 kg of animal feed at minimum cost, meeting minimum and
 * maximum nutrient contents. Percentage requirements are written as linear constraints on quantities, not as
 * ratios.
 *
 * @see https://www.ojalgo.org/optimisation-cookbook/#blending
 */
public class Blending {

    public static void main(final String[] args) {

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

        double batch = 1000; // kg

        String[] ingredients = { "Maize", "Soybean meal", "Fishmeal", "Limestone" };
        double[] costPerKg = { 0.30, 0.90, 1.50, 0.10 };
        double[] available = { 800, 400, 100, 50 }; // kg in stock

        // Nutrient content as a fraction of weight, per ingredient.
        double[] protein = { 0.09, 0.44, 0.65, 0.00 };
        double[] calcium = { 0.001, 0.003, 0.050, 0.380 };
        double[] fibre = { 0.020, 0.070, 0.000, 0.000 };

        ExpressionsBasedModel model = new ExpressionsBasedModel();

        Variable[] kg = new Variable[ingredients.length];
        for (int i = 0; i < ingredients.length; i++) {
            kg[i] = model.newVariable(ingredients[i]).lower(0).upper(available[i]).weight(costPerKg[i]);
        }

        // The batch must weigh exactly 1000 kg.
        Expression total = model.newExpression("Batch size").level(batch);
        for (Variable v : kg) {
            total.set(v, 1);
        }

        // "At least 18% protein" becomes: sum(protein_i * kg_i) >= 0.18 * batch.
        Expression proteinMin = model.newExpression("Protein >= 18%").lower(0.18 * batch);
        Expression calciumRange = model.newExpression("Calcium 0.8-1.2%").lower(0.008 * batch).upper(0.012 * batch);
        Expression fibreMax = model.newExpression("Fibre <= 5%").upper(0.05 * batch);
        for (int i = 0; i < ingredients.length; i++) {
            proteinMin.set(kg[i], protein[i]);
            calciumRange.set(kg[i], calcium[i]);
            fibreMax.set(kg[i], fibre[i]);
        }

        Optimisation.Result result = model.minimise();

        if (!result.getState().isOptimal()) {
            throw new IllegalStateException("No feasible blend: " + result.getState());
        }

        BasicLogger.debugFormatted("Cost: %.2f", result.getValue());
        for (Variable v : kg) {
            BasicLogger.debugFormatted("%s: %.1f kg", v.getName(), v.getValue().doubleValue());
        }
    }
}

Output:

Cost: 453.02
Maize: 708.1 kg
Soybean meal: 264.2 kg
Fishmeal: 0.0 kg
Limestone: 27.6 kg

Knapsack (MIP)

You choose a subset of items, projects or investments, each with a value and a cost, under a single budget. One binary variable per item. Several budgets (weight and volume, or cost per quarter) just means several constraint rows.

Knapsack.java
import org.ojalgo.OjAlgoUtils;
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;

/**
 * Knapsack (MIP): choose which items to take to maximise total value without exceeding a weight limit.
 * Each item is a binary (0/1) variable.
 *
 * @see https://www.ojalgo.org/optimisation-cookbook/#knapsack
 */
public class Knapsack {

    public static void main(final String[] args) {

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

        String[] items = { "Tent", "Stove", "Camera", "Book", "Water filter", "Laptop", "Jacket", "Rope" };
        double[] value = { 90, 40, 60, 10, 70, 50, 65, 20 };
        double[] weight = { 4.0, 1.5, 1.0, 0.5, 0.8, 2.5, 1.2, 0.9 };
        double capacity = 8.0;

        ExpressionsBasedModel model = new ExpressionsBasedModel();

        Variable[] take = new Variable[items.length];
        for (int i = 0; i < items.length; i++) {
            take[i] = model.newVariable(items[i]).binary().weight(value[i]);
        }

        Expression weightLimit = model.newExpression("Weight").upper(capacity);
        for (int i = 0; i < items.length; i++) {
            weightLimit.set(take[i], weight[i]);
        }

        Optimisation.Result result = model.maximise();

        if (!result.getState().isOptimal()) {
            throw new IllegalStateException("Not solved to optimality: " + result.getState());
        }

        BasicLogger.debug("Value: {}", result.getValue());
        for (Variable v : take) {
            // Read binaries with a threshold, never with == 1.
            if (v.getValue().doubleValue() > 0.5) {
                BasicLogger.debug("Take {}", v.getName());
            }
        }
    }
}

Output:

Value: 305.0
Take Tent
Take Camera
Take Water filter
Take Jacket
Take Rope

Assignment (MIP)

You match people to tasks, vehicles to jobs, or requests to servers, one-to-one or one-to-at-most-one, at minimum cost. The constraint matrix is totally unimodular, so the LP relaxation is already integral and this solves quickly even when large.

Assignment.java
import org.ojalgo.OjAlgoUtils;
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;

/**
 * Assignment (MIP): assign each task to exactly one worker, each worker to at most one task, at minimum total
 * cost. With equal numbers of workers and tasks the LP relaxation is already integral, so this solves fast
 * even at a few hundred by a few hundred.
 *
 * @see https://www.ojalgo.org/optimisation-cookbook/#assignment
 */
public class Assignment {

    public static void main(final String[] args) {

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

        String[] workers = { "Anna", "Bo", "Carl", "Dana", "Erik" };
        String[] tasks = { "Backend", "Frontend", "Database", "Testing" };
        double[][] cost = { // hours for worker w to do task t
                { 9, 11, 14, 11 }, //
                { 6, 15, 13, 13 }, //
                { 12, 13, 6, 8 }, //
                { 11, 9, 10, 12 }, //
                { 10, 12, 9, 7 } };

        ExpressionsBasedModel model = new ExpressionsBasedModel();

        Variable[][] x = new Variable[workers.length][tasks.length];
        for (int w = 0; w < workers.length; w++) {
            for (int t = 0; t < tasks.length; t++) {
                x[w][t] = model.newVariable(workers[w] + "->" + tasks[t]).binary().weight(cost[w][t]);
            }
        }

        // Every task is done by exactly one worker.
        for (int t = 0; t < tasks.length; t++) {
            Expression covered = model.newExpression("Task " + tasks[t]).level(1);
            for (int w = 0; w < workers.length; w++) {
                covered.set(x[w][t], 1);
            }
        }

        // Every worker does at most one task.
        for (int w = 0; w < workers.length; w++) {
            Expression busy = model.newExpression("Worker " + workers[w]).upper(1);
            for (int t = 0; t < tasks.length; t++) {
                busy.set(x[w][t], 1);
            }
        }

        Optimisation.Result result = model.minimise();

        if (!result.getState().isOptimal()) {
            throw new IllegalStateException("Not solved to optimality: " + result.getState());
        }

        BasicLogger.debug("Total hours: {}", result.getValue());
        for (int w = 0; w < workers.length; w++) {
            for (int t = 0; t < tasks.length; t++) {
                if (x[w][t].getValue().doubleValue() > 0.5) {
                    BasicLogger.debug("{} -> {}", workers[w], tasks[t]);
                }
            }
        }
    }
}

Output:

Total hours: 28.0
Bo -> Backend
Carl -> Database
Dana -> Frontend
Erik -> Testing

Bin packing (MIP)

You fit items into as few fixed-size containers as possible — trucks, pallets, rolls of material, virtual machines. The formulation below shows two things that decide whether a model like this solves in a second or never: the capacity row doubles as the link to the bin's on/off variable, and identical bins are forced to be used in order so the solver does not explore every permutation of the same packing.

BinPacking.java
import org.ojalgo.OjAlgoUtils;
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;

/**
 * Bin packing (MIP): pack items into as few bins (trucks, containers, VMs) as possible. Two details matter
 * for speed: link item-to-bin variables to the bin's capacity row (not with a separate big-M), and break the
 * symmetry between identical bins by requiring bins to be used in order.
 *
 * @see https://www.ojalgo.org/optimisation-cookbook/#bin-packing
 */
public class BinPacking {

    public static void main(final String[] args) {

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

        double[] size = { 42, 63, 25, 80, 37, 51, 18, 70, 29, 45, 33, 60 };
        double capacity = 100;
        int nbItems = size.length;
        int nbBins = nbItems; // upper bound: one item per bin

        ExpressionsBasedModel model = new ExpressionsBasedModel();

        Variable[] used = new Variable[nbBins];
        for (int b = 0; b < nbBins; b++) {
            used[b] = model.newVariable("Bin " + b).binary().weight(1); // minimise bins used
        }

        Variable[][] put = new Variable[nbItems][nbBins];
        for (int i = 0; i < nbItems; i++) {
            for (int b = 0; b < nbBins; b++) {
                put[i][b] = model.newVariable("Item " + i + " in bin " + b).binary();
            }
        }

        // Each item goes in exactly one bin.
        for (int i = 0; i < nbItems; i++) {
            Expression once = model.newExpression("Place item " + i).level(1);
            for (int b = 0; b < nbBins; b++) {
                once.set(put[i][b], 1);
            }
        }

        // Capacity: sum(size_i * put_ib) <= capacity * used_b, written as sum(...) - capacity * used_b <= 0.
        // This one row both limits the load and forces the bin to be "used" if anything is in it.
        for (int b = 0; b < nbBins; b++) {
            Expression load = model.newExpression("Capacity bin " + b).upper(0);
            for (int i = 0; i < nbItems; i++) {
                load.set(put[i][b], size[i]);
            }
            load.set(used[b], -capacity);
        }

        // Symmetry breaking: bin b+1 may only be used if bin b is.
        for (int b = 0; b + 1 < nbBins; b++) {
            model.newExpression("Order " + b).upper(0).set(used[b + 1], 1).set(used[b], -1);
        }

        Optimisation.Result result = model.minimise();

        if (!result.getState().isOptimal()) {
            throw new IllegalStateException("Not solved to optimality: " + result.getState());
        }

        BasicLogger.debug("Bins used: {}", Math.round(result.getValue()));
        for (int b = 0; b < nbBins; b++) {
            StringBuilder contents = new StringBuilder();
            double load = 0;
            for (int i = 0; i < nbItems; i++) {
                if (put[i][b].getValue().doubleValue() > 0.5) {
                    contents.append(" ").append(size[i]);
                    load += size[i];
                }
            }
            if (load > 0) {
                BasicLogger.debug("Bin {} ({}):{}", b, load, contents);
            }
        }
    }
}

Output:

Bins used: 6
Bin 0 (98.0): 80.0 18.0
Bin 1 (88.0): 37.0 51.0
Bin 2 (85.0): 25.0 60.0
Bin 3 (99.0): 70.0 29.0
Bin 4 (96.0): 63.0 33.0
Bin 5 (87.0): 42.0 45.0

Several packings use six bins, so which one is reported can differ between runs or versions. The number of bins will not.

Set covering (MIP)

You pick the cheapest set of options so that every requirement is met by at least one of them — sites covering areas, staff covering skills, tests covering features. The same shape with level(1) instead of lower(1) is set partitioning (each requirement covered exactly once).

SetCovering.java
import org.ojalgo.OjAlgoUtils;
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;

/**
 * Set covering (MIP): pick the cheapest set of candidate sites (warehouses, base stations, ambulance depots)
 * so that every customer area is served by at least one of them.
 *
 * @see https://www.ojalgo.org/optimisation-cookbook/#set-covering
 */
public class SetCovering {

    public static void main(final String[] args) {

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

        String[] sites = { "North", "South", "East", "West", "Centre", "Harbour" };
        double[] cost = { 120, 100, 90, 110, 150, 80 };
        String[] areas = { "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8" };
        boolean[][] covers = { // covers[site][area]
                { true, true, false, false, true, false, false, false }, //
                { false, false, true, true, false, false, false, true }, //
                { false, true, true, false, false, true, false, false }, //
                { true, false, false, true, false, false, true, false }, //
                { false, true, false, true, true, true, false, false }, //
                { false, false, false, false, false, true, true, true } };

        ExpressionsBasedModel model = new ExpressionsBasedModel();

        Variable[] open = new Variable[sites.length];
        for (int s = 0; s < sites.length; s++) {
            open[s] = model.newVariable(sites[s]).binary().weight(cost[s]);
        }

        for (int a = 0; a < areas.length; a++) {
            Expression served = model.newExpression("Cover " + areas[a]).lower(1);
            for (int s = 0; s < sites.length; s++) {
                if (covers[s][a]) {
                    served.set(open[s], 1);
                }
            }
        }

        Optimisation.Result result = model.minimise();

        if (!result.getState().isOptimal()) {
            throw new IllegalStateException("Not solved to optimality: " + result.getState());
        }

        BasicLogger.debug("Cost: {}", result.getValue());
        for (Variable v : open) {
            if (v.getValue().doubleValue() > 0.5) {
                BasicLogger.debug("Open {}", v.getName());
            }
        }
    }
}

Output:

Cost: 300.0
Open North
Open South
Open Harbour

Shift scheduling (MIP)

You staff a service across the day with a menu of shift patterns and need enough people on duty every hour. The variables are integer counts — how many people start each pattern — not one binary per person. Model individuals only when individuals genuinely differ; counting is dramatically faster.

ShiftScheduling.java
import org.ojalgo.OjAlgoUtils;
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;

/**
 * Shift scheduling (MIP): decide how many staff start each shift pattern so that required staffing is met in
 * every hour of the day, at minimum cost. Integer (not binary) variables: "how many people on this shift".
 *
 * @see https://www.ojalgo.org/optimisation-cookbook/#shift-scheduling
 */
public class ShiftScheduling {

    public static void main(final String[] args) {

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

        // Required staff per hour, 06:00 to 22:00 (16 hours).
        int[] required = { 2, 3, 5, 6, 6, 5, 7, 8, 6, 5, 5, 6, 7, 6, 4, 3 };
        int startHour = 6;

        // Shift patterns: start offset (hours after 06:00), length in hours, cost per person.
        int[] shiftStart = { 0, 0, 2, 4, 6, 8, 10, 12 };
        int[] shiftLength = { 8, 4, 8, 8, 8, 8, 6, 4 };
        double[] shiftCost = { 8, 5, 8, 8, 8, 8, 6.5, 5 };

        ExpressionsBasedModel model = new ExpressionsBasedModel();

        Variable[] staff = new Variable[shiftStart.length];
        for (int s = 0; s < staff.length; s++) {
            String name = String.format("%02d:00 +%dh", startHour + shiftStart[s], shiftLength[s]);
            staff[s] = model.newVariable(name).integer().lower(0).weight(shiftCost[s]);
        }

        for (int h = 0; h < required.length; h++) {
            Expression cover = model.newExpression("Hour " + (startHour + h)).lower(required[h]);
            for (int s = 0; s < staff.length; s++) {
                if (h >= shiftStart[s] && h < shiftStart[s] + shiftLength[s]) {
                    cover.set(staff[s], 1);
                }
            }
        }

        Optimisation.Result result = model.minimise();

        if (!result.getState().isOptimal()) {
            throw new IllegalStateException("Not solved to optimality: " + result.getState());
        }

        BasicLogger.debug("Cost: {}", result.getValue());
        for (Variable v : staff) {
            long count = Math.round(v.getValue().doubleValue());
            if (count > 0) {
                BasicLogger.debug("{}: {}", v.getName(), count);
            }
        }
    }
}

Output:

Cost: 96.5
06:00 +8h: 3
08:00 +8h: 3
12:00 +8h: 3
16:00 +6h: 3
18:00 +4h: 1

Facility location (MIP)

You decide which warehouses, depots or plants to open and which customers each one serves, trading fixed opening costs against transport costs. This recipe also shows how to set a time limit and a MIP gap, and how to handle the result when optimality has not been proven.

FacilityLocation.java
import org.ojalgo.OjAlgoUtils;
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.integer.IntegerStrategy;
import org.ojalgo.type.context.NumberContext;

/**
 * Capacitated facility location (MIP): choose which warehouses to open and how to serve customer demand from
 * them, minimising fixed opening costs plus transport costs. Also shows how to set a time limit and a MIP gap,
 * and how to accept a good-but-not-proven-optimal answer.
 *
 * @see https://www.ojalgo.org/optimisation-cookbook/#facility-location
 */
public class FacilityLocation {

    public static void main(final String[] args) {

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

        String[] warehouses = { "Gothenburg", "Jonkoping", "Stockholm", "Malmo", "Orebro" };
        double[] openCost = { 5000, 3500, 6000, 4500, 3000 };
        double[] capacity = { 500, 300, 600, 400, 250 };

        String[] customers = { "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8" };
        double[] demand = { 80, 120, 60, 150, 90, 110, 70, 130 };
        double[][] unitCost = { // unitCost[warehouse][customer]
                { 4, 6, 9, 12, 7, 3, 10, 8 }, //
                { 5, 4, 6, 8, 5, 6, 7, 6 }, //
                { 11, 9, 4, 3, 8, 12, 5, 7 }, //
                { 3, 8, 12, 14, 9, 4, 13, 11 }, //
                { 8, 5, 5, 6, 4, 9, 4, 5 } };

        ExpressionsBasedModel model = new ExpressionsBasedModel();

        Variable[] open = new Variable[warehouses.length];
        for (int w = 0; w < warehouses.length; w++) {
            open[w] = model.newVariable("Open " + warehouses[w]).binary().weight(openCost[w]);
        }

        // share[w][c] = fraction of customer c's demand served from warehouse w (continuous).
        Variable[][] share = new Variable[warehouses.length][customers.length];
        for (int w = 0; w < warehouses.length; w++) {
            for (int c = 0; c < customers.length; c++) {
                share[w][c] = model.newVariable(warehouses[w] + "->" + customers[c]).lower(0).upper(1)
                        .weight(unitCost[w][c] * demand[c]);
            }
        }

        // All demand is served.
        for (int c = 0; c < customers.length; c++) {
            Expression served = model.newExpression("Demand " + customers[c]).level(1);
            for (int w = 0; w < warehouses.length; w++) {
                served.set(share[w][c], 1);
            }
        }

        for (int w = 0; w < warehouses.length; w++) {
            // Capacity, only if open: sum(demand_c * share_wc) - capacity_w * open_w <= 0.
            Expression cap = model.newExpression("Capacity " + warehouses[w]).upper(0);
            for (int c = 0; c < customers.length; c++) {
                cap.set(share[w][c], demand[c]);
            }
            cap.set(open[w], -capacity[w]);

            // Strong linking: share_wc <= open_w for every pair. Redundant for correctness, but it makes the
            // LP relaxation much tighter than the capacity row alone, so branch-and-bound has far less to do.
            for (int c = 0; c < customers.length; c++) {
                model.newExpression("Link " + warehouses[w] + "/" + customers[c]).upper(0).set(share[w][c], 1).set(open[w], -1);
            }
        }

        // Stop after 30 s, or when the solution is proven within 0.1% of optimal.
        model.options.time_abort = 30_000L;
        model.options.integer(IntegerStrategy.DEFAULT.withGapTolerance(NumberContext.of(3)));

        Optimisation.Result result = model.minimise();

        // With limits set, FEASIBLE (a valid solution, optimality not proven) is a legitimate outcome.
        Optimisation.State state = result.getState();
        if (!state.isFeasible()) {
            throw new IllegalStateException("No feasible solution: " + state);
        }

        BasicLogger.debugFormatted("%s, total cost: %.2f", state, result.getValue());
        for (int w = 0; w < warehouses.length; w++) {
            if (open[w].getValue().doubleValue() > 0.5) {
                StringBuilder served = new StringBuilder();
                for (int c = 0; c < customers.length; c++) {
                    double s = share[w][c].getValue().doubleValue();
                    if (s > 1e-6) {
                        served.append(String.format(" %s(%.0f%%)", customers[c], 100 * s));
                    }
                }
                BasicLogger.debug("{}:{}", warehouses[w], served);
            }
        }
    }
}

Output:

OPTIMAL, total cost: 13760.00
Jonkoping: C1(100%) C2(92%) C6(100%)
Stockholm: C2(8%) C3(100%) C4(100%) C5(100%) C7(100%) C8(100%)

Portfolio optimisation (QP)

You allocate a budget across assets to minimise risk (variance) for a target expected return — the Markowitz model. A quadratic objective is an Expression with a weight and quadratic terms set with set(variable1, variable2, value). The covariance matrix must be positive semidefinite — a sample covariance matrix always is, but one assembled from separately estimated parts may not be.

PortfolioOptimisation.java
import org.ojalgo.OjAlgoUtils;
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;

/**
 * Mean-variance portfolio (QP): find the minimum-variance long-only portfolio that reaches a target expected
 * return. A quadratic objective is an Expression with a weight and quadratic (variable, variable) terms.
 *
 * @see https://www.ojalgo.org/optimisation-cookbook/#portfolio
 */
public class PortfolioOptimisation {

    public static void main(final String[] args) {

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

        String[] assets = { "Equity SE", "Equity US", "Equity EM", "Bonds", "Real estate" };
        double[] expectedReturn = { 0.070, 0.080, 0.095, 0.030, 0.060 };
        double[][] covariance = {
                { 0.0400, 0.0180, 0.0240, 0.0010, 0.0120 }, //
                { 0.0180, 0.0361, 0.0220, 0.0008, 0.0100 }, //
                { 0.0240, 0.0220, 0.0729, 0.0012, 0.0150 }, //
                { 0.0010, 0.0008, 0.0012, 0.0025, 0.0010 }, //
                { 0.0120, 0.0100, 0.0150, 0.0010, 0.0324 } };
        double targetReturn = 0.065;

        ExpressionsBasedModel model = new ExpressionsBasedModel();

        Variable[] w = new Variable[assets.length];
        for (int i = 0; i < assets.length; i++) {
            w[i] = model.newVariable(assets[i]).lower(0).upper(0.4); // long only, max 40% per asset
        }

        // Fully invested.
        Expression budget = model.newExpression("Budget").level(1);
        for (Variable v : w) {
            budget.set(v, 1);
        }

        // Expected return at least the target.
        Expression ret = model.newExpression("Return").lower(targetReturn);
        for (int i = 0; i < assets.length; i++) {
            ret.set(w[i], expectedReturn[i]);
        }

        // Objective: minimise portfolio variance w' C w. Weight 1 puts the expression in the objective.
        Expression variance = model.newExpression("Variance").weight(1);
        for (int i = 0; i < assets.length; i++) {
            for (int j = 0; j < assets.length; j++) {
                variance.set(w[i], w[j], covariance[i][j]);
            }
        }

        Optimisation.Result result = model.minimise();

        if (!result.getState().isOptimal()) {
            throw new IllegalStateException("No optimal portfolio: " + result.getState());
        }

        BasicLogger.debugFormatted("Volatility: %.2f%%", 100 * Math.sqrt(result.getValue()));
        for (Variable v : w) {
            BasicLogger.debugFormatted("%s: %.1f%%", v.getName(), 100 * v.getValue().doubleValue());
        }
    }
}

Output:

Volatility: 11.63%
Equity SE: 8.2%
Equity US: 32.9%
Equity EM: 16.7%
Bonds: 27.5%
Real estate: 14.6%

Using these with an AI coding assistant

These programs are written to be copied, by people and by tools. If an assistant writes your optimisation code, point it at this page (or at llms.txt) and have it adapt the closest recipe rather than write a model from scratch. Then check that the result still follows rules 1 to 3: it checks the state, rounds integer values, and names every constraint uniquely. For assistants that support skills, the ojAlgo skill packages these rules and recipes; in Claude Code, install it with /plugin marketplace add optimatika/ojAlgo-skills.