linny-r 1.5.8 → 1.6.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.
@@ -159,22 +159,28 @@ class LinnyRModel {
159
159
  }
160
160
 
161
161
  get outcomeNames() {
162
+ // Return the list of names of experiment outcome variables, i.e.,
163
+ // datasets that have been designated as outcomes, and all regular
164
+ // equations (not methods).
162
165
  const olist = [];
163
166
  for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
164
167
  const ds = this.datasets[k];
165
168
  if(ds !== this.equations_dataset && ds.outcome) {
166
- // NOTE: experiments store only ONE modifier result per run
169
+ // NOTE: Experiments store only ONE modifier result per run.
167
170
  olist.push(ds.displayName);
168
171
  }
169
172
  }
170
- // ALL equation results are stored, so add all equation selectors
173
+ // ALL equation results are stored, so add all equation selectors...
171
174
  const dsm = this.equations_dataset.modifiers;
172
- for(let k in dsm) if(dsm.hasOwnProperty(k)) olist.push(dsm[k].selector);
175
+ // ... except selectors starting with a colon (methods).
176
+ for(let k in dsm) if(dsm.hasOwnProperty(k) && !k.startsWith(':')) {
177
+ olist.push(dsm[k].selector);
178
+ }
173
179
  return olist;
174
180
  }
175
181
 
176
182
  get newProcessCode() {
177
- // Return the next unused process code
183
+ // Return the next unused process code.
178
184
  const n = this.next_process_number;
179
185
  this.next_process_number++;
180
186
  // Process codes are decimal number STRINGS
@@ -417,30 +423,67 @@ class LinnyRModel {
417
423
  }
418
424
 
419
425
  setByType(type) {
420
- // Returns a "dictionary" object with entities of the specified types
426
+ // Return a "dictionary" object with entities of the specified types
421
427
  if(type === 'Process') return this.processes;
422
428
  if(type === 'Product') return this.products;
423
429
  if(type === 'Cluster') return this.clusters;
430
+ // NOTE: the returned "dictionary" also contains the equations dataset
431
+ if(type === 'Dataset') return this.datasets;
424
432
  if(type === 'Link') return this.links;
425
433
  if(type === 'Constraint') return this.constraints;
426
434
  if(type === 'Actor') return this.actors;
427
- // NOTE: the returned "dictionary" also contains the equations dataset
428
- if(type === 'Dataset') return this.datasets;
429
435
  return {};
430
436
  }
431
-
437
+
438
+ get allEntities() {
439
+ // Return a "dictionary" of all entities in the model.
440
+ // NOTE: This includes equations (instances of DatasetModifier) but
441
+ // not the equations dataset itself.
442
+ const all = Object.assign({}, this.processes, this.products,
443
+ this.clusters, this.datasets, this.equations_dataset.modifiers,
444
+ this.links, this.actors, this.constraints);
445
+ // Remove the equations dataset from this dictionary
446
+ delete all[this.equations_dataset.identifier];
447
+ return all;
448
+ }
449
+
450
+ get allMethods() {
451
+ // Return a list with dataset modifiers that are "methods".
452
+ const
453
+ list = [],
454
+ keys = Object.keys(this.equations_dataset.modifiers);
455
+ for(let i = 0; i < keys.length; i++) {
456
+ if(keys[i].startsWith(':')) {
457
+ list.push(this.equations_dataset.modifiers[keys[i]]);
458
+ }
459
+ }
460
+ return list;
461
+ }
462
+
463
+ endsWithMethod(name) {
464
+ // Return method (instance of DatasetModifier) if `name` ends with
465
+ // ":(whitespace)m" for some method having selector ":m".
466
+ const ml = this.allMethods;
467
+ for(let i = 0; i < ml.length; i++) {
468
+ const re = new RegExp(
469
+ ':\\s*' + escapeRegex(ml[i].selector.substring(1)) + '$', 'i');
470
+ if(name.match(re)) return ml[i];
471
+ }
472
+ return null;
473
+ }
474
+
432
475
  entitiesWithAttribute(attr, et='ABCDLPQ') {
433
- // Returns a list of entities (of any type) having the specified attribute
476
+ // Return a list of entities (of any type) having the specified attribute.
434
477
  const list = [];
435
478
  if(attr === '' && et.indexOf('D') >= 0) {
436
- // Only datasets can have a value for "no attribute"
479
+ // Only datasets can have a value for "no attribute".
437
480
  for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
438
- // NOTE: ignore the equations dataset
481
+ // NOTE: Ignore the equations dataset.
439
482
  if(this.datasets[k] !== this.equations_dataset) {
440
483
  list.push(this.datasets[k]);
441
484
  }
442
485
  }
443
- // No other types of entity, so return this list
486
+ // No other types of entity, so return this list.
444
487
  return list;
445
488
  }
446
489
  if(VM.process_attr.indexOf(attr) >= 0 && et.indexOf('P') >= 0) {
@@ -477,15 +520,19 @@ class LinnyRModel {
477
520
  }
478
521
 
479
522
  allMatchingEntities(re, attr='') {
480
- // NOTE: this routine is computationally intensive as it performs matches
481
- // on the display names of entities while iterating over all relevant sets
523
+ // Return list of enties with a display name that matches RegExp `re`,
524
+ // and having attribute `attr` if specified.
525
+ // NOTE: This routine is computationally intensive as it performs
526
+ // matches on the display names of entities while iterating over all
527
+ // relevant entity sets.
482
528
  const
483
529
  me = [],
484
530
  res = re.toString();
485
531
 
486
532
  function scan(dict) {
487
- // Try to match all entities in `dict`
488
- for(let k in dict) if(dict.hasOwnProperty(k)) {
533
+ // Try to match all entities in `dict`.
534
+ // NOTE: Ignore method identifiers.
535
+ for(let k in dict) if(dict.hasOwnProperty(k) && !k.startsWith(':')) {
489
536
  const
490
537
  e = dict[k],
491
538
  m = [...e.displayName.matchAll(re)];
@@ -496,26 +543,63 @@ class LinnyRModel {
496
543
  for(let i = 1; same && i < m.length; i++) {
497
544
  same = parseInt(m[i][1]) === n;
498
545
  }
499
- // If so, add the entity to the set
546
+ // If so, add the entity to the set.
500
547
  if(same) me.push(e);
501
548
  }
502
549
  }
503
550
  }
504
551
 
505
- // Links limit the search (constraints have no attributes => skip)
552
+ // Links limit the search (constraints have no attributes => skip).
506
553
  if(res.indexOf(UI.LINK_ARROW) >= 0) {
507
554
  scan(this.links);
508
555
  } else {
556
+ // First get list of matching datasets.
557
+ scan(this.datasets);
558
+ if(me.length > 0 && attr) {
559
+ // If attribute is specified, retain only datasets having a
560
+ // modifier with selector = `attr`.
561
+ for(let i = me.length - 1; i >= 0; i--) {
562
+ if(!me[i].modifiers[attr]) me.splice(i, 1);
563
+ }
564
+ }
565
+ attr = attr.toUpperCase();
509
566
  if(!attr || VM.actor_attr.indexOf(attr) >= 0) scan(this.actors);
510
567
  if(!attr || VM.cluster_attr.indexOf(attr) >= 0) scan(this.clusters);
511
- if(!attr) scan(this.datasets);
512
568
  if(!attr || VM.process_attr.indexOf(attr) >= 0) scan(this.processes);
513
569
  if(!attr || VM.product_attr.indexOf(attr) >= 0) scan(this.products);
570
+ // NOTE: Equations cannot have an attribute.
514
571
  if(!attr && this.equations_dataset) scan(this.equations_dataset.modifiers);
515
572
  }
516
573
  return me;
517
574
  }
575
+
576
+ entitiesEndingOn(s, attr='') {
577
+ // Return a list of entities (of any type) having a display name that
578
+ // ends on string `s`.
579
+ // NOTE: The current implementation will overlook links having a FROM
580
+ // node that ends on `s`.
581
+ const re = new RegExp(escapeRegex(s) + '$', 'gi');
582
+ return this.allMatchingEntities(re, attr);
583
+ }
518
584
 
585
+ entitiesInString(s) {
586
+ // Return a list of entities referenced in string `s`.
587
+ if(s.indexOf('[') < 0) return [];
588
+ const
589
+ el = [],
590
+ ml = [...s.matchAll(/\[(\{[^\}]+\}){0,1}([^\]]+)\]/g)];
591
+ for(let i = 0; i < ml.length; i++) {
592
+ const n = ml[i][2].trim();
593
+ let sep = n.lastIndexOf('|');
594
+ if(sep < 0) sep = n.lastIndexOf('@');
595
+ const
596
+ en = (sep < 0 ? n : n.substring(0, sep)).trim(),
597
+ e = this.objectByName(en);
598
+ if(e) addDistinct(e, el);
599
+ }
600
+ return el;
601
+ }
602
+
519
603
  get clustersToIgnore() {
520
604
  // Returns a "dictionary" with all clusters that are to be ignored
521
605
  const cti = {};
@@ -906,6 +990,53 @@ class LinnyRModel {
906
990
  return ss.length > 0;
907
991
  }
908
992
 
993
+ renamePrefixedDatasets(old_prefix, new_prefix) {
994
+ // Rename all datasets having the specified old prefix so that they
995
+ // have the specified new prefix UNLESS this would cause name conflicts.
996
+ const
997
+ oldkey = old_prefix.toLowerCase().split(UI.PREFIXER).join(':_'),
998
+ newkey = new_prefix.toLowerCase().split(UI.PREFIXER).join(':_'),
999
+ dsl = [];
1000
+ // No change if new prefix is identical to old prefix.
1001
+ if(old_prefix !== new_prefix) {
1002
+ for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
1003
+ if(k.startsWith(oldkey)) dsl.push(k);
1004
+ }
1005
+ // NOTE: No check for name conflicts needed when name change is
1006
+ // merely some upper/lower case change.
1007
+ if(newkey !== oldkey) {
1008
+ let nc = 0;
1009
+ for(let i = 0; i < dsl.length; i++) {
1010
+ let nk = newkey + dsl[i].substring(oldkey.length);
1011
+ if(MODEL.datasets[nk]) nc++;
1012
+ }
1013
+ if(nc) {
1014
+ UI.warn('Renaming ' + pluralS(dsl.length, 'dataset') +
1015
+ ' would cause ' + pluralS(nc, 'name conflict'));
1016
+ return false;
1017
+ }
1018
+ }
1019
+ // Reset counts of effects of a rename operation.
1020
+ this.entity_count = 0;
1021
+ this.expression_count = 0;
1022
+ // Rename datasets one by one, suppressing notifications.
1023
+ for(let i = 0; i < dsl.length; i++) {
1024
+ const d = MODEL.datasets[dsl[i]];
1025
+ d.rename(d.displayName.replace(old_prefix, new_prefix), false);
1026
+ }
1027
+ let msg = 'Renamed ' + pluralS(dsl.length, 'dataset').toLowerCase();
1028
+ if(MODEL.variable_count) msg += ', and updated ' +
1029
+ pluralS(MODEL.variable_count, 'variable') + ' in ' +
1030
+ pluralS(MODEL.expression_count, 'expression');
1031
+ UI.notify(msg);
1032
+ if(EXPERIMENT_MANAGER.selected_experiment) {
1033
+ EXPERIMENT_MANAGER.selected_experiment.inferVariables();
1034
+ }
1035
+ UI.updateControllerDialogs('CDEFJX');
1036
+ }
1037
+ return true;
1038
+ }
1039
+
909
1040
  //
910
1041
  // Methods that add an entity to the model
911
1042
  //
@@ -1430,13 +1561,21 @@ class LinnyRModel {
1430
1561
  //
1431
1562
 
1432
1563
  alignToGrid() {
1433
- // Move all positioned model elements to the nearest grid point
1564
+ // Move all positioned model elements to the nearest grid point.
1434
1565
  if(!this.align_to_grid) return;
1435
1566
  let move = false;
1436
1567
  const fc = this.focal_cluster;
1437
1568
  // NOTE: Do not align notes to the grid. This will permit more
1438
1569
  // precise positioning, while aligning will not improve the layout
1439
1570
  // of the diagram because notes are not connected to arrows.
1571
+ // However, when notes relate to nearby nodes, preserve their relative
1572
+ // position to this node.
1573
+ for(let i = 0; i < fc.notes.length; i++) {
1574
+ const
1575
+ note = fc.notes[i],
1576
+ nbn = note.nearbyNode;
1577
+ note.nearby_pos = (nbn ? {node: nbn, oldx: x, oldy: y} : null);
1578
+ }
1440
1579
  for(let i = 0; i < fc.processes.length; i++) {
1441
1580
  move = fc.processes[i].alignToGrid() || move;
1442
1581
  }
@@ -1446,11 +1585,25 @@ class LinnyRModel {
1446
1585
  for(let i = 0; i < fc.sub_clusters.length; i++) {
1447
1586
  move = fc.sub_clusters[i].alignToGrid() || move;
1448
1587
  }
1449
- if(move) UI.drawDiagram(this);
1588
+ if(move) {
1589
+ // Reposition "associated" notes.
1590
+ for(let i = 0; i < fc.notes.length; i++) {
1591
+ const
1592
+ note = fc.notes[i],
1593
+ nbp = note.nearby_pos;
1594
+ if(nbp) {
1595
+ // Adjust (x, y) so as to retain the relative position.
1596
+ note.x += nbp.node.x - npb.oldx;
1597
+ note.y += nbp.node.y - npb.oldy;
1598
+ note.nearby_pos = null;
1599
+ }
1600
+ }
1601
+ UI.drawDiagram(this);
1602
+ }
1450
1603
  }
1451
1604
 
1452
1605
  translateGraph(dx, dy) {
1453
- // Move all entities in the focal cluster by (dx, dy) pixels
1606
+ // Move all entities in the focal cluster by (dx, dy) pixels.
1454
1607
  if(!dx && !dy) return;
1455
1608
  const fc = this.focal_cluster;
1456
1609
  for(let i = 0; i < fc.processes.length; i++) {
@@ -1469,9 +1622,9 @@ class LinnyRModel {
1469
1622
  fc.notes[i].x += dx;
1470
1623
  fc.notes[i].y += dy;
1471
1624
  }
1472
- // NOTE: force drawing, because SVG must immediately be downloadable
1625
+ // NOTE: force drawing, because SVG must immediately be downloadable.
1473
1626
  UI.drawDiagram(this);
1474
- // If dragging, add (dx, dy) to the properties of the top "move" UndoEdit
1627
+ // If dragging, add (dx, dy) to the properties of the top "move" UndoEdit.
1475
1628
  if(UI.dragged_node) UNDO_STACK.addOffset(dx, dy);
1476
1629
  }
1477
1630
 
@@ -1617,8 +1770,8 @@ class LinnyRModel {
1617
1770
  miny = Math.min(miny, obj.y - obj.height / 2);
1618
1771
  }
1619
1772
  }
1620
- // Translate entire graph if some elements are above and/or left of the
1621
- // paper edge
1773
+ // Translate entire graph if some elements are above and/or left of
1774
+ // the paper edge.
1622
1775
  if(minx < 0 || miny < 0) {
1623
1776
  // NOTE: limit translation to 5 pixels to prevent "run-away effect"
1624
1777
  this.translateGraph(Math.min(5, -minx), Math.min(5, -miny));
@@ -2055,8 +2208,8 @@ class LinnyRModel {
2055
2208
  }
2056
2209
 
2057
2210
  deleteSelection() {
2058
- // Removes all selected nodes (with their associated links and constraints)
2059
- // and selected links
2211
+ // Remove all selected nodes (with their associated links and constraints)
2212
+ // and selected links.
2060
2213
  // NOTE: This method implements the DELETE action, and hence should be
2061
2214
  // undoable. The UndoEdit is created by the calling routine; the methods
2062
2215
  // that actually delete model elements append their XML to the XML attribute
@@ -2064,7 +2217,7 @@ class LinnyRModel {
2064
2217
  let obj,
2065
2218
  fc = this.focal_cluster;
2066
2219
  // Update the documentation manager (GUI only) if selection contains the
2067
- // current entity
2220
+ // current entity.
2068
2221
  if(DOCUMENTATION_MANAGER) DOCUMENTATION_MANAGER.clearEntity(this.selection);
2069
2222
  // First delete links and constraints.
2070
2223
  for(let i = this.selection.length - 1; i >= 0; i--) {
@@ -2964,7 +3117,7 @@ class LinnyRModel {
2964
3117
  names = [],
2965
3118
  scale_re = /\s+\(x[0-9\.\,]+\)$/;
2966
3119
  // First create list of distinct variables used in charts.
2967
- // NOTE: Also include those that are not "visible" in a chart.
3120
+ // NOTE: Also include those that are not checked as "visible".
2968
3121
  for(let i = 0; i < this.charts.length; i++) {
2969
3122
  const c = this.charts[i];
2970
3123
  for(let j = 0; j < c.variables.length; j++) {
@@ -2976,7 +3129,7 @@ class LinnyRModel {
2976
3129
  vn = vn.replace(scale_re, '');
2977
3130
  // Add only if (now unscaled) variable has not been added already.
2978
3131
  if(names.indexOf(vn) < 0) {
2979
- // NOTE: Chart variable object is used ony as adummy, so NULL
3132
+ // NOTE: Chart variable object is used ony as a dummy, so NULL
2980
3133
  // can be used as its "owner chart".
2981
3134
  const cv = new ChartVariable(null);
2982
3135
  cv.setProperties(v.object, v.attribute, false, '#000000');
@@ -3028,12 +3181,12 @@ class LinnyRModel {
3028
3181
  }
3029
3182
 
3030
3183
  get listOfAllSelectors() {
3031
- // Returns list of all dataset modifier selectors as "dictionary" like so:
3032
- // {selector_1: [list of datasets], ...}
3184
+ // Returns list of all dataset modifier selectors as a "dictionary"
3185
+ // like so: {selector_1: [list of datasets], ...}
3033
3186
  const ds_dict = {};
3034
3187
  for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
3035
3188
  const ds = this.datasets[k];
3036
- // NOTE: ignore selectors of the equations dataset
3189
+ // NOTE: Ignore selectors of the equations dataset.
3037
3190
  if(ds !== this.equations_dataset) {
3038
3191
  for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
3039
3192
  const s = ds.modifiers[m].selector;
@@ -3091,6 +3244,29 @@ class LinnyRModel {
3091
3244
  sl.push(this.datasets[obj].displayName, this.datasets[obj].comments);
3092
3245
  }
3093
3246
  }
3247
+ const keys = Object.keys(this.equations_dataset.modifiers);
3248
+ sl.push('_____Equations');
3249
+ for(let i = 0; i < keys.length; i++) {
3250
+ const m = this.equations_dataset.modifiers[keys[i]];
3251
+ if(!m.selector.startsWith(':')) {
3252
+ sl.push(m.displayName, '`' + m.expression.text + '`\n');
3253
+ }
3254
+ }
3255
+ sl.push('_____Methods');
3256
+ for(let i = 0; i < keys.length; i++) {
3257
+ const m = this.equations_dataset.modifiers[keys[i]];
3258
+ if(m.selector.startsWith(':')) {
3259
+ let markup = '\n\nDoes not apply to any entity.';
3260
+ if(m.expression.eligible_prefixes) {
3261
+ const el = Object.keys(m.expression.eligible_prefixes)
3262
+ .sort(compareSelectors);
3263
+ if(el.length > 0) markup = '\n\nApplies to ' +
3264
+ pluralS(el.length, 'prefixed entity group') +
3265
+ ':\n- ' + el.join('\n- ');
3266
+ }
3267
+ sl.push(m.displayName, '`' + m.expression.text + '`' + markup);
3268
+ }
3269
+ }
3094
3270
  sl.push('_____Charts');
3095
3271
  for(let i = 0; i < this.charts.length; i++) {
3096
3272
  sl.push(this.charts[i].title, this.charts[i].comments);
@@ -4686,7 +4862,7 @@ class ObjectWithXYWH {
4686
4862
 
4687
4863
  alignToGrid() {
4688
4864
  // Align this object to the grid, and return TRUE if this involved
4689
- // a move
4865
+ // a move.
4690
4866
  const
4691
4867
  ox = this.x,
4692
4868
  oy = this.y,
@@ -4909,7 +5085,7 @@ class Note extends ObjectWithXYWH {
4909
5085
  bar = inner.lastIndexOf('|'),
4910
5086
  arrow = inner.lastIndexOf('->');
4911
5087
  // Special case: [[#]] denotes the number context of this note.
4912
- if(tag.replace(/\s+/, '') === '[[#]]') {
5088
+ if(inner === '#') {
4913
5089
  this.fields.push(new NoteField(this, tag, this));
4914
5090
  // Done, so move on to the next tag
4915
5091
  continue;
@@ -4937,7 +5113,6 @@ class Note extends ObjectWithXYWH {
4937
5113
  ena = inner.split('|');
4938
5114
  }
4939
5115
  // Look up entity for name and attribute.
4940
- // NOTE: A leading colon denotes "prefix with cluster name".
4941
5116
  let en = UI.colonPrefixedName(ena[0].trim(), this.clusterPrefix),
4942
5117
  id = UI.nameToID(en),
4943
5118
  // First try to match `id` with the IDs of wildcard equations,
@@ -4965,9 +5140,14 @@ class Note extends ObjectWithXYWH {
4965
5140
  }
4966
5141
  }
4967
5142
  if(!obj) {
4968
- UI.warn(`Unknown model entity "${en}"`);
5143
+ const m = MODEL.equations_dataset.modifiers[UI.nameToID(ena[0])];
5144
+ if(m) {
5145
+ UI.warn('Methods cannot be evaluated without prefix');
5146
+ } else {
5147
+ UI.warn(`Unknown model entity "${en}"`);
5148
+ }
4969
5149
  } else if(obj instanceof DatasetModifier) {
4970
- // NOTE: equations are (for now) dimensionless => unit '1'.
5150
+ // NOTE: equations are (for now) dimenssonless => unit '1'.
4971
5151
  if(obj.dataset !== MODEL.equations_dataset) {
4972
5152
  from_unit = obj.dataset.scale_unit;
4973
5153
  multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
@@ -6004,8 +6184,8 @@ class Cluster extends NodeBox {
6004
6184
  }
6005
6185
 
6006
6186
  addProductPosition(p, x=null, y=null) {
6007
- // Add a product position for product `p` to this cluster unless such pp
6008
- // already exists; then return this (new) product position
6187
+ // Add a product position for product `p` to this cluster unless such
6188
+ // "pp" already exists, and then return this (new) product position.
6009
6189
  let pp = this.indexOfProduct(p);
6010
6190
  if(pp >= 0) {
6011
6191
  pp = this.product_positions[pp];
@@ -6023,7 +6203,8 @@ class Cluster extends NodeBox {
6023
6203
  }
6024
6204
 
6025
6205
  containsProduct(p) {
6026
- // Return the subcluster of this cluster that contains product `p`, or null
6206
+ // Return the subcluster of this cluster that contains product `p`,
6207
+ // or NULL if `p` does not occur in this cluster.
6027
6208
  if(this.indexOfProduct(p) >= 0) return this;
6028
6209
  for(let i = 0; i < this.sub_clusters.length; i++) {
6029
6210
  if(this.sub_clusters[i].containsProduct(p)) {
@@ -6379,7 +6560,7 @@ class Cluster extends NodeBox {
6379
6560
  }
6380
6561
 
6381
6562
  /* DISABLED -- idea was OK but this results in many additional links
6382
- that clutter the diagram; represemting these lins by block arrows
6563
+ that clutter the diagram; representing these lines by block arrows
6383
6564
  produces better results
6384
6565
 
6385
6566
  // Special case: P1 --> Q with process Q outside this cluster that
@@ -8316,17 +8497,17 @@ class DatasetModifier {
8316
8497
  }
8317
8498
 
8318
8499
  get identifier() {
8319
- // NOTE: identifier will be unique only for equations
8500
+ // NOTE: Identifier will be unique only for equations.
8320
8501
  return UI.nameToID(this.selector);
8321
8502
  }
8322
8503
  get displayName() {
8323
- // NOTE: when "displayed", dataset modifiers have their selector as name
8504
+ // NOTE: When "displayed", dataset modifiers have their selector as name.
8324
8505
  return this.selector;
8325
8506
  }
8326
8507
 
8327
8508
  get asXML() {
8328
- // NOTE: for some reason, selector may become empty string, so prevent
8329
- // saving such unidentified modifiers
8509
+ // NOTE: For some reason, selector may become empty string, so prevent
8510
+ // saving such unidentified modifiers.
8330
8511
  if(this.selector.trim().length === 0) return '';
8331
8512
  return ['<modifier><selector>', xmlEncoded(this.selector),
8332
8513
  '</selector><expression>', xmlEncoded(this.expression.text),
@@ -8336,18 +8517,18 @@ class DatasetModifier {
8336
8517
  initFromXML(node) {
8337
8518
  this.expression.text = xmlDecoded(nodeContentByTag(node, 'expression'));
8338
8519
  if(IO_CONTEXT) {
8339
- // Contextualize the included expression
8520
+ // Contextualize the included expression.
8340
8521
  IO_CONTEXT.rewrite(this.expression);
8341
8522
  }
8342
8523
  }
8343
8524
 
8344
8525
  get hasWildcards() {
8345
- // Returns TRUE if this modifier contains wildcards
8526
+ // Return TRUE if this modifier contains wildcards.
8346
8527
  return this.dataset.isWildcardSelector(this.selector);
8347
8528
  }
8348
8529
 
8349
8530
  get numberContext() {
8350
- // Returns the string to be used to evaluate #.
8531
+ // Return the string to be used to evaluate #.
8351
8532
  // NOTE: If the selector contains wildcards, return "?" to indicate
8352
8533
  // that the value of # cannot be inferred at compile time.
8353
8534
  if(this.hasWildcards) return '?';
@@ -8358,12 +8539,12 @@ class DatasetModifier {
8358
8539
  }
8359
8540
 
8360
8541
  match(s) {
8361
- // Returns TRUE if string `s` matches with the wildcard pattern of
8542
+ // Return TRUE if string `s` matches with the wildcard pattern of
8362
8543
  // the selector.
8363
8544
  if(!this.hasWildcards) return s === this.selector;
8364
8545
  let re;
8365
8546
  if(this.dataset === MODEL.equations_dataset) {
8366
- // Equations wildcards only match with digits
8547
+ // Equations wildcards only match with digits.
8367
8548
  re = wildcardMatchRegex(this.selector, true);
8368
8549
  } else {
8369
8550
  // Selector wildcards match with any character, so replace ? by .
@@ -8508,14 +8689,22 @@ class Dataset {
8508
8689
  }
8509
8690
 
8510
8691
  get allModifiersAreStatic() {
8511
- // Returns TRUE if all modifier expressions are static
8692
+ // Return TRUE if all modifier expressions are static.
8512
8693
  return this.modifiersAreStatic(Object.keys(this.modifiers));
8513
8694
  }
8514
8695
 
8696
+ get mayBeDynamic() {
8697
+ // Return TRUE if this dataset has time series data, or if some of
8698
+ // its modifier expressions are dynamic.
8699
+ return !this.array && (this.data.length > 1 ||
8700
+ (this.data.length > 0 && !this.periodic) ||
8701
+ !this.allModifiersAreStatic);
8702
+ }
8703
+
8515
8704
  get inferPrefixableModifiers() {
8516
- // Returns list of dataset modifiers with expressions that do not
8705
+ // Return a list of dataset modifiers with expressions that do not
8517
8706
  // reference any variable and hence could probably better be represented
8518
- // by a prefixed dataset having the expression value as its default
8707
+ // by a prefixed dataset having the expression value as its default.
8519
8708
  const pml = [];
8520
8709
  if(this !== this.equations_dataset) {
8521
8710
  const sl = this.plainSelectors;
@@ -8525,7 +8714,7 @@ class Dataset {
8525
8714
  m = this.modifiers[sl[i].toLowerCase()],
8526
8715
  x = m.expression;
8527
8716
  // Static expressions without variables can also be used
8528
- // as dataset default value
8717
+ // as dataset default value.
8529
8718
  if(x.isStatic && x.text.indexOf('[') < 0) pml.push(m);
8530
8719
  }
8531
8720
  }
@@ -8534,13 +8723,13 @@ class Dataset {
8534
8723
  }
8535
8724
 
8536
8725
  get timeStepDuration() {
8537
- // Returns duration of 1 time step on the time scale of this dataset
8726
+ // Return duration of 1 time step on the time scale of this dataset.
8538
8727
  return this.time_scale * VM.time_unit_values[this.time_unit];
8539
8728
  }
8540
8729
 
8541
8730
  get defaultValue() {
8542
- // Returns default value *scaled to the model time step*
8543
- // NOTE: scaling is only needed for the weighted sum method
8731
+ // Return default value *scaled to the model time step*.
8732
+ // NOTE: Scaling is only needed for the weighted sum method.
8544
8733
  if(this.method !== 'w-sum' || this.default_value >= VM.PLUS_INFINITY) {
8545
8734
  return this.default_value;
8546
8735
  }
@@ -8569,8 +8758,8 @@ class Dataset {
8569
8758
  return d.join(';');
8570
8759
  }
8571
8760
 
8572
- // Returns a string denoting the properties of this dataset.
8573
8761
  get propertiesString() {
8762
+ // Return a string denoting the properties of this dataset.
8574
8763
  if(this.data.length === 0) return '';
8575
8764
  let time_prop;
8576
8765
  if(this.array) {
@@ -8581,12 +8770,12 @@ class Dataset {
8581
8770
  DATASET_MANAGER.method_symbols[
8582
8771
  DATASET_MANAGER.methods.indexOf(this.method)]].join('');
8583
8772
  }
8584
- // Circular arrow symbolizes "repeating"
8773
+ // Circular arrow symbolizes "repeating".
8585
8774
  return '&nbsp;(' + time_prop + (this.periodic ? '&nbsp;\u21BB' : '') + ')';
8586
8775
  }
8587
8776
 
8588
8777
  unpackDataString(str) {
8589
- // Converts semicolon-separated data to a numeric array.
8778
+ // Convert semicolon-separated data to a numeric array.
8590
8779
  this.data.length = 0;
8591
8780
  if(str) {
8592
8781
  const numbers = str.split(';');
@@ -8599,12 +8788,12 @@ class Dataset {
8599
8788
  }
8600
8789
 
8601
8790
  computeVector() {
8602
- // Converts data to a vector on the model's time scale, i.e., 1 time step
8603
- // lasting one unit on the model time scale
8791
+ // Convert data to a vector on the time scale of the model, i.e.,
8792
+ // 1 time step lasting one unit on the model time scale.
8604
8793
 
8605
- // NOTE: since 9 October 2021, a dataset can also be defined as an "array",
8606
- // which differs from a time series in that the vector is filled with the
8607
- // data values "as is" to permit accessing a specific value at index #
8794
+ // NOTE: A dataset can also be defined as an "array", which differs
8795
+ // from a time series in that the vector is filled with the data values
8796
+ // "as is" to permit accessing a specific value at index #.
8608
8797
  if(this.array) {
8609
8798
  this.vector = this.data.slice();
8610
8799
  return;
@@ -8612,16 +8801,16 @@ class Dataset {
8612
8801
  // Like all vectors, vector[0] corresponds to initial value, and vector[1]
8613
8802
  // to the model setting "Optimize from step t=..."
8614
8803
  // NOTES:
8615
- // (1) the first number of a datasets time series is ALWAYS assumed to
8804
+ // (1) The first number of a datasets time series is ALWAYS assumed to
8616
8805
  // correspond to t=1, whereas the simulation may be set to start later!
8617
- // (2) model run length includes 1 look-ahead period
8806
+ // (2) Model run length includes 1 look-ahead period.
8618
8807
  VM.scaleDataToVector(this.data, this.vector, this.timeStepDuration,
8619
8808
  MODEL.timeStepDuration, MODEL.runLength, MODEL.start_period,
8620
8809
  this.defaultValue, this.periodic, this.method);
8621
8810
  }
8622
8811
 
8623
8812
  computeStatistics() {
8624
- // Computes descriptive statistics for data (NOT vector!).
8813
+ // Compute descriptive statistics for data (NOT vector!).
8625
8814
  if(this.data.length === 0) {
8626
8815
  this.min = VM.UNDEFINED;
8627
8816
  this.max = VM.UNDEFINED;
@@ -8646,18 +8835,18 @@ class Dataset {
8646
8835
  }
8647
8836
 
8648
8837
  get statisticsAsString() {
8649
- // Returns descriptive statistics in human-readable form
8838
+ // Return descriptive statistics in human-readable form.
8650
8839
  let s = 'N = ' + this.data.length;
8651
8840
  if(N > 0) {
8652
- s += ', range = [' + VM.sig4Dig(this.min) + ', ' + VM.sig4Dig(this.max) +
8653
- ', mean = ' + VM.sig4Dig(this.mean) + ', s.d. = ' +
8654
- VM.sig4Dig(this.standard_deviation);
8841
+ s += [', range = [', VM.sig4Dig(this.min), ', ', VM.sig4Dig(this.max),
8842
+ '], mean = ', VM.sig4Dig(this.mean), ', s.d. = ',
8843
+ VM.sig4Dig(this.standard_deviation)].join('');
8655
8844
  }
8656
8845
  return s;
8657
8846
  }
8658
8847
 
8659
8848
  attributeValue(a) {
8660
- // Returns the computed result for attribute `a`.
8849
+ // Return the computed result for attribute `a`.
8661
8850
  // NOTE: Datasets have ONE attribute (their vector) denoted by the empty
8662
8851
  // string; all other "attributes" should be modifier selectors, and
8663
8852
  // their value should be obtained using attributeExpression (see below).
@@ -8666,9 +8855,9 @@ class Dataset {
8666
8855
  }
8667
8856
 
8668
8857
  attributeExpression(a) {
8669
- // Returns expression for selector `a` (also considering wildcard
8858
+ // Return the expression for selector `a` (also considering wildcard
8670
8859
  // modifiers), or NULL if no such selector exists.
8671
- // NOTE: selectors no longer are case-sensitive.
8860
+ // NOTE: Selectors no longer are case-sensitive.
8672
8861
  if(a) {
8673
8862
  const mm = this.matchingModifiers([a]);
8674
8863
  if(mm.length > 0) return mm[0].expression;
@@ -8679,62 +8868,72 @@ class Dataset {
8679
8868
  get activeModifierExpression() {
8680
8869
  if(MODEL.running_experiment) {
8681
8870
  // If an experiment is running, check if dataset modifiers match the
8682
- // combination of selectors for the active run
8871
+ // combination of selectors for the active run.
8683
8872
  const mm = this.matchingModifiers(MODEL.running_experiment.activeCombination);
8684
- // If so, use the first match
8873
+ // If so, use the first match.
8685
8874
  if(mm.length > 0) return mm[0].expression;
8686
8875
  }
8687
8876
  if(this.default_selector) {
8688
- // If no experiment (so "normal" run), use default selector if specified
8877
+ // If no experiment (so "normal" run), use default selector if specified.
8689
8878
  const dm = this.modifiers[UI.nameToID(this.default_selector)];
8690
8879
  if(dm) return dm.expression;
8691
- // Exception should never occur, but check anyway and log it
8880
+ // Exception should never occur, but check anyway and log it.
8692
8881
  console.log('WARNING: Dataset "' + this.name +
8693
8882
  `" has no default selector "${this.default_selector}"`, this.modifiers);
8694
8883
  }
8695
- // Fall-through: return vector instead of expression
8884
+ // Fall-through: return vector instead of expression.
8696
8885
  return this.vector;
8697
8886
  }
8698
8887
 
8699
8888
  addModifier(selector, node=null, ioc=null) {
8700
8889
  let s = selector;
8701
- // Firstly, sanitize the selector
8890
+ // First sanitize the selector.
8702
8891
  if(this === MODEL.equations_dataset) {
8703
8892
  // Equation identifiers cannot contain characters that have special
8704
- // meaning in a variable identifier
8893
+ // meaning in a variable identifier.
8705
8894
  s = s.replace(/[\*\|\[\]\{\}\@\#]/g, '');
8706
8895
  if(s !== selector) {
8707
8896
  UI.warn('Equation name cannot contain [, ], {, }, |, @, # or *');
8708
8897
  return null;
8709
8898
  }
8710
8899
  // Wildcard selectors must be exactly 2 consecutive question marks,
8711
- // so reduce longer sequences (no warning)
8900
+ // so reduce longer sequences (no warning).
8712
8901
  s = s.replace(/\?\?+/g, '??');
8713
8902
  if(s.split('??').length > 2) {
8714
8903
  UI.warn('Equation name can contain only 1 wildcard');
8715
8904
  return null;
8716
8905
  }
8717
- // Reduce inner spaces to one, and trim outer spaces
8906
+ // Reduce inner spaces to one, and trim outer spaces.
8718
8907
  s = s.replace(/\s+/g, ' ').trim();
8719
- // Then prefix it when the IO context argument is defined
8720
- if(ioc) s = ioc.actualName(s);
8721
- // If equation already exists, return its modifier
8908
+ if(s.startsWith(':')) {
8909
+ // Methods must have no spaces directly after their leading colon,
8910
+ // and must not contain other colons.
8911
+ if(s.startsWith(': ')) s = ':' + s.substring(2);
8912
+ if(s.lastIndexOf(':') > 0) {
8913
+ UI.warn('Method name can contain only 1 colon');
8914
+ return null;
8915
+ }
8916
+ } else {
8917
+ // Prefix it when the IO context argument is defined.
8918
+ if(ioc) s = ioc.actualName(s);
8919
+ }
8920
+ // If equation already exists, return its modifier.
8722
8921
  const id = UI.nameToID(s);
8723
8922
  if(this.modifiers.hasOwnProperty(id)) return this.modifiers[id];
8724
- // New equation identifier must not equal some entity ID
8923
+ // New equation identifier must not equal some entity ID.
8725
8924
  const obj = MODEL.objectByName(s);
8726
8925
  if(obj) {
8727
- // NOTE: also pass selector, or warning will display dataset name
8926
+ // NOTE: Also pass selector, or warning will display dataset name.
8728
8927
  UI.warningEntityExists(obj);
8729
8928
  return null;
8730
8929
  }
8731
8930
  } else {
8732
8931
  // Standard dataset modifier selectors are much more restricted, but
8733
- // to be user-friendly, special chars are removed automatically
8932
+ // to be user-friendly, special chars are removed automatically.
8734
8933
  s = s.replace(/[^a-zA-Z0-9\+\-\%\_\*\?]/g, '');
8735
8934
  let msg = '';
8736
8935
  if(s !== selector) msg = UI.WARNING.SELECTOR_SYNTAX;
8737
- // A selector can only contain 1 star
8936
+ // A selector can only contain 1 star.
8738
8937
  if(s.indexOf('*') !== s.lastIndexOf('*')) msg = UI.WARNING.SINGLE_WILDCARD;
8739
8938
  if(msg) {
8740
8939
  UI.warn(msg);
@@ -8745,12 +8944,12 @@ class Dataset {
8745
8944
  UI.warn(UI.WARNING.INVALID_SELECTOR);
8746
8945
  return null;
8747
8946
  }
8748
- // Then add a dataset modifier to this dataset
8947
+ // Then add a dataset modifier to this dataset.
8749
8948
  const id = UI.nameToID(s);
8750
8949
  if(!this.modifiers.hasOwnProperty(id)) {
8751
8950
  this.modifiers[id] = new DatasetModifier(this, s);
8752
8951
  }
8753
- // Finally, initialize it when the XML node argument is defined
8952
+ // Finally, initialize it when the XML node argument is defined.
8754
8953
  if(node) this.modifiers[id].initFromXML(node);
8755
8954
  return this.modifiers[id];
8756
8955
  }
@@ -8774,7 +8973,7 @@ class Dataset {
8774
8973
  for(let i = 0; i < sl.length; i++) {
8775
8974
  ml.push(this.modifiers[sl[i]].asXML);
8776
8975
  }
8777
- // NOTE: "black-boxed" datasets are stored anonymously without comments
8976
+ // NOTE: "black-boxed" datasets are stored anonymously without comments.
8778
8977
  const id = UI.nameToID(n);
8779
8978
  if(MODEL.black_box_entities.hasOwnProperty(id)) {
8780
8979
  n = MODEL.black_box_entities[id];
@@ -8806,7 +9005,7 @@ class Dataset {
8806
9005
  this.periodic = nodeParameterValue(node, 'periodic') === '1';
8807
9006
  this.array = nodeParameterValue(node, 'array') === '1';
8808
9007
  this.black_box = nodeParameterValue(node, 'black-box') === '1';
8809
- // NOTE: array-type datasets are by definition input => not an outcome
9008
+ // NOTE: Array-type datasets are by definition input => not an outcome.
8810
9009
  if(!this.array) this.outcome = nodeParameterValue(node, 'outcome') === '1';
8811
9010
  this.url = xmlDecoded(nodeContentByTag(node, 'url'));
8812
9011
  if(this.url) {
@@ -8832,10 +9031,10 @@ class Dataset {
8832
9031
  }
8833
9032
 
8834
9033
  rename(name, notify=true) {
8835
- // Change the name of this dataset
9034
+ // Change the name of this dataset.
8836
9035
  // When `notify` is FALSE, notifications are suppressed while the
8837
- // number of affected datasets and expressions are counted
8838
- // NOTE: prevent renaming the equations dataset (just in case...)
9036
+ // number of affected datasets and expressions are counted.
9037
+ // NOTE: Prevent renaming the equations dataset (just in case).
8839
9038
  if(this === MODEL.equations_dataset) return;
8840
9039
  name = UI.cleanName(name);
8841
9040
  if(!UI.validName(name)) {
@@ -8859,31 +9058,31 @@ class Dataset {
8859
9058
  }
8860
9059
 
8861
9060
  resetExpressions() {
8862
- // Recalculate vector to adjust to model time scale and run length
9061
+ // Recalculate vector to adjust to model time scale and run length.
8863
9062
  this.computeVector();
8864
- // Reset all modifier expressions
9063
+ // Reset all modifier expressions.
8865
9064
  for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
8866
- // NOTE: "empty" expressions for modifiers default to dataset default
9065
+ // NOTE: "empty" expressions for modifiers default to dataset default.
8867
9066
  this.modifiers[m].expression.reset(this.defaultValue);
8868
9067
  this.modifiers[m].expression_cache = {};
8869
9068
  }
8870
9069
  }
8871
9070
 
8872
9071
  compileExpressions() {
8873
- // Recompile all modifier expressions
9072
+ // Recompile all modifier expressions.
8874
9073
  for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
8875
9074
  this.modifiers[m].expression.compile();
8876
9075
  }
8877
9076
  }
8878
9077
 
8879
9078
  differences(ds) {
8880
- // Return "dictionary" of differences, or NULL if none
9079
+ // Return "dictionary" of differences, or NULL if none.
8881
9080
  const d = differences(this, ds, UI.MC.DATASET_PROPS);
8882
- // Check for differences in data
9081
+ // Check for differences in data.
8883
9082
  if(this.dataString !== ds.dataString) {
8884
9083
  d.data = {A: this.statisticsAsString, B: ds.statisticsAsString};
8885
9084
  }
8886
- // Check for differences in modifiers
9085
+ // Check for differences in modifiers.
8887
9086
  const mdiff = {};
8888
9087
  for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
8889
9088
  const
@@ -8902,7 +9101,7 @@ class Dataset {
8902
9101
  mdiff[m] = [UI.MC.DELETED, dsm.selector, dsm.expression.text];
8903
9102
  }
8904
9103
  }
8905
- // Only add modifiers property if differences were detected
9104
+ // Only add modifiers property if differences were detected.
8906
9105
  if(Object.keys(mdiff).length > 0) d.modifiers = mdiff;
8907
9106
  if(Object.keys(d).length > 0) return d;
8908
9107
  return null;
@@ -8911,7 +9110,7 @@ class Dataset {
8911
9110
  } // END of class Dataset
8912
9111
 
8913
9112
 
8914
- // CLASS ChartVariable defines properties of chart time series
9113
+ // CLASS ChartVariable defines properties of chart time series.
8915
9114
  class ChartVariable {
8916
9115
  constructor(c) {
8917
9116
  this.chart = c;
@@ -8941,15 +9140,33 @@ class ChartVariable {
8941
9140
  }
8942
9141
 
8943
9142
  get displayName() {
8944
- // Returns the name of the Linny-R entity and its attribute, followed
8945
- // by its scale factor unless it equals 1 (no scaling).
9143
+ // Returns the display name for this variable. This is the name of
9144
+ // the Linny-R entity and its attribute, followed by its scale factor
9145
+ // unless it equals 1 (no scaling).
8946
9146
  const sf = (this.scale_factor === 1 ? '' :
8947
9147
  ` (x${VM.sig4Dig(this.scale_factor)})`);
8948
- //Display name of equation is just the equations dataset selector.
9148
+ // Display name of equation is just the equations dataset selector.
8949
9149
  if(this.object instanceof DatasetModifier) {
8950
9150
  let eqn = this.object.selector;
9151
+ // If for this variable the `wildcard_index` property has been set,
9152
+ // this indicates that it is a Wildcard selector or a method, and
9153
+ // that the specified result vector should be used.
8951
9154
  if(this.wildcard_index !== false) {
8952
- eqn = eqn.replace('??', this.wildcard_index);
9155
+ // NOTE: A wildcard index (a number) can also indicate that this
9156
+ // variable is a method, so check for a leading colon.
9157
+ if(eqn.startsWith(':')) {
9158
+ // For methods, use "entity name or prefix: method" as variable
9159
+ // name, so first get the method object prefix, expand it if
9160
+ // it identifies a specific model entity, and then append the
9161
+ // method name (leading colon replaced by the prefixer ": ").
9162
+ const
9163
+ mop = this.object.expression.method_object_list[this.wildcard_index],
9164
+ obj = MODEL.objectByID(mop);
9165
+ eqn = (obj ? obj.displayName : (mop || '')) +
9166
+ UI.PREFIXER + eqn.substring(1);
9167
+ } else {
9168
+ eqn = eqn.replace('??', this.wildcard_index);
9169
+ }
8953
9170
  }
8954
9171
  return eqn + sf;
8955
9172
  }
@@ -9319,7 +9536,7 @@ class Chart {
9319
9536
 
9320
9537
  addVariable(n, a) {
9321
9538
  // Adds variable [entity name `n`|attribute `a`] to the chart unless it
9322
- // is already in the variable list
9539
+ // is already in the variable list.
9323
9540
  let dn = n + UI.OA_SEPARATOR + a;
9324
9541
  // Adapt display name for special cases
9325
9542
  if(n === UI.EQUATIONS_DATASET_NAME) {
@@ -9339,31 +9556,26 @@ class Chart {
9339
9556
  return null;
9340
9557
  }
9341
9558
  const eq = obj instanceof DatasetModifier;
9342
- if(!eq) {
9343
- // No equation and no attribute specified? Then assume default.
9344
- if(a === '') a = obj.defaultAttribute;
9345
- } else if(n.indexOf('??') >= 0) {
9346
- // Special case: for wildcard equations, add dummy variables
9347
- // for each vector in the wildcard vector set of the equation
9559
+ // No equation and no attribute specified? Then assume default.
9560
+ if(!eq && a === '') a = obj.defaultAttribute;
9561
+ if(eq && (n.indexOf('??') >= 0 || obj.expression.isMethod)) {
9562
+ // Special case: for wildcard equations and methods, add dummy
9563
+ // variables for each vector in the wildcard vector set of the
9348
9564
  // expression.
9349
9565
  const
9350
- vlist = [],
9351
9566
  clr = this.nextAvailableDefaultColor,
9352
9567
  indices = Object.keys(obj.expression.wildcard_vectors);
9353
9568
  for(let i = 0; i < indices.length; i++) {
9354
9569
  const v = new ChartVariable(this);
9355
- v.setProperties(MODEL.equations_dataset, dn, false, clr);
9570
+ v.setProperties(obj, dn, false, clr);
9356
9571
  v.wildcard_index = parseInt(indices[i]);
9357
9572
  this.variables.push(v);
9358
- vlist.push(v);
9359
9573
  }
9360
- return vlist;
9361
- // FALL-THROUGH: Equation name has no wildcard => use the equation
9362
- // object (= dataset modifier) as `obj`.
9574
+ } else {
9575
+ const v = new ChartVariable(this);
9576
+ v.setProperties(obj, a, false, this.nextAvailableDefaultColor, 1, 1);
9577
+ this.variables.push(v);
9363
9578
  }
9364
- const v = new ChartVariable(this);
9365
- v.setProperties(obj, a, false, this.nextAvailableDefaultColor, 1, 1);
9366
- this.variables.push(v);
9367
9579
  return this.variables.length - 1;
9368
9580
  }
9369
9581
 
@@ -10767,8 +10979,13 @@ class ExperimentRun {
10767
10979
  }
10768
10980
  // Calculate number of vectors/outcomes/equations to store.
10769
10981
  this.oc_list = MODEL.outcomes;
10770
- // NOTE: All equations are also considered to be outcomes.
10771
- this.eq_list = Object.keys(MODEL.equations_dataset.modifiers);
10982
+ // NOTE: All equations are also considered to be outcomes EXCEPT
10983
+ // methods (selectors starting with a colon).
10984
+ this.eq_list = [];
10985
+ const eml = Object.keys(MODEL.equations_dataset.modifiers);
10986
+ for(let i = 0; i < eml.length; i++) {
10987
+ if(!eml[i].startsWith(':')) this.eq_list.push(eml[i]);
10988
+ }
10772
10989
  const
10773
10990
  cv = this.experiment.variables.length,
10774
10991
  oc = this.oc_list.length,
@@ -10807,7 +11024,7 @@ class ExperimentRun {
10807
11024
  // NOTE: This stores results only for "active" selectors (current run).
10808
11025
  this.results.push(new ExperimentRunResult(this, MODEL.outcomes[oi]));
10809
11026
  this.step++;
10810
- UI.setProgressNeedle(this.step / this.steps);
11027
+ UI.setProgressNeedle(this.step / this.steps, '#d00080');
10811
11028
  setTimeout((x) => x.addOutcomeResults(oi + 1), 0, this);
10812
11029
  } else {
10813
11030
  this.addEquationResults(0);
@@ -10822,7 +11039,7 @@ class ExperimentRun {
10822
11039
  this.results.push(
10823
11040
  new ExperimentRunResult(this, MODEL.equations_dataset, k));
10824
11041
  this.step++;
10825
- UI.setProgressNeedle(this.step / this.steps);
11042
+ UI.setProgressNeedle(this.step / this.steps, '#2000d0');
10826
11043
  setTimeout((x) => x.addEquationResults(ei + 1), 0, this);
10827
11044
  } else {
10828
11045
  // Register when this result was stored.
@@ -11544,18 +11761,18 @@ class Experiment {
11544
11761
  return i;
11545
11762
  }
11546
11763
  }
11547
- // NOTE: variables are stored first, outcomes second, equations last,
11764
+ // NOTE: Variables are stored first, outcomes second, equations last,
11548
11765
  // *while numbering continues*, hence index is position in unsorted
11549
11766
  // variable list, or position in outcome list, where this method
11550
- // takes into account that experiments store ONE modifier expression per
11551
- // outcome, and ALL equations
11767
+ // takes into account that experiments store ONE modifier expression
11768
+ // per outcome, and ALL equations except methods.
11552
11769
  const oci = MODEL.outcomeNames.indexOf(dn);
11553
11770
  if(oci >= 0) return oci + this.variables.length;
11554
11771
  return -1;
11555
11772
  }
11556
11773
 
11557
11774
  differences(x) {
11558
- // Return "dictionary" of differences, or NULL if none
11775
+ // Return "dictionary" of differences, or NULL if none.
11559
11776
  const d = differences(this, x, UI.MC.EXPERIMENT_PROPS);
11560
11777
  /*
11561
11778
  @@TO DO: add diffs for array properties: