linny-r 1.1.0
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 +21 -0
- package/README.md +312 -0
- package/console.js +973 -0
- package/package.json +32 -0
- package/server.js +1547 -0
- package/static/fonts/FantasqueSansMono-Bold.ttf +0 -0
- package/static/fonts/FantasqueSansMono-BoldItalic.ttf +0 -0
- package/static/fonts/FantasqueSansMono-Italic.ttf +0 -0
- package/static/fonts/FantasqueSansMono-Regular.ttf +0 -0
- package/static/fonts/Hack-Bold.ttf +0 -0
- package/static/fonts/Hack-BoldItalic.ttf +0 -0
- package/static/fonts/Hack-Italic.ttf +0 -0
- package/static/fonts/Hack-Regular.ttf +0 -0
- package/static/fonts/Lato-Bold.ttf +0 -0
- package/static/fonts/Lato-BoldItalic.ttf +0 -0
- package/static/fonts/Lato-Italic.ttf +0 -0
- package/static/fonts/Lato-Regular.ttf +0 -0
- package/static/fonts/mplus-1m-bold.ttf +0 -0
- package/static/fonts/mplus-1m-light.ttf +0 -0
- package/static/fonts/mplus-1m-medium.ttf +0 -0
- package/static/fonts/mplus-1m-regular.ttf +0 -0
- package/static/fonts/mplus-1m-thin.ttf +0 -0
- package/static/images/access.png +0 -0
- package/static/images/actor.png +0 -0
- package/static/images/actors.png +0 -0
- package/static/images/add-selector.png +0 -0
- package/static/images/add.png +0 -0
- package/static/images/back.png +0 -0
- package/static/images/black-box.png +0 -0
- package/static/images/by-sa.svg +74 -0
- package/static/images/cancel.png +0 -0
- package/static/images/chart.png +0 -0
- package/static/images/check-disab.png +0 -0
- package/static/images/check-off.png +0 -0
- package/static/images/check-on.png +0 -0
- package/static/images/check-x.png +0 -0
- package/static/images/clone.png +0 -0
- package/static/images/close.png +0 -0
- package/static/images/cluster.png +0 -0
- package/static/images/compare.png +0 -0
- package/static/images/compress.png +0 -0
- package/static/images/constraint.png +0 -0
- package/static/images/copy.png +0 -0
- package/static/images/data-to-clpbrd.png +0 -0
- package/static/images/dataset.png +0 -0
- package/static/images/delete.png +0 -0
- package/static/images/diagram.png +0 -0
- package/static/images/down.png +0 -0
- package/static/images/edit-chart.png +0 -0
- package/static/images/edit.png +0 -0
- package/static/images/eq.png +0 -0
- package/static/images/equation.png +0 -0
- package/static/images/experiment.png +0 -0
- package/static/images/favicon.ico +0 -0
- package/static/images/fewer-dec.png +0 -0
- package/static/images/filter.png +0 -0
- package/static/images/find.png +0 -0
- package/static/images/forward.png +0 -0
- package/static/images/host-logo.png +0 -0
- package/static/images/icon.png +0 -0
- package/static/images/icon.svg +23 -0
- package/static/images/ignore.png +0 -0
- package/static/images/include.png +0 -0
- package/static/images/info-to-clpbrd.png +0 -0
- package/static/images/info.png +0 -0
- package/static/images/is-black-box.png +0 -0
- package/static/images/lbl.png +0 -0
- package/static/images/lift.png +0 -0
- package/static/images/link.png +0 -0
- package/static/images/linny-r.icns +0 -0
- package/static/images/linny-r.ico +0 -0
- package/static/images/linny-r.png +0 -0
- package/static/images/linny-r.svg +21 -0
- package/static/images/logo.png +0 -0
- package/static/images/model-info.png +0 -0
- package/static/images/module.png +0 -0
- package/static/images/monitor.png +0 -0
- package/static/images/more-dec.png +0 -0
- package/static/images/ne.png +0 -0
- package/static/images/new.png +0 -0
- package/static/images/note.png +0 -0
- package/static/images/ok.png +0 -0
- package/static/images/open.png +0 -0
- package/static/images/outcome.png +0 -0
- package/static/images/parent.png +0 -0
- package/static/images/paste.png +0 -0
- package/static/images/pause.png +0 -0
- package/static/images/print-chart.png +0 -0
- package/static/images/print.png +0 -0
- package/static/images/process.png +0 -0
- package/static/images/product.png +0 -0
- package/static/images/pwlf.png +0 -0
- package/static/images/receiver.png +0 -0
- package/static/images/redo.png +0 -0
- package/static/images/remove.png +0 -0
- package/static/images/rename.png +0 -0
- package/static/images/repo-logo.png +0 -0
- package/static/images/repository.png +0 -0
- package/static/images/reset.png +0 -0
- package/static/images/resize.png +0 -0
- package/static/images/restore.png +0 -0
- package/static/images/save-chart.png +0 -0
- package/static/images/save-data.png +0 -0
- package/static/images/save-diagram.png +0 -0
- package/static/images/save.png +0 -0
- package/static/images/sensitivity.png +0 -0
- package/static/images/settings.png +0 -0
- package/static/images/solve.png +0 -0
- package/static/images/solver-logo.png +0 -0
- package/static/images/stats-to-clpbrd.png +0 -0
- package/static/images/stats.png +0 -0
- package/static/images/stop.png +0 -0
- package/static/images/store.png +0 -0
- package/static/images/stretch.png +0 -0
- package/static/images/table-to-clpbrd.png +0 -0
- package/static/images/table.png +0 -0
- package/static/images/tree.png +0 -0
- package/static/images/tudelft.png +0 -0
- package/static/images/ubl.png +0 -0
- package/static/images/undo.png +0 -0
- package/static/images/up.png +0 -0
- package/static/images/zoom-in.png +0 -0
- package/static/images/zoom-out.png +0 -0
- package/static/index.html +3088 -0
- package/static/linny-r.css +4722 -0
- package/static/scripts/iro.min.js +7 -0
- package/static/scripts/linny-r-config.js +105 -0
- package/static/scripts/linny-r-ctrl.js +1199 -0
- package/static/scripts/linny-r-gui.js +14814 -0
- package/static/scripts/linny-r-milp.js +286 -0
- package/static/scripts/linny-r-model.js +10405 -0
- package/static/scripts/linny-r-utils.js +687 -0
- package/static/scripts/linny-r-vm.js +7079 -0
- package/static/show-diff.html +84 -0
- package/static/show-png.html +113 -0
- package/static/sounds/error.wav +0 -0
- package/static/sounds/notification.wav +0 -0
- package/static/sounds/warning.wav +0 -0
@@ -0,0 +1,1199 @@
|
|
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-ctrl.js) provides the basic controllers to
|
9
|
+
run a Linny-R model without its browser-based GUI. The classes defined in this
|
10
|
+
file all have their graphical extensions in file linny-r-gui.js.
|
11
|
+
|
12
|
+
*/
|
13
|
+
|
14
|
+
/*
|
15
|
+
Copyright (c) 2017-2022 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 Controller {
|
37
|
+
constructor() {
|
38
|
+
this.console = true;
|
39
|
+
// Initialize *graphical* controller elements as non-existent
|
40
|
+
this.paper = null;
|
41
|
+
this.buttons = {};
|
42
|
+
this.modals = {};
|
43
|
+
this.dialogs = {};
|
44
|
+
// Default chart colors (12 line colors + 12 matching lighter shades)
|
45
|
+
this.chart_colors = [
|
46
|
+
'#2e86de', '#ff9f43', '#8395a7', '#10ac84', '#f368e0',
|
47
|
+
'#0abde3', '#ee5253', '#222f3e', '#01a3a4', '#341f97',
|
48
|
+
'#974b33', '#999751',
|
49
|
+
// Lighter shades for areas (or additional lines if > 10)
|
50
|
+
'#54a0ff', '#feca57', '#c8d6e5', '#1dd1a1', '#ff9ff3',
|
51
|
+
'#48dbfb', '#ff6b6b', '#576574', '#00d2d3', '#5f27cd',
|
52
|
+
'#c86a5b', '#c2c18c'
|
53
|
+
];
|
54
|
+
// SVG stroke dash arrays for line types while drawing charts or arrows
|
55
|
+
this.sda = {
|
56
|
+
dash: '8,3',
|
57
|
+
dot: '2,3',
|
58
|
+
dash_dot: '7,3,2,3',
|
59
|
+
long_dash: '12,3',
|
60
|
+
longer_dash: '15,3',
|
61
|
+
short_dash: '5,2',
|
62
|
+
shorter_dash: '0.5,2.5',
|
63
|
+
long_dash_dot: '10,3,2,3',
|
64
|
+
even_dash: '6,5',
|
65
|
+
dot_dot: '2,3,2,6'
|
66
|
+
};
|
67
|
+
// Error messages
|
68
|
+
this.ERROR = {
|
69
|
+
CREATE_FAILED: 'ERROR: failed to create a new SVG element',
|
70
|
+
APPEND_FAILED: 'ERROR: failed to append SVG element to DOM',
|
71
|
+
NO_DATASET_DOT: '"." only makes sense in dataset modifier expressions'
|
72
|
+
};
|
73
|
+
this.WARNING = {
|
74
|
+
NO_CONNECTION: 'No connection with server',
|
75
|
+
INVALID_ACTOR_NAME: 'Invalid actor name',
|
76
|
+
SELECTOR_SYNTAX: 'Selector can contain only letters, digits, +, -, % and wildcards',
|
77
|
+
SINGLE_WILDCARD: 'Selector can contain only one *',
|
78
|
+
INVALID_SELECTOR: 'Invalid selector'
|
79
|
+
};
|
80
|
+
this.NOTICE = {
|
81
|
+
WORK_IN_PROGRESS: 'Planned feature -- work in progress!',
|
82
|
+
NO_CHARTS: 'While an experiment is running, charts cannot be viewed'
|
83
|
+
};
|
84
|
+
// Strings used to identify special entities
|
85
|
+
this.TOP_CLUSTER_NAME = '(top cluster)';
|
86
|
+
// In earlier versions, this name was different => automatic conversion
|
87
|
+
this.FORMER_TOP_CLUSTER_NAME = '___TOP_CLUSTER___';
|
88
|
+
// In legacy Linny-R, this name was again different
|
89
|
+
this.LEGACY_TOP_CLUSTER_NAME = '* TOP CLUSTER *';
|
90
|
+
// Likewise, the "no actor" actor has a standard name
|
91
|
+
this.NO_ACTOR = '(no actor)';
|
92
|
+
// As of version 0.9x50, equations are implemented as modifiers of a special
|
93
|
+
// dataset, that therefore must have a system name
|
94
|
+
this.EQUATIONS_DATASET_NAME = '___EQUATIONS___';
|
95
|
+
this.EQUATIONS_DATASET_ID = this.EQUATIONS_DATASET_NAME.toLowerCase();
|
96
|
+
// Character to separate object name from attribute in variable names
|
97
|
+
this.OA_SEPARATOR = '|';
|
98
|
+
// Use colon with space to separate prefixes and names of clones
|
99
|
+
this.PREFIXER = ': ';
|
100
|
+
// FROM->TO represented by solid right-pointing arrow with triangular head
|
101
|
+
this.LINK_ARROW = '\u279D';
|
102
|
+
// Right arrow with wave-curved shaft
|
103
|
+
this.CONSTRAINT_ARROW = '\u219D';
|
104
|
+
// Prefix for "black boxed" entities: solid black square
|
105
|
+
this.BLACK_BOX = '\u25FC';
|
106
|
+
this.BLACK_BOX_PREFIX = this.BLACK_BOX + ' ';
|
107
|
+
this.MC = {
|
108
|
+
// Difference types used in model comparison
|
109
|
+
ADDED: 1,
|
110
|
+
DELETED: 2,
|
111
|
+
MODIFIED: 3,
|
112
|
+
STATE: {1: 'added', 2: 'deleted', 3: 'modified'},
|
113
|
+
SETTINGS_PROPS: {
|
114
|
+
comments: 'Model description',
|
115
|
+
last_modified: 'Last modified',
|
116
|
+
version: 'Linny-R version',
|
117
|
+
encrypt: 'Encrypt',
|
118
|
+
time_scale: 'Time scale',
|
119
|
+
time_unit: 'Time unit',
|
120
|
+
currency_unit: 'Currency',
|
121
|
+
default_unit: 'Default unit',
|
122
|
+
decimal_comma: 'Use decimal comma',
|
123
|
+
grid_pixels: 'Grid resolution',
|
124
|
+
align_to_grid: 'Align to grid',
|
125
|
+
infer_cost_prices: 'Infer cost prices',
|
126
|
+
show_block_arrows: 'Show block arrows',
|
127
|
+
timeout_period: 'Solver time-out',
|
128
|
+
block_length: 'Block length',
|
129
|
+
start_period: 'Start at',
|
130
|
+
end_period: 'End at',
|
131
|
+
look_ahead: 'Look-ahead'
|
132
|
+
},
|
133
|
+
ENTITY_PROPS: ['actors', 'clusters', 'processes', 'products', 'datasets',
|
134
|
+
'equations', 'links', 'constraints'],
|
135
|
+
ACTOR_PROPS: ['weight', 'comments'],
|
136
|
+
CLUSTER_PROPS: ['comments', 'collapsed', 'ignore'],
|
137
|
+
PROCESS_PROPS: ['comments', 'lower_bound', 'upper_bound', 'initial_level',
|
138
|
+
'pace', 'equal_bounds', 'level_to_zero', 'integer_level', 'collapsed'],
|
139
|
+
PRODUCT_PROPS: ['comments', 'lower_bound', 'upper_bound', 'initial_level',
|
140
|
+
'scale_unit', 'equal_bounds', 'price', 'is_source', 'is_sink', 'is_buffer',
|
141
|
+
'is_data', 'integer_level', 'no_slack'],
|
142
|
+
DATASET_PROPS: ['comments', 'default_value', 'time_scale', 'time_unit',
|
143
|
+
'method', 'periodic', 'array', 'url'],
|
144
|
+
LINK_PROPS: ['comments', 'multiplier', 'relative_rate', 'share_of_cost',
|
145
|
+
'flow_delay'],
|
146
|
+
CONSTRAINT_PROPS: ['comments', 'no_slack', 'share_of_cost'],
|
147
|
+
NOTE_PROPS: ['contents', 'color'],
|
148
|
+
CHART_PROPS: ['comments', 'histogram', 'bins', 'show_title',
|
149
|
+
'legend_position'],
|
150
|
+
CHART_VAR_PROPS: ['stacked', 'color', 'scale_factor', 'line_width',
|
151
|
+
'visible'],
|
152
|
+
EXPERIMENT_PROPS: ['comments', 'configuration_dims',
|
153
|
+
'column_scenario_dims', 'excluded_selectors'],
|
154
|
+
};
|
155
|
+
this.MC.ALL_PROPS = this.MC.ENTITY_PROPS + this.MC.ACTOR_PROPS +
|
156
|
+
this.MC.CLUSTER_PROPS + this.MC.PROCESS_PROPS +
|
157
|
+
this.MC.PRODUCT_PROPS + this.MC.DATASET_PROPS + this.MC.LINK_PROPS +
|
158
|
+
this.MC.CONSTRAINT_PROPS + this.MC.NOTE_PROPS + this.MC.CHART_PROPS +
|
159
|
+
this.MC.CHART_VAR_PROPS + this.MC.EXPERIMENT_PROPS;
|
160
|
+
}
|
161
|
+
|
162
|
+
hidden() {
|
163
|
+
// Console always returns TRUE, as it has no DOM tree, so any element can
|
164
|
+
// be considered as "hidden"
|
165
|
+
return true;
|
166
|
+
}
|
167
|
+
|
168
|
+
pointInViewport(rx, ry) {
|
169
|
+
// Returns paper coordinates of the cursor position if the cursor were
|
170
|
+
// located at relative position (rx * window width, ry * window height)
|
171
|
+
// in the browser window
|
172
|
+
if(this.paper) return this.paper.cursorPosition(
|
173
|
+
window.innerWidth *rx, window.innerHeight *ry);
|
174
|
+
// If no graphics return values for a 100x100 pixel viewport
|
175
|
+
return [100 * rx, 100 * ry];
|
176
|
+
}
|
177
|
+
|
178
|
+
textSize(string, fsize=8, fweight=400) {
|
179
|
+
// Returns width and height (in px) of (multi-line) string
|
180
|
+
// If paper, use its method, which is more accurate
|
181
|
+
if(this.paper) return this.paper.textSize(string, fsize, fweight);
|
182
|
+
// If no paper, assume 144 px/inch, so 1 pt = 2 px
|
183
|
+
const
|
184
|
+
ch = fsize * 2,
|
185
|
+
cw = fsize;
|
186
|
+
// NOTE: Add '' in case string is a number
|
187
|
+
const lines = ('' + string).split('\n');
|
188
|
+
let w = 0;
|
189
|
+
for(let i = 0; i < lines.length; i++) {
|
190
|
+
w = Math.max(w, lines[i].length * cw);
|
191
|
+
}
|
192
|
+
return {width: w, height: lines.length * ch};
|
193
|
+
}
|
194
|
+
|
195
|
+
stringToLineArray(string, width=100, fsize=8) {
|
196
|
+
// Returns an array of strings wrapped to given width at given font size
|
197
|
+
// while preserving newlines -- used to format text of notes
|
198
|
+
const
|
199
|
+
multi = [],
|
200
|
+
lines = string.split('\n'),
|
201
|
+
ll = lines.length,
|
202
|
+
// If no paper, assume 144 px/inch, so 1 pt = 2 px
|
203
|
+
fh = (this.paper ? this.paper.font_heights[fsize] : 2 * fsize),
|
204
|
+
scalar = fh / 2;
|
205
|
+
for(let i = 0; i < ll; i++) {
|
206
|
+
// NOTE: interpret two spaces as a "non-breaking" space
|
207
|
+
const words = lines[i].replace(/ /g, '\u00A0').trim().split(/ +/);
|
208
|
+
// Split words at '-' when wider than width
|
209
|
+
for(let j = 0; j < words.length; j++) {
|
210
|
+
if(words[j].length * scalar > width) {
|
211
|
+
const sw = words[j].split('-');
|
212
|
+
if(sw.length > 1) {
|
213
|
+
// Replace j-th word by last fragment of split string
|
214
|
+
words[j] = sw.pop();
|
215
|
+
// Insert remaining fragments before
|
216
|
+
while(sw.length > 0) words.splice(j, 0, sw.pop() + '-');
|
217
|
+
}
|
218
|
+
}
|
219
|
+
}
|
220
|
+
let line = words[0] + ' ';
|
221
|
+
for(let j = 1; j < words.length; j++) {
|
222
|
+
const
|
223
|
+
l = line + words[j] + ' ',
|
224
|
+
w = (l.length - 1) * scalar;
|
225
|
+
if (w > width && j > 0) {
|
226
|
+
const
|
227
|
+
nl = line.trim(),
|
228
|
+
nw = Math.floor(nl.length * scalar);
|
229
|
+
multi.push(nl);
|
230
|
+
// If width of added line exceeds the given width, adjust width
|
231
|
+
// so that following lines fill out better
|
232
|
+
width = Math.max(width, nw);
|
233
|
+
line = words[j] + ' ';
|
234
|
+
} else {
|
235
|
+
line = l;
|
236
|
+
}
|
237
|
+
}
|
238
|
+
line = line.trim();
|
239
|
+
// NOTE: Chrome and Safari ignore empty lines in SVG text; as a workaround,
|
240
|
+
// we add a non-breaking space to lines containing only whitespace
|
241
|
+
if(!line) line = '\u00A0';
|
242
|
+
multi.push(line);
|
243
|
+
}
|
244
|
+
return multi;
|
245
|
+
}
|
246
|
+
|
247
|
+
sizeInBytes(n) {
|
248
|
+
// Returns `n` as string scaled to the most appropriate unit of bytes
|
249
|
+
n = Math.round(n);
|
250
|
+
if(n < 1024) return n + ' B';
|
251
|
+
let m = -1;
|
252
|
+
while(n >= 1024) {
|
253
|
+
m++;
|
254
|
+
n /= 1024;
|
255
|
+
}
|
256
|
+
return VM.sig2Dig(n) + ' ' + 'kMGTP'.charAt(m) + 'B';
|
257
|
+
}
|
258
|
+
|
259
|
+
// Shapes are only used to draw model diagrams
|
260
|
+
|
261
|
+
createShape(mdl) {
|
262
|
+
if(this.paper) return new Shape(mdl);
|
263
|
+
return null;
|
264
|
+
}
|
265
|
+
|
266
|
+
moveShapeTo(shape, x, y) {
|
267
|
+
if(shape) shape.moveTo(x, y);
|
268
|
+
}
|
269
|
+
|
270
|
+
removeShape(shape) {
|
271
|
+
if(shape) shape.removeFromDOM();
|
272
|
+
}
|
273
|
+
|
274
|
+
// Methods to ensure proper naming of entities
|
275
|
+
|
276
|
+
cleanName(name) {
|
277
|
+
// Returns `name` without the object-attribute separator |, backslashes,
|
278
|
+
// and leading and trailing whitespace, and with all internal whitespace
|
279
|
+
// reduced to a single space.
|
280
|
+
return name.replace(this.OA_SEPARATOR, ' ').replace(/\||\\/g, ' '
|
281
|
+
).trim().replace(/\s\s+/g, ' ');
|
282
|
+
}
|
283
|
+
|
284
|
+
validName(name) {
|
285
|
+
// Returns TRUE if `name` is a valid Linny-R entity name. These names
|
286
|
+
// must not be empty strings, may not contain brackets, backslashes or
|
287
|
+
// vertical bars, and must start with an underscore, a letter or a digit;
|
288
|
+
// this is enforced mainly to preclude parsing issues with variable names
|
289
|
+
// NOTE: normalize to also accept letters with accents
|
290
|
+
if(name === this.TOP_CLUSTER_NAME) return true;
|
291
|
+
name = name.normalize('NFKD').trim();
|
292
|
+
return name && !name.match(/\[\\\|\]/) &&
|
293
|
+
(name.startsWith(this.BLACK_BOX) || name[0].match(/[\w]/));
|
294
|
+
}
|
295
|
+
|
296
|
+
nameToID(name) {
|
297
|
+
// Returns a name in lower case with link arrow replaced by three
|
298
|
+
// underscores, constraint link arrow by four underscores, and spaces
|
299
|
+
// converted to underscores; in this way, IDs will always be valid
|
300
|
+
// JavaScript object properties
|
301
|
+
// NOTE: replace single quotes by Unicode apostrophe so that they cannot
|
302
|
+
// interfere with JavaScript strings delimited by single quotes
|
303
|
+
return name.replace(this.LINK_ARROW, '___').replace(this.CONSTRAINT_ARROW,
|
304
|
+
'____').toLowerCase().replace(/\s/g, '_').replace("'", '\u2019');
|
305
|
+
}
|
306
|
+
|
307
|
+
htmlEquationName(n) {
|
308
|
+
// Replaces the equations dataset name (system constant) by equation symbol
|
309
|
+
// (square root of x) in white on purple
|
310
|
+
return n.replace(this.EQUATIONS_DATASET_NAME + '|',
|
311
|
+
'<span class="eq">\u221Ax</span>');
|
312
|
+
}
|
313
|
+
|
314
|
+
replaceEntity(str, en1, en2) {
|
315
|
+
// Returns `en2` if `str` matches entity name `en1`; otherwise FALSE
|
316
|
+
const n = str.trim().replace(/\s+/g, ' ').toLowerCase();
|
317
|
+
if(n === en1) return en2;
|
318
|
+
// Link variables contain TWO entity names, one of which can match with `en1`
|
319
|
+
if(n.indexOf(this.LINK_ARROW) >= 0) {
|
320
|
+
const
|
321
|
+
ln = n.split(this.LINK_ARROW),
|
322
|
+
tn0 = ln[0].trim();
|
323
|
+
// Replace name of FROM node if it matches
|
324
|
+
if(tn0 === en1) return en2 + this.LINK_ARROW + ln[1].trim();
|
325
|
+
// Otherwise, replace name of TO node if it matches
|
326
|
+
if(ln[1].trim() === en1) return tn0 + this.LINK_ARROW + en2;
|
327
|
+
}
|
328
|
+
// Return FALSE to indicate "no replacement made"
|
329
|
+
return false;
|
330
|
+
}
|
331
|
+
|
332
|
+
// Methods to notify modeler
|
333
|
+
|
334
|
+
setMessage(msg, type, cause=null) {
|
335
|
+
// Only log errors and warnings on the browser console
|
336
|
+
// NOTE: optionally, the JavaScript error can be passed via `cause`
|
337
|
+
if(type === 'error' || type === 'warning') {
|
338
|
+
// Add type unless message already starts with it
|
339
|
+
type = type.toUpperCase() + ':';
|
340
|
+
if(!msg.startsWith(type)) msg = `${type} ${msg}`;
|
341
|
+
// Strip HTML tags from message text unless UI is graphical
|
342
|
+
if(!this.paper) msg = msg.replace(/<[^>]*>?/gm, '');
|
343
|
+
console.log(msg);
|
344
|
+
if(cause) console.log('Cause:', cause);
|
345
|
+
}
|
346
|
+
}
|
347
|
+
|
348
|
+
notify(msg) {
|
349
|
+
// Notifications are highlighted in blue, and sound a bell chime
|
350
|
+
this.setMessage(msg, 'notification');
|
351
|
+
}
|
352
|
+
|
353
|
+
warn(msg, err=null) {
|
354
|
+
// Warnings are highlighted in yellow, and sound a low beep
|
355
|
+
this.setMessage(msg, 'warning', err);
|
356
|
+
}
|
357
|
+
|
358
|
+
alert(msg, err=null) {
|
359
|
+
// Errors are highlighted in orange, and sound a "bloop" sound
|
360
|
+
this.setMessage(msg, 'error', err);
|
361
|
+
}
|
362
|
+
|
363
|
+
// Alerts, parametrized warnings and notifications signalled in more than
|
364
|
+
// one part of code
|
365
|
+
|
366
|
+
errorOnPost(xhr) {
|
367
|
+
this.alert(`Server error: ${xhr.status} ${xhr.statusText}`);
|
368
|
+
}
|
369
|
+
|
370
|
+
warningInvalidName(n) {
|
371
|
+
this.warn(`Invalid name "${n}"`);
|
372
|
+
}
|
373
|
+
|
374
|
+
warningEntityExists(e) {
|
375
|
+
// NOTE: `e` can be NULL when an invalid name was specified when renaming
|
376
|
+
if(e) {
|
377
|
+
let msg = `${e.type} "${e.displayName}" already exists`;
|
378
|
+
if(e.displayName === this.TOP_CLUSTER_NAME ||
|
379
|
+
e.displayName === this.EQUATIONS_DATASET_NAME) {
|
380
|
+
msg = 'System names cannot be used as entity name';
|
381
|
+
}
|
382
|
+
this.warn(msg);
|
383
|
+
}
|
384
|
+
}
|
385
|
+
|
386
|
+
warningInvalidWeightExpression(actor, err) {
|
387
|
+
const n = (actor ? ' for ' + actor.displayName : '');
|
388
|
+
this.warn(`Invalid weight expression${n}: ${err}`);
|
389
|
+
}
|
390
|
+
|
391
|
+
warningSetUpperBound(e) {
|
392
|
+
this.warn(['Upper bound must be set due to constraint by ',
|
393
|
+
e.type.toLowerCase(), ' <em>', e.displayName, '</em>'].join(''));
|
394
|
+
}
|
395
|
+
|
396
|
+
postResponseOK(text, notify=false) {
|
397
|
+
// Checks whether server reponse text is warning or error, and notifies
|
398
|
+
// the modeler if second argument is TRUE
|
399
|
+
let mtype = 'notification';
|
400
|
+
if(text.startsWith('ERROR:')) {
|
401
|
+
mtype = 'error';
|
402
|
+
} else if(text.startsWith('WARNING:')) {
|
403
|
+
mtype = 'warning';
|
404
|
+
// Remove the 'WARNING:'
|
405
|
+
text = text.substring(8).trim();
|
406
|
+
}
|
407
|
+
const ok = mtype === 'notification';
|
408
|
+
if(!ok || notify) this.setMessage(text, mtype);
|
409
|
+
return ok;
|
410
|
+
}
|
411
|
+
|
412
|
+
loginPrompt() {
|
413
|
+
// The VM needs credentials - his should only occur for the GUI
|
414
|
+
console.log('WARNING: VM needs credentials, but GUI not active');
|
415
|
+
}
|
416
|
+
|
417
|
+
resetModel() {
|
418
|
+
// Resets the Virtual Machine (clears solution)
|
419
|
+
VM.reset();
|
420
|
+
// Redraw model in the browser (GUI only)
|
421
|
+
MODEL.clearSelection();
|
422
|
+
this.drawDiagram(MODEL);
|
423
|
+
}
|
424
|
+
|
425
|
+
stopSolving() {
|
426
|
+
// Notify user only if VM was halted
|
427
|
+
if(VM.halted) {
|
428
|
+
this.notify('Solver HALTED');
|
429
|
+
} else {
|
430
|
+
this.setMessage('');
|
431
|
+
}
|
432
|
+
}
|
433
|
+
|
434
|
+
// NOTE: The following UI functions are implemented as "dummy" methods
|
435
|
+
// because they are called by the Virtual Machine and/or by other controllers
|
436
|
+
// while they can only be meaningfully performed by the GUI controller
|
437
|
+
addListeners() {}
|
438
|
+
readyToReset() {}
|
439
|
+
drawDiagram() {}
|
440
|
+
drawSelection() {}
|
441
|
+
drawObject() {}
|
442
|
+
drawLinkArrows() {}
|
443
|
+
show() {}
|
444
|
+
hide() {}
|
445
|
+
readyToSolve() {}
|
446
|
+
startSolving() {}
|
447
|
+
waitToStop() {}
|
448
|
+
normalCursor() {}
|
449
|
+
rotatingIcon() {}
|
450
|
+
setProgressNeedle() {}
|
451
|
+
updateTimeStep() {}
|
452
|
+
updateDraggableDialogs() {}
|
453
|
+
logHeapSize() {}
|
454
|
+
|
455
|
+
} // END of class Controller
|
456
|
+
|
457
|
+
|
458
|
+
// CLASS RepositoryBrowser
|
459
|
+
class RepositoryBrowser {
|
460
|
+
constructor() {
|
461
|
+
this.repositories = [];
|
462
|
+
this.repository_index = -1;
|
463
|
+
this.module_index = -1;
|
464
|
+
// Get the repository list from the server
|
465
|
+
this.getRepositories();
|
466
|
+
this.reset();
|
467
|
+
}
|
468
|
+
|
469
|
+
reset() {
|
470
|
+
this.visible = false;
|
471
|
+
// NOTE: do NOT reset repository list or module index, because:
|
472
|
+
// (1) they are properties of the local host, and hence model-independent
|
473
|
+
// (2) they must be known when loading a module as model, whereas the
|
474
|
+
// loadingModel method hides and resets all stay-on-top dialogs
|
475
|
+
}
|
476
|
+
|
477
|
+
get isLocalHost() {
|
478
|
+
// Returns TRUE if first repository on the list is 'local host'
|
479
|
+
return this.repositories.length > 0 &&
|
480
|
+
this.repositories[0].name === 'local host';
|
481
|
+
}
|
482
|
+
|
483
|
+
getRepositories() {
|
484
|
+
// Gets the list of repository names from the server
|
485
|
+
this.repositories.length = 0;
|
486
|
+
fetch('repo/', postData({action: 'list'}))
|
487
|
+
.then((response) => {
|
488
|
+
if(!response.ok) {
|
489
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
490
|
+
}
|
491
|
+
return response.text();
|
492
|
+
})
|
493
|
+
.then((data) => {
|
494
|
+
if(UI.postResponseOK(data)) {
|
495
|
+
// NOTE: trim to prevent empty name strings
|
496
|
+
const rl = data.trim().split('\n');
|
497
|
+
for(let i = 0; i < rl.length; i++) {
|
498
|
+
this.addRepository(rl[i].trim());
|
499
|
+
}
|
500
|
+
}
|
501
|
+
// NOTE: set index to first repository on list (typically local host)
|
502
|
+
// unless the list is empty
|
503
|
+
this.repository_index = Math.min(0, this.repositories.length - 1);
|
504
|
+
this.updateDialog();
|
505
|
+
})
|
506
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
507
|
+
}
|
508
|
+
|
509
|
+
repositoryByName(n) {
|
510
|
+
// Returns the repository having name `n` if already known, otherwise NULL
|
511
|
+
for(let i = 0; i < this.repositories.length; i++) {
|
512
|
+
if(this.repositories[i].name === n) {
|
513
|
+
return this.repositories[i];
|
514
|
+
}
|
515
|
+
}
|
516
|
+
return null;
|
517
|
+
}
|
518
|
+
|
519
|
+
asFileName(s) {
|
520
|
+
// Returns string `s` with whitespace converted to a single dash, and
|
521
|
+
// special characters converted to underscores
|
522
|
+
return s.normalize('NFKD').trim().replace(/[\s\-]+/g, '-'
|
523
|
+
).replace(/[^A-Za-z0-9_\-]/g, '_').trim('-_');
|
524
|
+
}
|
525
|
+
|
526
|
+
loadModuleAsModel() {
|
527
|
+
// Loads selected module as model
|
528
|
+
if(this.repository_index >= 0 && this.module_index >= 0) {
|
529
|
+
// NOTE: when loading new model, the stay-on-top dialogs must be reset
|
530
|
+
UI.hideStayOnTopDialogs();
|
531
|
+
const r = this.repositories[this.repository_index];
|
532
|
+
// NOTE: pass FALSE to indicate "no inclusion; load XML as model"
|
533
|
+
r.loadModule(this.module_index, false);
|
534
|
+
}
|
535
|
+
}
|
536
|
+
|
537
|
+
} // END of class RepositoryBrowser
|
538
|
+
|
539
|
+
|
540
|
+
// CLASS DatasetManager controls the collection of datasets of a model
|
541
|
+
class DatasetManager {
|
542
|
+
constructor() {
|
543
|
+
// Initialize dialog properties
|
544
|
+
this.methods = ['nearest', 'w-mean', 'w-sum', 'max'];
|
545
|
+
this.method_symbols = ['∼t', 'μ', 'Σ', 'MAX'];
|
546
|
+
this.method_names =
|
547
|
+
['at nearest t', 'weighted mean', 'weighted sum', 'maximum'];
|
548
|
+
this.reset();
|
549
|
+
}
|
550
|
+
|
551
|
+
reset() {
|
552
|
+
this.visible = false;
|
553
|
+
this.selected_dataset = null;
|
554
|
+
}
|
555
|
+
|
556
|
+
getRemoteDataset(url) {
|
557
|
+
// Get remote data for selected dataset
|
558
|
+
const ds = this.selected_dataset;
|
559
|
+
if(ds) FILE_MANAGER.getRemoteData(ds, url);
|
560
|
+
}
|
561
|
+
|
562
|
+
// Dummy methods, meaningful only for the graphical dataset manager
|
563
|
+
updateDialog() {}
|
564
|
+
|
565
|
+
} // END of class DatasetManager
|
566
|
+
|
567
|
+
|
568
|
+
// CLASS ChartManager controls the collection of charts of a model
|
569
|
+
class ChartManager {
|
570
|
+
constructor() {
|
571
|
+
this.new_chart_title = '(new chart)';
|
572
|
+
// NOTE: The SVG height is fixed at 500 units, as this gives good results
|
573
|
+
// for the SVG units for line width = 1; fill patterns definitions are
|
574
|
+
// defined to work for images of this height (see further down)
|
575
|
+
this.svg_height = 500;
|
576
|
+
this.container_height = this.svg_height;
|
577
|
+
// Default aspect ratio W:H is 1.75 -- stretch factor will make it more oblong
|
578
|
+
this.container_width = this.svg_height * 1.75;
|
579
|
+
this.legend_options = ['None', 'Top', 'Right', 'Bottom'];
|
580
|
+
// Basic properties -- also needed for console application
|
581
|
+
this.visible = false;
|
582
|
+
this.chart_index = -1;
|
583
|
+
this.variable_index = -1;
|
584
|
+
this.stretch_factor = 1;
|
585
|
+
this.drawing_graph = false;
|
586
|
+
this.runs_chart = false;
|
587
|
+
// Fill styles used to differentiate between experiments in histograms
|
588
|
+
this.fill_styles = [
|
589
|
+
'diagonal-cross-hatch', 'dots',
|
590
|
+
'diagonal-hatch', 'checkers', 'horizontal-hatch',
|
591
|
+
'cross-hatch', 'circles', 'vertical-hatch'
|
592
|
+
];
|
593
|
+
|
594
|
+
// SVG for chart fill patterns
|
595
|
+
// NOTE: mask width and height are based on SVG height = 500
|
596
|
+
this.fill_patterns = `
|
597
|
+
<pattern id="vertical-hatch" width="4" height="4"
|
598
|
+
patternUnits="userSpaceOnUse">
|
599
|
+
<line x1="0" y1="0" x2="0" y2="4" style="stroke:white; stroke-width:4" />
|
600
|
+
</pattern>
|
601
|
+
<mask id="vertical-hatch-mask" x="0" y="0" width="1" height="1" >
|
602
|
+
<rect x="0" y="0" width="5000" height="500" fill="url(#vertical-hatch)" />
|
603
|
+
</mask>
|
604
|
+
<pattern id="horizontal-hatch" width="4" height="4"
|
605
|
+
patternUnits="userSpaceOnUse">
|
606
|
+
<line x1="0" y1="0" x2="4" y2="0" style="stroke:white; stroke-width:4" />
|
607
|
+
</pattern>
|
608
|
+
<mask id="horizontal-hatch-mask" x="0" y="0" width="1" height="1" >
|
609
|
+
<rect x="0" y="0" width="5000" height="500" fill="url(#horizontal-hatch)" />
|
610
|
+
</mask>
|
611
|
+
<pattern id="diagonal-hatch" width="4" height="4"
|
612
|
+
patternTransform="rotate(45 0 0)" patternUnits="userSpaceOnUse">
|
613
|
+
<line x1="0" y1="0" x2="0" y2="4" style="stroke:white; stroke-width:6" />
|
614
|
+
</pattern>
|
615
|
+
<mask id="diagonal-hatch-mask" x="0" y="0" width="1" height="1" >
|
616
|
+
<rect x="0" y="0" width="5000" height="500" fill="url(#diagonal-hatch)" />
|
617
|
+
</mask>
|
618
|
+
<pattern id="cross-hatch" width="5" height="5"
|
619
|
+
patternUnits="userSpaceOnUse">
|
620
|
+
<line x1="0" y1="0" x2="5" y2="0" style="stroke:white; stroke-width:3" />
|
621
|
+
<line x1="0" y1="0" x2="0" y2="5" style="stroke:white; stroke-width:3" />
|
622
|
+
</pattern>
|
623
|
+
<mask id="cross-hatch-mask" x="0" y="0" width="1" height="1" >
|
624
|
+
<rect x="0" y="0" width="5000" height="500" fill="url(#cross-hatch)" />
|
625
|
+
</mask>
|
626
|
+
<pattern id="diagonal-cross-hatch" width="5" height="5"
|
627
|
+
patternTransform="rotate(45 0 0)" patternUnits="userSpaceOnUse">
|
628
|
+
<line x1="0" y1="0" x2="5" y2="0" style="stroke:white; stroke-width:3" />
|
629
|
+
<line x1="0" y1="0" x2="0" y2="5" style="stroke:white; stroke-width:3" />
|
630
|
+
</pattern>
|
631
|
+
<mask id="diagonal-cross-hatch-mask" x="0" y="0" width="1" height="1" >
|
632
|
+
<rect x="0" y="0" width="5000" height="500" fill="url(#diagonal-cross-hatch)" />
|
633
|
+
</mask>
|
634
|
+
<pattern id="dots" width="5" height="5"
|
635
|
+
patternUnits="userSpaceOnUse">
|
636
|
+
<circle cx="2.5" cy="2.5" r="1.5" style="stroke:none; fill:white" />
|
637
|
+
</pattern>
|
638
|
+
<mask id="dots-mask" x="0" y="0" width="1" height="1" >
|
639
|
+
<rect x="0" y="0" width="5000" height="500" fill="url(#dots)" />
|
640
|
+
</mask>
|
641
|
+
<pattern id="circles" width="8" height="8"
|
642
|
+
patternUnits="userSpaceOnUse">
|
643
|
+
<circle cx="4" cy="4" r="2.5" style="stroke:white; stroke-width:1" />
|
644
|
+
</pattern>
|
645
|
+
<mask id="circles-mask" x="0" y="0" width="1" height="1" >
|
646
|
+
<rect x="0" y="0" width="5000" height="500" fill="url(#circles)" />
|
647
|
+
</mask>
|
648
|
+
<pattern id="checkers" width="10" height="10"
|
649
|
+
patternUnits="userSpaceOnUse">
|
650
|
+
<rect width="5" height="5" style="stroke:none; fill:white" />
|
651
|
+
<rect x="5" y="5" width="5" height="5" style="stroke:none; fill:white" />
|
652
|
+
</pattern>
|
653
|
+
<mask id="checkers-mask" x="0" y="0" width="1" height="1" >
|
654
|
+
<rect x="0" y="0" width="5000" height="500" fill="url(#checkers)" />
|
655
|
+
</mask>`;
|
656
|
+
}
|
657
|
+
|
658
|
+
reset() {
|
659
|
+
this.visible = false;
|
660
|
+
this.chart_index = -1;
|
661
|
+
this.variable_index = -1;
|
662
|
+
this.stretch_factor = 1;
|
663
|
+
this.drawing_graph = false;
|
664
|
+
this.runs_chart = false;
|
665
|
+
}
|
666
|
+
|
667
|
+
resetChartVectors() {
|
668
|
+
// Reset vectors of all charts
|
669
|
+
for(let i = 0; i < MODEL.charts.length; i++) {
|
670
|
+
MODEL.charts[i].resetVectors();
|
671
|
+
}
|
672
|
+
}
|
673
|
+
|
674
|
+
setRunsChart(show) {
|
675
|
+
// Indicates whether the chart manager should display a run result chart
|
676
|
+
this.runs_chart = show;
|
677
|
+
}
|
678
|
+
|
679
|
+
// Dummy methods: actions that are meaningful only for the graphical UI
|
680
|
+
updateDialog() {}
|
681
|
+
updateExperimentInfo() {}
|
682
|
+
showChartImage() {}
|
683
|
+
|
684
|
+
} // END of class ChartManager
|
685
|
+
|
686
|
+
|
687
|
+
// CLASS SensitivityAnalysis provides the sensitivity analysis functionality
|
688
|
+
class SensitivityAnalysis {
|
689
|
+
constructor() {
|
690
|
+
// Initialize main dialog properties
|
691
|
+
this.reset();
|
692
|
+
// Sensitivity analysis creates & disposes an experiment and a chart
|
693
|
+
this.experiment_title = '___SENSITIVITY_ANALYSIS___';
|
694
|
+
this.chart_title = '___SENSITIVITY_ANALYSIS_CHART___';
|
695
|
+
}
|
696
|
+
|
697
|
+
reset() {
|
698
|
+
this.visible = false;
|
699
|
+
this.data = {};
|
700
|
+
this.perc = {};
|
701
|
+
this.shade = {};
|
702
|
+
this.options_shown = true;
|
703
|
+
this.selected_parameter = -1;
|
704
|
+
this.selected_outcome = -1;
|
705
|
+
this.checked_parameters = {};
|
706
|
+
this.checked_outcomes = {};
|
707
|
+
this.relative_scale = true;
|
708
|
+
this.color_scale = new ColorScale('no');
|
709
|
+
this.selected_statistic = 'mean';
|
710
|
+
this.chart = null;
|
711
|
+
this.experiment = null;
|
712
|
+
this.must_pause = false;
|
713
|
+
this.selected_run = -1;
|
714
|
+
}
|
715
|
+
|
716
|
+
start() {
|
717
|
+
// A sensitivity analysis is a series of runs with identical exogenous
|
718
|
+
// variables except for one parameter that is multiplied by (1 + delta %).
|
719
|
+
// Since expressions perform this multiplication when they are marked as
|
720
|
+
// the "active" parameter, it suffices to perform the *same* experiment run
|
721
|
+
// as many times as there are parameters while changing only the "active"
|
722
|
+
// parameter. The "base selectors" constitute the *single* combination that
|
723
|
+
// must be run. To ensure that the data on all outcome variables are stored,
|
724
|
+
// a dummy chart is created that includes all these outcomes as *chart*
|
725
|
+
// variables.
|
726
|
+
if(!this.experiment) {
|
727
|
+
// Clear results from previous analysis
|
728
|
+
this.clearResults();
|
729
|
+
this.parameters = [];
|
730
|
+
for(let i = 0; i < MODEL.sensitivity_parameters.length; i++) {
|
731
|
+
const
|
732
|
+
p = MODEL.sensitivity_parameters[i],
|
733
|
+
vn = p.split(UI.OA_SEPARATOR),
|
734
|
+
obj = MODEL.objectByName(vn[0]),
|
735
|
+
oax = (obj ? obj.attributeExpression(vn[1]) : null);
|
736
|
+
if(oax) {
|
737
|
+
this.parameters.push(oax);
|
738
|
+
} else {
|
739
|
+
UI.alert(`Parameter ${p} is not an expression`);
|
740
|
+
}
|
741
|
+
}
|
742
|
+
this.chart = new Chart(this.chart_title);
|
743
|
+
for(let i = 0; i < MODEL.sensitivity_outcomes.length; i++) {
|
744
|
+
const vn = MODEL.sensitivity_outcomes[i].split(UI.OA_SEPARATOR);
|
745
|
+
this.chart.addVariable(vn[0], vn[1]);
|
746
|
+
}
|
747
|
+
this.experiment = new Experiment(this.experiment_title);
|
748
|
+
this.experiment.charts = [this.chart];
|
749
|
+
this.experiment.inferVariables();
|
750
|
+
// This experiment always uses the same combination: the base selectors
|
751
|
+
const bs = MODEL.base_case_selectors.split(' ');
|
752
|
+
this.experiment.combinations = [];
|
753
|
+
// Add this combination N+1 times for N parameters
|
754
|
+
for(let i = 0; i <= this.parameters.length; i++) {
|
755
|
+
this.experiment.combinations.push(bs);
|
756
|
+
}
|
757
|
+
// NOTE: model settings will not be changed, but nevertheless restored
|
758
|
+
this.experiment.original_model_settings = MODEL.settingsString;
|
759
|
+
this.experiment.original_round_sequence = MODEL.round_sequence;
|
760
|
+
}
|
761
|
+
// Change the button (GUI only -- console will return FALSE)
|
762
|
+
const paused = this.resumeButtons();
|
763
|
+
if(!paused) {
|
764
|
+
this.experiment.time_started = new Date().getTime();
|
765
|
+
this.experiment.active_combination_index = 0;
|
766
|
+
// NOTE: start with base case run, hence no active parameter yet
|
767
|
+
MODEL.running_experiment = this.experiment;
|
768
|
+
}
|
769
|
+
// Let the experiment manager do the work!!
|
770
|
+
EXPERIMENT_MANAGER.runModel();
|
771
|
+
}
|
772
|
+
|
773
|
+
processRestOfRun() {
|
774
|
+
// This method is called by the experiment manager after an SA run
|
775
|
+
const x = MODEL.running_experiment;
|
776
|
+
if(!x) return;
|
777
|
+
// Double-check that indeed the SA experiment is running
|
778
|
+
if(x !== this.experiment) {
|
779
|
+
UI.alert('ERROR: Expected SA experiment run, but got ' + x.title);
|
780
|
+
return;
|
781
|
+
}
|
782
|
+
const aci = x.active_combination_index;
|
783
|
+
// Always add solver messages
|
784
|
+
x.runs[aci].addMessages();
|
785
|
+
// NOTE: use a "dummy experiment object" to ensure proper XML saving and
|
786
|
+
// loading , as the actual experiment is not stored
|
787
|
+
x.runs.experiment = {title: SENSITIVITY_ANALYSIS.experiment_title};
|
788
|
+
// Add run to the sensitivity analysis
|
789
|
+
MODEL.sensitivity_runs.push(x.runs[aci]);
|
790
|
+
this.showProgress('Run #' + aci);
|
791
|
+
// See if more runs should be done
|
792
|
+
const n = x.combinations.length;
|
793
|
+
if(!VM.halted && aci < n - 1) {
|
794
|
+
if(this.must_pause) {
|
795
|
+
this.pausedButtons(aci);
|
796
|
+
UI.setMessage('');
|
797
|
+
} else {
|
798
|
+
// NOTE: use aci because run #0 is the base case w/o active parameter
|
799
|
+
MODEL.active_sensitivity_parameter = this.parameters[aci];
|
800
|
+
x.active_combination_index++;
|
801
|
+
setTimeout(() => EXPERIMENT_MANAGER.runModel(), 5);
|
802
|
+
}
|
803
|
+
} else {
|
804
|
+
x.time_stopped = new Date().getTime();
|
805
|
+
x.completed = aci >= n - 1;
|
806
|
+
x.active_combination_index = -1;
|
807
|
+
if(VM.halted) {
|
808
|
+
UI.notify(
|
809
|
+
`Experiment <em>${x.title}</em> terminated during run #${aci}`);
|
810
|
+
} else {
|
811
|
+
this.showCheckmark(msecToTime(x.time_stopped - x.time_started));
|
812
|
+
}
|
813
|
+
// No more runs => perform wrap-up
|
814
|
+
// Restore original model settings
|
815
|
+
MODEL.running_experiment = null;
|
816
|
+
MODEL.active_sensitivity_parameter = null;
|
817
|
+
MODEL.parseSettings(x.original_model_settings);
|
818
|
+
MODEL.round_sequence = x.original_round_sequence;
|
819
|
+
// Reset the Virtual Machine so t=0 at the status line,
|
820
|
+
// and ALL expressions are reset as well
|
821
|
+
VM.reset();
|
822
|
+
// Free the SA experiment and SA chart
|
823
|
+
this.experiment = null;
|
824
|
+
this.chart = null;
|
825
|
+
// Reset buttons (GUI only)
|
826
|
+
this.readyButtons();
|
827
|
+
}
|
828
|
+
this.updateDialog();
|
829
|
+
// Reset the model, as results of last run will be showing still
|
830
|
+
UI.resetModel();
|
831
|
+
CHART_MANAGER.resetChartVectors();
|
832
|
+
// NOTE: clear chart only when done (charts do not update during experiment)
|
833
|
+
if(!MODEL.running_experiment) CHART_MANAGER.updateDialog();
|
834
|
+
}
|
835
|
+
|
836
|
+
stop() {
|
837
|
+
// Interrupt solver but retain data on server (and no resume)
|
838
|
+
VM.halt();
|
839
|
+
this.readyButtons();
|
840
|
+
this.showProgress('');
|
841
|
+
}
|
842
|
+
|
843
|
+
clearResults() {
|
844
|
+
// Clear results and reset control buttons
|
845
|
+
MODEL.sensitivity_runs.length = 0;
|
846
|
+
this.selected_run = -1;
|
847
|
+
}
|
848
|
+
|
849
|
+
computeData(sas) {
|
850
|
+
// Compute data value or status for statistic `sas`
|
851
|
+
this.perc = {};
|
852
|
+
this.shade = {};
|
853
|
+
this.data = {};
|
854
|
+
const
|
855
|
+
ol = MODEL.sensitivity_outcomes.length,
|
856
|
+
rl = MODEL.sensitivity_runs.length;
|
857
|
+
if(ol === 0) return;
|
858
|
+
// Always find highest relative change
|
859
|
+
let max_dif = 0;
|
860
|
+
for(let i = 0; i < ol; i++) {
|
861
|
+
this.data[i] = [];
|
862
|
+
for(let j = 0; j < rl; j++) {
|
863
|
+
// Get the selected statistic for each run to get an array of numbers
|
864
|
+
const rr = MODEL.sensitivity_runs[j].results[i];
|
865
|
+
if(!rr) {
|
866
|
+
this.data[i].push(VM.UNDEFINED);
|
867
|
+
} else if(sas === 'N') {
|
868
|
+
this.data[i].push(rr.N);
|
869
|
+
} else if(sas === 'sum') {
|
870
|
+
this.data[i].push(rr.sum);
|
871
|
+
} else if(sas === 'mean') {
|
872
|
+
this.data[i].push(rr.mean);
|
873
|
+
} else if(sas === 'sd') {
|
874
|
+
this.data[i].push(Math.sqrt(rr.variance));
|
875
|
+
} else if(sas === 'min') {
|
876
|
+
this.data[i].push(rr.minimum);
|
877
|
+
} else if(sas === 'max') {
|
878
|
+
this.data[i].push(rr.maximum);
|
879
|
+
} else if(sas === 'nz') {
|
880
|
+
this.data[i].push(rr.non_zero_tally);
|
881
|
+
} else if(sas === 'except') {
|
882
|
+
this.data[i].push(rr.exceptions);
|
883
|
+
} else if(sas === 'last') {
|
884
|
+
this.data[i].push(rr.last);
|
885
|
+
}
|
886
|
+
}
|
887
|
+
// Compute relative change
|
888
|
+
let bsv = this.data[i][0];
|
889
|
+
if(Math.abs(bsv) < VM.NEAR_ZERO) bsv = 0;
|
890
|
+
this.perc[i] = [];
|
891
|
+
if(bsv > VM.MINUS_INFINITY && bsv < VM.PLUS_INFINITY) {
|
892
|
+
for(let j = 1; j < this.data[i].length; j++) {
|
893
|
+
let v = this.data[i][j];
|
894
|
+
if(v > VM.MINUS_INFINITY && v < VM.PLUS_INFINITY) {
|
895
|
+
if(bsv === 0) {
|
896
|
+
v = (v === 0 ? 0 : VM.UNDEFINED);
|
897
|
+
} else {
|
898
|
+
v = (v - bsv) / bsv * 100;
|
899
|
+
max_dif = Math.max(max_dif, Math.abs(v));
|
900
|
+
}
|
901
|
+
this.perc[i].push(v);
|
902
|
+
}
|
903
|
+
}
|
904
|
+
} else {
|
905
|
+
for(let j = 1; j < this.data[i].length; j++) this.perc[i].push('-');
|
906
|
+
}
|
907
|
+
}
|
908
|
+
// Now use max_dif to compute shades
|
909
|
+
for(let i = 0; i < ol; i++) {
|
910
|
+
this.shade[i] = [];
|
911
|
+
// Color scale range is -max ... +max (0 in center => white)
|
912
|
+
for(let j = 0; j < this.perc[i].length; j++) {
|
913
|
+
const p = this.perc[i][j];
|
914
|
+
this.shade[i].push(p === VM.UNDEFINED || max_dif < VM.NEAR_ZERO ?
|
915
|
+
0.5 : (p / max_dif + 1) / 2);
|
916
|
+
}
|
917
|
+
// Convert to sig4Dig
|
918
|
+
for(let j = 0; j < this.data[i].length; j++) {
|
919
|
+
this.data[i][j] = VM.sig4Dig(this.data[i][j]);
|
920
|
+
}
|
921
|
+
// Format data such that they all have same number of decimals
|
922
|
+
if(this.relative_scale && this.perc[i][0] !== '-') {
|
923
|
+
for(let j = 0; j < this.perc[i].length; j++) {
|
924
|
+
this.perc[i][j] = VM.sig4Dig(this.perc[i][j]);
|
925
|
+
}
|
926
|
+
uniformDecimals(this.perc[i]);
|
927
|
+
// NOTE: only consider data of base scenario
|
928
|
+
this.data[i][0] = VM.sig4Dig(this.data[i][0]);
|
929
|
+
} else {
|
930
|
+
uniformDecimals(this.data[i]);
|
931
|
+
}
|
932
|
+
}
|
933
|
+
}
|
934
|
+
|
935
|
+
resumeButtons() {
|
936
|
+
// Console experiments cannot be paused, and hence not resumed
|
937
|
+
return false;
|
938
|
+
}
|
939
|
+
|
940
|
+
// Dummy methods: actions that are meaningful only for the graphical UI
|
941
|
+
updateDialog() {}
|
942
|
+
showCheckmark() {}
|
943
|
+
showProgress() {}
|
944
|
+
drawTable() {}
|
945
|
+
readyButtons() {}
|
946
|
+
pausedButtons() {}
|
947
|
+
|
948
|
+
} // END of class SensitivityAnalysis
|
949
|
+
|
950
|
+
|
951
|
+
// Class ExperimentManager controls the collection of experiments of the model
|
952
|
+
class ExperimentManager {
|
953
|
+
constructor() {
|
954
|
+
}
|
955
|
+
|
956
|
+
reset() {
|
957
|
+
this.visible = false;
|
958
|
+
this.callback = null;
|
959
|
+
this.selected_experiment = null;
|
960
|
+
this.suitable_charts = [];
|
961
|
+
}
|
962
|
+
|
963
|
+
updateChartList() {
|
964
|
+
// Select charts having 1 or more variables, as only these are meaningful
|
965
|
+
// as the dependent variables of an experiment
|
966
|
+
this.suitable_charts.length = 0;
|
967
|
+
for(let i = 0; i < MODEL.charts.length; i++) {
|
968
|
+
const c = MODEL.charts[i];
|
969
|
+
if(c.variables.length > 0) this.suitable_charts.push(c);
|
970
|
+
}
|
971
|
+
}
|
972
|
+
|
973
|
+
selectedRuns(chart) {
|
974
|
+
// Returns list of run numbers selected in the Experiment Manager
|
975
|
+
const selx = this.selected_experiment;
|
976
|
+
if(CHART_MANAGER.runs_chart && selx && selx.charts.indexOf(chart) >= 0) {
|
977
|
+
return selx.chart_combinations;
|
978
|
+
}
|
979
|
+
return [];
|
980
|
+
}
|
981
|
+
|
982
|
+
selectExperiment(title) {
|
983
|
+
const xi = MODEL.indexOfExperiment(title);
|
984
|
+
this.selected_experiment = (xi < 0 ? null : MODEL.experiments[xi]);
|
985
|
+
this.updateDialog();
|
986
|
+
}
|
987
|
+
|
988
|
+
updateDialog() {
|
989
|
+
// NOTE: no GUI elements to update, but experiment parameters must be set
|
990
|
+
MODEL.inferDimensions();
|
991
|
+
const x = this.selected_experiment;
|
992
|
+
if(!x) return;
|
993
|
+
x.updateActorDimension();
|
994
|
+
x.inferActualDimensions();
|
995
|
+
x.inferCombinations();
|
996
|
+
}
|
997
|
+
|
998
|
+
clearRunResults() {
|
999
|
+
// Clears all run results
|
1000
|
+
const x = this.selected_experiment;
|
1001
|
+
if(x) {
|
1002
|
+
x.clearRuns();
|
1003
|
+
this.updateDialog();
|
1004
|
+
}
|
1005
|
+
}
|
1006
|
+
|
1007
|
+
startExperiment(n=-1) {
|
1008
|
+
// Recompile expressions, as these may have been changed by the modeler
|
1009
|
+
MODEL.compileExpressions();
|
1010
|
+
// Start sequence of solving model parametrizations
|
1011
|
+
const x = this.selected_experiment;
|
1012
|
+
if(x) {
|
1013
|
+
// Store original model settings
|
1014
|
+
x.original_model_settings = MODEL.settingsString;
|
1015
|
+
x.original_round_sequence = MODEL.round_sequence;
|
1016
|
+
// NOTE: switch off run chart display
|
1017
|
+
CHART_MANAGER.setRunsChart(false);
|
1018
|
+
// When Chart manager is showing, close it and notify modeler that charts
|
1019
|
+
// should not be viewed during experiments
|
1020
|
+
if(CHART_MANAGER.visible) {
|
1021
|
+
UI.buttons.chart.dispatchEvent(new Event('click'));
|
1022
|
+
UI.notify(UI.NOTICE.NO_CHARTS);
|
1023
|
+
}
|
1024
|
+
// Change the buttons -- will return TRUE if experiment was paused
|
1025
|
+
const paused = this.resumeButtons();
|
1026
|
+
if(x.completed && n >= 0) {
|
1027
|
+
x.single_run = n;
|
1028
|
+
x.active_combination_index = n;
|
1029
|
+
MODEL.running_experiment = x;
|
1030
|
+
} else if(!paused) {
|
1031
|
+
// Clear previous run results (if any) unless resuming
|
1032
|
+
x.clearRuns();
|
1033
|
+
x.inferVariables();
|
1034
|
+
x.time_started = new Date().getTime();
|
1035
|
+
x.active_combination_index = 0;
|
1036
|
+
MODEL.running_experiment = x;
|
1037
|
+
} else {
|
1038
|
+
x.active_combination_index++;
|
1039
|
+
UI.notify('Experiment resumed at run #' + x.active_combination_index);
|
1040
|
+
}
|
1041
|
+
this.runModel();
|
1042
|
+
}
|
1043
|
+
}
|
1044
|
+
|
1045
|
+
runModel() {
|
1046
|
+
const x = MODEL.running_experiment;
|
1047
|
+
if(x) {
|
1048
|
+
const
|
1049
|
+
ci = x.active_combination_index,
|
1050
|
+
n = x.combinations.length,
|
1051
|
+
p = Math.floor(ci * 100 / n),
|
1052
|
+
combi = x.combinations[ci];
|
1053
|
+
let xr;
|
1054
|
+
if(x.single_run >= 0) {
|
1055
|
+
xr = x.runs[x.single_run];
|
1056
|
+
} else {
|
1057
|
+
xr = new ExperimentRun(x, ci);
|
1058
|
+
x.runs.push(xr);
|
1059
|
+
}
|
1060
|
+
xr.start();
|
1061
|
+
this.showProgress(ci, p, n);
|
1062
|
+
// NOTE: first restore original model settings (setings may be partial!)
|
1063
|
+
MODEL.parseSettings(x.original_model_settings);
|
1064
|
+
// Parse all active settings selector strings
|
1065
|
+
// NOTE: may be multiple strings; the later overwrite the earlier
|
1066
|
+
for(let i = 0; i < x.settings_selectors.length; i++) {
|
1067
|
+
const ssel = x.settings_selectors[i].split('|');
|
1068
|
+
if(combi.indexOf(ssel[0]) >= 0) MODEL.parseSettings(ssel[1]);
|
1069
|
+
}
|
1070
|
+
// Also set the correct round sequence
|
1071
|
+
// NOTE: if no match, default is retained
|
1072
|
+
for(let i = 0; i < x.actor_selectors.length; i++) {
|
1073
|
+
const asel = x.actor_selectors[i];
|
1074
|
+
if(combi.indexOf(asel.selector) >= 0) {
|
1075
|
+
MODEL.round_sequence = asel.round_sequence;
|
1076
|
+
}
|
1077
|
+
}
|
1078
|
+
// Only now compute the simulation run time (number of time steps)
|
1079
|
+
xr.time_steps = MODEL.end_period - MODEL.start_period + 1;
|
1080
|
+
VM.callback = this.callback;
|
1081
|
+
VM.solveModel();
|
1082
|
+
}
|
1083
|
+
}
|
1084
|
+
|
1085
|
+
processRun() {
|
1086
|
+
// This method is called by the solveBlocks method of the Virtual Machine
|
1087
|
+
const x = MODEL.running_experiment;
|
1088
|
+
if(!x) return;
|
1089
|
+
const aci = x.active_combination_index;
|
1090
|
+
if(MODEL.solved) {
|
1091
|
+
// NOTE: addresults will call processRestOfRun when completed
|
1092
|
+
x.runs[aci].addResults();
|
1093
|
+
} else {
|
1094
|
+
// Do not add results...
|
1095
|
+
UI.warn(`Model run #${aci} incomplete -- results will be invalid`);
|
1096
|
+
// ... but do perform the usual post-processing
|
1097
|
+
// NOTE: when sensitivity analysis is being performed, switch back to SA
|
1098
|
+
if(SENSITIVITY_ANALYSIS.experiment) {
|
1099
|
+
SENSITIVITY_ANALYSIS.processRestOfRun();
|
1100
|
+
} else {
|
1101
|
+
this.processRestOfRun();
|
1102
|
+
}
|
1103
|
+
}
|
1104
|
+
}
|
1105
|
+
|
1106
|
+
processRestOfRun() {
|
1107
|
+
// Performs post-processing after run results have been added
|
1108
|
+
const x = MODEL.running_experiment;
|
1109
|
+
if(!x) return;
|
1110
|
+
const aci = x.active_combination_index;
|
1111
|
+
// Always add solver messages
|
1112
|
+
x.runs[aci].addMessages();
|
1113
|
+
const n = x.combinations.length;
|
1114
|
+
if(!VM.halted && aci < n - 1 && aci != x.single_run) {
|
1115
|
+
if(this.must_pause) {
|
1116
|
+
this.pausedButtons(aci);
|
1117
|
+
this.must_pause = false;
|
1118
|
+
UI.setMessage('');
|
1119
|
+
} else {
|
1120
|
+
x.active_combination_index++;
|
1121
|
+
let delay = 5;
|
1122
|
+
// NOTE: when executing a remote command, wait for 1 second to
|
1123
|
+
// allow enough time for report writing
|
1124
|
+
if(RECEIVER.active && RECEIVER.experiment) {
|
1125
|
+
UI.setMessage('Reporting run #' + (x.active_combination_index - 1));
|
1126
|
+
delay = 1000;
|
1127
|
+
}
|
1128
|
+
setTimeout(() => EXPERIMENT_MANAGER.runModel(), delay);
|
1129
|
+
}
|
1130
|
+
} else {
|
1131
|
+
x.time_stopped = new Date().getTime();
|
1132
|
+
if(x.single_run >= 0) {
|
1133
|
+
x.single_run = -1;
|
1134
|
+
x.completed = true;
|
1135
|
+
} else {
|
1136
|
+
x.completed = aci >= n - 1;
|
1137
|
+
}
|
1138
|
+
x.active_combination_index = -1;
|
1139
|
+
if(VM.halted) {
|
1140
|
+
UI.notify(
|
1141
|
+
`Experiment <em>${x.title}</em> terminated during run #${aci}`);
|
1142
|
+
RECEIVER.deactivate();
|
1143
|
+
}
|
1144
|
+
// No more runs => stop experiment, and perform call-back
|
1145
|
+
// NOTE: if call-back is successful, the receiver will resume listening
|
1146
|
+
if(RECEIVER.active) {
|
1147
|
+
RECEIVER.experiment = '';
|
1148
|
+
RECEIVER.callBack();
|
1149
|
+
}
|
1150
|
+
// Restore original model settings
|
1151
|
+
MODEL.running_experiment = null;
|
1152
|
+
MODEL.parseSettings(x.original_model_settings);
|
1153
|
+
MODEL.round_sequence = x.original_round_sequence;
|
1154
|
+
// Reset the Virtual Machine so t=0 at the status line,
|
1155
|
+
// and ALL expressions are reset as well
|
1156
|
+
VM.reset();
|
1157
|
+
this.readyButtons();
|
1158
|
+
}
|
1159
|
+
this.drawTable();
|
1160
|
+
// Reset the model, as results of last run will be showing still
|
1161
|
+
UI.resetModel();
|
1162
|
+
CHART_MANAGER.resetChartVectors();
|
1163
|
+
// NOTE: clear chart only when done (charts do not update during experiment)
|
1164
|
+
if(!MODEL.running_experiment) CHART_MANAGER.updateDialog();
|
1165
|
+
}
|
1166
|
+
|
1167
|
+
stopExperiment() {
|
1168
|
+
// Interrupt solver but retain data on server (and no resume)
|
1169
|
+
VM.halt();
|
1170
|
+
}
|
1171
|
+
|
1172
|
+
showProgress(ci, p, n) {
|
1173
|
+
// Report progress on the console
|
1174
|
+
console.log('\nRun', ci, `(${p}% of ${n})`);
|
1175
|
+
}
|
1176
|
+
|
1177
|
+
resumeButtons() {
|
1178
|
+
// Console experiments cannot be paused, and hence not resumed
|
1179
|
+
return false;
|
1180
|
+
}
|
1181
|
+
|
1182
|
+
// Dummy methods: actions that are meaningful only for the graphical UI
|
1183
|
+
drawTable() {}
|
1184
|
+
readyButtons() {}
|
1185
|
+
pausedButtons() {}
|
1186
|
+
|
1187
|
+
} // END of class ExperimentManager
|
1188
|
+
|
1189
|
+
|
1190
|
+
/////////////////////////////////////////////////////////////////////////////
|
1191
|
+
// Define exports so this file can also be included as a module in Node.js //
|
1192
|
+
/////////////////////////////////////////////////////////////////////////////
|
1193
|
+
if(NODE) module.exports = {
|
1194
|
+
Controller: Controller,
|
1195
|
+
DatasetManager: DatasetManager,
|
1196
|
+
ChartManager: ChartManager,
|
1197
|
+
SensitivityAnalysis: SensitivityAnalysis,
|
1198
|
+
ExperimentManager: ExperimentManager
|
1199
|
+
}
|