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 +10 -10
- package/package.json +1 -1
- package/static/index.html +12 -5
- package/static/linny-r.css +11 -1
- package/static/scripts/linny-r-gui-controller.js +39 -10
- package/static/scripts/linny-r-gui-paper.js +13 -6
- package/static/scripts/linny-r-milp.js +55 -17
- package/static/scripts/linny-r-model.js +7 -1
- package/static/scripts/linny-r-vm.js +102 -37
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
|
-
>
|
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
|
-
>
|
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
|
-
>
|
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
|
-
>
|
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
|
-
>
|
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
|
-
>
|
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
|
-
>
|
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
|
-
>
|
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
|
-
>
|
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
|
-
>
|
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
package/static/index.html
CHANGED
@@ -723,26 +723,33 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ 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:
|
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:
|
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>
|
package/static/linny-r.css
CHANGED
@@ -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:
|
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',
|
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
|
-
|
2687
|
-
|
2688
|
-
|
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
|
-
|
2695
|
-
|
2696
|
-
|
2697
|
-
|
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
|
304
|
-
|
305
|
-
|
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.
|
1983
|
-
fill_color = this.palette.
|
1984
|
-
lrect_color = this.palette.
|
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(
|
617
|
-
|
618
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
2116
|
-
// LP_solve considers a problem to be unbounded if
|
2117
|
-
// reach +INF (1e+30) or -INF (-1e+30), and a solution
|
2118
|
-
// extreme values get too close to +/-INF. The higher
|
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.
|
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
|
-
//
|
7788
|
-
//
|
7789
|
-
|
7790
|
-
inf_val = (
|
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(
|
7816
|
-
|
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.
|
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;
|