linny-r 1.1.17 → 1.1.19

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.17",
3
+ "version": "1.1.19",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -91,7 +91,7 @@ function getVersionInfo() {
91
91
  info.latest = 0;
92
92
  }
93
93
  if(!info.latest) {
94
- console.log(connectionErrorText('Could not connect to https://nodejs.com'));
94
+ console.log(connectionErrorText('Could not connect to https://registry.npmjs.org/'));
95
95
  } else if(!info.up_to_date) {
96
96
  console.log('UPDATE: Version ' + info.latest + ' was released on ' +
97
97
  info.latest_time.toString());
@@ -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
 
@@ -8065,7 +8074,8 @@ class Repository {
8065
8074
  if(include) {
8066
8075
  // Include module into current model
8067
8076
  REPOSITORY_BROWSER.promptForInclusion(
8068
- this.name, this.module_names[n], parseXML(data));
8077
+ this.name, this.module_names[n],
8078
+ parseXML(data.replace(/%23/g, '#')));
8069
8079
  } else {
8070
8080
  if(UI.loadModelFromXML(data)) {
8071
8081
  UI.notify(`Model <tt>${this.module_names[n]}</tt> ` +
@@ -12003,7 +12013,8 @@ class GUIExperimentManager extends ExperimentManager {
12003
12013
  ss = VM.sig2Dig(r.solver_seconds),
12004
12014
  ssp = (rdt < VM.NEAR_ZERO ? '' :
12005
12015
  ' (' + Math.round(r.solver_seconds * 100 / rdt) + '%)'),
12006
- w = (r.warning_count > 0 ? pluralS(r.warning_count, 'warning') + '. ' : '');
12016
+ w = (r.warning_count > 0 ?
12017
+ ' ' + pluralS(r.warning_count, 'warning') + '. ' : '');
12007
12018
  cell.title = ['Run #', i, ' (', r.time_steps, ' time steps of ',
12008
12019
  r.time_step_duration, ' h) took ', rdts, ' s. Solver used ', ss, ' s',
12009
12020
  ssp, '.', w, (rr ? `
@@ -14792,6 +14803,3 @@ if (MODEL.focal_cluster === fc) {
14792
14803
  }
14793
14804
  }
14794
14805
  } // END of class UndoStack
14795
-
14796
-
14797
-
@@ -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
+
2629
2635
  }
2630
2636
  for(obj in this.links) if(this.links.hasOwnProperty(obj)) {
2631
2637
  l = this.links[obj];
@@ -7265,7 +7271,7 @@ class Link {
7265
7271
  IO_CONTEXT.addedLink(this);
7266
7272
  // Contextualize the rate and delay expressions
7267
7273
  IO_CONTEXT.rewrite(this.relative_rate);
7268
- IO_CONTEXT.rewrite(this.delay);
7274
+ IO_CONTEXT.rewrite(this.flow_delay);
7269
7275
  }
7270
7276
  }
7271
7277
 
@@ -1471,6 +1471,9 @@ class VirtualMachine {
1471
1471
  // Base penalty of 10 is high relative to the (scaled) coefficients of the
1472
1472
  // cash flows in the objective function (typically +/- 1)
1473
1473
  this.BASE_PENALTY = 10;
1474
+ // Peak variable penalty is added to make solver choose the *smallest*
1475
+ // value that is greater than or equal to X[t] for all t as "peak value"
1476
+ this.PEAK_VAR_PENALTY = 0.01;
1474
1477
 
1475
1478
  // NOTE: the VM uses numbers >> +INF to denote special computation results
1476
1479
  this.EXCEPTION = 1e+36; // to test for any exceptional value
@@ -2130,7 +2133,7 @@ class VirtualMachine {
2130
2133
  p.start_up_count_var_index = -1;
2131
2134
  p.suc_on_var_index = -1;
2132
2135
  p.first_commit_var_index = -1;
2133
- p.chunk_var_index = -1;
2136
+ p.peak_inc_var_index = -1;
2134
2137
  if(p instanceof Product) {
2135
2138
  p.stock_LE_slack_var_index = -1;
2136
2139
  p.stock_GE_slack_var_index = -1;
@@ -2183,10 +2186,11 @@ class VirtualMachine {
2183
2186
  const
2184
2187
  type = tuple[0],
2185
2188
  obj = tuple[1];
2186
- if(type === 'peak') {
2189
+ if(type.indexOf('-peak') > 0) {
2187
2190
  // Peak level variables have an array as node property
2188
2191
  const c = Math.trunc(t / this.block_length);
2189
- return obj.peak_level[c];
2192
+ if(type.startsWith('b')) return obj.b_peak_inc[c];
2193
+ return obj.la_peak_inc[c];
2190
2194
  }
2191
2195
  const prior_level = obj.actualLevel(t);
2192
2196
  if(type === 'OO') return prior_level > 0 ? 1 : 0;
@@ -2206,7 +2210,7 @@ class VirtualMachine {
2206
2210
  return prior_level;
2207
2211
  }
2208
2212
 
2209
- get variablesLegend() {
2213
+ variablesLegend(block) {
2210
2214
  // Returns a string with each variable code and full name on a separate line
2211
2215
  const
2212
2216
  vcnt = this.variables.length,
@@ -2222,11 +2226,14 @@ class VirtualMachine {
2222
2226
  l += v + ' [' + this.variables[i][0] + p + ']\n';
2223
2227
  }
2224
2228
  if(this.chunk_variables.length > 0) {
2229
+ // NOTE: chunk offset for last block may be lower than standard
2230
+ const chof = (block >= this.nr_of_blocks ? this.chunk_offset :
2231
+ this.cols * this.chunk_length + 1);
2225
2232
  for(let i = 0; i < this.chunk_variables.length; i++) {
2226
2233
  const
2227
2234
  obj = this.chunk_variables[i][1],
2228
2235
  // NOTE: chunk offset takes into account that indices are 0-based
2229
- cvi = this.chunk_offset + i;
2236
+ cvi = chof + i;
2230
2237
  let v = (this.solver_name === 'lp_solve' ?
2231
2238
  'C' + cvi : 'X' + cvi.toString().padStart(z, '0'));
2232
2239
  v += ' '.slice(v.length) + obj.displayName;
@@ -2440,15 +2447,20 @@ class VirtualMachine {
2440
2447
 
2441
2448
  // Now all variables that get a tableau column in each time step have
2442
2449
  // been defined; next step is to add "chunk variables"
2443
- // NOTE: chunk variables are "free", i.e., have no bound constraints
2444
2450
  let cvi = 0;
2445
- // Add chunk variable for processes having a peak increase link
2451
+ // Add *two* chunk variables for processes having a peak increase link
2446
2452
  for(i = 0; i < process_keys.length; i++) {
2447
2453
  k = process_keys[i];
2448
2454
  p = MODEL.processes[k];
2449
2455
  if(!MODEL.ignored_entities[k] && p.needsMaximumData) {
2450
- p.chunk_var_index = cvi;
2451
- this.chunk_variables.push(['peak', p]);
2456
+ // "peak increase" for block
2457
+ p.peak_inc_var_index = cvi;
2458
+ this.chunk_variables.push(['b-peak', p]);
2459
+ cvi++;
2460
+ // additional "peak increase" for the look-ahead period
2461
+ // NOTE: no need to record the second index as it wil allways be
2462
+ // equal to block peak index + 1
2463
+ this.chunk_variables.push(['la-peak', p]);
2452
2464
  cvi++;
2453
2465
  }
2454
2466
  }
@@ -2457,14 +2469,19 @@ class VirtualMachine {
2457
2469
  k = product_keys[i];
2458
2470
  p = MODEL.products[k];
2459
2471
  if(!MODEL.ignored_entities[k] && p.needsMaximumData) {
2460
- p.chunk_var_index = cvi;
2461
- this.chunk_variables.push(['peak', p]);
2472
+ p.peak_inc_var_index = cvi;
2473
+ this.chunk_variables.push(['b-peak', p]);
2474
+ cvi++;
2475
+ this.chunk_variables.push(['la-peak', p]);
2462
2476
  cvi++;
2463
2477
  }
2464
2478
  }
2465
2479
 
2466
2480
  // Now *all* variables have been defined; next step is to set their bounds
2467
2481
 
2482
+ // NOTE: chunk variables of node `p` have LB = 0 and UB = UB of `p`;
2483
+ // this is effectuated by the VM "set bounds" instructions at run time
2484
+
2468
2485
  // NOTE: under normal assumptions (all processes having LB >= 0), bounds on
2469
2486
  // actor cash flow variables need NOT be set because cash IN and cash OUT
2470
2487
  // will then always be >= 0 (solver's default bounds).
@@ -2668,12 +2685,13 @@ class VirtualMachine {
2668
2685
  VM.PRODUCE, VM.SPIN_RES, p.on_off_var_index, l.flow_delay, vi,
2669
2686
  l.from_node.upper_bound, tnpx, l.relative_rate]]);
2670
2687
  } 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
2688
+ // NOTE: "peak increase" may be > 0 only in the first time step
2689
+ // of the block being optimized, and in the first step of the
2690
+ // look-ahead period (if peak rises in that period), and will
2691
+ // be 0 in all other time steps; the VM instruction handles this
2674
2692
  // NOTE: delay is always 0 for this link flow
2675
2693
  this.code.push([VMI_update_cash_coefficient, [
2676
- VM.PRODUCE, VM.PEAK_INC, p.chunk_var_index, 0,
2694
+ VM.PRODUCE, VM.PEAK_INC, p.peak_inc_var_index, 0,
2677
2695
  tnpx, l.relative_rate]]);
2678
2696
  } else if(tnpx.isStatic && l.relative_rate.isStatic) {
2679
2697
  // If link rate and product price are static, only add the variable
@@ -2850,7 +2868,7 @@ class VirtualMachine {
2850
2868
  } else if(l.multiplier === VM.LM_SHUTDOWN) {
2851
2869
  vi = fn.shut_down_var_index;
2852
2870
  } else if(l.multiplier === VM.LM_PEAK_INC) {
2853
- vi = fn.chunk_var_index;
2871
+ vi = fn.peak_inc_var_index;
2854
2872
  } else {
2855
2873
  vi = fn.level_var_index;
2856
2874
  }
@@ -3089,22 +3107,23 @@ class VirtualMachine {
3089
3107
  (to prevent that FC[t] = 1 when SO[t-1] = 1 and SO[t] = 1, i.e.,
3090
3108
  SC was already > 0)
3091
3109
 
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.
3110
+ To calculate the peak increase values, we need two continuous
3111
+ "chunk variables", i.e., only 1 tableau column per chunk, not 1 for
3112
+ each time step. These variables BPI and CPI will compute the highest
3113
+ value (for all t in the block (B) and for the chunk (C)) of the
3114
+ difference L[t] - block peak (BP) of previous block. This requires
3115
+ one equation for every t = 1, ..., block length:
3116
+ (n) L[t] - BPI[b] <= BP[b-1] (where b denotes the block number)
3117
+ plus one equation for every t = block length + 1 to chunk length:
3118
+ (o) L[t] - BPI[b] - CPI[b] <= BP[b-1]
3119
+ This ensures that CPI is the *additional* increase in the look-ahead
3120
+ Then use BPI[b] in first time step if block, and CPI[b] at first
3121
+ time step of the look-ahead period to compute the actual flow for
3122
+ the "peak increase" links. For all other time steps this AF equals 0.
3123
+
3124
+ NOTE: These constraints alone set the lower bound for BPI and CPI, so
3125
+ these variables can take on higher values. The modeler must ensure
3126
+ that there is a cost associated with the actual flow, not a revenue.
3108
3127
  */
3109
3128
  // NOTE: as of 20 June 2021, binary attributes of products are also computed
3110
3129
  const pp_nodes = [];
@@ -3350,12 +3369,12 @@ class VirtualMachine {
3350
3369
  // Check whether constraints (n) through (p) need to be added
3351
3370
  // to compute the peak level for a block of time steps
3352
3371
  // NOTE: this is independent of the binary variables!
3353
- if(p.chunk_var_index >= 0) {
3372
+ if(p.peak_inc_var_index >= 0) {
3354
3373
  this.code.push(
3355
3374
  // One special instruction implements this operation, as part
3356
3375
  // 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]]
3376
+ [VMI_add_peak_increase_constraints,
3377
+ [p.level_var_index, p.peak_inc_var_index]]
3359
3378
  );
3360
3379
  }
3361
3380
  }
@@ -3648,13 +3667,17 @@ class VirtualMachine {
3648
3667
  b++;
3649
3668
  }
3650
3669
  }
3651
- // Get values of peak level variables from solution vector
3670
+ // Get values of peak increase variables from solution vector
3652
3671
  // NOTE: computed offset takes into account that chunk variable list
3653
3672
  // is zero-based!
3654
3673
  const offset = this.cols * abl;
3655
3674
  for(let i = 0; i < ncv; i++) {
3656
3675
  const p = this.chunk_variables[i][1];
3657
- p.peak_level[block] = x[offset + i];
3676
+ p.b_peak_inc[block] = x[offset + i];
3677
+ i++;
3678
+ p.la_peak_inc[block] = x[offset + i];
3679
+ // Compute the peak from the peak increase
3680
+ p.b_peak[block] = p.b_peak[block - 1] + p.b_peak_inc[block];
3658
3681
  }
3659
3682
  // Add warning to messages if slack has been used
3660
3683
  // NOTE: only check after the last round has been evaluated
@@ -3764,11 +3787,13 @@ class VirtualMachine {
3764
3787
  p.inputs[j].relative_rate.result(bt));
3765
3788
  }
3766
3789
  } else if(l.multiplier === VM.LM_PEAK_INC) {
3767
- // Actual flow can only be > 0 in first time step of block
3790
+ // Actual flow over "peak increase" link is zero unless...
3768
3791
  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]);
3792
+ // first time step, then "block peak increase"...
3793
+ pl = p.b_peak_inc[block];
3794
+ } else if(i === MODEL.block_length) {
3795
+ // or first step of look-ahead, then "additional increase"
3796
+ pl = p.la_peak_inc[block];
3772
3797
  } else {
3773
3798
  pl = 0;
3774
3799
  }
@@ -6207,6 +6232,7 @@ function VMI_set_const_bounds(args) {
6207
6232
  // `args`: [var_index, number, number]
6208
6233
  const
6209
6234
  vi = args[0],
6235
+ vbl = VM.variables[vi - 1][1],
6210
6236
  k = VM.offset + vi,
6211
6237
  r = VM.round_letters.indexOf(VM.round_sequence[VM.current_round]),
6212
6238
  // Optional fourth parameter indicates whether the solver's
@@ -6217,7 +6243,6 @@ function VMI_set_const_bounds(args) {
6217
6243
  u,
6218
6244
  fixed = (vi in VM.fixed_var_indices[r - 1]);
6219
6245
  if(fixed) {
6220
- const vbl = VM.variables[vi - 1][1];
6221
6246
  // Set both bounds equal to the level set in the previous round, or to 0
6222
6247
  // if this is the first round
6223
6248
  if(VM.current_round) {
@@ -6239,8 +6264,8 @@ function VMI_set_const_bounds(args) {
6239
6264
  }
6240
6265
  // NOTE: to check, add this to the condition below: fixed !== ''
6241
6266
  if(DEBUGGING) {
6242
- console.log(['set_const_bounds [', k, '] LB = ', VM.sig4Dig(l),
6243
- ', UB = ', VM.sig4Dig(u), fixed].join(''));
6267
+ console.log(['set_const_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
6268
+ ' LB = ', VM.sig4Dig(l), ', UB = ', VM.sig4Dig(u), fixed].join(''));
6244
6269
  }
6245
6270
  // NOTE: since the VM vectors for lower bounds and upper bounds are
6246
6271
  // initialized with default values (0 for LB, +INF for UB), there is
@@ -6248,6 +6273,21 @@ function VMI_set_const_bounds(args) {
6248
6273
  if(l !== 0 || u < inf_val) {
6249
6274
  VM.lower_bounds[k] = l;
6250
6275
  VM.upper_bounds[k] = u;
6276
+ // If associated node is FROM-node of a "peak increase" link, then
6277
+ // the "peak increase" variables of this node must have the highest
6278
+ // UB of the node (for all t in this block, hence MAX) MINUS their
6279
+ // peak level in previous block
6280
+ if(vbl.peak_inc_var_index >= 0) {
6281
+ u = Math.max(0, u - vbl.b_peak[VM.block_count - 1]);
6282
+ const
6283
+ cvi = VM.chunk_offset + vbl.peak_inc_var_index,
6284
+ // Check if peak UB already set for previous t
6285
+ piub = VM.upper_bounds[cvi];
6286
+ // If so, use the highest value
6287
+ if(piub) u = Math.max(piub, u);
6288
+ VM.upper_bounds[cvi] = u;
6289
+ VM.upper_bounds[cvi + 1] = u;
6290
+ }
6251
6291
  }
6252
6292
  }
6253
6293
 
@@ -6255,6 +6295,7 @@ function VMI_set_var_bounds(args) {
6255
6295
  // `args`: [var_index, expression, expression]
6256
6296
  const
6257
6297
  vi = args[0],
6298
+ vbl = VM.variables[vi - 1][1],
6258
6299
  k = VM.offset + vi,
6259
6300
  r = VM.round_letters.indexOf(VM.round_sequence[VM.current_round]),
6260
6301
  // Optional fourth parameter indicates whether the solver's
@@ -6265,7 +6306,6 @@ function VMI_set_var_bounds(args) {
6265
6306
  u,
6266
6307
  fixed = (vi in VM.fixed_var_indices[r - 1]);
6267
6308
  if(fixed) {
6268
- const vbl = VM.variables[vi - 1][1];
6269
6309
  // Set both bounds equal to the level set in the previous round, or to 0
6270
6310
  // if this is the first round
6271
6311
  if(VM.current_round) {
@@ -6289,10 +6329,20 @@ function VMI_set_var_bounds(args) {
6289
6329
  if(Math.abs(l) > VM.NEAR_ZERO || u !== inf_val) {
6290
6330
  VM.lower_bounds[k] = l;
6291
6331
  VM.upper_bounds[k] = u;
6332
+ // Check for peak increase -- see comments in VMI_set_const_bound
6333
+ if(vbl.peak_inc_var_index >= 0) {
6334
+ u = Math.max(0, u - vbl.b_peak[VM.block_count - 1]);
6335
+ const
6336
+ cvi = VM.chunk_offset + vbl.peak_inc_var_index,
6337
+ piub = VM.upper_bounds[cvi];
6338
+ if(piub) u = Math.max(piub, u);
6339
+ VM.upper_bounds[cvi] = u;
6340
+ VM.upper_bounds[cvi + 1] = u;
6341
+ }
6292
6342
  }
6293
6343
  if(DEBUGGING) {
6294
- console.log(['set_var_bounds [', k, '] LB = ', args[1].variableName,
6295
- ', UB = ', args[2].variableName, fixed].join(''));
6344
+ console.log(['set_var_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
6345
+ ' LB = ', l, ', UB = ', u, fixed].join(''));
6296
6346
  }
6297
6347
  }
6298
6348
 
@@ -6534,19 +6584,20 @@ function VMI_update_cash_coefficient(args) {
6534
6584
  if((type === VM.ONE_C && args.length === 6) ||
6535
6585
  (type === VM.TWO_X && args.length === 7)) d++;
6536
6586
  }
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;
6587
+ // `k` is the tableau column index of the variable that affects the CF
6588
+ let k = (type === VM.PEAK_INC ? VM.chunk_offset + vi :
6589
+ VM.offset + vi - d*VM.cols);
6590
+ // NOTE: delay > 0 affects only which variable is to be used,
6591
+ // not the expressions for rates or prices!
6592
+ const t = VM.t - d;
6544
6593
  // NOTE: this instruction is used only for objective function
6545
6594
  // coefficients; previously computed decision variables can be ignored
6546
6595
  if(k <= 0) return;
6547
6596
  // 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;
6597
+ // step of a block (when VM.offset = 0) and at the first time step
6598
+ // of the look-ahead period (when VM.offset = block length)
6599
+ if(type === VM.PEAK_INC &&
6600
+ VM.offset > 0 && VM.offset !== MODEL.block_length) return;
6550
6601
  // First compute the result to be processed
6551
6602
  let r = 0;
6552
6603
  if(type === VM.ONE_C) {
@@ -6591,11 +6642,13 @@ function VMI_update_cash_coefficient(args) {
6591
6642
  if(flow === VM.CONSUME) r = -r;
6592
6643
  if(DEBUGGING) {
6593
6644
  const vbl = (vi <= this.cols ? VM.variables[vi - 1] :
6594
- VM.chunk_variables[vi - this.cols]); //@@@ TO MAKE CORRECT!
6645
+ VM.chunk_variables[vi - this.cols]); //@@@ TO MAKE CORRECT FOR chunk vars!
6595
6646
  console.log(['update_cash_coefficient [', k, ']: ', vbl[0], ' ',
6596
6647
  vbl[1].displayName, ' (t = ', t, ') ', VM.CF_CONSTANTS[type], ' ',
6597
6648
  VM.CF_CONSTANTS[flow], ' r = ', VM.sig4Dig(r)].join(''));
6598
6649
  }
6650
+ // Use look-ahead peak increase when offset > 0
6651
+ if(type === VM.PEAK_INC && VM.offset) k++;
6599
6652
  // Then update the cash flow: cash IN if r > 0, otherwise cash OUT
6600
6653
  if(r > 0) {
6601
6654
  if(k in VM.cash_in_coefficients) {
@@ -6654,6 +6707,18 @@ function VMI_set_objective(empty) {
6654
6707
  for(let i in VM.coefficients) if(Number(i)) {
6655
6708
  VM.objective[i] = VM.coefficients[i];
6656
6709
  }
6710
+ // NOTE: For peak increase to function properly, the peak variables
6711
+ // must have a small penalty in the objective function
6712
+ if(VM.chunk_variables.length > 0) {
6713
+ for(let i = 0; i < VM.chunk_variables.length; i++) {
6714
+ const vn = VM.chunk_variables[i][0];
6715
+ if(vn.indexOf('peak') > 0) {
6716
+ // NOTE: chunk offset takes into account that indices are 0-based
6717
+ VM.objective[VM.chunk_offset + i] = -VM.PEAK_VAR_PENALTY;
6718
+ if(vn.startsWith('b')) VM.objective[VM.chunk_offset + i] -= VM.PEAK_VAR_PENALTY;
6719
+ }
6720
+ }
6721
+ }
6657
6722
  }
6658
6723
 
6659
6724
  function VMI_set_const_rhs(c) {
@@ -6832,60 +6897,61 @@ function VMI_add_bound_line_constraint(args) {
6832
6897
  VMI_add_constraint(bl.type);
6833
6898
  }
6834
6899
 
6835
- function VMI_add_peak_level_constraints(args) {
6836
- // Adds 1 or 2 constraints to compute max. level for current block
6900
+ function VMI_add_peak_increase_constraints(args) {
6901
+ // Adds constraints to compute peak increase for current block and
6902
+ // for current block + look-ahead
6837
6903
  const
6838
6904
  vi = args[0], // tableau column of L[t]
6839
6905
  cvi = args[1], // tableau column of peak
6840
6906
  lci = VM.offset + vi,
6841
- plci = VM.chunk_offset + cvi;
6907
+ cbici = VM.chunk_offset + cvi,
6908
+ cvbl = VM.chunk_variables[cvi][1];
6842
6909
  if(DEBUGGING) {
6843
6910
  console.log('add_peak_level_constraints (t = ' + VM.t + ')',
6844
6911
  VM.variables[vi - 1][0], VM.variables[vi - 1][1].displayName,
6845
- VM.chunk_variables[cvi][0], VM.chunk_variables[cvi][1].displayName);
6912
+ VM.chunk_variables[cvi][0], cvbl.displayName);
6913
+ }
6914
+ // For t = 1 to block length, add constraint to compute block peak increase
6915
+ if(VM.offset < MODEL.block_length * VM.cols) {
6916
+ // (n) L[t] - BPI[b] <= BP[b-1] (where b denotes the block number)
6917
+ VMI_clear_coefficients();
6918
+ VM.coefficients[lci] = 1;
6919
+ VM.coefficients[cbici] = -1;
6920
+ // Set RHS to highest level computed in previous blocks
6921
+ VM.rhs = cvbl.b_peak[VM.block_count - 1];
6922
+ VMI_add_constraint(VM.LE);
6923
+ return;
6846
6924
  }
6847
- // (n) L[t] - peak <= 0
6925
+ // For every t = block length + 1 to chunk length:
6848
6926
  VMI_clear_coefficients();
6927
+ // (o) L[t] - BPI[b] - CPI[b] <= BP[b-1]
6849
6928
  VM.coefficients[lci] = 1;
6850
- VM.coefficients[plci] = -1;
6851
- // No need to set RHS as it is already reset to 0
6929
+ VM.coefficients[cbici] = -1;
6930
+ // NOTE: next index always points to LA peak increase
6931
+ VM.coefficients[cbici + 1] = -1;
6932
+ // Set RHS to highest level computed in previous blocks
6933
+ VM.rhs = cvbl.b_peak[VM.block_count - 1];
6852
6934
  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
6935
  }
6867
6936
 
6868
6937
  function VMI_add_peak_increase_at_t_0(args) {
6869
- // This operation should result in adding (peak[b] - peak[b-1) * link rate
6938
+ // This operation should result in adding peak increase[b] * link rate
6870
6939
  // 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;
6940
+ // This means that the coefficient for (B or LA) peak increase[b] must
6941
+ // equal the link rate.
6942
+ // NOTE: only execute this operation at start of block or of LA period
6943
+ if(VM.offset && VM.offset !== MODEL.block_length * VM.cols) return;
6877
6944
  const
6878
- cvi = args[0],
6879
- obj = VM.chunk_variables[cvi][1],
6880
- prev_pl = obj.peak_level[VM.block_count - 1],
6945
+ cvi = args[0] + (VM.offset ? 1 : 0),
6946
+ tpl = VM.chunk_variables[cvi],
6881
6947
  rr = args[1].result(VM.t);
6882
6948
  if(DEBUGGING) {
6883
6949
  console.log('VMI_add_peak_increase_at_t_0 (t = ' + VM.t + ')',
6884
- obj.displayName,
6885
- 'previous peak =', prev_pl);
6950
+ tpl[0], tpl[1].displayName);
6886
6951
  }
6887
6952
  VM.coefficients[VM.chunk_offset + cvi] = rr;
6888
- VM.rhs += prev_pl * rr;
6953
+ // NOTE: no "add constraint" as this instruction is only part of the
6954
+ // series of coefficient-setting instructions
6889
6955
  }
6890
6956
 
6891
6957
  // NOTE: the global constants below are not defined in linny-r-globals.js