Skip to content

Commit

Permalink
jsLPsolver integration
Browse files Browse the repository at this point in the history
  • Loading branch information
DominikPeters committed Feb 21, 2024
1 parent cf4fc2c commit 9fa152f
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 9 deletions.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
87 changes: 87 additions & 0 deletions src/jsLPSolver-bridge.js
Original file line number Diff line number Diff line change
@@ -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
}
}
41 changes: 39 additions & 2 deletions src/model.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -286,17 +292,48 @@ 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.
* @param {Object} solver - The solver instance to use for solving the model, either HiGHS.js or glpk.js.
* @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);
}
Expand Down
62 changes: 55 additions & 7 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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();
allTests();

0 comments on commit 9fa152f

Please sign in to comment.