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.
- 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 +201 -16
- package/static/linny-r.css +214 -33
- package/static/scripts/linny-r-config.js +6 -0
- package/static/scripts/linny-r-ctrl.js +28 -8
- package/static/scripts/linny-r-gui.js +709 -113
- package/static/scripts/linny-r-model.js +943 -273
- package/static/scripts/linny-r-utils.js +5 -0
- package/static/scripts/linny-r-vm.js +324 -93
@@ -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]+|[\#
|
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 (
|
543
|
-
// 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)
|
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('#
|
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('#
|
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
|
731
|
+
// Attribute is string after LAST separator ...
|
730
732
|
attr = s.pop().trim();
|
731
|
-
// ... so restore name if itself contains other
|
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 &&
|
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 "${
|
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
|
-
//
|
1381
|
-
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 = '';
|
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.
|
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
|
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
|
4293
|
+
this.lines += line + '\n';
|
4241
4294
|
line = '';
|
4242
4295
|
}
|
4243
4296
|
}
|
4244
|
-
this.lines
|
4297
|
+
this.lines += line + ';\n';
|
4245
4298
|
line = '';
|
4246
4299
|
// Add the row constraints
|
4247
|
-
this.lines
|
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
|
4321
|
+
this.lines += line + '\n';
|
4269
4322
|
line = '';
|
4270
4323
|
}
|
4271
4324
|
}
|
4272
4325
|
c = this.right_hand_side[r];
|
4273
|
-
this.lines
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
4484
|
+
this.lines += 'RHS\n' + rhs.join('\n') + '\n';
|
4432
4485
|
rhs.length = 0;
|
4433
4486
|
// Add the BOUNDS section
|
4434
|
-
this.lines
|
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
|
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
|
4549
|
+
this.lines += ' FR' + bnd + '\n';
|
4497
4550
|
} else if(lb !== null && lb === ub && !semic) {
|
4498
|
-
this.lines
|
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
|
4566
|
+
this.lines += lbc + bnd + lb + '\n';
|
4514
4567
|
}
|
4515
4568
|
if(ub !== null) {
|
4516
|
-
this.lines
|
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
|
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
|
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
|
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
|
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
|
-
|
4738
|
-
|
4739
|
-
//
|
4740
|
-
|
4741
|
-
|
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 === '
|
5113
|
-
//
|
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
|
-
|
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
|
-
|
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:
|
5320
|
-
|
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
|
-
|
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
|
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
|
///////////////////////////////////////////////////////////////////////
|