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.
- package/README.md +7 -6
- package/package.json +1 -1
- package/static/images/combination.png +0 -0
- package/static/images/iterator.png +0 -0
- package/static/images/scale.png +0 -0
- package/static/index.html +198 -13
- package/static/linny-r.css +214 -33
- package/static/scripts/linny-r-config.js +6 -0
- package/static/scripts/linny-r-ctrl.js +23 -7
- package/static/scripts/linny-r-gui.js +666 -111
- package/static/scripts/linny-r-model.js +873 -224
- package/static/scripts/linny-r-utils.js +5 -0
- package/static/scripts/linny-r-vm.js +310 -89
@@ -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]+|[\#
|
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 (
|
542
|
-
// i
|
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('#
|
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('#
|
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
|
731
|
+
// Attribute is string after LAST separator ...
|
729
732
|
attr = s.pop().trim();
|
730
|
-
// ... so restore name if itself contains other
|
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 &&
|
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 "${
|
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
|
-
//
|
1380
|
-
this.
|
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.
|
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
|
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
|
4293
|
+
this.lines += line + '\n';
|
4240
4294
|
line = '';
|
4241
4295
|
}
|
4242
4296
|
}
|
4243
|
-
this.lines
|
4297
|
+
this.lines += line + ';\n';
|
4244
4298
|
line = '';
|
4245
4299
|
// Add the row constraints
|
4246
|
-
this.lines
|
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
|
4321
|
+
this.lines += line + '\n';
|
4268
4322
|
line = '';
|
4269
4323
|
}
|
4270
4324
|
}
|
4271
4325
|
c = this.right_hand_side[r];
|
4272
|
-
this.lines
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
4484
|
+
this.lines += 'RHS\n' + rhs.join('\n') + '\n';
|
4431
4485
|
rhs.length = 0;
|
4432
4486
|
// Add the BOUNDS section
|
4433
|
-
this.lines
|
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
|
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
|
4549
|
+
this.lines += ' FR' + bnd + '\n';
|
4496
4550
|
} else if(lb !== null && lb === ub && !semic) {
|
4497
|
-
this.lines
|
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
|
4566
|
+
this.lines += lbc + bnd + lb + '\n';
|
4513
4567
|
}
|
4514
4568
|
if(ub !== null) {
|
4515
|
-
this.lines
|
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
|
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
|
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
|
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
|
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
|
-
|
4737
|
-
|
4738
|
-
//
|
4739
|
-
|
4740
|
-
|
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 === '
|
5112
|
-
//
|
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
|
-
|
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
|
-
|
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
|
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
|
///////////////////////////////////////////////////////////////////////
|