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 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 (October 2023) this is 21.1.0.
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.1.0.
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
- > **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
@@ -147,12 +147,12 @@ directory and type:
147
147
 
148
148
  ## Configuring the MILP solver
149
149
 
150
- Linny-R presently supports four MILP solvers: Gurobi, CPLEX, SCIP and LP_solve.
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
- > **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,
@@ -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.0
238
- Node.js version: v21.1.0
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-10-29 22:55:17] Static file: /index.html
249
- [2023-10-29 22:55:17] Static file: /scripts/iro.min.js
250
- [2023-10-29 22:55:17] Static file: /images/open.png
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
- > **Important**
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
- > **Note**
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
- > **Note**
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
- > **Note**
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
- > **Important**
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.solver_name = SOLVER.id;
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.solver_name = mps;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.7.3",
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/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 &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>
@@ -1347,6 +1354,7 @@ NOTE: Unit symbols are case-sensitive, so BTU &ne; 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">
@@ -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
 
@@ -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: 100px;
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 {
@@ -1412,4 +1412,4 @@ if(NODE) module.exports = {
1412
1412
  ChartManager: ChartManager,
1413
1413
  SensitivityAnalysis: SensitivityAnalysis,
1414
1414
  ExperimentManager: ExperimentManager
1415
- }
1415
+ };
@@ -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
- var segeq = '';
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', () => 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
 
@@ -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.solver_name) {
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.solver_name ? '"selected="selected' : ''),
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.solver_name = sid;
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
- // Display text only if previous message has "timed out" or was less
2684
- // urgent than this one.
2685
- 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;
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
- const il = document.getElementById('info-line');
2692
- il.classList.remove(...types);
2693
- il.classList.add(type);
2694
- 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;
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
- const md = UI.modals.expression;
247
- md.element('property').innerHTML = this.selected_modifier.selector;
248
- md.element('text').value = m.expression.text;
249
- document.getElementById('variable-obj').value = 0;
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
- md.show('text');
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.solver_name = jsr.solver;
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.solver_name} solver: ` +
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.solver_name}))
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.solver_name) UI.notify(svr);
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.solver_name = jsr.solver;
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 = +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 ||