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.
- package/package.json +1 -1
- package/static/index.html +7 -3
- package/static/linny-r.css +19 -0
- package/static/scripts/linny-r-gui-actor-manager.js +3 -3
- package/static/scripts/linny-r-gui-chart-manager.js +10 -9
- package/static/scripts/linny-r-gui-controller.js +49 -36
- package/static/scripts/linny-r-gui-dataset-manager.js +17 -49
- package/static/scripts/linny-r-gui-documentation-manager.js +71 -47
- package/static/scripts/linny-r-gui-equation-manager.js +26 -10
- package/static/scripts/linny-r-gui-experiment-manager.js +41 -23
- package/static/scripts/linny-r-gui-expression-editor.js +26 -21
- package/static/scripts/linny-r-gui-finder.js +1 -0
- package/static/scripts/linny-r-gui-monitor.js +4 -4
- package/static/scripts/linny-r-milp.js +18 -15
- package/static/scripts/linny-r-model.js +354 -137
- package/static/scripts/linny-r-utils.js +68 -18
- package/static/scripts/linny-r-vm.js +593 -300
@@ -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:
|
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
|
-
|
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
|
-
//
|
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
|
-
//
|
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:
|
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
|
-
//
|
481
|
-
//
|
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
|
-
|
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)
|
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
|
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
|
-
//
|
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"
|
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
|
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"
|
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:
|
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(
|
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.
|
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)
|
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
|
6008
|
-
// already exists
|
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`,
|
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;
|
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:
|
8500
|
+
// NOTE: Identifier will be unique only for equations.
|
8320
8501
|
return UI.nameToID(this.selector);
|
8321
8502
|
}
|
8322
8503
|
get displayName() {
|
8323
|
-
// NOTE:
|
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:
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
8543
|
-
// NOTE:
|
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 ' (' + time_prop + (this.periodic ? ' \u21BB' : '') + ')';
|
8586
8775
|
}
|
8587
8776
|
|
8588
8777
|
unpackDataString(str) {
|
8589
|
-
//
|
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
|
-
//
|
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:
|
8606
|
-
//
|
8607
|
-
//
|
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)
|
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)
|
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
|
-
//
|
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
|
-
//
|
8838
|
+
// Return descriptive statistics in human-readable form.
|
8650
8839
|
let s = 'N = ' + this.data.length;
|
8651
8840
|
if(N > 0) {
|
8652
|
-
s += ', range = ['
|
8653
|
-
', mean = '
|
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
|
-
//
|
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
|
-
//
|
8858
|
+
// Return the expression for selector `a` (also considering wildcard
|
8670
8859
|
// modifiers), or NULL if no such selector exists.
|
8671
|
-
// NOTE:
|
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
|
-
//
|
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
|
-
|
8720
|
-
|
8721
|
-
|
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:
|
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:
|
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:
|
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
|
8945
|
-
//
|
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
|
-
|
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
|
-
|
9343
|
-
|
9344
|
-
|
9345
|
-
|
9346
|
-
//
|
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(
|
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
|
-
|
9361
|
-
|
9362
|
-
|
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
|
-
|
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:
|
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
|
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:
|