linny-r 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -910,12 +910,12 @@ function loadData(res, url) {
910
910
  // the call-back Python script specified for the channel
911
911
 
912
912
  function receiver(res, sp) {
913
- //This function processes all receiver actions
913
+ // This function processes all receiver actions.
914
914
  let
915
915
  rpath = anyOSpath(sp.get('path') || ''),
916
916
  rfile = anyOSpath(sp.get('file') || '');
917
- // Assume that path is relative to channel directory unless it starts with
918
- // a (back)slash or specifiess drive or volume
917
+ // Assume that path is relative to working directory unless it starts
918
+ // with a (back)slash or specifies drive or volume.
919
919
  if(!(rpath.startsWith(path.sep) || rpath.indexOf(':') >= 0 ||
920
920
  rpath.startsWith(WORKING_DIRECTORY))) {
921
921
  rpath = path.join(WORKING_DIRECTORY, rpath);
@@ -1038,8 +1038,49 @@ function rcvrAbort(res, rpath, rfile, log) {
1038
1038
  }
1039
1039
 
1040
1040
  function rcvrReport(res, rpath, rfile, run, data, stats, log) {
1041
+ // Purge reports older than 24 hours.
1041
1042
  try {
1042
- let fp = path.join(rpath, rfile + run + '-data.txt');
1043
+ const
1044
+ now = new Date(),
1045
+ flist = fs.readdirSync(WORKSPACE.reports);
1046
+ let n = 0;
1047
+ for(let i = 0; i < flist.length; i++) {
1048
+ const
1049
+ pp = path.parse(flist[i]),
1050
+ fp = path.join(WORKSPACE.reports, flist[i]);
1051
+ // NOTE: Only consider text files (extension .txt)
1052
+ if(pp.ext === '.txt') {
1053
+ // Delete only if file is older than 24 hours.
1054
+ const fstat = fs.statSync(fp);
1055
+ if(now - fstat.mtimeMs > 24 * 3600000) {
1056
+ // Delete text file
1057
+ try {
1058
+ fs.unlinkSync(fp);
1059
+ n++;
1060
+ } catch(err) {
1061
+ console.log('WARNING: Failed to delete', fp);
1062
+ console.log(err);
1063
+ }
1064
+ }
1065
+ }
1066
+ }
1067
+ if(n) console.log(n + 'report file' + (n > 1 ? 's' : '') + 'purged');
1068
+ } catch(err) {
1069
+ // Log error, but do not abort.
1070
+ console.log(err);
1071
+ }
1072
+ // Now save the reports.
1073
+ // NOTE: The optional @ indicates where the run number must be inserted.
1074
+ // If not specified, append run number to the base report file name.
1075
+ if(rfile.indexOf('@') < 0) {
1076
+ rfile += run;
1077
+ } else {
1078
+ rfile = rfile.replace('@', run);
1079
+ }
1080
+ const base = path.join(rpath, rfile);
1081
+ let fp;
1082
+ try {
1083
+ fp = path.join(base + '-data.txt');
1043
1084
  fs.writeFileSync(fp, data);
1044
1085
  } catch(err) {
1045
1086
  console.log(err);
@@ -1048,7 +1089,7 @@ function rcvrReport(res, rpath, rfile, run, data, stats, log) {
1048
1089
  return;
1049
1090
  }
1050
1091
  try {
1051
- fp = path.join(rpath, rfile + run + '-stats.txt');
1092
+ fp = path.join(base + '-stats.txt');
1052
1093
  fs.writeFileSync(fp, stats);
1053
1094
  } catch(err) {
1054
1095
  console.log(err);
@@ -1057,7 +1098,7 @@ function rcvrReport(res, rpath, rfile, run, data, stats, log) {
1057
1098
  return;
1058
1099
  }
1059
1100
  try {
1060
- fp = path.join(rpath, rfile + run + '-log.txt');
1101
+ fp = path.join(base + '-log.txt');
1061
1102
  fs.writeFileSync(fp, log);
1062
1103
  } catch(err) {
1063
1104
  console.log(err);
@@ -1630,6 +1671,7 @@ function createWorkspace() {
1630
1671
  data: path.join(SETTINGS.user_dir, 'data'),
1631
1672
  diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
1632
1673
  modules: path.join(SETTINGS.user_dir, 'modules'),
1674
+ reports: path.join(SETTINGS.user_dir, 'reports'),
1633
1675
  solver_output: path.join(SETTINGS.user_dir, 'solver')
1634
1676
  };
1635
1677
  // Create these sub-directories if not aready there
package/static/index.html CHANGED
@@ -601,6 +601,14 @@ and move the cursor over the status bar">
601
601
  </td>
602
602
  <td style="padding-bottom:4px">Infer and display cost prices</td>
603
603
  </tr>
604
+ <tr title="Reports will be saved in user/reports/, and removed after 24 h">
605
+ <td style="padding:0px">
606
+ <div id="settings-report-results" class="box clear"></div>
607
+ </td>
608
+ <td style="padding-bottom:4px">
609
+ Report results after each run
610
+ </td>
611
+ </tr>
604
612
  <tr>
605
613
  <td style="padding:0px">
606
614
  <div id="settings-block-arrows" class="box clear"></div>
@@ -355,6 +355,17 @@ class Controller {
355
355
  return nodes.join(arrow);
356
356
  }
357
357
 
358
+ tailNumber(name) {
359
+ // Returns the string of digits at the end of `name`. If not there,
360
+ // check prefixes (if any) from right to left for a tail number.
361
+ const pan = UI.prefixesAndName(name);
362
+ let n = endsWithDigits(pan.pop());
363
+ while(!n && pan.length > 0) {
364
+ n = endsWithDigits(pan.pop());
365
+ }
366
+ return n;
367
+ }
368
+
358
369
  nameToID(name) {
359
370
  // Returns a name in lower case with link arrow replaced by three
360
371
  // underscores, constraint link arrow by four underscores, and spaces
@@ -4901,7 +4901,7 @@ class GUIController extends Controller {
4901
4901
  // Logs MB's of used heap memory to console (to detect memory leaks)
4902
4902
  // NOTE: this feature is supported only by Chrome
4903
4903
  if(msg) msg += ' -- ';
4904
- if(typeof performance.memory !== 'undefined') {
4904
+ if(performance.memory !== undefined) {
4905
4905
  console.log(msg + 'Allocated memory: ' + Math.round(
4906
4906
  performance.memory.usedJSHeapSize/1048576.0).toFixed(1) + ' MB');
4907
4907
  }
@@ -5701,6 +5701,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
5701
5701
  this.setBox('settings-decimal-comma', model.decimal_comma);
5702
5702
  this.setBox('settings-align-to-grid', model.align_to_grid);
5703
5703
  this.setBox('settings-cost-prices', model.infer_cost_prices);
5704
+ this.setBox('settings-report-results', model.report_results);
5704
5705
  this.setBox('settings-block-arrows', model.show_block_arrows);
5705
5706
  md.show('name');
5706
5707
  }
@@ -5753,6 +5754,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
5753
5754
  if(!model.scale_units.hasOwnProperty(dsu)) model.addScaleUnit(dsu);
5754
5755
  model.default_unit = dsu;
5755
5756
  model.currency_unit = md.element('currency-unit').value.trim();
5757
+ model.report_results = UI.boxChecked('settings-report-results');
5756
5758
  model.encrypt = UI.boxChecked('settings-encrypt');
5757
5759
  model.decimal_comma = UI.boxChecked('settings-decimal-comma');
5758
5760
  // Some changes may necessitate redrawing the diagram
@@ -15991,8 +15993,8 @@ class GUIReceiver {
15991
15993
  }
15992
15994
 
15993
15995
  log(msg) {
15994
- // Logs a message displayed on the status line while solving
15995
- if(this.active) {
15996
+ // Logs a message displayed on the status line while solving.
15997
+ if(this.active || MODEL.report_results) {
15996
15998
  if(!msg.startsWith('[')) {
15997
15999
  const
15998
16000
  d = new Date(),
@@ -16135,21 +16137,34 @@ class GUIReceiver {
16135
16137
  report() {
16136
16138
  // Posts the run results to the local server, or signals an error
16137
16139
  let form,
16138
- run = '';
16140
+ run = '',
16141
+ path = this.channel,
16142
+ file = this.file_name;
16139
16143
  // NOTE: Always set `solving` to FALSE
16140
16144
  this.solving = false;
16141
- if(this.experiment){
16145
+ // NOTE: When reporting receiver while is not active, report the
16146
+ // results of the running experiment.
16147
+ if(this.experiment || !this.active) {
16142
16148
  if(MODEL.running_experiment) {
16143
16149
  run = MODEL.running_experiment.active_combination_index;
16144
16150
  this.log(`Reporting: ${this.file_name} (run #${run})`);
16145
16151
  }
16146
16152
  }
16153
+ // NOTE: If receiver is not active, path and file must be set.
16154
+ if(!this.active) {
16155
+ path = 'user/reports';
16156
+ // NOTE: The @ will be replaced by the run number, so that that
16157
+ // number precedes the clock time. The @ will be unique because
16158
+ // `asFileName()` replaces special characters by underscores.
16159
+ file = REPOSITORY_BROWSER.asFileName(MODEL.name || 'model') +
16160
+ '@-' + compactClockTime();
16161
+ }
16147
16162
  if(MODEL.solved && !VM.halted) {
16148
16163
  // Normal execution termination => report results
16149
16164
  const od = MODEL.outputData;
16150
16165
  form = {
16151
- path: this.channel,
16152
- file: this.file_name,
16166
+ path: path,
16167
+ file: file,
16153
16168
  action: 'report',
16154
16169
  run: run,
16155
16170
  data: od[0],
@@ -16178,10 +16193,11 @@ class GUIReceiver {
16178
16193
  .then((data) => {
16179
16194
  // For experiments, only display server response if warning or error
16180
16195
  UI.postResponseOK(data, !RECEIVER.experiment);
16181
- // If execution completed, perform the call-back action
16196
+ // If execution completed, perform the call-back action if the
16197
+ // receiver is active (so not when auto-reporting a run).
16182
16198
  // NOTE: for experiments, call-back is performed upon completion by
16183
- // the Experiment Manager
16184
- if(!RECEIVER.experiment) RECEIVER.callBack();
16199
+ // the Experiment Manager.
16200
+ if(RECEIVER.active && !RECEIVER.experiment) RECEIVER.callBack();
16185
16201
  })
16186
16202
  .catch(() => UI.warn(UI.WARNING.NO_CONNECTION, err));
16187
16203
  }
@@ -96,6 +96,7 @@ class LinnyRModel {
96
96
  this.grid_pixels = 20;
97
97
  this.align_to_grid = true;
98
98
  this.infer_cost_prices = false;
99
+ this.report_results = false;
99
100
  this.show_block_arrows = true;
100
101
  this.last_zoom_factor = 1;
101
102
 
@@ -2486,6 +2487,7 @@ class LinnyRModel {
2486
2487
  this.decimal_comma = nodeParameterValue(node, 'decimal-comma') === '1';
2487
2488
  this.align_to_grid = nodeParameterValue(node, 'align-to-grid') === '1';
2488
2489
  this.infer_cost_prices = nodeParameterValue(node, 'cost-prices') === '1';
2490
+ this.report_results = nodeParameterValue(node, 'report-results') === '1';
2489
2491
  this.show_block_arrows = nodeParameterValue(node, 'block-arrows') === '1';
2490
2492
  this.name = xmlDecoded(nodeContentByTag(node, 'name'));
2491
2493
  this.author = xmlDecoded(nodeContentByTag(node, 'author'));
@@ -2828,6 +2830,7 @@ class LinnyRModel {
2828
2830
  if(this.decimal_comma) p += ' decimal-comma="1"';
2829
2831
  if(this.align_to_grid) p += ' align-to-grid="1"';
2830
2832
  if(this.infer_cost_prices) p += ' cost-prices="1"';
2833
+ if(this.report_results) p += ' report-results="1"';
2831
2834
  if(this.show_block_arrows) p += ' block-arrows="1"';
2832
2835
  let xml = this.xml_header + ['<model', p, '><name>', xmlEncoded(this.name),
2833
2836
  '</name><author>', xmlEncoded(this.author),
@@ -4738,7 +4741,7 @@ class Note extends ObjectWithXYWH {
4738
4741
  get numberContext() {
4739
4742
  // Returns the string to be used to evaluate #. For notes this is
4740
4743
  // their note number if specified, otherwise the number context of a
4741
- // nearby enode, and otherwise the number context of their cluster.
4744
+ // nearby node, and otherwise the number context of their cluster.
4742
4745
  let n = this.number;
4743
4746
  if(n) return n;
4744
4747
  n = this.nearbyNode;
@@ -5203,16 +5206,8 @@ class NodeBox extends ObjectWithXYWH {
5203
5206
 
5204
5207
  get numberContext() {
5205
5208
  // Returns the string to be used to evaluate #, so for clusters,
5206
- // processes and products this is the string of trailing digits
5207
- // (or empty if none) of the node name, or if that does not end on
5208
- // a number, the trailing digits of the first prefix (from right to
5209
- // left) that does end on a number
5210
- const sn = UI.prefixesAndName(this.name);
5211
- let nc = endsWithDigits(sn.pop());
5212
- while(!nc && sn.length > 0) {
5213
- nc = endsWithDigits(sn.pop());
5214
- }
5215
- return nc;
5209
+ // processes and products this is their "tail number".
5210
+ return UI.tailNumber(this.name);
5216
5211
  }
5217
5212
 
5218
5213
  get similarNumberedEntities() {
@@ -8271,17 +8266,10 @@ class DatasetModifier {
8271
8266
  // NOTE: If the selector contains wildcards, return "?" to indicate
8272
8267
  // that the value of # cannot be inferred at compile time.
8273
8268
  if(this.hasWildcards) return '?';
8274
- // Otherwise, # is the string of digits at the end of the selector.
8275
- // NOTE: equation names are like entity names, so treat them as such,
8276
- // i.e., also check for prefixes that end on digits.
8277
- const sn = UI.prefixesAndName(this.name);
8278
- let nc = endsWithDigits(sn.pop());
8279
- while(!nc && sn.length > 0) {
8280
- nc = endsWithDigits(sn.pop());
8281
- }
8282
- // NOTE: if the selector has no tail number, return the number context
8283
- // of the dataset of this modifier.
8284
- return nc || this.dataset.numberContext;
8269
+ // Otherwise, return the "tail number" of the selector, or if the
8270
+ // selector has no tail number, return the number context of the
8271
+ // dataset of this modifier.
8272
+ return UI.tailnumber(this.name) || this.dataset.numberContext;
8285
8273
  }
8286
8274
 
8287
8275
  match(s) {
@@ -8366,14 +8354,8 @@ class Dataset {
8366
8354
 
8367
8355
  get numberContext() {
8368
8356
  // Returns the string to be used to evaluate #
8369
- // Like for nodes, this is the string of digits at the end of the
8370
- // dataset name (if any) or an empty string (to denote undefined)
8371
- const sn = UI.prefixesAndName(this.name);
8372
- let nc = endsWithDigits(sn.pop());
8373
- while(!nc && sn.length > 0) {
8374
- nc = endsWithDigits(sn.pop());
8375
- }
8376
- return nc;
8357
+ // Like for nodes, this is the "tail number" of the dataset name.
8358
+ return UI.tailNumber(this.name);
8377
8359
  }
8378
8360
 
8379
8361
  get selectorList() {
@@ -135,6 +135,14 @@ function msecToTime(msec) {
135
135
  return hms + '.' + ms.slice(0, 1) + ' sec';
136
136
  }
137
137
 
138
+ function compactClockTime() {
139
+ // Returns current time (no date) in 6 digits hhmmss.
140
+ const d = new Date();
141
+ return d.getHours().toString().padStart(2, '0') +
142
+ d.getMinutes().toString().padStart(2, '0') +
143
+ d.getSeconds().toString().padStart(2, '0');
144
+ }
145
+
138
146
  function uniformDecimals(data) {
139
147
  // Formats the numbers in the array `data` so that they have uniform decimals
140
148
  // NOTE: (1) this routine assumes that all number strings have sig4Dig format;
@@ -627,13 +635,11 @@ function customizeXML(str) {
627
635
  // for example to rename entities in one go -- USE WITH CARE!
628
636
  // First modify `str` -- by default, do nothing
629
637
 
630
-
631
- if(str.indexOf('<author>Emma van Kleef</author>') >= 0) {
632
- str = str.replace(/is-information="1"><name>Line (\d+): switch</g,
633
- 'is-information="1" no-slack="1"><name>Line $1: switch<');
634
- str = str.replace(/: zon/g, ': solar');
638
+ /*
639
+ if(str.indexOf('<author>XXX</author>') >= 0) {
640
+ str = str.replace(/<url>NL\/(\w+)\.csv<\/url>/g, '<url></url>');
635
641
  }
636
-
642
+ */
637
643
 
638
644
  // Finally, return the modified string
639
645
  return str;
@@ -911,6 +917,7 @@ if(NODE) module.exports = {
911
917
  rangeToList: rangeToList,
912
918
  dateToString: dateToString,
913
919
  msecToTime: msecToTime,
920
+ compactClockTime: compactClockTime,
914
921
  uniformDecimals: uniformDecimals,
915
922
  ellipsedText: ellipsedText,
916
923
  earlierVersion: earlierVersion,
@@ -270,53 +270,61 @@ class Expression {
270
270
  }
271
271
 
272
272
  chooseVector(number) {
273
- // Return the vector to use for computation (defaults to "own" vector)
274
- if(number === false || this.isStatic) return this.vector;
275
- if(number) {
276
- // Use the vector for the wildcard number (create it if necessary)
277
- if(!this.wildcard_vectors.hasOwnProperty(number)) {
278
- this.wildcard_vectors[number] = [];
273
+ // Return the vector to use for computation (defaults to "own" vector).
274
+ // NOTE: Static wildcard expressions must also choose a vector!
275
+ if(typeof number !== 'number' ||
276
+ (this.isStatic && !this.isWildcardExpression)) return this.vector;
277
+ // Use the vector for the wildcard number (create it if necessary).
278
+ if(!this.wildcard_vectors.hasOwnProperty(number)) {
279
+ this.wildcard_vectors[number] = [];
280
+ if(this.isStatic) {
281
+ this.wildcard_vectors[number][0] = VM.NOT_COMPUTED;
282
+ } else {
279
283
  MODEL.cleanVector(this.wildcard_vectors[number], VM.NOT_COMPUTED);
280
284
  }
281
- return this.wildcard_vectors[number];
282
285
  }
286
+ return this.wildcard_vectors[number];
283
287
  }
284
288
 
285
289
  compute(t, number=false) {
286
- // Executes the VM code for this expression for time step t
287
- // NOTE: `number` is passed (as integer) only if context for # is defined
290
+ // Executes the VM code for this expression for time step `t`.
291
+ // NOTE: `number` is passed only if context for # is defined.
288
292
  if(!this.compiled) this.compile();
289
- // Return FALSE if compilation resulted in error
293
+ // Return FALSE if compilation resulted in error.
290
294
  if(!this.compiled) return false;
291
- // Compute static expressions as if t = 0
295
+ // Compute static expressions as if t = 0.
292
296
  if(t < 0 || this.isStatic) t = 0;
293
- // Select the vector to use
297
+ // Select the vector to use.
294
298
  const v = this.chooseVector(number);
295
- // Check for potential error (that should NOT occur)
296
- if(!v || v.length === 0 || t >= v.length) {
299
+ // Check for potential error (that should NOT occur).
300
+ if(!Array.isArray(v) || v.length === 0 || t >= v.length) {
297
301
  const msg = 'ERROR: Undefined value during expression evaluation';
298
302
  UI.alert(msg);
299
303
  console.log(this.variableName, ':', this.text, '#', number, '@', t, v);
300
- // Throw exception to permit viewing the function call stack
304
+ // Throw exception to permit viewing the function call stack.
301
305
  throw msg;
302
306
  }
303
307
  // When called while already computing for time step t, signal this
304
308
  // as an error value.
305
309
  if(v[t] === VM.COMPUTING) v[t] = VM.CYCLIC;
306
- // Compute a value only once
307
- if(v[t] !== VM.NOT_COMPUTED) return true;
308
- // Provide selector context for # (number = FALSE => no wildcard match)
310
+ // Compute a value only once.
311
+ if(v[t] !== VM.NOT_COMPUTED) {
312
+ if(DEBUGGING) console.log('Already computed', this.variableName,
313
+ ':', this.text, '#', number, '@', t, v[t]);
314
+ return true;
315
+ }
316
+ // Provide selector context for # (number = FALSE => no wildcard match).
309
317
  this.wildcard_vector_index = number;
310
- // Push this expression onto the call stack
318
+ // Push this expression onto the call stack.
311
319
  VM.call_stack.push(this);
312
- // Push time step in case a VMI_push_var instruction references
313
- // this same variable
314
- this.trace('--START: ' + this.variableName);
320
+ // Push time step in case a VMI instruction for another expression
321
+ // references this same variable.
322
+ this.trace(`--START: ${this.variableName} (wvi = ${number})`);
315
323
  this.step.push(t);
316
- // NOTE: trace extression AFTER pushing the time step
324
+ // NOTE: Trace expression AFTER pushing the time step.
317
325
  this.trace(`"${this.text}"`);
318
326
  v[t] = VM.COMPUTING;
319
- // Execute the instructions
327
+ // Execute the instructions.
320
328
  let vmi = null,
321
329
  ok = true,
322
330
  cl = this.code.length;
@@ -325,13 +333,13 @@ class Expression {
325
333
  this.stack.length = 0;
326
334
  while(ok && this.program_counter < cl && v[t] === VM.COMPUTING) {
327
335
  vmi = this.code[this.program_counter];
328
- // Instructions are 2-element arrays [function, [arguments]];
329
- // the function is called with this expression as first parameter,
330
- // and the argument list as second parameter
336
+ // Instructions are 2-element arrays [function, [arguments]].
337
+ // The function is called with this expression as first parameter,
338
+ // and the argument list as second parameter.
331
339
  vmi[0](this, vmi[1]);
332
340
  this.program_counter++;
333
341
  }
334
- // Stack should now have length 1
342
+ // Stack should now have length 1.
335
343
  if(this.stack.length > 1) {
336
344
  v[t] = VM.OVERFLOW;
337
345
  } else if(this.stack.length < 1) {
@@ -340,19 +348,20 @@ class Expression {
340
348
  v[t] = this.stack.pop();
341
349
  }
342
350
  this.trace('RESULT = ' + VM.sig4Dig(v[t]));
343
- // Pop the time step
351
+ // Pop the time step.
344
352
  this.step.pop();
345
353
  this.trace('--STOP: ' + this.variableName);
346
- // Clear context for #
354
+ // Clear context for # for this expression (no stack needed, as
355
+ // wildcard expressions cannot reference themselves).
347
356
  this.wildcard_vector_index = false;
348
- // If error, display the call stack (only once)
357
+ // If error, display the call stack (only once).
349
358
  // NOTE: "undefined", "not computed" and "still computing" are NOT
350
359
  // problematic unless they result in an error (stack over/underflow)
351
360
  if(v[t] <= VM.ERROR) {
352
361
  MONITOR.showCallStack(t);
353
362
  VM.logCallStack(t);
354
363
  }
355
- // Always pop the expression from the call stack
364
+ // Always pop the expression from the call stack.
356
365
  VM.call_stack.pop(this);
357
366
  return true;
358
367
  }
@@ -365,7 +374,10 @@ class Expression {
365
374
  // "initial value" (these follow from the variables used in the expression)
366
375
  // Select the vector to use
367
376
  const v = this.chooseVector(number);
368
- if(!v) return VM.UNDEFINED;
377
+ if(!Array.isArray(v)) {
378
+ console.log('ANOMALY: No vector for result(t)');
379
+ return VM.UNDEFINED;
380
+ }
369
381
  if(t < 0 || this.isStatic) t = 0;
370
382
  if(t >= v.length) return VM.UNDEFINED;
371
383
  if(v[t] === VM.NOT_COMPUTED || v[t] === VM.COMPUTING) {
@@ -532,12 +544,9 @@ class Expression {
532
544
  // using the variable [partial load 1] to compute the partial load for
533
545
  // process P1.
534
546
  // 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.
547
+ // (1) This applies recursively, so [partial load #] can be used in some other
548
+ // wildcard equation like, for example, "percent load ??" having expression
549
+ // "100 * [partial load #]".
541
550
  // (2) The # may be used in patterns, so when a model comprises processes
542
551
  // P1 and P2, and products Q2 and Q3, and a wildcard equation "total level ??"
543
552
  // with expression "[SUM$#|L]", then [total level 1] will evaluate as the level
@@ -545,6 +554,8 @@ class Expression {
545
554
 
546
555
  class ExpressionParser {
547
556
  constructor(text, owner=null, attribute='') {
557
+ // Setting TRACE to TRUE will log parsing information to the console.
558
+ this.TRACE = false;
548
559
  // `text` is the expression string to be parsed.
549
560
  this.expr = text;
550
561
  // NOTE: When expressions for dataset modifiers or equations are
@@ -557,11 +568,12 @@ class ExpressionParser {
557
568
  this.selector = '';
558
569
  this.context_number = '';
559
570
  this.wildcard_selector = false;
560
- this.wildcard_equation = false;
561
571
  // 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.
572
+ // NOTE: This this will always be a string. Three possible cases:
573
+ // (1) a question mark "?" if `owner` is a dataset and `attribute`
574
+ // wildcards in its selector; this indicates that the value of # cannot be inferred at
575
+ // compile time.
576
+ //
565
577
  if(owner) {
566
578
  this.context_number = owner.numberContext;
567
579
  // NOTE: The owner prefix includes the trailing colon+space.
@@ -578,19 +590,34 @@ class ExpressionParser {
578
590
  if(owner instanceof Dataset) {
579
591
  this.dataset = owner;
580
592
  // The attribute (if specified) is a dataset modifier selector.
593
+ // This may be the name of an equation; this can be tested by
594
+ // checking whether the owner is the equations dataset.
581
595
  this.selector = attribute;
582
- // Record whether this selector contains wildcards.
596
+ // Record whether this selector contains wildcards (? and/or *
597
+ // for selectors, ?? for equations).
583
598
  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 {
599
+ if(this.wildcard_selector) {
600
+ // NOTE: Wildcard selectors override the context number that
601
+ // may have been inferred from the dataset name.
602
+ this.context_number = '?';
603
+ } else {
604
+ // Normal selectors may have a "tail number". If so, this
605
+ // overrides the tail number of the dataset.
606
+ const tn = UI.tailNumber(attribute);
607
+ if(tn) this.context_number = tn;
608
+ }
609
+ if(owner !== MODEL.equations_dataset) {
588
610
  // For "normal" modifier expressions, the "dot" (.) can be used
589
611
  // to refer to the dataset of the modifier.
590
612
  this.dot = this.dataset;
591
613
  }
592
614
  }
593
615
  }
616
+ // Ensure that context number is either '?' or a number or FALSE.
617
+ if(this.context_number !== '?') {
618
+ this.context_number = parseInt(this.context_number);
619
+ if(isNaN(this.context_number)) this.context_number = false;
620
+ }
594
621
  // Immediately compile; this may generate warnings
595
622
  this.compile();
596
623
  }
@@ -601,14 +628,18 @@ class ExpressionParser {
601
628
  let n = this.owner.displayName;
602
629
  if(this.attribute) n += '|' + this.attribute;
603
630
  if(this.wildcard_selector) {
604
- n += ' [wildcard ' + (this.wildcard_equation ? 'equation' : 'modifier') +
605
- (this.context_number ? ' # = ' + this.context_number : '') + ']';
631
+ n = [n, ' [wildcard ',
632
+ (this.dataset === MODEL.equations_dataset ?
633
+ 'equation' : 'modifier'),
634
+ (this.context_number !== false ?
635
+ ' # = ' + this.context_number : ''),
636
+ ']'].join('');
606
637
  }
607
638
  return n;
608
639
  }
609
640
 
610
641
  log(msg) {
611
- // FOR TRACING & DEBUGGING: Logs a message to the console.
642
+ // NOTE: This method is used only to profile dynamic expressions.
612
643
  if(true) return;
613
644
  // Set the above IF condition to FALSE to profile dynamic expressions.
614
645
  console.log(`Expression for ${this.ownerName}: ${this.expr}\n${msg}`);
@@ -630,6 +661,14 @@ class ExpressionParser {
630
661
  parseVariable(name) {
631
662
  // Reduce whitespace to single space.
632
663
  name = name.replace(/\s+/g, ' ');
664
+
665
+ // For debugging, TRACE can be used to log to the console for
666
+ // specific expressions and/or variables, for example:
667
+ // this.TRACE = name.endsWith('losses') || this.ownerName.endsWith('losses');
668
+ if(this.TRACE) console.log(
669
+ `TRACE: Parsing variable "${name}" in expression for`,
670
+ this.ownerName, ' --> ', this.expr);
671
+
633
672
  // Initialize possible components.
634
673
  let obj = null,
635
674
  attr = '',
@@ -640,6 +679,7 @@ class ExpressionParser {
640
679
  anchor2 = '',
641
680
  offset2 = 0,
642
681
  msg = '',
682
+ arg0 = null,
643
683
  args = null,
644
684
  s = name.split('@');
645
685
  if(s.length > 1) {
@@ -691,7 +731,7 @@ class ExpressionParser {
691
731
  }
692
732
  // Check whether # anchor is meaningful for this expression
693
733
  if((anchor1 === '#' || anchor2 === '#') &&
694
- !(this.wildcard_selector || this.context_number)) {
734
+ !(this.wildcard_selector || this.context_number !== false)) {
695
735
  // Log debugging information for this error
696
736
  console.log(this.owner.displayName, this.owner.type, this.selector);
697
737
  this.error = 'Anchor # is undefined in this context';
@@ -805,7 +845,8 @@ class ExpressionParser {
805
845
  if(x.x) {
806
846
  // Look up name in experiment outcomes list
807
847
  x.v = x.x.resultIndex(name);
808
- if(x.v < 0 && name.indexOf('#') >= 0 && this.context_number) {
848
+ if(x.v < 0 && name.indexOf('#') >= 0 &&
849
+ typeof this.context_number === 'number') {
809
850
  // Variable name may be parametrized with #, but not in
810
851
  // expressions for wildcard selectors
811
852
  name = name.replace('#', this.context_number);
@@ -819,9 +860,10 @@ class ExpressionParser {
819
860
  // Check outcome list of ALL experiments
820
861
  for(let i = 0; i < MODEL.experiments.length; i++) {
821
862
  let xri = MODEL.experiments[i].resultIndex(name);
822
- if(xri < 0 && name.indexOf('#') >= 0 && this.context_number) {
863
+ if(xri < 0 && name.indexOf('#') >= 0 &&
864
+ typeof this.context_number === 'number') {
823
865
  // Variable name may be parametrized with #, but not in
824
- // expressions for wildcard selectors
866
+ // expressions for wildcard selectors.
825
867
  name = name.replace('#', this.context_number);
826
868
  xri = MODEL.experiments[i].resultIndex(name);
827
869
  }
@@ -859,6 +901,7 @@ class ExpressionParser {
859
901
  // For experiment run results, default anchor is 't'.
860
902
  if(!anchor1) anchor1 = 't';
861
903
  if(!anchor2) anchor2 = 't';
904
+ if(this.TRACE) console.log('TRACE: Variable is run result. x =', x);
862
905
  // NOTE: compiler will recognize `x` to indicate "push run results".
863
906
  return [x, anchor1, offset1, anchor2, offset2];
864
907
  }
@@ -899,7 +942,7 @@ class ExpressionParser {
899
942
  let pat = name.split('$');
900
943
  if(pat.length > 1 &&
901
944
  VM.statistic_operators.indexOf(pat[0].toUpperCase()) >= 0) {
902
- // For statistics, default anchor is 't'.
945
+ // For statistics, the default anchor is 't'.
903
946
  if(!anchor1) anchor1 = 't';
904
947
  if(!anchor2) anchor2 = 't';
905
948
  // Check whether unit balance for clusters is asked for.
@@ -916,17 +959,20 @@ class ExpressionParser {
916
959
  // NOTE: The "dot" dataset is not level-dependent, and statistics
917
960
  // over its vector do NOT make the expression dynamic.
918
961
  if(this.dot) {
919
- return [stat, [this.dot.vector], anchor1, offset1, anchor2, offset2];
962
+ args = [stat, [this.dot.vector], anchor1, offset1, anchor2, offset2];
963
+ if(this.TRACE) console.log('TRACE: Variable is a statistic:', args);
964
+ return args;
920
965
  } else {
921
966
  this.error = UI.ERROR.NO_DATASET_DOT;
922
967
  return false;
923
968
  }
924
969
  }
925
970
  // Deal with "prefix inheritance" when pattern starts with a colon.
926
- if(pat.startsWith(':')) {
971
+ if(pat.startsWith(':') && this.owner_prefix) {
927
972
  // Add a "must start with" AND condition to all OR clauses of the
928
973
  // pattern.
929
- // NOTE: Issues may occur when prefix contains &, ^ or # (@@TO DO?)
974
+ // NOTE: Issues may occur when prefix contains &, ^ or #.
975
+ // @@TO DO: See if this can be easily prohibited.
930
976
  const oc = pat.substring(1).split('|');
931
977
  for(let i = 0; i < oc.length; i++) {
932
978
  oc[i] = `~${this.owner_prefix}&${oc[i]}`;
@@ -936,7 +982,9 @@ class ExpressionParser {
936
982
  // NOTE: For patterns, assume that # *always* denotes the context-
937
983
  // sensitive number #, because if modelers wishes to include
938
984
  // ANY number, they can make their pattern less selective.
939
- if(this.context_number) pat = pat.replace('#', this.context_number);
985
+ if(typeof this.context_number === 'number') {
986
+ pat = pat.replace('#', this.context_number);
987
+ }
940
988
  // By default, consider all entity types.
941
989
  let et = VM.entity_letters,
942
990
  patstr = pat;
@@ -956,7 +1004,7 @@ class ExpressionParser {
956
1004
  // returned list to the specified entity types.
957
1005
  ewa = MODEL.entitiesWithAttribute(attr, et);
958
1006
  // Create list of expression objects for the matching entities.
959
- // Also create a "dict" with for each matching wildcard number
1007
+ // Also create a "dict" with, for each matching wildcard number,
960
1008
  // the matching entities as a separate list. This will permit
961
1009
  // narrowing the selection at run time, based on the expression's
962
1010
  // wildcard number.
@@ -1002,16 +1050,18 @@ class ExpressionParser {
1002
1050
  }
1003
1051
  }
1004
1052
  if(list.length > 0) {
1005
- // NOTE: statistic MAY make expression level-based
1006
- // NOTE: assume NOT when offset has been specified, as this suggests
1007
- // that modelers know what they're doing
1053
+ // NOTE: Statistic MAY make expression level-based.
1054
+ // Assume that this is NOT so when an offset has been specified,
1055
+ // as this suggests that modelers know what they're doing.
1008
1056
  this.is_level_based = this.is_level_based ||
1009
1057
  VM.level_based_attr.indexOf(attr) >= 0 &&
1010
1058
  anchor1 === 't' && offset1 === 0 &&
1011
1059
  anchor2 === 't' && offset2 === 0;
1012
- const args = [stat, list, anchor1, offset1, anchor2, offset2];
1060
+ args = [stat, list, anchor1, offset1, anchor2, offset2];
1013
1061
  if(Object.keys(wdict).length > 0) args.push(wdict);
1014
- // NOTE: Compiler will recognize 6- or 7-element list as a "statistic"
1062
+ if(this.TRACE) console.log('TRACE: Variable is a statistic:', args);
1063
+ // NOTE: Compiler will recognize 6- or 7-element list as a
1064
+ // sign to use the VMI_push_statistic instruction.
1015
1065
  return args;
1016
1066
  }
1017
1067
  this.error = `No entities that match pattern "${patstr}"` +
@@ -1045,6 +1095,7 @@ class ExpressionParser {
1045
1095
  this.log('dynamic because of self-reference');
1046
1096
  if(('cips'.indexOf(anchor1) >= 0 || anchor1 === 't' && offset1 < 0) &&
1047
1097
  ('cips'.indexOf(anchor2) >= 0 ||anchor2 === 't' && offset2 < 0)) {
1098
+ if(this.TRACE) console.log('TRACE: Variable is a self-reference.');
1048
1099
  // The `xv` attribute will be recognized by VMI_push_var to denote
1049
1100
  // "use the vector of the expression for which this VMI is code".
1050
1101
  return [{xv: true, dv: this.dataset.defaultValue},
@@ -1080,6 +1131,9 @@ class ExpressionParser {
1080
1131
  id = UI.nameToID(name),
1081
1132
  w = MODEL.wildcardEquationByID(id);
1082
1133
  if(w) {
1134
+ if(this.TRACE) console.log('TRACE: Variable is a wildcard equation:',
1135
+ w[0], '-- number is', w[1], '\nTRACE: Equation expression: ',
1136
+ w[0].expression.text);
1083
1137
  // Variable matches wildcard equation w[0] with number w[1],
1084
1138
  // so this equation must be evaluated for that number.
1085
1139
  return [
@@ -1089,11 +1143,13 @@ class ExpressionParser {
1089
1143
  // If no match, try to match the object ID with any type of entity.
1090
1144
  obj = MODEL.objectByID(id);
1091
1145
  }
1092
- // If not, retry if wildcards can be substituted.
1146
+ // If not, try whether wildcards can be substituted.
1093
1147
  if(!obj && name.indexOf('#') >= 0) {
1094
- if(this.context_number) {
1148
+ if(typeof this.context_number === 'number') {
1095
1149
  obj = MODEL.objectByName(name.replace('#', this.context_number));
1096
1150
  }
1151
+ if(obj && TRACE) console.log('TRACE: Matched ', name,
1152
+ 'with entity:', obj.displayName);
1097
1153
  if(!obj) {
1098
1154
  // If immediate substitution of # does not identify an entity,
1099
1155
  // then name may still refer to a wildcard equation.
@@ -1104,9 +1160,9 @@ class ExpressionParser {
1104
1160
  } else {
1105
1161
  obj = MODEL.equationByID(UI.nameToID(wcname));
1106
1162
  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)) {
1163
+ // Special case: the parsed variable references a wildcard
1164
+ // equation, so now `obj` is an instance of DatasetModifier.
1165
+ if(!(this.wildcard_selector || this.context_number)) {
1110
1166
  msg = UI.ERROR.NO_NUMBER_CONTEXT;
1111
1167
  } else {
1112
1168
  // Acceptable reference to a wildcard equation.
@@ -1117,23 +1173,13 @@ class ExpressionParser {
1117
1173
  // NOTE: The referenced expression may be level-dependent.
1118
1174
  this.is_level_based = this.is_level_based ||
1119
1175
  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
- }
1176
+ if(this.TRACE) console.log('TRACE: Variable ', name,
1177
+ 'is a wildcard equation:', obj.displayName,
1178
+ '-- number is:', this.context_number,
1179
+ '\nTRACE: Expression:', obj.expression.text);
1134
1180
  // Use the context number as "selector" parameter of the VMI.
1135
1181
  return [
1136
- {d: obj.dataset, s: cnr, x: obj.expression},
1182
+ {d: obj.dataset, s: this.context_number, x: obj.expression},
1137
1183
  anchor1, offset1, anchor2, offset2];
1138
1184
  }
1139
1185
  }
@@ -1153,8 +1199,10 @@ class ExpressionParser {
1153
1199
  // at run time the VM can match with the value of #.
1154
1200
  // NOTE: Also pass whether the entity should be pushed
1155
1201
  // "by reference".
1156
- return [
1157
- {ee: ame, n: name, a: uca, br: by_reference},
1202
+ if(this.TRACE) console.log('TRACE: Variable', name,
1203
+ 'matches with tail-numbered entities:', ame,
1204
+ '\nTRACE: Attribute used:', uca);
1205
+ return [{n: name, ee: ame, a: uca, br: by_reference},
1158
1206
  anchor1, offset1, anchor2, offset2];
1159
1207
  }
1160
1208
  // Wildcard selector, but no number context for #.
@@ -1205,9 +1253,12 @@ class ExpressionParser {
1205
1253
  }
1206
1254
  // If `obj` is a dataset *modifier*, it must be a "normal" equation...
1207
1255
  if(obj instanceof DatasetModifier) {
1256
+ if(this.TRACE) console.log('TRACE: Dataset modifier "' + obj.displayName +
1257
+ '" mapped to dataset:', obj.dataset.name,
1258
+ 'and selector:', obj.selector);
1208
1259
  // ... so "map" it onto the equations dataset + selector...
1209
1260
  attr = obj.selector;
1210
- obj = MODEL.equations_dataset;
1261
+ obj = obj.dataset;
1211
1262
  }
1212
1263
  // ... so now it will be processed the same way dataset modifiers
1213
1264
  // are processed, especially when they have a tail number.
@@ -1224,30 +1275,40 @@ class ExpressionParser {
1224
1275
 
1225
1276
  // If "by reference", return the object itself plus its attribute
1226
1277
  if(by_reference) {
1278
+ if(this.TRACE) console.log('TRACE: Variable is a reference to',
1279
+ obj.displayName, '. Attribute:', attr);
1227
1280
  return [{r: obj, a: attr}, anchor1, offset1, anchor2, offset2];
1228
1281
  }
1229
1282
  if(obj === this.dataset && attr === '' && !obj.array) {
1230
1283
  // When dataset modifier expression refers to its dataset without
1231
1284
  // selector, then this is equivalent to [.] (use the series data
1232
1285
  // vector) unless it is an array, since then the series data is
1233
- // not a time-scaled vector => special case
1234
- args = obj.vector;
1286
+ // not a time-scaled vector => special case.
1287
+ if(this.TRACE) console.log(
1288
+ 'TRACE: Dataset without selector, no array:', obj.displayName,
1289
+ 'Use vector:', obj.vector);
1290
+ arg0 = obj.vector;
1235
1291
  } else if(attr === '') {
1236
1292
  // For all other variables, assume default attribute if none specified
1237
1293
  attr = obj.defaultAttribute;
1238
1294
  // For a dataset, check whether the VMI_push_dataset_modifier should be
1239
1295
  // used. This is the case for array-type datasets, and for datasets
1240
1296
  // having modifiers UNLESS the modeler used a vertical bar to indicate
1241
- // "use the data"
1297
+ // "use the data".
1242
1298
  if(obj instanceof Dataset &&
1243
1299
  (obj.array || (!use_data && obj.selectorList.length > 0))) {
1300
+ // No explicit selector means that this variable is dynamic if
1301
+ // the dataset has time series data, or if some of its modifier
1302
+ // expressions are dynamic.
1244
1303
  if(obj.data.length > 1 || (obj.data.length > 0 && !obj.periodic) ||
1245
1304
  !obj.allModifiersAreStatic) {
1246
- // No explicit selector => dynamic unless no time series data, and
1247
- // ALL modifier expressions are static
1248
1305
  this.is_static = false;
1249
1306
  this.log('dynamic because dataset without explicit selector is used');
1250
1307
  }
1308
+ if(this.TRACE) console.log(
1309
+ 'TRACE: Dataset without explicit selector:',
1310
+ (obj.array ? 'array' : 'has modifiers'), obj.displayName,
1311
+ '\nTRACE: Use VMI_push_dataset_modifier; use-data flag:', use_data);
1251
1312
  // NOTE: Also pass the "use data" flag so that experiment selectors
1252
1313
  // will be ignored if the modeler coded the vertical bar.
1253
1314
  return [{d: obj, ud: use_data}, anchor1, offset1, anchor2, offset2];
@@ -1269,7 +1330,7 @@ class ExpressionParser {
1269
1330
  this.is_static = false;
1270
1331
  this.log('dynamic because dataset modifier expression is dynamic');
1271
1332
  }
1272
- // NOTE: a single match may be due to wildcard(s) in the modifier,
1333
+ // NOTE: A single match may be due to wildcard(s) in the modifier,
1273
1334
  // e.g., a variable [dataset|abc] matches with a modifier having
1274
1335
  // wildcard selector "a?b", or [dataset|a12] matches with "a*".
1275
1336
  // In such cases, if the selector matches an integer like "a12"
@@ -1278,26 +1339,31 @@ class ExpressionParser {
1278
1339
  // for [datset34|a12], the number context is '12' and not '34').
1279
1340
  let mcn = matchingNumber(attr, m.selector);
1280
1341
  if(mcn === false) {
1281
- // NOTE: When no matching number if found, `attr` may still
1342
+ // NOTE: When no matching number is found, `attr` may still
1282
1343
  // contain a ?? wildcard. If it indeed identifies a wildcard
1283
1344
  // equation, then "?" should be passed to the VM instruction.
1284
1345
  if(obj === MODEL.equations_dataset && attr.indexOf('??') >= 0) {
1285
1346
  mcn = '?';
1286
1347
  } else {
1287
1348
  // Ensure that `mcn` is either an integer value or FALSE.
1288
- mcn = endsWithDigits(obj.name) || this.context_number || false;
1349
+ mcn = parseInt(UI.tailNumber(obj.name)) || this.context_number;
1289
1350
  }
1290
1351
  }
1291
- // Pass the dataset, the context number # (or FALSE) in place, and the
1292
- // modifier expression
1352
+ // Pass the dataset, the context number # (or FALSE) in place,
1353
+ // and the modifier expression.
1354
+ if(this.TRACE) console.log('TRACE: Variable is',
1355
+ (m.dataset === MODEL.equations_dataset ?
1356
+ 'an equation: ' + m.selector :
1357
+ 'a dataset with explicit selector: ' + m.displayName),
1358
+ '\nTRACE: Context number:', mcn, ' Expression:', m.expression.text);
1293
1359
  return [
1294
1360
  {d: m.dataset, s: mcn, x: m.expression},
1295
1361
  anchor1, offset1, anchor2, offset2];
1296
1362
  }
1297
1363
  }
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)) {
1364
+ // NOTE: `arg0` can now be a single value, a vector, or NULL.
1365
+ if(arg0 === null) arg0 = obj.attributeValue(attr);
1366
+ if(Array.isArray(arg0)) {
1301
1367
  if(obj instanceof Dataset) {
1302
1368
  if(obj.data.length > 1 || obj.data.length > 0 && !obj.periodic) {
1303
1369
  this.is_static = false;
@@ -1307,23 +1373,25 @@ class ExpressionParser {
1307
1373
  this.is_static = false;
1308
1374
  this.log('dynamic because level-based attribute');
1309
1375
  } else {
1310
- // Unusual (?) combi, so let's assume dynamic
1376
+ // Unusual (?) combi, so let's assume dynamic.
1311
1377
  this.is_static = false;
1312
1378
  this.log('probably dynamic -- check below:');
1313
- console.log(obj.displayName, obj, attr, args);
1379
+ console.log('ANOMALY: array for', obj.displayName, obj, attr, arg0);
1314
1380
  }
1381
+ if(this.TRACE) console.log('TRACE: arg[0] is a vector');
1315
1382
  }
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
1383
+ // If not a single value or vector, it must be an expression.
1384
+ if(arg0 === null) arg0 = obj.attributeExpression(attr);
1385
+ if(arg0 === null) {
1386
+ // Only NOW check whether unit balance for clusters is asked for.
1320
1387
  if(cluster_balance_unit !== false && obj instanceof Cluster) {
1321
- // NOTE: cluster balance ALWAYS makes expression level-based
1388
+ // NOTE: Cluster balance ALWAYS makes expression level-based
1389
+ // and dynamic.
1322
1390
  this.is_level_based = true;
1323
- // @TO DO: rethink whether this will indeed also make this expression
1324
- // dynamic
1325
1391
  this.is_static = false;
1326
- this.log('dynamic because cluster balance is believed to be dynamic');
1392
+ this.log('dynamic because cluster balance is level-based');
1393
+ if(this.TRACE) console.log('TRACE: Variable is a balance:',
1394
+ cluster_balance_unit, 'for cluster', obj.displayName);
1327
1395
  // NOTE: VM instructions VMI_push_var will recognize this special case
1328
1396
  return [{c: obj, u: cluster_balance_unit},
1329
1397
  anchor1, offset1, anchor2, offset2];
@@ -1331,26 +1399,31 @@ class ExpressionParser {
1331
1399
  // Fall-through: invalid attribute for this object
1332
1400
  msg = `${obj.type} entities have no attribute "${attr}"`;
1333
1401
  } else {
1334
- if(args instanceof Expression) {
1335
- this.is_static = this.is_static && args.isStatic;
1402
+ if(arg0 instanceof Expression) {
1403
+ this.is_static = this.is_static && arg0.isStatic;
1336
1404
  }
1337
- args = [args, anchor1, offset1, anchor2, offset2];
1405
+ if(this.TRACE) console.log('TRACE: arg[0] is the expression for',
1406
+ arg0.variableName, '\nTRACE: Expression:', arg0.text);
1407
+ args = [arg0, anchor1, offset1, anchor2, offset2];
1338
1408
  }
1339
1409
  if(msg) {
1340
1410
  this.error = msg;
1341
1411
  return false;
1342
1412
  }
1343
1413
  // 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
1414
+ // pushes an operand on the evaluation stack.
1415
+ // Check whether the attribute is level-based (i.e., can be computed
1416
+ // only after optimizing a block) while no offset is defined to use
1417
+ // prior data.
1347
1418
  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 &&
1419
+ // NOTE: Dataset modifier expressions may be level-based.
1420
+ (obj instanceof Dataset && attr && arg0.is_level_based) ||
1421
+ // Assume NOT level-based if anchor & offset are specified.
1422
+ // NOTE: This is based on the assumption that advanced modelers
1423
+ // know what they are doing.
1424
+ (VM.level_based_attr.indexOf(attr) >= 0 &&
1352
1425
  anchor1 === 't' && offset1 === 0 &&
1353
- anchor2 === 't' && offset2 === 0;
1426
+ anchor2 === 't' && offset2 === 0);
1354
1427
  return args;
1355
1428
  }
1356
1429
 
@@ -1688,6 +1761,7 @@ class ExpressionParser {
1688
1761
  // Either a statistic, a dataset (array-type or with modifier),
1689
1762
  // an experiment run result, or a variable.
1690
1763
  if(this.sym.length >= 6) {
1764
+ // 6 or 7 arguments indicates a statistic.
1691
1765
  this.code.push([VMI_push_statistic, this.sym]);
1692
1766
  } else if(this.sym[0].hasOwnProperty('d')) {
1693
1767
  this.code.push([VMI_push_dataset_modifier, this.sym]);
@@ -1724,7 +1798,7 @@ class ExpressionParser {
1724
1798
  this.error = 'Invalid parameter list';
1725
1799
  }
1726
1800
  }
1727
- if(DEBUGGING) console.log('PARSED', this.ownerName, ':',
1801
+ if(this.TRACE || DEBUGGING) console.log('PARSED', this.ownerName, ':',
1728
1802
  this.expr, this.code);
1729
1803
  }
1730
1804
 
@@ -5174,7 +5248,7 @@ Solver status = ${json.status}`);
5174
5248
  // Show the reset button (GUI only)
5175
5249
  UI.readyToReset();
5176
5250
  // If receiver is active, report results
5177
- if(RECEIVER.solving) RECEIVER.report();
5251
+ if(RECEIVER.solving || MODEL.report_results) RECEIVER.report();
5178
5252
  // If experiment is active, signal the manager
5179
5253
  if(MODEL.running_experiment) EXPERIMENT_MANAGER.processRun();
5180
5254
  // Warn modeler if any issues occurred
@@ -5796,10 +5870,9 @@ function VMI_push_wildcard_entity(x, args) {
5796
5870
  // n: name (with wildcard #), a: attribute, br: by reference (boolean)}
5797
5871
  // First select the first entity in `ee` that matches the wildcard vector
5798
5872
  // 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;
5873
+ const el = args[0].ee;
5874
+ let nn = args[0].n.replace('#', x.wildcard_vector_index),
5875
+ obj = null;
5803
5876
  for(let i = 0; !obj && i < el.length; i++) {
5804
5877
  if(el[i].name === nn) obj = el[i];
5805
5878
  }
@@ -5809,12 +5882,11 @@ function VMI_push_wildcard_entity(x, args) {
5809
5882
  x.push(VM.BAD_REF);
5810
5883
  return;
5811
5884
  }
5812
- // Otherwise, if `br` indicates "by reference", then VMI_push_entity can
5813
- // be called with the appropriate parameters.
5885
+ // Otherwise, if args[0] indicates "by reference", then VMI_push_entity
5886
+ // can be called with the appropriate parameters.
5814
5887
  const attr = args[0].a || obj.defaultAttribute;
5815
5888
  if(args[0].br) {
5816
- args[0] = {r: obj, a: attr};
5817
- VMI_push_entity(x, args);
5889
+ VMI_push_entity(x, {r: obj, a: attr});
5818
5890
  return;
5819
5891
  }
5820
5892
  // Otherwise, if the entity is a dataset modifier, this must be an
@@ -5822,8 +5894,9 @@ function VMI_push_wildcard_entity(x, args) {
5822
5894
  // push the result of this equation using the wildcard vector index
5823
5895
  // of the expression that is being computed.
5824
5896
  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);
5897
+ VMI_push_dataset_modifier(x,
5898
+ [{d: obj.dataset, s: x.wildcard_vector_index, x: obj.expression},
5899
+ args[1], args[2], args[3], args[4]]);
5827
5900
  return;
5828
5901
  }
5829
5902
  // Otherwise, it can be a vector type attribute or an expression.
@@ -5836,8 +5909,7 @@ function VMI_push_wildcard_entity(x, args) {
5836
5909
  return;
5837
5910
  }
5838
5911
  // Otherwise, VMI_push_var can be called with `v` as first argument.
5839
- args[0] = v;
5840
- VMI_push_var(x, args);
5912
+ VMI_push_var(x, [v, args[1], args[2], args[3], args[4]]);
5841
5913
  }
5842
5914
 
5843
5915
  function VMI_push_dataset_modifier(x, args) {
@@ -5849,7 +5921,7 @@ function VMI_push_dataset_modifier(x, args) {
5849
5921
  // the running experiment UNLESS the field `ud` ("use data") is defined
5850
5922
  // for the first argument, and evaluates as TRUE.
5851
5923
  // NOTE: Ensure that number 0 is not interpreted as FALSE.
5852
- let ms = (args[0].s === 0 || args[0].s) ? args[0].s : false;
5924
+ let wcnr = (args[0].s === undefined ? false : args[0].s);
5853
5925
  const
5854
5926
  ds = args[0].d,
5855
5927
  ud = args[0].ud || false,
@@ -5863,6 +5935,7 @@ function VMI_push_dataset_modifier(x, args) {
5863
5935
  if(!ds) console.log('ERROR: VMI_push_dataset_modifier without dataset',
5864
5936
  x.variableName, x.code);
5865
5937
  let t = tot[0],
5938
+ // By default, use the vector of the dataset to compute the value.
5866
5939
  obj = ds.vector;
5867
5940
 
5868
5941
  if(ds.array) {
@@ -5888,27 +5961,33 @@ function VMI_push_dataset_modifier(x, args) {
5888
5961
  t = Math.max(0, Math.min(
5889
5962
  MODEL.end_period - MODEL.start_period + MODEL.look_ahead + 1, t));
5890
5963
  }
5891
- if(ms !== false) {
5892
- // If a modifier selector is specified, use the associated expression.
5964
+ if(wcnr !== false || ds === MODEL.equations_dataset) {
5965
+ // If a wildcard number is specified, or when a normal (not-wildcard)
5966
+ // equation is referenced, use the modifier expression to calculate
5967
+ // the value to push.
5893
5968
  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;
5897
- } else if(!ud) {
5969
+ // If '?' is passed as wildcard number, use the wildcard vector index
5970
+ // of the expression that is being computed (this may be FALSE).
5971
+ if(wcnr === '?') {
5972
+ wcnr = x.wildcard_vector_index;
5973
+ }
5974
+ } else if(!ud ) {
5898
5975
  // In no selector and not "use data", check whether a running experiment
5899
5976
  // defines the expression to use. If not, `obj` will be the dataset
5900
5977
  // vector (so same as when "use data" is set).
5901
5978
  obj = ds.activeModifierExpression;
5902
5979
  }
5980
+ if(!obj) {
5981
+ console.log('ANOMALY: no object. obj, wcnr, args, x', obj, wcnr, args, x);
5982
+ }
5903
5983
  // Now determine what value `v` should be pushed onto the expression stack.
5904
5984
  // By default, use the dataset default value.
5905
5985
  let v = ds.defaultValue,
5906
5986
  // NOTE: `obstr` is used only when debugging, to log `obj` in human-
5907
5987
  // readable format.
5908
- obstr = (obj instanceof Expression ?
5909
- obj.text : '[' + obj.toString() + ']');
5988
+ obstr = (obj instanceof Expression ? obj.text : `[${obj.toString()}]`);
5910
5989
  if(Array.isArray(obj)) {
5911
- // Object is a vector.
5990
+ // `obj` is a vector.
5912
5991
  if(t >= 0 && t < obj.length) {
5913
5992
  v = obj[t];
5914
5993
  } else if(ds.array && t >= obj.length) {
@@ -5921,22 +6000,23 @@ function VMI_push_dataset_modifier(x, args) {
5921
6000
  }
5922
6001
  // Fall through: no change to `v` => dataset default value is pushed.
5923
6002
  } else {
5924
- // Object is an expression.
6003
+ // `obj` is an expression.
5925
6004
  // NOTE: Readjust `t` when `obj` is an expression for an *array-type*
5926
6005
  // dataset modifier.
5927
6006
  if(obj.object instanceof Dataset && obj.object.array) t++;
5928
6007
  // Pass modifier selector (if specified; may be FALSE) so that result
5929
- // will be recomputed with this selector as context for #
5930
- v = obj.result(t, ms);
6008
+ // will be recomputed with this selector as context for #.
6009
+ v = obj.result(t, wcnr);
5931
6010
  }
5932
- // Trace only now that time step t has been computed
6011
+ // Trace only now that time step t has been computed.
5933
6012
  if(DEBUGGING) {
5934
6013
  console.log('push dataset modifier:', obstr,
5935
- tot[1] + (tot[2] ? ':' + tot[2] : ''), 'value =', VM.sig4Dig(v));
5936
- console.log(' --', x.text, ' for owner ', x.object.displayName, x.attribute);
6014
+ tot[1] + (tot[2] ? ':' + tot[2] : ''), 'value =', VM.sig4Dig(v),
6015
+ '\nExpression: ', x.text, '\nVariable:', x.variableName,
6016
+ 'Context number:', wcnr);
5937
6017
  }
5938
- // NOTE: if value is exceptional ("undefined", etc.), use default value
5939
- if(v >= VM.PLUS_INFINITY) v = ds.defaultValue;
6018
+ // NOTE: If value is exceptional ("undefined", etc.), use default value.
6019
+ // DEPRECATED !! if(v >= VM.PLUS_INFINITY) v = ds.defaultValue;
5940
6020
  // Finally, push the value onto the expression stack
5941
6021
  x.push(v);
5942
6022
  }