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.
- package/README.md +102 -48
- package/package.json +1 -1
- package/server.js +31 -6
- package/static/images/check-off-not-same-changed.png +0 -0
- package/static/images/check-off-not-same-not-changed.png +0 -0
- package/static/images/check-off-same-changed.png +0 -0
- package/static/images/check-off-same-not-changed.png +0 -0
- package/static/images/check-on-not-same-changed.png +0 -0
- package/static/images/check-on-not-same-not-changed.png +0 -0
- package/static/images/check-on-same-changed.png +0 -0
- package/static/images/check-on-same-not-changed.png +0 -0
- package/static/images/eq-not-same-changed.png +0 -0
- package/static/images/eq-not-same-not-changed.png +0 -0
- package/static/images/eq-same-changed.png +0 -0
- package/static/images/eq-same-not-changed.png +0 -0
- package/static/images/ne-not-same-changed.png +0 -0
- package/static/images/ne-not-same-not-changed.png +0 -0
- package/static/images/ne-same-changed.png +0 -0
- package/static/images/ne-same-not-changed.png +0 -0
- package/static/images/sort-asc-lead.png +0 -0
- package/static/images/sort-asc.png +0 -0
- package/static/images/sort-desc-lead.png +0 -0
- package/static/images/sort-desc.png +0 -0
- package/static/images/sort-not.png +0 -0
- package/static/index.html +51 -35
- package/static/linny-r.css +167 -53
- package/static/scripts/linny-r-gui-actor-manager.js +340 -0
- package/static/scripts/linny-r-gui-chart-manager.js +944 -0
- package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
- package/static/scripts/linny-r-gui-controller.js +4005 -0
- package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
- package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
- package/static/scripts/linny-r-gui-equation-manager.js +307 -0
- package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
- package/static/scripts/linny-r-gui-expression-editor.js +450 -0
- package/static/scripts/linny-r-gui-file-manager.js +392 -0
- package/static/scripts/linny-r-gui-finder.js +727 -0
- package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
- package/static/scripts/linny-r-gui-monitor.js +448 -0
- package/static/scripts/linny-r-gui-paper.js +2789 -0
- package/static/scripts/linny-r-gui-receiver.js +323 -0
- package/static/scripts/linny-r-gui-repository-browser.js +819 -0
- package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
- package/static/scripts/linny-r-gui-undo-redo.js +560 -0
- package/static/scripts/linny-r-model.js +34 -15
- package/static/scripts/linny-r-utils.js +11 -1
- package/static/scripts/linny-r-vm.js +21 -12
- 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
|