linny-r 1.3.4 → 1.4.0

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.
@@ -36,36 +36,56 @@ SOFTWARE.
36
36
  // CLASS Expression (for all potentially time-dependent model parameters)
37
37
  class Expression {
38
38
  constructor(obj, attr, text) {
39
+ // Expressions are typically defined for some attribute of some
40
+ // entity -- legacy convention is to refer to a model entity
41
+ // as `object` rather than `entity`.
39
42
  this.object = obj;
40
43
  this.attribute = attr;
41
44
  this.text = text;
42
- // A stack for local time step (to allow lazy evaluation)
45
+ // A stack for local time step (to allow lazy evaluation).
43
46
  this.step = [];
44
- // An operand stack for computation (elements must be numeric)
47
+ // An operand stack for computation (elements must be numeric).
45
48
  this.stack = [];
46
- // NOTE: code = NULL indicates: not compiled yet
49
+ // NOTE: code = NULL indicates: not compiled yet.
47
50
  this.code = null;
48
- // NOTE: use a semaphore to prevent cyclic recursion
51
+ // NOTE: Use a semaphore to prevent cyclic recursion.
49
52
  this.compiling = false;
50
- // While compiling, check whether any operand depends on time
53
+ // While compiling, check whether any operand depends on time.
51
54
  this.is_static = true;
52
- // Likewise, check whether any operand is computed by the solver
55
+ // Likewise, check whether any operand is computed by the solver.
53
56
  this.is_level_based = false;
54
- // NOTE: VM expects result to be an array, even when expression is static
57
+ // NOTE: VM expects result to be an array, even when expression is static.
55
58
  this.vector = [VM.NOT_COMPUTED];
56
59
  // For dataset *wildcard* modifier expressions, results are stored in a
57
- // separate vector for each wildcard number (list expands "lazily")
58
- this.wildcard_vectors = [];
60
+ // separate vector for each wildcard number. The set of vectors expands
61
+ // "lazily", as new entries (number: vector) are added only when the
62
+ // expression is evaluated for a different wildcard number.
63
+ // For all other expressions (typically properties of nodes), the
64
+ // normal expression vector is used.
65
+ // NOTE: Wildcard expressions merit special treatment when plotted in
66
+ // a chart.
67
+ this.wildcard_vectors = {};
68
+ // NOTE: `wildcard_number` is used -- only during code execution --
69
+ // by VMI_push_dataset_modifier (read and/or write) and by
70
+ // VMI_push_contextual_number (read only).
71
+ this.wildcard_vector_index = false;
59
72
  // Special instructions can store results as cache properties to save
60
- // (re)computation time; cache is cleared when expression is reset
73
+ // (re)computation time; cache is cleared when expression is reset.
61
74
  this.cache = {};
62
75
  }
63
76
 
77
+ get isWildcardExpression() {
78
+ // Returns TRUE if the owner is a dataset, and the attribute contains
79
+ // wildcards.
80
+ return this.object instanceof Dataset &&
81
+ this.object.isWildcardSelector(this.attribute);
82
+ }
83
+
64
84
  get variableName() {
65
85
  // Return the name of the variable computed by this expression
66
- if(this.attribute === 'C') return 'note color expression';
67
86
  if(this.object === MODEL.equations_dataset) return 'equation ' + this.attribute;
68
- return this.object.displayName + UI.OA_SEPARATOR + this.attribute;
87
+ if(this.object) return this.object.displayName + UI.OA_SEPARATOR + this.attribute;
88
+ return 'Unknown variable (no object)';
69
89
  }
70
90
 
71
91
  get timeStepDuration() {
@@ -78,7 +98,7 @@ class Expression {
78
98
  }
79
99
 
80
100
  get referencedEntities() {
81
- // Returns a list of entities referenced in this expression
101
+ // Returns a list of entities referenced in this expression.
82
102
  if(this.text.indexOf('[') < 0) return [];
83
103
  const
84
104
  el = [],
@@ -110,11 +130,8 @@ class Expression {
110
130
  // Clears result of previous computation (if any)
111
131
  this.step.length = 0;
112
132
  this.stack.length = 0;
113
- // NOTE: `wildcard_number` is used only during code execution;
114
- // then it may be set only by VMI_push_dataset_modifier,
115
- // and read only by VMI_push_selector_wildcard
116
- this.wildcard_number = false;
117
- this.wildcard_vectors = [];
133
+ this.wildcard_vectors = {};
134
+ this.wildcard_vector_index = false;
118
135
  this.cache = {};
119
136
  this.compile(); // if(!this.compiled) REMOVED to ensure correct isStatic!!
120
137
  // Static expressions only need a vector with one element (having index 0)
@@ -141,20 +158,18 @@ class Expression {
141
158
  }
142
159
 
143
160
  compile() {
144
- // Do not compile recursively
161
+ // Do not compile recursively.
145
162
  if(this.compiling) return;
146
- // Set the "compiling" flag to prevent cyclic recursion
163
+ // Set the "compiling" flag to prevent cyclic recursion.
147
164
  this.compiling = true;
148
- // Clear the VM instruction list
165
+ // Clear the VM instruction list.
149
166
  this.code = null;
150
167
  const xp = new ExpressionParser(this.text, this.object, this.attribute);
151
168
  if(xp.error === '') {
152
- // NOTE: except for dataset modifiers and note colors, expressions
169
+ // NOTE: Except for dataset modifiers and note colors, expressions
153
170
  // should not be based on levels-still-to-be-computed-by-the-solver,
154
- // so caution the modeler when this appears to be the case
155
- // NOTE: when note color expressions are edited, they are compiled
156
- // to check their syntax; then their object is null
157
- if(xp.is_level_based && this.object &&
171
+ // so caution the modeler when this appears to be the case.
172
+ if(xp.is_level_based &&
158
173
  !(this.object instanceof Dataset || this.object instanceof Note)) {
159
174
  // NOTE: this should not occur, so log more details
160
175
  console.log('Level-based issue:',
@@ -176,7 +191,7 @@ class Expression {
176
191
  }
177
192
 
178
193
  get asXML() {
179
- // Returns XML-encoded expression (after replacing "black-boxed" entities)
194
+ // Returns XML-encoded expression after replacing "black-boxed" entities.
180
195
  let text = this.text;
181
196
  if(MODEL.black_box) {
182
197
  // Get all entity names that occur in this expression
@@ -219,17 +234,21 @@ class Expression {
219
234
  }
220
235
 
221
236
  get defined() {
237
+ // Returns TRUE if the expression string is not empty.
222
238
  return this.text !== '';
223
239
  }
224
240
 
225
241
  get compiled() {
242
+ // Returns TRUE if there is code for this expression.
243
+ // NOTE: The expression parser sets `code` to NULL when compiling an
244
+ // empty string.
226
245
  return this.code !== null;
227
246
  }
228
247
 
229
248
  get isStatic() {
230
- // Returns is_static property AFTER compiling if not compiled yet
231
- // NOTE: to prevent cylic recursion, return FALSE if this expression is
232
- // already being compiled
249
+ // Returns is_static property AFTER compiling if not compiled yet.
250
+ // NOTE: To prevent cylic recursion, return FALSE if this expression is
251
+ // already being compiled.
233
252
  if(this.compiling) return false;
234
253
  if(!this.compiled) this.compile();
235
254
  return this.is_static;
@@ -287,7 +306,7 @@ class Expression {
287
306
  // Compute a value only once
288
307
  if(v[t] !== VM.NOT_COMPUTED) return true;
289
308
  // Provide selector context for # (number = FALSE => no wildcard match)
290
- this.wildcard_number = number;
309
+ this.wildcard_vector_index = number;
291
310
  // Push this expression onto the call stack
292
311
  VM.call_stack.push(this);
293
312
  // Push time step in case a VMI_push_var instruction references
@@ -325,7 +344,7 @@ class Expression {
325
344
  this.step.pop();
326
345
  this.trace('--STOP: ' + this.variableName);
327
346
  // Clear context for #
328
- this.wildcard_number = false;
347
+ this.wildcard_vector_index = false;
329
348
  // If error, display the call stack (only once)
330
349
  // NOTE: "undefined", "not computed" and "still computing" are NOT
331
350
  // problematic unless they result in an error (stack over/underflow)
@@ -346,6 +365,7 @@ class Expression {
346
365
  // "initial value" (these follow from the variables used in the expression)
347
366
  // Select the vector to use
348
367
  const v = this.chooseVector(number);
368
+ if(!v) return VM.UNDEFINED;
349
369
  if(t < 0 || this.isStatic) t = 0;
350
370
  if(t >= v.length) return VM.UNDEFINED;
351
371
  if(v[t] === VM.NOT_COMPUTED || v[t] === VM.COMPUTING) {
@@ -487,46 +507,130 @@ class Expression {
487
507
 
488
508
  // CLASS ExpressionParser
489
509
  // Instances of ExpressionParser compile expressions into code, i.e.,
490
- // an array of VM instructions. The optional parameters `owner` and `attribute`
491
- // are used to prefix "local" entities, and to implement modifier expressions
492
- // that contain the dot that (when used within brackets) denotes the data value
493
- // of the dataset
510
+ // an array of VM instructions. The optional parameters `owner` and
511
+ // `attribute` are used to prefix "local" entities, and also to implement
512
+ // modifier expressions that contain the "dot" that (when used within
513
+ // brackets) denotes the data value of the dataset.
514
+
515
+ // Since version 1.4.0, a leading colon indicates that the variable
516
+ // "inherits" the prefixes of its owner. Thus, for example, in the expression
517
+ // for the upper bound of proces "Storage: Li-ion: battery 1", the variable
518
+ // [:capacity] will be interpreted as [Storage: Li-ion: capacity].
519
+ // The prefixed name will be parsed normally, so if "Storage: Li-ion: capacity"
520
+ // identifies an array-type dataset, [:capacity@#] will work, since the
521
+ // value of # can be inferred from the expression owner's name
522
+ // "Storage: Li-ion: battery 1".
523
+
524
+ // Also since version 1.4.0, the context sensitive number # can also be used
525
+ // as a "wildcard" in an entity name. This is useful mainly when combined with
526
+ // wildcard equations with names like "eq??ion" which can then be referred to
527
+ // in expressions not only as "eq12ion" (then # in the expression for the
528
+ // wildcard equation evaluates as 12), but also as "eq#ion" (then # in the
529
+ // expression for the wildcard equation will have the value of # in the
530
+ // "calling" expression. This permits, for example, defining as single
531
+ // equation "partial load ??" with expression "[P#|L] / [P#|UB]", and then
532
+ // using the variable [partial load 1] to compute the partial load for
533
+ // process P1.
534
+ // NOTES:
535
+ // (1) This will (for now) NOT apply recursively, so [partial load #] cannot
536
+ // be used in some other wildcard equation like, for example, "percent load ??"
537
+ // having expression "100 * [partial load #]". This restriction follows from
538
+ // the present limitation that only ONE context-sensitive number exists, which
539
+ // makes that "nested" wildcard expressions can always be rewritten as a single
540
+ // expression.
541
+ // (2) The # may be used in patterns, so when a model comprises processes
542
+ // P1 and P2, and products Q2 and Q3, and a wildcard equation "total level ??"
543
+ // with expression "[SUM$#|L]", then [total level 1] will evaluate as the level
544
+ // of P1, and [total level 2] as the level of P2 plus the level of Q3.
545
+
494
546
  class ExpressionParser {
495
547
  constructor(text, owner=null, attribute='') {
496
- // `text` is the expression string to be parsed
548
+ // `text` is the expression string to be parsed.
497
549
  this.expr = text;
550
+ // NOTE: When expressions for dataset modifiers or equations are
551
+ // parsed, `owner` is their dataset, and `attribute` is their name.
498
552
  this.owner = owner;
553
+ this.owner_prefix = '';
499
554
  this.attribute = attribute;
500
- // If `owner` is a dataset, then it is the dataset denoted by . (dot)
501
- this.dataset = (owner instanceof Dataset ? owner : null);
502
- this.selector = (this.dataset ? attribute : '');
555
+ this.dataset = null;
556
+ this.dot = null;
557
+ this.selector = '';
558
+ this.context_number = '';
559
+ this.wildcard_selector = false;
560
+ this.wildcard_equation = false;
561
+ // Always infer the value for the context-sensitive number #.
562
+ // NOTE: this will be "?" if `owner` is a dataset modifier with
563
+ // wildcards in its selector; this indicates that the value of #
564
+ // cannot be inferred at compile time.
565
+ if(owner) {
566
+ this.context_number = owner.numberContext;
567
+ // NOTE: The owner prefix includes the trailing colon+space.
568
+ if(owner instanceof Link || owner instanceof Constraint) {
569
+ // For links and constraints, use the longest prefix that
570
+ // their nodes have in common.
571
+ this.owner_prefix = UI.sharedPrefix(owner.from_node.displayName,
572
+ owner.to_node.displayName) + UI.PREFIXER;
573
+ } else if(owner === MODEL.equations_dataset) {
574
+ this.owner_prefix = UI.completePrefix(attribute);
575
+ } else {
576
+ this.owner_prefix = UI.completePrefix(owner.displayName);
577
+ }
578
+ if(owner instanceof Dataset) {
579
+ this.dataset = owner;
580
+ // The attribute (if specified) is a dataset modifier selector.
581
+ this.selector = attribute;
582
+ // Record whether this selector contains wildcards.
583
+ this.wildcard_selector = owner.isWildcardSelector(attribute);
584
+ if(owner === MODEL.equations_dataset) {
585
+ // Reocrd that the owner is a wildcard equation.
586
+ this.wildcard_equation = this.wildcard_selector;
587
+ } else {
588
+ // For "normal" modifier expressions, the "dot" (.) can be used
589
+ // to refer to the dataset of the modifier.
590
+ this.dot = this.dataset;
591
+ }
592
+ }
593
+ }
594
+ // Immediately compile; this may generate warnings
503
595
  this.compile();
504
596
  }
505
-
506
- log(msg) {
507
- if(false) { // Set to TRUE to profile dynamic expressions
508
- console.log(`Expression for ${this.owner.displayName}|${this.attribute}
509
- ${this.expr}\n${msg}`);
597
+
598
+ get ownerName() {
599
+ // FOR TRACING & DEBUGGING: Returns the owner of this equation (if any).
600
+ if(!this.owner) return '(no owner)';
601
+ let n = this.owner.displayName;
602
+ if(this.attribute) n += '|' + this.attribute;
603
+ if(this.wildcard_selector) {
604
+ n += ' [wildcard ' + (this.wildcard_equation ? 'equation' : 'modifier') +
605
+ (this.context_number ? ' # = ' + this.context_number : '') + ']';
510
606
  }
607
+ return n;
511
608
  }
512
609
 
513
- // Checks whether name fits this pattern:
610
+ log(msg) {
611
+ // FOR TRACING & DEBUGGING: Logs a message to the console.
612
+ if(true) return;
613
+ // Set the above IF condition to FALSE to profile dynamic expressions.
614
+ console.log(`Expression for ${this.ownerName}: ${this.expr}\n${msg}`);
615
+ }
616
+
617
+ // The method parseVariable(name) checks whether `name` fits this pattern:
514
618
  // {run}statistic$entity|attribute@offset_1:offset_2
515
619
  // allowing spaces within {run} and around | and @ and :
516
620
  // The entity is mandatory, but {run} and statistic$ are optional, and
517
621
  // attribute and offset have defaults.
518
- // Returns array [object, anchor_1, offset_1, anchor_2, offset_2] if the
519
- // pattern matches and no statistic, or the 6-element array
622
+ // It returns array [object, anchor_1, offset_1, anchor_2, offset_2] if
623
+ // the pattern matches and no statistic, or the 6-element array
520
624
  // [statistic, object list, anchor, offset, anchor_2, offset_2]
521
- // if a valid statistic is specified; otherwise FALSE.
625
+ // if a valid statistic is specified; otherwise it returns FALSE.
522
626
  // The object is either a vector or an expression, or a special object
523
627
  // (dataset specifier, experiment run specifier or unit balance specifier)
524
628
  // NOTE: this array is used as argument for the virtual machine instructions
525
- // VMI_push_var, VMI_push_statistic and VMI_push_run_result
629
+ // VMI_push_var, VMI_push_statistic and VMI_push_run_result.
526
630
  parseVariable(name) {
527
- // Reduce whitespace to single space
631
+ // Reduce whitespace to single space.
528
632
  name = name.replace(/\s+/g, ' ');
529
- // Initialize possible components
633
+ // Initialize possible components.
530
634
  let obj = null,
531
635
  attr = '',
532
636
  use_data = false,
@@ -539,6 +643,7 @@ class ExpressionParser {
539
643
  args = null,
540
644
  s = name.split('@');
541
645
  if(s.length > 1) {
646
+ // [variable@offset] where offset has form (anchor1)number1(:(anchor2)number 2)
542
647
  // Offsets make expression dynamic (for now, ignore exceptional cases)
543
648
  this.is_static = false;
544
649
  this.log('dynamic because of offset');
@@ -586,9 +691,7 @@ class ExpressionParser {
586
691
  }
587
692
  // Check whether # anchor is meaningful for this expression
588
693
  if((anchor1 === '#' || anchor2 === '#') &&
589
- !(this.selector.indexOf('*') >= 0 ||
590
- this.selector.indexOf('?') >= 0 ||
591
- this.owner.numberContext)) {
694
+ !(this.wildcard_selector || this.context_number)) {
592
695
  // Log debugging information for this error
593
696
  console.log(this.owner.displayName, this.owner.type, this.selector);
594
697
  this.error = 'Anchor # is undefined in this context';
@@ -597,7 +700,8 @@ class ExpressionParser {
597
700
  }
598
701
  }
599
702
  // Run specifier (optional) must be leading and braced
600
- // Specifier format: {method$title|run} where method and title are optional
703
+ // Specifier format: {method$title|run} where method and title are
704
+ // optional -- NOTE: # in title or run is NOT seen as a wildcard
601
705
  if(name.startsWith('{')) {
602
706
  s = name.split('}');
603
707
  if(s.length > 1) {
@@ -631,20 +735,29 @@ class ExpressionParser {
631
735
  }
632
736
  s = s.split('#');
633
737
  let rn = (s.length > 1 ? s[1].trim() : false);
634
- // Experiment specifier may contain selectors
738
+ // Experiment specifier may contain modifier selectors.
635
739
  s = s[0].trim().split(UI.OA_SEPARATOR);
636
740
  if(s.length > 1) {
637
- // If so, the selector list may indicate the run number
638
- // NOTE: permit selectors to be separated by various characters
741
+ // If so, the selector list may indicate the run number.
742
+ // NOTE: permit selectors to be separated by various characters.
639
743
  x.r = s.slice(1).join('|').split(/[\|\,\.\:\;\/\s]+/g);
640
744
  }
641
745
  if(rn) {
746
+ // NOTE: Special notation for run numbers to permit modelers
747
+ // to chart results as if run numbers are on the time axis
748
+ // (with a given step size). The chart will be made as usual,
749
+ // i.e., plot a point for each time step t, but the value v[t]
750
+ // will then stay the same for the time interval that corresponds
751
+ // to simulation period length / number of runs.
752
+ // NOTE: This will fail to produce a meaningful chart when the
753
+ // simulation period is small compared to the number of runs.
642
754
  if(rn.startsWith('n')) {
643
- // #n may be followed by a range, or defaults to 0 - last run number
644
- // Of this range, the i-th number will be used, where i equals
755
+ // #n may be followed by a range, or this range defaults to
756
+ // 0 - last run number. Of this range, the i-th number will
757
+ // be used, where i is computes as:
645
758
  // floor(current time step * number of runs / period length)
646
759
  const range = rn.substring(1);
647
- // Call rangeToList only to validate the range syntax
760
+ // Call rangeToList only to validate the range syntax.
648
761
  if(rangeToList(range)) {
649
762
  x.nr = range;
650
763
  this.is_static = false;
@@ -653,12 +766,12 @@ class ExpressionParser {
653
766
  msg = `Invalid experiment run number range "${range}"`;
654
767
  }
655
768
  } else {
656
- // Explicit run number
769
+ // Explicit run number is specified.
657
770
  const n = parseInt(rn);
658
771
  if(isNaN(n)) {
659
772
  msg = `Invalid experiment run number "${rn}"`;
660
773
  } else {
661
- // Explicit run number overrules selector list
774
+ // Explicit run number overrules selector list.
662
775
  x.r = n;
663
776
  }
664
777
  }
@@ -666,7 +779,7 @@ class ExpressionParser {
666
779
  // NOTE: s[0] still holds the experiment title
667
780
  s = s[0].trim();
668
781
  if(s) {
669
- // NOTE: title cannot be parametrized
782
+ // NOTE: title cannot be parametrized with a # wildcard
670
783
  const n = MODEL.indexOfExperiment(s);
671
784
  if(n < 0) {
672
785
  msg = `Unknown experiment "${s}"`;
@@ -674,24 +787,30 @@ class ExpressionParser {
674
787
  x.x = MODEL.experiments[n];
675
788
  }
676
789
  }
677
- // Variable name may start with a (case insensitive) statistic specifier
790
+ // Variable name may start with a (case insensitive) statistic
791
+ // specifier such as SUM or MEAN
678
792
  s = name.split('$');
679
793
  if(s.length > 1) {
680
794
  const stat = s[0].trim().toUpperCase();
681
- // NOTE: simply ignore $ unless it indicates a valid statistic
795
+ // NOTE: simply ignore $ (i.e., consider it as part of the
796
+ // variable name) unless it is preceded by a valid statistic
682
797
  if(VM.outcome_statistics.indexOf(stat) >= 0) {
683
798
  x.s = stat;
684
799
  name = s[1].trim();
685
800
  }
686
801
  }
802
+ // Variable name may start with a colon to denote that the owner
803
+ // prefix should be added.
804
+ name = UI.colonPrefixedName(name, this.owner_prefix);
687
805
  if(x.x) {
688
- // NOTE: variable name may be parametrized
689
- const vpar = name.split('\\');
690
- if(vpar.length > 1) {
691
- // @@ TO DO!
692
- }
693
806
  // Look up name in experiment outcomes list
694
807
  x.v = x.x.resultIndex(name);
808
+ if(x.v < 0 && name.indexOf('#') >= 0 && this.context_number) {
809
+ // Variable name may be parametrized with #, but not in
810
+ // expressions for wildcard selectors
811
+ name = name.replace('#', this.context_number);
812
+ x.v = x.x.resultIndex(name);
813
+ }
695
814
  if(x.v < 0) {
696
815
  msg = ['Variable "', name, '" is not a result of experiment "',
697
816
  x.x.displayName, '"'].join('');
@@ -699,15 +818,22 @@ class ExpressionParser {
699
818
  } else {
700
819
  // Check outcome list of ALL experiments
701
820
  for(let i = 0; i < MODEL.experiments.length; i++) {
702
- if(MODEL.experiments[i].resultIndex(name) >= 0) {
821
+ let xri = MODEL.experiments[i].resultIndex(name);
822
+ if(xri < 0 && name.indexOf('#') >= 0 && this.context_number) {
823
+ // Variable name may be parametrized with #, but not in
824
+ // expressions for wildcard selectors
825
+ name = name.replace('#', this.context_number);
826
+ xri = MODEL.experiments[i].resultIndex(name);
827
+ }
828
+ if(xri >= 0) {
703
829
  // If some match is found, the name specifies a variable
704
- x.v = name;
830
+ x.v = xri;
705
831
  break;
706
832
  }
707
833
  }
708
834
  }
709
- // NOTE: experiment may still be FALSE, as this will be interpreted as
710
- // "use current experiment", but run number should be specified
835
+ // NOTE: experiment may still be FALSE, as this will be interpreted
836
+ // as "use current experiment", but run number should be specified.
711
837
  if(!msg) {
712
838
  if(x.r === false && x.t === false) {
713
839
  msg = 'Experiment run not specified';
@@ -719,107 +845,139 @@ class ExpressionParser {
719
845
  this.error = msg;
720
846
  return false;
721
847
  }
722
- // Notify modeler when two statistics are used
848
+ // Notify modeler when two statistics are used.
723
849
  if(x.s && x.m) {
724
850
  UI.notify(`Method statistic (${x.m}) does not apply to ` +
725
851
  `run result statistic (${x.s})`);
726
852
  }
727
- // NOTE: using AGGREGATED run results does NOT make the expression
728
- // dynamic, so only set is_static to FALSE if NO statistic or method
853
+ // NOTE: Using AGGREGATED run results does NOT make the expression
854
+ // dynamic, so only set is_static to FALSE if NO statistic or method.
729
855
  if(!x.s && !x.m) {
730
856
  this.is_static = false;
731
857
  this.log('dynamic because UNaggregated experiment result');
732
858
  }
733
- // For experiment run results, default anchor is 't'
859
+ // For experiment run results, default anchor is 't'.
734
860
  if(!anchor1) anchor1 = 't';
735
861
  if(!anchor2) anchor2 = 't';
736
- // NOTE: compiler will recognize `x` to indicate "push run results"
862
+ // NOTE: compiler will recognize `x` to indicate "push run results".
737
863
  return [x, anchor1, offset1, anchor2, offset2];
738
864
  }
739
865
  }
740
866
 
741
867
  //
742
- // NOTE: for experiment results, the method will ALWAYS have returned
743
- // a result, so what follows does not apply to experiment results
868
+ // NOTE: For experiment results, the method will ALWAYS have returned
869
+ // a result, so what follows does not apply to experiment results.
744
870
  //
745
871
 
746
- // Attribute name (optional) follows object-attribute separator |
872
+ // If reached this stage, variable must be like this:
873
+ // [(statistic$)entity name pattern(|attribute)]
874
+ // Attribute name (optional) follows the object-attribute separator |
747
875
  s = name.split(UI.OA_SEPARATOR);
748
876
  if(s.length > 1) {
749
- // Attribute is string after LAST separator ...
877
+ // Attribute is string after the LAST separator...
750
878
  attr = s.pop().trim();
751
- // ... so restore name if itself contains other separators
879
+ // ... so restore `name` in case itself contains other separators.
752
880
  name = s.join(UI.OA_SEPARATOR).trim();
753
881
  if(!attr) {
754
882
  // Explicit *empty* attribute, e.g., [name|]
755
- // NOTE: this matters for datasets having specifiers: the vertical
883
+ // NOTE: This matters for datasets having specifiers: the vertical
756
884
  // bar indicates "do not infer a modifier from a running experiment,
757
- // but use the data"
885
+ // but use the data".
758
886
  use_data = true;
759
887
  } else if(attr.startsWith('=')) {
760
888
  // Attribute starting with = indicates cluster balance
761
- // NOTE: empty string is considered as "any unit"
889
+ // NOTE: empty string is considered as "any unit".
762
890
  cluster_balance_unit = attr.substring(1).trim();
891
+ } else if(attr.indexOf('?') >= 0 || attr.indexOf('#') >= 0) {
892
+ // Wildcard selectors of dataset modifiers cannot be used.
893
+ this.error = `Invalid attribute "${attr}"`;
894
+ return false;
763
895
  }
764
896
  }
765
897
 
766
- // Check whether statistic is specified
898
+ // Check whether a statistic is specified.
767
899
  let pat = name.split('$');
768
900
  if(pat.length > 1 &&
769
901
  VM.statistic_operators.indexOf(pat[0].toUpperCase()) >= 0) {
770
- // For statistics, default anchor is 't'
902
+ // For statistics, default anchor is 't'.
771
903
  if(!anchor1) anchor1 = 't';
772
904
  if(!anchor2) anchor2 = 't';
773
- // Check whether unit balance for clusters is asked for
905
+ // Check whether unit balance for clusters is asked for.
774
906
  if(cluster_balance_unit !== false) {
775
907
  this.error = 'Aggregation of unit balance over clusters is not supported';
776
908
  return false;
777
909
  }
778
- // Consider only the first $ as statistic separator ...
910
+ // Consider only the first $ as statistic separator.
779
911
  const stat = pat.shift().toUpperCase();
780
- // Reassemble pattern string, which may itself contain $
912
+ // Reassemble pattern string, which may itself contain $.
781
913
  pat = pat.join('$');
782
- // Special case: dataset "dot" is NOT a pattern
914
+ // Special case: dataset "dot" is NOT a pattern.
783
915
  if(pat === '.') {
784
- // NOTE: "dot" dataset is not level-dependent, and statistics over its
785
- // vector do NOT make the expression dynamic
786
- if(this.dataset) {
787
- return [stat, [this.dataset.vector], anchor1, offset1, anchor2, offset2];
916
+ // NOTE: The "dot" dataset is not level-dependent, and statistics
917
+ // over its vector do NOT make the expression dynamic.
918
+ if(this.dot) {
919
+ return [stat, [this.dot.vector], anchor1, offset1, anchor2, offset2];
788
920
  } else {
789
- msg = UI.ERROR.NO_DATASET_DOT;
921
+ this.error = UI.ERROR.NO_DATASET_DOT;
790
922
  return false;
791
923
  }
792
924
  }
793
- // @@@TO DO: also deal with "prefix inheritance" when pattern starts with :
794
- // By default, consider all entity types
925
+ // Deal with "prefix inheritance" when pattern starts with a colon.
926
+ if(pat.startsWith(':')) {
927
+ // Add a "must start with" AND condition to all OR clauses of the
928
+ // pattern.
929
+ // NOTE: Issues may occur when prefix contains &, ^ or # (@@TO DO?)
930
+ const oc = pat.substring(1).split('|');
931
+ for(let i = 0; i < oc.length; i++) {
932
+ oc[i] = `~${this.owner_prefix}&${oc[i]}`;
933
+ }
934
+ pat = oc.join('|');
935
+ }
936
+ // NOTE: For patterns, assume that # *always* denotes the context-
937
+ // sensitive number #, because if modelers wishes to include
938
+ // ANY number, they can make their pattern less selective.
939
+ if(this.context_number) pat = pat.replace('#', this.context_number);
940
+ // By default, consider all entity types.
795
941
  let et = VM.entity_letters,
796
942
  patstr = pat;
797
- // Selection may be limited to specific entity types by prefic "...?"
943
+ // Selection may be limited to specific entity types by prefix "...?"
944
+ // where ... is one or more entity letters (A for actor, etc.).
798
945
  if(/^[ABCDELPQ]+\?/i.test(pat)) {
799
946
  pat = pat.split('?');
800
947
  et = pat[0].toUpperCase();
801
948
  pat = pat.slice(1).join('=');
802
949
  }
803
- // Get the name pattern
950
+ // Get the name pattern.
804
951
  pat = patternList(pat);
805
- // Infer the entity type(s) from the attribute (if defined)
952
+ // Infer the entity type(s) from the attribute (if defined).
806
953
  const
807
954
  list = [],
808
- // NOTE: optional second parameter `et` may limit the returned list
955
+ // NOTE: The optional second parameter `et` will limit the
956
+ // returned list to the specified entity types.
809
957
  ewa = MODEL.entitiesWithAttribute(attr, et);
810
- // Create list of expression objects for the matching entities
958
+ // Create list of expression objects for the matching entities.
959
+ // Also create a "dict" with for each matching wildcard number
960
+ // the matching entities as a separate list. This will permit
961
+ // narrowing the selection at run time, based on the expression's
962
+ // wildcard number.
963
+ const wdict = {};
811
964
  for(let i = 0; i < ewa.length; i++) {
812
965
  const e = ewa[i];
813
966
  if(patternMatch(e.displayName, pat)) {
814
- // NOTE: attribute may be a single value, a vector, or an expression
967
+ const mnr = matchingWildcardNumber(e.displayName, pat);
968
+ // NOTE: Attribute may be a single value, a vector, or an expression.
815
969
  obj = e.attributeValue(attr);
816
- // Neither a single value nor a vector => must be an expression
970
+ // If neither a single value nor a vector, it must be an expression.
817
971
  if(obj === null) obj = e.attributeExpression(attr);
818
- // Double-check: only add it if it is not NULL
972
+ // Double-check: only add it if it is not NULL.
819
973
  if(obj) {
820
974
  list.push(obj);
975
+ if(mnr) {
976
+ if(!wdict[mnr]) wdict[mnr] = [];
977
+ wdict[mnr].push(obj);
978
+ }
821
979
  // Expression becomes dynamic if any element that is added is
822
- // neither a single value nor a static expression
980
+ // neither a single value nor a static expression.
823
981
  if(Array.isArray(obj) ||
824
982
  (obj instanceof Expression && !obj.isStatic)) {
825
983
  this.is_static = false;
@@ -828,15 +986,18 @@ class ExpressionParser {
828
986
  }
829
987
  }
830
988
  }
831
- // NOTE: also add expressions for equations that match
832
- const edm = MODEL.equations_dataset.modifiers;
833
- for(let k in edm) if(edm.hasOwnProperty(k)) {
834
- const m = edm[k];
835
- if(patternMatch(m.selector, pat)) {
836
- list.push(m.expression);
837
- if(!m.expression.isStatic) {
838
- this.is_static = false;
839
- this.log('dynamic because matching equation is dynamic');
989
+ // NOTE: If no attribute is specified, also add expressions for
990
+ // equations that match UNLESS entity type specifier excludes them.
991
+ if(!attr && (!et || et.indexOf('E') >= 0)) {
992
+ const edm = MODEL.equations_dataset.modifiers;
993
+ for(let k in edm) if(edm.hasOwnProperty(k)) {
994
+ const m = edm[k];
995
+ if(patternMatch(m.selector, pat)) {
996
+ list.push(m.expression);
997
+ if(!m.expression.isStatic) {
998
+ this.is_static = false;
999
+ this.log('dynamic because matching equation is dynamic');
1000
+ }
840
1001
  }
841
1002
  }
842
1003
  }
@@ -848,8 +1009,10 @@ class ExpressionParser {
848
1009
  VM.level_based_attr.indexOf(attr) >= 0 &&
849
1010
  anchor1 === 't' && offset1 === 0 &&
850
1011
  anchor2 === 't' && offset2 === 0;
851
- // NOTE: compiler will recognize 6-element list as a "statistic"
852
- return [stat, list, anchor1, offset1, anchor2, offset2];
1012
+ const args = [stat, list, anchor1, offset1, anchor2, offset2];
1013
+ if(Object.keys(wdict).length > 0) args.push(wdict);
1014
+ // NOTE: Compiler will recognize 6- or 7-element list as a "statistic"
1015
+ return args;
853
1016
  }
854
1017
  this.error = `No entities that match pattern "${patstr}"` +
855
1018
  (attr ? ' and have attribute ' + attr : ' when no attribute is specified');
@@ -857,180 +1020,345 @@ class ExpressionParser {
857
1020
  }
858
1021
 
859
1022
  //
860
- // NOTE: for statistics, the method will ALWAYS have returned a result,
861
- // so what follows does not apply to statistics results
1023
+ // NOTE: For statistics, the method will ALWAYS have returned a result,
1024
+ // so what follows does not apply to statistics results, but only to
1025
+ // "plain" variables like [entity name(|attribute)].
862
1026
  //
863
1027
 
864
- let by_reference = false;
865
- if(name === '.') {
866
- // NOTE: when name is a single dot, it refers to the data of a dataset
867
- obj = this.dataset;
868
- if(!obj) msg = UI.ERROR.NO_DATASET_DOT;
869
- } else if(!name && !attr && this.dataset) {
870
- // This accepts auto-referencing variables like [@t-1]
871
- // Dataset modifier expressions can reference themselves, provided that
872
- // an offset is specified
873
- // This makes the expression dynamic
874
- this.is_static = false;
875
- this.log('dynamic because of self-reference');
1028
+ // For all entity types except array-type datasets, the default anchor
1029
+ // for offsets is the current time step `t`.
1030
+ if(!(this.dataset && this.dataset.array)) {
876
1031
  if(!anchor1) anchor1 = 't';
877
1032
  if(!anchor2) anchor2 = 't';
1033
+ }
1034
+ // First handle this special case: no name or attribute. This is valid
1035
+ // only for dataset modifier expressions (and hence also equations).
1036
+ // Variables like [@t-1] are interpreted as a self-reference. This is
1037
+ // meaningful when a *negative* offset is specified to denote "use the
1038
+ // value of this expression for some earlier time step".
1039
+ // NOTES:
1040
+ // (1) This makes the expression dynamic.
1041
+ // (2) It does not apply to array-type datasets, as these have no
1042
+ // time dimension.
1043
+ if(!name && !attr && this.dataset && !this.dataset.array) {
1044
+ this.is_static = false;
1045
+ this.log('dynamic because of self-reference');
878
1046
  if(('cips'.indexOf(anchor1) >= 0 || anchor1 === 't' && offset1 < 0) &&
879
1047
  ('cips'.indexOf(anchor2) >= 0 ||anchor2 === 't' && offset2 < 0)) {
880
1048
  // The `xv` attribute will be recognized by VMI_push_var to denote
881
- // "use the vector of the expression for which this VMI is code"
1049
+ // "use the vector of the expression for which this VMI is code".
882
1050
  return [{xv: true, dv: this.dataset.defaultValue},
883
1051
  anchor1, offset1, anchor2, offset2];
884
- } else {
885
- this.error = 'Expression can reference only previous values of itself';
886
- return false;
887
1052
  }
888
- } else {
889
- // Check for special case: pass variable reference instead of by value
890
- if(name.startsWith('!')) {
891
- by_reference = true;
892
- name = name.substring(1);
893
- }
894
- if(!msg) {
895
- // Check whether name refers to a Linny-R entity defined by the model
896
- obj = MODEL.objectByName(name);
897
- // If dataset modifier, it is the name of an equation...
898
- if(obj instanceof DatasetModifier) {
899
- // ... so "map" it onto the equations dataset + selector
900
- attr = obj.selector;
901
- obj = MODEL.equations_dataset;
1053
+ msg = 'Expression can reference only previous values of itself';
1054
+ }
1055
+ // A leading "!" denotes: pass variable reference instead of its value.
1056
+ // NOTE: This also applies to the "dot", so [!.] is a valid variable.
1057
+ let by_reference = name.startsWith('!');
1058
+ if(by_reference) name = name.substring(1);
1059
+ // When `name` is a single dot, it refers to the dataset for which the
1060
+ // modifier expression is being parsed. Like all datasets, the "dot"
1061
+ // may also have an attribute.
1062
+ if(name === '.') {
1063
+ obj = this.dot;
1064
+ if(!obj) msg = UI.ERROR.NO_DATASET_DOT;
1065
+ } else if(name.indexOf('??') >= 0) {
1066
+ msg = 'Use # as wildcard, not ??';
1067
+ }
1068
+ if(msg) {
1069
+ this.error = msg;
1070
+ return false;
1071
+ }
1072
+ // Check whether name refers to a Linny-R entity defined by the model.
1073
+ if(!obj) {
1074
+ // Variable name may start with a colon to denote that the owner
1075
+ // prefix should be added.
1076
+ name = UI.colonPrefixedName(name, this.owner_prefix);
1077
+ // Start with wildcard equations, as these are likely to be few
1078
+ // (so a quick scan) and constitute a special case.
1079
+ const
1080
+ id = UI.nameToID(name),
1081
+ w = MODEL.wildcardEquationByID(id);
1082
+ if(w) {
1083
+ // Variable matches wildcard equation w[0] with number w[1],
1084
+ // so this equation must be evaluated for that number.
1085
+ return [
1086
+ {d: w[0].dataset, s: w[1], x: w[0].expression},
1087
+ anchor1, offset1, anchor2, offset2];
1088
+ }
1089
+ // If no match, try to match the object ID with any type of entity.
1090
+ obj = MODEL.objectByID(id);
1091
+ }
1092
+ // If not, retry if wildcards can be substituted.
1093
+ if(!obj && name.indexOf('#') >= 0) {
1094
+ if(this.context_number) {
1095
+ obj = MODEL.objectByName(name.replace('#', this.context_number));
1096
+ }
1097
+ if(!obj) {
1098
+ // If immediate substitution of # does not identify an entity,
1099
+ // then name may still refer to a wildcard equation.
1100
+ const wcname = name.replace('#', '??');
1101
+ // Check for self-reference.
1102
+ if(wcname === this.attribute) {
1103
+ msg = 'Equation cannot reference itself';
1104
+ } else {
1105
+ obj = MODEL.equationByID(UI.nameToID(wcname));
1106
+ if(obj) {
1107
+ // Special case: parsed variable references a wildcard
1108
+ // equation (so `obj` is an instance of DatasetModifier).
1109
+ if(!(this.wildcard_equation || this.context_number)) {
1110
+ msg = UI.ERROR.NO_NUMBER_CONTEXT;
1111
+ } else {
1112
+ // Acceptable reference to a wildcard equation.
1113
+ if(!obj.expression.isStatic) {
1114
+ this.is_static = false;
1115
+ this.log('dynamic because wildcard equation is dynamic');
1116
+ }
1117
+ // NOTE: The referenced expression may be level-dependent.
1118
+ this.is_level_based = this.is_level_based ||
1119
+ obj.expression.is_level_based;
1120
+ // NOTE: Pass a question mark "?" as selector when one
1121
+ // wildcard equation references another; otherwise, pass
1122
+ // the context number as integer.
1123
+ let cnr;
1124
+ if(this.wildcard_equation) {
1125
+ cnr = '?';
1126
+ } else {
1127
+ cnr = parseInt(this.context_number);
1128
+ // NOTE: Let script break if context number is not numeric.
1129
+ if(isNaN(cnr) ) throw ['FATAL ERROR in expression for ',
1130
+ this.ownerName, ' while parsing variable "', name,
1131
+ '"\nContext number "', this.context_number,
1132
+ '" is not a number\nExpression: ', this.expr].join('');
1133
+ }
1134
+ // Use the context number as "selector" parameter of the VMI.
1135
+ return [
1136
+ {d: obj.dataset, s: cnr, x: obj.expression},
1137
+ anchor1, offset1, anchor2, offset2];
1138
+ }
1139
+ }
1140
+ }
1141
+ }
1142
+ if(!obj) {
1143
+ // Final possibility is a match with a tail-numbered entity name.
1144
+ // NOTE: also pass `attr` so that only entities having this
1145
+ // attribute will match.
1146
+ const ame = MODEL.allMatchingEntities(wildcardMatchRegex(name), attr);
1147
+ if(ame.length > 0) {
1148
+ // NOTE: Some attributes make this expression level-dependent.
1149
+ const uca = attr.toUpperCase();
1150
+ this.is_level_based = this.is_level_based ||
1151
+ VM.level_based_attr.indexOf(uca) >= 0;
1152
+ // Pass the eligible entities along with the selector, so that
1153
+ // at run time the VM can match with the value of #.
1154
+ // NOTE: Also pass whether the entity should be pushed
1155
+ // "by reference".
1156
+ return [
1157
+ {ee: ame, n: name, a: uca, br: by_reference},
1158
+ anchor1, offset1, anchor2, offset2];
902
1159
  }
1160
+ // Wildcard selector, but no number context for #.
1161
+ msg = UI.ERROR.NO_NUMBER_CONTEXT;
903
1162
  }
904
1163
  }
1164
+ if(msg) {
1165
+ this.error = msg;
1166
+ return false;
1167
+ }
1168
+ // Now `obj` refers to a model entity (ABCDELPQ).
1169
+ // This parseVariable(...) function must return a tuple like this:
1170
+ // [object or vector, anchor 1, offset 1, anchor 2, offset 2]
1171
+ // because this will be passed along with the VM instruction that
1172
+ // pushes the variable on the operand stack of this expression.
905
1173
  if(obj === null) {
906
1174
  msg = `Unknown entity "${name}"`;
907
- } else if(obj === this.dataset && attr === this.selector) {
908
- // Prevent cyclic reference
909
- msg = (obj === MODEL.equations_dataset ?
910
- 'Equation' : 'Dataset modifier expression') +
911
- ' must not reference itself';
912
1175
  } else if(obj.array &&
913
1176
  (anchor1 && '#ijk'.indexOf(anchor1) < 0 ||
914
1177
  anchor2 && '#ijk'.indexOf(anchor2) < 0)) {
1178
+ // Only indices (i, j, k) and the # number can index arrays, as arrays
1179
+ // have no time dimension, while all other anchors relate to time.
915
1180
  msg = 'Invalid anchor(s) for array-type dataset ' + obj.displayName;
916
1181
  } else {
917
- // NOTE: except for array-type datasets, the default anchor is 't';
918
- // for array-type datasets in expressions for array-type datasets,
919
- // the SPECIAL anchor is '^' to indicate "use parent anchor"
920
- const default_anchor = (obj.array ?
921
- (this.dataset && this.dataset.array ? '^' : '') : 't');
922
- if(!anchor1) anchor1 = default_anchor;
923
- if(!anchor2) anchor2 = default_anchor;
924
- // If "by reference", return the object itself plus its attribute
925
- if(by_reference) {
926
- return [{r: obj, a: attr}, anchor1, offset1, anchor2, offset2];
927
- }
928
- if(obj === this.dataset && attr === '' && !obj.array) {
929
- // When dataset modifier expression refers to its dataset without
930
- // selector, then this is equivalent to [.] (use the series data
931
- // vector) unless it is an array, since then the series data is
932
- // not a time-scaled vector => special case
933
- args = obj.vector;
934
- } else if(attr === '') {
935
- // For all other variables, assume default attribute
936
- attr = obj.defaultAttribute;
937
- // For a dataset, check whether the VMI_push_dataset_modifier should be
938
- // used. This is the case for array-type datasets, and for datasets
939
- // having modifiers UNLESS the modeler used a vertical bar to indicate
940
- // "use the data"
941
- if(obj instanceof Dataset &&
942
- (obj.array || (!use_data && obj.selectorList.length > 0))) {
943
- if(obj.data.length > 1 || obj.data.length > 0 && !obj.periodic ||
944
- !obj.allModifiersAreStatic) {
945
- // No explicit selector => dynamic unless no time series data, and
946
- // ALL modifier expressions are static
947
- this.is_static = false;
948
- this.log('dynamic because dataset without explicit selector is used');
949
- }
950
- // NOTE: also pass the "use data" flag so that experiment selectors
951
- // will be ignored if the modeler coded the vertical bar
952
- return [{d: obj, ud: use_data}, anchor1, offset1, anchor2, offset2];
953
- }
1182
+ // If the variable denotes an equation or a dataset with a selector,
1183
+ // check whether this is the "owner" of the expression being parsed.
1184
+ let sel = '',
1185
+ xtype = '';
1186
+ if(obj instanceof DatasetModifier) {
1187
+ sel = obj.selector;
1188
+ xtype = 'Equation';
954
1189
  } else if(obj instanceof Dataset) {
955
- const mm = obj.matchingModifiers([attr]);
956
- // If attribute matches a wildcard modifier, pass the modifier
957
- // expression for this wildcard AND the specified attribute
958
- if(mm.length > 0) {
959
- const nr = endsWithDigits(attr);
960
- if(!mm[0].expression.isStatic) {
961
- this.is_static = false;
962
- this.log('dynamic because dataset modifier expression is dynamic');
963
- }
964
- // NOTE: the attribute is relevant only to set the context for #,
965
- // so if it ends on digits, put the number in the VM instruction
966
- // to save execution time
967
- return [{d: obj, s: (nr ? parseInt(nr) : attr),
968
- x: mm[0].expression}, anchor1, offset1, anchor2, offset2];
969
- }
1190
+ sel = attr;
1191
+ xtype = 'Dataset modifier expression';
970
1192
  }
971
- // NOTE: args can now be a single value, a vector, or NULL
972
- if(args === null) args = obj.attributeValue(attr);
973
- if(Array.isArray(args)) {
974
- if(obj instanceof Dataset) {
975
- if(obj.data.length > 1 || obj.data.length > 0 && !obj.periodic) {
976
- this.is_static = false;
977
- this.log('dynamic because dataset vector is used');
978
- }
979
- } else if(VM.level_based_attr.indexOf(attr) >= 0) {
1193
+ // In variable names, wildcards are denoted as #, so also check for
1194
+ // the (unlikely) case that [eq#x] is used in the expression for a
1195
+ // wildcard equation or dataset modifier with name "eq??x".
1196
+ if(sel && (sel === this.selector ||
1197
+ sel.replace('#', '??') === this.selector)) {
1198
+ // Match indicates a cyclic reference
1199
+ msg = `${xtype} must not reference itself`;
1200
+ }
1201
+ }
1202
+ if(msg) {
1203
+ this.error = msg;
1204
+ return false;
1205
+ }
1206
+ // If `obj` is a dataset *modifier*, it must be a "normal" equation...
1207
+ if(obj instanceof DatasetModifier) {
1208
+ // ... so "map" it onto the equations dataset + selector...
1209
+ attr = obj.selector;
1210
+ obj = MODEL.equations_dataset;
1211
+ }
1212
+ // ... so now it will be processed the same way dataset modifiers
1213
+ // are processed, especially when they have a tail number.
1214
+
1215
+ // Set default anchors in case no anchors are specified.
1216
+ // Except for array-type datasets, the default anchor is 't';
1217
+ // for array-type datasets in expressions for array-type datasets,
1218
+ // the SPECIAL anchor is '^' to indicate "use parent anchor"
1219
+ // (which will be the parent's context-sensitive number #)
1220
+ const default_anchor = (obj.array ?
1221
+ (this.dataset && this.dataset.array ? '^' : '') : 't');
1222
+ if(!anchor1) anchor1 = default_anchor;
1223
+ if(!anchor2) anchor2 = default_anchor;
1224
+
1225
+ // If "by reference", return the object itself plus its attribute
1226
+ if(by_reference) {
1227
+ return [{r: obj, a: attr}, anchor1, offset1, anchor2, offset2];
1228
+ }
1229
+ if(obj === this.dataset && attr === '' && !obj.array) {
1230
+ // When dataset modifier expression refers to its dataset without
1231
+ // selector, then this is equivalent to [.] (use the series data
1232
+ // vector) unless it is an array, since then the series data is
1233
+ // not a time-scaled vector => special case
1234
+ args = obj.vector;
1235
+ } else if(attr === '') {
1236
+ // For all other variables, assume default attribute if none specified
1237
+ attr = obj.defaultAttribute;
1238
+ // For a dataset, check whether the VMI_push_dataset_modifier should be
1239
+ // used. This is the case for array-type datasets, and for datasets
1240
+ // having modifiers UNLESS the modeler used a vertical bar to indicate
1241
+ // "use the data"
1242
+ if(obj instanceof Dataset &&
1243
+ (obj.array || (!use_data && obj.selectorList.length > 0))) {
1244
+ if(obj.data.length > 1 || (obj.data.length > 0 && !obj.periodic) ||
1245
+ !obj.allModifiersAreStatic) {
1246
+ // No explicit selector => dynamic unless no time series data, and
1247
+ // ALL modifier expressions are static
980
1248
  this.is_static = false;
981
- this.log('dynamic because level-based attribute');
982
- } else {
983
- // Unusual (?) combi, so let's assume dynamic
1249
+ this.log('dynamic because dataset without explicit selector is used');
1250
+ }
1251
+ // NOTE: Also pass the "use data" flag so that experiment selectors
1252
+ // will be ignored if the modeler coded the vertical bar.
1253
+ return [{d: obj, ud: use_data}, anchor1, offset1, anchor2, offset2];
1254
+ }
1255
+ } else if(obj instanceof Dataset) {
1256
+ // For datasets, the attribute must be a modifier selector, so first
1257
+ // check if this dataset has a modifier that matches `attr`.
1258
+ const mm = obj.matchingModifiers([attr]);
1259
+ if(mm.length === 0) {
1260
+ // No match indicates unknown attribute.
1261
+ this.error = `Dataset ${obj.displayName} has no modifier with selector "${attr}"`;
1262
+ return false;
1263
+ } else {
1264
+ // NOTE: Multiple matches are impossible because `attr` cannot
1265
+ // contain wildcards; hence this is a unique match, so the modifier
1266
+ // expression is known.
1267
+ const m = mm[0];
1268
+ if(!m.expression.isStatic) {
984
1269
  this.is_static = false;
985
- this.log('probably dynamic -- check below:');
986
- console.log(obj.displayName, obj, attr, args);
1270
+ this.log('dynamic because dataset modifier expression is dynamic');
987
1271
  }
1272
+ // NOTE: a single match may be due to wildcard(s) in the modifier,
1273
+ // e.g., a variable [dataset|abc] matches with a modifier having
1274
+ // wildcard selector "a?b", or [dataset|a12] matches with "a*".
1275
+ // In such cases, if the selector matches an integer like "a12"
1276
+ // in the example above, this number (12) should be used as
1277
+ // number context (overriding the number of the dataset, so
1278
+ // for [datset34|a12], the number context is '12' and not '34').
1279
+ let mcn = matchingNumber(attr, m.selector);
1280
+ if(mcn === false) {
1281
+ // NOTE: When no matching number if found, `attr` may still
1282
+ // contain a ?? wildcard. If it indeed identifies a wildcard
1283
+ // equation, then "?" should be passed to the VM instruction.
1284
+ if(obj === MODEL.equations_dataset && attr.indexOf('??') >= 0) {
1285
+ mcn = '?';
1286
+ } else {
1287
+ // Ensure that `mcn` is either an integer value or FALSE.
1288
+ mcn = endsWithDigits(obj.name) || this.context_number || false;
1289
+ }
1290
+ }
1291
+ // Pass the dataset, the context number # (or FALSE) in place, and the
1292
+ // modifier expression
1293
+ return [
1294
+ {d: m.dataset, s: mcn, x: m.expression},
1295
+ anchor1, offset1, anchor2, offset2];
988
1296
  }
989
- // If not a single value or vector, it must be an expression
990
- if(args === null) args = obj.attributeExpression(attr);
991
- if(args === null) {
992
- // Only NOW check whether unit balance for clusters is asked for
993
- if(cluster_balance_unit !== false && obj instanceof Cluster) {
994
- // NOTE: cluster balance ALWAYS makes expression level-based
995
- this.is_level_based = true;
996
- // @TO DO: rethink whether this will indeed also make this expression
997
- // dynamic
1297
+ }
1298
+ // NOTE: `args` can now be a single value, a vector, or NULL.
1299
+ if(args === null) args = obj.attributeValue(attr);
1300
+ if(Array.isArray(args)) {
1301
+ if(obj instanceof Dataset) {
1302
+ if(obj.data.length > 1 || obj.data.length > 0 && !obj.periodic) {
998
1303
  this.is_static = false;
999
- this.log('dynamic because cluster balance is believed to be dynamic');
1000
- // NOTE: VM instructions VMI_push_var will recognize this special case
1001
- return [{c: obj, u: cluster_balance_unit},
1002
- anchor1, offset1, anchor2, offset2];
1304
+ this.log('dynamic because dataset vector is used');
1003
1305
  }
1004
- // Fall-through: invalid attribute for this object
1005
- msg = `${obj.type} entities have no attribute "${attr}"`;
1306
+ } else if(VM.level_based_attr.indexOf(attr) >= 0) {
1307
+ this.is_static = false;
1308
+ this.log('dynamic because level-based attribute');
1006
1309
  } else {
1007
- if(args instanceof Expression) {
1008
- this.is_static = this.is_static && args.isStatic;
1009
- }
1010
- args = [args, anchor1, offset1, anchor2, offset2];
1310
+ // Unusual (?) combi, so let's assume dynamic
1311
+ this.is_static = false;
1312
+ this.log('probably dynamic -- check below:');
1313
+ console.log(obj.displayName, obj, attr, args);
1314
+ }
1315
+ }
1316
+ // If not a single value or vector, it must be an expression
1317
+ if(args === null) args = obj.attributeExpression(attr);
1318
+ if(args === null) {
1319
+ // Only NOW check whether unit balance for clusters is asked for
1320
+ if(cluster_balance_unit !== false && obj instanceof Cluster) {
1321
+ // NOTE: cluster balance ALWAYS makes expression level-based
1322
+ this.is_level_based = true;
1323
+ // @TO DO: rethink whether this will indeed also make this expression
1324
+ // dynamic
1325
+ this.is_static = false;
1326
+ this.log('dynamic because cluster balance is believed to be dynamic');
1327
+ // NOTE: VM instructions VMI_push_var will recognize this special case
1328
+ return [{c: obj, u: cluster_balance_unit},
1329
+ anchor1, offset1, anchor2, offset2];
1330
+ }
1331
+ // Fall-through: invalid attribute for this object
1332
+ msg = `${obj.type} entities have no attribute "${attr}"`;
1333
+ } else {
1334
+ if(args instanceof Expression) {
1335
+ this.is_static = this.is_static && args.isStatic;
1011
1336
  }
1337
+ args = [args, anchor1, offset1, anchor2, offset2];
1012
1338
  }
1013
- if(msg === '') {
1014
- // Check whether the attribute is level-based (i.e., can be computed only
1015
- // after optimizing a block) while no offset is defined to use prior data
1016
- this.is_level_based = this.is_level_based ||
1017
- // NOTE: dataset modifier expressions may be level_based
1018
- obj instanceof Dataset && attr && args[0].is_level_based ||
1019
- // NOTE: assume NOT level_based if anchor & offset are specified
1020
- VM.level_based_attr.indexOf(attr) >= 0 &&
1021
- anchor1 === 't' && offset1 === 0 &&
1022
- anchor2 === 't' && offset2 === 0;
1023
- return args;
1339
+ if(msg) {
1340
+ this.error = msg;
1341
+ return false;
1024
1342
  }
1025
- this.error = msg;
1026
- return false;
1343
+ // Now `args` should be a valid argument for a VM instruction that
1344
+ // pushes an operand on the evaluation stack
1345
+ // Check whether the attribute is level-based (i.e., can be computed only
1346
+ // after optimizing a block) while no offset is defined to use prior data
1347
+ this.is_level_based = this.is_level_based ||
1348
+ // NOTE: dataset modifier expressions may be level_based
1349
+ obj instanceof Dataset && attr && args[0].is_level_based ||
1350
+ // NOTE: assume NOT level_based if anchor & offset are specified
1351
+ VM.level_based_attr.indexOf(attr) >= 0 &&
1352
+ anchor1 === 't' && offset1 === 0 &&
1353
+ anchor2 === 't' && offset2 === 0;
1354
+ return args;
1027
1355
  }
1028
1356
 
1029
- // Gets the next substring in the expression that is a valid symbol
1030
- // while advancing the position-in-text (pit) and length-of-symbol (los),
1031
- // which are used to highlight the position of a syntax error in the
1032
- // expression editor
1033
1357
  getSymbol() {
1358
+ // Gets the next substring in the expression that is a valid symbol
1359
+ // while advancing the position-in-text (`pit`) and length-of-symbol
1360
+ // (`los`), which are used to highlight the position of a syntax error
1361
+ // in the expression editor
1034
1362
  let c, f, i, l, v;
1035
1363
  this.prev_sym = this.sym;
1036
1364
  this.sym = null;
@@ -1130,7 +1458,7 @@ class ExpressionParser {
1130
1458
  if(this.selector.indexOf('*') >= 0 ||
1131
1459
  this.selector.indexOf('?') >= 0 ||
1132
1460
  this.owner.numberContext) {
1133
- this.sym = VMI_push_selector_wildcard;
1461
+ this.sym = VMI_push_contextual_number;
1134
1462
  } else {
1135
1463
  this.error = '# is undefined in this context';
1136
1464
  }
@@ -1241,6 +1569,8 @@ class ExpressionParser {
1241
1569
  // Compiles expression into array of VM instructions `code`
1242
1570
  // NOTE: always create a new code array instance, as it will typically
1243
1571
  // become the code attribute of an expression object
1572
+ if(DEBUGGING) console.log('COMPILING', this.ownerName, ':\n',
1573
+ this.expr, '\ncontext number =', this.context_number);
1244
1574
  this.code = [];
1245
1575
  // Position in text
1246
1576
  this.pit = 0;
@@ -1347,7 +1677,7 @@ class ExpressionParser {
1347
1677
  // start by popping the FALSE result of the IF condition
1348
1678
  this.code.push([VMI_pop_false, null]);
1349
1679
  }
1350
- // END of new code
1680
+ // END of new code for IF-THEN-ELSE
1351
1681
 
1352
1682
  this.op_stack.push(this.sym);
1353
1683
  } else if(this.sym !== null) {
@@ -1356,11 +1686,13 @@ class ExpressionParser {
1356
1686
  this.code.push([this.sym, null]);
1357
1687
  } else if(Array.isArray(this.sym)) {
1358
1688
  // Either a statistic, a dataset (array-type or with modifier),
1359
- // an experiment run result, or a variable
1360
- if(this.sym.length === 6) {
1689
+ // an experiment run result, or a variable.
1690
+ if(this.sym.length >= 6) {
1361
1691
  this.code.push([VMI_push_statistic, this.sym]);
1362
1692
  } else if(this.sym[0].hasOwnProperty('d')) {
1363
1693
  this.code.push([VMI_push_dataset_modifier, this.sym]);
1694
+ } else if(this.sym[0].hasOwnProperty('ee')) {
1695
+ this.code.push([VMI_push_wildcard_entity, this.sym]);
1364
1696
  } else if(this.sym[0].hasOwnProperty('x')) {
1365
1697
  this.code.push([VMI_push_run_result, this.sym]);
1366
1698
  } else if(this.sym[0].hasOwnProperty('r')) {
@@ -1392,8 +1724,7 @@ class ExpressionParser {
1392
1724
  this.error = 'Invalid parameter list';
1393
1725
  }
1394
1726
  }
1395
- if(DEBUGGING) console.log('PARSED',
1396
- this.owner.displayName + '|' + this.attribute, ':',
1727
+ if(DEBUGGING) console.log('PARSED', this.ownerName, ':',
1397
1728
  this.expr, this.code);
1398
1729
  }
1399
1730
 
@@ -4261,9 +4592,8 @@ class VirtualMachine {
4261
4592
  } else if(this.error_codes.indexOf(n) < 0) {
4262
4593
  err += '? value = ' + n;
4263
4594
  }
4264
- const msg = `Numeric issue: ${err} for ${this.numeric_issue}`;
4265
- this.logMessage(msg);
4266
- UI.alert(msg);
4595
+ this.logMessage(this.block_count, err);
4596
+ UI.alert(err);
4267
4597
  }
4268
4598
 
4269
4599
  get actualBlockLength() {
@@ -4849,8 +5179,13 @@ Solver status = ${json.status}`);
4849
5179
  if(MODEL.running_experiment) EXPERIMENT_MANAGER.processRun();
4850
5180
  // Warn modeler if any issues occurred
4851
5181
  if(this.block_issues) {
4852
- UI.warn('Issues occurred in ' + pluralS(this.block_issues, 'block') +
4853
- ' -- details can be viewed in the monitor and by using \u25C1 \u25B7');
5182
+ let msg = 'Issues occurred in ' +
5183
+ pluralS(this.block_issues, 'block') +
5184
+ ' -- details can be viewed in the monitor';
5185
+ if(VM.issue_list.length) {
5186
+ msg += ' and by using \u25C1 \u25B7';
5187
+ }
5188
+ UI.warn(msg);
4854
5189
  UI.updateIssuePanel();
4855
5190
  }
4856
5191
  if(this.license_expired > 0) {
@@ -5230,11 +5565,11 @@ function VMI_push_second(x, empty) {
5230
5565
  pushTimeStepsPerTimeUnit(x, 'second');
5231
5566
  }
5232
5567
 
5233
- function VMI_push_selector_wildcard(x, empty) {
5234
- // Pushes the numeric value of the wildcard match
5568
+ function VMI_push_contextual_number(x, empty) {
5569
+ // Pushes the numeric value of the context-sensitive number #
5235
5570
  const n = valueOfNumberSign(x);
5236
5571
  if(DEBUGGING) {
5237
- console.log('push selector wildcard = ' + VM.sig2Dig(n));
5572
+ console.log('push contextual number: # = ' + VM.sig2Dig(n));
5238
5573
  }
5239
5574
  x.push(n);
5240
5575
  }
@@ -5242,118 +5577,121 @@ function VMI_push_selector_wildcard(x, empty) {
5242
5577
  /* VM instruction helper functions */
5243
5578
 
5244
5579
  function valueOfNumberSign(x) {
5245
- // Pushes the numeric value of the # sign in its local context
5580
+ // Pushes the numeric value of the # sign for the context of expression `x`
5246
5581
  // NOTE: this can be a wildcard match, an active experiment run selector
5247
- // ending on digits, or an entity name ending on digits
5582
+ // ending on digits, or tne number context of an entity. The latter typically
5583
+ // is the number its name or any of its prefixes ends on, but notes are
5584
+ // more "creative" and can return the number context of nearby entities.
5248
5585
  let s = 'NO SELECTOR',
5249
5586
  m = 'NO MATCH',
5250
- n = 0;
5251
- // NOTE: give wildcard selectors precedence over experiment selectors
5252
- // because wildcard selector is an immediate property of the dataset
5253
- // modifier expressions, so "closer" to the expression than the
5254
- // experiment selectors that identify the run
5255
- if(x.wildcard_number !== false) {
5256
- n = x.wildcard_number;
5587
+ n = VM.UNDEFINED;
5588
+ // NOTE: Give wildcard selectors precedence over experiment selectors
5589
+ // because a wildcard selector is an immediate property of the dataset
5590
+ // modifier expression, and hence "closer" to the expression than the
5591
+ // experiment selectors that identify the run.
5592
+ if(x.wildcard_vector_index !== false) {
5593
+ n = x.wildcard_vector_index;
5257
5594
  s = x.attribute;
5258
5595
  m = 'wildcard';
5259
5596
  } else {
5260
- if(x.object instanceof Dataset && x.attribute) {
5261
- s = x.attribute;
5262
- if(!endsWithDigits(m) && MODEL.running_experiment) {
5263
- const sl = MODEL.running_experiment.activeCombination;
5264
- if(sl) {
5265
- // Let `m` be the selector for the current experiment run
5266
- m = sl[0];
5597
+ // Check whether `x` is a dataset modifier expression.
5598
+ // NOTE: This includes equations.
5599
+ if(x.object instanceof Dataset) {
5600
+ if(x.attribute) {
5601
+ s = x.attribute;
5602
+ } else {
5603
+ // Selector may also be defined by a running experiment.
5604
+ if(MODEL.running_experiment) {
5605
+ // Let `m` be the primary selector for the current experiment run.
5606
+ const sl = MODEL.running_experiment.activeCombination;
5607
+ if(sl) {
5608
+ m = sl[0];
5609
+ s = 'experiment';
5610
+ }
5267
5611
  }
5268
5612
  }
5269
5613
  }
5270
- // If selector contains no wildcards, check if entity name ends with digits
5614
+ // If selector contains no wildcards, get number context (typically
5615
+ // inferred from a number in the name of the object)
5271
5616
  if(s.indexOf('*') < 0 && s.indexOf('?') < 0) {
5272
5617
  const d = x.object.numberContext;
5273
5618
  if(d) {
5274
- s = '*';
5619
+ s = x.object.displayName;
5275
5620
  m = d;
5621
+ n = parseInt(d);
5276
5622
  }
5277
- }
5278
- if(m !== 'NO MATCH') {
5279
- // Asterisk matches 0 or more characters, question mark 1 character
5280
- let raw = s.replace(/\*/g, '(.*)').replace(/\?/g, '(.)'),
5281
- match = m.match(new RegExp(raw));
5282
- if(match) {
5283
- // Concatenate all matching characters (need not be digits)
5284
- m = '';
5285
- for(let i = 1; i < match.length; i++) m += match[i];
5286
- // Try to convert to an integer
5287
- n = parseInt(m);
5288
- // Default to 0
5289
- if(isNaN(n)) n = 0;
5290
- }
5623
+ } else if(m !== 'NO MATCH') {
5624
+ // If matching `m` against `s` do not yield an integer string,
5625
+ // use UNDEFINED
5626
+ n = matchingNumber(m, s) || VM.UNDEFINED;
5291
5627
  }
5292
5628
  }
5629
+ // For datasets, set the parent anchor to be the context-sensitive number
5293
5630
  if(x.object instanceof Dataset) x.object.parent_anchor = n;
5294
5631
  if(DEBUGGING) {
5295
- console.log('context for # in expression for ' +
5296
- `${x.object.displayName}${x.attribute ? '|' + x.attribute : ''}
5632
+ console.log(`context for # in expression for ${x.variableName}
5297
5633
  - expression: ${x.text}
5298
- - inferred value of # ${s} => ${m} => ${VM.sig2Dig(n)}`, x.code);
5634
+ - inferred value of # ${s} => ${m} => ${n}`, x.code);
5299
5635
  }
5300
5636
  return n;
5301
5637
  }
5302
5638
 
5303
5639
  function relativeTimeStep(t, anchor, offset, dtm, x) {
5304
- // Returns the relative time step, given t, anchor, offset, delta-t-multiplier
5305
- // and the expression being evaluated (to provide context for anchor #)
5306
- // NOTE: t = 1 corresponds with first time step of simulation period
5307
-
5308
- // Anchors are checked for in order of *expected* frequency of occurrence
5640
+ // Returns the relative time step, given t, anchor, offset,
5641
+ // delta-t-multiplier and the expression being evaluated (to provide
5642
+ // context for anchor #).
5643
+ // NOTE: t = 1 corresponds with first time step of simulation period.
5644
+ // Anchors are checked for in order of *expected* frequency of occurrence.
5309
5645
  if(anchor === 't') {
5310
- // Offset relative to current time step (most likely to occur)
5646
+ // Offset relative to current time step (most likely to occur).
5311
5647
  return Math.floor(t + offset);
5312
5648
  }
5649
+ if(anchor === '#') {
5650
+ // Index: offset is added to the inferred value of the # symbol.
5651
+ return valueOfNumberSign(x) + offset;
5652
+ }
5653
+ if(anchor === '^') {
5654
+ // Inherited index (for dataset modifier expressions): offset is added
5655
+ // to the anchor of the modifier's dataset.
5656
+ if(x.object.array) {
5657
+ if(DEBUGGING) {
5658
+ console.log('Parent anchor', x.object.parent_anchor);
5659
+ }
5660
+ // NOTE: For not array-type datasets, ^ is equivalent to #
5661
+ return x.object.parent_anchor;
5662
+ }
5663
+ return valueOfNumberSign(x) + offset;
5664
+ }
5665
+ if('ijk'.indexOf(anchor) >= 0) {
5666
+ // Index: offset is added to the iterator index i, j or k.
5667
+ return valueOfIndexVariable(anchor) + offset;
5668
+ }
5313
5669
  if(anchor === 'r') {
5314
- // Offset relative to current time step, scaled to time unit of run
5670
+ // Offset relative to current time step, scaled to time unit of run.
5315
5671
  return Math.floor((t + offset) * dtm);
5316
5672
  }
5317
5673
  if(anchor === 'c') {
5318
- // Relative to start of current optimization block
5674
+ // Relative to start of current optimization block.
5319
5675
  return Math.trunc(t / MODEL.block_length) * MODEL.block_length + offset;
5320
5676
  }
5321
5677
  if(anchor === 'p') {
5322
- // Relative to start of previous optimization block
5678
+ // Relative to start of previous optimization block.
5323
5679
  return (Math.trunc(t / MODEL.block_length) - 1) * MODEL.block_length + offset;
5324
5680
  }
5325
5681
  if(anchor === 'n') {
5326
- // Relative to start of next optimization block
5682
+ // Relative to start of next optimization block.
5327
5683
  return (Math.trunc(t / MODEL.block_length) + 1) * MODEL.block_length + offset;
5328
5684
  }
5329
5685
  if(anchor === 'l') {
5330
- // Last: offset relative to the last index in the vector
5686
+ // Last: offset relative to the last index in the vector.
5331
5687
  return MODEL.end_period - MODEL.start_period + 1 + offset;
5332
5688
  }
5333
5689
  if(anchor === 's') {
5334
- // Scaled: offset is scaled to time unit of run
5690
+ // Scaled: offset is scaled to time unit of run.
5335
5691
  return Math.floor(offset * dtm);
5336
5692
  }
5337
- if('ijk'.indexOf(anchor) >= 0) {
5338
- // Index: offset is added to the iterator index i, j or k
5339
- return valueOfIndexVariable(anchor) + offset;
5340
- }
5341
- if(anchor === '#') {
5342
- // Index: offset is added to the inferred value of the # symbol
5343
- return valueOfNumberSign(x) + offset;
5344
- }
5345
- if(anchor === '^') {
5346
- // Inherited index: offset is added to anchor of "parent" dataset
5347
- if(x.object.array) {
5348
- if(DEBUGGING) {
5349
- console.log('Parent anchor', x.object.parent_anchor);
5350
- }
5351
- return x.object.parent_anchor;
5352
- }
5353
- return valueOfNumberSign(x) + offset;
5354
- }
5355
- // Fall-through: offset relative to the initial value index (0)
5356
- // NOTE: this also applies to anchor f (First)
5693
+ // Fall-through: offset relative to the initial value index (0).
5694
+ // NOTE: this also applies to anchor f (First).
5357
5695
  return offset;
5358
5696
  }
5359
5697
 
@@ -5433,9 +5771,9 @@ function VMI_push_var(x, args) {
5433
5771
  }
5434
5772
 
5435
5773
  function VMI_push_entity(x, args) {
5436
- // Pushes a special "entity reference" object based on `args`, being the list
5437
- // [obj, anchor1, offset1, anchor2, offset2] where `obj` has the format
5438
- // {r: entity object, a: attribute}
5774
+ // Pushes a special "entity reference" object based on `args`, being the
5775
+ // list [obj, anchor1, offset1, anchor2, offset2] where `obj` has the
5776
+ // format {r: entity object, a: attribute}
5439
5777
  // The object that is pushed on the stack passes the entity, the attribute
5440
5778
  // to use, and the time interval
5441
5779
  const
@@ -5451,35 +5789,93 @@ function VMI_push_entity(x, args) {
5451
5789
  x.push(er);
5452
5790
  }
5453
5791
 
5792
+ function VMI_push_wildcard_entity(x, args) {
5793
+ // Pushes the value of (or reference to) an entity attribute, based on
5794
+ // `args`, being the list [obj, anchor1, offset1, anchor2, offset2]
5795
+ // where `obj` has the format {ee: list of eligible entities,
5796
+ // n: name (with wildcard #), a: attribute, br: by reference (boolean)}
5797
+ // First select the first entity in `ee` that matches the wildcard vector
5798
+ // index of the expression `x` being executed.
5799
+ const
5800
+ el = args[0].ee,
5801
+ nn = args[0].n.replace('#', x.wildcard_vector_index);
5802
+ let obj = null;
5803
+ for(let i = 0; !obj && i < el.length; i++) {
5804
+ if(el[i].name === nn) obj = el[i];
5805
+ }
5806
+ // If no match, then this indicates a bad reference.
5807
+ if(!obj) {
5808
+ console.log(`ERROR: no match for "${nn}" in eligible entity list`, el);
5809
+ x.push(VM.BAD_REF);
5810
+ return;
5811
+ }
5812
+ // Otherwise, if `br` indicates "by reference", then VMI_push_entity can
5813
+ // be called with the appropriate parameters.
5814
+ const attr = args[0].a || obj.defaultAttribute;
5815
+ if(args[0].br) {
5816
+ args[0] = {r: obj, a: attr};
5817
+ VMI_push_entity(x, args);
5818
+ return;
5819
+ }
5820
+ // Otherwise, if the entity is a dataset modifier, this must be an
5821
+ // equation (identified by its name, not by a modifier selector) so
5822
+ // push the result of this equation using the wildcard vector index
5823
+ // of the expression that is being computed.
5824
+ if(obj instanceof DatasetModifier) {
5825
+ args[0] = {d: obj.dataset, s: x.wildcard_vector_index, x: obj.expression};
5826
+ VMI_push_dataset_modifier(x, args);
5827
+ return;
5828
+ }
5829
+ // Otherwise, it can be a vector type attribute or an expression.
5830
+ let v = obj.attributeValue(attr);
5831
+ if(v === null) v = obj.attributeExpression(attr);
5832
+ // If no match, then this indicates a bad reference.
5833
+ if(v === null) {
5834
+ console.log(`ERROR: bad variable "${obj.displayName}" with attribute "${attr}"`);
5835
+ x.push(VM.BAD_REF);
5836
+ return;
5837
+ }
5838
+ // Otherwise, VMI_push_var can be called with `v` as first argument.
5839
+ args[0] = v;
5840
+ VMI_push_var(x, args);
5841
+ }
5842
+
5454
5843
  function VMI_push_dataset_modifier(x, args) {
5455
- // NOTE: the first argument specifies the dataset `d` and (optionally!) the
5456
- // modifier selector `s`, and expression `x`;
5457
- // if not specified, the modifier to be used must be inferred from the
5458
- // running experiment UNLESS the field `ud` ("use data") is defined for the
5459
- // first argument and equals TRUE
5844
+ // NOTE: the first argument specifies the dataset `d` and (optionally!)
5845
+ // the modifier selector `s`, and expression `x`.
5846
+ // If `s` is a number, then the result of `x` must be computed with
5847
+ // this number als wildcard number.
5848
+ // If `s` is not specified, the modifier to be used must be inferred from
5849
+ // the running experiment UNLESS the field `ud` ("use data") is defined
5850
+ // for the first argument, and evaluates as TRUE.
5851
+ // NOTE: Ensure that number 0 is not interpreted as FALSE.
5852
+ let ms = (args[0].s === 0 || args[0].s) ? args[0].s : false;
5460
5853
  const
5461
5854
  ds = args[0].d,
5462
- // NOTE: `ud`, `ms`, and `mx` may get value "undefined"!
5463
- ud = args[0].ud,
5464
- ms = args[0].s,
5465
- mx = args[0].x,
5466
- // NOTE: use the "local" time step for expression x, i.e., the top value
5467
- // of the expression's time step stack `x.step`
5855
+ ud = args[0].ud || false,
5856
+ mx = args[0].x || null,
5857
+ // NOTE: Use the "local" time step for expression x, i.e., the top
5858
+ // value of the expression's time step stack `x.step`.
5468
5859
  tot = twoOffsetTimeStep(x.step[x.step.length - 1],
5469
5860
  args[1], args[2], args[3], args[4], 1, x);
5861
+ // NOTE: Sanity check to facilitate debugging; if no dataset is provided,
5862
+ // the script will still break at the LET statement below.
5863
+ if(!ds) console.log('ERROR: VMI_push_dataset_modifier without dataset',
5864
+ x.variableName, x.code);
5470
5865
  let t = tot[0],
5471
5866
  obj = ds.vector;
5867
+
5472
5868
  if(ds.array) {
5473
- // For array, do NOT adjust "index" t to model run period
5474
- // NOTE: indices start at 1, but array is zero-based
5869
+ // For array-type datasets, do NOT adjust "index" t to model run period.
5870
+ // NOTE: Indices start at 1, but arrays are zero-based, so subtract 1.
5475
5871
  t--;
5476
- // When periodic, adjust t to fall within the vector length
5872
+ // When data is periodic, adjust `t` to fall within the vector length.
5477
5873
  if(ds.periodic && obj.length > 0) {
5478
5874
  t = t % obj.length;
5479
5875
  if(t < 0) t += obj.length;
5480
5876
  }
5481
5877
  if(args[1] === '#' || args[3] === '#') {
5482
- // NOTE: add 1, since (parent) anchors are 1-based
5878
+ // NOTE: Add 1 because (parent) anchors are 1-based.
5483
5879
  ds.parent_anchor = t + 1;
5484
5880
  if(DEBUGGING) {
5485
5881
  console.log('ANCHOR for:', ds.displayName, '=', ds.parent_anchor);
@@ -5487,48 +5883,56 @@ function VMI_push_dataset_modifier(x, args) {
5487
5883
  }
5488
5884
  } else {
5489
5885
  // Negative time step is evaluated as t = 0 (initial value), t beyond
5490
- // optimization period is evaluated as its last time step
5491
- // NOTE: By default, use the dataset vector value for t
5886
+ // optimization period is evaluated as its last time step.
5887
+ // NOTE: By default, use the dataset vector value for `t`.
5492
5888
  t = Math.max(0, Math.min(
5493
5889
  MODEL.end_period - MODEL.start_period + MODEL.look_ahead + 1, t));
5494
5890
  }
5495
- if(ms) {
5496
- // If modifier selector is specified, use the associated expression
5891
+ if(ms !== false) {
5892
+ // If a modifier selector is specified, use the associated expression.
5497
5893
  obj = mx;
5894
+ // If '?' is passed as selector, use the wildcard vector index of the
5895
+ // expression that is being computed.
5896
+ if(ms === '?') ms = x.wildcard_vector_index;
5498
5897
  } else if(!ud) {
5898
+ // In no selector and not "use data", check whether a running experiment
5899
+ // defines the expression to use. If not, `obj` will be the dataset
5900
+ // vector (so same as when "use data" is set).
5499
5901
  obj = ds.activeModifierExpression;
5500
5902
  }
5501
- // By default, use the dataset default value
5903
+ // Now determine what value `v` should be pushed onto the expression stack.
5904
+ // By default, use the dataset default value.
5502
5905
  let v = ds.defaultValue,
5906
+ // NOTE: `obstr` is used only when debugging, to log `obj` in human-
5907
+ // readable format.
5503
5908
  obstr = (obj instanceof Expression ?
5504
5909
  obj.text : '[' + obj.toString() + ']');
5505
5910
  if(Array.isArray(obj)) {
5506
- // Object is a vector
5911
+ // Object is a vector.
5507
5912
  if(t >= 0 && t < obj.length) {
5508
5913
  v = obj[t];
5509
5914
  } else if(ds.array && t >= obj.length) {
5510
- // Set error value if array index is out of bounds
5915
+ // Set error value if array index is out of bounds.
5511
5916
  v = VM.ARRAY_INDEX;
5512
5917
  VM.out_of_bounds_array = ds.displayName;
5513
- VM.out_of_bounds_msg = `Index ${t + 1} not in array dataset ` +
5918
+ VM.out_of_bounds_msg = `Index ${VM.sig2Dig(t + 1)} not in array dataset ` +
5514
5919
  `${ds.displayName}, which has length ${obj.length}`;
5515
5920
  console.log(VM.out_of_bounds_msg);
5516
5921
  }
5517
- // Fall through: no change to `v` => dataset default value is pushed
5922
+ // Fall through: no change to `v` => dataset default value is pushed.
5518
5923
  } else {
5519
- // Object is an expression
5520
- // NOTE: readjust t when obj is expression for *array-type* dataset modifier
5924
+ // Object is an expression.
5925
+ // NOTE: Readjust `t` when `obj` is an expression for an *array-type*
5926
+ // dataset modifier.
5521
5927
  if(obj.object instanceof Dataset && obj.object.array) t++;
5522
- // Pass modifier selector (if specified; may be undefined) so that result
5928
+ // Pass modifier selector (if specified; may be FALSE) so that result
5523
5929
  // will be recomputed with this selector as context for #
5524
- // Also pass equation parameters (if specified; may be undefined) so that
5525
- // these will be used as the actual values of the formal parameters
5526
5930
  v = obj.result(t, ms);
5527
5931
  }
5528
5932
  // Trace only now that time step t has been computed
5529
5933
  if(DEBUGGING) {
5530
5934
  console.log('push dataset modifier:', obstr,
5531
- tot[1] + (tot[2] ? ':' + tot[2] : ''), ' value = ', VM.sig4Dig(v));
5935
+ tot[1] + (tot[2] ? ':' + tot[2] : ''), 'value =', VM.sig4Dig(v));
5532
5936
  console.log(' --', x.text, ' for owner ', x.object.displayName, x.attribute);
5533
5937
  }
5534
5938
  // NOTE: if value is exceptional ("undefined", etc.), use default value
@@ -5654,8 +6058,8 @@ function VMI_push_statistic(x, args) {
5654
6058
  // of MAX, MEAN, MIN, N, SD, SUM, and VAR, and `list` is a list of vectors
5655
6059
  // NOTE: each statistic may also be "suffixed" by NZ to denote that only
5656
6060
  // non-zero numbers should be considered
5657
- let stat = args[0];
5658
- const list = args[1];
6061
+ let stat = args[0],
6062
+ list = args[1];
5659
6063
  if(!list) {
5660
6064
  // Special case: null or empty list => push zero
5661
6065
  if(DEBUGGING) {
@@ -5664,8 +6068,17 @@ function VMI_push_statistic(x, args) {
5664
6068
  x.push(0);
5665
6069
  return;
5666
6070
  }
5667
- const anchor1 = args[2], offset1 = args[3],
5668
- anchor2 = args[4], offset2 = args[5];
6071
+ const
6072
+ anchor1 = args[2],
6073
+ offset1 = args[3],
6074
+ anchor2 = args[4],
6075
+ offset2 = args[5],
6076
+ wdict = args[6] || false;
6077
+ // If defined, the wildcard dictionary provides subsets of `list`
6078
+ // to be used when the wildcard number of the expression is set.
6079
+ if(wdict && x.wildcard_vector_index !== false) {
6080
+ list = wdict[x.wildcard_vector_index] || [];
6081
+ }
5669
6082
  // If no list specified, the result is undefined
5670
6083
  if(!Array.isArray(list) || list.length === 0) {
5671
6084
  x.push(VM.UNDEFINED);
@@ -5724,13 +6137,20 @@ function VMI_push_statistic(x, args) {
5724
6137
  }
5725
6138
  // Push value unless it is zero and NZ is TRUE, or if it is undefined
5726
6139
  // (this will occur when a variable has been deleted)
5727
- if(v <= VM.PLUS_INFINITY && (!nz || Math.abs(v) < VM.NEAR_ZERO)) {
6140
+ if(v <= VM.PLUS_INFINITY && (!nz || Math.abs(v) > VM.NEAR_ZERO)) {
5728
6141
  vlist.push(v);
5729
6142
  }
5730
6143
  }
5731
6144
  }
5732
- // If no values remain, the result is zero (as ALL values were zero)
5733
- const n = vlist.length;
6145
+ const
6146
+ n = vlist.length,
6147
+ // NOTE: count is the number of values used in the statistic
6148
+ count = (nz ? n : list.length);
6149
+ if(stat === 'N') {
6150
+ x.push(count);
6151
+ return;
6152
+ }
6153
+ // If no non-zero values remain, all statistics are zero (as ALL values were zero)
5734
6154
  if(n === 0) {
5735
6155
  x.push(0);
5736
6156
  return;
@@ -5744,10 +6164,6 @@ function VMI_push_statistic(x, args) {
5744
6164
  x.push(Math.max(...vlist));
5745
6165
  return;
5746
6166
  }
5747
- if(stat === 'N') {
5748
- x.push(n);
5749
- return;
5750
- }
5751
6167
  // For all remaining statistics, the sum must be calculated
5752
6168
  let sum = 0;
5753
6169
  for(let i = 0; i < n; i++) {
@@ -5759,7 +6175,7 @@ function VMI_push_statistic(x, args) {
5759
6175
  }
5760
6176
  // Now statistic must be either MEAN, SD or VAR, so start with the mean
5761
6177
  // NOTE: no more need to check for division by zero
5762
- const mean = sum / n;
6178
+ const mean = sum / count;
5763
6179
  if(stat === 'MEAN') {
5764
6180
  x.push(mean);
5765
6181
  return;
@@ -5770,11 +6186,11 @@ function VMI_push_statistic(x, args) {
5770
6186
  sumsq += Math.pow(vlist[i] - mean, 2);
5771
6187
  }
5772
6188
  if(stat === 'VAR') {
5773
- x.push(sumsq / n);
6189
+ x.push(sumsq / count);
5774
6190
  return;
5775
6191
  }
5776
6192
  if(stat === 'SD') {
5777
- x.push(Math.sqrt(sumsq / n));
6193
+ x.push(Math.sqrt(sumsq / count));
5778
6194
  return;
5779
6195
  }
5780
6196
  // Fall-through: unknown statistic
@@ -7145,7 +7561,7 @@ const
7145
7561
  VMI_push_look_ahead, VMI_push_round, VMI_push_last_round,
7146
7562
  VMI_push_number_of_rounds, VMI_push_run_number, VMI_push_number_of_runs,
7147
7563
  VMI_push_random, VMI_push_delta_t, VMI_push_true, VMI_push_false,
7148
- VMI_push_pi, VMI_push_infinity, VMI_push_selector_wildcard,
7564
+ VMI_push_pi, VMI_push_infinity, VMI_push_contextual_number,
7149
7565
  VMI_push_i, VMI_push_j, VMI_push_k,
7150
7566
  VMI_push_year, VMI_push_week, VMI_push_day, VMI_push_hour,
7151
7567
  VMI_push_minute, VMI_push_second],