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.
Files changed (39) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +172 -126
  3. package/console.js +2 -1
  4. package/package.json +1 -1
  5. package/post-install.js +93 -37
  6. package/server.js +73 -29
  7. package/static/images/eq-negated.png +0 -0
  8. package/static/images/power.png +0 -0
  9. package/static/images/tex.png +0 -0
  10. package/static/index.html +226 -11
  11. package/static/linny-r.css +458 -8
  12. package/static/scripts/linny-r-ctrl.js +6 -4
  13. package/static/scripts/linny-r-gui-actor-manager.js +1 -1
  14. package/static/scripts/linny-r-gui-chart-manager.js +20 -13
  15. package/static/scripts/linny-r-gui-constraint-editor.js +410 -50
  16. package/static/scripts/linny-r-gui-controller.js +138 -21
  17. package/static/scripts/linny-r-gui-dataset-manager.js +28 -20
  18. package/static/scripts/linny-r-gui-documentation-manager.js +11 -3
  19. package/static/scripts/linny-r-gui-equation-manager.js +1 -1
  20. package/static/scripts/linny-r-gui-experiment-manager.js +1 -1
  21. package/static/scripts/linny-r-gui-expression-editor.js +7 -1
  22. package/static/scripts/linny-r-gui-file-manager.js +63 -19
  23. package/static/scripts/linny-r-gui-finder.js +1 -1
  24. package/static/scripts/linny-r-gui-model-autosaver.js +1 -1
  25. package/static/scripts/linny-r-gui-monitor.js +1 -1
  26. package/static/scripts/linny-r-gui-paper.js +108 -25
  27. package/static/scripts/linny-r-gui-power-grid-manager.js +529 -0
  28. package/static/scripts/linny-r-gui-receiver.js +1 -1
  29. package/static/scripts/linny-r-gui-repository-browser.js +1 -1
  30. package/static/scripts/linny-r-gui-scale-unit-manager.js +1 -1
  31. package/static/scripts/linny-r-gui-sensitivity-analysis.js +1 -1
  32. package/static/scripts/linny-r-gui-tex-manager.js +110 -0
  33. package/static/scripts/linny-r-gui-undo-redo.js +1 -1
  34. package/static/scripts/linny-r-milp.js +1 -1
  35. package/static/scripts/linny-r-model.js +982 -123
  36. package/static/scripts/linny-r-utils.js +3 -3
  37. package/static/scripts/linny-r-vm.js +731 -252
  38. package/static/show-diff.html +1 -1
  39. 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-2023 Delft University of Technology
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, use the longest prefix that
667
- // their nodes have in common.
668
- this.owner_prefix = UI.sharedPrefix(owner.from_node.displayName,
669
- owner.to_node.displayName) + UI.PREFIXER;
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.PLUS_INFINITY = 1e+25;
2138
- this.MINUS_INFINITY = -1e+25;
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 1e+30 value is recognized by all supported solvers as "infinity",
2142
- // and hence can be used to indicate that a variable has no upper bound.
2143
- this.SOLVER_PLUS_INFINITY = 1e+30;
2144
- this.SOLVER_MINUS_INFINITY = -1e+30;
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 constraints.
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].slack_info = {};
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
- if(a >= 999999.5) return n.toPrecision(2);
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 true 0 by a leading + or - sign.
2581
- if(a <= this.ON_OFF_THRESHOLD) {
2582
- // The `tiny` flag indicates: display small number in E-notation.
2583
- if(tiny) return n.toPrecision(1);
2584
- return n > 0 ? '+0' : '-0';
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.points.length;
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.is_zero_var_index = this.addVariable('IZ', 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(this.block_count, 'DIAGNOSTIC RUN' +
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(this.block_count,
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.isActive && bl.constrainsY) {
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) ubx = ubx.result(0);
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
- // Iterate over links IN, but only consider consumed products
3486
- // having a market price.
3487
- for(j = 0; j < p.inputs.length; j++) {
3488
- l = p.inputs[j];
3489
- if(!MODEL.ignored_entities[l.identifier] &&
3490
- l.from_node.price.defined) {
3491
- if(l.from_node.price.isStatic && l.relative_rate.isStatic) {
3492
- k = l.from_node.price.result(0) * l.relative_rate.result(0);
3493
- // NOTE: VMI_update_cash_coefficient has at least 4 arguments:
3494
- // flow (CONSUME or PRODUCE), type (specifies the number and
3495
- // type of arguments), the level_var_index of the process,
3496
- // and the delay.
3497
- // NOTE: Input links cannot have delay, so then delay = 0.
3498
- if(Math.abs(k) > VM.NEAR_ZERO) {
3499
- // Consumption rate & price are static: pass one constant.
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.ONE_C, p.level_var_index, 0, k]]);
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
- } // END of FOR ALL input links
3511
-
3512
- // Iterate over links OUT, but only consider produced products
3513
- // having a (non-zero) market price.
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 tnpx = l.to_node.price;
3517
- if(!MODEL.ignored_entities[l.identifier] && tnpx.defined &&
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
- this.code[this.code.length - 1][0] !== VMI_update_cash_coefficient;
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
- this.code.push([VMI_add_constraint, VM.EQ]);
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 for throughput links, as these are elaborate
3822
- if(l.multiplier === VM.LM_THROUGHPUT) {
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
- // subtract outflow from product P to consuming processes (outputs)
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
- if(l.to_node instanceof Process) {
3945
- const rr = l.relative_rate;
3946
- if(rr.isStatic) {
3947
- this.code.push([VMI_subtract_const_from_coefficient,
3948
- [l.to_node.level_var_index, rr.result(0), l.flow_delay]]);
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
- this.code.push([VMI_subtract_var_from_coefficient,
3951
- [l.to_node.level_var_index, rr, l.flow_delay]]);
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 ? p.lower_bound : p.upper_bound);
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
- // NOTE: if UB <= 0 the following constraints will be prepared but NOT added
4097
-
4098
- this.code.push(
4099
- // Set coefficients vector to 0
4100
- [VMI_clear_coefficients, null],
4101
- // (a) L[t] - LB[t]*OO[t] >= 0
4102
- [VMI_add_const_to_coefficient, [p.level_var_index, 1]]
4103
- );
4104
- if(p.lower_bound.isStatic) {
4105
- let lb = p.lower_bound.result(0);
4106
- if(Math.abs(lb) < VM.NEAR_ZERO) lb = VM.ON_OFF_THRESHOLD;
4107
- this.code.push([VMI_subtract_const_from_coefficient,
4108
- [p.on_off_var_index, lb]]);
4109
- } else {
4110
- this.code.push([VMI_subtract_var_from_coefficient,
4111
- // NOTE: the 3rd parameter signals VM to use the ON/OFF threshold
4112
- // value when the LB evaluates as near-zero
4113
- [p.on_off_var_index, p.lower_bound, VM.ON_OFF_THRESHOLD]]);
4114
- }
4115
- this.code.push(
4116
- [VMI_add_constraint, VM.GE], // >= 0 as default RHS = 0
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
- if(hub !== ub) {
4140
- ub = hub;
4141
- this.logMessage(this.block_count,
4142
- `Inferred upper bound for ${p.displayName}: ${this.sig4Dig(ub)}`);
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([VMI_subtract_const_from_coefficient,
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
- this.code.push(
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
- [VMI_clear_coefficients, null],
4156
- // (c) OO[t] + IZ[t] = 1
4157
- [VMI_add_const_to_coefficient, [p.is_zero_var_index, 1]],
4158
- [VMI_add_const_to_coefficient, [p.on_off_var_index, 1]],
4159
- [VMI_set_const_rhs, 1],
4160
- [VMI_add_constraint, VM.EQ],
4161
- // (d) L[t] + IZ[t] >= LB[t]
4162
- [VMI_clear_coefficients, null],
4163
- [VMI_add_const_to_coefficient, [p.level_var_index, 1]],
4164
- [VMI_add_const_to_coefficient, [p.is_zero_var_index, 1]]
4165
- );
4166
- // NOTE: for semicontinuous variable, always use LB = 0
4167
- if(p.lower_bound.isStatic || p.level_to_zero) {
4168
- const plb = (p.level_to_zero ? 0 : p.lower_bound.result(0));
4169
- this.code.push([VMI_set_const_rhs, plb]);
4170
- } else {
4171
- this.code.push([VMI_set_var_rhs, p.lower_bound]);
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
- [VMI_toggle_add_constraints_flag, null],
4275
- // When UB <= 0, add these much simpler "exceptional" constraints:
4276
- // OO[t] = 0
4277
- [VMI_clear_coefficients, null],
4278
- [VMI_add_const_to_coefficient, [p.on_off_var_index, 1]],
4279
- [VMI_add_constraint, VM.EQ],
4280
- // IZ[t] = 1
4281
- [VMI_clear_coefficients, null],
4282
- [VMI_add_const_to_coefficient, [p.is_zero_var_index, 1]],
4283
- [VMI_set_const_rhs, 1], // RHS = 1
4284
- [VMI_add_constraint, VM.EQ]
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
- const bl = c.bound_lines[j];
4353
- // Only add constrains for bound lines that are "active" for the
4354
- // current run, and do constrain Y in some way.
4355
- if(bl.isActive && bl.constrainsY) {
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(this.block_count,
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 IN = ' + a.cash_out[b].toPrecision(2));
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
- const
4861
- rr = l.relative_rate.result(bt),
4862
- af = this.severestIssue([pl, rr], rr * pl);
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
- line = '';
5767
+ v = vbl(p);
5531
5768
  if(lb === ub) {
5532
- if(lb !== null) line = ` ${vbl(p)} = ${lb}`;
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
- line = ` ${vbl(p)}`;
5536
- if(cplex) {
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
- // NOTE: LP_solve has Infinity value 1e+25. Use this literal
5548
- // because VM.PLUS_INFINITY may be set to *diagnostic* value.
5549
- if(lb !== null && lb !== 0 && lb > -1e+25) {
5550
- line = lb + ' <= ' + 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 = 1e+25;
6194
- this.MINUS_INFINITY = -1e+25;
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
- inf_val = (VM.diagnose && !(args.length > 3 && args[3]) ?
7907
- VM.DIAGNOSIS_UPPER_BOUND : VM.SOLVER_PLUS_INFINITY);
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, VM.PLUS_INFINITY);
7932
- if(l === VM.MINUS_INFINITY) l = -inf_val;
7933
- if(u === VM.PLUS_INFINITY) u = inf_val;
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(DEBUGGING) {
7939
- console.log(['set_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
7940
- ' LB = ', VM.sig4Dig(l), ', UB = ', VM.sig4Dig(u), fixed, l, u, inf_val].join(''));
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), there is
7944
- // no need to set them.
7945
- if(l !== 0 || u < inf_val) {
7946
- VM.lower_bounds[k] = l;
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
- if(plk in VM.cash_in_coefficients) {
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
- if(plk in VM.cash_out_coefficients) {
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
- if(k in VM.cash_in_coefficients) {
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
- if(k in VM.cash_out_coefficients) {
8314
- VM.cash_out_coefficients[k] += r;
8315
- } else {
8316
- VM.cash_out_coefficients[k] = r;
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
- VM.matrix.push(row);
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
- if(rhs >= VM.PLUS_INFINITY) {
8448
- rhs = (VM.diagnose ? VM.DIAGNOSIS_UPPER_BOUND :
8449
- VM.SOLVER_PLUS_INFINITY);
8450
- } else if(rhs <= VM.MINUS_INFINITY) {
8451
- rhs = (VM.diagnose ? -VM.DIAGNOSIS_UPPER_BOUND :
8452
- VM.SOLVER_MINUS_INFINITY);
8453
- }
8454
- VM.right_hand_side.push(rhs);
8455
- VM.constraint_types.push(ct);
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, useBinaries]
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. The `use_binaries`
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.