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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -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 the
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
- // Add text inputs for selected cluster nodes
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
- // Add selectors for unresolved FROM/TO nodes
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
- // Updates the paste mapping as specified by the modeler and then
3047
- // proceeds to paste
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
- // Returns full entity name inferred from XML node data
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
- // Returns tuple [entity name, actor name] if `name` ends with
3138
- // a parenthesized string that identifies an actor in the selection
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: links and constraints require two mappings (recursion!)
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(MODEL.objectByName(mn) || (name_map[fn] && name_map[fn] !== mn)) {
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: do not add if an entity having this type and mapped name
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: selected products must be positioned
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: permit documentation of the model by raising the dialog
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 = 'Method <tt>' +
351
- e.selector + '</tt> applies to ' +
352
- pluralS(el.length, 'prefixed entity group').toLowerCase() +
353
- ':<ul><li>' + el.join('</li><li>') + '</li></ul>';
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 pattern
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: add it only if it contains text
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 arrow
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: equation names may contain spaces; if so, reduce to single space
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 = '.mps';
55
- this.user_model = path.join(workspace.solver_output, 'usr_model.mps');
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
- // (1) Sort on variable name
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
- x_values.push(sol.Vars[i].X);
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) UI.drawDiagram(this);
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 the
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
- // Removes all selected nodes (with their associated links and constraints)
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 pp
6143
- // already exists; then return this (new) product position
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`, or null
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 '&nbsp;(' + time_prop + (this.periodic ? '&nbsp;\u21BB' : '') + ')';
8729
8775
  }
8730
8776
 
8731
8777
  unpackDataString(str) {
8732
- // Converts semicolon-separated data to a numeric array.
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
- // Converts data to a vector on the model's time scale, i.e., 1 time step
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: since 9 October 2021, a dataset can also be defined as an "array",
8749
- // which differs from a time series in that the vector is filled with the
8750
- // data values "as is" to permit accessing a specific value at index #
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) the first number of a datasets time series is ALWAYS assumed to
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) model run length includes 1 look-ahead period
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
- // Computes descriptive statistics for data (NOT vector!).
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
- // Returns descriptive statistics in human-readable form
8838
+ // Return descriptive statistics in human-readable form.
8793
8839
  let s = 'N = ' + this.data.length;
8794
8840
  if(N > 0) {
8795
- s += ', range = [' + VM.sig4Dig(this.min) + ', ' + VM.sig4Dig(this.max) +
8796
- ', mean = ' + VM.sig4Dig(this.mean) + ', s.d. = ' +
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
- // Returns the computed result for attribute `a`.
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
- // Returns expression for selector `a` (also considering wildcard
8858
+ // Return the expression for selector `a` (also considering wildcard
8813
8859
  // modifiers), or NULL if no such selector exists.
8814
- // NOTE: selectors no longer are case-sensitive.
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
- // Firstly, sanitize the selector
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: also pass selector, or warning will display dataset name
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: array-type datasets are by definition input => not an outcome
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: prevent renaming the equations dataset (just in case...)
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: this function can be customized to pre-process a model file,
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
- // First modify `str` -- by default, do nothing
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
- if(str.indexOf('<version>1.4.') >= 0) {
752
- str = str.replace(/<url>NL\/(\w+)\.csv<\/url>/g, '<url></url>');
753
- }
757
+ const
758
+ re = /xyz/gi,
759
+ r = 'abc';
754
760
  */
755
761
 
756
- // Finally, return the modified string
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
- get actualBlockLength() {
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(this.block_count < this.nr_of_blocks) return this.chunk_length;
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
- this.numeric_issue = '';
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.