linny-r 1.4.3 → 1.4.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.
Files changed (49) hide show
  1. package/README.md +102 -48
  2. package/package.json +1 -1
  3. package/server.js +31 -6
  4. package/static/images/check-off-not-same-changed.png +0 -0
  5. package/static/images/check-off-not-same-not-changed.png +0 -0
  6. package/static/images/check-off-same-changed.png +0 -0
  7. package/static/images/check-off-same-not-changed.png +0 -0
  8. package/static/images/check-on-not-same-changed.png +0 -0
  9. package/static/images/check-on-not-same-not-changed.png +0 -0
  10. package/static/images/check-on-same-changed.png +0 -0
  11. package/static/images/check-on-same-not-changed.png +0 -0
  12. package/static/images/eq-not-same-changed.png +0 -0
  13. package/static/images/eq-not-same-not-changed.png +0 -0
  14. package/static/images/eq-same-changed.png +0 -0
  15. package/static/images/eq-same-not-changed.png +0 -0
  16. package/static/images/ne-not-same-changed.png +0 -0
  17. package/static/images/ne-not-same-not-changed.png +0 -0
  18. package/static/images/ne-same-changed.png +0 -0
  19. package/static/images/ne-same-not-changed.png +0 -0
  20. package/static/images/sort-asc-lead.png +0 -0
  21. package/static/images/sort-asc.png +0 -0
  22. package/static/images/sort-desc-lead.png +0 -0
  23. package/static/images/sort-desc.png +0 -0
  24. package/static/images/sort-not.png +0 -0
  25. package/static/index.html +51 -35
  26. package/static/linny-r.css +167 -53
  27. package/static/scripts/linny-r-gui-actor-manager.js +340 -0
  28. package/static/scripts/linny-r-gui-chart-manager.js +944 -0
  29. package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
  30. package/static/scripts/linny-r-gui-controller.js +4005 -0
  31. package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
  32. package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
  33. package/static/scripts/linny-r-gui-equation-manager.js +307 -0
  34. package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
  35. package/static/scripts/linny-r-gui-expression-editor.js +450 -0
  36. package/static/scripts/linny-r-gui-file-manager.js +392 -0
  37. package/static/scripts/linny-r-gui-finder.js +727 -0
  38. package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
  39. package/static/scripts/linny-r-gui-monitor.js +448 -0
  40. package/static/scripts/linny-r-gui-paper.js +2789 -0
  41. package/static/scripts/linny-r-gui-receiver.js +323 -0
  42. package/static/scripts/linny-r-gui-repository-browser.js +819 -0
  43. package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
  44. package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
  45. package/static/scripts/linny-r-gui-undo-redo.js +560 -0
  46. package/static/scripts/linny-r-model.js +34 -15
  47. package/static/scripts/linny-r-utils.js +11 -1
  48. package/static/scripts/linny-r-vm.js +21 -12
  49. package/static/scripts/linny-r-gui.js +0 -16908
@@ -0,0 +1,323 @@
1
+ /*
2
+ Linny-R is an executable graphical specification language for (mixed integer)
3
+ linear programming (MILP) problems, especially unit commitment problems (UCP).
4
+ The Linny-R language and tool have been developed by Pieter Bots at Delft
5
+ University of Technology, starting in 2009. The project to develop a browser-
6
+ based version started in 2017. See https://linny-r.org for more information.
7
+
8
+ This JavaScript file (linny-r-gui-rcvr.js) provides the GUI functionality
9
+ for the receiver: listen with a certain frequency to a "channel" to see
10
+ whether a remote RUN command for a specified experiment/model should be
11
+ executed, perform this operation, and write report files to the user space.
12
+
13
+ */
14
+
15
+ /*
16
+ Copyright (c) 2017-2023 Delft University of Technology
17
+
18
+ Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ of this software and associated documentation files (the "Software"), to deal
20
+ in the Software without restriction, including without limitation the rights to
21
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
22
+ of the Software, and to permit persons to whom the Software is furnished to do
23
+ so, subject to the following conditions:
24
+
25
+ The above copyright notice and this permission notice shall be included in
26
+ all copies or substantial portions of the Software.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
34
+ SOFTWARE.
35
+ */
36
+
37
+ // CLASS GUIReceiver defines a listener/interpreter for commands from local host
38
+ class GUIReceiver {
39
+ constructor() {
40
+ this.channel_modal = new ModalDialog('channel');
41
+ this.channel_modal.ok.addEventListener('click',
42
+ () => RECEIVER.activate());
43
+ this.channel_modal.cancel.addEventListener('click',
44
+ () => RECEIVER.channel_modal.hide());
45
+ this.channel_modal.element('path').title =
46
+ 'URL a of public channel, or path to a directory on local host\n' +
47
+ `(use shorthand @ for ${PUBLIC_LINNY_R_URL}/channel/)`;
48
+ this.channel_modal.element('callback').title =
49
+ 'Path to Linny-R command file\n' +
50
+ '(default path: (main)/command/; default extension: .lrc)';
51
+ // NOTE: each receiver instance listens to a "channel", being the directory
52
+ // on the local host specified by the modeler
53
+ this.channel = '';
54
+ // The file name is the name of the first Linny-R model file or command file
55
+ // that was found in the channel directory
56
+ this.file_name = '';
57
+ // The name of the experiment to be run can be specified in a command file
58
+ this.experiment = '';
59
+ // The call-back script is the path (on the local host) to the Python script
60
+ // that is to be executed after a successful run
61
+ this.call_back_script = '';
62
+ this.active = false;
63
+ this.solving = false;
64
+ this.interval = 1000;
65
+ this.error = '';
66
+ this.log_lines = [];
67
+ // NOTE: hide receiver button unless on a local server (127.0.0.1)
68
+ if(window.location.href.indexOf('/127.0.0.1') < 0) {
69
+ UI.buttons.receiver.classList.add('off');
70
+ }
71
+ }
72
+
73
+ setError(msg) {
74
+ // Record and display error message, and immediately stop listening
75
+ this.error = msg;
76
+ UI.warn(this.error);
77
+ this.deactivate();
78
+ }
79
+
80
+ log(msg) {
81
+ // Logs a message displayed on the status line while solving.
82
+ if(this.active || MODEL.report_results) {
83
+ if(!msg.startsWith('[')) {
84
+ const
85
+ d = new Date(),
86
+ now = d.getHours() + ':' +
87
+ d.getMinutes().toString().padStart(2, '0') + ':' +
88
+ d.getSeconds().toString().padStart(2, '0');
89
+ msg = `[${now}] ${msg}`;
90
+ }
91
+ this.log_lines.push(msg);
92
+ }
93
+ }
94
+
95
+ get logReport() {
96
+ // Returns log lines as a single string, and clears the log
97
+ const report = this.log_lines.join('\n');
98
+ this.log_lines.length = 0;
99
+ return report;
100
+ }
101
+
102
+ activate() {
103
+ // Sets channel path and (optional) call-back script
104
+ this.channel = this.channel_modal.element('path').value.trim();
105
+ this.call_back_script = this.channel_modal.element('callback').value.trim();
106
+ // Default channel is the `channel` sub-directory
107
+ if(this.channel === '') this.channel = 'channel';
108
+ // Clear experiment, error message and log
109
+ this.experiment = '';
110
+ this.error = '';
111
+ this.log_lines.length = 0;
112
+ this.active = true;
113
+ this.listen();
114
+ UI.buttons.receiver.classList.add('blink');
115
+ UI.notify(`Started listening at <tt>${this.channel}</tt>`);
116
+ this.channel_modal.hide();
117
+ }
118
+
119
+ deactivate() {
120
+ // Stops the receiver from listening at the channel
121
+ this.active = false;
122
+ UI.buttons.receiver.classList.remove('blink');
123
+ }
124
+
125
+ toggle() {
126
+ // Responds to receiver ON/OFF button at top bar
127
+ if(this.active) {
128
+ this.deactivate();
129
+ // NOTE: only notify when the modeler deactivates, so as to prevent
130
+ // overwriting error messages on the status line
131
+ UI.notify(`Stopped listening at <tt>${this.channel}</tt>`);
132
+ } else {
133
+ // Show channel dialog
134
+ this.channel_modal.element('path').value = this.channel;
135
+ this.channel_modal.element('callback').value = this.call_back_script;
136
+ this.channel_modal.show('path');
137
+ }
138
+ }
139
+
140
+ listen() {
141
+ // If active, checks with local server whether there is a new command
142
+ if(!this.active) return;
143
+ fetch('receiver/', postData({path: this.channel, action: 'listen'}))
144
+ .then((response) => {
145
+ if(!response.ok) {
146
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
147
+ }
148
+ return response.text();
149
+ })
150
+ .then((data) => {
151
+ if(UI.postResponseOK(data)) {
152
+ let jsr;
153
+ try {
154
+ jsr = JSON.parse(data);
155
+ } catch(err) {
156
+ console.log('ERROR while parsing JSON:', err);
157
+ RECEIVER.setError('SERVER ERROR: ' + data.slice(0, 250));
158
+ return;
159
+ }
160
+ if(jsr.stop) {
161
+ UI.notify('Receiver deactivated by script');
162
+ RECEIVER.deactivate();
163
+ } else if(jsr.file === '') {
164
+ // Nothing to do => check again after the set time interval
165
+ setTimeout(() => RECEIVER.listen(), RECEIVER.interval);
166
+ return;
167
+ } else if(jsr.file && jsr.model) {
168
+ RECEIVER.file_name = jsr.file;
169
+ let msg = '';
170
+ if(!UI.loadModelFromXML(jsr.model)) {
171
+ msg = 'ERROR: Received model is not valid';
172
+ } else if(jsr.experiment) {
173
+ EXPERIMENT_MANAGER.selectExperiment(jsr.experiment);
174
+ if(!EXPERIMENT_MANAGER.selected_experiment) {
175
+ msg = `ERROR: Unknown experiment "${jsr.experiment}"`;
176
+ } else {
177
+ RECEIVER.experiment = jsr.experiment;
178
+ }
179
+ }
180
+ if(msg) {
181
+ RECEIVER.setError(msg);
182
+ // Record abort on local host
183
+ fetch('receiver/', postData({
184
+ path: RECEIVER.channel,
185
+ file: RECEIVER.file_name,
186
+ action: 'abort',
187
+ log: RECEIVER.logReport
188
+ }))
189
+ .then((response) => {
190
+ if(!response.ok) {
191
+ UI.alert(
192
+ `ERROR ${response.status}: ${response.statusText}`);
193
+ }
194
+ return response.text();
195
+ })
196
+ .then((data) => {
197
+ // Always show response on status line
198
+ UI.postResponseOK(data, true);
199
+ // Keep listening, so check again after the time interval
200
+ setTimeout(() => RECEIVER.listen(), RECEIVER.interval);
201
+ })
202
+ .catch(() => UI.warn(UI.WARNING.NO_CONNECTION, err));
203
+ } else {
204
+ RECEIVER.log('Executing: ' + RECEIVER.file_name);
205
+ // NOTE: Virtual Machine will trigger the receiver's reporting
206
+ // action each time the model has been solved
207
+ if(RECEIVER.experiment) {
208
+ RECEIVER.log('Starting experiment: ' + RECEIVER.experiment);
209
+ EXPERIMENT_MANAGER.startExperiment();
210
+ } else {
211
+ VM.solveModel();
212
+ }
213
+ }
214
+ } else {
215
+ RECEIVER.setError('Receiver issue: ' + response);
216
+ }
217
+ }
218
+ })
219
+ .catch(() => UI.warn(UI.WARNING.NO_CONNECTION, err));
220
+ }
221
+
222
+ report() {
223
+ // Posts the run results to the local server, or signals an error
224
+ let form,
225
+ run = '',
226
+ path = this.channel,
227
+ file = this.file_name;
228
+ // NOTE: Always set `solving` to FALSE
229
+ this.solving = false;
230
+ // NOTE: When reporting receiver while is not active, report the
231
+ // results of the running experiment.
232
+ if(this.experiment || !this.active) {
233
+ if(MODEL.running_experiment) {
234
+ run = MODEL.running_experiment.active_combination_index;
235
+ this.log(`Reporting: ${this.file_name} (run #${run})`);
236
+ }
237
+ }
238
+ // NOTE: If receiver is not active, path and file must be set.
239
+ if(!this.active) {
240
+ path = 'user/reports';
241
+ // NOTE: The @ will be replaced by the run number, so that that
242
+ // number precedes the clock time. The @ will be unique because
243
+ // `asFileName()` replaces special characters by underscores.
244
+ file = REPOSITORY_BROWSER.asFileName(MODEL.name || 'model') +
245
+ '@-' + compactClockTime();
246
+ }
247
+ if(MODEL.solved && !VM.halted) {
248
+ // Normal execution termination => report results
249
+ const od = MODEL.outputData;
250
+ form = {
251
+ path: path,
252
+ file: file,
253
+ action: 'report',
254
+ run: run,
255
+ data: od[0],
256
+ stats: od[1],
257
+ log: RECEIVER.logReport
258
+ };
259
+ } else {
260
+ if(!VM.halted && !this.error) {
261
+ // No apparent cause => log this irregularity
262
+ this.setError('ERROR: Unknown solver problem');
263
+ }
264
+ form = {
265
+ path: this.channel,
266
+ file: this.file_name,
267
+ action: 'abort',
268
+ log: this.logReport
269
+ };
270
+ }
271
+ fetch('receiver/', postData(form))
272
+ .then((response) => {
273
+ if(!response.ok) {
274
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
275
+ }
276
+ return response.text();
277
+ })
278
+ .then((data) => {
279
+ // For experiments, only display server response if warning or error.
280
+ UI.postResponseOK(data, !RECEIVER.experiment);
281
+ // If execution completed, perform the call-back action if the
282
+ // receiver is active (so not when auto-reporting a run).
283
+ // NOTE: for experiments, call-back is performed upon completion by
284
+ // the Experiment Manager.
285
+ if(RECEIVER.active && !RECEIVER.experiment) RECEIVER.callBack();
286
+ })
287
+ .catch(() => UI.warn(UI.WARNING.NO_CONNECTION, err));
288
+ }
289
+
290
+ callBack() {
291
+ // Deletes the file in the channel directory (to prevent executing it again)
292
+ // and activates the call-back script on the local server
293
+ fetch('receiver/', postData({
294
+ path: this.channel,
295
+ file: this.file_name,
296
+ action: 'call-back',
297
+ script: this.call_back_script
298
+ }))
299
+ .then((response) => {
300
+ if(!response.ok) {
301
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
302
+ }
303
+ return response.text();
304
+ })
305
+ .then((data) => {
306
+ // Call-back completed => resume listening unless running experiment
307
+ if(RECEIVER.experiment) {
308
+ // For experiments, only display server response if warning or error
309
+ UI.postResponseOK(data);
310
+ } else {
311
+ // Always show server response for single runs
312
+ if(UI.postResponseOK(data, true)) {
313
+ // NOTE: resume listening only if no error
314
+ setTimeout(() => RECEIVER.listen(), RECEIVER.interval);
315
+ } else {
316
+ RECEIVER.deactivate();
317
+ }
318
+ }
319
+ })
320
+ .catch(() => UI.warn(UI.WARNING.NO_CONNECTION, err));
321
+ }
322
+
323
+ } // END of class GUIReceiver