linny-r 1.2.1 → 1.3.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/README.md +6 -6
- package/console.js +139 -7
- package/package.json +2 -2
- package/server.js +15 -11
- package/static/images/paperclip.png +0 -0
- package/static/images/paste.png +0 -0
- package/static/index.html +40 -8
- package/static/linny-r.css +97 -19
- package/static/scripts/linny-r-ctrl.js +22 -4
- package/static/scripts/linny-r-gui.js +723 -140
- package/static/scripts/linny-r-model.js +185 -29
- package/static/scripts/linny-r-utils.js +49 -11
- package/static/scripts/linny-r-vm.js +32 -20
@@ -120,6 +120,9 @@ class LinnyRModel {
|
|
120
120
|
// Set the indicator that the model has not been solved yet
|
121
121
|
this.set_up = false;
|
122
122
|
this.solved = false;
|
123
|
+
// Reset counts of effects of a rename operation
|
124
|
+
this.variable_count = 0;
|
125
|
+
this.expression_count = 0;
|
123
126
|
}
|
124
127
|
|
125
128
|
// NOTE: a model can also be the entity for the documentation manager,
|
@@ -557,7 +560,7 @@ class LinnyRModel {
|
|
557
560
|
inferPrefix(obj) {
|
558
561
|
// Return the inferred (!) prefixes of `obj` as a list
|
559
562
|
if(obj) {
|
560
|
-
const pl = obj.displayName
|
563
|
+
const pl = UI.prefixesAndName(obj.displayName);
|
561
564
|
if(pl.length > 1) {
|
562
565
|
pl.pop();
|
563
566
|
return pl;
|
@@ -565,7 +568,7 @@ class LinnyRModel {
|
|
565
568
|
}
|
566
569
|
return [];
|
567
570
|
}
|
568
|
-
|
571
|
+
|
569
572
|
inferParentCluster(obj) {
|
570
573
|
// Find the best "parent" cluster for link or constraint `obj`
|
571
574
|
let p, q;
|
@@ -633,6 +636,14 @@ class LinnyRModel {
|
|
633
636
|
}
|
634
637
|
return -1;
|
635
638
|
}
|
639
|
+
|
640
|
+
isDimensionSelector(s) {
|
641
|
+
// Returns TRUE if `s` is a dimension selector in some experiment
|
642
|
+
for(let i = 0; i < this.experiments.length; i++) {
|
643
|
+
if(this.experiments[i].isDimensionSelector(s)) return true;
|
644
|
+
}
|
645
|
+
return false;
|
646
|
+
}
|
636
647
|
|
637
648
|
canLink(from, to) {
|
638
649
|
// Return TRUE iff FROM-node can feature a "straight" link (i.e., a
|
@@ -915,7 +926,7 @@ class LinnyRModel {
|
|
915
926
|
for(let i = 0; i < c.notes.length; i++) {
|
916
927
|
const
|
917
928
|
n = c.notes[i],
|
918
|
-
tags = n.
|
929
|
+
tags = n.tagList;
|
919
930
|
if(tags) {
|
920
931
|
for(let i = 0; i < tags.length; i++) {
|
921
932
|
const
|
@@ -1562,6 +1573,75 @@ class LinnyRModel {
|
|
1562
1573
|
return true;
|
1563
1574
|
}
|
1564
1575
|
|
1576
|
+
get selectionAsXML() {
|
1577
|
+
// Returns XML for the selected entities, and also for the entities
|
1578
|
+
// referenced by expressions for their attributes.
|
1579
|
+
// NOTE: the name and actor name of the focal cluster are added as
|
1580
|
+
// attributes of the main node to permit "smart" renaming of
|
1581
|
+
// entities when PASTE would result in name conflicts.
|
1582
|
+
if(this.selection.length <= 0) return '';
|
1583
|
+
const
|
1584
|
+
fc_name = this.focal_cluster.name,
|
1585
|
+
fc_actor = this.focal_cluster.actor.name,
|
1586
|
+
entities = {
|
1587
|
+
Cluster: [],
|
1588
|
+
Link: [],
|
1589
|
+
Constraint: [],
|
1590
|
+
Note: [],
|
1591
|
+
Product: [],
|
1592
|
+
Process: []
|
1593
|
+
},
|
1594
|
+
extras = [],
|
1595
|
+
from_tos = [],
|
1596
|
+
xml = [],
|
1597
|
+
ft_xml = [],
|
1598
|
+
extra_xml = [];
|
1599
|
+
for(let i = 0; i < this.selection.length; i++) {
|
1600
|
+
const obj = this.selection[i];
|
1601
|
+
entities[obj.type].push(obj);
|
1602
|
+
}
|
1603
|
+
for(let i = 0; i < entities.Note.length; i++) {
|
1604
|
+
const n = entities.Note[i];
|
1605
|
+
xml.push(n.asXML);
|
1606
|
+
}
|
1607
|
+
for(let i = 0; i < entities.Product.length; i++) {
|
1608
|
+
xml.push(entities.Product[i].asXML);
|
1609
|
+
}
|
1610
|
+
for(let i = 0; i < entities.Process.length; i++) {
|
1611
|
+
xml.push(entities.Process[i].asXML);
|
1612
|
+
}
|
1613
|
+
for(let i = 0; i < entities.Cluster.length; i++) {
|
1614
|
+
xml.push(entities.Cluster[i].asXML);
|
1615
|
+
}
|
1616
|
+
for(let i = 0; i < entities.Link.length; i++) {
|
1617
|
+
const l = entities.Link[i];
|
1618
|
+
if(this.selection.indexOf(l.from_node) < 0) addDistinct(l.from_node, from_tos);
|
1619
|
+
if(this.selection.indexOf(l.to_node) < 0) addDistinct(l.to_node, from_tos);
|
1620
|
+
xml.push(l.asXML);
|
1621
|
+
}
|
1622
|
+
for(let i = 0; i < entities.Constraint.length; i++) {
|
1623
|
+
const c = entities.Constraint[i];
|
1624
|
+
if(this.selection.indexOf(c.from_node) < 0) addDistinct(c.from_node, from_tos);
|
1625
|
+
if(this.selection.indexOf(c.to_node) < 0) addDistinct(c.to_node, from_tos);
|
1626
|
+
xml.push(c.asXML);
|
1627
|
+
}
|
1628
|
+
for(let i = 0; i < from_tos.length; i++) {
|
1629
|
+
const p = from_tos[i];
|
1630
|
+
ft_xml.push('<from-to type="', p.type, '" name="', xmlEncoded(p.name));
|
1631
|
+
if(p instanceof Process) ft_xml.push('" actor-name="', xmlEncoded(p.actor.name));
|
1632
|
+
ft_xml.push('"></from-to>');
|
1633
|
+
}
|
1634
|
+
for(let i = 0; i < extras.length; i++) extra_xml.push(extras[i].asXML);
|
1635
|
+
return ['<copy timestamp="', Date.now(),
|
1636
|
+
'" model-timestamp="', this.time_created.getTime(),
|
1637
|
+
'" cluster-name="', xmlEncoded(fc_name),
|
1638
|
+
'" cluster-actor="', xmlEncoded(fc_actor),
|
1639
|
+
'"><entities>', xml.join(''),
|
1640
|
+
'</entities><from-tos>', ft_xml.join(''),
|
1641
|
+
'</from-tos><extras>', extra_xml.join(''),
|
1642
|
+
'</extras></copy>'].join('');
|
1643
|
+
}
|
1644
|
+
|
1565
1645
|
dropSelectionIntoCluster(c) {
|
1566
1646
|
// Move all selected nodes to cluster `c`
|
1567
1647
|
let n = 0,
|
@@ -2024,46 +2104,74 @@ class LinnyRModel {
|
|
2024
2104
|
}
|
2025
2105
|
}
|
2026
2106
|
}
|
2107
|
+
|
2108
|
+
get datasetVariables() {
|
2109
|
+
// Returns list with all ChartVariable objects in this model that
|
2110
|
+
// reference a regular dataset, i.e., not an equation
|
2111
|
+
const vl = [];
|
2112
|
+
for(let i = 0; i < MODEL.charts.length; i++) {
|
2113
|
+
const c = MODEL.charts[i];
|
2114
|
+
for(let j = 0; j < c.variables.length; j++) {
|
2115
|
+
const v = c.variables[j];
|
2116
|
+
if(v.object instanceof Dataset &&
|
2117
|
+
v.object !== MODEL.equations_dataset) vl.push(v);
|
2118
|
+
}
|
2119
|
+
}
|
2120
|
+
return vl;
|
2121
|
+
}
|
2122
|
+
|
2123
|
+
get notesWithTags() {
|
2124
|
+
// Returns a list with all notes having tags [[...]] in this model
|
2125
|
+
const nl = [];
|
2126
|
+
for(let k in this.clusters) if(this.clusters.hasOwnProperty(k)) {
|
2127
|
+
const c = this.clusters[k];
|
2128
|
+
for(let i = 0; i < c.notes.length; i++) {
|
2129
|
+
const n = c.notes[i];
|
2130
|
+
if(n.tagList) nl.push(n);
|
2131
|
+
}
|
2132
|
+
}
|
2133
|
+
return nl;
|
2134
|
+
}
|
2027
2135
|
|
2028
2136
|
get allExpressions() {
|
2029
|
-
// Returns list of all Expression objects
|
2137
|
+
// Returns list of all Expression objects in this model
|
2030
2138
|
// NOTE: start with dataset expressions, so that when recompiling
|
2031
2139
|
// their `level-based` property is set before recompiling the
|
2032
2140
|
// other expressions
|
2033
|
-
const
|
2141
|
+
const xl = [];
|
2034
2142
|
for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
|
2035
2143
|
const ds = this.datasets[k];
|
2036
2144
|
// NOTE: dataset modifier expressions include the equations
|
2037
2145
|
for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
|
2038
|
-
|
2146
|
+
xl.push(ds.modifiers[m].expression);
|
2039
2147
|
}
|
2040
2148
|
}
|
2041
2149
|
for(let k in this.actors) if(this.actors.hasOwnProperty(k)) {
|
2042
|
-
|
2150
|
+
xl.push(this.actors[k].weight);
|
2043
2151
|
}
|
2044
2152
|
for(let k in this.processes) if(this.processes.hasOwnProperty(k)) {
|
2045
2153
|
const p = this.processes[k];
|
2046
|
-
|
2154
|
+
xl.push(p.lower_bound, p.upper_bound, p.initial_level, p.pace_expression);
|
2047
2155
|
}
|
2048
2156
|
for(let k in this.products) if(this.products.hasOwnProperty(k)) {
|
2049
2157
|
const p = this.products[k];
|
2050
|
-
|
2158
|
+
xl.push(p.lower_bound, p.upper_bound, p.initial_level, p.price);
|
2051
2159
|
}
|
2052
2160
|
for(let k in this.clusters) if(this.clusters.hasOwnProperty(k)) {
|
2053
2161
|
const c = this.clusters[k];
|
2054
2162
|
for(let i = 0; i < c.notes.length; i++) {
|
2055
2163
|
const n = c.notes[i];
|
2056
|
-
|
2164
|
+
xl.push(n.color);
|
2057
2165
|
}
|
2058
2166
|
}
|
2059
2167
|
for(let k in this.links) if(this.links.hasOwnProperty(k)) {
|
2060
2168
|
const l = this.links[k];
|
2061
|
-
|
2169
|
+
xl.push(l.relative_rate, l.flow_delay);
|
2062
2170
|
}
|
2063
|
-
return
|
2171
|
+
return xl;
|
2064
2172
|
}
|
2065
2173
|
|
2066
|
-
replaceEntityInExpressions(en1, en2) {
|
2174
|
+
replaceEntityInExpressions(en1, en2, notify=true) {
|
2067
2175
|
// Replace entity name `en1` by `en2` in all variables in all expressions
|
2068
2176
|
// (provided that they are not identical)
|
2069
2177
|
if(en1 === en2) return;
|
@@ -2089,8 +2197,12 @@ class LinnyRModel {
|
|
2089
2197
|
}
|
2090
2198
|
}
|
2091
2199
|
if(ioc.replace_count) {
|
2092
|
-
|
2093
|
-
|
2200
|
+
this.variable_count += ioc.replace_count;
|
2201
|
+
this.expression_count += ioc.expression_count;
|
2202
|
+
if(notify) {
|
2203
|
+
UI.notify(`Renamed ${pluralS(ioc.replace_count, 'variable')} in ` +
|
2204
|
+
pluralS(ioc.expression_count, 'expression'));
|
2205
|
+
}
|
2094
2206
|
}
|
2095
2207
|
// Also rename entities in parameters and outcomes of sensitivity analysis
|
2096
2208
|
for(let i = 0; i < this.sensitivity_parameters.length; i++) {
|
@@ -2125,8 +2237,8 @@ class LinnyRModel {
|
|
2125
2237
|
const
|
2126
2238
|
en = escapeRegex(ena[0].trim().replace(/\s+/g, ' ').toLowerCase()),
|
2127
2239
|
at = ena[1].trim(),
|
2128
|
-
raw = en.replace(/\s/,
|
2129
|
-
re = new RegExp(`\[\s*${raw}\s*(\@[^\]]+)?\s*\]`, 'gi');
|
2240
|
+
raw = en.replace(/\s/, '\\s+') + '\\s*\\|\\s*' + escapeRegex(at),
|
2241
|
+
re = new RegExp(String.raw`\[\s*${raw}\s*(\@[^\]]+)?\s*\]`, 'gi');
|
2130
2242
|
// Count replacements made
|
2131
2243
|
let n = 0;
|
2132
2244
|
// Iterate over all expressions
|
@@ -4470,12 +4582,17 @@ class Note extends ObjectWithXYWH {
|
|
4470
4582
|
}
|
4471
4583
|
}
|
4472
4584
|
|
4585
|
+
get tagList() {
|
4586
|
+
// Returns a list of matches for [[...]], or NULL if none
|
4587
|
+
return this.contents.match(/\[\[[^\]]+\]\]/g);
|
4588
|
+
}
|
4589
|
+
|
4473
4590
|
parseFields() {
|
4474
4591
|
// Fills the list of fields by parsing all [[...]] tags in the text
|
4475
4592
|
// NOTE: this does not affect the text itself; tags will be replaced
|
4476
4593
|
// by numerical values only when drawing the note
|
4477
4594
|
this.fields.length = 0;
|
4478
|
-
const tags = this.
|
4595
|
+
const tags = this.tagList;
|
4479
4596
|
if(tags) {
|
4480
4597
|
for(let i = 0; i < tags.length; i++) {
|
4481
4598
|
const
|
@@ -4582,7 +4699,7 @@ class Note extends ObjectWithXYWH {
|
|
4582
4699
|
// Return a list with names of entities used in fields
|
4583
4700
|
const
|
4584
4701
|
fel = [],
|
4585
|
-
tags = this.
|
4702
|
+
tags = this.tagList;
|
4586
4703
|
for(let i = 0; i < tags.length; i++) {
|
4587
4704
|
const
|
4588
4705
|
tag = tags[i],
|
@@ -4606,7 +4723,7 @@ class Note extends ObjectWithXYWH {
|
|
4606
4723
|
if(en1 === en2) return;
|
4607
4724
|
const
|
4608
4725
|
raw = en1.split(/\s+/).join('\\\\s+'),
|
4609
|
-
re = new RegExp('\\[\\[\\s*' + raw + '\\s*(\\->|\\||\\])', '
|
4726
|
+
re = new RegExp('\\[\\[\\s*' + raw + '\\s*(\\->|\\||\\])', 'gi'),
|
4610
4727
|
tags = this.contents.match(re);
|
4611
4728
|
if(tags) {
|
4612
4729
|
for(let i = 0; i < tags.length; i++) {
|
@@ -4773,7 +4890,7 @@ class NodeBox extends ObjectWithXYWH {
|
|
4773
4890
|
// and products this is the string of trailing digits (or empty if none)
|
4774
4891
|
// of the node name, or if that does not end with a number, the trailing
|
4775
4892
|
// digits of the first prefix (from right to left) that does
|
4776
|
-
const sn = this.name
|
4893
|
+
const sn = UI.prefixesAndName(this.name);
|
4777
4894
|
let nc = endsWithDigits(sn.pop());
|
4778
4895
|
while(!nc && sn.length > 0) {
|
4779
4896
|
nc = endsWithDigits(sn.pop());
|
@@ -7852,7 +7969,7 @@ class Dataset {
|
|
7852
7969
|
|
7853
7970
|
get numberContext() {
|
7854
7971
|
// Returns the string to be used to evaluate # (empty string if undefined)
|
7855
|
-
const sn = this.name
|
7972
|
+
const sn = UI.prefixesAndName(this.name);
|
7856
7973
|
let nc = endsWithDigits(sn.pop());
|
7857
7974
|
while(!nc && sn.length > 0) {
|
7858
7975
|
nc = endsWithDigits(sn.pop());
|
@@ -7887,6 +8004,27 @@ class Dataset {
|
|
7887
8004
|
return true;
|
7888
8005
|
}
|
7889
8006
|
|
8007
|
+
get inferPrefixableModifiers() {
|
8008
|
+
// Returns list of dataset modifiers with expressions that do not
|
8009
|
+
// reference any variable and hence could probably better be represented
|
8010
|
+
// by a prefixed dataset having the expression value as its default
|
8011
|
+
const pml = [];
|
8012
|
+
if(this !== this.equations_dataset) {
|
8013
|
+
const sl = this.plainSelectors;
|
8014
|
+
for(let i = 0; i < sl.length; i++) {
|
8015
|
+
if(!MODEL.isDimensionSelector(sl[i])) {
|
8016
|
+
const
|
8017
|
+
m = this.modifiers[sl[i].toLowerCase()],
|
8018
|
+
x = m.expression;
|
8019
|
+
// Static expressions without variables can also be used
|
8020
|
+
// as dataset default value
|
8021
|
+
if(x.isStatic && x.text.indexOf('[') < 0) pml.push(m);
|
8022
|
+
}
|
8023
|
+
}
|
8024
|
+
}
|
8025
|
+
return pml;
|
8026
|
+
}
|
8027
|
+
|
7890
8028
|
get timeStepDuration() {
|
7891
8029
|
// Returns duration of 1 time step on the time scale of this dataset
|
7892
8030
|
return this.time_scale * VM.time_unit_values[this.time_unit];
|
@@ -8055,11 +8193,11 @@ class Dataset {
|
|
8055
8193
|
}
|
8056
8194
|
if(this.default_selector) {
|
8057
8195
|
// If no experiment (so "normal" run), use default selector if specified
|
8058
|
-
const dm = this.modifiers[this.default_selector];
|
8196
|
+
const dm = this.modifiers[UI.nameToID(this.default_selector)];
|
8059
8197
|
if(dm) return dm.expression;
|
8060
8198
|
// Exception should never occur, but check anyway and log it
|
8061
8199
|
console.log('WARNING: Dataset "' + this.name +
|
8062
|
-
`" has no default selector "${this.default_selector}"
|
8200
|
+
`" has no default selector "${this.default_selector}"`, this.modifiers);
|
8063
8201
|
}
|
8064
8202
|
// Fall-through: return vector instead of expression
|
8065
8203
|
return this.vector;
|
@@ -8186,15 +8324,17 @@ class Dataset {
|
|
8186
8324
|
}
|
8187
8325
|
}
|
8188
8326
|
const ds = xmlDecoded(nodeContentByTag(node, 'default-selector'));
|
8189
|
-
if(ds && !this.modifiers[ds]) {
|
8327
|
+
if(ds && !this.modifiers[UI.nameToID(ds)]) {
|
8190
8328
|
UI.warn(`Dataset <tt>${this.name}</tt> has no selector <tt>${ds}</tt>`);
|
8191
8329
|
} else {
|
8192
8330
|
this.default_selector = ds;
|
8193
8331
|
}
|
8194
8332
|
}
|
8195
8333
|
|
8196
|
-
rename(name) {
|
8334
|
+
rename(name, notify=true) {
|
8197
8335
|
// Change the name of this dataset
|
8336
|
+
// When `notify` is FALSE, notifications are suppressed while the
|
8337
|
+
// number of affected datasets and expressions are counted
|
8198
8338
|
// NOTE: prevent renaming the equations dataset (just in case...)
|
8199
8339
|
if(this === MODEL.equations_dataset) return;
|
8200
8340
|
name = UI.cleanName(name);
|
@@ -8214,7 +8354,7 @@ class Dataset {
|
|
8214
8354
|
this.name = name;
|
8215
8355
|
MODEL.datasets[new_id] = this;
|
8216
8356
|
if(old_id !== new_id) delete MODEL.datasets[old_id];
|
8217
|
-
MODEL.replaceEntityInExpressions(old_name, name);
|
8357
|
+
MODEL.replaceEntityInExpressions(old_name, name, notify);
|
8218
8358
|
return MODEL.datasets[new_id];
|
8219
8359
|
}
|
8220
8360
|
|
@@ -9687,7 +9827,7 @@ class ExperimentRunResult {
|
|
9687
9827
|
return ['<run-result', (this.x_variable ? ' x-variable="1"' : ''),
|
9688
9828
|
(this.was_ignored ? ' ignored="1"' : ''),
|
9689
9829
|
'><object-id>', xmlEncoded(this.object_id),
|
9690
|
-
'</object-id><attribute>', this.attribute,
|
9830
|
+
'</object-id><attribute>', xmlEncoded(this.attribute),
|
9691
9831
|
'</attribute><count>', this.N,
|
9692
9832
|
'</count><sum>', this.sum,
|
9693
9833
|
'</sum><mean>', this.mean,
|
@@ -9705,7 +9845,12 @@ class ExperimentRunResult {
|
|
9705
9845
|
this.x_variable = nodeParameterValue(node, 'x-variable') === '1';
|
9706
9846
|
this.was_ignored = nodeParameterValue(node, 'ignored') === '1';
|
9707
9847
|
this.object_id = xmlDecoded(nodeContentByTag(node, 'object-id'));
|
9708
|
-
|
9848
|
+
// NOTE: special check to guarantee upward compatibility to version
|
9849
|
+
// 1.3.0 and higher
|
9850
|
+
let attr = nodeContentByTag(node, 'attribute');
|
9851
|
+
if(this.object_id === UI.EQUATIONS_DATASET_ID &&
|
9852
|
+
!earlierVersion(MODEL.version, '1.3.0')) attr = xmlDecoded(attr);
|
9853
|
+
this.attribute = attr;
|
9709
9854
|
this.N = safeStrToInt(nodeContentByTag(node, 'count'));
|
9710
9855
|
this.sum = safeStrToFloat(nodeContentByTag(node, 'sum'));
|
9711
9856
|
this.mean = safeStrToFloat(nodeContentByTag(node, 'mean'));
|
@@ -10136,6 +10281,17 @@ class Experiment {
|
|
10136
10281
|
return index;
|
10137
10282
|
}
|
10138
10283
|
|
10284
|
+
isDimensionSelector(s) {
|
10285
|
+
// Returns TRUE if `s` is a dimension selector in this experiment
|
10286
|
+
for(let i = 0; i < this.dimensions.length; i++) {
|
10287
|
+
if(this.dimensions[i].indexOf(s) >= 0) return true;
|
10288
|
+
}
|
10289
|
+
if(this.settings_selectors.indexOf(s) >= 0) return true;
|
10290
|
+
if(this.combination_selectors.indexOf(s) >= 0) return true;
|
10291
|
+
if(this.actor_selectors.indexOf(s) >= 0) return true;
|
10292
|
+
return false;
|
10293
|
+
}
|
10294
|
+
|
10139
10295
|
get asXML() {
|
10140
10296
|
let d = '';
|
10141
10297
|
for(let i = 0; i < this.dimensions.length; i++) {
|
@@ -65,8 +65,11 @@ function pluralS(n, s, special='') {
|
|
65
65
|
function safeStrToFloat(str, val=0) {
|
66
66
|
// Returns numeric value of floating point string, interpreting both
|
67
67
|
// dot and comma as decimal point
|
68
|
-
// NOTE: returns default value val if str is empty, null or undefined
|
69
|
-
|
68
|
+
// NOTE: returns default value `val` if `str` is empty, null or undefined,
|
69
|
+
// or contains a character that is invalid in a number
|
70
|
+
if(!str || str.match(/[^0-9eE\.\,\+\-]/)) return val;
|
71
|
+
str = str.replace(',', '.');
|
72
|
+
const f = (str ? parseFloat(str) : val);
|
70
73
|
return (isNaN(f) ? val : f);
|
71
74
|
}
|
72
75
|
|
@@ -186,6 +189,20 @@ function ellipsedText(text, n=50, m=10) {
|
|
186
189
|
// Functions used when comparing two Linny-R models
|
187
190
|
//
|
188
191
|
|
192
|
+
function earlierVersion(v1, v2) {
|
193
|
+
// Compares two version numbers and returns TRUE iff `v1` is earlier
|
194
|
+
// than `v2`
|
195
|
+
v1 = v1.split('.');
|
196
|
+
v2 = v2.split('.');
|
197
|
+
for(let i = 0; i < Math.min(v1.length, v2.length); i++) {
|
198
|
+
// NOTE: for legacy JS models, the major version number evaluates as 0
|
199
|
+
if(safeStrToInt(v1[i]) < safeStrToInt(v2[i])) return true;
|
200
|
+
if(safeStrToInt(v1[i]) > safeStrToInt(v2[i])) return false;
|
201
|
+
}
|
202
|
+
// Fall-through: same version numbers => NOT earlier
|
203
|
+
return false;
|
204
|
+
}
|
205
|
+
|
189
206
|
function differences(a, b, props) {
|
190
207
|
// Compares values of properties (in list `props`) of entities `a` and `b`,
|
191
208
|
// and returns a "dictionary" object with differences
|
@@ -310,18 +327,34 @@ function patternList(str) {
|
|
310
327
|
|
311
328
|
function patternMatch(str, patterns) {
|
312
329
|
// Returns TRUE when `str` matches the &|^-pattern
|
330
|
+
// NOTE: if a pattern starts with equals sign = then `str` must
|
331
|
+
// equal the rest of the pattern to match; if it starts with a tilde
|
332
|
+
// ~ then `str` must start with the rest of the pattern to match
|
313
333
|
for(let i = 0; i < patterns.length; i++) {
|
314
334
|
const p = patterns[i];
|
315
|
-
let
|
335
|
+
let pm,
|
336
|
+
match = true;
|
316
337
|
for(let j = 0; j < p.plus.length; j++) {
|
317
|
-
|
338
|
+
pm = p.plus[j];
|
339
|
+
if(pm.startsWith('=')) {
|
340
|
+
match = match && str === pm.substring(1);
|
341
|
+
} else if(pm.startsWith('~')) {
|
342
|
+
match = match && str.startsWith(pm.substring(1));
|
343
|
+
} else {
|
344
|
+
match = match && str.indexOf(pm) >= 0;
|
345
|
+
}
|
318
346
|
}
|
319
347
|
for(let j = 0; j < p.min.length; j++) {
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
348
|
+
pm = p.min[j];
|
349
|
+
if(pm.startsWith('=')) {
|
350
|
+
match = match && str !== pm.substring(1);
|
351
|
+
} else if(pm.startsWith('~')) {
|
352
|
+
match = match && !str.startsWith(pm.substring(1));
|
353
|
+
} else {
|
354
|
+
match = match && str.indexOf(pm) < 0;
|
355
|
+
}
|
324
356
|
}
|
357
|
+
if(match) return true;
|
325
358
|
}
|
326
359
|
return false;
|
327
360
|
}
|
@@ -508,9 +541,9 @@ function childNodeByTag(node, tag) {
|
|
508
541
|
// Returns the XML child node of `node` having node name `tag`, or NULL if
|
509
542
|
// no such child node exists
|
510
543
|
let cn = null;
|
511
|
-
for (let i = 0; i < node.
|
512
|
-
if(node.
|
513
|
-
cn = node.
|
544
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
545
|
+
if(node.childNodes[i].tagName === tag) {
|
546
|
+
cn = node.childNodes[i];
|
514
547
|
break;
|
515
548
|
}
|
516
549
|
}
|
@@ -745,16 +778,21 @@ if(NODE) module.exports = {
|
|
745
778
|
pluralS: pluralS,
|
746
779
|
safeStrToFloat: safeStrToFloat,
|
747
780
|
safeStrToInt: safeStrToInt,
|
781
|
+
rangeToList: rangeToList,
|
748
782
|
dateToString: dateToString,
|
749
783
|
msecToTime: msecToTime,
|
750
784
|
uniformDecimals: uniformDecimals,
|
751
785
|
ellipsedText: ellipsedText,
|
786
|
+
earlierVersion: earlierVersion,
|
752
787
|
differences: differences,
|
753
788
|
markFirstDifference: markFirstDifference,
|
789
|
+
ciCompare: ciCompare,
|
754
790
|
endsWithDigits: endsWithDigits,
|
755
791
|
indexOfMatchingBracket: indexOfMatchingBracket,
|
756
792
|
patternList: patternList,
|
757
793
|
patternMatch: patternMatch,
|
794
|
+
compareSelectors: compareSelectors,
|
795
|
+
stringToFloatArray: stringToFloatArray,
|
758
796
|
escapeRegex: escapeRegex,
|
759
797
|
addDistinct: addDistinct,
|
760
798
|
setString: setString,
|
@@ -12,7 +12,7 @@ executed by the VM, construct the Simplex tableau that can be sent to the
|
|
12
12
|
MILP solver.
|
13
13
|
*/
|
14
14
|
/*
|
15
|
-
Copyright (c) 2017-
|
15
|
+
Copyright (c) 2017-2023 Delft University of Technology
|
16
16
|
|
17
17
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
18
18
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -834,7 +834,7 @@ class ExpressionParser {
|
|
834
834
|
return [stat, list, anchor1, offset1, anchor2, offset2];
|
835
835
|
}
|
836
836
|
this.error = `No entities that match pattern "${patstr}"` +
|
837
|
-
(attr ? ' and have attribute ' + attr : '');
|
837
|
+
(attr ? ' and have attribute ' + attr : ' when no attribute is specified');
|
838
838
|
return false;
|
839
839
|
}
|
840
840
|
|
@@ -1428,6 +1428,8 @@ class VirtualMachine {
|
|
1428
1428
|
this.lines = '';
|
1429
1429
|
// String specifying a numeric issue (empty if none)
|
1430
1430
|
this.numeric_issue = '';
|
1431
|
+
// Warnings are stored in a list to permit browsing through them
|
1432
|
+
this.issue_list = [];
|
1431
1433
|
// The call stack tracks evaluation of "nested" expression variables
|
1432
1434
|
this.call_stack = [];
|
1433
1435
|
this.block_count = 0;
|
@@ -1553,6 +1555,9 @@ class VirtualMachine {
|
|
1553
1555
|
this.ERROR, this.CYCLIC, this.DIV_ZERO, this.BAD_CALC, this.ARRAY_INDEX,
|
1554
1556
|
this.BAD_REF, this.UNDERFLOW, this.OVERFLOW, this.INVALID, this.PARAMS,
|
1555
1557
|
this.UNKNOWN_ERROR, this.UNDEFINED, this.NOT_COMPUTED, this.COMPUTING];
|
1558
|
+
|
1559
|
+
// Prefix for warning messages that are logged in the monitor
|
1560
|
+
this.WARNING = '-- Warning: ';
|
1556
1561
|
|
1557
1562
|
// Solver constants indicating constraint type
|
1558
1563
|
// NOTE: these correspond to the codes used by LP_solve; when generating
|
@@ -1695,6 +1700,10 @@ class VirtualMachine {
|
|
1695
1700
|
// Initialize error counters (error count will be reset to 0 for each block)
|
1696
1701
|
this.error_count = 0;
|
1697
1702
|
this.block_issues = 0;
|
1703
|
+
// Clear issue list with warnings and hide issue panel
|
1704
|
+
this.issue_list.length = 0;
|
1705
|
+
this.issue_index = -1;
|
1706
|
+
UI.updateIssuePanel();
|
1698
1707
|
// NOTE: special tracking of potential solver license errors
|
1699
1708
|
this.license_expired = 0;
|
1700
1709
|
// Reset solver result arrays
|
@@ -2085,6 +2094,10 @@ class VirtualMachine {
|
|
2085
2094
|
this.messages[block - 1] = '';
|
2086
2095
|
}
|
2087
2096
|
this.messages[block - 1] += msg + '\n';
|
2097
|
+
if(msg.startsWith(this.WARNING)) {
|
2098
|
+
this.error_count++;
|
2099
|
+
this.issue_list.push(msg);
|
2100
|
+
}
|
2088
2101
|
// Show message on console or in Monitor dialog
|
2089
2102
|
MONITOR.logMessage(block, msg);
|
2090
2103
|
}
|
@@ -2125,7 +2138,7 @@ class VirtualMachine {
|
|
2125
2138
|
MONITOR.updateContent('msg');
|
2126
2139
|
}
|
2127
2140
|
}
|
2128
|
-
|
2141
|
+
|
2129
2142
|
startTimer() {
|
2130
2143
|
// Record time of this reset
|
2131
2144
|
this.reset_time = new Date().getTime();
|
@@ -3627,14 +3640,12 @@ class VirtualMachine {
|
|
3627
3640
|
a.cash_flow[b] = a.cash_in[b] - a.cash_out[b];
|
3628
3641
|
// Count occurrences of a negative cash flow (threshold -0.5 cent)
|
3629
3642
|
if(a.cash_in[b] < -0.005) {
|
3630
|
-
this.logMessage(block,
|
3643
|
+
this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
|
3631
3644
|
a.displayName + ' cash IN = ' + a.cash_in[b].toPrecision(2));
|
3632
|
-
this.error_count++;
|
3633
3645
|
}
|
3634
3646
|
if(a.cash_out[b] < -0.005) {
|
3635
|
-
this.logMessage(block,
|
3647
|
+
this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
|
3636
3648
|
a.displayName + ' cash IN = ' + a.cash_out[b].toPrecision(2));
|
3637
|
-
this.error_count++;
|
3638
3649
|
}
|
3639
3650
|
// Advance column offset in tableau by the # cols per time step
|
3640
3651
|
j += this.cols;
|
@@ -3754,7 +3765,7 @@ class VirtualMachine {
|
|
3754
3765
|
v[1].constraint.slack_info[b] = v[0];
|
3755
3766
|
}
|
3756
3767
|
if(absl > VM.SIG_DIF_FROM_ZERO) {
|
3757
|
-
this.logMessage(block,
|
3768
|
+
this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
|
3758
3769
|
`${v[1].displayName} ${v[0]} slack = ${this.sig4Dig(slack)}`);
|
3759
3770
|
if(v[1] instanceof Product) {
|
3760
3771
|
const ppc = v[1].productPositionClusters;
|
@@ -3762,7 +3773,6 @@ class VirtualMachine {
|
|
3762
3773
|
ppc[ci].usesSlack(b, v[1], v[0]);
|
3763
3774
|
}
|
3764
3775
|
}
|
3765
|
-
this.error_count++;
|
3766
3776
|
} else if(CONFIGURATION.slight_slack_notices) {
|
3767
3777
|
this.logMessage(block, '-- Notice: (t=' + b + round + ') ' +
|
3768
3778
|
v[1].displayName + ' ' + v[0] + ' slack = ' +
|
@@ -3932,9 +3942,8 @@ class VirtualMachine {
|
|
3932
3942
|
b = bb;
|
3933
3943
|
for(let i = 0; i < this.chunk_length; i++) {
|
3934
3944
|
if(!MODEL.calculateCostPrices(b)) {
|
3935
|
-
this.logMessage(block,
|
3936
|
-
'
|
3937
|
-
this.error_count++;
|
3945
|
+
this.logMessage(block, `${this.WARNING}(t=${b}) ` +
|
3946
|
+
'Invalid cost prices due to negative flow(s)');
|
3938
3947
|
}
|
3939
3948
|
// move to the next time step of the block
|
3940
3949
|
b++;
|
@@ -4729,9 +4738,11 @@ Solver status = ${json.status}`);
|
|
4729
4738
|
// If experiment is active, signal the manager
|
4730
4739
|
if(MODEL.running_experiment) EXPERIMENT_MANAGER.processRun();
|
4731
4740
|
// Warn modeler if any issues occurred
|
4732
|
-
if(this.block_issues)
|
4733
|
-
|
4734
|
-
' --
|
4741
|
+
if(this.block_issues) {
|
4742
|
+
UI.warn('Issues occurred in ' + pluralS(this.block_issues, 'block') +
|
4743
|
+
' -- details can be viewed in the monitor and by using \u25C1 \u25B7');
|
4744
|
+
UI.updateIssuePanel();
|
4745
|
+
}
|
4735
4746
|
if(this.license_expired > 0) {
|
4736
4747
|
// Special message to draw attention to this critical error
|
4737
4748
|
UI.alert('SOLVER LICENSE EXPIRED: Please check!');
|
@@ -4845,11 +4856,12 @@ Solver status = ${json.status}`);
|
|
4845
4856
|
return;
|
4846
4857
|
} else {
|
4847
4858
|
// Wait no longer, but warn user that data may be incomplete
|
4848
|
-
dsl = [];
|
4859
|
+
const dsl = [];
|
4849
4860
|
for(let i = 0; i < MODEL.loading_datasets.length; i++) {
|
4850
4861
|
dsl.push(MODEL.loading_datasets[i].displayName);
|
4851
4862
|
}
|
4852
|
-
UI.warn(
|
4863
|
+
UI.warn('Loading of ' + pluralS(dsl.length, 'dataset') + ' (' +
|
4864
|
+
dsl.join(', ') + ') takes too long');
|
4853
4865
|
}
|
4854
4866
|
}
|
4855
4867
|
if(MONITOR.connectToServer()) {
|
@@ -5189,15 +5201,15 @@ function relativeTimeStep(t, anchor, offset, dtm, x) {
|
|
5189
5201
|
}
|
5190
5202
|
if(anchor === 'c') {
|
5191
5203
|
// Relative to start of current optimization block
|
5192
|
-
return
|
5204
|
+
return Math.trunc(t / MODEL.block_length) * MODEL.block_length + offset;
|
5193
5205
|
}
|
5194
5206
|
if(anchor === 'p') {
|
5195
5207
|
// Relative to start of previous optimization block
|
5196
|
-
return
|
5208
|
+
return (Math.trunc(t / MODEL.block_length) - 1) * MODEL.block_length + offset;
|
5197
5209
|
}
|
5198
5210
|
if(anchor === 'n') {
|
5199
5211
|
// Relative to start of next optimization block
|
5200
|
-
return
|
5212
|
+
return (Math.trunc(t / MODEL.block_length) + 1) * MODEL.block_length + offset;
|
5201
5213
|
}
|
5202
5214
|
if(anchor === 'l') {
|
5203
5215
|
// Last: offset relative to the last index in the vector
|