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 +1 -1
- package/static/scripts/linny-r-gui-chart-manager.js +2 -1
- package/static/scripts/linny-r-gui-file-manager.js +3 -3
- package/static/scripts/linny-r-gui-finder.js +3 -1
- package/static/scripts/linny-r-milp.js +24 -13
- package/static/scripts/linny-r-model.js +98 -6
- package/static/scripts/linny-r-vm.js +118 -71
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
|
}
|
|
@@ -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 <
|
|
317
|
+
if(mi < this.sd_count) {
|
|
318
318
|
path += sd.subdirs[mi].name;
|
|
319
319
|
} else {
|
|
320
|
-
path += sd.models[mi -
|
|
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 >=
|
|
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.
|
|
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
|
|
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');
|
|
@@ -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:
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1229
|
-
//
|
|
1230
|
-
//
|
|
1231
|
-
|
|
1232
|
-
|
|
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(('
|
|
1239
|
-
('
|
|
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:
|
|
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
|
-
|
|
1535
|
-
|
|
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 =
|
|
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.
|
|
4184
|
-
|
|
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:
|
|
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
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
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
|
|
5465
|
-
for(p = 1; 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
|
-
|
|
5490
|
-
for(let r = 0; 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
|
-
|
|
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'
|
|
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
|
-
|
|
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
|
|
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],
|
|
9085
|
-
addCashIn(gpv.down[i], (1 - lr[i]) *
|
|
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]) *
|
|
9100
|
-
addCashOut(gpv.down[i],
|
|
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
|
-
|
|
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
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|