linny-r 1.1.22 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,6 +64,7 @@ class Expression {
64
64
  get variableName() {
65
65
  // Return the name of the variable computed by this expression
66
66
  if(this.attribute === 'C') return 'note color expression';
67
+ if(this.object === MODEL.equations_dataset) return 'equation ' + this.attribute;
67
68
  return this.object.displayName + UI.OA_SEPARATOR + this.attribute;
68
69
  }
69
70
 
@@ -305,7 +306,6 @@ class Expression {
305
306
  // Pop the time step
306
307
  this.step.pop();
307
308
  this.trace('--STOP: ' + this.variableName);
308
- DEBUGGING = false;
309
309
  // Clear context for #
310
310
  this.wildcard_number = false;
311
311
  // If error, display the call stack (only once)
@@ -526,10 +526,11 @@ class ExpressionParser {
526
526
  this.log('dynamic because of offset');
527
527
  // String contains at least one @ character, then split at the last (pop)
528
528
  // and check that @ sign is followed by an offset (range if `:`)
529
+ // NOTE: offset anchors are case-insensitive
529
530
  const offs = s.pop().replace(/\s+/g, '').toLowerCase().split(':');
530
531
  // Re-assemble the other substrings, as name itself may contain @ signs
531
532
  name = s.join('@').trim();
532
- const re = /(^[\+\-]?[0-9]+|[\#cfinprst]([\+\-][0-9]+)?)$/;
533
+ const re = /(^[\+\-]?[0-9]+|[\#cfijklnprst]([\+\-][0-9]+)?)$/;
533
534
  if(!re.test(offs[0])) {
534
535
  msg = `Invalid offset "${offs[0]}"`;
535
536
  } else if(offs.length > 1 && !re.test(offs[1])) {
@@ -539,21 +540,22 @@ class ExpressionParser {
539
540
  // Anchor may be:
540
541
  // # (absolute index in vector)
541
542
  // c (start of current block)
542
- // f (final: last value of the vector)
543
- // i (initial, i.e. time step 0)
543
+ // f (first value of the vector, i.e., time step 0)
544
+ // i, j, k (iterator index variable)
545
+ // l (last value of the vector, i.e., time step t_N)
544
546
  // n (start of next block)
545
547
  // p (start of previous block)
546
548
  // r (relative: relative time step, i.e., t0 = 1)
547
549
  // s (scaled: time step 0, but offset is scaled to time unit of run)
548
550
  // t (current time step, this is the default),
549
- if('#cfinprst'.includes(offs[0].charAt(0))) {
551
+ if('#cfijklnprst'.includes(offs[0].charAt(0))) {
550
552
  anchor1 = offs[0].charAt(0);
551
553
  offset1 = safeStrToInt(offs[0].substr(1));
552
554
  } else {
553
555
  offset1 = safeStrToInt(offs[0]);
554
556
  }
555
557
  if(offs.length > 1) {
556
- if('#cfinprst'.includes(offs[1].charAt(0))) {
558
+ if('#cfijklnprst'.includes(offs[1].charAt(0))) {
557
559
  anchor2 = offs[1].charAt(0);
558
560
  offset2 = safeStrToInt(offs[1].substr(1));
559
561
  } else {
@@ -723,12 +725,12 @@ class ExpressionParser {
723
725
  // a result, so what follows does not apply to experiment results
724
726
  //
725
727
 
726
- // Attribute name (optional) follows object-attribute separator
728
+ // Attribute name (optional) follows object-attribute separator |
727
729
  s = name.split(UI.OA_SEPARATOR);
728
730
  if(s.length > 1) {
729
- // Attribute is string after LAST vertical bar ...
731
+ // Attribute is string after LAST separator ...
730
732
  attr = s.pop().trim();
731
- // ... so restore name if itself contains other vertical bars
733
+ // ... so restore name if itself contains other separators
732
734
  name = s.join(UI.OA_SEPARATOR).trim();
733
735
  if(!attr) {
734
736
  // Explicit *empty* attribute, e.g., [name|]
@@ -808,6 +810,18 @@ class ExpressionParser {
808
810
  }
809
811
  }
810
812
  }
813
+ // NOTE: also add expressions for equations that match
814
+ const edm = MODEL.equations_dataset.modifiers;
815
+ for(let k in edm) if(edm.hasOwnProperty(k)) {
816
+ const m = edm[k];
817
+ if(patternMatch(m.selector, pat)) {
818
+ list.push(m.expression);
819
+ if(!m.expression.isStatic) {
820
+ this.is_static = false;
821
+ this.log('dynamic because matching equation is dynamic');
822
+ }
823
+ }
824
+ }
811
825
  if(list.length > 0) {
812
826
  // NOTE: statistic MAY make expression level-based
813
827
  // NOTE: assume NOT when offset has been specified, as this suggests
@@ -878,7 +892,8 @@ class ExpressionParser {
878
892
  'Equation' : 'Dataset modifier expression') +
879
893
  ' must not reference itself';
880
894
  } else if(obj.array &&
881
- (anchor1 && anchor1 !== '#' || anchor2 && anchor2 !== '#')) {
895
+ (anchor1 && '#ijk'.indexOf(anchor1) < 0 ||
896
+ anchor2 && '#ijk'.indexOf(anchor2) < 0)) {
882
897
  msg = 'Invalid anchor(s) for array-type dataset ' + obj.displayName;
883
898
  } else {
884
899
  // NOTE: except for array-type datasets, the default anchor is 't';
@@ -893,6 +908,10 @@ class ExpressionParser {
893
908
  return [{r: obj, a: attr}, anchor1, offset1, anchor2, offset2];
894
909
  }
895
910
  if(obj === this.dataset && attr === '' && !obj.array) {
911
+ // When dataset modifier expression refers to its dataset without
912
+ // selector, then this is equivalent to [.] (use the series data
913
+ // vector) unless it is an array, since then the series data is
914
+ // not a time-scaled vector => special case
896
915
  args = obj.vector;
897
916
  } else if(attr === '') {
898
917
  // For all other variables, assume default attribute
@@ -903,8 +922,6 @@ class ExpressionParser {
903
922
  // "use the data"
904
923
  if(obj instanceof Dataset &&
905
924
  (obj.array || (!use_data && obj.selectorList.length > 0))) {
906
- // NOTE: also pass the "use data" flag so that experiment selectors
907
- // will be ignored if the modeler coded the vertical bar
908
925
  if(obj.data.length > 1 || obj.data.length > 0 && !obj.periodic ||
909
926
  !obj.allModifiersAreStatic) {
910
927
  // No explicit selector => dynamic unless no time series data, and
@@ -912,6 +929,8 @@ class ExpressionParser {
912
929
  this.is_static = false;
913
930
  this.log('dynamic because dataset without explicit selector is used');
914
931
  }
932
+ // NOTE: also pass the "use data" flag so that experiment selectors
933
+ // will be ignored if the modeler coded the vertical bar
915
934
  return [{d: obj, ud: use_data}, anchor1, offset1, anchor2, offset2];
916
935
  }
917
936
  } else if(obj instanceof Dataset) {
@@ -1005,10 +1024,10 @@ class ExpressionParser {
1005
1024
  c = this.expr.charAt(this.pit);
1006
1025
  if(c === '[') {
1007
1026
  // Left bracket denotes start of a variable name
1008
- // NOTE: As variable names may contain regular expressions, they may
1009
- // also contain brackets => allow *matched* [...] pairs inside
1010
1027
  i = indexOfMatchingBracket(this.expr, this.pit);
1011
1028
  if(i < 0) {
1029
+ this.pit++;
1030
+ this.los = 1;
1012
1031
  this.error = 'Missing closing bracket \']\'';
1013
1032
  } else {
1014
1033
  v = this.expr.substr(this.pit + 1, i - 1 - this.pit);
@@ -1020,6 +1039,27 @@ class ExpressionParser {
1020
1039
  this.sym = this.parseVariable(v);
1021
1040
  // NOTE: parseVariable may set is_static to FALSE
1022
1041
  }
1042
+ } else if(c === "'") {
1043
+ // Symbol is ALL text up to and including closing quote and trailing
1044
+ // spaces (but such spaces are trimmed)
1045
+ i = this.expr.indexOf("'", this.pit + 1);
1046
+ if(i < 0) {
1047
+ this.pit++;
1048
+ this.los = 1;
1049
+ this.error = 'Unmatched quote';
1050
+ } else {
1051
+ v = this.expr.substr(this.pit + 1, i - 1 - this.pit);
1052
+ this.pit = i + 1;
1053
+ // NOTE: Enclosing quotes are also part of this symbol
1054
+ this.los = v.length + 2;
1055
+ v = UI.cleanName(v);
1056
+ if(MODEL.scale_units.hasOwnProperty(v)) {
1057
+ // Symbol is a scale unit => use its multiplier as numerical value
1058
+ this.sym = MODEL.scale_units[v].multiplier;
1059
+ } else {
1060
+ this.error = `Unknown scale unit "${v}"`;
1061
+ }
1062
+ }
1023
1063
  } else if(c === '(' || c === ')') {
1024
1064
  this.sym = c;
1025
1065
  this.los = 1;
@@ -1039,7 +1079,7 @@ class ExpressionParser {
1039
1079
  this.sym = OPERATOR_CODES[OPERATORS.indexOf(c)];
1040
1080
  } else {
1041
1081
  // Take any text up to the next operator, parenthesis,
1042
- // opening bracket, or space
1082
+ // opening bracket, quote or space
1043
1083
  this.los = 0;
1044
1084
  let pl = this.pit + this.los,
1045
1085
  cpl = this.expr.charAt(pl),
@@ -1090,12 +1130,15 @@ class ExpressionParser {
1090
1130
  // If a valid number, keep it within the +/- infinity range
1091
1131
  this.sym = Math.max(VM.MINUS_INFINITY, Math.min(VM.PLUS_INFINITY, f));
1092
1132
  }
1133
+ } else if(MODEL.scale_units.hasOwnProperty(v)) {
1134
+ // Symbol is a scale unit => use its multiplier as numerical value
1135
+ this.sym = MODEL.scale_units[v].multiplier;
1093
1136
  } else {
1094
1137
  // Symbol does not start with a digit
1095
1138
  // NOTE: distinguish between run length N and block length n
1096
1139
  i = ACTUAL_SYMBOLS.indexOf(l === 'n' ? v : l);
1097
1140
  if(i < 0) {
1098
- this.error = `Invalid symbol "${l}"`;
1141
+ this.error = `Invalid symbol "${v}"`;
1099
1142
  } else {
1100
1143
  this.sym = SYMBOL_CODES[i];
1101
1144
  // NOTE: Using time symbols or `random` makes the expression dynamic!
@@ -1377,8 +1420,12 @@ class VirtualMachine {
1377
1420
  this.chunk_variables = [];
1378
1421
  // Array for VM instructions
1379
1422
  this.code = [];
1380
- // Array to hold lines of (solver-dependent) model equations
1381
- this.lines = [];
1423
+ // The Simplex tableau: matrix, rhs and ct will have same length
1424
+ this.matrix = [];
1425
+ this.right_hand_side = [];
1426
+ this.constraint_types = [];
1427
+ // String to hold lines of (solver-dependent) model equations
1428
+ this.lines = '';
1382
1429
  // String specifying a numeric issue (empty if none)
1383
1430
  this.numeric_issue = '';
1384
1431
  // The call stack tracks evaluation of "nested" expression variables
@@ -3988,6 +4035,15 @@ class VirtualMachine {
3988
4035
  setTimeout((n) => VM.initializeTableau(n), 0, abl);
3989
4036
  }
3990
4037
 
4038
+ resetTableau() {
4039
+ // Clears tableau data: matrix, rhs and constraint types
4040
+ // NOTE: this reset is called when initializing, and to free up
4041
+ // memory after posting a block to the server
4042
+ this.matrix.length = 0;
4043
+ this.right_hand_side.length = 0;
4044
+ this.constraint_types.length = 0;
4045
+ }
4046
+
3991
4047
  initializeTableau(abl) {
3992
4048
  // `offset` is used to calculate the actual column index for variables
3993
4049
  this.offset = 0;
@@ -4015,9 +4071,7 @@ class VirtualMachine {
4015
4071
  this.rhs = 0;
4016
4072
  // NOTE: the constraint coefficient matrix and the rhs and ct vectors
4017
4073
  // have equal length (#rows); the matrix is a list of sparse vectors
4018
- this.matrix = [];
4019
- this.right_hand_side = [];
4020
- this.constraint_types = [];
4074
+ this.resetTableau();
4021
4075
  // NOTE: setupBlock only works properly if setupProblem was successful
4022
4076
  // Every variable gets one column per time step => tableau is organized
4023
4077
  // in segments per time step, where each segment has `cols` columns
@@ -4199,9 +4253,8 @@ class VirtualMachine {
4199
4253
  // shorter than the standard, as it should not go beyond the end time
4200
4254
  const abl = this.actualBlockLength;
4201
4255
  this.numeric_issue = '';
4202
- this.lines.length = 0;
4203
4256
  // First add the objective (always MAXimize)
4204
- this.lines.push('/* Objective function */\nmax:');
4257
+ this.lines = '/* Objective function */\nmax:\n';
4205
4258
  let c,
4206
4259
  p,
4207
4260
  line = '';
@@ -4237,14 +4290,14 @@ class VirtualMachine {
4237
4290
  }
4238
4291
  // Keep lines under approx. 110 chars
4239
4292
  if(line.length >= 100) {
4240
- this.lines.push(line);
4293
+ this.lines += line + '\n';
4241
4294
  line = '';
4242
4295
  }
4243
4296
  }
4244
- this.lines.push(line + ';');
4297
+ this.lines += line + ';\n';
4245
4298
  line = '';
4246
4299
  // Add the row constraints
4247
- this.lines.push('\n/* Constraints */');
4300
+ this.lines += '\n/* Constraints */\n';
4248
4301
  n = this.matrix.length;
4249
4302
  for(let r = 0; r < n; r++) {
4250
4303
  const row = this.matrix[r];
@@ -4265,17 +4318,17 @@ class VirtualMachine {
4265
4318
  }
4266
4319
  // Keep lines under approx. 80 chars
4267
4320
  if(line.length >= 100) {
4268
- this.lines.push(line);
4321
+ this.lines += line + '\n';
4269
4322
  line = '';
4270
4323
  }
4271
4324
  }
4272
4325
  c = this.right_hand_side[r];
4273
- this.lines.push(line + ' ' +
4274
- this.constraint_symbols[this.constraint_types[r]] + ' ' + c + ';');
4326
+ this.lines += line + ' ' +
4327
+ this.constraint_symbols[this.constraint_types[r]] + ' ' + c + ';\n';
4275
4328
  line = '';
4276
4329
  }
4277
4330
  // Add the variable bounds
4278
- this.lines.push('\n/* Variable bounds */');
4331
+ this.lines += '\n/* Variable bounds */\n';
4279
4332
  n = abl * this.cols;
4280
4333
  for(p = 1; p <= n; p++) {
4281
4334
  let lb = null,
@@ -4304,25 +4357,25 @@ class VirtualMachine {
4304
4357
  if(lb !== null && lb !== 0) line = lb + ' <= ' + line;
4305
4358
  if(ub !== null) line += ' <= ' + ub;
4306
4359
  }
4307
- if(line) this.lines.push(line + ';');
4360
+ if(line) this.lines += line + ';\n';
4308
4361
  }
4309
4362
  // Add the special variable types
4310
4363
  const v_set = [];
4311
4364
  // NOTE: for binary variables, add the constraint <= 1
4312
4365
  for(let i in this.is_binary) if(Number(i)) {
4313
- this.lines.push('C' + i + ' <= 1;');
4366
+ this.lines += 'C' + i + ' <= 1;\n';
4314
4367
  v_set.push('C' + i);
4315
4368
  }
4316
4369
  for(let i in this.is_integer) if(Number(i)) v_set.push('C' + i);
4317
- if(v_set.length > 0) this.lines.push('int ' + v_set.join(', ') + ';');
4370
+ if(v_set.length > 0) this.lines += 'int ' + v_set.join(', ') + ';\n';
4318
4371
  // Clear the INT variable list
4319
4372
  v_set.length = 0;
4320
4373
  // Add the semi-continuous variables
4321
4374
  for(let i in this.is_semi_continuous) if(Number(i)) v_set.push('C' + i);
4322
- if(v_set.length > 0) this.lines.push('sec ' + v_set.join(', ') + ';');
4375
+ if(v_set.length > 0) this.lines += 'sec ' + v_set.join(', ') + ';\n';
4323
4376
  // Add the SOS section
4324
4377
  if(this.sos_var_indices.length > 0) {
4325
- this.lines.push('sos');
4378
+ this.lines += 'sos\n';
4326
4379
  let sos = 1;
4327
4380
  for(let j = 0; j < abl; j++) {
4328
4381
  for(let i = 0; i < this.sos_var_indices.length; i++) {
@@ -4333,7 +4386,7 @@ class VirtualMachine {
4333
4386
  v_set.push('C' + vi);
4334
4387
  vi++;
4335
4388
  }
4336
- this.lines.push(`SOS${sos}: ${v_set.join(',')} <= 2;`);
4389
+ this.lines += `SOS${sos}: ${v_set.join(',')} <= 2;\n`;
4337
4390
  sos++;
4338
4391
  }
4339
4392
  }
@@ -4368,19 +4421,18 @@ class VirtualMachine {
4368
4421
  p,
4369
4422
  r;
4370
4423
  this.numeric_issue = '';
4371
- this.lines.length = 0;
4424
+ this.lines = '';
4372
4425
  for(c = 1; c <= ncol; c++) cols.push([]);
4373
4426
  this.decimals = Math.max(nrow, ncol).toString().length;
4374
- this.lines.push('NAME block-' + this.blockWithRound);
4375
- this.lines.push('ROWS');
4427
+ this.lines += 'NAME block-' + this.blockWithRound + '\nROWS\n';
4376
4428
  // Start with the "free" row that will be the objective function
4377
- this.lines.push(' N OBJ');
4429
+ this.lines += ' N OBJ\n';
4378
4430
  for(r = 0; r < nrow; r++) {
4379
4431
  const
4380
4432
  row = this.matrix[r],
4381
4433
  row_lbl = 'R' + (r + 1).toString().padStart(this.decimals, '0');
4382
- this.lines.push(' ' + this.constraint_letters[this.constraint_types[r]] +
4383
- ' ' + row_lbl);
4434
+ this.lines += ' ' + this.constraint_letters[this.constraint_types[r]] +
4435
+ ' ' + row_lbl + '\n';
4384
4436
  for(p in row) if (row.hasOwnProperty(p)) {
4385
4437
  c = row[p];
4386
4438
  // Check for numeric issues
@@ -4421,17 +4473,18 @@ class VirtualMachine {
4421
4473
  return;
4422
4474
  }
4423
4475
  // Add the columns section
4424
- this.lines.push('COLUMNS');
4476
+ this.lines += 'COLUMNS\n';
4425
4477
  for(c = 1; c <= ncol; c++) {
4426
4478
  const col_lbl = ' X' + c.toString().padStart(this.decimals, '0') + ' ';
4427
- this.lines.push(col_lbl + cols[c].join('\n' + col_lbl));
4479
+ this.lines += col_lbl + cols[c].join('\n' + col_lbl) + '\n';
4428
4480
  }
4481
+ // Free up memory
4429
4482
  cols.length = 0;
4430
4483
  // Add the RHS section
4431
- this.lines.push('RHS\n' + rhs.join('\n'));
4484
+ this.lines += 'RHS\n' + rhs.join('\n') + '\n';
4432
4485
  rhs.length = 0;
4433
4486
  // Add the BOUNDS section
4434
- this.lines.push('BOUNDS');
4487
+ this.lines += 'BOUNDS\n';
4435
4488
  // NOTE: start at column number 1 (not 0)
4436
4489
  setTimeout((c, n) => VM.showMPSProgress(c, n), 0, 1, ncol);
4437
4490
  }
@@ -4490,12 +4543,12 @@ class VirtualMachine {
4490
4543
  */
4491
4544
  semic = p in this.is_semi_continuous;
4492
4545
  if(p in this.is_binary) {
4493
- this.lines.push(' BV' + bnd);
4546
+ this.lines += ' BV' + bnd + '\n';
4494
4547
  } else if(lb !== null && ub !== null && lb <= VM.SOLVER_MINUS_INFINITY &&
4495
4548
  ub >= VM.SOLVER_PLUS_INFINITY) {
4496
- this.lines.push(' FR' + bnd);
4549
+ this.lines += ' FR' + bnd + '\n';
4497
4550
  } else if(lb !== null && lb === ub && !semic) {
4498
- this.lines.push(' FX' + bnd + lb);
4551
+ this.lines += ' FX' + bnd + lb + '\n';
4499
4552
  } else {
4500
4553
  // Assume "standard" bounds
4501
4554
  lbc = ' LO';
@@ -4510,10 +4563,10 @@ class VirtualMachine {
4510
4563
  }
4511
4564
  // NOTE: by default, lower bound of variables is 0
4512
4565
  if(lb !== null && lb !== 0 || lbc !== ' LO') {
4513
- this.lines.push(lbc + bnd + lb);
4566
+ this.lines += lbc + bnd + lb + '\n';
4514
4567
  }
4515
4568
  if(ub !== null) {
4516
- this.lines.push(ubc + bnd + ub);
4569
+ this.lines += ubc + bnd + ub + '\n';
4517
4570
  }
4518
4571
  }
4519
4572
  }
@@ -4531,18 +4584,18 @@ class VirtualMachine {
4531
4584
  this.hideSetUpOrWriteProgress();
4532
4585
  // Add the SOS section
4533
4586
  if(this.sos_var_indices.length > 0) {
4534
- this.lines.push('SOS');
4587
+ this.lines += 'SOS\n';
4535
4588
  const abl = this.actualBlockLength;
4536
4589
  let sos = 1;
4537
4590
  for(let j = 0; j < abl; j++) {
4538
4591
  for(let i = 0; i < this.sos_var_indices.length; i++) {
4539
- this.lines.push(' S2 sos' + sos);
4592
+ this.lines += ' S2 sos' + sos + '\n';
4540
4593
  let vi = this.sos_var_indices[i][0] + j * this.cols;
4541
4594
  const n = this.sos_var_indices[i][1];
4542
4595
  for(let j = 1; j <= n; j++) {
4543
4596
  const s = ' X' + vi.toString().padStart(this.decimals, '0') +
4544
4597
  ' ';
4545
- this.lines.push(s.substring(0, 15) + j);
4598
+ this.lines += s.substring(0, 15) + j + '\n';
4546
4599
  vi++;
4547
4600
  }
4548
4601
  sos++;
@@ -4550,7 +4603,7 @@ class VirtualMachine {
4550
4603
  }
4551
4604
  }
4552
4605
  // Add the end-of-model marker
4553
- this.lines.push('ENDATA');
4606
+ this.lines += 'ENDATA';
4554
4607
  setTimeout(() => VM.submitFile(), 0);
4555
4608
  }
4556
4609
 
@@ -4726,6 +4779,9 @@ Solver status = ${json.status}`);
4726
4779
  }
4727
4780
 
4728
4781
  submitFile() {
4782
+ // Prepares to POST the model file (LP or MPS) to the Linny-R server
4783
+ // NOTE: the tableau is no longer needed, so free up its memory
4784
+ this.resetTableau();
4729
4785
  if(this.numeric_issue) {
4730
4786
  const msg = 'Invalid ' + this.numeric_issue;
4731
4787
  this.logMessage(this.block_count, msg);
@@ -4734,13 +4790,11 @@ Solver status = ${json.status}`);
4734
4790
  } else {
4735
4791
  // Log the time it took to create the code lines
4736
4792
  this.logMessage(this.block_count,
4737
- `Model file creation (${this.lines.length} lines) took ` +
4738
- this.elapsedTime + ' seconds.');
4739
- // Concatenate code lines to POST as a single data string
4740
- const ccl = this.lines.join('\n');
4741
- // Immediately free the constituent lines
4742
- this.lines.length = 0;
4743
- MONITOR.submitBlockToSolver(ccl);
4793
+ 'Model file creation (' + UI.sizeInBytes(this.lines.length) +
4794
+ ') took ' + this.elapsedTime + ' seconds.');
4795
+ // NOTE: monitor will use (and then clear) VM.lines, so no need
4796
+ // to pass it on as parameter
4797
+ MONITOR.submitBlockToSolver();
4744
4798
  // Now the round number can be increased...
4745
4799
  this.current_round++;
4746
4800
  // ... and also the blocknumber if all rounds have been played
@@ -4974,6 +5028,42 @@ function VMI_push_infinity(x, empty) {
4974
5028
  x.push(VM.PLUS_INFINITY);
4975
5029
  }
4976
5030
 
5031
+ function valueOfIndexVariable(v) {
5032
+ // AUXILIARY FUNCTION for the VMI_push_(i, j or k) instructions
5033
+ // Returns value of iterator index variable for the current experiment
5034
+ if(MODEL.running_experiment) {
5035
+ const
5036
+ lead = v + '=',
5037
+ combi = MODEL.running_experiment.activeCombination;
5038
+ for(let i = 0; i < combi.length; i++) {
5039
+ const sel = combi[i] ;
5040
+ if(sel.startsWith(lead)) return parseInt(sel.substring(2));
5041
+ }
5042
+ }
5043
+ return 0;
5044
+ }
5045
+
5046
+ function VMI_push_i(x, empty) {
5047
+ // Pushes the value of iterator index i
5048
+ const i = valueOfIndexVariable('i');
5049
+ if(DEBUGGING) console.log('push i = ' + i);
5050
+ x.push(i);
5051
+ }
5052
+
5053
+ function VMI_push_j(x, empty) {
5054
+ // Pushes the value of iterator index j
5055
+ const j = valueOfIndexVariable('j');
5056
+ if(DEBUGGING) console.log('push j = ' + j);
5057
+ x.push(j);
5058
+ }
5059
+
5060
+ function VMI_push_k(x, empty) {
5061
+ // Pushes the value of iterator index k
5062
+ const k = valueOfIndexVariable('k');
5063
+ if(DEBUGGING) console.log('push k = ' + k);
5064
+ x.push(k);
5065
+ }
5066
+
4977
5067
  function pushTimeStepsPerTimeUnit(x, unit) {
4978
5068
  // AUXILIARY FUNCTION for the VMI_push_(time unit) instructions
4979
5069
  // Pushes the number of model time steps represented by 1 unit
@@ -5109,14 +5199,18 @@ function relativeTimeStep(t, anchor, offset, dtm, x) {
5109
5199
  // Relative to start of next optimization block
5110
5200
  return VM.block_start + MODEL.block_length + offset;
5111
5201
  }
5112
- if(anchor === 'f') {
5113
- // Final: offset relative to the last index in the vector
5202
+ if(anchor === 'l') {
5203
+ // Last: offset relative to the last index in the vector
5114
5204
  return MODEL.end_period - MODEL.start_period + 1 + offset;
5115
5205
  }
5116
5206
  if(anchor === 's') {
5117
5207
  // Scaled: offset is scaled to time unit of run
5118
5208
  return Math.floor(offset * dtm);
5119
5209
  }
5210
+ if('ijk'.indexOf(anchor) >= 0) {
5211
+ // Index: offset is added to the iterator index i, j or k
5212
+ return valueOfIndexVariable(anchor) + offset;
5213
+ }
5120
5214
  if(anchor === '#') {
5121
5215
  // Index: offset is added to the inferred value of the # symbol
5122
5216
  return valueOfNumberSign(x) + offset;
@@ -5132,6 +5226,7 @@ function relativeTimeStep(t, anchor, offset, dtm, x) {
5132
5226
  return valueOfNumberSign(x) + offset;
5133
5227
  }
5134
5228
  // Fall-through: offset relative to the initial value index (0)
5229
+ // NOTE: this also applies to anchor f (First)
5135
5230
  return offset;
5136
5231
  }
5137
5232
 
@@ -5183,7 +5278,16 @@ function VMI_push_var(x, args) {
5183
5278
  }
5184
5279
  if(Array.isArray(obj)) {
5185
5280
  // Object is a vector
5186
- x.push(t < obj.length ? obj[t] : VM.UNDEFINED);
5281
+ let v = t < obj.length ? obj[t] : VM.UNDEFINED;
5282
+ // NOTE: when the vector is the "active" parameter for sensitivity
5283
+ // analysis, the value is multiplied by 1 + delta %
5284
+ if(obj === MODEL.active_sensitivity_parameter) {
5285
+ // NOTE: do NOT scale exceptional values
5286
+ if(v > VM.MINUS_INFINITY && v < VM.PLUS_INFINITY) {
5287
+ v *= (1 + MODEL.sensitivity_delta * 0.01);
5288
+ }
5289
+ }
5290
+ x.push(v);
5187
5291
  } else if(xv) {
5188
5292
  // Variable references an earlier value computed for this expression `x`
5189
5293
  x.push(t >= 0 && t < x.vector.length ? x.vector[t] : obj.dv);
@@ -5265,23 +5369,7 @@ function VMI_push_dataset_modifier(x, args) {
5265
5369
  // If modifier selector is specified, use the associated expression
5266
5370
  obj = mx;
5267
5371
  } else if(!ud) {
5268
- if(MODEL.running_experiment) {
5269
- // If an experiment is running, check if dataset modifiers match the
5270
- // combination of selectors for the active run
5271
- const mm = ds.matchingModifiers(MODEL.running_experiment.activeCombination);
5272
- // If so, use the first match
5273
- if(mm.length > 0) obj = mm[0].expression;
5274
- } else if(ds.default_selector) {
5275
- // If no expriment (so "normal" run), use default selector if specified
5276
- const dm = ds.modifiers[ds.default_selector];
5277
- if(dm) {
5278
- obj = dm.expression;
5279
- } else {
5280
- // Exception should never occur, but check anyway and log it
5281
- console.log('WARNING: Dataset "' + ds.name +
5282
- `" has no default selector "${ds.default_selector}"`);
5283
- }
5284
- }
5372
+ obj = ds.activeModifierExpression;
5285
5373
  }
5286
5374
  // By default, use the dataset default value
5287
5375
  let v = ds.defaultValue,
@@ -5291,7 +5379,7 @@ function VMI_push_dataset_modifier(x, args) {
5291
5379
  // Object is a vector
5292
5380
  if(t >= 0 && t < obj.length) {
5293
5381
  v = obj[t];
5294
- } else if(ds.array) {
5382
+ } else if(ds.array && t >= obj.length) {
5295
5383
  // Set error value if array index is out of bounds
5296
5384
  v = VM.ARRAY_INDEX;
5297
5385
  VM.out_of_bounds_array = ds.displayName;
@@ -5316,8 +5404,10 @@ function VMI_push_dataset_modifier(x, args) {
5316
5404
  tot[1] + (tot[2] ? ':' + tot[2] : ''), ' value = ', VM.sig4Dig(v));
5317
5405
  console.log(' --', x.text, ' for owner ', x.object.displayName, x.attribute);
5318
5406
  }
5319
- // NOTE: unless error, push default value if exceptional ("undefined", etc.)
5320
- x.push(v < VM.PLUS_INFINITY ? v : ds.defaultValue);
5407
+ // NOTE: if value is exceptional ("undefined", etc.), use default value
5408
+ if(v >= VM.PLUS_INFINITY) v = ds.defaultValue;
5409
+ // Finally, push the value onto the expression stack
5410
+ x.push(v);
5321
5411
  }
5322
5412
 
5323
5413
 
@@ -6964,12 +7054,13 @@ const
6964
7054
  // Valid symbols in expressions
6965
7055
  PARENTHESES = '()',
6966
7056
  OPERATOR_CHARS = ';?:+-*/%=!<>^|',
6967
- SEPARATOR_CHARS = PARENTHESES + OPERATOR_CHARS + '[ ',
7057
+ // Opening bracket, space and single quote indicate a separation
7058
+ SEPARATOR_CHARS = PARENTHESES + OPERATOR_CHARS + "[ '",
6968
7059
  COMPOUND_OPERATORS = ['!=', '<>', '>=', '<='],
6969
7060
  CONSTANT_SYMBOLS = [
6970
7061
  't', 'rt', 'bt', 'b', 'N', 'n', 'l', 'r', 'lr', 'nr', 'x', 'nx',
6971
7062
  'random', 'dt', 'true', 'false', 'pi', 'infinity', '#',
6972
- 'yr', 'wk', 'd', 'h', 'm', 's'],
7063
+ 'i', 'j', 'k', 'yr', 'wk', 'd', 'h', 'm', 's'],
6973
7064
  CONSTANT_CODES = [
6974
7065
  VMI_push_time_step, VMI_push_relative_time, VMI_push_block_time,
6975
7066
  VMI_push_block_number, VMI_push_run_length, VMI_push_block_length,
@@ -6977,9 +7068,10 @@ const
6977
7068
  VMI_push_number_of_rounds, VMI_push_run_number, VMI_push_number_of_runs,
6978
7069
  VMI_push_random, VMI_push_delta_t, VMI_push_true, VMI_push_false,
6979
7070
  VMI_push_pi, VMI_push_infinity, VMI_push_selector_wildcard,
7071
+ VMI_push_i, VMI_push_j, VMI_push_k,
6980
7072
  VMI_push_year, VMI_push_week, VMI_push_day, VMI_push_hour,
6981
7073
  VMI_push_minute, VMI_push_second],
6982
- DYNAMIC_SYMBOLS = ['t', 'rt', 'bt', 'b', 'r', 'random'],
7074
+ DYNAMIC_SYMBOLS = ['t', 'rt', 'bt', 'b', 'r', 'random', 'i', 'j', 'k'],
6983
7075
  MONADIC_OPERATORS = [
6984
7076
  '~', 'not', 'abs', 'sin', 'cos', 'atan', 'ln',
6985
7077
  'exp', 'sqrt', 'round', 'int', 'fract', 'min', 'max',
@@ -7028,7 +7120,7 @@ const
7028
7120
  // The first custom operator in this section demonstrates by example how custom
7029
7121
  // operators can be added.
7030
7122
 
7031
- // Custom operators should preferably have a short alphanumerical string as
7123
+ // Custom operators should preferably have a short alphanumeric string as
7032
7124
  // their identifying symbol. Custom operators are monadic and reducing, i.e.,
7033
7125
  // they must have a grouping as operand. The number of required arguments must
7034
7126
  // be checked at run time by the VM instruction for this operator.
@@ -7064,14 +7156,14 @@ function VMI_profitable_units(x, empty) {
7064
7156
  // the time horizon (by default the length of the simulation period)
7065
7157
  nt = (d.length > 5 ? d[5] : MODEL.end_period - MODEL.start_period + 1);
7066
7158
  // Handle exceptional values of `uc` and `mc`
7067
- if(uc >= VM.BEYOND_PLUS_INFINITY || mc >= VM.BEYOND_PLUS_INFINITY) {
7068
- x.retop(Math.max(uc, mc));
7069
- return;
7070
- }
7071
7159
  if(uc <= VM.BEYOND_MINUS_INFINITY || mc <= VM.BEYOND_MINUS_INFINITY) {
7072
7160
  x.retop(Math.min(uc, mc));
7073
7161
  return;
7074
7162
  }
7163
+ if(uc >= VM.BEYOND_PLUS_INFINITY || mc >= VM.BEYOND_PLUS_INFINITY) {
7164
+ x.retop(Math.max(uc, mc));
7165
+ return;
7166
+ }
7075
7167
 
7076
7168
  // NOTE: NPU is not time-dependent => result is stored in cache
7077
7169
  // As expressions may contain several NPU operators, create a unique key
@@ -7169,6 +7261,145 @@ DYNAMIC_SYMBOLS.push('npu');
7169
7261
  LEVEL_BASED_CODES.push(VMI_profitable_units);
7170
7262
 
7171
7263
 
7264
+ function VMI_highest_cumulative_consecutive_deviation(x, empty) {
7265
+ // Replaces the argument list that should be at the top of the stack by
7266
+ // the HCCD (as in the function name) of the vector V that is passed as
7267
+ // the first argument of this function. The HCCD value is computed by
7268
+ // first iterating over the vector to obtain a new vector A that
7269
+ // aggregates its values by blocks of B numbers of the original vector,
7270
+ // while computing the mean value M. Then it iterates over A to compute
7271
+ // the HCCD: the sum of deviations d = a[i] - M for consecutive i
7272
+ // until the sign of d changes. Then the HCCD (which is initially 0)
7273
+ // is udated to max(HCCD, |sum|). The eventual HCCD can be used as
7274
+ // estimator for the storage capacity required for a stock having a
7275
+ // net inflow as specified by the vector.
7276
+ // The function takes up to 4 elements: the vector V, the block length
7277
+ // B (defaults to 1), the index where to start (defaults to 1) and the
7278
+ // index where to end (defaults to the length of V)
7279
+ const
7280
+ d = x.top(),
7281
+ vmi = 'Highest Cumulative Consecutive Deviation';
7282
+ // Check whether the top stack element is a grouping of the correct size
7283
+ if(d instanceof Array && d.length >= 1 &&
7284
+ typeof d[0] === 'object' && d[0].hasOwnProperty('entity')) {
7285
+ const
7286
+ e = d[0].entity,
7287
+ a = d[0].attribute;
7288
+ let vector = e.attributeValue(a);
7289
+ // NOTE: equations can also be passed by reference
7290
+ if(e === MODEL.equations_dataset) {
7291
+ const x = e.modifiers[a].expression;
7292
+ // NOTE: an expression may not have been (fully) computed yet
7293
+ x.compute();
7294
+ if(!x.isStatic) {
7295
+ const nt = MODEL.end_period - MODEL.start_period + 1;
7296
+ for(let t = 1; t <= nt; t++) x.result(t);
7297
+ }
7298
+ vector = x.vector;
7299
+ }
7300
+ if(Array.isArray(vector) &&
7301
+ // Check that other arguments are numbers
7302
+ (d.length === 1 || (typeof d[1] === 'number' &&
7303
+ (d.length === 2 || typeof d[2] === 'number' &&
7304
+ (d.length === 3 || typeof d[3] === 'number'))))) {
7305
+ // Valid parameters => get the data required for computation
7306
+ const
7307
+ name = e.displayName + (a ? '|' + a : ''),
7308
+ block_size = d[1] || 1,
7309
+ first = d[2] || 1,
7310
+ last = d[3] || vector.length - 1,
7311
+ // Handle exceptional values of the parameters
7312
+ low = Math.min(block_size, first, last),
7313
+ high = Math.min(block_size, first, last);
7314
+ if(low <= VM.BEYOND_MINUS_INFINITY) {
7315
+ x.retop(low);
7316
+ return;
7317
+ }
7318
+ if(high >= VM.BEYOND_PLUS_INFINITY) {
7319
+ x.retop(high);
7320
+ return;
7321
+ }
7322
+
7323
+ // NOTE: HCCD is not time-dependent => result is stored in cache
7324
+ // As expressions may contain several HCCD operators, create a unique key
7325
+ // based on its parameters
7326
+ const cache_key = ['hccd', e.identifier, a, block_size, first, last].join('_');
7327
+ if(x.cache[cache_key]) {
7328
+ x.retop(x.cache[cache_key]);
7329
+ return;
7330
+ }
7331
+
7332
+ if(DEBUGGING) console.log(`*${vmi} for ${name}`);
7333
+
7334
+ // Compute the aggregated vector and sum
7335
+ let sum = 0,
7336
+ b = 0,
7337
+ n = 0,
7338
+ av = [];
7339
+ for(let i = first; i <= last; i++) {
7340
+ const v = vector[i];
7341
+ // Handle exceptional values in vector
7342
+ if(v <= VM.BEYOND_MINUS_INFINITY || v >= VM.BEYOND_PLUS_INFINITY) {
7343
+ x.retop(v);
7344
+ return;
7345
+ }
7346
+ sum += v;
7347
+ b += v;
7348
+ if(n++ === block_size) {
7349
+ av.push(b);
7350
+ n = 0;
7351
+ b = 0;
7352
+ }
7353
+ }
7354
+ // Always push the remaining block sum, even if it is 0
7355
+ av.push(b);
7356
+ // Compute the mean (per block)
7357
+ const mean = sum / av.length;
7358
+ let hccd = 0,
7359
+ positive = av[0] > mean;
7360
+ sum = 0;
7361
+ // Iterate over the aggregated vector
7362
+ for(let i = 0; i < av.length; i++) {
7363
+ const v = av[i];
7364
+ if((positive && v < mean) || (!positive && v > mean)) {
7365
+ hccd = Math.max(hccd, Math.abs(sum));
7366
+ sum = v;
7367
+ positive = !positive;
7368
+ } else {
7369
+ // No sign change => add deviation
7370
+ sum += v;
7371
+ }
7372
+ }
7373
+ hccd = Math.max(hccd, Math.abs(sum));
7374
+ // Store the result in the expression's cache
7375
+ x.cache[cache_key] = hccd;
7376
+ // Push the result onto the stack
7377
+ x.retop(hccd);
7378
+ return;
7379
+ }
7380
+ }
7381
+ // Fall-trough indicates error
7382
+ if(DEBUGGING) console.log(vmi + ': invalid parameter(s)\n', d);
7383
+ x.retop(VM.PARAMS);
7384
+ }
7385
+
7386
+ // Add the custom operator instruction to the global lists
7387
+ // NOTE: All custom operators are monadic (priority 9) and reducing
7388
+ OPERATORS.push('hccd');
7389
+ MONADIC_OPERATORS.push('hccd');
7390
+ ACTUAL_SYMBOLS.push('hccd');
7391
+ OPERATOR_CODES.push(VMI_highest_cumulative_consecutive_deviation);
7392
+ MONADIC_CODES.push(VMI_highest_cumulative_consecutive_deviation);
7393
+ REDUCING_CODES.push(VMI_highest_cumulative_consecutive_deviation);
7394
+ SYMBOL_CODES.push(VMI_highest_cumulative_consecutive_deviation);
7395
+ PRIORITIES.push(9);
7396
+ // Add to this list only if operation makes an expression dynamic
7397
+ DYNAMIC_SYMBOLS.push('hccd');
7398
+ // Add to this list only if operation makes an expression random
7399
+ // RANDOM_CODES.push(VMI_...);
7400
+ // Add to this list only if operation makes an expression level-based
7401
+ // LEVEL_BASED_CODES.push(VMI_...);
7402
+
7172
7403
  /*** END of custom operator API section ***/
7173
7404
 
7174
7405
  ///////////////////////////////////////////////////////////////////////