linny-r 1.6.8 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/console.js +250 -305
- package/package.json +1 -1
- package/server.js +40 -134
- package/static/index.html +64 -5
- package/static/linny-r.css +41 -0
- package/static/scripts/linny-r-ctrl.js +47 -42
- package/static/scripts/linny-r-gui-controller.js +153 -15
- package/static/scripts/linny-r-gui-monitor.js +21 -9
- package/static/scripts/linny-r-gui-paper.js +18 -18
- package/static/scripts/linny-r-gui-receiver.js +5 -5
- package/static/scripts/linny-r-milp.js +363 -188
- package/static/scripts/linny-r-model.js +43 -29
- package/static/scripts/linny-r-utils.js +20 -3
- package/static/scripts/linny-r-vm.js +162 -73
@@ -41,31 +41,169 @@ const
|
|
41
41
|
os = require('os'),
|
42
42
|
path = require('path');
|
43
43
|
|
44
|
-
// Class MILPSolver implements the connection with the solver
|
44
|
+
// Class MILPSolver implements the connection with the solver.
|
45
45
|
module.exports = class MILPSolver {
|
46
|
-
constructor(
|
47
|
-
|
48
|
-
this.solver_path =
|
49
|
-
|
50
|
-
this.
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
this.
|
59
|
-
|
46
|
+
constructor(name, workspace) {
|
47
|
+
if(name) console.log(`Preferred solver: "${name}"`);
|
48
|
+
this.solver_path = '';
|
49
|
+
this.best_solver = '';
|
50
|
+
this.default_solver = '';
|
51
|
+
this.locateInstalledSolvers(workspace);
|
52
|
+
if(!this.best_solver) {
|
53
|
+
console.log('WARNING: No compatible solver found on this machine');
|
54
|
+
return;
|
55
|
+
}
|
56
|
+
this.id = name.toLowerCase();
|
57
|
+
if(!(this.id in this.solver_list)) {
|
58
|
+
if(this.id) {
|
59
|
+
console.log('WARNING: Preferred solver was not found on this machine');
|
60
|
+
}
|
61
|
+
this.id = this.best_solver;
|
62
|
+
}
|
63
|
+
this.default_solver = this.id;
|
64
|
+
console.log('Default solver:', this.name);
|
65
|
+
}
|
66
|
+
|
67
|
+
get name() {
|
68
|
+
// Return the name of the current solver.
|
69
|
+
const s = this.solver_list[this.id];
|
70
|
+
if(s) return s.name;
|
71
|
+
return '(no solver)';
|
72
|
+
}
|
73
|
+
|
74
|
+
changeDefault(id) {
|
75
|
+
// Change default solver.
|
76
|
+
if(this.solver_list.hasOwnProperty(id)) {
|
77
|
+
this.id = id;
|
78
|
+
this.default_solver = this.id;
|
79
|
+
console.log('Default solver now is', this.name);
|
80
|
+
return true;
|
81
|
+
}
|
82
|
+
console.log(`WARNING: Unknown solver ID "${id}"`);
|
83
|
+
return false;
|
84
|
+
}
|
85
|
+
|
86
|
+
locateInstalledSolvers(workspace) {
|
87
|
+
// Create a catalogue of solvers detected on this machine, and warn
|
88
|
+
// if the preferred solver `name` is not found.
|
89
|
+
this.solver_list = {};
|
90
|
+
let sp,
|
91
|
+
match,
|
92
|
+
// Gurobi may have multiple versions installed. Therefore look
|
93
|
+
// for the highest version number.
|
94
|
+
max_vn = -1;
|
95
|
+
const
|
96
|
+
windows = os.platform().startsWith('win'),
|
97
|
+
path_list = process.env.PATH.split(path.delimiter);
|
98
|
+
// Iterate over all seprate paths in environment variable PATH.
|
99
|
+
for(let i = 0; i < path_list.length; i++) {
|
100
|
+
// Assume that path is not a solver path.
|
101
|
+
sp = '';
|
102
|
+
// Check whether it is a Gurobi path.
|
103
|
+
match = path_list[i].match(/gurobi(\d+)/i);
|
104
|
+
if(match) sp = path_list[i];
|
105
|
+
// If so, ensure that it has a higher version number.
|
106
|
+
if(sp && parseInt(match[1]) > max_vn) {
|
107
|
+
// Check whether command line version is executable.
|
108
|
+
sp = path.join(sp, 'gurobi_cl' + (windows ? '.exe' : ''));
|
109
|
+
try {
|
110
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
111
|
+
console.log('Path to Gurobi:', sp);
|
112
|
+
this.solver_list.gurobi = {name: 'Gurobi', path: sp};
|
113
|
+
max_vn = parseInt(match[1]);
|
114
|
+
} catch(err) {
|
115
|
+
console.log(err.message);
|
116
|
+
console.log(
|
117
|
+
'WARNING: Failed to access the Gurobi command line application');
|
118
|
+
}
|
119
|
+
}
|
120
|
+
if(sp) continue;
|
121
|
+
// If no Gurobi path, check whether it is a CPLEX path.
|
122
|
+
match = path_list[i].match(/[\/\\]cplex[\/\\]bin/i);
|
123
|
+
if(match) {
|
124
|
+
sp = path_list[i];
|
125
|
+
} else {
|
126
|
+
// CPLEX may create its own environment variable for its paths.
|
127
|
+
match = path_list[i].match(/%(.*cplex.*)%/i);
|
128
|
+
if(match) {
|
129
|
+
const cpl = process.env[match[1]].split(path.delimiter);
|
130
|
+
for(let i = 0; i < cpl.length && !sp; i++) {
|
131
|
+
match = cpl[i].match(/[\/\\]cplex[\/\\]bin/i);
|
132
|
+
if(match) sp = cpl[i];
|
133
|
+
}
|
134
|
+
}
|
135
|
+
}
|
136
|
+
if(sp) {
|
137
|
+
// Check whether cplex(.exe) exists in its directory.
|
138
|
+
sp = path.join(sp, 'cplex' + (windows ? '.exe' : ''));
|
139
|
+
try {
|
140
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
141
|
+
console.log('Path to CPLEX:', sp);
|
142
|
+
this.solver_list.cplex = {name: 'CPLEX', path: sp};
|
143
|
+
} catch(err) {
|
144
|
+
console.log(err.message);
|
145
|
+
console.log('WARNING: CPLEX application not found in', sp);
|
146
|
+
}
|
147
|
+
continue;
|
148
|
+
}
|
149
|
+
// If no CPLEX path, check whether it is a SCIP path.
|
150
|
+
match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
|
151
|
+
if(match) {
|
152
|
+
// Check whether scip(.exe) exists in its directory
|
153
|
+
sp = path.join(path_list[i], 'scip' + (windows ? '.exe' : ''));
|
154
|
+
try {
|
155
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
156
|
+
console.log('Path to SCIP:', sp);
|
157
|
+
this.solver_list.scip = {name: 'SCIP', path: sp};
|
158
|
+
} catch(err) {
|
159
|
+
console.log(err.message);
|
160
|
+
console.log('WARNING: SCIP application not found in', sp);
|
161
|
+
}
|
162
|
+
}
|
163
|
+
// NOTE: Order of paths is unknown, so keep iterating.
|
164
|
+
}
|
165
|
+
// For macOS, look in applications directory if not found in PATH.
|
166
|
+
if(!this.solver_list.gurobi && !windows) {
|
167
|
+
console.log('Looking for Gurobi in /usr/local/bin');
|
168
|
+
try {
|
169
|
+
// On macOS and Unix, Gurobi is in the user's local binaries.
|
170
|
+
sp = '/usr/local/bin/gurobi_cl';
|
171
|
+
fs.accessSync(sp);
|
172
|
+
this.solver_list.gurobi = {name: 'Gurobi', path: sp};
|
173
|
+
} catch(err) {
|
174
|
+
// No detection is not an error, so no action needed.
|
175
|
+
}
|
176
|
+
}
|
177
|
+
// Check if lp_solve(.exe) exists in working directory.
|
178
|
+
sp = path.join(workspace.working_directory,
|
179
|
+
'lp_solve' + (windows ? '.exe' : ''));
|
180
|
+
try {
|
181
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
182
|
+
console.log('Path to LP_solve:', sp);
|
183
|
+
this.solver_list.lp_solve = {name: 'LP_solve', path: sp};
|
184
|
+
} catch(err) {
|
185
|
+
// No error because LP_solve may not be needed.
|
186
|
+
}
|
187
|
+
this.best_solver = '';
|
188
|
+
let s = this.solver_list.gurobi;
|
189
|
+
if(s) {
|
190
|
+
s.ext = '.lp';
|
191
|
+
s.user_model = path.join(workspace.solver_output, 'usr_model.lp');
|
192
|
+
s.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
|
193
|
+
s.solution = path.join(workspace.solver_output, 'model.json');
|
194
|
+
s.log = path.join(workspace.solver_output, 'model.log');
|
195
|
+
// NOTE: Arguments 0, 1 and 2 will be updated for each solver run.
|
196
|
+
s.args = [
|
60
197
|
'timeLimit=30',
|
61
|
-
'intFeasTol=
|
198
|
+
'intFeasTol=5e-7',
|
199
|
+
'MIPGap=1e-4',
|
62
200
|
'JSONSolDetail=1',
|
63
|
-
`LogFile=${
|
64
|
-
`ResultFile=${
|
65
|
-
`ResultFile=${
|
66
|
-
`${
|
201
|
+
`LogFile=${s.log}`,
|
202
|
+
`ResultFile=${s.solution}`,
|
203
|
+
`ResultFile=${s.solver_model}`,
|
204
|
+
`${s.user_model}`
|
67
205
|
];
|
68
|
-
|
206
|
+
s.errors = {
|
69
207
|
1: 'Model loaded -- no further information',
|
70
208
|
2: 'Optimal solution found',
|
71
209
|
3: 'The model is infeasible',
|
@@ -82,47 +220,59 @@ module.exports = class MILPSolver {
|
|
82
220
|
14: 'Optimization still in progress',
|
83
221
|
15: 'User-specified objective limit has been reached'
|
84
222
|
};
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
223
|
+
this.best_solver = 'gurobi';
|
224
|
+
}
|
225
|
+
s = this.solver_list.cplex;
|
226
|
+
if(s) {
|
227
|
+
s.ext = '.lp';
|
228
|
+
s.user_model = path.join(workspace.solver_output, 'usr_model.lp');
|
229
|
+
s.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
|
230
|
+
s.solution = path.join(workspace.solver_output, 'model.sol');
|
90
231
|
// NOTE: CPLEX log file is located in the Linny-R working directory
|
91
|
-
|
232
|
+
s.log = path.join(workspace.solver_output, 'cplex.log');
|
92
233
|
// NOTE: CPLEX command line accepts space separated commands ...
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
'set timelimit
|
234
|
+
s.args = [
|
235
|
+
`read ${s.user_model}`,
|
236
|
+
`write ${s.solver_model} lp`,
|
237
|
+
'set timelimit %T%',
|
238
|
+
'set mip tolerances integrality %I%',
|
239
|
+
'set mip tolerances mipgap %M%',
|
97
240
|
'optimize',
|
98
|
-
|
241
|
+
`write ${s.solution} 0`,
|
99
242
|
'quit'
|
100
243
|
];
|
101
|
-
// ... when CPLEX is called with -c option
|
102
|
-
// enclosed in double quotes
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
244
|
+
// ... when CPLEX is called with -c option. Each command must then
|
245
|
+
// be enclosed in double quotes.
|
246
|
+
s.solve_cmd = `cplex -c "${s.args.join('" "')}"`;
|
247
|
+
// NOTE: CPLEX error message is inferred from solution file.
|
248
|
+
s.errors = {};
|
249
|
+
this.best_solver = this.best_solver || 'cplex';
|
250
|
+
}
|
251
|
+
s = this.solver_list.scip;
|
252
|
+
if(s) {
|
253
|
+
s.ext = '.lp';
|
254
|
+
s.user_model = path.join(workspace.solver_output, 'usr_model.lp');
|
255
|
+
s.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
|
256
|
+
s.solution = path.join(workspace.solver_output, 'model.sol');
|
257
|
+
s.log = path.join(workspace.solver_output, 'model.log');
|
112
258
|
// NOTE: SCIP command line accepts space separated commands ...
|
113
|
-
|
114
|
-
'read',
|
115
|
-
'write problem',
|
116
|
-
'set limit time',
|
259
|
+
s.args = [
|
260
|
+
'read', s.user_model,
|
261
|
+
'write problem', s.solver_model,
|
262
|
+
'set limit time %T%',
|
263
|
+
'set numerics feastol %I%',
|
264
|
+
// NOTE: MIP gap setting for SCIP is unclear, hence ignored.
|
117
265
|
'optimize',
|
118
|
-
'write solution',
|
266
|
+
'write solution', s.solution,
|
119
267
|
'quit'
|
120
268
|
];
|
121
|
-
// ... when SCIP is called with -c option
|
122
|
-
//
|
123
|
-
// terminal, so these must be
|
124
|
-
|
125
|
-
|
269
|
+
// ... when SCIP is called with -c option. The command string (not
|
270
|
+
// the separate commands) must then be enclosed in double quotes.
|
271
|
+
// SCIP outputs its messages to the terminal, so these must be
|
272
|
+
// caputured in a log file, hence the output redirection with > to
|
273
|
+
// the log file.
|
274
|
+
s.solve_cmd = `scip -c "${s.args.join(' ')}" >${s.log}`;
|
275
|
+
s.errors = {
|
126
276
|
1: 'User interrupt',
|
127
277
|
2: 'Node limit reached',
|
128
278
|
3: 'Total node limit reached',
|
@@ -139,27 +289,28 @@ module.exports = class MILPSolver {
|
|
139
289
|
14: 'Problem is either infeasible or unbounded',
|
140
290
|
15: 'Solver terminated by user'
|
141
291
|
};
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
this.solution = path.join('.', 'user', 'solver', 'output.txt');
|
153
|
-
this.args = [
|
154
|
-
'-timeout 300',
|
292
|
+
this.best_solver = this.best_solver || 'scip';
|
293
|
+
}
|
294
|
+
s = this.solver_list.lp_solve;
|
295
|
+
if(s) {
|
296
|
+
s.ext = '.lp';
|
297
|
+
s.user_model = path.join('user', 'solver', 'usr_model.lp');
|
298
|
+
s.solver_model = path.join('user', 'solver', 'solver_model.lp');
|
299
|
+
s.solution = path.join('.', 'user', 'solver', 'output.txt');
|
300
|
+
s.args = [
|
301
|
+
'-timeout %T%',
|
155
302
|
'-v4',
|
156
|
-
'-
|
157
|
-
'-
|
158
|
-
|
159
|
-
|
160
|
-
|
303
|
+
'-e %I%',
|
304
|
+
'-gr %M%',
|
305
|
+
'-epsel 1e-7',
|
306
|
+
`-wlp ${s.solver_model}`,
|
307
|
+
`>${s.solution}`,
|
308
|
+
s.user_model
|
161
309
|
];
|
162
|
-
|
310
|
+
// Execute file command differs across platforms.
|
311
|
+
s.solve_cmd = (windows ? 'lp_solve.exe ' : './lp_solve ') +
|
312
|
+
s.args.join(' ');
|
313
|
+
s.errors = {
|
163
314
|
'-2': 'Out of memory',
|
164
315
|
1: 'The model is sub-optimal',
|
165
316
|
2: 'The model is infeasible',
|
@@ -170,15 +321,13 @@ module.exports = class MILPSolver {
|
|
170
321
|
7: 'Solver time limit exceeded',
|
171
322
|
9: 'The model could be solved by presolve',
|
172
323
|
25: 'Accuracy error encountered'
|
173
|
-
|
174
|
-
|
175
|
-
console.log(`WARNING: Unknown solver "${this.name}"`);
|
176
|
-
this.id = '';
|
324
|
+
};
|
325
|
+
this.best_solver = this.best_solver || 'lp_solve';
|
177
326
|
}
|
178
327
|
}
|
179
|
-
|
328
|
+
|
180
329
|
solveBlock(sp) {
|
181
|
-
//
|
330
|
+
// Save model file, execute solver, and return results.
|
182
331
|
const result = {
|
183
332
|
block: sp.get('block'),
|
184
333
|
round: sp.get('round'),
|
@@ -188,96 +337,114 @@ module.exports = class MILPSolver {
|
|
188
337
|
};
|
189
338
|
// Number of columns (= decision variables) is passed to ensure
|
190
339
|
// that solution vector is complete and its values are placed in
|
191
|
-
// the correct order
|
340
|
+
// the correct order.
|
192
341
|
result.columns = parseInt(sp.get('columns')) || 0;
|
193
|
-
//
|
194
|
-
|
195
|
-
if(
|
342
|
+
// Request may specify a solver ID.
|
343
|
+
const sid = sp.get('solver');
|
344
|
+
if(sid) {
|
345
|
+
this.id = (this.solver_list[sid] ? sid : this.default_solver);
|
346
|
+
}
|
196
347
|
if(!this.id) {
|
197
348
|
result.status = -999;
|
198
349
|
result.error = 'No MILP solver';
|
199
350
|
return result;
|
351
|
+
}
|
352
|
+
const s = this.solver_list[this.id];
|
353
|
+
console.log('Solve block', result.block, result.round, 'with', s.name);
|
354
|
+
// Write the POSTed MILP model to a file.
|
355
|
+
fs.writeFileSync(s.user_model, sp.get('data').trim());
|
356
|
+
// Delete previous log file (if any).
|
357
|
+
try {
|
358
|
+
if(s.log) fs.unlinkSync(s.log);
|
359
|
+
} catch(err) {
|
360
|
+
// Ignore error.
|
361
|
+
}
|
362
|
+
// Delete previous solution file (if any).
|
363
|
+
try {
|
364
|
+
if(s.solution) fs.unlinkSync(s.solution);
|
365
|
+
} catch(err) {
|
366
|
+
// Ignore error
|
367
|
+
}
|
368
|
+
let timeout = parseInt(sp.get('timeout')),
|
369
|
+
inttol = parseFloat(sp.get('inttol')),
|
370
|
+
mipgap = parseFloat(sp.get('mipgap'));
|
371
|
+
// Default timeout per block is 30 seconds.
|
372
|
+
if(isNaN(timeout)) timeout = 30;
|
373
|
+
// Default integer feasibility tolerance is 5e-7.
|
374
|
+
if(isNaN(inttol)) {
|
375
|
+
inttol = 5e-7;
|
200
376
|
} else {
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
377
|
+
inttol = Math.max(1e-9, Math.min(0.1, inttol));
|
378
|
+
}
|
379
|
+
// Default relative MIP gap is 1e-4.
|
380
|
+
if(isNaN(mipgap)) {
|
381
|
+
mipgap = 1e-4;
|
382
|
+
} else {
|
383
|
+
mipgap = Math.max(0, Math.min(0.5, mipgap));
|
384
|
+
}
|
385
|
+
return this.runSolver(this.id, timeout, inttol, mipgap, result);
|
386
|
+
}
|
387
|
+
|
388
|
+
runSolver(id, timeout, inttol, mipgap, result) {
|
389
|
+
// Set `id` to be the active solver if it is installed, and set the
|
390
|
+
// solver parameters. NOTE: These will have been validated.
|
391
|
+
this.id = (this.solver_list[id] ? id : this.default_solver);
|
392
|
+
let spawn,
|
393
|
+
status = 0,
|
394
|
+
error = '',
|
395
|
+
s = this.solver_list[this.id];
|
396
|
+
try {
|
397
|
+
if(this.id === 'gurobi') {
|
398
|
+
// When using Gurobi, standard spawn with arguments works well.
|
399
|
+
s.args[0] = `timeLimit=${timeout}`;
|
400
|
+
s.args[1] = `intFeasTol=${inttol}`;
|
401
|
+
s.args[2] = `MIPGap=${mipgap}`;
|
402
|
+
const options = {windowsHide: true};
|
403
|
+
spawn = child_process.spawnSync(s.path, s.args, options);
|
404
|
+
} else {
|
405
|
+
// CPLEX, SCIP and LP_solve will not work when the arguments are
|
406
|
+
// passed as an array. Therefore they are executed with a single
|
407
|
+
// command string that includes all arguments.
|
408
|
+
// Spawn options must be set such that (1) the command is executed
|
409
|
+
// within an OS shell script, (2) output is ignored (warnings should
|
410
|
+
// not also appear on the console, and (3) Windows does not open
|
411
|
+
// a visible sub-process shell window.
|
412
|
+
const
|
413
|
+
cmd = s.solve_cmd.replace('%T%', timeout)
|
414
|
+
.replace('%I%', inttol).replace('%M%', mipgap),
|
415
|
+
options = {shell: true, stdio: 'ignore', windowsHide: true};
|
416
|
+
if(this.id === 'cplex') {
|
417
|
+
// NOTE: CPLEX must run in user directory.
|
418
|
+
options.cwd = 'user/solver';
|
419
|
+
// Delete previous solver model file (if any).
|
243
420
|
try {
|
244
|
-
if(
|
421
|
+
if(s.solver_model) fs.unlinkSync(s.solver_model);
|
245
422
|
} catch(err) {
|
246
|
-
// Ignore error
|
423
|
+
// Ignore error when file not found.
|
247
424
|
}
|
248
|
-
// Spawn using the LP_solve approach
|
249
|
-
const
|
250
|
-
cmd = this.solve_cmd.replace(/timelimit \d+/, `timelimit ${timeout}`),
|
251
|
-
options = {shell: true, cwd: 'user/solver', stdio: 'ignore', windowsHide: true};
|
252
|
-
spawn = child_process.spawnSync(cmd, options);
|
253
|
-
} else if(this.id === 'scip') {
|
254
|
-
// When using SCIP, take the LP_solve approach
|
255
|
-
const
|
256
|
-
cmd = this.solve_cmd.replace(/limit time \d+/, `limit time ${timeout}`),
|
257
|
-
options = {shell: true, stdio: 'ignore', windowsHide: true};
|
258
|
-
spawn = child_process.spawnSync(cmd, options);
|
259
425
|
}
|
260
|
-
|
261
|
-
} catch(err) {
|
262
|
-
status = -13;
|
263
|
-
error = err;
|
264
|
-
}
|
265
|
-
if(status) console.log(`Process status: ${status}`);
|
266
|
-
if(status in this.errors) {
|
267
|
-
// If solver exited with known status code, report message
|
268
|
-
result.status = status;
|
269
|
-
result.error = this.errors[status];
|
270
|
-
} else if(status !== 0) {
|
271
|
-
result.status = -13;
|
272
|
-
const msg = (error ? error.message : 'Unknown error');
|
273
|
-
result.error += 'ERROR: ' + msg;
|
426
|
+
spawn = child_process.spawnSync(cmd, options);
|
274
427
|
}
|
275
|
-
|
428
|
+
status = spawn.status;
|
429
|
+
} catch(err) {
|
430
|
+
status = -13;
|
431
|
+
error = err;
|
432
|
+
}
|
433
|
+
if(status) console.log(`Process status: ${status}`);
|
434
|
+
if(status in s.errors) {
|
435
|
+
// If solver exited with known status code, report message
|
436
|
+
result.status = status;
|
437
|
+
result.error = s.errors[status];
|
438
|
+
} else if(status !== 0) {
|
439
|
+
result.status = -13;
|
440
|
+
const msg = (error ? error.message : 'Unknown error');
|
441
|
+
result.error += 'ERROR: ' + msg;
|
276
442
|
}
|
443
|
+
return this.processSolverOutput(result);
|
277
444
|
}
|
278
|
-
|
445
|
+
|
279
446
|
processSolverOutput(result) {
|
280
|
-
// Read solver output files and return solution (or error)
|
447
|
+
// Read solver output files and return solution (or error).
|
281
448
|
const
|
282
449
|
x_values = [],
|
283
450
|
x_dict = {},
|
@@ -309,9 +476,11 @@ module.exports = class MILPSolver {
|
|
309
476
|
// No return value; function operates on x_values.
|
310
477
|
};
|
311
478
|
|
479
|
+
const s = this.solver_list[this.id];
|
480
|
+
// Solver output has different formats, hence separate routines.
|
312
481
|
if(this.id === 'gurobi') {
|
313
482
|
// `messages` must be an array of strings.
|
314
|
-
result.messages = fs.readFileSync(
|
483
|
+
result.messages = fs.readFileSync(s.log, 'utf8').split(os.EOL);
|
315
484
|
if(result.status !== 0) {
|
316
485
|
// Non-zero solver exit code may indicate expired license.
|
317
486
|
result.error = 'Your Gurobi license may have expired';
|
@@ -319,13 +488,13 @@ module.exports = class MILPSolver {
|
|
319
488
|
try {
|
320
489
|
// Read JSON string from solution file.
|
321
490
|
const
|
322
|
-
json = fs.readFileSync(
|
491
|
+
json = fs.readFileSync(s.solution, 'utf8').trim(),
|
323
492
|
sol = JSON.parse(json);
|
324
|
-
|
493
|
+
result.seconds = sol.SolutionInfo.Runtime;
|
325
494
|
// NOTE: Status = 2 indicates success!
|
326
495
|
if(sol.SolutionInfo.Status !== 2) {
|
327
496
|
result.status = sol.SolutionInfo.Status;
|
328
|
-
result.error =
|
497
|
+
result.error = s.errors[result.status];
|
329
498
|
if(!result.error) result.error = 'Unknown solver error';
|
330
499
|
console.log(`Solver status: ${result.status} - ${result.error}`);
|
331
500
|
}
|
@@ -350,9 +519,12 @@ module.exports = class MILPSolver {
|
|
350
519
|
} else if(this.id === 'cplex') {
|
351
520
|
result.seconds = 0;
|
352
521
|
const
|
353
|
-
msg = fs.readFileSync(
|
354
|
-
no_license = (msg.indexOf('No license found') >= 0)
|
355
|
-
|
522
|
+
msg = fs.readFileSync(s.log, 'utf8'),
|
523
|
+
no_license = (msg.indexOf('No license found') >= 0),
|
524
|
+
// NOTE: Solver reports time with 1/100 secs precision.
|
525
|
+
mst = msg.match(/Solution time \=\s+(\d+\.\d+) sec/);
|
526
|
+
if(mst && mst.length > 1) result.seconds = parseFloat(mst[1]);
|
527
|
+
// `messages` must be an array of strings.
|
356
528
|
result.messages = msg.split(os.EOL);
|
357
529
|
let solved = false,
|
358
530
|
output = [];
|
@@ -360,12 +532,12 @@ module.exports = class MILPSolver {
|
|
360
532
|
result.error = 'Too many variables for unlicensed CPLEX solver';
|
361
533
|
result.status = -13;
|
362
534
|
} else if(result.status !== 0) {
|
363
|
-
// Non-zero solver exit code indicates serious trouble
|
535
|
+
// Non-zero solver exit code indicates serious trouble.
|
364
536
|
result.error = 'CPLEX solver terminated with error';
|
365
537
|
result.status = -13;
|
366
538
|
} else {
|
367
539
|
try {
|
368
|
-
output = fs.readFileSync(
|
540
|
+
output = fs.readFileSync(s.solution, 'utf8').trim();
|
369
541
|
if(output.indexOf('CPLEXSolution') >= 0) {
|
370
542
|
solved = true;
|
371
543
|
output = output.split(os.EOL);
|
@@ -376,16 +548,16 @@ module.exports = class MILPSolver {
|
|
376
548
|
}
|
377
549
|
if(solved) {
|
378
550
|
// CPLEX saves solution as XML, but for now just extract the
|
379
|
-
// status and then the variables
|
551
|
+
// status and then the variables.
|
380
552
|
let i = 0;
|
381
553
|
while(i < output.length) {
|
382
|
-
const
|
383
|
-
if(
|
384
|
-
result.obj =
|
385
|
-
} else if(
|
386
|
-
result.status =
|
387
|
-
} else if(
|
388
|
-
result.error =
|
554
|
+
const o = output[i].split('"');
|
555
|
+
if(o[0].indexOf('objectiveValue') >= 0) {
|
556
|
+
result.obj = o[1];
|
557
|
+
} else if(o[0].indexOf('solutionStatusValue') >= 0) {
|
558
|
+
result.status = o[1];
|
559
|
+
} else if(o[0].indexOf('solutionStatusString') >= 0) {
|
560
|
+
result.error = o[1];
|
389
561
|
break;
|
390
562
|
}
|
391
563
|
i++;
|
@@ -394,34 +566,34 @@ module.exports = class MILPSolver {
|
|
394
566
|
result.status = 0;
|
395
567
|
result.error = '';
|
396
568
|
}
|
397
|
-
// Fill dictionary with variable name: value entries
|
569
|
+
// Fill dictionary with variable name: value entries.
|
398
570
|
while(i < output.length) {
|
399
571
|
const m = output[i].match(/^.*name="(X[^"]+)".*value="([^"]+)"/);
|
400
572
|
if(m !== null) x_dict[m[1]] = parseFloat(m[2]);
|
401
573
|
i++;
|
402
574
|
}
|
403
|
-
// Fill the solution vector, adding 0 for missing columns
|
575
|
+
// Fill the solution vector, adding 0 for missing columns.
|
404
576
|
getValuesFromDict();
|
405
577
|
} else {
|
406
578
|
console.log('No solution found');
|
407
579
|
}
|
408
580
|
} else if(this.id === 'scip') {
|
409
581
|
result.seconds = 0;
|
410
|
-
// `messages` must be an array of strings
|
411
|
-
result.messages = fs.readFileSync(
|
582
|
+
// `messages` must be an array of strings.
|
583
|
+
result.messages = fs.readFileSync(s.log, 'utf8').split(os.EOL);
|
412
584
|
let solved = false,
|
413
585
|
output = [];
|
414
586
|
if(result.status !== 0) {
|
415
|
-
// Non-zero solver exit code indicates serious trouble
|
587
|
+
// Non-zero solver exit code indicates serious trouble.
|
416
588
|
result.error = 'SCIP solver terminated with error';
|
417
589
|
} else {
|
418
590
|
try {
|
419
591
|
output = fs.readFileSync(
|
420
|
-
|
592
|
+
s.solution, 'utf8').trim().split(os.EOL);
|
421
593
|
} catch(err) {
|
422
594
|
console.log('No SCIP solution file');
|
423
595
|
}
|
424
|
-
// Look in messages for solver status and solving time
|
596
|
+
// Look in messages for solver status and solving time.
|
425
597
|
for(let i = 0; i < result.messages.length; i++) {
|
426
598
|
const m = result.messages[i];
|
427
599
|
if(m.startsWith('SCIP Status')) {
|
@@ -440,33 +612,34 @@ module.exports = class MILPSolver {
|
|
440
612
|
result.status = 6;
|
441
613
|
}
|
442
614
|
}
|
443
|
-
if(result.status)
|
615
|
+
if(result.status) {
|
616
|
+
result.error = this.solver_list.scip.errors[result.status];
|
617
|
+
}
|
444
618
|
} else if (m.startsWith('Solving Time')) {
|
445
619
|
result.seconds = parseFloat(m.split(':')[1]);
|
446
620
|
}
|
447
621
|
}
|
448
622
|
}
|
449
623
|
if(solved) {
|
450
|
-
// Look for line with first variable
|
624
|
+
// Look for line with first variable.
|
451
625
|
let i = 0;
|
452
626
|
while(i < output.length && !output[i].startsWith('X')) i++;
|
453
|
-
// Fill dictionary with variable name: value entries
|
627
|
+
// Fill dictionary with variable name: value entries .
|
454
628
|
while(i < output.length) {
|
455
629
|
const v = output[i].split(/\s+/);
|
456
630
|
x_dict[v[0]] = parseFloat(v[1]);
|
457
631
|
i++;
|
458
632
|
}
|
459
|
-
// Fill the solution vector, adding 0 for missing columns
|
633
|
+
// Fill the solution vector, adding 0 for missing columns.
|
460
634
|
getValuesFromDict();
|
461
635
|
} else {
|
462
636
|
console.log('No solution found');
|
463
637
|
}
|
464
638
|
} else if(this.id === 'lp_solve') {
|
465
|
-
// Read solver messages from file
|
466
|
-
// NOTE: Linny-R client expects a list of strings
|
639
|
+
// Read solver messages from file.
|
640
|
+
// NOTE: Linny-R client expects a list of strings.
|
467
641
|
const
|
468
|
-
output = fs.readFileSync(
|
469
|
-
this.solution, 'utf8').trim().split(os.EOL),
|
642
|
+
output = fs.readFileSync(s.solution, 'utf8').trim().split(os.EOL),
|
470
643
|
msgs = [];
|
471
644
|
result.seconds = 0;
|
472
645
|
let i = 0,
|
@@ -494,6 +667,7 @@ module.exports = class MILPSolver {
|
|
494
667
|
console.log('No solution found');
|
495
668
|
}
|
496
669
|
}
|
670
|
+
|
497
671
|
// Add data and model to the results dict
|
498
672
|
result.data = {
|
499
673
|
block: result.block,
|
@@ -502,11 +676,12 @@ module.exports = class MILPSolver {
|
|
502
676
|
x: x_values
|
503
677
|
};
|
504
678
|
try {
|
505
|
-
result.model = fs.readFileSync(
|
679
|
+
result.model = fs.readFileSync(s.solver_model, 'utf8');
|
506
680
|
} catch(err) {
|
507
|
-
console.log(err);
|
681
|
+
console.log(err.toString());
|
508
682
|
result.model = 'ERROR reading solver model file: ' + err;
|
509
683
|
}
|
684
|
+
if(result.error) console.log('Solver error:', result.error);
|
510
685
|
return result;
|
511
686
|
}
|
512
687
|
|