linny-r 1.7.3 → 1.8.0
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 +33 -22
- package/console.js +2 -2
- package/package.json +1 -1
- package/server.js +2 -2
- package/static/index.html +13 -5
- package/static/linny-r.css +21 -2
- package/static/scripts/linny-r-ctrl.js +1 -1
- package/static/scripts/linny-r-gui-constraint-editor.js +15 -2
- package/static/scripts/linny-r-gui-controller.js +45 -13
- package/static/scripts/linny-r-gui-equation-manager.js +6 -6
- package/static/scripts/linny-r-gui-expression-editor.js +6 -3
- package/static/scripts/linny-r-gui-finder.js +1 -1
- package/static/scripts/linny-r-gui-monitor.js +5 -5
- package/static/scripts/linny-r-gui-paper.js +13 -6
- package/static/scripts/linny-r-milp.js +306 -86
- package/static/scripts/linny-r-model.js +75 -22
- package/static/scripts/linny-r-vm.js +354 -127
@@ -118,7 +118,22 @@ module.exports = class MILPSolver {
|
|
118
118
|
}
|
119
119
|
}
|
120
120
|
if(sp) continue;
|
121
|
-
// If no Gurobi path, check whether it is a
|
121
|
+
// If no Gurobi path, check whether it is a MOSEK path.
|
122
|
+
match = path_list[i].match(/[\/\\]mosek.*[\/\\]bin/i);
|
123
|
+
if(match) {
|
124
|
+
// Check whether mosek(.exe) exists in its directory
|
125
|
+
sp = path.join(path_list[i], 'mosek' + (windows ? '.exe' : ''));
|
126
|
+
try {
|
127
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
128
|
+
console.log('Path to MOSEK:', sp);
|
129
|
+
this.solver_list.mosek = {name: 'MOSEK', path: sp};
|
130
|
+
} catch(err) {
|
131
|
+
console.log(err.message);
|
132
|
+
console.log('WARNING: MOSEK application not found in', sp);
|
133
|
+
}
|
134
|
+
}
|
135
|
+
if(sp) continue;
|
136
|
+
// If no MOSEK path, check whether it is a CPLEX path.
|
122
137
|
match = path_list[i].match(/[\/\\]cplex[\/\\]bin/i);
|
123
138
|
if(match) {
|
124
139
|
sp = path_list[i];
|
@@ -188,10 +203,10 @@ module.exports = class MILPSolver {
|
|
188
203
|
let s = this.solver_list.gurobi;
|
189
204
|
if(s) {
|
190
205
|
s.ext = '.lp';
|
191
|
-
s.user_model = path.join(workspace.solver_output, '
|
206
|
+
s.user_model = path.join(workspace.solver_output, 'user_model.lp');
|
192
207
|
s.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
|
193
|
-
s.solution = path.join(workspace.solver_output, '
|
194
|
-
s.log = path.join(workspace.solver_output, '
|
208
|
+
s.solution = path.join(workspace.solver_output, 'gurobi.json');
|
209
|
+
s.log = path.join(workspace.solver_output, 'gurobi.log');
|
195
210
|
// NOTE: Arguments 0, 1 and 2 will be updated for each solver run.
|
196
211
|
s.args = [
|
197
212
|
'timeLimit=30',
|
@@ -203,31 +218,92 @@ module.exports = class MILPSolver {
|
|
203
218
|
`ResultFile=${s.solver_model}`,
|
204
219
|
`${s.user_model}`
|
205
220
|
];
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
221
|
+
// Function to provide legend to status codes.
|
222
|
+
s.statusMessage = (s) => {
|
223
|
+
if(s >= 1 && s <= 15) return [
|
224
|
+
'Model loaded -- no further information',
|
225
|
+
'Optimal solution found',
|
226
|
+
'The model is infeasible',
|
227
|
+
'The model is either unbounded or infeasible',
|
228
|
+
'The model is unbounded',
|
229
|
+
'Aborted -- Optimal objective is worse than specified cut-off',
|
230
|
+
'Halted -- Iteration limit exceeded',
|
231
|
+
'Halted -- Node limit exceeded',
|
232
|
+
'Halted -- Solver time limit exceeded',
|
233
|
+
'Halted -- Solution count limit exceeded',
|
234
|
+
'Halted -- Optimization terminated by user',
|
235
|
+
'Halted -- Unrecoverable numerical difficulties',
|
236
|
+
'The model is sub-obtimal',
|
237
|
+
'Optimization still in progress',
|
238
|
+
'User-specified objective limit has been reached'
|
239
|
+
][s - 1];
|
240
|
+
// No message otherwise; if `s` is non-zero, exception is reported.
|
241
|
+
return '';
|
242
|
+
};
|
243
|
+
// For some status codes, solution may be sub-optimal, but useful.
|
244
|
+
s.usableSolution = (s) => {
|
245
|
+
return [2, 5, 7, 8, 9, 10, 13, 15].indexOf(s) >= 0;
|
222
246
|
};
|
223
247
|
this.best_solver = 'gurobi';
|
224
248
|
}
|
249
|
+
s = this.solver_list.mosek;
|
250
|
+
if(s) {
|
251
|
+
s.ext = '.lp';
|
252
|
+
s.user_model = path.join(workspace.solver_output, 'user_model.lp');
|
253
|
+
s.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
|
254
|
+
s.solution = path.join(workspace.solver_output, 'user_model.int');
|
255
|
+
s.log = path.join(workspace.solver_output, 'mosek.log');
|
256
|
+
// NOTE: MOSEK command line accepts space separated commands, but paths
|
257
|
+
// should be enclosed in quotes.
|
258
|
+
s.args = [
|
259
|
+
`-out "${s.solver_model}"`,
|
260
|
+
`-d MSK_DPAR_MIO_MAX_TIME %T%`,
|
261
|
+
`-d MSK_DPAR_MIO_TOL_ABS_RELAX_INT %I%`,
|
262
|
+
'-d MSK_DPAR_MIO_REL_GAP_CONST %M%',
|
263
|
+
`"${s.user_model}"`
|
264
|
+
];
|
265
|
+
s.solve_cmd = `mosek ${s.args.join(' ')} >${s.log}`;
|
266
|
+
// Function to provide legend to status codes.
|
267
|
+
s.statusMessage = (s) => {
|
268
|
+
if(s === 0) return '';
|
269
|
+
if(s >= 100000) {
|
270
|
+
s -= 100000;
|
271
|
+
const m = {
|
272
|
+
0: 'Maximum number of iterations exceeded',
|
273
|
+
1: 'Time limit exceeded',
|
274
|
+
2: 'Objective value outside range',
|
275
|
+
6: 'Terminated due to slow progress',
|
276
|
+
8: 'Maximum number of integer relaxations exceeded',
|
277
|
+
9: 'Maximum number of branches exceeded',
|
278
|
+
15: 'Maximum number of feasible solutions exceeded',
|
279
|
+
20: 'Maximum number of set-backs exceeded',
|
280
|
+
25: 'Terminated due to numerical problems',
|
281
|
+
30: 'Terminated due to internal error',
|
282
|
+
31: 'Terminated due to internal error'
|
283
|
+
};
|
284
|
+
return m[s] || '';
|
285
|
+
}
|
286
|
+
if(s >= 1000 && s <= 1030) {
|
287
|
+
return 'Invalid MOSEK license - see message in monitor';
|
288
|
+
}
|
289
|
+
// All other codes beyond 1000 indicate an error.
|
290
|
+
if(s > 1000) {
|
291
|
+
return 'Solver encoutered a problem - see messages in monitor';
|
292
|
+
}
|
293
|
+
return 'Solver warning(s) - see messages in monitor';
|
294
|
+
};
|
295
|
+
// For some status codes, solution may be sub-optimal, but useful.
|
296
|
+
s.usableSolution = (s) => {
|
297
|
+
return [2, 5, 7, 8, 9, 10, 13, 15].indexOf(s) >= 0;
|
298
|
+
};
|
299
|
+
this.best_solver = this.best_solver || 'mosek';
|
300
|
+
}
|
225
301
|
s = this.solver_list.cplex;
|
226
302
|
if(s) {
|
227
303
|
s.ext = '.lp';
|
228
|
-
s.user_model = path.join(workspace.solver_output, '
|
304
|
+
s.user_model = path.join(workspace.solver_output, 'user_model.lp');
|
229
305
|
s.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
|
230
|
-
s.solution = path.join(workspace.solver_output, '
|
306
|
+
s.solution = path.join(workspace.solver_output, 'cplex.sol');
|
231
307
|
// NOTE: CPLEX log file is located in the Linny-R working directory
|
232
308
|
s.log = path.join(workspace.solver_output, 'cplex.log');
|
233
309
|
// NOTE: CPLEX command line accepts space separated commands ...
|
@@ -245,16 +321,20 @@ module.exports = class MILPSolver {
|
|
245
321
|
// be enclosed in double quotes.
|
246
322
|
s.solve_cmd = `cplex -c "${s.args.join('" "')}"`;
|
247
323
|
// NOTE: CPLEX error message is inferred from solution file.
|
248
|
-
s.
|
324
|
+
s.statusMessage = () => { return ''; };
|
325
|
+
// For some status codes, solution may be sub-optimal, but useful.
|
326
|
+
s.usableSolution = (s) => {
|
327
|
+
return false; // @@@ STILL TO CHECK!
|
328
|
+
};
|
249
329
|
this.best_solver = this.best_solver || 'cplex';
|
250
330
|
}
|
251
331
|
s = this.solver_list.scip;
|
252
332
|
if(s) {
|
253
333
|
s.ext = '.lp';
|
254
|
-
s.user_model = path.join(workspace.solver_output, '
|
334
|
+
s.user_model = path.join(workspace.solver_output, 'user_model.lp');
|
255
335
|
s.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
|
256
|
-
s.solution = path.join(workspace.solver_output, '
|
257
|
-
s.log = path.join(workspace.solver_output, '
|
336
|
+
s.solution = path.join(workspace.solver_output, 'scip.sol');
|
337
|
+
s.log = path.join(workspace.solver_output, 'scip.log');
|
258
338
|
// NOTE: SCIP command line accepts space separated commands ...
|
259
339
|
s.args = [
|
260
340
|
'read', s.user_model,
|
@@ -272,31 +352,43 @@ module.exports = class MILPSolver {
|
|
272
352
|
// caputured in a log file, hence the output redirection with > to
|
273
353
|
// the log file.
|
274
354
|
s.solve_cmd = `scip -c "${s.args.join(' ')}" >${s.log}`;
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
355
|
+
// Function to provide legend to status codes.
|
356
|
+
s.statusMessage = (s) => {
|
357
|
+
if(s >= 1 && s <= 15) return [
|
358
|
+
'User interrupt',
|
359
|
+
'Node limit reached',
|
360
|
+
'Total node limit reached',
|
361
|
+
'Stalling node limit reached',
|
362
|
+
'Time limit reached',
|
363
|
+
'Memory limit reached',
|
364
|
+
'Gap limit reached',
|
365
|
+
'Solution limit reached',
|
366
|
+
'Solution improvement limit reached',
|
367
|
+
'Restart limit reached',
|
368
|
+
'Optimal solution found',
|
369
|
+
'Problem is infeasible',
|
370
|
+
'Problem is unbounded',
|
371
|
+
'Problem is either infeasible or unbounded',
|
372
|
+
'Solver terminated by user'
|
373
|
+
][s - 1];
|
374
|
+
// No message otherwise; if `s` is non-zero, exception is reported.
|
375
|
+
return '';
|
376
|
+
};
|
377
|
+
// For some status codes, solution may be sub-optimal, but useful.
|
378
|
+
s.usableSolution = (s) => {
|
379
|
+
return false; // @@@ STILL TO CHECK!
|
291
380
|
};
|
292
381
|
this.best_solver = this.best_solver || 'scip';
|
293
382
|
}
|
294
383
|
s = this.solver_list.lp_solve;
|
295
384
|
if(s) {
|
296
385
|
s.ext = '.lp';
|
297
|
-
s.user_model = path.join('user', 'solver', '
|
386
|
+
s.user_model = path.join('user', 'solver', 'user_model.lp');
|
298
387
|
s.solver_model = path.join('user', 'solver', 'solver_model.lp');
|
299
|
-
|
388
|
+
// NOTE: LP_solve outputs solver messages AND solution to console,
|
389
|
+
// hence no separate solution file.
|
390
|
+
s.solution = '';
|
391
|
+
s.log = path.join('.', 'user', 'solver', 'lp_solve.log');
|
300
392
|
s.args = [
|
301
393
|
'-timeout %T%',
|
302
394
|
'-v4',
|
@@ -304,24 +396,33 @@ module.exports = class MILPSolver {
|
|
304
396
|
'-gr %M%',
|
305
397
|
'-epsel 1e-7',
|
306
398
|
`-wlp ${s.solver_model}`,
|
307
|
-
`>${s.
|
399
|
+
`>${s.log}`,
|
308
400
|
s.user_model
|
309
401
|
];
|
310
402
|
// Execute file command differs across platforms.
|
311
403
|
s.solve_cmd = (windows ? 'lp_solve.exe ' : './lp_solve ') +
|
312
404
|
s.args.join(' ');
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
405
|
+
// Function to provide legend to status codes.
|
406
|
+
s.statusMessage = (s) => {
|
407
|
+
if(s === -2) return 'Out of memory';
|
408
|
+
if(s === 9) return 'The model could be solved by presolve';
|
409
|
+
if(s === 25) return 'Accuracy error encountered';
|
410
|
+
if(s >= 1 && s <= 7) return [
|
411
|
+
'The model is sub-optimal',
|
412
|
+
'The model is infeasible',
|
413
|
+
'The model is unbounded',
|
414
|
+
'The model is degenerative',
|
415
|
+
'Numerical failure encountered',
|
416
|
+
'Solver was stopped by user',
|
417
|
+
'Solver time limit exceeded'
|
418
|
+
][s - 1];
|
419
|
+
// No message otherwise; if `s` is non-zero, exception is reported.
|
420
|
+
return '';
|
421
|
+
};
|
422
|
+
// For some status codes, solution may be sub-optimal, but useful.
|
423
|
+
s.usableSolution = (s) => {
|
424
|
+
return [-2, 2, 6].indexOf(s) < 0;
|
425
|
+
};
|
325
426
|
this.best_solver = this.best_solver || 'lp_solve';
|
326
427
|
}
|
327
428
|
}
|
@@ -332,6 +433,7 @@ module.exports = class MILPSolver {
|
|
332
433
|
block: sp.get('block'),
|
333
434
|
round: sp.get('round'),
|
334
435
|
status: 0,
|
436
|
+
solution: true,
|
335
437
|
error: '',
|
336
438
|
messages: []
|
337
439
|
};
|
@@ -346,6 +448,7 @@ module.exports = class MILPSolver {
|
|
346
448
|
}
|
347
449
|
if(!this.id) {
|
348
450
|
result.status = -999;
|
451
|
+
result.solution = false;
|
349
452
|
result.error = 'No MILP solver';
|
350
453
|
return result;
|
351
454
|
}
|
@@ -363,7 +466,14 @@ module.exports = class MILPSolver {
|
|
363
466
|
try {
|
364
467
|
if(s.solution) fs.unlinkSync(s.solution);
|
365
468
|
} catch(err) {
|
366
|
-
//
|
469
|
+
// NOTE: MOSEK solution may also be a '.bas' file.
|
470
|
+
if(this.id === 'mosek') {
|
471
|
+
try {
|
472
|
+
fs.unlinkSync(s.solution.replace(/\.int$/, '.bas'));
|
473
|
+
} catch(err) {
|
474
|
+
// Ignore error.
|
475
|
+
}
|
476
|
+
}
|
367
477
|
}
|
368
478
|
let timeout = parseInt(sp.get('timeout')),
|
369
479
|
inttol = parseFloat(sp.get('inttol')),
|
@@ -402,8 +512,8 @@ module.exports = class MILPSolver {
|
|
402
512
|
const options = {windowsHide: true};
|
403
513
|
spawn = child_process.spawnSync(s.path, s.args, options);
|
404
514
|
} else {
|
405
|
-
// CPLEX, SCIP and LP_solve will not work when the arguments
|
406
|
-
// passed as an array. Therefore they are executed with a single
|
515
|
+
// MOSEK, CPLEX, SCIP and LP_solve will not work when the arguments
|
516
|
+
// are passed as an array. Therefore they are executed with a single
|
407
517
|
// command string that includes all arguments.
|
408
518
|
// Spawn options must be set such that (1) the command is executed
|
409
519
|
// within an OS shell script, (2) output is ignored (warnings should
|
@@ -431,13 +541,16 @@ module.exports = class MILPSolver {
|
|
431
541
|
error = err;
|
432
542
|
}
|
433
543
|
if(status) console.log(`Process status: ${status}`);
|
434
|
-
|
435
|
-
|
544
|
+
let msg = s.statusMessage(status);
|
545
|
+
if(msg) {
|
546
|
+
// If solver exited with known status code, report message.
|
436
547
|
result.status = status;
|
437
|
-
result.
|
548
|
+
result.solution = s.usableSolution(status);
|
549
|
+
result.error = msg;
|
438
550
|
} else if(status !== 0) {
|
439
551
|
result.status = -13;
|
440
|
-
|
552
|
+
result.solution = false;
|
553
|
+
msg = (error ? error.message : 'Unknown error');
|
441
554
|
result.error += 'ERROR: ' + msg;
|
442
555
|
}
|
443
556
|
return this.processSolverOutput(result);
|
@@ -449,9 +562,9 @@ module.exports = class MILPSolver {
|
|
449
562
|
x_values = [],
|
450
563
|
x_dict = {},
|
451
564
|
getValuesFromDict = () => {
|
452
|
-
//
|
565
|
+
// Return a result vector for as many real numbers (as strings!)
|
453
566
|
// as there are columns (0 if not reported by the solver).
|
454
|
-
// First sort on variable name
|
567
|
+
// First sort on variable name (assuming format Xn+).
|
455
568
|
const vlist = Object.keys(x_dict).sort();
|
456
569
|
// Start with column 1.
|
457
570
|
let col = 1;
|
@@ -477,29 +590,43 @@ module.exports = class MILPSolver {
|
|
477
590
|
};
|
478
591
|
|
479
592
|
const s = this.solver_list[this.id];
|
593
|
+
let log = '';
|
594
|
+
try {
|
595
|
+
log = fs.readFileSync(s.log, 'utf8');
|
596
|
+
} catch(err) {
|
597
|
+
console.log(`Failed to read solver log file ${s.log}`);
|
598
|
+
}
|
480
599
|
// Solver output has different formats, hence separate routines.
|
481
600
|
if(this.id === 'gurobi') {
|
482
601
|
// `messages` must be an array of strings.
|
483
|
-
result.messages =
|
484
|
-
if(result.status
|
485
|
-
|
602
|
+
result.messages = log.split(os.EOL);
|
603
|
+
if(result.status === 1 ||
|
604
|
+
(result.status !== 0 && log.indexOf('license') < 0)) {
|
605
|
+
// Exit code typically indicates expired license, but also
|
606
|
+
// assume this cause when log does not contain the word "license".
|
486
607
|
result.error = 'Your Gurobi license may have expired';
|
487
|
-
|
608
|
+
} else {
|
488
609
|
try {
|
489
610
|
// Read JSON string from solution file.
|
490
611
|
const
|
491
612
|
json = fs.readFileSync(s.solution, 'utf8').trim(),
|
492
613
|
sol = JSON.parse(json);
|
493
614
|
result.seconds = sol.SolutionInfo.Runtime;
|
615
|
+
let status = sol.SolutionInfo.Status;
|
494
616
|
// NOTE: Status = 2 indicates success!
|
495
|
-
if(
|
496
|
-
|
497
|
-
|
617
|
+
if(status !== 2) {
|
618
|
+
let msg = s.statusMessage(status);
|
619
|
+
if(msg) {
|
620
|
+
// If solver exited with known status code, report message.
|
621
|
+
result.status = status;
|
622
|
+
result.solution = s.usableSolution(status);
|
623
|
+
result.error = msg;
|
624
|
+
}
|
498
625
|
if(!result.error) result.error = 'Unknown solver error';
|
499
626
|
console.log(`Solver status: ${result.status} - ${result.error}`);
|
500
627
|
}
|
501
628
|
// Objective value.
|
502
|
-
result.obj = sol.SolutionInfo.ObjVal;
|
629
|
+
result.obj = sol.SolutionInfo.ObjVal || 0;
|
503
630
|
// Values of solution vector.
|
504
631
|
if(sol.Vars) {
|
505
632
|
// Fill dictionary with variable name: value entries.
|
@@ -516,16 +643,88 @@ module.exports = class MILPSolver {
|
|
516
643
|
result.error = 'No solution found';
|
517
644
|
}
|
518
645
|
}
|
646
|
+
} else if(this.id === 'mosek') {
|
647
|
+
let solved = false,
|
648
|
+
output = [];
|
649
|
+
// `messages` must be an array of strings.
|
650
|
+
result.messages = log.split(os.EOL);
|
651
|
+
// NOTE: MOSEK may also write solution to 'user_model.bas', so
|
652
|
+
// try that as well before reporting failure.
|
653
|
+
try {
|
654
|
+
output = fs.readFileSync(s.solution, 'utf8').trim();
|
655
|
+
} catch(err) {
|
656
|
+
try {
|
657
|
+
const bas = s.solution.replace(/\.int$/, '.bas');
|
658
|
+
output = fs.readFileSync(bas, 'utf8').trim();
|
659
|
+
} catch(err) {
|
660
|
+
output = '';
|
661
|
+
}
|
662
|
+
}
|
663
|
+
if(!output) {
|
664
|
+
console.log('No MOSEK solution file');
|
665
|
+
} else if(output.indexOf('SOLUTION STATUS') >= 0) {
|
666
|
+
solved = true;
|
667
|
+
output = output.split(os.EOL);
|
668
|
+
}
|
669
|
+
if(solved) {
|
670
|
+
// MOSEK saves solution in a proprietary format, so just extract
|
671
|
+
// the status and then the variables.
|
672
|
+
let i = 0;
|
673
|
+
while(i < output.length && output[i].indexOf('CONSTRAINTS') < 0) {
|
674
|
+
const o = output[i].split(':');
|
675
|
+
if(o[0].indexOf('PRIMAL OBJECTIVE') >= 0) {
|
676
|
+
result.obj = o[1].trim();
|
677
|
+
} else if(o[0].indexOf('SOLUTION STATUS') >= 0) {
|
678
|
+
result.status = o[1].trim();
|
679
|
+
}
|
680
|
+
i++;
|
681
|
+
}
|
682
|
+
if(result.status.indexOf('OPTIMAL') >= 0) {
|
683
|
+
result.status = 0;
|
684
|
+
result.error = '';
|
685
|
+
} else if(result.status.indexOf('DUAL_INFEASIBLE') >= 0) {
|
686
|
+
result.error = 'Problem is unbounded';
|
687
|
+
solved = false;
|
688
|
+
} else if(result.status.indexOf('INFEASIBLE') >= 0) {
|
689
|
+
result.error = 'Problem is infeasible';
|
690
|
+
solved = false;
|
691
|
+
}
|
692
|
+
if(solved) {
|
693
|
+
while(i < output.length && output[i].indexOf('VARIABLES') < 0) {
|
694
|
+
i++;
|
695
|
+
}
|
696
|
+
// Fill dictionary with variable name: value entries.
|
697
|
+
while(i < output.length) {
|
698
|
+
const m = output[i].match(/^\d+\s+X(\d+)\s+\w\w\s+([^\s]+)\s+/);
|
699
|
+
if(m !== null) {
|
700
|
+
const vn = 'X' + m[1].padStart(7, '0');
|
701
|
+
x_dict[vn] = parseFloat(m[2]);
|
702
|
+
}
|
703
|
+
i++;
|
704
|
+
}
|
705
|
+
// Fill the solution vector, adding 0 for missing columns.
|
706
|
+
getValuesFromDict();
|
707
|
+
}
|
708
|
+
} else {
|
709
|
+
console.log('No solution found');
|
710
|
+
}
|
519
711
|
} else if(this.id === 'cplex') {
|
520
712
|
result.seconds = 0;
|
521
713
|
const
|
522
|
-
|
523
|
-
|
714
|
+
no_license = (log.indexOf('No license found') >= 0),
|
715
|
+
// NOTE: Omit first letter U, I and P as they may be either in
|
716
|
+
// upper case or lower case.
|
717
|
+
unbounded = (log.indexOf('nbounded') >= 0),
|
718
|
+
infeasible = (log.indexOf('nfeasible') >= 0),
|
719
|
+
primal_unbounded = (log.indexOf('rimal unbounded') >= 0),
|
720
|
+
err = log.match(/CPLEX Error\s+(\d+):\s+(.+)\./),
|
721
|
+
err_nr = (err && err.length > 1 ? parseInt(err[1]) : 0),
|
722
|
+
err_msg = (err_nr ? err[2] : ''),
|
524
723
|
// NOTE: Solver reports time with 1/100 secs precision.
|
525
|
-
mst =
|
724
|
+
mst = log.match(/Solution time \=\s+(\d+\.\d+) sec/);
|
526
725
|
if(mst && mst.length > 1) result.seconds = parseFloat(mst[1]);
|
527
726
|
// `messages` must be an array of strings.
|
528
|
-
result.messages =
|
727
|
+
result.messages = log.split(os.EOL);
|
529
728
|
let solved = false,
|
530
729
|
output = [];
|
531
730
|
if(no_license) {
|
@@ -535,6 +734,15 @@ module.exports = class MILPSolver {
|
|
535
734
|
// Non-zero solver exit code indicates serious trouble.
|
536
735
|
result.error = 'CPLEX solver terminated with error';
|
537
736
|
result.status = -13;
|
737
|
+
} else if(err_nr) {
|
738
|
+
result.status = err_nr;
|
739
|
+
if(infeasible && !primal_unbounded) {
|
740
|
+
result.error = 'Problem is infeasible';
|
741
|
+
} else if(unbounded) {
|
742
|
+
result.error = 'Problem is unbounded';
|
743
|
+
} else {
|
744
|
+
result.error = err_msg;
|
745
|
+
}
|
538
746
|
} else {
|
539
747
|
try {
|
540
748
|
output = fs.readFileSync(s.solution, 'utf8').trim();
|
@@ -562,8 +770,10 @@ module.exports = class MILPSolver {
|
|
562
770
|
}
|
563
771
|
i++;
|
564
772
|
}
|
773
|
+
// CPLEX termination codes 1, 101 and 102 indicate success.
|
565
774
|
if(['1', '101', '102'].indexOf(result.status) >= 0) {
|
566
775
|
result.status = 0;
|
776
|
+
result.solution = true;
|
567
777
|
result.error = '';
|
568
778
|
}
|
569
779
|
// Fill dictionary with variable name: value entries.
|
@@ -580,7 +790,7 @@ module.exports = class MILPSolver {
|
|
580
790
|
} else if(this.id === 'scip') {
|
581
791
|
result.seconds = 0;
|
582
792
|
// `messages` must be an array of strings.
|
583
|
-
result.messages =
|
793
|
+
result.messages = log.split(os.EOL);
|
584
794
|
let solved = false,
|
585
795
|
output = [];
|
586
796
|
if(result.status !== 0) {
|
@@ -613,7 +823,14 @@ module.exports = class MILPSolver {
|
|
613
823
|
}
|
614
824
|
}
|
615
825
|
if(result.status) {
|
616
|
-
|
826
|
+
let msg = s.statusMessage(result.status);
|
827
|
+
if(msg) {
|
828
|
+
// If solver exited with known status code, report message.
|
829
|
+
result.solution = s.usableSolution(result.status);
|
830
|
+
result.error = msg;
|
831
|
+
}
|
832
|
+
if(!result.error) result.error = 'Unknown solver error';
|
833
|
+
console.log(`Solver status: ${result.status} - ${result.error}`);
|
617
834
|
}
|
618
835
|
} else if (m.startsWith('Solving Time')) {
|
619
836
|
result.seconds = parseFloat(m.split(':')[1]);
|
@@ -636,32 +853,35 @@ module.exports = class MILPSolver {
|
|
636
853
|
console.log('No solution found');
|
637
854
|
}
|
638
855
|
} else if(this.id === 'lp_solve') {
|
639
|
-
// Read solver messages from file.
|
640
|
-
// NOTE: Linny-R client expects a list of strings.
|
641
856
|
const
|
642
|
-
|
857
|
+
// NOTE: LP_solve both messages and solution console, hence
|
858
|
+
// the log file is processed in two "stages".
|
859
|
+
output = log.trim().split(os.EOL),
|
860
|
+
// NOTE: Linny-R client expects log messages as list of strings.
|
643
861
|
msgs = [];
|
644
862
|
result.seconds = 0;
|
645
863
|
let i = 0,
|
646
864
|
solved = false;
|
647
865
|
while(i < output.length && !solved) {
|
866
|
+
// All output lines are "log lines"...
|
648
867
|
msgs.push(output[i]);
|
649
868
|
const m = output[i].match(/in total (\d+\.\d+) seconds/);
|
650
869
|
if(m && m.length > 1) result.seconds = parseFloat(m[1]);
|
870
|
+
// ... until the value of the objective function is detected.
|
651
871
|
solved = output[i].startsWith('Value of objective function:');
|
652
872
|
i++;
|
653
873
|
}
|
654
874
|
result.messages = msgs;
|
655
875
|
if(solved) {
|
656
|
-
// Look for line with first variable
|
876
|
+
// Look for line with first variable.
|
657
877
|
while(i < output.length && !output[i].startsWith('X')) i++;
|
658
|
-
// Fill dictionary with variable name: value entries
|
878
|
+
// Fill dictionary with variable name: value entries.
|
659
879
|
while(i < output.length) {
|
660
880
|
const v = output[i].split(/\s+/);
|
661
881
|
x_dict[v[0]] = parseFloat(v[1]);
|
662
882
|
i++;
|
663
883
|
}
|
664
|
-
// Fill the solution vector, adding 0 for missing columns
|
884
|
+
// Fill the solution vector, adding 0 for missing columns.
|
665
885
|
getValuesFromDict();
|
666
886
|
} else {
|
667
887
|
console.log('No solution found');
|