linny-r 1.9.2 → 2.0.1
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/LICENSE +1 -1
- package/README.md +4 -4
- package/package.json +1 -1
- package/server.js +1 -1
- package/static/images/eq-negated.png +0 -0
- package/static/images/power.png +0 -0
- package/static/images/tex.png +0 -0
- package/static/index.html +225 -10
- package/static/linny-r.css +458 -8
- package/static/scripts/linny-r-ctrl.js +6 -4
- package/static/scripts/linny-r-gui-actor-manager.js +1 -1
- package/static/scripts/linny-r-gui-chart-manager.js +20 -13
- package/static/scripts/linny-r-gui-constraint-editor.js +410 -50
- package/static/scripts/linny-r-gui-controller.js +127 -12
- package/static/scripts/linny-r-gui-dataset-manager.js +28 -20
- package/static/scripts/linny-r-gui-documentation-manager.js +11 -3
- package/static/scripts/linny-r-gui-equation-manager.js +1 -1
- package/static/scripts/linny-r-gui-experiment-manager.js +1 -1
- package/static/scripts/linny-r-gui-expression-editor.js +7 -1
- package/static/scripts/linny-r-gui-file-manager.js +31 -13
- package/static/scripts/linny-r-gui-finder.js +1 -1
- package/static/scripts/linny-r-gui-model-autosaver.js +1 -1
- package/static/scripts/linny-r-gui-monitor.js +1 -1
- package/static/scripts/linny-r-gui-paper.js +108 -25
- package/static/scripts/linny-r-gui-power-grid-manager.js +529 -0
- package/static/scripts/linny-r-gui-receiver.js +1 -1
- package/static/scripts/linny-r-gui-repository-browser.js +1 -1
- package/static/scripts/linny-r-gui-scale-unit-manager.js +1 -1
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +1 -1
- package/static/scripts/linny-r-gui-tex-manager.js +110 -0
- package/static/scripts/linny-r-gui-undo-redo.js +1 -1
- package/static/scripts/linny-r-milp.js +1 -1
- package/static/scripts/linny-r-model.js +1016 -155
- package/static/scripts/linny-r-utils.js +3 -3
- package/static/scripts/linny-r-vm.js +714 -248
- package/static/show-diff.html +1 -1
- package/static/show-png.html +1 -1
@@ -0,0 +1,529 @@
|
|
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-power-grid-manager.js) provides the GUI
|
9
|
+
functionality for the Linny-R Power Grid Manager dialog.
|
10
|
+
|
11
|
+
*/
|
12
|
+
|
13
|
+
/*
|
14
|
+
Copyright (c) 2017-2024 Delft University of Technology
|
15
|
+
|
16
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
17
|
+
of this software and associated documentation files (the "Software"), to deal
|
18
|
+
in the Software without restriction, including without limitation the rights to
|
19
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
20
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
21
|
+
so, subject to the following conditions:
|
22
|
+
|
23
|
+
The above copyright notice and this permission notice shall be included in
|
24
|
+
all copies or substantial portions of the Software.
|
25
|
+
|
26
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
27
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
28
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
29
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
30
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
31
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
32
|
+
SOFTWARE.
|
33
|
+
*/
|
34
|
+
|
35
|
+
// CLASS PowerGridManager (modal dialog!)
|
36
|
+
class PowerGridManager {
|
37
|
+
constructor() {
|
38
|
+
// Add the power grids modal
|
39
|
+
this.dialog = new ModalDialog('power-grids');
|
40
|
+
this.dialog.close.addEventListener('click',
|
41
|
+
() => POWER_GRID_MANAGER.dialog.hide());
|
42
|
+
// Make the add, edit and delete buttons of this modal responsive
|
43
|
+
this.dialog.element('new-btn').addEventListener('click',
|
44
|
+
() => POWER_GRID_MANAGER.promptForPowerGrid());
|
45
|
+
this.dialog.element('edit-btn').addEventListener('click',
|
46
|
+
() => POWER_GRID_MANAGER.editPowerGrid());
|
47
|
+
this.dialog.element('delete-btn').addEventListener('click',
|
48
|
+
() => POWER_GRID_MANAGER.deletePowerGrid());
|
49
|
+
// Add the power grid definition modal
|
50
|
+
this.new_power_grid_modal = new ModalDialog('new-power-grid');
|
51
|
+
this.new_power_grid_modal.ok.addEventListener(
|
52
|
+
'click', () => POWER_GRID_MANAGER.addNewPowerGrid());
|
53
|
+
this.new_power_grid_modal.cancel.addEventListener(
|
54
|
+
'click', () => POWER_GRID_MANAGER.new_power_grid_modal.hide());
|
55
|
+
this.scroll_area = this.dialog.element('scroll-area');
|
56
|
+
this.table = this.dialog.element('table');
|
57
|
+
// Properties used to infer the cyle basis used by the Virtual Machine
|
58
|
+
// to add constraints that enforce Kirchhoff's voltage law.
|
59
|
+
this.nodes = {};
|
60
|
+
this.edges = {};
|
61
|
+
this.spanning_tree = [];
|
62
|
+
this.tree_incidence = {};
|
63
|
+
this.cycle_edges = [];
|
64
|
+
this.cycle_basis = [];
|
65
|
+
this.min_length = 0;
|
66
|
+
this.max_length = 0;
|
67
|
+
this.total_length = 0;
|
68
|
+
this.messages = [];
|
69
|
+
}
|
70
|
+
|
71
|
+
get sortedGridIDs() {
|
72
|
+
// Return list of grid Ids that sort grids by (1) voltage and (2) name.
|
73
|
+
function kVnSort(a, b) {
|
74
|
+
const
|
75
|
+
pga = MODEL.power_grids[a],
|
76
|
+
pgb = MODEL.power_grids[b];
|
77
|
+
// NOTE: Highest voltage comes first.
|
78
|
+
if(pga.kilovolts > pgb.kilovolts) return -1;
|
79
|
+
if(pga.kilovolts < pgb.kilovolts) return 1;
|
80
|
+
// Names are sorted alphabetically.
|
81
|
+
return pga.name.localeCompare(pgb.name);
|
82
|
+
}
|
83
|
+
return Object.keys(MODEL.power_grids).sort(kVnSort);
|
84
|
+
}
|
85
|
+
|
86
|
+
updateGridMenu(modal) {
|
87
|
+
// Create inner HTML for a menu with voltages and names as title.
|
88
|
+
// The parameter `modal` identifies the modal for which this menu
|
89
|
+
// is generated. Th e selected plate will then be shown in the DIV
|
90
|
+
// identified by "`modal`-grid-plate".
|
91
|
+
const menu = UI.modals[modal].element('grid-plate-menu');
|
92
|
+
if(menu) {
|
93
|
+
const
|
94
|
+
html = [],
|
95
|
+
grids = this.sortedGridIDs;
|
96
|
+
html.push('<div id="', modal, '-gm-none" class="no-grid-plate" ',
|
97
|
+
'title="No grid element" onclick="UI.setGridPlate(event.target)">',
|
98
|
+
'(↯)</div>');
|
99
|
+
for(let i = 0; i < grids.length; i++) {
|
100
|
+
const pg = MODEL.power_grids[grids[i]];
|
101
|
+
html.push('<div id="', modal, '-gm-', pg.id,
|
102
|
+
'"class="menu-plate" style="background-color: ', pg.color,
|
103
|
+
'" title="Element of grid “', pg.name,
|
104
|
+
'”" onclick="UI.setGridPlate(event.target);">',
|
105
|
+
pg.voltage, '</div>');
|
106
|
+
}
|
107
|
+
menu.innerHTML = html.join('');
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
show() {
|
112
|
+
// Show the power grids for the current model.
|
113
|
+
// NOTE: Add/edit/delete actions operate on this list, so changes
|
114
|
+
// take immediate effect.
|
115
|
+
// NOTE: Power grid objects have a unique abstract identifier.
|
116
|
+
this.selected_grid = '';
|
117
|
+
this.last_time_selected = 0;
|
118
|
+
this.updateDialog();
|
119
|
+
this.dialog.show();
|
120
|
+
}
|
121
|
+
|
122
|
+
updateDialog() {
|
123
|
+
// Create the HTML for the power grids table and update the state
|
124
|
+
// of the action buttons.
|
125
|
+
if(!MODEL.power_grids.hasOwnProperty(this.selected_grid)) {
|
126
|
+
this.selected_grid = '';
|
127
|
+
}
|
128
|
+
const
|
129
|
+
keys = this.sortedGridIDs,
|
130
|
+
sl = [],
|
131
|
+
ss = this.selected_grid;
|
132
|
+
let ssid = 'scntr';
|
133
|
+
if(!keys.length) {
|
134
|
+
sl.push('<tr><td><em>No grids defined</em></td></tr>');
|
135
|
+
} else {
|
136
|
+
for(let i = 0; i < keys.length; i++) {
|
137
|
+
const
|
138
|
+
s = keys[i],
|
139
|
+
clk = '" onclick="POWER_GRID_MANAGER.selectPowerGrid(event, \'' +
|
140
|
+
s + '\'',
|
141
|
+
pg = MODEL.power_grids[s];
|
142
|
+
if(s === ss) ssid += i;
|
143
|
+
sl.push(['<tr id="scntr', i, '" class="dataset-modif',
|
144
|
+
(s === ss ? ' sel-set' : ''),
|
145
|
+
'"><td class="dataset-selector', clk, ');">',
|
146
|
+
'<div class="grid-kV-plate" style="background-color: ',
|
147
|
+
pg.color, '">', pg.voltage, '</div>',
|
148
|
+
'<div class="grid-watts">', pg.power_unit, '</div>',
|
149
|
+
(pg.kirchhoff ?
|
150
|
+
'<div class="grid-kcl-symbol">⟳</div>': ''),
|
151
|
+
(pg.loss_approximation ?
|
152
|
+
'<div class="grid-loss-symbol">L&sup' +
|
153
|
+
pg.loss_approximation + ';</div>' : ''),
|
154
|
+
'</div>', pg.name, '</td></tr>'].join(''));
|
155
|
+
}
|
156
|
+
}
|
157
|
+
this.table.innerHTML = sl.join('');
|
158
|
+
if(ss) UI.scrollIntoView(document.getElementById(ssid));
|
159
|
+
const btns = 'power-grids-edit power-grids-delete';
|
160
|
+
if(ss) {
|
161
|
+
UI.enableButtons(btns);
|
162
|
+
} else {
|
163
|
+
UI.disableButtons(btns);
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
selectPowerGrid(event, id, focus) {
|
168
|
+
// Select power grid, and when double-clicked, allow to edit it.
|
169
|
+
const
|
170
|
+
ss = this.selected_grid,
|
171
|
+
now = Date.now(),
|
172
|
+
dt = now - this.last_time_selected,
|
173
|
+
// NOTE: Alt-click and double-click indicate: edit.
|
174
|
+
// Consider click to be "double" if the same modifier was clicked
|
175
|
+
// less than 300 ms ago.
|
176
|
+
edit = event.altKey || (id === ss && dt < 300);
|
177
|
+
this.selected_grid = id;
|
178
|
+
this.last_time_selected = now;
|
179
|
+
if(edit) {
|
180
|
+
this.last_time_selected = 0;
|
181
|
+
this.promptForPowerGrid('Edit', focus);
|
182
|
+
return;
|
183
|
+
}
|
184
|
+
this.updateDialog();
|
185
|
+
}
|
186
|
+
|
187
|
+
promptForPowerGrid(action='Define new', focus='name') {
|
188
|
+
// Show the Add/Edit power grid dialog for the indicated action.
|
189
|
+
const md = this.new_power_grid_modal;
|
190
|
+
md.element('action').innerText = action;
|
191
|
+
let pg;
|
192
|
+
if(action === 'Edit' && this.selected_grid) {
|
193
|
+
pg = MODEL.power_grids[this.selected_grid];
|
194
|
+
} else {
|
195
|
+
// Use a dummy object to obtain default properties.
|
196
|
+
pg = new PowerGrid('');
|
197
|
+
}
|
198
|
+
md.element('name').value = pg.name;
|
199
|
+
md.element('voltage').value = pg.kilovolts;
|
200
|
+
md.element('color').value = pg.color;
|
201
|
+
md.element('unit').value = pg.power_unit;
|
202
|
+
UI.setBox('grid-kirchhoff', pg.kirchhoff);
|
203
|
+
md.element('losses').value = pg.loss_approximation;
|
204
|
+
this.new_power_grid_modal.show(focus);
|
205
|
+
}
|
206
|
+
|
207
|
+
addNewPowerGrid() {
|
208
|
+
// Add the new power grid or update the one being edited.
|
209
|
+
const
|
210
|
+
md = this.new_power_grid_modal,
|
211
|
+
edited = md.element('action').innerText === 'Edit',
|
212
|
+
n = UI.cleanName(md.element('name').value);
|
213
|
+
if(!n) {
|
214
|
+
// Do not accept empty string as name
|
215
|
+
UI.warn('Power grid must have a name');
|
216
|
+
md.element('name').focus();
|
217
|
+
return;
|
218
|
+
}
|
219
|
+
let pg = MODEL.powerGridByName(n);
|
220
|
+
if(pg && !edited) {
|
221
|
+
// Do not accept name of existing grid as name for new grid.
|
222
|
+
UI.warn(`Power grid "${pg.name}" is already defined`);
|
223
|
+
md.element('name').focus();
|
224
|
+
return;
|
225
|
+
}
|
226
|
+
const
|
227
|
+
e = md.element('voltage'),
|
228
|
+
kv = safeStrToFloat(e.value, 0);
|
229
|
+
if(kv <= 0 || kv > 5000) {
|
230
|
+
UI.warn(`Voltage must be positive (up to 5 MV)`);
|
231
|
+
e.focus();
|
232
|
+
return;
|
233
|
+
}
|
234
|
+
pg = (edited ? MODEL.powerGridByID(this.selected_grid) :
|
235
|
+
MODEL.addPowerGrid(randomID()));
|
236
|
+
pg.name = n;
|
237
|
+
pg.kilovolts = kv;
|
238
|
+
pg.color = md.element('color').value;
|
239
|
+
pg.power_unit = md.element('unit').value;
|
240
|
+
pg.kirchhoff = UI.boxChecked('grid-kirchhoff');
|
241
|
+
pg.loss_approximation = parseInt(md.element('losses').value);
|
242
|
+
md.hide();
|
243
|
+
this.updateDialog();
|
244
|
+
}
|
245
|
+
|
246
|
+
editPowerGrid() {
|
247
|
+
// Allow user to edit name and/or value.
|
248
|
+
if(this.selected_grid) this.promptForPowerGrid('Edit');
|
249
|
+
}
|
250
|
+
|
251
|
+
deletePowerGrid() {
|
252
|
+
// Allow user to delete, but warn if some processes are labeled as
|
253
|
+
// part of this power grid.
|
254
|
+
if(this.selected_grid) {
|
255
|
+
// @@@TO DO: check whether grid is used in the model.
|
256
|
+
// If so, ask user to confirm to remove grid property from all
|
257
|
+
// process elements having this grid.
|
258
|
+
delete MODEL.power_grids[this.selected_grid];
|
259
|
+
this.updateDialog();
|
260
|
+
}
|
261
|
+
}
|
262
|
+
|
263
|
+
checkLengths() {
|
264
|
+
// Calculate lengt statistics for all grid processes.
|
265
|
+
this.min_length = 1e+10;
|
266
|
+
this.max_length = 0;
|
267
|
+
this.total_length = 0;
|
268
|
+
for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
|
269
|
+
const p = MODEL.processes[k];
|
270
|
+
// NOTE: Do not include processes in clusters that should be ignored.
|
271
|
+
if(p.grid && !MODEL.ignored_entities[p.identifier]) {
|
272
|
+
this.min_length = Math.min(p.length_in_km, this.min_length);
|
273
|
+
this.max_length = Math.max(p.length_in_km, this.max_length);
|
274
|
+
this.total_length += p.length_in_km;
|
275
|
+
}
|
276
|
+
}
|
277
|
+
}
|
278
|
+
|
279
|
+
inferNodesAndEdges() {
|
280
|
+
// Infer graph structure of combined power grids for which losses
|
281
|
+
// and/or Kirchhoff's voltage law must be enforced.
|
282
|
+
this.nodes = {};
|
283
|
+
this.edges = {};
|
284
|
+
this.messages.length = 0;
|
285
|
+
// NOTE: Recalculate length statistics now only for "real" grid edges.
|
286
|
+
this.min_length = 1e+10;
|
287
|
+
this.max_length = 0;
|
288
|
+
this.total_length = 0;
|
289
|
+
let link_delays = 0;
|
290
|
+
for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
|
291
|
+
const p = MODEL.processes[k];
|
292
|
+
// NOTE: Do not include processes in clusters that should be ignored.
|
293
|
+
if(p.grid && !MODEL.ignored_entities[p.identifier]) {
|
294
|
+
const mlmsg = [];
|
295
|
+
let fn = null,
|
296
|
+
tn = null;
|
297
|
+
for(let i = 0; i < p.inputs.length; i++) {
|
298
|
+
const l = p.inputs[i];
|
299
|
+
if(l.multiplier === VM.LM_LEVEL &&
|
300
|
+
!MODEL.ignored_entities[l.identifier]) {
|
301
|
+
if(fn) {
|
302
|
+
mlmsg.push('more than 1 input');
|
303
|
+
} else {
|
304
|
+
fn = l.from_node;
|
305
|
+
}
|
306
|
+
}
|
307
|
+
}
|
308
|
+
if(!fn) mlmsg.push('no inputs');
|
309
|
+
for(let i = 0; i < p.outputs.length; i++) {
|
310
|
+
const l = p.outputs[i];
|
311
|
+
if(l.multiplier === VM.LM_LEVEL &&
|
312
|
+
!MODEL.ignored_entities[l.identifier]) {
|
313
|
+
if(tn) {
|
314
|
+
mlmsg.push('more than 1 output');
|
315
|
+
} else {
|
316
|
+
tn = l.to_node;
|
317
|
+
}
|
318
|
+
}
|
319
|
+
}
|
320
|
+
if(!tn) mlmsg.push('no outputs');
|
321
|
+
if(mlmsg.length) {
|
322
|
+
// Process is not linked as a grid element.
|
323
|
+
this.messages.push(VM.WARNING + ' Grid process "' +
|
324
|
+
p.displayName + '" has ' + mlmsg.join(' and '));
|
325
|
+
} else {
|
326
|
+
// Check whether the output link has a delay; this will be ignored.
|
327
|
+
const delay = p.outputs[0].flow_delay;
|
328
|
+
if(delay.defined && delay.text !== '0') link_delays++;
|
329
|
+
// Add FROM node and TO node to graph.
|
330
|
+
const
|
331
|
+
fnid = fn.identifier,
|
332
|
+
tnid = tn.identifier,
|
333
|
+
edge = {process: p, from_node: fnid, to_node: tnid};
|
334
|
+
// NOTE: Key uniqueness ensures that nodes are unique.
|
335
|
+
this.nodes[fnid] = fn;
|
336
|
+
this.nodes[tnid] = tn;
|
337
|
+
// Add edge to graph, identified by its process ID.
|
338
|
+
this.edges[p.identifier] = edge;
|
339
|
+
this.min_length = Math.min(p.length_in_km, this.min_length);
|
340
|
+
this.max_length = Math.max(p.length_in_km, this.max_length);
|
341
|
+
this.total_length += p.length_in_km;
|
342
|
+
}
|
343
|
+
}
|
344
|
+
}
|
345
|
+
if(link_delays > 0) this.messages.push(
|
346
|
+
`${VM.WARNING} ${pluralS(link_delays, 'link delay')} will be ignored`);
|
347
|
+
var ecnt = Object.keys(this.edges).length,
|
348
|
+
grid = [pluralS(Object.keys(this.nodes).length, 'node'),
|
349
|
+
pluralS(ecnt, 'edge'), `total length: ${this.total_length} km`];
|
350
|
+
if(!ecnt) {
|
351
|
+
this.min_length = 0;
|
352
|
+
} else if(ecnt > 1) {
|
353
|
+
grid.push(`range: ${this.min_length} - ${this.max_length} km`);
|
354
|
+
}
|
355
|
+
this.messages.push('Overall power grid comprises ' +
|
356
|
+
grid.join(', ').toLowerCase());
|
357
|
+
}
|
358
|
+
|
359
|
+
inferSpanningTree() {
|
360
|
+
// Use Kruksal's algorithm to build spanning tree.
|
361
|
+
// NOTE: Tree needs not be minimal, so edges are not sorted.
|
362
|
+
this.spanning_tree.length = 0;
|
363
|
+
this.cycle_edges.length = 0;
|
364
|
+
this.tree_incidence = {};
|
365
|
+
const node_set = {};
|
366
|
+
for(let k in this.edges) if(this.edges.hasOwnProperty(k)) {
|
367
|
+
const
|
368
|
+
edge = this.edges[k],
|
369
|
+
efn = edge.from_node,
|
370
|
+
etn = edge.to_node,
|
371
|
+
kvl = edge.process.grid.kirchhoff,
|
372
|
+
fn_in_tree = node_set.hasOwnProperty(efn),
|
373
|
+
tn_in_tree = node_set.hasOwnProperty(etn);
|
374
|
+
// Only add edges of grids for which Kirchhoff's voltage law
|
375
|
+
// has to be enforced.
|
376
|
+
if(kvl) {
|
377
|
+
if(fn_in_tree && tn_in_tree) {
|
378
|
+
// Edge forms a cycle, so add it to the cycle edge list.
|
379
|
+
this.cycle_edges.push(edge);
|
380
|
+
} else {
|
381
|
+
// Edge is not incident with *two* nodes already in the tree, so
|
382
|
+
// add it to the tree.
|
383
|
+
this.spanning_tree.push(edge);
|
384
|
+
node_set[efn] = true;
|
385
|
+
node_set[etn] = true;
|
386
|
+
}
|
387
|
+
const ti = this.tree_incidence;
|
388
|
+
// Always record that both its nodes are incident with it.
|
389
|
+
if(ti.hasOwnProperty(efn)) {
|
390
|
+
ti[efn].push(edge);
|
391
|
+
} else {
|
392
|
+
ti[efn] = [edge];
|
393
|
+
}
|
394
|
+
if(ti.hasOwnProperty(etn)) {
|
395
|
+
ti[etn].push(edge);
|
396
|
+
} else {
|
397
|
+
ti[etn] = [edge];
|
398
|
+
}
|
399
|
+
}
|
400
|
+
}
|
401
|
+
}
|
402
|
+
|
403
|
+
pathInSpanningTree(fn, tn, path) {
|
404
|
+
// Recursively constructs `path` as the list of edges forming the path
|
405
|
+
// from `fn` to `tn` in the spanning tree of this grid.
|
406
|
+
// If edge connects path with TO node, `path` is complete.
|
407
|
+
if(fn === tn) return true;
|
408
|
+
const elist = this.tree_incidence[fn];
|
409
|
+
for(let i = 0; i < elist.length; i++) {
|
410
|
+
const e = elist[i];
|
411
|
+
// Ignore edges already in the path.
|
412
|
+
if(path.indexOf(e) < 0) {
|
413
|
+
// NOTE: Edges are directed, but should not be considered as such.
|
414
|
+
const nn = (e.from_node === fn ? e.to_node : e.from_node);
|
415
|
+
path.push(e);
|
416
|
+
if(this.pathInSpanningTree(nn, tn, path)) return true;
|
417
|
+
path.pop();
|
418
|
+
}
|
419
|
+
}
|
420
|
+
return false;
|
421
|
+
}
|
422
|
+
|
423
|
+
inferCycleBasis() {
|
424
|
+
// Construct the list of fundamental cycles in the network.
|
425
|
+
this.cycle_basis.length = 0;
|
426
|
+
if(!(MODEL.with_power_flow && MODEL.powerGridsWithKVL.length)) return;
|
427
|
+
this.inferNodesAndEdges();
|
428
|
+
this.inferSpanningTree();
|
429
|
+
for(let i = 0; i < this.cycle_edges.length; i++) {
|
430
|
+
const
|
431
|
+
edge = this.cycle_edges[i],
|
432
|
+
path = [];
|
433
|
+
if(this.pathInSpanningTree(edge.from_node, edge.to_node, path)) {
|
434
|
+
// Add flags that indicate whether the edge on the path is reversed.
|
435
|
+
// The closing edge determines the orientation.
|
436
|
+
const cycle = [{process: edge.process, orientation: 1}];
|
437
|
+
let node = edge.to_node;
|
438
|
+
for(let i = path.length - 1; i >= 0; i--) {
|
439
|
+
const
|
440
|
+
pe = path[i],
|
441
|
+
ce = {process: pe.process};
|
442
|
+
if(pe.from_node === node) {
|
443
|
+
ce.orientation = 1;
|
444
|
+
node = pe.to_node;
|
445
|
+
} else {
|
446
|
+
ce.orientation = -1;
|
447
|
+
node = pe.from_node;
|
448
|
+
}
|
449
|
+
cycle.push(ce);
|
450
|
+
}
|
451
|
+
this.cycle_basis.push(cycle);
|
452
|
+
}
|
453
|
+
}
|
454
|
+
}
|
455
|
+
|
456
|
+
get cycleBasisAsString() {
|
457
|
+
// Return description of cycle basis.
|
458
|
+
const ll = [pluralS(this.cycle_basis.length, 'fundamental cycle') + ':'];
|
459
|
+
for(let i = 0; i < this.cycle_basis.length; i++) {
|
460
|
+
const
|
461
|
+
c = this.cycle_basis[i],
|
462
|
+
l = [];
|
463
|
+
for(let j = 0; j < c.length; j++) {
|
464
|
+
l.push(c[j].process.displayName +
|
465
|
+
` [${c[j].orientation > 0 ? '+' : '-'}]`);
|
466
|
+
}
|
467
|
+
ll.push(`(${i+1}) ${l.join(', ')}`);
|
468
|
+
}
|
469
|
+
return ll.join('\n');
|
470
|
+
}
|
471
|
+
|
472
|
+
cycleFlowTable(c) {
|
473
|
+
// Return flows through cycle `c` as an HTML table.
|
474
|
+
if(!MODEL.solved) return '';
|
475
|
+
const html = ['<table class="power-flow">',
|
476
|
+
'<tr><th colspan="2">Grid process</th>' +
|
477
|
+
'<th title="Reactance"><em>x</em></th><th title="Power">P</th>' +
|
478
|
+
'<th><em>x</em>P</th></tr>'];
|
479
|
+
let sum = 0;
|
480
|
+
for(let i = 0; i < c.length; i++) {
|
481
|
+
const
|
482
|
+
edge = c[i],
|
483
|
+
p = edge.process,
|
484
|
+
x = p.length_in_km * p.grid.reactancePerKm,
|
485
|
+
l = p.actualLevel(MODEL.t);
|
486
|
+
html.push(`<tr><td title="${p.gridEdge}">${p.displayName}</td>` +
|
487
|
+
`</td><td>[${edge.orientation > 0 ? '+' : '−'}]</td>` +
|
488
|
+
`<td>${(Math.round(x * 10000) / 10000).toFixed(4)}</td>` +
|
489
|
+
`<td>${VM.sig4Dig(l)}</td>` +
|
490
|
+
`<td>${(x * l).toFixed(2)}</td></tr>`);
|
491
|
+
sum += edge.orientation * x * l;
|
492
|
+
}
|
493
|
+
html.push('<tr><td colspan="4"><strong>Sum Σ<em>x</em>P</strong> ' +
|
494
|
+
`<em>(should be zero)</em></td><td>${sum.toPrecision(2)}</td></tr>`);
|
495
|
+
html.push('</table><br>');
|
496
|
+
return html.join('\n');
|
497
|
+
}
|
498
|
+
|
499
|
+
inCycle(p) {
|
500
|
+
// Return TRUE if process `p` is an edge in some cycle in the cycle basis.
|
501
|
+
for(let i = 0; i < this.cycle_basis.length; i++) {
|
502
|
+
const c = this.cycle_basis[i];
|
503
|
+
for(let j = 0; j < c.length; j++) {
|
504
|
+
if(c[j].process === p) return true;
|
505
|
+
}
|
506
|
+
}
|
507
|
+
return false;
|
508
|
+
}
|
509
|
+
|
510
|
+
|
511
|
+
allCycleFlows(p) {
|
512
|
+
// Return power flows for each cycle that `p` is part of as an HTML
|
513
|
+
// table (so it can be displayed in the documentation dialog).
|
514
|
+
if(!MODEL.solved) return '';
|
515
|
+
const flows = [];
|
516
|
+
for(let i = 0; i < this.cycle_basis.length; i++) {
|
517
|
+
const c = this.cycle_basis[i];
|
518
|
+
for(let j = 0; j < c.length; j++) {
|
519
|
+
if(c[j].process === p) {
|
520
|
+
flows.push(`<h3>Flows through cycle (${i}):</h3>`,
|
521
|
+
this.cycleFlowTable(c));
|
522
|
+
break;
|
523
|
+
}
|
524
|
+
}
|
525
|
+
}
|
526
|
+
return flows.join('\n');
|
527
|
+
}
|
528
|
+
|
529
|
+
} // END of class PowerGridManager
|
@@ -13,7 +13,7 @@ executed, perform this operation, and write report files to the user space.
|
|
13
13
|
*/
|
14
14
|
|
15
15
|
/*
|
16
|
-
Copyright (c) 2017-
|
16
|
+
Copyright (c) 2017-2024 Delft University of Technology
|
17
17
|
|
18
18
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
19
19
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -12,7 +12,7 @@ GUIRepositoryBrowser).
|
|
12
12
|
*/
|
13
13
|
|
14
14
|
/*
|
15
|
-
Copyright (c) 2017-
|
15
|
+
Copyright (c) 2017-2024 Delft University of Technology
|
16
16
|
|
17
17
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
18
18
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -11,7 +11,7 @@ functionality for the Linny-R Scale Unit Manager dialog.
|
|
11
11
|
*/
|
12
12
|
|
13
13
|
/*
|
14
|
-
Copyright (c) 2017-
|
14
|
+
Copyright (c) 2017-2024 Delft University of Technology
|
15
15
|
|
16
16
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
17
17
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -11,7 +11,7 @@ for the Linny-R Sensitivity Analysis dialog.
|
|
11
11
|
*/
|
12
12
|
|
13
13
|
/*
|
14
|
-
Copyright (c) 2017-
|
14
|
+
Copyright (c) 2017-2024 Delft University of Technology
|
15
15
|
|
16
16
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
17
17
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -0,0 +1,110 @@
|
|
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-docu.js) provides the GUI functionality
|
9
|
+
for the Linny-R model documentation manager: the draggable dialog that allows
|
10
|
+
viewing and editing documentation text for model entities.
|
11
|
+
|
12
|
+
*/
|
13
|
+
|
14
|
+
/*
|
15
|
+
Copyright (c) 2017-2024 Delft University of Technology
|
16
|
+
|
17
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
18
|
+
of this software and associated documentation files (the "Software"), to deal
|
19
|
+
in the Software without restriction, including without limitation the rights to
|
20
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
21
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
22
|
+
so, subject to the following conditions:
|
23
|
+
|
24
|
+
The above copyright notice and this permission notice shall be included in
|
25
|
+
all copies or substantial portions of the Software.
|
26
|
+
|
27
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
28
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
29
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
30
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
31
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
32
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
33
|
+
SOFTWARE.
|
34
|
+
*/
|
35
|
+
|
36
|
+
// CLASS TEXManager
|
37
|
+
class TEXManager {
|
38
|
+
constructor() {
|
39
|
+
this.dialog = UI.draggableDialog('tex');
|
40
|
+
UI.resizableDialog('tex', 'TEX_MANAGER');
|
41
|
+
this.close_btn = document.getElementById('tex-close-btn');
|
42
|
+
this.entity_div = document.getElementById('tex-entity');
|
43
|
+
this.formula_pane = document.getElementById('tex-formula');
|
44
|
+
this.formula_tab = document.getElementById('tex-formula-tab');
|
45
|
+
this.code_pane = document.getElementById('tex-code');
|
46
|
+
this.code_tab = document.getElementById('tex-code-tab');
|
47
|
+
// Add listeners to controls.
|
48
|
+
this.close_btn.addEventListener(
|
49
|
+
'click', (event) => UI.toggleDialog(event));
|
50
|
+
this.formula_tab.addEventListener(
|
51
|
+
'click', () => TEX_MANAGER.showFormula());
|
52
|
+
this.code_tab.addEventListener(
|
53
|
+
'click', () => TEX_MANAGER.showCode());
|
54
|
+
// Initialize properties
|
55
|
+
this.reset();
|
56
|
+
}
|
57
|
+
|
58
|
+
reset() {
|
59
|
+
this.entity = null;
|
60
|
+
this.visible = false;
|
61
|
+
this.editing = false;
|
62
|
+
// KaTeX is loaded dynamically from remote site. If that fails,
|
63
|
+
// disable the button and hide it completely.
|
64
|
+
const btn = document.getElementById('tex-btn');
|
65
|
+
if(typeof window.katex === 'undefined') {
|
66
|
+
console.log('KaTeX not loaded - possibly not connected to internet');
|
67
|
+
btn.classList.remove('enab');
|
68
|
+
btn.classList.add('disab');
|
69
|
+
btn.style.display = 'none';
|
70
|
+
} else {
|
71
|
+
btn.classList.remove('disab');
|
72
|
+
btn.classList.add('enab');
|
73
|
+
}
|
74
|
+
this.showFormula();
|
75
|
+
}
|
76
|
+
|
77
|
+
updateDialog() {
|
78
|
+
// Resizing dialog may require re-rendering.
|
79
|
+
}
|
80
|
+
|
81
|
+
update(e) {
|
82
|
+
// Display name of entity under cursor on the infoline, and details
|
83
|
+
// in the documentation dialog.
|
84
|
+
if(!e || typeof window.katex === 'undefined') return;
|
85
|
+
let et = e.type,
|
86
|
+
edn = e.displayName;
|
87
|
+
if(et === 'Product' || et === 'Process') {
|
88
|
+
this.entity_div.innerHTML = `<em>${et}:</em> ${edn}`;
|
89
|
+
this.code_pane.value = e.TEXcode;
|
90
|
+
katex.render(this.code_pane.value, this.formula_pane,
|
91
|
+
{ throwOnError: false });
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
showFormula() {
|
96
|
+
this.code_pane.style.display = 'none';
|
97
|
+
this.code_tab.classList.remove('sel-tab');
|
98
|
+
this.formula_pane.style.display = 'block';
|
99
|
+
this.formula_tab.classList.add('sel-tab');
|
100
|
+
}
|
101
|
+
|
102
|
+
showCode() {
|
103
|
+
this.formula_pane.style.display = 'none';
|
104
|
+
this.formula_tab.classList.remove('sel-tab');
|
105
|
+
this.code_pane.style.display = 'block';
|
106
|
+
this.code_tab.classList.add('sel-tab');
|
107
|
+
}
|
108
|
+
|
109
|
+
|
110
|
+
} // END of class TEXManager
|
@@ -11,7 +11,7 @@ functionality for the Linny-R model editor.
|
|
11
11
|
*/
|
12
12
|
|
13
13
|
/*
|
14
|
-
Copyright (c) 2017-
|
14
|
+
Copyright (c) 2017-2024 Delft University of Technology
|
15
15
|
|
16
16
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
17
17
|
of this software and associated documentation files (the "Software"), to deal
|