linny-r 1.3.1 → 1.3.3
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 +45 -6
- package/package.json +1 -1
- package/server.js +47 -6
- package/static/index.html +59 -1
- package/static/linny-r.css +56 -0
- package/static/scripts/linny-r-ctrl.js +16 -1
- package/static/scripts/linny-r-gui.js +335 -7
- package/static/scripts/linny-r-milp.js +237 -13
- package/static/scripts/linny-r-model.js +109 -21
- package/static/scripts/linny-r-utils.js +10 -2
- package/static/scripts/linny-r-vm.js +233 -170
package/console.js
CHANGED
@@ -86,6 +86,9 @@ console.log('Platform:', PLATFORM, '(' + os.type() + ')');
|
|
86
86
|
console.log('Module directory:', MODULE_DIRECTORY);
|
87
87
|
console.log('Working directory:', WORKING_DIRECTORY);
|
88
88
|
|
89
|
+
// Currently, these external solvers are supported:
|
90
|
+
const SUPPORTED_SOLVERS = ['gurobi', 'cplex', 'scip', 'lp_solve'];
|
91
|
+
|
89
92
|
const
|
90
93
|
// Load the MILP solver (dependent on Node.js: `fs`, `os` and `path`)
|
91
94
|
MILPSolver = require('./static/scripts/linny-r-milp.js'),
|
@@ -123,7 +126,7 @@ Possible options are:
|
|
123
126
|
[name]-stats.txt in (workspace)/reports
|
124
127
|
run will run the loaded model
|
125
128
|
solver=[name] will select solver [name], or warn if not found
|
126
|
-
(name choices: Gurobi or LP_solve)
|
129
|
+
(name choices: Gurobi, CPLEX, SCIP or LP_solve)
|
127
130
|
user=[identifier] user ID will be used to log onto remote servers
|
128
131
|
verbose will output solver messages to the console
|
129
132
|
workspace=[path] will create workspace in [path] instead of (main)/user
|
@@ -817,7 +820,7 @@ function commandLineSettings() {
|
|
817
820
|
const av = lca.split('=');
|
818
821
|
if(av.length === 1) av.push('');
|
819
822
|
if(av[0] === 'solver') {
|
820
|
-
if(av[1]
|
823
|
+
if(SUPPORTED_SOLVERS.indexOf(av[1]) < 0) {
|
821
824
|
console.log(`WARNING: Unknown solver "${av[1]}"`);
|
822
825
|
} else {
|
823
826
|
settings.preferred_solver = av[1];
|
@@ -920,6 +923,7 @@ function commandLineSettings() {
|
|
920
923
|
// Check whether MILP solver(s) and Inkscape have been installed
|
921
924
|
const path_list = process.env.PATH.split(path.delimiter);
|
922
925
|
let gurobi_path = '',
|
926
|
+
scip_path = '',
|
923
927
|
match,
|
924
928
|
max_v = -1;
|
925
929
|
for(let i = 0; i < path_list.length; i++) {
|
@@ -928,6 +932,8 @@ function commandLineSettings() {
|
|
928
932
|
gurobi_path = path_list[i];
|
929
933
|
max_v = parseInt(match[1]);
|
930
934
|
}
|
935
|
+
match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
|
936
|
+
if(match) scip_path = path_list[i];
|
931
937
|
match = path_list[i].match(/inkscape/i);
|
932
938
|
if(match) settings.inkscape = path_list[i];
|
933
939
|
}
|
@@ -958,11 +964,44 @@ function commandLineSettings() {
|
|
958
964
|
'WARNING: Failed to access the Gurobi command line application');
|
959
965
|
}
|
960
966
|
}
|
967
|
+
// Check if cplex(.exe) exists in its directory
|
968
|
+
let sp = path.join(cplex_path, 'cplex' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
969
|
+
const need_cplex = !settings.solver || settings.preferred_solver === 'cplex';
|
970
|
+
try {
|
971
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
972
|
+
console.log('Path to CPLEX:', sp);
|
973
|
+
if(need_cplex) {
|
974
|
+
settings.solver = 'cplex';
|
975
|
+
settings.solver_path = sp;
|
976
|
+
}
|
977
|
+
} catch(err) {
|
978
|
+
// Only report error if CPLEX is needed
|
979
|
+
if(need_cplex) {
|
980
|
+
console.log(err.message);
|
981
|
+
console.log('WARNING: CPLEX application not found in', sp);
|
982
|
+
}
|
983
|
+
}
|
984
|
+
// Check if scip(.exe) exists in its directory
|
985
|
+
sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
986
|
+
const need_scip = !settings.solver || settings.preferred_solver === 'scip';
|
987
|
+
try {
|
988
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
989
|
+
console.log('Path to SCIP:', sp);
|
990
|
+
if(need_scip) {
|
991
|
+
settings.solver = 'scip';
|
992
|
+
settings.solver_path = sp;
|
993
|
+
}
|
994
|
+
} catch(err) {
|
995
|
+
// Only report error if SCIP is needed
|
996
|
+
if(need_scip) {
|
997
|
+
console.log(err.message);
|
998
|
+
console.log('WARNING: SCIP application not found in', sp);
|
999
|
+
}
|
1000
|
+
}
|
961
1001
|
// Check if lp_solve(.exe) exists in main directory
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
|
1002
|
+
sp = path.join(WORKING_DIRECTORY,
|
1003
|
+
'lp_solve' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1004
|
+
const need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
|
966
1005
|
try {
|
967
1006
|
fs.accessSync(sp, fs.constants.X_OK);
|
968
1007
|
console.log('Path to LP_solve:', sp);
|
package/package.json
CHANGED
package/server.js
CHANGED
@@ -122,6 +122,8 @@ function checkNodeModule(name) {
|
|
122
122
|
}
|
123
123
|
}
|
124
124
|
|
125
|
+
// Currently, these external solvers are supported:
|
126
|
+
const SUPPORTED_SOLVERS = ['gurobi', 'cplex', 'scip', 'lp_solve'];
|
125
127
|
|
126
128
|
// Load class MILPSolver
|
127
129
|
const MILPSolver = require('./static/scripts/linny-r-milp.js');
|
@@ -1422,7 +1424,7 @@ function commandLineSettings() {
|
|
1422
1424
|
settings.port = n;
|
1423
1425
|
}
|
1424
1426
|
} else if(av[0] === 'solver') {
|
1425
|
-
if(av[1]
|
1427
|
+
if(SUPPORTED_SOLVERS.indexOf(av[1]) < 0) {
|
1426
1428
|
console.log(`WARNING: Unknown solver "${av[1]}"`);
|
1427
1429
|
} else {
|
1428
1430
|
settings.preferred_solver = av[1];
|
@@ -1455,6 +1457,8 @@ function commandLineSettings() {
|
|
1455
1457
|
// Check whether MILP solver(s) and Inkscape have been installed
|
1456
1458
|
const path_list = process.env.PATH.split(path.delimiter);
|
1457
1459
|
let gurobi_path = '',
|
1460
|
+
scip_path = '',
|
1461
|
+
cplex_path = '',
|
1458
1462
|
match,
|
1459
1463
|
max_v = -1;
|
1460
1464
|
for(let i = 0; i < path_list.length; i++) {
|
@@ -1463,6 +1467,10 @@ function commandLineSettings() {
|
|
1463
1467
|
gurobi_path = path_list[i];
|
1464
1468
|
max_v = parseInt(match[1]);
|
1465
1469
|
}
|
1470
|
+
match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
|
1471
|
+
if(match) scip_path = path_list[i];
|
1472
|
+
match = path_list[i].match(/[\/\\]cplex[\/\\]bin/i);
|
1473
|
+
if(match) cplex_path = path_list[i];
|
1466
1474
|
match = path_list[i].match(/inkscape/i);
|
1467
1475
|
if(match) settings.inkscape = path_list[i];
|
1468
1476
|
}
|
@@ -1493,11 +1501,44 @@ function commandLineSettings() {
|
|
1493
1501
|
'WARNING: Failed to access the Gurobi command line application');
|
1494
1502
|
}
|
1495
1503
|
}
|
1504
|
+
// Check if cplex(.exe) exists in its directory
|
1505
|
+
let sp = path.join(cplex_path, 'cplex' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1506
|
+
const need_cplex = !settings.solver || settings.preferred_solver === 'cplex';
|
1507
|
+
try {
|
1508
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
1509
|
+
console.log('Path to CPLEX:', sp);
|
1510
|
+
if(need_cplex) {
|
1511
|
+
settings.solver = 'cplex';
|
1512
|
+
settings.solver_path = sp;
|
1513
|
+
}
|
1514
|
+
} catch(err) {
|
1515
|
+
// Only report error if CPLEX is needed
|
1516
|
+
if(need_cplex) {
|
1517
|
+
console.log(err.message);
|
1518
|
+
console.log('WARNING: CPLEX application not found in', sp);
|
1519
|
+
}
|
1520
|
+
}
|
1521
|
+
// Check if scip(.exe) exists in its directory
|
1522
|
+
sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1523
|
+
const need_scip = !settings.solver || settings.preferred_solver === 'scip';
|
1524
|
+
try {
|
1525
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
1526
|
+
console.log('Path to SCIP:', sp);
|
1527
|
+
if(need_scip) {
|
1528
|
+
settings.solver = 'scip';
|
1529
|
+
settings.solver_path = sp;
|
1530
|
+
}
|
1531
|
+
} catch(err) {
|
1532
|
+
// Only report error if SCIP is needed
|
1533
|
+
if(need_scip) {
|
1534
|
+
console.log(err.message);
|
1535
|
+
console.log('WARNING: SCIP application not found in', sp);
|
1536
|
+
}
|
1537
|
+
}
|
1496
1538
|
// Check if lp_solve(.exe) exists in working directory
|
1497
|
-
|
1498
|
-
|
1499
|
-
|
1500
|
-
need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
|
1539
|
+
sp = path.join(WORKING_DIRECTORY,
|
1540
|
+
'lp_solve' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1541
|
+
const need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
|
1501
1542
|
try {
|
1502
1543
|
fs.accessSync(sp, fs.constants.X_OK);
|
1503
1544
|
console.log('Path to LP_solve:', sp);
|
@@ -1574,7 +1615,7 @@ function createWorkspace() {
|
|
1574
1615
|
data: path.join(SETTINGS.user_dir, 'data'),
|
1575
1616
|
diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
|
1576
1617
|
modules: path.join(SETTINGS.user_dir, 'modules'),
|
1577
|
-
solver_output: path.join(SETTINGS.user_dir, 'solver')
|
1618
|
+
solver_output: path.join(SETTINGS.user_dir, 'solver')
|
1578
1619
|
};
|
1579
1620
|
// Create these sub-directories if not aready there
|
1580
1621
|
try {
|
package/static/index.html
CHANGED
@@ -2804,7 +2804,65 @@ where X can be one or several of these letters: ABCDELPQ">
|
|
2804
2804
|
</div>
|
2805
2805
|
</div>
|
2806
2806
|
</div>
|
2807
|
-
|
2807
|
+
|
2808
|
+
<!-- the PASTE dialog prompts for a prefix and parameter bindings -->
|
2809
|
+
<div id="paste-modal" class="modal">
|
2810
|
+
<div id="paste-dlg" class="inp-dlg">
|
2811
|
+
<div class="dlg-title">
|
2812
|
+
Name conflict resolution strategy
|
2813
|
+
<img class="cancel-btn" src="images/cancel.png">
|
2814
|
+
<img class="ok-btn" src="images/ok.png">
|
2815
|
+
</div>
|
2816
|
+
<div id="paste-ftp" class="paste-option">
|
2817
|
+
<div id="paste-ftp-box" class="box checked"></div>
|
2818
|
+
<div class="paste-tactic">
|
2819
|
+
Change <span id="paste-from-prefix"></span>…
|
2820
|
+
to <span id="paste-to-prefix"></span>…
|
2821
|
+
</div>
|
2822
|
+
</div>
|
2823
|
+
<div id="paste-fta" class="paste-option">
|
2824
|
+
<div id="paste-fta-box" class="box checked"></div>
|
2825
|
+
<div class="paste-tactic">
|
2826
|
+
Change actor <span id="paste-from-actor"></span>
|
2827
|
+
to <span id="paste-to-actor"></span>
|
2828
|
+
</div>
|
2829
|
+
</div>
|
2830
|
+
<div class="paste-option">
|
2831
|
+
<div id="paste-a-box" class="box checked"></div>
|
2832
|
+
<div class="paste-tactic">
|
2833
|
+
<label>If no actor, add:</label>
|
2834
|
+
<input id="paste-actor" type="text"
|
2835
|
+
placeholder="(no actor)" autocomplete="off">
|
2836
|
+
</div>
|
2837
|
+
</div>
|
2838
|
+
<div class="paste-option">
|
2839
|
+
<div id="paste-p-box" class="box checked"></div>
|
2840
|
+
<div class="paste-tactic">
|
2841
|
+
<label>Add prefix:</label>
|
2842
|
+
<input id="paste-prefix" type="text"
|
2843
|
+
placeholder="(none)" autocomplete="off">
|
2844
|
+
</div>
|
2845
|
+
</div>
|
2846
|
+
<div class="paste-option">
|
2847
|
+
<div id="paste-inc-box" class="box checked"></div>
|
2848
|
+
<div class="paste-tactic">
|
2849
|
+
Auto-increment tail number
|
2850
|
+
</div>
|
2851
|
+
</div>
|
2852
|
+
<div class="paste-option">
|
2853
|
+
<div id="paste-near-box" class="box checked"></div>
|
2854
|
+
<div class="paste-tactic">
|
2855
|
+
Link to nearest eligible node
|
2856
|
+
</div>
|
2857
|
+
</div>
|
2858
|
+
<div style="font-weight: bold; margin:4px 2px 2px 2px">
|
2859
|
+
Mapping of nodes to link from/to:
|
2860
|
+
</div>
|
2861
|
+
<div id="paste-scroll-area">
|
2862
|
+
</div>
|
2863
|
+
</div>
|
2864
|
+
</div>
|
2865
|
+
|
2808
2866
|
<!--- rotating Linny-R logo icon -->
|
2809
2867
|
<div id="rotating-icon">
|
2810
2868
|
<div>
|
package/static/linny-r.css
CHANGED
@@ -4053,6 +4053,7 @@ span.sd-clear {
|
|
4053
4053
|
height: 111px;
|
4054
4054
|
}
|
4055
4055
|
|
4056
|
+
#paste-dlg,
|
4056
4057
|
#include-dlg {
|
4057
4058
|
width: 320px;
|
4058
4059
|
height: min-content;
|
@@ -4078,6 +4079,7 @@ span.sd-clear {
|
|
4078
4079
|
margin-left: 9px;
|
4079
4080
|
}
|
4080
4081
|
|
4082
|
+
#paste-scroll-area,
|
4081
4083
|
#include-scroll-area {
|
4082
4084
|
margin: 2px;
|
4083
4085
|
height: min-content;
|
@@ -4088,6 +4090,60 @@ span.sd-clear {
|
|
4088
4090
|
overflow-y: auto;
|
4089
4091
|
}
|
4090
4092
|
|
4093
|
+
div.paste-tactic {
|
4094
|
+
display: inline-block;
|
4095
|
+
vertical-align: top;
|
4096
|
+
padding-top: 3px;
|
4097
|
+
}
|
4098
|
+
|
4099
|
+
div.paste-select::before {
|
4100
|
+
content: ' \1F872';
|
4101
|
+
margin-right: 3px;
|
4102
|
+
}
|
4103
|
+
|
4104
|
+
div.paste-select {
|
4105
|
+
display: inline-block;
|
4106
|
+
font-size: 12px;
|
4107
|
+
max-width: calc(100% - 25px);
|
4108
|
+
margin: 2px;
|
4109
|
+
}
|
4110
|
+
|
4111
|
+
#paste-prefix,
|
4112
|
+
#paste-actor {
|
4113
|
+
width: 150px;
|
4114
|
+
font-size: 12px;
|
4115
|
+
height: 17px !important;
|
4116
|
+
margin-left: 1px;
|
4117
|
+
}
|
4118
|
+
|
4119
|
+
#paste-from-prefix,
|
4120
|
+
#paste-to-prefix,
|
4121
|
+
#paste-from-actor,
|
4122
|
+
#paste-to-actor {
|
4123
|
+
max-width: 220px;
|
4124
|
+
overflow: clip;
|
4125
|
+
white-space: nowrap;
|
4126
|
+
text-overflow: ellipsis;
|
4127
|
+
}
|
4128
|
+
|
4129
|
+
#paste-from-prefix {
|
4130
|
+
color: #6060c0;
|
4131
|
+
}
|
4132
|
+
|
4133
|
+
#paste-to-prefix {
|
4134
|
+
color: #0000c0;
|
4135
|
+
}
|
4136
|
+
|
4137
|
+
#paste-from-actor {
|
4138
|
+
font-style: italic;
|
4139
|
+
color: #8050a0;
|
4140
|
+
}
|
4141
|
+
|
4142
|
+
#paste-to-actor {
|
4143
|
+
font-style: italic;
|
4144
|
+
color: #600080;
|
4145
|
+
}
|
4146
|
+
|
4091
4147
|
tr.i-param {
|
4092
4148
|
font-style: italic;
|
4093
4149
|
border: 1px solid Silver;
|
@@ -136,7 +136,7 @@ class Controller {
|
|
136
136
|
ACTOR_PROPS: ['weight', 'comments'],
|
137
137
|
CLUSTER_PROPS: ['comments', 'collapsed', 'ignore'],
|
138
138
|
PROCESS_PROPS: ['comments', 'lower_bound', 'upper_bound', 'initial_level',
|
139
|
-
'
|
139
|
+
'pace_expression', 'equal_bounds', 'level_to_zero', 'integer_level', 'collapsed'],
|
140
140
|
PRODUCT_PROPS: ['comments', 'lower_bound', 'upper_bound', 'initial_level',
|
141
141
|
'scale_unit', 'equal_bounds', 'price', 'is_source', 'is_sink', 'is_buffer',
|
142
142
|
'is_data', 'integer_level', 'no_slack'],
|
@@ -316,6 +316,21 @@ class Controller {
|
|
316
316
|
return pan;
|
317
317
|
}
|
318
318
|
|
319
|
+
sharedPrefix(n1, n2) {
|
320
|
+
const
|
321
|
+
pan1 = this.prefixesAndName(n1),
|
322
|
+
pan2 = this.prefixesAndName(n2),
|
323
|
+
l = Math.min(pan1.length - 1, pan2.length - 1),
|
324
|
+
shared = [];
|
325
|
+
let i = 0;
|
326
|
+
while(i < l && ciCompare(pan1[i], pan2[i]) === 0) {
|
327
|
+
// NOTE: if identical except for case, prefer "Abc" over "aBc"
|
328
|
+
shared.push(pan1[i] < pan2[i] ? pan1[i] : pan2[i]);
|
329
|
+
i++;
|
330
|
+
}
|
331
|
+
return shared.join(this.PREFIXER);
|
332
|
+
}
|
333
|
+
|
319
334
|
nameToID(name) {
|
320
335
|
// Returns a name in lower case with link arrow replaced by three
|
321
336
|
// underscores, constraint link arrow by four underscores, and spaces
|