linny-r 2.1.5 → 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 +1 -1
- package/static/index.html +18 -2
- package/static/linny-r.css +44 -0
- package/static/scripts/linny-r-config.js +2 -2
- package/static/scripts/linny-r-gui-chart-manager.js +3 -2
- package/static/scripts/linny-r-gui-controller.js +32 -25
- package/static/scripts/linny-r-gui-dataset-manager.js +8 -1
- package/static/scripts/linny-r-gui-experiment-manager.js +28 -16
- package/static/scripts/linny-r-gui-file-manager.js +2 -1
- package/static/scripts/linny-r-gui-finder.js +260 -22
- package/static/scripts/linny-r-gui-power-grid-manager.js +6 -0
- package/static/scripts/linny-r-milp.js +7 -1
- package/static/scripts/linny-r-model.js +94 -133
- package/static/scripts/linny-r-utils.js +210 -30
- package/static/scripts/linny-r-vm.js +21 -7
@@ -542,7 +542,7 @@ class LinnyRModel {
|
|
542
542
|
e = this.objectByName(en);
|
543
543
|
if(!e) return `Unknown model entity "${en}"`;
|
544
544
|
const
|
545
|
-
ao = ea[1].split('@'),
|
545
|
+
ao = (ea.length > 1 ? ea[1].split('@') : ['']),
|
546
546
|
a = ao[0].trim();
|
547
547
|
// Valid if no attribute, as all entity types have a default attribute.
|
548
548
|
if(!a) return true;
|
@@ -1636,8 +1636,7 @@ class LinnyRModel {
|
|
1636
1636
|
const ci = this.indexOfChart(title);
|
1637
1637
|
if(ci >= 0) return this.charts[ci];
|
1638
1638
|
// Otherwise, add it. NOTE: Unlike datasets, charts are not "entities".
|
1639
|
-
let c = new Chart();
|
1640
|
-
c.title = title;
|
1639
|
+
let c = new Chart(title);
|
1641
1640
|
if(node) c.initFromXML(node);
|
1642
1641
|
this.charts.push(c);
|
1643
1642
|
// Sort the chart titles alphabetically...
|
@@ -3323,7 +3322,7 @@ class LinnyRModel {
|
|
3323
3322
|
vbls.sort((a, b) => UI.compareFullNames(a.displayName, b.displayName));
|
3324
3323
|
// Create a new chart as dummy, so without adding it to this model.
|
3325
3324
|
const
|
3326
|
-
c = new Chart(),
|
3325
|
+
c = new Chart('__d_u_m_m_y__c_h_a_r_t__'),
|
3327
3326
|
wcdm = [];
|
3328
3327
|
for(const v of vbls) {
|
3329
3328
|
// NOTE: Prevent adding wildcard dataset modifiers more than once.
|
@@ -6242,19 +6241,19 @@ class Cluster extends NodeBox {
|
|
6242
6241
|
}
|
6243
6242
|
|
6244
6243
|
get nestingLevel() {
|
6245
|
-
// Return the "depth" of this cluster in the cluster hierarchy
|
6244
|
+
// Return the "depth" of this cluster in the cluster hierarchy.
|
6246
6245
|
if(this.cluster) return this.cluster.nestingLevel + 1; // recursion!
|
6247
6246
|
return 0;
|
6248
6247
|
}
|
6249
6248
|
|
6250
6249
|
get toBeIgnored() {
|
6251
|
-
// Return TRUE if this cluster or some parent cluster is set to be ignored
|
6250
|
+
// Return TRUE if this cluster or some parent cluster is set to be ignored.
|
6252
6251
|
return this.ignore || MODEL.ignoreClusterInThisRun(this) ||
|
6253
6252
|
(this.cluster && this.cluster.toBeIgnored); // recursion!
|
6254
6253
|
}
|
6255
6254
|
|
6256
6255
|
get blackBoxed() {
|
6257
|
-
// Return TRUE if this cluster or some parent cluster is marked as black box
|
6256
|
+
// Return TRUE if this cluster or some parent cluster is marked as black box.
|
6258
6257
|
return this.black_box ||
|
6259
6258
|
(this.cluster && this.cluster.blackBoxed); // recursion!
|
6260
6259
|
}
|
@@ -7930,6 +7929,7 @@ class Process extends Node {
|
|
7930
7929
|
const a = {name: this.displayName};
|
7931
7930
|
a.LB = this.lower_bound.asAttribute;
|
7932
7931
|
a.UB = (this.equal_bounds ? a.LB : this.upper_bound.asAttribute);
|
7932
|
+
if(this.grid) a.LB = -a.UB;
|
7933
7933
|
a.IL = this.initial_level.asAttribute;
|
7934
7934
|
a.LCF = this.pace_expression.asAttribute;
|
7935
7935
|
if(MODEL.solved) {
|
@@ -8863,13 +8863,13 @@ class Link {
|
|
8863
8863
|
}
|
8864
8864
|
|
8865
8865
|
get identifier() {
|
8866
|
-
// NOTE:
|
8867
|
-
// prevents problems when nodes are renamed
|
8866
|
+
// NOTE: Link IDs are based on the node codes rather than IDs, as this
|
8867
|
+
// prevents problems when nodes are renamed.
|
8868
8868
|
return this.from_node.code + '___' + this.to_node.code;
|
8869
8869
|
}
|
8870
8870
|
|
8871
8871
|
get attributes() {
|
8872
|
-
// NOTE:
|
8872
|
+
// NOTE: Link is named by its tab-separated node names.
|
8873
8873
|
const a = {name: this.from_node.displayName + '\t' + this.to_node.displayName};
|
8874
8874
|
a.R = this.relative_rate.asAttribute;
|
8875
8875
|
if(MODEL.infer_cost_prices) a.SOC = this.share_of_cost;
|
@@ -9222,7 +9222,7 @@ class Dataset {
|
|
9222
9222
|
}
|
9223
9223
|
|
9224
9224
|
get attributes() {
|
9225
|
-
// NOTE:
|
9225
|
+
// NOTE: Modifiers are appended as additional lines of text.
|
9226
9226
|
const a = {name: this.displayName};
|
9227
9227
|
a.D = '\t' + (this.vector ? this.vector[MODEL.t] : this.default_value);
|
9228
9228
|
for(let k in this.modifiers) if(this.modifiers.hasOwnProperty(k)) {
|
@@ -9355,22 +9355,9 @@ class Dataset {
|
|
9355
9355
|
}
|
9356
9356
|
|
9357
9357
|
get dataString() {
|
9358
|
-
//
|
9359
|
-
//
|
9360
|
-
|
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)
|
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
|
9615
|
-
|
9616
|
-
'
|
9617
|
-
|
9618
|
-
|
9619
|
-
|
9620
|
-
|
9621
|
-
|
9622
|
-
|
9623
|
-
|
9624
|
-
|
9625
|
-
|
9626
|
-
|
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
|
-
|
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) {
|
@@ -9981,8 +9989,8 @@ class ChartVariable {
|
|
9981
9989
|
}
|
9982
9990
|
// Scale the value unless run result (these are already scaled!).
|
9983
9991
|
if(!rr) {
|
9984
|
-
v *= this.scale_factor;
|
9985
9992
|
if(this.absolute) v = Math.abs(v);
|
9993
|
+
v *= this.scale_factor;
|
9986
9994
|
}
|
9987
9995
|
this.vector.push(v);
|
9988
9996
|
// Do not include values for t = 0 in statistics.
|
@@ -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
|
-
|
11438
|
-
|
11439
|
-
|
11440
|
-
|
11441
|
-
|
11442
|
-
|
11443
|
-
|
11444
|
-
|
11445
|
-
|
11446
|
-
|
11447
|
-
|
11448
|
-
|
11449
|
-
|
11450
|
-
|
11451
|
-
|
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 =
|
11472
|
-
this.exceptions = safeStrToInt(nodeContentByTag(node, 'exceptions'));
|
11473
|
-
|
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.
|
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');
|
@@ -13051,7 +13012,7 @@ class Constraint {
|
|
13051
13012
|
}
|
13052
13013
|
|
13053
13014
|
get typeLetter() {
|
13054
|
-
return '
|
13015
|
+
return 'B';
|
13055
13016
|
}
|
13056
13017
|
|
13057
13018
|
get identifier() {
|
@@ -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
|
@@ -189,11 +357,16 @@ function uniformDecimals(data) {
|
|
189
357
|
}
|
190
358
|
maxi = Math.max(maxi, ss[0].length);
|
191
359
|
}
|
192
|
-
// STEP 2: Convert the data to a uniform format
|
360
|
+
// STEP 2: Convert the data to a uniform format.
|
361
|
+
const special = ['\u221E', '-\u221E', '\u2047', '\u00A2'];
|
193
362
|
for(let i = 0; i < data.length; i++) {
|
194
|
-
const
|
363
|
+
const
|
364
|
+
v = data[i],
|
365
|
+
f = parseFloat(v);
|
195
366
|
if(isNaN(f)) {
|
196
|
-
|
367
|
+
// Keep special values such as infinity, and replace error values
|
368
|
+
// by Unicode warning sign.
|
369
|
+
if(special.indexOf(v) < 0) data[i] = '\u26A0';
|
197
370
|
} else if(maxe > 0) {
|
198
371
|
// Convert ALL numbers to exponential notation with two decimals (1.23e+7)
|
199
372
|
const v = f.toExponential(2);
|
@@ -405,51 +578,59 @@ function patternList(str) {
|
|
405
578
|
|
406
579
|
function patternMatch(str, patterns) {
|
407
580
|
// Returns TRUE when `str` matches the &|^-pattern.
|
408
|
-
// NOTE: If a pattern starts with
|
409
|
-
//
|
410
|
-
//
|
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.
|
411
585
|
for(let i = 0; i < patterns.length; i++) {
|
412
586
|
const p = patterns[i];
|
413
587
|
// NOTE: `p` is an OR sub-pattern that tests for a set of "plus"
|
414
588
|
// sub-sub-patterns (all of which should match) and a set of "min"
|
415
589
|
// sub-sub-patters (all should NOT match)
|
416
590
|
let pm,
|
591
|
+
swt,
|
592
|
+
ewt,
|
417
593
|
re,
|
418
594
|
match = true;
|
419
595
|
for(let j = 0; match && j < p.plus.length; j++) {
|
420
596
|
pm = p.plus[j];
|
421
|
-
|
422
|
-
|
423
|
-
|
597
|
+
swt = pm.startsWith('~');
|
598
|
+
ewt = pm.endsWith('~');
|
599
|
+
if(swt && ewt) {
|
600
|
+
match = (str === pm.slice(1, -1));
|
601
|
+
} else if(swt) {
|
424
602
|
match = str.startsWith(pm.substring(1));
|
603
|
+
} else if(ewt) {
|
604
|
+
match = str.endsWith(pm.slice(0, -1));
|
425
605
|
} else {
|
426
606
|
match = (str.indexOf(pm) >= 0);
|
427
607
|
}
|
428
|
-
// If no match, check whether pattern contains wildcards
|
608
|
+
// If no match, check whether pattern contains wildcards.
|
429
609
|
if(!match && pm.indexOf('#') >= 0) {
|
430
610
|
// If so, rematch using regular expression that tests for a
|
431
|
-
// number or a ?? wildcard
|
611
|
+
// number or a ?? wildcard.
|
432
612
|
let res = pm.split('#');
|
433
613
|
for(let i = 0; i < res.length; i++) {
|
434
614
|
res[i] = escapeRegex(res[i]);
|
435
615
|
}
|
436
616
|
res = res.join('(\\d+|\\?\\?)');
|
437
|
-
if(
|
438
|
-
|
439
|
-
} else if(pm.startsWith('~')) {
|
440
|
-
res = '^' + res;
|
441
|
-
}
|
617
|
+
if(swt) res = '^' + res;
|
618
|
+
if(ewt) res += '$';
|
442
619
|
re = new RegExp(res, 'g');
|
443
620
|
match = re.test(str);
|
444
621
|
}
|
445
622
|
}
|
446
|
-
// Any "min" match indicates NO match for this sub-pattern
|
623
|
+
// Any "min" match indicates NO match for this sub-pattern.
|
447
624
|
for(let j = 0; match && j < p.min.length; j++) {
|
448
625
|
pm = p.min[j];
|
449
|
-
|
450
|
-
|
451
|
-
|
626
|
+
swt = pm.startsWith('~');
|
627
|
+
ewt = pm.endsWith('~');
|
628
|
+
if(swt && ewt) {
|
629
|
+
match = (str !== pm.slice(1, -1));
|
630
|
+
} else if(swt) {
|
452
631
|
match = !str.startsWith(pm.substring(1));
|
632
|
+
} else if(ewt) {
|
633
|
+
match = !str.endsWith(pm.slice(0, -1));
|
453
634
|
} else {
|
454
635
|
match = (str.indexOf(pm) < 0);
|
455
636
|
}
|
@@ -461,11 +642,8 @@ function patternMatch(str, patterns) {
|
|
461
642
|
res[i] = escapeRegex(res[i]);
|
462
643
|
}
|
463
644
|
res = res.join('(\\d+|\\?\\?)');
|
464
|
-
if(
|
465
|
-
|
466
|
-
} else if(pm.startsWith('~')) {
|
467
|
-
res = '^' + res;
|
468
|
-
}
|
645
|
+
if(swt) res = '^' + res;
|
646
|
+
if(ewt) res += '$';
|
469
647
|
re = new RegExp(res, 'g');
|
470
648
|
match = !re.test(str);
|
471
649
|
}
|
@@ -973,18 +1151,20 @@ function nameToLines(name, actor_name = '') {
|
|
973
1151
|
// the node box.
|
974
1152
|
let m = actor_name.length;
|
975
1153
|
const
|
976
|
-
d = Math.floor(Math.sqrt(0.
|
1154
|
+
d = Math.floor(Math.sqrt(0.25 * name.length)),
|
977
1155
|
// Do not wrap strings shorter than 13 characters (about 50 pixels).
|
978
1156
|
limit = Math.max(Math.ceil(name.length / d), m, 13),
|
979
|
-
|
980
|
-
|
1157
|
+
// NOTE: Do not split on spaces followed by a number or a single
|
1158
|
+
// capital letter.
|
1159
|
+
a = name.split(/\s(?!\d+:|\d+$|[A-Z]\W)/);
|
1160
|
+
// Split words at '-' when wider than limit.
|
981
1161
|
for(let j = 0; j < a.length; j++) {
|
982
1162
|
if(a[j].length > limit) {
|
983
1163
|
const sw = a[j].split('-');
|
984
1164
|
if(sw.length > 1) {
|
985
|
-
// Replace j-th word by last fragment of split string
|
1165
|
+
// Replace j-th word by last fragment of split string.
|
986
1166
|
a[j] = sw.pop();
|
987
|
-
// Insert remaining fragments before
|
1167
|
+
// Insert remaining fragments before.
|
988
1168
|
while(sw.length > 0) a.splice(j, 0, sw.pop() + '-');
|
989
1169
|
}
|
990
1170
|
}
|