javascript-lp-solver
Version:
Easy to use, JSON oriented Linear Programming and Mixed Int. Programming Solver
277 lines (223 loc) • 8.49 kB
JavaScript
/*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.simplexIters = 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;
// console.time("tableau_build");
// 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();
}
//
// TODO: Benchmark This
//this.matrix = new Array(height).fill(0).map(() => new Array(width).fill(0));
// console.timeEnd("tableau_build");
// console.log("height",height);
// console.log("width",width);
// console.log("------");
// console.log("");
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];
var roundedEvaluation =
Math.round((Number.EPSILON + evaluation) * roundingCoeff) / roundingCoeff;
this.evaluation = roundedEvaluation;
if (this.simplexIters === 0) {
this.bestPossibleEval = roundedEvaluation;
}
};
//-------------------------------------------------------------------
//-------------------------------------------------------------------
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);
}
};