linny-r 3.0.5 → 3.0.7

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": "3.0.5",
3
+ "version": "3.0.7",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -906,6 +906,7 @@ class GUIChartManager extends ChartManager {
906
906
  }
907
907
 
908
908
  toggleVariable(vi, event) {
909
+ // Toggle the "display this variable in the chart" (yes/no) property.
909
910
  window.event.stopPropagation();
910
911
  if(vi >= 0 && this.chart_index >= 0) {
911
912
  const v_list = MODEL.charts[this.chart_index].variables;
@@ -937,9 +938,9 @@ class GUIChartManager extends ChartManager {
937
938
  UI.setBox('v-box-' + vi, nv);
938
939
  }
939
940
  }
940
- // redraw chart and table (with one variable more or less)
941
+ // Redraw chart and table (with one variable more or less).
941
942
  this.drawChart();
942
- // Also update the experiment viewer (charts define the output variables)
943
+ // Also update the experiment viewer (charts define the output variables).
943
944
  if(EXPERIMENT_MANAGER.selected_experiment) {
944
945
  EXPERIMENT_MANAGER.updateDialog();
945
946
  }
@@ -947,6 +948,7 @@ class GUIChartManager extends ChartManager {
947
948
  }
948
949
 
949
950
  moveVariable(dir) {
951
+ // Move variable up or down in the chart variable list.
950
952
  if(this.chart_index >= 0 && this.variable_index >= 0) {
951
953
  const c = MODEL.charts[this.chart_index];
952
954
  let vi = this.variable_index;
@@ -961,6 +963,7 @@ class GUIChartManager extends ChartManager {
961
963
  }
962
964
 
963
965
  modifyVariable() {
966
+ // Update the properties of the variable being edited.
964
967
  if(this.variable_index >= 0) {
965
968
  const s = UI.validNumericInput('variable-scale', 'scale factor');
966
969
  if(!s) return;
@@ -978,16 +981,20 @@ class GUIChartManager extends ChartManager {
978
981
  cv.color = this.color_picker.color.hexString;
979
982
  // NOTE: Clear the vector so it will be recalculated.
980
983
  cv.vector.length = 0;
984
+ // Likewise clear the display name cache.
985
+ cv.display_name = '';
981
986
  }
982
987
  this.variable_modal.hide();
983
988
  this.updateDialog();
984
989
  }
985
990
 
986
991
  renameEquation() {
987
- // Renames the selected variable (if it is an equation)
992
+ // Rename the selected variable (if it is an equation).
988
993
  if(this.chart_index >= 0 && this.variable_index >= 0) {
989
994
  const v = MODEL.charts[this.chart_index].variables[this.variable_index];
990
995
  if(v.object === MODEL.equations_dataset || v.object instanceof DatasetModifier) {
996
+ // Clear the display name cache (anticipating a name change).
997
+ v.display_name = '';
991
998
  const m = MODEL.equations_dataset.modifiers[UI.nameToID(v.attribute)];
992
999
  if(m instanceof DatasetModifier) {
993
1000
  EQUATION_MANAGER.selected_modifier = m;
@@ -998,7 +1005,7 @@ class GUIChartManager extends ChartManager {
998
1005
  }
999
1006
 
1000
1007
  editEquation() {
1001
- // Opens the expression editor for the selected variable (if equation)
1008
+ // Open the expression editor for the selected variable (if equation).
1002
1009
  if(this.chart_index >= 0 && this.variable_index >= 0) {
1003
1010
  const v = MODEL.charts[this.chart_index].variables[this.variable_index];
1004
1011
  if(v.object === MODEL.equations_dataset || v.object instanceof DatasetModifier) {
@@ -1012,14 +1019,15 @@ class GUIChartManager extends ChartManager {
1012
1019
  }
1013
1020
 
1014
1021
  deleteVariable() {
1015
- // Deletes the selected variable from the chart
1022
+ // Delete the selected variable from the chart.
1016
1023
  if(this.variable_index >= 0) {
1017
1024
  MODEL.charts[this.chart_index].variables.splice(this.variable_index, 1);
1018
1025
  this.variable_index = -1;
1019
1026
  this.updateDialog();
1020
1027
  // Also update the experiment viewer (charts define the output variables)
1021
1028
  // and finder dialog.
1022
- if(EXPERIMENT_MANAGER.selected_experiment) UI.updateControllerDialogs('FX');
1029
+ if(EXPERIMENT_MANAGER.selected_experiment) EXPERIMENT_MANAGER.updateDialog();
1030
+ FINDER.updateDialog();
1023
1031
  }
1024
1032
  this.variable_modal.hide();
1025
1033
  }
@@ -1106,7 +1114,7 @@ class GUIChartManager extends ChartManager {
1106
1114
 
1107
1115
  stretchChart(delta) {
1108
1116
  this.stretch_factor = Math.max(1, Math.min(10, this.stretch_factor + delta));
1109
- // NOTE: do not use 'auto', as this produces poor results
1117
+ // NOTE: Do not use 'auto', as this produces poor results.
1110
1118
  document.getElementById('chart-svg-scroller').style.overflowX =
1111
1119
  (this.stretch_factor === 1 ? 'hidden' : 'scroll');
1112
1120
  const csc = document.getElementById('chart-svg-container');
@@ -1157,7 +1165,7 @@ class GUIChartManager extends ChartManager {
1157
1165
  }
1158
1166
 
1159
1167
  downloadChart(shift) {
1160
- // Pushe the SVG of the selected chart as file to the browser.
1168
+ // Push the SVG of the selected chart as file to the browser.
1161
1169
  if(this.chart_index >= 0) {
1162
1170
  const
1163
1171
  chart = MODEL.charts[this.chart_index],
@@ -1185,7 +1193,7 @@ class GUIChartManager extends ChartManager {
1185
1193
  }
1186
1194
 
1187
1195
  actuallyDrawChart() {
1188
- // Draw the chart, and reset the cursor when done
1196
+ // Draw the chart, and reset the cursor when done.
1189
1197
  MODEL.charts[this.chart_index].draw();
1190
1198
  this.drawing_chart = false;
1191
1199
  this.drawTable();
@@ -554,7 +554,7 @@ class Finder {
554
554
  stack = UI.boxChecked('confirm-add-chart-variables-stacked'),
555
555
  equations = this.entities[0] instanceof DatasetModifier,
556
556
  enl = [];
557
- for(const e of this.entities) enl.push(equations ? e.selector : e.name);
557
+ for(const e of this.entities) enl.push(equations ? e.selector : e.displayName);
558
558
  enl.sort((a, b) => UI.compareFullNames(a, b, true));
559
559
  for(const en of enl) {
560
560
  let vi = null;
@@ -104,26 +104,25 @@ module.exports = class MILPSolver {
104
104
  const
105
105
  windows = os.platform().startsWith('win'),
106
106
  path_list = process.env.PATH.split(path.delimiter);
107
- // Iterate over all seprate paths in environment variable PATH.
107
+ // Iterate over all separate paths in environment variable PATH.
108
+ // NOTE: gurobi_cl.exe version 12 and higher appears not to exit cleanly
109
+ // for some models (not clear when), so keep track of all Gurobi paths.
110
+ let gsp = {};
108
111
  for(const p of path_list) {
109
112
  // Assume that path is not a solver path.
110
113
  sp = '';
111
114
  // Check whether it is a Gurobi path.
112
115
  match = p.match(/gurobi(\d+)/i);
113
- if(match) sp = p;
114
- // If so, ensure that it has a higher version number.
115
- if(sp && parseInt(match[1]) > max_vn) {
116
+ if(match) {
116
117
  // Check whether command line version is executable.
117
- sp = path.join(sp, 'gurobi_cl' + (windows ? '.exe' : ''));
118
+ sp = path.join(p, 'gurobi_cl' + (windows ? '.exe' : ''));
118
119
  try {
119
120
  fs.accessSync(sp, fs.constants.X_OK);
120
- console.log('Path to Gurobi:', sp);
121
- this.solver_list.gurobi = {name: 'Gurobi', path: sp};
122
- max_vn = parseInt(match[1]);
121
+ gsp[match[1]] = p;
123
122
  } catch(err) {
124
123
  console.log(err.message);
125
124
  console.log(
126
- 'WARNING: Failed to access the Gurobi command line application');
125
+ 'WARNING: Failed to access the Gurobi command line application', sp);
127
126
  }
128
127
  }
129
128
  if(sp) continue;
@@ -187,6 +186,22 @@ module.exports = class MILPSolver {
187
186
  }
188
187
  // NOTE: Order of paths is unknown, so keep iterating.
189
188
  }
189
+ // Only now set the Gurobi path. To force using a version < 12 (if installed),
190
+ // set before_12 to TRUE.
191
+ const
192
+ before_12 = false,
193
+ gsp_keys = Object.keys(gsp).sort();
194
+ while(gsp_keys.length) {
195
+ const
196
+ k = gsp_keys.pop(),
197
+ version = Math.trunc(parseInt(k) / 100);
198
+ if(version < 12 || !before_12) {
199
+ this.solver_list.gurobi = {name: 'Gurobi',
200
+ path: path.join(gsp[k], 'gurobi_cl')};
201
+ console.log('Path to Gurobi:', this.solver_list.gurobi.path);
202
+ break;
203
+ }
204
+ }
190
205
  // For macOS, look in applications directory if not found in PATH.
191
206
  if(!this.solver_list.gurobi && !windows) {
192
207
  console.log('Looking for Gurobi in /usr/local/bin');
@@ -2701,6 +2701,8 @@ class LinnyRModel {
2701
2701
  UNDO_STACK.addXML(a.asXML);
2702
2702
  this.removeImport(a);
2703
2703
  this.removeExport(a);
2704
+ // Ensure that chart variables referencing this actor are removed.
2705
+ this.removeActorChartVariables(a);
2704
2706
  delete this.actors[k];
2705
2707
  }
2706
2708
  }
@@ -2823,6 +2825,49 @@ class LinnyRModel {
2823
2825
  }
2824
2826
  return xl;
2825
2827
  }
2828
+
2829
+ updateChartVariables(e) {
2830
+ // Ensure that all chart variable names based on entity `e` will be
2831
+ // displayed correctly the next time they are drawn.
2832
+ console.log('HERE e', e.displayName);
2833
+ const sc = this.charts[CHART_MANAGER.chart_index];
2834
+ let ucm = false;
2835
+ for(const c of this.charts) {
2836
+ for(const v of c.variables) {
2837
+ console.log('HERE v', v.displayName);
2838
+ if(v.object === e) {
2839
+ v.display_name = '';
2840
+ console.log('HERE v new', v.displayName);
2841
+ ucm = ucm || c === sc;
2842
+ }
2843
+ }
2844
+ }
2845
+ if(ucm) CHART_MANAGER.updateDialog();
2846
+ }
2847
+
2848
+ removeActorChartVariables(a) {
2849
+ // Ensure that all chart variable names based on entity `e` or actor `a`
2850
+ // will be displayed correctly the next time they are drawn.
2851
+ const sc = this.charts[CHART_MANAGER.chart_index];
2852
+ let ucm = false;
2853
+ for(const c of this.charts) {
2854
+ for(let vi = 0; vi < c.variables.length; vi++) {
2855
+ if(c.variables[vi].object === a) {
2856
+ c.variables.splice(vi, 1);
2857
+ if(c === sc) {
2858
+ ucm = true;
2859
+ if(CHART_MANAGER.variable_index === vi) {
2860
+ CHART_MANAGER.variable_index = -1;
2861
+ }
2862
+ }
2863
+ }
2864
+ }
2865
+ }
2866
+ // Update stay-on-top dialogs (if needed).
2867
+ if(ucm) CHART_MANAGER.updateDialog();
2868
+ if(EXPERIMENT_MANAGER.selected_experiment) EXPERIMENT_MANAGER.updateDialog();
2869
+ FINDER.updateDialog();
2870
+ }
2826
2871
 
2827
2872
  replaceEntityInExpressions(en1, en2, notify=true) {
2828
2873
  // Replace entity name `en1` by `en2` in all variables in all expressions
@@ -2853,12 +2898,6 @@ class LinnyRModel {
2853
2898
  pluralS(ioc.expression_count, 'expression');
2854
2899
  if(notify) UI.notify('Renamed ' + replace_msg);
2855
2900
  }
2856
- // Clear display name cache of potentially affected chart variables.
2857
- for(const c of this.charts) {
2858
- for(const v of c.variables) {
2859
- if(v.display_name.indexOf(en1) >= 0) v.display_name = '';
2860
- }
2861
- }
2862
2901
  // Rename entities in parameters and outcomes of sensitivity analysis.
2863
2902
  for(let i = 0; i < this.sensitivity_parameters.length; i++) {
2864
2903
  const sp = this.sensitivity_parameters[i].split('|');
@@ -5181,6 +5220,8 @@ class Actor {
5181
5220
  MODEL.actors[a.identifier] = this;
5182
5221
  // Remove the old entry.
5183
5222
  delete MODEL.actors[old_id];
5223
+ // Ensure that cached variable names are updated.
5224
+ MODEL.updateChartVariables(this);
5184
5225
  MODEL.replaceEntityInExpressions(old_name, this.name);
5185
5226
  MODEL.inferIgnoredEntities();
5186
5227
  }
@@ -6042,6 +6083,8 @@ class NodeBox extends ObjectWithXYWH {
6042
6083
  }
6043
6084
  // Update actor list in case some actor name is no longer used.
6044
6085
  MODEL.cleanUpActors();
6086
+ // Ensure that cached variable names are updated.
6087
+ MODEL.updateChartVariables(this);
6045
6088
  // Update expression texts.
6046
6089
  MODEL.replaceEntityInExpressions(old_name, this.displayName);
6047
6090
  // NOTE: Renaming changes identifier that is used as index in
@@ -9747,6 +9790,8 @@ class Dataset {
9747
9790
  this.name = name;
9748
9791
  MODEL.datasets[new_id] = this;
9749
9792
  if(old_id !== new_id) delete MODEL.datasets[old_id];
9793
+ // Ensure that cached variable names are updated.
9794
+ MODEL.updateChartVariables(this);
9750
9795
  MODEL.replaceEntityInExpressions(old_name, name, notify);
9751
9796
  return MODEL.datasets[new_id];
9752
9797
  }
@@ -10698,7 +10743,7 @@ class Chart {
10698
10743
  }
10699
10744
  }
10700
10745
 
10701
- // NOTE: chart may display experiment run results, rather than MODEL results
10746
+ // NOTE: Chart may display experiment run results, rather than MODEL results.
10702
10747
  let runnrs = '';
10703
10748
  const runs = EXPERIMENT_MANAGER.selectedRuns(this);
10704
10749
  if(runs.length > 0) {
@@ -10739,6 +10784,8 @@ class Chart {
10739
10784
  const rri = selx.resultIndex(v.displayName);
10740
10785
  let bv;
10741
10786
  if(rri >= 0) {
10787
+ // When run results are available, use their already
10788
+ // computed statisitics.
10742
10789
  const
10743
10790
  r = selx.runs[this.run_index],
10744
10791
  rr = r.results[rri];
@@ -10767,7 +10814,9 @@ class Chart {
10767
10814
  minv = Math.min(minv, bv);
10768
10815
  maxv = Math.max(maxv, bv);
10769
10816
  }
10817
+ // ... otherwise, do not add data for the bar chart.
10770
10818
  } else {
10819
+ // Regular line chart => compute vector for this run.
10771
10820
  v.computeVector();
10772
10821
  minv = Math.min(minv, v.lowestValueInVector);
10773
10822
  maxv = Math.max(maxv, v.highestValueInVector);
@@ -10776,6 +10825,7 @@ class Chart {
10776
10825
  // Reset to prevent using experiment outcomes when this is not intended.
10777
10826
  this.run_index = -1;
10778
10827
  } else {
10828
+ // No experiment runs selected => compute vector as usual.
10779
10829
  this.run_index = -1;
10780
10830
  v.computeVector();
10781
10831
  minv = Math.min(minv, v.lowestValueInVector);
@@ -10787,6 +10837,7 @@ class Chart {
10787
10837
  // Now all vectors have been computed. If `display` is FALSE, this
10788
10838
  // indicates that data is used only to save model results.
10789
10839
  if(!display) return;
10840
+
10790
10841
  // Define the bins when drawing as histogram.
10791
10842
  if(this.histogram) {
10792
10843
  this.value_range = maxv - minv;
@@ -2304,7 +2304,7 @@ class VirtualMachine {
2304
2304
  // for the numerical stability. Meanwhile, the slack variables themselves
2305
2305
  // will have values 1/sm times the actual slack, so this needs to be
2306
2306
  // scaled back in solver messages.
2307
- this.SLACK_MULTIPLIER = 0.01;
2307
+ this.SLACK_MULTIPLIER = 1;
2308
2308
  // The "epsilon multiplier" divides the ON/OFF threshold over the POS
2309
2309
  // and NEG binaries and the EPS slack variable to avoid high coefficients.
2310
2310
  this.EPSILON_MULTIPLIER = 1 / Math.sqrt(this.ON_OFF_THRESHOLD);
@@ -3622,6 +3622,7 @@ class VirtualMachine {
3622
3622
  // However, Linny-R does not prohibit negative bounds on processes, nor
3623
3623
  // negative rates on links. To be consistently permissive, cash IN and
3624
3624
  // cash OUT of all actors are both allowed to become negative.
3625
+ /*
3625
3626
  for(const k of actor_keys) {
3626
3627
  const a = MODEL.actors[k];
3627
3628
  // NOTE: Add fourth parameter TRUE to signal that the SOLVER's
@@ -3635,7 +3636,7 @@ class VirtualMachine {
3635
3636
  VM.MINUS_INFINITY, VM.PLUS_INFINITY, true]]
3636
3637
  );
3637
3638
  }
3638
-
3639
+ */
3639
3640
  // NEXT: Define the bounds for all production level variables.
3640
3641
  // NOTE: The VM instructions check dynamically whether the variable
3641
3642
  // index is listed as "fixed" for the round that is being solved.
@@ -3669,7 +3670,7 @@ class VirtualMachine {
3669
3670
  if(rf != 0) {
3670
3671
  // Note: 32-bit integer `b` is used for bit-wise AND
3671
3672
  let b = 1;
3672
- for(j = 0; j < MODEL.rounds; j++) {
3673
+ for(let j = 0; j < MODEL.rounds; j++) {
3673
3674
  if((rf & b) != 0) {
3674
3675
  this.fixed_var_indices[j][p.level_var_index] = true;
3675
3676
  // @@ TO DO: fixate associated binary variables if applicable!
@@ -4180,8 +4181,9 @@ class VirtualMachine {
4180
4181
 
4181
4182
  (a) L = POSL - NEGL
4182
4183
 
4183
- This "partitions" the level in two components. The following constraints
4184
- ensure a (functionally) unique partitioning:
4184
+ This "partitions" the level in two components.
4185
+
4186
+ The following constraints ensure a (functionally) unique partitioning:
4185
4187
 
4186
4188
  (b) NEGL - M*NEG <= 0 (so NEG=1 if NEGL > 0)
4187
4189
  (c) POSL - M*POS <= 0 (so POS=1 if POSL > 0)
@@ -4872,14 +4874,31 @@ class VirtualMachine {
4872
4874
  pl = this.keepException(pl, pl / count);
4873
4875
  }
4874
4876
  } else if(l.multiplier === VM.LM_THROUGHPUT) {
4875
- // NOTE: calculate throughput on basis of levels and rates,
4876
- // as not all actual flows may have been computed yet
4877
+ // NOTE: Calculate throughput on basis of *process* levels and rates,
4878
+ // as not all actual flows may have been computed yet.
4877
4879
  pl = 0;
4878
- for(const ll of p.inputs) {
4880
+ for(const ll of p.inputs) if(ll.from_node instanceof Process) {
4879
4881
  const
4880
- ipl = ll.from_node.actualLevel(bt),
4881
- rr = ll.relative_rate.result(bt);
4882
- pl = this.severestIssue([pl, ipl, rr], pl + ipl * rr);
4882
+ lld = ll.actualDelay(b),
4883
+ ipl = ll.from_node.actualLevel(bt - lld),
4884
+ rr = ll.relative_rate.result(bt - lld),
4885
+ flow = ipl * rr;
4886
+ // NOTE: Only consider INflows, so flow must be > 0.
4887
+ if(flow > 0) {
4888
+ pl = this.severestIssue([pl, ipl, rr], pl + flow);
4889
+ }
4890
+ }
4891
+ // NOTE: Again, only consider processes.
4892
+ for(const ll of p.outputs) if(ll.to_node instanceof Process) {
4893
+ const
4894
+ // NOTE: Links TO a process cannot have a delay.
4895
+ opl = ll.to_node.actualLevel(bt),
4896
+ rr = ll.relative_rate.result(bt),
4897
+ flow = opl * rr;
4898
+ // NOTE: Only consider INflows, so now flow must be < 0.
4899
+ if(flow < 0) {
4900
+ pl = this.severestIssue([pl, opl, rr], pl - flow);
4901
+ }
4883
4902
  }
4884
4903
  } else if(l.multiplier === VM.LM_PEAK_INC) {
4885
4904
  // Actual flow over "peak increase" link is zero unless...
@@ -5461,8 +5480,8 @@ class VirtualMachine {
5461
5480
  v,
5462
5481
  line = '';
5463
5482
  // NOTE: Iterate over ALL columns to maintain variable order.
5464
- let n = abl * this.cols + this.chunk_variables.length;
5465
- for(p = 1; p <= n; p++) {
5483
+ let ncols = abl * this.cols + this.chunk_variables.length;
5484
+ for(p = 1; p <= ncols; p++) {
5466
5485
  if(this.objective.hasOwnProperty(p)) {
5467
5486
  c = this.objective[p];
5468
5487
  // Check for numeric issues.
@@ -5486,8 +5505,8 @@ class VirtualMachine {
5486
5505
  } else {
5487
5506
  this.lines += '\n/* Constraints */\n';
5488
5507
  }
5489
- n = this.matrix.length;
5490
- for(let r = 0; r < n; r++) {
5508
+ let nrows = this.matrix.length;
5509
+ for(let r = 0; r < nrows; r++) {
5491
5510
  const row = this.matrix[r];
5492
5511
  if(named_constraints) line = `C${r + 1}: `;
5493
5512
  for(p in row) if (row.hasOwnProperty(p)) {
@@ -5521,8 +5540,7 @@ class VirtualMachine {
5521
5540
  } else {
5522
5541
  this.lines += '\n/* Variable bounds */\n';
5523
5542
  }
5524
- n = abl * this.cols;
5525
- for(p = 1; p <= n; p++) {
5543
+ for(p = 1; p <= ncols; p++) {
5526
5544
  let lb = null,
5527
5545
  ub = null;
5528
5546
  if(this.lower_bounds.hasOwnProperty(p)) {
@@ -5658,7 +5676,7 @@ class VirtualMachine {
5658
5676
  for(let i in this.is_semi_continuous) if(Number(i)) v_set.push(vbl(i));
5659
5677
  if(v_set.length > 0) this.lines += 'sec ' + v_set.join(', ') + ';\n';
5660
5678
  // LP_solve supports SOS, so add the SOS section if needed.
5661
- if(this.nzp_var_indices.length ||this.sos_var_indices.length) {
5679
+ if(this.nzp_var_indices.length || this.sos_var_indices.length) {
5662
5680
  this.lines += 'sos\n';
5663
5681
  for(let j = 0; j < abl; j++) {
5664
5682
  // First add the SOS1 constraints for NZP-partitioned levels.
@@ -6149,11 +6167,11 @@ Solver status = ${json.status}`);
6149
6167
  // Generate lines of code in format that should be accepted by solver.
6150
6168
  if(this.solver_id === 'gurobi') {
6151
6169
  this.writeLpFormat(true);
6152
- } else if(this.solver_id === 'mosek') {
6170
+ } else if(this.solver_id === 'mosek' || this.solver_id === 'scip') {
6153
6171
  // NOTE: For MOSEK, constraints must be named, or variable names
6154
- // in solution file will not match.
6172
+ // in solution file will not match. SCIP works, but generates warnings.
6155
6173
  this.writeLpFormat(true, true);
6156
- } else if(this.solver_id === 'cplex' || this.solver_id === 'scip') {
6174
+ } else if(this.solver_id === 'cplex') {
6157
6175
  // NOTE: The more widely accepted CPLEX LP format differs from the
6158
6176
  // LP_solve format that was used by the first versions of Linny-R.
6159
6177
  // TRUE indicates "CPLEX format".
@@ -8329,7 +8347,7 @@ function VMI_set_bounds(args) {
8329
8347
  l = args[1];
8330
8348
  u = args[2];
8331
8349
  if(u instanceof Expression) u = u.result(VM.t);
8332
- if(u === VM.UNDEFINED) {
8350
+ if(u === VM.UNDEFINED || u === VM.DIAGNOSIS_UPPER_BOUND) {
8333
8351
  u = inf_val;
8334
8352
  } else {
8335
8353
  u = Math.min(u, inf_val);
@@ -8340,6 +8358,8 @@ function VMI_set_bounds(args) {
8340
8358
  if(l instanceof Expression) l = l.result(VM.t);
8341
8359
  if(l === VM.UNDEFINED || !l) {
8342
8360
  l = 0;
8361
+ } else if(l === -VM.DIAGNOSIS_UPPER_BOUND) {
8362
+ l = -inf_val;
8343
8363
  } else {
8344
8364
  l = Math.max(l, -inf_val);
8345
8365
  }
@@ -8407,7 +8427,7 @@ function VMI_set_bounds(args) {
8407
8427
  cvi = VM.chunk_offset + p.peak_inc_var_index,
8408
8428
  // Check if peak UB already set for previous t
8409
8429
  piub = VM.upper_bounds[cvi];
8410
- // If so, use the highest value
8430
+ // If so, use the highest value.
8411
8431
  if(piub) u = Math.max(piub, u);
8412
8432
  VM.upper_bounds[cvi] = u;
8413
8433
  VM.upper_bounds[cvi + 1] = u;
@@ -9123,7 +9143,10 @@ function VMI_set_objective() {
9123
9143
  for(let i = 0; i < VM.chunk_variables.length; i++) {
9124
9144
  const vn = VM.chunk_variables[i][0];
9125
9145
  if(vn.indexOf('peak') > 0) {
9126
- const pvp = VM.PEAK_VAR_PENALTY / VM.cash_scalar;
9146
+ // NOTE: When prices in model are low, the cash scalar is small
9147
+ // and then a peak variable penalty of 0.1 currency unit will
9148
+ // significantly impact the tipping point for investment choices
9149
+ const pvp = VM.PEAK_VAR_PENALTY / Math.max(VM.cash_scalar, 2000);
9127
9150
  // NOTE: Chunk offset takes into account that indices are 0-based.
9128
9151
  VM.objective[VM.chunk_offset + i] = -pvp;
9129
9152
  // Put higher penalty on "block peak" than on "look-ahead peak"
@@ -9210,7 +9233,7 @@ function VMI_add_semicontinuous_constraints(p) {
9210
9233
  // level - UB*binary <= 0
9211
9234
  row = {};
9212
9235
  row[l_index] = 1;
9213
- row[lb_index] = -ub;
9236
+ row[lb_index] = -ub - 1;
9214
9237
  VM.matrix.push(row);
9215
9238
  VM.right_hand_side.push(0);
9216
9239
  VM.constraint_types.push(VM.LE);
@@ -9227,11 +9250,26 @@ function VMI_add_NZP_continuous_constraints(p) {
9227
9250
  console.log('add_NZP_continuous_constraints (t = ' + VM.t + ')');
9228
9251
  }
9229
9252
  if(!p || p.posl_var_index < 0) throw 'ANOMALY: No NZP variable indices';
9230
- const row = {};
9231
- // (a) L + NEGL - POSL = 0 (so POSL - NEGL = L).
9232
- row[VM.offset + p.level_var_index] = 1;
9233
- row[VM.offset + p.negl_var_index] = 1;
9234
- row[VM.offset + p.posl_var_index] = -1;
9253
+ let row = {};
9254
+ if(p.level_to_zero) {
9255
+ // For semi-continuous processes, the level is always >= 0.
9256
+ // To prevent issues with binaries, set POSL = L and NEGL = 0 to rule out
9257
+ // the possibility of NEGL being used to compensate for a positive epsilon.
9258
+ // (a1) L - POSL = 0.
9259
+ row[VM.offset + p.level_var_index] = 1;
9260
+ row[VM.offset + p.posl_var_index] = -1;
9261
+ VM.matrix.push(row);
9262
+ VM.right_hand_side.push(0);
9263
+ VM.constraint_types.push(VM.EQ);
9264
+ row = {};
9265
+ // (a2) NEGL = 0.
9266
+ row[VM.offset + p.negl_var_index] = 1;
9267
+ } else {
9268
+ // (a) L + NEGL - POSL = 0 (so POSL - NEGL = L).
9269
+ row[VM.offset + p.level_var_index] = 1;
9270
+ row[VM.offset + p.negl_var_index] = 1;
9271
+ row[VM.offset + p.posl_var_index] = -1;
9272
+ }
9235
9273
  VM.matrix.push(row);
9236
9274
  VM.right_hand_side.push(0);
9237
9275
  VM.constraint_types.push(VM.EQ);
@@ -9291,14 +9329,17 @@ function VMI_add_NZP_binary_constraints(p) {
9291
9329
  row[pos_index] = VM.EPSILON_MULTIPLIER * VM.ON_OFF_THRESHOLD;
9292
9330
  row[posl_index] = -VM.EPSILON_MULTIPLIER;
9293
9331
  // Provide slack so the constraint can always be met, but at a significant cost.
9294
- row[eps_index] = -VM.SLACK_MULTIPLIER / VM.EPSILON_MULTIPLIER;
9332
+ // NOTE: Do *NOT* do this for semi-continuous processes.
9333
+ if(!p.level_to_zero) {
9334
+ row[eps_index] = -VM.SLACK_MULTIPLIER / VM.EPSILON_MULTIPLIER;
9335
+ }
9295
9336
  VM.matrix.push(row);
9296
9337
  VM.right_hand_side.push(0);
9297
9338
  VM.constraint_types.push(VM.LE);
9298
9339
  // NOTE: This VMI is added when LB *may* become negative, so check
9299
9340
  // whether now (at run time) LB >= 0, as then NZP partitioning is
9300
9341
  // trivial and need not be done by the solver.
9301
- if(lb >= 0) {
9342
+ if(lb >= 0 || p.level_to_zero) {
9302
9343
  // If L >= 0, NEG must be 0.
9303
9344
  row = {};
9304
9345
  row[neg_index] = 1;
@@ -9748,7 +9789,7 @@ function VMI_add_throughput_to_coefficients(link) {
9748
9789
  // Skip link when it has rate = 0.
9749
9790
  if(r2 === 0) continue;
9750
9791
  // By default, use the FROM node's level...
9751
- let vi = (lfn.is_zero_var_index < 0 ? lfn.level_var_index :
9792
+ let vi = (lfn.posl_var_index < 0 ? lfn.level_var_index :
9752
9793
  // ... but differentiate when this level is NZP-partitioned.
9753
9794
  // Then use positive level component when rate > 0, and negative
9754
9795
  // level component when rate < 0, so throughput flow is always >= 0.
@@ -9807,7 +9848,7 @@ function VMI_add_throughput_to_coefficients(link) {
9807
9848
  if(r2 === 0) continue;
9808
9849
  // Also skip when level is not NZP-partitioned, as then an output-link
9809
9850
  // cannot contribute to the *inflow* of the process being "read".
9810
- if(ltn.is_zero_var_index < 0) continue;
9851
+ if(ltn.posl_var_index < 0) continue;
9811
9852
  // Now use the negative level component when rate > 0, and positive
9812
9853
  // level component when rate < 0, so throughput flow is always >= 0.
9813
9854
  const