linny-r 1.6.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/static/scripts/linny-r-gui-controller.js +47 -35
- package/static/scripts/linny-r-gui-documentation-manager.js +36 -32
- package/static/scripts/linny-r-gui-equation-manager.js +10 -7
- package/static/scripts/linny-r-gui-finder.js +1 -0
- package/static/scripts/linny-r-milp.js +18 -15
- package/static/scripts/linny-r-model.js +107 -61
- package/static/scripts/linny-r-utils.js +18 -7
- package/static/scripts/linny-r-vm.js +11 -10
package/package.json
CHANGED
@@ -2935,8 +2935,8 @@ class GUIController extends Controller {
|
|
2935
2935
|
}
|
2936
2936
|
const err = MODEL.cloneSelection(prefix, actor_name, renumber);
|
2937
2937
|
if(err) {
|
2938
|
-
// Something went wrong, so do not hide the modal, but focus on
|
2939
|
-
// DOM element returned by the model's cloning method
|
2938
|
+
// Something went wrong, so do not hide the modal, but focus on
|
2939
|
+
// the DOM element returned by the model's cloning method.
|
2940
2940
|
const el = md.element(err);
|
2941
2941
|
if(el) {
|
2942
2942
|
el.focus();
|
@@ -2956,7 +2956,7 @@ class GUIController extends Controller {
|
|
2956
2956
|
}
|
2957
2957
|
|
2958
2958
|
copySelection() {
|
2959
|
-
// Save selection as XML in local storage of the browser
|
2959
|
+
// Save selection as XML in local storage of the browser.
|
2960
2960
|
const xml = MODEL.selectionAsXML;
|
2961
2961
|
if(xml) {
|
2962
2962
|
window.localStorage.setItem('Linny-R-selection-XML', xml);
|
@@ -2967,20 +2967,22 @@ class GUIController extends Controller {
|
|
2967
2967
|
}
|
2968
2968
|
|
2969
2969
|
get canPaste() {
|
2970
|
+
// Return TRUE if the browser has a recent selection-as-XML object
|
2971
|
+
// in its local storage.
|
2970
2972
|
const xml = window.localStorage.getItem('Linny-R-selection-XML');
|
2971
2973
|
if(xml) {
|
2972
2974
|
const timestamp = xml.match(/<copy timestamp="(\d+)"/);
|
2973
2975
|
if(timestamp) {
|
2974
2976
|
if(Date.now() - parseInt(timestamp[1]) < 8*3600000) return true;
|
2975
2977
|
}
|
2976
|
-
// Remove XML from local storage if older than 8 hours
|
2978
|
+
// Remove XML from local storage if older than 8 hours.
|
2977
2979
|
window.localStorage.removeItem('Linny-R-selection-XML');
|
2978
2980
|
}
|
2979
2981
|
return false;
|
2980
2982
|
}
|
2981
2983
|
|
2982
2984
|
promptForMapping(mapping) {
|
2983
|
-
// Prompt user to specify name conflict resolution strategy
|
2985
|
+
// Prompt user to specify name conflict resolution strategy.
|
2984
2986
|
const md = this.paste_modal;
|
2985
2987
|
md.mapping = mapping;
|
2986
2988
|
md.element('from-prefix').innerText = mapping.from_prefix || '';
|
@@ -3000,7 +3002,8 @@ class GUIController extends Controller {
|
|
3000
3002
|
if(tc.length) {
|
3001
3003
|
sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
|
3002
3004
|
'Names for top-level clusters:</div>');
|
3003
|
-
|
3005
|
+
const sll = sl.length;
|
3006
|
+
// Add text inputs for selected cluster nodes.
|
3004
3007
|
for(let i = 0; i < tc.length; i++) {
|
3005
3008
|
const
|
3006
3009
|
ti = mapping.top_clusters[tc[i]],
|
@@ -3012,11 +3015,14 @@ class GUIController extends Controller {
|
|
3012
3015
|
'" type="text" style="', state, 'font-size: 12px" value="',
|
3013
3016
|
ti, '"></div></div>');
|
3014
3017
|
}
|
3018
|
+
// Remove header when no items were added.
|
3019
|
+
if(sl.length === sll) sl.pop();
|
3015
3020
|
}
|
3016
3021
|
if(ft.length) {
|
3017
3022
|
sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
|
3018
3023
|
'Mapping of nodes to link from/to:</div>');
|
3019
|
-
|
3024
|
+
const sll = sl.length;
|
3025
|
+
// Add selectors for unresolved FROM/TO nodes.
|
3020
3026
|
for(let i = 0; i < ft.length; i++) {
|
3021
3027
|
const ti = mapping.from_to[ft[i]];
|
3022
3028
|
if(ft[i] === ti) {
|
@@ -3036,15 +3042,17 @@ class GUIController extends Controller {
|
|
3036
3042
|
sl.push('</div>');
|
3037
3043
|
}
|
3038
3044
|
}
|
3045
|
+
// Remove header when no items were added.
|
3046
|
+
if(sl.length === sll) sl.pop();
|
3039
3047
|
}
|
3040
3048
|
md.element('scroll-area').innerHTML = sl.join('');
|
3041
|
-
// Open dialog, which will call pasteSelection(...) on OK
|
3049
|
+
// Open dialog, which will call pasteSelection(...) on OK.
|
3042
3050
|
this.paste_modal.show();
|
3043
3051
|
}
|
3044
3052
|
|
3045
3053
|
setPasteMapping() {
|
3046
|
-
//
|
3047
|
-
//
|
3054
|
+
// Update the paste mapping as specified by the modeler and then
|
3055
|
+
// proceed to paste.
|
3048
3056
|
const
|
3049
3057
|
md = this.paste_modal,
|
3050
3058
|
mapping = Object.assign(md.mapping, {}),
|
@@ -3071,7 +3079,7 @@ class GUIController extends Controller {
|
|
3071
3079
|
pasteSelection(mapping={}) {
|
3072
3080
|
// If selection has been saved as XML in local storage, test to
|
3073
3081
|
// see whether PASTE would result in name conflicts, and if so,
|
3074
|
-
// open the name conflict resolution window
|
3082
|
+
// open the name conflict resolution window.
|
3075
3083
|
let xml = window.localStorage.getItem('Linny-R-selection-XML');
|
3076
3084
|
try {
|
3077
3085
|
xml = parseXML(xml);
|
@@ -3095,7 +3103,7 @@ class GUIController extends Controller {
|
|
3095
3103
|
// AUXILIARY FUNCTIONS
|
3096
3104
|
|
3097
3105
|
function fullName(node) {
|
3098
|
-
//
|
3106
|
+
// Return full entity name inferred from XML node data.
|
3099
3107
|
if(node.nodeName === 'from-to' || node.nodeName === 'selc') {
|
3100
3108
|
const
|
3101
3109
|
n = xmlDecoded(nodeParameterValue(node, 'name')),
|
@@ -3134,12 +3142,12 @@ class GUIController extends Controller {
|
|
3134
3142
|
}
|
3135
3143
|
|
3136
3144
|
function nameAndActor(name) {
|
3137
|
-
//
|
3138
|
-
//
|
3145
|
+
// Return tuple [entity name, actor name] if `name` ends with a
|
3146
|
+
// parenthesized string that identifies an actor in the selection.
|
3139
3147
|
const ai = name.lastIndexOf(' (');
|
3140
3148
|
if(ai < 0) return [name, ''];
|
3141
3149
|
let actor = name.slice(ai + 2, -1);
|
3142
|
-
// Test whether parenthesized string denotes an actor
|
3150
|
+
// Test whether parenthesized string denotes an actor.
|
3143
3151
|
if(actor_names.indexOf(actor) >= 0 || actor === mapping.actor ||
|
3144
3152
|
actor === mapping.from_actor || actor === mapping.to_actor) {
|
3145
3153
|
name = name.substring(0, ai);
|
@@ -3150,8 +3158,8 @@ class GUIController extends Controller {
|
|
3150
3158
|
}
|
3151
3159
|
|
3152
3160
|
function mappedName(n) {
|
3153
|
-
// Returns full name `n` modified according to the mapping
|
3154
|
-
// NOTE:
|
3161
|
+
// Returns full name `n` modified according to the mapping.
|
3162
|
+
// NOTE: Links and constraints require two mappings (recursion!).
|
3155
3163
|
if(n.indexOf(UI.LINK_ARROW) > 0) {
|
3156
3164
|
const ft = n.split(UI.LINK_ARROW);
|
3157
3165
|
return mappedName(ft[0]) + UI.LINK_ARROW + mappedName(ft[1]);
|
@@ -3174,7 +3182,7 @@ class GUIController extends Controller {
|
|
3174
3182
|
const ai = n.lastIndexOf(mapping.from_actor);
|
3175
3183
|
if(ai > 0) return n.substring(0, ai) + mapping.to_actor;
|
3176
3184
|
}
|
3177
|
-
// NOTE: specified actor cannot override existing actor
|
3185
|
+
// NOTE: specified actor cannot override existing actor.
|
3178
3186
|
if(mapping.actor && !nameAndActor(n)[1]) {
|
3179
3187
|
return `${n} (${mapping.actor})`;
|
3180
3188
|
}
|
@@ -3191,23 +3199,27 @@ class GUIController extends Controller {
|
|
3191
3199
|
if(mapping.from_to && mapping.from_to[n]) {
|
3192
3200
|
return mapping.from_to[n];
|
3193
3201
|
}
|
3194
|
-
// No mapping => return original name
|
3202
|
+
// No mapping => return original name.
|
3195
3203
|
return n;
|
3196
3204
|
}
|
3197
3205
|
|
3198
3206
|
function nameConflicts(node) {
|
3199
3207
|
// Maps names of entities defined by the child nodes of `node`
|
3200
|
-
// while detecting name conflicts
|
3208
|
+
// while detecting name conflicts.
|
3201
3209
|
for(let i = 0; i < node.childNodes.length; i++) {
|
3202
3210
|
const c = node.childNodes[i];
|
3203
3211
|
if(c.nodeName !== 'link' && c.nodeName !== 'constraint') {
|
3204
3212
|
const
|
3205
3213
|
fn = fullName(c),
|
3206
|
-
mn = mappedName(fn)
|
3214
|
+
mn = mappedName(fn),
|
3215
|
+
obj = MODEL.objectByName(mn),
|
3216
|
+
// Assume that existing products can be added as product
|
3217
|
+
// positions if they are not prefixed.
|
3218
|
+
add_pp = (obj instanceof Product && mn.indexOf(UI.PREFIXER) < 0);
|
3207
3219
|
// Name conflict occurs when the mapped name is already in use
|
3208
3220
|
// in the target model, or when the original name is mapped onto
|
3209
|
-
// different names (this might occur due to modeler input)
|
3210
|
-
if(
|
3221
|
+
// different names (this might occur due to modeler input).
|
3222
|
+
if((obj && !add_pp) || (name_map[fn] && name_map[fn] !== mn)) {
|
3211
3223
|
addDistinct(fn, name_conflicts);
|
3212
3224
|
} else {
|
3213
3225
|
name_map[fn] = mn;
|
@@ -3217,10 +3229,10 @@ class GUIController extends Controller {
|
|
3217
3229
|
}
|
3218
3230
|
|
3219
3231
|
function addEntityFromNode(node) {
|
3220
|
-
// Adds entity to model based on XML node data and mapping
|
3221
|
-
// NOTE:
|
3232
|
+
// Adds entity to model based on XML node data and mapping.
|
3233
|
+
// NOTE: Do not add if an entity having this type and mapped name
|
3222
3234
|
// already exists; name conflicts accross entity types may occur
|
3223
|
-
// and result in error messages
|
3235
|
+
// and result in error messages.
|
3224
3236
|
const
|
3225
3237
|
et = node.nodeName,
|
3226
3238
|
fn = fullName(node),
|
@@ -3282,17 +3294,17 @@ class GUIController extends Controller {
|
|
3282
3294
|
sp = this.sharedPrefix(cn, fcn),
|
3283
3295
|
fpn = (cn === UI.TOP_CLUSTER_NAME ? '' : cn.replace(sp, '')),
|
3284
3296
|
tpn = (fcn === UI.TOP_CLUSTER_NAME ? '' : fcn.replace(sp, ''));
|
3285
|
-
// Infer mapping from XML data and focal cluster name & actor name
|
3297
|
+
// Infer mapping from XML data and focal cluster name & actor name.
|
3286
3298
|
mapping.shared_prefix = sp;
|
3287
3299
|
mapping.from_prefix = (fpn ? sp + fpn + UI.PREFIXER : sp);
|
3288
3300
|
mapping.to_prefix = (tpn ? sp + tpn + UI.PREFIXER : sp);
|
3289
3301
|
mapping.from_actor = (ca === UI.NO_ACTOR ? '' : ca);
|
3290
3302
|
mapping.to_actor = (fca === UI.NO_ACTOR ? '' : fca);
|
3291
|
-
// Prompt for mapping when pasting to the same model and cluster
|
3303
|
+
// Prompt for mapping when pasting to the same model and cluster.
|
3292
3304
|
if(parseInt(mts) === MODEL.time_created.getTime() &&
|
3293
3305
|
ca === fca && mapping.from_prefix === mapping.to_prefix &&
|
3294
3306
|
!(mapping.prefix || mapping.actor || mapping.increment)) {
|
3295
|
-
// Prompt for names of selected cluster nodes
|
3307
|
+
// Prompt for names of selected cluster nodes.
|
3296
3308
|
if(selc_node.childNodes.length && !mapping.prefix) {
|
3297
3309
|
mapping.top_clusters = {};
|
3298
3310
|
for(let i = 0; i < selc_node.childNodes.length; i++) {
|
@@ -3307,7 +3319,7 @@ class GUIController extends Controller {
|
|
3307
3319
|
return;
|
3308
3320
|
}
|
3309
3321
|
// Also prompt if FROM and/or TO nodes are not selected, and map to
|
3310
|
-
// existing entities
|
3322
|
+
// existing entities.
|
3311
3323
|
if(from_tos_node.childNodes.length && !mapping.from_to) {
|
3312
3324
|
const
|
3313
3325
|
ft_map = {},
|
@@ -3323,7 +3335,7 @@ class GUIController extends Controller {
|
|
3323
3335
|
'Data' : nodeParameterValue(c, 'type'));
|
3324
3336
|
}
|
3325
3337
|
}
|
3326
|
-
// Prompt only for FROM/TO nodes that map to existing nodes
|
3338
|
+
// Prompt only for FROM/TO nodes that map to existing nodes.
|
3327
3339
|
if(Object.keys(ft_map).length) {
|
3328
3340
|
mapping.from_to = ft_map;
|
3329
3341
|
mapping.from_to_type = ft_type;
|
@@ -3334,7 +3346,7 @@ class GUIController extends Controller {
|
|
3334
3346
|
|
3335
3347
|
// Only check for selected entities; from-to's and extra's should be
|
3336
3348
|
// used if they exist, or should be created when copying to a different
|
3337
|
-
// model
|
3349
|
+
// model.
|
3338
3350
|
name_map.length = 0;
|
3339
3351
|
nameConflicts(entities_node);
|
3340
3352
|
if(name_conflicts.length) {
|
@@ -3353,20 +3365,20 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3353
3365
|
for(let i = 0; i < entities_node.childNodes.length; i++) {
|
3354
3366
|
addEntityFromNode(entities_node.childNodes[i]);
|
3355
3367
|
}
|
3356
|
-
// Update diagram, showing newly added nodes as selection
|
3368
|
+
// Update diagram, showing newly added nodes as selection.
|
3357
3369
|
MODEL.clearSelection();
|
3358
3370
|
for(let i = 0; i < selection_node.childNodes.length; i++) {
|
3359
3371
|
const
|
3360
3372
|
n = xmlDecoded(nodeContent(selection_node.childNodes[i])),
|
3361
3373
|
obj = MODEL.objectByName(mappedName(n));
|
3362
3374
|
if(obj) {
|
3363
|
-
// NOTE:
|
3375
|
+
// NOTE: Selected products must be positioned.
|
3364
3376
|
if(obj instanceof Product) MODEL.focal_cluster.addProductPosition(obj);
|
3365
3377
|
MODEL.select(obj);
|
3366
3378
|
}
|
3367
3379
|
}
|
3368
3380
|
// Force redrawing the selection to ensure that links to positioned
|
3369
|
-
// products are displayed as arrows instead of block arrows
|
3381
|
+
// products are displayed as arrows instead of block arrows.
|
3370
3382
|
fc.clearAllProcesses();
|
3371
3383
|
UI.drawDiagram(MODEL);
|
3372
3384
|
this.paste_modal.hide();
|
@@ -338,7 +338,7 @@ class DocumentationManager {
|
|
338
338
|
this.viewer.innerHTML = this.markdown;
|
339
339
|
this.edit_btn.classList.remove('disab');
|
340
340
|
this.edit_btn.classList.add('enab');
|
341
|
-
// NOTE:
|
341
|
+
// NOTE: Permit documentation of the model by raising the dialog.
|
342
342
|
if(this.entity === MODEL) this.dialog.style.zIndex = 101;
|
343
343
|
} else if(e instanceof DatasetModifier) {
|
344
344
|
this.title.innerHTML = e.selector;
|
@@ -347,17 +347,17 @@ class DocumentationManager {
|
|
347
347
|
if(e.expression.eligible_prefixes) {
|
348
348
|
const el = Object.keys(e.expression.eligible_prefixes)
|
349
349
|
.sort(compareSelectors);
|
350
|
-
if(el.length > 0) this.viewer.innerHTML =
|
351
|
-
e.selector
|
352
|
-
pluralS(el.length, 'prefixed entity group')
|
353
|
-
':<ul><li>'
|
350
|
+
if(el.length > 0) this.viewer.innerHTML = [
|
351
|
+
'Method <tt>', e.selector, '</tt> applies to ',
|
352
|
+
pluralS(el.length, 'prefixed entity group'),
|
353
|
+
':<ul><li>', el.join('</li><li>'), '</li></ul>'].join('');
|
354
354
|
}
|
355
355
|
}
|
356
356
|
}
|
357
357
|
}
|
358
358
|
|
359
359
|
rewrite(str) {
|
360
|
-
// Apply all the rewriting rules to `str
|
360
|
+
// Apply all the rewriting rules to `str`.
|
361
361
|
str = '\n' + str + '\n';
|
362
362
|
this.rules.forEach(
|
363
363
|
(rule) => { str = str.replace(rule.pattern, rule.rewrite); });
|
@@ -365,28 +365,29 @@ class DocumentationManager {
|
|
365
365
|
}
|
366
366
|
|
367
367
|
makeList(par, isp, type) {
|
368
|
-
// Split on the *global multi-line* item separator pattern
|
368
|
+
// Split on the *global multi-line* item separator pattern.
|
369
369
|
const splitter = new RegExp(isp, 'gm'),
|
370
370
|
list = par.split(splitter);
|
371
371
|
if(list.length < 2) return false;
|
372
|
-
// Now we know that the paragraph contains at least one list item line
|
372
|
+
// Now we know that the paragraph contains at least one list item line.
|
373
373
|
let start = 0;
|
374
|
-
// Paragraph may start with plain text, so check using the original
|
374
|
+
// Paragraph may start with plain text, so check using the original
|
375
|
+
// pattern.
|
375
376
|
if(!par.match(isp)) {
|
376
377
|
// If so, retain this first part as a separate paragraph...
|
377
378
|
start = 1;
|
378
|
-
// NOTE:
|
379
|
+
// NOTE: Add it only if it contains text.
|
379
380
|
par = (list[0].trim() ? `<p>${this.rewrite(list[0])}</p>` : '');
|
380
|
-
// ... and clear it as list item
|
381
|
+
// ... and clear it as list item.
|
381
382
|
list[0] = '';
|
382
383
|
} else {
|
383
384
|
par = '';
|
384
385
|
}
|
385
|
-
// Rewrite each list item fragment that contains text
|
386
|
+
// Rewrite each list item fragment that contains text.
|
386
387
|
for(let j = start; j < list.length; j++) {
|
387
388
|
list[j] = (list[j].trim() ? `<li>${this.rewrite(list[j])}</li>` : '');
|
388
389
|
}
|
389
|
-
// Return assembled parts
|
390
|
+
// Return assembled parts.
|
390
391
|
return [par, '<', type, 'l>', list.join(''), '</', type, 'l>'].join('');
|
391
392
|
}
|
392
393
|
|
@@ -395,16 +396,16 @@ class DocumentationManager {
|
|
395
396
|
const html = this.markup.split(/\n{2,}/);
|
396
397
|
let list;
|
397
398
|
for(let i = 0; i < html.length; i++) {
|
398
|
-
// Paragraph with only dashes and spaces becomes a horizontal rule
|
399
|
+
// Paragraph with only dashes and spaces becomes a horizontal rule.
|
399
400
|
if(html[i].match(/^( *-)+$/)) {
|
400
401
|
html[i] = '<hr>';
|
401
|
-
// Paragraph may contain a bulleted list
|
402
|
+
// Paragraph may contain a bulleted list.
|
402
403
|
} else if ((list = this.makeList(html[i], /^ *- +/, 'u')) !== false) {
|
403
404
|
html[i] = list;
|
404
|
-
// Paragraph may contain a numbered list
|
405
|
+
// Paragraph may contain a numbered list.
|
405
406
|
} else if ((list = this.makeList(html[i], /^ *\d+. +/, 'o')) !== false) {
|
406
407
|
html[i] = list;
|
407
|
-
// Otherwise: default HTML paragraph
|
408
|
+
// Otherwise: default HTML paragraph.
|
408
409
|
} else {
|
409
410
|
html[i] = `<p>${this.rewrite(html[i])}</p>`;
|
410
411
|
}
|
@@ -431,7 +432,7 @@ class DocumentationManager {
|
|
431
432
|
}
|
432
433
|
|
433
434
|
insertSymbol(sym) {
|
434
|
-
// Insert symbol (clicked item in list below text area) into text area
|
435
|
+
// Insert symbol (clicked item in list below text area) into text area.
|
435
436
|
this.editor.focus();
|
436
437
|
let p = this.editor.selectionStart;
|
437
438
|
const
|
@@ -454,7 +455,7 @@ class DocumentationManager {
|
|
454
455
|
} else if(this.entity instanceof Constraint) {
|
455
456
|
UI.paper.drawConstraint(this.entity);
|
456
457
|
} else if (typeof this.entity.draw === 'function') {
|
457
|
-
// Only draw if the entity responds to that method
|
458
|
+
// Only draw if the entity responds to that method.
|
458
459
|
this.entity.draw();
|
459
460
|
}
|
460
461
|
}
|
@@ -501,14 +502,14 @@ class DocumentationManager {
|
|
501
502
|
}
|
502
503
|
|
503
504
|
addMessage(msg) {
|
504
|
-
// Append message to the info messages list
|
505
|
+
// Append message to the info messages list.
|
505
506
|
if(msg) this.info_messages.push(msg);
|
506
|
-
// Update dialog only when it is showing
|
507
|
+
// Update dialog only when it is showing.
|
507
508
|
if(!UI.hidden(this.dialog.id)) this.showInfoMessages(true);
|
508
509
|
}
|
509
510
|
|
510
511
|
showInfoMessages(shift) {
|
511
|
-
// Show all messages that have appeared on the status line
|
512
|
+
// Show all messages that have appeared on the status line.
|
512
513
|
const
|
513
514
|
n = this.info_messages.length,
|
514
515
|
title = pluralS(n, 'message') + ' since the current model was loaded';
|
@@ -524,21 +525,21 @@ class DocumentationManager {
|
|
524
525
|
'<div class="', m.status, first, '-msg">', m.text, '</div></div>');
|
525
526
|
}
|
526
527
|
this.viewer.innerHTML = divs.join('');
|
527
|
-
// Set the dialog title
|
528
|
+
// Set the dialog title.
|
528
529
|
this.title.innerHTML = title;
|
529
530
|
}
|
530
531
|
}
|
531
532
|
|
532
533
|
showArrowLinks(arrow) {
|
533
|
-
// Show list of links represented by a composite arrow
|
534
|
+
// Show list of links represented by a composite arrow.
|
534
535
|
const
|
535
536
|
n = arrow.links.length,
|
536
537
|
msg = 'Arrow represents ' + pluralS(n, 'link');
|
537
538
|
UI.setMessage(msg);
|
538
539
|
if(this.visible && !this.editing) {
|
539
|
-
// Set the dialog title
|
540
|
+
// Set the dialog title.
|
540
541
|
this.title.innerHTML = msg;
|
541
|
-
// Show list
|
542
|
+
// Show list.
|
542
543
|
const lis = [];
|
543
544
|
let l, dn, c, af;
|
544
545
|
for(let i = 0; i < n; i++) {
|
@@ -570,7 +571,8 @@ class DocumentationManager {
|
|
570
571
|
}
|
571
572
|
|
572
573
|
showHiddenIO(node, arrow) {
|
573
|
-
// Show list of products or processes linked to node by an invisible
|
574
|
+
// Show list of products or processes linked to node by an invisible
|
575
|
+
// arrow (i.e., links represented by a block arrow).
|
574
576
|
let msg, iol;
|
575
577
|
if(arrow === UI.BLOCK_IN) {
|
576
578
|
iol = node.hidden_inputs;
|
@@ -586,9 +588,9 @@ class DocumentationManager {
|
|
586
588
|
UI.on_block_arrow = true;
|
587
589
|
UI.setMessage(msg);
|
588
590
|
if(this.visible && !this.editing) {
|
589
|
-
// Set the dialog title
|
591
|
+
// Set the dialog title.
|
590
592
|
this.title.innerHTML = msg;
|
591
|
-
// Show list
|
593
|
+
// Show list.
|
592
594
|
const lis = [];
|
593
595
|
for(let i = 0; i < iol.length; i++) {
|
594
596
|
lis.push(`<li>${iol[i].displayName}</li>`);
|
@@ -599,17 +601,19 @@ class DocumentationManager {
|
|
599
601
|
}
|
600
602
|
|
601
603
|
showAllDocumentation() {
|
604
|
+
// Show (as HTML) all model entities (categorized by type) with their
|
605
|
+
// associated comments (if added by the modeler).
|
602
606
|
const
|
603
607
|
html = [],
|
604
608
|
sl = MODEL.listOfAllComments;
|
605
609
|
for(let i = 0; i < sl.length; i++) {
|
606
610
|
if(sl[i].startsWith('_____')) {
|
607
|
-
// 5-underscore leader indicates: start of new category
|
611
|
+
// 5-underscore leader indicates: start of new category.
|
608
612
|
html.push('<h2>', sl[i].substring(5), '</h2>');
|
609
613
|
} else {
|
610
614
|
// Expect model element name...
|
611
615
|
html.push('<p><tt>', sl[i], '</tt><br><small>');
|
612
|
-
// ... immediately followed by its associated marked-up comments
|
616
|
+
// ... immediately followed by its associated marked-up comments.
|
613
617
|
i++;
|
614
618
|
this.markup = sl[i];
|
615
619
|
html.push(this.markdown, '</small></p>');
|
@@ -617,7 +621,7 @@ class DocumentationManager {
|
|
617
621
|
}
|
618
622
|
this.title.innerHTML = 'Complete model documentation';
|
619
623
|
this.viewer.innerHTML = html.join('');
|
620
|
-
// Deselect entity and disable editing
|
624
|
+
// Deselect entity and disable editing.
|
621
625
|
this.entity = null;
|
622
626
|
this.edit_btn.classList.remove('enab');
|
623
627
|
this.edit_btn.classList.add('disab');
|
@@ -151,7 +151,7 @@ class EquationManager {
|
|
151
151
|
(m === sm ? ' sel-set' : ''),
|
152
152
|
'"><td class="equation-selector',
|
153
153
|
(method ? ' method' : ''),
|
154
|
-
// Display in gray when method cannot be applied
|
154
|
+
// Display in gray when method cannot be applied.
|
155
155
|
(m.expression.noMethodObject ? ' no-object' : ''),
|
156
156
|
(m.expression.isStatic ? '' : ' it'), issue,
|
157
157
|
(wild ? ' wildcard' : ''), clk, ', false);"', mover, '>',
|
@@ -268,20 +268,23 @@ class EquationManager {
|
|
268
268
|
if(!this.selected_modifier) return;
|
269
269
|
const
|
270
270
|
sel = this.rename_modal.element('name').value,
|
271
|
-
// Keep track of old name
|
271
|
+
// Keep track of old name.
|
272
272
|
oldm = this.selected_modifier,
|
273
273
|
olds = oldm.selector,
|
274
|
-
// NOTE: addModifier returns existing one if selector not changed
|
274
|
+
// NOTE: addModifier returns existing one if selector not changed.
|
275
275
|
m = MODEL.equations_dataset.addModifier(sel);
|
276
|
-
// NULL indicates invalid name
|
276
|
+
// NULL indicates invalid name.
|
277
277
|
if(!m) return;
|
278
|
-
// If only case has changed, update the selector
|
279
|
-
// NOTE:
|
278
|
+
// If only case has changed, update the selector.
|
279
|
+
// NOTE: Equation names may contain spaces; if so, reduce to single space.
|
280
280
|
if(m === oldm) {
|
281
281
|
m.selector = sel.trim().replace(/\s+/g, ' ');
|
282
282
|
} else {
|
283
|
-
// When a new modifier has been added, more actions are needed
|
283
|
+
// When a new modifier has been added, more actions are needed.
|
284
284
|
m.expression = oldm.expression;
|
285
|
+
// NOTE: The `attribute` property of the expression must be updated
|
286
|
+
// because it identifies the "owner" of the expression.
|
287
|
+
m.expression.attribute = m.selector;
|
285
288
|
this.deleteEquation();
|
286
289
|
this.selected_modifier = m;
|
287
290
|
}
|
@@ -441,6 +441,7 @@ class Finder {
|
|
441
441
|
}
|
442
442
|
}
|
443
443
|
document.getElementById('finder-item-header').innerHTML = hdr;
|
444
|
+
occ.sort(compareSelectors);
|
444
445
|
for(let i = 0; i < occ.length; i++) {
|
445
446
|
const e = MODEL.objectByID(occ[i]);
|
446
447
|
el.push(['<tr id="eotr', i, '" class="dataset" onclick="FINDER.reveal(\'',
|
@@ -51,8 +51,8 @@ module.exports = class MILPSolver {
|
|
51
51
|
// Each external MILP solver application has its own interface
|
52
52
|
// NOTE: the list may be extended to accommodate more MILP solvers
|
53
53
|
if(this.id === 'gurobi') {
|
54
|
-
this.ext = '.
|
55
|
-
this.user_model = path.join(workspace.solver_output, 'usr_model.
|
54
|
+
this.ext = '.lp';
|
55
|
+
this.user_model = path.join(workspace.solver_output, 'usr_model.lp');
|
56
56
|
this.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
|
57
57
|
this.solution = path.join(workspace.solver_output, 'model.json');
|
58
58
|
this.log = path.join(workspace.solver_output, 'model.log');
|
@@ -283,17 +283,17 @@ module.exports = class MILPSolver {
|
|
283
283
|
x_dict = {},
|
284
284
|
getValuesFromDict = () => {
|
285
285
|
// Returns a result vector for as many real numbers (as strings!)
|
286
|
-
// as there are columns (0 if not reported by the solver)
|
287
|
-
//
|
286
|
+
// as there are columns (0 if not reported by the solver).
|
287
|
+
// First sort on variable name
|
288
288
|
const vlist = Object.keys(x_dict).sort();
|
289
|
-
// Start with column 1
|
289
|
+
// Start with column 1.
|
290
290
|
let col = 1;
|
291
291
|
for(let i = 0; i < vlist.length; i++) {
|
292
292
|
const
|
293
293
|
v = vlist[i],
|
294
|
-
// Variable names have zero-padded column numbers, e.g. "X001"
|
294
|
+
// Variable names have zero-padded column numbers, e.g. "X001".
|
295
295
|
vnr = parseInt(v.substring(1));
|
296
|
-
// Add zeros for unreported variables until column number matches
|
296
|
+
// Add zeros for unreported variables until column number matches.
|
297
297
|
while(col < vnr) {
|
298
298
|
x_values.push('0');
|
299
299
|
col++;
|
@@ -301,23 +301,23 @@ module.exports = class MILPSolver {
|
|
301
301
|
x_values.push(x_dict[v]);
|
302
302
|
col++;
|
303
303
|
}
|
304
|
-
// Add zeros to vector for remaining columns
|
304
|
+
// Add zeros to vector for remaining columns.
|
305
305
|
while(col <= result.columns) {
|
306
306
|
x_values.push('0');
|
307
307
|
col++;
|
308
308
|
}
|
309
|
-
// No return value; function operates on x_values
|
309
|
+
// No return value; function operates on x_values.
|
310
310
|
};
|
311
311
|
|
312
312
|
if(this.id === 'gurobi') {
|
313
|
-
// `messages` must be an array of strings
|
313
|
+
// `messages` must be an array of strings.
|
314
314
|
result.messages = fs.readFileSync(this.log, 'utf8').split(os.EOL);
|
315
315
|
if(result.status !== 0) {
|
316
|
-
// Non-zero solver exit code may indicate expired license
|
316
|
+
// Non-zero solver exit code may indicate expired license.
|
317
317
|
result.error = 'Your Gurobi license may have expired';
|
318
318
|
} else {
|
319
319
|
try {
|
320
|
-
// Read JSON string from solution file
|
320
|
+
// Read JSON string from solution file.
|
321
321
|
const
|
322
322
|
json = fs.readFileSync(this.solution, 'utf8').trim(),
|
323
323
|
sol = JSON.parse(json);
|
@@ -329,13 +329,16 @@ module.exports = class MILPSolver {
|
|
329
329
|
if(!result.error) result.error = 'Unknown solver error';
|
330
330
|
console.log(`Solver status: ${result.status} - ${result.error}`);
|
331
331
|
}
|
332
|
-
// Objective value
|
332
|
+
// Objective value.
|
333
333
|
result.obj = sol.SolutionInfo.ObjVal;
|
334
|
-
// Values of solution vector
|
334
|
+
// Values of solution vector.
|
335
335
|
if(sol.Vars) {
|
336
|
+
// Fill dictionary with variable name: value entries.
|
336
337
|
for(let i = 0; i < sol.Vars.length; i++) {
|
337
|
-
|
338
|
+
x_dict[sol.Vars[i].VarName] = sol.Vars[i].X;
|
338
339
|
}
|
340
|
+
// Fill the solution vector, adding 0 for missing columns.
|
341
|
+
getValuesFromDict();
|
339
342
|
}
|
340
343
|
} catch(err) {
|
341
344
|
console.log('WARNING: Could not read solution file');
|
@@ -1561,13 +1561,21 @@ class LinnyRModel {
|
|
1561
1561
|
//
|
1562
1562
|
|
1563
1563
|
alignToGrid() {
|
1564
|
-
// Move all positioned model elements to the nearest grid point
|
1564
|
+
// Move all positioned model elements to the nearest grid point.
|
1565
1565
|
if(!this.align_to_grid) return;
|
1566
1566
|
let move = false;
|
1567
1567
|
const fc = this.focal_cluster;
|
1568
1568
|
// NOTE: Do not align notes to the grid. This will permit more
|
1569
1569
|
// precise positioning, while aligning will not improve the layout
|
1570
1570
|
// of the diagram because notes are not connected to arrows.
|
1571
|
+
// However, when notes relate to nearby nodes, preserve their relative
|
1572
|
+
// position to this node.
|
1573
|
+
for(let i = 0; i < fc.notes.length; i++) {
|
1574
|
+
const
|
1575
|
+
note = fc.notes[i],
|
1576
|
+
nbn = note.nearbyNode;
|
1577
|
+
note.nearby_pos = (nbn ? {node: nbn, oldx: x, oldy: y} : null);
|
1578
|
+
}
|
1571
1579
|
for(let i = 0; i < fc.processes.length; i++) {
|
1572
1580
|
move = fc.processes[i].alignToGrid() || move;
|
1573
1581
|
}
|
@@ -1577,11 +1585,25 @@ class LinnyRModel {
|
|
1577
1585
|
for(let i = 0; i < fc.sub_clusters.length; i++) {
|
1578
1586
|
move = fc.sub_clusters[i].alignToGrid() || move;
|
1579
1587
|
}
|
1580
|
-
if(move)
|
1588
|
+
if(move) {
|
1589
|
+
// Reposition "associated" notes.
|
1590
|
+
for(let i = 0; i < fc.notes.length; i++) {
|
1591
|
+
const
|
1592
|
+
note = fc.notes[i],
|
1593
|
+
nbp = note.nearby_pos;
|
1594
|
+
if(nbp) {
|
1595
|
+
// Adjust (x, y) so as to retain the relative position.
|
1596
|
+
note.x += nbp.node.x - npb.oldx;
|
1597
|
+
note.y += nbp.node.y - npb.oldy;
|
1598
|
+
note.nearby_pos = null;
|
1599
|
+
}
|
1600
|
+
}
|
1601
|
+
UI.drawDiagram(this);
|
1602
|
+
}
|
1581
1603
|
}
|
1582
1604
|
|
1583
1605
|
translateGraph(dx, dy) {
|
1584
|
-
// Move all entities in the focal cluster by (dx, dy) pixels
|
1606
|
+
// Move all entities in the focal cluster by (dx, dy) pixels.
|
1585
1607
|
if(!dx && !dy) return;
|
1586
1608
|
const fc = this.focal_cluster;
|
1587
1609
|
for(let i = 0; i < fc.processes.length; i++) {
|
@@ -1600,9 +1622,9 @@ class LinnyRModel {
|
|
1600
1622
|
fc.notes[i].x += dx;
|
1601
1623
|
fc.notes[i].y += dy;
|
1602
1624
|
}
|
1603
|
-
// NOTE: force drawing, because SVG must immediately be downloadable
|
1625
|
+
// NOTE: force drawing, because SVG must immediately be downloadable.
|
1604
1626
|
UI.drawDiagram(this);
|
1605
|
-
// If dragging, add (dx, dy) to the properties of the top "move" UndoEdit
|
1627
|
+
// If dragging, add (dx, dy) to the properties of the top "move" UndoEdit.
|
1606
1628
|
if(UI.dragged_node) UNDO_STACK.addOffset(dx, dy);
|
1607
1629
|
}
|
1608
1630
|
|
@@ -1748,8 +1770,8 @@ class LinnyRModel {
|
|
1748
1770
|
miny = Math.min(miny, obj.y - obj.height / 2);
|
1749
1771
|
}
|
1750
1772
|
}
|
1751
|
-
// Translate entire graph if some elements are above and/or left of
|
1752
|
-
// paper edge
|
1773
|
+
// Translate entire graph if some elements are above and/or left of
|
1774
|
+
// the paper edge.
|
1753
1775
|
if(minx < 0 || miny < 0) {
|
1754
1776
|
// NOTE: limit translation to 5 pixels to prevent "run-away effect"
|
1755
1777
|
this.translateGraph(Math.min(5, -minx), Math.min(5, -miny));
|
@@ -2186,8 +2208,8 @@ class LinnyRModel {
|
|
2186
2208
|
}
|
2187
2209
|
|
2188
2210
|
deleteSelection() {
|
2189
|
-
//
|
2190
|
-
// and selected links
|
2211
|
+
// Remove all selected nodes (with their associated links and constraints)
|
2212
|
+
// and selected links.
|
2191
2213
|
// NOTE: This method implements the DELETE action, and hence should be
|
2192
2214
|
// undoable. The UndoEdit is created by the calling routine; the methods
|
2193
2215
|
// that actually delete model elements append their XML to the XML attribute
|
@@ -2195,7 +2217,7 @@ class LinnyRModel {
|
|
2195
2217
|
let obj,
|
2196
2218
|
fc = this.focal_cluster;
|
2197
2219
|
// Update the documentation manager (GUI only) if selection contains the
|
2198
|
-
// current entity
|
2220
|
+
// current entity.
|
2199
2221
|
if(DOCUMENTATION_MANAGER) DOCUMENTATION_MANAGER.clearEntity(this.selection);
|
2200
2222
|
// First delete links and constraints.
|
2201
2223
|
for(let i = this.selection.length - 1; i >= 0; i--) {
|
@@ -3222,6 +3244,29 @@ class LinnyRModel {
|
|
3222
3244
|
sl.push(this.datasets[obj].displayName, this.datasets[obj].comments);
|
3223
3245
|
}
|
3224
3246
|
}
|
3247
|
+
const keys = Object.keys(this.equations_dataset.modifiers);
|
3248
|
+
sl.push('_____Equations');
|
3249
|
+
for(let i = 0; i < keys.length; i++) {
|
3250
|
+
const m = this.equations_dataset.modifiers[keys[i]];
|
3251
|
+
if(!m.selector.startsWith(':')) {
|
3252
|
+
sl.push(m.displayName, '`' + m.expression.text + '`\n');
|
3253
|
+
}
|
3254
|
+
}
|
3255
|
+
sl.push('_____Methods');
|
3256
|
+
for(let i = 0; i < keys.length; i++) {
|
3257
|
+
const m = this.equations_dataset.modifiers[keys[i]];
|
3258
|
+
if(m.selector.startsWith(':')) {
|
3259
|
+
let markup = '\n\nDoes not apply to any entity.';
|
3260
|
+
if(m.expression.eligible_prefixes) {
|
3261
|
+
const el = Object.keys(m.expression.eligible_prefixes)
|
3262
|
+
.sort(compareSelectors);
|
3263
|
+
if(el.length > 0) markup = '\n\nApplies to ' +
|
3264
|
+
pluralS(el.length, 'prefixed entity group') +
|
3265
|
+
':\n- ' + el.join('\n- ');
|
3266
|
+
}
|
3267
|
+
sl.push(m.displayName, '`' + m.expression.text + '`' + markup);
|
3268
|
+
}
|
3269
|
+
}
|
3225
3270
|
sl.push('_____Charts');
|
3226
3271
|
for(let i = 0; i < this.charts.length; i++) {
|
3227
3272
|
sl.push(this.charts[i].title, this.charts[i].comments);
|
@@ -4817,7 +4862,7 @@ class ObjectWithXYWH {
|
|
4817
4862
|
|
4818
4863
|
alignToGrid() {
|
4819
4864
|
// Align this object to the grid, and return TRUE if this involved
|
4820
|
-
// a move
|
4865
|
+
// a move.
|
4821
4866
|
const
|
4822
4867
|
ox = this.x,
|
4823
4868
|
oy = this.y,
|
@@ -6139,8 +6184,8 @@ class Cluster extends NodeBox {
|
|
6139
6184
|
}
|
6140
6185
|
|
6141
6186
|
addProductPosition(p, x=null, y=null) {
|
6142
|
-
// Add a product position for product `p` to this cluster unless such
|
6143
|
-
// already exists
|
6187
|
+
// Add a product position for product `p` to this cluster unless such
|
6188
|
+
// "pp" already exists, and then return this (new) product position.
|
6144
6189
|
let pp = this.indexOfProduct(p);
|
6145
6190
|
if(pp >= 0) {
|
6146
6191
|
pp = this.product_positions[pp];
|
@@ -6158,7 +6203,8 @@ class Cluster extends NodeBox {
|
|
6158
6203
|
}
|
6159
6204
|
|
6160
6205
|
containsProduct(p) {
|
6161
|
-
// Return the subcluster of this cluster that contains product `p`,
|
6206
|
+
// Return the subcluster of this cluster that contains product `p`,
|
6207
|
+
// or NULL if `p` does not occur in this cluster.
|
6162
6208
|
if(this.indexOfProduct(p) >= 0) return this;
|
6163
6209
|
for(let i = 0; i < this.sub_clusters.length; i++) {
|
6164
6210
|
if(this.sub_clusters[i].containsProduct(p)) {
|
@@ -8712,8 +8758,8 @@ class Dataset {
|
|
8712
8758
|
return d.join(';');
|
8713
8759
|
}
|
8714
8760
|
|
8715
|
-
// Returns a string denoting the properties of this dataset.
|
8716
8761
|
get propertiesString() {
|
8762
|
+
// Return a string denoting the properties of this dataset.
|
8717
8763
|
if(this.data.length === 0) return '';
|
8718
8764
|
let time_prop;
|
8719
8765
|
if(this.array) {
|
@@ -8724,12 +8770,12 @@ class Dataset {
|
|
8724
8770
|
DATASET_MANAGER.method_symbols[
|
8725
8771
|
DATASET_MANAGER.methods.indexOf(this.method)]].join('');
|
8726
8772
|
}
|
8727
|
-
// Circular arrow symbolizes "repeating"
|
8773
|
+
// Circular arrow symbolizes "repeating".
|
8728
8774
|
return ' (' + time_prop + (this.periodic ? ' \u21BB' : '') + ')';
|
8729
8775
|
}
|
8730
8776
|
|
8731
8777
|
unpackDataString(str) {
|
8732
|
-
//
|
8778
|
+
// Convert semicolon-separated data to a numeric array.
|
8733
8779
|
this.data.length = 0;
|
8734
8780
|
if(str) {
|
8735
8781
|
const numbers = str.split(';');
|
@@ -8742,12 +8788,12 @@ class Dataset {
|
|
8742
8788
|
}
|
8743
8789
|
|
8744
8790
|
computeVector() {
|
8745
|
-
//
|
8746
|
-
// lasting one unit on the model time scale
|
8791
|
+
// Convert data to a vector on the time scale of the model, i.e.,
|
8792
|
+
// 1 time step lasting one unit on the model time scale.
|
8747
8793
|
|
8748
|
-
// NOTE:
|
8749
|
-
//
|
8750
|
-
//
|
8794
|
+
// NOTE: A dataset can also be defined as an "array", which differs
|
8795
|
+
// from a time series in that the vector is filled with the data values
|
8796
|
+
// "as is" to permit accessing a specific value at index #.
|
8751
8797
|
if(this.array) {
|
8752
8798
|
this.vector = this.data.slice();
|
8753
8799
|
return;
|
@@ -8755,16 +8801,16 @@ class Dataset {
|
|
8755
8801
|
// Like all vectors, vector[0] corresponds to initial value, and vector[1]
|
8756
8802
|
// to the model setting "Optimize from step t=..."
|
8757
8803
|
// NOTES:
|
8758
|
-
// (1)
|
8804
|
+
// (1) The first number of a datasets time series is ALWAYS assumed to
|
8759
8805
|
// correspond to t=1, whereas the simulation may be set to start later!
|
8760
|
-
// (2)
|
8806
|
+
// (2) Model run length includes 1 look-ahead period.
|
8761
8807
|
VM.scaleDataToVector(this.data, this.vector, this.timeStepDuration,
|
8762
8808
|
MODEL.timeStepDuration, MODEL.runLength, MODEL.start_period,
|
8763
8809
|
this.defaultValue, this.periodic, this.method);
|
8764
8810
|
}
|
8765
8811
|
|
8766
8812
|
computeStatistics() {
|
8767
|
-
//
|
8813
|
+
// Compute descriptive statistics for data (NOT vector!).
|
8768
8814
|
if(this.data.length === 0) {
|
8769
8815
|
this.min = VM.UNDEFINED;
|
8770
8816
|
this.max = VM.UNDEFINED;
|
@@ -8789,18 +8835,18 @@ class Dataset {
|
|
8789
8835
|
}
|
8790
8836
|
|
8791
8837
|
get statisticsAsString() {
|
8792
|
-
//
|
8838
|
+
// Return descriptive statistics in human-readable form.
|
8793
8839
|
let s = 'N = ' + this.data.length;
|
8794
8840
|
if(N > 0) {
|
8795
|
-
s += ', range = ['
|
8796
|
-
', mean = '
|
8797
|
-
VM.sig4Dig(this.standard_deviation);
|
8841
|
+
s += [', range = [', VM.sig4Dig(this.min), ', ', VM.sig4Dig(this.max),
|
8842
|
+
'], mean = ', VM.sig4Dig(this.mean), ', s.d. = ',
|
8843
|
+
VM.sig4Dig(this.standard_deviation)].join('');
|
8798
8844
|
}
|
8799
8845
|
return s;
|
8800
8846
|
}
|
8801
8847
|
|
8802
8848
|
attributeValue(a) {
|
8803
|
-
//
|
8849
|
+
// Return the computed result for attribute `a`.
|
8804
8850
|
// NOTE: Datasets have ONE attribute (their vector) denoted by the empty
|
8805
8851
|
// string; all other "attributes" should be modifier selectors, and
|
8806
8852
|
// their value should be obtained using attributeExpression (see below).
|
@@ -8809,9 +8855,9 @@ class Dataset {
|
|
8809
8855
|
}
|
8810
8856
|
|
8811
8857
|
attributeExpression(a) {
|
8812
|
-
//
|
8858
|
+
// Return the expression for selector `a` (also considering wildcard
|
8813
8859
|
// modifiers), or NULL if no such selector exists.
|
8814
|
-
// NOTE:
|
8860
|
+
// NOTE: Selectors no longer are case-sensitive.
|
8815
8861
|
if(a) {
|
8816
8862
|
const mm = this.matchingModifiers([a]);
|
8817
8863
|
if(mm.length > 0) return mm[0].expression;
|
@@ -8822,29 +8868,29 @@ class Dataset {
|
|
8822
8868
|
get activeModifierExpression() {
|
8823
8869
|
if(MODEL.running_experiment) {
|
8824
8870
|
// If an experiment is running, check if dataset modifiers match the
|
8825
|
-
// combination of selectors for the active run
|
8871
|
+
// combination of selectors for the active run.
|
8826
8872
|
const mm = this.matchingModifiers(MODEL.running_experiment.activeCombination);
|
8827
|
-
// If so, use the first match
|
8873
|
+
// If so, use the first match.
|
8828
8874
|
if(mm.length > 0) return mm[0].expression;
|
8829
8875
|
}
|
8830
8876
|
if(this.default_selector) {
|
8831
|
-
// If no experiment (so "normal" run), use default selector if specified
|
8877
|
+
// If no experiment (so "normal" run), use default selector if specified.
|
8832
8878
|
const dm = this.modifiers[UI.nameToID(this.default_selector)];
|
8833
8879
|
if(dm) return dm.expression;
|
8834
|
-
// Exception should never occur, but check anyway and log it
|
8880
|
+
// Exception should never occur, but check anyway and log it.
|
8835
8881
|
console.log('WARNING: Dataset "' + this.name +
|
8836
8882
|
`" has no default selector "${this.default_selector}"`, this.modifiers);
|
8837
8883
|
}
|
8838
|
-
// Fall-through: return vector instead of expression
|
8884
|
+
// Fall-through: return vector instead of expression.
|
8839
8885
|
return this.vector;
|
8840
8886
|
}
|
8841
8887
|
|
8842
8888
|
addModifier(selector, node=null, ioc=null) {
|
8843
8889
|
let s = selector;
|
8844
|
-
//
|
8890
|
+
// First sanitize the selector.
|
8845
8891
|
if(this === MODEL.equations_dataset) {
|
8846
8892
|
// Equation identifiers cannot contain characters that have special
|
8847
|
-
// meaning in a variable identifier
|
8893
|
+
// meaning in a variable identifier.
|
8848
8894
|
s = s.replace(/[\*\|\[\]\{\}\@\#]/g, '');
|
8849
8895
|
if(s !== selector) {
|
8850
8896
|
UI.warn('Equation name cannot contain [, ], {, }, |, @, # or *');
|
@@ -8868,26 +8914,26 @@ class Dataset {
|
|
8868
8914
|
return null;
|
8869
8915
|
}
|
8870
8916
|
} else {
|
8871
|
-
// Prefix it when the IO context argument is defined
|
8917
|
+
// Prefix it when the IO context argument is defined.
|
8872
8918
|
if(ioc) s = ioc.actualName(s);
|
8873
8919
|
}
|
8874
|
-
// If equation already exists, return its modifier
|
8920
|
+
// If equation already exists, return its modifier.
|
8875
8921
|
const id = UI.nameToID(s);
|
8876
8922
|
if(this.modifiers.hasOwnProperty(id)) return this.modifiers[id];
|
8877
|
-
// New equation identifier must not equal some entity ID
|
8923
|
+
// New equation identifier must not equal some entity ID.
|
8878
8924
|
const obj = MODEL.objectByName(s);
|
8879
8925
|
if(obj) {
|
8880
|
-
// NOTE:
|
8926
|
+
// NOTE: Also pass selector, or warning will display dataset name.
|
8881
8927
|
UI.warningEntityExists(obj);
|
8882
8928
|
return null;
|
8883
8929
|
}
|
8884
8930
|
} else {
|
8885
8931
|
// Standard dataset modifier selectors are much more restricted, but
|
8886
|
-
// to be user-friendly, special chars are removed automatically
|
8932
|
+
// to be user-friendly, special chars are removed automatically.
|
8887
8933
|
s = s.replace(/[^a-zA-Z0-9\+\-\%\_\*\?]/g, '');
|
8888
8934
|
let msg = '';
|
8889
8935
|
if(s !== selector) msg = UI.WARNING.SELECTOR_SYNTAX;
|
8890
|
-
// A selector can only contain 1 star
|
8936
|
+
// A selector can only contain 1 star.
|
8891
8937
|
if(s.indexOf('*') !== s.lastIndexOf('*')) msg = UI.WARNING.SINGLE_WILDCARD;
|
8892
8938
|
if(msg) {
|
8893
8939
|
UI.warn(msg);
|
@@ -8898,12 +8944,12 @@ class Dataset {
|
|
8898
8944
|
UI.warn(UI.WARNING.INVALID_SELECTOR);
|
8899
8945
|
return null;
|
8900
8946
|
}
|
8901
|
-
// Then add a dataset modifier to this dataset
|
8947
|
+
// Then add a dataset modifier to this dataset.
|
8902
8948
|
const id = UI.nameToID(s);
|
8903
8949
|
if(!this.modifiers.hasOwnProperty(id)) {
|
8904
8950
|
this.modifiers[id] = new DatasetModifier(this, s);
|
8905
8951
|
}
|
8906
|
-
// Finally, initialize it when the XML node argument is defined
|
8952
|
+
// Finally, initialize it when the XML node argument is defined.
|
8907
8953
|
if(node) this.modifiers[id].initFromXML(node);
|
8908
8954
|
return this.modifiers[id];
|
8909
8955
|
}
|
@@ -8927,7 +8973,7 @@ class Dataset {
|
|
8927
8973
|
for(let i = 0; i < sl.length; i++) {
|
8928
8974
|
ml.push(this.modifiers[sl[i]].asXML);
|
8929
8975
|
}
|
8930
|
-
// NOTE: "black-boxed" datasets are stored anonymously without comments
|
8976
|
+
// NOTE: "black-boxed" datasets are stored anonymously without comments.
|
8931
8977
|
const id = UI.nameToID(n);
|
8932
8978
|
if(MODEL.black_box_entities.hasOwnProperty(id)) {
|
8933
8979
|
n = MODEL.black_box_entities[id];
|
@@ -8959,7 +9005,7 @@ class Dataset {
|
|
8959
9005
|
this.periodic = nodeParameterValue(node, 'periodic') === '1';
|
8960
9006
|
this.array = nodeParameterValue(node, 'array') === '1';
|
8961
9007
|
this.black_box = nodeParameterValue(node, 'black-box') === '1';
|
8962
|
-
// NOTE:
|
9008
|
+
// NOTE: Array-type datasets are by definition input => not an outcome.
|
8963
9009
|
if(!this.array) this.outcome = nodeParameterValue(node, 'outcome') === '1';
|
8964
9010
|
this.url = xmlDecoded(nodeContentByTag(node, 'url'));
|
8965
9011
|
if(this.url) {
|
@@ -8985,10 +9031,10 @@ class Dataset {
|
|
8985
9031
|
}
|
8986
9032
|
|
8987
9033
|
rename(name, notify=true) {
|
8988
|
-
// Change the name of this dataset
|
9034
|
+
// Change the name of this dataset.
|
8989
9035
|
// When `notify` is FALSE, notifications are suppressed while the
|
8990
|
-
// number of affected datasets and expressions are counted
|
8991
|
-
// NOTE:
|
9036
|
+
// number of affected datasets and expressions are counted.
|
9037
|
+
// NOTE: Prevent renaming the equations dataset (just in case).
|
8992
9038
|
if(this === MODEL.equations_dataset) return;
|
8993
9039
|
name = UI.cleanName(name);
|
8994
9040
|
if(!UI.validName(name)) {
|
@@ -9012,31 +9058,31 @@ class Dataset {
|
|
9012
9058
|
}
|
9013
9059
|
|
9014
9060
|
resetExpressions() {
|
9015
|
-
// Recalculate vector to adjust to model time scale and run length
|
9061
|
+
// Recalculate vector to adjust to model time scale and run length.
|
9016
9062
|
this.computeVector();
|
9017
|
-
// Reset all modifier expressions
|
9063
|
+
// Reset all modifier expressions.
|
9018
9064
|
for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
|
9019
|
-
// NOTE: "empty" expressions for modifiers default to dataset default
|
9065
|
+
// NOTE: "empty" expressions for modifiers default to dataset default.
|
9020
9066
|
this.modifiers[m].expression.reset(this.defaultValue);
|
9021
9067
|
this.modifiers[m].expression_cache = {};
|
9022
9068
|
}
|
9023
9069
|
}
|
9024
9070
|
|
9025
9071
|
compileExpressions() {
|
9026
|
-
// Recompile all modifier expressions
|
9072
|
+
// Recompile all modifier expressions.
|
9027
9073
|
for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
|
9028
9074
|
this.modifiers[m].expression.compile();
|
9029
9075
|
}
|
9030
9076
|
}
|
9031
9077
|
|
9032
9078
|
differences(ds) {
|
9033
|
-
// Return "dictionary" of differences, or NULL if none
|
9079
|
+
// Return "dictionary" of differences, or NULL if none.
|
9034
9080
|
const d = differences(this, ds, UI.MC.DATASET_PROPS);
|
9035
|
-
// Check for differences in data
|
9081
|
+
// Check for differences in data.
|
9036
9082
|
if(this.dataString !== ds.dataString) {
|
9037
9083
|
d.data = {A: this.statisticsAsString, B: ds.statisticsAsString};
|
9038
9084
|
}
|
9039
|
-
// Check for differences in modifiers
|
9085
|
+
// Check for differences in modifiers.
|
9040
9086
|
const mdiff = {};
|
9041
9087
|
for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
|
9042
9088
|
const
|
@@ -9055,7 +9101,7 @@ class Dataset {
|
|
9055
9101
|
mdiff[m] = [UI.MC.DELETED, dsm.selector, dsm.expression.text];
|
9056
9102
|
}
|
9057
9103
|
}
|
9058
|
-
// Only add modifiers property if differences were detected
|
9104
|
+
// Only add modifiers property if differences were detected.
|
9059
9105
|
if(Object.keys(mdiff).length > 0) d.modifiers = mdiff;
|
9060
9106
|
if(Object.keys(d).length > 0) return d;
|
9061
9107
|
return null;
|
@@ -9064,7 +9110,7 @@ class Dataset {
|
|
9064
9110
|
} // END of class Dataset
|
9065
9111
|
|
9066
9112
|
|
9067
|
-
// CLASS ChartVariable defines properties of chart time series
|
9113
|
+
// CLASS ChartVariable defines properties of chart time series.
|
9068
9114
|
class ChartVariable {
|
9069
9115
|
constructor(c) {
|
9070
9116
|
this.chart = c;
|
@@ -743,17 +743,28 @@ function xmlDecoded(str) {
|
|
743
743
|
}
|
744
744
|
|
745
745
|
function customizeXML(str) {
|
746
|
-
// NOTE:
|
746
|
+
// NOTE: This function can be customized to pre-process a model file,
|
747
747
|
// for example to rename entities in one go -- USE WITH CARE!
|
748
|
-
//
|
749
|
-
|
748
|
+
// To prevent unintended customization, check whether the model name
|
749
|
+
// ends with "!!CUSTOMIZE". This check ensures that the modeler must
|
750
|
+
// first save the model with this text as the (end of the) model name
|
751
|
+
// and then load it again for the customization to be performed.
|
752
|
+
if(str.indexOf('!!CUSTOMIZE</name><author>') >= 0) {
|
753
|
+
// Modify `str` -- by default, do nothing, but typical modifications
|
754
|
+
// will replace RexEx patterns by other strings.
|
755
|
+
|
750
756
|
/*
|
751
|
-
|
752
|
-
|
753
|
-
|
757
|
+
const
|
758
|
+
re = /xyz/gi,
|
759
|
+
r = 'abc';
|
754
760
|
*/
|
755
761
|
|
756
|
-
|
762
|
+
// Trace the changes to the console.
|
763
|
+
console.log('Customizing:', re, r);
|
764
|
+
console.log('Matches:', str.match(re));
|
765
|
+
str = str.replace(re, r);
|
766
|
+
}
|
767
|
+
// Finally, return the modified string.
|
757
768
|
return str;
|
758
769
|
}
|
759
770
|
|
@@ -4319,7 +4319,7 @@ class VirtualMachine {
|
|
4319
4319
|
// `cbl` is the cropped block length (applies only to last block).
|
4320
4320
|
let bb = (block - 1) * MODEL.block_length + 1,
|
4321
4321
|
abl = this.chunk_length,
|
4322
|
-
cbl = this.actualBlockLength;
|
4322
|
+
cbl = this.actualBlockLength(block);
|
4323
4323
|
// For the last block, crop the actual block length so it does not
|
4324
4324
|
// extend beyond the simulation period (these results should be ignored).
|
4325
4325
|
// If no results computed, preserve those already computed for the
|
@@ -4761,7 +4761,7 @@ class VirtualMachine {
|
|
4761
4761
|
|
4762
4762
|
setupBlock() {
|
4763
4763
|
if(DEBUGGING) this.logCode();
|
4764
|
-
const abl = this.actualBlockLength;
|
4764
|
+
const abl = this.actualBlockLength(this.block_count);
|
4765
4765
|
// NOTE: Tableau segment length is the number of time steps between
|
4766
4766
|
// updates of the progress needle. The default progress needle interval
|
4767
4767
|
// is calibrated for 1000 VMI instructions.
|
@@ -4955,12 +4955,12 @@ class VirtualMachine {
|
|
4955
4955
|
setTimeout(() => VM.solveBlock(), 0);
|
4956
4956
|
}
|
4957
4957
|
|
4958
|
-
|
4958
|
+
actualBlockLength(block) {
|
4959
4959
|
// The actual block length is the number of time steps to be considered
|
4960
4960
|
// by the solver; the abl of the last block is likely to be shorter
|
4961
4961
|
// than the standard, as it should not go beyond the end time plus
|
4962
4962
|
// look-ahead.
|
4963
|
-
if(
|
4963
|
+
if(block < this.nr_of_blocks) return this.chunk_length;
|
4964
4964
|
// Last block length equals remainder of simulation period divided
|
4965
4965
|
// by block length.
|
4966
4966
|
let rem = (MODEL.runLength - MODEL.look_ahead) % MODEL.block_length;
|
@@ -5005,7 +5005,7 @@ class VirtualMachine {
|
|
5005
5005
|
// behavior can still be generated by limiting time series length to
|
5006
5006
|
// the simulation period.
|
5007
5007
|
const
|
5008
|
-
abl = this.actualBlockLength,
|
5008
|
+
abl = this.actualBlockLength(this.block_count),
|
5009
5009
|
// Get the number digits for variable names
|
5010
5010
|
z = this.columnsInBlock.toString().length,
|
5011
5011
|
// LP_solve uses semicolon as separator between equations
|
@@ -5240,7 +5240,7 @@ class VirtualMachine {
|
|
5240
5240
|
// instead of row-based, hence for each column a separate string list.
|
5241
5241
|
// NOTE: Columns are numbered from 1 to N, hence a dummy list for c=0.
|
5242
5242
|
const
|
5243
|
-
abl = this.actualBlockLength,
|
5243
|
+
abl = this.actualBlockLength(this.block_count),
|
5244
5244
|
cols = [[]],
|
5245
5245
|
rhs = [];
|
5246
5246
|
let nrow = this.matrix.length,
|
@@ -5248,7 +5248,7 @@ class VirtualMachine {
|
|
5248
5248
|
c,
|
5249
5249
|
p,
|
5250
5250
|
r;
|
5251
|
-
|
5251
|
+
this.numeric_issue = '';
|
5252
5252
|
this.lines = '';
|
5253
5253
|
for(c = 1; c <= ncol; c++) cols.push([]);
|
5254
5254
|
this.decimals = Math.max(nrow, ncol).toString().length;
|
@@ -5422,7 +5422,7 @@ class VirtualMachine {
|
|
5422
5422
|
// Add the SOS section.
|
5423
5423
|
if(this.sos_var_indices.length > 0) {
|
5424
5424
|
this.lines += 'SOS\n';
|
5425
|
-
const abl = this.actualBlockLength;
|
5425
|
+
const abl = this.actualBlockLength(this.block_count);
|
5426
5426
|
let sos = 1;
|
5427
5427
|
for(let j = 0; j < abl; j++) {
|
5428
5428
|
for(let i = 0; i < this.sos_var_indices.length; i++) {
|
@@ -5594,7 +5594,7 @@ Solver status = ${json.status}`);
|
|
5594
5594
|
const
|
5595
5595
|
bwr = this.blockWithRound,
|
5596
5596
|
fromt = (this.block_count - 1) * MODEL.block_length + 1,
|
5597
|
-
abl = this.actualBlockLength;
|
5597
|
+
abl = this.actualBlockLength(this.block_count);
|
5598
5598
|
MONITOR.updateBlockNumber(bwr);
|
5599
5599
|
// NOTE: Add blank line to message to visually separate rounds.
|
5600
5600
|
this.logMessage(this.block_count, ['\nSetting up block #', bwr,
|
@@ -5626,7 +5626,8 @@ Solver status = ${json.status}`);
|
|
5626
5626
|
}
|
5627
5627
|
// Generate lines of code in format that should be accepted by solver.
|
5628
5628
|
if(this.solver_name === 'gurobi') {
|
5629
|
-
this.writeMPSFormat();
|
5629
|
+
//this.writeMPSFormat();
|
5630
|
+
this.writeLpFormat(true);
|
5630
5631
|
} else if(this.solver_name === 'scip' || this.solver_name === 'cplex') {
|
5631
5632
|
// NOTE: The CPLEX LP format that is also used by SCIP differs from
|
5632
5633
|
// the LP_solve format that was used by the first versions of Linny-R.
|