linny-r 2.0.9 → 2.0.11

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.
@@ -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();
@@ -951,7 +951,13 @@ class ExpressionParser {
951
951
  // Variable name may start with a colon to denote that the owner
952
952
  // prefix should be added.
953
953
  name = UI.colonPrefixedName(name, this.owner_prefix);
954
- if(x.x) {
954
+ // First check whether name refers to a valid attribute of an
955
+ // existing model entity.
956
+ const check = MODEL.validVariable(name);
957
+ if(check !== true) {
958
+ // If not TRUE, check will be an error message.
959
+ msg = check;
960
+ } else if(x.x) {
955
961
  // Look up name in experiment outcomes list.
956
962
  x.v = x.x.resultIndex(name);
957
963
  if(x.v < 0 && name.indexOf('#') >= 0 &&
@@ -989,7 +995,26 @@ class ExpressionParser {
989
995
  if(x.r === false && x.t === false) {
990
996
  msg = 'Experiment run not specified';
991
997
  } else if(x.v === false) {
992
- msg = `No experiments have variable "${name}" as result`;
998
+ // NOTE: Variable may not be defined as outcome of any experiment.
999
+ // This will be handled at runtime by VMI_push_run_result, but
1000
+ // it will be helpful to notify modelers at compile time when an
1001
+ // experiment is running, and also when they are editing an
1002
+ // expression (so when a modal dialog is showing).
1003
+ const
1004
+ notice = `No experiments have variable "${name}" as result`,
1005
+ tm = UI.topModal;
1006
+ // NOTE: Only notify when expression-editing modals are showing.
1007
+ if(tm) {
1008
+ const mid = tm.id.replace('-modal', '');
1009
+ if(['actor', 'note', 'link', 'boundline-data', 'process',
1010
+ 'product', 'equation', 'expression'].indexOf(mid) >= 0) {
1011
+ UI.notify(notice);
1012
+ }
1013
+ }
1014
+ if(MODEL.running_experiment) {
1015
+ // Log message only for block 1.
1016
+ VM.logMessage(1, VM.WARNING + notice);
1017
+ }
993
1018
  }
994
1019
  }
995
1020
  if(msg) {
@@ -2311,6 +2336,17 @@ class VirtualMachine {
2311
2336
  P: 'process',
2312
2337
  Q: 'product'
2313
2338
  };
2339
+ // Reverse lookup for entity letter codes.
2340
+ this.entity_letter_codes = {
2341
+ actor: 'A',
2342
+ constraint: 'B',
2343
+ cluster: 'C',
2344
+ dataset: 'D',
2345
+ equation: 'E',
2346
+ link: 'L',
2347
+ process: 'P',
2348
+ product: 'Q'
2349
+ };
2314
2350
  this.entity_letters = 'ABCDELPQ';
2315
2351
  // Standard attributes of Linny-R entities.
2316
2352
  this.attribute_names = {
@@ -7145,7 +7181,7 @@ function VMI_push_dataset_modifier(x, args) {
7145
7181
 
7146
7182
 
7147
7183
  function VMI_push_run_result(x, args) {
7148
- // NOTE: the first argument specifies the experiment run results:
7184
+ // NOTE: The first argument specifies the experiment run results:
7149
7185
  // x: experiment object (FALSE indicates: use current experiment)
7150
7186
  // r: integer number, or selector list
7151
7187
  // v: variable index (integer number), or identifier (string)
@@ -7156,12 +7192,13 @@ function VMI_push_run_result(x, args) {
7156
7192
  // t: if integer t > 0, use floor(current time step / t) as run number
7157
7193
  const
7158
7194
  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
7195
+ // NOTE: When *expression* `x` for which this instruction is executed
7196
+ // is a dataset modifier, use the time scale of the dataset, not of the
7197
+ // model, because the dataset vector is scaled to the model time scale.
7162
7198
  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)
7199
+ // NOTE: Run result can now default to UNDEFINED, because the VM now handles
7200
+ // exceptional values better: no call stack dump on "undefined" etc., but
7201
+ // only on real errors.
7165
7202
  let v = rrspec.dv || VM.UNDEFINED;
7166
7203
  if(rrspec && rrspec.hasOwnProperty('x')) {
7167
7204
  let xp = rrspec.x,
@@ -7170,12 +7207,14 @@ function VMI_push_run_result(x, args) {
7170
7207
  if(xp === false) xp = MODEL.running_experiment;
7171
7208
  if(xp instanceof Experiment) {
7172
7209
  if(Array.isArray(rn)) {
7210
+ // Let the running experiment infer run number from selector list `rn`
7211
+ // and its own "active combination" of selectors.
7173
7212
  rn = xp.matchingCombinationIndex(rn);
7174
7213
  } else if(rn < 0) {
7175
- // Relative run number: use current run # + r (first run has number 0)
7214
+ // Relative run number: use current run # + r (first run has number 0).
7176
7215
  rn += xp.active_combination_index;
7177
7216
  } else if(rrspec.nr !== false) {
7178
- // Run number inferred from local time step of expression
7217
+ // Run number inferred from local time step of expression.
7179
7218
  const
7180
7219
  rl = VM.nr_of_time_steps,
7181
7220
  range = rangeToList(rrspec.nr, xp.runs.length - 1);
@@ -7186,9 +7225,9 @@ function VMI_push_run_result(x, args) {
7186
7225
  rn = (ri < l ? range[ri] : range[l - 1]);
7187
7226
  }
7188
7227
  }
7189
- // If variable is passed as identifier, get its index for the experiment
7228
+ // If variable is passed as identifier, get its index for the experiment.
7190
7229
  if(typeof rri === 'string') rri = xp.resultIndex(rri);
7191
- // Then proceed only if run number and result index both make sense
7230
+ // Then proceed only if run number and result index both make sense.
7192
7231
  const run_count = (xp.completed ? xp.runs.length :
7193
7232
  xp.active_combination_index);
7194
7233
  if(rn !== false && rn >= 0 && rn < run_count) {
@@ -7233,11 +7272,14 @@ function VMI_push_run_result(x, args) {
7233
7272
  } else {
7234
7273
  // No statistic => return the vector for local time step
7235
7274
  // using here, too, the delta-time-modifier to adjust the offsets
7236
- // for different time steps per experiment
7275
+ // for different time steps per experiment.
7237
7276
  const tot = twoOffsetTimeStep(x.step[x.step.length - 1],
7238
7277
  args[1], args[2], args[3], args[4], dtm, x);
7239
7278
  // Scale the (midpoint) time step (at current model run time scale)
7240
- // to the experiment run time scale and get the run result value
7279
+ // to the experiment run time scale and get the run result value.
7280
+ // NOTE: the .m property specifies the time scaling method, and
7281
+ // the .p property whether the run result vector should be used as
7282
+ // a periodic time series.
7241
7283
  v = rr.valueAtModelTime(tot[0], model_dt, rrspec.m, rrspec.p);
7242
7284
  if(DEBUGGING) {
7243
7285
  const trc = ['push run result: ', xp.title,