linny-r 1.6.1 → 1.6.3

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.
@@ -2312,17 +2312,18 @@ class Paper {
2312
2312
  }
2313
2313
  if(prod.hasBounds) {
2314
2314
  font_color = 'black';
2315
- // By default, "plain" factors having bounds are filled in silver
2315
+ // By default, "plain" factors having bounds are filled in silver.
2316
2316
  fill_color = this.palette.has_bounds;
2317
2317
  // Use relative distance to bounds so that 100000.1 is not shown
2318
- // as overflow, but 100.1 is
2318
+ // as overflow, but 100.1 is.
2319
2319
  let udif = this.relDif(l, ub),
2320
2320
  ldif = this.relDif(lb, l);
2321
- // Special case: for LB = 0, use the ON/OFF threshold
2321
+ // Special case: for LB = 0, use the ON/OFF threshold.
2322
2322
  if(Math.abs(lb) <= VM.SIG_DIF_LIMIT &&
2323
2323
  Math.abs(l) <= VM.ON_OFF_THRESHOLD) ldif = 0;
2324
2324
  if(MODEL.solved) {
2325
- // NOTE: use bright red and blue colors in case of "stock level out of bounds"
2325
+ // NOTE: Use bright red and blue colors in case of "stock level
2326
+ // out of bounds".
2326
2327
  if(ub < VM.PLUS_INFINITY && l < VM.UNDEFINED && udif > VM.SIG_DIF_LIMIT) {
2327
2328
  fill_color = this.palette.above_upper_bound;
2328
2329
  font_color = 'blue';
@@ -2332,51 +2333,52 @@ class Paper {
2332
2333
  } else if(l < VM.ERROR || l > VM.EXCEPTION) {
2333
2334
  font_color = this.palette.VM_error;
2334
2335
  } else if(l < VM.UNDEFINED) {
2335
- // Shades of green reflect whether level within bounds, where
2336
+ // Shades of green reflect whether level is within bounds, where
2336
2337
  // "sources" (negative level) and "sinks" (positive level) are
2337
- // shown as more reddish / bluish shades of green
2338
+ // shown as more reddish / bluish shades of green.
2338
2339
  if(l < -VM.ON_OFF_THRESHOLD) {
2339
2340
  fill_color = this.palette.neg_within_bounds;
2340
2341
  } else if(l > VM.ON_OFF_THRESHOLD) {
2341
2342
  fill_color = this.palette.pos_within_bounds;
2342
2343
  } else {
2343
- fill_color = this.palette.zero_within_bounds;
2344
+ fill_color = this.palette.zero_within_bounds;
2344
2345
  }
2345
2346
  if(ub - lb < VM.NEAR_ZERO) {
2347
+ // When LB = UB, fill completely in the color, but ...
2346
2348
  if(prod.isConstant && Math.abs(l) > VM.NEAR_ZERO) {
2347
- // Non-zero constants have less saturated shades
2349
+ // ... non-zero constants have less saturated shades.
2348
2350
  fill_color = (l < 0 ? this.palette.neg_constant :
2349
2351
  this.palette.pos_constant);
2350
2352
  }
2351
2353
  } else if(ub - l < VM.SIG_DIF_LIMIT) {
2352
- // Black font and darker fill color indicate "at upper bound"
2354
+ // Black font and darker fill color indicate "at upper bound".
2353
2355
  font_color = 'black';
2354
2356
  fill_color = (ub > 0 ? this.palette.at_pos_ub_fill :
2355
2357
  (ub < 0 ? this.palette.at_neg_ub_fill :
2356
2358
  this.palette.at_zero_ub_fill));
2357
2359
  at_bound = true;
2358
2360
  } else if (l - lb < VM.SIG_DIF_LIMIT) {
2359
- // Font and rim color indicate "at upper bound"
2361
+ // Font and rim color indicate "at lower bound".
2360
2362
  font_color = 'black';
2361
2363
  fill_color = (lb > 0 ? this.palette.at_pos_lb_fill :
2362
2364
  (lb < 0 ? this.palette.at_neg_lb_fill :
2363
2365
  this.palette.at_zero_lb_fill));
2364
2366
  at_bound = true;
2365
2367
  } else {
2366
- // set "partial fill" flag if not at lower bound and UB < INF
2368
+ // Set "partial fill" flag if not at lower bound and UB < INF.
2367
2369
  pf = ub < VM.PLUS_INFINITY;
2368
2370
  font_color = this.palette.within_bounds_font;
2369
2371
  }
2370
2372
  }
2371
2373
  } else if(ub - lb < VM.NEAR_ZERO) {
2372
- // Not solved but equal bounds => probably constants
2374
+ // Not solved but equal bounds => probably constants.
2373
2375
  if(prod.isConstant && Math.abs(ub) > VM.NEAR_ZERO) {
2374
- // Non-zero constants have less saturated shades
2376
+ // Non-zero constants have less saturated shades.
2375
2377
  fill_color = (ub < 0 ? this.palette.neg_constant :
2376
2378
  this.palette.pos_constant);
2377
2379
  }
2378
2380
  } else if(l < VM.UNDEFINED) {
2379
- // Different bounds and initial level set => partial fill
2381
+ // Different bounds and initial level set => partial fill.
2380
2382
  fill_color = this.palette.src_snk;
2381
2383
  pf = true;
2382
2384
  if(ub - l < VM.SIG_DIF_LIMIT || l - lb < VM.SIG_DIF_LIMIT) {
@@ -2418,8 +2420,8 @@ class Paper {
2418
2420
  let npfbg = 'white';
2419
2421
  if(fill_color === this.palette.above_upper_bound ||
2420
2422
  fill_color === this.palette.below_lower_bound ||
2421
- // NOTE: empty buffers (at level 0) should be entirely white
2422
- (at_bound && l > VM.ON_OFF_THRESHOLD)) {
2423
+ // NOTE: Empty buffers should be entirely white.
2424
+ (at_bound && l > lb + VM.ON_OFF_THRESHOLD)) {
2423
2425
  npfbg = fill_color;
2424
2426
  pf = false;
2425
2427
  }
@@ -2434,26 +2436,26 @@ class Paper {
2434
2436
  {fill: fill_color, stroke: stroke_color, 'stroke-width': stroke_width,
2435
2437
  'stroke-dasharray': sda, 'stroke-linecap': 'round',
2436
2438
  'rx': hh, 'ry': hh});
2437
- // NOTE: set fill color to darker shade for partial fill
2439
+ // NOTE: Set fill color to darker shade for partial fill.
2438
2440
  fill_color = (!MODEL.solved ? this.palette.src_snk :
2439
2441
  (l > VM.NEAR_ZERO ? this.palette.above_zero_fill :
2440
2442
  (l < -VM.NEAR_ZERO ? this.palette.below_zero_fill :
2441
2443
  this.palette.at_zero_fill)));
2442
2444
  }
2443
- // Add partial fill if appropriate
2445
+ // Add partial fill if appropriate.
2444
2446
  if(pf && l > lb && l < VM.UNDEFINED) {
2445
2447
  // Calculate used part of range (1 = 100%)
2446
2448
  let part,
2447
2449
  range = ub - lb;
2448
2450
  if(l >= VM.PLUS_INFINITY) {
2449
- // Show exceptions and +INF as "overflow"
2451
+ // Show exceptions and +INF as "overflow".
2450
2452
  part = 1;
2451
2453
  fill_color = this.palette.above_upper_bound;
2452
2454
  } else {
2453
2455
  part = (range > 0 ? (l - lb) / range : 1);
2454
2456
  }
2455
2457
  if(part > 0 && l >= lb) {
2456
- // Only fill the portion of used range with the fill color
2458
+ // Only fill the portion of used range with the fill color.
2457
2459
  const rad = Math.asin(1 - 2*part);
2458
2460
  prod.shape.addPath(['m', x + hw - hh + (hh - 1.5) * Math.cos(rad),
2459
2461
  ',', y + (hh - 1.5) * Math.sin(rad),
@@ -2467,7 +2469,7 @@ class Paper {
2467
2469
  stroke_color = 'none';
2468
2470
  stroke_width = 0;
2469
2471
  // Sources have a triangle pointing up from the bottom
2470
- // (in outline if *implicit* source)
2472
+ // (in outline if *implicit* source).
2471
2473
  if(prod.isSourceNode) {
2472
2474
  if(!prod.is_source) {
2473
2475
  fill_color = 'none';
@@ -2479,7 +2481,7 @@ class Paper {
2479
2481
  'stroke-width': stroke_width});
2480
2482
  }
2481
2483
  // Sinks have a triangle pointing down from the top
2482
- // (in outline if implicit sink)
2484
+ // (in outline if implicit sink).
2483
2485
  if(prod.isSinkNode) {
2484
2486
  if(!prod.is_sink) {
2485
2487
  fill_color = 'none';
@@ -2491,7 +2493,7 @@ class Paper {
2491
2493
  'stroke-width': stroke_width});
2492
2494
  }
2493
2495
  // Integer level is denoted by enclosing name in large [ and ]
2494
- // to denote "floor" as well as "ceiling"
2496
+ // to denote "floor" as well as "ceiling".
2495
2497
  if(prod.integer_level) {
2496
2498
  const
2497
2499
  brh = prod.name_lines.split('\n').length * this.font_heights[8] + 4,
@@ -170,11 +170,11 @@ class LinnyRModel {
170
170
  olist.push(ds.displayName);
171
171
  }
172
172
  }
173
- // ALL equation results are stored, so add all equation selectors...
173
+ // Also add all outcome equation selectors.
174
174
  const dsm = this.equations_dataset.modifiers;
175
- // ... except selectors starting with a colon (methods).
175
+ // Exclude selectors starting with a colon (methods) -- just in case.
176
176
  for(let k in dsm) if(dsm.hasOwnProperty(k) && !k.startsWith(':')) {
177
- olist.push(dsm[k].selector);
177
+ if(dsm[k].outcome_equation) olist.push(dsm[k].selector);
178
178
  }
179
179
  return olist;
180
180
  }
@@ -1574,7 +1574,7 @@ class LinnyRModel {
1574
1574
  const
1575
1575
  note = fc.notes[i],
1576
1576
  nbn = note.nearbyNode;
1577
- note.nearby_pos = (nbn ? {node: nbn, oldx: x, oldy: y} : null);
1577
+ note.nearby_pos = (nbn ? {node: nbn, oldx: nbn.x, oldy: nbn.y} : null);
1578
1578
  }
1579
1579
  for(let i = 0; i < fc.processes.length; i++) {
1580
1580
  move = fc.processes[i].alignToGrid() || move;
@@ -1593,8 +1593,8 @@ class LinnyRModel {
1593
1593
  nbp = note.nearby_pos;
1594
1594
  if(nbp) {
1595
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;
1596
+ note.x += nbp.node.x - nbp.oldx;
1597
+ note.y += nbp.node.y - nbp.oldy;
1598
1598
  note.nearby_pos = null;
1599
1599
  }
1600
1600
  }
@@ -3155,8 +3155,8 @@ class LinnyRModel {
3155
3155
  const
3156
3156
  dm = ds.modifiers[ms],
3157
3157
  n = dm.displayName;
3158
- // Do not add if already in the list.
3159
- if(names.indexOf(n) < 0) {
3158
+ // Do not add if already in the list, or equation is not outcome.
3159
+ if(names.indexOf(n) < 0 && (!eq || dm.outcome_equation)) {
3160
3160
  // Here, too, NULL can be used as "owner chart".
3161
3161
  const cv = new ChartVariable(null);
3162
3162
  // NOTE: For equations, the object is the dataset modifier.
@@ -3169,10 +3169,17 @@ class LinnyRModel {
3169
3169
  // Sort variables by their name.
3170
3170
  vbls.sort((a, b) => UI.compareFullNames(a.displayName, b.displayName));
3171
3171
  // Create a new chart as dummy, so without adding it to this model.
3172
- const c = new Chart();
3172
+ const
3173
+ c = new Chart(),
3174
+ wcdm = [];
3173
3175
  for(let i = 0; i < vbls.length; i++) {
3174
3176
  const v = vbls[i];
3175
- c.addVariable(v.object.displayName, v.attribute);
3177
+ // NOTE: Prevent adding wildcard dataset modifiers more than once.
3178
+ if(wcdm.indexOf(v.object) < 0) {
3179
+ if(v.object instanceof DatasetModifier &&
3180
+ v.object.selector.indexOf('??') >= 0) wcdm.push(v.object);
3181
+ c.addVariable(v.object.displayName, v.attribute);
3182
+ }
3176
3183
  }
3177
3184
  // NOTE: Call `draw` with FALSE to prevent display in the chart manager.
3178
3185
  c.draw(false);
@@ -5158,7 +5165,7 @@ class Note extends ObjectWithXYWH {
5158
5165
  // If attribute omitted, use default attribute of entity type.
5159
5166
  const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
5160
5167
  let val = null;
5161
- // NOTE: for datasets, use the active modifier if no attribute.
5168
+ // NOTE: For datasets, use the active modifier if no attribute.
5162
5169
  if(!attr && obj instanceof Dataset) {
5163
5170
  val = obj.activeModifierExpression;
5164
5171
  } else {
@@ -7894,13 +7901,11 @@ class Product extends Node {
7894
7901
  }
7895
7902
 
7896
7903
  get isConstant() {
7897
- // Return TRUE if this product is data, has no links to processes,
7898
- // is not an actor cash flow, and has set LB = UB
7904
+ // Return TRUE if this product is data, is not an actor cash flow,
7905
+ // has no ingoing links, has outgoing links ONLY to data objects,
7906
+ // and has set LB = UB.
7899
7907
  if(!this.is_data || this.name.startsWith('$') ||
7900
- !this.allOutputsAreData) return false;
7901
- for(let i = 0; i < this.inputs.length; i++) {
7902
- if(this.inputs[i].from_node instanceof Process) return false;
7903
- }
7908
+ this.inputs.length || !this.allOutputsAreData) return false;
7904
7909
  return (this.equal_bounds && this.lower_bound.defined);
7905
7910
  }
7906
7911
 
@@ -8479,7 +8484,7 @@ class DatasetModifier {
8479
8484
  this.dataset = dataset;
8480
8485
  this.selector = selector;
8481
8486
  this.expression = new Expression(dataset, selector, '');
8482
- this.expression_cache = {};
8487
+ this.outcome_equation = false;
8483
8488
  }
8484
8489
 
8485
8490
  get type() {
@@ -8509,12 +8514,21 @@ class DatasetModifier {
8509
8514
  // NOTE: For some reason, selector may become empty string, so prevent
8510
8515
  // saving such unidentified modifiers.
8511
8516
  if(this.selector.trim().length === 0) return '';
8512
- return ['<modifier><selector>', xmlEncoded(this.selector),
8517
+ const oe = (this.outcome_equation ? ' outcome="1"' : '');
8518
+ return ['<modifier', oe, '><selector>', xmlEncoded(this.selector),
8513
8519
  '</selector><expression>', xmlEncoded(this.expression.text),
8514
8520
  '</expression></modifier>'].join('');
8515
8521
  }
8516
8522
 
8517
8523
  initFromXML(node) {
8524
+ // NOTE: Up to version 1.6.2, all equations were considered as
8525
+ // outcomes. To maintain upward compatibility, check for the model
8526
+ // version number.
8527
+ if(earlierVersion(MODEL.version, '1.6.3')) {
8528
+ this.outcome_equation = true;
8529
+ } else {
8530
+ this.outcome_equation = nodeParameterValue(node, 'outcome') === '1';
8531
+ }
8518
8532
  this.expression.text = xmlDecoded(nodeContentByTag(node, 'expression'));
8519
8533
  if(IO_CONTEXT) {
8520
8534
  // Contextualize the included expression.
@@ -8826,12 +8840,19 @@ class Dataset {
8826
8840
  this.max = Math.max(this.max, this.data[i]);
8827
8841
  sum += this.data[i];
8828
8842
  }
8829
- this.mean = sum / this.data.length;
8830
- let sumsq = 0;
8831
- for(let i = 0; i < this.data.length; i++) {
8832
- sumsq += Math.pow(this.data[i] - this.mean, 2);
8843
+ // NOTE: Avoid small differences due to numerical imprecision.
8844
+ if(this.max - this.min < VM.NEAR_ZERO) {
8845
+ this.max = this.min;
8846
+ this.mean = this.min;
8847
+ this.standard_deviation = 0;
8848
+ } else {
8849
+ this.mean = sum / this.data.length;
8850
+ let sumsq = 0;
8851
+ for(let i = 0; i < this.data.length; i++) {
8852
+ sumsq += Math.pow(this.data[i] - this.mean, 2);
8853
+ }
8854
+ this.standard_deviation = Math.sqrt(sumsq / this.data.length);
8833
8855
  }
8834
- this.standard_deviation = Math.sqrt(sumsq / this.data.length);
8835
8856
  }
8836
8857
 
8837
8858
  get statisticsAsString() {
@@ -8847,11 +8868,30 @@ class Dataset {
8847
8868
 
8848
8869
  attributeValue(a) {
8849
8870
  // Return the computed result for attribute `a`.
8850
- // NOTE: Datasets have ONE attribute (their vector) denoted by the empty
8851
- // string; all other "attributes" should be modifier selectors, and
8852
- // their value should be obtained using attributeExpression (see below).
8853
- if(a === '') return this.vector;
8854
- return null;
8871
+ // NOTE: Datasets have ONE attribute (their vector) denoted by the
8872
+ // dot ".". All other "attributes" should be modifier selectors,
8873
+ // and their value should be obtained using `attributeExpression(a)`.
8874
+ // The empty string denotes "use default", which may have been set
8875
+ // by the modeler, or may follow from the active combination of a
8876
+ // running experiment.
8877
+ if(a === '') {
8878
+ const x = this.activeModifierExpression;
8879
+ if(x instanceof Expression) {
8880
+ x.compute(0);
8881
+ // Ensure that for dynamic modifier expressions the vector is
8882
+ // fully computed.
8883
+ if(!x.isStatic) {
8884
+ const nt = MODEL.end_period - MODEL.start_period + 1;
8885
+ for(let t = 1; t <= nt; t++) x.result(t);
8886
+ }
8887
+ return x.vector;
8888
+ }
8889
+ // No modifier expression? Then return the dataset vector.
8890
+ return this.vector;
8891
+ }
8892
+ if(a === '.') return this.vector;
8893
+ // Fall-through: return the default value of this dataset.
8894
+ return this.defaultValue;
8855
8895
  }
8856
8896
 
8857
8897
  attributeExpression(a) {
@@ -8881,7 +8921,7 @@ class Dataset {
8881
8921
  console.log('WARNING: Dataset "' + this.name +
8882
8922
  `" has no default selector "${this.default_selector}"`, this.modifiers);
8883
8923
  }
8884
- // Fall-through: return vector instead of expression.
8924
+ // Fall-through: return the dataset vector.
8885
8925
  return this.vector;
8886
8926
  }
8887
8927
 
@@ -9064,7 +9104,6 @@ class Dataset {
9064
9104
  for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
9065
9105
  // NOTE: "empty" expressions for modifiers default to dataset default.
9066
9106
  this.modifiers[m].expression.reset(this.defaultValue);
9067
- this.modifiers[m].expression_cache = {};
9068
9107
  }
9069
9108
  }
9070
9109
 
@@ -9124,6 +9163,8 @@ class ChartVariable {
9124
9163
  this.non_zero_tally = 0;
9125
9164
  this.exceptions = 0;
9126
9165
  this.bin_tallies = [];
9166
+ // The actual wildcard index is set for each variable that is added
9167
+ // during this "expansion".
9127
9168
  this.wildcard_index = false;
9128
9169
  }
9129
9170
 
@@ -9193,6 +9234,8 @@ class ChartVariable {
9193
9234
  }
9194
9235
  const xml = ['<chart-variable', (this.stacked ? ' stacked="1"' : ''),
9195
9236
  (this.visible ? ' visible="1"' : ''),
9237
+ (this.wildcard_index !== false ?
9238
+ ` wildcard-index="${this.wildcard_index}"` : ''),
9196
9239
  ` sorted="${this.sorted}"`,
9197
9240
  '><object-id>', xmlEncoded(id),
9198
9241
  '</object-id><attribute>', this.attribute,
@@ -9238,6 +9281,9 @@ class ChartVariable {
9238
9281
  UI.warn(`No chart variable entity with ID "${id}"`);
9239
9282
  return false;
9240
9283
  }
9284
+ // For wildcard variables, a subset of wildcard indices may be specified.
9285
+ const wci = nodeParameterValue(node, 'wildcard-index');
9286
+ this.wildcard_index = (wci ? parseInt(wci) : false);
9241
9287
  this.setProperties(
9242
9288
  obj,
9243
9289
  nodeContentByTag(node, 'attribute'),
@@ -9290,23 +9336,11 @@ class ChartVariable {
9290
9336
  t_end = tsteps;
9291
9337
  } else {
9292
9338
  // Get the variable's own value (number, vector or expression)
9293
- // Special case: when an experiment is running, variables that
9294
- // depict a dataset with no explicit modifier must recompute the
9295
- // vector using the current experiment run combination.
9296
- if(MODEL.running_experiment &&
9297
- this.object instanceof Dataset && !this.attribute) {
9298
- // Check if dataset modifiers match the combination of selectors
9299
- // for the active run.
9300
- const mm = this.object.matchingModifiers(
9301
- MODEL.running_experiment.activeCombination);
9302
- // If so, use the first (the list should contain at most 1 selector)
9303
- // to select the modifier expression; otherwise, use the unmodified
9304
- // vector of the dataset
9305
- if(mm.length > 0) {
9306
- av = mm[0].expression;
9307
- } else {
9308
- av = this.object.vector;
9309
- }
9339
+ if(this.object instanceof Dataset && !this.attribute) {
9340
+ // Special case: Variables that depict a dataset with no explicit
9341
+ // modifier selector must recompute the vector using the current
9342
+ // experiment run combination or the default selector.
9343
+ av = this.object.activeModifierExpression;
9310
9344
  } else if(this.object instanceof DatasetModifier) {
9311
9345
  av = this.object.expression;
9312
9346
  } else {
@@ -9322,7 +9356,7 @@ class ChartVariable {
9322
9356
  for(let t = 0; t <= t_end; t++) {
9323
9357
  // Get the result, store it, and incorporate it in statistics.
9324
9358
  if(!av) {
9325
- // Undefined attribute => zero (no error)
9359
+ // Undefined attribute => zero (no error).
9326
9360
  v = 0;
9327
9361
  } else if(Array.isArray(av)) {
9328
9362
  // Attribute value is a vector.
@@ -9334,19 +9368,19 @@ class ChartVariable {
9334
9368
  // this index as context number.
9335
9369
  v = av.result(t, this.wildcard_index);
9336
9370
  } else {
9337
- // Attribute value must be a number
9371
+ // Attribute value must be a number.
9338
9372
  v = av;
9339
9373
  }
9340
- // Map undefined values and all errors to 0
9374
+ // Map undefined values and all errors to 0.
9341
9375
  if(v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY) {
9342
- // Do not include values for t = 0 in statistics
9376
+ // Do not include values for t = 0 in statistics.
9343
9377
  if(t > 0) this.exceptions++;
9344
9378
  v = 0;
9345
9379
  }
9346
- // Scale the value unless run result (these are already scaled!)
9380
+ // Scale the value unless run result (these are already scaled!).
9347
9381
  if(!rr) v *= this.scale_factor;
9348
9382
  this.vector.push(v);
9349
- // Do not include values for t = 0 in statistics
9383
+ // Do not include values for t = 0 in statistics.
9350
9384
  if(t > 0) {
9351
9385
  if(Math.abs(v) > VM.NEAR_ZERO) {
9352
9386
  this.sum += v;
@@ -9356,17 +9390,24 @@ class ChartVariable {
9356
9390
  this.maximum = Math.max(this.maximum, v);
9357
9391
  }
9358
9392
  }
9359
- // Compute the mean
9360
- this.mean = this.sum / t_end;
9361
- // Compute the variance for t=1, ..., N
9362
- let sumsq = 0;
9363
- for(let t = 1; t <= t_end; t++) {
9364
- v = this.vector[t];
9365
- // Here, too, ignore exceptional values, and use 0 instead
9366
- if(v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY) v = 0;
9367
- sumsq += Math.pow(v - this.mean, 2);
9393
+ if(this.maximum - this.minimum < VM.NEAR_ZERO) {
9394
+ // Ignore minute differences.
9395
+ this.maximum = this.minimum;
9396
+ this.mean = this.minimum;
9397
+ this.variance = 0;
9398
+ } else {
9399
+ // Compute the mean.
9400
+ this.mean = this.sum / t_end;
9401
+ // Compute the variance for t=1, ..., N.
9402
+ let sumsq = 0;
9403
+ for(let t = 1; t <= t_end; t++) {
9404
+ v = this.vector[t];
9405
+ // Here, too, ignore exceptional values, and use 0 instead.
9406
+ if(v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY) v = 0;
9407
+ sumsq += Math.pow(v - this.mean, 2);
9408
+ }
9409
+ this.variance = sumsq / t_end;
9368
9410
  }
9369
- this.variance = sumsq / t_end;
9370
9411
  }
9371
9412
 
9372
9413
  tallyVector() {
@@ -9535,16 +9576,16 @@ class Chart {
9535
9576
  }
9536
9577
 
9537
9578
  addVariable(n, a) {
9538
- // Adds variable [entity name `n`|attribute `a`] to the chart unless it
9539
- // is already in the variable list.
9579
+ // Add variable [entity name `n`|attribute `a`] to the chart unless
9580
+ // it is already in the variable list.
9540
9581
  let dn = n + UI.OA_SEPARATOR + a;
9541
- // Adapt display name for special cases
9582
+ // Adapt display name for special cases.
9542
9583
  if(n === UI.EQUATIONS_DATASET_NAME) {
9543
- // For equations only the attribute (modifier selector)
9584
+ // For equations only the attribute (modifier selector).
9544
9585
  dn = a;
9545
9586
  n = a;
9546
9587
  } else if(!a) {
9547
- // If no attribute specified (=> dataset) only the entity name
9588
+ // If no attribute specified (=> dataset) only the entity name.
9548
9589
  dn = n;
9549
9590
  }
9550
9591
  let vi = this.variableIndexByName(dn);
@@ -9559,17 +9600,14 @@ class Chart {
9559
9600
  // No equation and no attribute specified? Then assume default.
9560
9601
  if(!eq && a === '') a = obj.defaultAttribute;
9561
9602
  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
9564
- // expression.
9565
- const
9566
- clr = this.nextAvailableDefaultColor,
9567
- indices = Object.keys(obj.expression.wildcard_vectors);
9568
- for(let i = 0; i < indices.length; i++) {
9569
- const v = new ChartVariable(this);
9570
- v.setProperties(obj, dn, false, clr);
9571
- v.wildcard_index = parseInt(indices[i]);
9572
- this.variables.push(v);
9603
+ // Special case: for wildcard equations and methods, prompt the
9604
+ // modeler which wildcard possibilities to add UNLESS this is an
9605
+ // untitled "dummy" chart used to report outcomes.
9606
+ if(this.title) {
9607
+ CHART_MANAGER.promptForWildcardIndices(this, obj);
9608
+ } else {
9609
+ this.addWildcardVariables(obj,
9610
+ Object.keys(obj.expression.wildcard_vectors));
9573
9611
  }
9574
9612
  } else {
9575
9613
  const v = new ChartVariable(this);
@@ -9578,6 +9616,19 @@ class Chart {
9578
9616
  }
9579
9617
  return this.variables.length - 1;
9580
9618
  }
9619
+
9620
+ addWildcardVariables(dsm, indices) {
9621
+ // For dataset modifier `dsm`, add dummy variables for those vectors
9622
+ // in the wildcard vector set of the expression that are indicated
9623
+ // by the list `indices`.
9624
+ const dn = dsm.displayName;
9625
+ for(let i = 0; i < indices.length; i++) {
9626
+ const v = new ChartVariable(this);
9627
+ v.setProperties(dsm, dn, false, this.nextAvailableDefaultColor);
9628
+ v.wildcard_index = parseInt(indices[i]);
9629
+ this.variables.push(v);
9630
+ }
9631
+ }
9581
9632
 
9582
9633
  addSVG(lines) {
9583
9634
  // Appends a string or an array of strings to the SVG
@@ -10396,7 +10447,7 @@ class Chart {
10396
10447
  if(CHART_MANAGER.drawing_chart) {
10397
10448
  return '(chart statistics not calculated yet)';
10398
10449
  }
10399
- // NOTE: unlike statistics, series data is output in columns
10450
+ // NOTE: Unlike statistics, series data is output in columns.
10400
10451
  const data = [], vbl = [], line = ['t'];
10401
10452
  // First line: column labels (variable names, but time step in first column)
10402
10453
  for(let i = 0; i < this.variables.length; i++) {
@@ -11075,7 +11126,8 @@ class ExperimentRun {
11075
11126
  bm.messages = VM.messages[i];
11076
11127
  this.block_messages.push(bm);
11077
11128
  this.warning_count += bm.warningCount;
11078
- this.solver_seconds += bm.solver_secs;
11129
+ // NOTE: When set by the VM, `solver_secs` is a string.
11130
+ this.solver_seconds += parseFloat(bm.solver_secs);
11079
11131
  }
11080
11132
  }
11081
11133
 
@@ -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
  //