linny-r 1.6.1 → 1.6.2

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.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -1521,7 +1521,7 @@ function connectionErrorText(msg) {
1521
1521
  //
1522
1522
 
1523
1523
  function commandLineSettings() {
1524
- // Sets default settings, and then checks the command line arguments
1524
+ // Sets default settings, and then checks the command line arguments.
1525
1525
  const settings = {
1526
1526
  cli_name: (PLATFORM.startsWith('win') ? 'Command Prompt' : 'Terminal'),
1527
1527
  inkscape: '',
@@ -1533,6 +1533,22 @@ function commandLineSettings() {
1533
1533
  solver_path: '',
1534
1534
  user_dir: path.join(WORKING_DIRECTORY, 'user')
1535
1535
  };
1536
+ const
1537
+ cmd = process.argv[0],
1538
+ app = (cmd.endsWith('node.exe') ? 'node' : 'linny-r'),
1539
+ usage = `Usage: ${app} server [options]
1540
+
1541
+ Possible options are:
1542
+ dpi=[number] will make InkScape render SVGs in the specified resolution
1543
+ help will display these command line options
1544
+ launch will open the Linny-R GUI in a browser window
1545
+ port=[number] will listen at the specified port number
1546
+ (default is 5050; number must be unique for each server)
1547
+ solver=[name] will select solver [name], or warn if not found
1548
+ (name choices: Gurobi, CPLEX, SCIP or LP_solve)
1549
+ verbose will output solver messages to the console
1550
+ workspace=[path] will create workspace in [path] instead of (Linny-R)/user
1551
+ `;
1536
1552
  for(let i = 2; i < process.argv.length; i++) {
1537
1553
  const lca = process.argv[i].toLowerCase();
1538
1554
  if(lca === 'launch') {
@@ -1541,7 +1557,7 @@ function commandLineSettings() {
1541
1557
  const av = lca.split('=');
1542
1558
  if(av.length === 1) av.push('');
1543
1559
  if(av[0] === 'port') {
1544
- // Accept any number greater than or equal to 1024
1560
+ // Accept any number greater than or equal to 1024.
1545
1561
  const n = parseInt(av[1]);
1546
1562
  if(isNaN(n) || n < 1024) {
1547
1563
  console.log(`WARNING: Invalid port number ${av[1]}`);
@@ -1555,7 +1571,7 @@ function commandLineSettings() {
1555
1571
  settings.preferred_solver = av[1];
1556
1572
  }
1557
1573
  } else if(av[0] === 'dpi') {
1558
- // Accept any number greater than or equal to 1024
1574
+ // Accept any number greater than or equal to 1024.
1559
1575
  const n = parseInt(av[1]);
1560
1576
  if(isNaN(n) || n > 1200) {
1561
1577
  console.log(`WARNING: Invalid resolution ${av[1]} (max. 1200 dpi)`);
@@ -1563,7 +1579,7 @@ function commandLineSettings() {
1563
1579
  settings.dpi = n;
1564
1580
  }
1565
1581
  } else if(av[0] === 'workspace') {
1566
- // User directory must be READ/WRITE-accessible
1582
+ // User directory must be READ/WRITE-accessible.
1567
1583
  try {
1568
1584
  fs.accessSync(av[1], fs.constants.R_OK | fs.constants.W_O);
1569
1585
  } catch(err) {
@@ -1571,10 +1587,15 @@ function commandLineSettings() {
1571
1587
  process.exit();
1572
1588
  }
1573
1589
  settings.user_dir = av[1];
1590
+ } else if(av[0] === 'help') {
1591
+ // Print command line options.
1592
+ console.log(usage);
1593
+ process.exit();
1574
1594
  } else {
1575
- // Terminate script
1595
+ // Terminate script.
1576
1596
  console.log(
1577
- `ERROR: Invalid command line argument "${process.argv[i]}"`);
1597
+ `ERROR: Invalid command line argument "${process.argv[i]}"\n`);
1598
+ console.log(usage);
1578
1599
  process.exit();
1579
1600
  }
1580
1601
  }
@@ -356,7 +356,11 @@ class GUIExperimentManager extends ExperimentManager {
356
356
  dim_count.innerHTML = pluralS(x.available_dimensions.length,
357
357
  'more dimension');
358
358
  x.inferActualDimensions();
359
+ for(let i = 0; i < x.actual_dimensions.length; i++) {
360
+ x.actual_dimensions[i].sort(compareSelectors);
361
+ }
359
362
  x.inferCombinations();
363
+ //x.combinations.sort(compareCombinations);
360
364
  combi_count.innerHTML = pluralS(x.combinations.length, 'combination');
361
365
  if(x.combinations.length === 0) canview = false;
362
366
  header.innerHTML = x.title;
@@ -850,6 +854,9 @@ class GUIExperimentManager extends ExperimentManager {
850
854
  }
851
855
  // Get the selected statistic for each run so as to get an array of numbers
852
856
  const data = [];
857
+ // Set reference column indices so that values for the reference|
858
+ // configuration can be displayed in orange.
859
+ const ref_conf_indices = [];
853
860
  for(let i = 0; i < x.runs.length; i++) {
854
861
  const
855
862
  r = x.runs[i],
@@ -878,7 +885,7 @@ class GUIExperimentManager extends ExperimentManager {
878
885
  data.push(rr.last);
879
886
  }
880
887
  }
881
- // Scale data as selected
888
+ // Scale data as selected.
882
889
  const scaled = data.slice();
883
890
  // NOTE: scale only after the experiment has been completed AND
884
891
  // configurations have been defined (otherwise comparison is pointless)
@@ -896,7 +903,9 @@ class GUIExperimentManager extends ExperimentManager {
896
903
  }
897
904
  // Set difference for reference configuration itself to 0
898
905
  for(let i = 0; i < n; i++) {
899
- scaled[rc * n + i] = 0;
906
+ const index = rc * n + i;
907
+ scaled[index] = 0;
908
+ ref_conf_indices.push(index);
900
909
  }
901
910
  } else if(x.selected_scale === 'reg') {
902
911
  // Compute regret: current config - high value config in same scenario
@@ -934,13 +943,15 @@ class GUIExperimentManager extends ExperimentManager {
934
943
  formatted.push(VM.sig4Dig(scaled[i]));
935
944
  }
936
945
  uniformDecimals(formatted);
937
- // Display formatted data in cells
946
+ // Display formatted data in cells.
938
947
  for(let i = 0; i < x.combinations.length; i++) {
939
948
  const cell = document.getElementById('xr' + i);
940
949
  if(i < x.runs.length) {
941
950
  cell.innerHTML = formatted[i];
942
951
  cell.classList.remove('not-run');
943
952
  cell.style.backgroundColor = this.color_scale.rgb(normalized[i]);
953
+ cell.style.color = (ref_conf_indices.indexOf(i) >= 0 ?
954
+ 'orange' : 'black');
944
955
  const
945
956
  r = x.runs[i],
946
957
  rr = r.results[rri],
@@ -2312,17 +2312,18 @@ class Paper {
2312
2312
  }
2313
2313
  if(prod.hasBounds) {
2314
2314
  font_color = 'black';
2315
- // By default, "plain" factors having bounds are filled in silver
2315
+ // By default, "plain" factors having bounds are filled in silver.
2316
2316
  fill_color = this.palette.has_bounds;
2317
2317
  // Use relative distance to bounds so that 100000.1 is not shown
2318
- // as overflow, but 100.1 is
2318
+ // as overflow, but 100.1 is.
2319
2319
  let udif = this.relDif(l, ub),
2320
2320
  ldif = this.relDif(lb, l);
2321
- // Special case: for LB = 0, use the ON/OFF threshold
2321
+ // Special case: for LB = 0, use the ON/OFF threshold.
2322
2322
  if(Math.abs(lb) <= VM.SIG_DIF_LIMIT &&
2323
2323
  Math.abs(l) <= VM.ON_OFF_THRESHOLD) ldif = 0;
2324
2324
  if(MODEL.solved) {
2325
- // NOTE: use bright red and blue colors in case of "stock level out of bounds"
2325
+ // NOTE: Use bright red and blue colors in case of "stock level
2326
+ // out of bounds".
2326
2327
  if(ub < VM.PLUS_INFINITY && l < VM.UNDEFINED && udif > VM.SIG_DIF_LIMIT) {
2327
2328
  fill_color = this.palette.above_upper_bound;
2328
2329
  font_color = 'blue';
@@ -2332,51 +2333,52 @@ class Paper {
2332
2333
  } else if(l < VM.ERROR || l > VM.EXCEPTION) {
2333
2334
  font_color = this.palette.VM_error;
2334
2335
  } else if(l < VM.UNDEFINED) {
2335
- // Shades of green reflect whether level within bounds, where
2336
+ // Shades of green reflect whether level is within bounds, where
2336
2337
  // "sources" (negative level) and "sinks" (positive level) are
2337
- // shown as more reddish / bluish shades of green
2338
+ // shown as more reddish / bluish shades of green.
2338
2339
  if(l < -VM.ON_OFF_THRESHOLD) {
2339
2340
  fill_color = this.palette.neg_within_bounds;
2340
2341
  } else if(l > VM.ON_OFF_THRESHOLD) {
2341
2342
  fill_color = this.palette.pos_within_bounds;
2342
2343
  } else {
2343
- fill_color = this.palette.zero_within_bounds;
2344
+ fill_color = this.palette.zero_within_bounds;
2344
2345
  }
2345
2346
  if(ub - lb < VM.NEAR_ZERO) {
2347
+ // When LB = UB, fill completely in the color, but ...
2346
2348
  if(prod.isConstant && Math.abs(l) > VM.NEAR_ZERO) {
2347
- // Non-zero constants have less saturated shades
2349
+ // ... non-zero constants have less saturated shades.
2348
2350
  fill_color = (l < 0 ? this.palette.neg_constant :
2349
2351
  this.palette.pos_constant);
2350
2352
  }
2351
2353
  } else if(ub - l < VM.SIG_DIF_LIMIT) {
2352
- // Black font and darker fill color indicate "at upper bound"
2354
+ // Black font and darker fill color indicate "at upper bound".
2353
2355
  font_color = 'black';
2354
2356
  fill_color = (ub > 0 ? this.palette.at_pos_ub_fill :
2355
2357
  (ub < 0 ? this.palette.at_neg_ub_fill :
2356
2358
  this.palette.at_zero_ub_fill));
2357
2359
  at_bound = true;
2358
2360
  } else if (l - lb < VM.SIG_DIF_LIMIT) {
2359
- // Font and rim color indicate "at upper bound"
2361
+ // Font and rim color indicate "at lower bound".
2360
2362
  font_color = 'black';
2361
2363
  fill_color = (lb > 0 ? this.palette.at_pos_lb_fill :
2362
2364
  (lb < 0 ? this.palette.at_neg_lb_fill :
2363
2365
  this.palette.at_zero_lb_fill));
2364
2366
  at_bound = true;
2365
2367
  } else {
2366
- // set "partial fill" flag if not at lower bound and UB < INF
2368
+ // Set "partial fill" flag if not at lower bound and UB < INF.
2367
2369
  pf = ub < VM.PLUS_INFINITY;
2368
2370
  font_color = this.palette.within_bounds_font;
2369
2371
  }
2370
2372
  }
2371
2373
  } else if(ub - lb < VM.NEAR_ZERO) {
2372
- // Not solved but equal bounds => probably constants
2374
+ // Not solved but equal bounds => probably constants.
2373
2375
  if(prod.isConstant && Math.abs(ub) > VM.NEAR_ZERO) {
2374
- // Non-zero constants have less saturated shades
2376
+ // Non-zero constants have less saturated shades.
2375
2377
  fill_color = (ub < 0 ? this.palette.neg_constant :
2376
2378
  this.palette.pos_constant);
2377
2379
  }
2378
2380
  } else if(l < VM.UNDEFINED) {
2379
- // Different bounds and initial level set => partial fill
2381
+ // Different bounds and initial level set => partial fill.
2380
2382
  fill_color = this.palette.src_snk;
2381
2383
  pf = true;
2382
2384
  if(ub - l < VM.SIG_DIF_LIMIT || l - lb < VM.SIG_DIF_LIMIT) {
@@ -2418,8 +2420,8 @@ class Paper {
2418
2420
  let npfbg = 'white';
2419
2421
  if(fill_color === this.palette.above_upper_bound ||
2420
2422
  fill_color === this.palette.below_lower_bound ||
2421
- // NOTE: empty buffers (at level 0) should be entirely white
2422
- (at_bound && l > VM.ON_OFF_THRESHOLD)) {
2423
+ // NOTE: Empty buffers should be entirely white.
2424
+ (at_bound && l > lb + VM.ON_OFF_THRESHOLD)) {
2423
2425
  npfbg = fill_color;
2424
2426
  pf = false;
2425
2427
  }
@@ -2434,26 +2436,26 @@ class Paper {
2434
2436
  {fill: fill_color, stroke: stroke_color, 'stroke-width': stroke_width,
2435
2437
  'stroke-dasharray': sda, 'stroke-linecap': 'round',
2436
2438
  'rx': hh, 'ry': hh});
2437
- // NOTE: set fill color to darker shade for partial fill
2439
+ // NOTE: Set fill color to darker shade for partial fill.
2438
2440
  fill_color = (!MODEL.solved ? this.palette.src_snk :
2439
2441
  (l > VM.NEAR_ZERO ? this.palette.above_zero_fill :
2440
2442
  (l < -VM.NEAR_ZERO ? this.palette.below_zero_fill :
2441
2443
  this.palette.at_zero_fill)));
2442
2444
  }
2443
- // Add partial fill if appropriate
2445
+ // Add partial fill if appropriate.
2444
2446
  if(pf && l > lb && l < VM.UNDEFINED) {
2445
2447
  // Calculate used part of range (1 = 100%)
2446
2448
  let part,
2447
2449
  range = ub - lb;
2448
2450
  if(l >= VM.PLUS_INFINITY) {
2449
- // Show exceptions and +INF as "overflow"
2451
+ // Show exceptions and +INF as "overflow".
2450
2452
  part = 1;
2451
2453
  fill_color = this.palette.above_upper_bound;
2452
2454
  } else {
2453
2455
  part = (range > 0 ? (l - lb) / range : 1);
2454
2456
  }
2455
2457
  if(part > 0 && l >= lb) {
2456
- // Only fill the portion of used range with the fill color
2458
+ // Only fill the portion of used range with the fill color.
2457
2459
  const rad = Math.asin(1 - 2*part);
2458
2460
  prod.shape.addPath(['m', x + hw - hh + (hh - 1.5) * Math.cos(rad),
2459
2461
  ',', y + (hh - 1.5) * Math.sin(rad),
@@ -2467,7 +2469,7 @@ class Paper {
2467
2469
  stroke_color = 'none';
2468
2470
  stroke_width = 0;
2469
2471
  // Sources have a triangle pointing up from the bottom
2470
- // (in outline if *implicit* source)
2472
+ // (in outline if *implicit* source).
2471
2473
  if(prod.isSourceNode) {
2472
2474
  if(!prod.is_source) {
2473
2475
  fill_color = 'none';
@@ -2479,7 +2481,7 @@ class Paper {
2479
2481
  'stroke-width': stroke_width});
2480
2482
  }
2481
2483
  // Sinks have a triangle pointing down from the top
2482
- // (in outline if implicit sink)
2484
+ // (in outline if implicit sink).
2483
2485
  if(prod.isSinkNode) {
2484
2486
  if(!prod.is_sink) {
2485
2487
  fill_color = 'none';
@@ -2491,7 +2493,7 @@ class Paper {
2491
2493
  'stroke-width': stroke_width});
2492
2494
  }
2493
2495
  // Integer level is denoted by enclosing name in large [ and ]
2494
- // to denote "floor" as well as "ceiling"
2496
+ // to denote "floor" as well as "ceiling".
2495
2497
  if(prod.integer_level) {
2496
2498
  const
2497
2499
  brh = prod.name_lines.split('\n').length * this.font_heights[8] + 4,
@@ -1574,7 +1574,7 @@ class LinnyRModel {
1574
1574
  const
1575
1575
  note = fc.notes[i],
1576
1576
  nbn = note.nearbyNode;
1577
- note.nearby_pos = (nbn ? {node: nbn, oldx: x, oldy: y} : null);
1577
+ note.nearby_pos = (nbn ? {node: nbn, oldx: nbn.x, oldy: nbn.y} : null);
1578
1578
  }
1579
1579
  for(let i = 0; i < fc.processes.length; i++) {
1580
1580
  move = fc.processes[i].alignToGrid() || move;
@@ -5158,7 +5158,7 @@ class Note extends ObjectWithXYWH {
5158
5158
  // If attribute omitted, use default attribute of entity type.
5159
5159
  const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
5160
5160
  let val = null;
5161
- // NOTE: for datasets, use the active modifier if no attribute.
5161
+ // NOTE: For datasets, use the active modifier if no attribute.
5162
5162
  if(!attr && obj instanceof Dataset) {
5163
5163
  val = obj.activeModifierExpression;
5164
5164
  } else {
@@ -7894,13 +7894,11 @@ class Product extends Node {
7894
7894
  }
7895
7895
 
7896
7896
  get isConstant() {
7897
- // Return TRUE if this product is data, has no links to processes,
7898
- // is not an actor cash flow, and has set LB = UB
7897
+ // Return TRUE if this product is data, is not an actor cash flow,
7898
+ // has no ingoing links, has outgoing links ONLY to data objects,
7899
+ // and has set LB = UB.
7899
7900
  if(!this.is_data || this.name.startsWith('$') ||
7900
- !this.allOutputsAreData) return false;
7901
- for(let i = 0; i < this.inputs.length; i++) {
7902
- if(this.inputs[i].from_node instanceof Process) return false;
7903
- }
7901
+ this.inputs.length || !this.allOutputsAreData) return false;
7904
7902
  return (this.equal_bounds && this.lower_bound.defined);
7905
7903
  }
7906
7904
 
@@ -8847,11 +8845,30 @@ class Dataset {
8847
8845
 
8848
8846
  attributeValue(a) {
8849
8847
  // Return the computed result for attribute `a`.
8850
- // NOTE: Datasets have ONE attribute (their vector) denoted by the empty
8851
- // string; all other "attributes" should be modifier selectors, and
8852
- // their value should be obtained using attributeExpression (see below).
8853
- if(a === '') return this.vector;
8854
- return null;
8848
+ // NOTE: Datasets have ONE attribute (their vector) denoted by the
8849
+ // dot ".". All other "attributes" should be modifier selectors,
8850
+ // and their value should be obtained using `attributeExpression(a)`.
8851
+ // The empty string denotes "use default", which may have been set
8852
+ // by the modeler, or may follow from the active combination of a
8853
+ // running experiment.
8854
+ if(a === '') {
8855
+ const x = this.activeModifierExpression;
8856
+ if(x instanceof Expression) {
8857
+ x.compute(0);
8858
+ // Ensure that for dynamic modifier expressions the vector is
8859
+ // fully computed.
8860
+ if(!x.isStatic) {
8861
+ const nt = MODEL.end_period - MODEL.start_period + 1;
8862
+ for(let t = 1; t <= nt; t++) x.result(t);
8863
+ }
8864
+ return x.vector;
8865
+ }
8866
+ // No modifier expression? Then return the dataset vector.
8867
+ return this.vector;
8868
+ }
8869
+ if(a === '.') return this.vector;
8870
+ // Fall-through: return the default value of this dataset.
8871
+ return this.defaultValue;
8855
8872
  }
8856
8873
 
8857
8874
  attributeExpression(a) {
@@ -8881,7 +8898,7 @@ class Dataset {
8881
8898
  console.log('WARNING: Dataset "' + this.name +
8882
8899
  `" has no default selector "${this.default_selector}"`, this.modifiers);
8883
8900
  }
8884
- // Fall-through: return vector instead of expression.
8901
+ // Fall-through: return the dataset vector.
8885
8902
  return this.vector;
8886
8903
  }
8887
8904
 
@@ -9290,23 +9307,11 @@ class ChartVariable {
9290
9307
  t_end = tsteps;
9291
9308
  } else {
9292
9309
  // Get the variable's own value (number, vector or expression)
9293
- // Special case: when an experiment is running, variables that
9294
- // depict a dataset with no explicit modifier must recompute the
9295
- // vector using the current experiment run combination.
9296
- if(MODEL.running_experiment &&
9297
- this.object instanceof Dataset && !this.attribute) {
9298
- // Check if dataset modifiers match the combination of selectors
9299
- // for the active run.
9300
- const mm = this.object.matchingModifiers(
9301
- MODEL.running_experiment.activeCombination);
9302
- // If so, use the first (the list should contain at most 1 selector)
9303
- // to select the modifier expression; otherwise, use the unmodified
9304
- // vector of the dataset
9305
- if(mm.length > 0) {
9306
- av = mm[0].expression;
9307
- } else {
9308
- av = this.object.vector;
9309
- }
9310
+ if(this.object instanceof Dataset && !this.attribute) {
9311
+ // Special case: Variables that depict a dataset with no explicit
9312
+ // modifier selector must recompute the vector using the current
9313
+ // experiment run combination or the default selector.
9314
+ av = this.object.activeModifierExpression;
9310
9315
  } else if(this.object instanceof DatasetModifier) {
9311
9316
  av = this.object.expression;
9312
9317
  } else {
@@ -11075,7 +11080,8 @@ class ExperimentRun {
11075
11080
  bm.messages = VM.messages[i];
11076
11081
  this.block_messages.push(bm);
11077
11082
  this.warning_count += bm.warningCount;
11078
- this.solver_seconds += bm.solver_secs;
11083
+ // NOTE: When set by the VM, `solver_secs` is a string.
11084
+ this.solver_seconds += parseFloat(bm.solver_secs);
11079
11085
  }
11080
11086
  }
11081
11087
 
@@ -491,7 +491,9 @@ function matchingNumber(m, s) {
491
491
  // Returns an integer value if string `m` matches selector pattern `s`
492
492
  // (where asterisks match 0 or more characters, and question marks 1
493
493
  // character) and the matching parts jointly denote an integer.
494
- let raw = s.replace(/\*/g, '(.*)').replace(/\?/g, '(.)'),
494
+ // NOTE: A "+" must be escaped, "*" and "?" must become groups.
495
+ let raw = s.replaceAll('+', '\+')
496
+ .replace(/\*/g, '(.*)').replace(/\?/g, '(.)'),
495
497
  match = m.match(new RegExp(`^${raw}$`)),
496
498
  n = '';
497
499
  if(match) {
@@ -601,6 +603,18 @@ function compareSelectors(s1, s2) {
601
603
  return 0;
602
604
  }
603
605
 
606
+ function compareCombinations(c1, c2) {
607
+ // Compare two selector lists.
608
+ const n = Math.min(c1.length, c2.length);
609
+ for(let i = 0; i < n; i++) {
610
+ const cs = compareSelectors(c1[i], c2[i]);
611
+ if(cs) return cs;
612
+ }
613
+ if(c1.length > l) return 1;
614
+ if(c2.length > l) return -1;
615
+ return 0;
616
+ }
617
+
604
618
  //
605
619
  // Functions that perform set-like operations on lists of string
606
620
  //
@@ -1394,9 +1394,13 @@ class ExpressionParser {
1394
1394
  '-- number is:', this.context_number,
1395
1395
  '\nTRACE: Expression:', obj.expression.text);
1396
1396
  // Use the context number as "selector" parameter of the VMI.
1397
- return [
1398
- {d: obj.dataset, s: this.context_number, x: obj.expression},
1399
- anchor1, offset1, anchor2, offset2];
1397
+ const arg0 = (by_reference ?
1398
+ // If equation is "by reference", use VMI_push_entity
1399
+ // while passing the context number as extra parameter.
1400
+ {r: obj.dataset, a: obj.selector, cn: this.context_number} :
1401
+ // Otherwise, use VMI_push_dataset_modifier.
1402
+ {d: obj.dataset, s: this.context_number, x: obj.expression});
1403
+ return [arg0, anchor1, offset1, anchor2, offset2];
1400
1404
  }
1401
1405
  }
1402
1406
  }
@@ -2452,7 +2456,7 @@ class VirtualMachine {
2452
2456
  if(n <= this.BAD_REF) return [true, '#REF?'];
2453
2457
  if(n <= this.ARRAY_INDEX) return [true, '#INDEX!'];
2454
2458
  if(n <= this.BAD_CALC) return [true, '#VALUE!'];
2455
- if(n <= this.DIV_ZERO) return [true, '#DIV0!'];
2459
+ if(n <= this.DIV_ZERO) return [true, '#DIV/0!'];
2456
2460
  if(n <= this.CYCLIC) return [true, '#CYCLE!'];
2457
2461
  // Any other number less than or equal to 10^30 is considered as
2458
2462
  // minus infinity.
@@ -4767,7 +4771,7 @@ class VirtualMachine {
4767
4771
  // is calibrated for 1000 VMI instructions.
4768
4772
  this.tsl = Math.ceil(CONFIGURATION.progress_needle_interval *
4769
4773
  1000 / this.code.length);
4770
- if(true||abl > this.tsl * 5) {
4774
+ if(abl > this.tsl * 5) {
4771
4775
  UI.setMessage('Constructing the Simplex tableau');
4772
4776
  UI.setProgressNeedle(0);
4773
4777
  this.show_progress = true;
@@ -5545,7 +5549,7 @@ Solver status = ${json.status}`);
5545
5549
  this.round_times.length = 0;
5546
5550
  this.solver_times[bnr - 1] = time;
5547
5551
  const ssecs = this.round_secs.reduce((a, b) => a + b, 0);
5548
- this.solver_secs[bnr - 1] = (ssecs ? VM.sig4Dig(ssecs) : '');
5552
+ this.solver_secs[bnr - 1] = (ssecs ? VM.sig4Dig(ssecs) : '0');
5549
5553
  this.round_secs.length = 0;
5550
5554
  MONITOR.addProgressBlock(bnr, issue, time);
5551
5555
  }
@@ -6265,13 +6269,19 @@ function VMI_push_wildcard_entity(x, args) {
6265
6269
  } else {
6266
6270
  // Select the first entity in `ee` that matches the wildcard vector
6267
6271
  // index of the expression `x` being executed.
6272
+ if(x.wildcard_vector_index === false && x.isWildcardExpression &&
6273
+ MODEL.running_experiment) {
6274
+ // If no wildcard vector index, try to infer it.
6275
+ x.wildcard_vector_index = matchingNumberInList(
6276
+ MODEL.running_experiment.activeCombination, x.attribute);
6277
+ }
6268
6278
  nn = nn.replace('#', x.wildcard_vector_index);
6269
6279
  for(let i = 0; !obj && i < el.length; i++) {
6270
6280
  if(el[i].name === nn) obj = el[i];
6271
6281
  }
6272
6282
  // If no match, then this indicates a bad reference.
6273
6283
  if(!obj) {
6274
- console.log(`ERROR: no match for "${nn}" in eligible entity list`, el);
6284
+ console.log(`ERROR: no match for "${nn}" in eligible entity list`, el, x);
6275
6285
  x.push(VM.BAD_REF);
6276
6286
  return;
6277
6287
  }
@@ -6281,7 +6291,8 @@ function VMI_push_wildcard_entity(x, args) {
6281
6291
  // called with the appropriate parameters.
6282
6292
  const attr = args[0].a || obj.defaultAttribute;
6283
6293
  if(args[0].br) {
6284
- VMI_push_entity(x, {r: obj, a: attr});
6294
+ VMI_push_entity(x, [{r: obj, a: attr},
6295
+ args[1], args[2], args[3], args[4]]);
6285
6296
  return;
6286
6297
  }
6287
6298
  // Otherwise, if the entity is a dataset modifier, this must be an
@@ -6824,7 +6835,7 @@ function VMI_mul(x) {
6824
6835
 
6825
6836
  function VMI_div(x) {
6826
6837
  // Pops the top number on the stack and divides the new top number
6827
- // by it. In case of division by zero, the top is replaced by #DIV0!
6838
+ // by it. In case of division by zero, the top is replaced by #DIV/0!
6828
6839
  const d = x.pop();
6829
6840
  if(d !== false) {
6830
6841
  if(DEBUGGING) console.log('DIV (' + d.join(', ') + ')');
@@ -6838,7 +6849,7 @@ function VMI_div(x) {
6838
6849
 
6839
6850
  function VMI_mod(x) {
6840
6851
  // Pops the top number on the stack, divides the new top number by it
6841
- // (if non-zero, or it pushes error code #DIV0!), takes the fraction
6852
+ // (if non-zero, or it pushes error code #DIV/0!), takes the fraction
6842
6853
  // part, and multiplies this with the divider; in other words, it
6843
6854
  // performs a "floating point MOD operation"
6844
6855
  const d = x.pop();
@@ -7159,39 +7170,41 @@ function VMI_jump(x, index) {
7159
7170
  }
7160
7171
 
7161
7172
  function VMI_jump_if_false(x, index) {
7162
- // Tests the top number A of the stack, and if A is FALSE (zero or
7163
- // VM.UNDEFINED) sets the program counter of the VM to `index` minus 1,
7164
- // as the counter is ALWAYS increased by 1 after calling a VMI function
7173
+ // Test the top number A on the stack, and if A is FALSE (zero or
7174
+ // VM.UNDEFINED) set the program counter of the VM to `index` minus 1,
7175
+ // as the counter is ALWAYS increased by 1 after calling a VMI function.
7165
7176
  const r = x.top(true);
7166
7177
  if(DEBUGGING) console.log(`JUMP-IF-FALSE (${r}, ${index})`);
7167
7178
  if(r === 0 || r === VM.UNDEFINED || r === false) {
7168
7179
  // Only jump on FALSE, leaving the stack "as is", so that in case
7169
- // of no THEN the expression result equals the IF condition value
7180
+ // of no THEN, the expression result equals the IF condition value.
7170
7181
  // NOTE: Also do this on a stack error (r === false)
7171
7182
  x.program_counter = index - 1;
7172
7183
  } else {
7173
- // Remove the value from the stack
7184
+ // Remove the value from the stack.
7174
7185
  x.stack.pop();
7175
7186
  }
7176
7187
  }
7177
7188
 
7178
7189
  function VMI_pop_false(x) {
7179
- // Removes the top value from the stack, which should be 0 or
7180
- // VM.UNDEFINED (but this is not checked)
7190
+ // Remove the top value from the stack, which should be 0 or
7191
+ // VM.UNDEFINED (but this is not checked).
7181
7192
  const r = x.stack.pop();
7182
7193
  if(DEBUGGING) console.log(`POP-FALSE (${r})`);
7183
7194
  }
7184
7195
 
7185
7196
  function VMI_if_then(x) {
7186
7197
  // NO operation -- as of version 1.0.14, this function only serves as
7187
- // operator symbol, and its executions would indicate an error
7188
- console.log('WARNING: this IF-THEN instruction is obsolete!');
7198
+ // placeholder in operator symbol arrays. The parser should no longer
7199
+ // code this, so its execution would indicate an error.
7200
+ console.log('WARNING: IF-THEN instruction is obsolete', x);
7189
7201
  }
7190
7202
 
7191
7203
  function VMI_if_else(x) {
7192
7204
  // NO operation -- as of version 1.0.14, this function only serves as
7193
- // operator symbol, and its executions would indicate an error
7194
- console.log('WARNING: this IF-THEN instruction is obsolete!');
7205
+ // placeholder in operator symbol arrays. The parser should no longer
7206
+ // code this, so its execution would indicate an error.
7207
+ console.log('WARNING: IF-ELSE instruction is obsolete', x);
7195
7208
  }
7196
7209
 
7197
7210
  //
@@ -7424,7 +7437,7 @@ function VMI_set_bounds(args) {
7424
7437
  }
7425
7438
  }
7426
7439
 
7427
- function VMI_clear_coefficients(empty) {
7440
+ function VMI_clear_coefficients() {
7428
7441
  if(DEBUGGING) console.log('clear_coefficients');
7429
7442
  VM.coefficients = {};
7430
7443
  VM.cash_in_coefficients = {};
@@ -7555,7 +7568,7 @@ function VMI_add_var_to_weighted_sum_coefficients(args) {
7555
7568
  VM.sig4Dig(w) + ' * ' + v.variableName + ' (t = ' + t + ')');
7556
7569
  }
7557
7570
  for(let i = 0; i <= d; i++) {
7558
- const r = v.result(t);
7571
+ let r = v.result(t);
7559
7572
  if(args.length > 3) r /= (d + 1);
7560
7573
  if(k <= 0) {
7561
7574
  // See NOTE in VMI_add_const_to_coefficient instruction
@@ -7779,7 +7792,7 @@ function VMI_add_throughput_to_coefficient(args) {
7779
7792
  }
7780
7793
  }
7781
7794
 
7782
- function VMI_set_objective(empty) {
7795
+ function VMI_set_objective() {
7783
7796
  // Copies the coefficients to the vector for the objective function
7784
7797
  if(DEBUGGING) console.log('set_objective');
7785
7798
  for(let i in VM.coefficients) if(Number(i)) {
@@ -7843,7 +7856,7 @@ function VMI_set_add_constraints_flag(args) {
7843
7856
  (VM.add_constraints_flag ? 'TRUE' : 'FALSE') + ')');
7844
7857
  }
7845
7858
 
7846
- function VMI_toggle_add_constraints_flag(empty) {
7859
+ function VMI_toggle_add_constraints_flag() {
7847
7860
  // Toggles the VM's "add constraints" flag
7848
7861
  VM.add_constraints_flag = !VM.add_constraints_flag;
7849
7862
  if(DEBUGGING) console.log('toggle_add_constraints_flag (now ' +
@@ -7956,7 +7969,7 @@ function VMI_add_bound_line_constraint(args) {
7956
7969
  VM.coefficients[w[i]] = 1;
7957
7970
  }
7958
7971
  VM.rhs = 1;
7959
- VMI_add_constraint(VM.EQ)
7972
+ VMI_add_constraint(VM.EQ);
7960
7973
  // Add constraint (2):
7961
7974
  VMI_clear_coefficients();
7962
7975
  VM.coefficients[VM.offset + vix] = 1;
@@ -7964,7 +7977,7 @@ function VMI_add_bound_line_constraint(args) {
7964
7977
  VM.coefficients[w[i]] = -x[i];
7965
7978
  }
7966
7979
  // No need to set RHS as it is already reset to 0
7967
- VMI_add_constraint(VM.EQ)
7980
+ VMI_add_constraint(VM.EQ);
7968
7981
  // Add constraint (3):
7969
7982
  VMI_clear_coefficients();
7970
7983
  VM.coefficients[VM.offset + viy] = 1;
@@ -8105,23 +8118,24 @@ const
8105
8118
  // *** API section for custom operators ***
8106
8119
  //
8107
8120
 
8108
- // Custom operators are typically used to implement computations on model results
8109
- // that cannot be coded (efficiently) using standard expressions.
8110
- // The first custom operator in this section demonstrates by example how custom
8111
- // operators can be added.
8121
+ // Custom operators are typically used to implement computations on model
8122
+ // results that cannot be coded (efficiently) using standard expressions.
8123
+ // The first custom operator in this section demonstrates by example how
8124
+ // custom operators can be added.
8112
8125
 
8113
8126
  // Custom operators should preferably have a short alphanumeric string as
8114
- // their identifying symbol. Custom operators are monadic and reducing, i.e.,
8115
- // they must have a grouping as operand. The number of required arguments must
8116
- // be checked at run time by the VM instruction for this operator.
8127
+ // their identifying symbol. Custom operators are monadic and reducing,
8128
+ // i.e., they must have a grouping as operand. The number of required
8129
+ // arguments must be checked at run time by the VM instruction for this
8130
+ // operator.
8117
8131
 
8118
8132
  // Each custom operator must have its own Virtual Machine instruction
8119
8133
 
8120
8134
  function VMI_profitable_units(x) {
8121
- // Replaces the argument list that should be at the top of the stack by the
8122
- // number of profitable units having a standard capacity (number), given the
8123
- // level (vector) of the process that represents multiple such units, the
8124
- // marginal cost (constant) and the market price (vector)
8135
+ // Replace the argument list that should be at the top of the stack by
8136
+ // the number of profitable units having a standard capacity (number),
8137
+ // given the level (vector) of the process that represents multiple such
8138
+ // units, the marginal cost (constant) and the market price (vector).
8125
8139
  const d = x.top();
8126
8140
  // Check whether the top stack element is a grouping of the correct size
8127
8141
  // that contains arguments of the correct type
@@ -8134,7 +8148,7 @@ function VMI_profitable_units(x) {
8134
8148
  d[3].entity.attributeExpression(d[3].attribute)) &&
8135
8149
  (d.length === 4 || (typeof d[4] === 'number' &&
8136
8150
  (d.length === 5 || typeof d[5] === 'number')))) {
8137
- // Valid parameters => get the data required for computation
8151
+ // Valid parameters => get the data required for computation.
8138
8152
  const
8139
8153
  mup = d[0].entity, // the multi-unit process
8140
8154
  ub = mup.upper_bound.result(0), // NOTE: UB is assumed to be static
@@ -8145,7 +8159,7 @@ function VMI_profitable_units(x) {
8145
8159
  pt = (d.length > 4 ? d[4] : 0), // the profit threshold (0 by default)
8146
8160
  // the time horizon (by default the length of the simulation period)
8147
8161
  nt = (d.length > 5 ? d[5] : MODEL.end_period - MODEL.start_period + 1);
8148
- // Handle exceptional values of `uc` and `mc`
8162
+ // Handle exceptional values of `uc` and `mc`.
8149
8163
  if(uc <= VM.BEYOND_MINUS_INFINITY || mc <= VM.BEYOND_MINUS_INFINITY) {
8150
8164
  x.retop(Math.min(uc, mc));
8151
8165
  return;
@@ -8155,16 +8169,16 @@ function VMI_profitable_units(x) {
8155
8169
  return;
8156
8170
  }
8157
8171
 
8158
- // NOTE: NPU is not time-dependent => result is stored in cache
8159
- // As expressions may contain several NPU operators, create a unique key
8160
- // based on its parameters
8172
+ // NOTE: NPU is not time-dependent => result is stored in cache.
8173
+ // As expressions may contain several NPU operators, create a unique
8174
+ // key based on its parameters.
8161
8175
  const cache_key = ['npu', mup.code, ub, uc, mc, mpe.code, mpa, pt].join('_');
8162
8176
  if(x.cache[cache_key]) {
8163
8177
  x.retop(x.cache[cache_key]);
8164
8178
  return;
8165
8179
  }
8166
8180
 
8167
- // mp can be a single value, a vector, or an expression
8181
+ // `mp` can be a single value, a vector, or an expression.
8168
8182
  let mp = mpe.attributeValue(mpa);
8169
8183
  if(mp === null) {
8170
8184
  mp = mpe.attributeExpression(mpa);
@@ -8179,8 +8193,8 @@ function VMI_profitable_units(x) {
8179
8193
  nu = Math.ceil(ub / uc), // Number of units
8180
8194
  r = [];
8181
8195
  if(mp && mp instanceof Expression) {
8182
- // NOTE: an expression may not have been (fully) computed yet
8183
- mp.compute();
8196
+ // NOTE: An expression may not have been (fully) computed yet.
8197
+ mp.compute(0);
8184
8198
  if(mp.isStatic) {
8185
8199
  mp = mp.result(0);
8186
8200
  } else {
@@ -8276,11 +8290,11 @@ function VMI_highest_cumulative_consecutive_deviation(x) {
8276
8290
  e = d[0].entity,
8277
8291
  a = d[0].attribute;
8278
8292
  let vector = e.attributeValue(a);
8279
- // NOTE: equations can also be passed by reference
8293
+ // NOTE: Equations can also be passed by reference.
8280
8294
  if(e === MODEL.equations_dataset) {
8281
8295
  const x = e.modifiers[a].expression;
8282
- // NOTE: an expression may not have been (fully) computed yet
8283
- x.compute();
8296
+ // NOTE: an expression may not have been (fully) computed yet.
8297
+ x.compute(0);
8284
8298
  if(!x.isStatic) {
8285
8299
  const nt = MODEL.end_period - MODEL.start_period + 1;
8286
8300
  for(let t = 1; t <= nt; t++) x.result(t);
@@ -8390,6 +8404,181 @@ DYNAMIC_SYMBOLS.push('hccd');
8390
8404
  // Add to this list only if operation makes an expression level-based
8391
8405
  // LEVEL_BASED_CODES.push(VMI_...);
8392
8406
 
8407
+ function correlation_or_slope(x, c_or_s) {
8408
+ // Replaces the argument list that should be at the top of the stack by
8409
+ // either Spearman's correlation (r) or the slope (b) of the regression
8410
+ // line y = a + bx for the two vectors X and Y that are passed as the
8411
+ // two arguments of this function. Reason to combine these two statistics
8412
+ // in one function is because the required operations are very similar.
8413
+ // NOTES:
8414
+ // (1) This function codes for two different operators and therefore
8415
+ // is a helper function. The two operators must each have their
8416
+ // own VM instruction -- see immediately after this function.
8417
+ // (2) String `c_or_s` must be either 'correl' or 'slope'.
8418
+ // (3) The operands for this function must be vectors, not numbers,
8419
+ // so in the Linny-R expression they must be passed "by reference".
8420
+ const
8421
+ d = x.top(),
8422
+ vmi = c_or_s;
8423
+ // Check whether the top stack element is a grouping of two variables.
8424
+ if(d instanceof Array && d.length === 2 &&
8425
+ typeof d[0] === 'object' && d[0].hasOwnProperty('entity') &&
8426
+ typeof d[1] === 'object' && d[1].hasOwnProperty('entity')) {
8427
+ // Convert the two variables to vectors.
8428
+ const vector = {x: {}, y: {}};
8429
+ for(let k in vector) if(vector.hasOwnProperty(k)) {
8430
+ const
8431
+ i = ['x', 'y'].indexOf(k),
8432
+ e = d[i].entity,
8433
+ a = d[i].attribute;
8434
+ vector[k].e = e;
8435
+ vector[k].a = a;
8436
+ vector[k].v = e.attributeValue(a);
8437
+ vector[k].name = e.displayName + (a ? '|' + a : '');
8438
+ vector[k].id = e.identifier;
8439
+ // NOTE: Equations can also be passed by reference.
8440
+ if(e === MODEL.equations_dataset) {
8441
+ const eq = e.modifiers[UI.nameToID(a)].expression;
8442
+ // Level-based equations require that the model has run.
8443
+ if(eq.is_level_based && !MODEL.solved) {
8444
+ x.retop(VM.NOT_COMPUTED);
8445
+ return;
8446
+ }
8447
+ // NOTE: An equation may not have been (fully) computed yet.
8448
+ eq.compute(0, x.wildcard_vector_index);
8449
+ if(!eq.isStatic) {
8450
+ const nt = MODEL.end_period - MODEL.start_period + 1;
8451
+ for(let t = 1; t <= nt; t++) eq.result(t, x.wildcard_vector_index);
8452
+ }
8453
+ vector[k].v = eq.vector;
8454
+ }
8455
+ }
8456
+ // If either operand is level-based, return "not computed" if the
8457
+ // model has not been run yet.
8458
+ if((VM.level_based_attr.indexOf(vector.x.a) >= 0 ||
8459
+ VM.level_based_attr.indexOf(vector.y.a) >= 0) &&
8460
+ !MODEL.solved) {
8461
+ x.retop(VM.NOT_COMPUTED);
8462
+ return;
8463
+ }
8464
+ if(Array.isArray(vector.x.v) && Array.isArray(vector.y.v)) {
8465
+ // Valid parameters => compute the terms used in the formulas
8466
+ // for correlation (r) and regression (slope and intercept)
8467
+ // NOTE: Statistics are not time-dependent, so the result is stored
8468
+ // in the expression's cache. As expressions may contain several
8469
+ // correl and slope operators, create a unique key based on the
8470
+ // operator name and its two operands.
8471
+ const cache_key = [vmi, vector.x.id, vector.x.a,
8472
+ vector.y.id, vector.y.a].join('_');
8473
+ if(x.cache[cache_key]) {
8474
+ x.retop(x.cache[cache_key]);
8475
+ return;
8476
+ }
8477
+ if(true||DEBUGGING) {
8478
+ console.log(`-- ${vmi}(${vector.x.name}, ${vector.y.name})`);
8479
+ }
8480
+ // NOTE: Vectors should have equal length.
8481
+ const N = Math.min(vector.x.v.length, vector.y.v.length);
8482
+ if(!N) {
8483
+ // No data => result should be "division by zero"
8484
+ x.retop(VM.DIV_ZERO);
8485
+ return;
8486
+ }
8487
+ // Calculate dsq = N*variance for X and Y.
8488
+ for(let k in vector) if(vector.hasOwnProperty(k)) {
8489
+ let sum = 0;
8490
+ // NOTE: Ignore first element of vector (t=0).
8491
+ for(let i = 1; i < N; i++) {
8492
+ const v = vector[k].v[i];
8493
+ // Handle exceptional values in vector.
8494
+ if(v <= VM.BEYOND_MINUS_INFINITY || v >= VM.BEYOND_PLUS_INFINITY) {
8495
+ x.retop(v);
8496
+ return;
8497
+ }
8498
+ sum += v;
8499
+ }
8500
+ vector[k].sum = sum;
8501
+ const mean = sum / N;
8502
+ vector[k].mean = mean;
8503
+ let dsq = 0;
8504
+ // NOTE: Ignore first element of vector (t=0).
8505
+ for(let i = 1; i < N; i++) {
8506
+ const d = vector[k].v[i] - mean;
8507
+ dsq += d * d;
8508
+ }
8509
+ vector[k].dsq = dsq;
8510
+ }
8511
+ // Divisor is sqrt(dsqX * dsqY). If zero, return #DIV/0
8512
+ const divisor = Math.sqrt(vector.x.dsq * vector.y.dsq);
8513
+ if(divisor < VM.NEAR_ZERO) {
8514
+ x.retop(VM.DIV_ZERO);
8515
+ return;
8516
+ }
8517
+ // Calculate N*covariance of X and Y.
8518
+ let covar = 0;
8519
+ // NOTE: Ignore first element of vector (t=0).
8520
+ for(let i = 1; i < N; i++) {
8521
+ covar += (vector.x.v[i] - vector.x.mean) * (vector.y.v[i] - vector.y.mean);
8522
+ }
8523
+ // Correlation = covarXY / sqrt(dsqX * dsqY), slope = covarXY / dsqX.
8524
+ // NOTE: dsqX will be non-zero (or divisor would have been zero).
8525
+ const result = covar / (vmi === 'correl' ? divisor : vector.x.dsq);
8526
+ // Store the result in the expression's cache.
8527
+ x.cache[cache_key] = result;
8528
+ // Push the result onto the stack.
8529
+ x.retop(result);
8530
+ return;
8531
+ }
8532
+ }
8533
+ // Fall-trough indicates error
8534
+ if(DEBUGGING) console.log(vmi + ': invalid parameter(s)\n', d);
8535
+ x.retop(VM.PARAMS);
8536
+ }
8537
+
8538
+ // NOTE: Separate function for each operator: VMI_correl and VMI_slope.
8539
+
8540
+ function VMI_correlation(x) {
8541
+ correlation_or_slope(x, 'correl');
8542
+ }
8543
+
8544
+ // Add the custom operator instruction to the global lists
8545
+ // NOTE: All custom operators are monadic (priority 9) and reducing
8546
+ OPERATORS.push('correl');
8547
+ MONADIC_OPERATORS.push('correl');
8548
+ ACTUAL_SYMBOLS.push('correl');
8549
+ OPERATOR_CODES.push(VMI_correlation);
8550
+ MONADIC_CODES.push(VMI_correlation);
8551
+ REDUCING_CODES.push(VMI_correlation);
8552
+ SYMBOL_CODES.push(VMI_correlation);
8553
+ PRIORITIES.push(9);
8554
+ // Add to this list only if operation makes an expression dynamic
8555
+ // DYNAMIC_SYMBOLS.push('...');
8556
+ // Add to this list only if operation makes an expression random
8557
+ // RANDOM_CODES.push(VMI_...);
8558
+ // Add to this list only if operation makes an expression level-based
8559
+ // LEVEL_BASED_CODES.push(VMI_...);
8560
+
8561
+ function VMI_slope(x) {
8562
+ correlation_or_slope(x, 'slope');
8563
+ }
8564
+
8565
+ // Add the custom operator instruction to the global lists
8566
+ // NOTE: All custom operators are monadic (priority 9) and reducing
8567
+ OPERATORS.push('slope');
8568
+ MONADIC_OPERATORS.push('slope');
8569
+ ACTUAL_SYMBOLS.push('slope');
8570
+ OPERATOR_CODES.push(VMI_slope);
8571
+ MONADIC_CODES.push(VMI_slope);
8572
+ REDUCING_CODES.push(VMI_slope);
8573
+ SYMBOL_CODES.push(VMI_slope);
8574
+ PRIORITIES.push(9);
8575
+ // Add to this list only if operation makes an expression dynamic
8576
+ // DYNAMIC_SYMBOLS.push('...');
8577
+ // Add to this list only if operation makes an expression random
8578
+ // RANDOM_CODES.push(VMI_...);
8579
+ // Add to this list only if operation makes an expression level-based
8580
+ // LEVEL_BASED_CODES.push(VMI_...);
8581
+
8393
8582
  /*** END of custom operator API section ***/
8394
8583
 
8395
8584
  ///////////////////////////////////////////////////////////////////////
@@ -8399,4 +8588,4 @@ if(NODE) module.exports = {
8399
8588
  Expression: Expression,
8400
8589
  ExpressionParser: ExpressionParser,
8401
8590
  VirtualMachine: VirtualMachine
8402
- }
8591
+ };