linny-r 1.1.23 → 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
 
@@ -525,10 +526,11 @@ class ExpressionParser {
525
526
  this.log('dynamic because of offset');
526
527
  // String contains at least one @ character, then split at the last (pop)
527
528
  // and check that @ sign is followed by an offset (range if `:`)
529
+ // NOTE: offset anchors are case-insensitive
528
530
  const offs = s.pop().replace(/\s+/g, '').toLowerCase().split(':');
529
531
  // Re-assemble the other substrings, as name itself may contain @ signs
530
532
  name = s.join('@').trim();
531
- const re = /(^[\+\-]?[0-9]+|[\#cfinprst]([\+\-][0-9]+)?)$/;
533
+ const re = /(^[\+\-]?[0-9]+|[\#cfijklnprst]([\+\-][0-9]+)?)$/;
532
534
  if(!re.test(offs[0])) {
533
535
  msg = `Invalid offset "${offs[0]}"`;
534
536
  } else if(offs.length > 1 && !re.test(offs[1])) {
@@ -538,21 +540,22 @@ class ExpressionParser {
538
540
  // Anchor may be:
539
541
  // # (absolute index in vector)
540
542
  // c (start of current block)
541
- // f (final: last value of the vector)
542
- // 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)
543
546
  // n (start of next block)
544
547
  // p (start of previous block)
545
548
  // r (relative: relative time step, i.e., t0 = 1)
546
549
  // s (scaled: time step 0, but offset is scaled to time unit of run)
547
550
  // t (current time step, this is the default),
548
- if('#cfinprst'.includes(offs[0].charAt(0))) {
551
+ if('#cfijklnprst'.includes(offs[0].charAt(0))) {
549
552
  anchor1 = offs[0].charAt(0);
550
553
  offset1 = safeStrToInt(offs[0].substr(1));
551
554
  } else {
552
555
  offset1 = safeStrToInt(offs[0]);
553
556
  }
554
557
  if(offs.length > 1) {
555
- if('#cfinprst'.includes(offs[1].charAt(0))) {
558
+ if('#cfijklnprst'.includes(offs[1].charAt(0))) {
556
559
  anchor2 = offs[1].charAt(0);
557
560
  offset2 = safeStrToInt(offs[1].substr(1));
558
561
  } else {
@@ -722,12 +725,12 @@ class ExpressionParser {
722
725
  // a result, so what follows does not apply to experiment results
723
726
  //
724
727
 
725
- // Attribute name (optional) follows object-attribute separator
728
+ // Attribute name (optional) follows object-attribute separator |
726
729
  s = name.split(UI.OA_SEPARATOR);
727
730
  if(s.length > 1) {
728
- // Attribute is string after LAST vertical bar ...
731
+ // Attribute is string after LAST separator ...
729
732
  attr = s.pop().trim();
730
- // ... so restore name if itself contains other vertical bars
733
+ // ... so restore name if itself contains other separators
731
734
  name = s.join(UI.OA_SEPARATOR).trim();
732
735
  if(!attr) {
733
736
  // Explicit *empty* attribute, e.g., [name|]
@@ -807,6 +810,18 @@ class ExpressionParser {
807
810
  }
808
811
  }
809
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
+ }
810
825
  if(list.length > 0) {
811
826
  // NOTE: statistic MAY make expression level-based
812
827
  // NOTE: assume NOT when offset has been specified, as this suggests
@@ -877,7 +892,8 @@ class ExpressionParser {
877
892
  'Equation' : 'Dataset modifier expression') +
878
893
  ' must not reference itself';
879
894
  } else if(obj.array &&
880
- (anchor1 && anchor1 !== '#' || anchor2 && anchor2 !== '#')) {
895
+ (anchor1 && '#ijk'.indexOf(anchor1) < 0 ||
896
+ anchor2 && '#ijk'.indexOf(anchor2) < 0)) {
881
897
  msg = 'Invalid anchor(s) for array-type dataset ' + obj.displayName;
882
898
  } else {
883
899
  // NOTE: except for array-type datasets, the default anchor is 't';
@@ -892,6 +908,10 @@ class ExpressionParser {
892
908
  return [{r: obj, a: attr}, anchor1, offset1, anchor2, offset2];
893
909
  }
894
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
895
915
  args = obj.vector;
896
916
  } else if(attr === '') {
897
917
  // For all other variables, assume default attribute
@@ -902,8 +922,6 @@ class ExpressionParser {
902
922
  // "use the data"
903
923
  if(obj instanceof Dataset &&
904
924
  (obj.array || (!use_data && obj.selectorList.length > 0))) {
905
- // NOTE: also pass the "use data" flag so that experiment selectors
906
- // will be ignored if the modeler coded the vertical bar
907
925
  if(obj.data.length > 1 || obj.data.length > 0 && !obj.periodic ||
908
926
  !obj.allModifiersAreStatic) {
909
927
  // No explicit selector => dynamic unless no time series data, and
@@ -911,6 +929,8 @@ class ExpressionParser {
911
929
  this.is_static = false;
912
930
  this.log('dynamic because dataset without explicit selector is used');
913
931
  }
932
+ // NOTE: also pass the "use data" flag so that experiment selectors
933
+ // will be ignored if the modeler coded the vertical bar
914
934
  return [{d: obj, ud: use_data}, anchor1, offset1, anchor2, offset2];
915
935
  }
916
936
  } else if(obj instanceof Dataset) {
@@ -1004,10 +1024,10 @@ class ExpressionParser {
1004
1024
  c = this.expr.charAt(this.pit);
1005
1025
  if(c === '[') {
1006
1026
  // Left bracket denotes start of a variable name
1007
- // NOTE: As variable names may contain regular expressions, they may
1008
- // also contain brackets => allow *matched* [...] pairs inside
1009
1027
  i = indexOfMatchingBracket(this.expr, this.pit);
1010
1028
  if(i < 0) {
1029
+ this.pit++;
1030
+ this.los = 1;
1011
1031
  this.error = 'Missing closing bracket \']\'';
1012
1032
  } else {
1013
1033
  v = this.expr.substr(this.pit + 1, i - 1 - this.pit);
@@ -1019,6 +1039,27 @@ class ExpressionParser {
1019
1039
  this.sym = this.parseVariable(v);
1020
1040
  // NOTE: parseVariable may set is_static to FALSE
1021
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
+ }
1022
1063
  } else if(c === '(' || c === ')') {
1023
1064
  this.sym = c;
1024
1065
  this.los = 1;
@@ -1038,7 +1079,7 @@ class ExpressionParser {
1038
1079
  this.sym = OPERATOR_CODES[OPERATORS.indexOf(c)];
1039
1080
  } else {
1040
1081
  // Take any text up to the next operator, parenthesis,
1041
- // opening bracket, or space
1082
+ // opening bracket, quote or space
1042
1083
  this.los = 0;
1043
1084
  let pl = this.pit + this.los,
1044
1085
  cpl = this.expr.charAt(pl),
@@ -1089,12 +1130,15 @@ class ExpressionParser {
1089
1130
  // If a valid number, keep it within the +/- infinity range
1090
1131
  this.sym = Math.max(VM.MINUS_INFINITY, Math.min(VM.PLUS_INFINITY, f));
1091
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;
1092
1136
  } else {
1093
1137
  // Symbol does not start with a digit
1094
1138
  // NOTE: distinguish between run length N and block length n
1095
1139
  i = ACTUAL_SYMBOLS.indexOf(l === 'n' ? v : l);
1096
1140
  if(i < 0) {
1097
- this.error = `Invalid symbol "${l}"`;
1141
+ this.error = `Invalid symbol "${v}"`;
1098
1142
  } else {
1099
1143
  this.sym = SYMBOL_CODES[i];
1100
1144
  // NOTE: Using time symbols or `random` makes the expression dynamic!
@@ -1376,8 +1420,12 @@ class VirtualMachine {
1376
1420
  this.chunk_variables = [];
1377
1421
  // Array for VM instructions
1378
1422
  this.code = [];
1379
- // Array to hold lines of (solver-dependent) model equations
1380
- 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 = '';
1381
1429
  // String specifying a numeric issue (empty if none)
1382
1430
  this.numeric_issue = '';
1383
1431
  // The call stack tracks evaluation of "nested" expression variables
@@ -3987,6 +4035,15 @@ class VirtualMachine {
3987
4035
  setTimeout((n) => VM.initializeTableau(n), 0, abl);
3988
4036
  }
3989
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
+
3990
4047
  initializeTableau(abl) {
3991
4048
  // `offset` is used to calculate the actual column index for variables
3992
4049
  this.offset = 0;
@@ -4014,9 +4071,7 @@ class VirtualMachine {
4014
4071
  this.rhs = 0;
4015
4072
  // NOTE: the constraint coefficient matrix and the rhs and ct vectors
4016
4073
  // have equal length (#rows); the matrix is a list of sparse vectors
4017
- this.matrix = [];
4018
- this.right_hand_side = [];
4019
- this.constraint_types = [];
4074
+ this.resetTableau();
4020
4075
  // NOTE: setupBlock only works properly if setupProblem was successful
4021
4076
  // Every variable gets one column per time step => tableau is organized
4022
4077
  // in segments per time step, where each segment has `cols` columns
@@ -4198,9 +4253,8 @@ class VirtualMachine {
4198
4253
  // shorter than the standard, as it should not go beyond the end time
4199
4254
  const abl = this.actualBlockLength;
4200
4255
  this.numeric_issue = '';
4201
- this.lines.length = 0;
4202
4256
  // First add the objective (always MAXimize)
4203
- this.lines.push('/* Objective function */\nmax:');
4257
+ this.lines = '/* Objective function */\nmax:\n';
4204
4258
  let c,
4205
4259
  p,
4206
4260
  line = '';
@@ -4236,14 +4290,14 @@ class VirtualMachine {
4236
4290
  }
4237
4291
  // Keep lines under approx. 110 chars
4238
4292
  if(line.length >= 100) {
4239
- this.lines.push(line);
4293
+ this.lines += line + '\n';
4240
4294
  line = '';
4241
4295
  }
4242
4296
  }
4243
- this.lines.push(line + ';');
4297
+ this.lines += line + ';\n';
4244
4298
  line = '';
4245
4299
  // Add the row constraints
4246
- this.lines.push('\n/* Constraints */');
4300
+ this.lines += '\n/* Constraints */\n';
4247
4301
  n = this.matrix.length;
4248
4302
  for(let r = 0; r < n; r++) {
4249
4303
  const row = this.matrix[r];
@@ -4264,17 +4318,17 @@ class VirtualMachine {
4264
4318
  }
4265
4319
  // Keep lines under approx. 80 chars
4266
4320
  if(line.length >= 100) {
4267
- this.lines.push(line);
4321
+ this.lines += line + '\n';
4268
4322
  line = '';
4269
4323
  }
4270
4324
  }
4271
4325
  c = this.right_hand_side[r];
4272
- this.lines.push(line + ' ' +
4273
- this.constraint_symbols[this.constraint_types[r]] + ' ' + c + ';');
4326
+ this.lines += line + ' ' +
4327
+ this.constraint_symbols[this.constraint_types[r]] + ' ' + c + ';\n';
4274
4328
  line = '';
4275
4329
  }
4276
4330
  // Add the variable bounds
4277
- this.lines.push('\n/* Variable bounds */');
4331
+ this.lines += '\n/* Variable bounds */\n';
4278
4332
  n = abl * this.cols;
4279
4333
  for(p = 1; p <= n; p++) {
4280
4334
  let lb = null,
@@ -4303,25 +4357,25 @@ class VirtualMachine {
4303
4357
  if(lb !== null && lb !== 0) line = lb + ' <= ' + line;
4304
4358
  if(ub !== null) line += ' <= ' + ub;
4305
4359
  }
4306
- if(line) this.lines.push(line + ';');
4360
+ if(line) this.lines += line + ';\n';
4307
4361
  }
4308
4362
  // Add the special variable types
4309
4363
  const v_set = [];
4310
4364
  // NOTE: for binary variables, add the constraint <= 1
4311
4365
  for(let i in this.is_binary) if(Number(i)) {
4312
- this.lines.push('C' + i + ' <= 1;');
4366
+ this.lines += 'C' + i + ' <= 1;\n';
4313
4367
  v_set.push('C' + i);
4314
4368
  }
4315
4369
  for(let i in this.is_integer) if(Number(i)) v_set.push('C' + i);
4316
- 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';
4317
4371
  // Clear the INT variable list
4318
4372
  v_set.length = 0;
4319
4373
  // Add the semi-continuous variables
4320
4374
  for(let i in this.is_semi_continuous) if(Number(i)) v_set.push('C' + i);
4321
- 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';
4322
4376
  // Add the SOS section
4323
4377
  if(this.sos_var_indices.length > 0) {
4324
- this.lines.push('sos');
4378
+ this.lines += 'sos\n';
4325
4379
  let sos = 1;
4326
4380
  for(let j = 0; j < abl; j++) {
4327
4381
  for(let i = 0; i < this.sos_var_indices.length; i++) {
@@ -4332,7 +4386,7 @@ class VirtualMachine {
4332
4386
  v_set.push('C' + vi);
4333
4387
  vi++;
4334
4388
  }
4335
- this.lines.push(`SOS${sos}: ${v_set.join(',')} <= 2;`);
4389
+ this.lines += `SOS${sos}: ${v_set.join(',')} <= 2;\n`;
4336
4390
  sos++;
4337
4391
  }
4338
4392
  }
@@ -4367,19 +4421,18 @@ class VirtualMachine {
4367
4421
  p,
4368
4422
  r;
4369
4423
  this.numeric_issue = '';
4370
- this.lines.length = 0;
4424
+ this.lines = '';
4371
4425
  for(c = 1; c <= ncol; c++) cols.push([]);
4372
4426
  this.decimals = Math.max(nrow, ncol).toString().length;
4373
- this.lines.push('NAME block-' + this.blockWithRound);
4374
- this.lines.push('ROWS');
4427
+ this.lines += 'NAME block-' + this.blockWithRound + '\nROWS\n';
4375
4428
  // Start with the "free" row that will be the objective function
4376
- this.lines.push(' N OBJ');
4429
+ this.lines += ' N OBJ\n';
4377
4430
  for(r = 0; r < nrow; r++) {
4378
4431
  const
4379
4432
  row = this.matrix[r],
4380
4433
  row_lbl = 'R' + (r + 1).toString().padStart(this.decimals, '0');
4381
- this.lines.push(' ' + this.constraint_letters[this.constraint_types[r]] +
4382
- ' ' + row_lbl);
4434
+ this.lines += ' ' + this.constraint_letters[this.constraint_types[r]] +
4435
+ ' ' + row_lbl + '\n';
4383
4436
  for(p in row) if (row.hasOwnProperty(p)) {
4384
4437
  c = row[p];
4385
4438
  // Check for numeric issues
@@ -4420,17 +4473,18 @@ class VirtualMachine {
4420
4473
  return;
4421
4474
  }
4422
4475
  // Add the columns section
4423
- this.lines.push('COLUMNS');
4476
+ this.lines += 'COLUMNS\n';
4424
4477
  for(c = 1; c <= ncol; c++) {
4425
4478
  const col_lbl = ' X' + c.toString().padStart(this.decimals, '0') + ' ';
4426
- this.lines.push(col_lbl + cols[c].join('\n' + col_lbl));
4479
+ this.lines += col_lbl + cols[c].join('\n' + col_lbl) + '\n';
4427
4480
  }
4481
+ // Free up memory
4428
4482
  cols.length = 0;
4429
4483
  // Add the RHS section
4430
- this.lines.push('RHS\n' + rhs.join('\n'));
4484
+ this.lines += 'RHS\n' + rhs.join('\n') + '\n';
4431
4485
  rhs.length = 0;
4432
4486
  // Add the BOUNDS section
4433
- this.lines.push('BOUNDS');
4487
+ this.lines += 'BOUNDS\n';
4434
4488
  // NOTE: start at column number 1 (not 0)
4435
4489
  setTimeout((c, n) => VM.showMPSProgress(c, n), 0, 1, ncol);
4436
4490
  }
@@ -4489,12 +4543,12 @@ class VirtualMachine {
4489
4543
  */
4490
4544
  semic = p in this.is_semi_continuous;
4491
4545
  if(p in this.is_binary) {
4492
- this.lines.push(' BV' + bnd);
4546
+ this.lines += ' BV' + bnd + '\n';
4493
4547
  } else if(lb !== null && ub !== null && lb <= VM.SOLVER_MINUS_INFINITY &&
4494
4548
  ub >= VM.SOLVER_PLUS_INFINITY) {
4495
- this.lines.push(' FR' + bnd);
4549
+ this.lines += ' FR' + bnd + '\n';
4496
4550
  } else if(lb !== null && lb === ub && !semic) {
4497
- this.lines.push(' FX' + bnd + lb);
4551
+ this.lines += ' FX' + bnd + lb + '\n';
4498
4552
  } else {
4499
4553
  // Assume "standard" bounds
4500
4554
  lbc = ' LO';
@@ -4509,10 +4563,10 @@ class VirtualMachine {
4509
4563
  }
4510
4564
  // NOTE: by default, lower bound of variables is 0
4511
4565
  if(lb !== null && lb !== 0 || lbc !== ' LO') {
4512
- this.lines.push(lbc + bnd + lb);
4566
+ this.lines += lbc + bnd + lb + '\n';
4513
4567
  }
4514
4568
  if(ub !== null) {
4515
- this.lines.push(ubc + bnd + ub);
4569
+ this.lines += ubc + bnd + ub + '\n';
4516
4570
  }
4517
4571
  }
4518
4572
  }
@@ -4530,18 +4584,18 @@ class VirtualMachine {
4530
4584
  this.hideSetUpOrWriteProgress();
4531
4585
  // Add the SOS section
4532
4586
  if(this.sos_var_indices.length > 0) {
4533
- this.lines.push('SOS');
4587
+ this.lines += 'SOS\n';
4534
4588
  const abl = this.actualBlockLength;
4535
4589
  let sos = 1;
4536
4590
  for(let j = 0; j < abl; j++) {
4537
4591
  for(let i = 0; i < this.sos_var_indices.length; i++) {
4538
- this.lines.push(' S2 sos' + sos);
4592
+ this.lines += ' S2 sos' + sos + '\n';
4539
4593
  let vi = this.sos_var_indices[i][0] + j * this.cols;
4540
4594
  const n = this.sos_var_indices[i][1];
4541
4595
  for(let j = 1; j <= n; j++) {
4542
4596
  const s = ' X' + vi.toString().padStart(this.decimals, '0') +
4543
4597
  ' ';
4544
- this.lines.push(s.substring(0, 15) + j);
4598
+ this.lines += s.substring(0, 15) + j + '\n';
4545
4599
  vi++;
4546
4600
  }
4547
4601
  sos++;
@@ -4549,7 +4603,7 @@ class VirtualMachine {
4549
4603
  }
4550
4604
  }
4551
4605
  // Add the end-of-model marker
4552
- this.lines.push('ENDATA');
4606
+ this.lines += 'ENDATA';
4553
4607
  setTimeout(() => VM.submitFile(), 0);
4554
4608
  }
4555
4609
 
@@ -4725,6 +4779,9 @@ Solver status = ${json.status}`);
4725
4779
  }
4726
4780
 
4727
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();
4728
4785
  if(this.numeric_issue) {
4729
4786
  const msg = 'Invalid ' + this.numeric_issue;
4730
4787
  this.logMessage(this.block_count, msg);
@@ -4733,13 +4790,11 @@ Solver status = ${json.status}`);
4733
4790
  } else {
4734
4791
  // Log the time it took to create the code lines
4735
4792
  this.logMessage(this.block_count,
4736
- `Model file creation (${this.lines.length} lines) took ` +
4737
- this.elapsedTime + ' seconds.');
4738
- // Concatenate code lines to POST as a single data string
4739
- const ccl = this.lines.join('\n');
4740
- // Immediately free the constituent lines
4741
- this.lines.length = 0;
4742
- 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();
4743
4798
  // Now the round number can be increased...
4744
4799
  this.current_round++;
4745
4800
  // ... and also the blocknumber if all rounds have been played
@@ -4973,6 +5028,42 @@ function VMI_push_infinity(x, empty) {
4973
5028
  x.push(VM.PLUS_INFINITY);
4974
5029
  }
4975
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
+
4976
5067
  function pushTimeStepsPerTimeUnit(x, unit) {
4977
5068
  // AUXILIARY FUNCTION for the VMI_push_(time unit) instructions
4978
5069
  // Pushes the number of model time steps represented by 1 unit
@@ -5108,14 +5199,18 @@ function relativeTimeStep(t, anchor, offset, dtm, x) {
5108
5199
  // Relative to start of next optimization block
5109
5200
  return VM.block_start + MODEL.block_length + offset;
5110
5201
  }
5111
- if(anchor === 'f') {
5112
- // 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
5113
5204
  return MODEL.end_period - MODEL.start_period + 1 + offset;
5114
5205
  }
5115
5206
  if(anchor === 's') {
5116
5207
  // Scaled: offset is scaled to time unit of run
5117
5208
  return Math.floor(offset * dtm);
5118
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
+ }
5119
5214
  if(anchor === '#') {
5120
5215
  // Index: offset is added to the inferred value of the # symbol
5121
5216
  return valueOfNumberSign(x) + offset;
@@ -5131,6 +5226,7 @@ function relativeTimeStep(t, anchor, offset, dtm, x) {
5131
5226
  return valueOfNumberSign(x) + offset;
5132
5227
  }
5133
5228
  // Fall-through: offset relative to the initial value index (0)
5229
+ // NOTE: this also applies to anchor f (First)
5134
5230
  return offset;
5135
5231
  }
5136
5232
 
@@ -5273,23 +5369,7 @@ function VMI_push_dataset_modifier(x, args) {
5273
5369
  // If modifier selector is specified, use the associated expression
5274
5370
  obj = mx;
5275
5371
  } else if(!ud) {
5276
- if(MODEL.running_experiment) {
5277
- // If an experiment is running, check if dataset modifiers match the
5278
- // combination of selectors for the active run
5279
- const mm = ds.matchingModifiers(MODEL.running_experiment.activeCombination);
5280
- // If so, use the first match
5281
- if(mm.length > 0) obj = mm[0].expression;
5282
- } else if(ds.default_selector) {
5283
- // If no expriment (so "normal" run), use default selector if specified
5284
- const dm = ds.modifiers[ds.default_selector];
5285
- if(dm) {
5286
- obj = dm.expression;
5287
- } else {
5288
- // Exception should never occur, but check anyway and log it
5289
- console.log('WARNING: Dataset "' + ds.name +
5290
- `" has no default selector "${ds.default_selector}"`);
5291
- }
5292
- }
5372
+ obj = ds.activeModifierExpression;
5293
5373
  }
5294
5374
  // By default, use the dataset default value
5295
5375
  let v = ds.defaultValue,
@@ -5299,7 +5379,7 @@ function VMI_push_dataset_modifier(x, args) {
5299
5379
  // Object is a vector
5300
5380
  if(t >= 0 && t < obj.length) {
5301
5381
  v = obj[t];
5302
- } else if(ds.array) {
5382
+ } else if(ds.array && t >= obj.length) {
5303
5383
  // Set error value if array index is out of bounds
5304
5384
  v = VM.ARRAY_INDEX;
5305
5385
  VM.out_of_bounds_array = ds.displayName;
@@ -6974,12 +7054,13 @@ const
6974
7054
  // Valid symbols in expressions
6975
7055
  PARENTHESES = '()',
6976
7056
  OPERATOR_CHARS = ';?:+-*/%=!<>^|',
6977
- SEPARATOR_CHARS = PARENTHESES + OPERATOR_CHARS + '[ ',
7057
+ // Opening bracket, space and single quote indicate a separation
7058
+ SEPARATOR_CHARS = PARENTHESES + OPERATOR_CHARS + "[ '",
6978
7059
  COMPOUND_OPERATORS = ['!=', '<>', '>=', '<='],
6979
7060
  CONSTANT_SYMBOLS = [
6980
7061
  't', 'rt', 'bt', 'b', 'N', 'n', 'l', 'r', 'lr', 'nr', 'x', 'nx',
6981
7062
  'random', 'dt', 'true', 'false', 'pi', 'infinity', '#',
6982
- 'yr', 'wk', 'd', 'h', 'm', 's'],
7063
+ 'i', 'j', 'k', 'yr', 'wk', 'd', 'h', 'm', 's'],
6983
7064
  CONSTANT_CODES = [
6984
7065
  VMI_push_time_step, VMI_push_relative_time, VMI_push_block_time,
6985
7066
  VMI_push_block_number, VMI_push_run_length, VMI_push_block_length,
@@ -6987,9 +7068,10 @@ const
6987
7068
  VMI_push_number_of_rounds, VMI_push_run_number, VMI_push_number_of_runs,
6988
7069
  VMI_push_random, VMI_push_delta_t, VMI_push_true, VMI_push_false,
6989
7070
  VMI_push_pi, VMI_push_infinity, VMI_push_selector_wildcard,
7071
+ VMI_push_i, VMI_push_j, VMI_push_k,
6990
7072
  VMI_push_year, VMI_push_week, VMI_push_day, VMI_push_hour,
6991
7073
  VMI_push_minute, VMI_push_second],
6992
- DYNAMIC_SYMBOLS = ['t', 'rt', 'bt', 'b', 'r', 'random'],
7074
+ DYNAMIC_SYMBOLS = ['t', 'rt', 'bt', 'b', 'r', 'random', 'i', 'j', 'k'],
6993
7075
  MONADIC_OPERATORS = [
6994
7076
  '~', 'not', 'abs', 'sin', 'cos', 'atan', 'ln',
6995
7077
  'exp', 'sqrt', 'round', 'int', 'fract', 'min', 'max',
@@ -7038,7 +7120,7 @@ const
7038
7120
  // The first custom operator in this section demonstrates by example how custom
7039
7121
  // operators can be added.
7040
7122
 
7041
- // Custom operators should preferably have a short alphanumerical string as
7123
+ // Custom operators should preferably have a short alphanumeric string as
7042
7124
  // their identifying symbol. Custom operators are monadic and reducing, i.e.,
7043
7125
  // they must have a grouping as operand. The number of required arguments must
7044
7126
  // be checked at run time by the VM instruction for this operator.
@@ -7074,14 +7156,14 @@ function VMI_profitable_units(x, empty) {
7074
7156
  // the time horizon (by default the length of the simulation period)
7075
7157
  nt = (d.length > 5 ? d[5] : MODEL.end_period - MODEL.start_period + 1);
7076
7158
  // Handle exceptional values of `uc` and `mc`
7077
- if(uc >= VM.BEYOND_PLUS_INFINITY || mc >= VM.BEYOND_PLUS_INFINITY) {
7078
- x.retop(Math.max(uc, mc));
7079
- return;
7080
- }
7081
7159
  if(uc <= VM.BEYOND_MINUS_INFINITY || mc <= VM.BEYOND_MINUS_INFINITY) {
7082
7160
  x.retop(Math.min(uc, mc));
7083
7161
  return;
7084
7162
  }
7163
+ if(uc >= VM.BEYOND_PLUS_INFINITY || mc >= VM.BEYOND_PLUS_INFINITY) {
7164
+ x.retop(Math.max(uc, mc));
7165
+ return;
7166
+ }
7085
7167
 
7086
7168
  // NOTE: NPU is not time-dependent => result is stored in cache
7087
7169
  // As expressions may contain several NPU operators, create a unique key
@@ -7179,6 +7261,145 @@ DYNAMIC_SYMBOLS.push('npu');
7179
7261
  LEVEL_BASED_CODES.push(VMI_profitable_units);
7180
7262
 
7181
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
+
7182
7403
  /*** END of custom operator API section ***/
7183
7404
 
7184
7405
  ///////////////////////////////////////////////////////////////////////