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.
@@ -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 CPLEX path.
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, 'usr_model.lp');
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, 'model.json');
194
- s.log = path.join(workspace.solver_output, 'model.log');
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
- s.errors = {
207
- 1: 'Model loaded -- no further information',
208
- 2: 'Optimal solution found',
209
- 3: 'The model is infeasible',
210
- 4: 'The model is either unbounded or infeasible',
211
- 5: 'The model is unbounded',
212
- 6: 'Aborted -- Optimal objective is worse than specified cut-off',
213
- 7: 'Halted -- Iteration limit exceeded',
214
- 8: 'Halted -- Node limit exceeded',
215
- 9: 'Halted -- Solver time limit exceeded',
216
- 10: 'Halted -- Solution count limit exceeded',
217
- 11: 'Halted -- Optimization terminated by user',
218
- 12: 'Halted -- Unrecoverable numerical difficulties',
219
- 13: 'The model is sub-obtimal',
220
- 14: 'Optimization still in progress',
221
- 15: 'User-specified objective limit has been reached'
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, 'usr_model.lp');
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, 'model.sol');
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.errors = {};
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, 'usr_model.lp');
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, 'model.sol');
257
- s.log = path.join(workspace.solver_output, 'model.log');
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
- s.errors = {
276
- 1: 'User interrupt',
277
- 2: 'Node limit reached',
278
- 3: 'Total node limit reached',
279
- 4: 'Stalling node limit reached',
280
- 5: 'Time limit reached',
281
- 6: 'Memory limit reached',
282
- 7: 'Gap limit reached',
283
- 8: 'Solution limit reached',
284
- 9: 'Solution improvement limit reached',
285
- 10: 'Restart limit reached',
286
- 11: 'Optimal solution found',
287
- 12: 'Problem is infeasible',
288
- 13: 'Problem is unbounded',
289
- 14: 'Problem is either infeasible or unbounded',
290
- 15: 'Solver terminated by user'
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', 'usr_model.lp');
386
+ s.user_model = path.join('user', 'solver', 'user_model.lp');
298
387
  s.solver_model = path.join('user', 'solver', 'solver_model.lp');
299
- s.solution = path.join('.', 'user', 'solver', 'output.txt');
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.solution}`,
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
- s.errors = {
314
- '-2': 'Out of memory',
315
- 1: 'The model is sub-optimal',
316
- 2: 'The model is infeasible',
317
- 3: 'The model is unbounded',
318
- 4: 'The model is degenerative',
319
- 5: 'Numerical failure encountered',
320
- 6: 'Solver was stopped by user',
321
- 7: 'Solver time limit exceeded',
322
- 9: 'The model could be solved by presolve',
323
- 25: 'Accuracy error encountered'
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
- // Ignore error
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 are
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
- if(status in s.errors) {
435
- // If solver exited with known status code, report message
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.error = s.errors[status];
548
+ result.solution = s.usableSolution(status);
549
+ result.error = msg;
438
550
  } else if(status !== 0) {
439
551
  result.status = -13;
440
- const msg = (error ? error.message : 'Unknown error');
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
- // Returns a result vector for as many real numbers (as strings!)
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 = fs.readFileSync(s.log, 'utf8').split(os.EOL);
484
- if(result.status !== 0) {
485
- // Non-zero solver exit code may indicate expired license.
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
- } else {
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(sol.SolutionInfo.Status !== 2) {
496
- result.status = sol.SolutionInfo.Status;
497
- result.error = s.errors[result.status];
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
- msg = fs.readFileSync(s.log, 'utf8'),
523
- no_license = (msg.indexOf('No license found') >= 0),
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 = msg.match(/Solution time \=\s+(\d+\.\d+) sec/);
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 = msg.split(os.EOL);
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 = fs.readFileSync(s.log, 'utf8').split(os.EOL);
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
- result.error = this.solver_list.scip.errors[result.status];
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
- output = fs.readFileSync(s.solution, 'utf8').trim().split(os.EOL),
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');