linny-r 1.6.0 → 1.6.2

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.
@@ -1561,13 +1561,21 @@ class LinnyRModel {
1561
1561
  //
1562
1562
 
1563
1563
  alignToGrid() {
1564
- // Move all positioned model elements to the nearest grid point
1564
+ // Move all positioned model elements to the nearest grid point.
1565
1565
  if(!this.align_to_grid) return;
1566
1566
  let move = false;
1567
1567
  const fc = this.focal_cluster;
1568
1568
  // NOTE: Do not align notes to the grid. This will permit more
1569
1569
  // precise positioning, while aligning will not improve the layout
1570
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: nbn.x, oldy: nbn.y} : null);
1578
+ }
1571
1579
  for(let i = 0; i < fc.processes.length; i++) {
1572
1580
  move = fc.processes[i].alignToGrid() || move;
1573
1581
  }
@@ -1577,11 +1585,25 @@ class LinnyRModel {
1577
1585
  for(let i = 0; i < fc.sub_clusters.length; i++) {
1578
1586
  move = fc.sub_clusters[i].alignToGrid() || move;
1579
1587
  }
1580
- 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
+ }
1581
1603
  }
1582
1604
 
1583
1605
  translateGraph(dx, dy) {
1584
- // Move all entities in the focal cluster by (dx, dy) pixels
1606
+ // Move all entities in the focal cluster by (dx, dy) pixels.
1585
1607
  if(!dx && !dy) return;
1586
1608
  const fc = this.focal_cluster;
1587
1609
  for(let i = 0; i < fc.processes.length; i++) {
@@ -1600,9 +1622,9 @@ class LinnyRModel {
1600
1622
  fc.notes[i].x += dx;
1601
1623
  fc.notes[i].y += dy;
1602
1624
  }
1603
- // NOTE: force drawing, because SVG must immediately be downloadable
1625
+ // NOTE: force drawing, because SVG must immediately be downloadable.
1604
1626
  UI.drawDiagram(this);
1605
- // 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.
1606
1628
  if(UI.dragged_node) UNDO_STACK.addOffset(dx, dy);
1607
1629
  }
1608
1630
 
@@ -1748,8 +1770,8 @@ class LinnyRModel {
1748
1770
  miny = Math.min(miny, obj.y - obj.height / 2);
1749
1771
  }
1750
1772
  }
1751
- // Translate entire graph if some elements are above and/or left of the
1752
- // paper edge
1773
+ // Translate entire graph if some elements are above and/or left of
1774
+ // the paper edge.
1753
1775
  if(minx < 0 || miny < 0) {
1754
1776
  // NOTE: limit translation to 5 pixels to prevent "run-away effect"
1755
1777
  this.translateGraph(Math.min(5, -minx), Math.min(5, -miny));
@@ -2186,8 +2208,8 @@ class LinnyRModel {
2186
2208
  }
2187
2209
 
2188
2210
  deleteSelection() {
2189
- // Removes all selected nodes (with their associated links and constraints)
2190
- // and selected links
2211
+ // Remove all selected nodes (with their associated links and constraints)
2212
+ // and selected links.
2191
2213
  // NOTE: This method implements the DELETE action, and hence should be
2192
2214
  // undoable. The UndoEdit is created by the calling routine; the methods
2193
2215
  // that actually delete model elements append their XML to the XML attribute
@@ -2195,7 +2217,7 @@ class LinnyRModel {
2195
2217
  let obj,
2196
2218
  fc = this.focal_cluster;
2197
2219
  // Update the documentation manager (GUI only) if selection contains the
2198
- // current entity
2220
+ // current entity.
2199
2221
  if(DOCUMENTATION_MANAGER) DOCUMENTATION_MANAGER.clearEntity(this.selection);
2200
2222
  // First delete links and constraints.
2201
2223
  for(let i = this.selection.length - 1; i >= 0; i--) {
@@ -3222,6 +3244,29 @@ class LinnyRModel {
3222
3244
  sl.push(this.datasets[obj].displayName, this.datasets[obj].comments);
3223
3245
  }
3224
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
+ }
3225
3270
  sl.push('_____Charts');
3226
3271
  for(let i = 0; i < this.charts.length; i++) {
3227
3272
  sl.push(this.charts[i].title, this.charts[i].comments);
@@ -4817,7 +4862,7 @@ class ObjectWithXYWH {
4817
4862
 
4818
4863
  alignToGrid() {
4819
4864
  // Align this object to the grid, and return TRUE if this involved
4820
- // a move
4865
+ // a move.
4821
4866
  const
4822
4867
  ox = this.x,
4823
4868
  oy = this.y,
@@ -5113,7 +5158,7 @@ class Note extends ObjectWithXYWH {
5113
5158
  // If attribute omitted, use default attribute of entity type.
5114
5159
  const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
5115
5160
  let val = null;
5116
- // NOTE: for datasets, use the active modifier if no attribute.
5161
+ // NOTE: For datasets, use the active modifier if no attribute.
5117
5162
  if(!attr && obj instanceof Dataset) {
5118
5163
  val = obj.activeModifierExpression;
5119
5164
  } else {
@@ -6139,8 +6184,8 @@ class Cluster extends NodeBox {
6139
6184
  }
6140
6185
 
6141
6186
  addProductPosition(p, x=null, y=null) {
6142
- // Add a product position for product `p` to this cluster unless such pp
6143
- // 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.
6144
6189
  let pp = this.indexOfProduct(p);
6145
6190
  if(pp >= 0) {
6146
6191
  pp = this.product_positions[pp];
@@ -6158,7 +6203,8 @@ class Cluster extends NodeBox {
6158
6203
  }
6159
6204
 
6160
6205
  containsProduct(p) {
6161
- // 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.
6162
6208
  if(this.indexOfProduct(p) >= 0) return this;
6163
6209
  for(let i = 0; i < this.sub_clusters.length; i++) {
6164
6210
  if(this.sub_clusters[i].containsProduct(p)) {
@@ -7848,13 +7894,11 @@ class Product extends Node {
7848
7894
  }
7849
7895
 
7850
7896
  get isConstant() {
7851
- // Return TRUE if this product is data, has no links to processes,
7852
- // is not an actor cash flow, and has set LB = UB
7897
+ // Return TRUE if this product is data, is not an actor cash flow,
7898
+ // has no ingoing links, has outgoing links ONLY to data objects,
7899
+ // and has set LB = UB.
7853
7900
  if(!this.is_data || this.name.startsWith('$') ||
7854
- !this.allOutputsAreData) return false;
7855
- for(let i = 0; i < this.inputs.length; i++) {
7856
- if(this.inputs[i].from_node instanceof Process) return false;
7857
- }
7901
+ this.inputs.length || !this.allOutputsAreData) return false;
7858
7902
  return (this.equal_bounds && this.lower_bound.defined);
7859
7903
  }
7860
7904
 
@@ -8712,8 +8756,8 @@ class Dataset {
8712
8756
  return d.join(';');
8713
8757
  }
8714
8758
 
8715
- // Returns a string denoting the properties of this dataset.
8716
8759
  get propertiesString() {
8760
+ // Return a string denoting the properties of this dataset.
8717
8761
  if(this.data.length === 0) return '';
8718
8762
  let time_prop;
8719
8763
  if(this.array) {
@@ -8724,12 +8768,12 @@ class Dataset {
8724
8768
  DATASET_MANAGER.method_symbols[
8725
8769
  DATASET_MANAGER.methods.indexOf(this.method)]].join('');
8726
8770
  }
8727
- // Circular arrow symbolizes "repeating"
8771
+ // Circular arrow symbolizes "repeating".
8728
8772
  return '&nbsp;(' + time_prop + (this.periodic ? '&nbsp;\u21BB' : '') + ')';
8729
8773
  }
8730
8774
 
8731
8775
  unpackDataString(str) {
8732
- // Converts semicolon-separated data to a numeric array.
8776
+ // Convert semicolon-separated data to a numeric array.
8733
8777
  this.data.length = 0;
8734
8778
  if(str) {
8735
8779
  const numbers = str.split(';');
@@ -8742,12 +8786,12 @@ class Dataset {
8742
8786
  }
8743
8787
 
8744
8788
  computeVector() {
8745
- // Converts data to a vector on the model's time scale, i.e., 1 time step
8746
- // lasting one unit on the model time scale
8789
+ // Convert data to a vector on the time scale of the model, i.e.,
8790
+ // 1 time step lasting one unit on the model time scale.
8747
8791
 
8748
- // NOTE: since 9 October 2021, a dataset can also be defined as an "array",
8749
- // which differs from a time series in that the vector is filled with the
8750
- // data values "as is" to permit accessing a specific value at index #
8792
+ // NOTE: A dataset can also be defined as an "array", which differs
8793
+ // from a time series in that the vector is filled with the data values
8794
+ // "as is" to permit accessing a specific value at index #.
8751
8795
  if(this.array) {
8752
8796
  this.vector = this.data.slice();
8753
8797
  return;
@@ -8755,16 +8799,16 @@ class Dataset {
8755
8799
  // Like all vectors, vector[0] corresponds to initial value, and vector[1]
8756
8800
  // to the model setting "Optimize from step t=..."
8757
8801
  // NOTES:
8758
- // (1) the first number of a datasets time series is ALWAYS assumed to
8802
+ // (1) The first number of a datasets time series is ALWAYS assumed to
8759
8803
  // correspond to t=1, whereas the simulation may be set to start later!
8760
- // (2) model run length includes 1 look-ahead period
8804
+ // (2) Model run length includes 1 look-ahead period.
8761
8805
  VM.scaleDataToVector(this.data, this.vector, this.timeStepDuration,
8762
8806
  MODEL.timeStepDuration, MODEL.runLength, MODEL.start_period,
8763
8807
  this.defaultValue, this.periodic, this.method);
8764
8808
  }
8765
8809
 
8766
8810
  computeStatistics() {
8767
- // Computes descriptive statistics for data (NOT vector!).
8811
+ // Compute descriptive statistics for data (NOT vector!).
8768
8812
  if(this.data.length === 0) {
8769
8813
  this.min = VM.UNDEFINED;
8770
8814
  this.max = VM.UNDEFINED;
@@ -8789,29 +8833,48 @@ class Dataset {
8789
8833
  }
8790
8834
 
8791
8835
  get statisticsAsString() {
8792
- // Returns descriptive statistics in human-readable form
8836
+ // Return descriptive statistics in human-readable form.
8793
8837
  let s = 'N = ' + this.data.length;
8794
8838
  if(N > 0) {
8795
- s += ', range = [' + VM.sig4Dig(this.min) + ', ' + VM.sig4Dig(this.max) +
8796
- ', mean = ' + VM.sig4Dig(this.mean) + ', s.d. = ' +
8797
- VM.sig4Dig(this.standard_deviation);
8839
+ s += [', range = [', VM.sig4Dig(this.min), ', ', VM.sig4Dig(this.max),
8840
+ '], mean = ', VM.sig4Dig(this.mean), ', s.d. = ',
8841
+ VM.sig4Dig(this.standard_deviation)].join('');
8798
8842
  }
8799
8843
  return s;
8800
8844
  }
8801
8845
 
8802
8846
  attributeValue(a) {
8803
- // Returns the computed result for attribute `a`.
8804
- // NOTE: Datasets have ONE attribute (their vector) denoted by the empty
8805
- // string; all other "attributes" should be modifier selectors, and
8806
- // their value should be obtained using attributeExpression (see below).
8807
- if(a === '') return this.vector;
8808
- return null;
8847
+ // Return the computed result for attribute `a`.
8848
+ // NOTE: Datasets have ONE attribute (their vector) denoted by the
8849
+ // dot ".". All other "attributes" should be modifier selectors,
8850
+ // and their value should be obtained using `attributeExpression(a)`.
8851
+ // The empty string denotes "use default", which may have been set
8852
+ // by the modeler, or may follow from the active combination of a
8853
+ // running experiment.
8854
+ if(a === '') {
8855
+ const x = this.activeModifierExpression;
8856
+ if(x instanceof Expression) {
8857
+ x.compute(0);
8858
+ // Ensure that for dynamic modifier expressions the vector is
8859
+ // fully computed.
8860
+ if(!x.isStatic) {
8861
+ const nt = MODEL.end_period - MODEL.start_period + 1;
8862
+ for(let t = 1; t <= nt; t++) x.result(t);
8863
+ }
8864
+ return x.vector;
8865
+ }
8866
+ // No modifier expression? Then return the dataset vector.
8867
+ return this.vector;
8868
+ }
8869
+ if(a === '.') return this.vector;
8870
+ // Fall-through: return the default value of this dataset.
8871
+ return this.defaultValue;
8809
8872
  }
8810
8873
 
8811
8874
  attributeExpression(a) {
8812
- // Returns expression for selector `a` (also considering wildcard
8875
+ // Return the expression for selector `a` (also considering wildcard
8813
8876
  // modifiers), or NULL if no such selector exists.
8814
- // NOTE: selectors no longer are case-sensitive.
8877
+ // NOTE: Selectors no longer are case-sensitive.
8815
8878
  if(a) {
8816
8879
  const mm = this.matchingModifiers([a]);
8817
8880
  if(mm.length > 0) return mm[0].expression;
@@ -8822,29 +8885,29 @@ class Dataset {
8822
8885
  get activeModifierExpression() {
8823
8886
  if(MODEL.running_experiment) {
8824
8887
  // If an experiment is running, check if dataset modifiers match the
8825
- // combination of selectors for the active run
8888
+ // combination of selectors for the active run.
8826
8889
  const mm = this.matchingModifiers(MODEL.running_experiment.activeCombination);
8827
- // If so, use the first match
8890
+ // If so, use the first match.
8828
8891
  if(mm.length > 0) return mm[0].expression;
8829
8892
  }
8830
8893
  if(this.default_selector) {
8831
- // If no experiment (so "normal" run), use default selector if specified
8894
+ // If no experiment (so "normal" run), use default selector if specified.
8832
8895
  const dm = this.modifiers[UI.nameToID(this.default_selector)];
8833
8896
  if(dm) return dm.expression;
8834
- // Exception should never occur, but check anyway and log it
8897
+ // Exception should never occur, but check anyway and log it.
8835
8898
  console.log('WARNING: Dataset "' + this.name +
8836
8899
  `" has no default selector "${this.default_selector}"`, this.modifiers);
8837
8900
  }
8838
- // Fall-through: return vector instead of expression
8901
+ // Fall-through: return the dataset vector.
8839
8902
  return this.vector;
8840
8903
  }
8841
8904
 
8842
8905
  addModifier(selector, node=null, ioc=null) {
8843
8906
  let s = selector;
8844
- // Firstly, sanitize the selector
8907
+ // First sanitize the selector.
8845
8908
  if(this === MODEL.equations_dataset) {
8846
8909
  // Equation identifiers cannot contain characters that have special
8847
- // meaning in a variable identifier
8910
+ // meaning in a variable identifier.
8848
8911
  s = s.replace(/[\*\|\[\]\{\}\@\#]/g, '');
8849
8912
  if(s !== selector) {
8850
8913
  UI.warn('Equation name cannot contain [, ], {, }, |, @, # or *');
@@ -8868,26 +8931,26 @@ class Dataset {
8868
8931
  return null;
8869
8932
  }
8870
8933
  } else {
8871
- // Prefix it when the IO context argument is defined
8934
+ // Prefix it when the IO context argument is defined.
8872
8935
  if(ioc) s = ioc.actualName(s);
8873
8936
  }
8874
- // If equation already exists, return its modifier
8937
+ // If equation already exists, return its modifier.
8875
8938
  const id = UI.nameToID(s);
8876
8939
  if(this.modifiers.hasOwnProperty(id)) return this.modifiers[id];
8877
- // New equation identifier must not equal some entity ID
8940
+ // New equation identifier must not equal some entity ID.
8878
8941
  const obj = MODEL.objectByName(s);
8879
8942
  if(obj) {
8880
- // NOTE: also pass selector, or warning will display dataset name
8943
+ // NOTE: Also pass selector, or warning will display dataset name.
8881
8944
  UI.warningEntityExists(obj);
8882
8945
  return null;
8883
8946
  }
8884
8947
  } else {
8885
8948
  // Standard dataset modifier selectors are much more restricted, but
8886
- // to be user-friendly, special chars are removed automatically
8949
+ // to be user-friendly, special chars are removed automatically.
8887
8950
  s = s.replace(/[^a-zA-Z0-9\+\-\%\_\*\?]/g, '');
8888
8951
  let msg = '';
8889
8952
  if(s !== selector) msg = UI.WARNING.SELECTOR_SYNTAX;
8890
- // A selector can only contain 1 star
8953
+ // A selector can only contain 1 star.
8891
8954
  if(s.indexOf('*') !== s.lastIndexOf('*')) msg = UI.WARNING.SINGLE_WILDCARD;
8892
8955
  if(msg) {
8893
8956
  UI.warn(msg);
@@ -8898,12 +8961,12 @@ class Dataset {
8898
8961
  UI.warn(UI.WARNING.INVALID_SELECTOR);
8899
8962
  return null;
8900
8963
  }
8901
- // Then add a dataset modifier to this dataset
8964
+ // Then add a dataset modifier to this dataset.
8902
8965
  const id = UI.nameToID(s);
8903
8966
  if(!this.modifiers.hasOwnProperty(id)) {
8904
8967
  this.modifiers[id] = new DatasetModifier(this, s);
8905
8968
  }
8906
- // Finally, initialize it when the XML node argument is defined
8969
+ // Finally, initialize it when the XML node argument is defined.
8907
8970
  if(node) this.modifiers[id].initFromXML(node);
8908
8971
  return this.modifiers[id];
8909
8972
  }
@@ -8927,7 +8990,7 @@ class Dataset {
8927
8990
  for(let i = 0; i < sl.length; i++) {
8928
8991
  ml.push(this.modifiers[sl[i]].asXML);
8929
8992
  }
8930
- // NOTE: "black-boxed" datasets are stored anonymously without comments
8993
+ // NOTE: "black-boxed" datasets are stored anonymously without comments.
8931
8994
  const id = UI.nameToID(n);
8932
8995
  if(MODEL.black_box_entities.hasOwnProperty(id)) {
8933
8996
  n = MODEL.black_box_entities[id];
@@ -8959,7 +9022,7 @@ class Dataset {
8959
9022
  this.periodic = nodeParameterValue(node, 'periodic') === '1';
8960
9023
  this.array = nodeParameterValue(node, 'array') === '1';
8961
9024
  this.black_box = nodeParameterValue(node, 'black-box') === '1';
8962
- // NOTE: array-type datasets are by definition input => not an outcome
9025
+ // NOTE: Array-type datasets are by definition input => not an outcome.
8963
9026
  if(!this.array) this.outcome = nodeParameterValue(node, 'outcome') === '1';
8964
9027
  this.url = xmlDecoded(nodeContentByTag(node, 'url'));
8965
9028
  if(this.url) {
@@ -8985,10 +9048,10 @@ class Dataset {
8985
9048
  }
8986
9049
 
8987
9050
  rename(name, notify=true) {
8988
- // Change the name of this dataset
9051
+ // Change the name of this dataset.
8989
9052
  // When `notify` is FALSE, notifications are suppressed while the
8990
- // number of affected datasets and expressions are counted
8991
- // NOTE: prevent renaming the equations dataset (just in case...)
9053
+ // number of affected datasets and expressions are counted.
9054
+ // NOTE: Prevent renaming the equations dataset (just in case).
8992
9055
  if(this === MODEL.equations_dataset) return;
8993
9056
  name = UI.cleanName(name);
8994
9057
  if(!UI.validName(name)) {
@@ -9012,31 +9075,31 @@ class Dataset {
9012
9075
  }
9013
9076
 
9014
9077
  resetExpressions() {
9015
- // Recalculate vector to adjust to model time scale and run length
9078
+ // Recalculate vector to adjust to model time scale and run length.
9016
9079
  this.computeVector();
9017
- // Reset all modifier expressions
9080
+ // Reset all modifier expressions.
9018
9081
  for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
9019
- // NOTE: "empty" expressions for modifiers default to dataset default
9082
+ // NOTE: "empty" expressions for modifiers default to dataset default.
9020
9083
  this.modifiers[m].expression.reset(this.defaultValue);
9021
9084
  this.modifiers[m].expression_cache = {};
9022
9085
  }
9023
9086
  }
9024
9087
 
9025
9088
  compileExpressions() {
9026
- // Recompile all modifier expressions
9089
+ // Recompile all modifier expressions.
9027
9090
  for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
9028
9091
  this.modifiers[m].expression.compile();
9029
9092
  }
9030
9093
  }
9031
9094
 
9032
9095
  differences(ds) {
9033
- // Return "dictionary" of differences, or NULL if none
9096
+ // Return "dictionary" of differences, or NULL if none.
9034
9097
  const d = differences(this, ds, UI.MC.DATASET_PROPS);
9035
- // Check for differences in data
9098
+ // Check for differences in data.
9036
9099
  if(this.dataString !== ds.dataString) {
9037
9100
  d.data = {A: this.statisticsAsString, B: ds.statisticsAsString};
9038
9101
  }
9039
- // Check for differences in modifiers
9102
+ // Check for differences in modifiers.
9040
9103
  const mdiff = {};
9041
9104
  for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
9042
9105
  const
@@ -9055,7 +9118,7 @@ class Dataset {
9055
9118
  mdiff[m] = [UI.MC.DELETED, dsm.selector, dsm.expression.text];
9056
9119
  }
9057
9120
  }
9058
- // Only add modifiers property if differences were detected
9121
+ // Only add modifiers property if differences were detected.
9059
9122
  if(Object.keys(mdiff).length > 0) d.modifiers = mdiff;
9060
9123
  if(Object.keys(d).length > 0) return d;
9061
9124
  return null;
@@ -9064,7 +9127,7 @@ class Dataset {
9064
9127
  } // END of class Dataset
9065
9128
 
9066
9129
 
9067
- // CLASS ChartVariable defines properties of chart time series
9130
+ // CLASS ChartVariable defines properties of chart time series.
9068
9131
  class ChartVariable {
9069
9132
  constructor(c) {
9070
9133
  this.chart = c;
@@ -9244,23 +9307,11 @@ class ChartVariable {
9244
9307
  t_end = tsteps;
9245
9308
  } else {
9246
9309
  // Get the variable's own value (number, vector or expression)
9247
- // Special case: when an experiment is running, variables that
9248
- // depict a dataset with no explicit modifier must recompute the
9249
- // vector using the current experiment run combination.
9250
- if(MODEL.running_experiment &&
9251
- this.object instanceof Dataset && !this.attribute) {
9252
- // Check if dataset modifiers match the combination of selectors
9253
- // for the active run.
9254
- const mm = this.object.matchingModifiers(
9255
- MODEL.running_experiment.activeCombination);
9256
- // If so, use the first (the list should contain at most 1 selector)
9257
- // to select the modifier expression; otherwise, use the unmodified
9258
- // vector of the dataset
9259
- if(mm.length > 0) {
9260
- av = mm[0].expression;
9261
- } else {
9262
- av = this.object.vector;
9263
- }
9310
+ if(this.object instanceof Dataset && !this.attribute) {
9311
+ // Special case: Variables that depict a dataset with no explicit
9312
+ // modifier selector must recompute the vector using the current
9313
+ // experiment run combination or the default selector.
9314
+ av = this.object.activeModifierExpression;
9264
9315
  } else if(this.object instanceof DatasetModifier) {
9265
9316
  av = this.object.expression;
9266
9317
  } else {
@@ -11029,7 +11080,8 @@ class ExperimentRun {
11029
11080
  bm.messages = VM.messages[i];
11030
11081
  this.block_messages.push(bm);
11031
11082
  this.warning_count += bm.warningCount;
11032
- this.solver_seconds += bm.solver_secs;
11083
+ // NOTE: When set by the VM, `solver_secs` is a string.
11084
+ this.solver_seconds += parseFloat(bm.solver_secs);
11033
11085
  }
11034
11086
  }
11035
11087
 
@@ -491,7 +491,9 @@ function matchingNumber(m, s) {
491
491
  // Returns an integer value if string `m` matches selector pattern `s`
492
492
  // (where asterisks match 0 or more characters, and question marks 1
493
493
  // character) and the matching parts jointly denote an integer.
494
- let raw = s.replace(/\*/g, '(.*)').replace(/\?/g, '(.)'),
494
+ // NOTE: A "+" must be escaped, "*" and "?" must become groups.
495
+ let raw = s.replaceAll('+', '\+')
496
+ .replace(/\*/g, '(.*)').replace(/\?/g, '(.)'),
495
497
  match = m.match(new RegExp(`^${raw}$`)),
496
498
  n = '';
497
499
  if(match) {
@@ -601,6 +603,18 @@ function compareSelectors(s1, s2) {
601
603
  return 0;
602
604
  }
603
605
 
606
+ function compareCombinations(c1, c2) {
607
+ // Compare two selector lists.
608
+ const n = Math.min(c1.length, c2.length);
609
+ for(let i = 0; i < n; i++) {
610
+ const cs = compareSelectors(c1[i], c2[i]);
611
+ if(cs) return cs;
612
+ }
613
+ if(c1.length > l) return 1;
614
+ if(c2.length > l) return -1;
615
+ return 0;
616
+ }
617
+
604
618
  //
605
619
  // Functions that perform set-like operations on lists of string
606
620
  //
@@ -743,17 +757,28 @@ function xmlDecoded(str) {
743
757
  }
744
758
 
745
759
  function customizeXML(str) {
746
- // NOTE: this function can be customized to pre-process a model file,
760
+ // NOTE: This function can be customized to pre-process a model file,
747
761
  // for example to rename entities in one go -- USE WITH CARE!
748
- // First modify `str` -- by default, do nothing
749
-
762
+ // To prevent unintended customization, check whether the model name
763
+ // ends with "!!CUSTOMIZE". This check ensures that the modeler must
764
+ // first save the model with this text as the (end of the) model name
765
+ // and then load it again for the customization to be performed.
766
+ if(str.indexOf('!!CUSTOMIZE</name><author>') >= 0) {
767
+ // Modify `str` -- by default, do nothing, but typical modifications
768
+ // will replace RexEx patterns by other strings.
769
+
750
770
  /*
751
- if(str.indexOf('<version>1.4.') >= 0) {
752
- str = str.replace(/<url>NL\/(\w+)\.csv<\/url>/g, '<url></url>');
753
- }
771
+ const
772
+ re = /xyz/gi,
773
+ r = 'abc';
754
774
  */
755
775
 
756
- // Finally, return the modified string
776
+ // Trace the changes to the console.
777
+ console.log('Customizing:', re, r);
778
+ console.log('Matches:', str.match(re));
779
+ str = str.replace(re, r);
780
+ }
781
+ // Finally, return the modified string.
757
782
  return str;
758
783
  }
759
784