linny-r 1.8.2 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/static/images/sel-order.png +0 -0
- package/static/index.html +31 -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 +8 -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 +2 -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 +45 -28
- package/static/scripts/linny-r-utils.js +35 -9
- package/static/scripts/linny-r-vm.js +108 -60
@@ -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,8 @@ 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;
|
110
|
+
this.show_notices = true;
|
108
111
|
|
109
112
|
// Sensitivity-related properties
|
110
113
|
this.base_case_selectors = '';
|
@@ -2668,6 +2671,7 @@ class LinnyRModel {
|
|
2668
2671
|
this.report_results = nodeParameterValue(node, 'report-results') === '1';
|
2669
2672
|
this.show_block_arrows = nodeParameterValue(node, 'block-arrows') === '1';
|
2670
2673
|
this.always_diagnose = nodeParameterValue(node, 'diagnose') === '1';
|
2674
|
+
this.show_notices = nodeParameterValue(node, 'show-notices') === '1';
|
2671
2675
|
this.name = xmlDecoded(nodeContentByTag(node, 'name'));
|
2672
2676
|
this.author = xmlDecoded(nodeContentByTag(node, 'author'));
|
2673
2677
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
@@ -2905,6 +2909,10 @@ class LinnyRModel {
|
|
2905
2909
|
}
|
2906
2910
|
}
|
2907
2911
|
}
|
2912
|
+
// Infer selector order list by splitting text on any white space.
|
2913
|
+
this.selector_order_string = xmlDecoded(nodeContentByTag(node,
|
2914
|
+
'selector-order'));
|
2915
|
+
this.selector_order_list = this.selector_order_string.trim().split(/\s+/);
|
2908
2916
|
// Infer dimensions of experimental design space
|
2909
2917
|
this.inferDimensions();
|
2910
2918
|
// NOTE: when including a model, IGNORE sensitivity analysis, experiments
|
@@ -3017,6 +3025,7 @@ class LinnyRModel {
|
|
3017
3025
|
if(this.report_results) p += ' report-results="1"';
|
3018
3026
|
if(this.show_block_arrows) p += ' block-arrows="1"';
|
3019
3027
|
if(this.always_diagnose) p += ' diagnose="1"';
|
3028
|
+
if(this.show_notices) p += ' show-notices="1"';
|
3020
3029
|
let xml = this.xml_header + ['<model', p, '><name>', xmlEncoded(this.name),
|
3021
3030
|
'</name><author>', xmlEncoded(this.author),
|
3022
3031
|
'</author><notes>', xmlEncoded(this.comments),
|
@@ -3074,8 +3083,11 @@ class LinnyRModel {
|
|
3074
3083
|
if(this.datasets.hasOwnProperty(obj)) xml += this.datasets[obj].asXML;
|
3075
3084
|
}
|
3076
3085
|
xml += '</datasets><charts>';
|
3077
|
-
|
3078
|
-
xml +=
|
3086
|
+
for(let i = 0; i < this.charts.length; i++) {
|
3087
|
+
xml += this.charts[i].asXML;
|
3088
|
+
}
|
3089
|
+
xml += '</charts><selector-order>' +
|
3090
|
+
xmlEncoded(this.selector_order_string) + '</selector-order>';
|
3079
3091
|
// NOTE: when "black-boxing", SA and experiments are not stored
|
3080
3092
|
if(!this.black_box) {
|
3081
3093
|
xml += '<base-case-selectors>' +
|
@@ -7943,54 +7955,59 @@ class Product extends Node {
|
|
7943
7955
|
}
|
7944
7956
|
|
7945
7957
|
get isSinkNode() {
|
7946
|
-
//
|
7958
|
+
// Return TRUE if this product behaves as a sink.
|
7947
7959
|
return (this.is_sink || this.allOutputsAreData) &&
|
7948
7960
|
!(this.upper_bound.defined ||
|
7949
|
-
// NOTE: UB may be set by equalling it to LB
|
7961
|
+
// NOTE: UB may be set by equalling it to LB.
|
7950
7962
|
(this.equal_bounds && this.lower_bound.defined));
|
7951
7963
|
}
|
7952
7964
|
|
7953
7965
|
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
|
7966
|
+
// Infer the upper bound for this product from its own UB, or from
|
7967
|
+
// its ingoing links (type, rate, and UB of their from nodes).
|
7968
|
+
// This is used while compiling the VM instructions that compute the
|
7969
|
+
// ON/OFF binary variable for this product.
|
7970
|
+
// This method performs a graph traversal. If this product is part
|
7971
|
+
// of a cycle in the graph, its highest UB co-depends on its own UB,
|
7972
|
+
// which is not constrained, so then return +INF.
|
7973
|
+
// No need to check for sink nodes, as even on those nodes a max. UB
|
7974
|
+
// might be inferred from their max. inflows.
|
7963
7975
|
if(visited.indexOf(this) >= 0) return VM.PLUS_INFINITY;
|
7964
7976
|
let ub = (this.equal_bounds ? this.lower_bound : this.upper_bound);
|
7965
|
-
// If
|
7977
|
+
// If dynamic expression, return +INF to signal "no lower UB can be inferred".
|
7966
7978
|
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
|
7979
|
+
// If static, use its value as initial highest value.
|
7980
|
+
const max_ub = (ub.defined ? ub.result(0) : VM.PLUS_INFINITY);
|
7981
|
+
// See if the sum of its max. inflows will be lower than this value.
|
7970
7982
|
let sum = 0;
|
7971
|
-
// Preclude infinite recursion
|
7983
|
+
// Preclude infinite recursion.
|
7972
7984
|
visited.push(this);
|
7973
7985
|
for(let i = 0; i < this.inputs.length; i++) {
|
7974
7986
|
const
|
7975
7987
|
l = this.inputs[i],
|
7976
7988
|
r = l.relative_rate,
|
7977
7989
|
fn = l.from_node;
|
7978
|
-
// Dynamic rate => inflows cannot constrain the UB any further
|
7990
|
+
// Dynamic rate => inflows cannot constrain the UB any further.
|
7979
7991
|
if(!r.isStatic) return max_ub;
|
7992
|
+
let rate = r.result(0);
|
7980
7993
|
if([VM.LM_STARTUP, VM.LM_POSITIVE, VM.LM_ZERO, VM.LM_FIRST_COMMIT,
|
7981
7994
|
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,
|
7995
|
+
// For binary multipliers, the rate is the highest possible flow.
|
7996
|
+
// NOTE: Do not add negative flows, as actual flow may be 0.
|
7997
|
+
sum += Math.max(0, rate);
|
7985
7998
|
} else {
|
7986
|
-
// For other multipliers, max flow = rate * UB of the FROM node
|
7987
|
-
//
|
7999
|
+
// For other multipliers, max flow = rate * UB of the FROM node.
|
8000
|
+
// For products, this will recurse; processes return their UB.
|
7988
8001
|
let fnub = fn.highestUpperBound(visited);
|
7989
8002
|
// If +INF, no lower UB can be inferred => return initial maximum
|
7990
8003
|
if(fnub >= VM.PLUS_INFINITY) return max_ub;
|
7991
|
-
// Otherwise, add rate * UB to the max. total inflow
|
7992
|
-
// NOTE:
|
7993
|
-
|
8004
|
+
// Otherwise, add rate * UB to the max. total inflow.
|
8005
|
+
// NOTE: For SUM multipliers, also consider the delay.
|
8006
|
+
if(l.multiplier === VM.LM_SUM) {
|
8007
|
+
rate *= Math.max(1, l.flow_delay.result(0));
|
8008
|
+
}
|
8009
|
+
// NOTE: Do not add negative flows, as actual flow may be 0.
|
8010
|
+
sum += Math.max(0, rate * fnub);
|
7994
8011
|
}
|
7995
8012
|
}
|
7996
8013
|
// 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;
|