linny-r 1.6.8 → 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/README.md +7 -7
- package/console.js +104 -220
- package/package.json +1 -1
- package/server.js +39 -133
- package/static/index.html +64 -5
- package/static/linny-r.css +41 -0
- package/static/scripts/linny-r-ctrl.js +31 -29
- 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-milp.js +363 -188
- package/static/scripts/linny-r-model.js +26 -12
- package/static/scripts/linny-r-vm.js +18 -4
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
|
|
@@ -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
|
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
|
177
|
-
// the console (will be ignored by the
|
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
|
-
//
|
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.
|
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
|
-
|
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
|
-
//
|
230
|
-
// NOTE:
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
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
|
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:
|
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
|
-
//
|
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:
|
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
|
-
|
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: ${
|
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
|
-
|
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(
|
704
|
-
fs.writeFile(log_path,
|
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(
|
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(
|
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(
|
733
|
-
fs.writeFileSync(fp,
|
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',
|
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(
|
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(
|
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);
|