linny-r 2.0.9 → 2.0.10
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 +6 -2
- package/static/images/solve-not-same-changed.png +0 -0
- package/static/images/solve-not-same-not-changed.png +0 -0
- package/static/images/solve-same-changed.png +0 -0
- package/static/images/solve-same-not-changed.png +0 -0
- package/static/index.html +79 -0
- package/static/linny-r.css +240 -7
- package/static/scripts/linny-r-ctrl.js +76 -13
- package/static/scripts/linny-r-gui-chart-manager.js +13 -12
- package/static/scripts/linny-r-gui-constraint-editor.js +4 -4
- package/static/scripts/linny-r-gui-controller.js +390 -30
- package/static/scripts/linny-r-gui-dataset-manager.js +48 -31
- package/static/scripts/linny-r-gui-experiment-manager.js +22 -12
- package/static/scripts/linny-r-gui-expression-editor.js +26 -12
- package/static/scripts/linny-r-gui-finder.js +190 -5
- package/static/scripts/linny-r-model.js +112 -38
- package/static/scripts/linny-r-vm.js +99 -93
@@ -856,8 +856,10 @@ class LinnyRModel {
|
|
856
856
|
|
857
857
|
indexOfChart(t) {
|
858
858
|
// Return the index of a chart having title `t` in the model's chart list.
|
859
|
+
// NOTE: Titles should not be case-sensitive.
|
860
|
+
t = t.toLowerCase();
|
859
861
|
for(let index = 0; index < this.charts.length; index++) {
|
860
|
-
if(this.charts[index].title === t) return index;
|
862
|
+
if(this.charts[index].title.toLowerCase() === t) return index;
|
861
863
|
}
|
862
864
|
return -1;
|
863
865
|
}
|
@@ -865,8 +867,11 @@ class LinnyRModel {
|
|
865
867
|
indexOfExperiment(t) {
|
866
868
|
// Return the index of an experiment having title `t` in the model's
|
867
869
|
// experiment list.
|
870
|
+
// NOTE: Titles should not be case-sensitive.
|
871
|
+
t = t.toLowerCase();
|
868
872
|
for(let index = 0; index < this.experiments.length; index++) {
|
869
|
-
|
873
|
+
// NOTE: Use nameToID to
|
874
|
+
if(this.experiments[index].title.toLowerCase() === t) return index;
|
870
875
|
}
|
871
876
|
return -1;
|
872
877
|
}
|
@@ -1078,49 +1083,63 @@ class LinnyRModel {
|
|
1078
1083
|
return ss.length > 0;
|
1079
1084
|
}
|
1080
1085
|
|
1081
|
-
renamePrefixedDatasets(old_prefix, new_prefix) {
|
1086
|
+
renamePrefixedDatasets(old_prefix, new_prefix, subset=null) {
|
1082
1087
|
// Rename all datasets having the specified old prefix so that they
|
1083
1088
|
// have the specified new prefix UNLESS this would cause name conflicts.
|
1089
|
+
// NOTE: If `subset` is defined, limit renaming to the datasets it contains.
|
1084
1090
|
const
|
1085
1091
|
oldkey = old_prefix.toLowerCase().split(UI.PREFIXER).join(':_'),
|
1086
1092
|
newkey = new_prefix.toLowerCase().split(UI.PREFIXER).join(':_'),
|
1087
|
-
|
1093
|
+
dskl = [];
|
1088
1094
|
// No change if new prefix is identical to old prefix.
|
1089
1095
|
if(old_prefix !== new_prefix) {
|
1090
1096
|
for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
|
1091
|
-
if(k.startsWith(oldkey)
|
1097
|
+
if(k.startsWith(oldkey) &&
|
1098
|
+
(!subset || subset.indexOf(MODEL.datasets[k]) >= 0)) dskl.push(k);
|
1092
1099
|
}
|
1093
1100
|
// NOTE: No check for name conflicts needed when name change is
|
1094
1101
|
// merely some upper/lower case change.
|
1095
1102
|
if(newkey !== oldkey) {
|
1096
1103
|
let nc = 0;
|
1097
|
-
for(const
|
1098
|
-
const nk = newkey +
|
1104
|
+
for(const k of dskl) {
|
1105
|
+
const nk = newkey + k.substring(oldkey.length);
|
1099
1106
|
if(MODEL.datasets[nk]) nc++;
|
1100
1107
|
}
|
1101
1108
|
if(nc) {
|
1102
|
-
UI.warn('Renaming ' + pluralS(
|
1109
|
+
UI.warn('Renaming ' + pluralS(dskl.length, 'dataset') +
|
1103
1110
|
' would cause ' + pluralS(nc, 'name conflict'));
|
1104
1111
|
return false;
|
1105
1112
|
}
|
1106
1113
|
}
|
1107
1114
|
// Reset counts of effects of a rename operation.
|
1108
|
-
this.
|
1115
|
+
this.variable_count = 0;
|
1109
1116
|
this.expression_count = 0;
|
1110
1117
|
// Rename datasets one by one, suppressing notifications.
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1118
|
+
// NOTE: Make a list of renamed datasets.
|
1119
|
+
const rdsl = [];
|
1120
|
+
for(const k of dskl) {
|
1121
|
+
const
|
1122
|
+
ds = this.datasets[k],
|
1123
|
+
// NOTE: When old prefix is empty string, add instead of replace.
|
1124
|
+
nn = (old_prefix ? ds.displayName.replace(old_prefix, new_prefix) :
|
1125
|
+
new_prefix + ds.displayName);
|
1126
|
+
rdsl.push(ds.rename(nn, false));
|
1127
|
+
}
|
1128
|
+
if(subset) {
|
1129
|
+
// Update the specified subset so it contains the renamed datasets.
|
1130
|
+
subset.length = 0;
|
1131
|
+
subset.push(...rdsl);
|
1132
|
+
} else {
|
1133
|
+
let msg = 'Renamed ' + pluralS(dskl.length, 'dataset').toLowerCase();
|
1134
|
+
if(this.variable_count) msg += ', and updated ' +
|
1135
|
+
pluralS(this.variable_count, 'variable') + ' in ' +
|
1136
|
+
pluralS(this.expression_count, 'expression');
|
1137
|
+
UI.notify(msg);
|
1138
|
+
if(EXPERIMENT_MANAGER.selected_experiment) {
|
1139
|
+
EXPERIMENT_MANAGER.selected_experiment.inferVariables();
|
1140
|
+
}
|
1141
|
+
UI.updateControllerDialogs('CDEFJX');
|
1122
1142
|
}
|
1123
|
-
UI.updateControllerDialogs('CDEFJX');
|
1124
1143
|
}
|
1125
1144
|
return true;
|
1126
1145
|
}
|
@@ -9006,16 +9025,16 @@ class Dataset {
|
|
9006
9025
|
this.black_box = false;
|
9007
9026
|
this.outcome = false;
|
9008
9027
|
this.parent_anchor = 0;
|
9009
|
-
// URL indicates that data must be read from external source
|
9028
|
+
// URL indicates that data must be read from external source.
|
9010
9029
|
this.url = '';
|
9011
9030
|
// Array `data` will contain modeler-defined values, starting at *dataset*
|
9012
|
-
// time step t = 1
|
9031
|
+
// time step t = 1.
|
9013
9032
|
this.data = [];
|
9014
9033
|
// Array `vector` will contain data values on model time scale, starting at
|
9015
|
-
// *model* time step t = 0
|
9034
|
+
// *model* time step t = 0.
|
9016
9035
|
this.vector = [];
|
9017
9036
|
this.modifiers = {};
|
9018
|
-
// Selector to be used when model is run normally, i.e., no experiment
|
9037
|
+
// Selector to be used when model is run normally, i.e., no experiment.
|
9019
9038
|
this.default_selector = '';
|
9020
9039
|
}
|
9021
9040
|
|
@@ -11319,6 +11338,7 @@ class ExperimentRun {
|
|
11319
11338
|
constructor(x, n) {
|
11320
11339
|
this.experiment = x;
|
11321
11340
|
this.number = n;
|
11341
|
+
this.combination = [];
|
11322
11342
|
this.time_started = 0;
|
11323
11343
|
this.time_recorded = 0;
|
11324
11344
|
this.time_steps = MODEL.end_period - MODEL.start_period + 1;
|
@@ -11330,6 +11350,8 @@ class ExperimentRun {
|
|
11330
11350
|
}
|
11331
11351
|
|
11332
11352
|
start() {
|
11353
|
+
// Initialize this run.
|
11354
|
+
this.combination = this.experiment.combinations[this.number].slice();
|
11333
11355
|
this.time_started = new Date().getTime();
|
11334
11356
|
this.time_recorded = 0;
|
11335
11357
|
this.results = [];
|
@@ -11346,7 +11368,8 @@ class ExperimentRun {
|
|
11346
11368
|
'" started="', this.time_started,
|
11347
11369
|
'" recorded="', this.time_recorded,
|
11348
11370
|
'"><x-title>', xmlEncoded(this.experiment.title),
|
11349
|
-
'</x-title><
|
11371
|
+
'</x-title><x-combi>', this.combination.join(' '),
|
11372
|
+
'</x-combi><time-steps>', this.time_steps,
|
11350
11373
|
'</time-steps><delta-t>', this.time_step_duration,
|
11351
11374
|
'</delta-t><results>', r,
|
11352
11375
|
'</results><messages>', bm,
|
@@ -11363,6 +11386,7 @@ class ExperimentRun {
|
|
11363
11386
|
UI.warn(`Run title "${t}" does not match experiment title "` +
|
11364
11387
|
this.experiment.title + '"');
|
11365
11388
|
}
|
11389
|
+
this.combi = nodeContentByTag(node, 'x-combi').split(' ');
|
11366
11390
|
this.time_steps = safeStrToInt(nodeContentByTag(node, 'time-steps'));
|
11367
11391
|
this.time_step_duration = safeStrToFloat(nodeContentByTag(node, 'delta-t'));
|
11368
11392
|
let n = childNodeByTag(node, 'results');
|
@@ -11543,7 +11567,6 @@ class Experiment {
|
|
11543
11567
|
this.selected_statistic = 'mean';
|
11544
11568
|
this.selected_scale = 'val';
|
11545
11569
|
this.selelected_color_scale = 'no';
|
11546
|
-
this.active_combination_index = -1;
|
11547
11570
|
// Set of combination indices to be displayed in chart.
|
11548
11571
|
this.chart_combinations = [];
|
11549
11572
|
// String to store original model settings while executing experiment runs.
|
@@ -11553,7 +11576,7 @@ class Experiment {
|
|
11553
11576
|
}
|
11554
11577
|
|
11555
11578
|
clearRuns() {
|
11556
|
-
// NOTE:
|
11579
|
+
// NOTE: Separated from basic initialization so that it can be called
|
11557
11580
|
// when the modeler clicks on the "Clear results" button.
|
11558
11581
|
// @@TO DO: prepare for UNDO.
|
11559
11582
|
this.runs.length = 0;
|
@@ -11561,7 +11584,7 @@ class Experiment {
|
|
11561
11584
|
this.completed = false;
|
11562
11585
|
this.time_started = 0;
|
11563
11586
|
this.time_stopped = 0;
|
11564
|
-
this.active_combination_index =
|
11587
|
+
this.active_combination_index = -1;
|
11565
11588
|
this.chart_combinations.length = 0;
|
11566
11589
|
}
|
11567
11590
|
|
@@ -11637,21 +11660,72 @@ class Experiment {
|
|
11637
11660
|
}
|
11638
11661
|
|
11639
11662
|
matchingCombinationIndex(sl) {
|
11640
|
-
// Return index of
|
11663
|
+
// Return the index of the run that can be inferred from selector list
|
11664
|
+
// `sl`, or FALSE if results for this run are not yet available.
|
11665
|
+
// NOTE: The selector list `sl` is a run specification that is *relative*
|
11666
|
+
// to the active combination of the *running* experiment, which may be
|
11667
|
+
// different from `this`. For example, consider an experiment with
|
11668
|
+
// three dimensions A = {a1, a2, a3), B = {b1, b2} and C = {c1, c2, c3},
|
11669
|
+
// and assume that the active combination is a2 + b2 + c2. Then the
|
11670
|
+
// "matching" combination will be:
|
11671
|
+
// a2 + b2 + c2 if `sl` is empty
|
11672
|
+
// a2 + b1 + c2 if `sl` is [b1]
|
11673
|
+
// a1 + b3 + c2 if `sl` is [b3, a1] (selector sequence)
|
11674
|
+
// NOTES:
|
11675
|
+
// (1) Elements of `sl` that are not element of A, B or C are ingnored.
|
11676
|
+
// (2) `sl` should not contain more than 1 selector from the same dimension.
|
11677
|
+
const
|
11678
|
+
valid = [],
|
11679
|
+
v_pos = {},
|
11680
|
+
matching = [];
|
11681
|
+
// First, retain only the (unique) valid selectors in `sl`.
|
11682
|
+
for(const s of sl) if(valid.indexOf(s) < 0) {
|
11683
|
+
for(const c of this.combinations) {
|
11684
|
+
// NOTE: Because of the way combinations are constructed, the index of
|
11685
|
+
// a valid selector in a combinations will always be the same.
|
11686
|
+
const pos = c.indexOf(s);
|
11687
|
+
// Conversely, when a new selector has the same position as a selector
|
11688
|
+
// that was already validated, this new selector is disregarded.
|
11689
|
+
if(pos >= 0 && !v_pos[pos]) {
|
11690
|
+
valid.push(s);
|
11691
|
+
v_pos[pos] = true;
|
11692
|
+
}
|
11693
|
+
}
|
11694
|
+
}
|
11695
|
+
// Then build a list of indices of combinations that match all valid selectors.
|
11696
|
+
// NOTE: The list of runs may not cover ALL combinations.
|
11697
|
+
for(let ri = 0; ri < this.runs.length; ri++) {
|
11698
|
+
if(intersection(valid, this.runs[ri].combination).length === valid.length) {
|
11699
|
+
matching.push(ri);
|
11700
|
+
}
|
11701
|
+
}
|
11702
|
+
// Results may already be conclusive:
|
11703
|
+
if(!matching.length) return false;
|
11704
|
+
// NOTE: If no experiment is running, there is no "active combination".
|
11705
|
+
// This should not occur, but in then return the last (= most recent) match.
|
11706
|
+
if(matching.length === 1 || !MODEL.running_experiment) return matching.pop();
|
11707
|
+
// If not conclusive, find the matching combination that -- for the remaining
|
11708
|
+
// dimensions of the experiment -- has most selectors in common with
|
11709
|
+
// the active combination of the *running* experiment.
|
11710
|
+
const ac = MODEL.running_experiment.activeCombination;
|
11641
11711
|
let high = 0,
|
11642
11712
|
index = false;
|
11643
|
-
|
11644
|
-
|
11645
|
-
|
11646
|
-
|
11647
|
-
|
11648
|
-
|
11713
|
+
for(const ri of matching) {
|
11714
|
+
const c = this.runs[ri].combination;
|
11715
|
+
let nm = 0;
|
11716
|
+
// NOTE: Ignore the matching valid selectors.
|
11717
|
+
for(let ci = 0; ci < c.length; ci++) {
|
11718
|
+
if(!v_pos[ci] && ac.indexOf(c[ci]) >= 0) nm++;
|
11719
|
+
}
|
11720
|
+
// NOTE: Using >= ensures that index will be set even for 0 matching.
|
11721
|
+
if(nm >= high) {
|
11722
|
+
high = nm;
|
11723
|
+
index = ri;
|
11649
11724
|
}
|
11650
11725
|
}
|
11651
|
-
// NOTE: If no matching selectors, return value is FALSE.
|
11652
11726
|
return index;
|
11653
11727
|
}
|
11654
|
-
|
11728
|
+
|
11655
11729
|
isDimensionSelector(s) {
|
11656
11730
|
// Return TRUE if `s` is a dimension selector in this experiment.
|
11657
11731
|
for(const dim of this.dimensions) if(dim.indexOf(s) >= 0) return true;
|
@@ -671,30 +671,8 @@ class ExpressionParser {
|
|
671
671
|
//
|
672
672
|
if(owner) {
|
673
673
|
this.context_number = owner.numberContext;
|
674
|
-
|
675
|
-
|
676
|
-
// For links and constraints, it depends:
|
677
|
-
const
|
678
|
-
fn = owner.from_node.displayName,
|
679
|
-
tn = owner.to_node.displayName;
|
680
|
-
if(fn.indexOf(UI.PREFIXER) >= 0) {
|
681
|
-
if(tn.indexOf(UI.PREFIXER) >= 0) {
|
682
|
-
// If both nodes are prefixed, use the longest prefix that these
|
683
|
-
// nodes have in common.
|
684
|
-
this.owner_prefix = UI.sharedPrefix(fn, tn) + UI.PREFIXER;
|
685
|
-
} else {
|
686
|
-
// Use the FROM node prefix.
|
687
|
-
this.owner_prefix = UI.completePrefix(fn);
|
688
|
-
}
|
689
|
-
} else if(tn.indexOf(UI.PREFIXER) >= 0) {
|
690
|
-
// Use the TO node prefix.
|
691
|
-
this.owner_prefix = UI.completePrefix(tn);
|
692
|
-
}
|
693
|
-
} else if(owner === MODEL.equations_dataset) {
|
694
|
-
this.owner_prefix = UI.completePrefix(attribute);
|
695
|
-
} else {
|
696
|
-
this.owner_prefix = UI.completePrefix(owner.displayName);
|
697
|
-
}
|
674
|
+
this.owner_prefix = UI.entityPrefix(
|
675
|
+
owner === MODEL.equations_dataset ? attribute : owner.displayName);
|
698
676
|
if(owner instanceof Dataset) {
|
699
677
|
this.dataset = owner;
|
700
678
|
// The attribute (if specified) is a dataset modifier selector.
|
@@ -848,30 +826,33 @@ class ExpressionParser {
|
|
848
826
|
}
|
849
827
|
}
|
850
828
|
}
|
851
|
-
//
|
852
|
-
// Specifier format: {method$title|run} where method and title are
|
853
|
-
// optional
|
829
|
+
// Experiment result specifier (optional) must be leading and braced.
|
830
|
+
// Specifier format: {method$title|run} where method$ and title| are
|
831
|
+
// optional. The run specifier may be a # followed by a run number, or
|
832
|
+
// a comma- or space-separated list of selectors.
|
833
|
+
// NOTE: # in title or run is NOT seen as a wildcard.
|
854
834
|
if(name.startsWith('{')) {
|
855
835
|
s = name.split('}');
|
856
836
|
if(s.length > 1) {
|
857
|
-
// Brace pair => interpret it as experiment result reference
|
837
|
+
// Brace pair => interpret it as experiment result reference.
|
858
838
|
const x = {
|
859
839
|
x: false, // experiment
|
860
840
|
r: false, // run number
|
861
841
|
v: false, // variable; if parametrized {n: name seg's, p: indices}
|
862
842
|
s: '', // statistic
|
863
|
-
m: '', // method
|
843
|
+
m: '', // method
|
864
844
|
p: false, // periodic
|
865
845
|
nr: false // run number range
|
866
846
|
};
|
867
|
-
// NOTE:
|
847
|
+
// NOTE: Name should then be in the experiment's variable list.
|
848
|
+
// This will be checked later, after validating the run specifier.
|
868
849
|
name = s[1].trim();
|
869
850
|
s = s[0].substring(1);
|
870
|
-
// Check for scaling method
|
871
|
-
// NOTE:
|
851
|
+
// Check for a time scaling method (used only for dataset run results).
|
852
|
+
// NOTE: Simply ignore $ unless it indicates a valid method.
|
872
853
|
const msep = s.indexOf('$');
|
873
854
|
if(msep <= 5) {
|
874
|
-
// Be tolerant as to case
|
855
|
+
// Be tolerant as to case.
|
875
856
|
let method = s.substring(0, msep).toUpperCase();
|
876
857
|
if(method.endsWith('P')) {
|
877
858
|
x.p = true;
|
@@ -879,70 +860,89 @@ class ExpressionParser {
|
|
879
860
|
}
|
880
861
|
if(['ABS', 'MEAN', 'SUM', 'MAX', ''].indexOf(method) >= 0) {
|
881
862
|
x.m = method;
|
882
|
-
s = s.substring(msep + 1);
|
863
|
+
s = s.substring(msep + 1).trim();
|
883
864
|
}
|
884
865
|
}
|
885
|
-
s
|
886
|
-
let
|
887
|
-
|
888
|
-
s = s
|
889
|
-
if(s.length >
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
//
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
//
|
905
|
-
|
906
|
-
//
|
907
|
-
//
|
908
|
-
const range = rn.substring(1);
|
909
|
-
// Call rangeToList only to validate the range syntax.
|
910
|
-
if(rangeToList(range)) {
|
911
|
-
x.nr = range;
|
912
|
-
this.is_static = false;
|
913
|
-
this.log('dynamic because experiment run number range');
|
914
|
-
} else {
|
915
|
-
msg = `Invalid experiment run number range "${range}"`;
|
916
|
-
}
|
866
|
+
// Now `s` may still have format title|run specifier.
|
867
|
+
let x_title = '',
|
868
|
+
run_spec = '';
|
869
|
+
s = s.split('|');
|
870
|
+
if(s.length > 2) {
|
871
|
+
msg = `Experiment result specifier may contain only one "|"`;
|
872
|
+
} else {
|
873
|
+
if(s.length == 2) {
|
874
|
+
run_spec = s.pop().trim();
|
875
|
+
x_title = s[0].trim();
|
876
|
+
} else {
|
877
|
+
// No vertical bar => no title, only the run specifier.
|
878
|
+
run_spec = s[0].trim();
|
879
|
+
}
|
880
|
+
// Run specifier can start with a # sign...
|
881
|
+
if(!run_spec.startsWith('#')) {
|
882
|
+
// ... and if not, it is assumed to be a list of modifier selectors
|
883
|
+
// that will identify (during problem set-up) a specific run.
|
884
|
+
// NOTE: Permit selectors to be separated by any combination
|
885
|
+
// of commas, semicolons and spaces.
|
886
|
+
x.r = run_spec.split(/[\,\;\/\s]+/g);
|
887
|
+
// NOTE: The VMI instruction accepts `x.r` to be a list of selectors
|
888
|
+
// or an integer number.
|
917
889
|
} else {
|
918
|
-
//
|
919
|
-
|
920
|
-
|
921
|
-
|
890
|
+
// If the specifier does start with a #, trim it...
|
891
|
+
run_spec = run_spec.substring(1);
|
892
|
+
// ... and then
|
893
|
+
// NOTE: Special notation for run numbers to permit modelers
|
894
|
+
// to chart results as if run numbers are on the time axis
|
895
|
+
// (with a given step size). The chart will be made as usual,
|
896
|
+
// i.e., plot a point for each time step t, but the value v[t]
|
897
|
+
// will then stay the same for the time interval that corresponds
|
898
|
+
// to simulation period length / number of runs.
|
899
|
+
// NOTE: This will fail to produce a meaningful chart when the
|
900
|
+
// simulation period is small compared to the number of runs.
|
901
|
+
if(run_spec.startsWith('n')) {
|
902
|
+
// #n may be followed by a range, or this range defaults to
|
903
|
+
// 0 - last run number. Of this range, the i-th number will
|
904
|
+
// be used, where i is computes as:
|
905
|
+
// floor(current time step * number of runs / period length)
|
906
|
+
const range = run_spec.substring(1);
|
907
|
+
// Call rangeToList only to validate the range syntax.
|
908
|
+
if(rangeToList(range)) {
|
909
|
+
x.nr = range;
|
910
|
+
this.is_static = false;
|
911
|
+
this.log('dynamic because experiment run number range');
|
912
|
+
} else {
|
913
|
+
msg = `Invalid experiment run number range "${range}"`;
|
914
|
+
}
|
922
915
|
} else {
|
923
|
-
// Explicit run number
|
924
|
-
|
916
|
+
// Explicit run number is specified.
|
917
|
+
const n = parseInt(run_spec);
|
918
|
+
if(isNaN(n)) {
|
919
|
+
msg = `Invalid experiment run number "${run_spec}"`;
|
920
|
+
} else {
|
921
|
+
// NOTE: Negative run numbers are acceptable.
|
922
|
+
x.r = n;
|
923
|
+
}
|
925
924
|
}
|
926
925
|
}
|
927
926
|
}
|
928
|
-
// NOTE:
|
929
|
-
|
930
|
-
|
931
|
-
// NOTE: title cannot be parametrized with a # wildcard
|
932
|
-
const n = MODEL.indexOfExperiment(s);
|
927
|
+
// NOTE: Experiment title cannot be parametrized with a # wildcard.
|
928
|
+
if(x_title) {
|
929
|
+
const n = MODEL.indexOfExperiment(x_title);
|
933
930
|
if(n < 0) {
|
934
|
-
msg = `Unknown experiment "${
|
931
|
+
msg = `Unknown experiment "${x_title}"`;
|
935
932
|
} else {
|
936
933
|
x.x = MODEL.experiments[n];
|
937
934
|
}
|
938
935
|
}
|
936
|
+
// END of code for parsing an experiment result specifier.
|
937
|
+
// Now proceed with parsing the variable name.
|
938
|
+
|
939
939
|
// Variable name may start with a (case insensitive) statistic
|
940
|
-
// specifier such as SUM or MEAN
|
940
|
+
// specifier such as SUM or MEAN.
|
941
941
|
s = name.split('$');
|
942
942
|
if(s.length > 1) {
|
943
943
|
const stat = s[0].trim().toUpperCase();
|
944
|
-
// NOTE:
|
945
|
-
// variable name) unless it is preceded by a valid statistic
|
944
|
+
// NOTE: Simply ignore $ (i.e., consider it as part of the
|
945
|
+
// variable name) unless it is preceded by a valid statistic.
|
946
946
|
if(VM.outcome_statistics.indexOf(stat) >= 0) {
|
947
947
|
x.s = stat;
|
948
948
|
name = s[1].trim();
|
@@ -7145,7 +7145,7 @@ function VMI_push_dataset_modifier(x, args) {
|
|
7145
7145
|
|
7146
7146
|
|
7147
7147
|
function VMI_push_run_result(x, args) {
|
7148
|
-
// NOTE:
|
7148
|
+
// NOTE: The first argument specifies the experiment run results:
|
7149
7149
|
// x: experiment object (FALSE indicates: use current experiment)
|
7150
7150
|
// r: integer number, or selector list
|
7151
7151
|
// v: variable index (integer number), or identifier (string)
|
@@ -7156,12 +7156,13 @@ function VMI_push_run_result(x, args) {
|
|
7156
7156
|
// t: if integer t > 0, use floor(current time step / t) as run number
|
7157
7157
|
const
|
7158
7158
|
rrspec = args[0],
|
7159
|
-
// NOTE:
|
7160
|
-
// a dataset modifier, use the time scale of the dataset, not of the
|
7161
|
-
// model, because the dataset vector is scaled to the model time scale
|
7159
|
+
// NOTE: When *expression* `x` for which this instruction is executed
|
7160
|
+
// is a dataset modifier, use the time scale of the dataset, not of the
|
7161
|
+
// model, because the dataset vector is scaled to the model time scale.
|
7162
7162
|
model_dt = MODEL.timeStepDuration;
|
7163
|
-
// NOTE:
|
7164
|
-
// better
|
7163
|
+
// NOTE: Run result can now default to UNDEFINED, because the VM now handles
|
7164
|
+
// exceptional values better: no call stack dump on "undefined" etc., but
|
7165
|
+
// only on real errors.
|
7165
7166
|
let v = rrspec.dv || VM.UNDEFINED;
|
7166
7167
|
if(rrspec && rrspec.hasOwnProperty('x')) {
|
7167
7168
|
let xp = rrspec.x,
|
@@ -7170,12 +7171,14 @@ function VMI_push_run_result(x, args) {
|
|
7170
7171
|
if(xp === false) xp = MODEL.running_experiment;
|
7171
7172
|
if(xp instanceof Experiment) {
|
7172
7173
|
if(Array.isArray(rn)) {
|
7174
|
+
// Let the running experiment infer run number from selector list `rn`
|
7175
|
+
// and its own "active combination" of selectors.
|
7173
7176
|
rn = xp.matchingCombinationIndex(rn);
|
7174
7177
|
} else if(rn < 0) {
|
7175
|
-
// Relative run number: use current run # + r (first run has number 0)
|
7178
|
+
// Relative run number: use current run # + r (first run has number 0).
|
7176
7179
|
rn += xp.active_combination_index;
|
7177
7180
|
} else if(rrspec.nr !== false) {
|
7178
|
-
// Run number inferred from local time step of expression
|
7181
|
+
// Run number inferred from local time step of expression.
|
7179
7182
|
const
|
7180
7183
|
rl = VM.nr_of_time_steps,
|
7181
7184
|
range = rangeToList(rrspec.nr, xp.runs.length - 1);
|
@@ -7186,9 +7189,9 @@ function VMI_push_run_result(x, args) {
|
|
7186
7189
|
rn = (ri < l ? range[ri] : range[l - 1]);
|
7187
7190
|
}
|
7188
7191
|
}
|
7189
|
-
// If variable is passed as identifier, get its index for the experiment
|
7192
|
+
// If variable is passed as identifier, get its index for the experiment.
|
7190
7193
|
if(typeof rri === 'string') rri = xp.resultIndex(rri);
|
7191
|
-
// Then proceed only if run number and result index both make sense
|
7194
|
+
// Then proceed only if run number and result index both make sense.
|
7192
7195
|
const run_count = (xp.completed ? xp.runs.length :
|
7193
7196
|
xp.active_combination_index);
|
7194
7197
|
if(rn !== false && rn >= 0 && rn < run_count) {
|
@@ -7233,11 +7236,14 @@ function VMI_push_run_result(x, args) {
|
|
7233
7236
|
} else {
|
7234
7237
|
// No statistic => return the vector for local time step
|
7235
7238
|
// using here, too, the delta-time-modifier to adjust the offsets
|
7236
|
-
// for different time steps per experiment
|
7239
|
+
// for different time steps per experiment.
|
7237
7240
|
const tot = twoOffsetTimeStep(x.step[x.step.length - 1],
|
7238
7241
|
args[1], args[2], args[3], args[4], dtm, x);
|
7239
7242
|
// Scale the (midpoint) time step (at current model run time scale)
|
7240
|
-
// to the experiment run time scale and get the run result value
|
7243
|
+
// to the experiment run time scale and get the run result value.
|
7244
|
+
// NOTE: the .m property specifies the time scaling method, and
|
7245
|
+
// the .p property whether the run result vector should be used as
|
7246
|
+
// a periodic time series.
|
7241
7247
|
v = rr.valueAtModelTime(tot[0], model_dt, rrspec.m, rrspec.p);
|
7242
7248
|
if(DEBUGGING) {
|
7243
7249
|
const trc = ['push run result: ', xp.title,
|