linny-r 1.9.2 → 2.0.1

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 (37) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +4 -4
  3. package/package.json +1 -1
  4. package/server.js +1 -1
  5. package/static/images/eq-negated.png +0 -0
  6. package/static/images/power.png +0 -0
  7. package/static/images/tex.png +0 -0
  8. package/static/index.html +225 -10
  9. package/static/linny-r.css +458 -8
  10. package/static/scripts/linny-r-ctrl.js +6 -4
  11. package/static/scripts/linny-r-gui-actor-manager.js +1 -1
  12. package/static/scripts/linny-r-gui-chart-manager.js +20 -13
  13. package/static/scripts/linny-r-gui-constraint-editor.js +410 -50
  14. package/static/scripts/linny-r-gui-controller.js +127 -12
  15. package/static/scripts/linny-r-gui-dataset-manager.js +28 -20
  16. package/static/scripts/linny-r-gui-documentation-manager.js +11 -3
  17. package/static/scripts/linny-r-gui-equation-manager.js +1 -1
  18. package/static/scripts/linny-r-gui-experiment-manager.js +1 -1
  19. package/static/scripts/linny-r-gui-expression-editor.js +7 -1
  20. package/static/scripts/linny-r-gui-file-manager.js +31 -13
  21. package/static/scripts/linny-r-gui-finder.js +1 -1
  22. package/static/scripts/linny-r-gui-model-autosaver.js +1 -1
  23. package/static/scripts/linny-r-gui-monitor.js +1 -1
  24. package/static/scripts/linny-r-gui-paper.js +108 -25
  25. package/static/scripts/linny-r-gui-power-grid-manager.js +529 -0
  26. package/static/scripts/linny-r-gui-receiver.js +1 -1
  27. package/static/scripts/linny-r-gui-repository-browser.js +1 -1
  28. package/static/scripts/linny-r-gui-scale-unit-manager.js +1 -1
  29. package/static/scripts/linny-r-gui-sensitivity-analysis.js +1 -1
  30. package/static/scripts/linny-r-gui-tex-manager.js +110 -0
  31. package/static/scripts/linny-r-gui-undo-redo.js +1 -1
  32. package/static/scripts/linny-r-milp.js +1 -1
  33. package/static/scripts/linny-r-model.js +1016 -155
  34. package/static/scripts/linny-r-utils.js +3 -3
  35. package/static/scripts/linny-r-vm.js +714 -248
  36. package/static/show-diff.html +1 -1
  37. 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.
@@ -2024,7 +2024,7 @@ class ExpressionParser {
2024
2024
  this.error = 'Missing operand';
2025
2025
  } else if(this.sym_stack > 1) {
2026
2026
  this.error = 'Missing operator';
2027
- } else if(this.concatenating) {
2027
+ } else if(this.concatenating && !(this.owner instanceof BoundLine)) {
2028
2028
  this.error = 'Invalid parameter list';
2029
2029
  }
2030
2030
  }
@@ -2121,6 +2121,7 @@ class VirtualMachine {
2121
2121
  this.solver_secs = [];
2122
2122
  this.messages = [];
2123
2123
  this.equations = [];
2124
+
2124
2125
  // Default texts to display for (still) empty results.
2125
2126
  this.no_messages = '(no messages)';
2126
2127
  this.no_variables = '(no variables)';
@@ -2134,18 +2135,23 @@ class VirtualMachine {
2134
2135
  // decision variables reach +INF (1e+30) or -INF (-1e+30), and a solution
2135
2136
  // inaccurate if extreme values get too close to +/-INF. The higher
2136
2137
  // values have been chosen arbitrarily.
2137
- this.PLUS_INFINITY = 1e+25;
2138
- this.MINUS_INFINITY = -1e+25;
2138
+ this.SOLVER_PLUS_INFINITY = 1e+25;
2139
+ this.SOLVER_MINUS_INFINITY = -1e+25;
2139
2140
  this.BEYOND_PLUS_INFINITY = 1e+35;
2140
2141
  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;
2142
+ // The VM properties "PLUS_INFINITY" and "MINUS_INFINITY" are used
2143
+ // when evaluating expressions. These propeties may be changed for
2144
+ // diagnostic purposes -- see below.
2145
+ this.PLUS_INFINITY = 1e+25;
2146
+ this.MINUS_INFINITY = -1e+25;
2145
2147
  // As of version 1.8.0, Linny-R imposes no +INF bounds on processes
2146
2148
  // unless diagnosing an unbounded problem. For such diagnosis, the
2147
2149
  // (relatively) low value 9.999999999e+9 is used.
2148
2150
  this.DIAGNOSIS_UPPER_BOUND = 9.999999999e+9;
2151
+ // For processes representing grid elements, upper bounds of +INF are
2152
+ // "capped" to 9999 grid element capacity units (typically MW for
2153
+ // high voltage grids).
2154
+ this.UNLIMITED_POWER_FLOW = 9999;
2149
2155
  // NOTE: Below the "near zero" limit, a number is considered zero
2150
2156
  // (this is to timely detect division-by-zero errors).
2151
2157
  this.NEAR_ZERO = 1e-10;
@@ -2257,6 +2263,7 @@ class VirtualMachine {
2257
2263
  this.LE = 1;
2258
2264
  this.GE = 2;
2259
2265
  this.EQ = 3;
2266
+ this.ACTOR_CASH = 4;
2260
2267
 
2261
2268
  this.constraint_codes = ['FR', 'LE', 'GE', 'EQ'];
2262
2269
  this.constraint_symbols = ['', '<=', '>=', '='];
@@ -2392,14 +2399,18 @@ class VirtualMachine {
2392
2399
  // Reset the virtual machine so that it can execute the model again.
2393
2400
  // First reset the expression attributes of all model entities.
2394
2401
  MODEL.resetExpressions();
2395
- // Clear slack use information for all constraints.
2402
+ // Clear slack use information and boundline point coordinates for all
2403
+ // constraints.
2396
2404
  for(let k in MODEL.constraints) if(MODEL.constraints.hasOwnProperty(k)) {
2397
- MODEL.constraints[k].slack_info = {};
2405
+ MODEL.constraints[k].reset();
2398
2406
  }
2399
2407
  // Likewise, clear slack use information for all clusters.
2400
2408
  for(let k in MODEL.clusters) if(MODEL.clusters.hasOwnProperty(k)) {
2401
2409
  MODEL.clusters[k].slack_info = {};
2402
2410
  }
2411
+ if(MODEL.with_power_flow) {
2412
+ POWER_GRID_MANAGER.checkLengths();
2413
+ }
2403
2414
  // Clear the expression call stack -- used only for diagnostics.
2404
2415
  this.call_stack.length = 0;
2405
2416
  // The out-of-bounds properties are set when the ARRAY_INDEX error
@@ -2559,12 +2570,20 @@ class VirtualMachine {
2559
2570
  const a = Math.abs(n);
2560
2571
  // Signal small differences from true 0 by leading + or - sign.
2561
2572
  if(n !== 0 && a <= this.ON_OFF_THRESHOLD) return n > 0 ? '+0' : '-0';
2562
- if(a >= 999999.5) return n.toPrecision(2);
2573
+ /*
2574
+ if(a >= 9999.5) return n.toPrecision(2);
2563
2575
  if(Math.abs(a-Math.round(a)) < 0.05) return Math.round(n);
2564
2576
  if(a < 1) return Math.round(n*100) / 100;
2565
2577
  if(a < 10) return Math.round(n*10) / 10;
2566
2578
  if(a < 100) return Math.round(n*10) / 10;
2567
2579
  return Math.round(n);
2580
+ */
2581
+ let s = n.toString();
2582
+ const prec = n.toPrecision(2);
2583
+ if(prec.length < s.length) s = prec;
2584
+ const expo = n.toExponential(1);
2585
+ if(expo.length < s.length) s = expo;
2586
+ return s;
2568
2587
  }
2569
2588
 
2570
2589
  sig4Dig(n, tiny=false) {
@@ -2577,19 +2596,25 @@ class VirtualMachine {
2577
2596
  if(sv[0]) return sv[1];
2578
2597
  const a = Math.abs(n);
2579
2598
  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);
2599
+ // Signal small differences from exactly 0 by a leading + or - sign
2600
+ // except when the `tiny` flag is set.
2601
+ if(a <= this.ON_OFF_THRESHOLD && !tiny) return n > 0 ? '+0' : '-0';
2602
+ /*
2603
+ if(a >= 9999.5) return n.toPrecision(4);
2587
2604
  if(Math.abs(a-Math.round(a)) < 0.0005) return Math.round(n);
2588
2605
  if(a < 1) return Math.round(n*10000) / 10000;
2589
2606
  if(a < 10) return Math.round(n*1000) / 1000;
2590
2607
  if(a < 100) return Math.round(n*100) / 100;
2591
2608
  if(a < 1000) return Math.round(n*10) / 10;
2592
2609
  return Math.round(n);
2610
+ */
2611
+ let s = n.toString();
2612
+ const prec = n.toPrecision(4);
2613
+ if(prec.length < s.length) s = prec;
2614
+ const expo = n.toExponential(2);
2615
+ if(expo.length < s.length) s = expo;
2616
+ if(s.indexOf('e') < 0) s = parseFloat(s).toString();
2617
+ return s;
2593
2618
  }
2594
2619
 
2595
2620
  //
@@ -2934,7 +2959,7 @@ class VirtualMachine {
2934
2959
  }
2935
2960
  if(type === 'I' || type === 'PiL') {
2936
2961
  this.int_var_indices[index] = true;
2937
- } else if('OO|IZ|SU|SD|SO|FC|SB'.indexOf(type) >= 0) {
2962
+ } else if('OO|IZ|SU|SD|SO|FC|SB|UO1|DO1|UO2|DO2|UO3|DO3'.indexOf(type) >= 0) {
2938
2963
  this.bin_var_indices[index] = true;
2939
2964
  }
2940
2965
  if(obj instanceof Process && obj.pace > 1) {
@@ -2944,7 +2969,7 @@ class VirtualMachine {
2944
2969
  // For constraint bound lines, add as many SOS variables as there
2945
2970
  // are points on the bound line.
2946
2971
  if(type === 'W1' && obj instanceof BoundLine) {
2947
- const n = obj.points.length;
2972
+ const n = obj.maxPoints;
2948
2973
  for(let i = 2; i <= n; i++) {
2949
2974
  this.variables.push(['W' + i, obj]);
2950
2975
  }
@@ -2967,6 +2992,33 @@ class VirtualMachine {
2967
2992
  return index;
2968
2993
  }
2969
2994
 
2995
+ gridProcessVarIndices(p, offset=0) {
2996
+ // Return an object with lists of 1, 2 or 3 slope variable indices.
2997
+ if(p.up_1_var_index <= 0) return null;
2998
+ const gpv = {
2999
+ slopes: 1,
3000
+ up: [p.up_1_var_index + offset],
3001
+ up_on: [p.up_1_on_var_index + offset],
3002
+ down: [p.down_1_var_index + offset],
3003
+ down_on: [p.down_1_on_var_index + offset]
3004
+ };
3005
+ if(p.up_2_var_index >= 0) {
3006
+ gpv.slopes++;
3007
+ gpv.up.push(p.up_2_var_index + offset);
3008
+ gpv.up_on.push(p.up_2_on_var_index + offset);
3009
+ gpv.down.push(p.down_2_var_index + offset);
3010
+ gpv.down_on.push(p.down_2_on_var_index + offset);
3011
+ if(p.up_3_var_index >= 0) {
3012
+ gpv.slopes++;
3013
+ gpv.up.push(p.up_3_var_index + offset);
3014
+ gpv.up_on.push(p.up_3_on_var_index + offset);
3015
+ gpv.down.push(p.down_3_var_index + offset);
3016
+ gpv.down_on.push(p.down_3_on_var_index + offset);
3017
+ }
3018
+ }
3019
+ return gpv;
3020
+ }
3021
+
2970
3022
  resetVariableIndices(p) {
2971
3023
  // Set all variable indices to -1 ("no such variable") for node `p`.
2972
3024
  p.level_var_index = -1;
@@ -2983,6 +3035,19 @@ class VirtualMachine {
2983
3035
  p.stock_GE_slack_var_index = -1;
2984
3036
  } else {
2985
3037
  p.semic_var_index = -1;
3038
+ // Additional indices for grid elements.
3039
+ p.up_1_var_index = -1;
3040
+ p.up_1_on_var_index = -1;
3041
+ p.down_1_var_index = -1;
3042
+ p.down_1_on_var_index = -1;
3043
+ p.up_2_var_index = -1;
3044
+ p.up_2_on_var_index = -1;
3045
+ p.down_2_var_index = -1;
3046
+ p.down_2_on_var_index = -1;
3047
+ p.up_3_var_index = -1;
3048
+ p.up_3_on_var_index = -1;
3049
+ p.down_3_var_index = -1;
3050
+ p.down_3_on_var_index = -1;
2986
3051
  }
2987
3052
  }
2988
3053
 
@@ -3001,7 +3066,9 @@ class VirtualMachine {
3001
3066
  // Some "data-only" link multipliers require additional variables.
3002
3067
  if(p.needsOnOffData) {
3003
3068
  p.on_off_var_index = this.addVariable('OO', p);
3004
- p.is_zero_var_index = this.addVariable('IZ', p);
3069
+ if(p.needsIsZeroData) {
3070
+ p.is_zero_var_index = this.addVariable('IZ', p);
3071
+ }
3005
3072
  // To detect startup, one more variable is needed
3006
3073
  if(p.needsStartUpData) {
3007
3074
  p.start_up_var_index = this.addVariable('SU', p);
@@ -3017,6 +3084,27 @@ class VirtualMachine {
3017
3084
  p.shut_down_var_index = this.addVariable('SD', p);
3018
3085
  }
3019
3086
  }
3087
+ if(p.grid) {
3088
+ // Processes representing power grid elements are bi-directional
3089
+ // and hence need separate UP and DOWN flow variables.
3090
+ p.up_1_var_index = this.addVariable('U1', p);
3091
+ p.up_1_on_var_index = this.addVariable('UO1', p);
3092
+ p.down_1_var_index = this.addVariable('D1', p);
3093
+ p.down_1_on_var_index = this.addVariable('DO1', p);
3094
+ // Additional UP and DOWN is needed for each additional loss slope.
3095
+ if(p.grid.loss_approximation > 1) {
3096
+ p.up_2_var_index = this.addVariable('U2', p);
3097
+ p.up_2_on_var_index = this.addVariable('UO2', p);
3098
+ p.down_2_var_index = this.addVariable('D2', p);
3099
+ p.down_2_on_var_index = this.addVariable('DO2', p);
3100
+ if(p.grid.loss_approximation > 2) {
3101
+ p.up_3_var_index = this.addVariable('U3', p);
3102
+ p.up_3_on_var_index = this.addVariable('UO3', p);
3103
+ p.down_3_var_index = this.addVariable('D3', p);
3104
+ p.down_3_on_var_index = this.addVariable('DO3', p);
3105
+ }
3106
+ }
3107
+ }
3020
3108
  // NOTES:
3021
3109
  // (1) Processes have NO slack variables, because sufficient slack is
3022
3110
  // provided by adding slack variables to products; these slack
@@ -3222,7 +3310,7 @@ class VirtualMachine {
3222
3310
 
3223
3311
  // Log if run is performed in "diagnosis" mode.
3224
3312
  if(this.diagnose) {
3225
- this.logMessage(this.block_count, 'DIAGNOSTIC RUN' +
3313
+ this.logMessage(1, 'DIAGNOSTIC RUN' +
3226
3314
  (MODEL.always_diagnose ? ' (default -- see model settings)': '') +
3227
3315
  '\n- slack variables on products and constraints' +
3228
3316
  '\n- finite bounds on all processes');
@@ -3232,10 +3320,25 @@ class VirtualMachine {
3232
3320
  MODEL.inferIgnoredEntities();
3233
3321
  const n = Object.keys(MODEL.ignored_entities).length;
3234
3322
  if(n > 0) {
3235
- this.logMessage(this.block_count,
3323
+ this.logMessage(1,
3236
3324
  pluralS(n, 'entity', 'entities') + ' will be ignored');
3237
3325
  }
3238
3326
 
3327
+ // Infer cycle basis for combined power grids for which Kirchhoff's
3328
+ // voltage law must be enforced.
3329
+ if(MODEL.with_power_flow) {
3330
+ MONITOR.logMessage(1, 'POWER FLOW: ' +
3331
+ pluralS(Object.keys(MODEL.power_grids).length, 'grid'));
3332
+ POWER_GRID_MANAGER.inferCycleBasis();
3333
+ if(POWER_GRID_MANAGER.messages.length > 1) {
3334
+ UI.warn('Check monitor for power grid warnings');
3335
+ }
3336
+ MONITOR.logMessage(1, POWER_GRID_MANAGER.messages.join('\n'));
3337
+ if(POWER_GRID_MANAGER.cycle_basis.length) this.logMessage(1,
3338
+ 'Enforcing Kirchhoff\'s voltage law for ' +
3339
+ POWER_GRID_MANAGER.cycleBasisAsString);
3340
+ }
3341
+
3239
3342
  // FIRST: Define indices for all variables (index = Simplex tableau
3240
3343
  // column number).
3241
3344
 
@@ -3288,7 +3391,7 @@ class VirtualMachine {
3288
3391
  for(l = 0; l < c.bound_lines.length; l++) {
3289
3392
  const bl = c.bound_lines[l];
3290
3393
  bl.sos_var_indices = [];
3291
- if(bl.isActive && bl.constrainsY) {
3394
+ if(bl.constrainsY) {
3292
3395
  // Define SOS2 variables w[i] (plus associated binaries if
3293
3396
  // solver does not support special ordered sets).
3294
3397
  // NOTE: `addVariable` will add as many as there are points!
@@ -3379,9 +3482,16 @@ class VirtualMachine {
3379
3482
  // NOTE: If UB = LB, set UB to LB only if LB is defined,
3380
3483
  // because LB expressions default to -INF while UB expressions
3381
3484
  // default to +INF.
3382
- ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
3485
+ ubx = (!p.grid && p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
3383
3486
  if(lbx.isStatic) lbx = lbx.result(0);
3384
- if(ubx.isStatic) ubx = ubx.result(0);
3487
+ if(ubx.isStatic) {
3488
+ ubx = ubx.result(0);
3489
+ if(p.grid) lbx = -ubx;
3490
+ } else if (p.grid) {
3491
+ // When UB is dynamic, pass NULL as LB; the VM instruction will
3492
+ // interpret this as "LB = -UB".
3493
+ lbx = null;
3494
+ }
3385
3495
  // NOTE: When semic_var_index is set, the lower bound must be
3386
3496
  // zero, as the semi-continuous lower bound is implemented with
3387
3497
  // a binary variable.
@@ -3482,39 +3592,50 @@ class VirtualMachine {
3482
3592
  const p = MODEL.processes[k];
3483
3593
  // Only consider processes owned by this actor.
3484
3594
  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.
3595
+ if(p.grid) {
3596
+ // Grid processes are a special case, as they can have a
3597
+ // negative level and potentially multiple slopes. Hence a
3598
+ // special VM instruction.
3599
+ this.code.push([VMI_update_grid_process_cash_coefficients, p]);
3600
+ } else {
3601
+ // Iterate over links IN, but only consider consumed products
3602
+ // having a market price.
3603
+ for(j = 0; j < p.inputs.length; j++) {
3604
+ l = p.inputs[j];
3605
+ if(!MODEL.ignored_entities[l.identifier] &&
3606
+ l.from_node.price.defined) {
3607
+ if(l.from_node.price.isStatic && l.relative_rate.isStatic) {
3608
+ k = l.from_node.price.result(0) * l.relative_rate.result(0);
3609
+ // NOTE: VMI_update_cash_coefficient has at least 4 arguments:
3610
+ // flow (CONSUME or PRODUCE), type (specifies the number and
3611
+ // type of arguments), the level_var_index of the process,
3612
+ // and the delay.
3613
+ // NOTE: Input links cannot have delay, so then delay = 0.
3614
+ if(Math.abs(k) > VM.NEAR_ZERO) {
3615
+ // Consumption rate & price are static: pass one constant.
3616
+ this.code.push([VMI_update_cash_coefficient,
3617
+ [VM.CONSUME, VM.ONE_C, p.level_var_index, 0, k]]);
3618
+ }
3619
+ } else {
3620
+ // No further optimization: assume two dynamic expressions.
3500
3621
  this.code.push([VMI_update_cash_coefficient,
3501
- [VM.CONSUME, VM.ONE_C, p.level_var_index, 0, k]]);
3622
+ [VM.CONSUME, VM.TWO_X, p.level_var_index, 0,
3623
+ l.from_node.price, l.relative_rate]]);
3502
3624
  }
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
3625
  }
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.
3626
+ } // END of FOR ALL input links
3627
+ }
3628
+ // Now iterate over links OUT, but only consider produced
3629
+ // products having a (non-zero) market price.
3630
+ // NOTE: Grid processes can have output links to *data* products,
3631
+ // so do NOT skip this iteration...
3514
3632
  for(j = 0; j < p.outputs.length; j++) {
3515
3633
  l = p.outputs[j];
3516
- const tnpx = l.to_node.price;
3517
- if(!MODEL.ignored_entities[l.identifier] && tnpx.defined &&
3634
+ const
3635
+ tnpx = l.to_node.price,
3636
+ // ... but DO skip links from grid processes to regular products.
3637
+ skip = p.grid && !l.to_node.is_data;
3638
+ if(!(skip || MODEL.ignored_entities[l.identifier]) && tnpx.defined &&
3518
3639
  !(tnpx.isStatic && Math.abs(tnpx.result(0)) < VM.NEAR_ZERO)) {
3519
3640
  // By default, use the process level as multiplier.
3520
3641
  vi = p.level_var_index;
@@ -3609,8 +3730,10 @@ class VirtualMachine {
3609
3730
  // Check whether any VMI_update_cash_coefficient instructions have
3610
3731
  // been added. If so, the objective function will maximze weighted
3611
3732
  // sum of actor cash flows, otherwise minimize sum of process levels.
3733
+ const last_vmi = this.code[this.code.length - 1][0];
3612
3734
  this.no_cash_flows = this.no_cash_flows &&
3613
- this.code[this.code.length - 1][0] !== VMI_update_cash_coefficient;
3735
+ last_vmi !== VMI_update_cash_coefficient &&
3736
+ last_vmi !== VMI_update_grid_process_cash_coefficients;
3614
3737
 
3615
3738
  // ALWAYS add the two cash flow constraints for this actor, as both
3616
3739
  // cash flow variables must be computed (will be 0 if no cash flows).
@@ -3749,6 +3872,21 @@ class VirtualMachine {
3749
3872
  }
3750
3873
  }
3751
3874
  }
3875
+
3876
+ // NEXT: Add constraints for processes representing grid elements.
3877
+ if(MODEL.with_power_flow) {
3878
+ for(i = 0; i < process_keys.length; i++) {
3879
+ k = process_keys[i];
3880
+ if(!MODEL.ignored_entities[k]) {
3881
+ p = MODEL.processes[k];
3882
+ if(p.grid) {
3883
+ this.code.push([VMI_add_grid_process_constraints, p]);
3884
+ }
3885
+ }
3886
+ }
3887
+ this.code.push(
3888
+ [VMI_add_kirchhoff_constraints, POWER_GRID_MANAGER.cycle_basis]);
3889
+ }
3752
3890
 
3753
3891
  // NEXT: Add product constraints to calculate (and constrain) their stock.
3754
3892
 
@@ -3786,7 +3924,9 @@ class VirtualMachine {
3786
3924
  // Add coefficient -1 for level index variable of `p`.
3787
3925
  this.code.push([VMI_add_const_to_coefficient,
3788
3926
  [p.level_var_index, -1, 0]]);
3789
- this.code.push([VMI_add_constraint, VM.EQ]);
3927
+ // NOTE: Pass special constraint type parameter to indicate
3928
+ // that this constraint must be scaled by the cash scalar.
3929
+ this.code.push([VMI_add_constraint, VM.ACTOR_CASH]);
3790
3930
  } else {
3791
3931
  console.log('ANOMALY: no actor for cash flow product', p.displayName);
3792
3932
  }
@@ -3818,8 +3958,20 @@ class VirtualMachine {
3818
3958
  } else {
3819
3959
  vi = fn.level_var_index;
3820
3960
  }
3821
- // First check for throughput links, as these are elaborate
3822
- if(l.multiplier === VM.LM_THROUGHPUT) {
3961
+ // First check whether the link is a power flow.
3962
+ if(l.multiplier === VM.LM_LEVEL && !p.is_data && fn.grid) {
3963
+ // If so, pass the grid process to a special VM instruction
3964
+ // that will add coefficients that account for losses.
3965
+ // NOTES:
3966
+ // (1) The second parameter (+1) indicates that the
3967
+ // coefficients of the UP flows should be positive
3968
+ // and those of the DOWN flows should be negative
3969
+ // (because it is a P -> Q link).
3970
+ // (2) The rate and delay properties of the link are ignored.
3971
+ this.code.push(
3972
+ [VMI_add_power_flow_to_coefficients, [fn, 1]]);
3973
+ // Then check for throughput links, as these are elaborate.
3974
+ } else if(l.multiplier === VM.LM_THROUGHPUT) {
3823
3975
  // Link `l` is Y-->Z and "reads" the total inflow into Y
3824
3976
  // over links Xi-->Y having rate Ri and when Y is a
3825
3977
  // product potentially also delay Di.
@@ -3936,19 +4088,35 @@ class VirtualMachine {
3936
4088
  } // END IF not ignored
3937
4089
  } // END FOR all inputs
3938
4090
 
3939
- // subtract outflow from product P to consuming processes (outputs)
4091
+ // Subtract outflow from product P to consuming processes (outputs)
3940
4092
  for(i = 0; i < p.outputs.length; i++) {
3941
- // NOTE: only consider outputs to processes; data outflows do not subtract
3942
4093
  l = p.outputs[i];
3943
4094
  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]]);
4095
+ const tn = l.to_node;
4096
+ // NOTE: Only consider outputs to processes; data flows do
4097
+ // not subtract from their tail nodes.
4098
+ if(tn instanceof Process) {
4099
+ if(tn.grid) {
4100
+ // If the link is a power flow, pass the grid process to
4101
+ // a special VM instruction that will add coefficients that
4102
+ // account for losses.
4103
+ // NOTES:
4104
+ // (1) The second parameter (-1) indicates that the
4105
+ // coefficients of the UP flows should be negative
4106
+ // and those of the DOWN flows should be positive
4107
+ // (because it is a Q -> P link).
4108
+ // (2) The rate and delay properties of the link are ignored.
4109
+ this.code.push(
4110
+ [VMI_add_power_flow_to_coefficients, [tn, -1]]);
3949
4111
  } else {
3950
- this.code.push([VMI_subtract_var_from_coefficient,
3951
- [l.to_node.level_var_index, rr, l.flow_delay]]);
4112
+ const rr = l.relative_rate;
4113
+ if(rr.isStatic) {
4114
+ this.code.push([VMI_subtract_const_from_coefficient,
4115
+ [tn.level_var_index, rr.result(0), l.flow_delay]]);
4116
+ } else {
4117
+ this.code.push([VMI_subtract_var_from_coefficient,
4118
+ [tn.level_var_index, rr, l.flow_delay]]);
4119
+ }
3952
4120
  }
3953
4121
  }
3954
4122
  }
@@ -4090,88 +4258,94 @@ class VirtualMachine {
4090
4258
  // To deal with this, the default equations will NOT be set when UB <= 0,
4091
4259
  // while the "exceptional" equations (q.v.) will NOT be set when UB > 0.
4092
4260
  // This can be realized using a special VM instruction:
4093
- ubx = (p.equal_bounds && p.lower_bound.defined ? p.lower_bound : p.upper_bound);
4261
+ ubx = (p.equal_bounds && p.lower_bound.defined && !p.grid ?
4262
+ p.lower_bound : p.upper_bound);
4094
4263
  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
- }
4264
+ // This instruction ensures that when UB <= 0, the constraints for
4265
+ // binaries will be prepared, but NOT added to the MILP problem.
4266
+
4267
+ // NOTE: For grid element processes, the ON/OFF binary is set by
4268
+ // the VMI_add_grid_process_constraints.
4269
+ if(!p.grid) {
4270
+ this.code.push(
4271
+ // Set coefficients vector to 0
4272
+ [VMI_clear_coefficients, null],
4273
+ // (a) L[t] - LB[t]*OO[t] >= 0
4274
+ [VMI_add_const_to_coefficient, [p.level_var_index, 1]]
4275
+ );
4276
+ if(p.lower_bound.isStatic) {
4277
+ let lb = p.lower_bound.result(0);
4278
+ if(Math.abs(lb) < VM.NEAR_ZERO) lb = VM.ON_OFF_THRESHOLD;
4279
+ this.code.push([VMI_subtract_const_from_coefficient,
4280
+ [p.on_off_var_index, lb]]);
4281
+ } else {
4282
+ this.code.push([VMI_subtract_var_from_coefficient,
4283
+ // NOTE: the 3rd parameter signals VM to use the ON/OFF threshold
4284
+ // value when the LB evaluates as near-zero
4285
+ [p.on_off_var_index, p.lower_bound, VM.ON_OFF_THRESHOLD]]);
4138
4286
  }
4139
- if(hub !== ub) {
4140
- ub = hub;
4141
- this.logMessage(this.block_count,
4142
- `Inferred upper bound for ${p.displayName}: ${this.sig4Dig(ub)}`);
4287
+ this.code.push(
4288
+ [VMI_add_constraint, VM.GE], // >= 0 as default RHS = 0
4289
+ // Set coefficients vector to 0
4290
+ [VMI_clear_coefficients, null],
4291
+ // (b) L[t] - UB[t]*OO[t] <= 0
4292
+ [VMI_add_const_to_coefficient, [p.level_var_index, 1]]
4293
+ );
4294
+ if(ubx.isStatic) {
4295
+ // If UB is very high (typically: undefined, so +INF), try to
4296
+ // infer a lower value for UB to use for the ON/OFF binary.
4297
+ let ub = ubx.result(0),
4298
+ hub = ub;
4299
+ if(ub > VM.MEGA_UPPER_BOUND) {
4300
+ hub = p.highestUpperBound([]);
4301
+ // If UB still very high, warn modeler on infoline and in monitor
4302
+ if(hub > VM.MEGA_UPPER_BOUND) {
4303
+ const msg = 'High upper bound (' + this.sig4Dig(hub) +
4304
+ ') for <strong>' + p.displayName + '</strong>' +
4305
+ ' will compromise computation of its binary variables';
4306
+ UI.warn(msg);
4307
+ this.logMessage(this.block_count,
4308
+ 'WARNING: ' + msg.replace(/<\/?strong>/g, '"'));
4309
+ }
4310
+ }
4311
+ if(hub !== ub) {
4312
+ ub = hub;
4313
+ this.logMessage(1,
4314
+ `Inferred upper bound for ${p.displayName}: ${this.sig4Dig(ub)}`);
4315
+ }
4316
+ this.code.push([VMI_subtract_const_from_coefficient,
4317
+ [p.on_off_var_index, ub]]);
4318
+ } else {
4319
+ // NOTE: no check (yet) for high values when UB is an expression
4320
+ // (this could be achieved by a special VM instruction)
4321
+ this.code.push([VMI_subtract_var_from_coefficient,
4322
+ [p.on_off_var_index, ubx]]);
4143
4323
  }
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]]);
4324
+ this.code.push([VMI_add_constraint, VM.LE]); // <= 0 as default RHS = 0
4151
4325
  }
4152
- this.code.push(
4153
- [VMI_add_constraint, VM.LE], // <= 0 as default RHS = 0
4326
+ if(p.is_zero_var_index >= 0) {
4154
4327
  // 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]);
4328
+ this.code.push(
4329
+ [VMI_clear_coefficients, null],
4330
+ // (c) OO[t] + IZ[t] = 1
4331
+ [VMI_add_const_to_coefficient, [p.is_zero_var_index, 1]],
4332
+ [VMI_add_const_to_coefficient, [p.on_off_var_index, 1]],
4333
+ [VMI_set_const_rhs, 1],
4334
+ [VMI_add_constraint, VM.EQ],
4335
+ // (d) L[t] + IZ[t] >= LB[t]
4336
+ [VMI_clear_coefficients, null],
4337
+ [VMI_add_const_to_coefficient, [p.level_var_index, 1]],
4338
+ [VMI_add_const_to_coefficient, [p.is_zero_var_index, 1]]
4339
+ );
4340
+ // NOTE: for semicontinuous variable, always use LB = 0
4341
+ if(p.lower_bound.isStatic || p.level_to_zero) {
4342
+ const plb = (p.level_to_zero ? 0 : p.lower_bound.result(0));
4343
+ this.code.push([VMI_set_const_rhs, plb]);
4344
+ } else {
4345
+ this.code.push([VMI_set_var_rhs, p.lower_bound]);
4346
+ }
4347
+ this.code.push([VMI_add_constraint, VM.GE]);
4172
4348
  }
4173
- this.code.push([VMI_add_constraint, VM.GE]);
4174
-
4175
4349
  // Also add constraints for start-up (if needed)
4176
4350
  if(p.start_up_var_index >= 0) {
4177
4351
  this.code.push(
@@ -4271,18 +4445,22 @@ class VirtualMachine {
4271
4445
  // NOTE: toggle the flag so that if UB <= 0, the following constraints
4272
4446
  // for setting the binary variables WILL be added
4273
4447
  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
- );
4448
+ [VMI_toggle_add_constraints_flag, null],
4449
+ // When UB <= 0, add these much simpler "exceptional" constraints:
4450
+ // OO[t] = 0
4451
+ [VMI_clear_coefficients, null],
4452
+ [VMI_add_const_to_coefficient, [p.on_off_var_index, 1]],
4453
+ [VMI_add_constraint, VM.EQ]
4454
+ );
4455
+ if(p.is_zero_var_index >= 0) {
4456
+ this.code.push(
4457
+ // IZ[t] = 1
4458
+ [VMI_clear_coefficients, null],
4459
+ [VMI_add_const_to_coefficient, [p.is_zero_var_index, 1]],
4460
+ [VMI_set_const_rhs, 1], // RHS = 1
4461
+ [VMI_add_constraint, VM.EQ]
4462
+ );
4463
+ }
4286
4464
  // Add constraints for start-up and first commit only if needed
4287
4465
  if(p.start_up_var_index >= 0) {
4288
4466
  this.code.push(
@@ -4328,7 +4506,7 @@ class VirtualMachine {
4328
4506
  }
4329
4507
  }
4330
4508
 
4331
- // NEXT: Add constraints.
4509
+ // NEXT: Add composite constraints.
4332
4510
  // NOTE: As of version 1.0.10, constraints are implemented using special
4333
4511
  // ordered sets (SOS2). This is effectuated with a dedicated VM instruction
4334
4512
  // for each of its "active" bound lines. This instruction requires these
@@ -4336,10 +4514,6 @@ class VirtualMachine {
4336
4514
  // - variable indices for the constraining node X, the constrained node Y
4337
4515
  // - expressions for the LB and UB of X and Y
4338
4516
  // - 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
4517
  for(i = 0; i < constraint_keys.length; i++) {
4344
4518
  k = constraint_keys[i];
4345
4519
  if(!MODEL.ignored_entities[k]) {
@@ -4349,20 +4523,16 @@ class VirtualMachine {
4349
4523
  x = c.from_node,
4350
4524
  y = c.to_node;
4351
4525
  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
- }
4526
+ this.code.push([VMI_add_bound_line_constraint,
4527
+ [x.level_var_index, x.lower_bound, x.upper_bound,
4528
+ y.level_var_index, y.lower_bound, y.upper_bound,
4529
+ c.bound_lines[j]]]);
4361
4530
  }
4362
4531
  }
4363
4532
  } // end FOR all constraints
4533
+
4364
4534
  MODEL.set_up = true;
4365
- this.logMessage(this.block_count,
4535
+ this.logMessage(1,
4366
4536
  `Problem formulation took ${this.elapsedTime} seconds.`);
4367
4537
  } // END of setup_problem function
4368
4538
 
@@ -4464,6 +4634,24 @@ class VirtualMachine {
4464
4634
  if(cv && !cv[0].startsWith('C')) cc[ci] *= m;
4465
4635
  }
4466
4636
  }
4637
+ // In case the model contains data products that represent an actor
4638
+ // cash flow, the coefficients of the constraint that equates the
4639
+ // product level to the cash flow must be *multiplied* by the cash
4640
+ // scalar so that they equal the cash flow in the model's monetary unit.
4641
+ for(let i = 0; i < this.actor_cash_constraints.length; i++) {
4642
+ const cc = this.matrix[this.actor_cash_constraints[i]];
4643
+ for(let ci in cc) if(cc.hasOwnProperty(ci)) {
4644
+ if(ci < this.chunk_offset) {
4645
+ // NOTE: Subtract 1 as variables array is zero-based.
4646
+ cv = this.variables[(ci - 1) % this.cols];
4647
+ } else {
4648
+ // Chunk variable array is zero-based.
4649
+ cv = this.chunk_variables[ci - this.chunk_offset];
4650
+ }
4651
+ // NOTE: Scale coefficients of cash variables only.
4652
+ if(cv && cv[0].startsWith('C')) cc[ci] *= this.cash_scalar;
4653
+ }
4654
+ }
4467
4655
  }
4468
4656
 
4469
4657
  checkForInfinity(n) {
@@ -4549,7 +4737,7 @@ class VirtualMachine {
4549
4737
  }
4550
4738
  if(b <= this.nr_of_time_steps && a.cash_out[b] < -0.005) {
4551
4739
  this.logMessage(block, `${this.WARNING}(t=${b}${round}) ` +
4552
- a.displayName + ' cash IN = ' + a.cash_out[b].toPrecision(2));
4740
+ a.displayName + ' cash OUT = ' + a.cash_out[b].toPrecision(2));
4553
4741
  }
4554
4742
  // Advance column offset in tableau by the # cols per time step.
4555
4743
  j += this.cols;
@@ -4564,7 +4752,8 @@ class VirtualMachine {
4564
4752
  p = MODEL.processes[o],
4565
4753
  has_OO = (p.on_off_var_index >= 0),
4566
4754
  has_SU = (p.start_up_var_index >= 0),
4567
- has_SD = (p.shut_down_var_index >= 0);
4755
+ has_SD = (p.shut_down_var_index >= 0),
4756
+ grid = p.grid;
4568
4757
  // Clear all start-ups and shut-downs at t >= bb.
4569
4758
  if(has_SU) p.resetStartUps(bb);
4570
4759
  if(has_SD) p.resetShutDowns(bb);
@@ -4747,6 +4936,9 @@ class VirtualMachine {
4747
4936
  this.variables_to_fixate = {};
4748
4937
  // FIRST: Calculate the actual flows on links.
4749
4938
  let b, bt, p, pl, ld, ci;
4939
+ for(let g in MODEL.power_grids) if(MODEL.power_grids.hasOwnProperty(g)) {
4940
+ MODEL.power_grids[g].total_losses = 0;
4941
+ }
4750
4942
  for(let l in MODEL.links) if(MODEL.links.hasOwnProperty(l) &&
4751
4943
  !MODEL.ignored_entities[l]) {
4752
4944
  l = MODEL.links[l];
@@ -4756,7 +4948,7 @@ class VirtualMachine {
4756
4948
  b = bb;
4757
4949
  // Iterate over all time steps in this chunk.
4758
4950
  for(let i = 0; i < cbl; i++) {
4759
- // NOTE: Flows may have a delay!
4951
+ // NOTE: Flows may have a delay (but will be 0 for grid processes).
4760
4952
  ld = l.actualDelay(b);
4761
4953
  bt = b - ld;
4762
4954
  latest_time_step = Math.max(latest_time_step, bt);
@@ -4857,13 +5049,38 @@ class VirtualMachine {
4857
5049
  }
4858
5050
  }
4859
5051
  // 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);
5052
+ let rr = l.relative_rate.result(bt);
5053
+ if(p.grid) {
5054
+ // For grid processes, rates depend on losses, which depend on
5055
+ // the process level, and whether the link is P -> Q or Q -> P.
5056
+ rr = 1;
5057
+ if(p.grid.loss_approximation > 0 &&
5058
+ ((pl > 0 && p === l.from_node) ||
5059
+ (pl < 0 && p === l.to_node))) {
5060
+ const alr = p.actualLossRate(bt);
5061
+ rr = 1 - alr;
5062
+ p.grid.total_losses += alr * Math.abs(pl);
5063
+ }
5064
+ }
5065
+ const af = this.severestIssue([pl, rr], rr * pl);
4863
5066
  l.actual_flow[b] = (Math.abs(af) > VM.NEAR_ZERO ? af : 0);
4864
5067
  b++;
4865
5068
  }
4866
5069
  }
5070
+ // Report power losses per grid, if applicable.
5071
+ if(MODEL.with_power_flow) {
5072
+ const ll = [];
5073
+ for(let g in MODEL.power_grids) if(MODEL.power_grids.hasOwnProperty(g)) {
5074
+ const pg = MODEL.power_grids[g];
5075
+ if(pg.loss_approximation > 0) {
5076
+ ll.push(`${pg.name}: ${VM.sig4Dig(pg.total_losses / cbl)} ${pg.power_unit}`);
5077
+ }
5078
+ }
5079
+ if(ll.length) {
5080
+ this.logMessage(block, 'Average power grid losses per time step:\n ' +
5081
+ ll.join('\n ') + '\n');
5082
+ }
5083
+ }
4867
5084
 
4868
5085
  // THEN: Calculate cash flows one step at a time because of delays.
4869
5086
  b = bb;
@@ -5188,6 +5405,12 @@ class VirtualMachine {
5188
5405
  // of matrix rows that then need to be scaled.
5189
5406
  this.cash_scalar = 1;
5190
5407
  this.cash_constraints = [];
5408
+ // NOTE: The model may contain data products that represent a cash
5409
+ // flow property of an actor. To calculate the actual value of such
5410
+ // properties, the coefficients in the effectuating constraint must
5411
+ // be *multiplied* by the scalar to compensate for the downscaling
5412
+ // explained above.
5413
+ this.actor_cash_constraints = [];
5191
5414
  // Vector for the objective function coefficients.
5192
5415
  this.objective = {};
5193
5416
  // Vectors for the bounds on decision variables.
@@ -5447,6 +5670,7 @@ class VirtualMachine {
5447
5670
  }
5448
5671
  let c,
5449
5672
  p,
5673
+ v,
5450
5674
  line = '';
5451
5675
  // NOTE: Iterate over ALL columns to maintain variable order.
5452
5676
  let n = abl * this.cols + this.chunk_variables.length;
@@ -5527,31 +5751,23 @@ class VirtualMachine {
5527
5751
  break;
5528
5752
  }
5529
5753
  }
5530
- line = '';
5754
+ v = vbl(p);
5531
5755
  if(lb === ub) {
5532
- if(lb !== null) line = ` ${vbl(p)} = ${lb}`;
5756
+ line = (lb !== null ? ` ${v} = ${lb}` : '');
5533
5757
  } else {
5758
+ const
5759
+ lbfree = (lb === null || lb <= VM.SOLVER_MINUS_INFINITY),
5760
+ ubfree = (ub === null || ub >= VM.SOLVER_PLUS_INFINITY);
5534
5761
  // 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
- }
5762
+ if(cplex && lbfree && ubfree) {
5763
+ line = ` ${v} ${this.is_binary[p] ? '<= 1' : 'free'}`;
5545
5764
  } else {
5546
5765
  // 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;
5766
+ if(lb || lb === 0) {
5767
+ line = ` ${lb} <= ${v}${ubfree ? '' : ' <= ' + ub}`;
5768
+ } else {
5769
+ line = (ubfree ? '' : ` ${v} <= ${ub}`);
5551
5770
  }
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
5771
  }
5556
5772
  }
5557
5773
  if(line) this.lines += line + EOL;
@@ -6190,8 +6406,8 @@ Solver status = ${json.status}`);
6190
6406
  this.MINUS_INFINITY = -this.DIAGNOSIS_UPPER_BOUND;
6191
6407
  console.log('DIAGNOSIS ON');
6192
6408
  } else {
6193
- this.PLUS_INFINITY = 1e+25;
6194
- this.MINUS_INFINITY = -1e+25;
6409
+ this.PLUS_INFINITY = this.SOLVER_PLUS_INFINITY;
6410
+ this.MINUS_INFINITY = this.SOLVER_MINUS_INFINITY;
6195
6411
  console.log('DIAGNOSIS OFF');
6196
6412
  }
6197
6413
  // The "propt to diagnose" flag is set when some block posed an
@@ -7799,6 +8015,18 @@ function randomBinomial(n, p) {
7799
8015
  }
7800
8016
  }
7801
8017
 
8018
+ // Function that computes the cumulative probability P(X <= x) when X
8019
+ // has a N(mu, sigma) distribution. Accuracy is about 1e-6.
8020
+ function normalCumulativeProbability(mu, sigma, x) {
8021
+ const
8022
+ t = 1 / (1 + 0.2316419 * Math.abs(x)),
8023
+ d = 0.3989423 * Math.exp(-0.5 * x * x),
8024
+ p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 +
8025
+ t * (-1.821256 + T * 1.330274))));
8026
+ if(x > 0) return 1 - p;
8027
+ return p;
8028
+ }
8029
+
7802
8030
  // Global array as cache for computation of factorial numbers.
7803
8031
  const FACTORIALS = [0, 1];
7804
8032
 
@@ -7903,8 +8131,12 @@ function VMI_set_bounds(args) {
7903
8131
  // When diagnosing an unbounded problem, use low value for INFINITY,
7904
8132
  // but the optional fourth parameter indicates whether the solver's
7905
8133
  // 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);
8134
+ // NOTE: For grid processes, the bounds are always "capped" so as
8135
+ // to permit Big M constraints for the associated binary variables.
8136
+ inf_is_free = (args.length > 3 && args[3]),
8137
+ inf_val = (vbl.grid ? VM.UNLIMITED_POWER_FLOW :
8138
+ (VM.diagnose && !inf_is_free ?
8139
+ VM.DIAGNOSIS_UPPER_BOUND : VM.SOLVER_PLUS_INFINITY));
7908
8140
  let l,
7909
8141
  u,
7910
8142
  fixed = (vi in VM.fixed_var_indices[r - 1]);
@@ -7924,26 +8156,35 @@ function VMI_set_bounds(args) {
7924
8156
  } else {
7925
8157
  // Set bounds as specified by the two arguments.
7926
8158
  l = args[1];
7927
- if(l instanceof Expression) l = l.result(VM.t);
7928
- if(l === VM.UNDEFINED) l = 0;
7929
8159
  u = args[2];
7930
8160
  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;
8161
+ u = Math.min(u, inf_val);
8162
+ // When LB is passed as NULL, this indicates: LB = -UB.
8163
+ if(l === null) {
8164
+ l = -u;
8165
+ } else {
8166
+ if(l instanceof Expression) l = l.result(VM.t);
8167
+ if(l === VM.UNDEFINED) {
8168
+ l = 0;
8169
+ } else {
8170
+ l = Math.max(l, -inf_val);
8171
+ }
8172
+ }
7934
8173
  fixed = '';
7935
8174
  }
7936
8175
  // NOTE: To see in the console whether fixing across rounds works, insert
7937
8176
  // "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(''));
8177
+ if(isNaN(l) || isNaN(u) ||
8178
+ typeof l !== 'number' || typeof u !== 'number' || DEBUGGING) {
8179
+ console.log(['set_bounds [', k, '] ', vbl.displayName, '[',
8180
+ VM.variables[vi - 1][0],'] t = ', VM.t, ' LB = ', VM.sig4Dig(l),
8181
+ ', UB = ', VM.sig4Dig(u), fixed].join(''), l, u, inf_val);
7941
8182
  }
7942
8183
  // 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;
8184
+ // initialized with default values (0 for LB, +INF for UB), the bounds
8185
+ // need only be set when they differ from these default values.
8186
+ if(l !== 0) VM.lower_bounds[k] = l;
8187
+ if(u < VM.SOLVER_PLUS_INFINITY) {
7947
8188
  VM.upper_bounds[k] = u;
7948
8189
  // If associated node is FROM-node of a "peak increase" link, then
7949
8190
  // the "peak increase" variables of this node must have the highest
@@ -7960,6 +8201,26 @@ function VMI_set_bounds(args) {
7960
8201
  VM.upper_bounds[cvi] = u;
7961
8202
  VM.upper_bounds[cvi + 1] = u;
7962
8203
  }
8204
+ // For grid elements, bounds must be set on UP and DOWN variables.
8205
+ if(vbl.grid) {
8206
+ // When considering losses, partition range 0...UB in sections.
8207
+ const step = (vbl.grid.loss_approximation < 2 ? u :
8208
+ u / vbl.grid.loss_approximation);
8209
+ VM.upper_bounds[VM.offset + vbl.up_1_var_index] = step;
8210
+ VM.upper_bounds[VM.offset + vbl.down_1_var_index] = step;
8211
+ if(vbl.grid.loss_approximation > 1) {
8212
+ // Set UB for semi-contiuous variables Up & Down slope 2.
8213
+ VM.upper_bounds[VM.offset + vbl.up_2_var_index] = 2 * step;
8214
+ VM.upper_bounds[VM.offset + vbl.down_2_var_index] = 2 * step;
8215
+ if(vbl.grid.loss_approximation > 2) {
8216
+ // Set UB for semi-contiuous variables Up & Down slope 3.
8217
+ VM.upper_bounds[VM.offset + vbl.up_3_var_index] = 3 * step;
8218
+ VM.upper_bounds[VM.offset + vbl.down_3_var_index] = 3 * step;
8219
+ }
8220
+ }
8221
+ // NOTE: lower bounds are 0 for all variables; their semi-continuous
8222
+ // ranges are set by VMI_add_grid_process_constraints.
8223
+ }
7963
8224
  }
7964
8225
  }
7965
8226
 
@@ -8191,6 +8452,28 @@ function VMI_subtract_var_from_coefficient(args) {
8191
8452
  }
8192
8453
  }
8193
8454
 
8455
+ /* AUXILIARY FUNCTIONS for setting cash flow coefficients */
8456
+
8457
+ function addCashIn(index, value) {
8458
+ if(index in VM.cash_in_coefficients) {
8459
+ // Add value to coefficient if it already exists...
8460
+ VM.cash_in_coefficients[index] += value;
8461
+ } else {
8462
+ // ... and set it if it is new.
8463
+ VM.cash_in_coefficients[index] = value;
8464
+ }
8465
+ }
8466
+
8467
+ function addCashOut(index, value) {
8468
+ if(index in VM.cash_out_coefficients) {
8469
+ // Add value to coefficient if it already exists...
8470
+ VM.cash_out_coefficients[index] += value;
8471
+ } else {
8472
+ // ... and set it if it is new.
8473
+ VM.cash_out_coefficients[index] = value;
8474
+ }
8475
+ }
8476
+
8194
8477
  function VMI_update_cash_coefficient(args) {
8195
8478
  // `args`: [flow, type, level_var_index, delay, x1, x2, ...]
8196
8479
  // NOTE: Flow is either CONSUME or PRODUCE; type can be ONE_C (one
@@ -8265,17 +8548,9 @@ function VMI_update_cash_coefficient(args) {
8265
8548
  VM.cash_out_rhs += pl * price_rate;
8266
8549
  }
8267
8550
  } 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
- }
8551
+ addCashIn(plk, price_rate);
8273
8552
  } 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
- }
8553
+ addCashOut(plk, -price_rate);
8279
8554
  }
8280
8555
  }
8281
8556
  // NOTE: For spinning reserve and highest increment, flow will always
@@ -8303,21 +8578,111 @@ function VMI_update_cash_coefficient(args) {
8303
8578
  VM.cash_out_rhs -= knownValue(vi, VM.t - d) * r;
8304
8579
  }
8305
8580
  } 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
- }
8581
+ addCashIn(k, -r);
8311
8582
  } else if(r < 0) {
8312
8583
  // 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;
8584
+ addCashOut(k, r);
8585
+ }
8586
+ }
8587
+
8588
+ function VMI_update_grid_process_cash_coefficients(p) {
8589
+ // Update cash flow coefficients for process `p` that relate to its
8590
+ // regular input and output link (data links are handled by means of
8591
+ // VMI_update_cash_coefficient).
8592
+ let fn = null,
8593
+ tn = null;
8594
+ for(let i = 0; i <= p.inputs.length; i++) {
8595
+ const l = p.inputs[i];
8596
+ if(l.multiplier === VM.LM_LEVEL &&
8597
+ !MODEL.ignored_entities[l.identifier]) {
8598
+ fn = l.from_node;
8599
+ break;
8600
+ }
8601
+ }
8602
+ for(let i = 0; i <= p.outputs.length; i++) {
8603
+ const l = p.outputs[i];
8604
+ if(l.multiplier === VM.LM_LEVEL &&
8605
+ !MODEL.ignored_entities[l.identifier]) {
8606
+ tn = l.to_node;
8607
+ break;
8608
+ }
8609
+ }
8610
+ const
8611
+ fp = (fn && fn.price.defined ? fn.price.result(VM.t) : 0),
8612
+ tp = (tn && tn.price.defined ? tn.price.result(VM.t) : 0);
8613
+ // Only proceed if process links to a product with a non-zero price.
8614
+ if(fp || tp) {
8615
+ const
8616
+ gpv = VM.gridProcessVarIndices(p, VM.offset),
8617
+ lr = p.lossRates(VM.t);
8618
+ if(fp > 0) {
8619
+ // If FROM node has price > 0, then all UP flows generate cash OUT
8620
+ // *without* loss while all DOWN flows generate cash IN *with* loss.
8621
+ for(let i = 0; i < gpv.slopes; i++) {
8622
+ addCashOut(gpv.up[i], -fp);
8623
+ addCashIn(gpv.down[i], (1 - lr[i]) * -fp);
8624
+ }
8625
+ } else if(fp < 0) {
8626
+ // If FROM node has price < 0, then all UP flows generate cash IN
8627
+ // *without* loss while all DOWN flows generate cash OUT *with* loss.
8628
+ for(let i = 0; i < gpv.slopes; i++) {
8629
+ addCashIn(gpv.up[i], fp);
8630
+ addCashOut(gpv.down[i], (1 - lr[i]) * fp);
8631
+ }
8632
+ }
8633
+ if(tp > 0) {
8634
+ // If TO node has price > 0, then all UP flows generate cash IN *with*
8635
+ // loss while all DOWN flows generate cash OUT *without* loss.
8636
+ for(let i = 0; i < gpv.slopes; i++) {
8637
+ addCashIn(gpv.up[i], (1 - lr[i]) * -tp);
8638
+ addCashOut(gpv.down[i], -tp);
8639
+ }
8640
+ } else if(tp < 0) {
8641
+ // If TO node has price < 0, then all UP flows generate cash OUT
8642
+ // *with* loss while all DOWN flows generate cash IN *without* loss.
8643
+ for(let i = 0; i < gpv.slopes; i++) {
8644
+ addCashOut(gpv.up[i], (1 - lr[i]) * tp);
8645
+ addCashIn(gpv.down[i], tp);
8646
+ }
8317
8647
  }
8318
8648
  }
8319
8649
  }
8320
8650
 
8651
+ function VMI_add_power_flow_to_coefficients(args) {
8652
+ // Special instruction to add power flow rates represented by process
8653
+ // P to the coefficient vector that is being constructed to compute the
8654
+ // level for product Q.
8655
+ // The instruction is added once for the link P -> Q (then UP flows
8656
+ // add to the level of Q, while DOWN flows subtract) and once for the
8657
+ // link Q -> P (then UP flows *subtract* from the level of Q while
8658
+ // DOWN flows *add*).
8659
+ // The instruction expects two arguments: a grid process and an integer
8660
+ // indicating the direction: P -> Q (1) or Q -> P (-1).
8661
+ const
8662
+ p = args[0],
8663
+ up = args[1] > 0,
8664
+ gpv = VM.gridProcessVarIndices(p, VM.offset),
8665
+ lr = p.lossRates(VM.t);
8666
+ for(let i = 0; i < gpv.slopes; i++) {
8667
+ // Losses must be subtracted only from flows *into* P.
8668
+ const
8669
+ uv = (up ? 1 - lr[i] : -1),
8670
+ dv = (up ? -1 : 1 - lr[i]);
8671
+ let k = gpv.up[i];
8672
+ if(k in VM.coefficients) {
8673
+ VM.coefficients[k] += uv;
8674
+ } else {
8675
+ VM.coefficients[k] = uv;
8676
+ }
8677
+ k = gpv.down[i];
8678
+ if(k in VM.coefficients) {
8679
+ VM.coefficients[k] += dv;
8680
+ } else {
8681
+ VM.coefficients[k] = dv;
8682
+ }
8683
+ }
8684
+ }
8685
+
8321
8686
  function VMI_add_throughput_to_coefficient(args) {
8322
8687
  // Special instruction to deal with throughput calculation.
8323
8688
  // Function: to add the contribution of variable X to the level of
@@ -8442,17 +8807,29 @@ function VMI_add_constraint(ct) {
8442
8807
  row[i] = c;
8443
8808
  }
8444
8809
  }
8445
- VM.matrix.push(row);
8810
+ // Special case:
8811
+ if(ct === VM.ACTOR_CASH) {
8812
+ VM.actor_cash_constraints.push(VM.matrix.length);
8813
+ ct = VM.EQ;
8814
+ }
8446
8815
  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);
8816
+ // Check for <= (near) +infinity and >= (near) -infinity: such
8817
+ // constraints should not be added to the model.
8818
+ if((ct === VM.LE && rhs >= 0.1 * VM.PLUS_INFINITY) ||
8819
+ (ct === VM.GE && rhs < 0.1 * VM.MINUS_INFINITY)) {
8820
+ if(DEBUGGING) console.log('Ignored infinite bound constraint');
8821
+ } else {
8822
+ VM.matrix.push(row);
8823
+ if(rhs >= VM.PLUS_INFINITY) {
8824
+ rhs = (VM.diagnose ? VM.DIAGNOSIS_UPPER_BOUND :
8825
+ VM.SOLVER_PLUS_INFINITY);
8826
+ } else if(rhs <= VM.MINUS_INFINITY) {
8827
+ rhs = (VM.diagnose ? -VM.DIAGNOSIS_UPPER_BOUND :
8828
+ VM.SOLVER_MINUS_INFINITY);
8829
+ }
8830
+ VM.right_hand_side.push(rhs);
8831
+ VM.constraint_types.push(ct);
8832
+ }
8456
8833
  } else if(DEBUGGING) {
8457
8834
  console.log('Constraint NOT added!');
8458
8835
  }
@@ -8523,13 +8900,90 @@ function VMI_add_cash_constraints(args) {
8523
8900
  VM.cash_out_rhs = 0;
8524
8901
  }
8525
8902
 
8903
+ function VMI_add_grid_process_constraints(p) {
8904
+ // Add constraints that will ensure that power flows either UP or DOWN,
8905
+ // and that loss slopes properties are set.
8906
+ const gpv = VM.gridProcessVarIndices(p, VM.offset);
8907
+ if(!gpv) return;
8908
+ // Now the variable index lists all contain 1, 2 or 3 indices,
8909
+ // depending on the loss approximation level.
8910
+ let ub = p.upper_bound.result(VM.t);
8911
+ if(ub >= VM.PLUS_INFINITY) {
8912
+ // When UB = +INF, this is interpreted as "unlimited", which is
8913
+ // implemented as 99999 grid power units.
8914
+ ub = VM.UNLIMITED_POWER_FLOW;
8915
+ }
8916
+ const
8917
+ step = ub / gpv.slopes,
8918
+ // NOTE: For slope 1 use a small positive number as LB.
8919
+ lbs = [VM.ON_OFF_THRESHOLD, step, 2*step],
8920
+ ubs = [step, 2*step, 3*step];
8921
+ for(let i = 0; i < gpv.slopes; i++) {
8922
+ // Add constraints to set the ON/OFF binary for each slope:
8923
+ VMI_clear_coefficients();
8924
+ // level - UB*binary <= 0
8925
+ VM.coefficients[gpv.up[i]] = 1;
8926
+ VM.coefficients[gpv.up_on[i]] = -ubs[i];
8927
+ VMI_add_constraint(VM.LE);
8928
+ // level - LB*binary >= 0
8929
+ VM.coefficients[gpv.up_on[i]] = -lbs[i];
8930
+ VMI_add_constraint(VM.GE);
8931
+ // Two similar constraints for the Down slope
8932
+ VMI_clear_coefficients();
8933
+ VM.coefficients[gpv.down[i]] = 1;
8934
+ VM.coefficients[gpv.down_on[i]] = -ubs[i];
8935
+ VMI_add_constraint(VM.LE);
8936
+ VM.coefficients[gpv.down_on[i]] = -lbs[i];
8937
+ VMI_add_constraint(VM.GE);
8938
+ }
8939
+ // Set level to sum of all Up variables minus sum of all Down variables.
8940
+ VMI_clear_coefficients();
8941
+ VM.coefficients[VM.offset + p.level_var_index] = -1;
8942
+ for(let i = 0; i < gpv.slopes; i++) {
8943
+ VM.coefficients[gpv.up[i]] = 1;
8944
+ VM.coefficients[gpv.down[i]] = -1;
8945
+ }
8946
+ VMI_add_constraint(VM.EQ);
8947
+ // Set OO to the sum of all binary ON variables. This not only makes
8948
+ // OO available for read-out but also ensures that at most one slope
8949
+ // can be active (because OO is binary).
8950
+ VMI_clear_coefficients();
8951
+ VM.coefficients[VM.offset + p.on_off_var_index] = -1;
8952
+ for(let i = 0; i < gpv.slopes; i++) {
8953
+ VM.coefficients[gpv.up_on[i]] = 1;
8954
+ VM.coefficients[gpv.down_on[i]] = 1;
8955
+ }
8956
+ VMI_add_constraint(VM.EQ);
8957
+ }
8958
+
8959
+ function VMI_add_kirchhoff_constraints(cb) {
8960
+ // Add Kirchhoff's voltage law constraint for each cycle in `cb`.
8961
+ // NOTE: Do not add a constraint for cyles that have been "broken"
8962
+ // because one or more of its processes have UB = 0.
8963
+ for(let i = 0; i < cb.length; i++) {
8964
+ const c = cb[i];
8965
+ let not_broken = true;
8966
+ VMI_clear_coefficients();
8967
+ for(let j = 0; j < c.length; j++) {
8968
+ const
8969
+ p = c[j].process,
8970
+ x = p.length_in_km * p.grid.reactancePerKm,
8971
+ o = c[j].orientation,
8972
+ ub = p.upper_bound.result(VM.t);
8973
+ if(ub <= VM.NEAR_ZERO) {
8974
+ not_broken = false;
8975
+ break;
8976
+ }
8977
+ VM.coefficients[VM.offset + p.level_var_index] = x * o;
8978
+ }
8979
+ if(not_broken) VMI_add_constraint(VM.EQ);
8980
+ }
8981
+ }
8982
+
8526
8983
  function VMI_add_bound_line_constraint(args) {
8527
8984
  // `args`: [variable index for X, LB expression for X, UB expression for X,
8528
8985
  // 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).
8986
+ // boundline object]
8533
8987
  const
8534
8988
  vix = args[0],
8535
8989
  vx = VM.variables[vix - 1], // `variables` is zero-based!
@@ -8540,14 +8994,22 @@ function VMI_add_bound_line_constraint(args) {
8540
8994
  objy= vy[1],
8541
8995
  uby = args[5].result(VM.t),
8542
8996
  bl = args[6],
8543
- use_binaries = args[7],
8544
8997
  n = bl.points.length,
8545
8998
  x = new Array(n),
8546
8999
  y = new Array(n),
8547
9000
  w = new Array(n);
9001
+ // Set bound line point coordinates for current run and time step.
9002
+ bl.setDynamicPoints(VM.t);
8548
9003
  if(DEBUGGING) {
8549
9004
  console.log('add_bound_line_constraint:', bl.displayName);
8550
9005
  }
9006
+ // Do not add constraints for bound lines that set no infeasible area.
9007
+ if(!bl.constrainsY) {
9008
+ if(DEBUGGING) {
9009
+ console.log('SKIP because bound line does not constrain');
9010
+ }
9011
+ return;
9012
+ }
8551
9013
  // NOTE: For semi-continuous processes, lower bounds > 0 should to be
8552
9014
  // adjusted to 0, as then 0 is part of the process level range.
8553
9015
  let lbx = args[1].result(VM.t),
@@ -8572,6 +9034,11 @@ function VMI_add_bound_line_constraint(args) {
8572
9034
  // For LE and GE type bound lines, one slack variable suffices, and = 0 must
8573
9035
  // be, respectively, <= 0 or >= 0
8574
9036
 
9037
+ // Since version 2.0.0, the `use_binaries` flag can no longer be determined
9038
+ // at compile time, as bound lines may be dynamic. When use_binaries = TRUE,
9039
+ // additional constraints on binary variables are needed (see below).
9040
+ let use_binaries = VM.noSupportForSOS && !bl.needsNoSOS;
9041
+
8575
9042
  // Scale X and Y and compute the block indices of w[i]
8576
9043
  let wi = VM.offset + bl.first_sos_var_index;
8577
9044
  const
@@ -8629,8 +9096,7 @@ function VMI_add_bound_line_constraint(args) {
8629
9096
  // and then to ensure that at most 2 binaries can be 1:
8630
9097
  // b[1] + ... + b[N] <= 2
8631
9098
  // 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.
9099
+ // when a bound line defines a convex feasible area.
8634
9100
  if(use_binaries) {
8635
9101
  // Add the constraints mentioned above. The index of b[i] is the
8636
9102
  // index of w[i] plus the number of points on the boundline N.