linny-r 1.7.4 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,7 +66,7 @@ and then type at the command line prompt:
66
66
 
67
67
  ``npm install --prefix . linny-r``
68
68
 
69
- > **Important**
69
+ > [!IMPORTANT]
70
70
  > The spacing around the dot is essential. Type the command in lower case.
71
71
 
72
72
  After installation has completed, `Linny-R` should have this directory tree structure:
@@ -98,7 +98,7 @@ on a Windows machine the batch script `linny-r.bat`. By default, this script fil
98
98
  two commands: first change to the Linny-R directory and then tell Node.js to launch the
99
99
  start the Linny-R server.
100
100
 
101
- > **Note**
101
+ > [!NOTE]
102
102
  > When configuring Linny-R for a network environment where individual users
103
103
  > each have their personal work space (e.g., a virtual drive U:), you must edit this script file,
104
104
  > adding the argument `workspace=path/to/workspace` to the `node` command.
@@ -129,7 +129,7 @@ version 1.4.0, open the CLI, change to your `Linny-R` directory, and then type:
129
129
 
130
130
  ``npm install linny-r@1.4.0``
131
131
 
132
- > **Note**
132
+ > [!NOTE]
133
133
  > This will overwrite the contents of the `node_modules` directory, but
134
134
  > it will not affect the files in your user space.
135
135
 
@@ -139,7 +139,7 @@ directory and type:
139
139
 
140
140
  ``npm install --prefix . linny-r@1.4.0``
141
141
 
142
- > **Note**
142
+ > [!NOTE]
143
143
  > To run a specific version in your browser, you must start the server from
144
144
  > the directory where you installed this version.
145
145
  > Should you wish to run two different versions concurrently, you must use
@@ -152,7 +152,7 @@ Gurobi, MOSEK and CPLEX are _considerably_ more powerful than the open source so
152
152
  but they require a license.
153
153
  Academic licenses can be obtained by students and staff of eligible institutions.
154
154
 
155
- > **Important**
155
+ > [!IMPORTANT]
156
156
  > When installing a solver, it is advisable to accept the default file
157
157
  > locations that are proposed by the installer.
158
158
  > After installation, do **not** move files to some other directory,
@@ -262,7 +262,7 @@ while in the CLI you should see a long series of server log messages like:
262
262
  ... etc.
263
263
  </pre>
264
264
 
265
- > **Important**
265
+ > [!IMPORTANT]
266
266
  > Do **not** close the CLI. If you do, the Linny-R GUI may still be
267
267
  > visible in your browser, but you will be warned that it cannot connect
268
268
  > to the server (at 127.0.0.1:5050). This means that you have to restart
@@ -348,7 +348,7 @@ The sub-directories of this directory `user` are used by Linny-R to store files.
348
348
  * `solver` will contain the files that are exchanged with the Mixed Integer Linear Programming (MILP) solver
349
349
  (the names of the files that will appear in this directory may vary, depending on the MILP-solver you use)
350
350
 
351
- > **Note**
351
+ > [!NOTE]
352
352
  > By default, the `user` directory is created in your `Linny-R` directory.
353
353
  > You can overrule this by starting the server with the `workspace=[path]` option.
354
354
  > This will create a new, empty workspace (the directories listed above) in the specified path.
@@ -381,7 +381,7 @@ Linny-R will automatically detect whether Inkscape is installed by searching
381
381
  for it in the environment variable PATH on your computer. On a macOS computer,
382
382
  Linny-R will look for Inkscape in `/Applications/Inkscape.app/Contents/MacOS`.
383
383
 
384
- > **Note**
384
+ > [!NOTE]
385
385
  > The installation wizard for Inkscape (version 1.3) may **not**
386
386
  > add the application to the PATH variable. Please check whether you need to
387
387
  > do this yourself.
@@ -396,7 +396,7 @@ If you open a CLI box, change to your `Linny-R` directory, and then type:
396
396
 
397
397
  you will see the command line options that allow you to run models in various ways.
398
398
 
399
- > **Note**
399
+ > [!NOTE]
400
400
  > The console-only version is still in development, and does not provide all functions yet.
401
401
 
402
402
  ## Troubleshooting problems
@@ -405,7 +405,7 @@ If during any of the steps above you encounter problems, please try to diagnose
405
405
  You can find a lot of useful information on the Linny-R user documentation website:
406
406
  <a href="https://linny-r.info" target="_blank">https://linny-r.info</a>.
407
407
 
408
- > **Important**
408
+ > [!IMPORTANT]
409
409
  > To diagnose a problem, always look in the CLI box where Node.js is running,
410
410
  > as informative server-side error messages will appear there.
411
411
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.7.4",
3
+ "version": "1.8.0",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/static/index.html CHANGED
@@ -723,26 +723,33 @@ NOTE: Unit symbols are case-sensitive, so BTU &ne; Btu">
723
723
  </div>
724
724
  <table style="width: 100%">
725
725
  <tr title="This solver will be used if it is installed">
726
- <td>
726
+ <td colspan="2">
727
727
  <label>Preferred solver:</label>
728
728
  <select id="solver-preference">
729
729
  </select>
730
730
  </td>
731
731
  </tr>
732
732
  <tr title="Tolerance may range from 1e-9 to 0.1">
733
- <td>
733
+ <td colspan="2">
734
734
  <label>Integer feasibility tolerance:</label>
735
- <input id="solver-int-feasibility" style="width: 55px"
735
+ <input id="solver-int-feasibility" style="width: 65px"
736
736
  placeholder="5e-7" type="text" autocomplete="off">
737
737
  </td>
738
738
  </tr>
739
739
  <tr title="Relative gap may range from 0 to 0.5">
740
- <td>
740
+ <td colspan="2">
741
741
  <label>Relative MIP gap:</label>
742
- <input id="solver-mip-gap" style="width: 55px"
742
+ <input id="solver-mip-gap" style="width: 65px"
743
743
  placeholder="1e-4" type="text" autocomplete="off">
744
744
  </td>
745
745
  </tr>
746
+ <tr title="When checked, finite process bounds and slack variables are always added">
747
+ <td style="padding:0px">
748
+ <div id="solver-diagnose" class="box clear"></div>
749
+ </td>
750
+ <td style="padding-bottom:4px">Diagnose infeasible/unbounded problems</td>
751
+ </td>
752
+ </tr>
746
753
  </table>
747
754
  </div>
748
755
  </div>
@@ -313,6 +313,15 @@ img.sbtn.senab:hover {
313
313
  filter: brightness(150%);
314
314
  }
315
315
 
316
+ img.sgray {
317
+ width: 16px;
318
+ height: 16px;
319
+ margin: -1px;
320
+ vertical-align: middle;
321
+ filter: grayscale(100%);
322
+ }
323
+
324
+
316
325
  /* Bounds button indicates whether LB = UB */
317
326
  div.bbtn {
318
327
  background-size: contain;
@@ -1002,7 +1011,7 @@ input.pws-5 {
1002
1011
  }
1003
1012
 
1004
1013
  #solver-dlg {
1005
- width: 212px;
1014
+ width: 250px;
1006
1015
  height: min-content;
1007
1016
  }
1008
1017
 
@@ -2727,6 +2736,7 @@ td.equation-expression {
2727
2736
  top: 134px;
2728
2737
  left: 2px;
2729
2738
  width: calc(100% - 5px);
2739
+ white-space: nowrap;
2730
2740
  }
2731
2741
 
2732
2742
  #series-clip {
@@ -501,6 +501,9 @@ class GUIController extends Controller {
501
501
 
502
502
  // Visible draggable dialogs are sorted by their z-index.
503
503
  this.dr_dialog_order = [];
504
+
505
+ // Record of message that was overridden by more important message.
506
+ this.old_info_line = null;
504
507
  }
505
508
 
506
509
  get color() {
@@ -617,7 +620,8 @@ class GUIController extends Controller {
617
620
  UI.updateButtons();
618
621
  }
619
622
  });
620
- this.buttons.solve.addEventListener('click', () => VM.solveModel());
623
+ this.buttons.solve.addEventListener('click',
624
+ (event) => VM.solveModel(event.altKey));
621
625
  this.buttons.stop.addEventListener('click', () => VM.halt());
622
626
  this.buttons.reset.addEventListener('click', () => UI.resetModel());
623
627
 
@@ -2665,12 +2669,13 @@ class GUIController extends Controller {
2665
2669
 
2666
2670
  setMessage(msg, type=null) {
2667
2671
  // Displays message on infoline unless no type (= plain text) and some
2668
- // info, warning or error message is already displayed
2672
+ // info, warning or error message is already displayed.
2669
2673
  super.setMessage(msg, type);
2670
2674
  const types = ['notification', 'warning', 'error'];
2671
2675
  let d = new Date(),
2672
2676
  t = d.getTime(),
2673
- dt = t - this.time_last_message,
2677
+ dt = t - this.time_last_message, // Time since display
2678
+ rt = this.message_display_time - dt, // Time remaining
2674
2679
  mti = types.indexOf(type),
2675
2680
  lmti = types.indexOf(this.last_message_type);
2676
2681
  if(type) {
@@ -2683,18 +2688,36 @@ class GUIController extends Controller {
2683
2688
  // When receiver is active, add message to its log.
2684
2689
  if(RECEIVER.active) RECEIVER.log(`[${now}] ${msg}`);
2685
2690
  }
2686
- // Display text only if previous message has "timed out" or was less
2687
- // urgent than this one.
2688
- if(lmti < 0 || mti > lmti || dt >= this.message_display_time) {
2691
+ if(mti === 1 && lmti === 2 && rt > 0) {
2692
+ // Queue warnings if an error message is still being displayed.
2693
+ setTimeout(() => {
2694
+ UI.info_line.innerHTML = msg;
2695
+ UI.info_line.classList.remove(...types);
2696
+ UI.info_line.classList.add(type);
2697
+ UI.updateIssuePanel();
2698
+ }, rt);
2699
+ } else if(lmti < 0 || mti > lmti || rt <= 0) {
2700
+ // Display text only if previous message has "timed out" or was less
2701
+ // urgent than this one.
2702
+ const override = mti === 2 && lmti === 1 && rt > 0;
2689
2703
  this.time_last_message = t;
2690
2704
  this.last_message_type = type;
2691
2705
  if(type) SOUNDS[type].play().catch(() => {
2692
2706
  console.log('NOTICE: Sounds will only play after first user action');
2693
2707
  });
2694
- const il = document.getElementById('info-line');
2695
- il.classList.remove(...types);
2696
- il.classList.add(type);
2697
- il.innerHTML = msg;
2708
+ if(override && !this.old_info_line) {
2709
+ // Set time-out to restore overridden warning.
2710
+ this.old_info_line = {msg: this.info_line.innerHTML, status: types[lmti]};
2711
+ setTimeout(() => {
2712
+ UI.info_line.innerHTML = UI.old_info_line.msg;
2713
+ UI.info_line.classList.add(UI.old_info_line.status);
2714
+ UI.old_info_line = null;
2715
+ UI.updateIssuePanel();
2716
+ }, this.message_display_time);
2717
+ }
2718
+ UI.info_line.classList.remove(...types);
2719
+ UI.info_line.classList.add(type);
2720
+ UI.info_line.innerHTML = msg;
2698
2721
  }
2699
2722
  }
2700
2723
 
@@ -3630,6 +3653,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3630
3653
  md.element('preference').innerHTML = html.join('');
3631
3654
  md.element('int-feasibility').value = MODEL.integer_tolerance;
3632
3655
  md.element('mip-gap').value = MODEL.MIP_gap;
3656
+ this.setBox('solver-diagnose', MODEL.always_diagnose);
3633
3657
  md.show();
3634
3658
  }
3635
3659
 
@@ -3658,6 +3682,11 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3658
3682
  }
3659
3683
  MODEL.integer_tolerance = Math.max(1e-9, Math.min(0.1, itol));
3660
3684
  MODEL.MIP_gap = Math.max(0, Math.min(0.5, mgap));
3685
+ MODEL.always_diagnose = this.boxChecked('solver-diagnose');
3686
+ if(MODEL.always_diagnose) {
3687
+ UI.notify('To diagnose unbounded problems, values beyond 1e+10 ' +
3688
+ 'are considered as infinite (\u221E)');
3689
+ }
3661
3690
  // Close the dialog.
3662
3691
  md.hide();
3663
3692
  }
@@ -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;