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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/console.js +973 -0
  4. package/package.json +32 -0
  5. package/server.js +1547 -0
  6. package/static/fonts/FantasqueSansMono-Bold.ttf +0 -0
  7. package/static/fonts/FantasqueSansMono-BoldItalic.ttf +0 -0
  8. package/static/fonts/FantasqueSansMono-Italic.ttf +0 -0
  9. package/static/fonts/FantasqueSansMono-Regular.ttf +0 -0
  10. package/static/fonts/Hack-Bold.ttf +0 -0
  11. package/static/fonts/Hack-BoldItalic.ttf +0 -0
  12. package/static/fonts/Hack-Italic.ttf +0 -0
  13. package/static/fonts/Hack-Regular.ttf +0 -0
  14. package/static/fonts/Lato-Bold.ttf +0 -0
  15. package/static/fonts/Lato-BoldItalic.ttf +0 -0
  16. package/static/fonts/Lato-Italic.ttf +0 -0
  17. package/static/fonts/Lato-Regular.ttf +0 -0
  18. package/static/fonts/mplus-1m-bold.ttf +0 -0
  19. package/static/fonts/mplus-1m-light.ttf +0 -0
  20. package/static/fonts/mplus-1m-medium.ttf +0 -0
  21. package/static/fonts/mplus-1m-regular.ttf +0 -0
  22. package/static/fonts/mplus-1m-thin.ttf +0 -0
  23. package/static/images/access.png +0 -0
  24. package/static/images/actor.png +0 -0
  25. package/static/images/actors.png +0 -0
  26. package/static/images/add-selector.png +0 -0
  27. package/static/images/add.png +0 -0
  28. package/static/images/back.png +0 -0
  29. package/static/images/black-box.png +0 -0
  30. package/static/images/by-sa.svg +74 -0
  31. package/static/images/cancel.png +0 -0
  32. package/static/images/chart.png +0 -0
  33. package/static/images/check-disab.png +0 -0
  34. package/static/images/check-off.png +0 -0
  35. package/static/images/check-on.png +0 -0
  36. package/static/images/check-x.png +0 -0
  37. package/static/images/clone.png +0 -0
  38. package/static/images/close.png +0 -0
  39. package/static/images/cluster.png +0 -0
  40. package/static/images/compare.png +0 -0
  41. package/static/images/compress.png +0 -0
  42. package/static/images/constraint.png +0 -0
  43. package/static/images/copy.png +0 -0
  44. package/static/images/data-to-clpbrd.png +0 -0
  45. package/static/images/dataset.png +0 -0
  46. package/static/images/delete.png +0 -0
  47. package/static/images/diagram.png +0 -0
  48. package/static/images/down.png +0 -0
  49. package/static/images/edit-chart.png +0 -0
  50. package/static/images/edit.png +0 -0
  51. package/static/images/eq.png +0 -0
  52. package/static/images/equation.png +0 -0
  53. package/static/images/experiment.png +0 -0
  54. package/static/images/favicon.ico +0 -0
  55. package/static/images/fewer-dec.png +0 -0
  56. package/static/images/filter.png +0 -0
  57. package/static/images/find.png +0 -0
  58. package/static/images/forward.png +0 -0
  59. package/static/images/host-logo.png +0 -0
  60. package/static/images/icon.png +0 -0
  61. package/static/images/icon.svg +23 -0
  62. package/static/images/ignore.png +0 -0
  63. package/static/images/include.png +0 -0
  64. package/static/images/info-to-clpbrd.png +0 -0
  65. package/static/images/info.png +0 -0
  66. package/static/images/is-black-box.png +0 -0
  67. package/static/images/lbl.png +0 -0
  68. package/static/images/lift.png +0 -0
  69. package/static/images/link.png +0 -0
  70. package/static/images/linny-r.icns +0 -0
  71. package/static/images/linny-r.ico +0 -0
  72. package/static/images/linny-r.png +0 -0
  73. package/static/images/linny-r.svg +21 -0
  74. package/static/images/logo.png +0 -0
  75. package/static/images/model-info.png +0 -0
  76. package/static/images/module.png +0 -0
  77. package/static/images/monitor.png +0 -0
  78. package/static/images/more-dec.png +0 -0
  79. package/static/images/ne.png +0 -0
  80. package/static/images/new.png +0 -0
  81. package/static/images/note.png +0 -0
  82. package/static/images/ok.png +0 -0
  83. package/static/images/open.png +0 -0
  84. package/static/images/outcome.png +0 -0
  85. package/static/images/parent.png +0 -0
  86. package/static/images/paste.png +0 -0
  87. package/static/images/pause.png +0 -0
  88. package/static/images/print-chart.png +0 -0
  89. package/static/images/print.png +0 -0
  90. package/static/images/process.png +0 -0
  91. package/static/images/product.png +0 -0
  92. package/static/images/pwlf.png +0 -0
  93. package/static/images/receiver.png +0 -0
  94. package/static/images/redo.png +0 -0
  95. package/static/images/remove.png +0 -0
  96. package/static/images/rename.png +0 -0
  97. package/static/images/repo-logo.png +0 -0
  98. package/static/images/repository.png +0 -0
  99. package/static/images/reset.png +0 -0
  100. package/static/images/resize.png +0 -0
  101. package/static/images/restore.png +0 -0
  102. package/static/images/save-chart.png +0 -0
  103. package/static/images/save-data.png +0 -0
  104. package/static/images/save-diagram.png +0 -0
  105. package/static/images/save.png +0 -0
  106. package/static/images/sensitivity.png +0 -0
  107. package/static/images/settings.png +0 -0
  108. package/static/images/solve.png +0 -0
  109. package/static/images/solver-logo.png +0 -0
  110. package/static/images/stats-to-clpbrd.png +0 -0
  111. package/static/images/stats.png +0 -0
  112. package/static/images/stop.png +0 -0
  113. package/static/images/store.png +0 -0
  114. package/static/images/stretch.png +0 -0
  115. package/static/images/table-to-clpbrd.png +0 -0
  116. package/static/images/table.png +0 -0
  117. package/static/images/tree.png +0 -0
  118. package/static/images/tudelft.png +0 -0
  119. package/static/images/ubl.png +0 -0
  120. package/static/images/undo.png +0 -0
  121. package/static/images/up.png +0 -0
  122. package/static/images/zoom-in.png +0 -0
  123. package/static/images/zoom-out.png +0 -0
  124. package/static/index.html +3088 -0
  125. package/static/linny-r.css +4722 -0
  126. package/static/scripts/iro.min.js +7 -0
  127. package/static/scripts/linny-r-config.js +105 -0
  128. package/static/scripts/linny-r-ctrl.js +1199 -0
  129. package/static/scripts/linny-r-gui.js +14814 -0
  130. package/static/scripts/linny-r-milp.js +286 -0
  131. package/static/scripts/linny-r-model.js +10405 -0
  132. package/static/scripts/linny-r-utils.js +687 -0
  133. package/static/scripts/linny-r-vm.js +7079 -0
  134. package/static/show-diff.html +84 -0
  135. package/static/show-png.html +113 -0
  136. package/static/sounds/error.wav +0 -0
  137. package/static/sounds/notification.wav +0 -0
  138. 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 = ['&sim;t', '&mu;', '&Sigma;', '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
+ }