diff --git a/package-lock.json b/package-lock.json index 4ec5678..3c130be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@rollup/plugin-terser": "^0.4.4", "glpk.js": "^4.0.2", "highs": "^1.0.1", + "javascript-lp-solver": "^0.4.24", "rollup": "^4.12.0" } }, @@ -323,6 +324,12 @@ "integrity": "sha512-RwVukT+x/BFAoSaIo8FP9zsTqSXfpdrY5MupQvBbSv4Umx79FkFrnHjcfMhy624YiBAI17GVPwNp4qpHNMAhbA==", "dev": true }, + "node_modules/javascript-lp-solver": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/javascript-lp-solver/-/javascript-lp-solver-0.4.24.tgz", + "integrity": "sha512-5edoDKnMrt/u3M6GnZKDDIPxOyFOg+WrwDv8mjNiMC2DePhy2H9/FFQgf4ggywaXT1utvkxusJcjQUER72cZmA==", + "dev": true + }, "node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", diff --git a/package.json b/package.json index 9366ac0..a37d825 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@rollup/plugin-terser": "^0.4.4", "glpk.js": "^4.0.2", "highs": "^1.0.1", + "javascript-lp-solver": "^0.4.24", "rollup": "^4.12.0" }, "files": [ diff --git a/src/jsLPSolver-bridge.js b/src/jsLPSolver-bridge.js new file mode 100644 index 0000000..002bea9 --- /dev/null +++ b/src/jsLPSolver-bridge.js @@ -0,0 +1,87 @@ +export function toJSLPSolverFormat(model, options) { + const jsLPModel = { + optimize: "objective", // We'll use a generic name for the objective + opType: model.objective.sense.toLowerCase().slice(0, 3), // Convert to "max" or "min" + constraints: {}, + variables: {}, + ints: {}, + binaries: {}, + unrestricted: {}, + options: options + }; + + // Translate variables and handle bounds + model.variables.forEach((varObj, varName) => { + jsLPModel.variables[varName] = {}; // Initialize variable entry + + // Handle unrestricted variables (allowed to be negative) + if (varObj.lb === "-infinity" || varObj.lb < 0) { + jsLPModel.unrestricted[varName] = 1; + } + + // If the variable has specific bounds, add virtual constraints + if (varObj.lb !== 0 && varObj.lb !== "-infinity") { + jsLPModel.constraints[`${varName}_lb`] = { min: varObj.lb }; + } + if (varObj.ub !== "+infinity") { + jsLPModel.constraints[`${varName}_ub`] = { max: varObj.ub }; + } + + // Mark binary and integer variables + if (varObj.vtype === "BINARY") { + jsLPModel.binaries[varName] = 1; + } else if (varObj.vtype === "INTEGER") { + jsLPModel.ints[varName] = 1; + } + }); + + // Translate the objective function + model.objective.expression.forEach(term => { + if (Array.isArray(term)) { // Exclude constant term + jsLPModel.variables[term[1].name]["objective"] = term[0]; + } + }); + + // Translate constraints + model.constraints.forEach((constr, index) => { + const constrName = `c${index}`; + jsLPModel.constraints[constrName] = {}; + if (constr.comparison === "<=") { + jsLPModel.constraints[constrName].max = constr.rhs; + } else if (constr.comparison === ">=") { + jsLPModel.constraints[constrName].min = constr.rhs; + } else if (constr.comparison === "=") { + jsLPModel.constraints[constrName].equal = constr.rhs; + } + constr.lhs.forEach(term => { + if (Array.isArray(term)) { + if (!(constrName in jsLPModel.variables[term[1].name])) { + jsLPModel.variables[term[1].name][constrName] = 0; + } + jsLPModel.variables[term[1].name][constrName] += term[0]; + } + }); + }); + + return jsLPModel; +} + +export function readJSLPSolverSolution(model, solution) { + // example { feasible: true, result: 1080000, bounded: true, isIntegral: true, var1: 24, var2: 20 } and unmentioned variables are 0 + // console.log("readJSLPSolverSolution", solution); + model.status = solution.feasible ? (solution.bounded ? "Optimal" : "Unbounded") : "Infeasible"; + + // Update variable values + model.variables.forEach((varObj, varName) => { + if (varName in solution) { + varObj.value = solution[varName]; + } else { + varObj.value = 0; + } + }); + + // Update objective value + if (solution.result) { + model.ObjVal = solution.result + model.objective.expression[0]; // Add constant term to objective value + } +} \ No newline at end of file diff --git a/src/model.js b/src/model.js index 3a9ac30..49d6894 100644 --- a/src/model.js +++ b/src/model.js @@ -1,4 +1,5 @@ import { toGLPKFormat, readGLPKSolution } from './glpk-js-bridge.js'; +import { toJSLPSolverFormat, readJSLPSolverSolution } from './jsLPSolver-bridge.js'; import { readHighsSolution } from './highs-js-bridge.js'; import { toLPFormat } from './write-lp-format.js'; @@ -147,6 +148,11 @@ export class Model { lhs = this.parseExpression(lhs); rhs = typeof rhs === 'number' ? [rhs] : this.parseExpression(rhs); + if (comparison === "==") comparison = "="; // Convert to standard comparison operator + if (!["<=", "=", ">="].includes(comparison)) { + throw new Error(`Invalid comparison operator: ${comparison}. Must be one of "<=", "=", or ">=".`); + } + // Combine LHS and negated RHS const combinedLhs = lhs.concat(rhs.map(term => { if (Array.isArray(term)) { @@ -286,6 +292,24 @@ export class Model { readGLPKSolution(this, solution); } + /** + * Converts the model to the JSON format for use with the jsLPSolver solver. + * @returns {Object} The model represented in the JSON format for jsLPSolver. + * @see {@link https://www.npmjs.com/package/jsLPSolver} + */ + toJSLPSolverFormat(options) { + return toJSLPSolverFormat(this, options); + } + + /** + * Reads and applies the solution from the jsLPSolver solver to the model's variables and constraints. + * @param {Object} solution - The solution object returned by the jsLPSolver solver. + * @see {@link https://www.npmjs.com/package/jsLPSolver} + */ + readJSLPSolverSolution(solution) { + readJSLPSolverSolution(this, solution); + } + /** * Solves the model using the provided solver. HiGHS.js or glpk.js can be used. * The solution can be accessed from the variables' `value` properties and the constraints' `primal` and `dual` properties. @@ -293,10 +317,23 @@ export class Model { * @param {Object} [options={}] - Options to pass to the solver's solve method (refer to their respective documentation: https://ergo-code.github.io/HiGHS/dev/options/definitions/, https://www.npmjs.com/package/glpk.js). */ async solve(solver, options = {}) { - if (Object.hasOwn(solver, 'GLP_OPT')) { + // clear previous solution + this.solution = null; + this.variables.forEach(variable => variable.value = null); + this.constraints.forEach(constraint => { + constraint.primal = null; + constraint.dual = null; + }); + this.ObjVal = null; + + // run solver + if (Object.hasOwn(solver, 'branchAndCut') && Object.hasOwn(solver, 'lastSolvedModel')) { // jsLPSolver + this.solution = solver.Solve(this.toJSLPSolverFormat(options)); + this.readJSLPSolverSolution(this.solution); + } else if (Object.hasOwn(solver, 'GLP_OPT')) { // glpk.js this.solution = await solver.solve(this.toGLPKFormat(), options); this.readGLPKSolution(this.solution); - } else if (Object.hasOwn(solver, '_Highs_run')) { + } else if (Object.hasOwn(solver, '_Highs_run')) { // highs-js this.solution = solver.solve(this.toLPFormat(), options); this.readHighsSolution(this.solution); } diff --git a/test/test.js b/test/test.js index fd4700d..71f82d5 100644 --- a/test/test.js +++ b/test/test.js @@ -2,23 +2,25 @@ async function test1() { const LPModel = require('../dist/lp-model.js'); const m = new LPModel.Model(); const x = m.addVar({ lb: 0, vtype: "BINARY" }); + // const x = m.addVar({ lb: 0 }); const y = m.addVar({ lb: 0, name: "y" }); m.setObjective([[4, x], [5, y]], "MAXIMIZE"); m.addConstr([x, [2, y], 3], "<=", 8); m.addConstr([[3, x], [4, y]], ">=", [12, [-1, x]]); const highs = await require("highs")(); - // m.solve(highs); - // console.log(x.value, y.value); - // should print 1, 2 - // assert it as a test - + m.solve(highs); + console.log(x.value, y.value); const glpk = require("glpk.js")(); await m.solve(glpk); console.log(x.value, y.value); + + const jsLPSolver = require("javascript-lp-solver"); + m.solve(jsLPSolver); + console.log(x.value, y.value); } -test1(); + async function testInfeasible() { const LPModel = require('../dist/lp-model.js'); @@ -34,5 +36,51 @@ async function testInfeasible() { const glpk = require("glpk.js")(); await m.solve(glpk); console.log(m.status); + + const jsLPSolver = require("javascript-lp-solver"); + m.solve(jsLPSolver); + console.log(m.status); +} + +// async function testQuadratic() { +// const LPModel = require('../dist/lp-model.js'); +// const m = new LPModel.Model(); +// const x = m.addVar({ name: "x" }); +// const y = m.addVar({ name: "y" }); +// m.setObjective([[1, x, x]], "MINIMIZE"); +// m.addConstr([x, y], ">=", 1); + +// console.log(m.toLPFormat()); + +// const highs = await require("highs")(); +// m.solve(highs); +// console.log(x.value, y.value); +// } +// testQuadratic(); + +async function testObjectiveConstantTerm() { + const LPModel = require('../dist/lp-model.js'); + const m = new LPModel.Model(); + const x = m.addVar(); + m.setObjective([[1, x], 2], "MAXIMIZE"); + m.addConstr([x], "<=", 3); + + const highs = await require("highs")(); + m.solve(highs); + console.log(m.ObjVal, m.ObjVal === 5); + + const glpk = require("glpk.js")(); + await m.solve(glpk); + console.log(m.ObjVal, m.ObjVal === 5); + + const jsLPSolver = require("javascript-lp-solver"); + m.solve(jsLPSolver); + console.log(m.ObjVal, m.ObjVal === 5); +} + +async function allTests() { + await test1(); + await testInfeasible(); + await testObjectiveConstantTerm(); } -testInfeasible(); \ No newline at end of file +allTests(); \ No newline at end of file