yalps
Version:
Yet another linear programming solver. (A rewrite of javascript-lp-solver.) Aims to be decently fast.
406 lines (401 loc) • 14.7 kB
JavaScript
// src/constraint.ts
var lessEq = (value) => ({ max: value });
var greaterEq = (value) => ({ min: value });
var equalTo = (value) => ({ equal: value });
var inRange = (lower, upper) => ({ min: lower, max: upper });
// src/tableau.ts
var index = (tableau, row, col) => tableau.matrix[Math.imul(row, tableau.width) + col];
var update = (tableau, row, col, value) => {
tableau.matrix[Math.imul(row, tableau.width) + col] = value;
};
var convertToIterable = (seq) => Symbol.iterator in seq && typeof seq[Symbol.iterator] === "function" ? seq : (
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
Object.entries(seq)
);
var convertToSet = (set) => set === true ? true : set === false ? /* @__PURE__ */ new Set() : set instanceof Set ? set : new Set(set);
var tableauModel = (model) => {
const { direction, objective, integers, binaries } = model;
const sign = direction === "minimize" ? -1 : 1;
const constraintsIter = convertToIterable(model.constraints);
const variablesIter = convertToIterable(model.variables);
const variables = Array.isArray(variablesIter) ? variablesIter : Array.from(variablesIter);
const binaryConstraintCol = [];
const ints = [];
if (integers != null || binaries != null) {
const binaryVariables = convertToSet(binaries);
const integerVariables = binaryVariables === true ? true : convertToSet(integers);
for (let i = 1; i <= variables.length; i++) {
const [key] = variables[i - 1];
if (binaryVariables === true || binaryVariables.has(key)) {
binaryConstraintCol.push(i);
ints.push(i);
} else if (integerVariables === true || integerVariables.has(key)) {
ints.push(i);
}
}
}
const constraints = /* @__PURE__ */ new Map();
for (const [key, constraint] of constraintsIter) {
const bounds = constraints.get(key) ?? { row: NaN, lower: -Infinity, upper: Infinity };
bounds.lower = Math.max(bounds.lower, constraint.equal ?? constraint.min ?? -Infinity);
bounds.upper = Math.min(bounds.upper, constraint.equal ?? constraint.max ?? Infinity);
if (!constraints.has(key)) constraints.set(key, bounds);
}
let numConstraints = 1;
for (const constraint of constraints.values()) {
constraint.row = numConstraints;
numConstraints += (Number.isFinite(constraint.lower) ? 1 : 0) + (Number.isFinite(constraint.upper) ? 1 : 0);
}
const width = variables.length + 1;
const height = numConstraints + binaryConstraintCol.length;
const numVars = width + height;
const matrix = new Float64Array(width * height);
const positionOfVariable = new Int32Array(numVars);
const variableAtPosition = new Int32Array(numVars);
const tableau = { matrix, width, height, positionOfVariable, variableAtPosition };
for (let i = 0; i < numVars; i++) {
positionOfVariable[i] = i;
variableAtPosition[i] = i;
}
for (let c = 1; c < width; c++) {
for (const [constraint, coef] of convertToIterable(variables[c - 1][1])) {
if (constraint === objective) {
update(tableau, 0, c, sign * coef);
}
const bounds = constraints.get(constraint);
if (bounds != null) {
if (Number.isFinite(bounds.upper)) {
update(tableau, bounds.row, c, coef);
if (Number.isFinite(bounds.lower)) {
update(tableau, bounds.row + 1, c, -coef);
}
} else if (Number.isFinite(bounds.lower)) {
update(tableau, bounds.row, c, -coef);
}
}
}
}
for (const bounds of constraints.values()) {
if (Number.isFinite(bounds.upper)) {
update(tableau, bounds.row, 0, bounds.upper);
if (Number.isFinite(bounds.lower)) {
update(tableau, bounds.row + 1, 0, -bounds.lower);
}
} else if (Number.isFinite(bounds.lower)) {
update(tableau, bounds.row, 0, -bounds.lower);
}
}
for (let b = 0; b < binaryConstraintCol.length; b++) {
const row = numConstraints + b;
update(tableau, row, 0, 1);
update(tableau, row, binaryConstraintCol[b], 1);
}
return { tableau, sign, variables, integers: ints };
};
// src/util.ts
var roundToPrecision = (num, precision) => {
const rounding = Math.round(1 / precision);
return Math.round((num + Number.EPSILON) * rounding) / rounding;
};
// src/simplex.ts
var pivot = (tableau, row, col) => {
const quotient = index(tableau, row, col);
const leaving = tableau.variableAtPosition[tableau.width + row];
const entering = tableau.variableAtPosition[col];
tableau.variableAtPosition[tableau.width + row] = entering;
tableau.variableAtPosition[col] = leaving;
tableau.positionOfVariable[leaving] = col;
tableau.positionOfVariable[entering] = tableau.width + row;
const nonZeroColumns = [];
for (let c = 0; c < tableau.width; c++) {
const value = index(tableau, row, c);
if (Math.abs(value) > 1e-16) {
update(tableau, row, c, value / quotient);
nonZeroColumns.push(c);
} else {
update(tableau, row, c, 0);
}
}
update(tableau, row, col, 1 / quotient);
for (let r = 0; r < tableau.height; r++) {
if (r === row) continue;
const coef = index(tableau, r, col);
if (Math.abs(coef) > 1e-16) {
for (let i = 0; i < nonZeroColumns.length; i++) {
const c = nonZeroColumns[i];
update(tableau, r, c, index(tableau, r, c) - coef * index(tableau, row, c));
}
update(tableau, r, col, -coef / quotient);
}
}
};
var hasCycle = (history, tableau, row, col) => {
history.push([tableau.variableAtPosition[tableau.width + row], tableau.variableAtPosition[col]]);
for (let length = 6; length <= Math.trunc(history.length / 2); length++) {
let cycle = true;
for (let i = 0; i < length; i++) {
const item = history.length - 1 - i;
const [row1, col1] = history[item];
const [row2, col2] = history[item - length];
if (row1 !== row2 || col1 !== col2) {
cycle = false;
break;
}
}
if (cycle) return true;
}
return false;
};
var phase2 = (tableau, options) => {
const pivotHistory = [];
const { precision, maxPivots, checkCycles } = options;
for (let iter = 0; iter < maxPivots; iter++) {
let col = 0;
let value = precision;
for (let c = 1; c < tableau.width; c++) {
const reducedCost = index(tableau, 0, c);
if (reducedCost > value) {
value = reducedCost;
col = c;
}
}
if (col === 0) return ["optimal", roundToPrecision(index(tableau, 0, 0), precision)];
let row = 0;
let minRatio = Infinity;
for (let r = 1; r < tableau.height; r++) {
const value2 = index(tableau, r, col);
if (value2 <= precision) continue;
const rhs = index(tableau, r, 0);
const ratio = rhs / value2;
if (ratio < minRatio) {
row = r;
minRatio = ratio;
if (ratio <= precision) break;
}
}
if (row === 0) return ["unbounded", col];
if (checkCycles && hasCycle(pivotHistory, tableau, row, col)) return ["cycled", NaN];
pivot(tableau, row, col);
}
return ["cycled", NaN];
};
var phase1 = (tableau, options) => {
const pivotHistory = [];
const { precision, maxPivots, checkCycles } = options;
for (let iter = 0; iter < maxPivots; iter++) {
let row = 0;
let rhs = -precision;
for (let r = 1; r < tableau.height; r++) {
const value = index(tableau, r, 0);
if (value < rhs) {
rhs = value;
row = r;
}
}
if (row === 0) return phase2(tableau, options);
let col = 0;
let maxRatio = -Infinity;
for (let c = 1; c < tableau.width; c++) {
const coefficient = index(tableau, row, c);
if (coefficient < -precision) {
const ratio = -index(tableau, 0, c) / coefficient;
if (ratio > maxRatio) {
maxRatio = ratio;
col = c;
}
}
}
if (col === 0) return ["infeasible", NaN];
if (checkCycles && hasCycle(pivotHistory, tableau, row, col)) return ["cycled", NaN];
pivot(tableau, row, col);
}
return ["cycled", NaN];
};
// src/branchAndCut.ts
import Heap from "heap";
var buffer = (matrixLength, posVarLength) => ({
matrix: new Float64Array(matrixLength),
positionOfVariable: new Int32Array(posVarLength),
variableAtPosition: new Int32Array(posVarLength)
});
var applyCuts = (tableau, { matrix, positionOfVariable, variableAtPosition }, cuts) => {
const { width, height } = tableau;
matrix.set(tableau.matrix);
for (let i = 0; i < cuts.length; i++) {
const [sign, variable, value] = cuts[i];
const r = (height + i) * width;
const pos = tableau.positionOfVariable[variable];
if (pos < width) {
matrix[r] = sign * value;
matrix.fill(0, r + 1, r + width);
matrix[r + pos] = sign;
} else {
const row = (pos - width) * width;
matrix[r] = sign * (value - matrix[row]);
for (let c = 1; c < width; c++) {
matrix[r + c] = -sign * matrix[row + c];
}
}
}
positionOfVariable.set(tableau.positionOfVariable);
variableAtPosition.set(tableau.variableAtPosition);
const length = width + height + cuts.length;
for (let i = width + height; i < length; i++) {
positionOfVariable[i] = i;
variableAtPosition[i] = i;
}
return {
matrix: matrix.subarray(0, tableau.matrix.length + width * cuts.length),
width,
height: height + cuts.length,
positionOfVariable: positionOfVariable.subarray(0, length),
variableAtPosition: variableAtPosition.subarray(0, length)
};
};
var mostFractionalVar = (tableau, intVars) => {
let highestFrac = 0;
let variable = 0;
let value = 0;
for (let i = 0; i < intVars.length; i++) {
const intVar = intVars[i];
const row = tableau.positionOfVariable[intVar] - tableau.width;
if (row < 0) continue;
const val = index(tableau, row, 0);
const frac = Math.abs(val - Math.round(val));
if (frac > highestFrac) {
highestFrac = frac;
variable = intVar;
value = val;
}
}
return [variable, value, highestFrac];
};
var branchAndCut = (tabmod, initResult, options) => {
const { tableau, sign, integers } = tabmod;
const { precision, maxIterations, tolerance, timeout } = options;
const [initVariable, initValue, initFrac] = mostFractionalVar(tableau, integers);
if (initFrac <= precision) return [tabmod, "optimal", initResult];
const branches = new Heap((x, y) => x[0] - y[0]);
branches.push([initResult, [[-1, initVariable, Math.ceil(initValue)]]]);
branches.push([initResult, [[1, initVariable, Math.floor(initValue)]]]);
const maxExtraRows = integers.length * 2;
const matrixLength = tableau.matrix.length + maxExtraRows * tableau.width;
const posVarLength = tableau.positionOfVariable.length + maxExtraRows;
let candidateBuffer = buffer(matrixLength, posVarLength);
let solutionBuffer = buffer(matrixLength, posVarLength);
const optimalThreshold = initResult * (1 - sign * tolerance);
const stopTime = timeout + Date.now();
let timedout = Date.now() >= stopTime;
let solutionFound = false;
let bestEval = Infinity;
let bestTableau = tableau;
let iter = 0;
while (iter < maxIterations && !branches.empty() && bestEval >= optimalThreshold && !timedout) {
const [relaxedEval, cuts] = branches.pop();
if (relaxedEval > bestEval) break;
const currentTableau = applyCuts(tableau, candidateBuffer, cuts);
const [status2, result] = phase1(currentTableau, options);
if (status2 === "optimal" && result < bestEval) {
const [variable, value, frac] = mostFractionalVar(currentTableau, integers);
if (frac <= precision) {
solutionFound = true;
bestEval = result;
bestTableau = currentTableau;
const temp = solutionBuffer;
solutionBuffer = candidateBuffer;
candidateBuffer = temp;
} else {
const cutsUpper = [];
const cutsLower = [];
for (let i = 0; i < cuts.length; i++) {
const cut = cuts[i];
const [dir, v] = cut;
if (v === variable) {
if (dir < 0) {
cutsLower.push(cut);
} else {
cutsUpper.push(cut);
}
} else {
cutsUpper.push(cut);
cutsLower.push(cut);
}
}
cutsLower.push([1, variable, Math.floor(value)]);
cutsUpper.push([-1, variable, Math.ceil(value)]);
branches.push([result, cutsUpper]);
branches.push([result, cutsLower]);
}
}
timedout = Date.now() >= stopTime;
iter++;
}
const unfinished = (timedout || iter >= maxIterations) && !branches.empty() && bestEval >= optimalThreshold;
const status = unfinished ? "timedout" : !solutionFound ? "infeasible" : "optimal";
return [{ ...tabmod, tableau: bestTableau }, status, solutionFound ? bestEval : NaN];
};
// src/YALPS.ts
var solution = ({ tableau, sign, variables: vars }, status, result, { precision, includeZeroVariables }) => {
if (status === "optimal" || status === "timedout" && !Number.isNaN(result)) {
const variables = [];
for (let i = 0; i < vars.length; i++) {
const [variable] = vars[i];
const row = tableau.positionOfVariable[i + 1] - tableau.width;
const value = row >= 0 ? index(tableau, row, 0) : 0;
if (value > precision) {
variables.push([variable, roundToPrecision(value, precision)]);
} else if (includeZeroVariables) {
variables.push([variable, 0]);
}
}
return {
status,
result: -sign * result,
variables
};
} else if (status === "unbounded") {
const variable = tableau.variableAtPosition[result] - 1;
return {
status: "unbounded",
result: sign * Infinity,
// prettier-ignore
variables: 0 <= variable && variable < vars.length ? [[vars[variable][0], Infinity]] : []
};
} else {
return {
status,
result: NaN,
variables: []
};
}
};
var defaultOptionValues = {
precision: 1e-8,
checkCycles: false,
maxPivots: 8192,
tolerance: 0,
timeout: Infinity,
maxIterations: 32768,
includeZeroVariables: false
};
var defaultOptions = { ...defaultOptionValues };
var solve = (model, options) => {
const tabmod = tableauModel(model);
const opt = { ...defaultOptionValues, ...options };
const [status, result] = phase1(tabmod.tableau, opt);
if (tabmod.integers.length === 0 || status !== "optimal") {
return solution(tabmod, status, result, opt);
} else {
const [intTabmod, intStatus, intResult] = branchAndCut(tabmod, result, opt);
return solution(intTabmod, intStatus, intResult, opt);
}
};
export {
defaultOptions,
equalTo,
greaterEq,
inRange,
lessEq,
solve
};
//# sourceMappingURL=index.js.map