linny-r 2.1.6 → 2.1.7

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": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -11,7 +11,7 @@ warning or information messages are displayed.
11
11
  */
12
12
 
13
13
  /*
14
- Copyright (c) 2017-2024 Delft University of Technology
14
+ Copyright (c) 2017-2025 Delft University of Technology
15
15
 
16
16
  Permission is hereby granted, free of charge, to any person obtaining a copy
17
17
  of this software and associated documentation files (the "Software"), to deal
@@ -41,7 +41,7 @@ const CONFIGURATION = {
41
41
  // To keep model files compact, floating point values in datasets and run
42
42
  // results are stored with a limited number of significant digits
43
43
  dataset_precision: 8,
44
- results_precision: 6,
44
+ results_precision: 8,
45
45
  // Default properties for new models
46
46
  default_currency_unit: 'EUR',
47
47
  default_time_unit: 'hour',
@@ -90,7 +90,8 @@ class GUIFileManager {
90
90
  if(!UI.hidden('series-modal')) {
91
91
  DATASET_MANAGER.series_data.value = data.split(';').join('\n');
92
92
  } else {
93
- dataset.unpackDataString(data);
93
+ // NOTE: FALSE indicates that data is *not* B62-encoded.
94
+ dataset.unpackDataString(data, false);
94
95
  }
95
96
  }
96
97
  // NOTE: remove dataset from the "loading" list
@@ -9355,22 +9355,9 @@ class Dataset {
9355
9355
  }
9356
9356
 
9357
9357
  get dataString() {
9358
- // Data is stored simply as semicolon-separated floating point numbers,
9359
- // with N-digit precision to keep model files compact (default: N = 8).
9360
- let d = [];
9361
- // NOTE: Guard against empty strings and other invalid data.
9362
- for(const v of this.data) if(v) {
9363
- try {
9364
- // Convert number to string with the desired precision.
9365
- const f = v.toPrecision(CONFIGURATION.dataset_precision);
9366
- // Then parse it again, so that the number will be represented
9367
- // (by JavaScript) in the most compact representation.
9368
- d.push(parseFloat(f));
9369
- } catch(err) {
9370
- console.log('-- Notice: dataset', this.displayName, 'has invalid data', v);
9371
- }
9372
- }
9373
- return d.join(';');
9358
+ // NOTE: As of version 2.1.7, data is stored as semicolon-separated
9359
+ // B62-encoded numbers.
9360
+ return packVector(this.data);
9374
9361
  }
9375
9362
 
9376
9363
  get propertiesString() {
@@ -9389,10 +9376,17 @@ class Dataset {
9389
9376
  return ' (' + time_prop + (this.periodic ? ' \u21BB' : '') + ')';
9390
9377
  }
9391
9378
 
9392
- unpackDataString(str) {
9379
+ unpackDataString(str, b62=true) {
9393
9380
  // Convert semicolon-separated data to a numeric array.
9381
+ // NOTE: When b62 is FALSE, data is not B62-encoded.
9394
9382
  this.data.length = 0;
9395
- if(str) for(const n of str.split(';')) this.data.push(parseFloat(n));
9383
+ if(str) {
9384
+ if(b62) {
9385
+ this.data = unpackVector(str);
9386
+ } else {
9387
+ for(const n of str.split(';')) this.data.push(parseFloat(n));
9388
+ }
9389
+ }
9396
9390
  this.computeVector();
9397
9391
  this.computeStatistics();
9398
9392
  }
@@ -9611,24 +9605,31 @@ class Dataset {
9611
9605
  n = MODEL.black_box_entities[id];
9612
9606
  cmnts = '';
9613
9607
  }
9614
- const xml = ['<dataset', p, '><name>', xmlEncoded(n),
9615
- '</name><notes>', cmnts,
9616
- '</notes><default>', this.default_value,
9617
- '</default><unit>', xmlEncoded(this.scale_unit),
9618
- '</unit><time-scale>', this.time_scale,
9619
- '</time-scale><time-unit>', this.time_unit,
9620
- '</time-unit><method>', this.method,
9621
- '</method><url>', xmlEncoded(this.url),
9622
- '</url><data>', xmlEncoded(this.dataString),
9623
- '</data><modifiers>', ml.join(''),
9624
- '</modifiers><default-selector>', xmlEncoded(this.default_selector),
9625
- '</default-selector></dataset>'].join('');
9626
- return xml;
9608
+ const
9609
+ data = xmlEncoded(this.dataString),
9610
+ xml = ['<dataset', p, '>',
9611
+ '<name>', xmlEncoded(n),
9612
+ '</name><unit>', xmlEncoded(this.scale_unit),
9613
+ '</unit><time-scale>', this.time_scale,
9614
+ '</time-scale><time-unit>', this.time_unit,
9615
+ '</time-unit><method>', this.method,
9616
+ '</method>'];
9617
+ // NOTE: Omit empty fields to reduce model file size.
9618
+ if(cmnts) xml.push('<notes>', cmnts, '</notes>');
9619
+ if(this.default_value) xml.push('<default>',
9620
+ this.default_value, '</default>');
9621
+ if(this.url) xml.push('<url>', xmlEncoded(this.url), '</url>');
9622
+ if(data) xml.push('<data b62="1">', data, '</data>');
9623
+ if(ml.length) xml.push('<modifiers>', ml.join(''), '</modifiers>');
9624
+ if(this.default_selector) xml.push('<default-selector>',
9625
+ xmlEncoded(this.default_selector), '</default-selector>');
9626
+ xml.push('</dataset>');
9627
+ return xml.join('');
9627
9628
  }
9628
9629
 
9629
9630
  initFromXML(node) {
9630
9631
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
9631
- this.default_value = safeStrToFloat(nodeContentByTag(node, 'default'));
9632
+ this.default_value = safeStrToFloat(nodeContentByTag(node, 'default'), 0);
9632
9633
  this.scale_unit = xmlDecoded(nodeContentByTag(node, 'unit')) || '1';
9633
9634
  this.time_scale = safeStrToFloat(nodeContentByTag(node, 'time-scale'), 1);
9634
9635
  this.time_unit = nodeContentByTag(node, 'time-unit') ||
@@ -9643,7 +9644,14 @@ class Dataset {
9643
9644
  if(this.url) {
9644
9645
  FILE_MANAGER.getRemoteData(this, this.url);
9645
9646
  } else {
9646
- this.unpackDataString(xmlDecoded(nodeContentByTag(node, 'data')));
9647
+ let data = '',
9648
+ b62 = false;
9649
+ const cn = childNodeByTag(node, 'data');
9650
+ if(cn) {
9651
+ data = nodeContent(cn);
9652
+ b62 = (nodeParameterValue(cn, 'b62') === '1');
9653
+ }
9654
+ this.unpackDataString(xmlDecoded(data), b62);
9647
9655
  }
9648
9656
  const n = childNodeByTag(node, 'modifiers');
9649
9657
  if(n) {
@@ -11372,83 +11380,30 @@ class ExperimentRunResult {
11372
11380
  return (this.attribute ? dn + '|' + this.attribute : dn);
11373
11381
  }
11374
11382
 
11375
- get vectorString() {
11376
- // Vector is coded as semicolon-separated floating point numbers
11377
- // reduced to N-digit precision to keep model files more compact.
11378
- // By default, N = 6; this can be altered in linny-r-config.js.
11379
- // To represent "sparse" vectors more compactly, sequences of
11380
- // identical values are encoded as NxF where N is the length of
11381
- // the sequence and F the numerical value, e.g., "17x0.4;0.2;7x0"
11382
- if(this.was_ignored) return '';
11383
- let prev = '',
11384
- cnt = 1;
11385
- const vl = [];
11386
- for(const v of this.vector) {
11387
- // Format number with desired precision.
11388
- const f = v.toPrecision(CONFIGURATION.results_precision);
11389
- // While value is same as previous, do not store, but count.
11390
- if(f === prev) {
11391
- cnt++;
11392
- } else {
11393
- if(cnt > 1) {
11394
- // More than one => "compress".
11395
- // NOTE: Parse so JavaScript will represent it most compactly.
11396
- vl.push(cnt + 'x' + parseFloat(prev));
11397
- cnt = 1;
11398
- } else if(prev) {
11399
- vl.push(parseFloat(prev));
11400
- }
11401
- prev = f;
11402
- }
11403
- }
11404
- // Add the last "batch" of numbers.
11405
- if(cnt > 1) {
11406
- // More than one => "compress".
11407
- // NOTE: Parse so JavaScript will represent it most compactly.
11408
- vl.push(cnt + 'x' + parseFloat(prev));
11409
- cnt = 1;
11410
- } else if(prev) {
11411
- vl.push(parseFloat(prev));
11412
- }
11413
- return vl.join(';');
11414
- }
11415
-
11416
- unpackVectorString(str) {
11417
- // Convert semicolon-separated data to a numeric array.
11418
- this.vector = [];
11419
- if(str && !this.was_ignored) {
11420
- for(const parts of str.split(';')) {
11421
- const tuple = parts.split('x');
11422
- if(tuple.length === 2) {
11423
- const
11424
- n = parseInt(tuple[0]),
11425
- f = parseFloat(tuple[1]);
11426
- for(let i = 0; i < n; i++) {
11427
- this.vector.push(f);
11428
- }
11429
- } else {
11430
- this.vector.push(parseFloat(tuple[0]));
11431
- }
11432
- }
11433
- }
11434
- }
11435
-
11436
11383
  get asXML() {
11437
- return ['<run-result', (this.x_variable ? ' x-variable="1"' : ''),
11438
- (this.was_ignored ? ' ignored="1"' : ''),
11439
- '><object-id>', xmlEncoded(this.object_id),
11440
- '</object-id><attribute>', xmlEncoded(this.attribute),
11441
- '</attribute><count>', this.N,
11442
- '</count><sum>', this.sum,
11443
- '</sum><mean>', this.mean,
11444
- '</mean><variance>', this.variance,
11445
- '</variance><minimum>', this.minimum,
11446
- '</minimum><maximum>', this.maximum,
11447
- '</maximum><non-zero-tally>', this.non_zero_tally,
11448
- '</non-zero-tally><last>', this.last,
11449
- '</last><exceptions>', this.exceptions,
11450
- '</exceptions><vector>', this.vectorString,
11451
- '</vector></run-result>'].join('');
11384
+ const
11385
+ data = (this.was_ignored ? '' : packVector(this.vector)),
11386
+ xml = ['<run-result',
11387
+ (this.x_variable ? ' x-variable="1"' : ''),
11388
+ (this.was_ignored ? ' ignored="1"' : ''),
11389
+ '><object-id>', xmlEncoded(this.object_id),
11390
+ '</object-id><attribute>', xmlEncoded(this.attribute),
11391
+ '</attribute>'];
11392
+ // NOTE: Reduce model size by saving only non-zero statistics.
11393
+ if(this.N) xml.push('<count>', this.N, '</count>');
11394
+ if(this.sum) xml.push('<sum>', this.sum, '</sum>');
11395
+ if(this.mean) xml.push('<mean>', this.mean, '</mean>');
11396
+ if(this.variance) xml.push('<variance>', this.variance, '</variance>');
11397
+ if(this.minimum) xml.push('<minimum>', this.minimum, '</minimum>');
11398
+ if(this.maximum) xml.push('<maximum>', this.maximum, '</maximum>');
11399
+ if(this.non_zero_tally) xml.push('<non-zero-tally>',
11400
+ this.non_zero_tally, '</non-zero-tally>');
11401
+ if(this.last) xml.push('<last>', this.last, '</last>');
11402
+ if(this.exceptions) xml.push('<exceptions>',
11403
+ this.exceptions, '</exceptions>');
11404
+ if(data) xml.push('<vector b62="1">', data, '</vector>');
11405
+ xml.push('</run-result>');
11406
+ return xml.join('');
11452
11407
  }
11453
11408
 
11454
11409
  initFromXML(node) {
@@ -11461,16 +11416,22 @@ class ExperimentRunResult {
11461
11416
  if(this.object_id === UI.EQUATIONS_DATASET_ID &&
11462
11417
  !earlierVersion(MODEL.version, '1.3.0')) attr = xmlDecoded(attr);
11463
11418
  this.attribute = attr;
11464
- this.N = safeStrToInt(nodeContentByTag(node, 'count'));
11465
- this.sum = safeStrToFloat(nodeContentByTag(node, 'sum'));
11466
- this.mean = safeStrToFloat(nodeContentByTag(node, 'mean'));
11467
- this.variance = safeStrToFloat(nodeContentByTag(node, 'variance'));
11468
- this.minimum = safeStrToFloat(nodeContentByTag(node, 'minimum'));
11469
- this.maximum = safeStrToFloat(nodeContentByTag(node, 'maximum'));
11470
- this.non_zero_tally = safeStrToInt(nodeContentByTag(node, 'non-zero-tally'));
11471
- this.last = safeStrToInt(nodeContentByTag(node, 'last'));
11472
- this.exceptions = safeStrToInt(nodeContentByTag(node, 'exceptions'));
11473
- this.unpackVectorString(nodeContentByTag(node, 'vector'));
11419
+ this.N = safeStrToInt(nodeContentByTag(node, 'count'), 0);
11420
+ this.sum = safeStrToFloat(nodeContentByTag(node, 'sum'), 0);
11421
+ this.mean = safeStrToFloat(nodeContentByTag(node, 'mean'), 0);
11422
+ this.variance = safeStrToFloat(nodeContentByTag(node, 'variance'), 0);
11423
+ this.minimum = safeStrToFloat(nodeContentByTag(node, 'minimum'), 0);
11424
+ this.maximum = safeStrToFloat(nodeContentByTag(node, 'maximum'), 0);
11425
+ this.non_zero_tally = safeStrToInt(nodeContentByTag(node, 'non-zero-tally'), 0);
11426
+ this.last = safeStrToFloat(nodeContentByTag(node, 'last'), 0);
11427
+ this.exceptions = safeStrToInt(nodeContentByTag(node, 'exceptions'), 0);
11428
+ const cn = childNodeByTag(node, 'vector');
11429
+ if(cn && !this.was_ignored) {
11430
+ const b62 = nodeParameterValue(cn, 'b62') === '1';
11431
+ this.vector = unpackVector(nodeContent(cn), b62);
11432
+ } else {
11433
+ this.vector = [];
11434
+ }
11474
11435
  }
11475
11436
 
11476
11437
  valueAtModelTime(t, mtsd, method, periodic) {
@@ -11597,7 +11558,7 @@ class ExperimentRun {
11597
11558
  UI.warn(`Run title "${t}" does not match experiment title "` +
11598
11559
  this.experiment.title + '"');
11599
11560
  }
11600
- this.combi = nodeContentByTag(node, 'x-combi').split(' ');
11561
+ this.combination = nodeContentByTag(node, 'x-combi').split(' ');
11601
11562
  this.time_steps = safeStrToInt(nodeContentByTag(node, 'time-steps'));
11602
11563
  this.time_step_duration = safeStrToFloat(nodeContentByTag(node, 'delta-t'));
11603
11564
  let n = childNodeByTag(node, 'results');
@@ -53,6 +53,174 @@ function postData(obj) {
53
53
  // Functions that convert numbers to strings, or strings to numbers
54
54
  //
55
55
 
56
+ const b62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
57
+
58
+ function posIntToB62(n) {
59
+ // Return the radix-62 encoding of positive integer value `n`.
60
+ let s = '';
61
+ while(n > 0) {
62
+ s = b62.charAt(n % 62) + s;
63
+ n = Math.floor(n / 62);
64
+ }
65
+ return s;
66
+ }
67
+
68
+ function B62ToInt(s) {
69
+ // Return the value of a B62-encoded integer, or -1 if `s` contains an
70
+ // invalid character.
71
+ const digits = s.split('');
72
+ let n = 0;
73
+ for(const d of digits) {
74
+ const index = b62.indexOf(d);
75
+ if(index < 0) return -1;
76
+ n = n * 62 + index;
77
+ }
78
+ return n;
79
+ }
80
+
81
+ function packFloat(f) {
82
+ // Return floating point number `f` in a compact string notation.
83
+ // The first character encodes whether `f` is an integer (then no exponent),
84
+ // whether the sign of `f` is + or -, and whether the sign of the exponent
85
+ // (if any) is + or - in the following manner:
86
+ // (no leading special character) => positive integer (no exponent)
87
+ // - negative integer (no exponent)
88
+ // ^ positive number, positive exponent
89
+ // _ negative number, positive exponent
90
+ // ~ positive number, negative exponent
91
+ // = negative number, negative exponent
92
+ const oldf = f;
93
+ if(!f) return '0';
94
+ let sign = '',
95
+ mant = '',
96
+ exp = 0;
97
+ if(f < 0.0) {
98
+ sign = '-';
99
+ f = -f;
100
+ }
101
+ const rf = Math.round(f);
102
+ // When number is integer, it has no exponent part.
103
+ if(rf === f) return sign + posIntToB62(rf);
104
+ const me = f.toExponential().split('e');
105
+ // Remove the decimal period from the mantissa.
106
+ mant = posIntToB62(parseInt(me[0].replace('.', '')));
107
+ // Determine the exponent part and its sign.
108
+ exp = parseInt(me[1]);
109
+ if(exp < 0) {
110
+ exp = -exp;
111
+ sign = (sign ? '=' : '~');
112
+ } else {
113
+ sign = (sign ? '_' : '^');
114
+ }
115
+ // NOTE: Exponent is always codes as a single character. This limits
116
+ // its value to 61, which for Linny-R suffices to code even its error
117
+ // values (highest error code exponent is +50).
118
+ return sign + mant + (exp ? posIntToB62(Math.min(exp, 61)) : '0');
119
+ }
120
+
121
+ function unpackFloat(s) {
122
+ // Return the number X that was encoded using packFloat(X).
123
+ // NOTE: When decoding fails, -1e+48 is returned to signal #INVALID.
124
+ if(s === '0') return 0;
125
+ const
126
+ INVALID = -1e+48,
127
+ ss = s.split('');
128
+ const index = '-^_~='.indexOf(ss[0]);
129
+ if(index < 1) {
130
+ // No exponent => get the absolute integer value.
131
+ const n = B62ToInt(s.substring(index + 1));
132
+ if(n < 0) return INVALID;
133
+ // Return the signed integer value.
134
+ return (index ? n : -n);
135
+ }
136
+ // Now the last character codes the exponent.
137
+ const
138
+ // Odd index (1 and 3) indicates positive number.
139
+ sign = (index % 2 ? '' : '-'),
140
+ // Low index (1 and 2) indicates positive exponent.
141
+ esign = (index < 3 ? 'e+' : 'e-');
142
+ // Remove the sign character.
143
+ ss.shift();
144
+ // Get and remove the exponent character, and decode it.
145
+ let exp = B62ToInt(ss.pop());
146
+ if(exp < 0) return INVALID;
147
+ exp = esign + exp.toString();
148
+ let mant = B62ToInt(ss.join(''));
149
+ if(mant < 0) return INVALID;
150
+ mant = mant.toString();
151
+ // NOTE: No decimal point if mantissa is a single decimal digit.
152
+ if(mant.length > 1) mant = mant.slice(0, 1) + '.' + mant.slice(1);
153
+ return parseFloat(sign + mant + exp);
154
+ }
155
+
156
+ function packVector(vector) {
157
+ // Vector is coded as semicolon-separated B62-encoded floating
158
+ // point numbers (no precision loss).
159
+ // To represent "sparse" vectors more compactly, sequences of
160
+ // identical values are encoded as N*F where N is the length of
161
+ // the sequence and F the B62-encoded numerical value.
162
+ let prev = false,
163
+ cnt = 0;
164
+ const vl = [];
165
+ for(const v of vector) {
166
+ // While value is same as previous, do not store, but count.
167
+ // NOTE: JavaScript precision is about 15 decimals, so test for
168
+ // equality with this precision.
169
+ if(prev === false || Math.abs(v - prev) < 0.5e-14) {
170
+ cnt++;
171
+ } else {
172
+ const b62 = packFloat(prev);
173
+ if(cnt > 1) {
174
+ // More than one => "compress".
175
+ vl.push(cnt + '*' + b62);
176
+ cnt = 1;
177
+ } else {
178
+ vl.push(b62);
179
+ }
180
+ }
181
+ prev = v;
182
+ }
183
+ if(cnt) {
184
+ const b62 = packFloat(prev);
185
+ // Add the last "batch" of numbers.
186
+ if(cnt > 1) {
187
+ // More than one => "compress".
188
+ vl.push(cnt + '*' + b62);
189
+ } else {
190
+ vl.push(b62);
191
+ }
192
+ }
193
+ return vl.join(';');
194
+ }
195
+
196
+ function unpackVector(str, b62=true) {
197
+ // Convert semicolon-separated data to a numeric array.
198
+ // NOTE: Until version 2.1.7, numbers were represented in standard
199
+ // decimal notation with limited precision. From v2.1.7 onwards, numbers
200
+ // are B62-encoded. When `b62` is FALSE, the legacy decoding is used.
201
+ vector = [];
202
+ if(str) {
203
+ const
204
+ ss = str.split(';'),
205
+ multi = (b62 ? '*' : 'x'),
206
+ parse = (b62 ? unpackFloat : parseFloat);
207
+ for(const parts of ss) {
208
+ const tuple = parts.split(multi);
209
+ if(tuple.length === 2) {
210
+ const f = parse(tuple[1]);
211
+ let n = parseInt(tuple[0]);
212
+ while(n > 0) {
213
+ vector.push(f);
214
+ n--;
215
+ }
216
+ } else {
217
+ vector.push(parse(tuple[0]));
218
+ }
219
+ }
220
+ }
221
+ return vector;
222
+ }
223
+
56
224
  function pluralS(n, s, special='') {
57
225
  // Returns string with noun `s` in singular only if `n` = 1
58
226
  // NOTE: third parameter can be used for nouns with irregular plural form
@@ -410,29 +578,29 @@ function patternList(str) {
410
578
 
411
579
  function patternMatch(str, patterns) {
412
580
  // Returns TRUE when `str` matches the &|^-pattern.
413
- // NOTE: If a pattern starts with an opening bracket [ then `str` must
414
- // start with the rest of the pattern to match. If it ends with a closing
415
- // bracket ] then `str` must end with the first part of the pattern.
416
- // In this way, [pattern] denotes that `str` should exactly match
581
+ // NOTE: If a pattern starts with a tilde ~ then `str` must start with
582
+ // the rest of the pattern to match. If it ends with a tilde, then `str`
583
+ // must end with the first part of the pattern.
584
+ // In this way, ~pattern~ denotes that `str` should exactly match.
417
585
  for(let i = 0; i < patterns.length; i++) {
418
586
  const p = patterns[i];
419
587
  // NOTE: `p` is an OR sub-pattern that tests for a set of "plus"
420
588
  // sub-sub-patterns (all of which should match) and a set of "min"
421
589
  // sub-sub-patters (all should NOT match)
422
590
  let pm,
423
- swob,
424
- ewcb,
591
+ swt,
592
+ ewt,
425
593
  re,
426
594
  match = true;
427
595
  for(let j = 0; match && j < p.plus.length; j++) {
428
596
  pm = p.plus[j];
429
- swob = pm.startsWith('[');
430
- ewcb = pm.endsWith(']');
431
- if(swob && ewcb) {
597
+ swt = pm.startsWith('~');
598
+ ewt = pm.endsWith('~');
599
+ if(swt && ewt) {
432
600
  match = (str === pm.slice(1, -1));
433
- } else if(swob) {
601
+ } else if(swt) {
434
602
  match = str.startsWith(pm.substring(1));
435
- } else if(ewcb) {
603
+ } else if(ewt) {
436
604
  match = str.endsWith(pm.slice(0, -1));
437
605
  } else {
438
606
  match = (str.indexOf(pm) >= 0);
@@ -446,8 +614,8 @@ function patternMatch(str, patterns) {
446
614
  res[i] = escapeRegex(res[i]);
447
615
  }
448
616
  res = res.join('(\\d+|\\?\\?)');
449
- if(swob) res = '^' + res;
450
- if(ewcb) res += '$';
617
+ if(swt) res = '^' + res;
618
+ if(ewt) res += '$';
451
619
  re = new RegExp(res, 'g');
452
620
  match = re.test(str);
453
621
  }
@@ -455,13 +623,13 @@ function patternMatch(str, patterns) {
455
623
  // Any "min" match indicates NO match for this sub-pattern.
456
624
  for(let j = 0; match && j < p.min.length; j++) {
457
625
  pm = p.min[j];
458
- swob = pm.startsWith('[');
459
- ewcb = pm.endsWith(']');
460
- if(swob && ewcb) {
626
+ swt = pm.startsWith('~');
627
+ ewt = pm.endsWith('~');
628
+ if(swt && ewt) {
461
629
  match = (str !== pm.slice(1, -1));
462
- } else if(swob) {
630
+ } else if(swt) {
463
631
  match = !str.startsWith(pm.substring(1));
464
- } else if(ewcb) {
632
+ } else if(ewt) {
465
633
  match = !str.endsWith(pm.slice(0, -1));
466
634
  } else {
467
635
  match = (str.indexOf(pm) < 0);
@@ -474,8 +642,8 @@ function patternMatch(str, patterns) {
474
642
  res[i] = escapeRegex(res[i]);
475
643
  }
476
644
  res = res.join('(\\d+|\\?\\?)');
477
- if(swob) res = '^' + res;
478
- if(ewcb) res += '$';
645
+ if(swt) res = '^' + res;
646
+ if(ewt) res += '$';
479
647
  re = new RegExp(res, 'g');
480
648
  match = !re.test(str);
481
649
  }
@@ -7249,14 +7249,14 @@ function VMI_push_run_result(x, args) {
7249
7249
  if(Array.isArray(rn)) {
7250
7250
  // Let the running experiment infer run number from selector list `rn`
7251
7251
  // and its own "active combination" of selectors.
7252
- rn = xp.matchingCombinationIndex(rn);
7252
+ rn = xp.matchingCombinationIndex(rn);
7253
7253
  } else if(rn < 0) {
7254
7254
  // Relative run number: use current run # + r (first run has number 0).
7255
7255
  if(xp === MODEL.running_experiment) {
7256
7256
  rn += xp.active_combination_index;
7257
7257
  } else if(xp.chart_combinations.length) {
7258
7258
  // Modeler has selected one or more runs in the viewer table.
7259
- // FInd the highest number of a selected run that has been performed.
7259
+ // Find the highest number of a selected run that has been performed.
7260
7260
  let last = -1;
7261
7261
  for(const ccn of xp.chart_combinations) {
7262
7262
  if(ccn > last && ccn < xp.runs.length) last = ccn;