linny-r 1.9.3 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +172 -126
- package/console.js +2 -1
- package/package.json +1 -1
- package/post-install.js +93 -37
- package/server.js +73 -29
- package/static/images/eq-negated.png +0 -0
- package/static/images/power.png +0 -0
- package/static/images/tex.png +0 -0
- package/static/index.html +226 -11
- package/static/linny-r.css +458 -8
- package/static/scripts/linny-r-ctrl.js +6 -4
- package/static/scripts/linny-r-gui-actor-manager.js +1 -1
- package/static/scripts/linny-r-gui-chart-manager.js +20 -13
- package/static/scripts/linny-r-gui-constraint-editor.js +410 -50
- package/static/scripts/linny-r-gui-controller.js +138 -21
- package/static/scripts/linny-r-gui-dataset-manager.js +28 -20
- package/static/scripts/linny-r-gui-documentation-manager.js +11 -3
- package/static/scripts/linny-r-gui-equation-manager.js +1 -1
- package/static/scripts/linny-r-gui-experiment-manager.js +1 -1
- package/static/scripts/linny-r-gui-expression-editor.js +7 -1
- package/static/scripts/linny-r-gui-file-manager.js +63 -19
- package/static/scripts/linny-r-gui-finder.js +1 -1
- package/static/scripts/linny-r-gui-model-autosaver.js +1 -1
- package/static/scripts/linny-r-gui-monitor.js +1 -1
- package/static/scripts/linny-r-gui-paper.js +108 -25
- package/static/scripts/linny-r-gui-power-grid-manager.js +529 -0
- package/static/scripts/linny-r-gui-receiver.js +1 -1
- package/static/scripts/linny-r-gui-repository-browser.js +1 -1
- package/static/scripts/linny-r-gui-scale-unit-manager.js +1 -1
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +1 -1
- package/static/scripts/linny-r-gui-tex-manager.js +110 -0
- package/static/scripts/linny-r-gui-undo-redo.js +1 -1
- package/static/scripts/linny-r-milp.js +1 -1
- package/static/scripts/linny-r-model.js +982 -123
- package/static/scripts/linny-r-utils.js +3 -3
- package/static/scripts/linny-r-vm.js +731 -252
- package/static/show-diff.html +1 -1
- package/static/show-png.html +1 -1
@@ -12,7 +12,7 @@ executed by the VM, construct the Simplex tableau that can be sent to the
|
|
12
12
|
MILP solver.
|
13
13
|
*/
|
14
14
|
/*
|
15
|
-
Copyright (c) 2017-
|
15
|
+
Copyright (c) 2017-2024 Delft University of Technology
|
16
16
|
|
17
17
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
18
18
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -136,7 +136,7 @@ class Expression {
|
|
136
136
|
if(this.object) return this.object.displayName + UI.OA_SEPARATOR + this.attribute;
|
137
137
|
return 'Unknown variable (no object)';
|
138
138
|
}
|
139
|
-
|
139
|
+
|
140
140
|
get timeStepDuration() {
|
141
141
|
// Return dt for dataset if this is a dataset modifier expression;
|
142
142
|
// otherwise dt for the current model.
|
@@ -663,10 +663,23 @@ class ExpressionParser {
|
|
663
663
|
this.context_number = owner.numberContext;
|
664
664
|
// NOTE: The owner prefix includes the trailing colon+space.
|
665
665
|
if(owner instanceof Link || owner instanceof Constraint) {
|
666
|
-
// For links and constraints,
|
667
|
-
|
668
|
-
|
669
|
-
owner.to_node.displayName
|
666
|
+
// For links and constraints, it depends:
|
667
|
+
const
|
668
|
+
fn = owner.from_node.displayName,
|
669
|
+
tn = owner.to_node.displayName;
|
670
|
+
if(fn.indexOf(UI.PREFIXER) >= 0) {
|
671
|
+
if(tn.indexOf(UI.PREFIXER) >= 0) {
|
672
|
+
// If both nodes are prefixed, use the longest prefix that these
|
673
|
+
// nodes have in common.
|
674
|
+
this.owner_prefix = UI.sharedPrefix(fn, tn) + UI.PREFIXER;
|
675
|
+
} else {
|
676
|
+
// Use the FROM node prefix.
|
677
|
+
this.owner_prefix = UI.completePrefix(fn);
|
678
|
+
}
|
679
|
+
} else if(tn.indexOf(UI.PREFIXER) >= 0) {
|
680
|
+
// Use the TO node prefix.
|
681
|
+
this.owner_prefix = UI.completePrefix(tn);
|
682
|
+
}
|
670
683
|
} else if(owner === MODEL.equations_dataset) {
|
671
684
|
this.owner_prefix = UI.completePrefix(attribute);
|
672
685
|
} else {
|
@@ -2024,7 +2037,7 @@ class ExpressionParser {
|
|
2024
2037
|
this.error = 'Missing operand';
|
2025
2038
|
} else if(this.sym_stack > 1) {
|
2026
2039
|
this.error = 'Missing operator';
|
2027
|
-
} else if(this.concatenating) {
|
2040
|
+
} else if(this.concatenating && !(this.owner instanceof BoundLine)) {
|
2028
2041
|
this.error = 'Invalid parameter list';
|
2029
2042
|
}
|
2030
2043
|
}
|
@@ -2121,6 +2134,7 @@ class VirtualMachine {
|
|
2121
2134
|
this.solver_secs = [];
|
2122
2135
|
this.messages = [];
|
2123
2136
|
this.equations = [];
|
2137
|
+
|
2124
2138
|
// Default texts to display for (still) empty results.
|
2125
2139
|
this.no_messages = '(no messages)';
|
2126
2140
|
this.no_variables = '(no variables)';
|
@@ -2134,18 +2148,23 @@ class VirtualMachine {
|
|
2134
2148
|
// decision variables reach +INF (1e+30) or -INF (-1e+30), and a solution
|
2135
2149
|
// inaccurate if extreme values get too close to +/-INF. The higher
|
2136
2150
|
// values have been chosen arbitrarily.
|
2137
|
-
this.
|
2138
|
-
this.
|
2151
|
+
this.SOLVER_PLUS_INFINITY = 1e+25;
|
2152
|
+
this.SOLVER_MINUS_INFINITY = -1e+25;
|
2139
2153
|
this.BEYOND_PLUS_INFINITY = 1e+35;
|
2140
2154
|
this.BEYOND_MINUS_INFINITY = -1e+35;
|
2141
|
-
// The
|
2142
|
-
//
|
2143
|
-
|
2144
|
-
this.
|
2155
|
+
// The VM properties "PLUS_INFINITY" and "MINUS_INFINITY" are used
|
2156
|
+
// when evaluating expressions. These propeties may be changed for
|
2157
|
+
// diagnostic purposes -- see below.
|
2158
|
+
this.PLUS_INFINITY = 1e+25;
|
2159
|
+
this.MINUS_INFINITY = -1e+25;
|
2145
2160
|
// As of version 1.8.0, Linny-R imposes no +INF bounds on processes
|
2146
2161
|
// unless diagnosing an unbounded problem. For such diagnosis, the
|
2147
2162
|
// (relatively) low value 9.999999999e+9 is used.
|
2148
2163
|
this.DIAGNOSIS_UPPER_BOUND = 9.999999999e+9;
|
2164
|
+
// For processes representing grid elements, upper bounds of +INF are
|
2165
|
+
// "capped" to 9999 grid element capacity units (typically MW for
|
2166
|
+
// high voltage grids).
|
2167
|
+
this.UNLIMITED_POWER_FLOW = 9999;
|
2149
2168
|
// NOTE: Below the "near zero" limit, a number is considered zero
|
2150
2169
|
// (this is to timely detect division-by-zero errors).
|
2151
2170
|
this.NEAR_ZERO = 1e-10;
|
@@ -2257,6 +2276,7 @@ class VirtualMachine {
|
|
2257
2276
|
this.LE = 1;
|
2258
2277
|
this.GE = 2;
|
2259
2278
|
this.EQ = 3;
|
2279
|
+
this.ACTOR_CASH = 4;
|
2260
2280
|
|
2261
2281
|
this.constraint_codes = ['FR', 'LE', 'GE', 'EQ'];
|
2262
2282
|
this.constraint_symbols = ['', '<=', '>=', '='];
|
@@ -2392,14 +2412,18 @@ class VirtualMachine {
|
|
2392
2412
|
// Reset the virtual machine so that it can execute the model again.
|
2393
2413
|
// First reset the expression attributes of all model entities.
|
2394
2414
|
MODEL.resetExpressions();
|
2395
|
-
// Clear slack use information for all
|
2415
|
+
// Clear slack use information and boundline point coordinates for all
|
2416
|
+
// constraints.
|
2396
2417
|
for(let k in MODEL.constraints) if(MODEL.constraints.hasOwnProperty(k)) {
|
2397
|
-
MODEL.constraints[k].
|
2418
|
+
MODEL.constraints[k].reset();
|
2398
2419
|
}
|
2399
2420
|
// Likewise, clear slack use information for all clusters.
|
2400
2421
|
for(let k in MODEL.clusters) if(MODEL.clusters.hasOwnProperty(k)) {
|
2401
2422
|
MODEL.clusters[k].slack_info = {};
|
2402
2423
|
}
|
2424
|
+
if(MODEL.with_power_flow) {
|
2425
|
+
POWER_GRID_MANAGER.checkLengths();
|
2426
|
+
}
|
2403
2427
|
// Clear the expression call stack -- used only for diagnostics.
|
2404
2428
|
this.call_stack.length = 0;
|
2405
2429
|
// The out-of-bounds properties are set when the ARRAY_INDEX error
|
@@ -2559,12 +2583,20 @@ class VirtualMachine {
|
|
2559
2583
|
const a = Math.abs(n);
|
2560
2584
|
// Signal small differences from true 0 by leading + or - sign.
|
2561
2585
|
if(n !== 0 && a <= this.ON_OFF_THRESHOLD) return n > 0 ? '+0' : '-0';
|
2562
|
-
|
2586
|
+
/*
|
2587
|
+
if(a >= 9999.5) return n.toPrecision(2);
|
2563
2588
|
if(Math.abs(a-Math.round(a)) < 0.05) return Math.round(n);
|
2564
2589
|
if(a < 1) return Math.round(n*100) / 100;
|
2565
2590
|
if(a < 10) return Math.round(n*10) / 10;
|
2566
2591
|
if(a < 100) return Math.round(n*10) / 10;
|
2567
2592
|
return Math.round(n);
|
2593
|
+
*/
|
2594
|
+
let s = n.toString();
|
2595
|
+
const prec = n.toPrecision(2);
|
2596
|
+
if(prec.length < s.length) s = prec;
|
2597
|
+
const expo = n.toExponential(1);
|
2598
|
+
if(expo.length < s.length) s = expo;
|
2599
|
+
return s;
|
2568
2600
|
}
|
2569
2601
|
|
2570
2602
|
sig4Dig(n, tiny=false) {
|
@@ -2577,19 +2609,25 @@ class VirtualMachine {
|
|
2577
2609
|
if(sv[0]) return sv[1];
|
2578
2610
|
const a = Math.abs(n);
|
2579
2611
|
if(a === 0) return 0;
|
2580
|
-
// Signal small differences from
|
2581
|
-
|
2582
|
-
|
2583
|
-
|
2584
|
-
|
2585
|
-
}
|
2586
|
-
if(a >= 9999995) return n.toPrecision(4);
|
2612
|
+
// Signal small differences from exactly 0 by a leading + or - sign
|
2613
|
+
// except when the `tiny` flag is set.
|
2614
|
+
if(a <= this.ON_OFF_THRESHOLD && !tiny) return n > 0 ? '+0' : '-0';
|
2615
|
+
/*
|
2616
|
+
if(a >= 9999.5) return n.toPrecision(4);
|
2587
2617
|
if(Math.abs(a-Math.round(a)) < 0.0005) return Math.round(n);
|
2588
2618
|
if(a < 1) return Math.round(n*10000) / 10000;
|
2589
2619
|
if(a < 10) return Math.round(n*1000) / 1000;
|
2590
2620
|
if(a < 100) return Math.round(n*100) / 100;
|
2591
2621
|
if(a < 1000) return Math.round(n*10) / 10;
|
2592
2622
|
return Math.round(n);
|
2623
|
+
*/
|
2624
|
+
let s = n.toString();
|
2625
|
+
const prec = n.toPrecision(4);
|
2626
|
+
if(prec.length < s.length) s = prec;
|
2627
|
+
const expo = n.toExponential(2);
|
2628
|
+
if(expo.length < s.length) s = expo;
|
2629
|
+
if(s.indexOf('e') < 0) s = parseFloat(s).toString();
|
2630
|
+
return s;
|
2593
2631
|
}
|
2594
2632
|
|
2595
2633
|
//
|
@@ -2934,7 +2972,7 @@ class VirtualMachine {
|
|
2934
2972
|
}
|
2935
2973
|
if(type === 'I' || type === 'PiL') {
|
2936
2974
|
this.int_var_indices[index] = true;
|
2937
|
-
} else if('OO|IZ|SU|SD|SO|FC|SB'.indexOf(type) >= 0) {
|
2975
|
+
} else if('OO|IZ|SU|SD|SO|FC|SB|UO1|DO1|UO2|DO2|UO3|DO3'.indexOf(type) >= 0) {
|
2938
2976
|
this.bin_var_indices[index] = true;
|
2939
2977
|
}
|
2940
2978
|
if(obj instanceof Process && obj.pace > 1) {
|
@@ -2944,7 +2982,7 @@ class VirtualMachine {
|
|
2944
2982
|
// For constraint bound lines, add as many SOS variables as there
|
2945
2983
|
// are points on the bound line.
|
2946
2984
|
if(type === 'W1' && obj instanceof BoundLine) {
|
2947
|
-
const n = obj.
|
2985
|
+
const n = obj.maxPoints;
|
2948
2986
|
for(let i = 2; i <= n; i++) {
|
2949
2987
|
this.variables.push(['W' + i, obj]);
|
2950
2988
|
}
|
@@ -2967,6 +3005,33 @@ class VirtualMachine {
|
|
2967
3005
|
return index;
|
2968
3006
|
}
|
2969
3007
|
|
3008
|
+
gridProcessVarIndices(p, offset=0) {
|
3009
|
+
// Return an object with lists of 1, 2 or 3 slope variable indices.
|
3010
|
+
if(p.up_1_var_index <= 0) return null;
|
3011
|
+
const gpv = {
|
3012
|
+
slopes: 1,
|
3013
|
+
up: [p.up_1_var_index + offset],
|
3014
|
+
up_on: [p.up_1_on_var_index + offset],
|
3015
|
+
down: [p.down_1_var_index + offset],
|
3016
|
+
down_on: [p.down_1_on_var_index + offset]
|
3017
|
+
};
|
3018
|
+
if(p.up_2_var_index >= 0) {
|
3019
|
+
gpv.slopes++;
|
3020
|
+
gpv.up.push(p.up_2_var_index + offset);
|
3021
|
+
gpv.up_on.push(p.up_2_on_var_index + offset);
|
3022
|
+
gpv.down.push(p.down_2_var_index + offset);
|
3023
|
+
gpv.down_on.push(p.down_2_on_var_index + offset);
|
3024
|
+
if(p.up_3_var_index >= 0) {
|
3025
|
+
gpv.slopes++;
|
3026
|
+
gpv.up.push(p.up_3_var_index + offset);
|
3027
|
+
gpv.up_on.push(p.up_3_on_var_index + offset);
|
3028
|
+
gpv.down.push(p.down_3_var_index + offset);
|
3029
|
+
gpv.down_on.push(p.down_3_on_var_index + offset);
|
3030
|
+
}
|
3031
|
+
}
|
3032
|
+
return gpv;
|
3033
|
+
}
|
3034
|
+
|
2970
3035
|
resetVariableIndices(p) {
|
2971
3036
|
// Set all variable indices to -1 ("no such variable") for node `p`.
|
2972
3037
|
p.level_var_index = -1;
|
@@ -2983,6 +3048,19 @@ class VirtualMachine {
|
|
2983
3048
|
p.stock_GE_slack_var_index = -1;
|
2984
3049
|
} else {
|
2985
3050
|
p.semic_var_index = -1;
|
3051
|
+
// Additional indices for grid elements.
|
3052
|
+
p.up_1_var_index = -1;
|
3053
|
+
p.up_1_on_var_index = -1;
|
3054
|
+
p.down_1_var_index = -1;
|
3055
|
+
p.down_1_on_var_index = -1;
|
3056
|
+
p.up_2_var_index = -1;
|
3057
|
+
p.up_2_on_var_index = -1;
|
3058
|
+
p.down_2_var_index = -1;
|
3059
|
+
p.down_2_on_var_index = -1;
|
3060
|
+
p.up_3_var_index = -1;
|
3061
|
+
p.up_3_on_var_index = -1;
|
3062
|
+
p.down_3_var_index = -1;
|
3063
|
+
p.down_3_on_var_index = -1;
|
2986
3064
|
}
|
2987
3065
|
}
|
2988
3066
|
|
@@ -3001,7 +3079,9 @@ class VirtualMachine {
|
|
3001
3079
|
// Some "data-only" link multipliers require additional variables.
|
3002
3080
|
if(p.needsOnOffData) {
|
3003
3081
|
p.on_off_var_index = this.addVariable('OO', p);
|
3004
|
-
p.
|
3082
|
+
if(p.needsIsZeroData) {
|
3083
|
+
p.is_zero_var_index = this.addVariable('IZ', p);
|
3084
|
+
}
|
3005
3085
|
// To detect startup, one more variable is needed
|
3006
3086
|
if(p.needsStartUpData) {
|
3007
3087
|
p.start_up_var_index = this.addVariable('SU', p);
|
@@ -3017,6 +3097,27 @@ class VirtualMachine {
|
|
3017
3097
|
p.shut_down_var_index = this.addVariable('SD', p);
|
3018
3098
|
}
|
3019
3099
|
}
|
3100
|
+
if(p.grid) {
|
3101
|
+
// Processes representing power grid elements are bi-directional
|
3102
|
+
// and hence need separate UP and DOWN flow variables.
|
3103
|
+
p.up_1_var_index = this.addVariable('U1', p);
|
3104
|
+
p.up_1_on_var_index = this.addVariable('UO1', p);
|
3105
|
+
p.down_1_var_index = this.addVariable('D1', p);
|
3106
|
+
p.down_1_on_var_index = this.addVariable('DO1', p);
|
3107
|
+
// Additional UP and DOWN is needed for each additional loss slope.
|
3108
|
+
if(p.grid.loss_approximation > 1) {
|
3109
|
+
p.up_2_var_index = this.addVariable('U2', p);
|
3110
|
+
p.up_2_on_var_index = this.addVariable('UO2', p);
|
3111
|
+
p.down_2_var_index = this.addVariable('D2', p);
|
3112
|
+
p.down_2_on_var_index = this.addVariable('DO2', p);
|
3113
|
+
if(p.grid.loss_approximation > 2) {
|
3114
|
+
p.up_3_var_index = this.addVariable('U3', p);
|
3115
|
+
p.up_3_on_var_index = this.addVariable('UO3', p);
|
3116
|
+
p.down_3_var_index = this.addVariable('D3', p);
|
3117
|
+
p.down_3_on_var_index = this.addVariable('DO3', p);
|
3118
|
+
}
|
3119
|
+
}
|
3120
|
+
}
|
3020
3121
|
// NOTES:
|
3021
3122
|
// (1) Processes have NO slack variables, because sufficient slack is
|
3022
3123
|
// provided by adding slack variables to products; these slack
|
@@ -3222,7 +3323,7 @@ class VirtualMachine {
|
|
3222
3323
|
|
3223
3324
|
// Log if run is performed in "diagnosis" mode.
|
3224
3325
|
if(this.diagnose) {
|
3225
|
-
this.logMessage(
|
3326
|
+
this.logMessage(1, 'DIAGNOSTIC RUN' +
|
3226
3327
|
(MODEL.always_diagnose ? ' (default -- see model settings)': '') +
|
3227
3328
|
'\n- slack variables on products and constraints' +
|
3228
3329
|
'\n- finite bounds on all processes');
|
@@ -3232,10 +3333,25 @@ class VirtualMachine {
|
|
3232
3333
|
MODEL.inferIgnoredEntities();
|
3233
3334
|
const n = Object.keys(MODEL.ignored_entities).length;
|
3234
3335
|
if(n > 0) {
|
3235
|
-
this.logMessage(
|
3336
|
+
this.logMessage(1,
|
3236
3337
|
pluralS(n, 'entity', 'entities') + ' will be ignored');
|
3237
3338
|
}
|
3238
3339
|
|
3340
|
+
// Infer cycle basis for combined power grids for which Kirchhoff's
|
3341
|
+
// voltage law must be enforced.
|
3342
|
+
if(MODEL.with_power_flow) {
|
3343
|
+
MONITOR.logMessage(1, 'POWER FLOW: ' +
|
3344
|
+
pluralS(Object.keys(MODEL.power_grids).length, 'grid'));
|
3345
|
+
POWER_GRID_MANAGER.inferCycleBasis();
|
3346
|
+
if(POWER_GRID_MANAGER.messages.length > 1) {
|
3347
|
+
UI.warn('Check monitor for power grid warnings');
|
3348
|
+
}
|
3349
|
+
MONITOR.logMessage(1, POWER_GRID_MANAGER.messages.join('\n'));
|
3350
|
+
if(POWER_GRID_MANAGER.cycle_basis.length) this.logMessage(1,
|
3351
|
+
'Enforcing Kirchhoff\'s voltage law for ' +
|
3352
|
+
POWER_GRID_MANAGER.cycleBasisAsString);
|
3353
|
+
}
|
3354
|
+
|
3239
3355
|
// FIRST: Define indices for all variables (index = Simplex tableau
|
3240
3356
|
// column number).
|
3241
3357
|
|
@@ -3288,7 +3404,7 @@ class VirtualMachine {
|
|
3288
3404
|
for(l = 0; l < c.bound_lines.length; l++) {
|
3289
3405
|
const bl = c.bound_lines[l];
|
3290
3406
|
bl.sos_var_indices = [];
|
3291
|
-
if(bl.
|
3407
|
+
if(bl.constrainsY) {
|
3292
3408
|
// Define SOS2 variables w[i] (plus associated binaries if
|
3293
3409
|
// solver does not support special ordered sets).
|
3294
3410
|
// NOTE: `addVariable` will add as many as there are points!
|
@@ -3379,9 +3495,16 @@ class VirtualMachine {
|
|
3379
3495
|
// NOTE: If UB = LB, set UB to LB only if LB is defined,
|
3380
3496
|
// because LB expressions default to -INF while UB expressions
|
3381
3497
|
// default to +INF.
|
3382
|
-
ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
|
3498
|
+
ubx = (!p.grid && p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
|
3383
3499
|
if(lbx.isStatic) lbx = lbx.result(0);
|
3384
|
-
if(ubx.isStatic)
|
3500
|
+
if(ubx.isStatic) {
|
3501
|
+
ubx = ubx.result(0);
|
3502
|
+
if(p.grid) lbx = -ubx;
|
3503
|
+
} else if (p.grid) {
|
3504
|
+
// When UB is dynamic, pass NULL as LB; the VM instruction will
|
3505
|
+
// interpret this as "LB = -UB".
|
3506
|
+
lbx = null;
|
3507
|
+
}
|
3385
3508
|
// NOTE: When semic_var_index is set, the lower bound must be
|
3386
3509
|
// zero, as the semi-continuous lower bound is implemented with
|
3387
3510
|
// a binary variable.
|
@@ -3482,39 +3605,50 @@ class VirtualMachine {
|
|
3482
3605
|
const p = MODEL.processes[k];
|
3483
3606
|
// Only consider processes owned by this actor.
|
3484
3607
|
if(p.actor === a) {
|
3485
|
-
|
3486
|
-
|
3487
|
-
|
3488
|
-
|
3489
|
-
|
3490
|
-
|
3491
|
-
|
3492
|
-
|
3493
|
-
|
3494
|
-
|
3495
|
-
|
3496
|
-
|
3497
|
-
|
3498
|
-
|
3499
|
-
//
|
3608
|
+
if(p.grid) {
|
3609
|
+
// Grid processes are a special case, as they can have a
|
3610
|
+
// negative level and potentially multiple slopes. Hence a
|
3611
|
+
// special VM instruction.
|
3612
|
+
this.code.push([VMI_update_grid_process_cash_coefficients, p]);
|
3613
|
+
} else {
|
3614
|
+
// Iterate over links IN, but only consider consumed products
|
3615
|
+
// having a market price.
|
3616
|
+
for(j = 0; j < p.inputs.length; j++) {
|
3617
|
+
l = p.inputs[j];
|
3618
|
+
if(!MODEL.ignored_entities[l.identifier] &&
|
3619
|
+
l.from_node.price.defined) {
|
3620
|
+
if(l.from_node.price.isStatic && l.relative_rate.isStatic) {
|
3621
|
+
k = l.from_node.price.result(0) * l.relative_rate.result(0);
|
3622
|
+
// NOTE: VMI_update_cash_coefficient has at least 4 arguments:
|
3623
|
+
// flow (CONSUME or PRODUCE), type (specifies the number and
|
3624
|
+
// type of arguments), the level_var_index of the process,
|
3625
|
+
// and the delay.
|
3626
|
+
// NOTE: Input links cannot have delay, so then delay = 0.
|
3627
|
+
if(Math.abs(k) > VM.NEAR_ZERO) {
|
3628
|
+
// Consumption rate & price are static: pass one constant.
|
3629
|
+
this.code.push([VMI_update_cash_coefficient,
|
3630
|
+
[VM.CONSUME, VM.ONE_C, p.level_var_index, 0, k]]);
|
3631
|
+
}
|
3632
|
+
} else {
|
3633
|
+
// No further optimization: assume two dynamic expressions.
|
3500
3634
|
this.code.push([VMI_update_cash_coefficient,
|
3501
|
-
[VM.CONSUME, VM.
|
3635
|
+
[VM.CONSUME, VM.TWO_X, p.level_var_index, 0,
|
3636
|
+
l.from_node.price, l.relative_rate]]);
|
3502
3637
|
}
|
3503
|
-
} else {
|
3504
|
-
// No further optimization: assume two dynamic expressions.
|
3505
|
-
this.code.push([VMI_update_cash_coefficient,
|
3506
|
-
[VM.CONSUME, VM.TWO_X, p.level_var_index, 0,
|
3507
|
-
l.from_node.price, l.relative_rate]]);
|
3508
3638
|
}
|
3509
|
-
}
|
3510
|
-
}
|
3511
|
-
|
3512
|
-
//
|
3513
|
-
//
|
3639
|
+
} // END of FOR ALL input links
|
3640
|
+
}
|
3641
|
+
// Now iterate over links OUT, but only consider produced
|
3642
|
+
// products having a (non-zero) market price.
|
3643
|
+
// NOTE: Grid processes can have output links to *data* products,
|
3644
|
+
// so do NOT skip this iteration...
|
3514
3645
|
for(j = 0; j < p.outputs.length; j++) {
|
3515
3646
|
l = p.outputs[j];
|
3516
|
-
const
|
3517
|
-
|
3647
|
+
const
|
3648
|
+
tnpx = l.to_node.price,
|
3649
|
+
// ... but DO skip links from grid processes to regular products.
|
3650
|
+
skip = p.grid && !l.to_node.is_data;
|
3651
|
+
if(!(skip || MODEL.ignored_entities[l.identifier]) && tnpx.defined &&
|
3518
3652
|
!(tnpx.isStatic && Math.abs(tnpx.result(0)) < VM.NEAR_ZERO)) {
|
3519
3653
|
// By default, use the process level as multiplier.
|
3520
3654
|
vi = p.level_var_index;
|
@@ -3609,8 +3743,10 @@ class VirtualMachine {
|
|
3609
3743
|
// Check whether any VMI_update_cash_coefficient instructions have
|
3610
3744
|
// been added. If so, the objective function will maximze weighted
|
3611
3745
|
// sum of actor cash flows, otherwise minimize sum of process levels.
|
3746
|
+
const last_vmi = this.code[this.code.length - 1][0];
|
3612
3747
|
this.no_cash_flows = this.no_cash_flows &&
|
3613
|
-
|
3748
|
+
last_vmi !== VMI_update_cash_coefficient &&
|
3749
|
+
last_vmi !== VMI_update_grid_process_cash_coefficients;
|
3614
3750
|
|
3615
3751
|
// ALWAYS add the two cash flow constraints for this actor, as both
|
3616
3752
|
// cash flow variables must be computed (will be 0 if no cash flows).
|
@@ -3749,6 +3885,21 @@ class VirtualMachine {
|
|
3749
3885
|
}
|
3750
3886
|
}
|
3751
3887
|
}
|
3888
|
+
|
3889
|
+
// NEXT: Add constraints for processes representing grid elements.
|
3890
|
+
if(MODEL.with_power_flow) {
|
3891
|
+
for(i = 0; i < process_keys.length; i++) {
|
3892
|
+
k = process_keys[i];
|
3893
|
+
if(!MODEL.ignored_entities[k]) {
|
3894
|
+
p = MODEL.processes[k];
|
3895
|
+
if(p.grid) {
|
3896
|
+
this.code.push([VMI_add_grid_process_constraints, p]);
|
3897
|
+
}
|
3898
|
+
}
|
3899
|
+
}
|
3900
|
+
this.code.push(
|
3901
|
+
[VMI_add_kirchhoff_constraints, POWER_GRID_MANAGER.cycle_basis]);
|
3902
|
+
}
|
3752
3903
|
|
3753
3904
|
// NEXT: Add product constraints to calculate (and constrain) their stock.
|
3754
3905
|
|
@@ -3786,7 +3937,9 @@ class VirtualMachine {
|
|
3786
3937
|
// Add coefficient -1 for level index variable of `p`.
|
3787
3938
|
this.code.push([VMI_add_const_to_coefficient,
|
3788
3939
|
[p.level_var_index, -1, 0]]);
|
3789
|
-
|
3940
|
+
// NOTE: Pass special constraint type parameter to indicate
|
3941
|
+
// that this constraint must be scaled by the cash scalar.
|
3942
|
+
this.code.push([VMI_add_constraint, VM.ACTOR_CASH]);
|
3790
3943
|
} else {
|
3791
3944
|
console.log('ANOMALY: no actor for cash flow product', p.displayName);
|
3792
3945
|
}
|
@@ -3818,8 +3971,20 @@ class VirtualMachine {
|
|
3818
3971
|
} else {
|
3819
3972
|
vi = fn.level_var_index;
|
3820
3973
|
}
|
3821
|
-
// First check
|
3822
|
-
if(l.multiplier === VM.
|
3974
|
+
// First check whether the link is a power flow.
|
3975
|
+
if(l.multiplier === VM.LM_LEVEL && !p.is_data && fn.grid) {
|
3976
|
+
// If so, pass the grid process to a special VM instruction
|
3977
|
+
// that will add coefficients that account for losses.
|
3978
|
+
// NOTES:
|
3979
|
+
// (1) The second parameter (+1) indicates that the
|
3980
|
+
// coefficients of the UP flows should be positive
|
3981
|
+
// and those of the DOWN flows should be negative
|
3982
|
+
// (because it is a P -> Q link).
|
3983
|
+
// (2) The rate and delay properties of the link are ignored.
|
3984
|
+
this.code.push(
|
3985
|
+
[VMI_add_power_flow_to_coefficients, [fn, 1]]);
|
3986
|
+
// Then check for throughput links, as these are elaborate.
|
3987
|
+
} else if(l.multiplier === VM.LM_THROUGHPUT) {
|
3823
3988
|
// Link `l` is Y-->Z and "reads" the total inflow into Y
|
3824
3989
|
// over links Xi-->Y having rate Ri and when Y is a
|
3825
3990
|
// product potentially also delay Di.
|
@@ -3936,19 +4101,35 @@ class VirtualMachine {
|
|
3936
4101
|
} // END IF not ignored
|
3937
4102
|
} // END FOR all inputs
|
3938
4103
|
|
3939
|
-
//
|
4104
|
+
// Subtract outflow from product P to consuming processes (outputs)
|
3940
4105
|
for(i = 0; i < p.outputs.length; i++) {
|
3941
|
-
// NOTE: only consider outputs to processes; data outflows do not subtract
|
3942
4106
|
l = p.outputs[i];
|
3943
4107
|
if(!MODEL.ignored_entities[l.identifier]) {
|
3944
|
-
|
3945
|
-
|
3946
|
-
|
3947
|
-
|
3948
|
-
|
4108
|
+
const tn = l.to_node;
|
4109
|
+
// NOTE: Only consider outputs to processes; data flows do
|
4110
|
+
// not subtract from their tail nodes.
|
4111
|
+
if(tn instanceof Process) {
|
4112
|
+
if(tn.grid) {
|
4113
|
+
// If the link is a power flow, pass the grid process to
|
4114
|
+
// a special VM instruction that will add coefficients that
|
4115
|
+
// account for losses.
|
4116
|
+
// NOTES:
|
4117
|
+
// (1) The second parameter (-1) indicates that the
|
4118
|
+
// coefficients of the UP flows should be negative
|
4119
|
+
// and those of the DOWN flows should be positive
|
4120
|
+
// (because it is a Q -> P link).
|
4121
|
+
// (2) The rate and delay properties of the link are ignored.
|
4122
|
+
this.code.push(
|
4123
|
+
[VMI_add_power_flow_to_coefficients, [tn, -1]]);
|
3949
4124
|
} else {
|
3950
|
-
|
3951
|
-
|
4125
|
+
const rr = l.relative_rate;
|
4126
|
+
if(rr.isStatic) {
|
4127
|
+
this.code.push([VMI_subtract_const_from_coefficient,
|
4128
|
+
[tn.level_var_index, rr.result(0), l.flow_delay]]);
|
4129
|
+
} else {
|
4130
|
+
this.code.push([VMI_subtract_var_from_coefficient,
|
4131
|
+
[tn.level_var_index, rr, l.flow_delay]]);
|
4132
|
+
}
|
3952
4133
|
}
|
3953
4134
|
}
|
3954
4135
|
}
|
@@ -4090,88 +4271,94 @@ class VirtualMachine {
|
|
4090
4271
|
// To deal with this, the default equations will NOT be set when UB <= 0,
|
4091
4272
|
// while the "exceptional" equations (q.v.) will NOT be set when UB > 0.
|
4092
4273
|
// This can be realized using a special VM instruction:
|
4093
|
-
ubx = (p.equal_bounds && p.lower_bound.defined
|
4274
|
+
ubx = (p.equal_bounds && p.lower_bound.defined && !p.grid ?
|
4275
|
+
p.lower_bound : p.upper_bound);
|
4094
4276
|
this.code.push([VMI_set_add_constraints_flag, [ubx, '>', 0]]);
|
4095
|
-
|
4096
|
-
//
|
4097
|
-
|
4098
|
-
|
4099
|
-
|
4100
|
-
|
4101
|
-
|
4102
|
-
|
4103
|
-
|
4104
|
-
|
4105
|
-
|
4106
|
-
|
4107
|
-
|
4108
|
-
|
4109
|
-
|
4110
|
-
|
4111
|
-
|
4112
|
-
|
4113
|
-
|
4114
|
-
|
4115
|
-
|
4116
|
-
|
4117
|
-
// Set coefficients vector to 0
|
4118
|
-
[VMI_clear_coefficients, null],
|
4119
|
-
// (b) L[t] - UB[t]*OO[t] <= 0
|
4120
|
-
[VMI_add_const_to_coefficient, [p.level_var_index, 1]]
|
4121
|
-
);
|
4122
|
-
if(ubx.isStatic) {
|
4123
|
-
// If UB is very high (typically: undefined, so +INF), try to
|
4124
|
-
// infer a lower value for UB to use for the ON/OFF binary.
|
4125
|
-
let ub = ubx.result(0),
|
4126
|
-
hub = ub;
|
4127
|
-
if(ub > VM.MEGA_UPPER_BOUND) {
|
4128
|
-
hub = p.highestUpperBound([]);
|
4129
|
-
// If UB still very high, warn modeler on infoline and in monitor
|
4130
|
-
if(hub > VM.MEGA_UPPER_BOUND) {
|
4131
|
-
const msg = 'High upper bound (' + this.sig4Dig(hub) +
|
4132
|
-
') for <strong>' + p.displayName + '</strong>' +
|
4133
|
-
' will compromise computation of its binary variables';
|
4134
|
-
UI.warn(msg);
|
4135
|
-
this.logMessage(this.block_count,
|
4136
|
-
'WARNING: ' + msg.replace(/<\/?strong>/g, '"'));
|
4137
|
-
}
|
4277
|
+
// This instruction ensures that when UB <= 0, the constraints for
|
4278
|
+
// binaries will be prepared, but NOT added to the MILP problem.
|
4279
|
+
|
4280
|
+
// NOTE: For grid element processes, the ON/OFF binary is set by
|
4281
|
+
// the VMI_add_grid_process_constraints.
|
4282
|
+
if(!p.grid) {
|
4283
|
+
this.code.push(
|
4284
|
+
// Set coefficients vector to 0
|
4285
|
+
[VMI_clear_coefficients, null],
|
4286
|
+
// (a) L[t] - LB[t]*OO[t] >= 0
|
4287
|
+
[VMI_add_const_to_coefficient, [p.level_var_index, 1]]
|
4288
|
+
);
|
4289
|
+
if(p.lower_bound.isStatic) {
|
4290
|
+
let lb = p.lower_bound.result(0);
|
4291
|
+
if(Math.abs(lb) < VM.NEAR_ZERO) lb = VM.ON_OFF_THRESHOLD;
|
4292
|
+
this.code.push([VMI_subtract_const_from_coefficient,
|
4293
|
+
[p.on_off_var_index, lb]]);
|
4294
|
+
} else {
|
4295
|
+
this.code.push([VMI_subtract_var_from_coefficient,
|
4296
|
+
// NOTE: the 3rd parameter signals VM to use the ON/OFF threshold
|
4297
|
+
// value when the LB evaluates as near-zero
|
4298
|
+
[p.on_off_var_index, p.lower_bound, VM.ON_OFF_THRESHOLD]]);
|
4138
4299
|
}
|
4139
|
-
|
4140
|
-
|
4141
|
-
|
4142
|
-
|
4300
|
+
this.code.push(
|
4301
|
+
[VMI_add_constraint, VM.GE], // >= 0 as default RHS = 0
|
4302
|
+
// Set coefficients vector to 0
|
4303
|
+
[VMI_clear_coefficients, null],
|
4304
|
+
// (b) L[t] - UB[t]*OO[t] <= 0
|
4305
|
+
[VMI_add_const_to_coefficient, [p.level_var_index, 1]]
|
4306
|
+
);
|
4307
|
+
if(ubx.isStatic) {
|
4308
|
+
// If UB is very high (typically: undefined, so +INF), try to
|
4309
|
+
// infer a lower value for UB to use for the ON/OFF binary.
|
4310
|
+
let ub = ubx.result(0),
|
4311
|
+
hub = ub;
|
4312
|
+
if(ub > VM.MEGA_UPPER_BOUND) {
|
4313
|
+
hub = p.highestUpperBound([]);
|
4314
|
+
// If UB still very high, warn modeler on infoline and in monitor
|
4315
|
+
if(hub > VM.MEGA_UPPER_BOUND) {
|
4316
|
+
const msg = 'High upper bound (' + this.sig4Dig(hub) +
|
4317
|
+
') for <strong>' + p.displayName + '</strong>' +
|
4318
|
+
' will compromise computation of its binary variables';
|
4319
|
+
UI.warn(msg);
|
4320
|
+
this.logMessage(this.block_count,
|
4321
|
+
'WARNING: ' + msg.replace(/<\/?strong>/g, '"'));
|
4322
|
+
}
|
4323
|
+
}
|
4324
|
+
if(hub !== ub) {
|
4325
|
+
ub = hub;
|
4326
|
+
this.logMessage(1,
|
4327
|
+
`Inferred upper bound for ${p.displayName}: ${this.sig4Dig(ub)}`);
|
4328
|
+
}
|
4329
|
+
this.code.push([VMI_subtract_const_from_coefficient,
|
4330
|
+
[p.on_off_var_index, ub]]);
|
4331
|
+
} else {
|
4332
|
+
// NOTE: no check (yet) for high values when UB is an expression
|
4333
|
+
// (this could be achieved by a special VM instruction)
|
4334
|
+
this.code.push([VMI_subtract_var_from_coefficient,
|
4335
|
+
[p.on_off_var_index, ubx]]);
|
4143
4336
|
}
|
4144
|
-
this.code.push([
|
4145
|
-
[p.on_off_var_index, ub]]);
|
4146
|
-
} else {
|
4147
|
-
// NOTE: no check (yet) for high values when UB is an expression
|
4148
|
-
// (this could be achieved by a special VM instruction)
|
4149
|
-
this.code.push([VMI_subtract_var_from_coefficient,
|
4150
|
-
[p.on_off_var_index, ubx]]);
|
4337
|
+
this.code.push([VMI_add_constraint, VM.LE]); // <= 0 as default RHS = 0
|
4151
4338
|
}
|
4152
|
-
|
4153
|
-
[VMI_add_constraint, VM.LE], // <= 0 as default RHS = 0
|
4339
|
+
if(p.is_zero_var_index >= 0) {
|
4154
4340
|
// Also add the constraints for is-zero
|
4155
|
-
|
4156
|
-
|
4157
|
-
|
4158
|
-
|
4159
|
-
|
4160
|
-
|
4161
|
-
|
4162
|
-
|
4163
|
-
|
4164
|
-
|
4165
|
-
|
4166
|
-
|
4167
|
-
|
4168
|
-
|
4169
|
-
|
4170
|
-
|
4171
|
-
|
4341
|
+
this.code.push(
|
4342
|
+
[VMI_clear_coefficients, null],
|
4343
|
+
// (c) OO[t] + IZ[t] = 1
|
4344
|
+
[VMI_add_const_to_coefficient, [p.is_zero_var_index, 1]],
|
4345
|
+
[VMI_add_const_to_coefficient, [p.on_off_var_index, 1]],
|
4346
|
+
[VMI_set_const_rhs, 1],
|
4347
|
+
[VMI_add_constraint, VM.EQ],
|
4348
|
+
// (d) L[t] + IZ[t] >= LB[t]
|
4349
|
+
[VMI_clear_coefficients, null],
|
4350
|
+
[VMI_add_const_to_coefficient, [p.level_var_index, 1]],
|
4351
|
+
[VMI_add_const_to_coefficient, [p.is_zero_var_index, 1]]
|
4352
|
+
);
|
4353
|
+
// NOTE: for semicontinuous variable, always use LB = 0
|
4354
|
+
if(p.lower_bound.isStatic || p.level_to_zero) {
|
4355
|
+
const plb = (p.level_to_zero ? 0 : p.lower_bound.result(0));
|
4356
|
+
this.code.push([VMI_set_const_rhs, plb]);
|
4357
|
+
} else {
|
4358
|
+
this.code.push([VMI_set_var_rhs, p.lower_bound]);
|
4359
|
+
}
|
4360
|
+
this.code.push([VMI_add_constraint, VM.GE]);
|
4172
4361
|
}
|
4173
|
-
this.code.push([VMI_add_constraint, VM.GE]);
|
4174
|
-
|
4175
4362
|
// Also add constraints for start-up (if needed)
|
4176
4363
|
if(p.start_up_var_index >= 0) {
|
4177
4364
|
this.code.push(
|
@@ -4271,18 +4458,22 @@ class VirtualMachine {
|
|
4271
4458
|
// NOTE: toggle the flag so that if UB <= 0, the following constraints
|
4272
4459
|
// for setting the binary variables WILL be added
|
4273
4460
|
this.code.push(
|
4274
|
-
|
4275
|
-
|
4276
|
-
|
4277
|
-
|
4278
|
-
|
4279
|
-
|
4280
|
-
|
4281
|
-
|
4282
|
-
|
4283
|
-
|
4284
|
-
|
4285
|
-
|
4461
|
+
[VMI_toggle_add_constraints_flag, null],
|
4462
|
+
// When UB <= 0, add these much simpler "exceptional" constraints:
|
4463
|
+
// OO[t] = 0
|
4464
|
+
[VMI_clear_coefficients, null],
|
4465
|
+
[VMI_add_const_to_coefficient, [p.on_off_var_index, 1]],
|
4466
|
+
[VMI_add_constraint, VM.EQ]
|
4467
|
+
);
|
4468
|
+
if(p.is_zero_var_index >= 0) {
|
4469
|
+
this.code.push(
|
4470
|
+
// IZ[t] = 1
|
4471
|
+
[VMI_clear_coefficients, null],
|
4472
|
+
[VMI_add_const_to_coefficient, [p.is_zero_var_index, 1]],
|
4473
|
+
[VMI_set_const_rhs, 1], // RHS = 1
|
4474
|
+
[VMI_add_constraint, VM.EQ]
|
4475
|
+
);
|
4476
|
+
}
|
4286
4477
|
// Add constraints for start-up and first commit only if needed
|
4287
4478
|
if(p.start_up_var_index >= 0) {
|
4288
4479
|
this.code.push(
|
@@ -4328,7 +4519,7 @@ class VirtualMachine {
|
|
4328
4519
|
}
|
4329
4520
|
}
|
4330
4521
|
|
4331
|
-
// NEXT: Add constraints.
|
4522
|
+
// NEXT: Add composite constraints.
|
4332
4523
|
// NOTE: As of version 1.0.10, constraints are implemented using special
|
4333
4524
|
// ordered sets (SOS2). This is effectuated with a dedicated VM instruction
|
4334
4525
|
// for each of its "active" bound lines. This instruction requires these
|
@@ -4336,10 +4527,6 @@ class VirtualMachine {
|
|
4336
4527
|
// - variable indices for the constraining node X, the constrained node Y
|
4337
4528
|
// - expressions for the LB and UB of X and Y
|
4338
4529
|
// - the bound line object, as this provides all further information
|
4339
|
-
// NOTE: For efficiency, the useBinaries flag is also passed, as it can
|
4340
|
-
// be determined at compile time whether a SOS constraint is needed
|
4341
|
-
// (bound lines are static), and whether the solver does not support
|
4342
|
-
// SOS, in which case binary variables must be used.
|
4343
4530
|
for(i = 0; i < constraint_keys.length; i++) {
|
4344
4531
|
k = constraint_keys[i];
|
4345
4532
|
if(!MODEL.ignored_entities[k]) {
|
@@ -4349,20 +4536,16 @@ class VirtualMachine {
|
|
4349
4536
|
x = c.from_node,
|
4350
4537
|
y = c.to_node;
|
4351
4538
|
for(j = 0; j < c.bound_lines.length; j++) {
|
4352
|
-
|
4353
|
-
|
4354
|
-
|
4355
|
-
|
4356
|
-
this.code.push([VMI_add_bound_line_constraint,
|
4357
|
-
[x.level_var_index, x.lower_bound, x.upper_bound,
|
4358
|
-
y.level_var_index, y.lower_bound, y.upper_bound,
|
4359
|
-
bl, this.noSupportForSOS && !bl.needsNoSOS]]);
|
4360
|
-
}
|
4539
|
+
this.code.push([VMI_add_bound_line_constraint,
|
4540
|
+
[x.level_var_index, x.lower_bound, x.upper_bound,
|
4541
|
+
y.level_var_index, y.lower_bound, y.upper_bound,
|
4542
|
+
c.bound_lines[j]]]);
|
4361
4543
|
}
|
4362
4544
|
}
|
4363
4545
|
} // end FOR all constraints
|
4546
|
+
|
4364
4547
|
MODEL.set_up = true;
|
4365
|
-
this.logMessage(
|
4548
|
+
this.logMessage(1,
|
4366
4549
|
`Problem formulation took ${this.elapsedTime} seconds.`);
|
4367
4550
|
} // END of setup_problem function
|
4368
4551
|
|
@@ -4464,6 +4647,24 @@ class VirtualMachine {
|
|
4464
4647
|
if(cv && !cv[0].startsWith('C')) cc[ci] *= m;
|
4465
4648
|
}
|
4466
4649
|
}
|
4650
|
+
// In case the model contains data products that represent an actor
|
4651
|
+
// cash flow, the coefficients of the constraint that equates the
|
4652
|
+
// product level to the cash flow must be *multiplied* by the cash
|
4653
|
+
// scalar so that they equal the cash flow in the model's monetary unit.
|
4654
|
+
for(let i = 0; i < this.actor_cash_constraints.length; i++) {
|
4655
|
+
const cc = this.matrix[this.actor_cash_constraints[i]];
|
4656
|
+
for(let ci in cc) if(cc.hasOwnProperty(ci)) {
|
4657
|
+
if(ci < this.chunk_offset) {
|
4658
|
+
// NOTE: Subtract 1 as variables array is zero-based.
|
4659
|
+
cv = this.variables[(ci - 1) % this.cols];
|
4660
|
+
} else {
|
4661
|
+
// Chunk variable array is zero-based.
|
4662
|
+
cv = this.chunk_variables[ci - this.chunk_offset];
|
4663
|
+
}
|
4664
|
+
// NOTE: Scale coefficients of cash variables only.
|
4665
|
+
if(cv && cv[0].startsWith('C')) cc[ci] *= this.cash_scalar;
|
4666
|
+
}
|
4667
|
+
}
|
4467
4668
|
}
|
4468
4669
|
|
4469
4670
|
checkForInfinity(n) {
|
@@ -4549,7 +4750,7 @@ class VirtualMachine {
|
|
4549
4750
|
}
|
4550
4751
|
if(b <= this.nr_of_time_steps && a.cash_out[b] < -0.005) {
|
4551
4752
|
this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
|
4552
|
-
a.displayName + ' cash
|
4753
|
+
a.displayName + ' cash OUT = ' + a.cash_out[b].toPrecision(2));
|
4553
4754
|
}
|
4554
4755
|
// Advance column offset in tableau by the # cols per time step.
|
4555
4756
|
j += this.cols;
|
@@ -4564,7 +4765,8 @@ class VirtualMachine {
|
|
4564
4765
|
p = MODEL.processes[o],
|
4565
4766
|
has_OO = (p.on_off_var_index >= 0),
|
4566
4767
|
has_SU = (p.start_up_var_index >= 0),
|
4567
|
-
has_SD = (p.shut_down_var_index >= 0)
|
4768
|
+
has_SD = (p.shut_down_var_index >= 0),
|
4769
|
+
grid = p.grid;
|
4568
4770
|
// Clear all start-ups and shut-downs at t >= bb.
|
4569
4771
|
if(has_SU) p.resetStartUps(bb);
|
4570
4772
|
if(has_SD) p.resetShutDowns(bb);
|
@@ -4747,6 +4949,9 @@ class VirtualMachine {
|
|
4747
4949
|
this.variables_to_fixate = {};
|
4748
4950
|
// FIRST: Calculate the actual flows on links.
|
4749
4951
|
let b, bt, p, pl, ld, ci;
|
4952
|
+
for(let g in MODEL.power_grids) if(MODEL.power_grids.hasOwnProperty(g)) {
|
4953
|
+
MODEL.power_grids[g].total_losses = 0;
|
4954
|
+
}
|
4750
4955
|
for(let l in MODEL.links) if(MODEL.links.hasOwnProperty(l) &&
|
4751
4956
|
!MODEL.ignored_entities[l]) {
|
4752
4957
|
l = MODEL.links[l];
|
@@ -4756,7 +4961,7 @@ class VirtualMachine {
|
|
4756
4961
|
b = bb;
|
4757
4962
|
// Iterate over all time steps in this chunk.
|
4758
4963
|
for(let i = 0; i < cbl; i++) {
|
4759
|
-
// NOTE: Flows may have a delay
|
4964
|
+
// NOTE: Flows may have a delay (but will be 0 for grid processes).
|
4760
4965
|
ld = l.actualDelay(b);
|
4761
4966
|
bt = b - ld;
|
4762
4967
|
latest_time_step = Math.max(latest_time_step, bt);
|
@@ -4857,13 +5062,38 @@ class VirtualMachine {
|
|
4857
5062
|
}
|
4858
5063
|
}
|
4859
5064
|
// Preserve special values such as INF, UNDEFINED and VM error codes.
|
4860
|
-
|
4861
|
-
|
4862
|
-
|
5065
|
+
let rr = l.relative_rate.result(bt);
|
5066
|
+
if(p.grid) {
|
5067
|
+
// For grid processes, rates depend on losses, which depend on
|
5068
|
+
// the process level, and whether the link is P -> Q or Q -> P.
|
5069
|
+
rr = 1;
|
5070
|
+
if(p.grid.loss_approximation > 0 &&
|
5071
|
+
((pl > 0 && p === l.from_node) ||
|
5072
|
+
(pl < 0 && p === l.to_node))) {
|
5073
|
+
const alr = p.actualLossRate(bt);
|
5074
|
+
rr = 1 - alr;
|
5075
|
+
p.grid.total_losses += alr * Math.abs(pl);
|
5076
|
+
}
|
5077
|
+
}
|
5078
|
+
const af = this.severestIssue([pl, rr], rr * pl);
|
4863
5079
|
l.actual_flow[b] = (Math.abs(af) > VM.NEAR_ZERO ? af : 0);
|
4864
5080
|
b++;
|
4865
5081
|
}
|
4866
5082
|
}
|
5083
|
+
// Report power losses per grid, if applicable.
|
5084
|
+
if(MODEL.with_power_flow) {
|
5085
|
+
const ll = [];
|
5086
|
+
for(let g in MODEL.power_grids) if(MODEL.power_grids.hasOwnProperty(g)) {
|
5087
|
+
const pg = MODEL.power_grids[g];
|
5088
|
+
if(pg.loss_approximation > 0) {
|
5089
|
+
ll.push(`${pg.name}: ${VM.sig4Dig(pg.total_losses / cbl)} ${pg.power_unit}`);
|
5090
|
+
}
|
5091
|
+
}
|
5092
|
+
if(ll.length) {
|
5093
|
+
this.logMessage(block, 'Average power grid losses per time step:\n ' +
|
5094
|
+
ll.join('\n ') + '\n');
|
5095
|
+
}
|
5096
|
+
}
|
4867
5097
|
|
4868
5098
|
// THEN: Calculate cash flows one step at a time because of delays.
|
4869
5099
|
b = bb;
|
@@ -5188,6 +5418,12 @@ class VirtualMachine {
|
|
5188
5418
|
// of matrix rows that then need to be scaled.
|
5189
5419
|
this.cash_scalar = 1;
|
5190
5420
|
this.cash_constraints = [];
|
5421
|
+
// NOTE: The model may contain data products that represent a cash
|
5422
|
+
// flow property of an actor. To calculate the actual value of such
|
5423
|
+
// properties, the coefficients in the effectuating constraint must
|
5424
|
+
// be *multiplied* by the scalar to compensate for the downscaling
|
5425
|
+
// explained above.
|
5426
|
+
this.actor_cash_constraints = [];
|
5191
5427
|
// Vector for the objective function coefficients.
|
5192
5428
|
this.objective = {};
|
5193
5429
|
// Vectors for the bounds on decision variables.
|
@@ -5447,6 +5683,7 @@ class VirtualMachine {
|
|
5447
5683
|
}
|
5448
5684
|
let c,
|
5449
5685
|
p,
|
5686
|
+
v,
|
5450
5687
|
line = '';
|
5451
5688
|
// NOTE: Iterate over ALL columns to maintain variable order.
|
5452
5689
|
let n = abl * this.cols + this.chunk_variables.length;
|
@@ -5527,31 +5764,23 @@ class VirtualMachine {
|
|
5527
5764
|
break;
|
5528
5765
|
}
|
5529
5766
|
}
|
5530
|
-
|
5767
|
+
v = vbl(p);
|
5531
5768
|
if(lb === ub) {
|
5532
|
-
|
5769
|
+
line = (lb !== null ? ` ${v} = ${lb}` : '');
|
5533
5770
|
} else {
|
5771
|
+
const
|
5772
|
+
lbfree = (lb === null || lb <= VM.SOLVER_MINUS_INFINITY),
|
5773
|
+
ubfree = (ub === null || ub >= VM.SOLVER_PLUS_INFINITY);
|
5534
5774
|
// NOTE: By default, lower bound of variables is 0.
|
5535
|
-
|
5536
|
-
|
5537
|
-
// Explicitly denote free variables.
|
5538
|
-
if(lb === null && ub === null && !this.is_binary[p]) {
|
5539
|
-
line += ' free';
|
5540
|
-
} else {
|
5541
|
-
// Separate lines for LB and UB if specified.
|
5542
|
-
if(ub !== null) line += ' <= ' + ub;
|
5543
|
-
if(lb !== null && lb !== 0) line += `\n ${vbl(p)} >= ${lb}`;
|
5544
|
-
}
|
5775
|
+
if(cplex && lbfree && ubfree) {
|
5776
|
+
line = ` ${v} ${this.is_binary[p] ? '<= 1' : 'free'}`;
|
5545
5777
|
} else {
|
5546
5778
|
// Bounds can be specified on a single line: lb <= X001 <= ub.
|
5547
|
-
|
5548
|
-
|
5549
|
-
|
5550
|
-
line =
|
5779
|
+
if(lb || lb === 0) {
|
5780
|
+
line = ` ${lb} <= ${v}${ubfree ? '' : ' <= ' + ub}`;
|
5781
|
+
} else {
|
5782
|
+
line = (ubfree ? '' : ` ${v} <= ${ub}`);
|
5551
5783
|
}
|
5552
|
-
if(ub !== null && ub < 1e+25) line += ' <= ' + ub;
|
5553
|
-
// NOTE: Do not add line if both bounds are infinite.
|
5554
|
-
if(line.indexOf('<=') < 0) line = '';
|
5555
5784
|
}
|
5556
5785
|
}
|
5557
5786
|
if(line) this.lines += line + EOL;
|
@@ -6190,8 +6419,8 @@ Solver status = ${json.status}`);
|
|
6190
6419
|
this.MINUS_INFINITY = -this.DIAGNOSIS_UPPER_BOUND;
|
6191
6420
|
console.log('DIAGNOSIS ON');
|
6192
6421
|
} else {
|
6193
|
-
this.PLUS_INFINITY =
|
6194
|
-
this.MINUS_INFINITY =
|
6422
|
+
this.PLUS_INFINITY = this.SOLVER_PLUS_INFINITY;
|
6423
|
+
this.MINUS_INFINITY = this.SOLVER_MINUS_INFINITY;
|
6195
6424
|
console.log('DIAGNOSIS OFF');
|
6196
6425
|
}
|
6197
6426
|
// The "propt to diagnose" flag is set when some block posed an
|
@@ -7799,6 +8028,18 @@ function randomBinomial(n, p) {
|
|
7799
8028
|
}
|
7800
8029
|
}
|
7801
8030
|
|
8031
|
+
// Function that computes the cumulative probability P(X <= x) when X
|
8032
|
+
// has a N(mu, sigma) distribution. Accuracy is about 1e-6.
|
8033
|
+
function normalCumulativeProbability(mu, sigma, x) {
|
8034
|
+
const
|
8035
|
+
t = 1 / (1 + 0.2316419 * Math.abs(x)),
|
8036
|
+
d = 0.3989423 * Math.exp(-0.5 * x * x),
|
8037
|
+
p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 +
|
8038
|
+
t * (-1.821256 + T * 1.330274))));
|
8039
|
+
if(x > 0) return 1 - p;
|
8040
|
+
return p;
|
8041
|
+
}
|
8042
|
+
|
7802
8043
|
// Global array as cache for computation of factorial numbers.
|
7803
8044
|
const FACTORIALS = [0, 1];
|
7804
8045
|
|
@@ -7903,8 +8144,12 @@ function VMI_set_bounds(args) {
|
|
7903
8144
|
// When diagnosing an unbounded problem, use low value for INFINITY,
|
7904
8145
|
// but the optional fourth parameter indicates whether the solver's
|
7905
8146
|
// infinity values should override the diagnosis INFINITY.
|
7906
|
-
|
7907
|
-
|
8147
|
+
// NOTE: For grid processes, the bounds are always "capped" so as
|
8148
|
+
// to permit Big M constraints for the associated binary variables.
|
8149
|
+
inf_is_free = (args.length > 3 && args[3]),
|
8150
|
+
inf_val = (vbl.grid ? VM.UNLIMITED_POWER_FLOW :
|
8151
|
+
(VM.diagnose && !inf_is_free ?
|
8152
|
+
VM.DIAGNOSIS_UPPER_BOUND : VM.SOLVER_PLUS_INFINITY));
|
7908
8153
|
let l,
|
7909
8154
|
u,
|
7910
8155
|
fixed = (vi in VM.fixed_var_indices[r - 1]);
|
@@ -7924,26 +8169,35 @@ function VMI_set_bounds(args) {
|
|
7924
8169
|
} else {
|
7925
8170
|
// Set bounds as specified by the two arguments.
|
7926
8171
|
l = args[1];
|
7927
|
-
if(l instanceof Expression) l = l.result(VM.t);
|
7928
|
-
if(l === VM.UNDEFINED) l = 0;
|
7929
8172
|
u = args[2];
|
7930
8173
|
if(u instanceof Expression) u = u.result(VM.t);
|
7931
|
-
u = Math.min(u,
|
7932
|
-
|
7933
|
-
if(
|
8174
|
+
u = Math.min(u, inf_val);
|
8175
|
+
// When LB is passed as NULL, this indicates: LB = -UB.
|
8176
|
+
if(l === null) {
|
8177
|
+
l = -u;
|
8178
|
+
} else {
|
8179
|
+
if(l instanceof Expression) l = l.result(VM.t);
|
8180
|
+
if(l === VM.UNDEFINED) {
|
8181
|
+
l = 0;
|
8182
|
+
} else {
|
8183
|
+
l = Math.max(l, -inf_val);
|
8184
|
+
}
|
8185
|
+
}
|
7934
8186
|
fixed = '';
|
7935
8187
|
}
|
7936
8188
|
// NOTE: To see in the console whether fixing across rounds works, insert
|
7937
8189
|
// "fixed !== '' || " before DEBUGGING below.
|
7938
|
-
if(
|
7939
|
-
|
7940
|
-
|
8190
|
+
if(isNaN(l) || isNaN(u) ||
|
8191
|
+
typeof l !== 'number' || typeof u !== 'number' || DEBUGGING) {
|
8192
|
+
console.log(['set_bounds [', k, '] ', vbl.displayName, '[',
|
8193
|
+
VM.variables[vi - 1][0],'] t = ', VM.t, ' LB = ', VM.sig4Dig(l),
|
8194
|
+
', UB = ', VM.sig4Dig(u), fixed].join(''), l, u, inf_val);
|
7941
8195
|
}
|
7942
8196
|
// NOTE: Since the VM vectors for lower bounds and upper bounds are
|
7943
|
-
// initialized with default values (0 for LB, +INF for UB),
|
7944
|
-
//
|
7945
|
-
if(l !== 0
|
7946
|
-
|
8197
|
+
// initialized with default values (0 for LB, +INF for UB), the bounds
|
8198
|
+
// need only be set when they differ from these default values.
|
8199
|
+
if(l !== 0) VM.lower_bounds[k] = l;
|
8200
|
+
if(u < VM.SOLVER_PLUS_INFINITY) {
|
7947
8201
|
VM.upper_bounds[k] = u;
|
7948
8202
|
// If associated node is FROM-node of a "peak increase" link, then
|
7949
8203
|
// the "peak increase" variables of this node must have the highest
|
@@ -7960,6 +8214,26 @@ function VMI_set_bounds(args) {
|
|
7960
8214
|
VM.upper_bounds[cvi] = u;
|
7961
8215
|
VM.upper_bounds[cvi + 1] = u;
|
7962
8216
|
}
|
8217
|
+
// For grid elements, bounds must be set on UP and DOWN variables.
|
8218
|
+
if(vbl.grid) {
|
8219
|
+
// When considering losses, partition range 0...UB in sections.
|
8220
|
+
const step = (vbl.grid.loss_approximation < 2 ? u :
|
8221
|
+
u / vbl.grid.loss_approximation);
|
8222
|
+
VM.upper_bounds[VM.offset + vbl.up_1_var_index] = step;
|
8223
|
+
VM.upper_bounds[VM.offset + vbl.down_1_var_index] = step;
|
8224
|
+
if(vbl.grid.loss_approximation > 1) {
|
8225
|
+
// Set UB for semi-contiuous variables Up & Down slope 2.
|
8226
|
+
VM.upper_bounds[VM.offset + vbl.up_2_var_index] = 2 * step;
|
8227
|
+
VM.upper_bounds[VM.offset + vbl.down_2_var_index] = 2 * step;
|
8228
|
+
if(vbl.grid.loss_approximation > 2) {
|
8229
|
+
// Set UB for semi-contiuous variables Up & Down slope 3.
|
8230
|
+
VM.upper_bounds[VM.offset + vbl.up_3_var_index] = 3 * step;
|
8231
|
+
VM.upper_bounds[VM.offset + vbl.down_3_var_index] = 3 * step;
|
8232
|
+
}
|
8233
|
+
}
|
8234
|
+
// NOTE: lower bounds are 0 for all variables; their semi-continuous
|
8235
|
+
// ranges are set by VMI_add_grid_process_constraints.
|
8236
|
+
}
|
7963
8237
|
}
|
7964
8238
|
}
|
7965
8239
|
|
@@ -8191,6 +8465,28 @@ function VMI_subtract_var_from_coefficient(args) {
|
|
8191
8465
|
}
|
8192
8466
|
}
|
8193
8467
|
|
8468
|
+
/* AUXILIARY FUNCTIONS for setting cash flow coefficients */
|
8469
|
+
|
8470
|
+
function addCashIn(index, value) {
|
8471
|
+
if(index in VM.cash_in_coefficients) {
|
8472
|
+
// Add value to coefficient if it already exists...
|
8473
|
+
VM.cash_in_coefficients[index] += value;
|
8474
|
+
} else {
|
8475
|
+
// ... and set it if it is new.
|
8476
|
+
VM.cash_in_coefficients[index] = value;
|
8477
|
+
}
|
8478
|
+
}
|
8479
|
+
|
8480
|
+
function addCashOut(index, value) {
|
8481
|
+
if(index in VM.cash_out_coefficients) {
|
8482
|
+
// Add value to coefficient if it already exists...
|
8483
|
+
VM.cash_out_coefficients[index] += value;
|
8484
|
+
} else {
|
8485
|
+
// ... and set it if it is new.
|
8486
|
+
VM.cash_out_coefficients[index] = value;
|
8487
|
+
}
|
8488
|
+
}
|
8489
|
+
|
8194
8490
|
function VMI_update_cash_coefficient(args) {
|
8195
8491
|
// `args`: [flow, type, level_var_index, delay, x1, x2, ...]
|
8196
8492
|
// NOTE: Flow is either CONSUME or PRODUCE; type can be ONE_C (one
|
@@ -8265,17 +8561,9 @@ function VMI_update_cash_coefficient(args) {
|
|
8265
8561
|
VM.cash_out_rhs += pl * price_rate;
|
8266
8562
|
}
|
8267
8563
|
} else if(r > 0) {
|
8268
|
-
|
8269
|
-
VM.cash_in_coefficients[plk] += price_rate;
|
8270
|
-
} else {
|
8271
|
-
VM.cash_in_coefficients[plk] = price_rate;
|
8272
|
-
}
|
8564
|
+
addCashIn(plk, price_rate);
|
8273
8565
|
} else if(r < 0) {
|
8274
|
-
|
8275
|
-
VM.cash_out_coefficients[plk] -= price_rate;
|
8276
|
-
} else {
|
8277
|
-
VM.cash_out_coefficients[plk] = -price_rate;
|
8278
|
-
}
|
8566
|
+
addCashOut(plk, -price_rate);
|
8279
8567
|
}
|
8280
8568
|
}
|
8281
8569
|
// NOTE: For spinning reserve and highest increment, flow will always
|
@@ -8303,21 +8591,111 @@ function VMI_update_cash_coefficient(args) {
|
|
8303
8591
|
VM.cash_out_rhs -= knownValue(vi, VM.t - d) * r;
|
8304
8592
|
}
|
8305
8593
|
} else if(r > 0) {
|
8306
|
-
|
8307
|
-
VM.cash_in_coefficients[k] -= r;
|
8308
|
-
} else {
|
8309
|
-
VM.cash_in_coefficients[k] = -r;
|
8310
|
-
}
|
8594
|
+
addCashIn(k, -r);
|
8311
8595
|
} else if(r < 0) {
|
8312
8596
|
// NOTE: Test for r < 0 because no action is needed if r = 0.
|
8313
|
-
|
8314
|
-
|
8315
|
-
|
8316
|
-
|
8597
|
+
addCashOut(k, r);
|
8598
|
+
}
|
8599
|
+
}
|
8600
|
+
|
8601
|
+
function VMI_update_grid_process_cash_coefficients(p) {
|
8602
|
+
// Update cash flow coefficients for process `p` that relate to its
|
8603
|
+
// regular input and output link (data links are handled by means of
|
8604
|
+
// VMI_update_cash_coefficient).
|
8605
|
+
let fn = null,
|
8606
|
+
tn = null;
|
8607
|
+
for(let i = 0; i <= p.inputs.length; i++) {
|
8608
|
+
const l = p.inputs[i];
|
8609
|
+
if(l.multiplier === VM.LM_LEVEL &&
|
8610
|
+
!MODEL.ignored_entities[l.identifier]) {
|
8611
|
+
fn = l.from_node;
|
8612
|
+
break;
|
8613
|
+
}
|
8614
|
+
}
|
8615
|
+
for(let i = 0; i <= p.outputs.length; i++) {
|
8616
|
+
const l = p.outputs[i];
|
8617
|
+
if(l.multiplier === VM.LM_LEVEL &&
|
8618
|
+
!MODEL.ignored_entities[l.identifier]) {
|
8619
|
+
tn = l.to_node;
|
8620
|
+
break;
|
8621
|
+
}
|
8622
|
+
}
|
8623
|
+
const
|
8624
|
+
fp = (fn && fn.price.defined ? fn.price.result(VM.t) : 0),
|
8625
|
+
tp = (tn && tn.price.defined ? tn.price.result(VM.t) : 0);
|
8626
|
+
// Only proceed if process links to a product with a non-zero price.
|
8627
|
+
if(fp || tp) {
|
8628
|
+
const
|
8629
|
+
gpv = VM.gridProcessVarIndices(p, VM.offset),
|
8630
|
+
lr = p.lossRates(VM.t);
|
8631
|
+
if(fp > 0) {
|
8632
|
+
// If FROM node has price > 0, then all UP flows generate cash OUT
|
8633
|
+
// *without* loss while all DOWN flows generate cash IN *with* loss.
|
8634
|
+
for(let i = 0; i < gpv.slopes; i++) {
|
8635
|
+
addCashOut(gpv.up[i], -fp);
|
8636
|
+
addCashIn(gpv.down[i], (1 - lr[i]) * -fp);
|
8637
|
+
}
|
8638
|
+
} else if(fp < 0) {
|
8639
|
+
// If FROM node has price < 0, then all UP flows generate cash IN
|
8640
|
+
// *without* loss while all DOWN flows generate cash OUT *with* loss.
|
8641
|
+
for(let i = 0; i < gpv.slopes; i++) {
|
8642
|
+
addCashIn(gpv.up[i], fp);
|
8643
|
+
addCashOut(gpv.down[i], (1 - lr[i]) * fp);
|
8644
|
+
}
|
8645
|
+
}
|
8646
|
+
if(tp > 0) {
|
8647
|
+
// If TO node has price > 0, then all UP flows generate cash IN *with*
|
8648
|
+
// loss while all DOWN flows generate cash OUT *without* loss.
|
8649
|
+
for(let i = 0; i < gpv.slopes; i++) {
|
8650
|
+
addCashIn(gpv.up[i], (1 - lr[i]) * -tp);
|
8651
|
+
addCashOut(gpv.down[i], -tp);
|
8652
|
+
}
|
8653
|
+
} else if(tp < 0) {
|
8654
|
+
// If TO node has price < 0, then all UP flows generate cash OUT
|
8655
|
+
// *with* loss while all DOWN flows generate cash IN *without* loss.
|
8656
|
+
for(let i = 0; i < gpv.slopes; i++) {
|
8657
|
+
addCashOut(gpv.up[i], (1 - lr[i]) * tp);
|
8658
|
+
addCashIn(gpv.down[i], tp);
|
8659
|
+
}
|
8317
8660
|
}
|
8318
8661
|
}
|
8319
8662
|
}
|
8320
8663
|
|
8664
|
+
function VMI_add_power_flow_to_coefficients(args) {
|
8665
|
+
// Special instruction to add power flow rates represented by process
|
8666
|
+
// P to the coefficient vector that is being constructed to compute the
|
8667
|
+
// level for product Q.
|
8668
|
+
// The instruction is added once for the link P -> Q (then UP flows
|
8669
|
+
// add to the level of Q, while DOWN flows subtract) and once for the
|
8670
|
+
// link Q -> P (then UP flows *subtract* from the level of Q while
|
8671
|
+
// DOWN flows *add*).
|
8672
|
+
// The instruction expects two arguments: a grid process and an integer
|
8673
|
+
// indicating the direction: P -> Q (1) or Q -> P (-1).
|
8674
|
+
const
|
8675
|
+
p = args[0],
|
8676
|
+
up = args[1] > 0,
|
8677
|
+
gpv = VM.gridProcessVarIndices(p, VM.offset),
|
8678
|
+
lr = p.lossRates(VM.t);
|
8679
|
+
for(let i = 0; i < gpv.slopes; i++) {
|
8680
|
+
// Losses must be subtracted only from flows *into* P.
|
8681
|
+
const
|
8682
|
+
uv = (up ? 1 - lr[i] : -1),
|
8683
|
+
dv = (up ? -1 : 1 - lr[i]);
|
8684
|
+
let k = gpv.up[i];
|
8685
|
+
if(k in VM.coefficients) {
|
8686
|
+
VM.coefficients[k] += uv;
|
8687
|
+
} else {
|
8688
|
+
VM.coefficients[k] = uv;
|
8689
|
+
}
|
8690
|
+
k = gpv.down[i];
|
8691
|
+
if(k in VM.coefficients) {
|
8692
|
+
VM.coefficients[k] += dv;
|
8693
|
+
} else {
|
8694
|
+
VM.coefficients[k] = dv;
|
8695
|
+
}
|
8696
|
+
}
|
8697
|
+
}
|
8698
|
+
|
8321
8699
|
function VMI_add_throughput_to_coefficient(args) {
|
8322
8700
|
// Special instruction to deal with throughput calculation.
|
8323
8701
|
// Function: to add the contribution of variable X to the level of
|
@@ -8442,17 +8820,29 @@ function VMI_add_constraint(ct) {
|
|
8442
8820
|
row[i] = c;
|
8443
8821
|
}
|
8444
8822
|
}
|
8445
|
-
|
8823
|
+
// Special case:
|
8824
|
+
if(ct === VM.ACTOR_CASH) {
|
8825
|
+
VM.actor_cash_constraints.push(VM.matrix.length);
|
8826
|
+
ct = VM.EQ;
|
8827
|
+
}
|
8446
8828
|
let rhs = VM.rhs;
|
8447
|
-
|
8448
|
-
|
8449
|
-
|
8450
|
-
|
8451
|
-
|
8452
|
-
|
8453
|
-
|
8454
|
-
|
8455
|
-
|
8829
|
+
// Check for <= (near) +infinity and >= (near) -infinity: such
|
8830
|
+
// constraints should not be added to the model.
|
8831
|
+
if((ct === VM.LE && rhs >= 0.1 * VM.PLUS_INFINITY) ||
|
8832
|
+
(ct === VM.GE && rhs < 0.1 * VM.MINUS_INFINITY)) {
|
8833
|
+
if(DEBUGGING) console.log('Ignored infinite bound constraint');
|
8834
|
+
} else {
|
8835
|
+
VM.matrix.push(row);
|
8836
|
+
if(rhs >= VM.PLUS_INFINITY) {
|
8837
|
+
rhs = (VM.diagnose ? VM.DIAGNOSIS_UPPER_BOUND :
|
8838
|
+
VM.SOLVER_PLUS_INFINITY);
|
8839
|
+
} else if(rhs <= VM.MINUS_INFINITY) {
|
8840
|
+
rhs = (VM.diagnose ? -VM.DIAGNOSIS_UPPER_BOUND :
|
8841
|
+
VM.SOLVER_MINUS_INFINITY);
|
8842
|
+
}
|
8843
|
+
VM.right_hand_side.push(rhs);
|
8844
|
+
VM.constraint_types.push(ct);
|
8845
|
+
}
|
8456
8846
|
} else if(DEBUGGING) {
|
8457
8847
|
console.log('Constraint NOT added!');
|
8458
8848
|
}
|
@@ -8523,13 +8913,90 @@ function VMI_add_cash_constraints(args) {
|
|
8523
8913
|
VM.cash_out_rhs = 0;
|
8524
8914
|
}
|
8525
8915
|
|
8916
|
+
function VMI_add_grid_process_constraints(p) {
|
8917
|
+
// Add constraints that will ensure that power flows either UP or DOWN,
|
8918
|
+
// and that loss slopes properties are set.
|
8919
|
+
const gpv = VM.gridProcessVarIndices(p, VM.offset);
|
8920
|
+
if(!gpv) return;
|
8921
|
+
// Now the variable index lists all contain 1, 2 or 3 indices,
|
8922
|
+
// depending on the loss approximation level.
|
8923
|
+
let ub = p.upper_bound.result(VM.t);
|
8924
|
+
if(ub >= VM.PLUS_INFINITY) {
|
8925
|
+
// When UB = +INF, this is interpreted as "unlimited", which is
|
8926
|
+
// implemented as 99999 grid power units.
|
8927
|
+
ub = VM.UNLIMITED_POWER_FLOW;
|
8928
|
+
}
|
8929
|
+
const
|
8930
|
+
step = ub / gpv.slopes,
|
8931
|
+
// NOTE: For slope 1 use a small positive number as LB.
|
8932
|
+
lbs = [VM.ON_OFF_THRESHOLD, step, 2*step],
|
8933
|
+
ubs = [step, 2*step, 3*step];
|
8934
|
+
for(let i = 0; i < gpv.slopes; i++) {
|
8935
|
+
// Add constraints to set the ON/OFF binary for each slope:
|
8936
|
+
VMI_clear_coefficients();
|
8937
|
+
// level - UB*binary <= 0
|
8938
|
+
VM.coefficients[gpv.up[i]] = 1;
|
8939
|
+
VM.coefficients[gpv.up_on[i]] = -ubs[i];
|
8940
|
+
VMI_add_constraint(VM.LE);
|
8941
|
+
// level - LB*binary >= 0
|
8942
|
+
VM.coefficients[gpv.up_on[i]] = -lbs[i];
|
8943
|
+
VMI_add_constraint(VM.GE);
|
8944
|
+
// Two similar constraints for the Down slope
|
8945
|
+
VMI_clear_coefficients();
|
8946
|
+
VM.coefficients[gpv.down[i]] = 1;
|
8947
|
+
VM.coefficients[gpv.down_on[i]] = -ubs[i];
|
8948
|
+
VMI_add_constraint(VM.LE);
|
8949
|
+
VM.coefficients[gpv.down_on[i]] = -lbs[i];
|
8950
|
+
VMI_add_constraint(VM.GE);
|
8951
|
+
}
|
8952
|
+
// Set level to sum of all Up variables minus sum of all Down variables.
|
8953
|
+
VMI_clear_coefficients();
|
8954
|
+
VM.coefficients[VM.offset + p.level_var_index] = -1;
|
8955
|
+
for(let i = 0; i < gpv.slopes; i++) {
|
8956
|
+
VM.coefficients[gpv.up[i]] = 1;
|
8957
|
+
VM.coefficients[gpv.down[i]] = -1;
|
8958
|
+
}
|
8959
|
+
VMI_add_constraint(VM.EQ);
|
8960
|
+
// Set OO to the sum of all binary ON variables. This not only makes
|
8961
|
+
// OO available for read-out but also ensures that at most one slope
|
8962
|
+
// can be active (because OO is binary).
|
8963
|
+
VMI_clear_coefficients();
|
8964
|
+
VM.coefficients[VM.offset + p.on_off_var_index] = -1;
|
8965
|
+
for(let i = 0; i < gpv.slopes; i++) {
|
8966
|
+
VM.coefficients[gpv.up_on[i]] = 1;
|
8967
|
+
VM.coefficients[gpv.down_on[i]] = 1;
|
8968
|
+
}
|
8969
|
+
VMI_add_constraint(VM.EQ);
|
8970
|
+
}
|
8971
|
+
|
8972
|
+
function VMI_add_kirchhoff_constraints(cb) {
|
8973
|
+
// Add Kirchhoff's voltage law constraint for each cycle in `cb`.
|
8974
|
+
// NOTE: Do not add a constraint for cyles that have been "broken"
|
8975
|
+
// because one or more of its processes have UB = 0.
|
8976
|
+
for(let i = 0; i < cb.length; i++) {
|
8977
|
+
const c = cb[i];
|
8978
|
+
let not_broken = true;
|
8979
|
+
VMI_clear_coefficients();
|
8980
|
+
for(let j = 0; j < c.length; j++) {
|
8981
|
+
const
|
8982
|
+
p = c[j].process,
|
8983
|
+
x = p.length_in_km * p.grid.reactancePerKm,
|
8984
|
+
o = c[j].orientation,
|
8985
|
+
ub = p.upper_bound.result(VM.t);
|
8986
|
+
if(ub <= VM.NEAR_ZERO) {
|
8987
|
+
not_broken = false;
|
8988
|
+
break;
|
8989
|
+
}
|
8990
|
+
VM.coefficients[VM.offset + p.level_var_index] = x * o;
|
8991
|
+
}
|
8992
|
+
if(not_broken) VMI_add_constraint(VM.EQ);
|
8993
|
+
}
|
8994
|
+
}
|
8995
|
+
|
8526
8996
|
function VMI_add_bound_line_constraint(args) {
|
8527
8997
|
// `args`: [variable index for X, LB expression for X, UB expression for X,
|
8528
8998
|
// variable index for Y, LB expression for Y, UB expression for Y,
|
8529
|
-
// boundline object
|
8530
|
-
// The `use_binaries` flag can be determined at compile time, as bound
|
8531
|
-
// lines are not dynamic. When use_binaries = TRUE, additional constraints
|
8532
|
-
// on binary variables are needed (see below).
|
8999
|
+
// boundline object]
|
8533
9000
|
const
|
8534
9001
|
vix = args[0],
|
8535
9002
|
vx = VM.variables[vix - 1], // `variables` is zero-based!
|
@@ -8540,14 +9007,22 @@ function VMI_add_bound_line_constraint(args) {
|
|
8540
9007
|
objy= vy[1],
|
8541
9008
|
uby = args[5].result(VM.t),
|
8542
9009
|
bl = args[6],
|
8543
|
-
use_binaries = args[7],
|
8544
9010
|
n = bl.points.length,
|
8545
9011
|
x = new Array(n),
|
8546
9012
|
y = new Array(n),
|
8547
9013
|
w = new Array(n);
|
9014
|
+
// Set bound line point coordinates for current run and time step.
|
9015
|
+
bl.setDynamicPoints(VM.t);
|
8548
9016
|
if(DEBUGGING) {
|
8549
9017
|
console.log('add_bound_line_constraint:', bl.displayName);
|
8550
9018
|
}
|
9019
|
+
// Do not add constraints for bound lines that set no infeasible area.
|
9020
|
+
if(!bl.constrainsY) {
|
9021
|
+
if(DEBUGGING) {
|
9022
|
+
console.log('SKIP because bound line does not constrain');
|
9023
|
+
}
|
9024
|
+
return;
|
9025
|
+
}
|
8551
9026
|
// NOTE: For semi-continuous processes, lower bounds > 0 should to be
|
8552
9027
|
// adjusted to 0, as then 0 is part of the process level range.
|
8553
9028
|
let lbx = args[1].result(VM.t),
|
@@ -8572,6 +9047,11 @@ function VMI_add_bound_line_constraint(args) {
|
|
8572
9047
|
// For LE and GE type bound lines, one slack variable suffices, and = 0 must
|
8573
9048
|
// be, respectively, <= 0 or >= 0
|
8574
9049
|
|
9050
|
+
// Since version 2.0.0, the `use_binaries` flag can no longer be determined
|
9051
|
+
// at compile time, as bound lines may be dynamic. When use_binaries = TRUE,
|
9052
|
+
// additional constraints on binary variables are needed (see below).
|
9053
|
+
let use_binaries = VM.noSupportForSOS && !bl.needsNoSOS;
|
9054
|
+
|
8575
9055
|
// Scale X and Y and compute the block indices of w[i]
|
8576
9056
|
let wi = VM.offset + bl.first_sos_var_index;
|
8577
9057
|
const
|
@@ -8629,8 +9109,7 @@ function VMI_add_bound_line_constraint(args) {
|
|
8629
9109
|
// and then to ensure that at most 2 binaries can be 1:
|
8630
9110
|
// b[1] + ... + b[N] <= 2
|
8631
9111
|
// NOTE: These additional variables and constraints are not needed
|
8632
|
-
// when a bound line defines a convex feasible area.
|
8633
|
-
// parameter takes this into account.
|
9112
|
+
// when a bound line defines a convex feasible area.
|
8634
9113
|
if(use_binaries) {
|
8635
9114
|
// Add the constraints mentioned above. The index of b[i] is the
|
8636
9115
|
// index of w[i] plus the number of points on the boundline N.
|