package no.birkett.kiwi; import java.util.*; /** * Created by alex on 30/01/15. */ public class Solver { private static class Tag { Symbol marker; Symbol other; } private static class EditInfo { Tag tag; Constraint constraint; double constant; } private static class RowAndTag { Tag tag; Row row; } private Map cns = new HashMap(); private Map rows = new HashMap(); private Map vars = new HashMap(); private Map edits = new HashMap(); private List infeasibleRows = new ArrayList(); private Row objective = new Row(); private Row artificial; private long idTick = 1; /** * Add a constraint to the solver. * * @param constraint * @throws DuplicateConstraintException The given constraint has already been added to the solver. * @throws UnsatisfiableConstraintException The given constraint is required and cannot be satisfied. */ public void addConstraint(Constraint constraint) throws DuplicateConstraintException, UnsatisfiableConstraintException { if (cns.containsKey(constraint)) { throw new DuplicateConstraintException(constraint); } // Creating a row causes symbols to reserved for the variables // in the constraint. If this method exits with an exception, // then its possible those variables will linger in the var map. // Since its likely that those variables will be used in other // constraints and since exceptional conditions are uncommon, // i'm not too worried about aggressive cleanup of the var map. RowAndTag rowAndTag = createRow(constraint); Symbol subject = chooseSubject(rowAndTag.row, rowAndTag.tag); // If chooseSubject could find a valid entering symbol, one // last option is available if the entire row is composed of // dummy variables. If the constant of the row is zero, then // this represents redundant constraints and the new dummy // marker can enter the basis. If the constant is non-zero, // then it represents an unsatisfiable constraint. if (subject.getType() == Symbol.Type.INVALID && allDummies(rowAndTag.row)) { if (!Util.nearZero(rowAndTag.row.getConstant())) { throw new UnsatisfiableConstraintException(constraint); } else { subject = rowAndTag.tag.marker; } } // If an entering symbol still isn't found, then the row must // be added using an artificial variable. If that fails, then // the row represents an unsatisfiable constraint. if (subject.getType() == Symbol.Type.INVALID) { if (!addWithArtificialVariable(rowAndTag.row)) { throw new UnsatisfiableConstraintException(constraint); } } else { rowAndTag.row.solveFor(subject); substitute(subject, rowAndTag.row); this.rows.put(subject, rowAndTag.row); } this.cns.put(constraint, rowAndTag.tag); // Optimizing after each constraint is added performs less // aggregate work due to a smaller average system size. It // also ensures the solver remains in a consistent state. optimize(this.objective); } /** * Update the values of the external solver variables. */ void updateVariables() { for (Map.Entry varEntry : vars.entrySet()) { Variable variable = varEntry.getKey(); Row row = this.rows.get(varEntry.getValue()); if (row == null) { variable.setValue(0); } else { variable.setValue(row.getConstant()); } } } /** * Create a new Row object for the given constraint. *

* The terms in the constraint will be converted to cells in the row. * Any term in the constraint with a coefficient of zero is ignored. * This method uses the `getVarSymbol` method to get the symbol for * the variables added to the row. If the symbol for a given cell * variable is basic, the cell variable will be substituted with the * basic row. *

* The necessary slack and error variables will be added to the row. * If the constant for the row is negative, the sign for the row * will be inverted so the constant becomes positive. *

* The tag will be updated with the marker and error symbols to use * for tracking the movement of the constraint in the tableau. */ RowAndTag createRow(Constraint constraint) { Expression expr = constraint.getExpression(); Row row = new Row(expr.getConstant()); Tag tag = new Tag(); // Substitute the current basic variables into the row. for (Term term : expr.getTerms()) { if (!Util.nearZero(term.getCoefficient())) { Symbol symbol = getVarSymbol(term.getVariable()); Row otherRow = rows.get(symbol); if (otherRow == null) { row.insert(symbol, term.getCoefficient()); } else { row.insert(otherRow, term.getCoefficient()); } } } // Add the necessary slack, error, and dummy variables. switch (constraint.getOp()) { case OP_LE: case OP_GE: { double coeff = constraint.getOp() == RelationalOperator.OP_LE ? 1.0 : -1.0; Symbol slack = new Symbol(Symbol.Type.SLACK, idTick++); tag.marker = slack; row.insert(slack, coeff); if (constraint.getStrength() < Strength.REQUIRED) { Symbol error = new Symbol(Symbol.Type.ERROR, idTick++); tag.other = error; row.insert(error, -coeff); this.objective.insert(error, constraint.getStrength()); } break; } case OP_EQ: { if (constraint.getStrength() < Strength.REQUIRED) { Symbol errplus = new Symbol(Symbol.Type.ERROR, idTick++); Symbol errminus = new Symbol(Symbol.Type.ERROR, idTick++); tag.marker = errplus; tag.other = errminus; row.insert(errplus, -1.0); // v = eplus - eminus row.insert(errminus, 1.0); // v - eplus + eminus = 0 this.objective.insert(errplus, constraint.getStrength()); this.objective.insert(errminus, constraint.getStrength()); } else { Symbol dummy = new Symbol(Symbol.Type.DUMMY, idTick++); tag.marker = dummy; row.insert(dummy); } break; } } // Ensure the row as a positive constant. if (row.getConstant() < 0.0) { row.reverseSign(); } RowAndTag rowAndTag = new RowAndTag(); rowAndTag.row = row; rowAndTag.tag = tag; return rowAndTag; } /** * Choose the subject for solving for the row *

* This method will choose the best subject for using as the solve * target for the row. An invalid symbol will be returned if there * is no valid target. * The symbols are chosen according to the following precedence: * 1) The first symbol representing an external variable. * 2) A negative slack or error tag variable. * If a subject cannot be found, an invalid symbol will be returned. */ private static Symbol chooseSubject(Row row, Tag tag) { for (Map.Entry cell : row.getCells().entrySet()) { if (cell.getKey().getType() == Symbol.Type.EXTERNAL) { return cell.getKey(); } } if (tag.marker.getType() == Symbol.Type.SLACK || tag.marker.getType() == Symbol.Type.ERROR) { if (row.coefficientFor(tag.marker) < 0.0) return tag.marker; } if (tag.other.getType() == Symbol.Type.SLACK || tag.other.getType() == Symbol.Type.ERROR) { if (row.coefficientFor(tag.other) < 0.0) return tag.other; } return new Symbol(); } /** * Add the row to the tableau using an artificial variable. *

* This will return false if the constraint cannot be satisfied. */ private boolean addWithArtificialVariable(Row row) { //TODO check this // Create and add the artificial variable to the tableau Symbol art = new Symbol(Symbol.Type.SLACK, idTick++); rows.put(art, new Row(row)); this.artificial = new Row(row); // Optimize the artificial objective. This is successful // only if the artificial objective is optimized to zero. optimize(this.artificial); boolean success = Util.nearZero(artificial.getConstant()); artificial = null; // If the artificial variable is basic, pivot the row so that // it becomes basic. If the row is constant, exit early. Row rowptr = this.rows.get(art); if (rowptr != null) { rows.remove(rowptr); if (rowptr.getCells().isEmpty()) { return success; } Symbol entering = anyPivotableSymbol(rowptr); if (entering.getType() == Symbol.Type.INVALID) { return false; // unsatisfiable (will this ever happen?) } rowptr.solveFor(art, entering); substitute(entering, rowptr); this.rows.put(entering, rowptr); } // Remove the artificial variable from the tableau. for (Map.Entry rowEntry : rows.entrySet()) { rowEntry.getValue().remove(art); } objective.remove(art); return success; } /** * Substitute the parametric symbol with the given row. *

* This method will substitute all instances of the parametric symbol * in the tableau and the objective function with the given row. */ void substitute(Symbol symbol, Row row) { for (Map.Entry rowEntry : rows.entrySet()) { rowEntry.getValue().substitute(symbol, row); if (rowEntry.getKey().getType() != Symbol.Type.EXTERNAL && rowEntry.getValue().getConstant() < 0.0) { infeasibleRows.add(rowEntry.getKey()); } } objective.substitute(symbol, row); if (artificial != null) { artificial.substitute(symbol, row); } } /** * Optimize the system for the given objective function. *

* This method performs iterations of Phase 2 of the simplex method * until the objective function reaches a minimum. * * @throws InternalSolverError The value of the objective function is unbounded. */ void optimize(Row objective) { while (true) { Symbol entering = getEnteringSymbol(objective); if (entering.getType() == Symbol.Type.INVALID) { return; } Map.Entry entry = getLeavingRow(entering); if (entry == null) { throw new InternalSolverError("The objective is unbounded."); } // pivot the entering symbol into the basis Symbol leaving = entry.getKey(); Row row = entry.getValue(); this.rows.remove(entry.getKey()); row.solveFor(leaving, entering); substitute(entering, row); this.rows.put(entering, row); } } /** * Compute the entering variable for a pivot operation. *

* This method will return first symbol in the objective function which * is non-dummy and has a coefficient less than zero. If no symbol meets * the criteria, it means the objective function is at a minimum, and an * invalid symbol is returned. */ private static Symbol getEnteringSymbol(Row objective) { for (Map.Entry cell : objective.getCells().entrySet()) { if (cell.getKey().getType() != Symbol.Type.DUMMY && cell.getValue() < 0.0) { return cell.getKey(); } } return new Symbol(); } /** * Get the first Slack or Error symbol in the row. *

* If no such symbol is present, and Invalid symbol will be returned. */ private Symbol anyPivotableSymbol(Row row) { Symbol symbol = null; for (Map.Entry entry : objective.getCells().entrySet()) { if (entry.getKey().getType() == Symbol.Type.SLACK || entry.getKey().getType() == Symbol.Type.ERROR) { symbol = entry.getKey(); } } if (symbol == null) { symbol = new Symbol(); } return symbol; } /** * Compute the row which holds the exit symbol for a pivot. *

* This documentation is copied from the C++ version and is outdated *

*

* This method will return an iterator to the row in the row map * which holds the exit symbol. If no appropriate exit symbol is * found, the end() iterator will be returned. This indicates that * the objective function is unbounded. */ private Map.Entry getLeavingRow(Symbol entering) { // TODO check double ratio = Double.MAX_VALUE; Map.Entry found = null; for (Map.Entry row : rows.entrySet()) { if (row.getKey().getType() != Symbol.Type.EXTERNAL) { double temp = row.getValue().coefficientFor(entering); if (temp < 0.0) { double temp_ratio = -row.getValue().getConstant() / temp; if (temp_ratio < ratio) { ratio = temp_ratio; found = row; } } } } return found; } /** * Get the symbol for the given variable. *

* If a symbol does not exist for the variable, one will be created. */ private Symbol getVarSymbol(Variable variable) { Symbol symbol; if (vars.containsKey(variable)) { symbol = vars.get(variable); } else { symbol = new Symbol(Symbol.Type.EXTERNAL, idTick++); vars.put(variable, symbol); } return symbol; } /** * Test whether a row is composed of all dummy variables. */ private static boolean allDummies(Row row) { for (Map.Entry cell : row.getCells().entrySet()) { if (cell.getKey().getType() != Symbol.Type.DUMMY) { return false; } } return true; } }