linny-r 3.0.6 → 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.6",
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": {
@@ -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
  }
@@ -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,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');
@@ -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
  }
@@ -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