UNPKG

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
// 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