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.
- package/package.json +1 -1
- package/server.js +27 -6
- package/static/index.html +18 -0
- package/static/linny-r.css +39 -2
- package/static/scripts/linny-r-ctrl.js +8 -1
- package/static/scripts/linny-r-gui-chart-manager.js +80 -16
- package/static/scripts/linny-r-gui-controller.js +3 -3
- package/static/scripts/linny-r-gui-equation-manager.js +19 -0
- package/static/scripts/linny-r-gui-experiment-manager.js +14 -3
- package/static/scripts/linny-r-gui-expression-editor.js +1 -1
- package/static/scripts/linny-r-gui-monitor.js +1 -0
- package/static/scripts/linny-r-gui-paper.js +25 -23
- package/static/scripts/linny-r-model.js +134 -82
- package/static/scripts/linny-r-utils.js +15 -1
- package/static/scripts/linny-r-vm.js +248 -58
@@ -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:
|
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
|
-
|
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
|
-
//
|
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
|
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
|
-
//
|
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:
|
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:
|
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
|
-
//
|
173
|
+
// Also add all outcome equation selectors.
|
174
174
|
const dsm = this.equations_dataset.modifiers;
|
175
|
-
//
|
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 -
|
1597
|
-
note.y += nbp.node.y -
|
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
|
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
|
-
|
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:
|
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,
|
7898
|
-
//
|
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.
|
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
|
-
|
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
|
-
|
8830
|
-
|
8831
|
-
|
8832
|
-
|
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
|
8851
|
-
//
|
8852
|
-
// their value should be obtained using attributeExpression
|
8853
|
-
|
8854
|
-
|
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
|
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
|
-
|
9294
|
-
|
9295
|
-
|
9296
|
-
|
9297
|
-
|
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
|
-
|
9360
|
-
|
9361
|
-
|
9362
|
-
|
9363
|
-
|
9364
|
-
|
9365
|
-
//
|
9366
|
-
|
9367
|
-
|
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
|
-
//
|
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,
|
9563
|
-
//
|
9564
|
-
//
|
9565
|
-
|
9566
|
-
|
9567
|
-
|
9568
|
-
|
9569
|
-
|
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:
|
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
|
-
|
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
|
-
|
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
|
//
|