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.
@@ -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.split(UI.PREFIXER);
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.contents.match(/\[\[[^\]]+\]\]/g);
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 x = [];
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
- x.push(ds.modifiers[m].expression);
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
- x.push(this.actors[k].weight);
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
- x.push(p.lower_bound, p.upper_bound, p.initial_level, p.pace_expression);
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
- x.push(p.lower_bound, p.upper_bound, p.initial_level, p.price);
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
- x.push(n.color);
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
- x.push(l.relative_rate, l.flow_delay);
2169
+ xl.push(l.relative_rate, l.flow_delay);
2062
2170
  }
2063
- return x;
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
- UI.notify(`Renamed ${pluralS(ioc.replace_count, 'variable')} in ` +
2093
- pluralS(ioc.expression_count, 'expression'));
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/, `\s+`) + `\s*\|\s*` + escapeRegex(at),
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.contents.match(/\[\[[^\]]+\]\]/g);
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.contents.match(/\[\[[^\]]+\]\]/g);
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*(\\->|\\||\\])', 'g'),
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.split(UI.PREFIXER);
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.split(UI.PREFIXER);
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
- this.attribute = nodeContentByTag(node, 'attribute');
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
- const f = (str ? parseFloat(str.replace(',', '.')) : val);
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 match = true;
335
+ let pm,
336
+ match = true;
316
337
  for(let j = 0; j < p.plus.length; j++) {
317
- match = match && str.indexOf(p.plus[j]) >= 0;
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
- match = match && str.indexOf(p.min[j]) < 0;
321
- }
322
- if(match) {
323
- return true;
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.children.length; i++) {
512
- if(node.children[i].tagName === tag) {
513
- cn = node.children[i];
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-2022 Delft University of Technology
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, `-- Warning: (t=${b}${round}) ` +
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, `-- Warning: (t=${b}${round}) ` +
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, `-- Warning: (t=${b}${round}) ` +
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, '-- Warning: (t=' + b +
3936
- ') Invalid cost prices due to negative flow(s)');
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) UI.warn('Issues occurred in ' +
4733
- pluralS(this.block_issues, 'block') +
4734
- ' -- check messages in monitor');
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(`Loading datasets (${dsl.join(', ')}) takes too long`);
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 VM.block_start + offset;
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 VM.block_start - MODEL.block_length + offset;
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 VM.block_start + MODEL.block_length + offset;
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