linny-r 1.7.3 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -22
- package/console.js +2 -2
- package/package.json +1 -1
- package/server.js +2 -2
- package/static/index.html +13 -5
- package/static/linny-r.css +21 -2
- package/static/scripts/linny-r-ctrl.js +1 -1
- package/static/scripts/linny-r-gui-constraint-editor.js +15 -2
- package/static/scripts/linny-r-gui-controller.js +45 -13
- package/static/scripts/linny-r-gui-equation-manager.js +6 -6
- package/static/scripts/linny-r-gui-expression-editor.js +6 -3
- package/static/scripts/linny-r-gui-finder.js +1 -1
- package/static/scripts/linny-r-gui-monitor.js +5 -5
- package/static/scripts/linny-r-gui-paper.js +13 -6
- package/static/scripts/linny-r-milp.js +306 -86
- package/static/scripts/linny-r-model.js +75 -22
- package/static/scripts/linny-r-vm.js +354 -127
package/README.md
CHANGED
@@ -30,7 +30,7 @@ Technical documentation will be developed on GitHub: https://github.com/pwgbots/
|
|
30
30
|
Linny-R is developed as a JavaScript package, and requires that **Node.js** is installed on your computer.
|
31
31
|
This software can be downloaded from <a href="https://nodejs.org" target="_blank">https://nodejs.org</a>.
|
32
32
|
Make sure that you choose the correct installer for your computer.
|
33
|
-
Linny-R is developed using the _current_ release. Presently (
|
33
|
+
Linny-R is developed using the _current_ release. Presently (November 2023) this is 21.2.0.
|
34
34
|
|
35
35
|
Run the installer and accept the default settings.
|
36
36
|
There is **no** need to install the optional _Tools for Native Modules_.
|
@@ -41,7 +41,7 @@ Verify the installation by typing:
|
|
41
41
|
|
42
42
|
``node --version``
|
43
43
|
|
44
|
-
The response should be the version number of Node.js, for example: v21.
|
44
|
+
The response should be the version number of Node.js, for example: v21.2.0.
|
45
45
|
|
46
46
|
## Installing Linny-R
|
47
47
|
It is advisable to install Linny-R in a directory on your computer, not in a cloud.
|
@@ -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
|
@@ -147,12 +147,12 @@ directory and type:
|
|
147
147
|
|
148
148
|
## Configuring the MILP solver
|
149
149
|
|
150
|
-
Linny-R presently supports
|
151
|
-
Gurobi and CPLEX are _considerably_ more powerful than the open source solvers SCIP and LP_solve,
|
150
|
+
Linny-R presently supports five MILP solvers: Gurobi, MOSEK, CPLEX, SCIP and LP_solve.
|
151
|
+
Gurobi, MOSEK and CPLEX are _considerably_ more powerful than the open source solvers SCIP and LP_solve,
|
152
152
|
but they require a license.
|
153
|
-
Academic licenses can be obtained by students and staff of eligible institutions.
|
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,
|
@@ -182,6 +182,17 @@ It will look for this application in the directory specified in the environment
|
|
182
182
|
or more specifically in the environment variable CPLEX_STUDIO_BINARIES<em>nnnn</em>
|
183
183
|
(where _nnnn_ denotes the CPLEX version number) on your computer.
|
184
184
|
|
185
|
+
#### Installing MOSEK
|
186
|
+
|
187
|
+
The software you need to install is **MOSEK**.
|
188
|
+
More information on how to obtain a license, and instructions for installing
|
189
|
+
MOSEK on your computer can be obtained via this URL:
|
190
|
+
<a href="https://www.mosek.com/resources/getting-started/"
|
191
|
+
target="_blank">https://www.mosek.com/resources/getting-started/</a>
|
192
|
+
|
193
|
+
When running a model, Linny-R will try to execute the command line application `mosek`.
|
194
|
+
It will look for this application in the directory specified in the environment variable PATH on your computer.
|
195
|
+
|
185
196
|
#### Installing SCIP
|
186
197
|
|
187
198
|
The SCIP software is open source. Instructions for installation can be found via this URL:
|
@@ -234,8 +245,8 @@ Open the Command Line Interface (CLI) of your computer, change to your Linny-R d
|
|
234
245
|
This response should be something similar to:
|
235
246
|
|
236
247
|
<pre>
|
237
|
-
Node.js server for Linny-R version 1.7.
|
238
|
-
Node.js version: v21.
|
248
|
+
Node.js server for Linny-R version 1.7.4
|
249
|
+
Node.js version: v21.2.0
|
239
250
|
... etc.
|
240
251
|
</pre>
|
241
252
|
|
@@ -245,13 +256,13 @@ The Linny-R GUI should show in your browser window,
|
|
245
256
|
while in the CLI you should see a long series of server log messages like:
|
246
257
|
|
247
258
|
<pre>
|
248
|
-
[2023-
|
249
|
-
[2023-
|
250
|
-
[2023-
|
259
|
+
[2023-11-19 22:55:17] Static file: /index.html
|
260
|
+
[2023-11-19 22:55:17] Static file: /scripts/iro.min.js
|
261
|
+
[2023-11-19 22:55:17] Static file: /images/open.png
|
251
262
|
... etc.
|
252
263
|
</pre>
|
253
264
|
|
254
|
-
>
|
265
|
+
> [!IMPORTANT]
|
255
266
|
> Do **not** close the CLI. If you do, the Linny-R GUI may still be
|
256
267
|
> visible in your browser, but you will be warned that it cannot connect
|
257
268
|
> to the server (at 127.0.0.1:5050). This means that you have to restart
|
@@ -278,7 +289,7 @@ and then the diagram will be updated to reflect the obtained solution.
|
|
278
289
|
Meanwhile, in the CLI, you should see a server log message like:
|
279
290
|
|
280
291
|
<pre>
|
281
|
-
Solve block 1 a
|
292
|
+
Solve block 1 a with SCIP
|
282
293
|
</pre>
|
283
294
|
|
284
295
|
To end a modeling session, you can shut down the server by clicking on the
|
@@ -295,7 +306,7 @@ Optionally, you can add more arguments to the `node` command:
|
|
295
306
|
dpi=[number] to overrule the default resolution (300 dpi) for Inkscape
|
296
307
|
launch to automatically launch Linny-R in your default browser
|
297
308
|
port=[number] to overrule the default port number (5050)
|
298
|
-
solver=[name] to overrule the default sequence (Gurobi, CPLEX, SCIP, LP_solve)
|
309
|
+
solver=[name] to overrule the default sequence (Gurobi, MOSEK, CPLEX, SCIP, LP_solve)
|
299
310
|
workspace=[path] to overrule the default path for the user directory
|
300
311
|
</pre>
|
301
312
|
|
@@ -337,7 +348,7 @@ The sub-directories of this directory `user` are used by Linny-R to store files.
|
|
337
348
|
* `solver` will contain the files that are exchanged with the Mixed Integer Linear Programming (MILP) solver
|
338
349
|
(the names of the files that will appear in this directory may vary, depending on the MILP-solver you use)
|
339
350
|
|
340
|
-
>
|
351
|
+
> [!NOTE]
|
341
352
|
> By default, the `user` directory is created in your `Linny-R` directory.
|
342
353
|
> You can overrule this by starting the server with the `workspace=[path]` option.
|
343
354
|
> This will create a new, empty workspace (the directories listed above) in the specified path.
|
@@ -370,7 +381,7 @@ Linny-R will automatically detect whether Inkscape is installed by searching
|
|
370
381
|
for it in the environment variable PATH on your computer. On a macOS computer,
|
371
382
|
Linny-R will look for Inkscape in `/Applications/Inkscape.app/Contents/MacOS`.
|
372
383
|
|
373
|
-
>
|
384
|
+
> [!NOTE]
|
374
385
|
> The installation wizard for Inkscape (version 1.3) may **not**
|
375
386
|
> add the application to the PATH variable. Please check whether you need to
|
376
387
|
> do this yourself.
|
@@ -385,7 +396,7 @@ If you open a CLI box, change to your `Linny-R` directory, and then type:
|
|
385
396
|
|
386
397
|
you will see the command line options that allow you to run models in various ways.
|
387
398
|
|
388
|
-
>
|
399
|
+
> [!NOTE]
|
389
400
|
> The console-only version is still in development, and does not provide all functions yet.
|
390
401
|
|
391
402
|
## Troubleshooting problems
|
@@ -394,7 +405,7 @@ If during any of the steps above you encounter problems, please try to diagnose
|
|
394
405
|
You can find a lot of useful information on the Linny-R user documentation website:
|
395
406
|
<a href="https://linny-r.info" target="_blank">https://linny-r.info</a>.
|
396
407
|
|
397
|
-
>
|
408
|
+
> [!IMPORTANT]
|
398
409
|
> To diagnose a problem, always look in the CLI box where Node.js is running,
|
399
410
|
> as informative server-side error messages will appear there.
|
400
411
|
|
package/console.js
CHANGED
@@ -198,7 +198,7 @@ class ConsoleMonitor {
|
|
198
198
|
logOnToServer() {
|
199
199
|
VM.solver_user = '';
|
200
200
|
VM.solver_token = 'local host';
|
201
|
-
VM.
|
201
|
+
VM.solver_id = SOLVER.id;
|
202
202
|
}
|
203
203
|
|
204
204
|
connectToServer() {
|
@@ -1081,7 +1081,7 @@ if(SETTINGS.model_path) {
|
|
1081
1081
|
// NOTE: Solver preference in model overrides default solver.
|
1082
1082
|
const mps = MODEL.preferred_solver;
|
1083
1083
|
if(mps && SOLVER.solver_list.hasOwnProperty(mps)) {
|
1084
|
-
VM.
|
1084
|
+
VM.solver_id = mps;
|
1085
1085
|
SOLVER.id = mps;
|
1086
1086
|
console.log(`Using solver ${SOLVER.name} (model preference)`);
|
1087
1087
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
@@ -124,7 +124,7 @@ function checkNodeModule(name) {
|
|
124
124
|
}
|
125
125
|
|
126
126
|
// Currently, these external solvers are supported:
|
127
|
-
const SUPPORTED_SOLVERS = ['gurobi', 'cplex', 'scip', 'lp_solve'];
|
127
|
+
const SUPPORTED_SOLVERS = ['gurobi', 'mosek', 'cplex', 'scip', 'lp_solve'];
|
128
128
|
|
129
129
|
// Load class MILPSolver
|
130
130
|
const MILPSolver = require('./static/scripts/linny-r-milp.js');
|
@@ -1555,7 +1555,7 @@ Possible options are:
|
|
1555
1555
|
port=[number] will listen at the specified port number
|
1556
1556
|
(default is 5050; number must be unique for each server)
|
1557
1557
|
solver=[name] will select solver [name], or warn if not found
|
1558
|
-
(name choices: Gurobi, CPLEX, SCIP or LP_solve)
|
1558
|
+
(name choices: Gurobi, MOSEK, CPLEX, SCIP or LP_solve)
|
1559
1559
|
verbose will output solver messages to the console
|
1560
1560
|
workspace=[path] will create workspace in [path] instead of (Linny-R)/user
|
1561
1561
|
`;
|
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>
|
@@ -1347,6 +1354,7 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ Btu">
|
|
1347
1354
|
<div id="constraint-no-slack" class="box clear"></div>
|
1348
1355
|
<div id="constraint-no-slack-lbl">No slack</div>
|
1349
1356
|
</div>
|
1357
|
+
<div id="constraint-convex"></div>
|
1350
1358
|
<div id="constraint-soc">
|
1351
1359
|
<label>Attributed share of cost:</label>
|
1352
1360
|
<select id="constraint-soc-direct">
|
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
|
|
@@ -1985,6 +1994,7 @@ div.menu-item:hover {
|
|
1985
1994
|
#constraint-from-name,
|
1986
1995
|
#constraint-to-name {
|
1987
1996
|
display: inline-block;
|
1997
|
+
vertical-align: middle;
|
1988
1998
|
max-width: 190px;
|
1989
1999
|
white-space: nowrap;
|
1990
2000
|
overflow: hidden;
|
@@ -2025,7 +2035,15 @@ div.menu-item:hover {
|
|
2025
2035
|
position: absolute;
|
2026
2036
|
left: -2px;
|
2027
2037
|
bottom: -1px;
|
2028
|
-
width:
|
2038
|
+
width: 75px;
|
2039
|
+
}
|
2040
|
+
|
2041
|
+
#constraint-convex {
|
2042
|
+
position: absolute;
|
2043
|
+
left: 75px;
|
2044
|
+
bottom: -2px;
|
2045
|
+
color: #00c030;
|
2046
|
+
font-size: 20px;
|
2029
2047
|
}
|
2030
2048
|
|
2031
2049
|
#constraint-no-slack-lbl {
|
@@ -2718,6 +2736,7 @@ td.equation-expression {
|
|
2718
2736
|
top: 134px;
|
2719
2737
|
left: 2px;
|
2720
2738
|
width: calc(100% - 5px);
|
2739
|
+
white-space: nowrap;
|
2721
2740
|
}
|
2722
2741
|
|
2723
2742
|
#series-clip {
|
@@ -66,6 +66,7 @@ class ConstraintEditor {
|
|
66
66
|
this.pos_y_div = document.getElementById('constraint-pos-y');
|
67
67
|
this.point_div = document.getElementById('constraint-point');
|
68
68
|
this.equation_div = document.getElementById('constraint-equation');
|
69
|
+
this.convex_div = document.getElementById('constraint-convex');
|
69
70
|
this.add_point_btn = document.getElementById('add-point-btn');
|
70
71
|
this.add_point_btn.addEventListener('click',
|
71
72
|
() => CONSTRAINT_EDITOR.addPointToLine());
|
@@ -395,7 +396,7 @@ class ConstraintEditor {
|
|
395
396
|
|
396
397
|
checkLines() {
|
397
398
|
// Checks whether cursor is on a bound line and updates the constraint
|
398
|
-
// editor status accordingly
|
399
|
+
// editor status accordingly.
|
399
400
|
this.on_line = null;
|
400
401
|
this.on_point = -1;
|
401
402
|
this.seg_points = null;
|
@@ -446,7 +447,10 @@ class ConstraintEditor {
|
|
446
447
|
}
|
447
448
|
|
448
449
|
updateEquation() {
|
449
|
-
|
450
|
+
// Show the equation for the line segment under the cursor, and
|
451
|
+
// indicate whether the bound line is concave or convex.
|
452
|
+
var segeq = '',
|
453
|
+
convex = '';
|
450
454
|
if(this.on_line && this.seg_points) {
|
451
455
|
const
|
452
456
|
p1 = this.on_line.points[this.seg_points[0]],
|
@@ -466,7 +470,16 @@ class ConstraintEditor {
|
|
466
470
|
(y0 < 0 ? ' - ' : ' + ') + Math.abs(y0).toPrecision(3));
|
467
471
|
}
|
468
472
|
}
|
473
|
+
if(this.on_line) {
|
474
|
+
const c = this.on_line.needsNoSOS;
|
475
|
+
if(c > 0) {
|
476
|
+
convex = '\u2934'; // Curved arrow up
|
477
|
+
} else if(c < 0) {
|
478
|
+
convex = '\u2935'; // Curved arrow down
|
479
|
+
}
|
480
|
+
}
|
469
481
|
this.equation_div.innerHTML = segeq;
|
482
|
+
this.convex_div.innerHTML = convex;
|
470
483
|
}
|
471
484
|
|
472
485
|
positionPoint() {
|
@@ -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
|
|
@@ -895,13 +899,16 @@ class GUIController extends Controller {
|
|
895
899
|
// If not a valid Linny-R model, ensure that the current model is clean.
|
896
900
|
if(!loaded) MODEL = new LinnyRModel();
|
897
901
|
// If model specifies a preferred solver, immediately try to switch.
|
898
|
-
if(MODEL.preferred_solver !== VM.
|
902
|
+
if(MODEL.preferred_solver !== VM.solver_id) {
|
899
903
|
UI.changeSolver(MODEL.preferred_solver);
|
900
904
|
}
|
901
905
|
this.updateScaleUnitList();
|
902
906
|
this.drawDiagram(MODEL);
|
903
907
|
// Cursor may have been set to `waiting` when decrypting.
|
904
908
|
this.normalCursor();
|
909
|
+
// Reset the Virtual Machine.
|
910
|
+
VM.reset();
|
911
|
+
this.updateIssuePanel();
|
905
912
|
this.setMessage('');
|
906
913
|
this.updateButtons();
|
907
914
|
// Undoable operations no longer apply!
|
@@ -992,7 +999,7 @@ class GUIController extends Controller {
|
|
992
999
|
for(let i = 0; i < VM.solver_list.length; i++) {
|
993
1000
|
const s = VM.solver_list[i];
|
994
1001
|
html.push(['<option value="', s,
|
995
|
-
(s === VM.
|
1002
|
+
(s === VM.solver_id ? '"selected="selected' : ''),
|
996
1003
|
'">', VM.solver_names[s], '</option>'].join(''));
|
997
1004
|
}
|
998
1005
|
md.element('solver').innerHTML = html.join('');
|
@@ -1028,7 +1035,7 @@ class GUIController extends Controller {
|
|
1028
1035
|
})
|
1029
1036
|
.then((data) => {
|
1030
1037
|
if(UI.postResponseOK(data, true)) {
|
1031
|
-
VM.
|
1038
|
+
VM.selectSolver(sid);
|
1032
1039
|
UI.modals.server.hide();
|
1033
1040
|
}
|
1034
1041
|
})
|
@@ -2662,12 +2669,13 @@ class GUIController extends Controller {
|
|
2662
2669
|
|
2663
2670
|
setMessage(msg, type=null) {
|
2664
2671
|
// Displays message on infoline unless no type (= plain text) and some
|
2665
|
-
// info, warning or error message is already displayed
|
2672
|
+
// info, warning or error message is already displayed.
|
2666
2673
|
super.setMessage(msg, type);
|
2667
2674
|
const types = ['notification', 'warning', 'error'];
|
2668
2675
|
let d = new Date(),
|
2669
2676
|
t = d.getTime(),
|
2670
|
-
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
|
2671
2679
|
mti = types.indexOf(type),
|
2672
2680
|
lmti = types.indexOf(this.last_message_type);
|
2673
2681
|
if(type) {
|
@@ -2680,18 +2688,36 @@ class GUIController extends Controller {
|
|
2680
2688
|
// When receiver is active, add message to its log.
|
2681
2689
|
if(RECEIVER.active) RECEIVER.log(`[${now}] ${msg}`);
|
2682
2690
|
}
|
2683
|
-
|
2684
|
-
|
2685
|
-
|
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;
|
2686
2703
|
this.time_last_message = t;
|
2687
2704
|
this.last_message_type = type;
|
2688
2705
|
if(type) SOUNDS[type].play().catch(() => {
|
2689
2706
|
console.log('NOTICE: Sounds will only play after first user action');
|
2690
2707
|
});
|
2691
|
-
|
2692
|
-
|
2693
|
-
|
2694
|
-
|
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;
|
2695
2721
|
}
|
2696
2722
|
}
|
2697
2723
|
|
@@ -3627,6 +3653,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3627
3653
|
md.element('preference').innerHTML = html.join('');
|
3628
3654
|
md.element('int-feasibility').value = MODEL.integer_tolerance;
|
3629
3655
|
md.element('mip-gap').value = MODEL.MIP_gap;
|
3656
|
+
this.setBox('solver-diagnose', MODEL.always_diagnose);
|
3630
3657
|
md.show();
|
3631
3658
|
}
|
3632
3659
|
|
@@ -3655,6 +3682,11 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3655
3682
|
}
|
3656
3683
|
MODEL.integer_tolerance = Math.max(1e-9, Math.min(0.1, itol));
|
3657
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
|
+
}
|
3658
3690
|
// Close the dialog.
|
3659
3691
|
md.hide();
|
3660
3692
|
}
|
@@ -234,7 +234,7 @@ class EquationManager {
|
|
234
234
|
this.new_modal.hide();
|
235
235
|
this.selected_modifier = m;
|
236
236
|
this.updateDialog();
|
237
|
-
// Open expression editor if expression is still undefined
|
237
|
+
// Open expression editor if expression is still undefined.
|
238
238
|
if(!m.expression.text) this.editEquation();
|
239
239
|
}
|
240
240
|
}
|
@@ -243,13 +243,13 @@ class EquationManager {
|
|
243
243
|
const m = this.selected_modifier;
|
244
244
|
if(m) {
|
245
245
|
this.edited_expression = m.expression;
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
246
|
+
X_EDIT.edited_input_id = '';
|
247
|
+
X_EDIT.property.innerHTML = this.selected_modifier.selector;
|
248
|
+
X_EDIT.text.value = m.expression.text;
|
249
|
+
X_EDIT.obj.value = 0;
|
250
250
|
X_EDIT.updateVariableBar();
|
251
251
|
X_EDIT.clearStatusBar();
|
252
|
-
|
252
|
+
UI.modals.expression.show('text');
|
253
253
|
}
|
254
254
|
}
|
255
255
|
|
@@ -38,16 +38,16 @@ class ExpressionEditor {
|
|
38
38
|
this.dataset_dot_option = '. (this dataset)';
|
39
39
|
this.edited_input_id = '';
|
40
40
|
this.edited_expression = null;
|
41
|
-
// Dialog DOM elements
|
41
|
+
// Dialog DOM elements.
|
42
42
|
this.property = document.getElementById('expression-property');
|
43
43
|
this.text = document.getElementById('expression-text');
|
44
44
|
this.status = document.getElementById('expression-status');
|
45
45
|
this.info = document.getElementById('expression-info');
|
46
|
-
// The DOM elements for the "insert variable" bar
|
46
|
+
// The DOM elements for the "insert variable" bar.
|
47
47
|
this.obj = document.getElementById('variable-obj');
|
48
48
|
this.name = document.getElementById('variable-name');
|
49
49
|
this.attr = document.getElementById('variable-attr');
|
50
|
-
// The quick guide to Linny-R expressions
|
50
|
+
// The quick guide to Linny-R expressions.
|
51
51
|
this.info.innerHTML = `
|
52
52
|
<h3>Linny-R expressions</h3>
|
53
53
|
<p><em>NOTE: Move cursor over a</em> <code>symbol</code>
|
@@ -229,6 +229,9 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
229
229
|
// CLear other properties that relate to the edited expression.
|
230
230
|
this.edited_input_id = '';
|
231
231
|
this.edited_expression = null;
|
232
|
+
// Clear edited expression attributes of other dialogs.
|
233
|
+
DATASET_MANAGER.edited_expression = null;
|
234
|
+
EQUATION_MANAGER.edited_expression = null;
|
232
235
|
}
|
233
236
|
|
234
237
|
parseExpression() {
|
@@ -294,7 +294,7 @@ class Finder {
|
|
294
294
|
for(let i = 0; i < n; i++) {
|
295
295
|
const e = this.entities[i];
|
296
296
|
// Exclude "no actor" and top cluster.
|
297
|
-
if(e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
|
297
|
+
if(e.name && e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
|
298
298
|
// Also exclude actor cash flow data products because
|
299
299
|
// many of their properties should not be changed.
|
300
300
|
!e.name.startsWith('$')) {
|
@@ -325,12 +325,12 @@ class GUIMonitor {
|
|
325
325
|
UI.alert(jsr.error);
|
326
326
|
} else if(jsr.server) {
|
327
327
|
VM.solver_token = jsr.token;
|
328
|
-
VM.
|
328
|
+
VM.selectSolver(jsr.solver);
|
329
329
|
// Remote solver may indicate user-specific solver time limit.
|
330
330
|
let utl = '';
|
331
331
|
if(jsr.time_limit) {
|
332
332
|
VM.max_solver_time = jsr.time_limit;
|
333
|
-
utl = ` -- ${VM.
|
333
|
+
utl = ` -- ${VM.solver_names[VM.solver_id]} solver: ` +
|
334
334
|
`max. ${VM.max_solver_time} seconds per block`;
|
335
335
|
// If user has a set time limit, no restrictions on tableau size.
|
336
336
|
VM.max_tableau_size = 0;
|
@@ -354,7 +354,7 @@ class GUIMonitor {
|
|
354
354
|
VM.solver_token = 'local host';
|
355
355
|
fetch('solver/', postData({
|
356
356
|
action: 'logon',
|
357
|
-
solver: MODEL.preferred_solver || VM.
|
357
|
+
solver: MODEL.preferred_solver || VM.solver_id}))
|
358
358
|
.then((response) => {
|
359
359
|
if(!response.ok) {
|
360
360
|
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
@@ -367,10 +367,10 @@ class GUIMonitor {
|
|
367
367
|
jsr = JSON.parse(data),
|
368
368
|
sname = VM.solver_names[jsr.solver] || 'unknown',
|
369
369
|
svr = `Solver on ${jsr.server} is ${sname}`;
|
370
|
-
if(jsr.solver !== VM.
|
370
|
+
if(jsr.solver !== VM.solver_id) UI.notify(svr);
|
371
371
|
VM.server = jsr.server;
|
372
372
|
VM.working_directory = jsr.path;
|
373
|
-
VM.
|
373
|
+
VM.selectSolver(jsr.solver);
|
374
374
|
VM.solver_list = jsr.solver_list;
|
375
375
|
document.getElementById('host-logo').title = svr;
|
376
376
|
VM.connected = true;
|
@@ -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 ||
|