linny-r 1.7.4 → 1.8.1

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.
@@ -283,7 +283,7 @@ class DocumentationManager {
283
283
  if(list.indexOf(this.entity) >= 0) {
284
284
  this.stopEditing();
285
285
  this.entity = null;
286
- this.title.innerHTML = 'Documentation';
286
+ this.title.innerHTML = 'Information and documentation';
287
287
  this.viewer.innerHTML = this.about_linny_r;
288
288
  }
289
289
  }
@@ -74,16 +74,16 @@ Attributes, however, are case sensitive!">[Actor X|CF]</code> for cash flow.
74
74
  <code title="Number of last round in the sequence (1=a, 2=b, etc.)">lr</code>,
75
75
  <code title="Number of rounds in the sequence">nr</code>,
76
76
  <code title="Number of current experiment run (starts at 0)">x</code>,
77
- <code title="Number of runs in the experiment">nx</code>,
78
- <span title="Index variables of iterator dimensions)">
77
+ <code title="Number of runs in the current experiment">nx</code>,
78
+ <span title="Index variables of iterator dimensions">
79
79
  <code>i</code>, <code>j</code>, <code>k</code>,
80
80
  </span>
81
- <code title="Number of time steps in 1 year)">yr</code>,
82
- <code title="Number of time steps in 1 week)">wk</code>,
83
- <code title="Number of time steps in 1 day)">d</code>,
84
- <code title="Number of time steps in 1 hour)">h</code>,
85
- <code title="Number of time steps in 1 minute)">m</code>,
86
- <code title="Number of time steps in 1 second)">s</code>,
81
+ <code title="Number of time steps in 1 year">yr</code>,
82
+ <code title="Number of time steps in 1 week">wk</code>,
83
+ <code title="Number of time steps in 1 day">d</code>,
84
+ <code title="Number of time steps in 1 hour">h</code>,
85
+ <code title="Number of time steps in 1 minute">m</code>,
86
+ <code title="Number of time steps in 1 second">s</code>,
87
87
  <code title="A random number from the uniform distribution U(0, 1)">random</code>),
88
88
  constants (<code title="Mathematical constant &pi; = ${Math.PI}">pi</code>,
89
89
  <code title="Logical constant true = 1
@@ -178,8 +178,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
178
178
  }
179
179
 
180
180
  editExpression(event) {
181
- // Infer which entity property expression is to edited from the button
182
- // that was clicked, and then opens the dialog.
181
+ // Infer which entity property expression is to be edited from the
182
+ // button that was clicked, and then opens the dialog.
183
183
  const
184
184
  btn = event.target,
185
185
  ids = btn.id.split('-'), // 3-tuple [entity type, attribute, 'x']
@@ -300,9 +300,9 @@ class Paper {
300
300
  at_process_ub_arrow: '#f0b0e8',
301
301
  // NOTE: special color when level at negative lower bound
302
302
  at_process_neg_lb: '#800050',
303
- // Process with unbound level = +INF is displayed in maroon-red
304
- infinite_level: '#a00001',
305
- infinite_level_fill: '#ff90a0',
303
+ // Process with unbound level: +INF marine-blue, -INF maroon-red
304
+ plus_infinite_level: '#1000a0',
305
+ minus_infinite_level: '#a00010',
306
306
  // Process state change symbols are displayed in red
307
307
  switch_on_off: '#b00000',
308
308
  // Compound arrows with non-zero actual flow are displayed in red-purple
@@ -1979,9 +1979,16 @@ class Paper {
1979
1979
  if(MODEL.solved && !ignored) {
1980
1980
  if(l === VM.PLUS_INFINITY) {
1981
1981
  // Infinite level => unbounded solution
1982
- stroke_color = this.palette.infinite_level;
1983
- fill_color = this.palette.infinite_level_fill;
1984
- lrect_color = this.palette.infinite_level;
1982
+ stroke_color = this.palette.plus_infinite_level;
1983
+ fill_color = this.palette.above_upper_bound;
1984
+ lrect_color = this.palette.plus_infinite_level;
1985
+ font_color = 'white';
1986
+ stroke_width = 2;
1987
+ } else if(l === VM.MINUS_INFINITY) {
1988
+ // Infinite level => unbounded solution
1989
+ stroke_color = this.palette.minus_infinite_level;
1990
+ fill_color = this.palette.below_lower_bound;
1991
+ lrect_color = this.palette.minus_infinite_level;
1985
1992
  font_color = 'white';
1986
1993
  stroke_width = 2;
1987
1994
  } else if(l > ub - VM.SIG_DIF_FROM_ZERO ||
@@ -612,15 +612,21 @@ module.exports = class MILPSolver {
612
612
  json = fs.readFileSync(s.solution, 'utf8').trim(),
613
613
  sol = JSON.parse(json);
614
614
  result.seconds = sol.SolutionInfo.Runtime;
615
+ let status = sol.SolutionInfo.Status;
615
616
  // NOTE: Status = 2 indicates success!
616
- if(sol.SolutionInfo.Status !== 2) {
617
- result.status = sol.SolutionInfo.Status;
618
- result.error = s.errors[result.status];
617
+ if(status !== 2) {
618
+ let msg = s.statusMessage(status);
619
+ if(msg) {
620
+ // If solver exited with known status code, report message.
621
+ result.status = status;
622
+ result.solution = s.usableSolution(status);
623
+ result.error = msg;
624
+ }
619
625
  if(!result.error) result.error = 'Unknown solver error';
620
626
  console.log(`Solver status: ${result.status} - ${result.error}`);
621
627
  }
622
628
  // Objective value.
623
- result.obj = sol.SolutionInfo.ObjVal;
629
+ result.obj = sol.SolutionInfo.ObjVal || 0;
624
630
  // Values of solution vector.
625
631
  if(sol.Vars) {
626
632
  // Fill dictionary with variable name: value entries.
@@ -676,21 +682,29 @@ module.exports = class MILPSolver {
676
682
  if(result.status.indexOf('OPTIMAL') >= 0) {
677
683
  result.status = 0;
678
684
  result.error = '';
685
+ } else if(result.status.indexOf('DUAL_INFEASIBLE') >= 0) {
686
+ result.error = 'Problem is unbounded';
687
+ solved = false;
688
+ } else if(result.status.indexOf('INFEASIBLE') >= 0) {
689
+ result.error = 'Problem is infeasible';
690
+ solved = false;
679
691
  }
680
- while(i < output.length && output[i].indexOf('VARIABLES') < 0) {
681
- i++;
682
- }
683
- // Fill dictionary with variable name: value entries.
684
- while(i < output.length) {
685
- const m = output[i].match(/^\d+\s+X(\d+)\s+SB\s+([^\s]+)\s+/);
686
- if(m !== null) {
687
- const vn = 'X' + m[1].padStart(7, '0');
688
- x_dict[vn] = parseFloat(m[2]);
692
+ if(solved) {
693
+ while(i < output.length && output[i].indexOf('VARIABLES') < 0) {
694
+ i++;
689
695
  }
690
- i++;
696
+ // Fill dictionary with variable name: value entries.
697
+ while(i < output.length) {
698
+ const m = output[i].match(/^\d+\s+X(\d+)\s+\w\w\s+([^\s]+)\s+/);
699
+ if(m !== null) {
700
+ const vn = 'X' + m[1].padStart(7, '0');
701
+ x_dict[vn] = parseFloat(m[2]);
702
+ }
703
+ i++;
704
+ }
705
+ // Fill the solution vector, adding 0 for missing columns.
706
+ getValuesFromDict();
691
707
  }
692
- // Fill the solution vector, adding 0 for missing columns.
693
- getValuesFromDict();
694
708
  } else {
695
709
  console.log('No solution found');
696
710
  }
@@ -698,6 +712,14 @@ module.exports = class MILPSolver {
698
712
  result.seconds = 0;
699
713
  const
700
714
  no_license = (log.indexOf('No license found') >= 0),
715
+ // NOTE: Omit first letter U, I and P as they may be either in
716
+ // upper case or lower case.
717
+ unbounded = (log.indexOf('nbounded') >= 0),
718
+ infeasible = (log.indexOf('nfeasible') >= 0),
719
+ primal_unbounded = (log.indexOf('rimal unbounded') >= 0),
720
+ err = log.match(/CPLEX Error\s+(\d+):\s+(.+)\./),
721
+ err_nr = (err && err.length > 1 ? parseInt(err[1]) : 0),
722
+ err_msg = (err_nr ? err[2] : ''),
701
723
  // NOTE: Solver reports time with 1/100 secs precision.
702
724
  mst = log.match(/Solution time \=\s+(\d+\.\d+) sec/);
703
725
  if(mst && mst.length > 1) result.seconds = parseFloat(mst[1]);
@@ -712,6 +734,15 @@ module.exports = class MILPSolver {
712
734
  // Non-zero solver exit code indicates serious trouble.
713
735
  result.error = 'CPLEX solver terminated with error';
714
736
  result.status = -13;
737
+ } else if(err_nr) {
738
+ result.status = err_nr;
739
+ if(infeasible && !primal_unbounded) {
740
+ result.error = 'Problem is infeasible';
741
+ } else if(unbounded) {
742
+ result.error = 'Problem is unbounded';
743
+ } else {
744
+ result.error = err_msg;
745
+ }
715
746
  } else {
716
747
  try {
717
748
  output = fs.readFileSync(s.solution, 'utf8').trim();
@@ -792,7 +823,14 @@ module.exports = class MILPSolver {
792
823
  }
793
824
  }
794
825
  if(result.status) {
795
- result.error = this.solver_list.scip.errors[result.status];
826
+ let msg = s.statusMessage(result.status);
827
+ if(msg) {
828
+ // If solver exited with known status code, report message.
829
+ result.solution = s.usableSolution(result.status);
830
+ result.error = msg;
831
+ }
832
+ if(!result.error) result.error = 'Unknown solver error';
833
+ console.log(`Solver status: ${result.status} - ${result.error}`);
796
834
  }
797
835
  } else if (m.startsWith('Solving Time')) {
798
836
  result.seconds = parseFloat(m.split(':')[1]);
@@ -104,6 +104,7 @@ class LinnyRModel {
104
104
  this.preferred_solver = ''; // empty string denotes "use default"
105
105
  this.integer_tolerance = 5e-7; // integer feasibility tolerance
106
106
  this.MIP_gap = 1e-4; // relative MIP gap
107
+ this.always_diagnose = false;
107
108
 
108
109
  // Sensitivity-related properties
109
110
  this.base_case_selectors = '';
@@ -2666,6 +2667,7 @@ class LinnyRModel {
2666
2667
  this.infer_cost_prices = nodeParameterValue(node, 'cost-prices') === '1';
2667
2668
  this.report_results = nodeParameterValue(node, 'report-results') === '1';
2668
2669
  this.show_block_arrows = nodeParameterValue(node, 'block-arrows') === '1';
2670
+ this.always_diagnose = nodeParameterValue(node, 'diagnose') === '1';
2669
2671
  this.name = xmlDecoded(nodeContentByTag(node, 'name'));
2670
2672
  this.author = xmlDecoded(nodeContentByTag(node, 'author'));
2671
2673
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
@@ -3014,6 +3016,7 @@ class LinnyRModel {
3014
3016
  if(this.infer_cost_prices) p += ' cost-prices="1"';
3015
3017
  if(this.report_results) p += ' report-results="1"';
3016
3018
  if(this.show_block_arrows) p += ' block-arrows="1"';
3019
+ if(this.always_diagnose) p += ' diagnose="1"';
3017
3020
  let xml = this.xml_header + ['<model', p, '><name>', xmlEncoded(this.name),
3018
3021
  '</name><author>', xmlEncoded(this.author),
3019
3022
  '</author><notes>', xmlEncoded(this.comments),
@@ -6302,7 +6305,9 @@ class Cluster extends NodeBox {
6302
6305
  }
6303
6306
 
6304
6307
  usesSlack(t, p, slack_type) {
6305
- // Adds slack-using product `p` to slack info for this cluster
6308
+ // Adds slack-using product `p` to slack info for this cluster.
6309
+ // NOTE: When diagnosing an unbounded problem, `p` can also be a
6310
+ // process with an infinite level.
6306
6311
  let s;
6307
6312
  if(t in this.slack_info) {
6308
6313
  s = this.slack_info[t];
@@ -6311,6 +6316,7 @@ class Cluster extends NodeBox {
6311
6316
  this.slack_info[t] = s;
6312
6317
  }
6313
6318
  addDistinct(p, s[slack_type]);
6319
+ // NOTE: Recursive call to let the slack use info "bubble up".
6314
6320
  if(this.cluster) this.cluster.usesSlack(t, p, slack_type);
6315
6321
  }
6316
6322
 
@@ -2078,6 +2078,15 @@ class VirtualMachine {
2078
2078
  // so far, type is always HI (highest increment); object can be
2079
2079
  // a process or a product.
2080
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;
2081
2090
  // Array for VM instructions.
2082
2091
  this.code = [];
2083
2092
  // The Simplex tableau: matrix, rhs and ct will have same length.
@@ -2112,17 +2121,23 @@ class VirtualMachine {
2112
2121
  // Floating-point constants used in calculations
2113
2122
  // Meaningful solver results are assumed to lie wihin reasonable bounds.
2114
2123
  // Extreme absolute values (10^25 and above) are used to signal particular
2115
- // outcomes. This 10^25 limit is used because the default MILP solver
2116
- // LP_solve considers a problem to be unbounded if decision variables
2117
- // reach +INF (1e+30) or -INF (-1e+30), and a solution inaccurate if
2118
- // extreme values get too close to +/-INF. The higher values have been
2119
- // 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.
2120
2129
  this.PLUS_INFINITY = 1e+25;
2121
2130
  this.MINUS_INFINITY = -1e+25;
2122
2131
  this.BEYOND_PLUS_INFINITY = 1e+35;
2123
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.
2124
2135
  this.SOLVER_PLUS_INFINITY = 1e+30;
2125
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;
2126
2141
  // NOTE: Below the "near zero" limit, a number is considered zero
2127
2142
  // (this is to timely detect division-by-zero errors).
2128
2143
  this.NEAR_ZERO = 1e-10;
@@ -2345,17 +2360,6 @@ class VirtualMachine {
2345
2360
  selectSolver(id) {
2346
2361
  if(id in this.solver_names) {
2347
2362
  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
2363
  } else {
2360
2364
  UI.alert(`Invalid solver ID "${id}"`);
2361
2365
  }
@@ -2996,7 +3000,7 @@ class VirtualMachine {
2996
3000
  // to respect certain constraints. This may result in infeasible
2997
3001
  // MILP problems. The solver will report this, but provide no
2998
3002
  // clue as to which constraints may be critical.
2999
- if(p instanceof Product && !p.no_slack) {
3003
+ if(p instanceof Product && this.diagnose && !p.no_slack) {
3000
3004
  p.stock_LE_slack_var_index = this.addVariable('LE', p);
3001
3005
  p.stock_GE_slack_var_index = this.addVariable('GE', p);
3002
3006
  }
@@ -3125,7 +3129,7 @@ class VirtualMachine {
3125
3129
  );
3126
3130
  // ... or if P is neither source nor sink.
3127
3131
  } else if(p.equal_bounds && notsrc && notsnk) {
3128
- if(!p.no_slack) {
3132
+ if(this.diagnose && !p.no_slack) {
3129
3133
  // NOTE: For EQ, both slack variables should be used, having
3130
3134
  // respectively -1 and +1 as coefficients.
3131
3135
  this.code.push(
@@ -3141,7 +3145,7 @@ class VirtualMachine {
3141
3145
  } else {
3142
3146
  // Add lower bound (GE) constraint unless product is a source node.
3143
3147
  if(notsrc) {
3144
- if(!p.no_slack) {
3148
+ if(this.diagnose && !p.no_slack) {
3145
3149
  // Add the GE slack index with coefficient +1 (so it can
3146
3150
  // INcrease the left-hand side of the equation)
3147
3151
  this.code.push([VMI_add_const_to_coefficient, [gesvi, 1]]);
@@ -3154,7 +3158,7 @@ class VirtualMachine {
3154
3158
  }
3155
3159
  // Add upper bound (LE) constraint unless product is a sink node
3156
3160
  if(notsnk) {
3157
- if(!p.no_slack) {
3161
+ if(this.diagnose && !p.no_slack) {
3158
3162
  // Add the stock LE index with coefficient -1 (so it can
3159
3163
  // DEcrease the LHS).
3160
3164
  this.code.push([VMI_add_const_to_coefficient, [lesvi, -1]]);
@@ -3189,6 +3193,14 @@ class VirtualMachine {
3189
3193
  this.fixed_var_indices.push([]);
3190
3194
  }
3191
3195
 
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
+
3192
3204
  // Just in case: re-determine which entities can be ignored.
3193
3205
  MODEL.inferIgnoredEntities();
3194
3206
  const n = Object.keys(MODEL.ignored_entities).length;
@@ -3254,7 +3266,7 @@ class VirtualMachine {
3254
3266
  // solver does not support special ordered sets).
3255
3267
  // NOTE: `addVariable` will add as many as there are points!
3256
3268
  bl.first_sos_var_index = this.addVariable('W1', bl);
3257
- if(!c.no_slack) {
3269
+ if(this.diagnose && !c.no_slack) {
3258
3270
  // Define the slack variable(s) for bound line constraints.
3259
3271
  // NOTE: Category [2] means: highest slack penalty.
3260
3272
  if(bl.type !== VM.GE) {
@@ -3375,7 +3387,7 @@ class VirtualMachine {
3375
3387
  p = MODEL.products[k];
3376
3388
  // Get index of variable that is constrained by LB and UB.
3377
3389
  vi = p.level_var_index;
3378
- if(p.no_slack) {
3390
+ if(p.no_slack || !this.diagnose) {
3379
3391
  // If no slack, the bound constraints can be set on the
3380
3392
  // variables themselves.
3381
3393
  lbx = p.lower_bound;
@@ -3658,7 +3670,7 @@ class VirtualMachine {
3658
3670
  k = product_keys[i];
3659
3671
  if(!MODEL.ignored_entities[k]) {
3660
3672
  p = MODEL.products[k];
3661
- if(p.level_var_index >= 0 && !p.no_slack) {
3673
+ if(p.level_var_index >= 0 && !p.no_slack && this.diagnose) {
3662
3674
  hb = p.hasBounds;
3663
3675
  pen = (p.is_data ? 2 :
3664
3676
  // NOTE: Lowest penalty also for IMPLIED sources and sinks.
@@ -4605,7 +4617,8 @@ class VirtualMachine {
4605
4617
  // Compute the peak from the peak increase.
4606
4618
  p.b_peak[block] = p.b_peak[block - 1] + p.b_peak_inc[block];
4607
4619
  }
4608
- // 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.
4609
4622
  // NOTE: Only check after the last round has been evaluated.
4610
4623
  if(round === this.lastRound) {
4611
4624
  let b = bb;
@@ -4646,6 +4659,27 @@ class VirtualMachine {
4646
4659
  }
4647
4660
  }
4648
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
+ }
4649
4683
  j += this.cols;
4650
4684
  b++;
4651
4685
  }
@@ -5878,6 +5912,9 @@ Solver status = ${json.status}`);
5878
5912
  }
5879
5913
  this.logMessage(bnr, errmsg);
5880
5914
  UI.alert(errmsg);
5915
+ if(errmsg.indexOf('nfeasible') >= 0 || errmsg.indexOf('nbounded') >= 0) {
5916
+ this.prompt_to_diagnose = true;
5917
+ }
5881
5918
  }
5882
5919
  this.logMessage(bnr, msg);
5883
5920
  this.equations[bnr - 1] = json.model;
@@ -5936,7 +5973,12 @@ Solver status = ${json.status}`);
5936
5973
  RECEIVER.report();
5937
5974
  }
5938
5975
  // Warn modeler if any issues occurred.
5939
- 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) {
5940
5982
  let msg = 'Issues occurred in ' +
5941
5983
  pluralS(this.block_issues, 'block') +
5942
5984
  ' -- details can be viewed in the monitor';
@@ -6091,7 +6133,7 @@ Solver status = ${json.status}`);
6091
6133
  this.solveBlocks();
6092
6134
  }
6093
6135
 
6094
- solveModel() {
6136
+ solveModel(diagnose=false) {
6095
6137
  // Start the sequence of data loading, model translation, solving
6096
6138
  // consecutive blocks, and finally calculating dependent variables.
6097
6139
  // NOTE: Do this only if the model defines a MILP problem, i.e.,
@@ -6101,6 +6143,22 @@ Solver status = ${json.status}`);
6101
6143
  UI.notify('Nothing to solve');
6102
6144
  return;
6103
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;
6104
6162
  const n = MODEL.loading_datasets.length;
6105
6163
  if(n > 0) {
6106
6164
  // Still within reasonable time? (3 seconds per dataset)
@@ -7784,10 +7842,11 @@ function VMI_set_bounds(args) {
7784
7842
  vbl = VM.variables[vi - 1][1],
7785
7843
  k = VM.offset + vi,
7786
7844
  r = VM.round_letters.indexOf(VM.round_sequence[VM.current_round]),
7787
- // Optional fourth parameter indicates whether the solver's
7788
- // infinity values should be used.
7789
- solver_inf = args.length > 3 && args[3],
7790
- 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);
7791
7850
  let l,
7792
7851
  u,
7793
7852
  fixed = (vi in VM.fixed_var_indices[r - 1]);
@@ -7812,17 +7871,15 @@ function VMI_set_bounds(args) {
7812
7871
  u = args[2];
7813
7872
  if(u instanceof Expression) u = u.result(VM.t);
7814
7873
  u = Math.min(u, VM.PLUS_INFINITY);
7815
- if(solver_inf) {
7816
- if(l === VM.MINUS_INFINITY) l = -inf_val;
7817
- if(u === VM.PLUS_INFINITY) u = inf_val;
7818
- }
7874
+ if(l === VM.MINUS_INFINITY) l = -inf_val;
7875
+ if(u === VM.PLUS_INFINITY) u = inf_val;
7819
7876
  fixed = '';
7820
7877
  }
7821
7878
  // NOTE: To see in the console whether fixing across rounds works, insert
7822
7879
  // "fixed !== '' || " before DEBUGGING below.
7823
7880
  if(DEBUGGING) {
7824
7881
  console.log(['set_bounds [', k, '] ', vbl.displayName, ' t = ', VM.t,
7825
- ' 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(''));
7826
7883
  }
7827
7884
  // NOTE: Since the VM vectors for lower bounds and upper bounds are
7828
7885
  // initialized with default values (0 for LB, +INF for UB), there is
@@ -8327,7 +8384,15 @@ function VMI_add_constraint(ct) {
8327
8384
  }
8328
8385
  }
8329
8386
  VM.matrix.push(row);
8330
- 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);
8331
8396
  VM.constraint_types.push(ct);
8332
8397
  } else if(DEBUGGING) {
8333
8398
  console.log('Constraint NOT added!');
@@ -8480,7 +8545,7 @@ function VMI_add_bound_line_constraint(args) {
8480
8545
  for(let i = 0; i < w.length; i++) {
8481
8546
  VM.coefficients[w[i]] = -y[i];
8482
8547
  }
8483
- if(!bl.constraint.no_slack) {
8548
+ if(VM.diagnose && !bl.constraint.no_slack) {
8484
8549
  // Add coefficients for slack variables unless omitted.
8485
8550
  if(bl.type != VM.LE) VM.coefficients[VM.offset + bl.GE_slack_var_index] = 1;
8486
8551
  if(bl.type != VM.GE) VM.coefficients[VM.offset + bl.LE_slack_var_index] = -1;