linny-r 3.0.5 → 3.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -906,6 +906,7 @@ class GUIChartManager extends ChartManager {
|
|
|
906
906
|
}
|
|
907
907
|
|
|
908
908
|
toggleVariable(vi, event) {
|
|
909
|
+
// Toggle the "display this variable in the chart" (yes/no) property.
|
|
909
910
|
window.event.stopPropagation();
|
|
910
911
|
if(vi >= 0 && this.chart_index >= 0) {
|
|
911
912
|
const v_list = MODEL.charts[this.chart_index].variables;
|
|
@@ -937,9 +938,9 @@ class GUIChartManager extends ChartManager {
|
|
|
937
938
|
UI.setBox('v-box-' + vi, nv);
|
|
938
939
|
}
|
|
939
940
|
}
|
|
940
|
-
//
|
|
941
|
+
// Redraw chart and table (with one variable more or less).
|
|
941
942
|
this.drawChart();
|
|
942
|
-
// Also update the experiment viewer (charts define the output variables)
|
|
943
|
+
// Also update the experiment viewer (charts define the output variables).
|
|
943
944
|
if(EXPERIMENT_MANAGER.selected_experiment) {
|
|
944
945
|
EXPERIMENT_MANAGER.updateDialog();
|
|
945
946
|
}
|
|
@@ -947,6 +948,7 @@ class GUIChartManager extends ChartManager {
|
|
|
947
948
|
}
|
|
948
949
|
|
|
949
950
|
moveVariable(dir) {
|
|
951
|
+
// Move variable up or down in the chart variable list.
|
|
950
952
|
if(this.chart_index >= 0 && this.variable_index >= 0) {
|
|
951
953
|
const c = MODEL.charts[this.chart_index];
|
|
952
954
|
let vi = this.variable_index;
|
|
@@ -961,6 +963,7 @@ class GUIChartManager extends ChartManager {
|
|
|
961
963
|
}
|
|
962
964
|
|
|
963
965
|
modifyVariable() {
|
|
966
|
+
// Update the properties of the variable being edited.
|
|
964
967
|
if(this.variable_index >= 0) {
|
|
965
968
|
const s = UI.validNumericInput('variable-scale', 'scale factor');
|
|
966
969
|
if(!s) return;
|
|
@@ -978,16 +981,20 @@ class GUIChartManager extends ChartManager {
|
|
|
978
981
|
cv.color = this.color_picker.color.hexString;
|
|
979
982
|
// NOTE: Clear the vector so it will be recalculated.
|
|
980
983
|
cv.vector.length = 0;
|
|
984
|
+
// Likewise clear the display name cache.
|
|
985
|
+
cv.display_name = '';
|
|
981
986
|
}
|
|
982
987
|
this.variable_modal.hide();
|
|
983
988
|
this.updateDialog();
|
|
984
989
|
}
|
|
985
990
|
|
|
986
991
|
renameEquation() {
|
|
987
|
-
//
|
|
992
|
+
// Rename the selected variable (if it is an equation).
|
|
988
993
|
if(this.chart_index >= 0 && this.variable_index >= 0) {
|
|
989
994
|
const v = MODEL.charts[this.chart_index].variables[this.variable_index];
|
|
990
995
|
if(v.object === MODEL.equations_dataset || v.object instanceof DatasetModifier) {
|
|
996
|
+
// Clear the display name cache (anticipating a name change).
|
|
997
|
+
v.display_name = '';
|
|
991
998
|
const m = MODEL.equations_dataset.modifiers[UI.nameToID(v.attribute)];
|
|
992
999
|
if(m instanceof DatasetModifier) {
|
|
993
1000
|
EQUATION_MANAGER.selected_modifier = m;
|
|
@@ -998,7 +1005,7 @@ class GUIChartManager extends ChartManager {
|
|
|
998
1005
|
}
|
|
999
1006
|
|
|
1000
1007
|
editEquation() {
|
|
1001
|
-
//
|
|
1008
|
+
// Open the expression editor for the selected variable (if equation).
|
|
1002
1009
|
if(this.chart_index >= 0 && this.variable_index >= 0) {
|
|
1003
1010
|
const v = MODEL.charts[this.chart_index].variables[this.variable_index];
|
|
1004
1011
|
if(v.object === MODEL.equations_dataset || v.object instanceof DatasetModifier) {
|
|
@@ -1012,14 +1019,15 @@ class GUIChartManager extends ChartManager {
|
|
|
1012
1019
|
}
|
|
1013
1020
|
|
|
1014
1021
|
deleteVariable() {
|
|
1015
|
-
//
|
|
1022
|
+
// Delete the selected variable from the chart.
|
|
1016
1023
|
if(this.variable_index >= 0) {
|
|
1017
1024
|
MODEL.charts[this.chart_index].variables.splice(this.variable_index, 1);
|
|
1018
1025
|
this.variable_index = -1;
|
|
1019
1026
|
this.updateDialog();
|
|
1020
1027
|
// Also update the experiment viewer (charts define the output variables)
|
|
1021
1028
|
// and finder dialog.
|
|
1022
|
-
if(EXPERIMENT_MANAGER.selected_experiment)
|
|
1029
|
+
if(EXPERIMENT_MANAGER.selected_experiment) EXPERIMENT_MANAGER.updateDialog();
|
|
1030
|
+
FINDER.updateDialog();
|
|
1023
1031
|
}
|
|
1024
1032
|
this.variable_modal.hide();
|
|
1025
1033
|
}
|
|
@@ -1106,7 +1114,7 @@ class GUIChartManager extends ChartManager {
|
|
|
1106
1114
|
|
|
1107
1115
|
stretchChart(delta) {
|
|
1108
1116
|
this.stretch_factor = Math.max(1, Math.min(10, this.stretch_factor + delta));
|
|
1109
|
-
// NOTE:
|
|
1117
|
+
// NOTE: Do not use 'auto', as this produces poor results.
|
|
1110
1118
|
document.getElementById('chart-svg-scroller').style.overflowX =
|
|
1111
1119
|
(this.stretch_factor === 1 ? 'hidden' : 'scroll');
|
|
1112
1120
|
const csc = document.getElementById('chart-svg-container');
|
|
@@ -1157,7 +1165,7 @@ class GUIChartManager extends ChartManager {
|
|
|
1157
1165
|
}
|
|
1158
1166
|
|
|
1159
1167
|
downloadChart(shift) {
|
|
1160
|
-
//
|
|
1168
|
+
// Push the SVG of the selected chart as file to the browser.
|
|
1161
1169
|
if(this.chart_index >= 0) {
|
|
1162
1170
|
const
|
|
1163
1171
|
chart = MODEL.charts[this.chart_index],
|
|
@@ -1185,7 +1193,7 @@ class GUIChartManager extends ChartManager {
|
|
|
1185
1193
|
}
|
|
1186
1194
|
|
|
1187
1195
|
actuallyDrawChart() {
|
|
1188
|
-
// Draw the chart, and reset the cursor when done
|
|
1196
|
+
// Draw the chart, and reset the cursor when done.
|
|
1189
1197
|
MODEL.charts[this.chart_index].draw();
|
|
1190
1198
|
this.drawing_chart = false;
|
|
1191
1199
|
this.drawTable();
|
|
@@ -554,7 +554,7 @@ class Finder {
|
|
|
554
554
|
stack = UI.boxChecked('confirm-add-chart-variables-stacked'),
|
|
555
555
|
equations = this.entities[0] instanceof DatasetModifier,
|
|
556
556
|
enl = [];
|
|
557
|
-
for(const e of this.entities) enl.push(equations ? e.selector : e.
|
|
557
|
+
for(const e of this.entities) enl.push(equations ? e.selector : e.displayName);
|
|
558
558
|
enl.sort((a, b) => UI.compareFullNames(a, b, true));
|
|
559
559
|
for(const en of enl) {
|
|
560
560
|
let vi = null;
|
|
@@ -104,26 +104,25 @@ module.exports = class MILPSolver {
|
|
|
104
104
|
const
|
|
105
105
|
windows = os.platform().startsWith('win'),
|
|
106
106
|
path_list = process.env.PATH.split(path.delimiter);
|
|
107
|
-
// Iterate over all
|
|
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
|
-
if(sp && parseInt(match[1]) > max_vn) {
|
|
116
|
+
if(match) {
|
|
116
117
|
// Check whether command line version is executable.
|
|
117
|
-
sp = path.join(
|
|
118
|
+
sp = path.join(p, 'gurobi_cl' + (windows ? '.exe' : ''));
|
|
118
119
|
try {
|
|
119
120
|
fs.accessSync(sp, fs.constants.X_OK);
|
|
120
|
-
|
|
121
|
-
this.solver_list.gurobi = {name: 'Gurobi', path: sp};
|
|
122
|
-
max_vn = parseInt(match[1]);
|
|
121
|
+
gsp[match[1]] = p;
|
|
123
122
|
} catch(err) {
|
|
124
123
|
console.log(err.message);
|
|
125
124
|
console.log(
|
|
126
|
-
'WARNING: Failed to access the Gurobi command line application');
|
|
125
|
+
'WARNING: Failed to access the Gurobi command line application', sp);
|
|
127
126
|
}
|
|
128
127
|
}
|
|
129
128
|
if(sp) continue;
|
|
@@ -187,6 +186,22 @@ module.exports = class MILPSolver {
|
|
|
187
186
|
}
|
|
188
187
|
// NOTE: Order of paths is unknown, so keep iterating.
|
|
189
188
|
}
|
|
189
|
+
// Only now set the Gurobi path. To force using a version < 12 (if installed),
|
|
190
|
+
// set before_12 to TRUE.
|
|
191
|
+
const
|
|
192
|
+
before_12 = false,
|
|
193
|
+
gsp_keys = Object.keys(gsp).sort();
|
|
194
|
+
while(gsp_keys.length) {
|
|
195
|
+
const
|
|
196
|
+
k = gsp_keys.pop(),
|
|
197
|
+
version = Math.trunc(parseInt(k) / 100);
|
|
198
|
+
if(version < 12 || !before_12) {
|
|
199
|
+
this.solver_list.gurobi = {name: 'Gurobi',
|
|
200
|
+
path: path.join(gsp[k], 'gurobi_cl')};
|
|
201
|
+
console.log('Path to Gurobi:', this.solver_list.gurobi.path);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
190
205
|
// For macOS, look in applications directory if not found in PATH.
|
|
191
206
|
if(!this.solver_list.gurobi && !windows) {
|
|
192
207
|
console.log('Looking for Gurobi in /usr/local/bin');
|
|
@@ -2701,6 +2701,8 @@ class LinnyRModel {
|
|
|
2701
2701
|
UNDO_STACK.addXML(a.asXML);
|
|
2702
2702
|
this.removeImport(a);
|
|
2703
2703
|
this.removeExport(a);
|
|
2704
|
+
// Ensure that chart variables referencing this actor are removed.
|
|
2705
|
+
this.removeActorChartVariables(a);
|
|
2704
2706
|
delete this.actors[k];
|
|
2705
2707
|
}
|
|
2706
2708
|
}
|
|
@@ -2823,6 +2825,49 @@ class LinnyRModel {
|
|
|
2823
2825
|
}
|
|
2824
2826
|
return xl;
|
|
2825
2827
|
}
|
|
2828
|
+
|
|
2829
|
+
updateChartVariables(e) {
|
|
2830
|
+
// Ensure that all chart variable names based on entity `e` will be
|
|
2831
|
+
// displayed correctly the next time they are drawn.
|
|
2832
|
+
console.log('HERE e', e.displayName);
|
|
2833
|
+
const sc = this.charts[CHART_MANAGER.chart_index];
|
|
2834
|
+
let ucm = false;
|
|
2835
|
+
for(const c of this.charts) {
|
|
2836
|
+
for(const v of c.variables) {
|
|
2837
|
+
console.log('HERE v', v.displayName);
|
|
2838
|
+
if(v.object === e) {
|
|
2839
|
+
v.display_name = '';
|
|
2840
|
+
console.log('HERE v new', v.displayName);
|
|
2841
|
+
ucm = ucm || c === sc;
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
if(ucm) CHART_MANAGER.updateDialog();
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
removeActorChartVariables(a) {
|
|
2849
|
+
// Ensure that all chart variable names based on entity `e` or actor `a`
|
|
2850
|
+
// will be displayed correctly the next time they are drawn.
|
|
2851
|
+
const sc = this.charts[CHART_MANAGER.chart_index];
|
|
2852
|
+
let ucm = false;
|
|
2853
|
+
for(const c of this.charts) {
|
|
2854
|
+
for(let vi = 0; vi < c.variables.length; vi++) {
|
|
2855
|
+
if(c.variables[vi].object === a) {
|
|
2856
|
+
c.variables.splice(vi, 1);
|
|
2857
|
+
if(c === sc) {
|
|
2858
|
+
ucm = true;
|
|
2859
|
+
if(CHART_MANAGER.variable_index === vi) {
|
|
2860
|
+
CHART_MANAGER.variable_index = -1;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
// Update stay-on-top dialogs (if needed).
|
|
2867
|
+
if(ucm) CHART_MANAGER.updateDialog();
|
|
2868
|
+
if(EXPERIMENT_MANAGER.selected_experiment) EXPERIMENT_MANAGER.updateDialog();
|
|
2869
|
+
FINDER.updateDialog();
|
|
2870
|
+
}
|
|
2826
2871
|
|
|
2827
2872
|
replaceEntityInExpressions(en1, en2, notify=true) {
|
|
2828
2873
|
// Replace entity name `en1` by `en2` in all variables in all expressions
|
|
@@ -2853,12 +2898,6 @@ class LinnyRModel {
|
|
|
2853
2898
|
pluralS(ioc.expression_count, 'expression');
|
|
2854
2899
|
if(notify) UI.notify('Renamed ' + replace_msg);
|
|
2855
2900
|
}
|
|
2856
|
-
// Clear display name cache of potentially affected chart variables.
|
|
2857
|
-
for(const c of this.charts) {
|
|
2858
|
-
for(const v of c.variables) {
|
|
2859
|
-
if(v.display_name.indexOf(en1) >= 0) v.display_name = '';
|
|
2860
|
-
}
|
|
2861
|
-
}
|
|
2862
2901
|
// Rename entities in parameters and outcomes of sensitivity analysis.
|
|
2863
2902
|
for(let i = 0; i < this.sensitivity_parameters.length; i++) {
|
|
2864
2903
|
const sp = this.sensitivity_parameters[i].split('|');
|
|
@@ -5181,6 +5220,8 @@ class Actor {
|
|
|
5181
5220
|
MODEL.actors[a.identifier] = this;
|
|
5182
5221
|
// Remove the old entry.
|
|
5183
5222
|
delete MODEL.actors[old_id];
|
|
5223
|
+
// Ensure that cached variable names are updated.
|
|
5224
|
+
MODEL.updateChartVariables(this);
|
|
5184
5225
|
MODEL.replaceEntityInExpressions(old_name, this.name);
|
|
5185
5226
|
MODEL.inferIgnoredEntities();
|
|
5186
5227
|
}
|
|
@@ -6042,6 +6083,8 @@ class NodeBox extends ObjectWithXYWH {
|
|
|
6042
6083
|
}
|
|
6043
6084
|
// Update actor list in case some actor name is no longer used.
|
|
6044
6085
|
MODEL.cleanUpActors();
|
|
6086
|
+
// Ensure that cached variable names are updated.
|
|
6087
|
+
MODEL.updateChartVariables(this);
|
|
6045
6088
|
// Update expression texts.
|
|
6046
6089
|
MODEL.replaceEntityInExpressions(old_name, this.displayName);
|
|
6047
6090
|
// NOTE: Renaming changes identifier that is used as index in
|
|
@@ -9747,6 +9790,8 @@ class Dataset {
|
|
|
9747
9790
|
this.name = name;
|
|
9748
9791
|
MODEL.datasets[new_id] = this;
|
|
9749
9792
|
if(old_id !== new_id) delete MODEL.datasets[old_id];
|
|
9793
|
+
// Ensure that cached variable names are updated.
|
|
9794
|
+
MODEL.updateChartVariables(this);
|
|
9750
9795
|
MODEL.replaceEntityInExpressions(old_name, name, notify);
|
|
9751
9796
|
return MODEL.datasets[new_id];
|
|
9752
9797
|
}
|
|
@@ -10698,7 +10743,7 @@ class Chart {
|
|
|
10698
10743
|
}
|
|
10699
10744
|
}
|
|
10700
10745
|
|
|
10701
|
-
// NOTE:
|
|
10746
|
+
// NOTE: Chart may display experiment run results, rather than MODEL results.
|
|
10702
10747
|
let runnrs = '';
|
|
10703
10748
|
const runs = EXPERIMENT_MANAGER.selectedRuns(this);
|
|
10704
10749
|
if(runs.length > 0) {
|
|
@@ -10739,6 +10784,8 @@ class Chart {
|
|
|
10739
10784
|
const rri = selx.resultIndex(v.displayName);
|
|
10740
10785
|
let bv;
|
|
10741
10786
|
if(rri >= 0) {
|
|
10787
|
+
// When run results are available, use their already
|
|
10788
|
+
// computed statisitics.
|
|
10742
10789
|
const
|
|
10743
10790
|
r = selx.runs[this.run_index],
|
|
10744
10791
|
rr = r.results[rri];
|
|
@@ -10767,7 +10814,9 @@ class Chart {
|
|
|
10767
10814
|
minv = Math.min(minv, bv);
|
|
10768
10815
|
maxv = Math.max(maxv, bv);
|
|
10769
10816
|
}
|
|
10817
|
+
// ... otherwise, do not add data for the bar chart.
|
|
10770
10818
|
} else {
|
|
10819
|
+
// Regular line chart => compute vector for this run.
|
|
10771
10820
|
v.computeVector();
|
|
10772
10821
|
minv = Math.min(minv, v.lowestValueInVector);
|
|
10773
10822
|
maxv = Math.max(maxv, v.highestValueInVector);
|
|
@@ -10776,6 +10825,7 @@ class Chart {
|
|
|
10776
10825
|
// Reset to prevent using experiment outcomes when this is not intended.
|
|
10777
10826
|
this.run_index = -1;
|
|
10778
10827
|
} else {
|
|
10828
|
+
// No experiment runs selected => compute vector as usual.
|
|
10779
10829
|
this.run_index = -1;
|
|
10780
10830
|
v.computeVector();
|
|
10781
10831
|
minv = Math.min(minv, v.lowestValueInVector);
|
|
@@ -10787,6 +10837,7 @@ class Chart {
|
|
|
10787
10837
|
// Now all vectors have been computed. If `display` is FALSE, this
|
|
10788
10838
|
// indicates that data is used only to save model results.
|
|
10789
10839
|
if(!display) return;
|
|
10840
|
+
|
|
10790
10841
|
// Define the bins when drawing as histogram.
|
|
10791
10842
|
if(this.histogram) {
|
|
10792
10843
|
this.value_range = maxv - minv;
|
|
@@ -2304,7 +2304,7 @@ class VirtualMachine {
|
|
|
2304
2304
|
// for the numerical stability. Meanwhile, the slack variables themselves
|
|
2305
2305
|
// will have values 1/sm times the actual slack, so this needs to be
|
|
2306
2306
|
// scaled back in solver messages.
|
|
2307
|
-
this.SLACK_MULTIPLIER =
|
|
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
|