UNPKG

@uandi/javascript-lp-solver

Version:

Easy to use, JSON oriented Linear Programming and Mixed Int. Programming Solver

1,602 lines (1,313 loc) 100 kB
(function(){if (typeof exports === "object") {module.exports = require("./main");}})(); (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ /*global describe*/ /*global require*/ /*global module*/ /*global it*/ /*global console*/ /*global process*/ var Tableau = require("./Tableau/Tableau.js"); var branchAndCut = require("./Tableau/branchAndCut.js"); var expressions = require("./expressions.js"); var Constraint = expressions.Constraint; var Equality = expressions.Equality; var Variable = expressions.Variable; var IntegerVariable = expressions.IntegerVariable; var Term = expressions.Term; /************************************************************* * Class: Model * Description: Holds the model of a linear optimisation problem **************************************************************/ function Model(precision, name) { this.tableau = new Tableau(precision); this.name = name; this.variables = []; this.integerVariables = []; this.unrestrictedVariables = {}; this.constraints = []; this.nConstraints = 0; this.nVariables = 0; this.isMinimization = true; this.tableauInitialized = false; this.relaxationIndex = 1; this.useMIRCuts = true; this.checkForCycles = false; } module.exports = Model; Model.prototype.minimize = function () { this.isMinimization = true; return this; }; Model.prototype.maximize = function () { this.isMinimization = false; return this; }; // Model.prototype.addConstraint = function (constraint) { // // TODO: make sure that the constraint does not belong do another model // // and make // this.constraints.push(constraint); // return this; // }; Model.prototype._getNewElementIndex = function () { if (this.availableIndexes.length > 0) { return this.availableIndexes.pop(); } var index = this.lastElementIndex; this.lastElementIndex += 1; return index; }; Model.prototype._addConstraint = function (constraint) { var slackVariable = constraint.slack; this.tableau.variablesPerIndex[slackVariable.index] = slackVariable; this.constraints.push(constraint); this.nConstraints += 1; if (this.tableauInitialized === true) { this.tableau.addConstraint(constraint); } }; Model.prototype.smallerThan = function (rhs) { var constraint = new Constraint(rhs, true, this.tableau.getNewElementIndex(), this); this._addConstraint(constraint); return constraint; }; Model.prototype.greaterThan = function (rhs) { var constraint = new Constraint(rhs, false, this.tableau.getNewElementIndex(), this); this._addConstraint(constraint); return constraint; }; Model.prototype.equal = function (rhs) { var constraintUpper = new Constraint(rhs, true, this.tableau.getNewElementIndex(), this); this._addConstraint(constraintUpper); var constraintLower = new Constraint(rhs, false, this.tableau.getNewElementIndex(), this); this._addConstraint(constraintLower); return new Equality(constraintUpper, constraintLower); }; Model.prototype.addVariable = function (cost, id, isInteger, isUnrestricted, priority) { if (typeof priority === "string") { switch (priority) { case "required": priority = 0; break; case "strong": priority = 1; break; case "medium": priority = 2; break; case "weak": priority = 3; break; default: priority = 0; break; } } var varIndex = this.tableau.getNewElementIndex(); if (id === null || id === undefined) { id = "v" + varIndex; } if (cost === null || cost === undefined) { cost = 0; } if (priority === null || priority === undefined) { priority = 0; } var variable; if (isInteger) { variable = new IntegerVariable(id, cost, varIndex, priority); this.integerVariables.push(variable); } else { variable = new Variable(id, cost, varIndex, priority); } this.variables.push(variable); this.tableau.variablesPerIndex[varIndex] = variable; if (isUnrestricted) { this.unrestrictedVariables[varIndex] = true; } this.nVariables += 1; if (this.tableauInitialized === true) { this.tableau.addVariable(variable); } return variable; }; Model.prototype._removeConstraint = function (constraint) { var idx = this.constraints.indexOf(constraint); if (idx === -1) { console.warn("[Model.removeConstraint] Constraint not present in model"); return; } this.constraints.splice(idx, 1); this.nConstraints -= 1; if (this.tableauInitialized === true) { this.tableau.removeConstraint(constraint); } if (constraint.relaxation) { this.removeVariable(constraint.relaxation); } }; //------------------------------------------------------------------- // For dynamic model modification //------------------------------------------------------------------- Model.prototype.removeConstraint = function (constraint) { if (constraint.isEquality) { this._removeConstraint(constraint.upperBound); this._removeConstraint(constraint.lowerBound); } else { this._removeConstraint(constraint); } return this; }; Model.prototype.removeVariable = function (variable) { var idx = this.variables.indexOf(variable); if (idx === -1) { console.warn("[Model.removeVariable] Variable not present in model"); return; } this.variables.splice(idx, 1); if (this.tableauInitialized === true) { this.tableau.removeVariable(variable); } return this; }; Model.prototype.updateRightHandSide = function (constraint, difference) { if (this.tableauInitialized === true) { this.tableau.updateRightHandSide(constraint, difference); } return this; }; Model.prototype.updateConstraintCoefficient = function (constraint, variable, difference) { if (this.tableauInitialized === true) { this.tableau.updateConstraintCoefficient(constraint, variable, difference); } return this; }; Model.prototype.setCost = function (cost, variable) { var difference = cost - variable.cost; if (this.isMinimization === false) { difference = -difference; } variable.cost = cost; this.tableau.updateCost(variable, difference); return this; }; //------------------------------------------------------------------- //------------------------------------------------------------------- Model.prototype.loadJson = function (jsonModel) { this.isMinimization = (jsonModel.opType !== "max"); var variables = jsonModel.variables; var constraints = jsonModel.constraints; var constraintsMin = {}; var constraintsMax = {}; // Instantiating constraints var constraintIds = Object.keys(constraints); var nConstraintIds = constraintIds.length; for (var c = 0; c < nConstraintIds; c += 1) { var constraintId = constraintIds[c]; var constraint = constraints[constraintId]; var equal = constraint.equal; var weight = constraint.weight; var priority = constraint.priority; var relaxed = weight !== undefined || priority !== undefined; var lowerBound, upperBound; if (equal === undefined) { var min = constraint.min; if (min !== undefined) { lowerBound = this.greaterThan(min); constraintsMin[constraintId] = lowerBound; if (relaxed) { lowerBound.relax(weight, priority); } } var max = constraint.max; if (max !== undefined) { upperBound = this.smallerThan(max); constraintsMax[constraintId] = upperBound; if (relaxed) { upperBound.relax(weight, priority); } } } else { lowerBound = this.greaterThan(equal); constraintsMin[constraintId] = lowerBound; upperBound = this.smallerThan(equal); constraintsMax[constraintId] = upperBound; var equality = new Equality(lowerBound, upperBound); if (relaxed) { equality.relax(weight, priority); } } } var variableIds = Object.keys(variables); var nVariables = variableIds.length; var integerVarIds = jsonModel.ints || {}; var binaryVarIds = jsonModel.binaries || {}; var unrestrictedVarIds = jsonModel.unrestricted || {}; // Instantiating variables and constraint terms var objectiveName = jsonModel.optimize; for (var v = 0; v < nVariables; v += 1) { // Creation of the variables var variableId = variableIds[v]; var variableConstraints = variables[variableId]; var cost = variableConstraints[objectiveName] || 0; var isBinary = !!binaryVarIds[variableId]; var isInteger = !!integerVarIds[variableId] || isBinary; var isUnrestricted = !!unrestrictedVarIds[variableId]; var variable = this.addVariable(cost, variableId, isInteger, isUnrestricted); if (isBinary) { // Creating an upperbound constraint for this variable this.smallerThan(1).addTerm(1, variable); } var constraintNames = Object.keys(variableConstraints); for (c = 0; c < constraintNames.length; c += 1) { var constraintName = constraintNames[c]; if (constraintName === objectiveName) { continue; } var coefficient = variableConstraints[constraintName]; var constraintMin = constraintsMin[constraintName]; if (constraintMin !== undefined) { constraintMin.addTerm(coefficient, variable); } var constraintMax = constraintsMax[constraintName]; if (constraintMax !== undefined) { constraintMax.addTerm(coefficient, variable); } } } return this; }; //------------------------------------------------------------------- //------------------------------------------------------------------- Model.prototype.getNumberOfIntegerVariables = function () { return this.integerVariables.length; }; Model.prototype.solve = function () { // Setting tableau if not done if (this.tableauInitialized === false) { this.tableau.setModel(this); this.tableauInitialized = true; } return this.tableau.solve(); }; Model.prototype.isFeasible = function () { return this.tableau.feasible; }; Model.prototype.save = function () { return this.tableau.save(); }; Model.prototype.restore = function () { return this.tableau.restore(); }; Model.prototype.activateMIRCuts = function (useMIRCuts) { this.useMIRCuts = useMIRCuts; }; Model.prototype.debug = function (debugCheckForCycles) { this.checkForCycles = debugCheckForCycles; }; Model.prototype.log = function (message) { return this.tableau.log(message); }; },{"./Tableau/Tableau.js":6,"./Tableau/branchAndCut.js":8,"./expressions.js":17}],2:[function(require,module,exports){ /*global describe*/ /*global require*/ /*global module*/ /*global it*/ /*global console*/ /*global process*/ /*************************************************************** * Method: polyopt * Scope: private * Agruments: * model: The model we want solver to operate on. Because we're in here, we're assuming that we're solving a multi-objective optimization problem. Poly-Optimization. polyopt. This model has to be formed a little differently because it has multiple objective functions. Normally, a model has 2 attributes: opType (string, "max" or "min"), and optimize (string, whatever attribute we're optimizing. Now, there is no opType attribute on the model, and optimize is an object of attributes to be optimized, and how they're to be optimized. For example: ... "optimize": { "pancakes": "max", "cost": "minimize" } ... **************************************************************/ module.exports = function(solver, model){ // I have no idea if this is actually works, or what, // but here is my algorithm to solve linear programs // with multiple objective functions // 1. Optimize for each constraint // 2. The results for each solution is a vector // representing a vertex on the polytope we're creating // 3. The results for all solutions describes the shape // of the polytope (would be nice to have the equation // representing this) // 4. Find the mid-point between all vertices by doing the // following (a_1 + a_2 ... a_n) / n; var objectives = model.optimize, new_constraints = JSON.parse(JSON.stringify(model.optimize)), keys = Object.keys(model.optimize), tmp, counter = 0, vectors = {}, vector_key = "", obj = {}, pareto = [], i,j,x,y,z; // Delete the optimize object from the model delete model.optimize; // Iterate and Clear for(i = 0; i < keys.length; i++){ // Clean up the new_constraints new_constraints[keys[i]] = 0; } // Solve and add for(i = 0; i < keys.length; i++){ // Prep the model model.optimize = keys[i]; model.opType = objectives[keys[i]]; // solve the model tmp = solver.Solve(model, undefined, undefined, true); // Only the variables make it into the solution; // not the attributes. // // Because of this, we have to add the attributes // back onto the solution so we can do math with // them later... // Loop over the keys for(y in keys){ // We're only worried about attributes, not variables if(!model.variables[keys[y]]){ // Create space for the attribute in the tmp object tmp[keys[y]] = tmp[keys[y]] ? tmp[keys[y]] : 0; // Go over each of the variables for(x in model.variables){ // Does the variable exist in tmp *and* does attribute exist in this model? if(model.variables[x][keys[y]] && tmp[x]){ // Add it to tmp tmp[keys[y]] += tmp[x] * model.variables[x][keys[y]]; } } } } // clear our key vector_key = "base"; // this makes sure that if we get // the same vector more than once, // we only count it once when finding // the midpoint for(j = 0; j < keys.length; j++){ if(tmp[keys[j]]){ vector_key += "-" + ((tmp[keys[j]] * 1000) | 0) / 1000; } else { vector_key += "-0"; } } // Check here to ensure it doesn't exist if(!vectors[vector_key]){ // Add the vector-key in vectors[vector_key] = 1; counter++; // Iterate over the keys // and update our new constraints for(j = 0; j < keys.length; j++){ if(tmp[keys[j]]){ new_constraints[keys[j]] += tmp[keys[j]]; } } // Push the solution into the paretos // array after cleaning it of some // excess data markers delete tmp.feasible; delete tmp.result; pareto.push(tmp); } } // Trying to find the mid-point // divide each constraint by the // number of constraints // *midpoint formula* // (x1 + x2 + x3) / 3 for(i = 0; i < keys.length; i++){ model.constraints[keys[i]] = {"equal": new_constraints[keys[i]] / counter}; } // Give the model a fake thing to optimize on model.optimize = "cheater-" + Math.random(); model.opType = "max"; // And add the fake attribute to the variables // in the model for(i in model.variables){ model.variables[i].cheater = 1; } // Build out the object with all attributes for(i in pareto){ for(x in pareto[i]){ obj[x] = obj[x] || {min: 1e99, max: -1e99}; } } // Give each pareto a full attribute list // while getting the max and min values // for each attribute for(i in obj){ for(x in pareto){ if(pareto[x][i]){ if(pareto[x][i] > obj[i].max){ obj[i].max = pareto[x][i]; } if(pareto[x][i] < obj[i].min){ obj[i].min = pareto[x][i]; } } else { pareto[x][i] = 0; obj[i].min = 0; } } } // Solve the model for the midpoints tmp = solver.Solve(model, undefined, undefined, true); return { midpoint: tmp, vertices: pareto, ranges: obj }; }; },{}],3:[function(require,module,exports){ /*global describe*/ /*global require*/ /*global module*/ /*global it*/ /*global console*/ /*global process*/ /*jshint -W083 */ /************************************************************* * Method: to_JSON * Scope: Public: * Agruments: input: Whatever the user gives us * Purpose: Convert an unfriendly formatted LP * into something that our library can * work with **************************************************************/ function to_JSON(input){ var rxo = { /* jshint ignore:start */ "is_blank": /^\W{0,}$/, "is_objective": /(max|min)(imize){0,}\:/i, //previous version //"is_int": /^\W{0,}int/i, //new version to avoid comments "is_int": /^(?!\/\*)\W{0,}int/i, "is_constraint": /(\>|\<){0,}\=/i, "is_unrestricted": /^\S{0,}unrestricted/i, "parse_lhs": /(\-|\+){0,1}\s{0,1}\d{0,}\.{0,}\d{0,}\s{0,}[A-Za-z]\S{0,}/gi, "parse_rhs": /(\-|\+){0,1}\d{1,}\.{0,}\d{0,}\W{0,}\;{0,1}$/i, "parse_dir": /(\>|\<){0,}\=/gi, "parse_int": /[^\s|^\,]+/gi, "get_num": /(\-|\+){0,1}(\W|^)\d+\.{0,1}\d{0,}/g, // Why accepting character \W before the first digit? "get_word": /[A-Za-z].*/ /* jshint ignore:end */ }, model = { "opType": "", "optimize": "_obj", "constraints": {}, "variables": {} }, constraints = { ">=": "min", "<=": "max", "=": "equal" }, tmp = "", tst = 0, ary = null, hldr = "", hldr2 = "", constraint = "", rhs = 0; // Handle input if its coming // to us as a hard string // instead of as an array of // strings if(typeof input === "string"){ input = input.split("\n"); } // Start iterating over the rows // to see what all we have for(var i = 0; i < input.length; i++){ constraint = "__" + i; // Get the string we're working with tmp = input[i]; // Set the test = 0 tst = 0; // Reset the array ary = null; // Test to see if we're the objective if(rxo.is_objective.test(tmp)){ // Set up in model the opType model.opType = tmp.match(/(max|min)/gi)[0]; // Pull apart lhs ary = tmp.match(rxo.parse_lhs).map(function(d){ return d.replace(/\s+/,""); }).slice(1); // *** STEP 1 *** /// // Get the variables out ary.forEach(function(d){ // Get the number if its there hldr = d.match(rxo.get_num); // If it isn't a number, it might // be a standalone variable if(hldr === null){ if(d.substr(0,1) === "-"){ hldr = -1; } else { hldr = 1; } } else { hldr = hldr[0]; } hldr = parseFloat(hldr); // Get the variable type hldr2 = d.match(rxo.get_word)[0].replace(/\;$/,""); // Make sure the variable is in the model model.variables[hldr2] = model.variables[hldr2] || {}; model.variables[hldr2]._obj = hldr; }); //////////////////////////////////// }else if(rxo.is_int.test(tmp)){ // Get the array of ints ary = tmp.match(rxo.parse_int).slice(1); // Since we have an int, our model should too model.ints = model.ints || {}; ary.forEach(function(d){ d = d.replace(";",""); model.ints[d] = 1; }); //////////////////////////////////// } else if(rxo.is_constraint.test(tmp)){ var separatorIndex = tmp.indexOf(":"); var constraintExpression = (separatorIndex === -1) ? tmp : tmp.slice(separatorIndex + 1); // Pull apart lhs ary = constraintExpression.match(rxo.parse_lhs).map(function(d){ return d.replace(/\s+/,""); }); // *** STEP 1 *** /// // Get the variables out ary.forEach(function(d){ // Get the number if its there hldr = d.match(rxo.get_num); if(hldr === null){ if(d.substr(0,1) === "-"){ hldr = -1; } else { hldr = 1; } } else { hldr = hldr[0]; } hldr = parseFloat(hldr); // Get the variable name hldr2 = d.match(rxo.get_word)[0]; // Make sure the variable is in the model model.variables[hldr2] = model.variables[hldr2] || {}; model.variables[hldr2][constraint] = hldr; }); // *** STEP 2 *** /// // Get the RHS out rhs = parseFloat(tmp.match(rxo.parse_rhs)[0]); // *** STEP 3 *** /// // Get the Constrainer out tmp = constraints[tmp.match(rxo.parse_dir)[0]]; model.constraints[constraint] = model.constraints[constraint] || {}; model.constraints[constraint][tmp] = rhs; //////////////////////////////////// } else if(rxo.is_unrestricted.test(tmp)){ // Get the array of unrestricted ary = tmp.match(rxo.parse_int).slice(1); // Since we have an int, our model should too model.unrestricted = model.unrestricted || {}; ary.forEach(function(d){ d = d.replace(";",""); model.unrestricted[d] = 1; }); } } return model; } /************************************************************* * Method: from_JSON * Scope: Public: * Agruments: model: The model we want solver to operate on * Purpose: Convert a friendly JSON model into a model for a * real solving library...in this case * lp_solver **************************************************************/ function from_JSON(model){ // Make sure we at least have a model if (!model) { throw new Error("Solver requires a model to operate on"); } var output = "", ary = [], norm = 1, lookup = { "max": "<=", "min": ">=", "equal": "=" }, rxClean = new RegExp("[^A-Za-z0-9]+", "gi"); // Build the objective statement output += model.opType + ":"; // Iterate over the variables for(var x in model.variables){ // Give each variable a self of 1 unless // it exists already model.variables[x][x] = model.variables[x][x] ? model.variables[x][x] : 1; // Does our objective exist here? if(model.variables[x][model.optimize]){ output += " " + model.variables[x][model.optimize] + " " + x.replace(rxClean,"_"); } } // Add some closure to our line thing output += ";\n"; // And now... to iterate over the constraints for(x in model.constraints){ for(var y in model.constraints[x]){ for(var z in model.variables){ // Does our Constraint exist here? if(model.variables[z][x]){ output += " " + model.variables[z][x] + " " + z.replace(rxClean,"_"); } } // Add the constraint type and value... output += " " + lookup[y] + " " + model.constraints[x][y]; output += ";\n"; } } // Are there any ints? if(model.ints){ output += "\n\n"; for(x in model.ints){ output += "int " + x.replace(rxClean,"_") + ";\n"; } } // Are there any unrestricted? if(model.unrestricted){ output += "\n\n"; for(x in model.unrestricted){ output += "unrestricted " + x.replace(rxClean,"_") + ";\n"; } } // And kick the string back return output; } module.exports = function (model) { // If the user is giving us an array // or a string, convert it to a JSON Model // otherwise, spit it out as a string if(model.length){ return to_JSON(model); } else { return from_JSON(model); } }; },{}],4:[function(require,module,exports){ /*global module*/ /*global require*/ var Solution = require("./Solution.js"); function MilpSolution(tableau, evaluation, feasible, bounded, branchAndCutIterations) { Solution.call(this, tableau, evaluation, feasible, bounded); this.iter = branchAndCutIterations; } module.exports = MilpSolution; MilpSolution.prototype = Object.create(Solution.prototype); MilpSolution.constructor = MilpSolution; },{"./Solution.js":5}],5:[function(require,module,exports){ /*global module*/ function Solution(tableau, evaluation, feasible, bounded) { this.feasible = feasible; this.evaluation = evaluation; this.bounded = bounded; this._tableau = tableau; } module.exports = Solution; Solution.prototype.generateSolutionSet = function () { var solutionSet = {}; var tableau = this._tableau; var varIndexByRow = tableau.varIndexByRow; var variablesPerIndex = tableau.variablesPerIndex; var matrix = tableau.matrix; var rhsColumn = tableau.rhsColumn; var lastRow = tableau.height - 1; var roundingCoeff = Math.round(1 / tableau.precision); for (var r = 1; r <= lastRow; r += 1) { var varIndex = varIndexByRow[r]; var variable = variablesPerIndex[varIndex]; if (variable === undefined || variable.isSlack === true) { continue; } var varValue = matrix[r][rhsColumn]; solutionSet[variable.id] = Math.round(varValue * roundingCoeff) / roundingCoeff; } return solutionSet; }; },{}],6:[function(require,module,exports){ /*global describe*/ /*global require*/ /*global module*/ /*global it*/ /*global console*/ /*global process*/ var Solution = require("./Solution.js"); var MilpSolution = require("./MilpSolution.js"); /************************************************************* * Class: Tableau * Description: Simplex tableau, holding a the tableau matrix * and all the information necessary to perform * the simplex algorithm * Agruments: * precision: If we're solving a MILP, how tight * do we want to define an integer, given * that 20.000000000000001 is not an integer. * (defaults to 1e-8) **************************************************************/ function Tableau(precision) { this.model = null; this.matrix = null; this.width = 0; this.height = 0; this.costRowIndex = 0; this.rhsColumn = 0; this.variablesPerIndex = []; this.unrestrictedVars = null; // Solution attributes this.feasible = true; // until proven guilty this.evaluation = 0; this.varIndexByRow = null; this.varIndexByCol = null; this.rowByVarIndex = null; this.colByVarIndex = null; this.precision = precision || 1e-8; this.optionalObjectives = []; this.objectivesByPriority = {}; this.savedState = null; this.availableIndexes = []; this.lastElementIndex = 0; this.variables = null; this.nVars = 0; this.bounded = true; this.unboundedVarIndex = null; this.branchAndCutIterations = 0; } module.exports = Tableau; Tableau.prototype.solve = function () { if (this.model.getNumberOfIntegerVariables() > 0) { this.branchAndCut(); } else { this.simplex(); } this.updateVariableValues(); return this.getSolution(); }; function OptionalObjective(priority, nColumns) { this.priority = priority; this.reducedCosts = new Array(nColumns); for (var c = 0; c < nColumns; c += 1) { this.reducedCosts[c] = 0; } } OptionalObjective.prototype.copy = function () { var copy = new OptionalObjective(this.priority, this.reducedCosts.length); copy.reducedCosts = this.reducedCosts.slice(); return copy; }; Tableau.prototype.setOptionalObjective = function (priority, column, cost) { var objectiveForPriority = this.objectivesByPriority[priority]; if (objectiveForPriority === undefined) { var nColumns = Math.max(this.width, column + 1); objectiveForPriority = new OptionalObjective(priority, nColumns); this.objectivesByPriority[priority] = objectiveForPriority; this.optionalObjectives.push(objectiveForPriority); this.optionalObjectives.sort(function (a, b) { return a.priority - b.priority; }); } objectiveForPriority.reducedCosts[column] = cost; }; //------------------------------------------------------------------- //------------------------------------------------------------------- Tableau.prototype.initialize = function (width, height, variables, unrestrictedVars) { this.variables = variables; this.unrestrictedVars = unrestrictedVars; this.width = width; this.height = height; // BUILD AN EMPTY ARRAY OF THAT WIDTH var tmpRow = new Array(width); for (var i = 0; i < width; i++) { tmpRow[i] = 0; } // BUILD AN EMPTY TABLEAU this.matrix = new Array(height); for (var j = 0; j < height; j++) { this.matrix[j] = tmpRow.slice(); } this.varIndexByRow = new Array(this.height); this.varIndexByCol = new Array(this.width); this.varIndexByRow[0] = -1; this.varIndexByCol[0] = -1; this.nVars = width + height - 2; this.rowByVarIndex = new Array(this.nVars); this.colByVarIndex = new Array(this.nVars); this.lastElementIndex = this.nVars; }; Tableau.prototype._resetMatrix = function () { var variables = this.model.variables; var constraints = this.model.constraints; var nVars = variables.length; var nConstraints = constraints.length; var v, varIndex; var costRow = this.matrix[0]; var coeff = (this.model.isMinimization === true) ? -1 : 1; for (v = 0; v < nVars; v += 1) { var variable = variables[v]; var priority = variable.priority; var cost = coeff * variable.cost; if (priority === 0) { costRow[v + 1] = cost; } else { this.setOptionalObjective(priority, v + 1, cost); } varIndex = variables[v].index; this.rowByVarIndex[varIndex] = -1; this.colByVarIndex[varIndex] = v + 1; this.varIndexByCol[v + 1] = varIndex; } var rowIndex = 1; for (var c = 0; c < nConstraints; c += 1) { var constraint = constraints[c]; var constraintIndex = constraint.index; this.rowByVarIndex[constraintIndex] = rowIndex; this.colByVarIndex[constraintIndex] = -1; this.varIndexByRow[rowIndex] = constraintIndex; var t, term, column; var terms = constraint.terms; var nTerms = terms.length; var row = this.matrix[rowIndex++]; if (constraint.isUpperBound) { for (t = 0; t < nTerms; t += 1) { term = terms[t]; column = this.colByVarIndex[term.variable.index]; row[column] = term.coefficient; } row[0] = constraint.rhs; } else { for (t = 0; t < nTerms; t += 1) { term = terms[t]; column = this.colByVarIndex[term.variable.index]; row[column] = -term.coefficient; } row[0] = -constraint.rhs; } } }; //------------------------------------------------------------------- //------------------------------------------------------------------- Tableau.prototype.setModel = function (model) { this.model = model; var width = model.nVariables + 1; var height = model.nConstraints + 1; this.initialize(width, height, model.variables, model.unrestrictedVariables); this._resetMatrix(); return this; }; Tableau.prototype.getNewElementIndex = function () { if (this.availableIndexes.length > 0) { return this.availableIndexes.pop(); } var index = this.lastElementIndex; this.lastElementIndex += 1; return index; }; Tableau.prototype.density = function () { var density = 0; var matrix = this.matrix; for (var r = 0; r < this.height; r++) { var row = matrix[r]; for (var c = 0; c < this.width; c++) { if (row[c] !== 0) { density += 1; } } } return density / (this.height * this.width); }; //------------------------------------------------------------------- //------------------------------------------------------------------- Tableau.prototype.setEvaluation = function () { // Rounding objective value var roundingCoeff = Math.round(1 / this.precision); var evaluation = this.matrix[this.costRowIndex][this.rhsColumn]; this.evaluation = Math.round(evaluation * roundingCoeff) / roundingCoeff; }; //------------------------------------------------------------------- //------------------------------------------------------------------- Tableau.prototype.getSolution = function () { var evaluation = (this.model.isMinimization === true) ? this.evaluation : -this.evaluation; if (this.model.getNumberOfIntegerVariables() > 0) { return new MilpSolution(this, evaluation, this.feasible, this.bounded, this.branchAndCutIterations); } else { return new Solution(this, evaluation, this.feasible, this.bounded); } }; },{"./MilpSolution.js":4,"./Solution.js":5}],7:[function(require,module,exports){ /*global require*/ var Tableau = require("./Tableau.js"); Tableau.prototype.copy = function () { var copy = new Tableau(this.precision); copy.width = this.width; copy.height = this.height; copy.nVars = this.nVars; copy.model = this.model; // Making a shallow copy of integer variable indexes // and variable ids copy.variables = this.variables; copy.variablesPerIndex = this.variablesPerIndex; copy.unrestrictedVars = this.unrestrictedVars; copy.lastElementIndex = this.lastElementIndex; // All the other arrays are deep copied copy.varIndexByRow = this.varIndexByRow.slice(); copy.varIndexByCol = this.varIndexByCol.slice(); copy.rowByVarIndex = this.rowByVarIndex.slice(); copy.colByVarIndex = this.colByVarIndex.slice(); copy.availableIndexes = this.availableIndexes.slice(); var optionalObjectivesCopy = []; for(var o = 0; o < this.optionalObjectives.length; o++){ optionalObjectivesCopy[o] = this.optionalObjectives[o].copy(); } copy.optionalObjectives = optionalObjectivesCopy; var matrix = this.matrix; var matrixCopy = new Array(this.height); for (var r = 0; r < this.height; r++) { matrixCopy[r] = matrix[r].slice(); } copy.matrix = matrixCopy; return copy; }; Tableau.prototype.save = function () { this.savedState = this.copy(); }; Tableau.prototype.restore = function () { if (this.savedState === null) { return; } var save = this.savedState; var savedMatrix = save.matrix; this.nVars = save.nVars; this.model = save.model; // Shallow restore this.variables = save.variables; this.variablesPerIndex = save.variablesPerIndex; this.unrestrictedVars = save.unrestrictedVars; this.lastElementIndex = save.lastElementIndex; this.width = save.width; this.height = save.height; // Restoring matrix var r, c; for (r = 0; r < this.height; r += 1) { var savedRow = savedMatrix[r]; var row = this.matrix[r]; for (c = 0; c < this.width; c += 1) { row[c] = savedRow[c]; } } // Restoring all the other structures var savedBasicIndexes = save.varIndexByRow; for (c = 0; c < this.height; c += 1) { this.varIndexByRow[c] = savedBasicIndexes[c]; } while (this.varIndexByRow.length > this.height) { this.varIndexByRow.pop(); } var savedNonBasicIndexes = save.varIndexByCol; for (r = 0; r < this.width; r += 1) { this.varIndexByCol[r] = savedNonBasicIndexes[r]; } while (this.varIndexByCol.length > this.width) { this.varIndexByCol.pop(); } var savedRows = save.rowByVarIndex; var savedCols = save.colByVarIndex; for (var v = 0; v < this.nVars; v += 1) { this.rowByVarIndex[v] = savedRows[v]; this.colByVarIndex[v] = savedCols[v]; } if (save.optionalObjectives.length > 0 && this.optionalObjectives.length > 0) { this.optionalObjectives = []; this.optionalObjectivePerPriority = {}; for(var o = 0; o < save.optionalObjectives.length; o++){ var optionalObjectiveCopy = save.optionalObjectives[o].copy(); this.optionalObjectives[o] = optionalObjectiveCopy; this.optionalObjectivePerPriority[optionalObjectiveCopy.priority] = optionalObjectiveCopy; } } }; },{"./Tableau.js":6}],8:[function(require,module,exports){ /*global describe*/ /*global require*/ /*global module*/ /*global it*/ /*global console*/ /*global process*/ var Tableau = require("./Tableau.js"); //------------------------------------------------------------------- //------------------------------------------------------------------- function Cut(type, varIndex, value) { this.type = type; this.varIndex = varIndex; this.value = value; } //------------------------------------------------------------------- //------------------------------------------------------------------- function Branch(relaxedEvaluation, cuts) { this.relaxedEvaluation = relaxedEvaluation; this.cuts = cuts; } //------------------------------------------------------------------- // Branch sorting strategies //------------------------------------------------------------------- function sortByEvaluation(a, b) { return b.relaxedEvaluation - a.relaxedEvaluation; } //------------------------------------------------------------------- // Applying cuts on a tableau and resolving //------------------------------------------------------------------- Tableau.prototype.applyCuts = function (branchingCuts){ // Restoring initial solution this.restore(); this.addCutConstraints(branchingCuts); this.simplex(); // Adding MIR cuts if (this.model.useMIRCuts){ var fractionalVolumeImproved = true; while(fractionalVolumeImproved){ var fractionalVolumeBefore = this.computeFractionalVolume(true); this.applyMIRCuts(); this.simplex(); var fractionalVolumeAfter = this.computeFractionalVolume(true); // If the new fractional volume is bigger than 90% of the previous one // we assume there is no improvement from the MIR cuts if(fractionalVolumeAfter >= 0.9 * fractionalVolumeBefore){ fractionalVolumeImproved = false; } } } }; //------------------------------------------------------------------- // Function: MILP // Detail: Main function, my attempt at a mixed integer linear programming // solver //------------------------------------------------------------------- Tableau.prototype.branchAndCut = function () { var branches = []; var iterations = 0; // This is the default result // If nothing is both *integral* and *feasible* var bestEvaluation = Infinity; var bestBranch = null; var bestOptionalObjectivesEvaluations = []; for (var oInit = 0; oInit < this.optionalObjectives.length; oInit += 1){ bestOptionalObjectivesEvaluations.push(Infinity); } // And here...we...go! // 1.) Load a model into the queue var branch = new Branch(-Infinity, []); branches.push(branch); // If all branches have been exhausted terminate the loop while (branches.length > 0) { // Get a model from the queue branch = branches.pop(); if (branch.relaxedEvaluation > bestEvaluation) { continue; } // Solving from initial relaxed solution // with additional cut constraints // Adding cut constraints var cuts = branch.cuts; this.applyCuts(cuts); iterations++; if (this.feasible === false) { continue; } var evaluation = this.evaluation; if (evaluation > bestEvaluation) { // This branch does not contain the optimal solution continue; } // To deal with the optional objectives if (evaluation === bestEvaluation){ var isCurrentEvaluationWorse = true; for (var o = 0; o < this.optionalObjectives.length; o += 1){ if (this.optionalObjectives[o].reducedCosts[0] > bestOptionalObjectivesEvaluations[o]){ break; } else if (this.optionalObjectives[o].reducedCosts[0] < bestOptionalObjectivesEvaluations[o]) { isCurrentEvaluationWorse = false; break; } } if (isCurrentEvaluationWorse){ continue; } } // Is the model both integral and feasible? if (this.isIntegral() === true) { if (iterations === 1) { this.branchAndCutIterations = iterations; return; } // Store the solution as the bestSolution bestBranch = branch; bestEvaluation = evaluation; for (var oCopy = 0; oCopy < this.optionalObjectives.length; oCopy += 1){ bestOptionalObjectivesEvaluations[oCopy] = this.optionalObjectives[oCopy].reducedCosts[0]; } } else { if (iterations === 1) { // Saving the first iteration // TODO: implement a better strategy for saving the tableau? this.save(); } // If the solution is // a. Feasible // b. Better than the current solution // c. but *NOT* integral // So the solution isn't integral? How do we solve this. // We create 2 new models, that are mirror images of the prior // model, with 1 exception. // Say we're trying to solve some stupid problem requiring you get // animals for your daughter's kindergarten petting zoo party // and you have to choose how many ducks, goats, and lambs to get. // Say that the optimal solution to this problem if we didn't have // to make it integral was {duck: 8, lambs: 3.5} // // To keep from traumatizing your daughter and the other children // you're going to want to have whole animals // What we would do is find the most fractional variable (lambs) // and create new models from the old models, but with a new constraint // on apples. The constraints on the low model would look like: // constraints: {... // lamb: {max: 3} // ... // } // // while the constraints on the high model would look like: // // constraints: {... // lamb: {min: 4} // ... // } // If neither of these models is feasible because of this constraint, // the model is not integral at this point, and fails. // Find out where we want to split the solution var variable = this.getMostFractionalVar(); var varIndex = variable.index; var cutsHigh = []; var cutsLow = []; var nCuts = cuts.length; for (var c = 0; c < nCuts; c += 1) { var cut = cuts[c]; if (cut.varIndex === varIndex) { if (cut.type === "min") { cutsLow.push(cut); } else { cutsHigh.push(cut); } } else { cutsHigh.push(cut); cutsLow.push(cut); } } var min = Math.ceil(variable.value); var max = Math.floor(variable.value); var cutHigh = new Cut("min", varIndex, min); cutsHigh.push(cutHigh); var cutLow = new Cut("max", varIndex, max); cutsLow.push(cutLow); branches.push(new Branch(evaluation, cutsHigh)); branches.push(new Branch(evaluation, cutsLow)); // Sorting branches // Branches with the most promising lower bounds // will be picked first branches.sort(sortByEvaluation); } } // Adding cut constraints for the optimal solution if (bestBranch !== null) { // The model is feasible this.applyCuts(bestBranch.cuts); } this.branchAndCutIterations = iterations; }; },{"./Tableau.js":6}],9:[function(require,module,exports){ /*global require*/ var Tableau = require("./Tableau.js"); function VariableData(index, value) { this.index = index; this.value = value; } //------------------------------------------------------------------- //------------------------------------------------------------------- Tableau.prototype.getMostFractionalVar = function () { var biggestFraction = 0; var selectedVarIndex = null; var selectedVarValue = null; var mid = 0.5; var integerVariables = this.model.integerVariables; var nIntegerVars = integerVariables.length; for (var v = 0; v < nIntegerVars; v++) { var varIndex = integerVariables[v].index; var varRow = this.rowByVarIndex[varIndex]; if (varRow === -1) { continue; } var varValue = this.matrix[varRow][this.rhsColumn]; var fraction = Math.abs(varValue - Math.round(varValue)); if (biggestFraction < fraction) { biggestFraction = fraction; selectedVarIndex = varIndex; selectedVarValue = varValue; } } return new VariableData(selectedVarIndex, selectedVarValue); }; //------------------------------------------------------------------- //------------------------------------------------------------------- Tableau.prototype.getFractionalVarWithLowestCost = function () { var highestCost = Infinity; var selectedVarIndex = null; var selectedVarValue = null; var integerVariables = this.model.integerVariables; var nIntegerVars = integerVariables.length; for (var v = 0; v < nIntegerVars; v++) { var variable = integerVariables[v]; var varIndex = variable.index; var varRow = this.rowByVarIndex[varIndex]; if (varRow === -1) { // Variable value is non basic // its value is 0 continue; } var varValue = this.matrix[varRow][this.rhsColumn]; if (Math.abs(varValue - Math.round(varValue)) > this.precision) { var cost = variable.cost; if (highestCost > cost) { highestCost = cost; selectedVarIndex = varIndex; selectedVarValue = varValue; } } } return new VariableData(selectedVarIndex, selectedVarValue); }; },{"./Tableau.js":6}],10:[function(require,module,exports){ /*global require*/ var Tableau = require("./Tableau.js"); var SlackVariable = require("../expressions.js").SlackVariable; Tableau.prototype.addCutConstraints = function (cutConstraints) { var nCutConstraints = cutConst