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/README.md +7 -7
- package/console.js +250 -305
- package/package.json +1 -1
- package/server.js +40 -134
- package/static/index.html +64 -5
- package/static/linny-r.css +41 -0
- package/static/scripts/linny-r-ctrl.js +47 -42
- package/static/scripts/linny-r-gui-controller.js +153 -15
- package/static/scripts/linny-r-gui-monitor.js +21 -9
- package/static/scripts/linny-r-gui-paper.js +18 -18
- package/static/scripts/linny-r-gui-receiver.js +5 -5
- package/static/scripts/linny-r-milp.js +363 -188
- package/static/scripts/linny-r-model.js +43 -29
- package/static/scripts/linny-r-utils.js +20 -3
- package/static/scripts/linny-r-vm.js +162 -73
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
|
-
//
|
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
|
-
//
|
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:
|
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:
|
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
|
111
|
-
// command
|
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
|
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
|
177
|
-
// the console (will be ignored by the
|
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
|
-
//
|
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
|
-
|
186
|
-
|
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.
|
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
|
-
|
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
|
-
//
|
230
|
-
// NOTE:
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
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
|
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:
|
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
|
-
//
|
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:
|
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:
|
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
|
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
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
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,
|
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:
|
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
|
-
//
|
562
|
-
let run = ''
|
563
|
-
|
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
|
-
|
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: ${
|
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:
|
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
|
-
//
|
590
|
-
//
|
591
|
-
|
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:
|
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
|
-
// "
|
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
|
-
|
704
|
-
|
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(
|
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(
|
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(
|
733
|
-
fs.writeFileSync(fp,
|
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',
|
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(
|
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(
|
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
|
-
//
|
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
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
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
|
-
|
1188
|
-
|
1189
|
-
|
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
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
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
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
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
|
}
|