linny-r 1.6.8 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
 
@@ -132,7 +132,6 @@ Possible options are:
132
132
  workspace=[path] will create workspace in [path] instead of (main)/user
133
133
  xrun=[title#list] will perform experiment runs in given range
134
134
  (list is comma-separated sequence of run numbers)
135
- (FUTURE OPTION)
136
135
  `;
137
136
 
138
137
  const SETTINGS = commandLineSettings();
@@ -141,8 +140,8 @@ const SETTINGS = commandLineSettings();
141
140
  const WORKSPACE = createWorkspace();
142
141
 
143
142
  // 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`
143
+ // NOTE: the function serves to catch the error in case the module has
144
+ // not been installed with `npm`.
146
145
  const { DOMParser } = checkNodeModule('@xmldom/xmldom');
147
146
 
148
147
  function checkNodeModule(name) {
@@ -156,10 +155,10 @@ function checkNodeModule(name) {
156
155
  }
157
156
 
158
157
  // 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`
158
+ // XML-related functions defined in `linny-r-utils.js`.
160
159
  global.XML_PARSER = new DOMParser();
161
160
 
162
- // Set the current version number
161
+ // Set the current version number.
163
162
  global.LINNY_R_VERSION = VERSION_INFO.current;
164
163
 
165
164
  ///////////////////////////////////////////////////////////////////////////////
@@ -173,32 +172,37 @@ class ConsoleMonitor {
173
172
  constructor() {
174
173
  this.console = true;
175
174
  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)
175
+ // The "show log" flag indicates whether log messages should be output
176
+ // to the console (will be ignored by the GUIMonitor).
178
177
  this.show_log = false;
179
178
  this.block_number = 0;
180
179
  }
181
180
 
182
181
  logMessage(block, msg) {
183
- // Outputs a solver message to the console if logging is activated
182
+ // Output a solver message to the console if logging is activated.
183
+ let new_block = false;
184
+ if(block > this.block_number) {
185
+ this.block_number = block;
186
+ new_block = true;
187
+ }
184
188
  if(this.show_log) {
185
- if(block > this.block_number) {
186
- // Mark advance to nex block with a blank line
187
- console.log('\nBlock #', block);
188
- this.block_number = block;
189
- }
189
+ // Mark advance to nex block with a blank line.
190
+ if(new_block) console.log('\nBlock #', block);
190
191
  console.log(msg);
191
192
  }
193
+ // Always log solver message to receiver report.
194
+ if(new_block) RECEIVER.log('Block #' + block, true);
195
+ RECEIVER.log(msg, true);
192
196
  }
193
197
 
194
198
  logOnToServer() {
195
199
  VM.solver_user = '';
196
200
  VM.solver_token = 'local host';
197
- VM.solver_name = SOLVER.name;
201
+ VM.solver_name = SOLVER.id;
198
202
  }
199
203
 
200
204
  connectToServer() {
201
- // Console always uses local server => no logon prompt
205
+ // Console always uses local server => no logon prompt.
202
206
  this.logOnToServer();
203
207
  return true;
204
208
  }
@@ -218,17 +222,21 @@ class ConsoleMonitor {
218
222
  token: VM.solver_token,
219
223
  block: VM.block_count,
220
224
  round: VM.round_sequence[VM.current_round],
225
+ columns: VM.columnsInBlock,
221
226
  data: VM.lines,
222
- timeout: top
227
+ solver: MODEL.preferred_solver,
228
+ timeout: top,
229
+ inttol: MODEL.integer_tolerance,
230
+ mipgap: MODEL.MIP_gap
223
231
  }));
224
232
  VM.processServerResponse(data);
225
233
  const msg =
226
234
  `Solving block #${VM.blockWithRound} took ${VM.elapsedTime} seconds.`;
227
235
  VM.logMessage(VM.block_count, msg);
228
236
  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
237
+ // Solve next block (if any).
238
+ // NOTE: Use setTimeout so that this calling function returns
239
+ // and hence frees its local variables.
232
240
  setTimeout(() => VM.solveBlocks(), 1);
233
241
  } catch(err) {
234
242
  console.log(err);
@@ -260,7 +268,7 @@ class ConsoleRepositoryBrowser {
260
268
  this.repositories = [];
261
269
  this.repository_index = -1;
262
270
  this.module_index = -1;
263
- // Get the repository list from the modules
271
+ // Get the repository list from the modules.
264
272
  this.getRepositories();
265
273
  this.reset();
266
274
  }
@@ -270,19 +278,19 @@ class ConsoleRepositoryBrowser {
270
278
  }
271
279
 
272
280
  get isLocalHost() {
273
- // Returns TRUE if first repository on the list is 'local host'
281
+ // Return TRUE if first repository on the list is 'local host'.
274
282
  return this.repositories.length > 0 &&
275
283
  this.repositories[0].name === 'local host';
276
284
  }
277
285
 
278
286
  getRepositories() {
279
- // Gets the list of repository names from the server
287
+ // Gets the list of repository names from the server.
280
288
  this.repositories.length = 0;
281
289
  // @@TO DO!!
282
290
  }
283
291
 
284
292
  repositoryByName(n) {
285
- // Returns the repository having name `n` if already known, otherwise NULL
293
+ // Return the repository having name `n` if already known, otherwise NULL.
286
294
  for(let i = 0; i < this.repositories.length; i++) {
287
295
  if(this.repositories[i].name === n) {
288
296
  return this.repositories[i];
@@ -293,15 +301,15 @@ class ConsoleRepositoryBrowser {
293
301
 
294
302
  asFileName(s) {
295
303
  // NOTE: asFileName is implemented as function (see below) to permit
296
- // its use prior to instantiation of the RepositoryBrowser
304
+ // its use prior to instantiation of the RepositoryBrowser.
297
305
  return stringToFileName(s);
298
306
  }
299
307
 
300
308
  }
301
309
 
302
310
  function stringToFileName(s) {
303
- // Returns string `s` with whitespace converted to a single dash, and
304
- // special characters converted to underscores
311
+ // Return string `s` with whitespace converted to a single dash, and
312
+ // special characters converted to underscores.
305
313
  return s.normalize('NFKD').trim()
306
314
  .replace(/[\s\-]+/g, '-')
307
315
  .replace(/[^A-Za-z0-9_\-]/g, '_')
@@ -315,17 +323,17 @@ class ConsoleFileManager {
315
323
 
316
324
  anyOSpath(p) {
317
325
  // Helper function that converts any path notation to platform notation
318
- // based on the predominant separator
326
+ // based on the predominant separator.
319
327
  const
320
328
  s_parts = p.split('/'),
321
329
  bs_parts = p.split('\\'),
322
330
  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
331
+ // On macOS machines, paths start with a slash, so first substring is empty.
324
332
  if(parts[0].endsWith(':') && path.sep === '\\') {
325
- // On Windows machines, add a backslash after the disk (if specified)
333
+ // On Windows machines, add a backslash after the disk (if specified).
326
334
  parts[0] += path.sep;
327
335
  }
328
- // Reassemble path for the OS of this machine
336
+ // Reassemble path for the OS of this machine.
329
337
  return path.join(...parts);
330
338
  }
331
339
 
@@ -334,18 +342,18 @@ class ConsoleFileManager {
334
342
  if(url === '') return;
335
343
  // NOTE: add this dataset to the "loading" list...
336
344
  addDistinct(dataset, MODEL.loading_datasets);
337
- // ... and allow for 3 more seconds (6 times 500 ms) to complete
345
+ // ... and allow for 3 more seconds (6 times 500 ms) to complete.
338
346
  MODEL.max_time_to_load += 6;
339
- // Passed parameter is the URL or full path
347
+ // Passed parameter is the URL or full path.
340
348
  console.log('Load data from', url);
341
349
  if(!url) {
342
350
  console.log('ERROR: No URL or path');
343
351
  return;
344
352
  }
345
353
  if(url.toLowerCase().startsWith('http')) {
346
- // URL => validate it, and then try to download its content as text
354
+ // URL => validate it, and then try to download its content as text.
347
355
  try {
348
- new URL(url); // Will throw an error if URL is not valid
356
+ new URL(url); // Will throw an error if URL is not .
349
357
  getTextFromURL(url,
350
358
  (data) => FILE_MANAGER.setData(dataset, data),
351
359
  (error) => {
@@ -361,7 +369,7 @@ class ConsoleFileManager {
361
369
  let fp = this.anyOSpath(url);
362
370
  if(!(fp.startsWith('/') || fp.startsWith('\\') || fp.indexOf(':\\') > 0)) {
363
371
  // Relative path => add path to specified data path or to the
364
- // default location user/data
372
+ // default location user/data.
365
373
  fp = path.join(SETTINGS.data_path || WORKSPACE.data, fp);
366
374
  console.log('Full path: ', fp);
367
375
  }
@@ -379,44 +387,44 @@ class ConsoleFileManager {
379
387
  setData(dataset, data) {
380
388
  if(data !== '' && UI.postResponseOK(data)) {
381
389
  // Server must return either semicolon-separated or
382
- // newline-separated string of numbers
390
+ // newline-separated string of numbers.
383
391
  if(data.indexOf(';') < 0) {
384
- // If no semicolon found, replace newlines by semicolons
392
+ // If no semicolon found, replace newlines by semicolons.
385
393
  data = data.trim().split('\n').join(';');
386
394
  }
387
- // Remove all white space
395
+ // Remove all white space.
388
396
  data = data.replace(/\s+/g, '');
389
397
  dataset.unpackDataString(data);
390
- // NOTE: remove dataset from the "loading" list
398
+ // NOTE: Remove dataset from the "loading" list.
391
399
  const i = MODEL.loading_datasets.indexOf(dataset);
392
400
  if(i >= 0) MODEL.loading_datasets.splice(i, 1);
393
401
  }
394
402
  }
395
403
 
396
404
  decryptIfNeeded(data, callback) {
397
- // Checks whether XML is encrypted; if not, processes data "as is",
398
- // otherwise decrypt using password specified in command line
405
+ // Check whether XML is encrypted; if not, processes data "as is",
406
+ // otherwise decrypt using password specified in command line.
399
407
  if(data.indexOf('model latch="') < 0) {
400
408
  setTimeout(callback, 0, data);
401
409
  return;
402
410
  }
403
411
  const xml = XML_PARSER.parseFromString(data, 'text/xml');
404
412
  const de = xml.documentElement;
405
- // Linny-R model must contain a model node
413
+ // Linny-R model must contain a model node.
406
414
  if(de.nodeName !== 'model') throw 'XML document has no model element';
407
415
  const encr_msg = {
408
416
  encryption: nodeContentByTag(de, 'content'),
409
417
  latch: nodeParameterValue(de, 'latch')
410
418
  };
411
419
  console.log('Decrypting...');
412
- // NOTE: function `tryToDecrypt` is defined in linny-r-utils.js
420
+ // NOTE: Function `tryToDecrypt` is defined in linny-r-utils.js.
413
421
  setTimeout((msg, pwd, ok, err) => tryToDecrypt(msg, pwd, ok, err), 5,
414
422
  encr_msg, SETTINGS.password,
415
- // The on_ok function
423
+ // The on_ok function.
416
424
  (data) => {
417
425
  if(data) callback(data);
418
426
  },
419
- // The on_error function
427
+ // The on_error function.
420
428
  (err) => {
421
429
  console.log(err);
422
430
  console.log('Failed to load encrypted model');
@@ -424,8 +432,7 @@ class ConsoleFileManager {
424
432
  }
425
433
 
426
434
  loadModel(fp, callback) {
427
- // Get the XML of the file specified via the command line
428
- // NOTE: asynchronous method with callback because decryption is
435
+ // Get the XML of the file specified via the command line.
429
436
  fs.readFile(fp, 'utf8', (err, data) => {
430
437
  if(err) {
431
438
  console.log(err);
@@ -438,7 +445,7 @@ class ConsoleFileManager {
438
445
  }
439
446
 
440
447
  writeStringToFile(s, fp) {
441
- // Write string `s` to path `fp`
448
+ // Write string `s` to path `fp`.
442
449
  try {
443
450
  fs.writeFileSync(fp, s);
444
451
  console.log(pluralS(s.length, 'character') + ' written to file ' + fp);
@@ -450,18 +457,19 @@ class ConsoleFileManager {
450
457
 
451
458
  } // END of class ConsoleFileManager
452
459
 
453
- // CLASS ConsoleReceiver defines a listener/interpreter for channel commands
460
+ // CLASS ConsoleReceiver defines a listener/interpreter for channel commands.
454
461
  class ConsoleReceiver {
455
462
  constructor() {
456
- // NOTE: each receiver instance listens to a "channel", being the directory
457
- // on the local host specified by the modeler
463
+ // NOTE: Each receiver instance listens to a "channel", being the
464
+ // directory on the local host specified by the modeler.
458
465
  this.channel = '';
459
- // The file name is the name of the first Linny-R model file or command file
460
- // that was found in the channel directory
466
+ // The file name is the name of the first Linny-R model file or
467
+ // command file that was found in the channel directory.
461
468
  this.file_name = '';
462
- // The name of the experiment to be run can be specified in a command file
469
+ // The name of the experiment to be run can be specified in a
470
+ // command file.
463
471
  this.experiment = '';
464
- // The call-back script is the path to file with a shell command
472
+ // The call-back script is the path to file with a shell command.
465
473
  this.call_back_script = '';
466
474
  this.active = false;
467
475
  this.solving = false;
@@ -471,16 +479,16 @@ class ConsoleReceiver {
471
479
  }
472
480
 
473
481
  setError(msg) {
474
- // Record and display error message, and immediately stop listening
482
+ // Record and display error message, and immediately stop listening.
475
483
  this.error = msg;
476
484
  UI.warn(this.error);
477
485
  this.deactivate();
478
486
  }
479
487
 
480
- log(msg) {
481
- // Logs a UI message so it will appear in the log file
482
- if(this.active) {
483
- if(!msg.startsWith('[')) {
488
+ log(msg, running=false) {
489
+ // Log a UI message so it will appear in the log file.
490
+ if(this.active || running) {
491
+ if(!(msg.startsWith('[') || running)) {
484
492
  const
485
493
  d = new Date(),
486
494
  now = d.getHours() + ':' +
@@ -493,17 +501,17 @@ class ConsoleReceiver {
493
501
  }
494
502
 
495
503
  get logReport() {
496
- // Returns log lines as a single string, and clears the log
504
+ // Return log lines as a single string, and clear the log.
497
505
  const report = this.log_lines.join('\n');
498
506
  this.log_lines.length = 0;
499
507
  return report;
500
508
  }
501
509
 
502
510
  activate() {
503
- // Sets channel path and (optional) call-back script
511
+ // Set channel path and (optional) call-back script.
504
512
  this.channel = SETTINGS.channel;
505
513
  this.call_back_script = SETTINGS.callback;
506
- // Clear experiment, error message and log
514
+ // Clear experiment, error message and log.
507
515
  this.experiment = '';
508
516
  this.error = '';
509
517
  this.log_lines.length = 0;
@@ -513,7 +521,8 @@ class ConsoleReceiver {
513
521
  }
514
522
 
515
523
  listen() {
516
- // If active, checks with local server whether there is a new command
524
+ // If active, check whether there is a new command in the channel
525
+ // directory.
517
526
  if(!this.active) return;
518
527
  const jsr = rcvrListen(this.channel);
519
528
  if(jsr.error) {
@@ -522,10 +531,10 @@ class ConsoleReceiver {
522
531
  console.log('Receiver deactivated by script');
523
532
  this.deactivate();
524
533
  } else if(jsr.file === '') {
525
- // Nothing to do => check again after the set time interval
534
+ // Nothing to do => check again after the set time interval.
526
535
  setTimeout(() => RECEIVER.listen(), this.interval);
527
536
  } else if(jsr.file && jsr.model) {
528
- // NOTE: model will NOT be encrypted, so it can be parsed
537
+ // NOTE: Model will NOT be encrypted, so it can be parsed.
529
538
  this.file_name = jsr.file;
530
539
  let msg = '';
531
540
  if(!MODEL.parseXML(jsr.model)) {
@@ -540,13 +549,13 @@ class ConsoleReceiver {
540
549
  }
541
550
  if(msg) {
542
551
  this.setError(msg);
543
- rcvrReport();
544
- // Keep listening, so check again after the time interval
552
+ rcvrReport(this.channel, this.file_name);
553
+ // Keep listening, so check again after the time interval.
545
554
  setTimeout(() => RECEIVER.listen(), this.interval);
546
555
  } else {
547
556
  this.log('Executing: ' + this.file_name);
548
557
  // NOTE: Virtual Machine will trigger the receiver's reporting
549
- // action each time the model has been solved
558
+ // action each time the model has been solved.
550
559
  if(this.experiment) {
551
560
  this.log('Starting experiment: ' + this.experiment);
552
561
  EXPERIMENT_MANAGER.startExperiment();
@@ -558,27 +567,39 @@ class ConsoleReceiver {
558
567
  }
559
568
 
560
569
  report() {
561
- // Saves the run results in the channel, or signals an error
562
- let run = '';
563
- // NOTE: Always set `solving` to FALSE
570
+ // Save the run results in the channel, or signal an error.
571
+ let run = '',
572
+ rpath = this.channel,
573
+ file = this.file_name;
574
+ // NOTE: Always set `solving` to FALSE.
564
575
  this.solving = false;
565
- if(this.experiment){
576
+ // NOTE: When reporting while the receiver is not active, report the
577
+ // results of the running experiment.
578
+ if(this.experiment || !this.active) {
566
579
  if(MODEL.running_experiment) {
567
580
  run = MODEL.running_experiment.active_combination_index;
568
- this.log(`Reporting: ${this.file_name} (run #${run})`);
581
+ this.log(`Reporting: ${file} (run #${run})`);
569
582
  }
570
583
  }
584
+ // NOTE: If receiver is not active, path and file must be set.
585
+ if(!this.active) {
586
+ rpath = 'user/reports';
587
+ // Zero-pad the run number.
588
+ file = REPOSITORY_BROWSER.asFileName(MODEL.name || 'model') +
589
+ (run === '' ? '' : '-' + run.toString().padStart(3, '0')) +
590
+ `-${compactClockTime()}`;
591
+ }
571
592
  if(MODEL.solved && !VM.halted) {
572
- // Normal execution termination => report results
593
+ // Normal execution termination => report results.
573
594
  const data = MODEL.outputData;
574
- rcvrReport(run, data[0], data[1]);
575
- // If execution completed, perform the call-back action
576
- // NOTE: for experiments, call-back is performed upon completion by
577
- // the Experiment Manager
595
+ rcvrReport(rpath, file, run, data[0], data[1]);
596
+ // If execution completed, perform the call-back action.
597
+ // NOTE: For experiments, call-back is performed upon completion by
598
+ // the Experiment Manager.
578
599
  if(!this.experiment) this.callBack();
579
600
  } else {
580
601
  if(!VM.halted && !this.error) {
581
- // No apparent cause => log this irregularity
602
+ // No apparent cause => log this irregularity.
582
603
  this.setError('ERROR: Unknown solver problem');
583
604
  rcvrAbort();
584
605
  }
@@ -586,53 +607,26 @@ class ConsoleReceiver {
586
607
  }
587
608
 
588
609
  callBack() {
589
- // Deletes the file in the channel directory (to prevent executing it again)
590
- // 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));
610
+ // Run the call-back script (if specified) only when the receiver is
611
+ // active (so not when its reporting function is called by the VM).
612
+ if(this.active) rcvrCallBack(this.call_back_script);
619
613
  }
620
614
 
621
615
  } // END of class ConsoleReceiver
622
616
 
623
- // Receiver helper functions
624
- // NOTE: these functions are adapted versions of those having the same
617
+ // Receiver helper functions.
618
+ // NOTE: These functions are adapted versions of those having the same
625
619
  // name in file `server.js`; the main difference is that those functions
626
- // respond to HTTP requests, whereas now they return objects
620
+ // respond to HTTP requests, whereas now they return objects.
627
621
 
628
622
  function rcvrListen(rpath) {
629
- // "Listens" at the channel, i.e., looks for work to do
623
+ // "Listen" at the channel, i.e., look for work to do.
630
624
  let mdl = '',
631
625
  cmd = '';
632
626
  try {
633
- // Look for a model file and/or a command file in the channel directory
627
+ // Look for a model file and/or a command file in the channel directory.
634
628
  const flist = fs.readdirSync(rpath);
635
- // NOTE: `flist` contains file names relative to the channel path
629
+ // NOTE: `flist` contains file names relative to the channel path.
636
630
  for(let i = 0; i < flist.length; i++) {
637
631
  const f = path.parse(flist[i]);
638
632
  if(f.ext === '.lnr' && !mdl) mdl = flist[i];
@@ -642,7 +636,7 @@ function rcvrListen(rpath) {
642
636
  console.log(err);
643
637
  return {error: `Failed to get file list from ${rpath}`};
644
638
  }
645
- // Model files take precedence over command files
639
+ // Model files take precedence over command files.
646
640
  if(mdl) {
647
641
  try {
648
642
  const data = fs.readFileSync(path.join(rpath, mdl), 'utf8');
@@ -659,7 +653,7 @@ function rcvrListen(rpath) {
659
653
  console.log(err);
660
654
  return {error: `Failed to read command file ${cmd}`};
661
655
  }
662
- // Special command to deactivate the receiver
656
+ // Special command to deactivate the receiver.
663
657
  if(cmd === 'STOP LISTENING') {
664
658
  return {stop: 1};
665
659
  } else {
@@ -669,24 +663,24 @@ function rcvrListen(rpath) {
669
663
  r = '',
670
664
  x = '';
671
665
  const m_r = cmd.split('@');
672
- // Repository `r` is local host unless specified
666
+ // Repository `r` is local host unless specified.
673
667
  if(m_r.length === 2) {
674
668
  r = m_r[1];
675
669
  } else if(m_r.length === 1) {
676
670
  r = 'local host';
677
671
  } else {
678
- // Multiple occurrences of @
672
+ // Multiple occurrences of @ are not allowed.
679
673
  return {error: `Invalid command "${cmd}"`};
680
674
  }
681
675
  m = m_r[0];
682
- // Module `m` can be prefixed by an experiment title
676
+ // Module `m` can be prefixed by an experiment title.
683
677
  const x_m = m.split('|');
684
678
  if(x_m.length === 2) {
685
679
  x = x_m[0];
686
680
  m = x_m[1];
687
681
  }
688
682
  // Call the repository helper function `repoLoad` with its callback
689
- // function to get the model XML
683
+ // function to get the model XML.
690
684
  return {
691
685
  file: path.parse(cmd).name,
692
686
  model: repoLoad(r.trim(), m.trim()),
@@ -694,14 +688,15 @@ function rcvrListen(rpath) {
694
688
  };
695
689
  }
696
690
  } else {
697
- // Empty fields will be interpreted as "nothing to do"
691
+ // Empty fields will be interpreted as "nothing to do".
698
692
  return {file: '', model: '', experiment: ''};
699
693
  }
700
694
  }
701
695
 
702
696
  function rcvrAbort() {
703
- const log_path = path.join(this.channel, this.file_name + '-log.txt');
704
- fs.writeFile(log_path, this.logReport, (err) => {
697
+ // Log that receiver actions have been aborted.
698
+ const log_path = path.join(RECEIVER.channel, RECEIVER.file_name + '-log.txt');
699
+ fs.writeFile(log_path, RECEIVER.logReport, (err) => {
705
700
  if(err) {
706
701
  console.log(err);
707
702
  console.log('ERROR: Failed to write event log to file', log_path);
@@ -711,9 +706,10 @@ function rcvrAbort() {
711
706
  });
712
707
  }
713
708
 
714
- function rcvrReport(run='', data='no data', stats='no statistics') {
709
+ function rcvrReport(rpath, file, run='', data='no data', stats='no statistics') {
710
+ // Write series data, statistics and log to files.
715
711
  try {
716
- let fp = path.join(this.channel, this.file_name + run + '-data.txt');
712
+ let fp = path.join(rpath, file + run + '-data.txt');
717
713
  fs.writeFileSync(fp, data);
718
714
  } catch(err) {
719
715
  console.log(err);
@@ -721,7 +717,7 @@ function rcvrReport(run='', data='no data', stats='no statistics') {
721
717
  return;
722
718
  }
723
719
  try {
724
- fp = path.join(this.channel, this.file_name + run + '-stats.txt');
720
+ fp = path.join(rpath, file + run + '-stats.txt');
725
721
  fs.writeFileSync(fp, stats);
726
722
  } catch(err) {
727
723
  console.log(err);
@@ -729,23 +725,25 @@ function rcvrReport(run='', data='no data', stats='no statistics') {
729
725
  return;
730
726
  }
731
727
  try {
732
- fp = path.join(this.channel, this.file_name + run + '-log.txt');
733
- fs.writeFileSync(fp, this.logReport);
728
+ fp = path.join(rpath, file + run + '-log.txt');
729
+ fs.writeFileSync(fp, RECEIVER.logReport);
734
730
  } catch(err) {
735
731
  console.log(err);
736
732
  console.log('ERROR: Failed to write event log to file', fp);
737
733
  }
738
- console.log('Data and statistics reported for', this.file_name);
734
+ console.log('Data and statistics reported for', file);
739
735
  }
740
736
 
741
737
  function rcvrCallBack(script) {
738
+ // Delete the file in the channel directory (to prevent executing it
739
+ // again) and activate the call-back script on the local server.
742
740
  let file_type = '',
743
- cpath = path.join(this.channel, this.file_name + '.lnr');
741
+ cpath = path.join(RECEIVER.channel, RECEIVER.file_name + '.lnr');
744
742
  try {
745
743
  fs.accessSync(cpath);
746
744
  file_type = 'model';
747
745
  } catch(err) {
748
- cpath = path.join(this.channel, this.file_name + '.lnrc');
746
+ cpath = path.join(RECEIVER.channel, RECEIVER.file_name + '.lnrc');
749
747
  try {
750
748
  fs.accessSync(cpath);
751
749
  file_type = 'command';
@@ -792,7 +790,7 @@ function rcvrCallBack(script) {
792
790
  //
793
791
 
794
792
  function commandLineSettings() {
795
- // Sets default settings, and then checks the command line arguments
793
+ // Set default settings, and then check the command line arguments.
796
794
  const settings = {
797
795
  cli_name: (PLATFORM.startsWith('win') ? 'Command Prompt' : 'Terminal'),
798
796
  check: false,
@@ -800,6 +798,8 @@ function commandLineSettings() {
800
798
  preferred_solver: '',
801
799
  report: '',
802
800
  run: false,
801
+ x_title: '',
802
+ x_list: false,
803
803
  solver: '',
804
804
  solver_path: '',
805
805
  user_dir: path.join(WORKING_DIRECTORY, 'user'),
@@ -886,24 +886,29 @@ function commandLineSettings() {
886
886
  // Check is repository exists, etc.
887
887
  // @@@TO DO!
888
888
  } else if(av[0] === 'xrun') {
889
- // NOTE: use original argument to preserve upper/lower case
890
- const x = process.argv[i].split('=')[1].split('#');
891
- settings.x_title = x[0];
892
- settings.x_runs = [];
893
- x.splice(0, 1);
894
- // In case of multiple #, interpret them as commas
895
- const r = x.join(',').split(',');
896
- for(let i = 0; i < r.length; i++) {
897
- if(/^\d+$/.test(r[i])) {
898
- settings.x_runs.push(parseInt(r[i]));
899
- } else {
900
- console.log(`WARNING: Invalid run number "${r[i]}"`);
889
+ if(!av[1].trim()) {
890
+ // NOTE: `x_title` = TRUE indicates: list available experiments.
891
+ settings.x_title = true;
892
+ } else {
893
+ // NOTE: use original argument to preserve upper/lower case
894
+ const x = process.argv[i].split('=')[1].split('#');
895
+ settings.x_title = x[0].trim();
896
+ if(!settings.x_title) settings.x_title = true;
897
+ settings.x_runs = [];
898
+ x.splice(0, 1);
899
+ // In case of multiple #, interpret them as commas.
900
+ const r = (x.length > 0 ? x.join(',').split(',') : []);
901
+ for(let i = 0; i < r.length; i++) {
902
+ if(/^\d+$/.test(r[i])) {
903
+ settings.x_runs.push(parseInt(r[i]));
904
+ } else {
905
+ console.log(`WARNING: Invalid run number "${r[i]}"`);
906
+ }
907
+ }
908
+ // If only invalid numbers, do not run the experiment at all.
909
+ if(r.length > 0 && settings.x_runs.length === 0) {
910
+ settings.x_runs = false;
901
911
  }
902
- }
903
- // If only invalid numbers, do not run the experiment at all
904
- if(r.length > 0 && settings.x_runs === 0) {
905
- console.log(`Experiment "${settings.x_title}" will not be run`);
906
- settings.x_title = '';
907
912
  }
908
913
  } else {
909
914
  // Terminate script
@@ -913,126 +918,13 @@ function commandLineSettings() {
913
918
  }
914
919
  }
915
920
  }
916
- // If help is asked for, or command is invalid, show usage and then quit
921
+ // If help is asked for, or command is invalid, show usage and then quit.
917
922
  if(show_usage) {
918
923
  console.log(usage);
919
924
  process.exit();
920
925
  }
921
- // Perform version check only if asked for
926
+ // Perform version check only if asked for.
922
927
  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
928
  return settings;
1037
929
  }
1038
930
 
@@ -1081,6 +973,8 @@ function createWorkspace() {
1081
973
  }
1082
974
  // The file containing name, URL and access token for remote repositories
1083
975
  ws.repositories = path.join(SETTINGS.user_dir, 'repositories.cfg');
976
+ // For completeness, add path to Linny-R directory.
977
+ ws.working_directory = WORKING_DIRECTORY;
1084
978
  // Return the updated workspace object
1085
979
  return ws;
1086
980
  }
@@ -1115,7 +1009,7 @@ function checkForUpdates() {
1115
1009
  }
1116
1010
 
1117
1011
  // Initialize the solver
1118
- const SOLVER = new MILPSolver(SETTINGS, WORKSPACE);
1012
+ const SOLVER = new MILPSolver(SETTINGS.preferred_solver, WORKSPACE);
1119
1013
  /*
1120
1014
  // Initialize the dialog for interaction with the user
1121
1015
  const PROMPTER = readline.createInterface(
@@ -1156,54 +1050,105 @@ global.RECEIVER = new ConsoleReceiver();
1156
1050
  global.IO_CONTEXT = null;
1157
1051
  global.MODEL = new LinnyRModel();
1158
1052
 
1159
- // Connect the virtual machine (may prompt for password)
1053
+ // Connect the virtual machine (may prompt for password).
1160
1054
  MONITOR.connectToServer();
1161
1055
 
1162
- // Load the model if specified
1056
+ // Load the model if specified.
1163
1057
  if(SETTINGS.model_path) {
1164
1058
  FILE_MANAGER.loadModel(SETTINGS.model_path, (model) => {
1165
- // Command `run` takes precedence over `xrun`
1059
+ // Command `run` takes precedence over `xrun`.
1166
1060
  if(SETTINGS.run) {
1167
1061
  MONITOR.show_log = SETTINGS.verbose;
1062
+ // Callback hook "tells" VM where to return after solving.
1168
1063
  VM.callback = () => {
1169
1064
  const od = model.outputData;
1170
- // Output data is two-string list [time series, statistics]
1065
+ // Output data is two-string list [time series, statistics].
1171
1066
  if(SETTINGS.report) {
1172
- // Output time series
1067
+ // Output time series.
1173
1068
  FILE_MANAGER.writeStringToFile(od[0],
1174
1069
  SETTINGS.report + '-series.txt');
1175
- // Output statistics
1070
+ // Output statistics.
1176
1071
  FILE_MANAGER.writeStringToFile(od[1],
1177
1072
  SETTINGS.report + '-stats.txt');
1178
- } else {
1179
- // Output strings to console
1073
+ } else if(!MODEL.report_results) {
1074
+ // Output strings to console.
1180
1075
  console.log(od[0]);
1181
1076
  console.log(od[1]);
1182
1077
  }
1078
+ // Clear callback hook (to be neat).
1183
1079
  VM.callback = null;
1184
1080
  };
1081
+ // NOTE: Solver preference in model overrides default solver.
1082
+ const mps = MODEL.preferred_solver;
1083
+ if(mps && SOLVER.solver_list.hasOwnProperty(mps)) {
1084
+ VM.solver_name = mps;
1085
+ SOLVER.id = mps;
1086
+ console.log(`Using solver ${SOLVER.name} (model preference)`);
1087
+ }
1185
1088
  VM.solveModel();
1186
1089
  } else if(SETTINGS.x_title) {
1187
- const xi = MODEL.indexOfExperiment(SETTINGS.x_title);
1188
- if(xi < 0) {
1189
- console.log(`WARNING: Unknown experiment "${SETTINGS.x_title}"`);
1090
+ if(SETTINGS.x_title === true) {
1091
+ // List titles of experiments in model.
1092
+ if(MODEL.experiments.length === 0) {
1093
+ console.log('NOTE: Model defines no experiments');
1094
+ } else {
1095
+ console.log('No experiment specified. Options are:');
1096
+ for(let i = 0; i < MODEL.experiments.length; i++) {
1097
+ console.log(`${i+1}. ${MODEL.experiments[i].title}`);
1098
+ }
1099
+ }
1190
1100
  } else {
1191
- EXPERIMENT_MANAGER.selectExperiment(SETTINGS.x_title);
1192
- EXPERIMENT_MANAGER.callback = () => {
1193
- const od = model.outputData;
1194
- console.log(od[0]);
1195
- console.log(od[1]);
1196
- VM.callback = null;
1197
- };
1198
- if(SETTINGS.x_runs.length === 0) {
1199
- // Perform complete experiment
1200
- EXPERIMENT_MANAGER.startExperiment();
1101
+ // Check whether experiment exists.
1102
+ let xi = MODEL.indexOfExperiment(SETTINGS.x_title);
1103
+ // NOTE: Experiments can also be specified by their index number.
1104
+ if(xi < 0) {
1105
+ xi = safeStrToInt(SETTINGS.x_title, 0) - 1;
1106
+ if(xi >= MODEL.experiments.length) xi = -1;
1107
+ if(xi >= 0) SETTINGS.x_title = MODEL.experiments[xi].title;
1108
+ }
1109
+ if(xi < 0) {
1110
+ console.log(`WARNING: Unknown experiment "${SETTINGS.x_title}"`);
1201
1111
  } else {
1202
- // Announce, and then perform, only the selected runs
1203
- console.log('Experiment:', SETTINGS.x_title,
1204
- 'Runs:', SETTINGS.x_runs);
1205
- for(let i = 0; i < SETTINGS.x_runs.length; i++) {
1206
- EXPERIMENT_MANAGER.startExperiment(SETTINGS.x_runs[i]);
1112
+ console.log('Experiment:', SETTINGS.x_title);
1113
+ EXPERIMENT_MANAGER.selectExperiment(SETTINGS.x_title);
1114
+ const x = EXPERIMENT_MANAGER.selected_experiment;
1115
+ if(!x) {
1116
+ console.log('ERROR: Experiment not found');
1117
+ return;
1118
+ }
1119
+ // NOTE: Only set callback when model does not auto-report runs.
1120
+ if(!MODEL.report_results) EXPERIMENT_MANAGER.callback = () => {
1121
+ const od = model.outputData;
1122
+ console.log(od[0]);
1123
+ console.log(od[1]);
1124
+ VM.callback = null;
1125
+ };
1126
+ if(SETTINGS.x_runs.length === 0) {
1127
+ // Perform complete experiment.
1128
+ EXPERIMENT_MANAGER.startExperiment();
1129
+ } else {
1130
+ // Announce, and then perform, only the selected runs.
1131
+ console.log('Runs:', SETTINGS.x_runs);
1132
+ for(let i = SETTINGS.x_runs.length - 1; i >= 0; i--) {
1133
+ const rc = x.combinations[SETTINGS.x_runs[i]];
1134
+ if(!rc) {
1135
+ console.log(
1136
+ 'WARNING: For this experiment, run number range is ' +
1137
+ `[0 - ${x.combinations.length - 1}]`);
1138
+ return;
1139
+ }
1140
+ }
1141
+ SETTINGS.run_index = 0;
1142
+ EXPERIMENT_MANAGER.callback = () => {
1143
+ SETTINGS.run_index++;
1144
+ if(SETTINGS.run_index < SETTINGS.x_runs.length) {
1145
+ EXPERIMENT_MANAGER.startExperiment(
1146
+ SETTINGS.x_runs[SETTINGS.run_index]);
1147
+ } else {
1148
+ VM.callback = null;
1149
+ }
1150
+ };
1151
+ EXPERIMENT_MANAGER.startExperiment(SETTINGS.x_runs[0]);
1207
1152
  }
1208
1153
  }
1209
1154
  }