diff options
Diffstat (limited to 'src/main/java/org/apache/commons/math3/optim/linear/SimplexTableau.java')
-rw-r--r-- | src/main/java/org/apache/commons/math3/optim/linear/SimplexTableau.java | 713 |
1 files changed, 713 insertions, 0 deletions
diff --git a/src/main/java/org/apache/commons/math3/optim/linear/SimplexTableau.java b/src/main/java/org/apache/commons/math3/optim/linear/SimplexTableau.java new file mode 100644 index 0000000..31e71d2 --- /dev/null +++ b/src/main/java/org/apache/commons/math3/optim/linear/SimplexTableau.java @@ -0,0 +1,713 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math3.optim.linear; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.commons.math3.linear.Array2DRowRealMatrix; +import org.apache.commons.math3.linear.MatrixUtils; +import org.apache.commons.math3.linear.RealVector; +import org.apache.commons.math3.optim.nonlinear.scalar.GoalType; +import org.apache.commons.math3.optim.PointValuePair; +import org.apache.commons.math3.util.Precision; + +/** + * A tableau for use in the Simplex method. + * + * <p> + * Example: + * <pre> + * W | Z | x1 | x2 | x- | s1 | s2 | a1 | RHS + * --------------------------------------------------- + * -1 0 0 0 0 0 0 1 0 <= phase 1 objective + * 0 1 -15 -10 0 0 0 0 0 <= phase 2 objective + * 0 0 1 0 0 1 0 0 2 <= constraint 1 + * 0 0 0 1 0 0 1 0 3 <= constraint 2 + * 0 0 1 1 0 0 0 1 4 <= constraint 3 + * </pre> + * W: Phase 1 objective function</br> + * Z: Phase 2 objective function</br> + * x1 & x2: Decision variables</br> + * x-: Extra decision variable to allow for negative values</br> + * s1 & s2: Slack/Surplus variables</br> + * a1: Artificial variable</br> + * RHS: Right hand side</br> + * </p> + * @since 2.0 + */ +class SimplexTableau implements Serializable { + + /** Column label for negative vars. */ + private static final String NEGATIVE_VAR_COLUMN_LABEL = "x-"; + + /** Serializable version identifier. */ + private static final long serialVersionUID = -1369660067587938365L; + + /** Linear objective function. */ + private final LinearObjectiveFunction f; + + /** Linear constraints. */ + private final List<LinearConstraint> constraints; + + /** Whether to restrict the variables to non-negative values. */ + private final boolean restrictToNonNegative; + + /** The variables each column represents */ + private final List<String> columnLabels = new ArrayList<String>(); + + /** Simple tableau. */ + private transient Array2DRowRealMatrix tableau; + + /** Number of decision variables. */ + private final int numDecisionVariables; + + /** Number of slack variables. */ + private final int numSlackVariables; + + /** Number of artificial variables. */ + private int numArtificialVariables; + + /** Amount of error to accept when checking for optimality. */ + private final double epsilon; + + /** Amount of error to accept in floating point comparisons. */ + private final int maxUlps; + + /** Maps basic variables to row they are basic in. */ + private int[] basicVariables; + + /** Maps rows to their corresponding basic variables. */ + private int[] basicRows; + + /** + * Builds a tableau for a linear problem. + * + * @param f Linear objective function. + * @param constraints Linear constraints. + * @param goalType Optimization goal: either {@link GoalType#MAXIMIZE} + * or {@link GoalType#MINIMIZE}. + * @param restrictToNonNegative Whether to restrict the variables to non-negative values. + * @param epsilon Amount of error to accept when checking for optimality. + */ + SimplexTableau(final LinearObjectiveFunction f, + final Collection<LinearConstraint> constraints, + final GoalType goalType, + final boolean restrictToNonNegative, + final double epsilon) { + this(f, constraints, goalType, restrictToNonNegative, epsilon, SimplexSolver.DEFAULT_ULPS); + } + + /** + * Build a tableau for a linear problem. + * @param f linear objective function + * @param constraints linear constraints + * @param goalType type of optimization goal: either {@link GoalType#MAXIMIZE} or {@link GoalType#MINIMIZE} + * @param restrictToNonNegative whether to restrict the variables to non-negative values + * @param epsilon amount of error to accept when checking for optimality + * @param maxUlps amount of error to accept in floating point comparisons + */ + SimplexTableau(final LinearObjectiveFunction f, + final Collection<LinearConstraint> constraints, + final GoalType goalType, + final boolean restrictToNonNegative, + final double epsilon, + final int maxUlps) { + this.f = f; + this.constraints = normalizeConstraints(constraints); + this.restrictToNonNegative = restrictToNonNegative; + this.epsilon = epsilon; + this.maxUlps = maxUlps; + this.numDecisionVariables = f.getCoefficients().getDimension() + (restrictToNonNegative ? 0 : 1); + this.numSlackVariables = getConstraintTypeCounts(Relationship.LEQ) + + getConstraintTypeCounts(Relationship.GEQ); + this.numArtificialVariables = getConstraintTypeCounts(Relationship.EQ) + + getConstraintTypeCounts(Relationship.GEQ); + this.tableau = createTableau(goalType == GoalType.MAXIMIZE); + // initialize the basic variables for phase 1: + // we know that only slack or artificial variables can be basic + initializeBasicVariables(getSlackVariableOffset()); + initializeColumnLabels(); + } + + /** + * Initialize the labels for the columns. + */ + protected void initializeColumnLabels() { + if (getNumObjectiveFunctions() == 2) { + columnLabels.add("W"); + } + columnLabels.add("Z"); + for (int i = 0; i < getOriginalNumDecisionVariables(); i++) { + columnLabels.add("x" + i); + } + if (!restrictToNonNegative) { + columnLabels.add(NEGATIVE_VAR_COLUMN_LABEL); + } + for (int i = 0; i < getNumSlackVariables(); i++) { + columnLabels.add("s" + i); + } + for (int i = 0; i < getNumArtificialVariables(); i++) { + columnLabels.add("a" + i); + } + columnLabels.add("RHS"); + } + + /** + * Create the tableau by itself. + * @param maximize if true, goal is to maximize the objective function + * @return created tableau + */ + protected Array2DRowRealMatrix createTableau(final boolean maximize) { + + // create a matrix of the correct size + int width = numDecisionVariables + numSlackVariables + + numArtificialVariables + getNumObjectiveFunctions() + 1; // + 1 is for RHS + int height = constraints.size() + getNumObjectiveFunctions(); + Array2DRowRealMatrix matrix = new Array2DRowRealMatrix(height, width); + + // initialize the objective function rows + if (getNumObjectiveFunctions() == 2) { + matrix.setEntry(0, 0, -1); + } + + int zIndex = (getNumObjectiveFunctions() == 1) ? 0 : 1; + matrix.setEntry(zIndex, zIndex, maximize ? 1 : -1); + RealVector objectiveCoefficients = maximize ? f.getCoefficients().mapMultiply(-1) : f.getCoefficients(); + copyArray(objectiveCoefficients.toArray(), matrix.getDataRef()[zIndex]); + matrix.setEntry(zIndex, width - 1, maximize ? f.getConstantTerm() : -1 * f.getConstantTerm()); + + if (!restrictToNonNegative) { + matrix.setEntry(zIndex, getSlackVariableOffset() - 1, + getInvertedCoefficientSum(objectiveCoefficients)); + } + + // initialize the constraint rows + int slackVar = 0; + int artificialVar = 0; + for (int i = 0; i < constraints.size(); i++) { + LinearConstraint constraint = constraints.get(i); + int row = getNumObjectiveFunctions() + i; + + // decision variable coefficients + copyArray(constraint.getCoefficients().toArray(), matrix.getDataRef()[row]); + + // x- + if (!restrictToNonNegative) { + matrix.setEntry(row, getSlackVariableOffset() - 1, + getInvertedCoefficientSum(constraint.getCoefficients())); + } + + // RHS + matrix.setEntry(row, width - 1, constraint.getValue()); + + // slack variables + if (constraint.getRelationship() == Relationship.LEQ) { + matrix.setEntry(row, getSlackVariableOffset() + slackVar++, 1); // slack + } else if (constraint.getRelationship() == Relationship.GEQ) { + matrix.setEntry(row, getSlackVariableOffset() + slackVar++, -1); // excess + } + + // artificial variables + if ((constraint.getRelationship() == Relationship.EQ) || + (constraint.getRelationship() == Relationship.GEQ)) { + matrix.setEntry(0, getArtificialVariableOffset() + artificialVar, 1); + matrix.setEntry(row, getArtificialVariableOffset() + artificialVar++, 1); + matrix.setRowVector(0, matrix.getRowVector(0).subtract(matrix.getRowVector(row))); + } + } + + return matrix; + } + + /** + * Get new versions of the constraints which have positive right hand sides. + * @param originalConstraints original (not normalized) constraints + * @return new versions of the constraints + */ + public List<LinearConstraint> normalizeConstraints(Collection<LinearConstraint> originalConstraints) { + List<LinearConstraint> normalized = new ArrayList<LinearConstraint>(originalConstraints.size()); + for (LinearConstraint constraint : originalConstraints) { + normalized.add(normalize(constraint)); + } + return normalized; + } + + /** + * Get a new equation equivalent to this one with a positive right hand side. + * @param constraint reference constraint + * @return new equation + */ + private LinearConstraint normalize(final LinearConstraint constraint) { + if (constraint.getValue() < 0) { + return new LinearConstraint(constraint.getCoefficients().mapMultiply(-1), + constraint.getRelationship().oppositeRelationship(), + -1 * constraint.getValue()); + } + return new LinearConstraint(constraint.getCoefficients(), + constraint.getRelationship(), constraint.getValue()); + } + + /** + * Get the number of objective functions in this tableau. + * @return 2 for Phase 1. 1 for Phase 2. + */ + protected final int getNumObjectiveFunctions() { + return this.numArtificialVariables > 0 ? 2 : 1; + } + + /** + * Get a count of constraints corresponding to a specified relationship. + * @param relationship relationship to count + * @return number of constraint with the specified relationship + */ + private int getConstraintTypeCounts(final Relationship relationship) { + int count = 0; + for (final LinearConstraint constraint : constraints) { + if (constraint.getRelationship() == relationship) { + ++count; + } + } + return count; + } + + /** + * Get the -1 times the sum of all coefficients in the given array. + * @param coefficients coefficients to sum + * @return the -1 times the sum of all coefficients in the given array. + */ + protected static double getInvertedCoefficientSum(final RealVector coefficients) { + double sum = 0; + for (double coefficient : coefficients.toArray()) { + sum -= coefficient; + } + return sum; + } + + /** + * Checks whether the given column is basic. + * @param col index of the column to check + * @return the row that the variable is basic in. null if the column is not basic + */ + protected Integer getBasicRow(final int col) { + final int row = basicVariables[col]; + return row == -1 ? null : row; + } + + /** + * Returns the variable that is basic in this row. + * @param row the index of the row to check + * @return the variable that is basic for this row. + */ + protected int getBasicVariable(final int row) { + return basicRows[row]; + } + + /** + * Initializes the basic variable / row mapping. + * @param startColumn the column to start + */ + private void initializeBasicVariables(final int startColumn) { + basicVariables = new int[getWidth() - 1]; + basicRows = new int[getHeight()]; + + Arrays.fill(basicVariables, -1); + + for (int i = startColumn; i < getWidth() - 1; i++) { + Integer row = findBasicRow(i); + if (row != null) { + basicVariables[i] = row; + basicRows[row] = i; + } + } + } + + /** + * Returns the row in which the given column is basic. + * @param col index of the column + * @return the row that the variable is basic in, or {@code null} if the variable is not basic. + */ + private Integer findBasicRow(final int col) { + Integer row = null; + for (int i = 0; i < getHeight(); i++) { + final double entry = getEntry(i, col); + if (Precision.equals(entry, 1d, maxUlps) && (row == null)) { + row = i; + } else if (!Precision.equals(entry, 0d, maxUlps)) { + return null; + } + } + return row; + } + + /** + * Removes the phase 1 objective function, positive cost non-artificial variables, + * and the non-basic artificial variables from this tableau. + */ + protected void dropPhase1Objective() { + if (getNumObjectiveFunctions() == 1) { + return; + } + + final Set<Integer> columnsToDrop = new TreeSet<Integer>(); + columnsToDrop.add(0); + + // positive cost non-artificial variables + for (int i = getNumObjectiveFunctions(); i < getArtificialVariableOffset(); i++) { + final double entry = getEntry(0, i); + if (Precision.compareTo(entry, 0d, epsilon) > 0) { + columnsToDrop.add(i); + } + } + + // non-basic artificial variables + for (int i = 0; i < getNumArtificialVariables(); i++) { + int col = i + getArtificialVariableOffset(); + if (getBasicRow(col) == null) { + columnsToDrop.add(col); + } + } + + final double[][] matrix = new double[getHeight() - 1][getWidth() - columnsToDrop.size()]; + for (int i = 1; i < getHeight(); i++) { + int col = 0; + for (int j = 0; j < getWidth(); j++) { + if (!columnsToDrop.contains(j)) { + matrix[i - 1][col++] = getEntry(i, j); + } + } + } + + // remove the columns in reverse order so the indices are correct + Integer[] drop = columnsToDrop.toArray(new Integer[columnsToDrop.size()]); + for (int i = drop.length - 1; i >= 0; i--) { + columnLabels.remove((int) drop[i]); + } + + this.tableau = new Array2DRowRealMatrix(matrix); + this.numArtificialVariables = 0; + // need to update the basic variable mappings as row/columns have been dropped + initializeBasicVariables(getNumObjectiveFunctions()); + } + + /** + * @param src the source array + * @param dest the destination array + */ + private void copyArray(final double[] src, final double[] dest) { + System.arraycopy(src, 0, dest, getNumObjectiveFunctions(), src.length); + } + + /** + * Returns whether the problem is at an optimal state. + * @return whether the model has been solved + */ + boolean isOptimal() { + final double[] objectiveFunctionRow = getRow(0); + final int end = getRhsOffset(); + for (int i = getNumObjectiveFunctions(); i < end; i++) { + final double entry = objectiveFunctionRow[i]; + if (Precision.compareTo(entry, 0d, epsilon) < 0) { + return false; + } + } + return true; + } + + /** + * Get the current solution. + * @return current solution + */ + protected PointValuePair getSolution() { + int negativeVarColumn = columnLabels.indexOf(NEGATIVE_VAR_COLUMN_LABEL); + Integer negativeVarBasicRow = negativeVarColumn > 0 ? getBasicRow(negativeVarColumn) : null; + double mostNegative = negativeVarBasicRow == null ? 0 : getEntry(negativeVarBasicRow, getRhsOffset()); + + final Set<Integer> usedBasicRows = new HashSet<Integer>(); + final double[] coefficients = new double[getOriginalNumDecisionVariables()]; + for (int i = 0; i < coefficients.length; i++) { + int colIndex = columnLabels.indexOf("x" + i); + if (colIndex < 0) { + coefficients[i] = 0; + continue; + } + Integer basicRow = getBasicRow(colIndex); + if (basicRow != null && basicRow == 0) { + // if the basic row is found to be the objective function row + // set the coefficient to 0 -> this case handles unconstrained + // variables that are still part of the objective function + coefficients[i] = 0; + } else if (usedBasicRows.contains(basicRow)) { + // if multiple variables can take a given value + // then we choose the first and set the rest equal to 0 + coefficients[i] = 0 - (restrictToNonNegative ? 0 : mostNegative); + } else { + usedBasicRows.add(basicRow); + coefficients[i] = + (basicRow == null ? 0 : getEntry(basicRow, getRhsOffset())) - + (restrictToNonNegative ? 0 : mostNegative); + } + } + return new PointValuePair(coefficients, f.value(coefficients)); + } + + /** + * Perform the row operations of the simplex algorithm with the selected + * pivot column and row. + * @param pivotCol the pivot column + * @param pivotRow the pivot row + */ + protected void performRowOperations(int pivotCol, int pivotRow) { + // set the pivot element to 1 + final double pivotVal = getEntry(pivotRow, pivotCol); + divideRow(pivotRow, pivotVal); + + // set the rest of the pivot column to 0 + for (int i = 0; i < getHeight(); i++) { + if (i != pivotRow) { + final double multiplier = getEntry(i, pivotCol); + if (multiplier != 0.0) { + subtractRow(i, pivotRow, multiplier); + } + } + } + + // update the basic variable mappings + final int previousBasicVariable = getBasicVariable(pivotRow); + basicVariables[previousBasicVariable] = -1; + basicVariables[pivotCol] = pivotRow; + basicRows[pivotRow] = pivotCol; + } + + /** + * Divides one row by a given divisor. + * <p> + * After application of this operation, the following will hold: + * <pre>dividendRow = dividendRow / divisor</pre> + * + * @param dividendRowIndex index of the row + * @param divisor value of the divisor + */ + protected void divideRow(final int dividendRowIndex, final double divisor) { + final double[] dividendRow = getRow(dividendRowIndex); + for (int j = 0; j < getWidth(); j++) { + dividendRow[j] /= divisor; + } + } + + /** + * Subtracts a multiple of one row from another. + * <p> + * After application of this operation, the following will hold: + * <pre>minuendRow = minuendRow - multiple * subtrahendRow</pre> + * + * @param minuendRowIndex row index + * @param subtrahendRowIndex row index + * @param multiplier multiplication factor + */ + protected void subtractRow(final int minuendRowIndex, final int subtrahendRowIndex, final double multiplier) { + final double[] minuendRow = getRow(minuendRowIndex); + final double[] subtrahendRow = getRow(subtrahendRowIndex); + for (int i = 0; i < getWidth(); i++) { + minuendRow[i] -= subtrahendRow[i] * multiplier; + } + } + + /** + * Get the width of the tableau. + * @return width of the tableau + */ + protected final int getWidth() { + return tableau.getColumnDimension(); + } + + /** + * Get the height of the tableau. + * @return height of the tableau + */ + protected final int getHeight() { + return tableau.getRowDimension(); + } + + /** + * Get an entry of the tableau. + * @param row row index + * @param column column index + * @return entry at (row, column) + */ + protected final double getEntry(final int row, final int column) { + return tableau.getEntry(row, column); + } + + /** + * Set an entry of the tableau. + * @param row row index + * @param column column index + * @param value for the entry + */ + protected final void setEntry(final int row, final int column, final double value) { + tableau.setEntry(row, column, value); + } + + /** + * Get the offset of the first slack variable. + * @return offset of the first slack variable + */ + protected final int getSlackVariableOffset() { + return getNumObjectiveFunctions() + numDecisionVariables; + } + + /** + * Get the offset of the first artificial variable. + * @return offset of the first artificial variable + */ + protected final int getArtificialVariableOffset() { + return getNumObjectiveFunctions() + numDecisionVariables + numSlackVariables; + } + + /** + * Get the offset of the right hand side. + * @return offset of the right hand side + */ + protected final int getRhsOffset() { + return getWidth() - 1; + } + + /** + * Get the number of decision variables. + * <p> + * If variables are not restricted to positive values, this will include 1 extra decision variable to represent + * the absolute value of the most negative variable. + * + * @return number of decision variables + * @see #getOriginalNumDecisionVariables() + */ + protected final int getNumDecisionVariables() { + return numDecisionVariables; + } + + /** + * Get the original number of decision variables. + * @return original number of decision variables + * @see #getNumDecisionVariables() + */ + protected final int getOriginalNumDecisionVariables() { + return f.getCoefficients().getDimension(); + } + + /** + * Get the number of slack variables. + * @return number of slack variables + */ + protected final int getNumSlackVariables() { + return numSlackVariables; + } + + /** + * Get the number of artificial variables. + * @return number of artificial variables + */ + protected final int getNumArtificialVariables() { + return numArtificialVariables; + } + + /** + * Get the row from the tableau. + * @param row the row index + * @return the reference to the underlying row data + */ + protected final double[] getRow(int row) { + return tableau.getDataRef()[row]; + } + + /** + * Get the tableau data. + * @return tableau data + */ + protected final double[][] getData() { + return tableau.getData(); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object other) { + + if (this == other) { + return true; + } + + if (other instanceof SimplexTableau) { + SimplexTableau rhs = (SimplexTableau) other; + return (restrictToNonNegative == rhs.restrictToNonNegative) && + (numDecisionVariables == rhs.numDecisionVariables) && + (numSlackVariables == rhs.numSlackVariables) && + (numArtificialVariables == rhs.numArtificialVariables) && + (epsilon == rhs.epsilon) && + (maxUlps == rhs.maxUlps) && + f.equals(rhs.f) && + constraints.equals(rhs.constraints) && + tableau.equals(rhs.tableau); + } + return false; + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return Boolean.valueOf(restrictToNonNegative).hashCode() ^ + numDecisionVariables ^ + numSlackVariables ^ + numArtificialVariables ^ + Double.valueOf(epsilon).hashCode() ^ + maxUlps ^ + f.hashCode() ^ + constraints.hashCode() ^ + tableau.hashCode(); + } + + /** + * Serialize the instance. + * @param oos stream where object should be written + * @throws IOException if object cannot be written to stream + */ + private void writeObject(ObjectOutputStream oos) + throws IOException { + oos.defaultWriteObject(); + MatrixUtils.serializeRealMatrix(tableau, oos); + } + + /** + * Deserialize the instance. + * @param ois stream from which the object should be read + * @throws ClassNotFoundException if a class in the stream cannot be found + * @throws IOException if object cannot be read from the stream + */ + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + MatrixUtils.deserializeRealMatrix(this, "tableau", ois); + } +} |