archaeopteryx 3.16.0 → 3.18.0
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/README.md +129 -38
- package/archaeopteryx.d.ts +2 -0
- package/archaeopteryx.js +1166 -255
- package/forester.js +479 -111
- package/package.json +2 -2
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.18.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -225,17 +225,89 @@
|
|
|
225
225
|
throw ("this should never have happened, child node index = " + cni);
|
|
226
226
|
}
|
|
227
227
|
let x = p.children[0];
|
|
228
|
-
let
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
let stated = function (v) {
|
|
229
|
+
return typeof v === 'number' && isFinite(v);
|
|
230
|
+
};
|
|
231
|
+
// What the merged branch is made of, for a clock-model tree's
|
|
232
|
+
// divergence: each piece's span in time and the rate its node
|
|
233
|
+
// stated, kept on x (see divergenceOfPieces). A removed node's
|
|
234
|
+
// rate goes on scaling its own piece, as the desktop keeps it.
|
|
235
|
+
let showsDiv = statesDivergenceUnit(phy);
|
|
236
|
+
let root = forester.getTreeRoot(phy);
|
|
237
|
+
let pieceOf = function (n) {
|
|
238
|
+
if (Array.isArray(n._pieces)) {
|
|
239
|
+
return n._pieces;
|
|
240
|
+
}
|
|
241
|
+
let span = null;
|
|
242
|
+
if (stated(n._timeLength)) {
|
|
243
|
+
span = n._timeLength;
|
|
244
|
+
} else if (showsDiv) {
|
|
245
|
+
let d = auspiceNodeDate(n);
|
|
246
|
+
let pd = auspiceNodeDate(n.parent);
|
|
247
|
+
span = (d !== null && pd !== null) ? timeGapOf(root)(d, pd) : null;
|
|
248
|
+
} else if (stated(n.branch_length)) {
|
|
249
|
+
span = n.branch_length;
|
|
250
|
+
}
|
|
251
|
+
return span === null ? null : [{span: span, rate: clockRateOf(n)}];
|
|
252
|
+
};
|
|
253
|
+
let xs = pieceOf(x);
|
|
254
|
+
let ps = pieceOf(p);
|
|
255
|
+
let pieces = (xs && ps) ? xs.concat(ps) : null;
|
|
256
|
+
// The two lengths add up, sign and all: a stated 0 is a length (a
|
|
257
|
+
// truthiness test once turned two zeros into no length at all),
|
|
258
|
+
// and a negative span stays a negative span, so that a node keeps
|
|
259
|
+
// its date. One length stated and one not: the one stated.
|
|
260
|
+
let nbl = (stated(x.branch_length) && stated(p.branch_length)) ? (x.branch_length + p.branch_length)
|
|
261
|
+
: (stated(x.branch_length) ? x.branch_length : (stated(p.branch_length) ? p.branch_length : undefined));
|
|
262
|
+
// The length kept for the Time | Div switch goes with the branch
|
|
263
|
+
// the same way: x now spans, in time, what it and the node removed
|
|
264
|
+
// above it spanned. Left as it was, x came back from Div short by
|
|
265
|
+
// its parent's length (0.65 years on a real Nextstrain build). A
|
|
266
|
+
// length not kept for either leaves x with none.
|
|
267
|
+
if (Object.prototype.hasOwnProperty.call(x, '_timeLength')
|
|
268
|
+
|| Object.prototype.hasOwnProperty.call(p, '_timeLength')) {
|
|
269
|
+
x._timeLength = (stated(x._timeLength) && stated(p._timeLength))
|
|
270
|
+
? (x._timeLength + p._timeLength) : undefined;
|
|
231
271
|
}
|
|
232
272
|
x.parent = pp;
|
|
233
273
|
pp.children[cni] = x;
|
|
234
274
|
x.branch_length = nbl;
|
|
275
|
+
if (pieces) {
|
|
276
|
+
x._pieces = pieces;
|
|
277
|
+
} else {
|
|
278
|
+
delete x._pieces;
|
|
279
|
+
}
|
|
280
|
+
// On a captured clock-model tree the cumulative divergence of x,
|
|
281
|
+
// and of everything under it, moves by what the merged branch's
|
|
282
|
+
// divergence is now against what x's was.
|
|
283
|
+
if (pieces && stated(x._divergence) && stated(pp._divergence)) {
|
|
284
|
+
let own = clockRateOf(x);
|
|
285
|
+
if (own !== null) {
|
|
286
|
+
let shift = (pp._divergence + divergenceOfPieces(pieces, own)) - x._divergence;
|
|
287
|
+
if (shift !== 0) {
|
|
288
|
+
forester.preOrderTraversalAll(x, function (n) {
|
|
289
|
+
n._divergence += shift;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
235
294
|
}
|
|
236
295
|
|
|
237
296
|
};
|
|
238
297
|
|
|
298
|
+
// The divergence along a branch made of pieces (see forester.deleteSubtree):
|
|
299
|
+
// each piece's span, where it is positive, times the rate its node stated,
|
|
300
|
+
// or the surviving node's where it stated none. Clamped piece by piece,
|
|
301
|
+
// as each was when it was a branch of its own, so that deleting a node
|
|
302
|
+
// moves nobody in Div.
|
|
303
|
+
function divergenceOfPieces(pieces, ownRate) {
|
|
304
|
+
let sum = 0;
|
|
305
|
+
for (let i = 0; i < pieces.length; ++i) {
|
|
306
|
+
let r = (pieces[i].rate === null || pieces[i].rate === undefined) ? ownRate : pieces[i].rate;
|
|
307
|
+
sum += Math.max(0, pieces[i].span) * r;
|
|
308
|
+
}
|
|
309
|
+
return sum;
|
|
310
|
+
}
|
|
239
311
|
|
|
240
312
|
/**
|
|
241
313
|
* To re-root a tree object.
|
|
@@ -6388,13 +6460,22 @@
|
|
|
6388
6460
|
if (typeof title === 'string' && title.trim().length > 0) {
|
|
6389
6461
|
phy.name = title.trim();
|
|
6390
6462
|
}
|
|
6391
|
-
|
|
6392
|
-
|
|
6463
|
+
// A build has no branch lengths of its own, so it opens in the metric
|
|
6464
|
+
// it states COMPLETELY: time when every node states num_date, else
|
|
6465
|
+
// divergence when every node states div, else as it always did (time
|
|
6466
|
+
// if any node is dated, divergence if none is). Joint with the
|
|
6467
|
+
// desktop, 2026-09-29. A num_date is a calendar year, so the gap is
|
|
6468
|
+
// child minus parent, and it keeps its sign.
|
|
6469
|
+
let opensInTime = everyNodeStates(root, auspiceNodeDate)
|
|
6470
|
+
|| (!everyNodeStates(root, recordedDiv) && auspiceHasAnyDate(root));
|
|
6471
|
+
if (opensInTime) {
|
|
6472
|
+
setDeltaBranchLengths(root, null, auspiceNodeDate, calendarGap);
|
|
6393
6473
|
} else {
|
|
6394
|
-
|
|
6395
|
-
// keep the layout meaningful instead of a cladogram
|
|
6396
|
-
setDeltaBranchLengths(root, null, auspiceNodeDiv);
|
|
6474
|
+
setDeltaBranchLengths(root, null, recordedDiv, divergenceGap);
|
|
6397
6475
|
}
|
|
6476
|
+
// ...and says which, as the desktop's reader does: a saved copy then
|
|
6477
|
+
// states what its branch lengths measure
|
|
6478
|
+
phy.branch_length_unit = opensInTime ? CALENDAR_LENGTH_UNIT : DIVERGENCE_LENGTH_UNIT;
|
|
6398
6479
|
// A tip keeps its date INTERVAL: on a Nextstrain build it is the
|
|
6399
6480
|
// sampling-date uncertainty of a sample dated only to its month or
|
|
6400
6481
|
// year, which is data. (It was dropped here until 2026-09-17 because
|
|
@@ -6409,38 +6490,57 @@
|
|
|
6409
6490
|
? node.date.value : null;
|
|
6410
6491
|
}
|
|
6411
6492
|
|
|
6412
|
-
//
|
|
6413
|
-
//
|
|
6414
|
-
//
|
|
6415
|
-
//
|
|
6416
|
-
//
|
|
6417
|
-
|
|
6493
|
+
// The divergence a node RECORDS (nextstrain:div), or null. A plain decimal
|
|
6494
|
+
// number and nothing else (parseBeastNumber), as a clock rate is: every
|
|
6495
|
+
// number the switch reads is read one way, on both programs. parseFloat
|
|
6496
|
+
// read one out of the front of "0.005abc", "1,5" and "0x10" -- which the
|
|
6497
|
+
// Nexus reader had never let through, so the same value was a divergence
|
|
6498
|
+
// in a phyloXML file and none in a Nexus one. A NEGATIVE one is a value;
|
|
6499
|
+
// the branch along which it falls is drawn at 0 (divergenceGap).
|
|
6500
|
+
// Christian, 2026-09-29.
|
|
6501
|
+
function recordedDiv(node) {
|
|
6418
6502
|
if (node.properties) {
|
|
6419
6503
|
for (let i = 0; i < node.properties.length; ++i) {
|
|
6420
6504
|
if (node.properties[i].ref === NEXTSTRAIN_PREFIX + 'div') {
|
|
6421
|
-
|
|
6422
|
-
return isFinite(d) ? d : null;
|
|
6505
|
+
return parseBeastNumber(node.properties[i].value);
|
|
6423
6506
|
}
|
|
6424
6507
|
}
|
|
6425
6508
|
}
|
|
6426
|
-
return
|
|
6427
|
-
? node._divergence : null;
|
|
6509
|
+
return null;
|
|
6428
6510
|
}
|
|
6429
6511
|
|
|
6430
|
-
function
|
|
6512
|
+
function recordsDivergence(root) {
|
|
6431
6513
|
let found = false;
|
|
6432
|
-
forester.preOrderTraversalAll(
|
|
6433
|
-
if (
|
|
6514
|
+
forester.preOrderTraversalAll(root, function (n) {
|
|
6515
|
+
if (!found && recordedDiv(n) !== null) {
|
|
6434
6516
|
found = true;
|
|
6435
6517
|
}
|
|
6436
6518
|
});
|
|
6437
6519
|
return found;
|
|
6438
6520
|
}
|
|
6439
6521
|
|
|
6440
|
-
|
|
6522
|
+
// A node's CUMULATIVE divergence from the root. Auspice states it outright
|
|
6523
|
+
// as nextstrain:div; a BEAST clock-model tree states it as branch length
|
|
6524
|
+
// x rate, summed at load and kept on the node (see forester.
|
|
6525
|
+
// captureDivergence). A tree states it ONE way: where any node records a
|
|
6526
|
+
// divergence nothing is captured, so a node that records none has none.
|
|
6527
|
+
// Until 2026-09-29 such a node fell back to the sum of the loaded lengths,
|
|
6528
|
+
// which on an Auspice tree are YEARS -- a tip whose div of 0.0008 was
|
|
6529
|
+
// missing drew its branch at 0.2794, where the file's own numbers said
|
|
6530
|
+
// 0.0002.
|
|
6531
|
+
function auspiceNodeDiv(node) {
|
|
6532
|
+
let d = recordedDiv(node);
|
|
6533
|
+
if (d !== null) {
|
|
6534
|
+
return d;
|
|
6535
|
+
}
|
|
6536
|
+
return (typeof node._divergence === 'number' && isFinite(node._divergence))
|
|
6537
|
+
? node._divergence : null;
|
|
6538
|
+
}
|
|
6539
|
+
|
|
6540
|
+
function auspiceHasAnyDate(node) {
|
|
6441
6541
|
let found = false;
|
|
6442
6542
|
forester.preOrderTraversalAll(node, function (n) {
|
|
6443
|
-
if (
|
|
6543
|
+
if (auspiceNodeDate(n) !== null) {
|
|
6444
6544
|
found = true;
|
|
6445
6545
|
}
|
|
6446
6546
|
});
|
|
@@ -6451,42 +6551,209 @@
|
|
|
6451
6551
|
// (num_date -> the time view; nextstrain:div -> the divergence view).
|
|
6452
6552
|
// The root's length is 0, and a node missing the metric (or whose parent
|
|
6453
6553
|
// misses it) gets 0 -- so a time<->divergence toggle can never leave a
|
|
6454
|
-
// stale cross-scale length behind.
|
|
6455
|
-
function setDeltaBranchLengths(node, parentValue, metricOf) {
|
|
6554
|
+
// stale cross-scale length behind.
|
|
6555
|
+
function setDeltaBranchLengths(node, parentValue, metricOf, gapOf) {
|
|
6456
6556
|
let v = metricOf(node);
|
|
6457
|
-
node.branch_length = (parentValue !== null && v !== null)
|
|
6458
|
-
? Math.max(0, v - parentValue) : 0;
|
|
6557
|
+
node.branch_length = (parentValue !== null && v !== null) ? gapOf(v, parentValue) : 0;
|
|
6459
6558
|
let children = node.children;
|
|
6460
6559
|
if (children) {
|
|
6461
6560
|
for (let i = 0; i < children.length; ++i) {
|
|
6462
|
-
setDeltaBranchLengths(children[i], v, metricOf);
|
|
6561
|
+
setDeltaBranchLengths(children[i], v, metricOf, gapOf);
|
|
6463
6562
|
}
|
|
6464
6563
|
}
|
|
6465
6564
|
}
|
|
6466
6565
|
|
|
6566
|
+
// A TIME gap keeps its sign. A child dated before its parent is what the
|
|
6567
|
+
// file states -- real Nextstrain builds state it (42 branches in 4 of 11,
|
|
6568
|
+
// one of them by 4.09 years) -- so the length is negative and every node
|
|
6569
|
+
// keeps its own date. Clamped at 0 until 2026-09-29, which also made the
|
|
6570
|
+
// switch lossy: a time tree stating negative lengths came back from Div
|
|
6571
|
+
// with 0 in their place. (A negative length is DRAWN at 0, as it always
|
|
6572
|
+
// was and as the desktop draws it; this is about the value.)
|
|
6573
|
+
function calendarGap(v, parentValue) {
|
|
6574
|
+
return v - parentValue;
|
|
6575
|
+
}
|
|
6576
|
+
|
|
6577
|
+
function ageGap(v, parentValue) {
|
|
6578
|
+
return parentValue - v;
|
|
6579
|
+
}
|
|
6580
|
+
|
|
6581
|
+
// In the direction the tree's dates run: calendar dates increase toward
|
|
6582
|
+
// the tips, ages (a height) toward the root. As the desktop measures it.
|
|
6583
|
+
function timeGapOf(root) {
|
|
6584
|
+
return timeIncreasesTowardTips(root) ? calendarGap : ageGap;
|
|
6585
|
+
}
|
|
6586
|
+
|
|
6587
|
+
// Divergence accumulates: a node recording less than its parent is noise,
|
|
6588
|
+
// not a branch running backwards, and draws at 0.
|
|
6589
|
+
function divergenceGap(v, parentValue) {
|
|
6590
|
+
return Math.max(0, v - parentValue);
|
|
6591
|
+
}
|
|
6592
|
+
|
|
6467
6593
|
// The time<->divergence plumbing: both metrics are RETAINED on a parsed
|
|
6468
6594
|
// Auspice tree (the date values + the nextstrain:div properties), so a
|
|
6469
6595
|
// future display toggle can rewrite the branch lengths from EITHER at any
|
|
6470
6596
|
// time -- lossless and reversible, and reusing the exact recompute the
|
|
6471
6597
|
// parser itself used, so the toggle can never drift from the loaded view.
|
|
6472
6598
|
|
|
6599
|
+
// Both leave the tree exactly as it was, and answer false, when their
|
|
6600
|
+
// layout cannot state every branch: a branch drawn at 0 because a datum is
|
|
6601
|
+
// MISSING says "nothing happened along this branch", which the file never
|
|
6602
|
+
// said. A datum that is stated is stated, whether zero or negative.
|
|
6603
|
+
|
|
6604
|
+
// What a tree's branch lengths MEASURE is stated on the tree (phyloXML's
|
|
6605
|
+
// branch_length_unit), and each layout stamps its own: the scale bar, the
|
|
6606
|
+
// tree's properties and a saved file then all say the same. Divergence is
|
|
6607
|
+
// in substitutions per site; time is in the unit of the tree's own dates,
|
|
6608
|
+
// or plainly "time" where they state none (BEAST heights). The desktop's
|
|
6609
|
+
// words, byte for byte (BranchLengthLayout.distanceUnit).
|
|
6610
|
+
//
|
|
6611
|
+
// It is also how a clock-model tree saved from the Div view is known for
|
|
6612
|
+
// what it is when it is opened again (see captureDivergence).
|
|
6613
|
+
const DIVERGENCE_LENGTH_UNIT = 'subs/site';
|
|
6614
|
+
const CALENDAR_LENGTH_UNIT = 'year';
|
|
6615
|
+
const PLAIN_TIME_LENGTH_UNIT = 'time';
|
|
6616
|
+
|
|
6617
|
+
// The unit of the first dated node that states one, in preorder, children
|
|
6618
|
+
// in file order; null where none does.
|
|
6619
|
+
function dateUnitOf(root) {
|
|
6620
|
+
let unit = null;
|
|
6621
|
+
(function walk(n) {
|
|
6622
|
+
if (unit !== null) {
|
|
6623
|
+
return;
|
|
6624
|
+
}
|
|
6625
|
+
if (n.date && typeof n.date.unit === 'string' && n.date.unit.trim().length > 0) {
|
|
6626
|
+
unit = n.date.unit.trim();
|
|
6627
|
+
return;
|
|
6628
|
+
}
|
|
6629
|
+
let children = n.children;
|
|
6630
|
+
if (children) {
|
|
6631
|
+
for (let i = 0; i < children.length; ++i) {
|
|
6632
|
+
walk(children[i]);
|
|
6633
|
+
}
|
|
6634
|
+
}
|
|
6635
|
+
}(root));
|
|
6636
|
+
return unit;
|
|
6637
|
+
}
|
|
6638
|
+
|
|
6639
|
+
// `phy` may be the tree or its root; only the tree carries a unit.
|
|
6640
|
+
function stampLengthUnit(phy, root, unit) {
|
|
6641
|
+
if (phy && phy !== root && typeof phy === 'object') {
|
|
6642
|
+
phy.branch_length_unit = unit;
|
|
6643
|
+
}
|
|
6644
|
+
}
|
|
6645
|
+
|
|
6646
|
+
function statesDivergenceUnit(phy) {
|
|
6647
|
+
return !!phy && typeof phy.branch_length_unit === 'string'
|
|
6648
|
+
&& phy.branch_length_unit.trim() === DIVERGENCE_LENGTH_UNIT;
|
|
6649
|
+
}
|
|
6650
|
+
|
|
6651
|
+
// Back in TIME a tree gets the lengths it had in time: the ones it
|
|
6652
|
+
// arrived with, kept at load (see captureDivergence). So a file's own
|
|
6653
|
+
// lengths are what is shown, to the digit, where they and its node dates
|
|
6654
|
+
// disagree (carnivore.tree: 1.407 stated on a branch whose two heights are
|
|
6655
|
+
// 1.098 apart). Joint with the desktop (Christian, 2026-09-29). A tree that
|
|
6656
|
+
// kept none -- it did not arrive in time -- is laid out by its date gaps.
|
|
6473
6657
|
forester.applyTimeBranchLengths = function (phy) {
|
|
6474
|
-
|
|
6658
|
+
let root = forester.getTreeRoot(phy);
|
|
6659
|
+
if (root && root._timeLengthsKept === true) {
|
|
6660
|
+
if (!everyLengthWasStated(root)) {
|
|
6661
|
+
return false; // a branch with no kept length: nothing to give back
|
|
6662
|
+
}
|
|
6663
|
+
forester.preOrderTraversalAll(root, function (n) {
|
|
6664
|
+
if (n !== root) { // the root has no branch, and no layout touches what it states
|
|
6665
|
+
n.branch_length = n._timeLength;
|
|
6666
|
+
}
|
|
6667
|
+
});
|
|
6668
|
+
stampLengthUnit(phy, root, dateUnitOf(root) || PLAIN_TIME_LENGTH_UNIT);
|
|
6669
|
+
return true;
|
|
6670
|
+
}
|
|
6671
|
+
if (!root || !everyNodeStates(root, auspiceNodeDate)) {
|
|
6672
|
+
return false;
|
|
6673
|
+
}
|
|
6674
|
+
layOutBelowRoot(root, auspiceNodeDate, timeGapOf(root));
|
|
6675
|
+
stampLengthUnit(phy, root, dateUnitOf(root) || PLAIN_TIME_LENGTH_UNIT);
|
|
6676
|
+
return true;
|
|
6475
6677
|
};
|
|
6476
6678
|
|
|
6477
6679
|
forester.applyDivergenceBranchLengths = function (phy) {
|
|
6478
|
-
|
|
6680
|
+
let root = forester.getTreeRoot(phy);
|
|
6681
|
+
if (!root || !divergenceStatesEveryBranch(root)) {
|
|
6682
|
+
return false;
|
|
6683
|
+
}
|
|
6684
|
+
layOutBelowRoot(root, auspiceNodeDiv, divergenceGap);
|
|
6685
|
+
stampLengthUnit(phy, root, DIVERGENCE_LENGTH_UNIT);
|
|
6686
|
+
return true;
|
|
6479
6687
|
};
|
|
6480
6688
|
|
|
6689
|
+
// The root has no branch to lay out: it keeps the length it has, or none.
|
|
6690
|
+
// Given a length of 0 by the switch, a tree saved from Div stated a root
|
|
6691
|
+
// branch its file never had, and the desktop's copy of it did not.
|
|
6692
|
+
function layOutBelowRoot(root, metricOf, gapOf) {
|
|
6693
|
+
let had = Object.prototype.hasOwnProperty.call(root, 'branch_length');
|
|
6694
|
+
let length = root.branch_length;
|
|
6695
|
+
setDeltaBranchLengths(root, null, metricOf, gapOf);
|
|
6696
|
+
if (had) {
|
|
6697
|
+
root.branch_length = length;
|
|
6698
|
+
} else {
|
|
6699
|
+
delete root.branch_length;
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
6702
|
+
|
|
6703
|
+
function everyNodeStates(root, metricOf) {
|
|
6704
|
+
let every = true;
|
|
6705
|
+
forester.preOrderTraversalAll(root, function (n) {
|
|
6706
|
+
if (every && metricOf(n) === null) {
|
|
6707
|
+
every = false;
|
|
6708
|
+
}
|
|
6709
|
+
});
|
|
6710
|
+
return every;
|
|
6711
|
+
}
|
|
6712
|
+
|
|
6713
|
+
// Recorded on every node (the root too: it is the parent its children are
|
|
6714
|
+
// measured from), or derived from a clock rate on every branch and a
|
|
6715
|
+
// length every one of which the file stated. A tree with neither has no
|
|
6716
|
+
// divergence to lay out: its branch lengths are what they are.
|
|
6717
|
+
function divergenceStatesEveryBranch(root) {
|
|
6718
|
+
if (recordsDivergence(root)) {
|
|
6719
|
+
return everyNodeStates(root, recordedDiv);
|
|
6720
|
+
}
|
|
6721
|
+
return root._divergenceFromRates === true && root._everyLengthStated === true;
|
|
6722
|
+
}
|
|
6723
|
+
|
|
6724
|
+
// Asked at load where it was (captureDivergence): once the switch has been
|
|
6725
|
+
// pressed every branch has a length, whatever the file stated.
|
|
6726
|
+
function everyLengthWasStated(root) {
|
|
6727
|
+
return (root._everyLengthStated === undefined) ? everyBranchStatesLength(root)
|
|
6728
|
+
: (root._everyLengthStated === true);
|
|
6729
|
+
}
|
|
6730
|
+
|
|
6481
6731
|
/**
|
|
6482
|
-
*
|
|
6483
|
-
*
|
|
6484
|
-
*
|
|
6485
|
-
*
|
|
6732
|
+
* Keeps, at load, what the Time | Div switch needs to be lossless:
|
|
6733
|
+
*
|
|
6734
|
+
* - the branch lengths the tree ARRIVED with, where they are its time
|
|
6735
|
+
* (`_timeLength`): on a BEAST clock-model tree always, and on a tree
|
|
6736
|
+
* recording its divergence when its lengths are its date gaps. Back in
|
|
6737
|
+
* Time the tree gets exactly these;
|
|
6738
|
+
* - on a clock-model tree -- a clock rate on every branch -- each node's
|
|
6739
|
+
* cumulative divergence from the root (`_divergence`), summed from
|
|
6740
|
+
* those lengths times each branch's rate.
|
|
6741
|
+
*
|
|
6742
|
+
* Both are private: no writer emits them, and a re-read of our own output
|
|
6743
|
+
* re-derives them. Nothing is kept on a tree with neither a recorded
|
|
6744
|
+
* divergence nor rates: it has no second layout, and the switch is never
|
|
6745
|
+
* offered on it.
|
|
6486
6746
|
*
|
|
6487
|
-
* A tree
|
|
6488
|
-
*
|
|
6489
|
-
*
|
|
6747
|
+
* A tree may ARRIVE showing divergence: it was saved while Div was on
|
|
6748
|
+
* screen. One that records its divergence is known by its lengths, which
|
|
6749
|
+
* are not its date gaps. A clock-model tree is known by what it states --
|
|
6750
|
+
* the unit of its branch lengths is the one the divergence layout stamps
|
|
6751
|
+
* -- since its lengths are not its date gaps even in time. Such a tree
|
|
6752
|
+
* keeps no time lengths (it carries none): its time is laid out from its
|
|
6753
|
+
* dates, and its divergence is each rate times that. Until 2026-09-29 a
|
|
6754
|
+
* clock-model tree's lengths were taken for its time whatever they were,
|
|
6755
|
+
* and one saved from Div came back labelled Time, with a Div of rate x
|
|
6756
|
+
* divergence. Joint with the desktop.
|
|
6490
6757
|
*
|
|
6491
6758
|
* Call once per tree at load, before anything rewrites a branch length.
|
|
6492
6759
|
*
|
|
@@ -6497,77 +6764,152 @@
|
|
|
6497
6764
|
if (!root) {
|
|
6498
6765
|
return;
|
|
6499
6766
|
}
|
|
6767
|
+
root._divergenceFromRates = false;
|
|
6768
|
+
root._timeLengthsKept = false;
|
|
6769
|
+
root._everyLengthStated = everyBranchStatesLength(root);
|
|
6770
|
+
// A tree that RECORDS its divergence states it that way and no other:
|
|
6771
|
+
// none is derived beside it, so a node that records none has none (see
|
|
6772
|
+
// auspiceNodeDiv).
|
|
6773
|
+
let recorded = recordsDivergence(root);
|
|
6774
|
+
// A BEAST clock-model tree states its branch lengths in TIME and each
|
|
6775
|
+
// branch's clock rate beside it, so its divergence is length x rate
|
|
6776
|
+
// (substitutions per site). Only when EVERY branch states a rate: a
|
|
6777
|
+
// partial one would mix substitutions with years along a path.
|
|
6778
|
+
let rates = recorded ? null : everyBranchClockRate(root);
|
|
6779
|
+
if (!recorded && rates === null) {
|
|
6780
|
+
return;
|
|
6781
|
+
}
|
|
6782
|
+
let showsDivergence = rates !== null && statesDivergenceUnit(phy);
|
|
6783
|
+
let keep = (rates !== null && !showsDivergence) || (recorded && branchLengthsAreTime(root));
|
|
6784
|
+
let gapOf = showsDivergence ? timeGapOf(root) : null;
|
|
6500
6785
|
(function walk(node, cumulative) {
|
|
6501
|
-
|
|
6786
|
+
if (keep) {
|
|
6787
|
+
node._timeLength = node.branch_length;
|
|
6788
|
+
}
|
|
6789
|
+
if (rates) {
|
|
6790
|
+
node._divergence = cumulative;
|
|
6791
|
+
}
|
|
6502
6792
|
let children = node.children;
|
|
6503
6793
|
if (children) {
|
|
6504
6794
|
for (let i = 0; i < children.length; ++i) {
|
|
6505
|
-
|
|
6506
|
-
|
|
6795
|
+
// the branch's time: the length it arrived with, or, on
|
|
6796
|
+
// a tree that arrived showing divergence, its date gap
|
|
6797
|
+
let span = children[i].branch_length;
|
|
6798
|
+
if (gapOf) {
|
|
6799
|
+
let d = auspiceNodeDate(children[i]);
|
|
6800
|
+
let pd = auspiceNodeDate(node);
|
|
6801
|
+
span = (d !== null && pd !== null) ? gapOf(d, pd) : 0;
|
|
6802
|
+
}
|
|
6803
|
+
// a span of 0, or a negative one, adds no divergence; a
|
|
6804
|
+
// branch made of pieces (a node deleted above it) adds
|
|
6805
|
+
// each piece's
|
|
6806
|
+
let step = 0;
|
|
6807
|
+
if (rates && Array.isArray(children[i]._pieces)) {
|
|
6808
|
+
step = divergenceOfPieces(children[i]._pieces, rates.get(children[i]));
|
|
6809
|
+
} else if (rates && typeof span === 'number' && isFinite(span) && span > 0) {
|
|
6810
|
+
step = span * rates.get(children[i]);
|
|
6811
|
+
}
|
|
6507
6812
|
walk(children[i], cumulative + step);
|
|
6508
6813
|
}
|
|
6509
6814
|
}
|
|
6510
6815
|
}(root, 0));
|
|
6816
|
+
root._divergenceFromRates = rates !== null;
|
|
6817
|
+
root._timeLengthsKept = keep;
|
|
6511
6818
|
};
|
|
6512
6819
|
|
|
6513
|
-
//
|
|
6514
|
-
//
|
|
6515
|
-
|
|
6820
|
+
// A length of 0, or a negative one, is a length the file stated; one that
|
|
6821
|
+
// is not there is not.
|
|
6822
|
+
function everyBranchStatesLength(root) {
|
|
6823
|
+
let every = true;
|
|
6824
|
+
forester.preOrderTraversalAll(root, function (n) {
|
|
6825
|
+
if (every && n !== root && !(typeof n.branch_length === 'number' && isFinite(n.branch_length))) {
|
|
6826
|
+
every = false;
|
|
6827
|
+
}
|
|
6828
|
+
});
|
|
6829
|
+
return every;
|
|
6830
|
+
}
|
|
6831
|
+
|
|
6832
|
+
// A node's BEAST clock rate (the `rate` annotation) as everyBranchClockRate
|
|
6833
|
+
// reads it: a plain decimal, not negative; else null.
|
|
6834
|
+
function clockRateOf(n) {
|
|
6835
|
+
if (n.properties) {
|
|
6836
|
+
for (let i = 0; i < n.properties.length; ++i) {
|
|
6837
|
+
if (n.properties[i].ref === BEAST_PREFIX + 'rate') {
|
|
6838
|
+
let r = parseBeastNumber(n.properties[i].value);
|
|
6839
|
+
return (r === null || r < 0) ? null : r;
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
}
|
|
6843
|
+
return null;
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6846
|
+
// Each non-root node's BEAST clock rate (the `rate` annotation), or null
|
|
6847
|
+
// unless every branch states a finite, non-negative one. A rate is a plain
|
|
6848
|
+
// decimal number and nothing else (parseBeastNumber): parseFloat reads a
|
|
6849
|
+
// number out of the front of "0.01abc" or "1,5", and a rate nobody wrote
|
|
6850
|
+
// would then scale a branch.
|
|
6851
|
+
function everyBranchClockRate(root) {
|
|
6852
|
+
let rates = new Map();
|
|
6853
|
+
let ok = true;
|
|
6854
|
+
let branches = 0;
|
|
6855
|
+
forester.preOrderTraversalAll(root, function (n) {
|
|
6856
|
+
if (!ok || n === root) {
|
|
6857
|
+
return;
|
|
6858
|
+
}
|
|
6859
|
+
++branches;
|
|
6860
|
+
let r = null;
|
|
6861
|
+
if (n.properties) {
|
|
6862
|
+
for (let i = 0; i < n.properties.length; ++i) {
|
|
6863
|
+
if (n.properties[i].ref === BEAST_PREFIX + 'rate') {
|
|
6864
|
+
r = parseBeastNumber(n.properties[i].value);
|
|
6865
|
+
break;
|
|
6866
|
+
}
|
|
6867
|
+
}
|
|
6868
|
+
}
|
|
6869
|
+
if (r === null || r < 0) {
|
|
6870
|
+
ok = false;
|
|
6871
|
+
return;
|
|
6872
|
+
}
|
|
6873
|
+
rates.set(n, r);
|
|
6874
|
+
});
|
|
6875
|
+
return (ok && branches > 0) ? rates : null;
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
// Whether each picture has any DEPTH: some tip away from the root, in
|
|
6879
|
+
// time and in divergence. A tree whose tips all carry the root's date has
|
|
6880
|
+
// no time picture, and one whose divergence is 0 on every branch (every
|
|
6881
|
+
// rate 0, say) has no divergence picture: a switch to a tree drawn as a
|
|
6882
|
+
// point is not offered. Joint with the desktop (Christian, 2026-09-29:
|
|
6883
|
+
// "Always offer; refuse only zero").
|
|
6516
6884
|
//
|
|
6517
|
-
//
|
|
6518
|
-
//
|
|
6519
|
-
//
|
|
6520
|
-
//
|
|
6521
|
-
// 0.3% of where the other metric puts them. Its branch lengths ARE time --
|
|
6522
|
-
// a BEAST time tree states time -- so a toggle would redraw one picture
|
|
6523
|
-
// twice. A real Nextstrain build, where divergence is genuinely a
|
|
6524
|
-
// different measurement, moves tips by 24.8% of the tree's width.
|
|
6885
|
+
// Until then the switch was offered only where the two pictures differed
|
|
6886
|
+
// by more than 2% of the tree's width at some tip, which refused a strict
|
|
6887
|
+
// clock, whose divergence is its time rescaled. One rule for both
|
|
6888
|
+
// programs replaced a threshold both would have had to justify.
|
|
6525
6889
|
//
|
|
6526
6890
|
// Both metrics are read WITHOUT touching branch_length, so this can be
|
|
6527
|
-
// asked at load without disturbing the tree.
|
|
6528
|
-
|
|
6891
|
+
// asked at load without disturbing the tree. Depth is taken at the tips,
|
|
6892
|
+
// summed along the path as the layout would draw it.
|
|
6893
|
+
function bothPicturesHaveDepth(root) {
|
|
6529
6894
|
let rootDate = auspiceNodeDate(root);
|
|
6530
|
-
let
|
|
6531
|
-
let
|
|
6532
|
-
|
|
6533
|
-
(function walk(n, t, d) {
|
|
6534
|
-
let nt = auspiceNodeDate(n);
|
|
6895
|
+
let timeDepth = 0;
|
|
6896
|
+
let divDepth = 0;
|
|
6897
|
+
(function walk(n, parentDiv, d) {
|
|
6535
6898
|
let nd = auspiceNodeDiv(n);
|
|
6536
|
-
let
|
|
6537
|
-
let dHere = (nd !== null) ? nd : d;
|
|
6899
|
+
let dHere = (n === root || nd === null || parentDiv === null) ? d : (d + divergenceGap(nd, parentDiv));
|
|
6538
6900
|
if (!n.children || n.children.length === 0) {
|
|
6539
|
-
|
|
6540
|
-
|
|
6901
|
+
let nt = auspiceNodeDate(n);
|
|
6902
|
+
if (nt !== null && rootDate !== null) {
|
|
6903
|
+
timeDepth = Math.max(timeDepth, Math.abs(nt - rootDate));
|
|
6541
6904
|
}
|
|
6542
|
-
|
|
6543
|
-
div.push(dHere);
|
|
6905
|
+
divDepth = Math.max(divDepth, dHere);
|
|
6544
6906
|
return;
|
|
6545
6907
|
}
|
|
6546
6908
|
for (let i = 0; i < n.children.length; ++i) {
|
|
6547
|
-
walk(n.children[i],
|
|
6909
|
+
walk(n.children[i], nd, dHere);
|
|
6548
6910
|
}
|
|
6549
|
-
}(root,
|
|
6550
|
-
|
|
6551
|
-
return 0;
|
|
6552
|
-
}
|
|
6553
|
-
let tMax = Math.max.apply(null, time.length > 50000 ? time.slice(0, 50000) : time);
|
|
6554
|
-
let dMax = Math.max.apply(null, div.length > 50000 ? div.slice(0, 50000) : div);
|
|
6555
|
-
if (!(tMax > 0) || !(dMax > 0)) {
|
|
6556
|
-
return 0;
|
|
6557
|
-
}
|
|
6558
|
-
let worst = 0;
|
|
6559
|
-
for (let i = 0; i < time.length; ++i) {
|
|
6560
|
-
worst = Math.max(worst, Math.abs((time[i] / tMax) - (div[i] / dMax)));
|
|
6561
|
-
}
|
|
6562
|
-
return worst;
|
|
6563
|
-
}
|
|
6564
|
-
|
|
6565
|
-
// Below this the two views are the same picture and the switch is noise.
|
|
6566
|
-
// influenza.tree measures 0.003, nextstrain-ncov.json 0.248.
|
|
6567
|
-
const BRANCH_METRIC_SHIFT_MIN = 0.02;
|
|
6568
|
-
|
|
6569
|
-
function divergenceDiffersFromTime(root) {
|
|
6570
|
-
return layoutShiftBetweenMetrics(root) > BRANCH_METRIC_SHIFT_MIN;
|
|
6911
|
+
}(root, null, 0));
|
|
6912
|
+
return timeDepth > 0 && divDepth > 0;
|
|
6571
6913
|
}
|
|
6572
6914
|
|
|
6573
6915
|
// Which metric the CURRENT branch lengths hold -- a different question
|
|
@@ -6578,6 +6920,7 @@
|
|
|
6578
6920
|
function branchLengthsAreTime(root) {
|
|
6579
6921
|
let pairs = 0;
|
|
6580
6922
|
let same = 0;
|
|
6923
|
+
let gapOf = timeGapOf(root);
|
|
6581
6924
|
forester.preOrderTraversalAll(root, function (n) {
|
|
6582
6925
|
if (!n.children) {
|
|
6583
6926
|
return;
|
|
@@ -6594,8 +6937,11 @@
|
|
|
6594
6937
|
continue;
|
|
6595
6938
|
}
|
|
6596
6939
|
++pairs;
|
|
6597
|
-
|
|
6598
|
-
|
|
6940
|
+
// signed, as the time view writes it: a length the file
|
|
6941
|
+
// states negative is its date gap too
|
|
6942
|
+
let gap = gapOf(cv, pv);
|
|
6943
|
+
let scale = Math.max(Math.abs(bl), Math.abs(gap), 1e-9);
|
|
6944
|
+
if (Math.abs(gap - bl) <= (scale * 1e-6)) {
|
|
6599
6945
|
++same;
|
|
6600
6946
|
}
|
|
6601
6947
|
}
|
|
@@ -6603,13 +6949,14 @@
|
|
|
6603
6949
|
return pairs > 0 && (same * 20) >= (pairs * 19);
|
|
6604
6950
|
}
|
|
6605
6951
|
|
|
6606
|
-
//
|
|
6607
|
-
//
|
|
6608
|
-
//
|
|
6609
|
-
//
|
|
6610
|
-
//
|
|
6611
|
-
//
|
|
6612
|
-
//
|
|
6952
|
+
// Which way the tree's dates run: a CALENDAR date increases toward the
|
|
6953
|
+
// tips; a BEAST height runs the other way -- it is an age, largest at the
|
|
6954
|
+
// root. Measured, not assumed, by the majority of the parent-child pairs
|
|
6955
|
+
// that differ (a tie, or none, reads as ages): forester.
|
|
6956
|
+
// convertHeightsToDates turns heights into calendar dates, and a tree it
|
|
6957
|
+
// REFUSED still states ages. The time view takes its gaps in this
|
|
6958
|
+
// direction (timeGapOf), so the few pairs that run AGAINST it come out
|
|
6959
|
+
// negative.
|
|
6613
6960
|
function timeIncreasesTowardTips(root) {
|
|
6614
6961
|
let up = 0;
|
|
6615
6962
|
let pairs = 0;
|
|
@@ -6647,31 +6994,52 @@
|
|
|
6647
6994
|
*/
|
|
6648
6995
|
forester.branchLengthScale = function (phy) {
|
|
6649
6996
|
let root = forester.getTreeRoot(phy);
|
|
6650
|
-
|
|
6997
|
+
if (!root) {
|
|
6998
|
+
return 'divergence';
|
|
6999
|
+
}
|
|
7000
|
+
// A tree that kept the lengths it arrived with is in time exactly
|
|
7001
|
+
// while they are the ones on it (the switch back restores them).
|
|
7002
|
+
if (root._timeLengthsKept === true) {
|
|
7003
|
+
let kept = true;
|
|
7004
|
+
forester.preOrderTraversalAll(root, function (n) {
|
|
7005
|
+
if (n !== root && n.branch_length !== n._timeLength) {
|
|
7006
|
+
kept = false;
|
|
7007
|
+
}
|
|
7008
|
+
});
|
|
7009
|
+
return kept ? 'time' : 'divergence';
|
|
7010
|
+
}
|
|
7011
|
+
return branchLengthsAreTime(root) ? 'time' : 'divergence';
|
|
6651
7012
|
};
|
|
6652
7013
|
|
|
6653
7014
|
/**
|
|
6654
|
-
* True when the tree
|
|
6655
|
-
*
|
|
6656
|
-
*
|
|
6657
|
-
*
|
|
6658
|
-
*
|
|
6659
|
-
*
|
|
7015
|
+
* True when the tree can be laid out by TIME and by DIVERGENCE. The rule
|
|
7016
|
+
* is joint with the desktop (Christian, 2026-09-29):
|
|
7017
|
+
*
|
|
7018
|
+
* - it states a divergence: recorded on its nodes (nextstrain:div), or a
|
|
7019
|
+
* clock rate on every branch (BEAST; divergence is length x rate, and
|
|
7020
|
+
* the lengths are its time). A tree with neither is never offered the
|
|
7021
|
+
* switch, whatever its dates say;
|
|
7022
|
+
* - BOTH layouts can state EVERY branch: every node is dated, every
|
|
7023
|
+
* branch states a length, and every node records its divergence (the
|
|
7024
|
+
* root too) or every branch states a rate. A value that is stated is
|
|
7025
|
+
* stated, whether zero or negative; only an absent one is missing. A
|
|
7026
|
+
* branch is never drawn at 0 because a datum is missing;
|
|
7027
|
+
* - each picture has some depth (bothPicturesHaveDepth).
|
|
7028
|
+
*
|
|
7029
|
+
* Otherwise the switch is not offered and the tree stays in the layout it
|
|
7030
|
+
* arrived in. Whether the two pictures DIFFER is not asked, nor which way
|
|
7031
|
+
* the dates run.
|
|
6660
7032
|
*
|
|
6661
7033
|
* @param phy the tree
|
|
6662
7034
|
* @returns {boolean}
|
|
6663
7035
|
*/
|
|
6664
7036
|
forester.hasTimeAndDivergence = function (phy) {
|
|
6665
7037
|
let root = forester.getTreeRoot(phy);
|
|
6666
|
-
if (!root || !
|
|
7038
|
+
if (!root || !everyNodeStates(root, auspiceNodeDate) || !everyLengthWasStated(root)
|
|
7039
|
+
|| !divergenceStatesEveryBranch(root)) {
|
|
6667
7040
|
return false;
|
|
6668
7041
|
}
|
|
6669
|
-
|
|
6670
|
-
// captured, so it cannot decide this on its own -- it only says a
|
|
6671
|
-
// divergence measure EXISTS. Whether it says anything different is the
|
|
6672
|
-
// shift test, and that is the question worth asking of both kinds of
|
|
6673
|
-
// tree.
|
|
6674
|
-
return auspiceHasAnyDiv(root) && divergenceDiffersFromTime(root);
|
|
7042
|
+
return bothPicturesHaveDepth(root);
|
|
6675
7043
|
};
|
|
6676
7044
|
|
|
6677
7045
|
// A number that is not NaN. It used to test only for null, undefined and
|