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.
@@ -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 default MILP solver
2115
- // LP_solve considers a problem to be unbounded if decision variables
2116
- // reach +INF (1e+30) or -INF (-1e+30), and a solution inaccurate if
2117
- // extreme values get too close to +/-INF. The higher values have been
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
- this.sos_var_indices.push([index, n]);
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: chunk offset takes into account that variable
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: stock constraints must take into account extra inflows
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: no slack on constants
3067
- // Use the lower bound (number or expression) as RHS
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: for EQ, both slack variables should be used,
3076
- // having respectively -1 and +1 as coefficients
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
- // (so it can INcrease the LHS)
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
- // (so it can DEcrease the LHS)
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
- // Just in case: re-determine which entities can be ignored
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: define indices for all variables (index = Simplex tableau column number)
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: slack variables are omitted when the "no slack" property
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
- // NOTE: method will add as many as there are points!
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: if UB = LB, set UB to LB only if LB is defined,
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 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
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: lowest penalty also for IMPLIED sources and sinks
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: add product constraints to calculate (and constrain) their stock
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: add constraints
4191
- // NOTE: as of version 1.0.10, constraints are implemented using special
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, bl]]);
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-contiuous variables.
5193
+ // Set list with indices of semi-continuous variables.
5053
5194
  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;
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: this flag can be set/unset dynamically by VM instructions
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 to
5066
- // the "absolute time" of the simulated period. VM.t always starts at 1,
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 indicator
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(this.sos_var_indices.length > 0) {
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
- // Add the SOS section.
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.block_issues) {
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.solver_name === 'gurobi') {
6064
+ if(this.solver_id === 'gurobi') {
5923
6065
  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.
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.solver_name === 'lp_solve') {
6075
+ } else if(this.solver_id === 'lp_solve') {
5930
6076
  this.writeLpFormat(false);
5931
6077
  } else {
5932
- this.numeric_issue = 'solver name: ' + this.solver_name;
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
- // In no selector and not "use data", check whether a running experiment
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 LP_SOLVE convention.
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
- // Optional fourth parameter indicates whether the solver's
7678
- // infinity values should be used.
7679
- solver_inf = args.length > 3 && args[3],
7680
- inf_val = (solver_inf ? VM.SOLVER_PLUS_INFINITY : VM.PLUS_INFINITY);
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(solver_inf) {
7706
- if(l === VM.MINUS_INFINITY) l = -inf_val;
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.right_hand_side.push(VM.rhs);
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
- x = [],
8307
- y = [],
8308
- w = [];
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: for semi-continuous processes, lower bounds > 0 should to be
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 < bl.points.length; 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 < w.length; 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) {