linny-r 1.7.3 → 1.8.0
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/README.md +33 -22
- package/console.js +2 -2
- package/package.json +1 -1
- package/server.js +2 -2
- package/static/index.html +13 -5
- package/static/linny-r.css +21 -2
- package/static/scripts/linny-r-ctrl.js +1 -1
- package/static/scripts/linny-r-gui-constraint-editor.js +15 -2
- package/static/scripts/linny-r-gui-controller.js +45 -13
- package/static/scripts/linny-r-gui-equation-manager.js +6 -6
- package/static/scripts/linny-r-gui-expression-editor.js +6 -3
- package/static/scripts/linny-r-gui-finder.js +1 -1
- package/static/scripts/linny-r-gui-monitor.js +5 -5
- package/static/scripts/linny-r-gui-paper.js +13 -6
- package/static/scripts/linny-r-milp.js +306 -86
- package/static/scripts/linny-r-model.js +75 -22
- package/static/scripts/linny-r-vm.js +354 -127
@@ -742,6 +742,7 @@ class ExpressionParser {
|
|
742
742
|
// For debugging, TRACE can be used to log to the console for
|
743
743
|
// specific expressions and/or variables, for example:
|
744
744
|
// this.TRACE = name.endsWith('losses') || this.ownerName.endsWith('losses');
|
745
|
+
|
745
746
|
if(this.TRACE) console.log(
|
746
747
|
`TRACE: Parsing variable "${name}" in expression for`,
|
747
748
|
this.ownerName, ' --> ', this.expr);
|
@@ -2077,6 +2078,15 @@ class VirtualMachine {
|
|
2077
2078
|
// so far, type is always HI (highest increment); object can be
|
2078
2079
|
// a process or a product.
|
2079
2080
|
this.chunk_variables = [];
|
2081
|
+
// NOTE: As of version 1.8.0, diagnosis is performed only when the
|
2082
|
+
// modeler Alt-clicks the "run" button or clicks the link in the
|
2083
|
+
// infoline warning that is displayed when the solver reports that a
|
2084
|
+
// block poses a problem that is infeasible (too tight constraints)
|
2085
|
+
// or unbounded (no upper limit on some processes). Diagnosis is
|
2086
|
+
// implemented by adding slack and setting finite bounds on processes
|
2087
|
+
// and then make a second attempt to solve the block.
|
2088
|
+
this.diagnose = false;
|
2089
|
+
this.prompt_to_diagnose = false;
|
2080
2090
|
// Array for VM instructions.
|
2081
2091
|
this.code = [];
|
2082
2092
|
// The Simplex tableau: matrix, rhs and ct will have same length.
|
@@ -2111,17 +2121,23 @@ class VirtualMachine {
|
|
2111
2121
|
// Floating-point constants used in calculations
|
2112
2122
|
// Meaningful solver results are assumed to lie wihin reasonable bounds.
|
2113
2123
|
// Extreme absolute values (10^25 and above) are used to signal particular
|
2114
|
-
// outcomes. This 10^25 limit is used because the
|
2115
|
-
// LP_solve considers a problem to be unbounded if
|
2116
|
-
// reach +INF (1e+30) or -INF (-1e+30), and a solution
|
2117
|
-
// extreme values get too close to +/-INF. The higher
|
2118
|
-
// chosen arbitrarily.
|
2124
|
+
// outcomes. This 10^25 limit is used because the original MILP solver
|
2125
|
+
// used by Linny-R (LP_solve) considers a problem to be unbounded if
|
2126
|
+
// decision variables reach +INF (1e+30) or -INF (-1e+30), and a solution
|
2127
|
+
// inaccurate if extreme values get too close to +/-INF. The higher
|
2128
|
+
// values have been chosen arbitrarily.
|
2119
2129
|
this.PLUS_INFINITY = 1e+25;
|
2120
2130
|
this.MINUS_INFINITY = -1e+25;
|
2121
2131
|
this.BEYOND_PLUS_INFINITY = 1e+35;
|
2122
2132
|
this.BEYOND_MINUS_INFINITY = -1e+35;
|
2133
|
+
// The 1e+30 value is recognized by all supported solvers as "infinity",
|
2134
|
+
// and hence can be used to indicate that a variable has no upper bound.
|
2123
2135
|
this.SOLVER_PLUS_INFINITY = 1e+30;
|
2124
2136
|
this.SOLVER_MINUS_INFINITY = -1e+30;
|
2137
|
+
// As of version 1.8.0, Linny-R imposes no +INF bounds on processes
|
2138
|
+
// unless diagnosing an unbounded problem. For such diagnosis, the
|
2139
|
+
// (relatively) low value 9.99999999e+9 is used.
|
2140
|
+
this.DIAGNOSIS_UPPER_BOUND = 9.99999999e+9;
|
2125
2141
|
// NOTE: Below the "near zero" limit, a number is considered zero
|
2126
2142
|
// (this is to timely detect division-by-zero errors).
|
2127
2143
|
this.NEAR_ZERO = 1e-10;
|
@@ -2334,11 +2350,33 @@ class VirtualMachine {
|
|
2334
2350
|
['LAST', 'MAX', 'MEAN', 'MIN', 'N', 'NZ', 'SD', 'SUM', 'VAR'];
|
2335
2351
|
this.solver_names = {
|
2336
2352
|
gurobi: 'Gurobi',
|
2353
|
+
mosek: 'MOSEK',
|
2337
2354
|
cplex: 'CPLEX',
|
2338
2355
|
scip: 'SCIP',
|
2339
2356
|
lp_solve: 'LP_solve'
|
2340
2357
|
};
|
2341
2358
|
}
|
2359
|
+
|
2360
|
+
selectSolver(id) {
|
2361
|
+
if(id in this.solver_names) {
|
2362
|
+
this.solver_id = id;
|
2363
|
+
} else {
|
2364
|
+
UI.alert(`Invalid solver ID "${id}"`);
|
2365
|
+
}
|
2366
|
+
}
|
2367
|
+
|
2368
|
+
get noSemiContinuous() {
|
2369
|
+
// Return TRUE if the selected solver does NOT support semi-continuous
|
2370
|
+
// variables (used to implement "shut down when lower bound constraints"
|
2371
|
+
// for processes).
|
2372
|
+
return this.solver_id === 'mosek';
|
2373
|
+
}
|
2374
|
+
|
2375
|
+
get noSupportForSOS() {
|
2376
|
+
// Return TRUE if the selected solver does NOT support special
|
2377
|
+
// ordered sets (SOS).
|
2378
|
+
return this.solver_id === 'mosek';
|
2379
|
+
}
|
2342
2380
|
|
2343
2381
|
reset() {
|
2344
2382
|
// Reset the virtual machine so that it can execute the model again.
|
@@ -2870,7 +2908,7 @@ class VirtualMachine {
|
|
2870
2908
|
}
|
2871
2909
|
if(type === 'I' || type === 'PiL') {
|
2872
2910
|
this.int_var_indices[index] = true;
|
2873
|
-
} else if('OO|IZ|SU|SD|SO|FC'.indexOf(type) >= 0) {
|
2911
|
+
} else if('OO|IZ|SU|SD|SO|FC|SB'.indexOf(type) >= 0) {
|
2874
2912
|
this.bin_var_indices[index] = true;
|
2875
2913
|
}
|
2876
2914
|
if(obj instanceof Process && obj.pace > 1) {
|
@@ -2884,7 +2922,20 @@ class VirtualMachine {
|
|
2884
2922
|
for(let i = 2; i <= n; i++) {
|
2885
2923
|
this.variables.push(['W' + i, obj]);
|
2886
2924
|
}
|
2887
|
-
|
2925
|
+
// NOTE: Some solvers do not support SOS. To ensure that only 2
|
2926
|
+
// adjacent w[i]-variables are non-zero (they range from 0 to 1),
|
2927
|
+
// as many binary variables b[i] must be defined, and additional
|
2928
|
+
// constraints must be added (see function VMI_add_boundline).
|
2929
|
+
// NOTE: These additional variables and constraints are not needed
|
2930
|
+
// when a bound line defines a convex feasible area.
|
2931
|
+
const sos_with_bin = this.noSupportForSOS && !obj.needsNoSOS;
|
2932
|
+
this.sos_var_indices.push([index, n, sos_with_bin]);
|
2933
|
+
if(sos_with_bin) {
|
2934
|
+
for(let i = 1; i <= n; i++) {
|
2935
|
+
const bi = this.variables.push(['b' + i, obj]);
|
2936
|
+
this.bin_var_indices[bi] = true;
|
2937
|
+
}
|
2938
|
+
}
|
2888
2939
|
}
|
2889
2940
|
return index;
|
2890
2941
|
}
|
@@ -2903,6 +2954,8 @@ class VirtualMachine {
|
|
2903
2954
|
if(p instanceof Product) {
|
2904
2955
|
p.stock_LE_slack_var_index = -1;
|
2905
2956
|
p.stock_GE_slack_var_index = -1;
|
2957
|
+
} else {
|
2958
|
+
p.semic_var_index = -1;
|
2906
2959
|
}
|
2907
2960
|
}
|
2908
2961
|
|
@@ -2913,6 +2966,11 @@ class VirtualMachine {
|
|
2913
2966
|
// storage capacity, because it simplifies the formulation of
|
2914
2967
|
// product-related (data) constraints.
|
2915
2968
|
p.level_var_index = this.addVariable(p.integer_level ? 'PiL': 'PL', p);
|
2969
|
+
if(p.level_to_zero && this.noSemiContinuous) {
|
2970
|
+
// When the selected solver does not support semi-continous variables,
|
2971
|
+
// they must be implemented with an additional binary variable.
|
2972
|
+
p.semic_var_index = this.addVariable('SB', p);
|
2973
|
+
}
|
2916
2974
|
// Some "data-only" link multipliers require additional variables.
|
2917
2975
|
if(p.needsOnOffData) {
|
2918
2976
|
p.on_off_var_index = this.addVariable('OO', p);
|
@@ -2942,7 +3000,7 @@ class VirtualMachine {
|
|
2942
3000
|
// to respect certain constraints. This may result in infeasible
|
2943
3001
|
// MILP problems. The solver will report this, but provide no
|
2944
3002
|
// clue as to which constraints may be critical.
|
2945
|
-
if(p instanceof Product && !p.no_slack) {
|
3003
|
+
if(p instanceof Product && this.diagnose && !p.no_slack) {
|
2946
3004
|
p.stock_LE_slack_var_index = this.addVariable('LE', p);
|
2947
3005
|
p.stock_GE_slack_var_index = this.addVariable('GE', p);
|
2948
3006
|
}
|
@@ -3000,7 +3058,7 @@ class VirtualMachine {
|
|
3000
3058
|
for(let i = 0; i < this.chunk_variables.length; i++) {
|
3001
3059
|
const
|
3002
3060
|
obj = this.chunk_variables[i][1],
|
3003
|
-
// NOTE:
|
3061
|
+
// NOTE: Chunk offset takes into account that variable
|
3004
3062
|
// indices are 0-based.
|
3005
3063
|
cvi = chof + i;
|
3006
3064
|
let v = 'X' + cvi.toString().padStart(z, '0');
|
@@ -3040,7 +3098,7 @@ class VirtualMachine {
|
|
3040
3098
|
l = p.lower_bound;
|
3041
3099
|
}
|
3042
3100
|
}
|
3043
|
-
// Likewise get the upper bound
|
3101
|
+
// Likewise get the upper bound.
|
3044
3102
|
if(p.equal_bounds && p.lower_bound.defined) {
|
3045
3103
|
u = l;
|
3046
3104
|
} else if(p.upper_bound.defined) {
|
@@ -3052,47 +3110,47 @@ class VirtualMachine {
|
|
3052
3110
|
}
|
3053
3111
|
}
|
3054
3112
|
} else {
|
3055
|
-
// Implicit bounds: if not a source, then LB is set to 0
|
3113
|
+
// Implicit bounds: if not a source, then LB is set to 0.
|
3056
3114
|
if(notsrc) l = 0;
|
3057
|
-
// If not a sink, UB is set to 0
|
3115
|
+
// If not a sink, UB is set to 0.
|
3058
3116
|
if(notsnk) u = 0;
|
3059
3117
|
}
|
3060
3118
|
|
3061
|
-
// NOTE:
|
3119
|
+
// NOTE: Stock constraints must take into account extra inflows
|
3062
3120
|
// (source) or outflows (sink).
|
3063
3121
|
// Check for special case of equal bounds, as then one EQ constraint
|
3064
3122
|
// suffices. This applies if P is a constant ...
|
3065
3123
|
if(p.isConstant) {
|
3066
|
-
// NOTE:
|
3067
|
-
//
|
3124
|
+
// NOTE: No slack on constants. Use the lower bound (number or
|
3125
|
+
// expression) as RHS.
|
3068
3126
|
this.code.push(
|
3069
3127
|
[l instanceof Expression ? VMI_set_var_rhs : VMI_set_const_rhs, l],
|
3070
3128
|
[VMI_add_constraint, VM.EQ]
|
3071
3129
|
);
|
3072
|
-
// ... or if P is neither source nor sink
|
3130
|
+
// ... or if P is neither source nor sink.
|
3073
3131
|
} else if(p.equal_bounds && notsrc && notsnk) {
|
3074
|
-
if(!p.no_slack) {
|
3075
|
-
// NOTE:
|
3076
|
-
//
|
3132
|
+
if(this.diagnose && !p.no_slack) {
|
3133
|
+
// NOTE: For EQ, both slack variables should be used, having
|
3134
|
+
// respectively -1 and +1 as coefficients.
|
3077
3135
|
this.code.push(
|
3078
3136
|
[VMI_add_const_to_coefficient, [lesvi, -1]],
|
3079
3137
|
[VMI_add_const_to_coefficient, [gesvi, 1]]
|
3080
3138
|
);
|
3081
3139
|
}
|
3082
|
-
// Use the lower bound (number or expression) as RHS
|
3140
|
+
// Use the lower bound (number or expression) as RHS.
|
3083
3141
|
this.code.push(
|
3084
3142
|
[l instanceof Expression ? VMI_set_var_rhs : VMI_set_const_rhs, l],
|
3085
3143
|
[VMI_add_constraint, VM.EQ]
|
3086
3144
|
);
|
3087
3145
|
} else {
|
3088
|
-
// Add lower bound (GE) constraint unless product is a source node
|
3146
|
+
// Add lower bound (GE) constraint unless product is a source node.
|
3089
3147
|
if(notsrc) {
|
3090
|
-
if(!p.no_slack) {
|
3091
|
-
// Add the GE slack index with coefficient +1
|
3092
|
-
//
|
3148
|
+
if(this.diagnose && !p.no_slack) {
|
3149
|
+
// Add the GE slack index with coefficient +1 (so it can
|
3150
|
+
// INcrease the left-hand side of the equation)
|
3093
3151
|
this.code.push([VMI_add_const_to_coefficient, [gesvi, 1]]);
|
3094
3152
|
}
|
3095
|
-
// Use the lower bound (number or expression) as RHS
|
3153
|
+
// Use the lower bound (number or expression) as RHS.
|
3096
3154
|
this.code.push(
|
3097
3155
|
[l instanceof Expression? VMI_set_var_rhs : VMI_set_const_rhs, l],
|
3098
3156
|
[VMI_add_constraint, VM.GE]
|
@@ -3100,12 +3158,12 @@ class VirtualMachine {
|
|
3100
3158
|
}
|
3101
3159
|
// Add upper bound (LE) constraint unless product is a sink node
|
3102
3160
|
if(notsnk) {
|
3103
|
-
if(!p.no_slack) {
|
3104
|
-
// Add the stock LE index with coefficient -1
|
3105
|
-
//
|
3161
|
+
if(this.diagnose && !p.no_slack) {
|
3162
|
+
// Add the stock LE index with coefficient -1 (so it can
|
3163
|
+
// DEcrease the LHS).
|
3106
3164
|
this.code.push([VMI_add_const_to_coefficient, [lesvi, -1]]);
|
3107
3165
|
}
|
3108
|
-
// Use the upper bound (number or expression) as RHS
|
3166
|
+
// Use the upper bound (number or expression) as RHS.
|
3109
3167
|
this.code.push(
|
3110
3168
|
[u instanceof Expression ? VMI_set_var_rhs : VMI_set_const_rhs, u],
|
3111
3169
|
[VMI_add_constraint, VM.LE]
|
@@ -3119,7 +3177,7 @@ class VirtualMachine {
|
|
3119
3177
|
// Linny-R! It sets up the VM variable list, and then generates VM code
|
3120
3178
|
// that that, when executed, creates the MILP tableau for a chunk.
|
3121
3179
|
let i, j, k, l, vi, p, c, lbx, ubx;
|
3122
|
-
// Reset variable arrays and code array
|
3180
|
+
// Reset variable arrays and code array.
|
3123
3181
|
this.variables.length = 0;
|
3124
3182
|
this.chunk_variables.length = 0;
|
3125
3183
|
this.int_var_indices = [];
|
@@ -3130,12 +3188,20 @@ class VirtualMachine {
|
|
3130
3188
|
this.sos_var_indices = [];
|
3131
3189
|
this.slack_variables = [[], [], []];
|
3132
3190
|
this.code.length = 0;
|
3133
|
-
// Initialize fixed variable array: 1 list per round
|
3191
|
+
// Initialize fixed variable array: 1 list per round.
|
3134
3192
|
for(i = 0; i < MODEL.rounds; i++) {
|
3135
3193
|
this.fixed_var_indices.push([]);
|
3136
3194
|
}
|
3137
3195
|
|
3138
|
-
//
|
3196
|
+
// Log if run is performed in "diagnosis" mode.
|
3197
|
+
if(this.diagnose) {
|
3198
|
+
this.logMessage(this.block_count, 'DIAGNOSTIC RUN' +
|
3199
|
+
(MODEL.always_diagnose ? ' (default -- see model settings)': '') +
|
3200
|
+
'\n- slack variables on products and constraints' +
|
3201
|
+
'\n- finite bounds on all processes');
|
3202
|
+
}
|
3203
|
+
|
3204
|
+
// Just in case: re-determine which entities can be ignored.
|
3139
3205
|
MODEL.inferIgnoredEntities();
|
3140
3206
|
const n = Object.keys(MODEL.ignored_entities).length;
|
3141
3207
|
if(n > 0) {
|
@@ -3143,7 +3209,8 @@ class VirtualMachine {
|
|
3143
3209
|
pluralS(n, 'entity', 'entities') + ' will be ignored');
|
3144
3210
|
}
|
3145
3211
|
|
3146
|
-
// FIRST:
|
3212
|
+
// FIRST: Define indices for all variables (index = Simplex tableau
|
3213
|
+
// column number).
|
3147
3214
|
|
3148
3215
|
// Each actor has a variable to compute its cash in and its cash out.
|
3149
3216
|
const actor_keys = Object.keys(MODEL.actors).sort();
|
@@ -3184,8 +3251,8 @@ class VirtualMachine {
|
|
3184
3251
|
// The slack variables prevent that the solver will consider an
|
3185
3252
|
// overconstrained model "infeasible". EQ bound lines have 2 slack
|
3186
3253
|
// variables, LE and GE bound lines need only 1.
|
3187
|
-
// NOTE:
|
3188
|
-
// of the constraint is set
|
3254
|
+
// NOTE: Slack variables are omitted when the "no slack" property
|
3255
|
+
// of the constraint is set.
|
3189
3256
|
const constraint_keys = Object.keys(MODEL.constraints).sort();
|
3190
3257
|
for(i = 0; i < constraint_keys.length; i++) {
|
3191
3258
|
k = constraint_keys[i];
|
@@ -3195,11 +3262,13 @@ class VirtualMachine {
|
|
3195
3262
|
const bl = c.bound_lines[l];
|
3196
3263
|
bl.sos_var_indices = [];
|
3197
3264
|
if(bl.isActive && bl.constrainsY) {
|
3198
|
-
// Define SOS2 variables w[i]
|
3199
|
-
//
|
3265
|
+
// Define SOS2 variables w[i] (plus associated binaries if
|
3266
|
+
// solver does not support special ordered sets).
|
3267
|
+
// NOTE: `addVariable` will add as many as there are points!
|
3200
3268
|
bl.first_sos_var_index = this.addVariable('W1', bl);
|
3201
|
-
if(!c.no_slack) {
|
3202
|
-
// Define the slack variable(s) for bound line constraints
|
3269
|
+
if(this.diagnose && !c.no_slack) {
|
3270
|
+
// Define the slack variable(s) for bound line constraints.
|
3271
|
+
// NOTE: Category [2] means: highest slack penalty.
|
3203
3272
|
if(bl.type !== VM.GE) {
|
3204
3273
|
bl.LE_slack_var_index = this.addVariable('CLE', bl);
|
3205
3274
|
this.slack_variables[2].push(bl.LE_slack_var_index);
|
@@ -3286,6 +3355,10 @@ class VirtualMachine {
|
|
3286
3355
|
ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
|
3287
3356
|
if(lbx.isStatic) lbx = lbx.result(0);
|
3288
3357
|
if(ubx.isStatic) ubx = ubx.result(0);
|
3358
|
+
// NOTE: When semic_var_index is set, the lower bound must be
|
3359
|
+
// zero, as the semi-continuous lower bound is implemented with
|
3360
|
+
// a binary variable.
|
3361
|
+
if(p.semic_var_index >= 0) lbx = 0;
|
3289
3362
|
// NOTE: Pass TRUE as fourth parameter to indicate that +INF
|
3290
3363
|
// and -INF can be coded as the infinity values used by the
|
3291
3364
|
// solver, rather than the Linny-R values used to detect
|
@@ -3307,34 +3380,34 @@ class VirtualMachine {
|
|
3307
3380
|
}
|
3308
3381
|
}
|
3309
3382
|
|
3310
|
-
// NEXT: Define the bounds for all stock level variables
|
3383
|
+
// NEXT: Define the bounds for all stock level variables.
|
3311
3384
|
for(i = 0; i < product_keys.length; i++) {
|
3312
3385
|
k = product_keys[i];
|
3313
3386
|
if(!MODEL.ignored_entities[k]) {
|
3314
3387
|
p = MODEL.products[k];
|
3315
|
-
// Get index of variable that is constrained by LB and UB
|
3388
|
+
// Get index of variable that is constrained by LB and UB.
|
3316
3389
|
vi = p.level_var_index;
|
3317
|
-
if(p.no_slack) {
|
3390
|
+
if(p.no_slack || !this.diagnose) {
|
3318
3391
|
// If no slack, the bound constraints can be set on the
|
3319
|
-
// variables themselves
|
3392
|
+
// variables themselves.
|
3320
3393
|
lbx = p.lower_bound;
|
3321
|
-
// NOTE:
|
3394
|
+
// NOTE: If UB = LB, set UB to LB only if LB is defined,
|
3322
3395
|
// because LB expressions default to -INF while UB expressions
|
3323
|
-
// default to + INF
|
3396
|
+
// default to + INF.
|
3324
3397
|
ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
|
3325
3398
|
if(lbx.isStatic) lbx = lbx.result(0);
|
3326
3399
|
if(ubx.isStatic) ubx = ubx.result(0);
|
3327
3400
|
this.code.push([VMI_set_bounds, [vi, lbx, ubx]]);
|
3328
3401
|
} else {
|
3329
3402
|
// Otherwise, set bounds of stock variable to -INF and +INF,
|
3330
|
-
// as product constraints will be added later on
|
3403
|
+
// as product constraints will be added later on.
|
3331
3404
|
this.code.push([VMI_set_bounds,
|
3332
3405
|
[vi, VM.MINUS_INFINITY, VM.PLUS_INFINITY]]);
|
3333
3406
|
}
|
3334
3407
|
}
|
3335
3408
|
}
|
3336
3409
|
|
3337
|
-
// NEXT: Define objective function that maximizes total cash flow
|
3410
|
+
// NEXT: Define objective function that maximizes total cash flow.
|
3338
3411
|
|
3339
3412
|
// NOTE: As of 19 October 2020, the objective function is *explicitly*
|
3340
3413
|
// calculated as the (weighted) sum of the cash flows of actors
|
@@ -3580,27 +3653,27 @@ class VirtualMachine {
|
|
3580
3653
|
}
|
3581
3654
|
}
|
3582
3655
|
|
3583
|
-
// Copy the VM coefficient vector to the objective function coefficients
|
3656
|
+
// Copy the VM coefficient vector to the objective function coefficients.
|
3584
3657
|
// NOTE: for the VM's current time step (VM.t)!
|
3585
3658
|
this.code.push([VMI_set_objective, null]);
|
3586
3659
|
|
3587
3660
|
// NOTES:
|
3588
|
-
// (1) Scaling of the objective function coefficients is performed by
|
3589
|
-
// VM just before the tableau is submitted to the solver, so
|
3590
|
-
// suffices to differentiate between the different
|
3591
|
-
// variables
|
3661
|
+
// (1) Scaling of the objective function coefficients is performed by
|
3662
|
+
// the VM just before the tableau is submitted to the solver, so
|
3663
|
+
// for now it suffices to differentiate between the different
|
3664
|
+
// "priorities" of slack variables.
|
3592
3665
|
// (2) Slack variables have different penalties: type 0 = market demands,
|
3593
3666
|
// i.e., EQ constraints on stocks, 1 = GE and LE constraints on product
|
3594
|
-
// levels, 2 = strongest constraints: on data, or set by boundlines
|
3667
|
+
// levels, 2 = strongest constraints: on data, or set by boundlines.
|
3595
3668
|
let pen, hb;
|
3596
3669
|
for(i = 0; i < product_keys.length; i++) {
|
3597
3670
|
k = product_keys[i];
|
3598
3671
|
if(!MODEL.ignored_entities[k]) {
|
3599
3672
|
p = MODEL.products[k];
|
3600
|
-
if(p.level_var_index >= 0 && !p.no_slack) {
|
3673
|
+
if(p.level_var_index >= 0 && !p.no_slack && this.diagnose) {
|
3601
3674
|
hb = p.hasBounds;
|
3602
3675
|
pen = (p.is_data ? 2 :
|
3603
|
-
// NOTE:
|
3676
|
+
// NOTE: Lowest penalty also for IMPLIED sources and sinks.
|
3604
3677
|
(p.equal_bounds || (!hb && (p.isSourceNode || p.isSinkNode)) ? 0 :
|
3605
3678
|
(hb ? 1 : 2)));
|
3606
3679
|
this.slack_variables[pen].push(
|
@@ -3609,7 +3682,48 @@ class VirtualMachine {
|
|
3609
3682
|
}
|
3610
3683
|
}
|
3611
3684
|
|
3612
|
-
// NEXT:
|
3685
|
+
// NEXT: Add semi-continuous constraints only if not supported by solver.
|
3686
|
+
if(!this.noSemiContinuous) {
|
3687
|
+
for(i = 0; i < process_keys.length; i++) {
|
3688
|
+
k = process_keys[i];
|
3689
|
+
if(!MODEL.ignored_entities[k]) {
|
3690
|
+
p = MODEL.processes[k];
|
3691
|
+
const svi = p.semic_var_index;
|
3692
|
+
if(svi >= 0) {
|
3693
|
+
const
|
3694
|
+
vi = p.level_var_index,
|
3695
|
+
lbx = p.lower_bound,
|
3696
|
+
ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
|
3697
|
+
// LB*binary - level <= 0
|
3698
|
+
this.code.push(
|
3699
|
+
[VMI_clear_coefficients, null],
|
3700
|
+
[VMI_add_const_to_coefficient, [vi, -1]]
|
3701
|
+
);
|
3702
|
+
if(lbx.isStatic) {
|
3703
|
+
this.code.push([VMI_add_const_to_coefficient,
|
3704
|
+
[svi, lbx.result(0)]]);
|
3705
|
+
} else {
|
3706
|
+
this.code.push([VMI_add_var_to_coefficient, [svi, lbx]]);
|
3707
|
+
}
|
3708
|
+
this.code.push([VMI_add_constraint, VM.LE]);
|
3709
|
+
// level - UB*binary <= 0
|
3710
|
+
this.code.push(
|
3711
|
+
[VMI_clear_coefficients, null],
|
3712
|
+
[VMI_add_const_to_coefficient, [vi, 1]]
|
3713
|
+
);
|
3714
|
+
if(ubx.isStatic) {
|
3715
|
+
this.code.push([VMI_subtract_const_from_coefficient,
|
3716
|
+
[svi, ubx.result(0)]]);
|
3717
|
+
} else {
|
3718
|
+
this.code.push([VMI_subtract_var_from_coefficient, [svi, ubx]]);
|
3719
|
+
}
|
3720
|
+
this.code.push([VMI_add_constraint, VM.LE]);
|
3721
|
+
}
|
3722
|
+
}
|
3723
|
+
}
|
3724
|
+
}
|
3725
|
+
|
3726
|
+
// NEXT: Add product constraints to calculate (and constrain) their stock.
|
3613
3727
|
|
3614
3728
|
for(let pi = 0; pi < product_keys.length; pi++) {
|
3615
3729
|
k = product_keys[pi];
|
@@ -4187,30 +4301,35 @@ class VirtualMachine {
|
|
4187
4301
|
}
|
4188
4302
|
}
|
4189
4303
|
|
4190
|
-
// NEXT:
|
4191
|
-
// NOTE:
|
4304
|
+
// NEXT: Add constraints.
|
4305
|
+
// NOTE: As of version 1.0.10, constraints are implemented using special
|
4192
4306
|
// ordered sets (SOS2). This is effectuated with a dedicated VM instruction
|
4193
4307
|
// for each of its "active" bound lines. This instruction requires these
|
4194
4308
|
// parameters:
|
4195
4309
|
// - variable indices for the constraining node X, the constrained node Y
|
4196
4310
|
// - expressions for the LB and UB of X and Y
|
4197
4311
|
// - the bound line object, as this provides all further information
|
4312
|
+
// NOTE: For efficiency, the useBinaries flag is also passed, as it can
|
4313
|
+
// be determined at compile time whether a SOS constraint is needed
|
4314
|
+
// (bound lines are static), and whether the solver does not support
|
4315
|
+
// SOS, in which case binary variables must be used.
|
4198
4316
|
for(i = 0; i < constraint_keys.length; i++) {
|
4199
4317
|
k = constraint_keys[i];
|
4200
4318
|
if(!MODEL.ignored_entities[k]) {
|
4201
4319
|
c = MODEL.constraints[k];
|
4202
|
-
// Get the two associated nodes
|
4320
|
+
// Get the two associated nodes.
|
4203
4321
|
const
|
4204
4322
|
x = c.from_node,
|
4205
4323
|
y = c.to_node;
|
4206
4324
|
for(j = 0; j < c.bound_lines.length; j++) {
|
4207
4325
|
const bl = c.bound_lines[j];
|
4208
4326
|
// Only add constrains for bound lines that are "active" for the
|
4209
|
-
// current run, and do constrain Y in some way
|
4327
|
+
// current run, and do constrain Y in some way.
|
4210
4328
|
if(bl.isActive && bl.constrainsY) {
|
4211
4329
|
this.code.push([VMI_add_bound_line_constraint,
|
4212
4330
|
[x.level_var_index, x.lower_bound, x.upper_bound,
|
4213
|
-
y.level_var_index, y.lower_bound, y.upper_bound,
|
4331
|
+
y.level_var_index, y.lower_bound, y.upper_bound,
|
4332
|
+
bl, this.noSupportForSOS && !bl.needsNoSOS]]);
|
4214
4333
|
}
|
4215
4334
|
}
|
4216
4335
|
}
|
@@ -4498,7 +4617,8 @@ class VirtualMachine {
|
|
4498
4617
|
// Compute the peak from the peak increase.
|
4499
4618
|
p.b_peak[block] = p.b_peak[block - 1] + p.b_peak_inc[block];
|
4500
4619
|
}
|
4501
|
-
// Add warning to messages if slack has been used
|
4620
|
+
// Add warning to messages if slack has been used, or some process
|
4621
|
+
// level is "infinite" while diagnosing an unbounded problem.
|
4502
4622
|
// NOTE: Only check after the last round has been evaluated.
|
4503
4623
|
if(round === this.lastRound) {
|
4504
4624
|
let b = bb;
|
@@ -4539,6 +4659,27 @@ class VirtualMachine {
|
|
4539
4659
|
}
|
4540
4660
|
}
|
4541
4661
|
}
|
4662
|
+
if(this.diagnose) {
|
4663
|
+
// Iterate over all processes, and set the "slack use" flag
|
4664
|
+
// for their cluster so that these clusters will be highlighted.
|
4665
|
+
for(let o in MODEL.processes) if(MODEL.processes.hasOwnProperty(o) &&
|
4666
|
+
!MODEL.ignored_entities[o]) {
|
4667
|
+
const
|
4668
|
+
p = MODEL.processes[o],
|
4669
|
+
l = p.level[b];
|
4670
|
+
if(l >= VM.PLUS_INFINITY) {
|
4671
|
+
this.logMessage(block,
|
4672
|
+
`${this.WARNING}(t=${b}${round}) ${p.displayName} has level +INF`);
|
4673
|
+
// NOTE: +INF is signalled in blue, just like use of LE slack.
|
4674
|
+
p.cluster.usesSlack(b, p, 'LE');
|
4675
|
+
} else if(l <= VM.MINUS_INFINITY) {
|
4676
|
+
this.logMessage(block,
|
4677
|
+
`${this.WARNING}(t=${b}${round}) ${p.displayName} has level -INF`);
|
4678
|
+
// NOTE: -INF is signalled in red, just like use of GE slack.
|
4679
|
+
p.cluster.usesSlack(b, p, 'GE');
|
4680
|
+
}
|
4681
|
+
}
|
4682
|
+
}
|
4542
4683
|
j += this.cols;
|
4543
4684
|
b++;
|
4544
4685
|
}
|
@@ -5049,24 +5190,28 @@ class VirtualMachine {
|
|
5049
5190
|
this.is_binary[parseInt(i) + j*this.cols] = true;
|
5050
5191
|
}
|
5051
5192
|
}
|
5052
|
-
// Set list with indices of semi-
|
5193
|
+
// Set list with indices of semi-continuous variables.
|
5053
5194
|
this.is_semi_continuous = {};
|
5054
|
-
|
5055
|
-
|
5056
|
-
|
5195
|
+
// NOTE: Solver may not support semi-continuous variables.
|
5196
|
+
if(!this.noSemiContinuous) {
|
5197
|
+
for(let i in this.sec_var_indices) if(Number(i)) {
|
5198
|
+
for(let j = 0; j < abl; j++) {
|
5199
|
+
this.is_semi_continuous[parseInt(i) + j*this.cols] = true;
|
5200
|
+
}
|
5057
5201
|
}
|
5058
5202
|
}
|
5059
|
-
// Initialize the "add constraints flag" to TRUE
|
5060
|
-
// NOTE:
|
5203
|
+
// Initialize the "add constraints flag" to TRUE.
|
5204
|
+
// NOTE: This flag can be set/unset dynamically by VM instructions.
|
5061
5205
|
this.add_constraints_flag = true;
|
5062
|
-
// Execute code for each time step in this block
|
5206
|
+
// Execute code for each time step in this block.
|
5063
5207
|
this.logTrace('START executing block code (' +
|
5064
5208
|
pluralS(this.code.length, ' instruction)'));
|
5065
|
-
// NOTE: `t` is the VM's "time tick", which is "relative time" compared
|
5066
|
-
// the "absolute time" of the simulated period. VM.t always starts
|
5067
|
-
// which corresponds to MODEL.start_period
|
5209
|
+
// NOTE: `t` is the VM's "time tick", which is "relative time" compared
|
5210
|
+
// to the "absolute time" of the simulated period. VM.t always starts
|
5211
|
+
// at 1, which corresponds to MODEL.start_period.
|
5068
5212
|
this.t = (this.block_count - 1) * MODEL.block_length + 1;
|
5069
|
-
// Show this relative (!) time step on the status bar as progress
|
5213
|
+
// Show this relative (!) time step on the status bar as progress
|
5214
|
+
// indicator.
|
5070
5215
|
UI.updateTimeStep(this.t);
|
5071
5216
|
setTimeout((t, n) => VM.addTableauSegment(t, n), 0, 0, abl);
|
5072
5217
|
}
|
@@ -5120,10 +5265,10 @@ class VirtualMachine {
|
|
5120
5265
|
VMI_add_constraint(VM.EQ);
|
5121
5266
|
}
|
5122
5267
|
}
|
5123
|
-
// Proceed to the next time tick
|
5268
|
+
// Proceed to the next time tick.
|
5124
5269
|
this.t++;
|
5125
5270
|
// This also means advancing the offset, because all VM instructions
|
5126
|
-
// pass variable indices relative to the first column in the tableau
|
5271
|
+
// pass variable indices relative to the first column in the tableau.
|
5127
5272
|
this.offset += this.cols;
|
5128
5273
|
}
|
5129
5274
|
if(next_start < abl) {
|
@@ -5136,7 +5281,7 @@ class VirtualMachine {
|
|
5136
5281
|
|
5137
5282
|
finishBlockSetup(abl) {
|
5138
5283
|
// Scale the coefficients of the objective function, and calculate
|
5139
|
-
// the "base" slack penalty
|
5284
|
+
// the "base" slack penalty.
|
5140
5285
|
this.scaleObjective();
|
5141
5286
|
this.scaleCashFlowConstraints();
|
5142
5287
|
// Add (appropriately scaled!) slack penalties to the objective function
|
@@ -5235,7 +5380,7 @@ class VirtualMachine {
|
|
5235
5380
|
}
|
5236
5381
|
}
|
5237
5382
|
|
5238
|
-
writeLpFormat(cplex=false) {
|
5383
|
+
writeLpFormat(cplex=false, named_constraints=false) {
|
5239
5384
|
// NOTE: Up to version 1.5.6, actual block length of last block used
|
5240
5385
|
// to be shorter than the chunk length so as not to go beyond the
|
5241
5386
|
// simulation end time. The look-ahead is now *always* part of the
|
@@ -5301,6 +5446,7 @@ class VirtualMachine {
|
|
5301
5446
|
n = this.matrix.length;
|
5302
5447
|
for(let r = 0; r < n; r++) {
|
5303
5448
|
const row = this.matrix[r];
|
5449
|
+
if(named_constraints) line = `C${r + 1}: `;
|
5304
5450
|
for(p in row) if (row.hasOwnProperty(p)) {
|
5305
5451
|
c = row[p];
|
5306
5452
|
if (c < VM.SOLVER_MINUS_INFINITY || c > VM.SOLVER_PLUS_INFINITY) {
|
@@ -5419,7 +5565,8 @@ class VirtualMachine {
|
|
5419
5565
|
line = '';
|
5420
5566
|
scv = 0;
|
5421
5567
|
}
|
5422
|
-
if
|
5568
|
+
// NOTE: Add SOS section only if the solver supports SOS.
|
5569
|
+
if(this.sos_var_indices.length > 0 && !this.noSupportForSOS) {
|
5423
5570
|
this.lines += 'SOS\n';
|
5424
5571
|
let sos = 0;
|
5425
5572
|
const v_set = [];
|
@@ -5439,6 +5586,7 @@ class VirtualMachine {
|
|
5439
5586
|
}
|
5440
5587
|
this.lines += 'End';
|
5441
5588
|
} else {
|
5589
|
+
// Follow LP_solve conventions.
|
5442
5590
|
// NOTE: LP_solve does not differentiate between binary and integer,
|
5443
5591
|
// so for binary variables, the constraint <= 1 must be added.
|
5444
5592
|
const v_set = [];
|
@@ -5454,7 +5602,7 @@ class VirtualMachine {
|
|
5454
5602
|
// Add the semi-continuous variables.
|
5455
5603
|
for(let i in this.is_semi_continuous) if(Number(i)) v_set.push(vbl(i));
|
5456
5604
|
if(v_set.length > 0) this.lines += 'sec ' + v_set.join(', ') + ';\n';
|
5457
|
-
//
|
5605
|
+
// LP_solve supports SOS, so add the SOS section if needed.
|
5458
5606
|
if(this.sos_var_indices.length > 0) {
|
5459
5607
|
this.lines += 'sos\n';
|
5460
5608
|
let sos = 1;
|
@@ -5698,18 +5846,6 @@ class VirtualMachine {
|
|
5698
5846
|
setTimeout(() => VM.submitFile(), 0);
|
5699
5847
|
}
|
5700
5848
|
|
5701
|
-
get noSolutionStatus() {
|
5702
|
-
// Return the set of status codes that indicate that solver did not
|
5703
|
-
// return a solution (so look-ahead should be conserved).
|
5704
|
-
if(this.solver_name === 'lp_solve') {
|
5705
|
-
return [-2, 2, 6];
|
5706
|
-
} else if(this.solver_name === 'gurobi') {
|
5707
|
-
return [1, 3, 4, 6, 11, 12, 14];
|
5708
|
-
} else {
|
5709
|
-
return [];
|
5710
|
-
}
|
5711
|
-
}
|
5712
|
-
|
5713
5849
|
checkLicense() {
|
5714
5850
|
// Compare license expiry date (if set) with current time, and notify
|
5715
5851
|
// when three days or less remain.
|
@@ -5776,6 +5912,9 @@ Solver status = ${json.status}`);
|
|
5776
5912
|
}
|
5777
5913
|
this.logMessage(bnr, errmsg);
|
5778
5914
|
UI.alert(errmsg);
|
5915
|
+
if(errmsg.indexOf('nfeasible') >= 0 || errmsg.indexOf('nbounded') >= 0) {
|
5916
|
+
this.prompt_to_diagnose = true;
|
5917
|
+
}
|
5779
5918
|
}
|
5780
5919
|
this.logMessage(bnr, msg);
|
5781
5920
|
this.equations[bnr - 1] = json.model;
|
@@ -5785,9 +5924,7 @@ Solver status = ${json.status}`);
|
|
5785
5924
|
// if this block was not solved (indicated by the 4th parameter that
|
5786
5925
|
// tests the status).
|
5787
5926
|
try {
|
5788
|
-
this.setLevels(bnr, rl, json.data.x,
|
5789
|
-
// NOTE: Appropriate status codes are solver-dependent.
|
5790
|
-
this.noSolutionStatus.indexOf(json.status) >= 0);
|
5927
|
+
this.setLevels(bnr, rl, json.data.x, !json.solution);
|
5791
5928
|
// NOTE: Post-process levels only AFTER the last round!
|
5792
5929
|
if(rl === this.lastRound) {
|
5793
5930
|
// Calculate data for all other dependent variables.
|
@@ -5836,7 +5973,12 @@ Solver status = ${json.status}`);
|
|
5836
5973
|
RECEIVER.report();
|
5837
5974
|
}
|
5838
5975
|
// Warn modeler if any issues occurred.
|
5839
|
-
if(this.
|
5976
|
+
if(this.prompt_to_diagnose && !this.diagnose) {
|
5977
|
+
UI.warn('Model is infeasible or unbounded -- ' +
|
5978
|
+
'<strong>Alt</strong>-click on the <em>Run</em> button ' +
|
5979
|
+
'<img id="solve-btn" class="sgray" src="images/solve.png">' +
|
5980
|
+
' for diagnosis');
|
5981
|
+
} else if(this.block_issues) {
|
5840
5982
|
let msg = 'Issues occurred in ' +
|
5841
5983
|
pluralS(this.block_issues, 'block') +
|
5842
5984
|
' -- details can be viewed in the monitor';
|
@@ -5919,17 +6061,24 @@ Solver status = ${json.status}`);
|
|
5919
6061
|
this.show_progress = false;
|
5920
6062
|
}
|
5921
6063
|
// Generate lines of code in format that should be accepted by solver.
|
5922
|
-
if(this.
|
6064
|
+
if(this.solver_id === 'gurobi') {
|
5923
6065
|
this.writeLpFormat(true);
|
5924
|
-
} else if(this.
|
5925
|
-
// NOTE:
|
5926
|
-
//
|
6066
|
+
} else if(this.solver_id === 'mosek') {
|
6067
|
+
// NOTE: For MOSEK, constraints must be named, or variable names
|
6068
|
+
// in solution file will not match.
|
6069
|
+
this.writeLpFormat(true, true);
|
6070
|
+
} else if(this.solver_id === 'cplex' || this.solver_id === 'scip') {
|
6071
|
+
// NOTE: The more widely accepted CPLEX LP format differs from the
|
6072
|
+
// LP_solve format that was used by the first versions of Linny-R.
|
5927
6073
|
// TRUE indicates "CPLEX format".
|
5928
6074
|
this.writeLpFormat(true);
|
5929
|
-
} else if(this.
|
6075
|
+
} else if(this.solver_id === 'lp_solve') {
|
5930
6076
|
this.writeLpFormat(false);
|
5931
6077
|
} else {
|
5932
|
-
|
6078
|
+
const msg = `Cannot write LP format: invalid solver ID "${this.solver_id}"`;
|
6079
|
+
this.logMessage(this.block_count, msg);
|
6080
|
+
UI.alert(msg);
|
6081
|
+
this.stopSolving();
|
5933
6082
|
}
|
5934
6083
|
}
|
5935
6084
|
|
@@ -5984,7 +6133,7 @@ Solver status = ${json.status}`);
|
|
5984
6133
|
this.solveBlocks();
|
5985
6134
|
}
|
5986
6135
|
|
5987
|
-
solveModel() {
|
6136
|
+
solveModel(diagnose=false) {
|
5988
6137
|
// Start the sequence of data loading, model translation, solving
|
5989
6138
|
// consecutive blocks, and finally calculating dependent variables.
|
5990
6139
|
// NOTE: Do this only if the model defines a MILP problem, i.e.,
|
@@ -5994,6 +6143,22 @@ Solver status = ${json.status}`);
|
|
5994
6143
|
UI.notify('Nothing to solve');
|
5995
6144
|
return;
|
5996
6145
|
}
|
6146
|
+
// Diagnosis (by adding slack variables and finite bounds on processes)
|
6147
|
+
// is activated when Alt-clicking the "run" button, or by clicking the
|
6148
|
+
// "clicke HERE to diagnose" link on the infoline.
|
6149
|
+
this.diagnose = diagnose || MODEL.always_diagnose;
|
6150
|
+
if(this.diagnose) {
|
6151
|
+
this.PLUS_INFINITY = this.DIAGNOSIS_UPPER_BOUND;
|
6152
|
+
this.MINUS_INFINITY = -this.DIAGNOSIS_UPPER_BOUND;
|
6153
|
+
console.log('DIAGNOSIS ON');
|
6154
|
+
} else {
|
6155
|
+
this.PLUS_INFINITY = 1e+25;
|
6156
|
+
this.MINUS_INFINITY = -1e+25;
|
6157
|
+
console.log('DIAGNOSIS OFF');
|
6158
|
+
}
|
6159
|
+
// The "propt to diagnose" flag is set when some block posed an
|
6160
|
+
// infeasible or unbounded problem.
|
6161
|
+
this.prompt_to_diagnose = false;
|
5997
6162
|
const n = MODEL.loading_datasets.length;
|
5998
6163
|
if(n > 0) {
|
5999
6164
|
// Still within reasonable time? (3 seconds per dataset)
|
@@ -6674,8 +6839,11 @@ function VMI_push_dataset_modifier(x, args) {
|
|
6674
6839
|
if(wcnr === '?') {
|
6675
6840
|
wcnr = x.wildcard_vector_index;
|
6676
6841
|
}
|
6842
|
+
} else if(mx && wcnr === false) {
|
6843
|
+
// Regular dataset with explicit modifier.
|
6844
|
+
obj = mx;
|
6677
6845
|
} else if(!ud) {
|
6678
|
-
//
|
6846
|
+
// If no selector and not "use data", check whether a running experiment
|
6679
6847
|
// defines the expression to use. If not, `obj` will be the dataset
|
6680
6848
|
// vector (so same as when "use data" is set).
|
6681
6849
|
obj = ds.activeModifierExpression;
|
@@ -7652,7 +7820,7 @@ of VM instructions to get the "right" column index.
|
|
7652
7820
|
|
7653
7821
|
A delay of d "time ticks" means that cols*d must be subtracted from this
|
7654
7822
|
index, hence the actual column index k = var_index + VM.offset - d*VM.cols.
|
7655
|
-
Keep in mind that var_index starts at 1 to comply with
|
7823
|
+
Keep in mind that var_index starts at 1 to comply with LP_solve convention.
|
7656
7824
|
|
7657
7825
|
If k <= 0, this means that the decision variable for that particular time
|
7658
7826
|
tick (t - d) was already calculated while solving the previous block
|
@@ -7674,10 +7842,11 @@ function VMI_set_bounds(args) {
|
|
7674
7842
|
vbl = VM.variables[vi - 1][1],
|
7675
7843
|
k = VM.offset + vi,
|
7676
7844
|
r = VM.round_letters.indexOf(VM.round_sequence[VM.current_round]),
|
7677
|
-
//
|
7678
|
-
//
|
7679
|
-
|
7680
|
-
inf_val = (
|
7845
|
+
// When diagnosing an unbounded problem, use low value for INFINITY,
|
7846
|
+
// but the optional fourth parameter indicates whether the solver's
|
7847
|
+
// infinity values should override the diagnosis INFINITY.
|
7848
|
+
inf_val = (VM.diagnose && !(args.length > 3 && args[3]) ?
|
7849
|
+
VM.DIAGNOSIS_UPPER_BOUND : VM.SOLVER_PLUS_INFINITY);
|
7681
7850
|
let l,
|
7682
7851
|
u,
|
7683
7852
|
fixed = (vi in VM.fixed_var_indices[r - 1]);
|
@@ -7702,17 +7871,15 @@ function VMI_set_bounds(args) {
|
|
7702
7871
|
u = args[2];
|
7703
7872
|
if(u instanceof Expression) u = u.result(VM.t);
|
7704
7873
|
u = Math.min(u, VM.PLUS_INFINITY);
|
7705
|
-
if(
|
7706
|
-
|
7707
|
-
if(u === VM.PLUS_INFINITY) u = inf_val;
|
7708
|
-
}
|
7874
|
+
if(l === VM.MINUS_INFINITY) l = -inf_val;
|
7875
|
+
if(u === VM.PLUS_INFINITY) u = inf_val;
|
7709
7876
|
fixed = '';
|
7710
7877
|
}
|
7711
7878
|
// NOTE: To see in the console whether fixing across rounds works, insert
|
7712
7879
|
// "fixed !== '' || " before DEBUGGING below.
|
7713
7880
|
if(DEBUGGING) {
|
7714
7881
|
console.log(['set_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
|
7715
|
-
' LB = ', VM.sig4Dig(l), ', UB = ', VM.sig4Dig(u), fixed].join(''));
|
7882
|
+
' LB = ', VM.sig4Dig(l), ', UB = ', VM.sig4Dig(u), fixed, l, u, inf_val].join(''));
|
7716
7883
|
}
|
7717
7884
|
// NOTE: Since the VM vectors for lower bounds and upper bounds are
|
7718
7885
|
// initialized with default values (0 for LB, +INF for UB), there is
|
@@ -8217,7 +8384,15 @@ function VMI_add_constraint(ct) {
|
|
8217
8384
|
}
|
8218
8385
|
}
|
8219
8386
|
VM.matrix.push(row);
|
8220
|
-
VM.
|
8387
|
+
let rhs = VM.rhs;
|
8388
|
+
if(rhs >= VM.PLUS_INFINITY) {
|
8389
|
+
rhs = (VM.diagnose ? VM.DIAGNOSIS_UPPER_BOUND :
|
8390
|
+
VM.SOLVER_PLUS_INFINITY);
|
8391
|
+
} else if(rhs <= VM.MINUS_INFINITY) {
|
8392
|
+
rhs = (VM.diagnose ? -VM.DIAGNOSIS_UPPER_BOUND :
|
8393
|
+
VM.SOLVER_MINUS_INFINITY);
|
8394
|
+
}
|
8395
|
+
VM.right_hand_side.push(rhs);
|
8221
8396
|
VM.constraint_types.push(ct);
|
8222
8397
|
} else if(DEBUGGING) {
|
8223
8398
|
console.log('Constraint NOT added!');
|
@@ -8292,10 +8467,13 @@ function VMI_add_cash_constraints(args) {
|
|
8292
8467
|
function VMI_add_bound_line_constraint(args) {
|
8293
8468
|
// `args`: [variable index for X, LB expression for X, UB expression for X,
|
8294
8469
|
// variable index for Y, LB expression for Y, UB expression for Y,
|
8295
|
-
// boundline object]
|
8470
|
+
// boundline object, useBinaries]
|
8471
|
+
// The `use_binaries` flag can be determined at compile time, as bound
|
8472
|
+
// lines are not dynamic. When use_binaries = TRUE, additional constraints
|
8473
|
+
// on binary variables are needed (see below).
|
8296
8474
|
const
|
8297
8475
|
vix = args[0],
|
8298
|
-
vx = VM.variables[vix - 1], // variables is zero-based!
|
8476
|
+
vx = VM.variables[vix - 1], // `variables` is zero-based!
|
8299
8477
|
objx = vx[1],
|
8300
8478
|
ubx = args[2].result(VM.t),
|
8301
8479
|
viy = args[3],
|
@@ -8303,14 +8481,16 @@ function VMI_add_bound_line_constraint(args) {
|
|
8303
8481
|
objy= vy[1],
|
8304
8482
|
uby = args[5].result(VM.t),
|
8305
8483
|
bl = args[6],
|
8306
|
-
|
8307
|
-
|
8308
|
-
|
8484
|
+
use_binaries = args[7],
|
8485
|
+
n = bl.points.length,
|
8486
|
+
x = new Array(n),
|
8487
|
+
y = new Array(n),
|
8488
|
+
w = new Array(n);
|
8309
8489
|
if(DEBUGGING) {
|
8310
8490
|
console.log('add_bound_line_constraint:', bl.displayName);
|
8311
8491
|
}
|
8312
|
-
// NOTE:
|
8313
|
-
// adjusted to 0, as then 0 is part of the process level range
|
8492
|
+
// NOTE: For semi-continuous processes, lower bounds > 0 should to be
|
8493
|
+
// adjusted to 0, as then 0 is part of the process level range.
|
8314
8494
|
let lbx = args[1].result(VM.t),
|
8315
8495
|
lby = args[4].result(VM.t);
|
8316
8496
|
if(lbx > 0 && objx instanceof Process && objx.level_to_zero) lbx = 0;
|
@@ -8338,7 +8518,7 @@ function VMI_add_bound_line_constraint(args) {
|
|
8338
8518
|
const
|
8339
8519
|
rx = (ubx - lbx) / 100,
|
8340
8520
|
ry = (uby - lby) / 100;
|
8341
|
-
for(let i = 0; i <
|
8521
|
+
for(let i = 0; i < n; i++) {
|
8342
8522
|
x[i] = lbx + bl.points[i][0] * rx;
|
8343
8523
|
y[i] = lby + bl.points[i][1] * ry;
|
8344
8524
|
w[i] = wi;
|
@@ -8346,7 +8526,7 @@ function VMI_add_bound_line_constraint(args) {
|
|
8346
8526
|
}
|
8347
8527
|
// Add constraint (1):
|
8348
8528
|
VMI_clear_coefficients();
|
8349
|
-
for(let i = 0; i <
|
8529
|
+
for(let i = 0; i < n; i++) {
|
8350
8530
|
VM.coefficients[w[i]] = 1;
|
8351
8531
|
}
|
8352
8532
|
VM.rhs = 1;
|
@@ -8357,7 +8537,7 @@ function VMI_add_bound_line_constraint(args) {
|
|
8357
8537
|
for(let i = 0; i < w.length; i++) {
|
8358
8538
|
VM.coefficients[w[i]] = -x[i];
|
8359
8539
|
}
|
8360
|
-
// No need to set RHS as it is already reset to 0
|
8540
|
+
// No need to set RHS as it is already reset to 0.
|
8361
8541
|
VMI_add_constraint(VM.EQ);
|
8362
8542
|
// Add constraint (3):
|
8363
8543
|
VMI_clear_coefficients();
|
@@ -8365,13 +8545,60 @@ function VMI_add_bound_line_constraint(args) {
|
|
8365
8545
|
for(let i = 0; i < w.length; i++) {
|
8366
8546
|
VM.coefficients[w[i]] = -y[i];
|
8367
8547
|
}
|
8368
|
-
if(!bl.constraint.no_slack) {
|
8369
|
-
// Add coefficients for slack variables unless omitted
|
8548
|
+
if(VM.diagnose && !bl.constraint.no_slack) {
|
8549
|
+
// Add coefficients for slack variables unless omitted.
|
8370
8550
|
if(bl.type != VM.LE) VM.coefficients[VM.offset + bl.GE_slack_var_index] = 1;
|
8371
8551
|
if(bl.type != VM.GE) VM.coefficients[VM.offset + bl.LE_slack_var_index] = -1;
|
8372
8552
|
}
|
8373
|
-
// No need to set RHS as it is already reset to 0
|
8553
|
+
// No need to set RHS as it is already reset to 0.
|
8374
8554
|
VMI_add_constraint(bl.type);
|
8555
|
+
// NOTE: SOS variables w[i] have bounds [0, 1], but these have not been
|
8556
|
+
// set yet.
|
8557
|
+
for(let i = 0; i < w.length; i++) {
|
8558
|
+
VM.lower_bounds[w[i]] = 0;
|
8559
|
+
VM.upper_bounds[w[i]] = 1;
|
8560
|
+
}
|
8561
|
+
// NOTE: Some solvers do not support SOS. To ensure that only 2
|
8562
|
+
// adjacent w[i]-variables can be non-zero (they range from 0 to 1),
|
8563
|
+
// as many binary variables b[i] are defined, and the following
|
8564
|
+
// constraints are added:
|
8565
|
+
// w[1] <= b[1]
|
8566
|
+
// W[2] <= b[1] + b[2]
|
8567
|
+
// W[3] <= b[2] + b[3]
|
8568
|
+
// and so on for all pairs of consecutive binaries, until finally:
|
8569
|
+
// w[N] <= b[N]
|
8570
|
+
// and then to ensure that at most 2 binaries can be 1:
|
8571
|
+
// b[1] + ... + b[N] <= 2
|
8572
|
+
// NOTE: These additional variables and constraints are not needed
|
8573
|
+
// when a bound line defines a convex feasible area. The `use_binaries`
|
8574
|
+
// parameter takes this into account.
|
8575
|
+
if(use_binaries) {
|
8576
|
+
// Add the constraints mentioned above. The index of b[i] is the
|
8577
|
+
// index of w[i] plus the number of points on the boundline N.
|
8578
|
+
VMI_clear_coefficients();
|
8579
|
+
VM.coefficients[w[0]] = 1;
|
8580
|
+
VM.coefficients[w[0] + n] = -1;
|
8581
|
+
VMI_add_constraint(VM.LE); // w[1] - b[1] <= 0
|
8582
|
+
VMI_clear_coefficients();
|
8583
|
+
for(let i = 1; i < n - 1; i++) {
|
8584
|
+
VMI_clear_coefficients();
|
8585
|
+
VM.coefficients[w[i]] = 1;
|
8586
|
+
VM.coefficients[w[i] + n - 1] = -1;
|
8587
|
+
VM.coefficients[w[i] + n] = -1;
|
8588
|
+
VMI_add_constraint(VM.LE); // w[i] - b[i-1] - b[i] <= 0
|
8589
|
+
}
|
8590
|
+
VMI_clear_coefficients();
|
8591
|
+
VM.coefficients[w[n - 1]] = 1;
|
8592
|
+
VM.coefficients[w[n - 1] + n] = -1;
|
8593
|
+
VMI_add_constraint(VM.LE); // w[N] - b[N] <= 0
|
8594
|
+
// Add last constraint: sum of binaries must be <= 2.
|
8595
|
+
VMI_clear_coefficients();
|
8596
|
+
for(let i = 0; i < n; i++) {
|
8597
|
+
VM.coefficients[w[i] + n] = 1;
|
8598
|
+
}
|
8599
|
+
VM.rhs = 2;
|
8600
|
+
VMI_add_constraint(VM.LE);
|
8601
|
+
}
|
8375
8602
|
}
|
8376
8603
|
|
8377
8604
|
function VMI_add_peak_increase_constraints(args) {
|