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.
- package/package.json +6 -2
- package/static/images/solve-not-same-changed.png +0 -0
- package/static/images/solve-not-same-not-changed.png +0 -0
- package/static/images/solve-same-changed.png +0 -0
- package/static/images/solve-same-not-changed.png +0 -0
- package/static/index.html +81 -11
- package/static/linny-r.css +250 -7
- package/static/scripts/linny-r-ctrl.js +76 -13
- package/static/scripts/linny-r-gui-chart-manager.js +13 -12
- package/static/scripts/linny-r-gui-constraint-editor.js +4 -4
- package/static/scripts/linny-r-gui-controller.js +416 -43
- package/static/scripts/linny-r-gui-dataset-manager.js +122 -93
- package/static/scripts/linny-r-gui-experiment-manager.js +22 -12
- package/static/scripts/linny-r-gui-expression-editor.js +26 -12
- package/static/scripts/linny-r-gui-finder.js +190 -5
- package/static/scripts/linny-r-gui-repository-browser.js +18 -0
- package/static/scripts/linny-r-model.js +192 -84
- package/static/scripts/linny-r-utils.js +13 -2
- package/static/scripts/linny-r-vm.js +137 -95
@@ -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
|
-
|
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
|
-
|
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)
|
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
|
1098
|
-
const nk = newkey +
|
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(
|
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.
|
1139
|
+
this.variable_count = 0;
|
1109
1140
|
this.expression_count = 0;
|
1110
1141
|
// Rename datasets one by one, suppressing notifications.
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
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:
|
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>⤷<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
|
-
|
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:
|
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
|
-
//
|
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:
|
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:
|
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 +
|
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
|
-
//
|
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
|
-
//
|
4573
|
-
// actual name after inclusion
|
4574
|
-
// NOTE:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
|
-
|
9181
|
-
|
9182
|
-
|
9183
|
-
|
9184
|
-
|
9185
|
-
|
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><
|
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:
|
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 =
|
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
|
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
|
-
|
11644
|
-
|
11645
|
-
|
11646
|
-
|
11647
|
-
|
11648
|
-
|
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
|
-
//
|
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,
|