linny-r 1.1.18 → 1.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -3301,6 +3301,11 @@ td.in-chart {
3301
3301
  font-weight: bold;
3302
3302
  }
3303
3303
 
3304
+ td.warnings {
3305
+ background-color: yellow;
3306
+ font-style: italic;
3307
+ }
3308
+
3304
3309
  #viewer-spinners {
3305
3310
  position: absolute;
3306
3311
  bottom: 2px;
@@ -3482,13 +3482,17 @@ class GUIController extends Controller {
3482
3482
  const
3483
3483
  dlg = e.target.id.split('-')[0],
3484
3484
  tde = document.getElementById(dlg + '-dlg'),
3485
- mgr = tde.getAttribute('data-manager');
3485
+ was_hidden = this.hidden(tde.id);
3486
+ let mgr = tde.getAttribute('data-manager');
3487
+ if(mgr) mgr = window[mgr];
3486
3488
  // NOTE: prevent modeler from viewing charts while an experiment is running
3487
- if(dlg === 'chart' && this.hidden(tde.id) && MODEL.running_experiment) {
3489
+ if(dlg === 'chart' && was_hidden && MODEL.running_experiment) {
3488
3490
  UI.notify(UI.NOTICE.NO_CHARTS);
3491
+ mgr.visible = false;
3489
3492
  return;
3490
3493
  }
3491
3494
  this.toggle(tde.id);
3495
+ if(mgr) mgr.visible = was_hidden;
3492
3496
  // Open at position after last drag (recorded in DOM data attributes)
3493
3497
  let t = tde.getAttribute('data-top'),
3494
3498
  l = tde.getAttribute('data-left');
@@ -3506,15 +3510,14 @@ class GUIController extends Controller {
3506
3510
  this.reorderDialogs();
3507
3511
  // Update the diagram if its manager has been specified
3508
3512
  if(mgr) {
3509
- window[mgr].visible = true;
3510
- window[mgr].updateDialog();
3511
- if(mgr === 'DOCUMENTATION_MANAGER') {
3513
+ mgr.visible = true;
3514
+ mgr.updateDialog();
3515
+ if(mgr === DOCUMENTATION_MANAGER) {
3512
3516
  if(this.info_line.innerHTML.length === 0) {
3513
- DOCUMENTATION_MANAGER.title.innerHTML = 'About Linny-R';
3514
- DOCUMENTATION_MANAGER.viewer.innerHTML =
3515
- DOCUMENTATION_MANAGER.about_linny_r;
3516
- DOCUMENTATION_MANAGER.edit_btn.classList.remove('enab');
3517
- DOCUMENTATION_MANAGER.edit_btn.classList.add('disab');
3517
+ mgr.title.innerHTML = 'About Linny-R';
3518
+ mgr.viewer.innerHTML = mgr.about_linny_r;
3519
+ mgr.edit_btn.classList.remove('enab');
3520
+ mgr.edit_btn.classList.add('disab');
3518
3521
  }
3519
3522
  UI.drawDiagram(MODEL);
3520
3523
  }
@@ -3527,10 +3530,10 @@ class GUIController extends Controller {
3527
3530
  this.reorderDialogs();
3528
3531
  }
3529
3532
  if(mgr) {
3530
- window[mgr].visible = true;
3531
- if(mgr === 'DOCUMENTATION_MANAGER') {
3532
- DOCUMENTATION_MANAGER.visible = false;
3533
- DOCUMENTATION_MANAGER.title.innerHTML = 'Documentation';
3533
+ mgr.visible = true;
3534
+ if(mgr === DOCUMENTATION_MANAGER) {
3535
+ mgr.visible = false;
3536
+ mgr.title.innerHTML = 'Documentation';
3534
3537
  UI.drawDiagram(MODEL);
3535
3538
  }
3536
3539
  }
@@ -5499,7 +5502,8 @@ class GUIController extends Controller {
5499
5502
  a1 = document.getElementById('link-arrow-1'),
5500
5503
  a2 = document.getElementById('link-arrow-2'),
5501
5504
  lm = document.getElementById('link-multiplier').value,
5502
- d = document.getElementById('link-D');
5505
+ d = document.getElementById('link-D'),
5506
+ deb = document.getElementById('link-D-x');
5503
5507
  // NOTE: selector value is a string, not a number
5504
5508
  if(lm === '0') {
5505
5509
  // Default link symbol is a solid arrow
@@ -5517,10 +5521,15 @@ class GUIController extends Controller {
5517
5521
  d.style.color = 'gray';
5518
5522
  d.style.backgroundColor = 'inherit';
5519
5523
  d.value = '0';
5524
+ // Also disable its "edit expression" button
5525
+ deb.classList.remove('enab');
5526
+ deb.classList.add('disab');
5520
5527
  } else {
5521
5528
  d.disabled = false;
5522
5529
  d.style.color = 'black';
5523
5530
  d.style.backgroundColor = 'white';
5531
+ deb.classList.remove('disab');
5532
+ deb.classList.add('enab');
5524
5533
  }
5525
5534
  }
5526
5535
 
@@ -5816,7 +5825,7 @@ class GUIMonitor {
5816
5825
  this.equations_text.value = VM.equations[b - 1];
5817
5826
  }
5818
5827
  // Legend to variables is not block-dependent
5819
- this.variables_text.value = VM.variablesLegend;
5828
+ this.variables_text.value = VM.variablesLegend(b);
5820
5829
  // Show the text area for the selected tab
5821
5830
  if(this.tab !== tab) {
5822
5831
  let mt = 'monitor-' + this.tab;
@@ -12004,7 +12013,8 @@ class GUIExperimentManager extends ExperimentManager {
12004
12013
  ss = VM.sig2Dig(r.solver_seconds),
12005
12014
  ssp = (rdt < VM.NEAR_ZERO ? '' :
12006
12015
  ' (' + Math.round(r.solver_seconds * 100 / rdt) + '%)'),
12007
- w = (r.warning_count > 0 ? pluralS(r.warning_count, 'warning') + '. ' : '');
12016
+ w = (r.warning_count > 0 ?
12017
+ ' ' + pluralS(r.warning_count, 'warning') + '. ' : '');
12008
12018
  cell.title = ['Run #', i, ' (', r.time_steps, ' time steps of ',
12009
12019
  r.time_step_duration, ' h) took ', rdts, ' s. Solver used ', ss, ' s',
12010
12020
  ssp, '.', w, (rr ? `
@@ -14793,6 +14803,3 @@ if (MODEL.focal_cluster === fc) {
14793
14803
  }
14794
14804
  }
14795
14805
  } // END of class UndoStack
14796
-
14797
-
14798
-
@@ -2603,9 +2603,14 @@ class LinnyRModel {
2603
2603
  this.cleanVector(p.cash_out, 0, 0);
2604
2604
  // NOTE: `start_ups` is a list of time steps when start-up occurred
2605
2605
  p.start_ups.length = 0;
2606
- // NOTE: `peak_level` records the highest level for each block,
2607
- // and for block 0 this means the initial level of p
2608
- p.peak_level = [p.level[0]];
2606
+ // NOTE: `b_peak_inc` records the peak increase for each block,
2607
+ // so at t=0 (block *before* block #1) this is the initial level
2608
+ p.b_peak_inc = [p.level[0]];
2609
+ // `la_peak_inc` records the additional peak increase in the
2610
+ // look-ahead period
2611
+ p.la_peak_inc = [p.level[0]];
2612
+ // b_peak[b] records peak level value up to and including block b
2613
+ p.b_peak = [p.level[0]];
2609
2614
  }
2610
2615
  for(obj in this.products) if(this.products.hasOwnProperty(obj)) {
2611
2616
  p = this.products[obj];
@@ -2623,9 +2628,10 @@ class LinnyRModel {
2623
2628
  this.cleanVector(p.highest_cost_price, VM.UNDEFINED);
2624
2629
  if(p.is_buffer) this.cleanVector(p.stock_price, VM.UNDEFINED);
2625
2630
  p.start_ups.length = 0;
2626
- // NOTE: `peak_level` records the peak level for each block,
2627
- // so at t=0 (block *before* block #1) this is the initial level
2628
- p.peak_level = [p.level[0]];
2631
+ // NOTE: peak increase also applies to products
2632
+ p.b_peak_inc = [p.level[0]];
2633
+ p.la_peak_inc = [p.level[0]];
2634
+ p.b_peak = [p.level[0]];
2629
2635
  }
2630
2636
  for(obj in this.links) if(this.links.hasOwnProperty(obj)) {
2631
2637
  l = this.links[obj];
@@ -4220,7 +4226,8 @@ class Note extends ObjectWithXYWH {
4220
4226
  if(obj instanceof DatasetModifier) {
4221
4227
  this.fields.push(new NoteField(tag, obj.expression));
4222
4228
  } else if(obj) {
4223
- const attr = (ena.length > 1 ? ena[1].trim() : '');
4229
+ // If attribute omitted, use default attribute of entity type
4230
+ const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
4224
4231
  // Variable may specify a vector-type attribute
4225
4232
  let val = obj.attributeValue(attr);
4226
4233
  // If not, it may be a cluster unit balance
@@ -130,10 +130,12 @@ class Expression {
130
130
  this.code = null;
131
131
  const xp = new ExpressionParser(this.text, this.object, this.attribute);
132
132
  if(xp.error === '') {
133
- // NOTE: except for dataset modifiers, expressions should not be based
134
- // on levels-still-to-be-computed-by-the-solver, so caution the modeler
135
- // when this appears to be the case
136
- if(xp.is_level_based &&
133
+ // NOTE: except for dataset modifiers and note colors, expressions
134
+ // should not be based on levels-still-to-be-computed-by-the-solver,
135
+ // so caution the modeler when this appears to be the case
136
+ // NOTE: when note color expressions are edited, they are compiled
137
+ // to check their syntax; then their object is null
138
+ if(xp.is_level_based && this.object &&
137
139
  !(this.object instanceof Dataset || this.object instanceof Note)) {
138
140
  // NOTE: this should not occur, so log more details
139
141
  console.log('Level-based issue:',
@@ -1471,6 +1473,9 @@ class VirtualMachine {
1471
1473
  // Base penalty of 10 is high relative to the (scaled) coefficients of the
1472
1474
  // cash flows in the objective function (typically +/- 1)
1473
1475
  this.BASE_PENALTY = 10;
1476
+ // Peak variable penalty is added to make solver choose the *smallest*
1477
+ // value that is greater than or equal to X[t] for all t as "peak value"
1478
+ this.PEAK_VAR_PENALTY = 0.01;
1474
1479
 
1475
1480
  // NOTE: the VM uses numbers >> +INF to denote special computation results
1476
1481
  this.EXCEPTION = 1e+36; // to test for any exceptional value
@@ -2130,7 +2135,7 @@ class VirtualMachine {
2130
2135
  p.start_up_count_var_index = -1;
2131
2136
  p.suc_on_var_index = -1;
2132
2137
  p.first_commit_var_index = -1;
2133
- p.chunk_var_index = -1;
2138
+ p.peak_inc_var_index = -1;
2134
2139
  if(p instanceof Product) {
2135
2140
  p.stock_LE_slack_var_index = -1;
2136
2141
  p.stock_GE_slack_var_index = -1;
@@ -2183,10 +2188,11 @@ class VirtualMachine {
2183
2188
  const
2184
2189
  type = tuple[0],
2185
2190
  obj = tuple[1];
2186
- if(type === 'peak') {
2191
+ if(type.indexOf('-peak') > 0) {
2187
2192
  // Peak level variables have an array as node property
2188
2193
  const c = Math.trunc(t / this.block_length);
2189
- return obj.peak_level[c];
2194
+ if(type.startsWith('b')) return obj.b_peak_inc[c];
2195
+ return obj.la_peak_inc[c];
2190
2196
  }
2191
2197
  const prior_level = obj.actualLevel(t);
2192
2198
  if(type === 'OO') return prior_level > 0 ? 1 : 0;
@@ -2206,7 +2212,7 @@ class VirtualMachine {
2206
2212
  return prior_level;
2207
2213
  }
2208
2214
 
2209
- get variablesLegend() {
2215
+ variablesLegend(block) {
2210
2216
  // Returns a string with each variable code and full name on a separate line
2211
2217
  const
2212
2218
  vcnt = this.variables.length,
@@ -2222,11 +2228,14 @@ class VirtualMachine {
2222
2228
  l += v + ' [' + this.variables[i][0] + p + ']\n';
2223
2229
  }
2224
2230
  if(this.chunk_variables.length > 0) {
2231
+ // NOTE: chunk offset for last block may be lower than standard
2232
+ const chof = (block >= this.nr_of_blocks ? this.chunk_offset :
2233
+ this.cols * this.chunk_length + 1);
2225
2234
  for(let i = 0; i < this.chunk_variables.length; i++) {
2226
2235
  const
2227
2236
  obj = this.chunk_variables[i][1],
2228
2237
  // NOTE: chunk offset takes into account that indices are 0-based
2229
- cvi = this.chunk_offset + i;
2238
+ cvi = chof + i;
2230
2239
  let v = (this.solver_name === 'lp_solve' ?
2231
2240
  'C' + cvi : 'X' + cvi.toString().padStart(z, '0'));
2232
2241
  v += ' '.slice(v.length) + obj.displayName;
@@ -2440,15 +2449,20 @@ class VirtualMachine {
2440
2449
 
2441
2450
  // Now all variables that get a tableau column in each time step have
2442
2451
  // been defined; next step is to add "chunk variables"
2443
- // NOTE: chunk variables are "free", i.e., have no bound constraints
2444
2452
  let cvi = 0;
2445
- // Add chunk variable for processes having a peak increase link
2453
+ // Add *two* chunk variables for processes having a peak increase link
2446
2454
  for(i = 0; i < process_keys.length; i++) {
2447
2455
  k = process_keys[i];
2448
2456
  p = MODEL.processes[k];
2449
2457
  if(!MODEL.ignored_entities[k] && p.needsMaximumData) {
2450
- p.chunk_var_index = cvi;
2451
- this.chunk_variables.push(['peak', p]);
2458
+ // "peak increase" for block
2459
+ p.peak_inc_var_index = cvi;
2460
+ this.chunk_variables.push(['b-peak', p]);
2461
+ cvi++;
2462
+ // additional "peak increase" for the look-ahead period
2463
+ // NOTE: no need to record the second index as it wil allways be
2464
+ // equal to block peak index + 1
2465
+ this.chunk_variables.push(['la-peak', p]);
2452
2466
  cvi++;
2453
2467
  }
2454
2468
  }
@@ -2457,14 +2471,19 @@ class VirtualMachine {
2457
2471
  k = product_keys[i];
2458
2472
  p = MODEL.products[k];
2459
2473
  if(!MODEL.ignored_entities[k] && p.needsMaximumData) {
2460
- p.chunk_var_index = cvi;
2461
- this.chunk_variables.push(['peak', p]);
2474
+ p.peak_inc_var_index = cvi;
2475
+ this.chunk_variables.push(['b-peak', p]);
2476
+ cvi++;
2477
+ this.chunk_variables.push(['la-peak', p]);
2462
2478
  cvi++;
2463
2479
  }
2464
2480
  }
2465
2481
 
2466
2482
  // Now *all* variables have been defined; next step is to set their bounds
2467
2483
 
2484
+ // NOTE: chunk variables of node `p` have LB = 0 and UB = UB of `p`;
2485
+ // this is effectuated by the VM "set bounds" instructions at run time
2486
+
2468
2487
  // NOTE: under normal assumptions (all processes having LB >= 0), bounds on
2469
2488
  // actor cash flow variables need NOT be set because cash IN and cash OUT
2470
2489
  // will then always be >= 0 (solver's default bounds).
@@ -2668,12 +2687,13 @@ class VirtualMachine {
2668
2687
  VM.PRODUCE, VM.SPIN_RES, p.on_off_var_index, l.flow_delay, vi,
2669
2688
  l.from_node.upper_bound, tnpx, l.relative_rate]]);
2670
2689
  } else if(l.multiplier === VM.LM_PEAK_INC) {
2671
- // "peak increase" will be > 0 only in the first time step
2672
- // of the chunk being optimized, and 0 in all other time steps;
2673
- // the VM instruction will handle this
2690
+ // NOTE: "peak increase" may be > 0 only in the first time step
2691
+ // of the block being optimized, and in the first step of the
2692
+ // look-ahead period (if peak rises in that period), and will
2693
+ // be 0 in all other time steps; the VM instruction handles this
2674
2694
  // NOTE: delay is always 0 for this link flow
2675
2695
  this.code.push([VMI_update_cash_coefficient, [
2676
- VM.PRODUCE, VM.PEAK_INC, p.chunk_var_index, 0,
2696
+ VM.PRODUCE, VM.PEAK_INC, p.peak_inc_var_index, 0,
2677
2697
  tnpx, l.relative_rate]]);
2678
2698
  } else if(tnpx.isStatic && l.relative_rate.isStatic) {
2679
2699
  // If link rate and product price are static, only add the variable
@@ -2850,7 +2870,7 @@ class VirtualMachine {
2850
2870
  } else if(l.multiplier === VM.LM_SHUTDOWN) {
2851
2871
  vi = fn.shut_down_var_index;
2852
2872
  } else if(l.multiplier === VM.LM_PEAK_INC) {
2853
- vi = fn.chunk_var_index;
2873
+ vi = fn.peak_inc_var_index;
2854
2874
  } else {
2855
2875
  vi = fn.level_var_index;
2856
2876
  }
@@ -3089,22 +3109,23 @@ class VirtualMachine {
3089
3109
  (to prevent that FC[t] = 1 when SO[t-1] = 1 and SO[t] = 1, i.e.,
3090
3110
  SC was already > 0)
3091
3111
 
3092
- To calculate the highest increment for a "chunk", we need a continuous
3093
- "chunk variable" MX, i.e., only 1 tableau column per chunk, not 1 for
3094
- each time step. This variable MX will compute the highest value of
3095
- the node level variable L[t] (for all t in the chunk):
3096
- (n) L[t] - MX <= 0
3097
- Once per chunk, add two more constraints to ensure that MX is at
3098
- least as high as MX in the previous chunk:
3099
- (o) MX - MX[c-1] >= 0 (where c is the chunk number, and MX[0] = 0)
3100
- (p) MX[c-1] - MX <= 0
3101
- Then use this MX only once (at block time bt = 0) to compute the
3102
- actual flow as MX - MX[c-1]; for all other time steps, the AF for
3103
- highest increment links equals 0.
3104
- NOTE: These constraints do not keep MX from becoming higher than
3105
- the highest value (for all t) of L[t]; the modeler must ensure
3106
- that there is a cost associated with MX. Forcing MX to be exactly
3107
- equal to MAX(L[t]) would require a binary variable for each t.
3112
+ To calculate the peak increase values, we need two continuous
3113
+ "chunk variables", i.e., only 1 tableau column per chunk, not 1 for
3114
+ each time step. These variables BPI and CPI will compute the highest
3115
+ value (for all t in the block (B) and for the chunk (C)) of the
3116
+ difference L[t] - block peak (BP) of previous block. This requires
3117
+ one equation for every t = 1, ..., block length:
3118
+ (n) L[t] - BPI[b] <= BP[b-1] (where b denotes the block number)
3119
+ plus one equation for every t = block length + 1 to chunk length:
3120
+ (o) L[t] - BPI[b] - CPI[b] <= BP[b-1]
3121
+ This ensures that CPI is the *additional* increase in the look-ahead
3122
+ Then use BPI[b] in first time step if block, and CPI[b] at first
3123
+ time step of the look-ahead period to compute the actual flow for
3124
+ the "peak increase" links. For all other time steps this AF equals 0.
3125
+
3126
+ NOTE: These constraints alone set the lower bound for BPI and CPI, so
3127
+ these variables can take on higher values. The modeler must ensure
3128
+ that there is a cost associated with the actual flow, not a revenue.
3108
3129
  */
3109
3130
  // NOTE: as of 20 June 2021, binary attributes of products are also computed
3110
3131
  const pp_nodes = [];
@@ -3350,12 +3371,12 @@ class VirtualMachine {
3350
3371
  // Check whether constraints (n) through (p) need to be added
3351
3372
  // to compute the peak level for a block of time steps
3352
3373
  // NOTE: this is independent of the binary variables!
3353
- if(p.chunk_var_index >= 0) {
3374
+ if(p.peak_inc_var_index >= 0) {
3354
3375
  this.code.push(
3355
3376
  // One special instruction implements this operation, as part
3356
3377
  // of it must be performed only at block time = 0
3357
- [VMI_add_peak_level_constraints,
3358
- [p.level_var_index, p.chunk_var_index]]
3378
+ [VMI_add_peak_increase_constraints,
3379
+ [p.level_var_index, p.peak_inc_var_index]]
3359
3380
  );
3360
3381
  }
3361
3382
  }
@@ -3648,13 +3669,17 @@ class VirtualMachine {
3648
3669
  b++;
3649
3670
  }
3650
3671
  }
3651
- // Get values of peak level variables from solution vector
3672
+ // Get values of peak increase variables from solution vector
3652
3673
  // NOTE: computed offset takes into account that chunk variable list
3653
3674
  // is zero-based!
3654
3675
  const offset = this.cols * abl;
3655
3676
  for(let i = 0; i < ncv; i++) {
3656
3677
  const p = this.chunk_variables[i][1];
3657
- p.peak_level[block] = x[offset + i];
3678
+ p.b_peak_inc[block] = x[offset + i];
3679
+ i++;
3680
+ p.la_peak_inc[block] = x[offset + i];
3681
+ // Compute the peak from the peak increase
3682
+ p.b_peak[block] = p.b_peak[block - 1] + p.b_peak_inc[block];
3658
3683
  }
3659
3684
  // Add warning to messages if slack has been used
3660
3685
  // NOTE: only check after the last round has been evaluated
@@ -3764,11 +3789,13 @@ class VirtualMachine {
3764
3789
  p.inputs[j].relative_rate.result(bt));
3765
3790
  }
3766
3791
  } else if(l.multiplier === VM.LM_PEAK_INC) {
3767
- // Actual flow can only be > 0 in first time step of block
3792
+ // Actual flow over "peak increase" link is zero unless...
3768
3793
  if(i === 0) {
3769
- // Increase is 0 when peak value < peak in previous block
3770
- pl = Math.max(0,
3771
- p.peak_level[block] - p.peak_level[block - 1]);
3794
+ // first time step, then "block peak increase"...
3795
+ pl = p.b_peak_inc[block];
3796
+ } else if(i === MODEL.block_length) {
3797
+ // or first step of look-ahead, then "additional increase"
3798
+ pl = p.la_peak_inc[block];
3772
3799
  } else {
3773
3800
  pl = 0;
3774
3801
  }
@@ -6207,6 +6234,7 @@ function VMI_set_const_bounds(args) {
6207
6234
  // `args`: [var_index, number, number]
6208
6235
  const
6209
6236
  vi = args[0],
6237
+ vbl = VM.variables[vi - 1][1],
6210
6238
  k = VM.offset + vi,
6211
6239
  r = VM.round_letters.indexOf(VM.round_sequence[VM.current_round]),
6212
6240
  // Optional fourth parameter indicates whether the solver's
@@ -6217,7 +6245,6 @@ function VMI_set_const_bounds(args) {
6217
6245
  u,
6218
6246
  fixed = (vi in VM.fixed_var_indices[r - 1]);
6219
6247
  if(fixed) {
6220
- const vbl = VM.variables[vi - 1][1];
6221
6248
  // Set both bounds equal to the level set in the previous round, or to 0
6222
6249
  // if this is the first round
6223
6250
  if(VM.current_round) {
@@ -6239,8 +6266,8 @@ function VMI_set_const_bounds(args) {
6239
6266
  }
6240
6267
  // NOTE: to check, add this to the condition below: fixed !== ''
6241
6268
  if(DEBUGGING) {
6242
- console.log(['set_const_bounds [', k, '] LB = ', VM.sig4Dig(l),
6243
- ', UB = ', VM.sig4Dig(u), fixed].join(''));
6269
+ console.log(['set_const_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
6270
+ ' LB = ', VM.sig4Dig(l), ', UB = ', VM.sig4Dig(u), fixed].join(''));
6244
6271
  }
6245
6272
  // NOTE: since the VM vectors for lower bounds and upper bounds are
6246
6273
  // initialized with default values (0 for LB, +INF for UB), there is
@@ -6248,6 +6275,21 @@ function VMI_set_const_bounds(args) {
6248
6275
  if(l !== 0 || u < inf_val) {
6249
6276
  VM.lower_bounds[k] = l;
6250
6277
  VM.upper_bounds[k] = u;
6278
+ // If associated node is FROM-node of a "peak increase" link, then
6279
+ // the "peak increase" variables of this node must have the highest
6280
+ // UB of the node (for all t in this block, hence MAX) MINUS their
6281
+ // peak level in previous block
6282
+ if(vbl.peak_inc_var_index >= 0) {
6283
+ u = Math.max(0, u - vbl.b_peak[VM.block_count - 1]);
6284
+ const
6285
+ cvi = VM.chunk_offset + vbl.peak_inc_var_index,
6286
+ // Check if peak UB already set for previous t
6287
+ piub = VM.upper_bounds[cvi];
6288
+ // If so, use the highest value
6289
+ if(piub) u = Math.max(piub, u);
6290
+ VM.upper_bounds[cvi] = u;
6291
+ VM.upper_bounds[cvi + 1] = u;
6292
+ }
6251
6293
  }
6252
6294
  }
6253
6295
 
@@ -6255,6 +6297,7 @@ function VMI_set_var_bounds(args) {
6255
6297
  // `args`: [var_index, expression, expression]
6256
6298
  const
6257
6299
  vi = args[0],
6300
+ vbl = VM.variables[vi - 1][1],
6258
6301
  k = VM.offset + vi,
6259
6302
  r = VM.round_letters.indexOf(VM.round_sequence[VM.current_round]),
6260
6303
  // Optional fourth parameter indicates whether the solver's
@@ -6265,7 +6308,6 @@ function VMI_set_var_bounds(args) {
6265
6308
  u,
6266
6309
  fixed = (vi in VM.fixed_var_indices[r - 1]);
6267
6310
  if(fixed) {
6268
- const vbl = VM.variables[vi - 1][1];
6269
6311
  // Set both bounds equal to the level set in the previous round, or to 0
6270
6312
  // if this is the first round
6271
6313
  if(VM.current_round) {
@@ -6289,10 +6331,20 @@ function VMI_set_var_bounds(args) {
6289
6331
  if(Math.abs(l) > VM.NEAR_ZERO || u !== inf_val) {
6290
6332
  VM.lower_bounds[k] = l;
6291
6333
  VM.upper_bounds[k] = u;
6334
+ // Check for peak increase -- see comments in VMI_set_const_bound
6335
+ if(vbl.peak_inc_var_index >= 0) {
6336
+ u = Math.max(0, u - vbl.b_peak[VM.block_count - 1]);
6337
+ const
6338
+ cvi = VM.chunk_offset + vbl.peak_inc_var_index,
6339
+ piub = VM.upper_bounds[cvi];
6340
+ if(piub) u = Math.max(piub, u);
6341
+ VM.upper_bounds[cvi] = u;
6342
+ VM.upper_bounds[cvi + 1] = u;
6343
+ }
6292
6344
  }
6293
6345
  if(DEBUGGING) {
6294
- console.log(['set_var_bounds [', k, '] LB = ', args[1].variableName,
6295
- ', UB = ', args[2].variableName, fixed].join(''));
6346
+ console.log(['set_var_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
6347
+ ' LB = ', l, ', UB = ', u, fixed].join(''));
6296
6348
  }
6297
6349
  }
6298
6350
 
@@ -6534,19 +6586,20 @@ function VMI_update_cash_coefficient(args) {
6534
6586
  if((type === VM.ONE_C && args.length === 6) ||
6535
6587
  (type === VM.TWO_X && args.length === 7)) d++;
6536
6588
  }
6537
- const
6538
- // NOTE: delay > 0 affects only which variable is to be used,
6539
- // not the expressions for rates or prices!
6540
- k = (type === VM.PEAK_INC ?
6541
- VM.chunk_offset + vi :
6542
- VM.offset + vi - d*VM.cols),
6543
- t = VM.t - d;
6589
+ // `k` is the tableau column index of the variable that affects the CF
6590
+ let k = (type === VM.PEAK_INC ? VM.chunk_offset + vi :
6591
+ VM.offset + vi - d*VM.cols);
6592
+ // NOTE: delay > 0 affects only which variable is to be used,
6593
+ // not the expressions for rates or prices!
6594
+ const t = VM.t - d;
6544
6595
  // NOTE: this instruction is used only for objective function
6545
6596
  // coefficients; previously computed decision variables can be ignored
6546
6597
  if(k <= 0) return;
6547
6598
  // NOTE: peak increase can generate cash only at the first time
6548
- // step of a block, and that is when VM.offset = 0
6549
- if(type === VM.PEAK_INC && VM.offset > 0) return;
6599
+ // step of a block (when VM.offset = 0) and at the first time step
6600
+ // of the look-ahead period (when VM.offset = block length)
6601
+ if(type === VM.PEAK_INC &&
6602
+ VM.offset > 0 && VM.offset !== MODEL.block_length) return;
6550
6603
  // First compute the result to be processed
6551
6604
  let r = 0;
6552
6605
  if(type === VM.ONE_C) {
@@ -6591,11 +6644,13 @@ function VMI_update_cash_coefficient(args) {
6591
6644
  if(flow === VM.CONSUME) r = -r;
6592
6645
  if(DEBUGGING) {
6593
6646
  const vbl = (vi <= this.cols ? VM.variables[vi - 1] :
6594
- VM.chunk_variables[vi - this.cols]); //@@@ TO MAKE CORRECT!
6647
+ VM.chunk_variables[vi - this.cols]); //@@@ TO MAKE CORRECT FOR chunk vars!
6595
6648
  console.log(['update_cash_coefficient [', k, ']: ', vbl[0], ' ',
6596
6649
  vbl[1].displayName, ' (t = ', t, ') ', VM.CF_CONSTANTS[type], ' ',
6597
6650
  VM.CF_CONSTANTS[flow], ' r = ', VM.sig4Dig(r)].join(''));
6598
6651
  }
6652
+ // Use look-ahead peak increase when offset > 0
6653
+ if(type === VM.PEAK_INC && VM.offset) k++;
6599
6654
  // Then update the cash flow: cash IN if r > 0, otherwise cash OUT
6600
6655
  if(r > 0) {
6601
6656
  if(k in VM.cash_in_coefficients) {
@@ -6654,6 +6709,18 @@ function VMI_set_objective(empty) {
6654
6709
  for(let i in VM.coefficients) if(Number(i)) {
6655
6710
  VM.objective[i] = VM.coefficients[i];
6656
6711
  }
6712
+ // NOTE: For peak increase to function properly, the peak variables
6713
+ // must have a small penalty in the objective function
6714
+ if(VM.chunk_variables.length > 0) {
6715
+ for(let i = 0; i < VM.chunk_variables.length; i++) {
6716
+ const vn = VM.chunk_variables[i][0];
6717
+ if(vn.indexOf('peak') > 0) {
6718
+ // NOTE: chunk offset takes into account that indices are 0-based
6719
+ VM.objective[VM.chunk_offset + i] = -VM.PEAK_VAR_PENALTY;
6720
+ if(vn.startsWith('b')) VM.objective[VM.chunk_offset + i] -= VM.PEAK_VAR_PENALTY;
6721
+ }
6722
+ }
6723
+ }
6657
6724
  }
6658
6725
 
6659
6726
  function VMI_set_const_rhs(c) {
@@ -6832,60 +6899,61 @@ function VMI_add_bound_line_constraint(args) {
6832
6899
  VMI_add_constraint(bl.type);
6833
6900
  }
6834
6901
 
6835
- function VMI_add_peak_level_constraints(args) {
6836
- // Adds 1 or 2 constraints to compute max. level for current block
6902
+ function VMI_add_peak_increase_constraints(args) {
6903
+ // Adds constraints to compute peak increase for current block and
6904
+ // for current block + look-ahead
6837
6905
  const
6838
6906
  vi = args[0], // tableau column of L[t]
6839
6907
  cvi = args[1], // tableau column of peak
6840
6908
  lci = VM.offset + vi,
6841
- plci = VM.chunk_offset + cvi;
6909
+ cbici = VM.chunk_offset + cvi,
6910
+ cvbl = VM.chunk_variables[cvi][1];
6842
6911
  if(DEBUGGING) {
6843
6912
  console.log('add_peak_level_constraints (t = ' + VM.t + ')',
6844
6913
  VM.variables[vi - 1][0], VM.variables[vi - 1][1].displayName,
6845
- VM.chunk_variables[cvi][0], VM.chunk_variables[cvi][1].displayName);
6914
+ VM.chunk_variables[cvi][0], cvbl.displayName);
6915
+ }
6916
+ // For t = 1 to block length, add constraint to compute block peak increase
6917
+ if(VM.offset < MODEL.block_length * VM.cols) {
6918
+ // (n) L[t] - BPI[b] <= BP[b-1] (where b denotes the block number)
6919
+ VMI_clear_coefficients();
6920
+ VM.coefficients[lci] = 1;
6921
+ VM.coefficients[cbici] = -1;
6922
+ // Set RHS to highest level computed in previous blocks
6923
+ VM.rhs = cvbl.b_peak[VM.block_count - 1];
6924
+ VMI_add_constraint(VM.LE);
6925
+ return;
6846
6926
  }
6847
- // (n) L[t] - peak <= 0
6927
+ // For every t = block length + 1 to chunk length:
6848
6928
  VMI_clear_coefficients();
6929
+ // (o) L[t] - BPI[b] - CPI[b] <= BP[b-1]
6849
6930
  VM.coefficients[lci] = 1;
6850
- VM.coefficients[plci] = -1;
6851
- // No need to set RHS as it is already reset to 0
6931
+ VM.coefficients[cbici] = -1;
6932
+ // NOTE: next index always points to LA peak increase
6933
+ VM.coefficients[cbici + 1] = -1;
6934
+ // Set RHS to highest level computed in previous blocks
6935
+ VM.rhs = cvbl.b_peak[VM.block_count - 1];
6852
6936
  VMI_add_constraint(VM.LE);
6853
-
6854
- // NOTE: the other two constraints need to be defined only for bt = 0
6855
- if(VM.offset > 0) return;
6856
- VMI_clear_coefficients();
6857
- // (o) peak[b] >= peak[b-1]
6858
- VM.coefficients[plci] = 1;
6859
- // Set RHS to peak computed in previous block
6860
- VM.rhs = VM.chunk_variables[cvi][1].peak_level[VM.block_count - 1];
6861
- VMI_add_constraint(VM.GE);
6862
- // NOTE: these constraints will ensure that peak >= all L[t] in the
6863
- // chunk, but this does not prevent peak from taking a value higher
6864
- // than the highest L[t]; preventing this would require defining as
6865
- // many binary variables as there are time steps in the chunk.
6866
6937
  }
6867
6938
 
6868
6939
  function VMI_add_peak_increase_at_t_0(args) {
6869
- // This operation should result in adding (peak[b] - peak[b-1) * link rate
6940
+ // This operation should result in adding peak increase[b] * link rate
6870
6941
  // to the product level for which a constraint is being defined.
6871
- // This means that the coefficient for the chunk variable peak[b] must
6872
- // equal the link rate, while the value of peak[b-1] (computed in the
6873
- // previous block) * link rate must be subtracted, hence *added* to
6874
- // the RHS.
6875
- // NOTE: only execute this operation at block time = 0
6876
- if(VM.offset > 0) return;
6942
+ // This means that the coefficient for (B or LA) peak increase[b] must
6943
+ // equal the link rate.
6944
+ // NOTE: only execute this operation at start of block or of LA period
6945
+ if(VM.offset && VM.offset !== MODEL.block_length * VM.cols) return;
6877
6946
  const
6878
- cvi = args[0],
6879
- obj = VM.chunk_variables[cvi][1],
6880
- prev_pl = obj.peak_level[VM.block_count - 1],
6947
+ cvi = args[0] + (VM.offset ? 1 : 0),
6948
+ tpl = VM.chunk_variables[cvi],
6881
6949
  rr = args[1].result(VM.t);
6882
6950
  if(DEBUGGING) {
6883
6951
  console.log('VMI_add_peak_increase_at_t_0 (t = ' + VM.t + ')',
6884
- obj.displayName,
6885
- 'previous peak =', prev_pl);
6952
+ tpl[0], tpl[1].displayName);
6886
6953
  }
6887
6954
  VM.coefficients[VM.chunk_offset + cvi] = rr;
6888
- VM.rhs += prev_pl * rr;
6955
+ // NOTE: no "add constraint" as this instruction is only part of the
6956
+ // series of coefficient-setting instructions
6889
6957
  }
6890
6958
 
6891
6959
  // NOTE: the global constants below are not defined in linny-r-globals.js