linny-r 1.3.2 → 1.3.4
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 +40 -7
- package/package.json +1 -1
- package/server.js +38 -3
- package/static/scripts/linny-r-gui.js +50 -8
- package/static/scripts/linny-r-milp.js +99 -6
- package/static/scripts/linny-r-model.js +17 -11
- package/static/scripts/linny-r-vm.js +42 -91
package/console.js
CHANGED
@@ -87,7 +87,7 @@ console.log('Module directory:', MODULE_DIRECTORY);
|
|
87
87
|
console.log('Working directory:', WORKING_DIRECTORY);
|
88
88
|
|
89
89
|
// Currently, these external solvers are supported:
|
90
|
-
const SUPPORTED_SOLVERS = ['gurobi', 'scip', 'lp_solve'];
|
90
|
+
const SUPPORTED_SOLVERS = ['gurobi', 'cplex', 'scip', 'lp_solve'];
|
91
91
|
|
92
92
|
const
|
93
93
|
// Load the MILP solver (dependent on Node.js: `fs`, `os` and `path`)
|
@@ -126,7 +126,7 @@ Possible options are:
|
|
126
126
|
[name]-stats.txt in (workspace)/reports
|
127
127
|
run will run the loaded model
|
128
128
|
solver=[name] will select solver [name], or warn if not found
|
129
|
-
(name choices: Gurobi, SCIP or LP_solve)
|
129
|
+
(name choices: Gurobi, CPLEX, SCIP or LP_solve)
|
130
130
|
user=[identifier] user ID will be used to log onto remote servers
|
131
131
|
verbose will output solver messages to the console
|
132
132
|
workspace=[path] will create workspace in [path] instead of (main)/user
|
@@ -932,6 +932,23 @@ function commandLineSettings() {
|
|
932
932
|
gurobi_path = path_list[i];
|
933
933
|
max_v = parseInt(match[1]);
|
934
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
|
+
}
|
935
952
|
match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
|
936
953
|
if(match) scip_path = path_list[i];
|
937
954
|
match = path_list[i].match(/inkscape/i);
|
@@ -964,8 +981,25 @@ function commandLineSettings() {
|
|
964
981
|
'WARNING: Failed to access the Gurobi command line application');
|
965
982
|
}
|
966
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
|
+
}
|
967
1001
|
// Check if scip(.exe) exists in its directory
|
968
|
-
|
1002
|
+
sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
969
1003
|
const need_scip = !settings.solver || settings.preferred_solver === 'scip';
|
970
1004
|
try {
|
971
1005
|
fs.accessSync(sp, fs.constants.X_OK);
|
@@ -982,10 +1016,9 @@ function commandLineSettings() {
|
|
982
1016
|
}
|
983
1017
|
}
|
984
1018
|
// Check if lp_solve(.exe) exists in main directory
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
|
1019
|
+
sp = path.join(WORKING_DIRECTORY,
|
1020
|
+
'lp_solve' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1021
|
+
const need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
|
989
1022
|
try {
|
990
1023
|
fs.accessSync(sp, fs.constants.X_OK);
|
991
1024
|
console.log('Path to LP_solve:', sp);
|
package/package.json
CHANGED
package/server.js
CHANGED
@@ -123,7 +123,7 @@ function checkNodeModule(name) {
|
|
123
123
|
}
|
124
124
|
|
125
125
|
// Currently, these external solvers are supported:
|
126
|
-
const SUPPORTED_SOLVERS = ['gurobi', 'scip', 'lp_solve'];
|
126
|
+
const SUPPORTED_SOLVERS = ['gurobi', 'cplex', 'scip', 'lp_solve'];
|
127
127
|
|
128
128
|
// Load class MILPSolver
|
129
129
|
const MILPSolver = require('./static/scripts/linny-r-milp.js');
|
@@ -1458,6 +1458,7 @@ function commandLineSettings() {
|
|
1458
1458
|
const path_list = process.env.PATH.split(path.delimiter);
|
1459
1459
|
let gurobi_path = '',
|
1460
1460
|
scip_path = '',
|
1461
|
+
cplex_path = '',
|
1461
1462
|
match,
|
1462
1463
|
max_v = -1;
|
1463
1464
|
for(let i = 0; i < path_list.length; i++) {
|
@@ -1466,6 +1467,23 @@ function commandLineSettings() {
|
|
1466
1467
|
gurobi_path = path_list[i];
|
1467
1468
|
max_v = parseInt(match[1]);
|
1468
1469
|
}
|
1470
|
+
match = path_list[i].match(/[\/\\]cplex[\/\\]bin/i);
|
1471
|
+
if(match) {
|
1472
|
+
cplex_path = path_list[i];
|
1473
|
+
} else {
|
1474
|
+
// NOTE: CPLEX may create its own environment variable for its paths
|
1475
|
+
match = path_list[i].match(/%(.*cplex.*)%/i);
|
1476
|
+
if(match) {
|
1477
|
+
const cpl = process.env[match[1]].split(path.delimiter);
|
1478
|
+
for(let i = 0; i < cpl.length; i++) {
|
1479
|
+
match = cpl[i].match(/[\/\\]cplex[\/\\]bin/i);
|
1480
|
+
if(match) {
|
1481
|
+
cplex_path = cpl[i];
|
1482
|
+
break;
|
1483
|
+
}
|
1484
|
+
}
|
1485
|
+
}
|
1486
|
+
}
|
1469
1487
|
match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
|
1470
1488
|
if(match) scip_path = path_list[i];
|
1471
1489
|
match = path_list[i].match(/inkscape/i);
|
@@ -1498,8 +1516,25 @@ function commandLineSettings() {
|
|
1498
1516
|
'WARNING: Failed to access the Gurobi command line application');
|
1499
1517
|
}
|
1500
1518
|
}
|
1519
|
+
// Check if cplex(.exe) exists in its directory
|
1520
|
+
let sp = path.join(cplex_path, 'cplex' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1521
|
+
const need_cplex = !settings.solver || settings.preferred_solver === 'cplex';
|
1522
|
+
try {
|
1523
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
1524
|
+
console.log('Path to CPLEX:', sp);
|
1525
|
+
if(need_cplex) {
|
1526
|
+
settings.solver = 'cplex';
|
1527
|
+
settings.solver_path = sp;
|
1528
|
+
}
|
1529
|
+
} catch(err) {
|
1530
|
+
// Only report error if CPLEX is needed
|
1531
|
+
if(need_cplex) {
|
1532
|
+
console.log(err.message);
|
1533
|
+
console.log('WARNING: CPLEX application not found in', sp);
|
1534
|
+
}
|
1535
|
+
}
|
1501
1536
|
// Check if scip(.exe) exists in its directory
|
1502
|
-
|
1537
|
+
sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1503
1538
|
const need_scip = !settings.solver || settings.preferred_solver === 'scip';
|
1504
1539
|
try {
|
1505
1540
|
fs.accessSync(sp, fs.constants.X_OK);
|
@@ -1595,7 +1630,7 @@ function createWorkspace() {
|
|
1595
1630
|
data: path.join(SETTINGS.user_dir, 'data'),
|
1596
1631
|
diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
|
1597
1632
|
modules: path.join(SETTINGS.user_dir, 'modules'),
|
1598
|
-
solver_output: path.join(SETTINGS.user_dir, 'solver')
|
1633
|
+
solver_output: path.join(SETTINGS.user_dir, 'solver')
|
1599
1634
|
};
|
1600
1635
|
// Create these sub-directories if not aready there
|
1601
1636
|
try {
|
@@ -5273,9 +5273,30 @@ class GUIController extends Controller {
|
|
5273
5273
|
md.element('actor').value = mapping.actor || '';
|
5274
5274
|
md.element('prefix').value = mapping.prefix || '';
|
5275
5275
|
const
|
5276
|
-
|
5276
|
+
tc = (mapping.top_clusters ?
|
5277
|
+
Object.keys(mapping.top_clusters).sort(ciCompare) : []),
|
5278
|
+
ft = (mapping.from_to ?
|
5279
|
+
Object.keys(mapping.from_to).sort(ciCompare) : []),
|
5277
5280
|
sl = [];
|
5281
|
+
if(tc.length) {
|
5282
|
+
sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
|
5283
|
+
'Names for top-level clusters:</div>');
|
5284
|
+
// Add text inputs for selected cluster nodes
|
5285
|
+
for(let i = 0; i < tc.length; i++) {
|
5286
|
+
const
|
5287
|
+
ti = mapping.top_clusters[tc[i]],
|
5288
|
+
state = (ti === tc[i] ? 'color: #e09000; ' :
|
5289
|
+
this.validName(ti) ? 'color: #0000c0; ' :
|
5290
|
+
'font-style: italic; color: red; ');
|
5291
|
+
sl.push('<div class="paste-option"><span>', tc[i], '</span> ',
|
5292
|
+
'<div class="paste-select"><input id="paste-selc-', i,
|
5293
|
+
'" type="text" style="', state, 'font-size: 12px" value="',
|
5294
|
+
ti, '"></div></div>');
|
5295
|
+
}
|
5296
|
+
}
|
5278
5297
|
if(ft.length) {
|
5298
|
+
sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
|
5299
|
+
'Mapping of nodes to link from/to:</div>');
|
5279
5300
|
// Add selectors for unresolved FROM/TO nodes
|
5280
5301
|
for(let i = 0; i < ft.length; i++) {
|
5281
5302
|
const ti = mapping.from_to[ft[i]];
|
@@ -5286,8 +5307,8 @@ class GUIController extends Controller {
|
|
5286
5307
|
'</option></select></div></div>');
|
5287
5308
|
}
|
5288
5309
|
}
|
5289
|
-
md.element('scroll-area').innerHTML = sl.join('');
|
5290
5310
|
}
|
5311
|
+
md.element('scroll-area').innerHTML = sl.join('');
|
5291
5312
|
// Open dialog, which will call pasteSelection(...) on OK
|
5292
5313
|
this.paste_modal.show();
|
5293
5314
|
}
|
@@ -5297,10 +5318,18 @@ class GUIController extends Controller {
|
|
5297
5318
|
// proceeds to paste
|
5298
5319
|
const
|
5299
5320
|
md = this.paste_modal,
|
5300
|
-
mapping = Object.assign(md.mapping, {})
|
5321
|
+
mapping = Object.assign(md.mapping, {}),
|
5322
|
+
tc = (mapping.top_clusters ?
|
5323
|
+
Object.keys(mapping.top_clusters).sort(ciCompare) : []),
|
5324
|
+
ft = (mapping.from_to ?
|
5325
|
+
Object.keys(mapping.from_to).sort(ciCompare) : []);
|
5301
5326
|
mapping.actor = md.element('actor').value;
|
5302
5327
|
mapping.prefix = md.element('prefix').value.trim();
|
5303
5328
|
mapping.increment = true;
|
5329
|
+
for(let i = 0; i < tc.length; i++) {
|
5330
|
+
const cn = md.element('selc-' + i).value.trim();
|
5331
|
+
if(this.validName(cn)) mapping.top_clusters[tc[i]] = cn;
|
5332
|
+
}
|
5304
5333
|
this.pasteSelection(mapping);
|
5305
5334
|
}
|
5306
5335
|
|
@@ -5325,6 +5354,7 @@ console.log('HERE xml', xml);
|
|
5325
5354
|
entities_node = childNodeByTag(xml, 'entities'),
|
5326
5355
|
from_tos_node = childNodeByTag(xml, 'from-tos'),
|
5327
5356
|
extras_node = childNodeByTag(xml, 'extras'),
|
5357
|
+
selc_node = childNodeByTag(xml, 'selected-clusters'),
|
5328
5358
|
selection_node = childNodeByTag(xml, 'selection'),
|
5329
5359
|
actor_names = [],
|
5330
5360
|
new_entities = [],
|
@@ -5335,7 +5365,7 @@ console.log('HERE xml', xml);
|
|
5335
5365
|
|
5336
5366
|
function fullName(node) {
|
5337
5367
|
// Returns full entity name inferred from XML node data
|
5338
|
-
if(node.nodeName === 'from-to') {
|
5368
|
+
if(node.nodeName === 'from-to' || node.nodeName === 'selc') {
|
5339
5369
|
const
|
5340
5370
|
n = xmlDecoded(nodeParameterValue(node, 'name')),
|
5341
5371
|
an = xmlDecoded(nodeParameterValue(node, 'actor-name'));
|
@@ -5525,6 +5555,17 @@ console.log('HERE xml', xml);
|
|
5525
5555
|
if(parseInt(mts) === MODEL.time_created.getTime() &&
|
5526
5556
|
ca === fca && mapping.from_prefix === mapping.to_prefix &&
|
5527
5557
|
!(mapping.prefix || mapping.actor || mapping.increment)) {
|
5558
|
+
// Prompt for names of selected cluster nodes
|
5559
|
+
if(selc_node.childNodes.length && !mapping.prefix) {
|
5560
|
+
mapping.top_clusters = {};
|
5561
|
+
for(let i = 0; i < selc_node.childNodes.length; i++) {
|
5562
|
+
const
|
5563
|
+
c = selc_node.childNodes[i],
|
5564
|
+
fn = fullName(c),
|
5565
|
+
mn = mappedName(fn);
|
5566
|
+
mapping.top_clusters[fn] = mn;
|
5567
|
+
}
|
5568
|
+
}
|
5528
5569
|
this.promptForMapping(mapping);
|
5529
5570
|
return;
|
5530
5571
|
}
|
@@ -6525,11 +6566,12 @@ class GUIMonitor {
|
|
6525
6566
|
})
|
6526
6567
|
.then((data) => {
|
6527
6568
|
try {
|
6528
|
-
const
|
6529
|
-
|
6530
|
-
|
6531
|
-
|
6569
|
+
const
|
6570
|
+
jsr = JSON.parse(data),
|
6571
|
+
svr = `Solver on ${jsr.server} is ${jsr.solver}`;
|
6572
|
+
if(jsr.solver !== VM.solver_name) UI.notify(svr);
|
6532
6573
|
VM.solver_name = jsr.solver;
|
6574
|
+
document.getElementById('host-logo').title = svr;
|
6533
6575
|
} catch(err) {
|
6534
6576
|
console.log(err, data);
|
6535
6577
|
UI.alert('ERROR: Unexpected data from server: ' +
|
@@ -82,6 +82,27 @@ module.exports = class MILPSolver {
|
|
82
82
|
14: 'Optimization still in progress',
|
83
83
|
15: 'User-specified objective limit has been reached'
|
84
84
|
};
|
85
|
+
} else if(this.id === 'cplex') {
|
86
|
+
this.ext = '.lp';
|
87
|
+
this.user_model = path.join(workspace.solver_output, 'usr_model.lp');
|
88
|
+
this.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
|
89
|
+
this.solution = path.join(workspace.solver_output, 'model.sol');
|
90
|
+
// NOTE: CPLEX log file is located in the Linny-R working directory
|
91
|
+
this.log = path.join(workspace.solver_output, 'cplex.log');
|
92
|
+
// NOTE: CPLEX command line accepts space separated commands ...
|
93
|
+
this.args = [
|
94
|
+
'read ' + this.user_model,
|
95
|
+
'write ' + this.solver_model + ' lp',
|
96
|
+
'set timelimit 300',
|
97
|
+
'optimize',
|
98
|
+
'write ' + this.solution + ' 0',
|
99
|
+
'quit'
|
100
|
+
];
|
101
|
+
// ... when CPLEX is called with -c option; then each command must be
|
102
|
+
// enclosed in double quotes; SCIP outputs its messages to a log file
|
103
|
+
// terminal, so these must be caputured in a log file
|
104
|
+
this.solve_cmd = 'cplex -c "' + this.args.join('" "') + '"';
|
105
|
+
this.errors = {};
|
85
106
|
} else if(this.id === 'scip') {
|
86
107
|
this.ext = '.lp';
|
87
108
|
this.user_model = path.join(workspace.solver_output, 'usr_model.lp');
|
@@ -217,6 +238,18 @@ module.exports = class MILPSolver {
|
|
217
238
|
this.args[0] = 'TimeLimit=' + timeout;
|
218
239
|
const options = {windowsHide: true};
|
219
240
|
spawn = child_process.spawnSync(this.solver_path, this.args, options);
|
241
|
+
} else if(this.id === 'cplex') {
|
242
|
+
// Delete previous solver model file (if any)
|
243
|
+
try {
|
244
|
+
if(this.solver_model) fs.unlinkSync(this.solver_model);
|
245
|
+
} catch(err) {
|
246
|
+
// Ignore error
|
247
|
+
}
|
248
|
+
// Spawn using the LP_solve approach
|
249
|
+
const
|
250
|
+
cmd = this.solve_cmd.replace(/timelimit \d+/, `timelimit ${timeout}`),
|
251
|
+
options = {shell: true, cwd: 'user/solver', stdio: 'ignore', windowsHide: true};
|
252
|
+
spawn = child_process.spawnSync(cmd, options);
|
220
253
|
} else if(this.id === 'scip') {
|
221
254
|
// When using SCIP, take the LP_solve approach
|
222
255
|
const
|
@@ -311,6 +344,64 @@ module.exports = class MILPSolver {
|
|
311
344
|
result.error = 'No solution found';
|
312
345
|
}
|
313
346
|
}
|
347
|
+
} else if(this.id === 'cplex') {
|
348
|
+
result.seconds = 0;
|
349
|
+
const
|
350
|
+
msg = fs.readFileSync(this.log, 'utf8'),
|
351
|
+
no_license = (msg.indexOf('No license found') >= 0);
|
352
|
+
// `messages` must be an array of strings
|
353
|
+
result.messages = msg.split(os.EOL);
|
354
|
+
let solved = false,
|
355
|
+
output = [];
|
356
|
+
if(no_license) {
|
357
|
+
result.error = 'Too many variables for unlicensed CPLEX solver';
|
358
|
+
result.status = -13;
|
359
|
+
} else if(result.status !== 0) {
|
360
|
+
// Non-zero solver exit code indicates serious trouble
|
361
|
+
result.error = 'CPLEX solver terminated with error';
|
362
|
+
result.status = -13;
|
363
|
+
} else {
|
364
|
+
try {
|
365
|
+
output = fs.readFileSync(this.solution, 'utf8').trim();
|
366
|
+
if(output.indexOf('CPLEXSolution') >= 0) {
|
367
|
+
solved = true;
|
368
|
+
output = output.split(os.EOL);
|
369
|
+
}
|
370
|
+
} catch(err) {
|
371
|
+
console.log('No CPLEX solution file');
|
372
|
+
}
|
373
|
+
}
|
374
|
+
if(solved) {
|
375
|
+
// CPLEX saves solution as XML, but for now just extract the
|
376
|
+
// status and then the variables
|
377
|
+
let i = 0;
|
378
|
+
while(i < output.length) {
|
379
|
+
const s = output[i].split('"');
|
380
|
+
if(s[0].indexOf('objectiveValue') >= 0) {
|
381
|
+
result.obj = s[1];
|
382
|
+
} else if(s[0].indexOf('solutionStatusValue') >= 0) {
|
383
|
+
result.status = s[1];
|
384
|
+
} else if(s[0].indexOf('solutionStatusString') >= 0) {
|
385
|
+
result.error = s[1];
|
386
|
+
break;
|
387
|
+
}
|
388
|
+
i++;
|
389
|
+
}
|
390
|
+
if(['1', '101', '102'].indexOf(result.status) >= 0) {
|
391
|
+
result.status = 0;
|
392
|
+
result.error = '';
|
393
|
+
}
|
394
|
+
// Fill dictionary with variable name: value entries
|
395
|
+
while(i < output.length) {
|
396
|
+
const m = output[i].match(/^.*name="(X[^"]+)".*value="([^"]+)"/);
|
397
|
+
if(m !== null) x_dict[m[1]] = m[2];
|
398
|
+
i++;
|
399
|
+
}
|
400
|
+
// Fill the solution vector, adding 0 for missing columns
|
401
|
+
getValuesFromDict();
|
402
|
+
} else {
|
403
|
+
console.log('No solution found');
|
404
|
+
}
|
314
405
|
} else if(this.id === 'scip') {
|
315
406
|
result.seconds = 0;
|
316
407
|
// `messages` must be an array of strings
|
@@ -332,19 +423,21 @@ module.exports = class MILPSolver {
|
|
332
423
|
const m = result.messages[i];
|
333
424
|
if(m.startsWith('SCIP Status')) {
|
334
425
|
if(m.indexOf('problem is solved') >= 0) {
|
335
|
-
|
426
|
+
if(m.indexOf('infeasible') >= 0) {
|
427
|
+
result.status = (m.indexOf('unbounded') >= 0 ? 14 : 12);
|
428
|
+
} else if(m.indexOf('unbounded') >= 0) {
|
429
|
+
result.status = 13;
|
430
|
+
} else {
|
431
|
+
solved = true;
|
432
|
+
}
|
336
433
|
} else if(m.indexOf('interrupted') >= 0) {
|
337
434
|
if(m.indexOf('time limit') >= 0) {
|
338
435
|
result.status = 5;
|
339
436
|
} else if(m.indexOf('memory limit') >= 0) {
|
340
437
|
result.status = 6;
|
341
|
-
} else if(m.indexOf('infeasible') >= 0) {
|
342
|
-
result.status = (m.indexOf('unbounded') >= 0 ? 14 : 12);
|
343
|
-
} else if(m.indexOf('unbounded') >= 0) {
|
344
|
-
result.status = 13;
|
345
438
|
}
|
346
|
-
result.error = this.errors[result.status];
|
347
439
|
}
|
440
|
+
if(result.status) result.error = this.errors[result.status];
|
348
441
|
} else if (m.startsWith('Solving Time')) {
|
349
442
|
result.seconds = parseFloat(m.split(':')[1]);
|
350
443
|
}
|
@@ -1609,16 +1609,20 @@ class LinnyRModel {
|
|
1609
1609
|
extras = [],
|
1610
1610
|
from_tos = [],
|
1611
1611
|
xml = [],
|
1612
|
-
ft_xml = [],
|
1613
1612
|
extra_xml = [],
|
1613
|
+
ft_xml = [],
|
1614
|
+
selc_xml = [],
|
1614
1615
|
selected_xml = [];
|
1615
1616
|
for(let i = 0; i < this.selection.length; i++) {
|
1616
1617
|
const obj = this.selection[i];
|
1617
1618
|
entities[obj.type].push(obj);
|
1618
|
-
|
1619
|
+
if(obj instanceof Cluster) selc_xml.push(
|
1620
|
+
'<selc name="', xmlEncoded(obj.name),
|
1621
|
+
'" actor-name="', xmlEncoded(obj.actor.name), '"></selc>');
|
1622
|
+
selected_xml.push(`<sel>${xmlEncoded(obj.displayName)}</sel>`);
|
1619
1623
|
}
|
1620
|
-
// Expand clusters by adding all
|
1621
|
-
// lists
|
1624
|
+
// Expand (sub)clusters by adding all their model entities to their
|
1625
|
+
// respective lists
|
1622
1626
|
for(let i = 0; i < entities.Cluster.length; i++) {
|
1623
1627
|
const c = entities.Cluster[i];
|
1624
1628
|
c.clearAllProcesses();
|
@@ -1714,7 +1718,8 @@ class LinnyRModel {
|
|
1714
1718
|
'"><entities>', xml.join(''),
|
1715
1719
|
'</entities><from-tos>', ft_xml.join(''),
|
1716
1720
|
'</from-tos><extras>', extra_xml.join(''),
|
1717
|
-
'</extras><
|
1721
|
+
'</extras><selected-clusters>', selc_xml.join(''),
|
1722
|
+
'</selected-clusters><selection>', selected_xml.join(''),
|
1718
1723
|
'</selection></copy>'].join('');
|
1719
1724
|
}
|
1720
1725
|
|
@@ -3236,6 +3241,7 @@ class LinnyRModel {
|
|
3236
3241
|
for(let i = 0; i < constraints.length; i++) {
|
3237
3242
|
const
|
3238
3243
|
c = constraints[i],
|
3244
|
+
// NOTE: constraints in list have levels greater than near-zero
|
3239
3245
|
fl = c.from_node.actualLevel(t),
|
3240
3246
|
tl = c.to_node.actualLevel(t);
|
3241
3247
|
let tcp;
|
@@ -3306,10 +3312,10 @@ class LinnyRModel {
|
|
3306
3312
|
if(af > VM.NEAR_ZERO) {
|
3307
3313
|
// Prevent division by zero
|
3308
3314
|
// NOTE: level can be zero even if actual flow > 0!
|
3309
|
-
const al = p.
|
3315
|
+
const al = p.nonZeroLevel(dt);
|
3310
3316
|
// NOTE: scale to level only when level > 1, or fixed
|
3311
3317
|
// costs for start-up or first commit will be amplified
|
3312
|
-
if(al > VM.
|
3318
|
+
if(al > VM.NEAR_ZERO) cp -= pr * af / Math.max(al, 1);
|
3313
3319
|
}
|
3314
3320
|
}
|
3315
3321
|
}
|
@@ -3428,8 +3434,8 @@ class LinnyRModel {
|
|
3428
3434
|
p.cost_price[t] = cp;
|
3429
3435
|
// For stocks, the CP includes stock price on t-1
|
3430
3436
|
if(p.is_buffer) {
|
3431
|
-
const prevl = p.
|
3432
|
-
if(prevl >
|
3437
|
+
const prevl = p.nonZeroLevel(t-1);
|
3438
|
+
if(prevl > VM.NEAR_ZERO) {
|
3433
3439
|
cp = (cnp + prevl * p.stockPrice(t-1)) / (qnp + prevl);
|
3434
3440
|
}
|
3435
3441
|
p.stock_price[t] = cp;
|
@@ -3486,7 +3492,7 @@ class LinnyRModel {
|
|
3486
3492
|
// Then (also) look for links having AF = 0 ...
|
3487
3493
|
for(let i = links.length-1; i >= 0; i--) {
|
3488
3494
|
const af = links[i].actualFlow(t);
|
3489
|
-
if(Math.abs(af) < VM.
|
3495
|
+
if(Math.abs(af) < VM.NEAR_ZERO) {
|
3490
3496
|
// ... and set their UCP to 0
|
3491
3497
|
links[i].unit_cost_price = 0;
|
3492
3498
|
links.splice(i, 1);
|
@@ -3522,7 +3528,7 @@ class LinnyRModel {
|
|
3522
3528
|
let hcp = VM.MINUS_INFINITY;
|
3523
3529
|
for(let i = 0; i < p.inputs.length; i++) {
|
3524
3530
|
const l = p.inputs[i];
|
3525
|
-
if(l.from_node instanceof Process && l.actualFlow(t) >
|
3531
|
+
if(l.from_node instanceof Process && l.actualFlow(t) > VM.NEAR_ZERO) {
|
3526
3532
|
const ld = l.actualDelay(t);
|
3527
3533
|
// NOTE: only consider the allocated share of cost
|
3528
3534
|
let cp = l.from_node.costPrice(t - ld) * l.share_of_cost;
|
@@ -2249,7 +2249,7 @@ class VirtualMachine {
|
|
2249
2249
|
}
|
2250
2250
|
// NOTES:
|
2251
2251
|
// (1) Processes have NO slack variables, because sufficient slack is
|
2252
|
-
// provided by
|
2252
|
+
// provided by adding slack variables to products; these slack
|
2253
2253
|
// variables will have high cost penalty values in the objective
|
2254
2254
|
// function, to serve as "last resort" to still obtain a solution when
|
2255
2255
|
// the "real" product levels are overconstrained
|
@@ -2352,7 +2352,7 @@ class VirtualMachine {
|
|
2352
2352
|
}
|
2353
2353
|
}
|
2354
2354
|
// Likewise get the upper bound
|
2355
|
-
if(p.equal_bounds) {
|
2355
|
+
if(p.equal_bounds && p.lower_bound.defined) {
|
2356
2356
|
u = l;
|
2357
2357
|
} else if(p.upper_bound.defined) {
|
2358
2358
|
if(p.upper_bound.isStatic) {
|
@@ -2575,9 +2575,9 @@ class VirtualMachine {
|
|
2575
2575
|
// efficient, while cash flows are inferred properties and will not
|
2576
2576
|
// result in an "unbounded problem" error message from the solver
|
2577
2577
|
this.code.push(
|
2578
|
-
[
|
2578
|
+
[VMI_set_bounds, [a.cash_in_var_index,
|
2579
2579
|
VM.MINUS_INFINITY, VM.PLUS_INFINITY, true]],
|
2580
|
-
[
|
2580
|
+
[VMI_set_bounds, [a.cash_out_var_index,
|
2581
2581
|
VM.MINUS_INFINITY, VM.PLUS_INFINITY, true]]
|
2582
2582
|
);
|
2583
2583
|
}
|
@@ -2590,18 +2590,17 @@ class VirtualMachine {
|
|
2590
2590
|
if(!MODEL.ignored_entities[k]) {
|
2591
2591
|
p = MODEL.processes[k];
|
2592
2592
|
lbx = p.lower_bound;
|
2593
|
-
|
2593
|
+
// NOTE: if UB = LB, set UB to LB only if LB is defined,
|
2594
|
+
// because LB expressions default to -INF while UB expressions
|
2595
|
+
// default to + INF
|
2596
|
+
ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
|
2597
|
+
if(lbx.isStatic) lbx = lbx.result(0);
|
2598
|
+
if(ubx.isStatic) ubx = ubx.result(0);
|
2594
2599
|
// NOTE: pass TRUE as fourth parameter to indicate that +INF
|
2595
2600
|
// and -INF can be coded as the infinity values used by the
|
2596
2601
|
// solver, rather than the Linny-R values used to detect
|
2597
2602
|
// unbounded problems
|
2598
|
-
|
2599
|
-
this.code.push([VMI_set_const_bounds,
|
2600
|
-
[p.level_var_index, lbx.result(0), ubx.result(0), true]]);
|
2601
|
-
} else {
|
2602
|
-
this.code.push([VMI_set_var_bounds,
|
2603
|
-
[p.level_var_index, lbx, ubx, true]]);
|
2604
|
-
}
|
2603
|
+
this.code.push([VMI_set_bounds, [p.level_var_index, lbx, ubx, true]]);
|
2605
2604
|
// Add level variable index to "fixed" list for specified rounds
|
2606
2605
|
const rf = p.actor.round_flags;
|
2607
2606
|
if(rf != 0) {
|
@@ -2629,17 +2628,17 @@ class VirtualMachine {
|
|
2629
2628
|
// If no slack, the bound constraints can be set on the
|
2630
2629
|
// variables themselves
|
2631
2630
|
lbx = p.lower_bound;
|
2632
|
-
|
2633
|
-
|
2634
|
-
|
2635
|
-
|
2636
|
-
|
2637
|
-
|
2638
|
-
|
2631
|
+
// NOTE: if UB = LB, set UB to LB only if LB is defined,
|
2632
|
+
// because LB expressions default to -INF while UB expressions
|
2633
|
+
// default to + INF
|
2634
|
+
ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
|
2635
|
+
if(lbx.isStatic) lbx = lbx.result(0);
|
2636
|
+
if(ubx.isStatic) ubx = ubx.result(0);
|
2637
|
+
this.code.push([VMI_set_bounds, [vi, lbx, ubx]]);
|
2639
2638
|
} else {
|
2640
2639
|
// Otherwise, set bounds of stock variable to -INF and +INF,
|
2641
2640
|
// as product constraints will be added later on
|
2642
|
-
this.code.push([
|
2641
|
+
this.code.push([VMI_set_bounds,
|
2643
2642
|
[vi, VM.MINUS_INFINITY, VM.PLUS_INFINITY]]);
|
2644
2643
|
}
|
2645
2644
|
}
|
@@ -3224,7 +3223,7 @@ class VirtualMachine {
|
|
3224
3223
|
// To deal with this, the default equations will NOT be set when UB <= 0,
|
3225
3224
|
// while the "exceptional" equations (q.v.) will NOT be set when UB > 0.
|
3226
3225
|
// This can be realized using a special VM instruction:
|
3227
|
-
ubx = (p.equal_bounds && p.lower_bound.
|
3226
|
+
ubx = (p.equal_bounds && p.lower_bound.defined ? p.lower_bound : p.upper_bound);
|
3228
3227
|
this.code.push([VMI_set_add_constraints_flag, [ubx, '>', 0]]);
|
3229
3228
|
|
3230
3229
|
// NOTE: if UB <= 0 the following constraints will be prepared but NOT added
|
@@ -3256,19 +3255,22 @@ class VirtualMachine {
|
|
3256
3255
|
if(ubx.isStatic) {
|
3257
3256
|
// If UB is very high (typically: undefined, so +INF), try to infer
|
3258
3257
|
// a lower value for UB to use for the ON/OFF binary
|
3259
|
-
let ub = ubx.result(0)
|
3258
|
+
let ub = ubx.result(0),
|
3259
|
+
hub = ub;
|
3260
3260
|
if(ub > VM.MEGA_UPPER_BOUND) {
|
3261
|
-
|
3261
|
+
hub = p.highestUpperBound([]);
|
3262
3262
|
// If UB still very high, warn modeler on infoline and in monitor
|
3263
|
-
if(
|
3264
|
-
const msg = 'High upper bound (' + this.sig4Dig(
|
3263
|
+
if(hub > VM.MEGA_UPPER_BOUND) {
|
3264
|
+
const msg = 'High upper bound (' + this.sig4Dig(hub) +
|
3265
3265
|
') for <strong>' + p.displayName + '</strong>' +
|
3266
3266
|
' will compromise computation of its binary variables';
|
3267
3267
|
UI.warn(msg);
|
3268
3268
|
this.logMessage(this.block_count,
|
3269
3269
|
'WARNING: ' + msg.replace(/<\/?strong>/g, '"'));
|
3270
3270
|
}
|
3271
|
-
}
|
3271
|
+
}
|
3272
|
+
if(hub !== ub) {
|
3273
|
+
ub = hub;
|
3272
3274
|
this.logMessage(this.block_count,
|
3273
3275
|
`Inferred upper bound for ${p.displayName}: ${this.sig4Dig(ub)}`);
|
3274
3276
|
}
|
@@ -3772,7 +3774,7 @@ class VirtualMachine {
|
|
3772
3774
|
for(let k = 0; k < l; k++) {
|
3773
3775
|
const
|
3774
3776
|
vi = svl[k],
|
3775
|
-
slack = x[vi + j],
|
3777
|
+
slack = parseFloat(x[vi + j]),
|
3776
3778
|
absl = Math.abs(slack);
|
3777
3779
|
if(absl > VM.NEAR_ZERO) {
|
3778
3780
|
const v = this.variables[vi - 1];
|
@@ -4731,8 +4733,6 @@ class VirtualMachine {
|
|
4731
4733
|
return [-2, 2, 6];
|
4732
4734
|
} else if(this.solver_name === 'gurobi') {
|
4733
4735
|
return [1, 3, 4, 6, 11, 12, 14];
|
4734
|
-
} else if(this.solver_name === 'scip') {
|
4735
|
-
return [];
|
4736
4736
|
} else {
|
4737
4737
|
return [];
|
4738
4738
|
}
|
@@ -6446,8 +6446,8 @@ from) the k'th coefficient.
|
|
6446
6446
|
|
6447
6447
|
*/
|
6448
6448
|
|
6449
|
-
function
|
6450
|
-
// `args`: [var_index, number, number]
|
6449
|
+
function VMI_set_bounds(args) {
|
6450
|
+
// `args`: [var_index, number or expression, number or expression]
|
6451
6451
|
const
|
6452
6452
|
vi = args[0],
|
6453
6453
|
vbl = VM.variables[vi - 1][1],
|
@@ -6465,7 +6465,8 @@ function VMI_set_const_bounds(args) {
|
|
6465
6465
|
// if this is the first round
|
6466
6466
|
if(VM.current_round) {
|
6467
6467
|
l = vbl.actualLevel(VM.t);
|
6468
|
-
//PATCH
|
6468
|
+
// QUICK PATCH! should resolve that small non-zero process levels
|
6469
|
+
// computed in prior round make problem infeasible
|
6469
6470
|
if(l < 0.0005) l = 0;
|
6470
6471
|
} else {
|
6471
6472
|
l = 0;
|
@@ -6474,17 +6475,22 @@ function VMI_set_const_bounds(args) {
|
|
6474
6475
|
fixed = ' (FIXED ' + vbl.displayName + ')';
|
6475
6476
|
} else {
|
6476
6477
|
// Set bounds as specified by the two arguments
|
6477
|
-
l =
|
6478
|
-
|
6478
|
+
l = args[1];
|
6479
|
+
if(l instanceof Expression) l = l.result(VM.t);
|
6480
|
+
if(l === VM.UNDEFINED) l = 0;
|
6481
|
+
u = args[2];
|
6482
|
+
if(u instanceof Expression) u = u.result(VM.t);
|
6483
|
+
u = Math.min(u, VM.PLUS_INFINITY);
|
6479
6484
|
if(solver_inf) {
|
6480
6485
|
if(l === VM.MINUS_INFINITY) l = -inf_val;
|
6481
6486
|
if(u === VM.PLUS_INFINITY) u = inf_val;
|
6482
6487
|
}
|
6483
6488
|
fixed = '';
|
6484
6489
|
}
|
6485
|
-
// NOTE: to
|
6490
|
+
// NOTE: to see in the console whether fixing across rounds works, insert
|
6491
|
+
// "fixed !== '' || " before DEBUGGING below
|
6486
6492
|
if(DEBUGGING) {
|
6487
|
-
console.log(['
|
6493
|
+
console.log(['set_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
|
6488
6494
|
' LB = ', VM.sig4Dig(l), ', UB = ', VM.sig4Dig(u), fixed].join(''));
|
6489
6495
|
}
|
6490
6496
|
// NOTE: since the VM vectors for lower bounds and upper bounds are
|
@@ -6511,61 +6517,6 @@ function VMI_set_const_bounds(args) {
|
|
6511
6517
|
}
|
6512
6518
|
}
|
6513
6519
|
|
6514
|
-
function VMI_set_var_bounds(args) {
|
6515
|
-
// `args`: [var_index, expression, expression]
|
6516
|
-
const
|
6517
|
-
vi = args[0],
|
6518
|
-
vbl = VM.variables[vi - 1][1],
|
6519
|
-
k = VM.offset + vi,
|
6520
|
-
r = VM.round_letters.indexOf(VM.round_sequence[VM.current_round]),
|
6521
|
-
// Optional fourth parameter indicates whether the solver's
|
6522
|
-
// infinity values should be used
|
6523
|
-
solver_inf = args.length > 3 && args[3],
|
6524
|
-
inf_val = (solver_inf ? VM.SOLVER_PLUS_INFINITY : VM.PLUS_INFINITY);
|
6525
|
-
let l,
|
6526
|
-
u,
|
6527
|
-
fixed = (vi in VM.fixed_var_indices[r - 1]);
|
6528
|
-
if(fixed) {
|
6529
|
-
// Set both bounds equal to the level set in the previous round, or to 0
|
6530
|
-
// if this is the first round
|
6531
|
-
if(VM.current_round) {
|
6532
|
-
l = vbl.actualLevel(VM.t);
|
6533
|
-
} else {
|
6534
|
-
l = 0;
|
6535
|
-
}
|
6536
|
-
u = l;
|
6537
|
-
fixed = ' (FIXED ' + vbl.displayName + ')';
|
6538
|
-
} else {
|
6539
|
-
l = args[1].result(VM.t);
|
6540
|
-
if(l === VM.UNDEFINED) l = 0;
|
6541
|
-
u = Math.min(args[2].result(VM.t), inf_val);
|
6542
|
-
if(solver_inf) {
|
6543
|
-
if(l === VM.MINUS_INFINITY) l = -inf_val;
|
6544
|
-
if(u === VM.PLUS_INFINITY) u = inf_val;
|
6545
|
-
}
|
6546
|
-
fixed = '';
|
6547
|
-
}
|
6548
|
-
// Here, too, no need to set default values
|
6549
|
-
if(Math.abs(l) > VM.NEAR_ZERO || u !== inf_val) {
|
6550
|
-
VM.lower_bounds[k] = l;
|
6551
|
-
VM.upper_bounds[k] = u;
|
6552
|
-
// Check for peak increase -- see comments in VMI_set_const_bound
|
6553
|
-
if(vbl.peak_inc_var_index >= 0) {
|
6554
|
-
u = Math.max(0, u - vbl.b_peak[VM.block_count - 1]);
|
6555
|
-
const
|
6556
|
-
cvi = VM.chunk_offset + vbl.peak_inc_var_index,
|
6557
|
-
piub = VM.upper_bounds[cvi];
|
6558
|
-
if(piub) u = Math.max(piub, u);
|
6559
|
-
VM.upper_bounds[cvi] = u;
|
6560
|
-
VM.upper_bounds[cvi + 1] = u;
|
6561
|
-
}
|
6562
|
-
}
|
6563
|
-
if(DEBUGGING) {
|
6564
|
-
console.log(['set_var_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
|
6565
|
-
' LB = ', l, ', UB = ', u, fixed].join(''));
|
6566
|
-
}
|
6567
|
-
}
|
6568
|
-
|
6569
6520
|
function VMI_clear_coefficients(empty) {
|
6570
6521
|
if(DEBUGGING) console.log('clear_coefficients');
|
6571
6522
|
VM.coefficients = {};
|