linny-r 1.8.1 → 1.9.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/package.json +1 -1
- package/static/images/sel-order.png +0 -0
- package/static/index.html +24 -2
- package/static/linny-r.css +25 -0
- package/static/scripts/linny-r-config.js +1 -1
- package/static/scripts/linny-r-gui-controller.js +6 -1
- package/static/scripts/linny-r-gui-equation-manager.js +20 -2
- package/static/scripts/linny-r-gui-experiment-manager.js +25 -1
- package/static/scripts/linny-r-gui-expression-editor.js +1 -1
- package/static/scripts/linny-r-gui-file-manager.js +6 -1
- package/static/scripts/linny-r-gui-finder.js +74 -43
- package/static/scripts/linny-r-gui-model-autosaver.js +4 -1
- package/static/scripts/linny-r-gui-monitor.js +1 -0
- package/static/scripts/linny-r-gui-repository-browser.js +6 -5
- package/static/scripts/linny-r-milp.js +8 -6
- package/static/scripts/linny-r-model.js +42 -28
- package/static/scripts/linny-r-utils.js +35 -9
- package/static/scripts/linny-r-vm.js +62 -48
package/package.json
CHANGED
Binary file
|
package/static/index.html
CHANGED
@@ -876,7 +876,7 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ Btu">
|
|
876
876
|
<a href="https://creativecommons.org/licenses/by-sa/4.0"
|
877
877
|
target="_blank">
|
878
878
|
Creative Commons Attribution-ShareAlike (CC BY-SA) license</a>.
|
879
|
-
<img src="
|
879
|
+
<img src="images/by-sa.svg" style="height:21px; margin-top:3px">
|
880
880
|
</td>
|
881
881
|
</tr>
|
882
882
|
</table>
|
@@ -908,7 +908,7 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ Btu">
|
|
908
908
|
<a href="https://creativecommons.org/licenses/by-sa/4.0"
|
909
909
|
target="_blank">
|
910
910
|
Creative Commons Attribution-ShareAlike (CC BY-SA) license</a>.
|
911
|
-
<img src="
|
911
|
+
<img src="images/by-sa.svg" style="height:21px; margin-top:3px">
|
912
912
|
</td>
|
913
913
|
</tr>
|
914
914
|
</table>
|
@@ -1848,6 +1848,8 @@ NOTE: * and ? will be interpreted as wildcards"
|
|
1848
1848
|
title="Edit selected equation">
|
1849
1849
|
<img id="eq-delete-btn" class="btn disab" src="images/delete.png"
|
1850
1850
|
title="Delete selected equation">
|
1851
|
+
<img id="eq-view-btn" class="btn enab" src="images/zoom-in.png"
|
1852
|
+
title="Switch to multi-line expression display">
|
1851
1853
|
</div>
|
1852
1854
|
<img id="equation-outcome" class="not-selected" src="images/outcome.png"
|
1853
1855
|
title="Click to consider/ignore selected equation as experiment outcome">
|
@@ -2266,6 +2268,8 @@ NOTE: * and ? will be interpreted as wildcards"
|
|
2266
2268
|
title="Rename selected experiment">
|
2267
2269
|
<img id="xp-view-btn" class="btn disab" src="images/table.png"
|
2268
2270
|
title="View selected experiment">
|
2271
|
+
<img id="xp-order-btn" class="btn enab" src="images/sel-order.png"
|
2272
|
+
title="Specify ordering of selectors">
|
2269
2273
|
<img id="xp-reset-btn" class="btn enab off" src="images/reset.png"
|
2270
2274
|
title="Clear results for selected experiment">
|
2271
2275
|
<img id="xp-delete-btn" class="btn disab" src="images/delete.png"
|
@@ -2429,6 +2433,24 @@ NOTE: * and ? will be interpreted as wildcards"
|
|
2429
2433
|
</div>
|
2430
2434
|
</div>
|
2431
2435
|
|
2436
|
+
<!-- the SELECTOR ORDER dialog permits specifying how selectors should
|
2437
|
+
be sorted. The selectors may contain a leading or trailing * as
|
2438
|
+
wildcard, and can be separated by spacrs, comma's or semicolons.
|
2439
|
+
-->
|
2440
|
+
<div id="sel-order-modal" class="modal">
|
2441
|
+
<div id="sel-order-dlg" class="inp-dlg">
|
2442
|
+
<div class="dlg-title">
|
2443
|
+
Ordering of selectors
|
2444
|
+
<img class="ok-btn" src="images/ok.png">
|
2445
|
+
<img class="cancel-btn" src="images/cancel.png">
|
2446
|
+
</div>
|
2447
|
+
<textarea id="sel-order-lines" autocomplete="off"
|
2448
|
+
placeholder="Selectors must be separated by spaces or new lines."
|
2449
|
+
autocorrect="off" autocapitalize="off" spellcheck="false">
|
2450
|
+
</textarea>
|
2451
|
+
</div>
|
2452
|
+
</div>
|
2453
|
+
|
2432
2454
|
<!-- the PARAMETER dialog prompts for a dataset or chart name -->
|
2433
2455
|
<div id="xp-parameter-modal" class="modal">
|
2434
2456
|
<div id="xp-parameter-dlg" class="inp-dlg">
|
package/static/linny-r.css
CHANGED
@@ -2506,12 +2506,26 @@ td.equation-expression {
|
|
2506
2506
|
white-space: nowrap;
|
2507
2507
|
overflow: hidden;
|
2508
2508
|
text-overflow: ellipsis;
|
2509
|
+
font-family: monospace;
|
2510
|
+
font-size: 12px;
|
2509
2511
|
}
|
2510
2512
|
|
2511
2513
|
td.equation-expression {
|
2512
2514
|
min-width: 60%;
|
2513
2515
|
}
|
2514
2516
|
|
2517
|
+
td.equation-expression-multi {
|
2518
|
+
border-left: solid 1px Silver;
|
2519
|
+
white-space: normal;
|
2520
|
+
overflow-x: hidden;
|
2521
|
+
overflow-y: auto;
|
2522
|
+
text-overflow: ellipsis;
|
2523
|
+
font-family: monospace;
|
2524
|
+
font-size: 11px;
|
2525
|
+
line-height: 13px;
|
2526
|
+
max-height: 65px;
|
2527
|
+
}
|
2528
|
+
|
2515
2529
|
#dataset-blackbox {
|
2516
2530
|
position: absolute;
|
2517
2531
|
bottom: 6px;
|
@@ -4006,6 +4020,17 @@ div.no-colors {
|
|
4006
4020
|
width: calc(100% - 6px);
|
4007
4021
|
}
|
4008
4022
|
|
4023
|
+
#sel-order-dlg {
|
4024
|
+
width: 220px;
|
4025
|
+
height: min-content;
|
4026
|
+
}
|
4027
|
+
|
4028
|
+
#sel-order-lines {
|
4029
|
+
margin: 2px;
|
4030
|
+
width: calc(100% - 4px);
|
4031
|
+
height: 220px;
|
4032
|
+
}
|
4033
|
+
|
4009
4034
|
#xp-parameter-dlg {
|
4010
4035
|
width: 270px;
|
4011
4036
|
height: 45px;
|
@@ -11,7 +11,7 @@ warning or information messages are displayed.
|
|
11
11
|
*/
|
12
12
|
|
13
13
|
/*
|
14
|
-
Copyright (c) 2017-
|
14
|
+
Copyright (c) 2017-2024 Delft University of Technology
|
15
15
|
|
16
16
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
17
17
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -1025,7 +1025,12 @@ class GUIController extends Controller {
|
|
1025
1025
|
' as preferred solver');
|
1026
1026
|
return;
|
1027
1027
|
}
|
1028
|
-
const pd = postData({
|
1028
|
+
const pd = postData({
|
1029
|
+
action: 'change',
|
1030
|
+
solver: sid,
|
1031
|
+
user: VM.solver_user,
|
1032
|
+
token: VM.solver_token
|
1033
|
+
});
|
1029
1034
|
fetch('solver/', pd)
|
1030
1035
|
.then((response) => {
|
1031
1036
|
if(!response.ok) {
|
@@ -54,6 +54,9 @@ class EquationManager {
|
|
54
54
|
'click', () => EQUATION_MANAGER.editEquation());
|
55
55
|
document.getElementById('eq-delete-btn').addEventListener(
|
56
56
|
'click', () => EQUATION_MANAGER.deleteEquation());
|
57
|
+
this.view_btn = document.getElementById('eq-view-btn');
|
58
|
+
this.view_btn.addEventListener(
|
59
|
+
'click', () => EQUATION_MANAGER.toggleMultiLineDisplay());
|
57
60
|
this.outcome_btn = document.getElementById('equation-outcome');
|
58
61
|
this.outcome_btn.addEventListener(
|
59
62
|
'click', () => EQUATION_MANAGER.toggleOutcome());
|
@@ -78,6 +81,7 @@ class EquationManager {
|
|
78
81
|
'click', () => EQUATION_MANAGER.clone_modal.hide());
|
79
82
|
|
80
83
|
// Initialize the dialog properties
|
84
|
+
this.multi_line = false;
|
81
85
|
this.reset();
|
82
86
|
}
|
83
87
|
|
@@ -130,6 +134,19 @@ class EquationManager {
|
|
130
134
|
}
|
131
135
|
}
|
132
136
|
|
137
|
+
toggleMultiLineDisplay() {
|
138
|
+
// Toggle between single-line view and multi-line expressions.
|
139
|
+
this.multi_line = !this.multi_line;
|
140
|
+
if(this.multi_line) {
|
141
|
+
this.view_btn.src = 'images/zoom-out.png';
|
142
|
+
this.view_btn.title = 'Switch to single-line expression display';
|
143
|
+
} else {
|
144
|
+
this.view_btn.src = 'images/zoom-in.png';
|
145
|
+
this.view_btn.title = 'Switch to multi-line expression display';
|
146
|
+
}
|
147
|
+
this.updateDialog();
|
148
|
+
}
|
149
|
+
|
133
150
|
updateDialog() {
|
134
151
|
// Updates equation list, highlighting selected equation (if any)
|
135
152
|
const
|
@@ -148,10 +165,11 @@ class EquationManager {
|
|
148
165
|
m = ed.modifiers[UI.nameToID(msl[i])],
|
149
166
|
wild = (m.selector.indexOf('??') >= 0),
|
150
167
|
method = m.selector.startsWith(':'),
|
168
|
+
multi = (this.multi_line ? '-multi' : ''),
|
151
169
|
issue = (m.expression.compile_issue ? ' compile-issue' :
|
152
170
|
(m.expression.compute_issue ? ' compute-issue' : '')),
|
153
171
|
clk = '" onclick="EQUATION_MANAGER.selectModifier(event, \'' +
|
154
|
-
m.selector + '\'',
|
172
|
+
escapedSingleQuotes(m.selector) + '\'',
|
155
173
|
mover = (method ? ' onmouseover="EQUATION_MANAGER.showInfo(\'' +
|
156
174
|
m.identifier + '\', event.shiftKey);"' : '');
|
157
175
|
if(m === sm) smid += i;
|
@@ -165,7 +183,7 @@ class EquationManager {
|
|
165
183
|
(wild ? ' wildcard' : ''), clk, ', false);"', mover, '>',
|
166
184
|
(m.outcome_equation ? '<span class="outcome"></span>' : ''),
|
167
185
|
(wild ? wildcardFormat(m.selector) : m.selector),
|
168
|
-
'</td><td class="equation-expression', issue,
|
186
|
+
'</td><td class="equation-expression', multi, issue,
|
169
187
|
(issue ? '"title="' +
|
170
188
|
safeDoubleQuotes(m.expression.compile_issue ||
|
171
189
|
m.expression.compute_issue) : ''),
|
@@ -46,6 +46,9 @@ class GUIExperimentManager extends ExperimentManager {
|
|
46
46
|
this.view_btn = document.getElementById('xp-view-btn');
|
47
47
|
this.view_btn.addEventListener(
|
48
48
|
'click', () => EXPERIMENT_MANAGER.viewerMode());
|
49
|
+
this.sel_order_btn = document.getElementById('xp-order-btn');
|
50
|
+
this.sel_order_btn.addEventListener(
|
51
|
+
'click', () => EXPERIMENT_MANAGER.showSelectorOrder());
|
49
52
|
this.reset_btn = document.getElementById('xp-reset-btn');
|
50
53
|
this.reset_btn.addEventListener(
|
51
54
|
'click', () => EXPERIMENT_MANAGER.clearRunResults());
|
@@ -154,6 +157,13 @@ class GUIExperimentManager extends ExperimentManager {
|
|
154
157
|
this.rename_modal.cancel.addEventListener(
|
155
158
|
'click', () => EXPERIMENT_MANAGER.rename_modal.hide());
|
156
159
|
|
160
|
+
this.sel_order_modal = new ModalDialog('sel-order');
|
161
|
+
this.sel_order_modal.ok.addEventListener(
|
162
|
+
'click', () => EXPERIMENT_MANAGER.modifySelectorOrder());
|
163
|
+
this.sel_order_modal.cancel.addEventListener(
|
164
|
+
'click', () => EXPERIMENT_MANAGER.sel_order_modal.hide());
|
165
|
+
this.sel_order_lines = this.sel_order_modal.element('lines');
|
166
|
+
|
157
167
|
this.parameter_modal = new ModalDialog('xp-parameter');
|
158
168
|
this.parameter_modal.ok.addEventListener(
|
159
169
|
'click', () => EXPERIMENT_MANAGER.addParameter());
|
@@ -473,9 +483,23 @@ class GUIExperimentManager extends ExperimentManager {
|
|
473
483
|
}
|
474
484
|
}
|
475
485
|
}
|
486
|
+
|
487
|
+
showSelectorOrder() {
|
488
|
+
// Show selector order modal.
|
489
|
+
this.sel_order_lines.value = MODEL.selector_order_string;
|
490
|
+
this.sel_order_modal.show();
|
491
|
+
}
|
476
492
|
|
493
|
+
modifySelectorOrder() {
|
494
|
+
// Save text area contents as new selector order string.
|
495
|
+
MODEL.selector_order_string = this.sel_order_lines.value.trim();
|
496
|
+
MODEL.selector_order_list = MODEL.selector_order_string.trim().split(/\s+/);
|
497
|
+
this.sel_order_modal.hide();
|
498
|
+
UI.updateControllerDialogs('DX');
|
499
|
+
}
|
500
|
+
|
477
501
|
designMode() {
|
478
|
-
// Switch to default view
|
502
|
+
// Switch to default view.
|
479
503
|
this.viewer.style.display = 'none';
|
480
504
|
this.design.style.display = 'block';
|
481
505
|
}
|
@@ -380,7 +380,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
380
380
|
}
|
381
381
|
va.innerHTML = options.join('');
|
382
382
|
// NOTE: Chart Manager variable dialog is 60px wider
|
383
|
-
va.style.width = (prefix ? 'calc(100% -
|
383
|
+
va.style.width = (prefix ? 'calc(100% - 86px)' : 'calc(100% - 142px)');
|
384
384
|
return;
|
385
385
|
}
|
386
386
|
// Add "empty" as first and initial option, as it denotes "use default"
|
@@ -333,7 +333,12 @@ class GUIFileManager {
|
|
333
333
|
return response.text();
|
334
334
|
})
|
335
335
|
.then((data) => {
|
336
|
-
UI.postResponseOK(data)
|
336
|
+
if(!UI.postResponseOK(data) && data.indexOf('not implemented') >= 0) {
|
337
|
+
// Switch off auto-save when server does not implement it.
|
338
|
+
AUTO_SAVE.interval = 0;
|
339
|
+
AUTO_SAVE.not_implemented = true;
|
340
|
+
console.log('Auto-save disabled');
|
341
|
+
}
|
337
342
|
bcl.remove('stay-activ');
|
338
343
|
})
|
339
344
|
.catch((err) => {
|
@@ -40,7 +40,7 @@ class Finder {
|
|
40
40
|
this.dialog = UI.draggableDialog('finder');
|
41
41
|
UI.resizableDialog('finder', 'FINDER');
|
42
42
|
this.close_btn = document.getElementById('finder-close-btn');
|
43
|
-
// Make toolbar buttons responsive
|
43
|
+
// Make toolbar buttons responsive.
|
44
44
|
this.close_btn.addEventListener('click', (e) => UI.toggleDialog(e));
|
45
45
|
this.filter_input = document.getElementById('finder-filter-text');
|
46
46
|
this.filter_input.addEventListener('input', () => FINDER.changeFilter());
|
@@ -78,6 +78,7 @@ class Finder {
|
|
78
78
|
this.filtered_types.length = 0;
|
79
79
|
this.selected_entity = null;
|
80
80
|
this.filter_input.value = '';
|
81
|
+
this.filter_string = '';
|
81
82
|
this.filter_pattern = null;
|
82
83
|
this.entity_types = VM.entity_letters;
|
83
84
|
this.find_links = true;
|
@@ -105,7 +106,7 @@ class Finder {
|
|
105
106
|
}
|
106
107
|
|
107
108
|
enterKey() {
|
108
|
-
// Open "edit properties" dialog for the selected entity
|
109
|
+
// Open "edit properties" dialog for the selected entity.
|
109
110
|
const srl = this.entity_table.getElementsByClassName('sel-set');
|
110
111
|
if(srl.length > 0) {
|
111
112
|
const r = this.entity_table.rows[srl[0].rowIndex];
|
@@ -118,7 +119,7 @@ class Finder {
|
|
118
119
|
}
|
119
120
|
|
120
121
|
upDownKey(dir) {
|
121
|
-
// Select row above or below the selected one (if possible)
|
122
|
+
// Select row above or below the selected one (if possible).
|
122
123
|
const srl = this.entity_table.getElementsByClassName('sel-set');
|
123
124
|
if(srl.length > 0) {
|
124
125
|
const r = this.entity_table.rows[srl[0].rowIndex + dir];
|
@@ -139,7 +140,7 @@ class Finder {
|
|
139
140
|
let imgs = '';
|
140
141
|
this.entities.length = 0;
|
141
142
|
this.filtered_types.length = 0;
|
142
|
-
// No list unless a pattern OR a specified SUB-set of entity types
|
143
|
+
// No list unless a pattern OR a specified SUB-set of entity types.
|
143
144
|
if(fp || et && et !== VM.entity_letters) {
|
144
145
|
if(et.indexOf('A') >= 0) {
|
145
146
|
imgs += '<img src="images/actor.png">';
|
@@ -151,7 +152,7 @@ class Finder {
|
|
151
152
|
}
|
152
153
|
}
|
153
154
|
}
|
154
|
-
// NOTE:
|
155
|
+
// NOTE: Do not list black-boxed entities.
|
155
156
|
if(et.indexOf('P') >= 0) {
|
156
157
|
imgs += '<img src="images/process.png">';
|
157
158
|
for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
|
@@ -191,7 +192,7 @@ class Finder {
|
|
191
192
|
const ds = MODEL.datasets[k];
|
192
193
|
if(!k.startsWith(UI.BLACK_BOX) && (!fp || patternMatch(
|
193
194
|
ds.displayName, this.filter_pattern))) {
|
194
|
-
// NOTE:
|
195
|
+
// NOTE: Do not list the equations dataset.
|
195
196
|
if(ds !== MODEL.equations_dataset) {
|
196
197
|
enl.push(k);
|
197
198
|
this.entities.push(MODEL.datasets[k]);
|
@@ -217,11 +218,11 @@ class Finder {
|
|
217
218
|
if(et.indexOf('L') >= 0) {
|
218
219
|
imgs += '<img src="images/link.png">';
|
219
220
|
for(let k in MODEL.links) if(MODEL.links.hasOwnProperty(k)) {
|
220
|
-
// NOTE: "black-boxed" link identifiers are not prefixed => other test
|
221
|
+
// NOTE: "black-boxed" link identifiers are not prefixed => other test.
|
221
222
|
const
|
222
223
|
l = MODEL.links[k],
|
223
224
|
ldn = l.displayName,
|
224
|
-
// A
|
225
|
+
// A link is "black-boxed" when BOTH nodes are "black-boxed".
|
225
226
|
bb = ldn.split(UI.BLACK_BOX).length > 2;
|
226
227
|
if(!bb && (!fp || patternMatch(ldn, this.filter_pattern))) {
|
227
228
|
enl.push(k);
|
@@ -233,7 +234,7 @@ class Finder {
|
|
233
234
|
if(et.indexOf('B') >= 0) {
|
234
235
|
imgs += '<img src="images/constraint.png">';
|
235
236
|
for(let k in MODEL.constraints) {
|
236
|
-
// NOTE:
|
237
|
+
// NOTE: Likewise, constraint identifiers can be prefixed by %.
|
237
238
|
if(MODEL.constraints.hasOwnProperty(k)) {
|
238
239
|
if(!k.startsWith(UI.BLACK_BOX) && (!fp || patternMatch(
|
239
240
|
MODEL.constraints[k].displayName, this.filter_pattern))) {
|
@@ -244,6 +245,35 @@ class Finder {
|
|
244
245
|
}
|
245
246
|
}
|
246
247
|
}
|
248
|
+
// Also allow search for scale unit names.
|
249
|
+
if(et.indexOf('U') >= 0) {
|
250
|
+
imgs += '<img src="images/scale.png">';
|
251
|
+
for(let k in MODEL.products) if(MODEL.products.hasOwnProperty(k)) {
|
252
|
+
if(fp && !k.startsWith(UI.BLACK_BOX) && patternMatch(
|
253
|
+
MODEL.products[k].scale_unit, this.filter_pattern)) {
|
254
|
+
enl.push(k);
|
255
|
+
this.entities.push(MODEL.products[k]);
|
256
|
+
addDistinct('Q', this.filtered_types);
|
257
|
+
}
|
258
|
+
}
|
259
|
+
}
|
260
|
+
// Also allow search for link multiplier symbols.
|
261
|
+
if(et.indexOf('M') >= 0) {
|
262
|
+
if(imgs.indexOf('/link.') < 0) imgs += '<img src="images/link.png">';
|
263
|
+
for(let k in MODEL.links) if(MODEL.links.hasOwnProperty(k)) {
|
264
|
+
// NOTE: "black-boxed" link identifiers are not prefixed => other test.
|
265
|
+
const
|
266
|
+
l = MODEL.links[k],
|
267
|
+
m = VM.LM_LETTERS.charAt(l.multiplier),
|
268
|
+
// A link is "black-boxed" when BOTH nodes are "black-boxed".
|
269
|
+
bb = l.displayName.split(UI.BLACK_BOX).length > 2;
|
270
|
+
if(fp && !bb && this.filter_string.indexOf(m) >= 0) {
|
271
|
+
enl.push(k);
|
272
|
+
this.entities.push(l);
|
273
|
+
addDistinct('L', this.filtered_types);
|
274
|
+
}
|
275
|
+
}
|
276
|
+
}
|
247
277
|
enl.sort((a, b) => UI.compareFullNames(a, b, true));
|
248
278
|
}
|
249
279
|
document.getElementById('finder-entity-imgs').innerHTML = imgs;
|
@@ -259,7 +289,7 @@ class Finder {
|
|
259
289
|
e.type.toLowerCase(), '.png">', e.displayName,
|
260
290
|
'</td></tr>'].join(''));
|
261
291
|
}
|
262
|
-
// NOTE:
|
292
|
+
// NOTE: Reset `selected_entity` if not in the new list.
|
263
293
|
if(seid === 'etr') this.selected_entity = null;
|
264
294
|
this.entity_table.innerHTML = el.join('');
|
265
295
|
UI.scrollIntoView(document.getElementById(seid));
|
@@ -316,18 +346,18 @@ class Finder {
|
|
316
346
|
let hdr = '(no entity selected)';
|
317
347
|
if(se) {
|
318
348
|
hdr = `<em>${se.type}:</em> <strong>${se.displayName}</strong>`;
|
319
|
-
// Make occurrence list
|
349
|
+
// Make occurrence list.
|
320
350
|
if(se instanceof Process || se instanceof Cluster) {
|
321
|
-
// Processes and clusters "occur" in their parent cluster
|
351
|
+
// Processes and clusters "occur" in their parent cluster.
|
322
352
|
if(se.cluster) occ.push(se.cluster.identifier);
|
323
353
|
} else if(se instanceof Product) {
|
324
|
-
// Products "occur" in clusters where they have a position
|
354
|
+
// Products "occur" in clusters where they have a position.
|
325
355
|
const cl = se.productPositionClusters;
|
326
356
|
for(let i = 0; i < cl.length; i++) {
|
327
357
|
occ.push(cl[i].identifier);
|
328
358
|
}
|
329
359
|
} else if(se instanceof Actor) {
|
330
|
-
// Actors "occur" in clusters where they "own" processes or clusters
|
360
|
+
// Actors "occur" in clusters where they "own" processes or clusters.
|
331
361
|
for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
|
332
362
|
const p = MODEL.processes[k];
|
333
363
|
if(p.actor === se) occ.push(p.identifier);
|
@@ -337,13 +367,13 @@ class Finder {
|
|
337
367
|
if(c.actor === se) occ.push(c.identifier);
|
338
368
|
}
|
339
369
|
} else if(se instanceof Link || se instanceof Constraint) {
|
340
|
-
// Links and constraints "occur" in their "best" parent cluster
|
370
|
+
// Links and constraints "occur" in their "best" parent cluster.
|
341
371
|
const c = MODEL.inferParentCluster(se);
|
342
372
|
if(c) occ.push(c.identifier);
|
343
373
|
}
|
344
|
-
// NOTE:
|
374
|
+
// NOTE: No "occurrence" of datasets or equations.
|
345
375
|
// @@TO DO: identify MODULES (?)
|
346
|
-
// All entities can also occur as chart variables
|
376
|
+
// All entities can also occur as chart variables.
|
347
377
|
for(let j = 0; j < MODEL.charts.length; j++) {
|
348
378
|
const c = MODEL.charts[j];
|
349
379
|
for(let k = 0; k < c.variables.length; k++) {
|
@@ -355,12 +385,12 @@ class Finder {
|
|
355
385
|
}
|
356
386
|
}
|
357
387
|
}
|
358
|
-
// Now also look for occurrences of entity references in expressions
|
388
|
+
// Now also look for occurrences of entity references in expressions.
|
359
389
|
const
|
360
390
|
raw = escapeRegex(se.displayName),
|
361
391
|
re = new RegExp(
|
362
392
|
'\\[\\s*!?' + raw.replace(/\s+/g, '\\s+') + '\\s*[\\|\\@\\]]');
|
363
|
-
// Check actor weight expressions
|
393
|
+
// Check actor weight expressions.
|
364
394
|
for(let k in MODEL.actors) if(MODEL.actors.hasOwnProperty(k)) {
|
365
395
|
const a = MODEL.actors[k];
|
366
396
|
if(re.test(a.weight.text)) {
|
@@ -368,7 +398,7 @@ class Finder {
|
|
368
398
|
xol.push(a.identifier);
|
369
399
|
}
|
370
400
|
}
|
371
|
-
// Check all process attribute expressions
|
401
|
+
// Check all process attribute expressions.
|
372
402
|
for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
|
373
403
|
const p = MODEL.processes[k];
|
374
404
|
if(re.test(p.lower_bound.text)) {
|
@@ -384,7 +414,7 @@ class Finder {
|
|
384
414
|
xol.push(p.identifier);
|
385
415
|
}
|
386
416
|
}
|
387
|
-
// Check all product attribute expressions
|
417
|
+
// Check all product attribute expressions.
|
388
418
|
for(let k in MODEL.products) if(MODEL.products.hasOwnProperty(k)) {
|
389
419
|
const p = MODEL.products[k];
|
390
420
|
if(re.test(p.lower_bound.text)) {
|
@@ -404,7 +434,7 @@ class Finder {
|
|
404
434
|
xol.push(p.identifier);
|
405
435
|
}
|
406
436
|
}
|
407
|
-
// Check all notes in clusters for their color expressions and field
|
437
|
+
// Check all notes in clusters for their color expressions and field.
|
408
438
|
for(let k in MODEL.clusters) if(MODEL.clusters.hasOwnProperty(k)) {
|
409
439
|
const c = MODEL.clusters[k];
|
410
440
|
for(let i = 0; i < c.notes.length; i++) {
|
@@ -416,7 +446,7 @@ class Finder {
|
|
416
446
|
}
|
417
447
|
}
|
418
448
|
}
|
419
|
-
// Check all link rate expressions
|
449
|
+
// Check all link rate expressions.
|
420
450
|
for(let k in MODEL.links) if(MODEL.links.hasOwnProperty(k)) {
|
421
451
|
const l = MODEL.links[k];
|
422
452
|
if(re.test(l.relative_rate.text)) {
|
@@ -428,7 +458,7 @@ class Finder {
|
|
428
458
|
xol.push(l.identifier);
|
429
459
|
}
|
430
460
|
}
|
431
|
-
// Check all dataset modifier expressions
|
461
|
+
// Check all dataset modifier expressions.
|
432
462
|
for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
|
433
463
|
const ds = MODEL.datasets[k];
|
434
464
|
for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
|
@@ -451,22 +481,22 @@ class Finder {
|
|
451
481
|
'</td></tr>'].join(''));
|
452
482
|
}
|
453
483
|
this.item_table.innerHTML = el.join('');
|
454
|
-
// Clear the table row list
|
484
|
+
// Clear the table row list.
|
455
485
|
el.length = 0;
|
456
|
-
// Now fill it with entity+attribute having a matching expression
|
486
|
+
// Now fill it with entity+attribute having a matching expression.
|
457
487
|
for(let i = 0; i < xal.length; i++) {
|
458
488
|
const
|
459
489
|
id = xol[i],
|
460
490
|
e = MODEL.objectByID(id),
|
461
491
|
attr = (e instanceof Note ? '' : xal[i]);
|
462
492
|
let img = e.type.toLowerCase(),
|
463
|
-
// NOTE:
|
464
|
-
// part has the left hand part as its attribute
|
493
|
+
// NOTE: A small left-pointing triangle denotes that the right-hand
|
494
|
+
// part has the left hand part as its attribute.
|
465
495
|
cs = '',
|
466
496
|
td = attr + '</td><td>◂</td><td style="width:95%">' +
|
467
497
|
e.displayName;
|
468
|
-
// NOTE:
|
469
|
-
// is irrelevant, hence use 3 columns (no triangle)
|
498
|
+
// NOTE: Equations may have LONG names while the equations dataset
|
499
|
+
// name is irrelevant, hence use 3 columns (no triangle).
|
470
500
|
if(e === MODEL.equations_dataset) {
|
471
501
|
img = 'equation';
|
472
502
|
cs = ' colspan="3"';
|
@@ -484,7 +514,7 @@ class Finder {
|
|
484
514
|
}
|
485
515
|
|
486
516
|
drag(ev) {
|
487
|
-
// Start dragging the selected entity
|
517
|
+
// Start dragging the selected entity.
|
488
518
|
let t = ev.target;
|
489
519
|
while(t && t.nodeName !== 'TD') t = t.parentNode;
|
490
520
|
ev.dataTransfer.setData('text', MODEL.objectByName(t.innerText).identifier);
|
@@ -493,30 +523,31 @@ class Finder {
|
|
493
523
|
|
494
524
|
changeFilter() {
|
495
525
|
// Filter expression can start with 1+ entity letters plus `?` to
|
496
|
-
// look only for the entity types denoted by these letters
|
526
|
+
// look only for the entity types denoted by these letters.
|
497
527
|
let ft = this.filter_input.value,
|
498
528
|
et = VM.entity_letters;
|
499
|
-
if(/^(\*|[ABCDELPQ]+)\?/i.test(ft)) {
|
529
|
+
if(/^(\*|U|M|[ABCDELPQ]+)\?/i.test(ft)) {
|
500
530
|
ft = ft.split('?');
|
501
|
-
// NOTE: *? denotes "all entity types except constraints"
|
531
|
+
// NOTE: *? denotes "all entity types except constraints".
|
502
532
|
et = (ft[0] === '*' ? 'ACDELPQ' : ft[0].toUpperCase());
|
503
533
|
ft = ft.slice(1).join('=');
|
504
534
|
}
|
535
|
+
this.filter_string = ft;
|
505
536
|
this.filter_pattern = patternList(ft);
|
506
537
|
this.entity_types = et;
|
507
538
|
this.updateDialog();
|
508
539
|
}
|
509
540
|
|
510
541
|
showInfo(id, shift) {
|
511
|
-
//
|
542
|
+
// Display documentation for the entity identified by `id`.
|
512
543
|
const e = MODEL.objectByID(id);
|
513
544
|
if(e) DOCUMENTATION_MANAGER.update(e, shift);
|
514
545
|
}
|
515
546
|
|
516
547
|
selectEntity(id, alt=false) {
|
517
|
-
//
|
518
|
-
//
|
519
|
-
// and Alt-click if the entity is editable
|
548
|
+
// Look up entity, select it in the left pane, and update the right
|
549
|
+
// pane. Open the "edit properties" modal dialog on double-click
|
550
|
+
// and Alt-click if the entity is editable.
|
520
551
|
const obj = MODEL.objectByID(id);
|
521
552
|
this.selected_entity = obj;
|
522
553
|
this.updateDialog();
|
@@ -551,7 +582,7 @@ class Finder {
|
|
551
582
|
}
|
552
583
|
|
553
584
|
reveal(id) {
|
554
|
-
//
|
585
|
+
// Show selected occurrence.
|
555
586
|
const
|
556
587
|
se = this.selected_entity,
|
557
588
|
obj = (se ? MODEL.objectByID(id) : null);
|
@@ -559,7 +590,7 @@ class Finder {
|
|
559
590
|
// If cluster, make it focal...
|
560
591
|
if(obj instanceof Cluster) {
|
561
592
|
UI.makeFocalCluster(obj);
|
562
|
-
// ... and select the entity unless it is an actor or dataset
|
593
|
+
// ... and select the entity unless it is an actor or dataset.
|
563
594
|
if(!(se instanceof Actor || se instanceof Dataset)) {
|
564
595
|
MODEL.select(se);
|
565
596
|
if(se instanceof Link || se instanceof Constraint) {
|
@@ -572,7 +603,7 @@ class Finder {
|
|
572
603
|
} else if(obj instanceof Process || obj instanceof Note) {
|
573
604
|
// If occurrence is a process or a note, then make its cluster focal...
|
574
605
|
UI.makeFocalCluster(obj.cluster);
|
575
|
-
// ... and select it
|
606
|
+
// ... and select it.
|
576
607
|
MODEL.select(obj);
|
577
608
|
UI.scrollIntoView(obj.shape.element.childNodes[0]);
|
578
609
|
} else if(obj instanceof Product) {
|
@@ -586,7 +617,7 @@ class Finder {
|
|
586
617
|
if(a) UI.scrollIntoView(a.shape.element.childNodes[0]);
|
587
618
|
}
|
588
619
|
} else if(obj instanceof Chart) {
|
589
|
-
// If occurrence is a chart, select and show it in the chart manager
|
620
|
+
// If occurrence is a chart, select and show it in the chart manager.
|
590
621
|
CHART_MANAGER.chart_index = MODEL.charts.indexOf(obj);
|
591
622
|
if(CHART_MANAGER.chart_index >= 0) {
|
592
623
|
if(UI.hidden('chart-dlg')) {
|
@@ -595,7 +626,7 @@ class Finder {
|
|
595
626
|
}
|
596
627
|
CHART_MANAGER.updateDialog();
|
597
628
|
}
|
598
|
-
// NOTE:
|
629
|
+
// NOTE: Return the object to save a second lookup by revealExpression.
|
599
630
|
return obj;
|
600
631
|
}
|
601
632
|
|
@@ -41,6 +41,7 @@ class ModelAutoSaver {
|
|
41
41
|
this.timeout_id = 0;
|
42
42
|
this.interval = 10; // auto-save every 10 minutes
|
43
43
|
this.period = 24; // delete models older than 24 hours
|
44
|
+
this.not_implemented = false;
|
44
45
|
this.model_list = [];
|
45
46
|
// Overwite defaults if settings still in local storage of browser.
|
46
47
|
this.getSettings();
|
@@ -65,6 +66,7 @@ class ModelAutoSaver {
|
|
65
66
|
|
66
67
|
getSettings() {
|
67
68
|
// Reads custom auto-save settings from local storage.
|
69
|
+
if(this.not_implemented) return;
|
68
70
|
try {
|
69
71
|
const item = window.localStorage.getItem('Linny-R-autosave');
|
70
72
|
if(item) {
|
@@ -86,6 +88,7 @@ class ModelAutoSaver {
|
|
86
88
|
|
87
89
|
setSettings() {
|
88
90
|
// Writes custom auto-save settings to local storage.
|
91
|
+
if(this.not_implemented) return;
|
89
92
|
try {
|
90
93
|
window.localStorage.setItem('Linny-R-autosave',
|
91
94
|
this.interval + '|' + this.period);
|
@@ -199,7 +202,7 @@ class ModelAutoSaver {
|
|
199
202
|
// Close the restore auto-save model dialog.
|
200
203
|
document.getElementById('confirm-remove-models').style.display = 'none';
|
201
204
|
// NOTE: Cancel button or ESC will pass `cancel` as FALSE => do not save.
|
202
|
-
if(!save) {
|
205
|
+
if(!save || this.not_implemented) {
|
203
206
|
document.getElementById('restore-modal').style.display = 'none';
|
204
207
|
return;
|
205
208
|
}
|
@@ -326,6 +326,7 @@ class GUIMonitor {
|
|
326
326
|
} else if(jsr.server) {
|
327
327
|
VM.solver_token = jsr.token;
|
328
328
|
VM.selectSolver(jsr.solver);
|
329
|
+
VM.solver_list = jsr.solver_list;
|
329
330
|
// Remote solver may indicate user-specific solver time limit.
|
330
331
|
let utl = '';
|
331
332
|
if(jsr.time_limit) {
|
@@ -368,7 +368,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
368
368
|
}
|
369
369
|
|
370
370
|
addRepository(name) {
|
371
|
-
// Adds repository if name is unique and valid
|
371
|
+
// Adds repository if name is unique and valid.
|
372
372
|
let r = null,
|
373
373
|
can_store = false;
|
374
374
|
if(name.endsWith('+')) {
|
@@ -389,8 +389,8 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
389
389
|
}
|
390
390
|
|
391
391
|
removeRepository() {
|
392
|
-
// Removes selected repository from list
|
393
|
-
// NOTE:
|
392
|
+
// Removes selected repository from list.
|
393
|
+
// NOTE: Do not remove the first item (local host).
|
394
394
|
if(this.repository_index < 1) return;
|
395
395
|
fetch('repo/', postData({
|
396
396
|
action: 'remove',
|
@@ -415,7 +415,8 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
415
415
|
}
|
416
416
|
|
417
417
|
promptForRepository() {
|
418
|
-
//
|
418
|
+
// Open "Add repository" dialog (only on local host).
|
419
|
+
if(!this.isLocalHost) return;
|
419
420
|
this.add_modal.element('name').value = '';
|
420
421
|
this.add_modal.element('url').value = '';
|
421
422
|
this.add_modal.element('token').value = '';
|
@@ -423,7 +424,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
423
424
|
}
|
424
425
|
|
425
426
|
registerRepository() {
|
426
|
-
//
|
427
|
+
// Check whether URL defines a Linny-R repository, and if so, add it.
|
427
428
|
fetch('repo/', postData({
|
428
429
|
action: 'add',
|
429
430
|
repo: this.add_modal.element('name').value,
|
@@ -341,7 +341,7 @@ module.exports = class MILPSolver {
|
|
341
341
|
'write problem', s.solver_model,
|
342
342
|
'set limit time %T%',
|
343
343
|
'set numerics feastol %I%',
|
344
|
-
|
344
|
+
'set limit gap %M%',
|
345
345
|
'optimize',
|
346
346
|
'write solution', s.solution,
|
347
347
|
'quit'
|
@@ -392,6 +392,7 @@ module.exports = class MILPSolver {
|
|
392
392
|
s.args = [
|
393
393
|
'-timeout %T%',
|
394
394
|
'-v4',
|
395
|
+
'-ac 5e-6',
|
395
396
|
'-e %I%',
|
396
397
|
'-gr %M%',
|
397
398
|
'-epsel 1e-7',
|
@@ -838,14 +839,13 @@ module.exports = class MILPSolver {
|
|
838
839
|
}
|
839
840
|
}
|
840
841
|
if(solved) {
|
841
|
-
//
|
842
|
-
|
843
|
-
|
842
|
+
// Line 0 holds solution status, line 1 the objective value,
|
843
|
+
// and lines 2+ the variables.
|
844
|
+
result.obj = parseFloat(output[1].split(':')[1]);
|
844
845
|
// Fill dictionary with variable name: value entries .
|
845
|
-
|
846
|
+
for(let i = 2; i < output.length; i++) {
|
846
847
|
const v = output[i].split(/\s+/);
|
847
848
|
x_dict[v[0]] = parseFloat(v[1]);
|
848
|
-
i++;
|
849
849
|
}
|
850
850
|
// Fill the solution vector, adding 0 for missing columns.
|
851
851
|
getValuesFromDict();
|
@@ -873,6 +873,8 @@ module.exports = class MILPSolver {
|
|
873
873
|
}
|
874
874
|
result.messages = msgs;
|
875
875
|
if(solved) {
|
876
|
+
// Get value of objective function
|
877
|
+
result.obj = parseFloat(output[i].split(':')[1]);
|
876
878
|
// Look for line with first variable.
|
877
879
|
while(i < output.length && !output[i].startsWith('X')) i++;
|
878
880
|
// Fill dictionary with variable name: value entries.
|
@@ -70,6 +70,8 @@ class LinnyRModel {
|
|
70
70
|
this.charts = [];
|
71
71
|
this.experiments = [];
|
72
72
|
this.dimensions = [];
|
73
|
+
this.selector_order_string = '';
|
74
|
+
this.selector_order_list = [];
|
73
75
|
this.next_process_number = 0;
|
74
76
|
this.next_product_number = 0;
|
75
77
|
this.focal_cluster = null;
|
@@ -104,7 +106,7 @@ class LinnyRModel {
|
|
104
106
|
this.preferred_solver = ''; // empty string denotes "use default"
|
105
107
|
this.integer_tolerance = 5e-7; // integer feasibility tolerance
|
106
108
|
this.MIP_gap = 1e-4; // relative MIP gap
|
107
|
-
this.always_diagnose =
|
109
|
+
this.always_diagnose = true;
|
108
110
|
|
109
111
|
// Sensitivity-related properties
|
110
112
|
this.base_case_selectors = '';
|
@@ -2905,6 +2907,10 @@ class LinnyRModel {
|
|
2905
2907
|
}
|
2906
2908
|
}
|
2907
2909
|
}
|
2910
|
+
// Infer selector order list by splitting text on any white space.
|
2911
|
+
this.selector_order_string = xmlDecoded(nodeContentByTag(node,
|
2912
|
+
'selector-order'));
|
2913
|
+
this.selector_order_list = this.selector_order_string.trim().split(/\s+/);
|
2908
2914
|
// Infer dimensions of experimental design space
|
2909
2915
|
this.inferDimensions();
|
2910
2916
|
// NOTE: when including a model, IGNORE sensitivity analysis, experiments
|
@@ -3074,8 +3080,11 @@ class LinnyRModel {
|
|
3074
3080
|
if(this.datasets.hasOwnProperty(obj)) xml += this.datasets[obj].asXML;
|
3075
3081
|
}
|
3076
3082
|
xml += '</datasets><charts>';
|
3077
|
-
|
3078
|
-
xml +=
|
3083
|
+
for(let i = 0; i < this.charts.length; i++) {
|
3084
|
+
xml += this.charts[i].asXML;
|
3085
|
+
}
|
3086
|
+
xml += '</charts><selector-order>' +
|
3087
|
+
xmlEncoded(this.selector_order_string) + '</selector-order>';
|
3079
3088
|
// NOTE: when "black-boxing", SA and experiments are not stored
|
3080
3089
|
if(!this.black_box) {
|
3081
3090
|
xml += '<base-case-selectors>' +
|
@@ -7943,54 +7952,59 @@ class Product extends Node {
|
|
7943
7952
|
}
|
7944
7953
|
|
7945
7954
|
get isSinkNode() {
|
7946
|
-
//
|
7955
|
+
// Return TRUE if this product behaves as a sink.
|
7947
7956
|
return (this.is_sink || this.allOutputsAreData) &&
|
7948
7957
|
!(this.upper_bound.defined ||
|
7949
|
-
// NOTE: UB may be set by equalling it to LB
|
7958
|
+
// NOTE: UB may be set by equalling it to LB.
|
7950
7959
|
(this.equal_bounds && this.lower_bound.defined));
|
7951
7960
|
}
|
7952
7961
|
|
7953
7962
|
highestUpperBound(visited) {
|
7954
|
-
//
|
7955
|
-
// ingoing links (type, rate, and UB of their from nodes)
|
7956
|
-
//
|
7957
|
-
// ON/OFF binary variable for this product
|
7958
|
-
//
|
7959
|
-
// of a cycle in the graph, its highest UB co-depends on its own,
|
7960
|
-
// is not constrained, so return +INF
|
7961
|
-
//
|
7962
|
-
// might be inferred from their max. inflows
|
7963
|
+
// Infer the upper bound for this product from its own UB, or from
|
7964
|
+
// its ingoing links (type, rate, and UB of their from nodes).
|
7965
|
+
// This is used while compiling the VM instructions that compute the
|
7966
|
+
// ON/OFF binary variable for this product.
|
7967
|
+
// This method performs a graph traversal. If this product is part
|
7968
|
+
// of a cycle in the graph, its highest UB co-depends on its own UB,
|
7969
|
+
// which is not constrained, so then return +INF.
|
7970
|
+
// No need to check for sink nodes, as even on those nodes a max. UB
|
7971
|
+
// might be inferred from their max. inflows.
|
7963
7972
|
if(visited.indexOf(this) >= 0) return VM.PLUS_INFINITY;
|
7964
7973
|
let ub = (this.equal_bounds ? this.lower_bound : this.upper_bound);
|
7965
|
-
// If
|
7974
|
+
// If dynamic expression, return +INF to signal "no lower UB can be inferred".
|
7966
7975
|
if(ub.defined && !ub.isStatic) return VM.PLUS_INFINITY;
|
7967
|
-
// If static, use its value as initial highest value
|
7968
|
-
const max_ub = ub.result(0);
|
7969
|
-
// See if the sum of its max. inflows will be lower than this value
|
7976
|
+
// If static, use its value as initial highest value.
|
7977
|
+
const max_ub = (ub.defined ? ub.result(0) : VM.PLUS_INFINITY);
|
7978
|
+
// See if the sum of its max. inflows will be lower than this value.
|
7970
7979
|
let sum = 0;
|
7971
|
-
// Preclude infinite recursion
|
7980
|
+
// Preclude infinite recursion.
|
7972
7981
|
visited.push(this);
|
7973
7982
|
for(let i = 0; i < this.inputs.length; i++) {
|
7974
7983
|
const
|
7975
7984
|
l = this.inputs[i],
|
7976
7985
|
r = l.relative_rate,
|
7977
7986
|
fn = l.from_node;
|
7978
|
-
// Dynamic rate => inflows cannot constrain the UB any further
|
7987
|
+
// Dynamic rate => inflows cannot constrain the UB any further.
|
7979
7988
|
if(!r.isStatic) return max_ub;
|
7989
|
+
let rate = r.result(0);
|
7980
7990
|
if([VM.LM_STARTUP, VM.LM_POSITIVE, VM.LM_ZERO, VM.LM_FIRST_COMMIT,
|
7981
7991
|
VM.LM_SHUTDOWN].indexOf(l.multiplier) >= 0) {
|
7982
|
-
// For binary multipliers, the rate is the highest possible flow
|
7983
|
-
// NOTE:
|
7984
|
-
sum += Math.max(0,
|
7992
|
+
// For binary multipliers, the rate is the highest possible flow.
|
7993
|
+
// NOTE: Do not add negative flows, as actual flow may be 0.
|
7994
|
+
sum += Math.max(0, rate);
|
7985
7995
|
} else {
|
7986
|
-
// For other multipliers, max flow = rate * UB of the FROM node
|
7987
|
-
//
|
7996
|
+
// For other multipliers, max flow = rate * UB of the FROM node.
|
7997
|
+
// For products, this will recurse; processes return their UB.
|
7988
7998
|
let fnub = fn.highestUpperBound(visited);
|
7989
7999
|
// If +INF, no lower UB can be inferred => return initial maximum
|
7990
8000
|
if(fnub >= VM.PLUS_INFINITY) return max_ub;
|
7991
|
-
// Otherwise, add rate * UB to the max. total inflow
|
7992
|
-
// NOTE:
|
7993
|
-
|
8001
|
+
// Otherwise, add rate * UB to the max. total inflow.
|
8002
|
+
// NOTE: For SUM multipliers, also consider the delay.
|
8003
|
+
if(l.multiplier === VM.LM_SUM) {
|
8004
|
+
rate *= Math.max(1, l.flow_delay.result(0));
|
8005
|
+
}
|
8006
|
+
// NOTE: Do not add negative flows, as actual flow may be 0.
|
8007
|
+
sum += Math.max(0, rate * fnub);
|
7994
8008
|
}
|
7995
8009
|
}
|
7996
8010
|
// Return the sum of max. inflows as the lowest max. UB, or the initial
|
@@ -492,7 +492,7 @@ function matchingNumber(m, s) {
|
|
492
492
|
// (where asterisks match 0 or more characters, and question marks 1
|
493
493
|
// character) and the matching parts jointly denote an integer.
|
494
494
|
// NOTE: A "+" must be escaped, "*" and "?" must become groups.
|
495
|
-
let raw = s.replaceAll('+', '
|
495
|
+
let raw = s.replaceAll('+', '\\+')
|
496
496
|
.replace(/\*/g, '(.*)').replace(/\?/g, '(.)'),
|
497
497
|
match = m.match(new RegExp(`^${raw}$`)),
|
498
498
|
n = '';
|
@@ -534,9 +534,21 @@ function compareSelectors(s1, s2) {
|
|
534
534
|
// Dataset selectors comparison is case-insensitive, and puts wildcards
|
535
535
|
// last, where * comes later than ?, and leading colons come AFTER
|
536
536
|
// regular selector names.
|
537
|
-
//
|
538
|
-
//
|
539
|
-
//
|
537
|
+
// NOTES:
|
538
|
+
// (1) Without wildcards, strings that are identical except for
|
539
|
+
// the digits they *end* on are sorted on this "end number"
|
540
|
+
// (so abc12 > abc2).
|
541
|
+
// (2) This also applies to percentages ("end number"+ %).
|
542
|
+
// (3) As of version 1.9.0, the order of selectors can be (partially)
|
543
|
+
// specified by the modeler.
|
544
|
+
if(MODEL) {
|
545
|
+
const
|
546
|
+
i1 = MODEL.selector_order_list.indexOf(s1),
|
547
|
+
i2 = MODEL.selector_order_list.indexOf(s2);
|
548
|
+
// BOTH selectors must be in the list, or regular sorting order is
|
549
|
+
// applied.
|
550
|
+
if(i1 >= 0 && i2 >= 0) return i1 - i2;
|
551
|
+
}
|
540
552
|
if(s1 === s2) return 0;
|
541
553
|
if(s1 === '*') return 1;
|
542
554
|
if(s2 === '*') return -1;
|
@@ -580,22 +592,36 @@ function compareSelectors(s1, s2) {
|
|
580
592
|
// by as many spaces (ASCII 32) and add a '!' (ASCII 33). This will then
|
581
593
|
// "sorts things out".
|
582
594
|
let n = s_1.length,
|
583
|
-
i = n - 1
|
595
|
+
i = n - 1,
|
596
|
+
plusmin = false;
|
584
597
|
while(i >= 0 && s_1[i] === '-') i--;
|
585
|
-
// If trailing minuses, replace by as many spaces and add an exclamation
|
598
|
+
// If trailing minuses, replace by as many spaces and add an exclamation
|
599
|
+
// point.
|
586
600
|
if(i < n - 1) {
|
587
|
-
s_1 = s_1.substring(0, i);
|
601
|
+
s_1 = s_1.substring(0, i + 1);
|
602
|
+
// NOTE: Consider X- as lower than just X.
|
603
|
+
if(s_1 === s_2) return -1;
|
588
604
|
while(s_1.length < n) s_1 += ' ';
|
589
605
|
s_1 += '!';
|
606
|
+
plusmin = true;
|
590
607
|
}
|
591
|
-
// Do the same for the second "normalized" selector
|
608
|
+
// Do the same for the second "normalized" selector.
|
592
609
|
n = s_2.length;
|
593
610
|
i = n - 1;
|
594
611
|
while(i >= 0 && s_2[i] === '-') i--;
|
595
612
|
if(i < n - 1) {
|
596
|
-
s_2 = s_2.substring(0, i);
|
613
|
+
s_2 = s_2.substring(0, i + 1);
|
614
|
+
// NOTE: Consider X as higher than X-.
|
615
|
+
if(s_2 === s_1) return 1;
|
597
616
|
while(s_2.length < n) s_2 += ' ';
|
598
617
|
s_2 += '!';
|
618
|
+
plusmin = true;
|
619
|
+
}
|
620
|
+
if(plusmin) {
|
621
|
+
// As X0 typically denotes "base case", replace a trailing zero by
|
622
|
+
// ")" to ensure that X- < X0 < X+.
|
623
|
+
s_1.replace(/([^0])0$/, '$1)');
|
624
|
+
s_2.replace(/([^0])0$/, '$1)');
|
599
625
|
}
|
600
626
|
// Now compare the two "normalized" selectors
|
601
627
|
if(s_1 < s_2) return -1;
|
@@ -317,6 +317,7 @@ class Expression {
|
|
317
317
|
if((typeof number !== 'number' ||
|
318
318
|
(this.isStatic && !this.isWildcardExpression)) &&
|
319
319
|
!this.isMethod) return this.vector;
|
320
|
+
//console.log('HERE choosing wcnr', number, this);
|
320
321
|
// Method expressions are not "numbered" but differentiate by the
|
321
322
|
// entity to which they are applied. Their "vector number" is then
|
322
323
|
// inferred by looking up this entity in a method object list.
|
@@ -331,7 +332,9 @@ class Expression {
|
|
331
332
|
}
|
332
333
|
// Use the vector for the wildcard number (create it if necessary).
|
333
334
|
if(!this.wildcard_vectors.hasOwnProperty(number)) {
|
335
|
+
//console.log('HERE adding wc vector', number, this);
|
334
336
|
this.wildcard_vectors[number] = [];
|
337
|
+
//console.log('HERE adding wc vector', number, this.wildcard_vectors);
|
335
338
|
if(this.isStatic) {
|
336
339
|
this.wildcard_vectors[number][0] = VM.NOT_COMPUTED;
|
337
340
|
} else {
|
@@ -2136,8 +2139,8 @@ class VirtualMachine {
|
|
2136
2139
|
this.SOLVER_MINUS_INFINITY = -1e+30;
|
2137
2140
|
// As of version 1.8.0, Linny-R imposes no +INF bounds on processes
|
2138
2141
|
// unless diagnosing an unbounded problem. For such diagnosis, the
|
2139
|
-
// (relatively) low value 9.
|
2140
|
-
this.DIAGNOSIS_UPPER_BOUND = 9.
|
2142
|
+
// (relatively) low value 9.999999999e+9 is used.
|
2143
|
+
this.DIAGNOSIS_UPPER_BOUND = 9.999999999e+9;
|
2141
2144
|
// NOTE: Below the "near zero" limit, a number is considered zero
|
2142
2145
|
// (this is to timely detect division-by-zero errors).
|
2143
2146
|
this.NEAR_ZERO = 1e-10;
|
@@ -2149,7 +2152,8 @@ class VirtualMachine {
|
|
2149
2152
|
// their target without displaying them in red or blue to signal
|
2150
2153
|
// shortage or surplus.
|
2151
2154
|
this.SIG_DIF_LIMIT = 0.001;
|
2152
|
-
|
2155
|
+
// Numbers near zero are displayed as +0 or -0.
|
2156
|
+
this.SIG_DIF_FROM_ZERO = 5e-5;
|
2153
2157
|
// ON/OFF threshold is used to differentiate between level = 0 and
|
2154
2158
|
// still "ON" (will be displayed as +0).
|
2155
2159
|
this.ON_OFF_THRESHOLD = 1.5e-4;
|
@@ -2194,6 +2198,7 @@ class VirtualMachine {
|
|
2194
2198
|
this.LM_NEEDING_ON_OFF = [5, 6, 7, 8, 9, 10];
|
2195
2199
|
this.LM_SYMBOLS = ['', '\u21C9', '\u0394', '\u03A3', '\u03BC', '\u25B2',
|
2196
2200
|
'+', '0', '\u2934', '\u2732', '\u25BC', '\u2A39'];
|
2201
|
+
this.LM_LETTERS = ' TDSMU+0RFDP';
|
2197
2202
|
|
2198
2203
|
// VM max. expression stack size.
|
2199
2204
|
this.MAX_STACK = 200;
|
@@ -2537,7 +2542,7 @@ class VirtualMachine {
|
|
2537
2542
|
if(sv[0]) return sv[1];
|
2538
2543
|
const a = Math.abs(n);
|
2539
2544
|
// Signal small differences from true 0 by leading + or - sign.
|
2540
|
-
if(n !== 0 && a
|
2545
|
+
if(n !== 0 && a <= this.ON_OFF_THRESHOLD) return n > 0 ? '+0' : '-0';
|
2541
2546
|
if(a >= 999999.5) return n.toPrecision(2);
|
2542
2547
|
if(Math.abs(a-Math.round(a)) < 0.05) return Math.round(n);
|
2543
2548
|
if(a < 1) return Math.round(n*100) / 100;
|
@@ -2556,7 +2561,7 @@ class VirtualMachine {
|
|
2556
2561
|
if(sv[0]) return sv[1];
|
2557
2562
|
const a = Math.abs(n);
|
2558
2563
|
// Signal small differences from true 0 by a leading + or - sign.
|
2559
|
-
if(n !== 0 && a
|
2564
|
+
if(n !== 0 && a <= this.ON_OFF_THRESHOLD) return n > 0 ? '+0' : '-0';
|
2560
2565
|
if(a >= 9999995) return n.toPrecision(4);
|
2561
2566
|
if(Math.abs(a-Math.round(a)) < 0.0005) return Math.round(n);
|
2562
2567
|
if(a < 1) return Math.round(n*10000) / 10000;
|
@@ -2922,19 +2927,20 @@ class VirtualMachine {
|
|
2922
2927
|
for(let i = 2; i <= n; i++) {
|
2923
2928
|
this.variables.push(['W' + i, obj]);
|
2924
2929
|
}
|
2925
|
-
// NOTE:
|
2926
|
-
//
|
2927
|
-
|
2928
|
-
|
2929
|
-
|
2930
|
-
|
2931
|
-
|
2932
|
-
|
2933
|
-
|
2934
|
-
|
2935
|
-
|
2936
|
-
|
2937
|
-
|
2930
|
+
// NOTE: SOS constraints are not needed when a bound line defines
|
2931
|
+
// a convex feasible area.
|
2932
|
+
if(!obj.needsNoSOS) {
|
2933
|
+
this.sos_var_indices.push([index, n]);
|
2934
|
+
// NOTE: Some solvers do not support SOS. To ensure that only 2
|
2935
|
+
// adjacent w[i]-variables are non-zero (they range from 0 to 1),
|
2936
|
+
// as many binary variables b[i] must be defined, and additional
|
2937
|
+
// constraints must be added (see VMI_add_bound_line_constraint).
|
2938
|
+
if(this.noSupportForSOS) {
|
2939
|
+
for(let i = 1; i <= n; i++) {
|
2940
|
+
const bi = this.variables.push(['b' + i, obj]);
|
2941
|
+
this.bin_var_indices[bi] = true;
|
2942
|
+
}
|
2943
|
+
}
|
2938
2944
|
}
|
2939
2945
|
}
|
2940
2946
|
return index;
|
@@ -4044,7 +4050,7 @@ class VirtualMachine {
|
|
4044
4050
|
these variables can take on higher values. The modeler must ensure
|
4045
4051
|
that there is a cost associated with the actual flow, not a revenue.
|
4046
4052
|
*/
|
4047
|
-
// NOTE:
|
4053
|
+
// NOTE: As of 20 June 2021, binary attributes of products are also computed.
|
4048
4054
|
const pp_nodes = [];
|
4049
4055
|
for(i = 0; i < process_keys.length; i++) {
|
4050
4056
|
k = process_keys[i];
|
@@ -4093,8 +4099,8 @@ class VirtualMachine {
|
|
4093
4099
|
[VMI_add_const_to_coefficient, [p.level_var_index, 1]]
|
4094
4100
|
);
|
4095
4101
|
if(ubx.isStatic) {
|
4096
|
-
// If UB is very high (typically: undefined, so +INF), try to
|
4097
|
-
// a lower value for UB to use for the ON/OFF binary
|
4102
|
+
// If UB is very high (typically: undefined, so +INF), try to
|
4103
|
+
// infer a lower value for UB to use for the ON/OFF binary.
|
4098
4104
|
let ub = ubx.result(0),
|
4099
4105
|
hub = ub;
|
4100
4106
|
if(ub > VM.MEGA_UPPER_BOUND) {
|
@@ -4642,7 +4648,7 @@ class VirtualMachine {
|
|
4642
4648
|
if(v[1] instanceof BoundLine) {
|
4643
4649
|
v[1].constraint.slack_info[b] = v[0];
|
4644
4650
|
}
|
4645
|
-
if(b <= this.nr_of_time_steps && absl > VM.
|
4651
|
+
if(b <= this.nr_of_time_steps && absl > VM.ON_OFF_THRESHOLD) {
|
4646
4652
|
this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
|
4647
4653
|
`${v[1].displayName} ${v[0]} slack = ${this.sig4Dig(slack)}`);
|
4648
4654
|
if(v[1] instanceof Product) {
|
@@ -4652,9 +4658,9 @@ class VirtualMachine {
|
|
4652
4658
|
}
|
4653
4659
|
}
|
4654
4660
|
} else if(CONFIGURATION.slight_slack_notices) {
|
4655
|
-
this.logMessage(block, '
|
4661
|
+
this.logMessage(block, '---- Notice: (t=' + b + round + ') ' +
|
4656
4662
|
v[1].displayName + ' ' + v[0] + ' slack = ' +
|
4657
|
-
slack.toPrecision(
|
4663
|
+
slack.toPrecision(1));
|
4658
4664
|
}
|
4659
4665
|
}
|
4660
4666
|
}
|
@@ -5410,7 +5416,7 @@ class VirtualMachine {
|
|
5410
5416
|
this.numeric_issue = '';
|
5411
5417
|
// First add the objective (always MAXimize).
|
5412
5418
|
if(cplex) {
|
5413
|
-
this.lines =
|
5419
|
+
this.lines = `\\${this.solver_id}\nMaximize\n`;
|
5414
5420
|
} else {
|
5415
5421
|
this.lines = '/* Objective function */\nmax:\n';
|
5416
5422
|
}
|
@@ -5461,6 +5467,12 @@ class VirtualMachine {
|
|
5461
5467
|
}
|
5462
5468
|
}
|
5463
5469
|
c = this.right_hand_side[r];
|
5470
|
+
// NOTE: When previous block was infeasible or unbounded (no solution),
|
5471
|
+
// expressions for RHS may not evaluate as a number.
|
5472
|
+
if(Number.isNaN(c)) {
|
5473
|
+
this.setNumericIssue(c, r, 'constraint RHS');
|
5474
|
+
c = 0;
|
5475
|
+
}
|
5464
5476
|
this.lines += line + ' ' +
|
5465
5477
|
this.constraint_symbols[this.constraint_types[r]] + ' ' + c + EOL;
|
5466
5478
|
line = '';
|
@@ -5507,8 +5519,14 @@ class VirtualMachine {
|
|
5507
5519
|
}
|
5508
5520
|
} else {
|
5509
5521
|
// Bounds can be specified on a single line: lb <= X001 <= ub.
|
5510
|
-
|
5511
|
-
|
5522
|
+
// NOTE: LP_solve has Infinity value 1e+25. Use this literal
|
5523
|
+
// because VM.PLUS_INFINITY may be set to *diagnostic* value.
|
5524
|
+
if(lb !== null && lb !== 0 && lb > -1e+25) {
|
5525
|
+
line = lb + ' <= ' + line;
|
5526
|
+
}
|
5527
|
+
if(ub !== null && ub < 1e+25) line += ' <= ' + ub;
|
5528
|
+
// NOTE: Do not add line if both bounds are infinite.
|
5529
|
+
if(line.indexOf('<=') < 0) line = '';
|
5512
5530
|
}
|
5513
5531
|
}
|
5514
5532
|
if(line) this.lines += line + EOL;
|
@@ -5568,19 +5586,17 @@ class VirtualMachine {
|
|
5568
5586
|
// NOTE: Add SOS section only if the solver supports SOS.
|
5569
5587
|
if(this.sos_var_indices.length > 0 && !this.noSupportForSOS) {
|
5570
5588
|
this.lines += 'SOS\n';
|
5571
|
-
let sos = 0;
|
5572
5589
|
const v_set = [];
|
5573
5590
|
for(let j = 0; j < abl; j++) {
|
5574
5591
|
for(let i = 0; i < this.sos_var_indices.length; i++) {
|
5592
|
+
const svi = this.sos_var_indices[i];
|
5575
5593
|
v_set.length = 0;
|
5576
|
-
let vi =
|
5577
|
-
|
5578
|
-
for(let j = 1; j <= n; j++) {
|
5594
|
+
let vi = svi[0] + j * this.cols;
|
5595
|
+
for(let j = 1; j <= svi[1]; j++) {
|
5579
5596
|
v_set.push(`${vbl(vi)}:${j}`);
|
5580
5597
|
vi++;
|
5581
5598
|
}
|
5582
|
-
this.lines += ` s${
|
5583
|
-
sos++;
|
5599
|
+
this.lines += ` s${i}: S2:: ${v_set.join(' ')}\n`;
|
5584
5600
|
}
|
5585
5601
|
}
|
5586
5602
|
}
|
@@ -5605,18 +5621,16 @@ class VirtualMachine {
|
|
5605
5621
|
// LP_solve supports SOS, so add the SOS section if needed.
|
5606
5622
|
if(this.sos_var_indices.length > 0) {
|
5607
5623
|
this.lines += 'sos\n';
|
5608
|
-
let sos = 1;
|
5609
5624
|
for(let j = 0; j < abl; j++) {
|
5610
5625
|
for(let i = 0; i < this.sos_var_indices.length; i++) {
|
5626
|
+
const svi = this.sos_var_indices[i];
|
5611
5627
|
v_set.length = 0;
|
5612
|
-
let vi =
|
5613
|
-
|
5614
|
-
for(let j = 1; j <= n; j++) {
|
5628
|
+
let vi = svi[0] + j * this.cols;
|
5629
|
+
for(let j = 1; j <= svi[1]; j++) {
|
5615
5630
|
v_set.push(vbl(vi));
|
5616
5631
|
vi++;
|
5617
5632
|
}
|
5618
5633
|
this.lines += `SOS${sos}: ${v_set.join(',')} <= 2;\n`;
|
5619
|
-
sos++;
|
5620
5634
|
}
|
5621
5635
|
}
|
5622
5636
|
}
|
@@ -5825,19 +5839,18 @@ class VirtualMachine {
|
|
5825
5839
|
if(this.sos_var_indices.length > 0) {
|
5826
5840
|
this.lines += 'SOS\n';
|
5827
5841
|
const abl = this.actualBlockLength(this.block_count);
|
5828
|
-
let sos = 1;
|
5829
5842
|
for(let j = 0; j < abl; j++) {
|
5830
5843
|
for(let i = 0; i < this.sos_var_indices.length; i++) {
|
5831
|
-
|
5832
|
-
|
5833
|
-
|
5834
|
-
for(let j = 1; j <=
|
5835
|
-
const s = ' X' +
|
5844
|
+
const svi = this.sos_var_indices[i];
|
5845
|
+
this.lines += ` S2 sos${i + 1}\n`;
|
5846
|
+
let vi = svi[0] + j * this.cols;
|
5847
|
+
for(let j = 1; j <= svi[1]; j++) {
|
5848
|
+
const s = ' X' +
|
5849
|
+
vi.toString().padStart(this.decimals, '0') +
|
5836
5850
|
' ';
|
5837
5851
|
this.lines += s.substring(0, 15) + j + '\n';
|
5838
5852
|
vi++;
|
5839
5853
|
}
|
5840
|
-
sos++;
|
5841
5854
|
}
|
5842
5855
|
}
|
5843
5856
|
}
|
@@ -6145,7 +6158,7 @@ Solver status = ${json.status}`);
|
|
6145
6158
|
}
|
6146
6159
|
// Diagnosis (by adding slack variables and finite bounds on processes)
|
6147
6160
|
// is activated when Alt-clicking the "run" button, or by clicking the
|
6148
|
-
// "clicke
|
6161
|
+
// "clicke *here* to diagnose" link on the infoline.
|
6149
6162
|
this.diagnose = diagnose || MODEL.always_diagnose;
|
6150
6163
|
if(this.diagnose) {
|
6151
6164
|
this.PLUS_INFINITY = this.DIAGNOSIS_UPPER_BOUND;
|
@@ -8329,10 +8342,11 @@ function VMI_set_const_rhs(c) {
|
|
8329
8342
|
}
|
8330
8343
|
|
8331
8344
|
function VMI_set_var_rhs(x) {
|
8345
|
+
VM.rhs = x.result(VM.t);
|
8332
8346
|
if(DEBUGGING) {
|
8333
|
-
console.log(
|
8347
|
+
console.log(`set_var_rhs: ${x.variableName} (t = ${VM.t}) = ` +
|
8348
|
+
VM.sig4Dig(VM.rhs));
|
8334
8349
|
}
|
8335
|
-
VM.rhs = x.result(VM.t);
|
8336
8350
|
}
|
8337
8351
|
|
8338
8352
|
function VMI_add_const_to_rhs(c) {
|