linny-r 2.0.9 → 2.0.11

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.
@@ -501,6 +501,30 @@ class LinnyRModel {
501
501
  return this.namedObjectByID(UI.nameToID(name));
502
502
  }
503
503
 
504
+ validVariable(name) {
505
+ // Return TRUE if `name` references an entity plus valid attribute.
506
+ const
507
+ ea = name.split('|'),
508
+ en = ea[0].trim(),
509
+ e = this.objectByName(en);
510
+ if(!e) return `Unknown model entity "${en}"`;
511
+ const
512
+ ao = ea[1].split('@'),
513
+ a = ao[0].trim();
514
+ // Valid if no attribute, as all entity types have a default attribute.
515
+ if(!a) return true;
516
+ // Attribute should be valid for the entity type.
517
+ const
518
+ et = e.type.toLowerCase(),
519
+ ac = VM.attribute_codes[VM.entity_letter_codes[et]];
520
+ if(ac.indexOf(a) >= 0 || (e instanceof Cluster && a.startsWith('=')) ||
521
+ (e instanceof Dataset && e.modifiers.hasOwnProperty(a.toLowerCase()))) {
522
+ return true;
523
+ }
524
+ if(e instanceof Dataset) return `Dataset ${e.displayName} has no modifier "${a}"`;
525
+ return `Invalid attribute "${a}"`;
526
+ }
527
+
504
528
  setByType(type) {
505
529
  // Return a "dictionary" object with entities of the specified types
506
530
  if(type === 'Process') return this.processes;
@@ -856,8 +880,10 @@ class LinnyRModel {
856
880
 
857
881
  indexOfChart(t) {
858
882
  // Return the index of a chart having title `t` in the model's chart list.
883
+ // NOTE: Titles should not be case-sensitive.
884
+ t = t.toLowerCase();
859
885
  for(let index = 0; index < this.charts.length; index++) {
860
- if(this.charts[index].title === t) return index;
886
+ if(this.charts[index].title.toLowerCase() === t) return index;
861
887
  }
862
888
  return -1;
863
889
  }
@@ -865,8 +891,11 @@ class LinnyRModel {
865
891
  indexOfExperiment(t) {
866
892
  // Return the index of an experiment having title `t` in the model's
867
893
  // experiment list.
894
+ // NOTE: Titles should not be case-sensitive.
895
+ t = t.toLowerCase();
868
896
  for(let index = 0; index < this.experiments.length; index++) {
869
- if(this.experiments[index].title === t) return index;
897
+ // NOTE: Use nameToID to
898
+ if(this.experiments[index].title.toLowerCase() === t) return index;
870
899
  }
871
900
  return -1;
872
901
  }
@@ -1078,49 +1107,63 @@ class LinnyRModel {
1078
1107
  return ss.length > 0;
1079
1108
  }
1080
1109
 
1081
- renamePrefixedDatasets(old_prefix, new_prefix) {
1110
+ renamePrefixedDatasets(old_prefix, new_prefix, subset=null) {
1082
1111
  // Rename all datasets having the specified old prefix so that they
1083
1112
  // have the specified new prefix UNLESS this would cause name conflicts.
1113
+ // NOTE: If `subset` is defined, limit renaming to the datasets it contains.
1084
1114
  const
1085
1115
  oldkey = old_prefix.toLowerCase().split(UI.PREFIXER).join(':_'),
1086
1116
  newkey = new_prefix.toLowerCase().split(UI.PREFIXER).join(':_'),
1087
- dsl = [];
1117
+ dskl = [];
1088
1118
  // No change if new prefix is identical to old prefix.
1089
1119
  if(old_prefix !== new_prefix) {
1090
1120
  for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
1091
- if(k.startsWith(oldkey)) dsl.push(k);
1121
+ if(k.startsWith(oldkey) &&
1122
+ (!subset || subset.indexOf(MODEL.datasets[k]) >= 0)) dskl.push(k);
1092
1123
  }
1093
1124
  // NOTE: No check for name conflicts needed when name change is
1094
1125
  // merely some upper/lower case change.
1095
1126
  if(newkey !== oldkey) {
1096
1127
  let nc = 0;
1097
- for(const ds of dsl) {
1098
- const nk = newkey + ds.substring(oldkey.length);
1128
+ for(const k of dskl) {
1129
+ const nk = newkey + k.substring(oldkey.length);
1099
1130
  if(MODEL.datasets[nk]) nc++;
1100
1131
  }
1101
1132
  if(nc) {
1102
- UI.warn('Renaming ' + pluralS(dsl.length, 'dataset') +
1133
+ UI.warn('Renaming ' + pluralS(dskl.length, 'dataset') +
1103
1134
  ' would cause ' + pluralS(nc, 'name conflict'));
1104
1135
  return false;
1105
1136
  }
1106
1137
  }
1107
1138
  // Reset counts of effects of a rename operation.
1108
- this.entity_count = 0;
1139
+ this.variable_count = 0;
1109
1140
  this.expression_count = 0;
1110
1141
  // Rename datasets one by one, suppressing notifications.
1111
- for(const ds of dsl) {
1112
- const d = MODEL.datasets[ds];
1113
- d.rename(d.displayName.replace(old_prefix, new_prefix), false);
1114
- }
1115
- let msg = 'Renamed ' + pluralS(dsl.length, 'dataset').toLowerCase();
1116
- if(MODEL.variable_count) msg += ', and updated ' +
1117
- pluralS(MODEL.variable_count, 'variable') + ' in ' +
1118
- pluralS(MODEL.expression_count, 'expression');
1119
- UI.notify(msg);
1120
- if(EXPERIMENT_MANAGER.selected_experiment) {
1121
- EXPERIMENT_MANAGER.selected_experiment.inferVariables();
1142
+ // NOTE: Make a list of renamed datasets.
1143
+ const rdsl = [];
1144
+ for(const k of dskl) {
1145
+ const
1146
+ ds = this.datasets[k],
1147
+ // NOTE: When old prefix is empty string, add instead of replace.
1148
+ nn = (old_prefix ? ds.displayName.replace(old_prefix, new_prefix) :
1149
+ new_prefix + ds.displayName);
1150
+ rdsl.push(ds.rename(nn, false));
1151
+ }
1152
+ if(subset) {
1153
+ // Update the specified subset so it contains the renamed datasets.
1154
+ subset.length = 0;
1155
+ subset.push(...rdsl);
1156
+ } else {
1157
+ let msg = 'Renamed ' + pluralS(dskl.length, 'dataset').toLowerCase();
1158
+ if(this.variable_count) msg += ', and updated ' +
1159
+ pluralS(this.variable_count, 'variable') + ' in ' +
1160
+ pluralS(this.expression_count, 'expression');
1161
+ UI.notify(msg);
1162
+ if(EXPERIMENT_MANAGER.selected_experiment) {
1163
+ EXPERIMENT_MANAGER.selected_experiment.inferVariables();
1164
+ }
1165
+ UI.updateControllerDialogs('CDEFJX');
1122
1166
  }
1123
- UI.updateControllerDialogs('CDEFJX');
1124
1167
  }
1125
1168
  return true;
1126
1169
  }
@@ -4351,22 +4394,24 @@ class IOBinding {
4351
4394
  datastyle = (this.is_data ?
4352
4395
  '; text-decoration: 1.5px dashed underline' : '');
4353
4396
  let html = ['<tr class="', ioc[this.io_type], '-param">',
4354
- '<td style="padding-bottom:2px">',
4355
- '<span style="font-style:normal; font-weight:normal', datastyle, '">',
4397
+ '<td style="padding-bottom: 2px">',
4398
+ '<span style="font-style: normal; font-weight: normal', datastyle, '">',
4356
4399
  this.entity_type, ':</span> ', this.name_in_module].join('');
4357
4400
  if(this.io_type === 1) {
4358
4401
  // An IMPORT binding generates two rows: the formal name (in the module)
4359
- // and the actual name (in the current model) as dropdown box
4360
- // NOTE: the first (default) option is the *prefixed* formal name, which
4361
- // means that the parameter is not bound to an entity in the current model
4402
+ // and the actual name (in the current model) as dropdown box.
4403
+ // NOTE: The first (default) option is the *prefixed* formal name, which
4404
+ // means that the parameter is not bound to an entity in the current model.
4362
4405
  html += ['<br>&rdca;<select id="', this.id, '" name="', this.id,
4363
- '" class="i-param"><option value="_CLUSTER">Cluster: ',
4406
+ '" class="i-param"><option value="_CLUSTER" style="color: purple">Cluster: ',
4364
4407
  this.name_in_module, '</option>'].join('');
4365
4408
  const
4366
4409
  s = MODEL.setByType(this.entity_type),
4367
- index = Object.keys(s).sort();
4410
+ tail = ':_' + this.name_in_module.toLowerCase(),
4411
+ index = Object.keys(s).sort(
4412
+ (a, b) => compareTailFirst(a, b, tail));
4368
4413
  if(s === MODEL.datasets) {
4369
- // NOTE: do not list the model equations as dataset
4414
+ // NOTE: Do not list the model equations as dataset.
4370
4415
  const i = index.indexOf(UI.EQUATIONS_DATASET_ID);
4371
4416
  if(i >= 0) index.splice(i, 1);
4372
4417
  }
@@ -4448,12 +4493,14 @@ class IOContext {
4448
4493
  }
4449
4494
 
4450
4495
  actualName(n, an='') {
4451
- // Returns the actual name for a parameter with formal name `n`
4496
+ // Return the actual name for a parameter with formal name `n`
4452
4497
  // (and for processes and clusters: with actor name `an` if specified and
4453
- // not "(no actor)")
4454
- // NOTE: do not modify (no actor), nor the "dataset dot"
4498
+ // not "(no actor)").
4499
+ // NOTE: Do not modify (no actor), nor the "dataset dot".
4455
4500
  if(n === UI.NO_ACTOR || n === '.') return n;
4456
- // NOTE: the top cluster of the included model has the prefix as its name
4501
+ // NOTE: Do not modify "prefix-relative" variables.
4502
+ if(n.startsWith(':')) return n;
4503
+ // NOTE: The top cluster of the included model has the prefix as its name.
4457
4504
  if(n === UI.TOP_CLUSTER_NAME || n === UI.FORMER_TOP_CLUSTER_NAME) {
4458
4505
  return this.prefix;
4459
4506
  }
@@ -4470,7 +4517,7 @@ class IOContext {
4470
4517
  return n;
4471
4518
  }
4472
4519
  // All other entities are prefixed
4473
- return (this.prefix ? this.prefix + ': ' : '') + n;
4520
+ return (this.prefix ? this.prefix + UI.PREFIXER : '') + n;
4474
4521
  }
4475
4522
 
4476
4523
  get clusterName() {
@@ -4562,19 +4609,19 @@ class IOContext {
4562
4609
  }
4563
4610
 
4564
4611
  supersede(obj) {
4565
- // Logs that entity `obj` is superseded, i.e., that this entity already
4612
+ // Log that entity `obj` is superseded, i.e., that this entity already
4566
4613
  // exists in the current model, and is initialized anew from the XML of
4567
4614
  // the model that is being included. The log is shown to modeler afterwards.
4568
4615
  addDistinct(obj.type + UI.PREFIXER + obj.displayName, this.superseded);
4569
4616
  }
4570
4617
 
4571
4618
  rewrite(x, n1='', n2='') {
4572
- // Replaces entity names of variables used in expression `x` by their
4573
- // actual name after inclusion
4574
- // NOTE: when strings `n1` and `n2` are passed, replace entity name `n1`
4619
+ // Replace entity names of variables used in expression `x` by their
4620
+ // actual name after inclusion.
4621
+ // NOTE: When strings `n1` and `n2` are passed, replace entity name `n1`
4575
4622
  // by `n2` in all variables (this is not IO-related, but used when the
4576
- // modeler renames an entity)
4577
- // NOTE: nothing to do if expression contains no variables
4623
+ // modeler renames an entity).
4624
+ // NOTE: Nothing to do if expression contains no variables.
4578
4625
  if(x.text.indexOf('[') < 0) return;
4579
4626
  const rcnt = this.replace_count;
4580
4627
  let s = '',
@@ -4588,28 +4635,28 @@ class IOContext {
4588
4635
  while(true) {
4589
4636
  p = x.text.indexOf('[', q + 1);
4590
4637
  if(p < 0) {
4591
- // No more '[' => add remaining part of text, and quit
4638
+ // No more '[' => add remaining part of text, and quit.
4592
4639
  s += x.text.slice(q + 1);
4593
4640
  break;
4594
4641
  }
4595
- // Add part from last ']' up to new '['
4642
+ // Add part from last ']' up to new '['.
4596
4643
  s += x.text.slice(q + 1, p);
4597
4644
  // Find next ']'
4598
4645
  q = indexOfMatchingBracket(x.text, p);
4599
- // Get the bracketed text (without brackets)
4646
+ // Get the bracketed text (without brackets).
4600
4647
  ss = x.text.slice(p + 1, q);
4601
- // Separate into variable and attribute + offset string (if any)
4648
+ // Separate into variable and attribute + offset string (if any).
4602
4649
  vb = ss.lastIndexOf('|');
4603
4650
  if(vb >= 0) {
4604
4651
  v = ss.slice(0, vb);
4605
- // NOTE: attribute string includes the vertical bar '|'
4652
+ // NOTE: Attribute string includes the vertical bar '|'.
4606
4653
  a = ss.slice(vb);
4607
4654
  } else {
4608
- // Separate into variable and offset string (if any)
4655
+ // Separate into variable and offset string (if any).
4609
4656
  vb = ss.lastIndexOf('@');
4610
4657
  if(vb >= 0) {
4611
4658
  v = ss.slice(0, vb);
4612
- // NOTE: attribute string includes the "at" sign '@'
4659
+ // NOTE: Attribute string includes the "at" sign '@'.
4613
4660
  a = ss.slice(vb);
4614
4661
  } else {
4615
4662
  v = ss;
@@ -4631,12 +4678,13 @@ class IOContext {
4631
4678
  brace = '';
4632
4679
  }
4633
4680
  }
4634
- // NOTE: patterns used to compute statistics must not be rewritten
4681
+ // NOTE: Patterns used to compute statistics must not be rewritten.
4635
4682
  let doit = true;
4636
4683
  stat = v.split('$');
4637
4684
  if(stat.length > 1 && VM.statistic_operators.indexOf(stat[0]) >= 0) {
4638
4685
  if(brace) {
4639
- // NOTE: this does not hold for statistics for experiment outcomes
4686
+ // NOTE: This does not hold for statistics for experiment outcomes,
4687
+ // because there no patterns but actual names are used.
4640
4688
  brace += stat[0] + '$';
4641
4689
  v = stat.slice(1).join('$');
4642
4690
  } else {
@@ -4644,21 +4692,21 @@ class IOContext {
4644
4692
  }
4645
4693
  }
4646
4694
  if(doit) {
4647
- // NOTE: when `n1` and `n2` have been specified, compare `v` with `n1`,
4648
- // and if matching, replace it by `n2`
4695
+ // NOTE: When `n1` and `n2` have been specified, compare `v` with `n1`,
4696
+ // and if matching, replace it by `n2`.
4649
4697
  if(n1 && n2) {
4650
4698
  // NOTE: UI.replaceEntity handles link names by replacing either the
4651
- // FROM or TO node name if it matches with `n1`
4699
+ // FROM or TO node name if it matches with `n1`.
4652
4700
  const r = UI.replaceEntity(v, n1, n2);
4653
- // Only replace `v` by `r` in case of a match
4701
+ // Only replace `v` by `r` in case of a match.
4654
4702
  if(r) {
4655
4703
  this.replace_count++;
4656
4704
  v = r;
4657
4705
  }
4658
4706
  } else {
4659
4707
  // When `n1` and `n2` are NOT specified, rewrite the variable
4660
- // using the parameter bindings
4661
- // NOTE: link variables contain TWO entity names
4708
+ // using the parameter bindings.
4709
+ // NOTE: Link variables contain TWO entity names.
4662
4710
  if(v.indexOf(UI.LINK_ARROW) >= 0) {
4663
4711
  const ln = v.split(UI.LINK_ARROW);
4664
4712
  v = this.actualName(ln[0]) + UI.LINK_ARROW + this.actualName(ln[1]);
@@ -4667,24 +4715,24 @@ class IOContext {
4667
4715
  }
4668
4716
  }
4669
4717
  }
4670
- // Add [actual name|attribute string] while preserving "by reference"
4718
+ // Add [actual name|attribute string] while preserving "by reference".
4671
4719
  s += `[${brace}${by_ref}${v}${a}]`;
4672
4720
  }
4673
- // Increase expression count when 1 or more variables were replaced
4721
+ // Increase expression count when 1 or more variables were replaced.
4674
4722
  if(this.replace_count > rcnt) this.expression_count++;
4675
- // Replace the original expression by the new one
4723
+ // Replace the original expression by the new one.
4676
4724
  x.text = s;
4677
- // Force expression to recompile
4725
+ // Force expression to recompile.
4678
4726
  x.code = null;
4679
4727
  }
4680
4728
 
4681
4729
  addedNode(node) {
4682
- // Record that node was added
4730
+ // Record that node was added.
4683
4731
  this.added_nodes.push(node);
4684
4732
  }
4685
4733
 
4686
4734
  addedLink(link) {
4687
- // Record that link was added
4735
+ // Record that link was added.
4688
4736
  this.added_links.push(link);
4689
4737
  }
4690
4738
 
@@ -9006,16 +9054,16 @@ class Dataset {
9006
9054
  this.black_box = false;
9007
9055
  this.outcome = false;
9008
9056
  this.parent_anchor = 0;
9009
- // URL indicates that data must be read from external source
9057
+ // URL indicates that data must be read from external source.
9010
9058
  this.url = '';
9011
9059
  // Array `data` will contain modeler-defined values, starting at *dataset*
9012
- // time step t = 1
9060
+ // time step t = 1.
9013
9061
  this.data = [];
9014
9062
  // Array `vector` will contain data values on model time scale, starting at
9015
- // *model* time step t = 0
9063
+ // *model* time step t = 0.
9016
9064
  this.vector = [];
9017
9065
  this.modifiers = {};
9018
- // Selector to be used when model is run normally, i.e., no experiment
9066
+ // Selector to be used when model is run normally, i.e., no experiment.
9019
9067
  this.default_selector = '';
9020
9068
  }
9021
9069
 
@@ -9177,12 +9225,17 @@ class Dataset {
9177
9225
  // Data is stored simply as semicolon-separated floating point numbers,
9178
9226
  // with N-digit precision to keep model files compact (default: N = 8).
9179
9227
  let d = [];
9180
- for(const v of this.data) {
9181
- // Convert number to string with the desired precision.
9182
- const f = v.toPrecision(CONFIGURATION.dataset_precision);
9183
- // Then parse it again, so that the number will be represented
9184
- // (by JavaScript) in the most compact representation.
9185
- d.push(parseFloat(f));
9228
+ // NOTE: Guard against empty strings and other invalid data.
9229
+ for(const v of this.data) if(v) {
9230
+ try {
9231
+ // Convert number to string with the desired precision.
9232
+ const f = v.toPrecision(CONFIGURATION.dataset_precision);
9233
+ // Then parse it again, so that the number will be represented
9234
+ // (by JavaScript) in the most compact representation.
9235
+ d.push(parseFloat(f));
9236
+ } catch(err) {
9237
+ console.log('-- Notice: dataset', this.displayName, 'has invalid data', v);
9238
+ }
9186
9239
  }
9187
9240
  return d.join(';');
9188
9241
  }
@@ -11319,6 +11372,7 @@ class ExperimentRun {
11319
11372
  constructor(x, n) {
11320
11373
  this.experiment = x;
11321
11374
  this.number = n;
11375
+ this.combination = [];
11322
11376
  this.time_started = 0;
11323
11377
  this.time_recorded = 0;
11324
11378
  this.time_steps = MODEL.end_period - MODEL.start_period + 1;
@@ -11330,6 +11384,8 @@ class ExperimentRun {
11330
11384
  }
11331
11385
 
11332
11386
  start() {
11387
+ // Initialize this run.
11388
+ this.combination = this.experiment.combinations[this.number].slice();
11333
11389
  this.time_started = new Date().getTime();
11334
11390
  this.time_recorded = 0;
11335
11391
  this.results = [];
@@ -11346,7 +11402,8 @@ class ExperimentRun {
11346
11402
  '" started="', this.time_started,
11347
11403
  '" recorded="', this.time_recorded,
11348
11404
  '"><x-title>', xmlEncoded(this.experiment.title),
11349
- '</x-title><time-steps>', this.time_steps,
11405
+ '</x-title><x-combi>', this.combination.join(' '),
11406
+ '</x-combi><time-steps>', this.time_steps,
11350
11407
  '</time-steps><delta-t>', this.time_step_duration,
11351
11408
  '</delta-t><results>', r,
11352
11409
  '</results><messages>', bm,
@@ -11363,6 +11420,7 @@ class ExperimentRun {
11363
11420
  UI.warn(`Run title "${t}" does not match experiment title "` +
11364
11421
  this.experiment.title + '"');
11365
11422
  }
11423
+ this.combi = nodeContentByTag(node, 'x-combi').split(' ');
11366
11424
  this.time_steps = safeStrToInt(nodeContentByTag(node, 'time-steps'));
11367
11425
  this.time_step_duration = safeStrToFloat(nodeContentByTag(node, 'delta-t'));
11368
11426
  let n = childNodeByTag(node, 'results');
@@ -11543,7 +11601,6 @@ class Experiment {
11543
11601
  this.selected_statistic = 'mean';
11544
11602
  this.selected_scale = 'val';
11545
11603
  this.selelected_color_scale = 'no';
11546
- this.active_combination_index = -1;
11547
11604
  // Set of combination indices to be displayed in chart.
11548
11605
  this.chart_combinations = [];
11549
11606
  // String to store original model settings while executing experiment runs.
@@ -11553,7 +11610,7 @@ class Experiment {
11553
11610
  }
11554
11611
 
11555
11612
  clearRuns() {
11556
- // NOTE: separated from basic initialization so that it can be called
11613
+ // NOTE: Separated from basic initialization so that it can be called
11557
11614
  // when the modeler clicks on the "Clear results" button.
11558
11615
  // @@TO DO: prepare for UNDO.
11559
11616
  this.runs.length = 0;
@@ -11561,7 +11618,7 @@ class Experiment {
11561
11618
  this.completed = false;
11562
11619
  this.time_started = 0;
11563
11620
  this.time_stopped = 0;
11564
- this.active_combination_index = 0;
11621
+ this.active_combination_index = -1;
11565
11622
  this.chart_combinations.length = 0;
11566
11623
  }
11567
11624
 
@@ -11637,21 +11694,72 @@ class Experiment {
11637
11694
  }
11638
11695
 
11639
11696
  matchingCombinationIndex(sl) {
11640
- // Return index of combination with most selectors in common with `sl`.
11697
+ // Return the index of the run that can be inferred from selector list
11698
+ // `sl`, or FALSE if results for this run are not yet available.
11699
+ // NOTE: The selector list `sl` is a run specification that is *relative*
11700
+ // to the active combination of the *running* experiment, which may be
11701
+ // different from `this`. For example, consider an experiment with
11702
+ // three dimensions A = {a1, a2, a3), B = {b1, b2} and C = {c1, c2, c3},
11703
+ // and assume that the active combination is a2 + b2 + c2. Then the
11704
+ // "matching" combination will be:
11705
+ // a2 + b2 + c2 if `sl` is empty
11706
+ // a2 + b1 + c2 if `sl` is [b1]
11707
+ // a1 + b3 + c2 if `sl` is [b3, a1] (selector sequence)
11708
+ // NOTES:
11709
+ // (1) Elements of `sl` that are not element of A, B or C are ingnored.
11710
+ // (2) `sl` should not contain more than 1 selector from the same dimension.
11711
+ const
11712
+ valid = [],
11713
+ v_pos = {},
11714
+ matching = [];
11715
+ // First, retain only the (unique) valid selectors in `sl`.
11716
+ for(const s of sl) if(valid.indexOf(s) < 0) {
11717
+ for(const c of this.combinations) {
11718
+ // NOTE: Because of the way combinations are constructed, the index of
11719
+ // a valid selector in a combinations will always be the same.
11720
+ const pos = c.indexOf(s);
11721
+ // Conversely, when a new selector has the same position as a selector
11722
+ // that was already validated, this new selector is disregarded.
11723
+ if(pos >= 0 && !v_pos[pos]) {
11724
+ valid.push(s);
11725
+ v_pos[pos] = true;
11726
+ }
11727
+ }
11728
+ }
11729
+ // Then build a list of indices of combinations that match all valid selectors.
11730
+ // NOTE: The list of runs may not cover ALL combinations.
11731
+ for(let ri = 0; ri < this.runs.length; ri++) {
11732
+ if(intersection(valid, this.runs[ri].combination).length === valid.length) {
11733
+ matching.push(ri);
11734
+ }
11735
+ }
11736
+ // Results may already be conclusive:
11737
+ if(!matching.length) return false;
11738
+ // NOTE: If no experiment is running, there is no "active combination".
11739
+ // This should not occur, but in then return the last (= most recent) match.
11740
+ if(matching.length === 1 || !MODEL.running_experiment) return matching.pop();
11741
+ // If not conclusive, find the matching combination that -- for the remaining
11742
+ // dimensions of the experiment -- has most selectors in common with
11743
+ // the active combination of the *running* experiment.
11744
+ const ac = MODEL.running_experiment.activeCombination;
11641
11745
  let high = 0,
11642
11746
  index = false;
11643
- // NOTE: Results of current run are not available yet, hence length-1.
11644
- for(let ci = 0; ci < this.active_combination_index; ci++) {
11645
- const l = intersection(sl, this.combinations[ci]).length;
11646
- if(l > high) {
11647
- high = l;
11648
- index = ci;
11747
+ for(const ri of matching) {
11748
+ const c = this.runs[ri].combination;
11749
+ let nm = 0;
11750
+ // NOTE: Ignore the matching valid selectors.
11751
+ for(let ci = 0; ci < c.length; ci++) {
11752
+ if(!v_pos[ci] && ac.indexOf(c[ci]) >= 0) nm++;
11753
+ }
11754
+ // NOTE: Using >= ensures that index will be set even for 0 matching.
11755
+ if(nm >= high) {
11756
+ high = nm;
11757
+ index = ri;
11649
11758
  }
11650
11759
  }
11651
- // NOTE: If no matching selectors, return value is FALSE.
11652
11760
  return index;
11653
11761
  }
11654
-
11762
+
11655
11763
  isDimensionSelector(s) {
11656
11764
  // Return TRUE if `s` is a dimension selector in this experiment.
11657
11765
  for(const dim of this.dimensions) if(dim.indexOf(s) >= 0) return true;
@@ -303,11 +303,21 @@ function markFirstDifference(s1, s2) {
303
303
  //
304
304
 
305
305
  function ciCompare(a, b) {
306
- // Performs case-insensitive comparison that does differentiate
307
- // between accented characters (as this differentiates between identifiers)
306
+ // Perform case-insensitive comparison that does differentiate
307
+ // between accented characters (as this differentiates between identifiers).
308
308
  return a.localeCompare(b, undefined, {sensitivity: 'accent'});
309
309
  }
310
310
 
311
+ function compareTailFirst(a, b, tail) {
312
+ // Sort strings while prioritizing the group of elements that end on `tail`.
313
+ const
314
+ a_tail = a.endsWith(tail),
315
+ b_tail = b.endsWith(tail);
316
+ if(a_tail && !b_tail) return -1;
317
+ if(!a_tail && b_tail) return 1;
318
+ return ciCompare(a, b);
319
+ }
320
+
311
321
  function endsWithDigits(str) {
312
322
  // Returns trailing digts of `str` (empty string will evaluate as FALSE)
313
323
  let i = str.length - 1,
@@ -1143,6 +1153,7 @@ if(NODE) module.exports = {
1143
1153
  differences: differences,
1144
1154
  markFirstDifference: markFirstDifference,
1145
1155
  ciCompare: ciCompare,
1156
+ compareTailFirst: compareTailFirst,
1146
1157
  endsWithDigits: endsWithDigits,
1147
1158
  indexOfMatchingBracket: indexOfMatchingBracket,
1148
1159
  monoSpaced: monoSpaced,