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.
@@ -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(settings, workspace) {
47
- this.name = settings.solver;
48
- this.solver_path = settings.solver_path;
49
- console.log(`Selected solver: "${this.name}"`);
50
- this.id = this.name.toLowerCase();
51
- // Each external MILP solver application has its own interface
52
- // NOTE: the list may be extended to accommodate more MILP solvers
53
- if(this.id === 'gurobi') {
54
- this.ext = '.lp';
55
- this.user_model = path.join(workspace.solver_output, 'usr_model.lp');
56
- this.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
57
- this.solution = path.join(workspace.solver_output, 'model.json');
58
- this.log = path.join(workspace.solver_output, 'model.log');
59
- this.args = [
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=0.5e-6',
198
+ 'intFeasTol=5e-7',
199
+ 'MIPGap=1e-4',
62
200
  'JSONSolDetail=1',
63
- `LogFile=${this.log}`,
64
- `ResultFile=${this.solution}`,
65
- `ResultFile=${this.solver_model}`,
66
- `${this.user_model}`
201
+ `LogFile=${s.log}`,
202
+ `ResultFile=${s.solution}`,
203
+ `ResultFile=${s.solver_model}`,
204
+ `${s.user_model}`
67
205
  ];
68
- this.errors = {
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
- } else if(this.id === 'cplex') {
86
- this.ext = '.lp';
87
- this.user_model = path.join(workspace.solver_output, 'usr_model.lp');
88
- this.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
89
- this.solution = path.join(workspace.solver_output, 'model.sol');
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
- this.log = path.join(workspace.solver_output, 'cplex.log');
232
+ s.log = path.join(workspace.solver_output, 'cplex.log');
92
233
  // NOTE: CPLEX command line accepts space separated commands ...
93
- this.args = [
94
- 'read ' + this.user_model,
95
- 'write ' + this.solver_model + ' lp',
96
- 'set timelimit 300',
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
- 'write ' + this.solution + ' 0',
241
+ `write ${s.solution} 0`,
99
242
  'quit'
100
243
  ];
101
- // ... when CPLEX is called with -c option; then each command must be
102
- // enclosed in double quotes; SCIP outputs its messages to a log file
103
- // terminal, so these must be caputured in a log file
104
- this.solve_cmd = 'cplex -c "' + this.args.join('" "') + '"';
105
- this.errors = {};
106
- } else if(this.id === 'scip') {
107
- this.ext = '.lp';
108
- this.user_model = path.join(workspace.solver_output, 'usr_model.lp');
109
- this.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
110
- this.solution = path.join(workspace.solver_output, 'model.sol');
111
- this.log = path.join(workspace.solver_output, 'model.log');
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
- this.args = [
114
- 'read', this.user_model,
115
- 'write problem', this.solver_model,
116
- 'set limit time', 300,
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', this.solution,
266
+ 'write solution', s.solution,
119
267
  'quit'
120
268
  ];
121
- // ... when SCIP is called with -c option; then commands must be
122
- // enclosed in double quotes; SCIP outputs its messages to the
123
- // terminal, so these must be caputured in a log file
124
- this.solve_cmd = 'scip -c "' + this.args.join(' ') + '" >' + this.log;
125
- this.errors = {
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
- } else if(this.id === 'lp_solve') {
143
- // Execute file commands differ across platforms
144
- if(os.platform().startsWith('win')) {
145
- this.solve_cmd = 'lp_solve.exe ';
146
- } else {
147
- this.solve_cmd = './lp_solve ';
148
- }
149
- this.ext = '.lp';
150
- this.user_model = path.join('user', 'solver', 'usr_model.lp');
151
- this.solver_model = path.join('user', 'solver', 'solver_model.lp');
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
- '-g 1.0e-11',
157
- '-epsel 1.0e-7',
158
- `-wlp ${this.solver_model}`,
159
- `>${this.solution}`,
160
- this.user_model
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
- this.errors = {
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
- } else {
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
- // Saves model file, executes solver, and returns results
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
- // Default timeout per block is 30 seconds
194
- let timeout = parseInt(sp.get('timeout'));
195
- if(isNaN(timeout)) timeout = 30;
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
- console.log('Solve block', result.block, result.round);
202
- // Write the POSTed MILP model to a file
203
- fs.writeFileSync(this.user_model, sp.get('data').trim());
204
- // Delete previous log file (if any)
205
- try {
206
- if(this.log) fs.unlinkSync(this.log);
207
- } catch(err) {
208
- // Ignore error
209
- }
210
- // Delete previous solution file (if any)
211
- try {
212
- if(this.solution) fs.unlinkSync(this.solution);
213
- } catch(err) {
214
- // Ignore error
215
- }
216
- let spawn = null,
217
- error = null,
218
- status = 0;
219
- try {
220
- if(this.id === 'lp_solve') {
221
- this.args[0] = '-timeout ' + timeout;
222
- // NOTES:
223
- // (1) LP_solve is picky about its command line, and will not work
224
- // when the arguments are passed as an array; therefore execute
225
- // it as a single command string that includes all arguments
226
- // (2) the shell option must be set to TRUE (so the command is
227
- // executed within an OS shell script) or LP_solve will interpret
228
- // the first argument as the model file, and complain
229
- // (3) output must be ignored, as LP_solve warnings should not also
230
- // appear on the console
231
- // (4) prevent Windows opening a visible sub-process shell window
232
- const
233
- cmd = this.solve_cmd + ' ' + this.args.join(' '),
234
- options = {shell: true, stdio: 'ignore', windowsHide: true};
235
- spawn = child_process.spawnSync(cmd, options);
236
- } else if(this.id === 'gurobi') {
237
- // When using Gurobi, the standard way with arguments works well
238
- this.args[0] = 'TimeLimit=' + timeout;
239
- const options = {windowsHide: true};
240
- spawn = child_process.spawnSync(this.solver_path, this.args, options);
241
- } else if(this.id === 'cplex') {
242
- // Delete previous solver model file (if any)
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(this.solver_model) fs.unlinkSync(this.solver_model);
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
- status = spawn.status;
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
- return this.processSolverOutput(result);
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(this.log, 'utf8').split(os.EOL);
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(this.solution, 'utf8').trim(),
491
+ json = fs.readFileSync(s.solution, 'utf8').trim(),
323
492
  sol = JSON.parse(json);
324
- result.seconds = sol.SolutionInfo.Runtime;
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 = this.errors[result.status];
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(this.log, 'utf8'),
354
- no_license = (msg.indexOf('No license found') >= 0);
355
- // `messages` must be an array of strings
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(this.solution, 'utf8').trim();
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 s = output[i].split('"');
383
- if(s[0].indexOf('objectiveValue') >= 0) {
384
- result.obj = s[1];
385
- } else if(s[0].indexOf('solutionStatusValue') >= 0) {
386
- result.status = s[1];
387
- } else if(s[0].indexOf('solutionStatusString') >= 0) {
388
- result.error = s[1];
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(this.log, 'utf8').split(os.EOL);
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
- this.solution, 'utf8').trim().split(os.EOL);
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) result.error = this.errors[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(this.solver_model, 'utf8');
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