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 +81 -55
- package/package.json +1 -1
- package/server.js +27 -23
- package/static/images/replace-data-product.png +0 -0
- package/static/images/replace-product.png +0 -0
- package/static/index.html +6 -1
- package/static/scripts/linny-r-gui-controller.js +60 -12
- package/static/scripts/linny-r-model.js +23 -15
- package/static/scripts/linny-r-vm.js +77 -10
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)\
|
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
|
109
|
-
|
110
|
-
|
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
|
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
|
247
|
-
|
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
|
-
|
256
|
-
|
285
|
+
On a Windows machine, open `Command Prompt`, change to your Linny-R
|
286
|
+
directory and type:
|
257
287
|
|
258
|
-
``
|
288
|
+
``linny-r``
|
259
289
|
|
260
|
-
|
290
|
+
On a macOS machine, open `Terminal`, change to your Linny-R directory
|
291
|
+
and type:
|
261
292
|
|
262
|
-
|
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
|
-
[
|
275
|
-
[
|
276
|
-
[
|
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
|
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
|
-
|
331
|
-
|
332
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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
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
|
-
//
|
259
|
-
const
|
260
|
-
|
261
|
-
|
262
|
-
`<p>You can close the <em>Terminal</em> window that shows
|
263
|
-
|
264
|
-
</
|
265
|
-
|
266
|
-
|
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
|
269
|
-
|
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>${
|
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
|
-
|
298
|
-
|
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
|
|
Binary file
|
Binary file
|
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="
|
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) – 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
|
▼ (shut-down: 1 if X[t-1] > 0 ∧ X[t] = 0, otherwise 0)
|
1377
1379
|
</option>
|
1380
|
+
<option id="link-slack" value="12">
|
1381
|
+
↥ (available capacity: UB - X[t])
|
1382
|
+
</option>
|
1378
1383
|
<option id="link-spinning" value="8">
|
1379
1384
|
⤴ (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
|
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
|
-
|
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)
|
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
|
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"
|
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
|
-
//
|
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:
|
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 = {
|
4085
|
-
|
4086
|
-
|
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:
|
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
|
-
//
|
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+
|
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.
|
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.
|
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
|
-
|
8543
|
-
//
|
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
|
|