linny-r 2.1.4 → 2.1.6

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.
@@ -542,7 +542,7 @@ class LinnyRModel {
542
542
  e = this.objectByName(en);
543
543
  if(!e) return `Unknown model entity "${en}"`;
544
544
  const
545
- ao = ea[1].split('@'),
545
+ ao = (ea.length > 1 ? ea[1].split('@') : ['']),
546
546
  a = ao[0].trim();
547
547
  // Valid if no attribute, as all entity types have a default attribute.
548
548
  if(!a) return true;
@@ -1037,11 +1037,11 @@ class LinnyRModel {
1037
1037
  // NOTE: A dimension is a list of one or more relevant selectors.
1038
1038
  this.dimensions.length = 0;
1039
1039
  // NOTE: Ignore the equations dataset.
1040
- for(let d in this.datasets) if(this.datasets.hasOwnProperty(d) &&
1041
- this.datasets[d] !== this.equations_dataset) {
1040
+ for(let k in this.datasets) if(this.datasets.hasOwnProperty(k) &&
1041
+ this.datasets[k] !== this.equations_dataset) {
1042
1042
  // Get the selector list for this dataset.
1043
1043
  // NOTE: Ignore wildcard selectors!
1044
- this.processSelectorList(this.datasets[d].plainSelectors);
1044
+ this.processSelectorList(this.datasets[k].plainSelectors);
1045
1045
  }
1046
1046
  // Analyze constraint bound lines in the same way.
1047
1047
  for(let k in this.constraints) if(this.constraints.hasOwnProperty(k)) {
@@ -1636,8 +1636,7 @@ class LinnyRModel {
1636
1636
  const ci = this.indexOfChart(title);
1637
1637
  if(ci >= 0) return this.charts[ci];
1638
1638
  // Otherwise, add it. NOTE: Unlike datasets, charts are not "entities".
1639
- let c = new Chart();
1640
- c.title = title;
1639
+ let c = new Chart(title);
1641
1640
  if(node) c.initFromXML(node);
1642
1641
  this.charts.push(c);
1643
1642
  // Sort the chart titles alphabetically...
@@ -3075,10 +3074,12 @@ class LinnyRModel {
3075
3074
  if(n) {
3076
3075
  // NOTE: Use a "dummy experiment object" as parent for SA runs.
3077
3076
  const dummy = {title: SENSITIVITY_ANALYSIS.experiment_title};
3077
+ let r = 0;
3078
3078
  for(const c of n.childNodes) if(c.nodeName === 'experiment-run') {
3079
- const xr = new ExperimentRun(dummy, i);
3079
+ const xr = new ExperimentRun(dummy, r);
3080
3080
  xr.initFromXML(c);
3081
3081
  this.sensitivity_runs.push(xr);
3082
+ r++;
3082
3083
  }
3083
3084
  }
3084
3085
  n = childNodeByTag(node, 'experiments');
@@ -3321,7 +3322,7 @@ class LinnyRModel {
3321
3322
  vbls.sort((a, b) => UI.compareFullNames(a.displayName, b.displayName));
3322
3323
  // Create a new chart as dummy, so without adding it to this model.
3323
3324
  const
3324
- c = new Chart(),
3325
+ c = new Chart('__d_u_m_m_y__c_h_a_r_t__'),
3325
3326
  wcdm = [];
3326
3327
  for(const v of vbls) {
3327
3328
  // NOTE: Prevent adding wildcard dataset modifiers more than once.
@@ -3337,9 +3338,9 @@ class LinnyRModel {
3337
3338
  return [c.dataAsString, c.statisticsAsString];
3338
3339
  }
3339
3340
 
3340
- get listOfAllSelectors() {
3341
- // Returns list of all dataset modifier selectors as a "dictionary"
3342
- // like so: {selector_1: [list of datasets], ...}
3341
+ get dictOfAllSelectors() {
3342
+ // Returns "dictionary" of all dataset modifier selectors like so:
3343
+ // {selector_1: [list of datasets], ...}
3343
3344
  const ds_dict = {};
3344
3345
  for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
3345
3346
  const ds = this.datasets[k];
@@ -6240,19 +6241,19 @@ class Cluster extends NodeBox {
6240
6241
  }
6241
6242
 
6242
6243
  get nestingLevel() {
6243
- // Return the "depth" of this cluster in the cluster hierarchy
6244
+ // Return the "depth" of this cluster in the cluster hierarchy.
6244
6245
  if(this.cluster) return this.cluster.nestingLevel + 1; // recursion!
6245
6246
  return 0;
6246
6247
  }
6247
6248
 
6248
6249
  get toBeIgnored() {
6249
- // Return TRUE if this cluster or some parent cluster is set to be ignored
6250
+ // Return TRUE if this cluster or some parent cluster is set to be ignored.
6250
6251
  return this.ignore || MODEL.ignoreClusterInThisRun(this) ||
6251
6252
  (this.cluster && this.cluster.toBeIgnored); // recursion!
6252
6253
  }
6253
6254
 
6254
6255
  get blackBoxed() {
6255
- // Return TRUE if this cluster or some parent cluster is marked as black box
6256
+ // Return TRUE if this cluster or some parent cluster is marked as black box.
6256
6257
  return this.black_box ||
6257
6258
  (this.cluster && this.cluster.blackBoxed); // recursion!
6258
6259
  }
@@ -7928,6 +7929,7 @@ class Process extends Node {
7928
7929
  const a = {name: this.displayName};
7929
7930
  a.LB = this.lower_bound.asAttribute;
7930
7931
  a.UB = (this.equal_bounds ? a.LB : this.upper_bound.asAttribute);
7932
+ if(this.grid) a.LB = -a.UB;
7931
7933
  a.IL = this.initial_level.asAttribute;
7932
7934
  a.LCF = this.pace_expression.asAttribute;
7933
7935
  if(MODEL.solved) {
@@ -8861,13 +8863,13 @@ class Link {
8861
8863
  }
8862
8864
 
8863
8865
  get identifier() {
8864
- // NOTE: link IDs are based on the node codes rather than IDs, as this
8865
- // prevents problems when nodes are renamed
8866
+ // NOTE: Link IDs are based on the node codes rather than IDs, as this
8867
+ // prevents problems when nodes are renamed.
8866
8868
  return this.from_node.code + '___' + this.to_node.code;
8867
8869
  }
8868
8870
 
8869
8871
  get attributes() {
8870
- // NOTE: link is named by its tab-separated node names
8872
+ // NOTE: Link is named by its tab-separated node names.
8871
8873
  const a = {name: this.from_node.displayName + '\t' + this.to_node.displayName};
8872
8874
  a.R = this.relative_rate.asAttribute;
8873
8875
  if(MODEL.infer_cost_prices) a.SOC = this.share_of_cost;
@@ -9220,7 +9222,7 @@ class Dataset {
9220
9222
  }
9221
9223
 
9222
9224
  get attributes() {
9223
- // NOTE: modifiers are appended as additional lines of text
9225
+ // NOTE: Modifiers are appended as additional lines of text.
9224
9226
  const a = {name: this.displayName};
9225
9227
  a.D = '\t' + (this.vector ? this.vector[MODEL.t] : this.default_value);
9226
9228
  for(let k in this.modifiers) if(this.modifiers.hasOwnProperty(k)) {
@@ -9755,13 +9757,14 @@ class ChartVariable {
9755
9757
  this.wildcard_index = false;
9756
9758
  }
9757
9759
 
9758
- setProperties(obj, attr, stck, clr, sf=1, lw=1, vis=true, sort='not') {
9760
+ setProperties(obj, attr, stck, clr, sf=1, abs=false, lw=1, vis=true, sort='not') {
9759
9761
  // Sets the defining properties for this chart variable.
9760
9762
  this.object = obj;
9761
9763
  this.attribute = attr;
9762
9764
  this.stacked = stck;
9763
9765
  this.color = clr;
9764
9766
  this.scale_factor = sf;
9767
+ this.absolute = abs;
9765
9768
  this.line_width = lw;
9766
9769
  this.visible = vis;
9767
9770
  this.sorted = sort;
@@ -9777,9 +9780,11 @@ class ChartVariable {
9777
9780
  // Returns the display name for this variable. This is the name of
9778
9781
  // the Linny-R entity and its attribute, followed by its scale factor
9779
9782
  // unless it equals 1 (no scaling).
9780
- const sf = (this.scale_factor === 1 ? '' :
9781
- // NOTE: Pass tiny = TRUE to permit very small scaling factors.
9782
- ` (x${VM.sig4Dig(this.scale_factor, true)})`);
9783
+ const
9784
+ bar = (this.absolute ? '\u2503' : ''),
9785
+ sf = (this.scale_factor === 1 ? '' :
9786
+ // NOTE: Pass tiny = TRUE to permit very small scaling factors.
9787
+ ` (x${VM.sig4Dig(this.scale_factor, true)})`);
9783
9788
  // Display name of equation is just the equations dataset selector.
9784
9789
  if(this.object instanceof DatasetModifier) {
9785
9790
  let eqn = this.object.selector;
@@ -9795,7 +9800,7 @@ class ChartVariable {
9795
9800
  // method name (leading colon replaced by the prefixer ": ").
9796
9801
  eqn = this.chart.prefix + UI.PREFIXER + eqn.substring(1);
9797
9802
  }
9798
- return eqn + sf;
9803
+ return bar + eqn + bar + sf;
9799
9804
  }
9800
9805
  // NOTE: Same holds for "dummy variables" added for wildcard
9801
9806
  // dataset selectors.
@@ -9804,11 +9809,13 @@ class ChartVariable {
9804
9809
  if(this.wildcard_index !== false) {
9805
9810
  eqn = eqn.replace('??', this.wildcard_index);
9806
9811
  }
9807
- return eqn + sf;
9812
+ return bar + eqn + bar + sf;
9808
9813
  }
9809
9814
  // NOTE: Do not display the vertical bar if no attribute is specified.
9810
- if(!this.attribute) return this.object.displayName + sf;
9811
- return this.object.displayName + UI.OA_SEPARATOR + this.attribute + sf;
9815
+ if(!this.attribute) {
9816
+ return bar + this.object.displayName + bar + sf;
9817
+ }
9818
+ return bar + this.object.displayName + '|' + this.attribute + bar + sf;
9812
9819
  }
9813
9820
 
9814
9821
  get asXML() {
@@ -9818,7 +9825,9 @@ class ChartVariable {
9818
9825
  if(MODEL.black_box_entities.hasOwnProperty(id)) {
9819
9826
  id = UI.nameToID(MODEL.black_box_entities[id]);
9820
9827
  }
9821
- const xml = ['<chart-variable', (this.stacked ? ' stacked="1"' : ''),
9828
+ const xml = ['<chart-variable',
9829
+ (this.stacked ? ' stacked="1"' : ''),
9830
+ (this.absolute ? ' absolute="1"' : ''),
9822
9831
  (this.visible ? ' visible="1"' : ''),
9823
9832
  (this.wildcard_index !== false ?
9824
9833
  ` wildcard-index="${this.wildcard_index}"` : ''),
@@ -9876,6 +9885,7 @@ class ChartVariable {
9876
9885
  nodeParameterValue(node, 'stacked') === '1',
9877
9886
  nodeContentByTag(node, 'color'),
9878
9887
  safeStrToFloat(nodeContentByTag(node, 'scale-factor')),
9888
+ nodeParameterValue(node, 'absolute') === '1',
9879
9889
  safeStrToFloat(nodeContentByTag(node, 'line-width')),
9880
9890
  nodeParameterValue(node, 'visible') === '1',
9881
9891
  nodeParameterValue(node, 'sorted') || 'not');
@@ -9970,7 +9980,10 @@ class ChartVariable {
9970
9980
  v = 0;
9971
9981
  }
9972
9982
  // Scale the value unless run result (these are already scaled!).
9973
- if(!rr) v *= this.scale_factor;
9983
+ if(!rr) {
9984
+ if(this.absolute) v = Math.abs(v);
9985
+ v *= this.scale_factor;
9986
+ }
9974
9987
  this.vector.push(v);
9975
9988
  // Do not include values for t = 0 in statistics.
9976
9989
  if(t > 0) {
@@ -10184,7 +10197,7 @@ class Chart {
10184
10197
  return '#c00000';
10185
10198
  }
10186
10199
 
10187
- addVariable(n, a) {
10200
+ addVariable(n, a='') {
10188
10201
  // Add variable [entity name `n`|attribute `a`] to the chart unless
10189
10202
  // it is already in the variable list.
10190
10203
  let dn = n + UI.OA_SEPARATOR + a;
@@ -10220,7 +10233,7 @@ class Chart {
10220
10233
  }
10221
10234
  } else {
10222
10235
  const v = new ChartVariable(this);
10223
- v.setProperties(obj, a, false, this.nextAvailableDefaultColor, 1, 1);
10236
+ v.setProperties(obj, a, false, this.nextAvailableDefaultColor);
10224
10237
  this.variables.push(v);
10225
10238
  }
10226
10239
  return this.variables.length - 1;
@@ -11197,9 +11210,9 @@ class ExperimentRunResult {
11197
11210
  this.x_variable = true;
11198
11211
  this.object_id = v.object.identifier;
11199
11212
  this.attribute = v.attribute;
11200
- this.was_ignored = MODEL.ignored_entities[this.object_id];
11213
+ this.was_ignored = MODEL.ignored_entities[this.object_id] || false;
11201
11214
  if(this.was_ignored) {
11202
- // Chart variable entity was ignored => all results are undefined
11215
+ // Chart variable entity was ignored => all results are undefined.
11203
11216
  this.vector = [];
11204
11217
  this.N = VM.UNDEFINED;
11205
11218
  this.sum = VM.UNDEFINED;
@@ -11236,6 +11249,11 @@ class ExperimentRunResult {
11236
11249
  // statistic.
11237
11250
  this.last = (this.vector.length > 0 ?
11238
11251
  this.vector[this.vector.length - 1] : VM.UNDEFINED);
11252
+ // NOTE: For sensitivity analyses, the vector is NOT stored because
11253
+ // the SA reports only the descriptive statistics.
11254
+ if(this.run.experiment === SENSITIVITY_ANALYSIS.experiment) {
11255
+ this.vector.length = 0;
11256
+ }
11239
11257
  }
11240
11258
  } else if(v instanceof Dataset) {
11241
11259
  // This dataset will be an "outcome" dataset => store statistics only
@@ -11329,12 +11347,12 @@ class ExperimentRunResult {
11329
11347
  }
11330
11348
  // The vector MAY need to be scaled to model time by different methods,
11331
11349
  // but since this is likely to be rare, such scaling is performed
11332
- // "lazily", so the method-specific vectors are initially set to NULL.
11350
+ // "lazily", so the method-specific vectors are initially empty.
11333
11351
  this.resetScaledVectors();
11334
11352
  }
11335
11353
 
11336
11354
  resetScaledVectors() {
11337
- // Set the special vectors to null, so they will be recalculated.
11355
+ // Clear the special vectors, so they will be recalculated.
11338
11356
  this.scaled_vectors = {'NEAREST': [], 'MEAN': [], 'SUM': [], 'MAX': []};
11339
11357
  }
11340
11358
 
@@ -11612,8 +11630,12 @@ class ExperimentRun {
11612
11630
  // NOTE: All equations are also considered to be outcomes EXCEPT
11613
11631
  // methods (selectors starting with a colon).
11614
11632
  this.eq_list = [];
11615
- const eml = Object.keys(MODEL.equations_dataset.modifiers);
11616
- for(const em of eml) if(!em.startsWith(':')) this.eq_list.push(em);
11633
+ // NOTE: For sensitivity analyses, equations are NOT outcomes, as all
11634
+ // SA outcomes must be specified explicitly.
11635
+ if(this.experiment !== SENSITIVITY_ANALYSIS.experiment) {
11636
+ const eml = Object.keys(MODEL.equations_dataset.modifiers);
11637
+ for(const em of eml) if(!em.startsWith(':')) this.eq_list.push(em);
11638
+ }
11617
11639
  const
11618
11640
  cv = this.experiment.variables.length,
11619
11641
  oc = this.oc_list.length,
@@ -11918,7 +11940,7 @@ class Experiment {
11918
11940
  }
11919
11941
  return index;
11920
11942
  }
11921
-
11943
+
11922
11944
  isDimensionSelector(s) {
11923
11945
  // Return TRUE if `s` is a dimension selector in this experiment.
11924
11946
  for(const dim of this.dimensions) if(dim.indexOf(s) >= 0) return true;
@@ -11928,6 +11950,15 @@ class Experiment {
11928
11950
  return false;
11929
11951
  }
11930
11952
 
11953
+ get allDimensionSelectors() {
11954
+ // Return list of all dimension selectors in this experiment.
11955
+ const
11956
+ dict = MODEL.dictOfAllSelectors,
11957
+ dims = [];
11958
+ for(let s in dict) if(this.isDimensionSelector(s)) dims.push(s);
11959
+ return dims;
11960
+ }
11961
+
11931
11962
  get asXML() {
11932
11963
  let d = '';
11933
11964
  for(const dim of this.dimensions) {
@@ -12152,12 +12183,6 @@ class Experiment {
12152
12183
  }
12153
12184
  }
12154
12185
 
12155
- get allDimensionSelectors() {
12156
- const sl = Object.keys(MODEL.listOfAllSelectors);
12157
- // Add selectors of actor, iterator and settings dimensions.
12158
- return sl;
12159
- }
12160
-
12161
12186
  orthogonalSelectors(c) {
12162
12187
  // Return TRUE iff the selectors in set `c` all are elements of
12163
12188
  // different experiment dimensions.
@@ -13026,7 +13051,7 @@ class Constraint {
13026
13051
  }
13027
13052
 
13028
13053
  get typeLetter() {
13029
- return 'C';
13054
+ return 'B';
13030
13055
  }
13031
13056
 
13032
13057
  get identifier() {
@@ -189,11 +189,16 @@ function uniformDecimals(data) {
189
189
  }
190
190
  maxi = Math.max(maxi, ss[0].length);
191
191
  }
192
- // STEP 2: Convert the data to a uniform format
192
+ // STEP 2: Convert the data to a uniform format.
193
+ const special = ['\u221E', '-\u221E', '\u2047', '\u00A2'];
193
194
  for(let i = 0; i < data.length; i++) {
194
- const f = parseFloat(data[i]);
195
+ const
196
+ v = data[i],
197
+ f = parseFloat(v);
195
198
  if(isNaN(f)) {
196
- data[i] = '\u26A0'; // Unicode warning sign
199
+ // Keep special values such as infinity, and replace error values
200
+ // by Unicode warning sign.
201
+ if(special.indexOf(v) < 0) data[i] = '\u26A0';
197
202
  } else if(maxe > 0) {
198
203
  // Convert ALL numbers to exponential notation with two decimals (1.23e+7)
199
204
  const v = f.toExponential(2);
@@ -405,51 +410,59 @@ function patternList(str) {
405
410
 
406
411
  function patternMatch(str, patterns) {
407
412
  // Returns TRUE when `str` matches the &|^-pattern.
408
- // NOTE: If a pattern starts with equals sign = then `str` must
409
- // equal the rest of the pattern to match; if it starts with a tilde
410
- // ~ then `str` must start with the rest of the pattern to match.
413
+ // NOTE: If a pattern starts with an opening bracket [ then `str` must
414
+ // start with the rest of the pattern to match. If it ends with a closing
415
+ // bracket ] then `str` must end with the first part of the pattern.
416
+ // In this way, [pattern] denotes that `str` should exactly match
411
417
  for(let i = 0; i < patterns.length; i++) {
412
418
  const p = patterns[i];
413
419
  // NOTE: `p` is an OR sub-pattern that tests for a set of "plus"
414
420
  // sub-sub-patterns (all of which should match) and a set of "min"
415
421
  // sub-sub-patters (all should NOT match)
416
422
  let pm,
423
+ swob,
424
+ ewcb,
417
425
  re,
418
426
  match = true;
419
427
  for(let j = 0; match && j < p.plus.length; j++) {
420
428
  pm = p.plus[j];
421
- if(pm.startsWith('=')) {
422
- match = (str === pm.substring(1));
423
- } else if(pm.startsWith('~')) {
429
+ swob = pm.startsWith('[');
430
+ ewcb = pm.endsWith(']');
431
+ if(swob && ewcb) {
432
+ match = (str === pm.slice(1, -1));
433
+ } else if(swob) {
424
434
  match = str.startsWith(pm.substring(1));
435
+ } else if(ewcb) {
436
+ match = str.endsWith(pm.slice(0, -1));
425
437
  } else {
426
438
  match = (str.indexOf(pm) >= 0);
427
439
  }
428
- // If no match, check whether pattern contains wildcards
440
+ // If no match, check whether pattern contains wildcards.
429
441
  if(!match && pm.indexOf('#') >= 0) {
430
442
  // If so, rematch using regular expression that tests for a
431
- // number or a ?? wildcard
443
+ // number or a ?? wildcard.
432
444
  let res = pm.split('#');
433
445
  for(let i = 0; i < res.length; i++) {
434
446
  res[i] = escapeRegex(res[i]);
435
447
  }
436
448
  res = res.join('(\\d+|\\?\\?)');
437
- if(pm.startsWith('=')) {
438
- res = '^' + res + '$';
439
- } else if(pm.startsWith('~')) {
440
- res = '^' + res;
441
- }
449
+ if(swob) res = '^' + res;
450
+ if(ewcb) res += '$';
442
451
  re = new RegExp(res, 'g');
443
452
  match = re.test(str);
444
453
  }
445
454
  }
446
- // Any "min" match indicates NO match for this sub-pattern,
455
+ // Any "min" match indicates NO match for this sub-pattern.
447
456
  for(let j = 0; match && j < p.min.length; j++) {
448
457
  pm = p.min[j];
449
- if(pm.startsWith('=')) {
450
- match = (str !== pm.substring(1));
451
- } else if(pm.startsWith('~')) {
458
+ swob = pm.startsWith('[');
459
+ ewcb = pm.endsWith(']');
460
+ if(swob && ewcb) {
461
+ match = (str !== pm.slice(1, -1));
462
+ } else if(swob) {
452
463
  match = !str.startsWith(pm.substring(1));
464
+ } else if(ewcb) {
465
+ match = !str.endsWith(pm.slice(0, -1));
453
466
  } else {
454
467
  match = (str.indexOf(pm) < 0);
455
468
  }
@@ -461,11 +474,8 @@ function patternMatch(str, patterns) {
461
474
  res[i] = escapeRegex(res[i]);
462
475
  }
463
476
  res = res.join('(\\d+|\\?\\?)');
464
- if(pm.startsWith('=')) {
465
- res = '^' + res + '$';
466
- } else if(pm.startsWith('~')) {
467
- res = '^' + res;
468
- }
477
+ if(swob) res = '^' + res;
478
+ if(ewcb) res += '$';
469
479
  re = new RegExp(res, 'g');
470
480
  match = !re.test(str);
471
481
  }
@@ -973,18 +983,20 @@ function nameToLines(name, actor_name = '') {
973
983
  // the node box.
974
984
  let m = actor_name.length;
975
985
  const
976
- d = Math.floor(Math.sqrt(0.3 * name.length)),
986
+ d = Math.floor(Math.sqrt(0.25 * name.length)),
977
987
  // Do not wrap strings shorter than 13 characters (about 50 pixels).
978
988
  limit = Math.max(Math.ceil(name.length / d), m, 13),
979
- a = name.split(' ');
980
- // Split words at '-' when wider than limit
989
+ // NOTE: Do not split on spaces followed by a number or a single
990
+ // capital letter.
991
+ a = name.split(/\s(?!\d+:|\d+$|[A-Z]\W)/);
992
+ // Split words at '-' when wider than limit.
981
993
  for(let j = 0; j < a.length; j++) {
982
994
  if(a[j].length > limit) {
983
995
  const sw = a[j].split('-');
984
996
  if(sw.length > 1) {
985
- // Replace j-th word by last fragment of split string
997
+ // Replace j-th word by last fragment of split string.
986
998
  a[j] = sw.pop();
987
- // Insert remaining fragments before
999
+ // Insert remaining fragments before.
988
1000
  while(sw.length > 0) a.splice(j, 0, sw.pop() + '-');
989
1001
  }
990
1002
  }
@@ -885,7 +885,7 @@ class ExpressionParser {
885
885
  // of commas, semicolons and spaces.
886
886
  x.r = run_spec.split(/[\,\;\/\s]+/g);
887
887
  // NOTE: The VMI instruction accepts `x.r` to be a list of selectors
888
- // or an integer number.
888
+ // or an integer number.
889
889
  } else {
890
890
  // If the specifier does start with a #, trim it...
891
891
  run_spec = run_spec.substring(1);
@@ -933,6 +933,20 @@ class ExpressionParser {
933
933
  x.x = MODEL.experiments[n];
934
934
  }
935
935
  }
936
+ // If run specifier `x.r` is a list, check whether all elements in the
937
+ // list are selectors in a dimension of experiment `x.x` (if specified).
938
+ // If experiment is unknown, check against the list of all selectors
939
+ // defined in the model.
940
+ if(Array.isArray(x.r)) {
941
+ const
942
+ sl = (x.x instanceof Experiment ? x.x.allDimensionSelectors :
943
+ Object.keys(MODEL.dictOfAllSelectors)),
944
+ unknown = complement(x.r, sl);
945
+ if(unknown.length) {
946
+ msg = pluralS(unknown.length, 'unknown selector') + ': <tt>' +
947
+ unknown.join(' ') + '</tt>';
948
+ }
949
+ }
936
950
  // END of code for parsing an experiment result specifier.
937
951
  // Now proceed with parsing the variable name.
938
952
 
@@ -1350,7 +1364,10 @@ class ExpressionParser {
1350
1364
  const
1351
1365
  parts = name.split(UI.PREFIXER),
1352
1366
  tail = parts.pop();
1353
- if(parts.length > 0) {
1367
+ if(!tail && parts.length) {
1368
+ // Prefix without its trailing colon+space could identify an entity.
1369
+ obj = MODEL.objectByID(UI.nameToID(parts.join(UI.PREFIXER)));
1370
+ } else if(parts.length > 0) {
1354
1371
  // Name contains at least one prefix => last part *could* be a
1355
1372
  // method name, so look it up after adding a leading colon.
1356
1373
  const method = MODEL.equationByID(UI.nameToID(':' + tail));
@@ -3377,13 +3394,19 @@ class VirtualMachine {
3377
3394
  // Infer cycle basis for combined power grids for which Kirchhoff's
3378
3395
  // voltage law must be enforced.
3379
3396
  if(MODEL.with_power_flow) {
3380
- MONITOR.logMessage(1, 'POWER FLOW: ' +
3397
+ this.logMessage(1, 'POWER FLOW: ' +
3381
3398
  pluralS(Object.keys(MODEL.power_grids).length, 'grid'));
3399
+ if(MODEL.ignore_grid_capacity) this.logMessage(1,
3400
+ 'NOTE: Assuming infinite grid line cacity');
3401
+ if(MODEL.ignore_KVL) this.logMessage(1,
3402
+ 'NOTE: Disregarding Kirchhoff\'s Voltage Law');
3403
+ if(MODEL.ignore_power_losses) this.logMessage(1,
3404
+ 'NOTE: Disregarding transmission losses');
3382
3405
  POWER_GRID_MANAGER.inferCycleBasis();
3383
3406
  if(POWER_GRID_MANAGER.messages.length > 1) {
3384
3407
  UI.warn('Check monitor for power grid warnings');
3385
3408
  }
3386
- MONITOR.logMessage(1, POWER_GRID_MANAGER.messages.join('\n'));
3409
+ this.logMessage(1, POWER_GRID_MANAGER.messages.join('\n'));
3387
3410
  if(POWER_GRID_MANAGER.cycle_basis.length) this.logMessage(1,
3388
3411
  'Enforcing Kirchhoff\'s voltage law for ' +
3389
3412
  POWER_GRID_MANAGER.cycleBasisAsString);
@@ -6190,7 +6213,7 @@ Solver status = ${json.status}`);
6190
6213
  } catch(err) {
6191
6214
  const msg = `ERROR while processing solver data for block ${bnr}: ${err}`;
6192
6215
  console.log(msg);
6193
- MONITOR.logMessage(bnr, msg);
6216
+ this.logMessage(bnr, msg);
6194
6217
  UI.alert(msg);
6195
6218
  this.stopSolving();
6196
6219
  this.halted = true;
@@ -7269,12 +7292,12 @@ function VMI_push_run_result(x, args) {
7269
7292
  rr = r.results[rri],
7270
7293
  tsd = r.time_step_duration,
7271
7294
  // Get the delta-t multiplier: divide model time step duration
7272
- // by time step duration of the experiment run if they differ
7295
+ // by time step duration of the experiment run if they differ.
7273
7296
  dtm = (Math.abs(tsd - model_dt) < VM.NEAR_ZERO ? 1 : model_dt / tsd);
7274
7297
  let stat = rrspec.s;
7275
- // For outcome datasets without specific statistic, default to LAST
7298
+ // For outcome datasets without specific statistic, default to LAST.
7276
7299
  if(!(stat || rr.x_variable)) stat = 'LAST';
7277
- // For a valid experiment variable, the default value is 0
7300
+ // For a valid experiment variable, the default value is 0.
7278
7301
  v = 0;
7279
7302
  if(stat) {
7280
7303
  if(stat === 'LAST') {
@@ -7302,14 +7325,14 @@ function VMI_push_run_result(x, args) {
7302
7325
  console.log(trc.join(''));
7303
7326
  }
7304
7327
  } else {
7305
- // No statistic => return the vector for local time step
7328
+ // No statistic => return the vector for local time step,
7306
7329
  // using here, too, the delta-time-modifier to adjust the offsets
7307
7330
  // for different time steps per experiment.
7308
7331
  const tot = twoOffsetTimeStep(x.step[x.step.length - 1],
7309
7332
  args[1], args[2], args[3], args[4], dtm, x);
7310
7333
  // Scale the (midpoint) time step (at current model run time scale)
7311
7334
  // to the experiment run time scale and get the run result value.
7312
- // NOTE: the .m property specifies the time scaling method, and
7335
+ // NOTE: The .m property specifies the time scaling method, and
7313
7336
  // the .p property whether the run result vector should be used as
7314
7337
  // a periodic time series.
7315
7338
  v = rr.valueAtModelTime(tot[0], model_dt, rrspec.m, rrspec.p);
@@ -7325,6 +7348,11 @@ function VMI_push_run_result(x, args) {
7325
7348
  }
7326
7349
  }
7327
7350
  }
7351
+ // Truncate near-zero values.
7352
+ if(v && Math.abs(v) < VM.SIG_DIF_FROM_ZERO) {
7353
+ console.log('NOTE: Truncated experiment run result', v, 'to zero');
7354
+ v = 0;
7355
+ }
7328
7356
  x.push(v);
7329
7357
  }
7330
7358
 
@@ -8413,11 +8441,18 @@ function VMI_set_bounds(args) {
8413
8441
  VM.variables[vi - 1][0],'] t = ', VM.t, ' LB = ', VM.sig4Dig(l),
8414
8442
  ', UB = ', VM.sig4Dig(u), fixed].join(''), l, u, inf_val);
8415
8443
  } else if(u < l) {
8416
- // Warn that "impossible" bounds would have been set...
8417
- const vk = vbl.displayName;
8418
- if(!VM.bound_issues[vk]) VM.bound_issues[vk] = [];
8419
- VM.bound_issues[vk].push(VM.t);
8420
- // ... and set LB to UB, so that lowest value is bounding.
8444
+ // Check the difference, as this may be negligible.
8445
+ if(u - l < VM.SIG_DIF_FROM_ZERO) {
8446
+ u = Math.round(u * 1e5) / 1e5;
8447
+ // NOTE: This may result in -0 (minus zero) => then set to 0.
8448
+ if(u < 0 && u > -VM.NEAR_ZERO) u = 0;
8449
+ } else {
8450
+ // If substantial, warn that "impossible" bounds would have been set.
8451
+ const vk = vbl.displayName;
8452
+ if(!VM.bound_issues[vk]) VM.bound_issues[vk] = [];
8453
+ VM.bound_issues[vk].push(VM.t);
8454
+ }
8455
+ // Set LB to UB, so that lowest value is bounding.
8421
8456
  l = u;
8422
8457
  }
8423
8458
  // NOTE: Since the VM vectors for lower bounds and upper bounds are