linny-r 2.0.2 → 2.0.5

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
@@ -56,7 +56,7 @@ in a cloud.
56
56
 
57
57
  In this installation guide, the path to this directory is denoted by `Linny-R`,
58
58
  so in all commands you should replace this with the actual directory path.
59
- On a Windows machine the suggested path is `C:\Users\(your user name)\Documents\Linny-R`,
59
+ On a Windows machine the suggested path is `C:\Users\(your user name)\Linny-R`,
60
60
  and on a macOS machine `/Users/(your user name)/Linny-R`.
61
61
 
62
62
  To install Linny-R in this directory, first change to the parent directory
@@ -104,20 +104,50 @@ Linny-R
104
104
 
105
105
  `Linny-R` should contain two JSON files `package.json` and `package-lock.json`
106
106
  that should **not** be removed, or you will have to re-install Linny-R.
107
+ It should also contain the launch script. On a macOS machine, this will be
108
+ the shell script `linny-r.command`, on a Windows machine the batch script
109
+ `linny-r.bat`.
110
+
111
+ All other software is contained in the `node_modules` directory. It comprises
112
+ two Node.js packages: `@xlmdom` and `linny-r`.
107
113
 
108
- The `linny-r` directory should also contain this file `README.md`,
109
- the files `server.js` and `console.js` that will be run by Node.js,
110
- and the sub-directory `static`. This `static` directory should contain three
111
- HTML files:
114
+ The `linny-r` package directory should contain this file `README.md`, the files
115
+ `server.js` and `console.js` that will be run by Node.js, and the sub-directory
116
+ `static`. This `static` directory should contain three HTML files:
112
117
 
113
118
  * `index.html` (the browser-based GUI)
114
119
  * `show-png.html` (to render SVG diagrams as PNG images)
115
- * `show-diff.html` (to display differences betwee two Linny-R models)
120
+ * `show-diff.html` (to display differences between two Linny-R models)
116
121
 
117
122
  It should also contain the style sheet `linny-r.css` required by the GUI.
118
123
 
119
124
  The sub-directories of `static` contain files that are served to the browser
120
- by the script `server.js` when it is running in Node.js.
125
+ by the script `server.js` when it is running in Node.js.
126
+
127
+ > [!IMPORTANT]
128
+ > Unless you _really_ know what you are doing, do **not** move or rename
129
+ > your Linny-R directory or change its contents. If for some reason Linny-R
130
+ > does not work, remove _all_ Linny-R software from your machine, and then
131
+ > install it anew, following the procedure described above.
132
+
133
+ #### Updating to the latest version of Linny-R
134
+
135
+ When a newer version has beeh released, Linny-R will prompt you to update
136
+ automatically. Click on the link in this prompt to see the release notes
137
+ on GitHub and find out about new features and bug fixes. When you click on
138
+ the _OK_ button, Linny-R will shut down its local server script, and then
139
+ the launch script should perform the `npm update` command and then restart
140
+ the server script. Your browser will prompt you to close its current tab,
141
+ and then Linny-R should reappear in a new browser tab or window.
142
+
143
+ > [!NOTE]
144
+ > The built-in updating function of Linny-R will _**not**_ automatically
145
+ > upgrade to a new _major_ version. To update from a version like 1.9.x to
146
+ > a version like 2.0.x, you have to open a command line interface
147
+ > (`Command Prompt` or `Terminal`), change to your Linny-R directory, and
148
+ > then type `npm install linny-r@2`. This should perform the upgrade.
149
+ > You can then launch Linny-R as usual by typing `linny-r` (Windows) or
150
+ > `./linny-r.command` (macOS).
121
151
 
122
152
  #### Installing and using an earlier version of Linny-R
123
153
 
@@ -234,7 +264,7 @@ as this is where Linny-R will look for it when it does not find one of the
234
264
  other solvers.
235
265
 
236
266
  On a macOS machine, you must then make the file `lp_solve` executable.
237
- Open Terminal and change to your Linny-R directory, and then type:
267
+ Open `Terminal` and change to your Linny-R directory, and then type:
238
268
 
239
269
  ``chmod +x lp_solve``
240
270
 
@@ -243,37 +273,36 @@ When you then type:
243
273
  ``./lp_solve -h``
244
274
 
245
275
  a window may appear that warns you that the software may be malicious.
246
- To allow running LP_solve, you must then go to Security & Privacy (via
247
- System Preferences) and there click the Open Anyway button in the General
248
- pane to confirm that you wish to use LP_solve. Then return to Terminal
276
+ To allow running LP_solve, you must then go to _Security & Privacy_ (via
277
+ _System Preferences_) and there click the _Open Anyway_ button in the _General_
278
+ pane to confirm that you wish to use LP_solve. Then return to `Terminal`
249
279
  and once more type `./lp_solve -h`. The response should then be a listing
250
280
  of all the command line options of LP_solve. If you reach this stage,
251
281
  Linny-R will be able to run LP_solve.
252
282
 
253
283
  ## Running Linny-R
254
284
 
255
- Open the Command Line Interface (CLI) of your computer, change to your
256
- Linny-R directory and type:
285
+ On a Windows machine, open `Command Prompt`, change to your Linny-R
286
+ directory and type:
257
287
 
258
- ``node node_modules/linny-r/server launch``
288
+ ``linny-r``
259
289
 
260
- This response should be something similar to:
290
+ On a macOS machine, open `Terminal`, change to your Linny-R directory
291
+ and type:
261
292
 
262
- <pre>
263
- Node.js server for Linny-R version 2.0.0
264
- Node.js version: v22.2.0
265
- ... etc.
266
- </pre>
293
+ ``./linny-r.command``
267
294
 
295
+ This should run the launch script for Linny-R, which will start the
296
+ local server script that connects your browser with the solver.
268
297
  Meanwhile, your default web browser should have opened a tab for the local
269
298
  server URL, which by default will be http://127.0.0.1:5050.
270
299
  The Linny-R GUI should show in your browser window, while in the CLI you
271
300
  should see a long series of server log messages like:
272
301
 
273
302
  <pre>
274
- [2023-11-19 22:55:17] Static file: /index.html
275
- [2023-11-19 22:55:17] Static file: /scripts/iro.min.js
276
- [2023-11-19 22:55:17] Static file: /images/open.png
303
+ [2024-06-11 22:55:17] Static file: /index.html
304
+ [2024-06-11 22:55:17] Static file: /scripts/iro.min.js
305
+ [2024-06-11 22:55:17] Static file: /images/open.png
277
306
  ... etc.
278
307
  </pre>
279
308
 
@@ -281,7 +310,7 @@ should see a long series of server log messages like:
281
310
  > Do **not** close the CLI. If you do, the Linny-R GUI may still be
282
311
  > visible in your browser, but you will be warned that it cannot connect
283
312
  > to the server (at 127.0.0.1:5050). This means that you have to restart
284
- > Linny-R from a new CLI.
313
+ > Linny-R as described above.
285
314
 
286
315
  After loading into the browser, Linny-R will try to connect to the solver.
287
316
  If successful, a notification (blue background) will appear on the status
@@ -313,36 +342,18 @@ confirming that you want to leave, and then closing your browser (tab).
313
342
  If you do not shut down the server from the browser, you can also stop the
314
343
  server by repeatedly pressing ``Ctrl+C`` in the CLI.
315
344
 
316
- ## Command line options
317
-
318
- Optionally, you can add more arguments to the `node` command:
319
-
320
- <pre>
321
- dpi=[number] to overrule the default resolution (300 dpi) for Inkscape
322
- launch to automatically launch Linny-R in your default browser
323
- port=[number] to overrule the default port number (5050)
324
- solver=[name] to overrule the default sequence (Gurobi, MOSEK, CPLEX, SCIP, LP_solve)
325
- workspace=[path] to overrule the default path for the user directory
326
- </pre>
327
-
328
345
  ## Click-start for Linny-R
329
346
 
330
- The first time you start Linny-R after a fresh install or an update,
331
- you will have to open the Command Line Interface (CLI) of your computer,
332
- change to your Linny-R directory and type:
347
+ When `npm` installs the Linny-R package, it creates a script file in your
348
+ Linny-R directory that launches Linny-R. On a macOS machine, this will be
349
+ the shell script `linny-r.command`, on a Windows machine the batch script
350
+ `linny-r.bat`. When you create a desktop shortcut to this script, this will
351
+ allow you to click-start Linny-R.
333
352
 
334
- ``node node_modules/linny-r/server launch``
353
+ How you can create a shortcut icon for Linny-R on your desktop depends on
354
+ the type of computer you use.
335
355
 
336
- This will not only start Linny-R, but also create a script file in your
337
- Linny-R directory that will allow you to start Linny-R by clicking its
338
- icon on your machine. On a macOS machine, this fill will be the shell
339
- script `linny-r.command`, on a Windows machine the batch script
340
- `linny-r.bat`.
341
-
342
- To facilitate start-up, you can create a shortcut icon for Linny-R on your
343
- desktop.
344
-
345
- On a Windows machine, open the _File Explorer_, select your Linny-R folder,
356
+ On a Windows machine, open the `File Explorer`, select your Linny-R folder,
346
357
  right-click on the batch file `linny-r.bat`, and select the _Create shortcut_
347
358
  option. Then right-click on the shortcut file to edit its properties, and
348
359
  click the _Change Icon_ button. The dialog that then appears will allow
@@ -350,22 +361,35 @@ you to go to the sub-folder `node_modules\linny-r\static\images`, where
350
361
  you should select the file `linny-r.ico`. Finally, rename the shortcut to
351
362
  `Linny-R` and move or copy it to your desktop.
352
363
 
353
- On a macOS machine, open Terminal and change to your Linny-R directory,
364
+ On a macOS machine, open `Terminal` and change to your Linny-R directory,
354
365
  and then type:
355
366
 
356
367
  ``chmod +x linny-r.command``
357
368
 
358
- to make the script file executable. To set the icon, use Finder to open
369
+ to make the script file executable. To set the icon, use `Finder` to open
359
370
  the folder that contains the file `linny-r.command`, click on its icon
360
371
  (which still is plain) and open the _Info dialog_ by pressing ``Cmd+I``.
361
- Then open your Linny-R folder in Finder, change to the sub-folder
372
+ Then open your Linny-R folder in `Finder`, change to the sub-folder
362
373
  `node_modules/linny-r/static/images`, and from there drag/drop the file
363
374
  `linny-r.icns` on the icon shown in the top left corner of the _Info dialog_.
364
375
 
376
+ ## Command line options
377
+
378
+ You can customize Linny-R by adding more arguments to the `node` command
379
+ in the launch script:
380
+
381
+ <pre>
382
+ dpi=[number] to overrule the default resolution (300 dpi) for Inkscape
383
+ launch to automatically launch Linny-R in your default browser
384
+ port=[number] to overrule the default port number (5050)
385
+ solver=[name] to overrule the default sequence (Gurobi, MOSEK, CPLEX, SCIP, LP_solve)
386
+ workspace=[path] to overrule the default path for the user directory
387
+ </pre>
388
+
365
389
  > [!NOTE]
366
390
  > When configuring Linny-R for a network environment where individual users
367
- > each have their personal work space (e.g., a virtual drive U:), you must
368
- > edit this script file, adding the argument `workspace=path/to/workspace`
391
+ > each have their personal work space (e.g., a virtual drive U:), you **must**
392
+ > edit the launch script file, adding the argument `workspace=path/to/workspace`
369
393
  > to the `node` command. This will instruct Linny-R to create the `user`
370
394
  > directory in this workspace directory instead of the Linny-R directory.
371
395
 
@@ -380,6 +404,8 @@ The sub-directories of this directory `user` are used by Linny-R to store files.
380
404
  a path has been specified
381
405
  * `diagrams` will be used to render Scalable Vector Graphics (SVG) files as
382
406
  Portable Network Graphics (PNG) using Inkscape (if installed)
407
+ * `models` will contain models that you saved by Shift-clicking on the
408
+ _Save_ button, or using the keyboard shortcut Ctrl-Shift-S
383
409
  * `modules` will contain models stored in the `local host` _repository_
384
410
  * `reports` will contain text files with time series data and statistics in
385
411
  tab-separated format that can be imported or copy/pasted into Excel
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "2.0.2",
3
+ "version": "2.0.5",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -255,22 +255,24 @@ function clearNewerVersion() {
255
255
  }
256
256
 
257
257
  // HTML page to show when the server is shut down by the user.
258
- // NOTE: on a macOS machine, this is slightly more work
259
- const OS_TEXT = {close: '', reopen: ''};
260
- if(PLATFORM === 'darwin') {
261
- OS_TEXT.close =
262
- `<p>You can close the <em>Terminal</em> window that shows
263
- <tt>[Process Terminated]</tt> at the bottom.
264
- </p>`;
265
- OS_TEXT.reopen =
266
- `open <em>Terminal</em> again, change to your Linny-R directory by typing:
258
+ // Parts of the text are platform-specific.
259
+ const
260
+ macOS = (PLATFORM === 'darwin'),
261
+ close = (macOS ?
262
+ `<p>You can now close the <em>Terminal</em> window that shows
263
+ <tt>[Process Terminated]</tt> at the bottom.</p>` :
264
+ `<p>The <em>Command Prompt</em> window where the server was
265
+ running will be closed automatically.</p>`),
266
+ cli = (macOS ? 'Terminal' : 'Command Prompt'),
267
+ ext = (macOS ? '.command' : ''),
268
+ chmod = (!macOS ? '' : `
269
+ <p>If launch fails, you may still need to make the script executable.</p>
270
+ <p>
271
+ You can do this by typing <code>chmod +x linny-r.command</code>
272
+ at the command prompt.
267
273
  </p>
268
- <p><code>cd ${WORKING_DIRECTORY}</code></p>
269
- <p>`;
270
- } else {
271
- OS_TEXT.reopen = 'switch to your <em>Command Prompt</em> window ';
272
- }
273
- const SHUTDOWN_MESSAGE = `<!DOCTYPE html>
274
+ <p>Then retype <code>linny-r.command</code> to launch Linny-R.</p>`),
275
+ SHUTDOWN_MESSAGE = `<!DOCTYPE html>
274
276
  <html lang="en-US">
275
277
  <head>
276
278
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
@@ -290,16 +292,18 @@ const SHUTDOWN_MESSAGE = `<!DOCTYPE html>
290
292
  </style>
291
293
  </head>
292
294
  <body>
293
- <h3>Linny-R server (127.0.0.1) is shutting down</h3>${OS_TEXT.close}
294
- <p>To restart Linny-R, ${OS_TEXT.reopen} and then at the prompt type:</p>
295
- <p><code>node node_modules${path.sep}linny-r${path.sep}server</code></p>
295
+ <h3>Linny-R server (127.0.0.1) is shutting down</h3>${close}
296
296
  <p>
297
- Then switch back to this window, and click this
298
- <button type="button"
299
- onclick="window.location.href = 'http://127.0.0.1:${SETTINGS.port}';">
300
- Restart
301
- </button> button.
297
+ To restart Linny-R, open <em>${cli}</em> again, change to
298
+ your Linny-R directory by typing:
302
299
  </p>
300
+ <p><code>cd ${WORKING_DIRECTORY}</code></p>
301
+ <p>and then type:</p>
302
+ <p><code>linny-r${ext}</code></p>
303
+ <p>
304
+ This should launch Linny-R in a new browser window or tab, so you
305
+ can close this one.
306
+ </p>${chmod}
303
307
  </body>
304
308
  </html>`;
305
309
 
package/static/index.html CHANGED
@@ -378,7 +378,9 @@ and move the cursor over the status bar">
378
378
  title="Add cluster">
379
379
  <img id="note-btn" class="btn toggle enab sep" src="images/note.png"
380
380
  title="Add note">
381
- <img id="clone-btn" class="btn disab sep" src="images/clone.png"
381
+ <img id="replace-btn" class="btn disab" src="images/replace-product.png"
382
+ title="Replace selected product by some other product (Alt-P)">
383
+ <img id="clone-btn" class="btn disab" src="images/clone.png"
382
384
  title="Copy selection (Ctrl-C) &ndash; Alt-click to clone (Alt-C)">
383
385
  <img id="paste-btn" class="btn disab sep" src="images/paste.png"
384
386
  title="Paste selection (Ctrl-V)">
@@ -1375,6 +1377,9 @@ NOTE: Products directly linked to such processes should have a proportional unit
1375
1377
  <option id="link-shutdown" value="10">
1376
1378
  &#x25BC; (shut-down: 1 if X[t-1] > 0 &and; X[t] = 0, otherwise 0)
1377
1379
  </option>
1380
+ <option id="link-slack" value="12">
1381
+ &#x21A5; (available capacity: UB - X[t])
1382
+ </option>
1378
1383
  <option id="link-spinning" value="8">
1379
1384
  &#x2934; (spinning reserve: UB - X[t] if X[t] > 0, otherwise 0)
1380
1385
  </option>
@@ -418,7 +418,7 @@ class GUIController extends Controller {
418
418
  // Initialize controller buttons.
419
419
  this.node_btns = ['process', 'product', 'link', 'constraint',
420
420
  'cluster', 'module', 'note'];
421
- this.edit_btns = ['clone', 'paste', 'delete', 'undo', 'redo'];
421
+ this.edit_btns = ['replace', 'clone', 'paste', 'delete', 'undo', 'redo'];
422
422
  this.model_btns = ['settings', 'save', 'repository', 'actors',
423
423
  'dataset', 'equation', 'chart', 'sensitivity', 'experiment',
424
424
  'diagram', 'savediagram', 'finder', 'monitor', 'tex', 'solve'];
@@ -599,6 +599,8 @@ class GUIController extends Controller {
599
599
  UI.copySelection();
600
600
  }
601
601
  });
602
+ this.buttons.replace.addEventListener('click',
603
+ () => UI.replaceSelectedProduct());
602
604
  this.buttons.paste.addEventListener('click',
603
605
  () => UI.pasteSelection());
604
606
  this.buttons['delete'].addEventListener('click',
@@ -827,7 +829,7 @@ class GUIController extends Controller {
827
829
  this.modals.move.cancel.addEventListener('click',
828
830
  () => UI.doNotMoveNode());
829
831
 
830
- // The REPLACE dialog appears when a product is Ctrl-clicked.
832
+ // The REPLACE dialog appears when a product is Shift-Alt-clicked.
831
833
  this.modals.replace.ok.addEventListener('click',
832
834
  () => UI.replaceProduct());
833
835
  this.modals.replace.cancel.addEventListener('click',
@@ -925,6 +927,9 @@ class GUIController extends Controller {
925
927
  UNDO_STACK.clear();
926
928
  // Autosaving should start anew.
927
929
  AUTO_SAVE.setAutoSaveInterval();
930
+ // Finder dialog is closed, but may still display results for
931
+ // previous model.
932
+ FINDER.updateDialog();
928
933
  // Signal success or failure.
929
934
  return loaded;
930
935
  }
@@ -1132,7 +1137,6 @@ class GUIController extends Controller {
1132
1137
  'confirm when prompted by your browser.');
1133
1138
  // Hide "update" button in server dialog.
1134
1139
  UI.modals.server.element('update').style.display = 'none';
1135
- return;
1136
1140
  } else {
1137
1141
  // Inform user that install appears to have failed.
1138
1142
  msg.push(
@@ -1142,7 +1146,8 @@ class GUIController extends Controller {
1142
1146
  }
1143
1147
  md.element('msg').innerHTML = msg.join('<br>');
1144
1148
  // Reload `index.html`. This will start Linny-R anew.
1145
- window.open('./', '_self');
1149
+ // NOTE: Wait for 2 seconds so the message can be read.
1150
+ setTimeout(() => { window.open('./', '_self'); }, 2000);
1146
1151
  }
1147
1152
  })
1148
1153
  .catch((err) => {
@@ -1591,7 +1596,7 @@ class GUIController extends Controller {
1591
1596
  // Updates the buttons on the main GUI toolbars
1592
1597
  const
1593
1598
  node_btns = 'process product link constraint cluster note ',
1594
- edit_btns = 'clone paste delete undo redo ',
1599
+ edit_btns = 'replace clone paste delete undo redo ',
1595
1600
  model_btns = 'settings save actors dataset equation chart ' +
1596
1601
  'diagram savediagram finder monitor solve';
1597
1602
  if(MODEL === null) {
@@ -1617,9 +1622,30 @@ class GUIController extends Controller {
1617
1622
  this.enableButtons(node_btns + model_btns);
1618
1623
  this.active_button = this.stayActiveButton;
1619
1624
  this.disableButtons(edit_btns);
1620
- if(MODEL.selection.length > 0) this.enableButtons('clone delete');
1625
+ if(MODEL.selection.length > 0) {
1626
+ this.enableButtons('clone delete');
1627
+ // Replace applies only to a single product.
1628
+ if(MODEL.selection.length === 1) {
1629
+ const p = MODEL.selection[0];
1630
+ if(p instanceof Product) {
1631
+ const
1632
+ b = this.buttons.replace,
1633
+ t = 'Replace selected product by some other product (Alt-P)';
1634
+ // Differentiate between product types, as products can be
1635
+ // replaced only by products of the same type.
1636
+ if(p.is_data) {
1637
+ b.title = t.replaceAll('product', 'data product');
1638
+ b.src = 'images/replace-data-product.png';
1639
+ } else {
1640
+ b.title = t;
1641
+ b.src = 'images/replace-product.png';
1642
+ }
1643
+ this.enableButtons('replace');
1644
+ }
1645
+ }
1646
+ }
1621
1647
  if(this.canPaste) this.enableButtons('paste');
1622
- // Only allow target seeking when some target or process constraint is defined
1648
+ // Only allow soling when some target or process constraint is defined.
1623
1649
  if(MODEL.hasTargets) this.enableButtons('solve');
1624
1650
  var u = UNDO_STACK.canUndo;
1625
1651
  if(u) {
@@ -2315,13 +2341,16 @@ class GUIController extends Controller {
2315
2341
  } else if(alt && code === 'KeyR') {
2316
2342
  // Alt-R means: run to diagnose infeasible/unbounded problem.
2317
2343
  VM.solveModel(true);
2318
- } else if(alt && ['KeyC', 'KeyM'].indexOf(code) >= 0) {
2319
- // Special shortcut keys for "clone selection" and "model settings".
2344
+ } else if(alt && ['KeyC', 'KeyM', 'KeyP'].indexOf(code) >= 0) {
2345
+ // Special shortcut keys for "clone selection", "model settings"
2346
+ // and "replace product".
2320
2347
  const be = new Event('click');
2321
2348
  if(code === 'KeyC') {
2322
2349
  this.buttons.clone.dispatchEvent(be);
2323
- } else {
2350
+ } else if(code === 'KeyM') {
2324
2351
  this.buttons.settings.dispatchEvent(be);
2352
+ } else if(code === 'KeyP') {
2353
+ this.buttons.replace.dispatchEvent(be);
2325
2354
  }
2326
2355
  } else if(!e.shiftKey && !alt &&
2327
2356
  (!topmod || ['KeyA', 'KeyC', 'KeyV'].indexOf(code) < 0)) {
@@ -2333,7 +2362,9 @@ class GUIController extends Controller {
2333
2362
  CONSTRAINT_EDITOR.deleteBoundLine();
2334
2363
  } else if(!this.hidden('variable-modal')) {
2335
2364
  CHART_MANAGER.deleteVariable();
2336
- } else {
2365
+ } else if(!topmod) {
2366
+ // Do not delete entity from model diagram when some modal
2367
+ // is showing.
2337
2368
  this.buttons['delete'].dispatchEvent(new Event('click'));
2338
2369
  }
2339
2370
  } else if (code === 'Period' && (e.ctrlKey || e.metaKey)) {
@@ -4276,6 +4307,15 @@ console.log('HERE name conflicts', name_conflicts, mapping);
4276
4307
  soc = 0;
4277
4308
  this.warn('Cost can only be attributed to level-based links');
4278
4309
  }
4310
+ // For multipliers requiring a binary variable, and also for those
4311
+ // based on the node's upper bound, warn the modeler when the UB for
4312
+ // this node is infinite or unspecified.
4313
+ if(VM.LM_NEEDING_ON_OFF.indexOf(m) >= 0 || m === VM.LM_AVAILABLE_CAPACITY) {
4314
+ if(!l.from_node.upper_bound.text) {
4315
+ UI.warn('Infinite upper bound of <strong>' + l.from_node.displayName +
4316
+ `</strong> will cause issues for ${VM.LM_SYMBOLS[m]} link`);
4317
+ }
4318
+ }
4279
4319
  // NOTE: share of cost is input as a percentage, but stored as a floating
4280
4320
  // point value between 0 and 1
4281
4321
  l.share_of_cost = soc / 100;
@@ -4307,9 +4347,17 @@ console.log('HERE name conflicts', name_conflicts, mapping);
4307
4347
  'constraint-to-name').innerHTML = c.to_node.displayName;
4308
4348
  CONSTRAINT_EDITOR.showDialog();
4309
4349
  }
4350
+
4351
+ replaceSelectedProduct() {
4352
+ // Check whether selection contains one product, and if so, prompt
4353
+ // for replacement.
4354
+ if(MODEL.selection.length !== 1) return;
4355
+ const p = MODEL.selection[0];
4356
+ if(p instanceof Product) this.showReplaceProductDialog(p);
4357
+ }
4310
4358
 
4311
4359
  showReplaceProductDialog(p) {
4312
- // Prompts for a product (different from `p`) by which `p` should be
4360
+ // Prompt for a product (different from `p`) by which `p` should be
4313
4361
  // replaced for the selected product position
4314
4362
  const pp = MODEL.focal_cluster.indexOfProduct(p);
4315
4363
  if(pp >= 0) {
@@ -4076,16 +4076,24 @@ class LinnyRModel {
4076
4076
  replaceProduct(p, r, global) {
4077
4077
  const
4078
4078
  ppi = this.focal_cluster.indexOfProduct(p),
4079
- // NOTE: record whether `r` is show in focal cluster
4079
+ // NOTE: Record whether `r` is shown in focal cluster.
4080
4080
  rshown = this.focal_cluster.indexOfProduct(r) >= 0;
4081
4081
  // NOTE: since `ppi` should always be >= 0
4082
4082
  if(ppi >= 0) {
4083
- // Build list of information needed for "undo"
4084
- const undo_info = {p: p.displayName, r: r.displayName, g: global,
4085
- lf: [], lt: [], cf: [], ct: [], cl: []};
4086
- // Keep track of redirected links
4083
+ // Build list of information needed for "undo".
4084
+ const undo_info = {
4085
+ p: p.displayName,
4086
+ r: r.displayName,
4087
+ g: global,
4088
+ lf: [],
4089
+ lt: [],
4090
+ cf: [],
4091
+ ct: [],
4092
+ cl: []
4093
+ };
4094
+ // Keep track of redirected links.
4087
4095
  const rl = [];
4088
- // First replace product in (local) links
4096
+ // First replace product in (local) links.
4089
4097
  for(let i = p.inputs.length - 1; i >= 0; i--) {
4090
4098
  const l = p.inputs[i];
4091
4099
  if(global || l.hasArrow) {
@@ -4093,7 +4101,7 @@ class LinnyRModel {
4093
4101
  ml.copyPropertiesFrom(l);
4094
4102
  this.deleteLink(l);
4095
4103
  rl.push(ml);
4096
- // NOTE: push identifier of *modified* link
4104
+ // NOTE: push identifier of *modified* link.
4097
4105
  undo_info.lt.push(ml.identifier);
4098
4106
  }
4099
4107
  }
@@ -4107,7 +4115,7 @@ class LinnyRModel {
4107
4115
  }
4108
4116
  }
4109
4117
  // Then also replace product in (local) constraints
4110
- // (also keeping track of affected constraints)
4118
+ // (also keeping track of affected constraints).
4111
4119
  const rc = [];
4112
4120
  for(let k in this.constraints) {
4113
4121
  if(this.constraints.hasOwnProperty(k)) {
@@ -4125,26 +4133,26 @@ class LinnyRModel {
4125
4133
  }
4126
4134
  }
4127
4135
  }
4128
- // Replace `p` by `r` as the positioned product
4136
+ // Replace `p` by `r` as the positioned product.
4129
4137
  const pp = this.focal_cluster.product_positions[ppi];
4130
4138
  undo_info.x = pp.x;
4131
4139
  undo_info.y = pp.y;
4132
4140
  pp.product = r;
4133
- // Change coordinates only if `r` is also shown in the focal cluster
4141
+ // Change coordinates only if `r` is also shown in the focal cluster.
4134
4142
  if(rshown) {
4135
4143
  pp.x = r.x;
4136
4144
  pp.y = r.y;
4137
4145
  }
4138
- // Likewise replace product of other placeholders of `p` by `r`
4146
+ // Likewise replace product of other placeholders of `p` by `r`.
4139
4147
  for(let k in this.clusters) if(this.clusters.hasOwnProperty(k)) {
4140
4148
  const
4141
4149
  c = this.clusters[k],
4142
4150
  ppi = c.indexOfProduct(p);
4143
- // NOTE: when local, replace only if sub-cluster is in view
4151
+ // NOTE: When local, replace only if sub-cluster is in view...
4144
4152
  if(ppi >= 0 && (global || this.focal_cluster.containsCluster(c))) {
4145
4153
  const pp = c.product_positions[ppi];
4146
- // And then it MAY be that within this sub-cluster, the local
4147
- // links to `p` were NOT redirected
4154
+ // ... and then it MAY be that within this sub-cluster, the local
4155
+ // links to `p` were NOT redirected.
4148
4156
  const ll = [];
4149
4157
  for(let i = 0; i < p.inputs.length; i++) {
4150
4158
  const l = p.inputs[i];
@@ -4169,7 +4177,7 @@ class LinnyRModel {
4169
4177
  }
4170
4178
  // Now prepare for undo, so that deleteNode can add its XML
4171
4179
  UNDO_STACK.push('replace', undo_info);
4172
- // Delete original product `p` if it has no more product positions
4180
+ // Delete original product `p` if it has no more product positions.
4173
4181
  if(!this.top_cluster.containsProduct(p)) this.deleteNode(p);
4174
4182
  }
4175
4183
  // Prepare for redraw
@@ -2157,6 +2157,10 @@ class VirtualMachine {
2157
2157
  // diagnostic purposes -- see below.
2158
2158
  this.PLUS_INFINITY = 1e+25;
2159
2159
  this.MINUS_INFINITY = -1e+25;
2160
+ // Expression results having an infinite term may be less than infinity,
2161
+ // but still exceptionally high, and this should be shown.
2162
+ this.NEAR_PLUS_INFINITY = this.PLUS_INFINITY / 200;
2163
+ this.NEAR_MINUS_INFINITY = this.MINUS_INFINITY / 200;
2160
2164
  // As of version 1.8.0, Linny-R imposes no +INF bounds on processes
2161
2165
  // unless diagnosing an unbounded problem. For such diagnosis, the
2162
2166
  // (relatively) low value 9.999999999e+9 is used.
@@ -2218,11 +2222,12 @@ class VirtualMachine {
2218
2222
  this.LM_FIRST_COMMIT = 9; // Symbol: hollow asterisk
2219
2223
  this.LM_SHUTDOWN = 10; // Symbol: thick chevron down
2220
2224
  this.LM_PEAK_INC = 11; // Symbol: plus inside triangle ("peak-plus")
2225
+ this.LM_AVAILABLE_CAPACITY = 12; // Symbol: up-arrow with baseline
2221
2226
  // List of link multipliers that require binary ON/OFF variables
2222
2227
  this.LM_NEEDING_ON_OFF = [5, 6, 7, 8, 9, 10];
2223
2228
  this.LM_SYMBOLS = ['', '\u21C9', '\u0394', '\u03A3', '\u03BC', '\u25B2',
2224
- '+', '0', '\u2934', '\u2732', '\u25BC', '\u2A39'];
2225
- this.LM_LETTERS = ' TDSMU+0RFDP';
2229
+ '+', '0', '\u2934', '\u2732', '\u25BC', '\u2A39', '\u21A5'];
2230
+ this.LM_LETTERS = ' TDSMU+0RFDPA';
2226
2231
 
2227
2232
  // VM max. expression stack size.
2228
2233
  this.MAX_STACK = 200;
@@ -2560,14 +2565,14 @@ class VirtualMachine {
2560
2565
  if(n <= this.CYCLIC) return [true, '#CYCLE!'];
2561
2566
  // Any other number less than or equal to 10^30 is considered as
2562
2567
  // minus infinity.
2563
- if(n <= this.MINUS_INFINITY) return [true, '-\u221E'];
2568
+ if(n <= this.NEAR_MINUS_INFINITY) return [true, '-\u221E'];
2564
2569
  // Other special values are very big POSITIVE numbers, so start
2565
2570
  // comparing `n` with the highest value.
2566
2571
  if(n >= this.COMPUTING) return [true, '\u25A6']; // Checkered square
2567
2572
  // NOTE: The prettier circled bold X 2BBF does not display on macOS !!
2568
2573
  if(n >= this.NOT_COMPUTED) return [true, '\u2297']; // Circled X
2569
2574
  if(n >= this.UNDEFINED) return [true, '\u2047']; // Double question mark ??
2570
- if(n >= this.PLUS_INFINITY) return [true, '\u221E'];
2575
+ if(n >= this.NEAR_PLUS_INFINITY) return [true, '\u221E'];
2571
2576
  if(n === this.NO_COST) return [true, '\u00A2']; // c-slash (cent symbol)
2572
2577
  return [false, n];
2573
2578
  }
@@ -3693,6 +3698,15 @@ class VirtualMachine {
3693
3698
  VM.PRODUCE, VM.SPIN_RES, p.on_off_var_index,
3694
3699
  l.flow_delay, vi, l.from_node.upper_bound, tnpx,
3695
3700
  l.relative_rate]]);
3701
+ } else if(l.multiplier === VM.LM_REMAINING_CAPACITY) {
3702
+ // "remaining capacity" equals UB - level. This is a
3703
+ // simpler version of "spinning reserve". We signal this
3704
+ // by passing -1 as the index of the secondary variable,
3705
+ // and the level variable index as the primary variable.
3706
+ this.code.push([VMI_update_cash_coefficient, [
3707
+ VM.PRODUCE, VM.SPIN_RES, vi, // <-- now as primary
3708
+ l.flow_delay, -1, // <-- signal that it is "REM_CAP"
3709
+ l.from_node.upper_bound, tnpx, l.relative_rate]]);
3696
3710
  } else if(l.multiplier === VM.LM_PEAK_INC) {
3697
3711
  // NOTE: "peak increase" may be > 0 only in the first
3698
3712
  // time step of the block being optimized, and in the
@@ -4035,6 +4049,12 @@ class VirtualMachine {
4035
4049
  // NOTE: no delay on this type of link
4036
4050
  this.code.push([VMI_add_peak_increase_at_t_0,
4037
4051
  [vi, l.relative_rate]]);
4052
+ } else if(l.multiplier === VM.LM_AVAILABLE_CAPACITY) {
4053
+ // The "available capacity" equals UB - level, so subtract
4054
+ // UB * rate from RHS, while considering the delay.
4055
+ // NOTE: New instruction style that passes pointers to
4056
+ // model entities instead of their properties.
4057
+ this.code.push([VMI_add_available_capacity, l]);
4038
4058
  } else if(l.relative_rate.isStatic) {
4039
4059
  // Static rates permit simpler VM instructions
4040
4060
  c = l.relative_rate.result(0);
@@ -4311,7 +4331,7 @@ class VirtualMachine {
4311
4331
  hub = ub;
4312
4332
  if(ub > VM.MEGA_UPPER_BOUND) {
4313
4333
  hub = p.highestUpperBound([]);
4314
- // If UB still very high, warn modeler on infoline and in monitor
4334
+ // If UB still very high, warn modeler on infoline and in monitor.
4315
4335
  if(hub > VM.MEGA_UPPER_BOUND) {
4316
4336
  const msg = 'High upper bound (' + this.sig4Dig(hub) +
4317
4337
  ') for <strong>' + p.displayName + '</strong>' +
@@ -5005,6 +5025,8 @@ class VirtualMachine {
5005
5025
  } else if(l.multiplier === VM.LM_SHUTDOWN) {
5006
5026
  // Similar to STARTUP, but now look in the shut-down list.
5007
5027
  pl = (p.shut_downs.indexOf(bt) < 0 ? 0 : 1);
5028
+ } else if(l.multiplier === VM.LM_AVAILABLE_CAPACITY) {
5029
+ pl = p.upper_bound.result(bt) - pl;
5008
5030
  } else if(l.multiplier === VM.LM_INCREASE) {
5009
5031
  const ppl = p.actualLevel(bt - 1);
5010
5032
  pl = this.severestIssue([pl, ppl], pl - ppl);
@@ -6423,6 +6445,8 @@ Solver status = ${json.status}`);
6423
6445
  this.MINUS_INFINITY = this.SOLVER_MINUS_INFINITY;
6424
6446
  console.log('DIAGNOSIS OFF');
6425
6447
  }
6448
+ this.NEAR_PLUS_INFINITY = this.PLUS_INFINITY / 200;
6449
+ this.NEAR_MINUS_INFINITY = this.MINUS_INFINITY / 200;
6426
6450
  // The "propt to diagnose" flag is set when some block posed an
6427
6451
  // infeasible or unbounded problem.
6428
6452
  this.prompt_to_diagnose = false;
@@ -8538,9 +8562,12 @@ function VMI_update_cash_coefficient(args) {
8538
8562
  // The ON/OFF variable index is passed as third argument, hence `plvi`
8539
8563
  // (process level variable index) as first extra parameter, plus three
8540
8564
  // expressions (UB, price, rate).
8565
+ // NOTE: This type is also used to compute "available capacity".
8541
8566
  const
8542
- plvi = args[4],
8543
- // NOTE: Column of second variable will be relative to same offset.
8567
+ // args[4] = -1 signals that the remaining capacity should be
8568
+ // computed, which means that ON/OFF should be disregarded.
8569
+ plvi = (args[4] < 0 ? vi : args[4]),
8570
+ // Column of second variable will be relative to same offset.
8544
8571
  plk = k + plvi - vi,
8545
8572
  ub = args[5].result(VM.t),
8546
8573
  price_rate = args[6].result(VM.t) * args[7].result(VM.t);
@@ -9006,13 +9033,15 @@ function VMI_add_bound_line_constraint(args) {
9006
9033
  vy = VM.variables[viy - 1],
9007
9034
  objy= vy[1],
9008
9035
  uby = args[5].result(VM.t),
9009
- bl = args[6],
9036
+ bl = args[6];
9037
+ // Set bound line point coordinates for current run and time step.
9038
+ bl.setDynamicPoints(VM.t);
9039
+ // Then use the actualized points.
9040
+ const
9010
9041
  n = bl.points.length,
9011
9042
  x = new Array(n),
9012
9043
  y = new Array(n),
9013
9044
  w = new Array(n);
9014
- // Set bound line point coordinates for current run and time step.
9015
- bl.setDynamicPoints(VM.t);
9016
9045
  if(DEBUGGING) {
9017
9046
  console.log('add_bound_line_constraint:', bl.displayName);
9018
9047
  }
@@ -9196,6 +9225,44 @@ function VMI_add_peak_increase_at_t_0(args) {
9196
9225
  // series of coefficient-setting instructions
9197
9226
  }
9198
9227
 
9228
+ function VMI_add_available_capacity(link) {
9229
+ // Adds the "available capacity" of the FROM node (process) to the
9230
+ // level of the TO node (data product) while considering the delay.
9231
+ // NOTE: New instruction style that passes pointers to model entities
9232
+ // instead of their properties.
9233
+ const
9234
+ d = link.actualDelay(VM.t),
9235
+ fnvi = link.from_node.level_var_index,
9236
+ // Column number in the tableau.
9237
+ fnk = VM.offset + fnvi - d * VM.cols,
9238
+ // Use flow rate and upper bound for t minus delay.
9239
+ t = VM.t - d,
9240
+ r = link.relative_rate.result(t),
9241
+ u = link.from_node.upper_bound.result(t);
9242
+ if(DEBUGGING) {
9243
+ console.log('VMI_add_available_capacity (t = ' + VM.t + ')',
9244
+ link.displayName, 'UB', u, 'rate', r);
9245
+ }
9246
+ // Available capacity equals UB - level, so subtract UB * rate
9247
+ // from RHS...
9248
+ VM.rhs -= u * r;
9249
+ // ... and subtract rate from FROM node coefficient.
9250
+ if(fnk <= 0) {
9251
+ // NOTE: If `fnk` falls PRIOR to the start of the block being solved,
9252
+ // this means that the value of the decision variable X for which the
9253
+ // coefficient C is to be set by this instruction has been calculated
9254
+ // while solving a previous block. Since the value of X is known,
9255
+ // adding X*rate to C is implemented as subtracting X*rate from the
9256
+ // right hand side of the constraint.
9257
+ VM.rhs += knownValue(fnvi, t) * r;
9258
+ } else if(fnk in VM.coefficients) {
9259
+ VM.coefficients[fnk] -= r;
9260
+ } else {
9261
+ VM.coefficients[fnk] = -r;
9262
+ }
9263
+ }
9264
+
9265
+
9199
9266
  // NOTE: the global constants below are not defined in linny-r-globals.js
9200
9267
  // because some comprise the identifiers of functions for VM instructions
9201
9268