linny-r 1.6.0 → 1.6.2

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.2",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -1521,7 +1521,7 @@ function connectionErrorText(msg) {
1521
1521
  //
1522
1522
 
1523
1523
  function commandLineSettings() {
1524
- // Sets default settings, and then checks the command line arguments
1524
+ // Sets default settings, and then checks the command line arguments.
1525
1525
  const settings = {
1526
1526
  cli_name: (PLATFORM.startsWith('win') ? 'Command Prompt' : 'Terminal'),
1527
1527
  inkscape: '',
@@ -1533,6 +1533,22 @@ function commandLineSettings() {
1533
1533
  solver_path: '',
1534
1534
  user_dir: path.join(WORKING_DIRECTORY, 'user')
1535
1535
  };
1536
+ const
1537
+ cmd = process.argv[0],
1538
+ app = (cmd.endsWith('node.exe') ? 'node' : 'linny-r'),
1539
+ usage = `Usage: ${app} server [options]
1540
+
1541
+ Possible options are:
1542
+ dpi=[number] will make InkScape render SVGs in the specified resolution
1543
+ help will display these command line options
1544
+ launch will open the Linny-R GUI in a browser window
1545
+ port=[number] will listen at the specified port number
1546
+ (default is 5050; number must be unique for each server)
1547
+ solver=[name] will select solver [name], or warn if not found
1548
+ (name choices: Gurobi, CPLEX, SCIP or LP_solve)
1549
+ verbose will output solver messages to the console
1550
+ workspace=[path] will create workspace in [path] instead of (Linny-R)/user
1551
+ `;
1536
1552
  for(let i = 2; i < process.argv.length; i++) {
1537
1553
  const lca = process.argv[i].toLowerCase();
1538
1554
  if(lca === 'launch') {
@@ -1541,7 +1557,7 @@ function commandLineSettings() {
1541
1557
  const av = lca.split('=');
1542
1558
  if(av.length === 1) av.push('');
1543
1559
  if(av[0] === 'port') {
1544
- // Accept any number greater than or equal to 1024
1560
+ // Accept any number greater than or equal to 1024.
1545
1561
  const n = parseInt(av[1]);
1546
1562
  if(isNaN(n) || n < 1024) {
1547
1563
  console.log(`WARNING: Invalid port number ${av[1]}`);
@@ -1555,7 +1571,7 @@ function commandLineSettings() {
1555
1571
  settings.preferred_solver = av[1];
1556
1572
  }
1557
1573
  } else if(av[0] === 'dpi') {
1558
- // Accept any number greater than or equal to 1024
1574
+ // Accept any number greater than or equal to 1024.
1559
1575
  const n = parseInt(av[1]);
1560
1576
  if(isNaN(n) || n > 1200) {
1561
1577
  console.log(`WARNING: Invalid resolution ${av[1]} (max. 1200 dpi)`);
@@ -1563,7 +1579,7 @@ function commandLineSettings() {
1563
1579
  settings.dpi = n;
1564
1580
  }
1565
1581
  } else if(av[0] === 'workspace') {
1566
- // User directory must be READ/WRITE-accessible
1582
+ // User directory must be READ/WRITE-accessible.
1567
1583
  try {
1568
1584
  fs.accessSync(av[1], fs.constants.R_OK | fs.constants.W_O);
1569
1585
  } catch(err) {
@@ -1571,10 +1587,15 @@ function commandLineSettings() {
1571
1587
  process.exit();
1572
1588
  }
1573
1589
  settings.user_dir = av[1];
1590
+ } else if(av[0] === 'help') {
1591
+ // Print command line options.
1592
+ console.log(usage);
1593
+ process.exit();
1574
1594
  } else {
1575
- // Terminate script
1595
+ // Terminate script.
1576
1596
  console.log(
1577
- `ERROR: Invalid command line argument "${process.argv[i]}"`);
1597
+ `ERROR: Invalid command line argument "${process.argv[i]}"\n`);
1598
+ console.log(usage);
1578
1599
  process.exit();
1579
1600
  }
1580
1601
  }
@@ -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
  }
@@ -356,7 +356,11 @@ class GUIExperimentManager extends ExperimentManager {
356
356
  dim_count.innerHTML = pluralS(x.available_dimensions.length,
357
357
  'more dimension');
358
358
  x.inferActualDimensions();
359
+ for(let i = 0; i < x.actual_dimensions.length; i++) {
360
+ x.actual_dimensions[i].sort(compareSelectors);
361
+ }
359
362
  x.inferCombinations();
363
+ //x.combinations.sort(compareCombinations);
360
364
  combi_count.innerHTML = pluralS(x.combinations.length, 'combination');
361
365
  if(x.combinations.length === 0) canview = false;
362
366
  header.innerHTML = x.title;
@@ -850,6 +854,9 @@ class GUIExperimentManager extends ExperimentManager {
850
854
  }
851
855
  // Get the selected statistic for each run so as to get an array of numbers
852
856
  const data = [];
857
+ // Set reference column indices so that values for the reference|
858
+ // configuration can be displayed in orange.
859
+ const ref_conf_indices = [];
853
860
  for(let i = 0; i < x.runs.length; i++) {
854
861
  const
855
862
  r = x.runs[i],
@@ -878,7 +885,7 @@ class GUIExperimentManager extends ExperimentManager {
878
885
  data.push(rr.last);
879
886
  }
880
887
  }
881
- // Scale data as selected
888
+ // Scale data as selected.
882
889
  const scaled = data.slice();
883
890
  // NOTE: scale only after the experiment has been completed AND
884
891
  // configurations have been defined (otherwise comparison is pointless)
@@ -896,7 +903,9 @@ class GUIExperimentManager extends ExperimentManager {
896
903
  }
897
904
  // Set difference for reference configuration itself to 0
898
905
  for(let i = 0; i < n; i++) {
899
- scaled[rc * n + i] = 0;
906
+ const index = rc * n + i;
907
+ scaled[index] = 0;
908
+ ref_conf_indices.push(index);
900
909
  }
901
910
  } else if(x.selected_scale === 'reg') {
902
911
  // Compute regret: current config - high value config in same scenario
@@ -934,13 +943,15 @@ class GUIExperimentManager extends ExperimentManager {
934
943
  formatted.push(VM.sig4Dig(scaled[i]));
935
944
  }
936
945
  uniformDecimals(formatted);
937
- // Display formatted data in cells
946
+ // Display formatted data in cells.
938
947
  for(let i = 0; i < x.combinations.length; i++) {
939
948
  const cell = document.getElementById('xr' + i);
940
949
  if(i < x.runs.length) {
941
950
  cell.innerHTML = formatted[i];
942
951
  cell.classList.remove('not-run');
943
952
  cell.style.backgroundColor = this.color_scale.rgb(normalized[i]);
953
+ cell.style.color = (ref_conf_indices.indexOf(i) >= 0 ?
954
+ 'orange' : 'black');
944
955
  const
945
956
  r = x.runs[i],
946
957
  rr = r.results[rri],
@@ -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(\'',