linny-r 1.6.7 → 1.7.0

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.
@@ -88,7 +88,6 @@ class LinnyRModel {
88
88
  this.selected_round = 0;
89
89
 
90
90
  // Model settings
91
- this.timeout_period = 30; // max. solver time in seconds
92
91
  this.block_length = 1;
93
92
  this.start_period = 1; // defines starting point in datasets
94
93
  this.end_period = 1;
@@ -99,6 +98,12 @@ class LinnyRModel {
99
98
  this.report_results = false;
100
99
  this.show_block_arrows = true;
101
100
  this.last_zoom_factor = 1;
101
+
102
+ // Default solver settings
103
+ this.timeout_period = 30; // max. solver time in seconds
104
+ this.preferred_solver = ''; // empty string denotes "use default"
105
+ this.integer_tolerance = 5e-7; // integer feasibility tolerance
106
+ this.MIP_gap = 1e-4; // relative MIP gap
102
107
 
103
108
  // Sensitivity-related properties
104
109
  this.base_case_selectors = '';
@@ -140,17 +145,18 @@ class LinnyRModel {
140
145
  /* METHODS THAT LOOKUP ENTITIES, OR INFER PROPERTIES */
141
146
 
142
147
  get simulationTimeStep() {
143
- // Returns actual model time step, rather than `t`, which is relative to the
144
- // start of the simulation period
148
+ // Return actual model time step, rather than `t`, which is relative
149
+ // to the start of the simulation period.
145
150
  return this.t + this.start_period - 1;
146
151
  }
147
152
 
148
153
  get timeStepDuration() {
149
- // Returns duration of 1 time step in hours
154
+ // Return duration of 1 time step in hours.
150
155
  return this.time_scale * VM.time_unit_values[this.time_unit];
151
156
  }
152
157
 
153
158
  get outcomes() {
159
+ // Return the list of outcome datasets.
154
160
  const olist = [];
155
161
  for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
156
162
  if(this.datasets[k].outcome) olist.push(this.datasets[k]);
@@ -2631,17 +2637,17 @@ class LinnyRModel {
2631
2637
  }
2632
2638
 
2633
2639
  initFromXML(node) {
2634
- // Initialize a model from the XML tree with `node` as root
2635
- // NOTE: do NOT reset and initialize basic model properties when *including*
2636
- // a module into the current model
2637
- // NOTE: obsolete XML nodes indicate: legacy Linny-R model
2640
+ // Initialize a model from the XML tree with `node` as root.
2641
+ // NOTE: do NOT reset and initialize basic model properties when
2642
+ // *including* a module into the current model.
2643
+ // NOTE: Obsolete XML nodes indicate: legacy Linny-R model.
2638
2644
  const legacy_model = (nodeParameterValue(node, 'view-options') +
2639
2645
  nodeParameterValue(node, 'autosave') +
2640
2646
  nodeParameterValue(node, 'look-ahead') +
2641
2647
  nodeParameterValue(node, 'save-series') +
2642
2648
  nodeParameterValue(node, 'show-lp') +
2643
2649
  nodeParameterValue(node, 'optional-slack')).length > 0;
2644
- // Flag to set when legacy time series data are added
2650
+ // Flag to set when legacy time series data are added.
2645
2651
  this.legacy_datasets = false;
2646
2652
  if(!IO_CONTEXT) {
2647
2653
  this.reset();
@@ -2668,7 +2674,12 @@ class LinnyRModel {
2668
2674
  this.version = xmlDecoded(nodeContentByTag(node, 'version'));
2669
2675
  this.timeout_period = Math.max(0,
2670
2676
  safeStrToInt(nodeContentByTag(node, 'timeout-period')));
2671
- // Legacy models have tag "optimization-period" instead of "block-length"
2677
+ this.preferred_solver = xmlDecoded(
2678
+ nodeContentByTag(node, 'preferred-solver'));
2679
+ this.integer_tolerance = safeStrToFloat(
2680
+ nodeContentByTag(node, 'integer-tolerance'), 5e-7);
2681
+ this.MIP_gap = safeStrToFloat(nodeContentByTag(node, 'mip-gap'), 1e-4);
2682
+ // Legacy models have tag "optimization-period" instead of "block-length".
2672
2683
  const bl_str = nodeContentByTag(node, 'block-length') ||
2673
2684
  nodeContentByTag(node, 'optimization-period');
2674
2685
  this.block_length = Math.max(1, safeStrToInt(node, bl_str));
@@ -2693,7 +2704,7 @@ class LinnyRModel {
2693
2704
  if(!this.default_unit) this.default_unit = CONFIGURATION.default_scale_unit;
2694
2705
  } // END IF *not* including a model
2695
2706
 
2696
- // Declare some local variables that will be used a lot
2707
+ // Declare some local variables that will be used a lot.
2697
2708
  let i,
2698
2709
  c,
2699
2710
  name,
@@ -3014,7 +3025,10 @@ class LinnyRModel {
3014
3025
  '</default-scale-unit><currency-unit>', xmlEncoded(this.currency_unit),
3015
3026
  '</currency-unit><grid-pixels>', this.grid_pixels,
3016
3027
  '</grid-pixels><timeout-period>', this.timeout_period,
3017
- '</timeout-period><block-length>', this.block_length,
3028
+ '</timeout-period><preferred-solver>', xmlEncoded(this.preferred_solver),
3029
+ '</preferred-solver><integer-tolerance>', this.integer_tolerance,
3030
+ '</integer-tolerance><mip-gap>', this.MIP_gap,
3031
+ '</mip-gap><block-length>', this.block_length,
3018
3032
  '</block-length><start-period>', this.start_period,
3019
3033
  '</start-period><end-period>', this.end_period,
3020
3034
  '</end-period><look-ahead-period>', this.look_ahead,
@@ -2328,7 +2328,13 @@ class VirtualMachine {
2328
2328
  // Statistics that can be calculated for outcomes and experiment run results
2329
2329
  this.outcome_statistics =
2330
2330
  ['LAST', 'MAX', 'MEAN', 'MIN', 'N', 'NZ', 'SD', 'SUM', 'VAR'];
2331
- }
2331
+ this.solver_names = {
2332
+ gurobi: 'Gurobi',
2333
+ cplex: 'CPLEX',
2334
+ scip: 'SCIP',
2335
+ lp_solve: 'LP_solve'
2336
+ };
2337
+ }
2332
2338
 
2333
2339
  reset() {
2334
2340
  // Reset the virtual machine so that it can execute the model again.
@@ -2945,6 +2951,7 @@ class VirtualMachine {
2945
2951
  return obj.la_peak_inc[c];
2946
2952
  }
2947
2953
  const prior_level = obj.actualLevel(t);
2954
+ //console.log('HERE obj prilev t', obj.displayName, prior_level, t, obj.level);
2948
2955
  if(type === 'OO') return prior_level > 0 ? 1 : 0;
2949
2956
  if(type === 'IZ') return prior_level === 0 ? 1 : 0;
2950
2957
  // Start-up at time t entails that t is in the list of start-up
@@ -4305,7 +4312,7 @@ class VirtualMachine {
4305
4312
  // Return floating point number `n`, or +INF or -INF if the absolute
4306
4313
  // value of `n` is relatively (!) close to the VM infinity constants
4307
4314
  // (since the solver may return imprecise values of such magnitude).
4308
- if(n > 0.5 * VM.PLUS_INFINITY && n < VM.BEYOND_PLUS_INFINITY) {
4315
+ if(n > 0.5 * VM.PLUS_INFINITY && n < VM.BEYOND_PLUS_INFINITY) {
4309
4316
  return VM.PLUS_INFINITY;
4310
4317
  }
4311
4318
  if(n < 0.5 * VM.MINUS_INFINITY && n > VM.BEYOND_MINUS_INFINITY) {
@@ -4526,6 +4533,21 @@ class VirtualMachine {
4526
4533
  }
4527
4534
  }
4528
4535
 
4536
+ severestIssue(list, result) {
4537
+ // Returns severest exception code or +/- INFINITY in `list`, or the
4538
+ // result of the computation that involves the elements of `list`.
4539
+ let issue = 0;
4540
+ for(let i = 0; i < list.length; i++) {
4541
+ if(list[i] <= VM.MINUS_INFINITY) {
4542
+ issue = Math.min(list[i], issue);
4543
+ } else if(list[i] >= VM.PLUS_INFINITY) {
4544
+ issue = Math.max(list[i], issue);
4545
+ }
4546
+ }
4547
+ if(issue) return issue;
4548
+ return result;
4549
+ }
4550
+
4529
4551
  calculateDependentVariables(block) {
4530
4552
  // Calculate the values of all model variables that depend on the
4531
4553
  // values of the decision variables output by the solver.
@@ -4539,7 +4561,7 @@ class VirtualMachine {
4539
4561
  cbl = this.actualBlockLength(block);
4540
4562
 
4541
4563
  // FIRST: Calculate the actual flows on links.
4542
- let b, bt, p, pl, ld;
4564
+ let b, bt, p, pl, ld, ci;
4543
4565
  for(let l in MODEL.links) if(MODEL.links.hasOwnProperty(l) &&
4544
4566
  !MODEL.ignored_entities[l]) {
4545
4567
  l = MODEL.links[l];
@@ -4552,6 +4574,8 @@ class VirtualMachine {
4552
4574
  // NOTE: Flows may have a delay!
4553
4575
  ld = l.actualDelay(b);
4554
4576
  bt = b - ld;
4577
+ // NOTE: Block index may fall beyond actual chunk length.
4578
+ ci = i - ld;
4555
4579
  // NOTE: Use non-zero level here to ignore non-zero values that
4556
4580
  // are very small relative to the bounds on the process
4557
4581
  // (typically values below the non-zero tolerance of the solver).
@@ -4575,20 +4599,22 @@ class VirtualMachine {
4575
4599
  // Similar to STARTUP, but now look in the shut-down list.
4576
4600
  pl = (p.shut_downs.indexOf(bt) < 0 ? 0 : 1);
4577
4601
  } else if(l.multiplier === VM.LM_INCREASE) {
4578
- pl = VM.keepException(pl, pl - p.actualLevel(bt - 1));
4602
+ const ppl = p.actualLevel(bt - 1);
4603
+ pl = this.severestIssue([pl, ppl], pl - ppl);
4579
4604
  } else if(l.multiplier === VM.LM_SUM || l.multiplier === VM.LM_MEAN) {
4580
4605
  // Level for `bt` counts as first value.
4581
4606
  let count = 1;
4582
4607
  // NOTE: Link delay may be < 0!
4583
4608
  if(ld < 0) {
4584
- // NOTE: Actual levels beyond end of period are undefined,
4609
+ // NOTE: Actual levels beyond the chunk length are undefined,
4585
4610
  // and should be ignored while summing / averaging.
4586
- if(bt >= p.level.length) pl = 0;
4611
+ if(ci >= cbl) pl = 0;
4587
4612
  // If so, take sum over t, t+1, ..., t+(d-1).
4588
4613
  for(let j = ld + 1; j <= 0; j++) {
4589
- // Again: ignore levels beyond end of period.
4590
- if(b - j < p.level.length) {
4591
- pl = VM.keepException(pl, pl + p.actualLevel(b - j));
4614
+ // Again: only consider levels up to the end of the chunk.
4615
+ if(ci - j < cbl) {
4616
+ const spl = p.actualLevel(b - j);
4617
+ pl = this.severestIssue([pl, spl], pl + spl);
4592
4618
  count++;
4593
4619
  }
4594
4620
  }
@@ -4597,22 +4623,24 @@ class VirtualMachine {
4597
4623
  for(let j = 0; j < ld; j++) {
4598
4624
  // NOTE: Actual levels before t=0 are considered equal to
4599
4625
  // the initial level, and hence should NOT be ignored.
4600
- pl = VM.keepException(pl, pl + p.actualLevel(b - j));
4626
+ const spl = p.actualLevel(b - j);
4627
+ pl = this.severestIssue([pl, spl], pl + spl);
4601
4628
  count++;
4602
4629
  }
4603
4630
  }
4604
4631
  if(l.multiplier === VM.LM_MEAN && count > 1) {
4605
4632
  // Average if more than 1 values have been summed.
4606
- pl = VM.keepException(pl, pl / count);
4633
+ pl = this.keepException(pl, pl / count);
4607
4634
  }
4608
4635
  } else if(l.multiplier === VM.LM_THROUGHPUT) {
4609
4636
  // NOTE: calculate throughput on basis of levels and rates,
4610
4637
  // as not all actual flows may have been computed yet
4611
4638
  pl = 0;
4612
4639
  for(let j = 0; j < p.inputs.length; j++) {
4613
- pl = VM.keepException(pl,
4614
- pl + (p.inputs[j].from_node.actualLevel(bt) *
4615
- p.inputs[j].relative_rate.result(bt)));
4640
+ const
4641
+ ipl = p.inputs[j].from_node.actualLevel(bt),
4642
+ rr = p.inputs[j].relative_rate.result(bt);
4643
+ pl = this.severestIssue([pl, ipl, rr], pl + ipl * rr);
4616
4644
  }
4617
4645
  } else if(l.multiplier === VM.LM_PEAK_INC) {
4618
4646
  // Actual flow over "peak increase" link is zero unless...
@@ -4627,17 +4655,10 @@ class VirtualMachine {
4627
4655
  }
4628
4656
  }
4629
4657
  // Preserve special values such as INF, UNDEFINED and VM error codes.
4630
- if(pl <= VM.MINUS_INFINITY || pl > VM.PLUS_INFINITY) {
4631
- l.actual_flow[b] = pl;
4632
- } else {
4633
- const rr = l.relative_rate.result(bt);
4634
- if(rr <= VM.MINUS_INFINITY || rr > VM.PLUS_INFINITY) {
4635
- l.actual_flow[b] = rr;
4636
- } else {
4637
- const af = rr * pl;
4638
- l.actual_flow[b] = (Math.abs(af) > VM.NEAR_ZERO ? af : 0);
4639
- }
4640
- }
4658
+ const
4659
+ rr = l.relative_rate.result(bt),
4660
+ af = this.severestIssue([pl, rr], rr * pl);
4661
+ l.actual_flow[b] = (Math.abs(af) > VM.NEAR_ZERO ? af : 0);
4641
4662
  b++;
4642
4663
  }
4643
4664
  }
@@ -5586,24 +5607,33 @@ Solver status = ${json.status}`);
5586
5607
  // levels and stock level), but do NOT overwrite "look-ahead" levels
5587
5608
  // if this block was not solved (indicated by the 4th parameter that
5588
5609
  // tests the status).
5589
- // NOTE: Appropriate status codes are solver-dependent.
5590
- this.setLevels(bnr, rl, json.data.x,
5591
- this.noSolutionStatus.indexOf(json.status) >= 0);
5592
- // NOTE: Post-process levels only AFTER the last round!
5593
- if(rl === this.lastRound) {
5594
- // Calculate data for all other dependent variables.
5595
- this.calculateDependentVariables(bnr);
5596
- // Add progress bar segment only now, knowing status AND slack use.
5597
- const issue = json.status !== 0 || this.error_count > 0;
5598
- if(issue) this.block_issues++;
5599
- // NOTE: in case of multiple rounds, use the sum of the round times.
5600
- const time = this.round_times.reduce((a, b) => a + b, 0);
5601
- this.round_times.length = 0;
5602
- this.solver_times[bnr - 1] = time;
5603
- const ssecs = this.round_secs.reduce((a, b) => a + b, 0);
5604
- this.solver_secs[bnr - 1] = (ssecs ? VM.sig4Dig(ssecs) : '0');
5605
- this.round_secs.length = 0;
5606
- MONITOR.addProgressBlock(bnr, issue, time);
5610
+ try {
5611
+ this.setLevels(bnr, rl, json.data.x,
5612
+ // NOTE: Appropriate status codes are solver-dependent.
5613
+ this.noSolutionStatus.indexOf(json.status) >= 0);
5614
+ // NOTE: Post-process levels only AFTER the last round!
5615
+ if(rl === this.lastRound) {
5616
+ // Calculate data for all other dependent variables.
5617
+ this.calculateDependentVariables(bnr);
5618
+ // Add progress bar segment only now, knowing status AND slack use.
5619
+ const issue = json.status !== 0 || this.error_count > 0;
5620
+ if(issue) this.block_issues++;
5621
+ // NOTE: in case of multiple rounds, use the sum of the round times.
5622
+ const time = this.round_times.reduce((a, b) => a + b, 0);
5623
+ this.round_times.length = 0;
5624
+ this.solver_times[bnr - 1] = time;
5625
+ const ssecs = this.round_secs.reduce((a, b) => a + b, 0);
5626
+ this.solver_secs[bnr - 1] = (ssecs ? VM.sig4Dig(ssecs) : '0');
5627
+ this.round_secs.length = 0;
5628
+ MONITOR.addProgressBlock(bnr, issue, time);
5629
+ }
5630
+ } catch(err) {
5631
+ const msg = `ERROR while processing solver data for block ${bnr}: ${err}`;
5632
+ console.log(msg);
5633
+ MONITOR.logMessage(bnr, msg);
5634
+ UI.alert(msg);
5635
+ this.stopSolving();
5636
+ this.halted = true;
5607
5637
  }
5608
5638
  // Free up memory.
5609
5639
  json = null;
@@ -5696,7 +5726,7 @@ Solver status = ${json.status}`);
5696
5726
  }
5697
5727
 
5698
5728
  submitFile() {
5699
- // Prepares to POST the model file (LP or MPS) to the Linny-R server.
5729
+ // Prepare to POST the model file (LP or MPS) to the Linny-R server.
5700
5730
  // NOTE: The tableau is no longer needed, so free up its memory.
5701
5731
  this.resetTableau();
5702
5732
  if(this.numeric_issue) {
@@ -5749,6 +5779,13 @@ Solver status = ${json.status}`);
5749
5779
  solveModel() {
5750
5780
  // Start the sequence of data loading, model translation, solving
5751
5781
  // consecutive blocks, and finally calculating dependent variables.
5782
+ // NOTE: Do this only if the model defines a MILP problem, i.e.,
5783
+ // contains at least one process or product.
5784
+ if(!(Object.keys(MODEL.processes).length ||
5785
+ Object.keys(MODEL.products).length)) {
5786
+ UI.notify('Nothing to solve');
5787
+ return;
5788
+ }
5752
5789
  const n = MODEL.loading_datasets.length;
5753
5790
  if(n > 0) {
5754
5791
  // Still within reasonable time? (3 seconds per dataset)
@@ -5757,7 +5794,7 @@ Solver status = ${json.status}`);
5757
5794
  UI.setMessage(`Waiting for ${pluralS(n, 'dataset')} to load`);
5758
5795
  // Decrease the remaining time to wait (half second units)
5759
5796
  MODEL.max_time_to_load--;
5760
- // Try again after half a second
5797
+ // Try again after half a second.
5761
5798
  setTimeout(() => VM.solveModel(), 500);
5762
5799
  return;
5763
5800
  } else {