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.
@@ -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
- this.sos_var_indices.push([index, n]);
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: chunk offset takes into account that variable
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: stock constraints must take into account extra inflows
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: no slack on constants
3067
- // Use the lower bound (number or expression) as RHS
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: for EQ, both slack variables should be used,
3076
- // having respectively -1 and +1 as coefficients
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
- // (so it can INcrease the LHS)
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
- // (so it can DEcrease the LHS)
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: define indices for all variables (index = Simplex tableau column number)
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: slack variables are omitted when the "no slack" property
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
- // NOTE: method will add as many as there are points!
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: if UB = LB, set UB to LB only if LB is defined,
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 the
3589
- // VM just before the tableau is submitted to the solver, so for now it
3590
- // suffices to differentiate between the different "priorities" of slack
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: lowest penalty also for IMPLIED sources and sinks
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: add product constraints to calculate (and constrain) their stock
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: add constraints
4191
- // NOTE: as of version 1.0.10, constraints are implemented using special
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, bl]]);
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-contiuous variables.
5159
+ // Set list with indices of semi-continuous variables.
5053
5160
  this.is_semi_continuous = {};
5054
- for(let i in this.sec_var_indices) if(Number(i)) {
5055
- for(let j = 0; j < abl; j++) {
5056
- this.is_semi_continuous[parseInt(i) + j*this.cols] = true;
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: this flag can be set/unset dynamically by VM instructions
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 to
5066
- // the "absolute time" of the simulated period. VM.t always starts at 1,
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 indicator
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(this.sos_var_indices.length > 0) {
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
- // Add the SOS section.
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.solver_name === 'gurobi') {
6022
+ if(this.solver_id === 'gurobi') {
5923
6023
  this.writeLpFormat(true);
5924
- } else if(this.solver_name === 'scip' || this.solver_name === 'cplex') {
5925
- // NOTE: The CPLEX LP format that is also used by SCIP differs from
5926
- // the LP_solve format that was used by the first versions of Linny-R.
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.solver_name === 'lp_solve') {
6033
+ } else if(this.solver_id === 'lp_solve') {
5930
6034
  this.writeLpFormat(false);
5931
6035
  } else {
5932
- this.numeric_issue = 'solver name: ' + this.solver_name;
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
- // In no selector and not "use data", check whether a running experiment
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 LP_SOLVE convention.
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
- x = [],
8307
- y = [],
8308
- w = [];
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: for semi-continuous processes, lower bounds > 0 should to be
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 < bl.points.length; 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 < w.length; 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) {