linny-r 1.6.7 → 1.7.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/console.js CHANGED
@@ -37,26 +37,26 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
37
  SOFTWARE.
38
38
  */
39
39
 
40
- // Set global flag to indicate that this is a Node.js application
41
- // (this will make the "module" files linny-r-xxx.js export their properties)
40
+ // Set global flag to indicate that this is a Node.js application.
41
+ // This will make the "module" files linny-r-xxx.js export their properties.
42
42
  global.NODE = true;
43
43
 
44
44
  const
45
45
  WORKING_DIRECTORY = process.cwd(),
46
46
  path = require('path'),
47
47
  MODULE_DIRECTORY = path.join(WORKING_DIRECTORY, 'node_modules', 'linny-r'),
48
- // Load the required Node.js modules
48
+ // Load the required Node.js modules.
49
49
  child_process = require('child_process'),
50
50
  fs = require('fs'),
51
51
  os = require('os'),
52
52
  readline = require('readline'),
53
- // Get the platform name (win32, macOS, linux) of the user's computer
53
+ // Get the platform name (win32, macOS, linux) of the user's computer.
54
54
  PLATFORM = os.platform(),
55
- // Get version of the installed Linny-R package
55
+ // Get version of the installed Linny-R package.
56
56
  VERSION_INFO = getVersionInfo();
57
57
 
58
58
  function getVersionInfo() {
59
- // Reads version info from `package.json`
59
+ // Read version info from `package.json`.
60
60
  const info = {
61
61
  current: 0,
62
62
  current_time: 0,
@@ -72,10 +72,10 @@ function getVersionInfo() {
72
72
  console.log('This indicates that Linny-R is not installed properly.');
73
73
  process.exit();
74
74
  }
75
- // NOTE: unlike the Linny-R server, the console does not routinely
75
+ // NOTE: Unlike the Linny-R server, the console does not routinely
76
76
  // check whether version is up-to-date is optional because this is
77
77
  // a time-consuming action that would reduce multi-run performance.
78
- // See command line options (much further down)
78
+ // See command line options (much further down).
79
79
  console.log('\nLinny-R Console version', info.current);
80
80
  return info;
81
81
  }
@@ -99,16 +99,16 @@ const
99
99
  model = require('./static/scripts/linny-r-model.js'),
100
100
  ctrl = require('./static/scripts/linny-r-ctrl.js');
101
101
 
102
- // NOTE: the variables, functions and classes defined in these scripts
103
- // must still be "imported" into the global scope of this Node.js script
102
+ // NOTE: The variables, functions and classes defined in these scripts
103
+ // must still be "imported" into the global scope of this Node.js script.
104
104
  for(let k in config) if(config.hasOwnProperty(k)) global[k] = config[k];
105
105
  for(let k in utils) if(utils.hasOwnProperty(k)) global[k] = utils[k];
106
106
  for(let k in vm) if(vm.hasOwnProperty(k)) global[k] = vm[k];
107
107
  for(let k in model) if(model.hasOwnProperty(k)) global[k] = model[k];
108
108
  for(let k in ctrl) if(ctrl.hasOwnProperty(k)) global[k] = ctrl[k];
109
109
 
110
- // Default settings are used unless these are overruled by arguments on the
111
- // command line
110
+ // Default settings are used unless these are overruled by arguments on
111
+ // the command .
112
112
  const usage = `
113
113
  Usage: node console [options]
114
114
 
@@ -141,8 +141,8 @@ const SETTINGS = commandLineSettings();
141
141
  const WORKSPACE = createWorkspace();
142
142
 
143
143
  // Only then require the Node.js modules that are not "built-in"
144
- // NOTE: the function serves to catch the error in case the module has not
145
- // been installed with `npm`
144
+ // NOTE: the function serves to catch the error in case the module has
145
+ // not been installed with `npm`.
146
146
  const { DOMParser } = checkNodeModule('@xmldom/xmldom');
147
147
 
148
148
  function checkNodeModule(name) {
@@ -156,10 +156,10 @@ function checkNodeModule(name) {
156
156
  }
157
157
 
158
158
  // Add the XML parser to the global scope so it can be referenced by the
159
- // XML-related functions defined in `linny-r-utils.js`
159
+ // XML-related functions defined in `linny-r-utils.js`.
160
160
  global.XML_PARSER = new DOMParser();
161
161
 
162
- // Set the current version number
162
+ // Set the current version number.
163
163
  global.LINNY_R_VERSION = VERSION_INFO.current;
164
164
 
165
165
  ///////////////////////////////////////////////////////////////////////////////
@@ -173,17 +173,17 @@ class ConsoleMonitor {
173
173
  constructor() {
174
174
  this.console = true;
175
175
  this.visible = false;
176
- // The "show log" flag indicates whether log messages should be output to
177
- // the console (will be ignored by the GraphicalMonitor)
176
+ // The "show log" flag indicates whether log messages should be output
177
+ // to the console (will be ignored by the GUIMonitor).
178
178
  this.show_log = false;
179
179
  this.block_number = 0;
180
180
  }
181
181
 
182
182
  logMessage(block, msg) {
183
- // Outputs a solver message to the console if logging is activated
183
+ // Output a solver message to the console if logging is activated.
184
184
  if(this.show_log) {
185
185
  if(block > this.block_number) {
186
- // Mark advance to nex block with a blank line
186
+ // Mark advance to nex block with a blank line.
187
187
  console.log('\nBlock #', block);
188
188
  this.block_number = block;
189
189
  }
@@ -194,11 +194,11 @@ class ConsoleMonitor {
194
194
  logOnToServer() {
195
195
  VM.solver_user = '';
196
196
  VM.solver_token = 'local host';
197
- VM.solver_name = SOLVER.name;
197
+ VM.solver_name = SOLVER.id;
198
198
  }
199
199
 
200
200
  connectToServer() {
201
- // Console always uses local server => no logon prompt
201
+ // Console always uses local server => no logon prompt.
202
202
  this.logOnToServer();
203
203
  return true;
204
204
  }
@@ -218,17 +218,21 @@ class ConsoleMonitor {
218
218
  token: VM.solver_token,
219
219
  block: VM.block_count,
220
220
  round: VM.round_sequence[VM.current_round],
221
+ columns: VM.columnsInBlock,
221
222
  data: VM.lines,
222
- timeout: top
223
+ solver: MODEL.preferred_solver,
224
+ timeout: top,
225
+ inttol: MODEL.integer_tolerance,
226
+ mipgap: MODEL.MIP_gap
223
227
  }));
224
228
  VM.processServerResponse(data);
225
229
  const msg =
226
230
  `Solving block #${VM.blockWithRound} took ${VM.elapsedTime} seconds.`;
227
231
  VM.logMessage(VM.block_count, msg);
228
232
  console.log(msg);
229
- // solve next block (if any)
230
- // NOTE: use setTimeout so that this calling function returns
231
- // and hence frees its local variables
233
+ // Solve next block (if any).
234
+ // NOTE: Use setTimeout so that this calling function returns
235
+ // and hence frees its local variables.
232
236
  setTimeout(() => VM.solveBlocks(), 1);
233
237
  } catch(err) {
234
238
  console.log(err);
@@ -260,7 +264,7 @@ class ConsoleRepositoryBrowser {
260
264
  this.repositories = [];
261
265
  this.repository_index = -1;
262
266
  this.module_index = -1;
263
- // Get the repository list from the modules
267
+ // Get the repository list from the modules.
264
268
  this.getRepositories();
265
269
  this.reset();
266
270
  }
@@ -270,19 +274,19 @@ class ConsoleRepositoryBrowser {
270
274
  }
271
275
 
272
276
  get isLocalHost() {
273
- // Returns TRUE if first repository on the list is 'local host'
277
+ // Return TRUE if first repository on the list is 'local host'.
274
278
  return this.repositories.length > 0 &&
275
279
  this.repositories[0].name === 'local host';
276
280
  }
277
281
 
278
282
  getRepositories() {
279
- // Gets the list of repository names from the server
283
+ // Gets the list of repository names from the server.
280
284
  this.repositories.length = 0;
281
285
  // @@TO DO!!
282
286
  }
283
287
 
284
288
  repositoryByName(n) {
285
- // Returns the repository having name `n` if already known, otherwise NULL
289
+ // Return the repository having name `n` if already known, otherwise NULL.
286
290
  for(let i = 0; i < this.repositories.length; i++) {
287
291
  if(this.repositories[i].name === n) {
288
292
  return this.repositories[i];
@@ -293,15 +297,15 @@ class ConsoleRepositoryBrowser {
293
297
 
294
298
  asFileName(s) {
295
299
  // NOTE: asFileName is implemented as function (see below) to permit
296
- // its use prior to instantiation of the RepositoryBrowser
300
+ // its use prior to instantiation of the RepositoryBrowser.
297
301
  return stringToFileName(s);
298
302
  }
299
303
 
300
304
  }
301
305
 
302
306
  function stringToFileName(s) {
303
- // Returns string `s` with whitespace converted to a single dash, and
304
- // special characters converted to underscores
307
+ // Return string `s` with whitespace converted to a single dash, and
308
+ // special characters converted to underscores.
305
309
  return s.normalize('NFKD').trim()
306
310
  .replace(/[\s\-]+/g, '-')
307
311
  .replace(/[^A-Za-z0-9_\-]/g, '_')
@@ -315,17 +319,17 @@ class ConsoleFileManager {
315
319
 
316
320
  anyOSpath(p) {
317
321
  // Helper function that converts any path notation to platform notation
318
- // based on the predominant separator
322
+ // based on the predominant separator.
319
323
  const
320
324
  s_parts = p.split('/'),
321
325
  bs_parts = p.split('\\'),
322
326
  parts = (s_parts.length > bs_parts.length ? s_parts : bs_parts);
323
- // On macOS machines, paths start with a slash, so first substring is empty
327
+ // On macOS machines, paths start with a slash, so first substring is empty.
324
328
  if(parts[0].endsWith(':') && path.sep === '\\') {
325
- // On Windows machines, add a backslash after the disk (if specified)
329
+ // On Windows machines, add a backslash after the disk (if specified).
326
330
  parts[0] += path.sep;
327
331
  }
328
- // Reassemble path for the OS of this machine
332
+ // Reassemble path for the OS of this machine.
329
333
  return path.join(...parts);
330
334
  }
331
335
 
@@ -334,18 +338,18 @@ class ConsoleFileManager {
334
338
  if(url === '') return;
335
339
  // NOTE: add this dataset to the "loading" list...
336
340
  addDistinct(dataset, MODEL.loading_datasets);
337
- // ... and allow for 3 more seconds (6 times 500 ms) to complete
341
+ // ... and allow for 3 more seconds (6 times 500 ms) to complete.
338
342
  MODEL.max_time_to_load += 6;
339
- // Passed parameter is the URL or full path
343
+ // Passed parameter is the URL or full path.
340
344
  console.log('Load data from', url);
341
345
  if(!url) {
342
346
  console.log('ERROR: No URL or path');
343
347
  return;
344
348
  }
345
349
  if(url.toLowerCase().startsWith('http')) {
346
- // URL => validate it, and then try to download its content as text
350
+ // URL => validate it, and then try to download its content as text.
347
351
  try {
348
- new URL(url); // Will throw an error if URL is not valid
352
+ new URL(url); // Will throw an error if URL is not .
349
353
  getTextFromURL(url,
350
354
  (data) => FILE_MANAGER.setData(dataset, data),
351
355
  (error) => {
@@ -361,7 +365,7 @@ class ConsoleFileManager {
361
365
  let fp = this.anyOSpath(url);
362
366
  if(!(fp.startsWith('/') || fp.startsWith('\\') || fp.indexOf(':\\') > 0)) {
363
367
  // Relative path => add path to specified data path or to the
364
- // default location user/data
368
+ // default location user/data.
365
369
  fp = path.join(SETTINGS.data_path || WORKSPACE.data, fp);
366
370
  console.log('Full path: ', fp);
367
371
  }
@@ -379,44 +383,44 @@ class ConsoleFileManager {
379
383
  setData(dataset, data) {
380
384
  if(data !== '' && UI.postResponseOK(data)) {
381
385
  // Server must return either semicolon-separated or
382
- // newline-separated string of numbers
386
+ // newline-separated string of numbers.
383
387
  if(data.indexOf(';') < 0) {
384
- // If no semicolon found, replace newlines by semicolons
388
+ // If no semicolon found, replace newlines by semicolons.
385
389
  data = data.trim().split('\n').join(';');
386
390
  }
387
- // Remove all white space
391
+ // Remove all white space.
388
392
  data = data.replace(/\s+/g, '');
389
393
  dataset.unpackDataString(data);
390
- // NOTE: remove dataset from the "loading" list
394
+ // NOTE: Remove dataset from the "loading" list.
391
395
  const i = MODEL.loading_datasets.indexOf(dataset);
392
396
  if(i >= 0) MODEL.loading_datasets.splice(i, 1);
393
397
  }
394
398
  }
395
399
 
396
400
  decryptIfNeeded(data, callback) {
397
- // Checks whether XML is encrypted; if not, processes data "as is",
398
- // otherwise decrypt using password specified in command line
401
+ // Check whether XML is encrypted; if not, processes data "as is",
402
+ // otherwise decrypt using password specified in command line.
399
403
  if(data.indexOf('model latch="') < 0) {
400
404
  setTimeout(callback, 0, data);
401
405
  return;
402
406
  }
403
407
  const xml = XML_PARSER.parseFromString(data, 'text/xml');
404
408
  const de = xml.documentElement;
405
- // Linny-R model must contain a model node
409
+ // Linny-R model must contain a model node.
406
410
  if(de.nodeName !== 'model') throw 'XML document has no model element';
407
411
  const encr_msg = {
408
412
  encryption: nodeContentByTag(de, 'content'),
409
413
  latch: nodeParameterValue(de, 'latch')
410
414
  };
411
415
  console.log('Decrypting...');
412
- // NOTE: function `tryToDecrypt` is defined in linny-r-utils.js
416
+ // NOTE: Function `tryToDecrypt` is defined in linny-r-utils.js.
413
417
  setTimeout((msg, pwd, ok, err) => tryToDecrypt(msg, pwd, ok, err), 5,
414
418
  encr_msg, SETTINGS.password,
415
- // The on_ok function
419
+ // The on_ok function.
416
420
  (data) => {
417
421
  if(data) callback(data);
418
422
  },
419
- // The on_error function
423
+ // The on_error function.
420
424
  (err) => {
421
425
  console.log(err);
422
426
  console.log('Failed to load encrypted model');
@@ -424,8 +428,7 @@ class ConsoleFileManager {
424
428
  }
425
429
 
426
430
  loadModel(fp, callback) {
427
- // Get the XML of the file specified via the command line
428
- // NOTE: asynchronous method with callback because decryption is
431
+ // Get the XML of the file specified via the command line.
429
432
  fs.readFile(fp, 'utf8', (err, data) => {
430
433
  if(err) {
431
434
  console.log(err);
@@ -438,7 +441,7 @@ class ConsoleFileManager {
438
441
  }
439
442
 
440
443
  writeStringToFile(s, fp) {
441
- // Write string `s` to path `fp`
444
+ // Write string `s` to path `fp`.
442
445
  try {
443
446
  fs.writeFileSync(fp, s);
444
447
  console.log(pluralS(s.length, 'character') + ' written to file ' + fp);
@@ -540,7 +543,7 @@ class ConsoleReceiver {
540
543
  }
541
544
  if(msg) {
542
545
  this.setError(msg);
543
- rcvrReport();
546
+ rcvrReport(this.channel, this.file_name);
544
547
  // Keep listening, so check again after the time interval
545
548
  setTimeout(() => RECEIVER.listen(), this.interval);
546
549
  } else {
@@ -559,19 +562,29 @@ class ConsoleReceiver {
559
562
 
560
563
  report() {
561
564
  // Saves the run results in the channel, or signals an error
562
- let run = '';
565
+ let run = '',
566
+ rpath = this.channel,
567
+ file = this.file_name;
563
568
  // NOTE: Always set `solving` to FALSE
564
569
  this.solving = false;
565
- if(this.experiment){
570
+ // NOTE: When reporting receiver while is not active, report the
571
+ // results of the running experiment.
572
+ if(this.experiment || !this.active) {
566
573
  if(MODEL.running_experiment) {
567
574
  run = MODEL.running_experiment.active_combination_index;
568
- this.log(`Reporting: ${this.file_name} (run #${run})`);
575
+ this.log(`Reporting: ${file} (run #${run})`);
569
576
  }
570
577
  }
578
+ // NOTE: If receiver is not active, path and file must be set.
579
+ if(!this.active) {
580
+ rpath = 'user/reports';
581
+ file = REPOSITORY_BROWSER.asFileName(MODEL.name || 'model') +
582
+ run + '-' + compactClockTime();
583
+ }
571
584
  if(MODEL.solved && !VM.halted) {
572
585
  // Normal execution termination => report results
573
586
  const data = MODEL.outputData;
574
- rcvrReport(run, data[0], data[1]);
587
+ rcvrReport(rpath, file, run, data[0], data[1]);
575
588
  // If execution completed, perform the call-back action
576
589
  // NOTE: for experiments, call-back is performed upon completion by
577
590
  // the Experiment Manager
@@ -588,34 +601,7 @@ class ConsoleReceiver {
588
601
  callBack() {
589
602
  // Deletes the file in the channel directory (to prevent executing it again)
590
603
  // and activates the call-back script on the local server
591
- fetch('receiver/', postData({
592
- path: this.channel,
593
- file: this.file_name,
594
- action: 'call-back',
595
- script: this.call_back_script
596
- }))
597
- .then((response) => {
598
- if(!response.ok) {
599
- UI.alert(`ERROR ${response.status}: ${response.statusText}`);
600
- }
601
- return response.text();
602
- })
603
- .then((data) => {
604
- // Call-back completed => resume listening unless running experiment
605
- if(RECEIVER.experiment) {
606
- // For experiments, only display server response if warning or error
607
- UI.postResponseOK(data);
608
- } else {
609
- // Always show server response for single runs
610
- if(UI.postResponseOK(data, true)) {
611
- // NOTE: resume listening only if no error
612
- setTimeout(() => RECEIVER.listen(), RECEIVER.interval);
613
- } else {
614
- RECEIVER.deactivate();
615
- }
616
- }
617
- })
618
- .catch(() => UI.warn(UI.WARNING.NO_CONNECTION, err));
604
+ rcvrCallBack(this.call_back_script);
619
605
  }
620
606
 
621
607
  } // END of class ConsoleReceiver
@@ -700,8 +686,8 @@ function rcvrListen(rpath) {
700
686
  }
701
687
 
702
688
  function rcvrAbort() {
703
- const log_path = path.join(this.channel, this.file_name + '-log.txt');
704
- fs.writeFile(log_path, this.logReport, (err) => {
689
+ const log_path = path.join(RECEIVER.channel, RECEIVER.file_name + '-log.txt');
690
+ fs.writeFile(log_path, RECEIVER.logReport, (err) => {
705
691
  if(err) {
706
692
  console.log(err);
707
693
  console.log('ERROR: Failed to write event log to file', log_path);
@@ -711,9 +697,9 @@ function rcvrAbort() {
711
697
  });
712
698
  }
713
699
 
714
- function rcvrReport(run='', data='no data', stats='no statistics') {
700
+ function rcvrReport(rpath, file, run='', data='no data', stats='no statistics') {
715
701
  try {
716
- let fp = path.join(this.channel, this.file_name + run + '-data.txt');
702
+ let fp = path.join(rpath, file + run + '-data.txt');
717
703
  fs.writeFileSync(fp, data);
718
704
  } catch(err) {
719
705
  console.log(err);
@@ -721,7 +707,7 @@ function rcvrReport(run='', data='no data', stats='no statistics') {
721
707
  return;
722
708
  }
723
709
  try {
724
- fp = path.join(this.channel, this.file_name + run + '-stats.txt');
710
+ fp = path.join(rpath, file + run + '-stats.txt');
725
711
  fs.writeFileSync(fp, stats);
726
712
  } catch(err) {
727
713
  console.log(err);
@@ -729,23 +715,23 @@ function rcvrReport(run='', data='no data', stats='no statistics') {
729
715
  return;
730
716
  }
731
717
  try {
732
- fp = path.join(this.channel, this.file_name + run + '-log.txt');
733
- fs.writeFileSync(fp, this.logReport);
718
+ fp = path.join(rpath, file + run + '-log.txt');
719
+ fs.writeFileSync(fp, RECEIVER.logReport);
734
720
  } catch(err) {
735
721
  console.log(err);
736
722
  console.log('ERROR: Failed to write event log to file', fp);
737
723
  }
738
- console.log('Data and statistics reported for', this.file_name);
724
+ console.log('Data and statistics reported for', file);
739
725
  }
740
726
 
741
727
  function rcvrCallBack(script) {
742
728
  let file_type = '',
743
- cpath = path.join(this.channel, this.file_name + '.lnr');
729
+ cpath = path.join(RECEIVER.channel, RECEIVER.file_name + '.lnr');
744
730
  try {
745
731
  fs.accessSync(cpath);
746
732
  file_type = 'model';
747
733
  } catch(err) {
748
- cpath = path.join(this.channel, this.file_name + '.lnrc');
734
+ cpath = path.join(RECEIVER.channel, RECEIVER.file_name + '.lnrc');
749
735
  try {
750
736
  fs.accessSync(cpath);
751
737
  file_type = 'command';
@@ -920,119 +906,6 @@ function commandLineSettings() {
920
906
  }
921
907
  // Perform version check only if asked for
922
908
  if(settings.check) checkForUpdates();
923
- // Check whether MILP solver(s) and Inkscape have been installed
924
- const path_list = process.env.PATH.split(path.delimiter);
925
- let gurobi_path = '',
926
- scip_path = '',
927
- match,
928
- max_v = -1;
929
- for(let i = 0; i < path_list.length; i++) {
930
- match = path_list[i].match(/gurobi(\d+)/i);
931
- if(match && parseInt(match[1]) > max_v) {
932
- gurobi_path = path_list[i];
933
- max_v = parseInt(match[1]);
934
- }
935
- match = path_list[i].match(/[\/\\]cplex[\/\\]bin/i);
936
- if(match) {
937
- cplex_path = path_list[i];
938
- } else {
939
- // NOTE: CPLEX may create its own environment variable for its paths
940
- match = path_list[i].match(/%(.*cplex.*)%/i);
941
- if(match) {
942
- const cpl = process.env[match[1]].split(path.delimiter);
943
- for(let i = 0; i < cpl.length; i++) {
944
- match = cpl[i].match(/[\/\\]cplex[\/\\]bin/i);
945
- if(match) {
946
- cplex_path = cpl[i];
947
- break;
948
- }
949
- }
950
- }
951
- }
952
- match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
953
- if(match) scip_path = path_list[i];
954
- match = path_list[i].match(/inkscape/i);
955
- if(match) settings.inkscape = path_list[i];
956
- }
957
- if(!gurobi_path && !PLATFORM.startsWith('win')) {
958
- console.log('Looking for Gurobi in /usr/local/bin');
959
- try {
960
- // On macOS and Unix, Gurobi is in the user's local binaries
961
- const gp = '/usr/local/bin';
962
- fs.accessSync(gp + '/gurobi_cl');
963
- gurobi_path = gp;
964
- } catch(err) {
965
- // No real error, so no action needed
966
- }
967
- }
968
- if(gurobi_path) {
969
- console.log('Path to Gurobi:', gurobi_path);
970
- // Check if command line version is executable
971
- const sp = path.join(gurobi_path,
972
- 'gurobi_cl' + (PLATFORM.startsWith('win') ? '.exe' : ''));
973
- try {
974
- fs.accessSync(sp, fs.constants.X_OK);
975
- if(settings.solver !== 'gurobi')
976
- settings.solver = 'gurobi';
977
- settings.solver_path = sp;
978
- } catch(err) {
979
- console.log(err.message);
980
- console.log(
981
- 'WARNING: Failed to access the Gurobi command line application');
982
- }
983
- }
984
- // Check if cplex(.exe) exists in its directory
985
- let sp = path.join(cplex_path, 'cplex' + (PLATFORM.startsWith('win') ? '.exe' : ''));
986
- const need_cplex = !settings.solver || settings.preferred_solver === 'cplex';
987
- try {
988
- fs.accessSync(sp, fs.constants.X_OK);
989
- console.log('Path to CPLEX:', sp);
990
- if(need_cplex) {
991
- settings.solver = 'cplex';
992
- settings.solver_path = sp;
993
- }
994
- } catch(err) {
995
- // Only report error if CPLEX is needed
996
- if(need_cplex) {
997
- console.log(err.message);
998
- console.log('WARNING: CPLEX application not found in', sp);
999
- }
1000
- }
1001
- // Check if scip(.exe) exists in its directory
1002
- sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
1003
- const need_scip = !settings.solver || settings.preferred_solver === 'scip';
1004
- try {
1005
- fs.accessSync(sp, fs.constants.X_OK);
1006
- console.log('Path to SCIP:', sp);
1007
- if(need_scip) {
1008
- settings.solver = 'scip';
1009
- settings.solver_path = sp;
1010
- }
1011
- } catch(err) {
1012
- // Only report error if SCIP is needed
1013
- if(need_scip) {
1014
- console.log(err.message);
1015
- console.log('WARNING: SCIP application not found in', sp);
1016
- }
1017
- }
1018
- // Check if lp_solve(.exe) exists in main directory
1019
- sp = path.join(WORKING_DIRECTORY,
1020
- 'lp_solve' + (PLATFORM.startsWith('win') ? '.exe' : ''));
1021
- const need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
1022
- try {
1023
- fs.accessSync(sp, fs.constants.X_OK);
1024
- console.log('Path to LP_solve:', sp);
1025
- if(need_lps) {
1026
- settings.solver = 'lp_solve';
1027
- settings.solver_path = sp;
1028
- }
1029
- } catch(err) {
1030
- // Only report error if LP_solve is needed
1031
- if(need_lps) {
1032
- console.log(err.message);
1033
- console.log('WARNING: LP_solve application not found in', sp);
1034
- }
1035
- }
1036
909
  return settings;
1037
910
  }
1038
911
 
@@ -1081,6 +954,8 @@ function createWorkspace() {
1081
954
  }
1082
955
  // The file containing name, URL and access token for remote repositories
1083
956
  ws.repositories = path.join(SETTINGS.user_dir, 'repositories.cfg');
957
+ // For completeness, add path to Linny-R directory.
958
+ ws.working_directory = WORKING_DIRECTORY;
1084
959
  // Return the updated workspace object
1085
960
  return ws;
1086
961
  }
@@ -1115,7 +990,7 @@ function checkForUpdates() {
1115
990
  }
1116
991
 
1117
992
  // Initialize the solver
1118
- const SOLVER = new MILPSolver(SETTINGS, WORKSPACE);
993
+ const SOLVER = new MILPSolver(SETTINGS.preferred_solver, WORKSPACE);
1119
994
  /*
1120
995
  // Initialize the dialog for interaction with the user
1121
996
  const PROMPTER = readline.createInterface(
@@ -1156,32 +1031,41 @@ global.RECEIVER = new ConsoleReceiver();
1156
1031
  global.IO_CONTEXT = null;
1157
1032
  global.MODEL = new LinnyRModel();
1158
1033
 
1159
- // Connect the virtual machine (may prompt for password)
1034
+ // Connect the virtual machine (may prompt for password).
1160
1035
  MONITOR.connectToServer();
1161
1036
 
1162
- // Load the model if specified
1037
+ // Load the model if specified.
1163
1038
  if(SETTINGS.model_path) {
1164
1039
  FILE_MANAGER.loadModel(SETTINGS.model_path, (model) => {
1165
- // Command `run` takes precedence over `xrun`
1040
+ // Command `run` takes precedence over `xrun`.
1166
1041
  if(SETTINGS.run) {
1167
1042
  MONITOR.show_log = SETTINGS.verbose;
1043
+ // Callback hook "tells" VM where to return after solving.
1168
1044
  VM.callback = () => {
1169
1045
  const od = model.outputData;
1170
- // Output data is two-string list [time series, statistics]
1046
+ // Output data is two-string list [time series, statistics].
1171
1047
  if(SETTINGS.report) {
1172
- // Output time series
1048
+ // Output time series.
1173
1049
  FILE_MANAGER.writeStringToFile(od[0],
1174
1050
  SETTINGS.report + '-series.txt');
1175
- // Output statistics
1051
+ // Output statistics.
1176
1052
  FILE_MANAGER.writeStringToFile(od[1],
1177
1053
  SETTINGS.report + '-stats.txt');
1178
- } else {
1179
- // Output strings to console
1054
+ } else if(!MODEL.report_results) {
1055
+ // Output strings to console.
1180
1056
  console.log(od[0]);
1181
1057
  console.log(od[1]);
1182
1058
  }
1059
+ // Clear callback hook (to be neat).
1183
1060
  VM.callback = null;
1184
1061
  };
1062
+ // NOTE: Solver preference in model overrides default solver.
1063
+ const mps = MODEL.preferred_solver;
1064
+ if(mps && SOLVER.solver_list.hasOwnProperty(mps)) {
1065
+ VM.solver_name = mps;
1066
+ SOLVER.id = mps;
1067
+ console.log(`Using solver ${SOLVER.name} (model preference)`);
1068
+ }
1185
1069
  VM.solveModel();
1186
1070
  } else if(SETTINGS.x_title) {
1187
1071
  const xi = MODEL.indexOfExperiment(SETTINGS.x_title);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.6.7",
3
+ "version": "1.7.0",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {