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.
@@ -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
- if(this.experiments[index].title === t) return index;
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
- dsl = [];
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)) dsl.push(k);
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 ds of dsl) {
1098
- const nk = newkey + ds.substring(oldkey.length);
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(dsl.length, 'dataset') +
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.entity_count = 0;
1115
+ this.variable_count = 0;
1109
1116
  this.expression_count = 0;
1110
1117
  // Rename datasets one by one, suppressing notifications.
1111
- for(const ds of dsl) {
1112
- const d = MODEL.datasets[ds];
1113
- d.rename(d.displayName.replace(old_prefix, new_prefix), false);
1114
- }
1115
- let msg = 'Renamed ' + pluralS(dsl.length, 'dataset').toLowerCase();
1116
- if(MODEL.variable_count) msg += ', and updated ' +
1117
- pluralS(MODEL.variable_count, 'variable') + ' in ' +
1118
- pluralS(MODEL.expression_count, 'expression');
1119
- UI.notify(msg);
1120
- if(EXPERIMENT_MANAGER.selected_experiment) {
1121
- EXPERIMENT_MANAGER.selected_experiment.inferVariables();
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><time-steps>', this.time_steps,
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: separated from basic initialization so that it can be called
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 = 0;
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 combination with most selectors in common with `sl`.
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
- // NOTE: Results of current run are not available yet, hence length-1.
11644
- for(let ci = 0; ci < this.active_combination_index; ci++) {
11645
- const l = intersection(sl, this.combinations[ci]).length;
11646
- if(l > high) {
11647
- high = l;
11648
- index = ci;
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
- // NOTE: The owner prefix includes the trailing colon+space.
675
- if(owner instanceof Link || owner instanceof Constraint) {
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
- // Run specifier (optional) must be leading and braced
852
- // Specifier format: {method$title|run} where method and title are
853
- // optional -- NOTE: # in title or run is NOT seen as a wildcard
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: name should then be in the experiment's variable list
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: simply ignore $ unless it indicates a valid method
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 = s.split('#');
886
- let rn = (s.length > 1 ? s[1].trim() : false);
887
- // Experiment specifier may contain modifier selectors.
888
- s = s[0].trim().split(UI.OA_SEPARATOR);
889
- if(s.length > 1) {
890
- // If so, the selector list may indicate the run number.
891
- // NOTE: permit selectors to be separated by various characters.
892
- x.r = s.slice(1).join('|').split(/[\|\,\.\:\;\/\s]+/g);
893
- }
894
- if(rn) {
895
- // NOTE: Special notation for run numbers to permit modelers
896
- // to chart results as if run numbers are on the time axis
897
- // (with a given step size). The chart will be made as usual,
898
- // i.e., plot a point for each time step t, but the value v[t]
899
- // will then stay the same for the time interval that corresponds
900
- // to simulation period length / number of runs.
901
- // NOTE: This will fail to produce a meaningful chart when the
902
- // simulation period is small compared to the number of runs.
903
- if(rn.startsWith('n')) {
904
- // #n may be followed by a range, or this range defaults to
905
- // 0 - last run number. Of this range, the i-th number will
906
- // be used, where i is computes as:
907
- // floor(current time step * number of runs / period length)
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
- // Explicit run number is specified.
919
- const n = parseInt(rn);
920
- if(isNaN(n)) {
921
- msg = `Invalid experiment run number "${rn}"`;
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 overrules selector list.
924
- x.r = n;
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: s[0] still holds the experiment title
929
- s = s[0].trim();
930
- if(s) {
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 "${s}"`;
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: simply ignore $ (i.e., consider it as part of the
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: the first argument specifies the experiment run results:
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: when expression `x` for which this instruction is executed is
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: run result now defaults to UNDEFINED, because the VM handles errors
7164
- // better now (no call stack dump on "undefined" etc., but only on errors)
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,