All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.chocosolver.lp.LinearProgram Maven / Gradle / Ivy

There is a newer version: 4.10.17
Show newest version
/*
 * This file is part of choco-solver, http://choco-solver.org/
 *
 * Copyright (c) 2022, IMT Atlantique. All rights reserved.
 *
 * Licensed under the BSD 4-clause license.
 *
 * See LICENSE file in the project root for full license information.
 */
package org.chocosolver.lp;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Optional;

/**
 * 

A linear program, equipped with a Simplex method.

*

This is based on "Introduction to Algorithms, Third Edition", * By Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein, * 29.3 The simplex algorithm.

*

There are various ways to declare a LP. * Either, by giving it as a standard form providing the A nxm-matrix, the m-vector b and the n-vector c. *

 {@code
 * double[] c = {5, 7};
 * double[][] A = {{4, 3}, {2, 3}};
 * double[] b = {36, 48};
 * LinearProgram lp = new LinearProgram(A, b, c);
 * }
*

* Or, by declaring the variables first and then adding some constraints and the objective function. * In that case, all the variables must be declared first and have to be nonnegative (≥ 0). *

 {@code
 *  LinearProgram lp = new LinearProgram(false);
 *  lp.makeVariables(2);
 *  lp.addLeq(new double[]{4, 3}, 36);
 *  lp.addLeq(new double[]{2, 3}, 48);
 *  lp.setObjective(true, new double[]{5, 7});
 *  }
*

* Next, a call to {@link #simplex()} runs the Simplex algorithm and returns the status of the resolution. * If the status is {@link Status#FEASIBLE} * then the value assigned to each variable is accessible with {@code lp.lp.value(i);} * and the value of the objective function with {@code lp.objective();}. *

*
* * @author Charles Prud'homme * @since 04/07/2022 */ public class LinearProgram { private static class Slack { // number of non-basic variables private final int n; // number of basic variables private final int m; // a mxn matrix private final double[][] A; // an m-vector private final double[] b; // an n-vector private final double[] c; // an m-vector, the basic variables private final int[] B; // an n-vector, the non-basic variables private final int[] N; // an integer private double v; /** * Slack form of a linear program * * @param a an m.n matrix * @param b an m-vector * @param c an n-vector */ public Slack(double[][] a, double[] b, double[] c) { this.m = b.length; this.n = c.length; this.A = a.clone(); this.b = b.clone(); this.c = c.clone(); this.N = new int[n]; this.B = new int[m]; this.v = 0; initialBasicSolution(); } /** * Slack form of a linear program with N and B already defined * * @param a an m.n matrix * @param b an m-vector * @param c an n-vector * @param N an n-vector * @param B an m-vector * @param v a double */ public Slack(double[][] a, double[] b, double[] c, int[] N, int[] B, double v) { this.m = b.length; this.n = c.length; this.A = a; this.b = b; this.c = c; this.N = N; this.B = B; this.v = v; } /** * Initialize N and B wrt basic solution */ private void initialBasicSolution() { this.v = 0; for (int i = 0; i < n; i++) { this.N[i] = i; } for (int i = 0; i < m; i++) { this.B[i] = i + n; } } /** * The method takes as input a slack form and it modifies in-place A, b, c, v, N and B. * * @param l index of the leaving variable * @param e index of the entering variable */ void pivot(int l, int e, boolean trace) { if (trace) System.out.printf("[pivot] e: x%d, l: x%d\n", B[l], N[e]); // Compute the coefficients of the equation for new basic variable x_e b[l] /= A[l][e]; for (int j = 0; j < n; j++) { if (j != e) { A[l][j] /= A[l][e]; } } A[l][e] = 1 / A[l][e]; // Compute the coefficients of the remaining constraints for (int i = 0; i < m; i++) { if (i != l) { b[i] -= A[i][e] * b[l]; for (int j = 0; j < n; j++) { if (j != e) { A[i][j] = A[i][j] - A[i][e] * A[l][j]; } } A[i][e] *= -A[l][e]; } } // Compute the objective function v += c[e] * b[l]; for (int j = 0; j < n; j++) { if (j != e) { c[j] -= c[e] * A[l][j]; } } c[e] *= -A[l][e]; // Update basic and non-basic variables set int x = N[e]; N[e] = B[l]; B[l] = x; } private void setValues(double[] x) { for (int i = 0; i < m; ++i) { if (B[i] < n) x[B[i]] = b[i]; } } @Override public String toString() { StringBuilder st = new StringBuilder(); st.append("z = ").append(v); for (int i = 0; i < n; i++) { st.append(c[i] >= 0. ? " + " : " - ") .append(String.format("%.4f", Math.abs(c[i]))) .append(" * x").append(N[i]); } st.append("\n"); for (int j = 0; j < m; j++) { st.append("x") .append(B[j]) .append(" = ") .append(b[j]); for (int i = 0; i < n; i++) { st.append(A[j][i] <= 0. ? " + " : " - ") .append(String.format("%.4f", Math.abs(A[j][i]))) .append(" * x") .append(N[i]); } st.append("\n"); } return st.toString(); } } public enum Status { UNKNOWN, FEASIBLE, INFEASIBLE, UNBOUNDED } // number of variables private int n; // number of constraints private int m; // a mxn matrix private double[][] A; // an m-vector private double[] b; // an n-vector private double[] c; // an n-vector private double[] x; private double z; // feasibility of the LP private Status status = Status.UNKNOWN; // trace the resolution private final boolean trace; /** * Create a LinearProgram instance that take a linear program in standard form as input. * * @param matA is an mxn matrix * @param vecB is an m-vector * @param vecC is n-vector * @param trace set to true to trace the resolution */ public LinearProgram(double[][] matA, double[] vecB, double[] vecC, boolean trace) { super(); this.m = vecB.length; this.n = vecC.length; this.A = matA.clone(); this.b = vecB.clone(); this.c = vecC.clone(); this.x = new double[n]; this.trace = trace; } /** * Create a LinearProgram instance that take a linear program in standard form as input. * * @param matA is an mxn matrix * @param vecB is an m-vector * @param vecC is n-vector */ public LinearProgram(double[][] matA, double[] vecB, double[] vecC) { this(matA, vecB, vecC, false); } /** * Initialize a LinearProgram instance. * * @param trace set to true to trace the resolution */ public LinearProgram(boolean trace) { this(new double[0][0], new double[0], new double[0], trace); } /** * Initialize a LinearProgram instance. */ public LinearProgram() { this(false); } /** * Declare a new variable. * A variable is supposed to be nonnegative (≥ 0). * * @return the index of the variable */ public int makeVariable() { if (m > 0) { throw new UnsupportedOperationException("Some constraints are alreadey declared"); } return n++; } /** * Declare n new variables */ public void makeVariables(int n) { if (m > 0) { throw new UnsupportedOperationException("Some constraints are alreadey declared"); } this.n += n; } private void checkLength(double[] a) { if (a.length != n) { throw new UnsupportedOperationException("" + "The number of coefficients in the objective function differs from " + "the number of variables declared."); } } /** * Set the objective function to optimize * * @param maximize set to true for maximization, false otherwise * @param ci coefficients of the objective function */ public void setObjective(boolean maximize, double[] ci) { checkLength(ci); this.c = Arrays.copyOf(ci, n); if (!maximize) { for (int i = 0; i < n; i++) { c[i] *= -1; } } } /** * Add a linear inequality (≤) to the system. * * @param ci coefficients of the linear inequality, respecting variable indices * @param b the right-hand side value */ public void addLeq(double[] ci, double b) { checkLength(ci); // increase capacity of A double[][] At = this.A; A = new double[this.m + 1][this.n]; System.arraycopy(At, 0, this.A, 0, m); // then add the new constraint System.arraycopy(ci, 0, this.A[m], 0, n); // increase capacity of b double[] bt = this.b; this.b = new double[this.m + 1]; System.arraycopy(bt, 0, this.b, 0, m); // and add the new rhs this.b[m] = b; m++; } /** * Add a linear inequality (≤) to the system. * * @param map coefficients of the linear inequality, as a map * @param b the right-hand side value */ public void addLeq(HashMap map, double b) { double[] ci = new double[n]; map.forEach((v, c) -> ci[v] = c); addLeq(ci, b); } /** * Add a linear inequality (≥) to the system. * * @param ci coefficients of the linear inequality, respecting variable indices * @param b the right-hand side value * @implNote the (≥)-inequality is turned into a (≤)-inequality constraint */ public void addGeq(double[] ci, double b) { addLeq(ci, b); for (int i = 0; i < n; i++) { this.A[m - 1][i] *= -1; } this.b[m - 1] *= -1; } /** * Add a linear inequality (≥) to the system. * * @param map coefficients of the linear inequality, as a map * @param b the right-hand side value */ public void addGeq(HashMap map, double b) { double[] ci = new double[n]; map.forEach((v, c) -> ci[v] = c); addGeq(ci, b); } /** * Add a linear equality (=) to the system. * * @param ci coefficients of the linear equality, respecting variable indices * @param b the right-hand side value * @implNote the equality constraint is turned into two inequality constraints. */ public void addEq(double[] ci, double b) { checkLength(ci); addLeq(ci, b); addGeq(ci, b); } /** * Add a linear equality (=) to the system. * * @param map coefficients of the linear equality, as a map * @param b the right-hand side value */ public void addEq(HashMap map, double b) { double[] ci = new double[n]; map.forEach((v, c) -> ci[v] = c); addEq(ci, b); } /** * Apply the Simplex algorithm on this linear program. *

If the problem is infeasible, this method terminates. * Otherwise, the optimal solution of this linear program is computed and values of the variables * can be read calling {@link #value(int)}. *

* * @return true is this LP is feasible, false otherwise. */ public Status simplex() { // slack form of the LP Optional opt = initialize(); if (opt.isPresent()) { Slack slack = opt.get(); if (Status.FEASIBLE.equals(status = solve(slack))) { if (this.x.length != n) { this.x = new double[n]; } slack.setValues(x); this.z = slack.v; } } return status; } /** * Main loop of the simplex. * * @param lipr a linear program, under slack form * @return the status of the resolution */ private Status solve(Slack lipr) { if (trace) System.out.printf("%s", lipr); int e = enteringVariable(lipr); while (e < Integer.MAX_VALUE) { int l = leavingVariable(lipr, e); if (l < 0) { return Status.UNBOUNDED; } lipr.pivot(l, e, trace); e = enteringVariable(lipr); if (trace) System.out.printf("%s", lipr); } return Status.FEASIBLE; } /** * Determines that the linear program is infeasible * or returns a slack form for which the basic solution is feasible * * @return optional slack form of the linear program defined is this */ private Optional initialize() { this.status = Status.UNKNOWN; Arrays.fill(this.x, 0.); this.z = 0.; int k = argmin(b); if (b[k] >= 0) { if (trace) System.out.println("Initial basic solution feasible"); return Optional.of(new Slack(A, b, c)); } if (trace) System.out.println("Formulate an auxiliary linear program, adding x0"); int x0 = 0; Slack laux = auxiliaryLinearProgram(A, b, c); if (trace) System.out.print(laux); laux.pivot(k, x0, trace); // The basic solution is now feasible for laux this.status = Status.INFEASIBLE; if (Status.FEASIBLE.equals(solve(laux))) { double[] x = new double[laux.n]; laux.setValues(x); if (x[x0] == 0.) { // if the optimal solution to laux sets x0 to 0 // Since this solution has x0 = 0, we know that our initial problem was feasible this.status = Status.FEASIBLE; for (int i = 0; i < laux.m; i++) { if (laux.B[i] == x0) { // if x0 is basic // perform one (degenerate) pivot to make it nonbasic // using any e in N such that a0e != 0 int e = -1; for (int j = 0; j < laux.n; j++) { if (laux.A[i][j] != 0) { e = j; break; } } if (e == -1) throw new UnsupportedOperationException(); if (trace) System.out.println("Perform one (degenerate) pivot to make it nonbasic"); laux.pivot(i, e, trace); break; } } // from the final slack form of laux if (isFeasible()) { if (trace) System.out.println("Modified final slack form"); return Optional.of(finalSlack(laux, this.c)); } } } return Optional.empty(); } /** * Determine the index of the constraint with the smallest b. * * @param b right-hand side coefficients * @return the index of the smallest b */ private int argmin(double[] b) { int k = 0; double bk = b[k]; for (int i = 1; i < m; i++) { if (bk > b[i]) { bk = b[i]; k = i; } } return k; } /** * Form the auxiliary linear program by adding -x0 to the LHS of each constraint * and setting the objective function to -x0. * * @return the resulting slack form of L_aux */ private static Slack auxiliaryLinearProgram(double[][] A, double[] b, double[] c) { int n = c.length; int m = b.length; double[][] A2 = new double[m][n + 1]; for (int i = 0; i < m; i++) { System.arraycopy(A[i], 0, A2[i], 1, n); A2[i][0] = -1; } double[] c2 = new double[n + 1]; c2[0] = -1; return new Slack(A2, b, c2); } /** * remove x0 from the constraints and restore the original objective function of L, * but replace each basic variable in this objective function by the right-hand side of its associated constraint. * * @param laux the auxiliary linear program to transform * @return final slack form of laux */ private static Slack finalSlack(Slack laux, double[] oriC) { int n = laux.n - 1; int m = laux.m; // since x0 = 0, we can just remove it from the set of constraints double[][] A2 = new double[m][n]; for (int i = 0; i < m; i++) { for (int j = 0, k = 0; j <= n; j++) { if (laux.N[j] > 0) { A2[i][k++] = laux.A[i][j]; } } } double[] b2 = new double[m]; System.arraycopy(laux.b, 0, b2, 0, m); // We then restore the original objective function, double[] ct = new double[n + m + 1]; System.arraycopy(oriC, 0, ct, 1, n); // with appropriate substitutions made to include only nonbasic variables double z = 0.; for (int j = 0; j < laux.m; j++) { // for every nonbasic variables int nx = laux.B[j]; if (nx <= n) { double cx = ct[nx]; ct[nx] = 0.; // do the substitutions z += cx * laux.b[j]; for (int i = 0; i < laux.n; i++) { ct[laux.N[i]] -= cx * laux.A[j][i]; } } } double[] c2 = new double[n]; int[] N2 = new int[n]; for (int i = 0, j = 0; i < laux.n; i++) { if (laux.N[i] > 0) { c2[j] = ct[laux.N[i]]; N2[j++] = laux.N[i] - 1; } } int[] B2 = new int[m]; for (int i = 0; i < laux.m; i++) { B2[i] = laux.B[i] - 1; } return new Slack(A2, b2, c2, N2, B2, z); } /** * @return true if the application of the Simplex algorithm computed the (optimal) solution. */ public boolean isFeasible() { return status == Status.FEASIBLE; } /** * Return the value of the ith variable in the linear program. *

* If this is infeasible, return {@code -1.}, otherwise the value is returned. *

* * @param i index of the variable. * @return the value assigned the ith variable in this linear program. */ public double value(int i) { if (isFeasible()) { return x[i]; } else return -1.; } /** * Return the value of the objective function defined in this linear program. *

* If this is not feasible, returns {@link Double#NEGATIVE_INFINITY}, * otherwise, the optimal value is returnd. *

* * @return the value of the objective function. */ public double objective() { if (isFeasible()) { return z; } else return Double.NEGATIVE_INFINITY; } /** * Choose the entering variable with c > 0 * and break ties by always choosing the variable * with the smallest index (Bland's rule). * * @return a value between [0, n) if an entering variable is found, {@link Integer#MAX_VALUE} otherwise. */ private static int enteringVariable(Slack s) { int j = Integer.MAX_VALUE; for (int i = 0; i < s.n; i++) { if (s.c[i] > 0. && s.N[i] < j) { j = i; } } return j; } /** * Choose the leaving variable, the one with the smallest ratio b(i) / A(i,e). * Ties are broken with index value (Bland's rule) * * @param s the slack * @param e index of the entering variable * @return index of the leaving variable */ private static int leavingVariable(Slack s, int e) { double min_v = Double.POSITIVE_INFINITY; int l = -1; for (int i = 0; i < s.m; i++) { if (s.A[i][e] > 0) { double d = s.b[i] / s.A[i][e]; if (min_v > d) { l = i; min_v = d; } } } return l; } @Override public String toString() { return toLP(A, b, c, 0.); } private static String toLP(double[][] A, double[] b, double[] c, double v) { StringBuilder st = new StringBuilder(); st.append("Maximize").append('\n'); st.append("\\ v = ").append(v).append("\n"); st.append(" obj:"); for (int i = 0; i < c.length; i++) { st.append(c[i] >= 0 ? " +" : " ") .append(c[i]) .append(" x") .append(i + 1); } st.append("\nSubject to\n"); for (int j = 0; j < b.length; j++) { st.append(" c").append(j + 1).append(": "); st.append(A[j][0]).append(" x1"); for (int i = 1; i < c.length; i++) { st.append(A[j][i] >= 0 ? " +" : " ") .append(A[j][i]) .append(" x") .append(i + 1); } st.append(" <= ").append(b[j]).append('\n'); } st.append("End"); return st.toString(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy