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
|
@@ -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)
|
|
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.
|
|
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
|
|
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)
|
|
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(
|
|
118
|
+
sp = path.join(p, 'gurobi_cl' + (windows ? '.exe' : ''));
|
|
122
119
|
try {
|
|
123
120
|
fs.accessSync(sp, fs.constants.X_OK);
|
|
124
|
-
|
|
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 =
|
|
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.
|
|
4184
|
-
|
|
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:
|
|
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
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
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
|
|
5465
|
-
for(p = 1; 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
|
-
|
|
5490
|
-
for(let r = 0; 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
|
-
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|