linny-r 3.0.6 → 3.0.8

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.6",
3
+ "version": "3.0.8",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -1026,7 +1026,8 @@ class GUIChartManager extends ChartManager {
1026
1026
  this.updateDialog();
1027
1027
  // Also update the experiment viewer (charts define the output variables)
1028
1028
  // and finder dialog.
1029
- if(EXPERIMENT_MANAGER.selected_experiment) UI.updateControllerDialogs('FX');
1029
+ if(EXPERIMENT_MANAGER.selected_experiment) EXPERIMENT_MANAGER.updateDialog();
1030
+ FINDER.updateDialog();
1030
1031
  }
1031
1032
  this.variable_modal.hide();
1032
1033
  }
@@ -314,10 +314,10 @@ class GUIFileManager {
314
314
  const mi = this.model_index;
315
315
  if(mi >= 0) {
316
316
  path += this.separator;
317
- if(mi < sd.sdcount) {
317
+ if(mi < this.sd_count) {
318
318
  path += sd.subdirs[mi].name;
319
319
  } else {
320
- path += sd.models[mi - sd.sdcount].name + '.lnr';
320
+ path += sd.models[mi - this.sd_count].name + '.lnr';
321
321
  this.new_window_btn.title =
322
322
  'Open selected model in new Linny-R tab in browser';
323
323
  }
@@ -1260,7 +1260,7 @@ class GUIFileManager {
1260
1260
  const
1261
1261
  mi = this.model_index,
1262
1262
  sd = this.selected_dir;
1263
- if(mi >= sd.sdcount) {
1263
+ if(mi >= this.sd_count) {
1264
1264
  const mdl = sd.models[mi - this.sd_count];
1265
1265
  if(mdl) {
1266
1266
  window.localStorage.setItem('linny-r-model-file',
@@ -167,6 +167,8 @@ class Finder {
167
167
  enl = [],
168
168
  et = this.entity_types,
169
169
  fp = this.filter_pattern && this.filter_pattern.length > 0;
170
+ // Position "orphan" products (if any) in focal cluster and notify modeler.
171
+ MODEL.revealOrphans();
170
172
  let imgs = '';
171
173
  this.entities.length = 0;
172
174
  this.filtered_types.length = 0;
@@ -554,7 +556,7 @@ class Finder {
554
556
  stack = UI.boxChecked('confirm-add-chart-variables-stacked'),
555
557
  equations = this.entities[0] instanceof DatasetModifier,
556
558
  enl = [];
557
- for(const e of this.entities) enl.push(equations ? e.selector : e.name);
559
+ for(const e of this.entities) enl.push(equations ? e.selector : e.displayName);
558
560
  enl.sort((a, b) => UI.compareFullNames(a, b, true));
559
561
  for(const en of enl) {
560
562
  let vi = null;
@@ -104,30 +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
- // NOTE: gurobi_cl.exe version 12 and higher appears not to exit cleanly for
116
- // some models (not clear when). To force using version 11 (if installed),
117
- // remove "true ||" from line below.
118
- const version_OK = true || !(match[1].startsWith('12') || match[1].startsWith('13'));
119
- if(sp && parseInt(match[1]) > max_vn && version_OK) {
116
+ if(match) {
120
117
  // Check whether command line version is executable.
121
- sp = path.join(sp, 'gurobi_cl' + (windows ? '.exe' : ''));
118
+ sp = path.join(p, 'gurobi_cl' + (windows ? '.exe' : ''));
122
119
  try {
123
120
  fs.accessSync(sp, fs.constants.X_OK);
124
- console.log('Path to Gurobi:', sp);
125
- this.solver_list.gurobi = {name: 'Gurobi', path: sp};
126
- max_vn = parseInt(match[1]);
121
+ gsp[match[1]] = p;
127
122
  } catch(err) {
128
123
  console.log(err.message);
129
124
  console.log(
130
- 'WARNING: Failed to access the Gurobi command line application');
125
+ 'WARNING: Failed to access the Gurobi command line application', sp);
131
126
  }
132
127
  }
133
128
  if(sp) continue;
@@ -191,6 +186,22 @@ module.exports = class MILPSolver {
191
186
  }
192
187
  // NOTE: Order of paths is unknown, so keep iterating.
193
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
+ }
194
205
  // For macOS, look in applications directory if not found in PATH.
195
206
  if(!this.solver_list.gurobi && !windows) {
196
207
  console.log('Looking for Gurobi in /usr/local/bin');
@@ -1102,6 +1102,8 @@ class LinnyRModel {
1102
1102
  canLink(from, to) {
1103
1103
  // Return TRUE iff FROM-node can feature a "straight" link (i.e., a
1104
1104
  // product flow) to TO-node.
1105
+ // FROM and TO *must* be different nodes.
1106
+ if(from === to) return false;
1105
1107
  if(from.type === to.type) {
1106
1108
  // No "straight" link between nodes of same type (see canConstrain
1107
1109
  // for "curved" links) UNLESS TO-node is a data product.
@@ -1135,6 +1137,37 @@ class LinnyRModel {
1135
1137
  return this.end_period - this.start_period + 1 + this.look_ahead;
1136
1138
  }
1137
1139
 
1140
+ revealOrphans() {
1141
+ // Find all products that are *not* positioned in some cluster,
1142
+ // position them in the focal cluster, and notify the user.
1143
+ const orphans = [];
1144
+ for(const k in this.products) if(this.products.hasOwnProperty(k)) {
1145
+ const p = this.products[k];
1146
+ if(p.isOrphan) orphans.push(p);
1147
+ }
1148
+ if(orphans.length) {
1149
+ let x = 100,
1150
+ y = 70,
1151
+ n = 0;
1152
+ const fc = this.focal_cluster;
1153
+ for(const p of orphans) {
1154
+ const pp = fc.addProductPosition(p, x, y);
1155
+ if(pp) {
1156
+ x += 90;
1157
+ y += 45;
1158
+ n++;
1159
+ }
1160
+ }
1161
+ // Prepare focal cluster for redrawing.
1162
+ fc.clearAllProcesses();
1163
+ // Select the added product positions and redraw.
1164
+ this.selectList(orphans);
1165
+ UI.drawDiagram(this);
1166
+ // Finally, notify the modeler.
1167
+ UI.warn(pluralS(n, '"orphaned" product') + ' added to the focal cluster');
1168
+ }
1169
+ }
1170
+
1138
1171
  processSelectorList(sl) {
1139
1172
  // Check whether selector list `sl` constitutes a new dimension.
1140
1173
  // Ignore lists of fewer than 2 "plain" selectors.
@@ -1675,6 +1708,12 @@ class LinnyRModel {
1675
1708
  if(node) l.initFromXML(node);
1676
1709
  return l;
1677
1710
  }
1711
+ // FROM and TO nodes must be different. The UI should not permit
1712
+ // drawing such nodes, but check nonetheless.
1713
+ if(from === to) {
1714
+ UI.warn(`${from.type} "${from.displayName}" cannot be linked to itself`);
1715
+ return null;
1716
+ }
1678
1717
  l = new Link(from, to);
1679
1718
  if(node) l.initFromXML(node);
1680
1719
  this.links[l.identifier] = l;
@@ -1705,6 +1744,12 @@ class LinnyRModel {
1705
1744
  if(node) c.initFromXML(node);
1706
1745
  return c;
1707
1746
  }
1747
+ // FROM and TO nodes must be different. The UI should not permit
1748
+ // drawing such nodes, but check nonetheless.
1749
+ if(from === to) {
1750
+ UI.warn(`${from.type} "${from.displayName}" cannot constrain itself`);
1751
+ return null;
1752
+ }
1708
1753
  c = new Constraint(from, to);
1709
1754
  if(node) c.initFromXML(node);
1710
1755
  // New constraint => prepare for redraw.
@@ -2701,6 +2746,8 @@ class LinnyRModel {
2701
2746
  UNDO_STACK.addXML(a.asXML);
2702
2747
  this.removeImport(a);
2703
2748
  this.removeExport(a);
2749
+ // Ensure that chart variables referencing this actor are removed.
2750
+ this.removeActorChartVariables(a);
2704
2751
  delete this.actors[k];
2705
2752
  }
2706
2753
  }
@@ -2823,6 +2870,46 @@ class LinnyRModel {
2823
2870
  }
2824
2871
  return xl;
2825
2872
  }
2873
+
2874
+ updateChartVariables(e) {
2875
+ // Ensure that all chart variable names based on entity `e` will be
2876
+ // displayed correctly the next time they are drawn.
2877
+ const sc = this.charts[CHART_MANAGER.chart_index];
2878
+ let ucm = false;
2879
+ for(const c of this.charts) {
2880
+ for(const v of c.variables) {
2881
+ if(v.object === e) {
2882
+ v.display_name = '';
2883
+ ucm = ucm || c === sc;
2884
+ }
2885
+ }
2886
+ }
2887
+ if(ucm) CHART_MANAGER.updateDialog();
2888
+ }
2889
+
2890
+ removeActorChartVariables(a) {
2891
+ // Ensure that all chart variable names based on entity `e` or actor `a`
2892
+ // will be displayed correctly the next time they are drawn.
2893
+ const sc = this.charts[CHART_MANAGER.chart_index];
2894
+ let ucm = false;
2895
+ for(const c of this.charts) {
2896
+ for(let vi = 0; vi < c.variables.length; vi++) {
2897
+ if(c.variables[vi].object === a) {
2898
+ c.variables.splice(vi, 1);
2899
+ if(c === sc) {
2900
+ ucm = true;
2901
+ if(CHART_MANAGER.variable_index === vi) {
2902
+ CHART_MANAGER.variable_index = -1;
2903
+ }
2904
+ }
2905
+ }
2906
+ }
2907
+ }
2908
+ // Update stay-on-top dialogs (if needed).
2909
+ if(ucm) CHART_MANAGER.updateDialog();
2910
+ if(EXPERIMENT_MANAGER.selected_experiment) EXPERIMENT_MANAGER.updateDialog();
2911
+ FINDER.updateDialog();
2912
+ }
2826
2913
 
2827
2914
  replaceEntityInExpressions(en1, en2, notify=true) {
2828
2915
  // Replace entity name `en1` by `en2` in all variables in all expressions
@@ -2853,12 +2940,6 @@ class LinnyRModel {
2853
2940
  pluralS(ioc.expression_count, 'expression');
2854
2941
  if(notify) UI.notify('Renamed ' + replace_msg);
2855
2942
  }
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
2943
  // Rename entities in parameters and outcomes of sensitivity analysis.
2863
2944
  for(let i = 0; i < this.sensitivity_parameters.length; i++) {
2864
2945
  const sp = this.sensitivity_parameters[i].split('|');
@@ -5181,6 +5262,8 @@ class Actor {
5181
5262
  MODEL.actors[a.identifier] = this;
5182
5263
  // Remove the old entry.
5183
5264
  delete MODEL.actors[old_id];
5265
+ // Ensure that cached variable names are updated.
5266
+ MODEL.updateChartVariables(this);
5184
5267
  MODEL.replaceEntityInExpressions(old_name, this.name);
5185
5268
  MODEL.inferIgnoredEntities();
5186
5269
  }
@@ -6042,6 +6125,8 @@ class NodeBox extends ObjectWithXYWH {
6042
6125
  }
6043
6126
  // Update actor list in case some actor name is no longer used.
6044
6127
  MODEL.cleanUpActors();
6128
+ // Ensure that cached variable names are updated.
6129
+ MODEL.updateChartVariables(this);
6045
6130
  // Update expression texts.
6046
6131
  MODEL.replaceEntityInExpressions(old_name, this.displayName);
6047
6132
  // NOTE: Renaming changes identifier that is used as index in
@@ -8783,6 +8868,11 @@ class Product extends Node {
8783
8868
  return ppc;
8784
8869
  }
8785
8870
 
8871
+ get isOrphan() {
8872
+ // Return TRUE if this product has no position in any cluster.
8873
+ return this.productPositionClusters.length <= 0;
8874
+ }
8875
+
8786
8876
  get toBeBlackBoxed() {
8787
8877
  // Return TRUE if this product occurs only in "black box" clusters.
8788
8878
  for(const c of this.productPositionClusters) if(!c.blackBoxed) return false;
@@ -9747,6 +9837,8 @@ class Dataset {
9747
9837
  this.name = name;
9748
9838
  MODEL.datasets[new_id] = this;
9749
9839
  if(old_id !== new_id) delete MODEL.datasets[old_id];
9840
+ // Ensure that cached variable names are updated.
9841
+ MODEL.updateChartVariables(this);
9750
9842
  MODEL.replaceEntityInExpressions(old_name, name, notify);
9751
9843
  return MODEL.datasets[new_id];
9752
9844
  }
@@ -173,9 +173,6 @@ class Expression {
173
173
  this.wildcard_vectors = {};
174
174
  this.wildcard_vector_index = false;
175
175
  this.method_object_list.length = 0;
176
- if(!isEmpty(this.cache)) {
177
- console.log('HERE Clearing cache', this.text, '\n', Object.keys(this.cache));
178
- }
179
176
  this.cache = {};
180
177
  this.compile(); // if(!this.compiled) REMOVED to ensure correct isStatic!!
181
178
  // Static expressions only need a vector with one element (having index 0)
@@ -1223,26 +1220,24 @@ class ExpressionParser {
1223
1220
  if(!anchor1) anchor1 = 't';
1224
1221
  if(!anchor2) anchor2 = 't';
1225
1222
  }
1226
- // First handle this special case: no name or attribute. This is valid
1227
- // only for dataset modifier expressions (and hence also equations).
1228
- // Variables like [@t-1] are interpreted as a self-reference. This is
1229
- // meaningful when a *negative* offset is specified to denote "use the
1230
- // value of this expression for some earlier time step".
1231
- // NOTES:
1232
- // (1) This makes the expression dynamic.
1233
- // (2) It does not apply to array-type datasets, as these have no
1234
- // time dimension.
1235
- if(!name && !attr && this.dataset && !this.dataset.array) {
1223
+ // First handle this special case: an equation self-reference.
1224
+ // Variables like [@t-1] are interpreted as an implicit self-reference.
1225
+ // Self-references are meaningful when a *negative* offset is specified
1226
+ // to denote "use the value of this expression for some earlier time step".
1227
+ // NOTE: This makes the expression dynamic.
1228
+ if(!attr && this.dataset === MODEL.equations_dataset &&
1229
+ (!name || UI.nameToID(name) === UI.nameToID(this.attribute))) {
1236
1230
  this.is_static = false;
1237
1231
  this.log('dynamic because of self-reference');
1238
- if(('cips'.indexOf(anchor1) >= 0 || anchor1 === 't' && offset1 < 0) &&
1239
- ('cips'.indexOf(anchor2) >= 0 ||anchor2 === 't' && offset2 < 0)) {
1232
+ if(('cfps'.indexOf(anchor1) >= 0 || anchor1 === 't' && offset1 < 0) &&
1233
+ ('cfps'.indexOf(anchor2) >= 0 ||anchor2 === 't' && offset2 < 0)) {
1240
1234
  if(this.TRACE) console.log('TRACE: Variable is a self-reference.');
1241
1235
  // The `xv` attribute will be recognized by VMI_push_var to denote
1242
1236
  // "use the vector of the expression for which this VMI is code".
1243
- return [{xv: true, dv: this.dataset.defaultValue},
1237
+ return [{xv: true, dv: VM.UNDEFINED},
1244
1238
  anchor1, offset1, anchor2, offset2];
1245
1239
  }
1240
+ // Fall-through: invalid offset => warning.
1246
1241
  msg = 'Expression can reference only previous values of itself';
1247
1242
  }
1248
1243
  // A leading "!" denotes: pass variable reference instead of its value.
@@ -1531,8 +1526,12 @@ class ExpressionParser {
1531
1526
  let sel = '',
1532
1527
  xtype = '';
1533
1528
  if(obj instanceof DatasetModifier) {
1534
- sel = obj.selector;
1535
- xtype = 'Equation';
1529
+ if(attr) {
1530
+ msg = 'Equations have no attributes';
1531
+ } else {
1532
+ sel = obj.selector;
1533
+ xtype = 'Equation';
1534
+ }
1536
1535
  } else if(obj instanceof Dataset) {
1537
1536
  sel = attr;
1538
1537
  xtype = 'Dataset modifier expression';
@@ -2304,7 +2303,7 @@ class VirtualMachine {
2304
2303
  // for the numerical stability. Meanwhile, the slack variables themselves
2305
2304
  // will have values 1/sm times the actual slack, so this needs to be
2306
2305
  // scaled back in solver messages.
2307
- this.SLACK_MULTIPLIER = 0.01;
2306
+ this.SLACK_MULTIPLIER = 1;
2308
2307
  // The "epsilon multiplier" divides the ON/OFF threshold over the POS
2309
2308
  // and NEG binaries and the EPS slack variable to avoid high coefficients.
2310
2309
  this.EPSILON_MULTIPLIER = 1 / Math.sqrt(this.ON_OFF_THRESHOLD);
@@ -3394,7 +3393,7 @@ class VirtualMachine {
3394
3393
  // If not a sink, UB is set to 0.
3395
3394
  if(notsnk) u = 0;
3396
3395
  }
3397
-
3396
+
3398
3397
  // NOTE: Stock constraints must take into account extra inflows
3399
3398
  // (source) or outflows (sink).
3400
3399
  // Check for special case of equal bounds, as then one EQ constraint
@@ -3434,7 +3433,7 @@ class VirtualMachine {
3434
3433
  this.code.push(
3435
3434
  [l instanceof Expression? VMI_set_var_rhs : VMI_set_const_rhs, l],
3436
3435
  [VMI_add_constraint, VM.GE]
3437
- );
3436
+ );
3438
3437
  }
3439
3438
  // Add upper bound (LE) constraint unless product is a sink node
3440
3439
  if(notsnk) {
@@ -3448,7 +3447,7 @@ class VirtualMachine {
3448
3447
  this.code.push(
3449
3448
  [u instanceof Expression ? VMI_set_var_rhs : VMI_set_const_rhs, u],
3450
3449
  [VMI_add_constraint, VM.LE]
3451
- );
3450
+ );
3452
3451
  }
3453
3452
  }
3454
3453
  }
@@ -3622,6 +3621,7 @@ class VirtualMachine {
3622
3621
  // However, Linny-R does not prohibit negative bounds on processes, nor
3623
3622
  // negative rates on links. To be consistently permissive, cash IN and
3624
3623
  // cash OUT of all actors are both allowed to become negative.
3624
+ /*
3625
3625
  for(const k of actor_keys) {
3626
3626
  const a = MODEL.actors[k];
3627
3627
  // NOTE: Add fourth parameter TRUE to signal that the SOLVER's
@@ -3635,7 +3635,7 @@ class VirtualMachine {
3635
3635
  VM.MINUS_INFINITY, VM.PLUS_INFINITY, true]]
3636
3636
  );
3637
3637
  }
3638
-
3638
+ */
3639
3639
  // NEXT: Define the bounds for all production level variables.
3640
3640
  // NOTE: The VM instructions check dynamically whether the variable
3641
3641
  // index is listed as "fixed" for the round that is being solved.
@@ -3669,7 +3669,7 @@ class VirtualMachine {
3669
3669
  if(rf != 0) {
3670
3670
  // Note: 32-bit integer `b` is used for bit-wise AND
3671
3671
  let b = 1;
3672
- for(j = 0; j < MODEL.rounds; j++) {
3672
+ for(let j = 0; j < MODEL.rounds; j++) {
3673
3673
  if((rf & b) != 0) {
3674
3674
  this.fixed_var_indices[j][p.level_var_index] = true;
3675
3675
  // @@ TO DO: fixate associated binary variables if applicable!
@@ -4180,8 +4180,9 @@ class VirtualMachine {
4180
4180
 
4181
4181
  (a) L = POSL - NEGL
4182
4182
 
4183
- This "partitions" the level in two components. The following constraints
4184
- ensure a (functionally) unique partitioning:
4183
+ This "partitions" the level in two components.
4184
+
4185
+ The following constraints ensure a (functionally) unique partitioning:
4185
4186
 
4186
4187
  (b) NEGL - M*NEG <= 0 (so NEG=1 if NEGL > 0)
4187
4188
  (c) POSL - M*POS <= 0 (so POS=1 if POSL > 0)
@@ -4872,14 +4873,31 @@ class VirtualMachine {
4872
4873
  pl = this.keepException(pl, pl / count);
4873
4874
  }
4874
4875
  } 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
4876
+ // NOTE: Calculate throughput on basis of *process* levels and rates,
4877
+ // as not all actual flows may have been computed yet.
4877
4878
  pl = 0;
4878
- for(const ll of p.inputs) {
4879
+ for(const ll of p.inputs) if(ll.from_node instanceof Process) {
4879
4880
  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);
4881
+ lld = ll.actualDelay(b),
4882
+ ipl = ll.from_node.actualLevel(bt - lld),
4883
+ rr = ll.relative_rate.result(bt - lld),
4884
+ flow = ipl * rr;
4885
+ // NOTE: Only consider INflows, so flow must be > 0.
4886
+ if(flow > 0) {
4887
+ pl = this.severestIssue([pl, ipl, rr], pl + flow);
4888
+ }
4889
+ }
4890
+ // NOTE: Again, only consider processes.
4891
+ for(const ll of p.outputs) if(ll.to_node instanceof Process) {
4892
+ const
4893
+ // NOTE: Links TO a process cannot have a delay.
4894
+ opl = ll.to_node.actualLevel(bt),
4895
+ rr = ll.relative_rate.result(bt),
4896
+ flow = opl * rr;
4897
+ // NOTE: Only consider INflows, so now flow must be < 0.
4898
+ if(flow < 0) {
4899
+ pl = this.severestIssue([pl, opl, rr], pl - flow);
4900
+ }
4883
4901
  }
4884
4902
  } else if(l.multiplier === VM.LM_PEAK_INC) {
4885
4903
  // Actual flow over "peak increase" link is zero unless...
@@ -5060,7 +5078,7 @@ class VirtualMachine {
5060
5078
  MODEL.calculateCostPrices(b);
5061
5079
  }
5062
5080
  }
5063
-
5081
+ /*
5064
5082
  // THEN: Reset all datasets that are outcomes or serve as "formulas".
5065
5083
  for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
5066
5084
  const ds = MODEL.datasets[k];
@@ -5073,7 +5091,7 @@ class VirtualMachine {
5073
5091
  }
5074
5092
  }
5075
5093
  }
5076
-
5094
+ */
5077
5095
  // THEN: Reset the vectors of all chart variables.
5078
5096
  for(const c of MODEL.charts) c.resetVectors();
5079
5097
 
@@ -5461,8 +5479,8 @@ class VirtualMachine {
5461
5479
  v,
5462
5480
  line = '';
5463
5481
  // 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++) {
5482
+ let ncols = abl * this.cols + this.chunk_variables.length;
5483
+ for(p = 1; p <= ncols; p++) {
5466
5484
  if(this.objective.hasOwnProperty(p)) {
5467
5485
  c = this.objective[p];
5468
5486
  // Check for numeric issues.
@@ -5486,8 +5504,8 @@ class VirtualMachine {
5486
5504
  } else {
5487
5505
  this.lines += '\n/* Constraints */\n';
5488
5506
  }
5489
- n = this.matrix.length;
5490
- for(let r = 0; r < n; r++) {
5507
+ let nrows = this.matrix.length;
5508
+ for(let r = 0; r < nrows; r++) {
5491
5509
  const row = this.matrix[r];
5492
5510
  if(named_constraints) line = `C${r + 1}: `;
5493
5511
  for(p in row) if (row.hasOwnProperty(p)) {
@@ -5521,8 +5539,7 @@ class VirtualMachine {
5521
5539
  } else {
5522
5540
  this.lines += '\n/* Variable bounds */\n';
5523
5541
  }
5524
- n = abl * this.cols;
5525
- for(p = 1; p <= n; p++) {
5542
+ for(p = 1; p <= ncols; p++) {
5526
5543
  let lb = null,
5527
5544
  ub = null;
5528
5545
  if(this.lower_bounds.hasOwnProperty(p)) {
@@ -5658,7 +5675,7 @@ class VirtualMachine {
5658
5675
  for(let i in this.is_semi_continuous) if(Number(i)) v_set.push(vbl(i));
5659
5676
  if(v_set.length > 0) this.lines += 'sec ' + v_set.join(', ') + ';\n';
5660
5677
  // LP_solve supports SOS, so add the SOS section if needed.
5661
- if(this.nzp_var_indices.length ||this.sos_var_indices.length) {
5678
+ if(this.nzp_var_indices.length || this.sos_var_indices.length) {
5662
5679
  this.lines += 'sos\n';
5663
5680
  for(let j = 0; j < abl; j++) {
5664
5681
  // First add the SOS1 constraints for NZP-partitioned levels.
@@ -6149,11 +6166,11 @@ Solver status = ${json.status}`);
6149
6166
  // Generate lines of code in format that should be accepted by solver.
6150
6167
  if(this.solver_id === 'gurobi') {
6151
6168
  this.writeLpFormat(true);
6152
- } else if(this.solver_id === 'mosek') {
6169
+ } else if(this.solver_id === 'mosek' || this.solver_id === 'scip') {
6153
6170
  // NOTE: For MOSEK, constraints must be named, or variable names
6154
- // in solution file will not match.
6171
+ // in solution file will not match. SCIP works, but generates warnings.
6155
6172
  this.writeLpFormat(true, true);
6156
- } else if(this.solver_id === 'cplex' || this.solver_id === 'scip') {
6173
+ } else if(this.solver_id === 'cplex') {
6157
6174
  // NOTE: The more widely accepted CPLEX LP format differs from the
6158
6175
  // LP_solve format that was used by the first versions of Linny-R.
6159
6176
  // TRUE indicates "CPLEX format".
@@ -6725,7 +6742,12 @@ function VMI_push_var(x, args) {
6725
6742
  x.push(v);
6726
6743
  } else if(xv) {
6727
6744
  // Variable references an earlier value computed for this expression `x`.
6728
- x.push(t >= 0 && t < x.vector.length ? x.vector[t] : obj.dv);
6745
+ // NOTE: When this value has NOT been computed yet, use the specified default.
6746
+ if(t >= 0 && t < x.vector.length && x.vector[t] !== VM.NOT_COMPUTED) {
6747
+ x.push(x.vector[t]);
6748
+ } else {
6749
+ x.push(obj.dv);
6750
+ }
6729
6751
  } else if(obj.hasOwnProperty('c') && obj.hasOwnProperty('u')) {
6730
6752
  // Object holds link lists for cluster balance computation.
6731
6753
  x.push(MODEL.flowBalance(obj, t));
@@ -8329,7 +8351,7 @@ function VMI_set_bounds(args) {
8329
8351
  l = args[1];
8330
8352
  u = args[2];
8331
8353
  if(u instanceof Expression) u = u.result(VM.t);
8332
- if(u === VM.UNDEFINED) {
8354
+ if(u === VM.UNDEFINED || u === VM.DIAGNOSIS_UPPER_BOUND) {
8333
8355
  u = inf_val;
8334
8356
  } else {
8335
8357
  u = Math.min(u, inf_val);
@@ -8340,6 +8362,8 @@ function VMI_set_bounds(args) {
8340
8362
  if(l instanceof Expression) l = l.result(VM.t);
8341
8363
  if(l === VM.UNDEFINED || !l) {
8342
8364
  l = 0;
8365
+ } else if(l === -VM.DIAGNOSIS_UPPER_BOUND) {
8366
+ l = -inf_val;
8343
8367
  } else {
8344
8368
  l = Math.max(l, -inf_val);
8345
8369
  }
@@ -8355,7 +8379,7 @@ function VMI_set_bounds(args) {
8355
8379
  VM.variables[vi - 1][0],'] t = ', VM.t, ' LB = ', VM.sig4Dig(l),
8356
8380
  ', UB = ', VM.sig4Dig(u), fixed].join(''), l, u, inf_val, 'args:', args);
8357
8381
  console.log(p);
8358
- throw "STOP";
8382
+ if(!DEBUGGING) throw "STOP";
8359
8383
  } else if(u < l) {
8360
8384
  // Check the difference, as this may be negligible.
8361
8385
  if(u - l < VM.SIG_DIF_FROM_ZERO) {
@@ -8407,7 +8431,7 @@ function VMI_set_bounds(args) {
8407
8431
  cvi = VM.chunk_offset + p.peak_inc_var_index,
8408
8432
  // Check if peak UB already set for previous t
8409
8433
  piub = VM.upper_bounds[cvi];
8410
- // If so, use the highest value
8434
+ // If so, use the highest value.
8411
8435
  if(piub) u = Math.max(piub, u);
8412
8436
  VM.upper_bounds[cvi] = u;
8413
8437
  VM.upper_bounds[cvi + 1] = u;
@@ -9055,6 +9079,8 @@ function VMI_update_grid_process_cash_coefficients(p) {
9055
9079
  // VMI_update_cash_coefficient).
9056
9080
  let fn = null,
9057
9081
  tn = null;
9082
+ // NOTE: Grid processes are assumed to connect exactly *two* products by
9083
+ // regular links, so it suffices to find the *first* ingoing...
9058
9084
  for(const l of p.inputs) {
9059
9085
  if(l.multiplier === VM.LM_LEVEL &&
9060
9086
  !MODEL.ignored_entities[l.identifier]) {
@@ -9062,6 +9088,7 @@ function VMI_update_grid_process_cash_coefficients(p) {
9062
9088
  break;
9063
9089
  }
9064
9090
  }
9091
+ // ... and the first outgoing regular link.
9065
9092
  for(const l of p.outputs) {
9066
9093
  if(l.multiplier === VM.LM_LEVEL &&
9067
9094
  !MODEL.ignored_entities[l.identifier]) {
@@ -9072,7 +9099,7 @@ function VMI_update_grid_process_cash_coefficients(p) {
9072
9099
  const
9073
9100
  fp = (fn && fn.price.defined ? fn.price.result(VM.t) : 0),
9074
9101
  tp = (tn && tn.price.defined ? tn.price.result(VM.t) : 0);
9075
- // Only proceed if process links to a product with a non-zero price.
9102
+ // Only proceed if process links to at least one product with a non-zero price.
9076
9103
  if(fp || tp) {
9077
9104
  const
9078
9105
  gpv = VM.gridProcessVarIndices(p, VM.offset),
@@ -9081,30 +9108,30 @@ function VMI_update_grid_process_cash_coefficients(p) {
9081
9108
  // If FROM node has price > 0, then all UP flows generate cash OUT
9082
9109
  // *without* loss while all DOWN flows generate cash IN *with* loss.
9083
9110
  for(let i = 0; i < gpv.slopes; i++) {
9084
- addCashOut(gpv.up[i], -fp);
9085
- addCashIn(gpv.down[i], (1 - lr[i]) * -fp);
9111
+ addCashOut(gpv.up[i], fp);
9112
+ addCashIn(gpv.down[i], (1 - lr[i]) * fp);
9086
9113
  }
9087
9114
  } else if(fp < 0) {
9088
9115
  // If FROM node has price < 0, then all UP flows generate cash IN
9089
9116
  // *without* loss while all DOWN flows generate cash OUT *with* loss.
9090
9117
  for(let i = 0; i < gpv.slopes; i++) {
9091
- addCashIn(gpv.up[i], fp);
9092
- addCashOut(gpv.down[i], (1 - lr[i]) * fp);
9118
+ addCashIn(gpv.up[i], -fp);
9119
+ addCashOut(gpv.down[i], (1 - lr[i]) * -fp);
9093
9120
  }
9094
9121
  }
9095
9122
  if(tp > 0) {
9096
9123
  // If TO node has price > 0, then all UP flows generate cash IN *with*
9097
9124
  // loss while all DOWN flows generate cash OUT *without* loss.
9098
9125
  for(let i = 0; i < gpv.slopes; i++) {
9099
- addCashIn(gpv.up[i], (1 - lr[i]) * -tp);
9100
- addCashOut(gpv.down[i], -tp);
9126
+ addCashIn(gpv.up[i], (1 - lr[i]) * tp);
9127
+ addCashOut(gpv.down[i], tp);
9101
9128
  }
9102
9129
  } else if(tp < 0) {
9103
9130
  // If TO node has price < 0, then all UP flows generate cash OUT
9104
9131
  // *with* loss while all DOWN flows generate cash IN *without* loss.
9105
9132
  for(let i = 0; i < gpv.slopes; i++) {
9106
- addCashOut(gpv.up[i], (1 - lr[i]) * tp);
9107
- addCashIn(gpv.down[i], tp);
9133
+ addCashOut(gpv.up[i], (1 - lr[i]) * -tp);
9134
+ addCashIn(gpv.down[i], -tp);
9108
9135
  }
9109
9136
  }
9110
9137
  }
@@ -9123,7 +9150,10 @@ function VMI_set_objective() {
9123
9150
  for(let i = 0; i < VM.chunk_variables.length; i++) {
9124
9151
  const vn = VM.chunk_variables[i][0];
9125
9152
  if(vn.indexOf('peak') > 0) {
9126
- const pvp = VM.PEAK_VAR_PENALTY / VM.cash_scalar;
9153
+ // NOTE: When prices in model are low, the cash scalar is small
9154
+ // and then a peak variable penalty of 0.1 currency unit will
9155
+ // significantly impact the tipping point for investment choices
9156
+ const pvp = VM.PEAK_VAR_PENALTY / Math.max(VM.cash_scalar, 2000);
9127
9157
  // NOTE: Chunk offset takes into account that indices are 0-based.
9128
9158
  VM.objective[VM.chunk_offset + i] = -pvp;
9129
9159
  // Put higher penalty on "block peak" than on "look-ahead peak"
@@ -9156,15 +9186,14 @@ function VMI_add_constraint(ct) {
9156
9186
  for(let i in VM.coefficients) if(Number(i)) {
9157
9187
  // Do not add (near)zero coefficients to the matrix.
9158
9188
  const c = VM.coefficients[i];
9159
- if(Math.abs(c) >= VM.NEAR_ZERO) {
9160
- row[i] = c;
9161
- }
9189
+ if(Math.abs(c) >= VM.NEAR_ZERO) row[i] = c;
9162
9190
  }
9163
9191
  // Special case:
9164
9192
  if(ct === VM.ACTOR_CASH) {
9165
9193
  VM.actor_cash_constraints.push(VM.matrix.length);
9166
9194
  ct = VM.EQ;
9167
9195
  }
9196
+
9168
9197
  let rhs = VM.rhs;
9169
9198
  // Check for <= (near) +infinity and >= (near) -infinity: such
9170
9199
  // constraints should not be added to the model.
@@ -9210,7 +9239,7 @@ function VMI_add_semicontinuous_constraints(p) {
9210
9239
  // level - UB*binary <= 0
9211
9240
  row = {};
9212
9241
  row[l_index] = 1;
9213
- row[lb_index] = -ub;
9242
+ row[lb_index] = -ub - 1;
9214
9243
  VM.matrix.push(row);
9215
9244
  VM.right_hand_side.push(0);
9216
9245
  VM.constraint_types.push(VM.LE);
@@ -9227,11 +9256,26 @@ function VMI_add_NZP_continuous_constraints(p) {
9227
9256
  console.log('add_NZP_continuous_constraints (t = ' + VM.t + ')');
9228
9257
  }
9229
9258
  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;
9259
+ let row = {};
9260
+ if(p.level_to_zero) {
9261
+ // For semi-continuous processes, the level is always >= 0.
9262
+ // To prevent issues with binaries, set POSL = L and NEGL = 0 to rule out
9263
+ // the possibility of NEGL being used to compensate for a positive epsilon.
9264
+ // (a1) L - POSL = 0.
9265
+ row[VM.offset + p.level_var_index] = 1;
9266
+ row[VM.offset + p.posl_var_index] = -1;
9267
+ VM.matrix.push(row);
9268
+ VM.right_hand_side.push(0);
9269
+ VM.constraint_types.push(VM.EQ);
9270
+ row = {};
9271
+ // (a2) NEGL = 0.
9272
+ row[VM.offset + p.negl_var_index] = 1;
9273
+ } else {
9274
+ // (a) L + NEGL - POSL = 0 (so POSL - NEGL = L).
9275
+ row[VM.offset + p.level_var_index] = 1;
9276
+ row[VM.offset + p.negl_var_index] = 1;
9277
+ row[VM.offset + p.posl_var_index] = -1;
9278
+ }
9235
9279
  VM.matrix.push(row);
9236
9280
  VM.right_hand_side.push(0);
9237
9281
  VM.constraint_types.push(VM.EQ);
@@ -9291,14 +9335,17 @@ function VMI_add_NZP_binary_constraints(p) {
9291
9335
  row[pos_index] = VM.EPSILON_MULTIPLIER * VM.ON_OFF_THRESHOLD;
9292
9336
  row[posl_index] = -VM.EPSILON_MULTIPLIER;
9293
9337
  // Provide slack so the constraint can always be met, but at a significant cost.
9294
- row[eps_index] = -VM.SLACK_MULTIPLIER / VM.EPSILON_MULTIPLIER;
9338
+ // NOTE: Do *NOT* do this for semi-continuous processes.
9339
+ if(!p.level_to_zero) {
9340
+ row[eps_index] = -VM.SLACK_MULTIPLIER / VM.EPSILON_MULTIPLIER;
9341
+ }
9295
9342
  VM.matrix.push(row);
9296
9343
  VM.right_hand_side.push(0);
9297
9344
  VM.constraint_types.push(VM.LE);
9298
9345
  // NOTE: This VMI is added when LB *may* become negative, so check
9299
9346
  // whether now (at run time) LB >= 0, as then NZP partitioning is
9300
9347
  // trivial and need not be done by the solver.
9301
- if(lb >= 0) {
9348
+ if(lb >= 0 || p.level_to_zero) {
9302
9349
  // If L >= 0, NEG must be 0.
9303
9350
  row = {};
9304
9351
  row[neg_index] = 1;
@@ -9748,7 +9795,7 @@ function VMI_add_throughput_to_coefficients(link) {
9748
9795
  // Skip link when it has rate = 0.
9749
9796
  if(r2 === 0) continue;
9750
9797
  // By default, use the FROM node's level...
9751
- let vi = (lfn.is_zero_var_index < 0 ? lfn.level_var_index :
9798
+ let vi = (lfn.posl_var_index < 0 ? lfn.level_var_index :
9752
9799
  // ... but differentiate when this level is NZP-partitioned.
9753
9800
  // Then use positive level component when rate > 0, and negative
9754
9801
  // level component when rate < 0, so throughput flow is always >= 0.
@@ -9807,7 +9854,7 @@ function VMI_add_throughput_to_coefficients(link) {
9807
9854
  if(r2 === 0) continue;
9808
9855
  // Also skip when level is not NZP-partitioned, as then an output-link
9809
9856
  // cannot contribute to the *inflow* of the process being "read".
9810
- if(ltn.is_zero_var_index < 0) continue;
9857
+ if(ltn.posl_var_index < 0) continue;
9811
9858
  // Now use the negative level component when rate > 0, and positive
9812
9859
  // level component when rate < 0, so throughput flow is always >= 0.
9813
9860
  const