nodesolve
Version:
Use [LP Solve](http://lpsolve.sourceforge.net) from NodeJs. This module uses LP Solve 5.5.2
819 lines (717 loc) • 22.2 kB
JavaScript
'use strict'
var lpsolve = require('bindings')('lpsolve')
var _ = require('lodash')
/**
* Creates a new NodeSolve problem
* @constructor
*/
function NodeSolve () {
this._name = 'nodesolve'
this._breakAtFirst = false
this._verbosity = NodeSolve.VERBOSITY.NORMAL
this._timeout = 0
this._nRows = 0
this._rows = []
this._constraints = []
this._nCols = 0
this._columns = []
this._objective = []
this._maxim = false
this._variables = []
this._lpsolve = null
this._status = null
this._solveVariables = null
}
NodeSolve.VERBOSITY = {
NEUTRAL: lpsolve.VERBOSITY.NEUTRAL,
CRITICAL: lpsolve.VERBOSITY.CRITICAL,
SEVERE: lpsolve.VERBOSITY.SEVERE,
IMPORTANT: lpsolve.VERBOSITY.IMPORTANT,
NORMAL: lpsolve.VERBOSITY.NORMAL,
DETAILED: lpsolve.VERBOSITY.DETAILED,
FULL: lpsolve.VERBOSITY.FULL
}
NodeSolve.CONSTRAINT_TYPE = {
LE: lpsolve.CONSTRAINT_TYPE.LE,
GE: lpsolve.CONSTRAINT_TYPE.GE,
EQ: lpsolve.CONSTRAINT_TYPE.EQ
}
NodeSolve.STATUS = {
NOMEMORY: lpsolve.STATUS.NOMEMORY,
OPTIMAL: lpsolve.STATUS.OPTIMAL,
SUBOPTIMAL: lpsolve.STATUS.SUBOPTIMAL,
INFEASIBLE: lpsolve.STATUS.INFEASIBLE,
UNBOUNDED: lpsolve.STATUS.UNBOUNDED,
DEGENERATE: lpsolve.STATUS.DEGENERATE,
NUMFAILURE: lpsolve.STATUS.NUMFAILURE,
USERABORT: lpsolve.STATUS.USERABORT,
TIMEOUT: lpsolve.STATUS.TIMEOUT,
PRESOLVED: lpsolve.STATUS.PRESOLVED,
PROCFAIL: lpsolve.STATUS.PROCFAIL,
PROCBREAK: lpsolve.STATUS.PROCBREAK,
FEASFOUND: lpsolve.STATUS.FEASFOUND,
NOFEASFOUND: lpsolve.STATUS.NOFEASFOUND
}
/**
* @example
* problem.name() // returns 'nodesolve'
*
* @example
* problem.name('myproblem') // Set the problem name to 'myproblem'
*
* @param {String|undefined} name
* @returns {String} name of the problem
*/
NodeSolve.prototype.name = function (name) {
if (name) {
this._name = name
}
return this._name
}
/**
* @example
* problem.breakAtFirst() // returns false
*
* @example
* problem.breakAtFirst(true) // Set breakAtFirst to true
*
* @param {Boolean|undefined} breakAtFirst
* @returns {Boolean} value of breakAtFirst option
*/
NodeSolve.prototype.breakAtFirst = function (breakAtFirst) {
if (breakAtFirst !== undefined) {
this._breakAtFirst = breakAtFirst
}
return this._breakAtFirst
}
/**
* @example
* problem.verbose() // returns NodeSolve.VERBOSITY.NORMAL
*
* @example
* problem.verbose(true) // Set breakAtFirst to true
*
* @param {Boolean|undefined} verbosity
* @returns {Boolean} value of verbosity option
*/
NodeSolve.prototype.verbose = function (verbosity) {
if (verbosity !== undefined) {
switch (verbosity) {
case NodeSolve.VERBOSITY.NEUTRAL:
case NodeSolve.VERBOSITY.CRITICAL:
case NodeSolve.VERBOSITY.SEVERE:
case NodeSolve.VERBOSITY.IMPORTANT:
case NodeSolve.VERBOSITY.NORMAL:
case NodeSolve.VERBOSITY.DETAILED:
case NodeSolve.VERBOSITY.FULL:
this._verbosity = verbosity
break
default:
throw new Error('Invalid value')
}
}
return this._verbosity
}
/**
* @example
* problem.timeout() // returns false
*
* @example
* problem.timeout(5) // Set timeout to 5 seconds
*
* @param {Number|undefined} timeout
* @returns {Number} value of timeout option
*/
NodeSolve.prototype.timeout = function (timeout) {
if (timeout !== undefined) {
if (!_.isNumber(timeout)) {
throw new Error('Argument must be a Number')
}
this._timeout = timeout
}
return this._timeout
}
/**
* @example
* problem.rows() // return 0
*
* @returns {Number} Number of rows
*/
NodeSolve.prototype.rows = function () {
return this._nRows
}
/**
* @example
* problem.columns() // return 0
*
* @returns {Number} Number of columns
*/
NodeSolve.prototype.columns = function () {
return this._nCols
}
/**
* @example
* problem.objective() // return problem objective function
*
* @example
* problem.objective(0, 1) // Set the problem objective function column 0 to value 1
*
* @example
* problem.objective([0, 1]) // Set the problem objective function to [0, 1]
*
* @example
* problem.objective([[0, 1], [4, 5]]) // Set the problem objective function columns 0 and 1 to values 4 and 5
*
* @example
* problem.objective('0 1') // Set the problem objective columns to [0, 1]
*
* @param {...*|undefined} args
* @returns {Array} objective function
*/
NodeSolve.prototype.objective = function () {
if (arguments.length > 0) {
if (_.isString(arguments[0])) {
var objective = arguments[0].split(' ')
objective = objective.map(function (value) {
return Number(value)
})
this._objective = objective
} else if (_.isArray(arguments[0])) {
if (_.isArray(arguments[0][0])) {
for (var i = 0; i < arguments[0][0].length; i++) {
this._objective[arguments[0][0][i]] = arguments[0][1][i]
}
} else {
this._objective = arguments[0]
}
} else if (arguments.length > 1 && _.isNumber(arguments[0]) && _.isNumber(arguments[1])) {
this._objective[arguments[0]] = arguments[1]
} else {
throw new Error('Invalid arguments')
}
}
return this._objective
}
/**
* @example
* problem.maxim() // return false
*
* @example
* problem.maxim(true) // Set maxim to true
*
* @param {Boolean|undefined} args
* @returns {Boolean} value of maxim option
*/
NodeSolve.prototype.maxim = function (maxim) {
if (maxim !== undefined) {
this._maxim = maxim
}
return this._maxim
}
function variableType (type, args) {
if (args.length === 0) {
return this._variables.map(function (v) {
return v.type === type
})
}
if (args.length >= 2) {
if (args[0] > this._nCols - 1) {
throw new Error('Error setting ' + type === 'b' ? 'binary' : 'int' + ' variable for variable ' + args[0] + '. There are only ' + (this._nCols) + ' variable(s)')
}
if (!this._variables[args[0]]) {
this._variables[args[0]] = {type: ''}
}
this._variables[args[0]].type = args[1] ? type : this._variables[args[0]].type === type ? '' : this._variables[args[0]] === type
}
return this._variables[args[0]].type === type
}
/**
* @example
* problem.binary() // return problem binaries columns
*
* @example
* problem.binary(0) // Return if the problem column 0 is a binary variable
*
* @example
* problem.binary(0, true) // Set the problem column 0 as a binary variable
*
* @param {Number|Boolean|undefined} args
* @returns {Boolean|Array} binaries columns
*/
NodeSolve.prototype.binary = function () {
return variableType.call(this, 'b', arguments)
}
/**
* @example
* problem.intVar() // return problem integer variables
*
* @example
* problem.intVar(0) // Return if the problem column 0 is an intenger variable
*
* @example
* problem.intVar(0, true) // Set the problem column 0 as an intenger variable
*
* @param {Number|Boolean|undefined} args
* @returns {Boolean|Array} binaries columns
*/
NodeSolve.prototype.intVar = function () {
return variableType.call(this, 'i', arguments)
}
/**
* @example
* problem.bounds(1, 2, 3) // set the lower bound fo the variable with index 1 to 2 and the upper bound to 3
*
* @param {Number} variable index
* @param {Number} lower bound
* @param {Number} upper bound
*/
NodeSolve.prototype.bounds = function (variable, lower, upper) {
if (!_.isNumber(variable)) {
throw new Error('First parameter must be a Number')
}
if (this._nCols <= variable) {
throw new Error('Error setting bounds for variable ' + variable + '. There are only ' + this._nCols + ' column(s)')
}
if (!_.isNumber(lower)) {
throw new Error('Second parameter must be a Number')
}
if (!_.isNumber(upper)) {
throw new Error('Third parameter must be a Number')
}
if (!this._variables[variable]) {
this._variables[variable] = {}
}
this._variables[variable].bounds = [lower, upper]
}
function bound (type, variable, value) {
if (!_.isNumber(variable)) {
throw new Error('First parameter must be a Number')
}
if (value) {
if (this._nCols <= variable) {
throw new Error('Error setting ' + type + ' bound for variable ' + variable + '. There are only ' + this._nCols + ' column(s)')
}
if (!this._variables[variable]) {
this._variables[variable] = {bounds: [0, Infinity]}
} else if (!this._variables[variable].bounds) {
this._variables[variable].bounds = [0, Infinity]
}
this._variables[variable].bounds[type === 'lower' ? 0 : 1] = value
} else {
if (this._nCols <= variable) {
throw new Error('Error getting ' + type + ' bound for variable ' + variable + '. There are only ' + this._nCols + ' column(s)')
}
return this._variables[variable] && this._variables[variable].bounds ? this._variables[variable].bounds[type === 'lower' ? 0 : 1] : Infinity
}
}
/**
* @example
* problem.upBound(1) // return the upper bound for variable with index 1
*
* @example
* problem.upBound(1, 3) // set the upper bound for variable with index 1 to 3
*
* @param {Number} variable index
* @param [Number] value upper bound
* @returns {Number} variable upper bound
*/
NodeSolve.prototype.upBound = function (variable, value) {
return bound.call(this, 'upper', variable, value)
}
/**
* @example
* problem.lowBound(1) // return the lower bound for variable with index 1
*
* @example
* problem.lowBound(1, 3) // set the lower bound for variable with index 1 to 3
*
* @param {Number} variable index
* @param [Number] value lower bound
* @returns {Number} variable lower bound
*/
NodeSolve.prototype.lowBound = function (variable, value) {
return bound.call(this, 'lower', variable, value)
}
/**
* @example
* problem.resize(1, 3) // set rows length to 1 and rows length to 0
*
* @param {Number} rows
* @param {Number} columns
*/
NodeSolve.prototype.resize = function (rows, columns) {
/*
var resize = function (dimension, size) {
if (dimension.length < size) {
var z = Array(size - dimension.length)
z = _.fill(z, 0)
dimension = _.concat(dimension, z)
} else {
dimension = _.slice(dimension, 0, size)
}
return dimension
}
this._rows = resize(this._rows, rows)
this._columns = resize(this._columns, columns)
*/
this._nRows = rows
this._nCols = columns
}
/**
* @example
* problem.constraint() // Return the problem constraints
*
* @example
* problem.constraint(0) // Return the constraint with index 0
*
* @example
* problem.constraint([0, 1], NodeSolve.CONSTRAINT_TYPE.GE, 5) // Add a constraint with values [0, 1] and set it to be greater than or equals 5
*
* @example
* problem.constraint([[0, 3], [4, 5]], NodeSolve.CONSTRAINT_TYPE.EQ, 2, 1) // Set the constraint 1 columns 0 and 3 to values 4 and 5 and set it to be equals 2
*
* @example
* problem.constraint('0 1', NodeSolve.CONSTRAINT_TYPE.LE, 3) // Add a constraint with values [0, 1] and set it to be less than or equals 5
*
* @param {...*|undefined} args
* @returns {Array|undefined} constraints
*/
NodeSolve.prototype.constraint = function () {
if (arguments.length > 4) {
throw new Error('Invalid number of arguments')
}
if (arguments.length === 0) {
var self = this
var constraints = _.clone(this._constraints).map(function (constraint, index) {
constraint.row = _.clone(self._rows[index])
return constraint
})
return constraints
}
if (arguments.length === 1) {
if (!_.isNumber(arguments[0])) {
throw new Error('First parameter must be a Number')
}
if (!this._constraints[arguments[0]]) {
return
}
var constraint = _.clone(this._constraints[arguments[0]])
constraint.row = _.clone(this._rows[arguments[0]])
return constraint
}
if (arguments.length > 1) {
if (arguments[1] !== NodeSolve.CONSTRAINT_TYPE.LE &&
arguments[1] !== NodeSolve.CONSTRAINT_TYPE.GE &&
arguments[1] !== NodeSolve.CONSTRAINT_TYPE.EQ) {
throw new Error('Second argument must be a NodeSolve.CONSTRAINT_TYPE value')
}
if (!_.isNumber(arguments[2])) {
throw new Error('Third parameter must be a Number')
}
if (_.isString(arguments[0])) {
var row = arguments[0].split(' ')
row = row.map(function (value) {
return Number(value)
})
if (row.length > this._nCols) {
throw new Error('Error setting column ' + this._nCols + '. There are only ' + this._nCols + ' column(s)')
}
this._rows.push(row)
this._nRows = this._rows.length > this._nRows ? this._rows.length : this._nRows
this._constraints.push({type: arguments[1], rhs: arguments[2]})
} else if (_.isArray(arguments[0])) {
if (_.isArray(arguments[0][0])) {
if (arguments.length !== 4) {
throw new Error('You must indicate the constraint target to set the values')
}
if (!_.isNumber(arguments[3])) {
throw new Error('Fourth parameter must be a Number')
}
if (arguments[3] > this._nRows - 1) {
throw new Error('Constraint ' + arguments[3] + ' is not set')
}
var max = _.max(arguments[0][0])
if (max > this._nCols) {
throw new Error('Error setting column ' + max + '. There are only ' + this._nCols + ' column(s)')
}
var arr
if (!this._rows[arguments[3]]) {
arr = Array(max)
arr = _.fill(arr, 0)
} else {
arr = _.clone(this._rows[arguments[3]])
}
for (var i = 0; i < arguments[0][0].length; i++) {
arr[arguments[0][0][i]] = arguments[0][1][i]
}
this._rows[arguments[3]] = arr
this._constraints[arguments[3]] = {type: arguments[1], rhs: arguments[2]}
} else {
if (arguments[0].length > this._nCols) {
throw new Error('Error setting column ' + this._nCols + '. There are only ' + this._nCols + ' column(s)')
}
this._rows.push(arguments[0])
this._nRows = this._rows.length > this._nRows ? this._rows.length : this._nRows
this._constraints.push({type: arguments[1], rhs: arguments[2]})
}
} else {
throw new Error('Invalid arguments')
}
}
}
/**
* @example
* problem.rh(0) // Return the rhs from the constraint 0
*
* @example
* problem.rh(0, 3) // Set to 3 the value of the rhs from the constraint with index 0
*
* @param {...Number|Number} args
* @returns {Number|undefined} rhs
*/
NodeSolve.prototype.rh = function () {
if (arguments.length < 1 || arguments.length > 2) {
throw new Error('Invalid number of arguments')
}
if (!_.isNumber(arguments[0])) {
throw new Error('First parameter must be a Number')
}
if (arguments.length === 1) {
if (!this._constraints[arguments[0]]) {
return
}
return this._constraints[arguments[0]].rhs
} else {
if (!_.isNumber(arguments[1])) {
throw new Error('Second parameter must be a Number')
}
if (arguments[0] > this._nRows - 1) {
throw new Error('Error setting rhs from row ' + arguments[0] + '. There are only ' + this._nRows + ' row(s)')
}
this._constraints[arguments[0]].rhs = arguments[1]
}
}
/**
* @example
* problem.rhVec() // Returns the rhs vector from the constraints
*
* @example
* problem.rhVec([0, 1]) // Set to [0, 1] the rhs vector from the constraints
*
* @example
* problem.rh('0 3') // Set to [0, 3] the rhs vector from the constraints
*
* @param {Array|String|undefined} args
* @returns {Array|undefined} rhs
*/
NodeSolve.prototype.rhVec = function () {
if (arguments.length > 1) {
throw new Error('Invalid number of arguments')
}
if (arguments.length === 0) {
return this._constraints.map(function (constraint) {
return constraint.rhs
})
}
var arr
if (_.isArray(arguments[0])) {
arr = arguments[0]
} else if (_.isString(arguments[0])) {
arr = arguments[0].split(' ').map(function (value) {
return Number(value)
})
} else {
throw new Error('Invalid arguments')
}
if (arr.length !== this._nRows) {
throw new Error('Error setting rhs vector with size of ' + arr.length + '. There are ' + this._nRows + ' row(s)')
}
this._constraints.map(function (constraint, index) {
constraint.rhs = arr[index]
})
}
/**
* @example
* problem.rhRange(0) // Return the rhs range from the constraint 0
*
* @example
* problem.rhRange(0, 3) // Set the rhs range from the constraint with index 0 to a 3 delta value
*
* @param {...Number|Number} args
* @returns {Number|undefined} rhs
*/
NodeSolve.prototype.rhRange = function () {
if (arguments.length < 1 || arguments.length > 2) {
throw new Error('Invalid number of arguments')
}
if (!_.isNumber(arguments[0])) {
throw new Error('First parameter must be a Number')
}
if (arguments.length === 1) {
if (!this._constraints[arguments[0]]) {
return
}
return this._constraints[arguments[0]].range
} else {
if (!_.isNumber(arguments[1])) {
throw new Error('Second parameter must be a Number')
}
if (arguments[0] > this._nRows - 1) {
throw new Error('Error setting rhs range from row ' + arguments[0] + '. There are only ' + this._nRows + ' row(s)')
}
this._constraints[arguments[0]].range = arguments[1]
}
}
/**
* Reads a LP file and returns a NodeSolve instance
*
* @example
* NodeSolve.readLP('file.lp') // Returns a NodeSolve instance with NORMAL verbosity level and name 'problem' from file 'file.lp'
*
* @example
* NodeSolve.readLP('file.lp', 'myname') // Returns a NodeSolve instance with NORMAL verbosity level and name 'myname' from file 'file.lp'
*
* @example
* NodeSolve.readLP('file.lp', NodeSolve.VERBOSITY.CRITICAL) // Returns a NodeSolve instance with CRITICAL verbosity level and name 'problem' from file 'file.lp'
*
* @example
* NodeSolve.readLP('file.lp', NodeSolve.VERBOSITY.CRITICAL, 'myname') // Returns a NodeSolve instance with CRITICAL verbosity level and name 'myname' from file 'file.lp'
*
* @param {String} path
* @param [NodeSolve.VERBOSITY] verbosity
* @param [String] name
* @returns {NodeSolve}
*/
NodeSolve.readLP = function (path, verbosity, name) {
if (!_.isString(path) || !path) {
throw new Error('First parameter must be a String')
}
if (_.isString(verbosity)) {
name = verbosity
verbosity = NodeSolve.VERBOSITY.NORMAL
}
if (!name) {
name = 'problem'
}
var nodesolve = new NodeSolve()
nodesolve._lpsolve = lpsolve.readLP(path, verbosity, name)
nodesolve._name = nodesolve._lpsolve.name()
nodesolve._nRows = nodesolve._lpsolve.rows()
nodesolve._nCols = nodesolve._lpsolve.columns()
nodesolve._maxim = nodesolve._lpsolve.maxim()
// TODO Generate constraints, rows, cols, etc...
return nodesolve
}
function generateLP () {
this._lpsolve = lpsolve.makeLP(0, this._nCols)
this._lpsolve.resize(this._nRows, this._nCols)
this._lpsolve.name(this._name)
this._lpsolve.breakAtFirst(this._breakAtFirst)
this._lpsolve.verbose(this._verbosity)
this._lpsolve.timeout(this._timeout)
this._lpsolve.addRowMode(true)
if (this._objective.length) {
var obj = _.concat([ 0 ], _.clone(this._objective))
this._lpsolve.objFn(obj)
}
this._lpsolve.maxim(this._maxim)
var self = this
this._variables.forEach(function (variable, index) {
if (variable.type === 'b') {
self._lpsolve.binary(index + 1, true)
} else if (variable.type === 'i') {
self._lpsolve.intVar(index + 1, true)
}
})
this._constraints.forEach(function (constraint, index) {
var row = _.clone(self._rows[ index ])
var indexes = []
for (var i = 0; i < self._nCols; i++) {
indexes.push(i + 1)
}
self._lpsolve.constraintEx(self._nCols, row, indexes, constraint.type, constraint.rhs)
})
this._variables.forEach(function (variable, index) {
if (variable.bounds) {
if (variable.bounds[ 0 ] !== 0 && variable.bounds[ 1 ] !== Infinity) {
self._lpsolve.bounds(index + 1, variable.bounds[ 0 ], variable.bounds[ 1 ])
} else if (variable.bounds[ 0 ] !== 0) {
self._lpsolve.lowBound(index + 1, variable.bounds[ 0 ])
} else {
self._lpsolve.upperBound(index + 1, variable.bounds[ 1 ])
}
}
})
this._lpsolve.addRowMode(false)
}
/**
* Writes a LP file based on the NodeSolve properties
*
* @example
* problem.writeLP('file.lp') // Writes a LP file to the file 'file.lp'
*
* @param {String} path
*/
NodeSolve.prototype.writeLP = function (path) {
if (!_.isString(path) || !path) {
throw new Error('First parameter must be a String')
}
if (this._lpsolve == null) {
generateLP.call(this)
}
this._lpsolve.writeLP(path)
}
/**
* Solve the problem synchronously o asynchronously
*
* @example
* problem.solve() // solve the problem synchronously and returns the status
*
* @example
* problem.solve(callback) // solve the problem asynchronously and call the callback function with arguments error and status
*
* @params [callback]
* @returns {NodeSolve.STATUS}
*/
NodeSolve.prototype.solve = function (callback) {
if (this._lpsolve == null) {
generateLP.call(this)
}
if (callback && _.isFunction(callback)) {
var self = this
this._lpsolve.solve(function (err, status) {
self._status = status
if (status === NodeSolve.STATUS.OPTIMAL || status === NodeSolve.STATUS.SUBOPTIMAL) {
self.variables()
self._lpsolve.delete()
}
return callback(err, status)
})
} else {
this._status = this._lpsolve.solveSync()
if (this._status === NodeSolve.STATUS.OPTIMAL || this._status === NodeSolve.STATUS.SUBOPTIMAL) {
this.variables()
this._lpsolve.delete()
}
return this._status
}
}
/**
* Returns the status of the problem or null if it's not solved
*
* @returns {NodeSolve.STATUS|null}
*/
NodeSolve.prototype.status = function () {
return this._status
}
NodeSolve.prototype.variables = function () {
if (this._status == null) {
throw Error('Problem is not solved')
}
if (this._solveVariables == null) {
this._solveVariables = []
this._lpsolve.variables(this._solveVariables)
}
return this._solveVariables
}
module.exports = NodeSolve