linny-r 1.7.3 → 1.7.4
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 +23 -12
- package/console.js +2 -2
- package/package.json +1 -1
- package/server.js +2 -2
- package/static/index.html +1 -0
- package/static/linny-r.css +10 -1
- 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 +6 -3
- 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-milp.js +263 -81
- package/static/scripts/linny-r-model.js +68 -21
- package/static/scripts/linny-r-vm.js +263 -101
@@ -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);
|
@@ -2334,11 +2335,44 @@ class VirtualMachine {
|
|
2334
2335
|
['LAST', 'MAX', 'MEAN', 'MIN', 'N', 'NZ', 'SD', 'SUM', 'VAR'];
|
2335
2336
|
this.solver_names = {
|
2336
2337
|
gurobi: 'Gurobi',
|
2338
|
+
mosek: 'MOSEK',
|
2337
2339
|
cplex: 'CPLEX',
|
2338
2340
|
scip: 'SCIP',
|
2339
2341
|
lp_solve: 'LP_solve'
|
2340
2342
|
};
|
2341
2343
|
}
|
2344
|
+
|
2345
|
+
selectSolver(id) {
|
2346
|
+
if(id in this.solver_names) {
|
2347
|
+
this.solver_id = id;
|
2348
|
+
/*
|
2349
|
+
if(id === 'mosek') {
|
2350
|
+
this.PLUS_INFINITY = 1e+6;
|
2351
|
+
this.MINUS_INFINITY = -1e+6;
|
2352
|
+
this.MAX_SLACK_PENALTY = 1e+6;
|
2353
|
+
} else {
|
2354
|
+
this.PLUS_INFINITY = 1e+25;
|
2355
|
+
this.PLUS_INFINITY = -1e+25;
|
2356
|
+
this.MAX_SLACK_PENALTY = 1e+24;
|
2357
|
+
}
|
2358
|
+
*/
|
2359
|
+
} else {
|
2360
|
+
UI.alert(`Invalid solver ID "${id}"`);
|
2361
|
+
}
|
2362
|
+
}
|
2363
|
+
|
2364
|
+
get noSemiContinuous() {
|
2365
|
+
// Return TRUE if the selected solver does NOT support semi-continuous
|
2366
|
+
// variables (used to implement "shut down when lower bound constraints"
|
2367
|
+
// for processes).
|
2368
|
+
return this.solver_id === 'mosek';
|
2369
|
+
}
|
2370
|
+
|
2371
|
+
get noSupportForSOS() {
|
2372
|
+
// Return TRUE if the selected solver does NOT support special
|
2373
|
+
// ordered sets (SOS).
|
2374
|
+
return this.solver_id === 'mosek';
|
2375
|
+
}
|
2342
2376
|
|
2343
2377
|
reset() {
|
2344
2378
|
// Reset the virtual machine so that it can execute the model again.
|
@@ -2870,7 +2904,7 @@ class VirtualMachine {
|
|
2870
2904
|
}
|
2871
2905
|
if(type === 'I' || type === 'PiL') {
|
2872
2906
|
this.int_var_indices[index] = true;
|
2873
|
-
} else if('OO|IZ|SU|SD|SO|FC'.indexOf(type) >= 0) {
|
2907
|
+
} else if('OO|IZ|SU|SD|SO|FC|SB'.indexOf(type) >= 0) {
|
2874
2908
|
this.bin_var_indices[index] = true;
|
2875
2909
|
}
|
2876
2910
|
if(obj instanceof Process && obj.pace > 1) {
|
@@ -2884,7 +2918,20 @@ class VirtualMachine {
|
|
2884
2918
|
for(let i = 2; i <= n; i++) {
|
2885
2919
|
this.variables.push(['W' + i, obj]);
|
2886
2920
|
}
|
2887
|
-
|
2921
|
+
// NOTE: Some solvers do not support SOS. To ensure that only 2
|
2922
|
+
// adjacent w[i]-variables are non-zero (they range from 0 to 1),
|
2923
|
+
// as many binary variables b[i] must be defined, and additional
|
2924
|
+
// constraints must be added (see function VMI_add_boundline).
|
2925
|
+
// NOTE: These additional variables and constraints are not needed
|
2926
|
+
// when a bound line defines a convex feasible area.
|
2927
|
+
const sos_with_bin = this.noSupportForSOS && !obj.needsNoSOS;
|
2928
|
+
this.sos_var_indices.push([index, n, sos_with_bin]);
|
2929
|
+
if(sos_with_bin) {
|
2930
|
+
for(let i = 1; i <= n; i++) {
|
2931
|
+
const bi = this.variables.push(['b' + i, obj]);
|
2932
|
+
this.bin_var_indices[bi] = true;
|
2933
|
+
}
|
2934
|
+
}
|
2888
2935
|
}
|
2889
2936
|
return index;
|
2890
2937
|
}
|
@@ -2903,6 +2950,8 @@ class VirtualMachine {
|
|
2903
2950
|
if(p instanceof Product) {
|
2904
2951
|
p.stock_LE_slack_var_index = -1;
|
2905
2952
|
p.stock_GE_slack_var_index = -1;
|
2953
|
+
} else {
|
2954
|
+
p.semic_var_index = -1;
|
2906
2955
|
}
|
2907
2956
|
}
|
2908
2957
|
|
@@ -2913,6 +2962,11 @@ class VirtualMachine {
|
|
2913
2962
|
// storage capacity, because it simplifies the formulation of
|
2914
2963
|
// product-related (data) constraints.
|
2915
2964
|
p.level_var_index = this.addVariable(p.integer_level ? 'PiL': 'PL', p);
|
2965
|
+
if(p.level_to_zero && this.noSemiContinuous) {
|
2966
|
+
// When the selected solver does not support semi-continous variables,
|
2967
|
+
// they must be implemented with an additional binary variable.
|
2968
|
+
p.semic_var_index = this.addVariable('SB', p);
|
2969
|
+
}
|
2916
2970
|
// Some "data-only" link multipliers require additional variables.
|
2917
2971
|
if(p.needsOnOffData) {
|
2918
2972
|
p.on_off_var_index = this.addVariable('OO', p);
|
@@ -3000,7 +3054,7 @@ class VirtualMachine {
|
|
3000
3054
|
for(let i = 0; i < this.chunk_variables.length; i++) {
|
3001
3055
|
const
|
3002
3056
|
obj = this.chunk_variables[i][1],
|
3003
|
-
// NOTE:
|
3057
|
+
// NOTE: Chunk offset takes into account that variable
|
3004
3058
|
// indices are 0-based.
|
3005
3059
|
cvi = chof + i;
|
3006
3060
|
let v = 'X' + cvi.toString().padStart(z, '0');
|
@@ -3040,7 +3094,7 @@ class VirtualMachine {
|
|
3040
3094
|
l = p.lower_bound;
|
3041
3095
|
}
|
3042
3096
|
}
|
3043
|
-
// Likewise get the upper bound
|
3097
|
+
// Likewise get the upper bound.
|
3044
3098
|
if(p.equal_bounds && p.lower_bound.defined) {
|
3045
3099
|
u = l;
|
3046
3100
|
} else if(p.upper_bound.defined) {
|
@@ -3052,47 +3106,47 @@ class VirtualMachine {
|
|
3052
3106
|
}
|
3053
3107
|
}
|
3054
3108
|
} else {
|
3055
|
-
// Implicit bounds: if not a source, then LB is set to 0
|
3109
|
+
// Implicit bounds: if not a source, then LB is set to 0.
|
3056
3110
|
if(notsrc) l = 0;
|
3057
|
-
// If not a sink, UB is set to 0
|
3111
|
+
// If not a sink, UB is set to 0.
|
3058
3112
|
if(notsnk) u = 0;
|
3059
3113
|
}
|
3060
3114
|
|
3061
|
-
// NOTE:
|
3115
|
+
// NOTE: Stock constraints must take into account extra inflows
|
3062
3116
|
// (source) or outflows (sink).
|
3063
3117
|
// Check for special case of equal bounds, as then one EQ constraint
|
3064
3118
|
// suffices. This applies if P is a constant ...
|
3065
3119
|
if(p.isConstant) {
|
3066
|
-
// NOTE:
|
3067
|
-
//
|
3120
|
+
// NOTE: No slack on constants. Use the lower bound (number or
|
3121
|
+
// expression) as RHS.
|
3068
3122
|
this.code.push(
|
3069
3123
|
[l instanceof Expression ? VMI_set_var_rhs : VMI_set_const_rhs, l],
|
3070
3124
|
[VMI_add_constraint, VM.EQ]
|
3071
3125
|
);
|
3072
|
-
// ... or if P is neither source nor sink
|
3126
|
+
// ... or if P is neither source nor sink.
|
3073
3127
|
} else if(p.equal_bounds && notsrc && notsnk) {
|
3074
3128
|
if(!p.no_slack) {
|
3075
|
-
// NOTE:
|
3076
|
-
//
|
3129
|
+
// NOTE: For EQ, both slack variables should be used, having
|
3130
|
+
// respectively -1 and +1 as coefficients.
|
3077
3131
|
this.code.push(
|
3078
3132
|
[VMI_add_const_to_coefficient, [lesvi, -1]],
|
3079
3133
|
[VMI_add_const_to_coefficient, [gesvi, 1]]
|
3080
3134
|
);
|
3081
3135
|
}
|
3082
|
-
// Use the lower bound (number or expression) as RHS
|
3136
|
+
// Use the lower bound (number or expression) as RHS.
|
3083
3137
|
this.code.push(
|
3084
3138
|
[l instanceof Expression ? VMI_set_var_rhs : VMI_set_const_rhs, l],
|
3085
3139
|
[VMI_add_constraint, VM.EQ]
|
3086
3140
|
);
|
3087
3141
|
} else {
|
3088
|
-
// Add lower bound (GE) constraint unless product is a source node
|
3142
|
+
// Add lower bound (GE) constraint unless product is a source node.
|
3089
3143
|
if(notsrc) {
|
3090
3144
|
if(!p.no_slack) {
|
3091
|
-
// Add the GE slack index with coefficient +1
|
3092
|
-
//
|
3145
|
+
// Add the GE slack index with coefficient +1 (so it can
|
3146
|
+
// INcrease the left-hand side of the equation)
|
3093
3147
|
this.code.push([VMI_add_const_to_coefficient, [gesvi, 1]]);
|
3094
3148
|
}
|
3095
|
-
// Use the lower bound (number or expression) as RHS
|
3149
|
+
// Use the lower bound (number or expression) as RHS.
|
3096
3150
|
this.code.push(
|
3097
3151
|
[l instanceof Expression? VMI_set_var_rhs : VMI_set_const_rhs, l],
|
3098
3152
|
[VMI_add_constraint, VM.GE]
|
@@ -3101,11 +3155,11 @@ class VirtualMachine {
|
|
3101
3155
|
// Add upper bound (LE) constraint unless product is a sink node
|
3102
3156
|
if(notsnk) {
|
3103
3157
|
if(!p.no_slack) {
|
3104
|
-
// Add the stock LE index with coefficient -1
|
3105
|
-
//
|
3158
|
+
// Add the stock LE index with coefficient -1 (so it can
|
3159
|
+
// DEcrease the LHS).
|
3106
3160
|
this.code.push([VMI_add_const_to_coefficient, [lesvi, -1]]);
|
3107
3161
|
}
|
3108
|
-
// Use the upper bound (number or expression) as RHS
|
3162
|
+
// Use the upper bound (number or expression) as RHS.
|
3109
3163
|
this.code.push(
|
3110
3164
|
[u instanceof Expression ? VMI_set_var_rhs : VMI_set_const_rhs, u],
|
3111
3165
|
[VMI_add_constraint, VM.LE]
|
@@ -3119,7 +3173,7 @@ class VirtualMachine {
|
|
3119
3173
|
// Linny-R! It sets up the VM variable list, and then generates VM code
|
3120
3174
|
// that that, when executed, creates the MILP tableau for a chunk.
|
3121
3175
|
let i, j, k, l, vi, p, c, lbx, ubx;
|
3122
|
-
// Reset variable arrays and code array
|
3176
|
+
// Reset variable arrays and code array.
|
3123
3177
|
this.variables.length = 0;
|
3124
3178
|
this.chunk_variables.length = 0;
|
3125
3179
|
this.int_var_indices = [];
|
@@ -3130,12 +3184,12 @@ class VirtualMachine {
|
|
3130
3184
|
this.sos_var_indices = [];
|
3131
3185
|
this.slack_variables = [[], [], []];
|
3132
3186
|
this.code.length = 0;
|
3133
|
-
// Initialize fixed variable array: 1 list per round
|
3187
|
+
// Initialize fixed variable array: 1 list per round.
|
3134
3188
|
for(i = 0; i < MODEL.rounds; i++) {
|
3135
3189
|
this.fixed_var_indices.push([]);
|
3136
3190
|
}
|
3137
3191
|
|
3138
|
-
// Just in case: re-determine which entities can be ignored
|
3192
|
+
// Just in case: re-determine which entities can be ignored.
|
3139
3193
|
MODEL.inferIgnoredEntities();
|
3140
3194
|
const n = Object.keys(MODEL.ignored_entities).length;
|
3141
3195
|
if(n > 0) {
|
@@ -3143,7 +3197,8 @@ class VirtualMachine {
|
|
3143
3197
|
pluralS(n, 'entity', 'entities') + ' will be ignored');
|
3144
3198
|
}
|
3145
3199
|
|
3146
|
-
// FIRST:
|
3200
|
+
// FIRST: Define indices for all variables (index = Simplex tableau
|
3201
|
+
// column number).
|
3147
3202
|
|
3148
3203
|
// Each actor has a variable to compute its cash in and its cash out.
|
3149
3204
|
const actor_keys = Object.keys(MODEL.actors).sort();
|
@@ -3184,8 +3239,8 @@ class VirtualMachine {
|
|
3184
3239
|
// The slack variables prevent that the solver will consider an
|
3185
3240
|
// overconstrained model "infeasible". EQ bound lines have 2 slack
|
3186
3241
|
// variables, LE and GE bound lines need only 1.
|
3187
|
-
// NOTE:
|
3188
|
-
// of the constraint is set
|
3242
|
+
// NOTE: Slack variables are omitted when the "no slack" property
|
3243
|
+
// of the constraint is set.
|
3189
3244
|
const constraint_keys = Object.keys(MODEL.constraints).sort();
|
3190
3245
|
for(i = 0; i < constraint_keys.length; i++) {
|
3191
3246
|
k = constraint_keys[i];
|
@@ -3195,11 +3250,13 @@ class VirtualMachine {
|
|
3195
3250
|
const bl = c.bound_lines[l];
|
3196
3251
|
bl.sos_var_indices = [];
|
3197
3252
|
if(bl.isActive && bl.constrainsY) {
|
3198
|
-
// Define SOS2 variables w[i]
|
3199
|
-
//
|
3253
|
+
// Define SOS2 variables w[i] (plus associated binaries if
|
3254
|
+
// solver does not support special ordered sets).
|
3255
|
+
// NOTE: `addVariable` will add as many as there are points!
|
3200
3256
|
bl.first_sos_var_index = this.addVariable('W1', bl);
|
3201
3257
|
if(!c.no_slack) {
|
3202
|
-
// Define the slack variable(s) for bound line constraints
|
3258
|
+
// Define the slack variable(s) for bound line constraints.
|
3259
|
+
// NOTE: Category [2] means: highest slack penalty.
|
3203
3260
|
if(bl.type !== VM.GE) {
|
3204
3261
|
bl.LE_slack_var_index = this.addVariable('CLE', bl);
|
3205
3262
|
this.slack_variables[2].push(bl.LE_slack_var_index);
|
@@ -3286,6 +3343,10 @@ class VirtualMachine {
|
|
3286
3343
|
ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
|
3287
3344
|
if(lbx.isStatic) lbx = lbx.result(0);
|
3288
3345
|
if(ubx.isStatic) ubx = ubx.result(0);
|
3346
|
+
// NOTE: When semic_var_index is set, the lower bound must be
|
3347
|
+
// zero, as the semi-continuous lower bound is implemented with
|
3348
|
+
// a binary variable.
|
3349
|
+
if(p.semic_var_index >= 0) lbx = 0;
|
3289
3350
|
// NOTE: Pass TRUE as fourth parameter to indicate that +INF
|
3290
3351
|
// and -INF can be coded as the infinity values used by the
|
3291
3352
|
// solver, rather than the Linny-R values used to detect
|
@@ -3307,34 +3368,34 @@ class VirtualMachine {
|
|
3307
3368
|
}
|
3308
3369
|
}
|
3309
3370
|
|
3310
|
-
// NEXT: Define the bounds for all stock level variables
|
3371
|
+
// NEXT: Define the bounds for all stock level variables.
|
3311
3372
|
for(i = 0; i < product_keys.length; i++) {
|
3312
3373
|
k = product_keys[i];
|
3313
3374
|
if(!MODEL.ignored_entities[k]) {
|
3314
3375
|
p = MODEL.products[k];
|
3315
|
-
// Get index of variable that is constrained by LB and UB
|
3376
|
+
// Get index of variable that is constrained by LB and UB.
|
3316
3377
|
vi = p.level_var_index;
|
3317
3378
|
if(p.no_slack) {
|
3318
3379
|
// If no slack, the bound constraints can be set on the
|
3319
|
-
// variables themselves
|
3380
|
+
// variables themselves.
|
3320
3381
|
lbx = p.lower_bound;
|
3321
|
-
// NOTE:
|
3382
|
+
// NOTE: If UB = LB, set UB to LB only if LB is defined,
|
3322
3383
|
// because LB expressions default to -INF while UB expressions
|
3323
|
-
// default to + INF
|
3384
|
+
// default to + INF.
|
3324
3385
|
ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
|
3325
3386
|
if(lbx.isStatic) lbx = lbx.result(0);
|
3326
3387
|
if(ubx.isStatic) ubx = ubx.result(0);
|
3327
3388
|
this.code.push([VMI_set_bounds, [vi, lbx, ubx]]);
|
3328
3389
|
} else {
|
3329
3390
|
// Otherwise, set bounds of stock variable to -INF and +INF,
|
3330
|
-
// as product constraints will be added later on
|
3391
|
+
// as product constraints will be added later on.
|
3331
3392
|
this.code.push([VMI_set_bounds,
|
3332
3393
|
[vi, VM.MINUS_INFINITY, VM.PLUS_INFINITY]]);
|
3333
3394
|
}
|
3334
3395
|
}
|
3335
3396
|
}
|
3336
3397
|
|
3337
|
-
// NEXT: Define objective function that maximizes total cash flow
|
3398
|
+
// NEXT: Define objective function that maximizes total cash flow.
|
3338
3399
|
|
3339
3400
|
// NOTE: As of 19 October 2020, the objective function is *explicitly*
|
3340
3401
|
// calculated as the (weighted) sum of the cash flows of actors
|
@@ -3580,18 +3641,18 @@ class VirtualMachine {
|
|
3580
3641
|
}
|
3581
3642
|
}
|
3582
3643
|
|
3583
|
-
// Copy the VM coefficient vector to the objective function coefficients
|
3644
|
+
// Copy the VM coefficient vector to the objective function coefficients.
|
3584
3645
|
// NOTE: for the VM's current time step (VM.t)!
|
3585
3646
|
this.code.push([VMI_set_objective, null]);
|
3586
3647
|
|
3587
3648
|
// 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
|
3649
|
+
// (1) Scaling of the objective function coefficients is performed by
|
3650
|
+
// the VM just before the tableau is submitted to the solver, so
|
3651
|
+
// for now it suffices to differentiate between the different
|
3652
|
+
// "priorities" of slack variables.
|
3592
3653
|
// (2) Slack variables have different penalties: type 0 = market demands,
|
3593
3654
|
// i.e., EQ constraints on stocks, 1 = GE and LE constraints on product
|
3594
|
-
// levels, 2 = strongest constraints: on data, or set by boundlines
|
3655
|
+
// levels, 2 = strongest constraints: on data, or set by boundlines.
|
3595
3656
|
let pen, hb;
|
3596
3657
|
for(i = 0; i < product_keys.length; i++) {
|
3597
3658
|
k = product_keys[i];
|
@@ -3600,7 +3661,7 @@ class VirtualMachine {
|
|
3600
3661
|
if(p.level_var_index >= 0 && !p.no_slack) {
|
3601
3662
|
hb = p.hasBounds;
|
3602
3663
|
pen = (p.is_data ? 2 :
|
3603
|
-
// NOTE:
|
3664
|
+
// NOTE: Lowest penalty also for IMPLIED sources and sinks.
|
3604
3665
|
(p.equal_bounds || (!hb && (p.isSourceNode || p.isSinkNode)) ? 0 :
|
3605
3666
|
(hb ? 1 : 2)));
|
3606
3667
|
this.slack_variables[pen].push(
|
@@ -3609,7 +3670,48 @@ class VirtualMachine {
|
|
3609
3670
|
}
|
3610
3671
|
}
|
3611
3672
|
|
3612
|
-
// NEXT:
|
3673
|
+
// NEXT: Add semi-continuous constraints only if not supported by solver.
|
3674
|
+
if(!this.noSemiContinuous) {
|
3675
|
+
for(i = 0; i < process_keys.length; i++) {
|
3676
|
+
k = process_keys[i];
|
3677
|
+
if(!MODEL.ignored_entities[k]) {
|
3678
|
+
p = MODEL.processes[k];
|
3679
|
+
const svi = p.semic_var_index;
|
3680
|
+
if(svi >= 0) {
|
3681
|
+
const
|
3682
|
+
vi = p.level_var_index,
|
3683
|
+
lbx = p.lower_bound,
|
3684
|
+
ubx = (p.equal_bounds && lbx.defined ? lbx : p.upper_bound);
|
3685
|
+
// LB*binary - level <= 0
|
3686
|
+
this.code.push(
|
3687
|
+
[VMI_clear_coefficients, null],
|
3688
|
+
[VMI_add_const_to_coefficient, [vi, -1]]
|
3689
|
+
);
|
3690
|
+
if(lbx.isStatic) {
|
3691
|
+
this.code.push([VMI_add_const_to_coefficient,
|
3692
|
+
[svi, lbx.result(0)]]);
|
3693
|
+
} else {
|
3694
|
+
this.code.push([VMI_add_var_to_coefficient, [svi, lbx]]);
|
3695
|
+
}
|
3696
|
+
this.code.push([VMI_add_constraint, VM.LE]);
|
3697
|
+
// level - UB*binary <= 0
|
3698
|
+
this.code.push(
|
3699
|
+
[VMI_clear_coefficients, null],
|
3700
|
+
[VMI_add_const_to_coefficient, [vi, 1]]
|
3701
|
+
);
|
3702
|
+
if(ubx.isStatic) {
|
3703
|
+
this.code.push([VMI_subtract_const_from_coefficient,
|
3704
|
+
[svi, ubx.result(0)]]);
|
3705
|
+
} else {
|
3706
|
+
this.code.push([VMI_subtract_var_from_coefficient, [svi, ubx]]);
|
3707
|
+
}
|
3708
|
+
this.code.push([VMI_add_constraint, VM.LE]);
|
3709
|
+
}
|
3710
|
+
}
|
3711
|
+
}
|
3712
|
+
}
|
3713
|
+
|
3714
|
+
// NEXT: Add product constraints to calculate (and constrain) their stock.
|
3613
3715
|
|
3614
3716
|
for(let pi = 0; pi < product_keys.length; pi++) {
|
3615
3717
|
k = product_keys[pi];
|
@@ -4187,30 +4289,35 @@ class VirtualMachine {
|
|
4187
4289
|
}
|
4188
4290
|
}
|
4189
4291
|
|
4190
|
-
// NEXT:
|
4191
|
-
// NOTE:
|
4292
|
+
// NEXT: Add constraints.
|
4293
|
+
// NOTE: As of version 1.0.10, constraints are implemented using special
|
4192
4294
|
// ordered sets (SOS2). This is effectuated with a dedicated VM instruction
|
4193
4295
|
// for each of its "active" bound lines. This instruction requires these
|
4194
4296
|
// parameters:
|
4195
4297
|
// - variable indices for the constraining node X, the constrained node Y
|
4196
4298
|
// - expressions for the LB and UB of X and Y
|
4197
4299
|
// - the bound line object, as this provides all further information
|
4300
|
+
// NOTE: For efficiency, the useBinaries flag is also passed, as it can
|
4301
|
+
// be determined at compile time whether a SOS constraint is needed
|
4302
|
+
// (bound lines are static), and whether the solver does not support
|
4303
|
+
// SOS, in which case binary variables must be used.
|
4198
4304
|
for(i = 0; i < constraint_keys.length; i++) {
|
4199
4305
|
k = constraint_keys[i];
|
4200
4306
|
if(!MODEL.ignored_entities[k]) {
|
4201
4307
|
c = MODEL.constraints[k];
|
4202
|
-
// Get the two associated nodes
|
4308
|
+
// Get the two associated nodes.
|
4203
4309
|
const
|
4204
4310
|
x = c.from_node,
|
4205
4311
|
y = c.to_node;
|
4206
4312
|
for(j = 0; j < c.bound_lines.length; j++) {
|
4207
4313
|
const bl = c.bound_lines[j];
|
4208
4314
|
// Only add constrains for bound lines that are "active" for the
|
4209
|
-
// current run, and do constrain Y in some way
|
4315
|
+
// current run, and do constrain Y in some way.
|
4210
4316
|
if(bl.isActive && bl.constrainsY) {
|
4211
4317
|
this.code.push([VMI_add_bound_line_constraint,
|
4212
4318
|
[x.level_var_index, x.lower_bound, x.upper_bound,
|
4213
|
-
y.level_var_index, y.lower_bound, y.upper_bound,
|
4319
|
+
y.level_var_index, y.lower_bound, y.upper_bound,
|
4320
|
+
bl, this.noSupportForSOS && !bl.needsNoSOS]]);
|
4214
4321
|
}
|
4215
4322
|
}
|
4216
4323
|
}
|
@@ -5049,24 +5156,28 @@ class VirtualMachine {
|
|
5049
5156
|
this.is_binary[parseInt(i) + j*this.cols] = true;
|
5050
5157
|
}
|
5051
5158
|
}
|
5052
|
-
// Set list with indices of semi-
|
5159
|
+
// Set list with indices of semi-continuous variables.
|
5053
5160
|
this.is_semi_continuous = {};
|
5054
|
-
|
5055
|
-
|
5056
|
-
|
5161
|
+
// NOTE: Solver may not support semi-continuous variables.
|
5162
|
+
if(!this.noSemiContinuous) {
|
5163
|
+
for(let i in this.sec_var_indices) if(Number(i)) {
|
5164
|
+
for(let j = 0; j < abl; j++) {
|
5165
|
+
this.is_semi_continuous[parseInt(i) + j*this.cols] = true;
|
5166
|
+
}
|
5057
5167
|
}
|
5058
5168
|
}
|
5059
|
-
// Initialize the "add constraints flag" to TRUE
|
5060
|
-
// NOTE:
|
5169
|
+
// Initialize the "add constraints flag" to TRUE.
|
5170
|
+
// NOTE: This flag can be set/unset dynamically by VM instructions.
|
5061
5171
|
this.add_constraints_flag = true;
|
5062
|
-
// Execute code for each time step in this block
|
5172
|
+
// Execute code for each time step in this block.
|
5063
5173
|
this.logTrace('START executing block code (' +
|
5064
5174
|
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
|
5175
|
+
// NOTE: `t` is the VM's "time tick", which is "relative time" compared
|
5176
|
+
// to the "absolute time" of the simulated period. VM.t always starts
|
5177
|
+
// at 1, which corresponds to MODEL.start_period.
|
5068
5178
|
this.t = (this.block_count - 1) * MODEL.block_length + 1;
|
5069
|
-
// Show this relative (!) time step on the status bar as progress
|
5179
|
+
// Show this relative (!) time step on the status bar as progress
|
5180
|
+
// indicator.
|
5070
5181
|
UI.updateTimeStep(this.t);
|
5071
5182
|
setTimeout((t, n) => VM.addTableauSegment(t, n), 0, 0, abl);
|
5072
5183
|
}
|
@@ -5120,10 +5231,10 @@ class VirtualMachine {
|
|
5120
5231
|
VMI_add_constraint(VM.EQ);
|
5121
5232
|
}
|
5122
5233
|
}
|
5123
|
-
// Proceed to the next time tick
|
5234
|
+
// Proceed to the next time tick.
|
5124
5235
|
this.t++;
|
5125
5236
|
// This also means advancing the offset, because all VM instructions
|
5126
|
-
// pass variable indices relative to the first column in the tableau
|
5237
|
+
// pass variable indices relative to the first column in the tableau.
|
5127
5238
|
this.offset += this.cols;
|
5128
5239
|
}
|
5129
5240
|
if(next_start < abl) {
|
@@ -5136,7 +5247,7 @@ class VirtualMachine {
|
|
5136
5247
|
|
5137
5248
|
finishBlockSetup(abl) {
|
5138
5249
|
// Scale the coefficients of the objective function, and calculate
|
5139
|
-
// the "base" slack penalty
|
5250
|
+
// the "base" slack penalty.
|
5140
5251
|
this.scaleObjective();
|
5141
5252
|
this.scaleCashFlowConstraints();
|
5142
5253
|
// Add (appropriately scaled!) slack penalties to the objective function
|
@@ -5235,7 +5346,7 @@ class VirtualMachine {
|
|
5235
5346
|
}
|
5236
5347
|
}
|
5237
5348
|
|
5238
|
-
writeLpFormat(cplex=false) {
|
5349
|
+
writeLpFormat(cplex=false, named_constraints=false) {
|
5239
5350
|
// NOTE: Up to version 1.5.6, actual block length of last block used
|
5240
5351
|
// to be shorter than the chunk length so as not to go beyond the
|
5241
5352
|
// simulation end time. The look-ahead is now *always* part of the
|
@@ -5301,6 +5412,7 @@ class VirtualMachine {
|
|
5301
5412
|
n = this.matrix.length;
|
5302
5413
|
for(let r = 0; r < n; r++) {
|
5303
5414
|
const row = this.matrix[r];
|
5415
|
+
if(named_constraints) line = `C${r + 1}: `;
|
5304
5416
|
for(p in row) if (row.hasOwnProperty(p)) {
|
5305
5417
|
c = row[p];
|
5306
5418
|
if (c < VM.SOLVER_MINUS_INFINITY || c > VM.SOLVER_PLUS_INFINITY) {
|
@@ -5419,7 +5531,8 @@ class VirtualMachine {
|
|
5419
5531
|
line = '';
|
5420
5532
|
scv = 0;
|
5421
5533
|
}
|
5422
|
-
if
|
5534
|
+
// NOTE: Add SOS section only if the solver supports SOS.
|
5535
|
+
if(this.sos_var_indices.length > 0 && !this.noSupportForSOS) {
|
5423
5536
|
this.lines += 'SOS\n';
|
5424
5537
|
let sos = 0;
|
5425
5538
|
const v_set = [];
|
@@ -5439,6 +5552,7 @@ class VirtualMachine {
|
|
5439
5552
|
}
|
5440
5553
|
this.lines += 'End';
|
5441
5554
|
} else {
|
5555
|
+
// Follow LP_solve conventions.
|
5442
5556
|
// NOTE: LP_solve does not differentiate between binary and integer,
|
5443
5557
|
// so for binary variables, the constraint <= 1 must be added.
|
5444
5558
|
const v_set = [];
|
@@ -5454,7 +5568,7 @@ class VirtualMachine {
|
|
5454
5568
|
// Add the semi-continuous variables.
|
5455
5569
|
for(let i in this.is_semi_continuous) if(Number(i)) v_set.push(vbl(i));
|
5456
5570
|
if(v_set.length > 0) this.lines += 'sec ' + v_set.join(', ') + ';\n';
|
5457
|
-
//
|
5571
|
+
// LP_solve supports SOS, so add the SOS section if needed.
|
5458
5572
|
if(this.sos_var_indices.length > 0) {
|
5459
5573
|
this.lines += 'sos\n';
|
5460
5574
|
let sos = 1;
|
@@ -5698,18 +5812,6 @@ class VirtualMachine {
|
|
5698
5812
|
setTimeout(() => VM.submitFile(), 0);
|
5699
5813
|
}
|
5700
5814
|
|
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
5815
|
checkLicense() {
|
5714
5816
|
// Compare license expiry date (if set) with current time, and notify
|
5715
5817
|
// when three days or less remain.
|
@@ -5785,9 +5887,7 @@ Solver status = ${json.status}`);
|
|
5785
5887
|
// if this block was not solved (indicated by the 4th parameter that
|
5786
5888
|
// tests the status).
|
5787
5889
|
try {
|
5788
|
-
this.setLevels(bnr, rl, json.data.x,
|
5789
|
-
// NOTE: Appropriate status codes are solver-dependent.
|
5790
|
-
this.noSolutionStatus.indexOf(json.status) >= 0);
|
5890
|
+
this.setLevels(bnr, rl, json.data.x, !json.solution);
|
5791
5891
|
// NOTE: Post-process levels only AFTER the last round!
|
5792
5892
|
if(rl === this.lastRound) {
|
5793
5893
|
// Calculate data for all other dependent variables.
|
@@ -5919,17 +6019,24 @@ Solver status = ${json.status}`);
|
|
5919
6019
|
this.show_progress = false;
|
5920
6020
|
}
|
5921
6021
|
// Generate lines of code in format that should be accepted by solver.
|
5922
|
-
if(this.
|
6022
|
+
if(this.solver_id === 'gurobi') {
|
5923
6023
|
this.writeLpFormat(true);
|
5924
|
-
} else if(this.
|
5925
|
-
// NOTE:
|
5926
|
-
//
|
6024
|
+
} else if(this.solver_id === 'mosek') {
|
6025
|
+
// NOTE: For MOSEK, constraints must be named, or variable names
|
6026
|
+
// in solution file will not match.
|
6027
|
+
this.writeLpFormat(true, true);
|
6028
|
+
} else if(this.solver_id === 'cplex' || this.solver_id === 'scip') {
|
6029
|
+
// NOTE: The more widely accepted CPLEX LP format differs from the
|
6030
|
+
// LP_solve format that was used by the first versions of Linny-R.
|
5927
6031
|
// TRUE indicates "CPLEX format".
|
5928
6032
|
this.writeLpFormat(true);
|
5929
|
-
} else if(this.
|
6033
|
+
} else if(this.solver_id === 'lp_solve') {
|
5930
6034
|
this.writeLpFormat(false);
|
5931
6035
|
} else {
|
5932
|
-
|
6036
|
+
const msg = `Cannot write LP format: invalid solver ID "${this.solver_id}"`;
|
6037
|
+
this.logMessage(this.block_count, msg);
|
6038
|
+
UI.alert(msg);
|
6039
|
+
this.stopSolving();
|
5933
6040
|
}
|
5934
6041
|
}
|
5935
6042
|
|
@@ -6674,8 +6781,11 @@ function VMI_push_dataset_modifier(x, args) {
|
|
6674
6781
|
if(wcnr === '?') {
|
6675
6782
|
wcnr = x.wildcard_vector_index;
|
6676
6783
|
}
|
6784
|
+
} else if(mx && wcnr === false) {
|
6785
|
+
// Regular dataset with explicit modifier.
|
6786
|
+
obj = mx;
|
6677
6787
|
} else if(!ud) {
|
6678
|
-
//
|
6788
|
+
// If no selector and not "use data", check whether a running experiment
|
6679
6789
|
// defines the expression to use. If not, `obj` will be the dataset
|
6680
6790
|
// vector (so same as when "use data" is set).
|
6681
6791
|
obj = ds.activeModifierExpression;
|
@@ -7652,7 +7762,7 @@ of VM instructions to get the "right" column index.
|
|
7652
7762
|
|
7653
7763
|
A delay of d "time ticks" means that cols*d must be subtracted from this
|
7654
7764
|
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
|
7765
|
+
Keep in mind that var_index starts at 1 to comply with LP_solve convention.
|
7656
7766
|
|
7657
7767
|
If k <= 0, this means that the decision variable for that particular time
|
7658
7768
|
tick (t - d) was already calculated while solving the previous block
|
@@ -8292,10 +8402,13 @@ function VMI_add_cash_constraints(args) {
|
|
8292
8402
|
function VMI_add_bound_line_constraint(args) {
|
8293
8403
|
// `args`: [variable index for X, LB expression for X, UB expression for X,
|
8294
8404
|
// variable index for Y, LB expression for Y, UB expression for Y,
|
8295
|
-
// boundline object]
|
8405
|
+
// boundline object, useBinaries]
|
8406
|
+
// The `use_binaries` flag can be determined at compile time, as bound
|
8407
|
+
// lines are not dynamic. When use_binaries = TRUE, additional constraints
|
8408
|
+
// on binary variables are needed (see below).
|
8296
8409
|
const
|
8297
8410
|
vix = args[0],
|
8298
|
-
vx = VM.variables[vix - 1], // variables is zero-based!
|
8411
|
+
vx = VM.variables[vix - 1], // `variables` is zero-based!
|
8299
8412
|
objx = vx[1],
|
8300
8413
|
ubx = args[2].result(VM.t),
|
8301
8414
|
viy = args[3],
|
@@ -8303,14 +8416,16 @@ function VMI_add_bound_line_constraint(args) {
|
|
8303
8416
|
objy= vy[1],
|
8304
8417
|
uby = args[5].result(VM.t),
|
8305
8418
|
bl = args[6],
|
8306
|
-
|
8307
|
-
|
8308
|
-
|
8419
|
+
use_binaries = args[7],
|
8420
|
+
n = bl.points.length,
|
8421
|
+
x = new Array(n),
|
8422
|
+
y = new Array(n),
|
8423
|
+
w = new Array(n);
|
8309
8424
|
if(DEBUGGING) {
|
8310
8425
|
console.log('add_bound_line_constraint:', bl.displayName);
|
8311
8426
|
}
|
8312
|
-
// NOTE:
|
8313
|
-
// adjusted to 0, as then 0 is part of the process level range
|
8427
|
+
// NOTE: For semi-continuous processes, lower bounds > 0 should to be
|
8428
|
+
// adjusted to 0, as then 0 is part of the process level range.
|
8314
8429
|
let lbx = args[1].result(VM.t),
|
8315
8430
|
lby = args[4].result(VM.t);
|
8316
8431
|
if(lbx > 0 && objx instanceof Process && objx.level_to_zero) lbx = 0;
|
@@ -8338,7 +8453,7 @@ function VMI_add_bound_line_constraint(args) {
|
|
8338
8453
|
const
|
8339
8454
|
rx = (ubx - lbx) / 100,
|
8340
8455
|
ry = (uby - lby) / 100;
|
8341
|
-
for(let i = 0; i <
|
8456
|
+
for(let i = 0; i < n; i++) {
|
8342
8457
|
x[i] = lbx + bl.points[i][0] * rx;
|
8343
8458
|
y[i] = lby + bl.points[i][1] * ry;
|
8344
8459
|
w[i] = wi;
|
@@ -8346,7 +8461,7 @@ function VMI_add_bound_line_constraint(args) {
|
|
8346
8461
|
}
|
8347
8462
|
// Add constraint (1):
|
8348
8463
|
VMI_clear_coefficients();
|
8349
|
-
for(let i = 0; i <
|
8464
|
+
for(let i = 0; i < n; i++) {
|
8350
8465
|
VM.coefficients[w[i]] = 1;
|
8351
8466
|
}
|
8352
8467
|
VM.rhs = 1;
|
@@ -8357,7 +8472,7 @@ function VMI_add_bound_line_constraint(args) {
|
|
8357
8472
|
for(let i = 0; i < w.length; i++) {
|
8358
8473
|
VM.coefficients[w[i]] = -x[i];
|
8359
8474
|
}
|
8360
|
-
// No need to set RHS as it is already reset to 0
|
8475
|
+
// No need to set RHS as it is already reset to 0.
|
8361
8476
|
VMI_add_constraint(VM.EQ);
|
8362
8477
|
// Add constraint (3):
|
8363
8478
|
VMI_clear_coefficients();
|
@@ -8366,12 +8481,59 @@ function VMI_add_bound_line_constraint(args) {
|
|
8366
8481
|
VM.coefficients[w[i]] = -y[i];
|
8367
8482
|
}
|
8368
8483
|
if(!bl.constraint.no_slack) {
|
8369
|
-
// Add coefficients for slack variables unless omitted
|
8484
|
+
// Add coefficients for slack variables unless omitted.
|
8370
8485
|
if(bl.type != VM.LE) VM.coefficients[VM.offset + bl.GE_slack_var_index] = 1;
|
8371
8486
|
if(bl.type != VM.GE) VM.coefficients[VM.offset + bl.LE_slack_var_index] = -1;
|
8372
8487
|
}
|
8373
|
-
// No need to set RHS as it is already reset to 0
|
8488
|
+
// No need to set RHS as it is already reset to 0.
|
8374
8489
|
VMI_add_constraint(bl.type);
|
8490
|
+
// NOTE: SOS variables w[i] have bounds [0, 1], but these have not been
|
8491
|
+
// set yet.
|
8492
|
+
for(let i = 0; i < w.length; i++) {
|
8493
|
+
VM.lower_bounds[w[i]] = 0;
|
8494
|
+
VM.upper_bounds[w[i]] = 1;
|
8495
|
+
}
|
8496
|
+
// NOTE: Some solvers do not support SOS. To ensure that only 2
|
8497
|
+
// adjacent w[i]-variables can be non-zero (they range from 0 to 1),
|
8498
|
+
// as many binary variables b[i] are defined, and the following
|
8499
|
+
// constraints are added:
|
8500
|
+
// w[1] <= b[1]
|
8501
|
+
// W[2] <= b[1] + b[2]
|
8502
|
+
// W[3] <= b[2] + b[3]
|
8503
|
+
// and so on for all pairs of consecutive binaries, until finally:
|
8504
|
+
// w[N] <= b[N]
|
8505
|
+
// and then to ensure that at most 2 binaries can be 1:
|
8506
|
+
// b[1] + ... + b[N] <= 2
|
8507
|
+
// NOTE: These additional variables and constraints are not needed
|
8508
|
+
// when a bound line defines a convex feasible area. The `use_binaries`
|
8509
|
+
// parameter takes this into account.
|
8510
|
+
if(use_binaries) {
|
8511
|
+
// Add the constraints mentioned above. The index of b[i] is the
|
8512
|
+
// index of w[i] plus the number of points on the boundline N.
|
8513
|
+
VMI_clear_coefficients();
|
8514
|
+
VM.coefficients[w[0]] = 1;
|
8515
|
+
VM.coefficients[w[0] + n] = -1;
|
8516
|
+
VMI_add_constraint(VM.LE); // w[1] - b[1] <= 0
|
8517
|
+
VMI_clear_coefficients();
|
8518
|
+
for(let i = 1; i < n - 1; i++) {
|
8519
|
+
VMI_clear_coefficients();
|
8520
|
+
VM.coefficients[w[i]] = 1;
|
8521
|
+
VM.coefficients[w[i] + n - 1] = -1;
|
8522
|
+
VM.coefficients[w[i] + n] = -1;
|
8523
|
+
VMI_add_constraint(VM.LE); // w[i] - b[i-1] - b[i] <= 0
|
8524
|
+
}
|
8525
|
+
VMI_clear_coefficients();
|
8526
|
+
VM.coefficients[w[n - 1]] = 1;
|
8527
|
+
VM.coefficients[w[n - 1] + n] = -1;
|
8528
|
+
VMI_add_constraint(VM.LE); // w[N] - b[N] <= 0
|
8529
|
+
// Add last constraint: sum of binaries must be <= 2.
|
8530
|
+
VMI_clear_coefficients();
|
8531
|
+
for(let i = 0; i < n; i++) {
|
8532
|
+
VM.coefficients[w[i] + n] = 1;
|
8533
|
+
}
|
8534
|
+
VM.rhs = 2;
|
8535
|
+
VMI_add_constraint(VM.LE);
|
8536
|
+
}
|
8375
8537
|
}
|
8376
8538
|
|
8377
8539
|
function VMI_add_peak_increase_constraints(args) {
|