archaeopteryx 3.4.0 → 3.5.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 +91 -9
- package/archaeopteryx.d.ts +3 -3
- package/archaeopteryx.js +501 -264
- package/forester.js +652 -9
- package/package.json +1 -1
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.5.0
|
|
24
24
|
// 2026-09-10
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -252,7 +252,9 @@
|
|
|
252
252
|
if (!node) {
|
|
253
253
|
throw ("cannot re-root on null node");
|
|
254
254
|
}
|
|
255
|
-
|
|
255
|
+
// no position: the middle of the branch. 0 is a position -- the root
|
|
256
|
+
// right at the node, where MAD rooting can put it.
|
|
257
|
+
if (typeof branchLength !== 'number' || isNaN(branchLength)) {
|
|
256
258
|
branchLength = -1;
|
|
257
259
|
}
|
|
258
260
|
if (forester.isString(node)) {
|
|
@@ -449,6 +451,510 @@
|
|
|
449
451
|
}
|
|
450
452
|
};
|
|
451
453
|
|
|
454
|
+
/** The confidence type of the per-branch values madRoot records. */
|
|
455
|
+
forester.MAD_CONFIDENCE_TYPE = 'MAD';
|
|
456
|
+
|
|
457
|
+
const MAD_EPSILON = 1e-9;
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Roots the tree by Minimal Ancestor Deviation (Tria, Landan & Dagan,
|
|
461
|
+
* Nature Ecology & Evolution 1, 0193, 2017; doi:10.1038/s41559-017-0193),
|
|
462
|
+
* ported from the desktop's PhylogenyMethods.madRoot.
|
|
463
|
+
*
|
|
464
|
+
* Under a (relaxed) clock the ancestor of two tips is equidistant from
|
|
465
|
+
* both; the pair (i,j) with ancestor a deviates by |2*dist(a,i)/dist(i,j) - 1|.
|
|
466
|
+
* For every branch the root position minimizing the summed squared
|
|
467
|
+
* deviation of all tip pairs is found analytically, and the branch and
|
|
468
|
+
* position with the smallest total become the root.
|
|
469
|
+
*
|
|
470
|
+
* Every internal branch gets a confidence of type 'MAD': the root-mean-
|
|
471
|
+
* square deviation were the root placed on that branch -- low is good,
|
|
472
|
+
* and the root's branch carries the smallest. Pendant branches get none,
|
|
473
|
+
* and MAD values from an earlier run are replaced.
|
|
474
|
+
*
|
|
475
|
+
* A no-op for fewer than three tips or a tree without branch lengths.
|
|
476
|
+
* O(n^2) time and O(n) memory: the desktop fills an n x n distance matrix
|
|
477
|
+
* only to sum its columns (1.4 GB at 13,000 tips), so the column sums are
|
|
478
|
+
* added up here as the pairs are met, and a subtree's tips are a range of
|
|
479
|
+
* tip numbers rather than a list.
|
|
480
|
+
*
|
|
481
|
+
* @param phy the tree
|
|
482
|
+
* @returns {boolean} whether the tree was re-rooted
|
|
483
|
+
*/
|
|
484
|
+
forester.madRoot = function (phy) {
|
|
485
|
+
let root = forester.getTreeRoot(phy);
|
|
486
|
+
if (!root) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
let t = madTraversal(root);
|
|
490
|
+
let pre = t.pre;
|
|
491
|
+
let kids = t.kids;
|
|
492
|
+
let post = t.post;
|
|
493
|
+
let m = pre.length;
|
|
494
|
+
let tipNo = new Int32Array(m).fill(-1);
|
|
495
|
+
let tipPos = [];
|
|
496
|
+
for (let k = 0; k < m; ++k) {
|
|
497
|
+
if (kids[k].length === 0) {
|
|
498
|
+
tipNo[k] = tipPos.length;
|
|
499
|
+
tipPos.push(k);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
let n = tipPos.length;
|
|
503
|
+
if (n < 3) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
// depth (distance from the current root) of every node
|
|
507
|
+
let depth = new Float64Array(m);
|
|
508
|
+
let maxDepth = 0;
|
|
509
|
+
for (let k = 1; k < m; ++k) {
|
|
510
|
+
depth[k] = depth[t.parentOf[k]] + madLength(pre[k]);
|
|
511
|
+
maxDepth = Math.max(maxDepth, depth[k]);
|
|
512
|
+
}
|
|
513
|
+
if (maxDepth <= 0) {
|
|
514
|
+
return false; // no usable branch lengths
|
|
515
|
+
}
|
|
516
|
+
let tipDepth = new Float64Array(n);
|
|
517
|
+
for (let i = 0; i < n; ++i) {
|
|
518
|
+
tipDepth[i] = depth[tipPos[i]];
|
|
519
|
+
}
|
|
520
|
+
// Pass 1 (post-order): the within-subtree deviation sums, and the
|
|
521
|
+
// column sums sum_i 1/d^2 and sum_i 1/d of every tip. For a pair whose
|
|
522
|
+
// common ancestor sits at depth dm, d = depth[i] + depth[j] - 2*dm and
|
|
523
|
+
// the deviation is 2*(depth[i]-dm)/d - 1.
|
|
524
|
+
let lo = new Int32Array(m); // a subtree's tips are the numbers lo..hi-1
|
|
525
|
+
let hi = new Int32Array(m);
|
|
526
|
+
let down = new Float64Array(m); // sum dev^2 over pairs with their ancestor inside the subtree
|
|
527
|
+
let w0 = new Float64Array(m); // sum_{i!=j in subtree} 1/d^2 (ordered)
|
|
528
|
+
let w1 = new Float64Array(m); // sum_{i!=j in subtree} (2*depth[j]/d^2 - 1/d) (j second)
|
|
529
|
+
let w2 = new Float64Array(m); // sum_{i!=j in subtree} (2*depth[j]/d - 1)^2 (j second)
|
|
530
|
+
let col0 = new Float64Array(n);
|
|
531
|
+
let colInv = new Float64Array(n);
|
|
532
|
+
let partners = new Int32Array(n); // the tips each tip is apart from (d > MAD_EPSILON)
|
|
533
|
+
for (let q = 0; q < m; ++q) {
|
|
534
|
+
let k = post[q];
|
|
535
|
+
let ch = kids[k];
|
|
536
|
+
if (ch.length === 0) {
|
|
537
|
+
lo[k] = tipNo[k];
|
|
538
|
+
hi[k] = tipNo[k] + 1;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
let dm = depth[k];
|
|
542
|
+
let dwn = 0, sw0 = 0, sw1 = 0, sw2 = 0;
|
|
543
|
+
for (let c = 0; c < ch.length; ++c) {
|
|
544
|
+
dwn += down[ch[c]];
|
|
545
|
+
sw0 += w0[ch[c]];
|
|
546
|
+
sw1 += w1[ch[c]];
|
|
547
|
+
sw2 += w2[ch[c]];
|
|
548
|
+
}
|
|
549
|
+
// the pairs whose ancestor is this node: one tip from each of two children
|
|
550
|
+
for (let a = 0; a < ch.length; ++a) {
|
|
551
|
+
for (let b = a + 1; b < ch.length; ++b) {
|
|
552
|
+
for (let i = lo[ch[a]]; i < hi[ch[a]]; ++i) {
|
|
553
|
+
let di = tipDepth[i];
|
|
554
|
+
for (let j = lo[ch[b]]; j < hi[ch[b]]; ++j) {
|
|
555
|
+
let dj = tipDepth[j];
|
|
556
|
+
let dij = (di - dm) + (dj - dm);
|
|
557
|
+
if (dij > MAD_EPSILON) {
|
|
558
|
+
let inv = 1.0 / dij;
|
|
559
|
+
let inv2 = inv * inv;
|
|
560
|
+
let dev = (2.0 * (di - dm) * inv) - 1.0;
|
|
561
|
+
dwn += dev * dev;
|
|
562
|
+
sw0 += 2.0 * inv2; // both orderings
|
|
563
|
+
sw1 += (2.0 * (di + dj) * inv2) - (2.0 * inv);
|
|
564
|
+
let gi = (2.0 * di * inv) - 1.0;
|
|
565
|
+
let gj = (2.0 * dj * inv) - 1.0;
|
|
566
|
+
sw2 += (gi * gi) + (gj * gj);
|
|
567
|
+
col0[i] += inv2;
|
|
568
|
+
col0[j] += inv2;
|
|
569
|
+
colInv[i] += inv;
|
|
570
|
+
colInv[j] += inv;
|
|
571
|
+
++partners[i];
|
|
572
|
+
++partners[j];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
lo[k] = lo[ch[0]];
|
|
579
|
+
hi[k] = hi[ch[ch.length - 1]];
|
|
580
|
+
down[k] = dwn;
|
|
581
|
+
w0[k] = sw0;
|
|
582
|
+
w1[k] = sw1;
|
|
583
|
+
w2[k] = sw2;
|
|
584
|
+
}
|
|
585
|
+
// Pass 2 (post-order): per-subtree "all-i" sums a0,a1,a2; the cross
|
|
586
|
+
// sums between a subtree and its complement are then b_k = a_k - w_k.
|
|
587
|
+
let a0 = new Float64Array(m);
|
|
588
|
+
let a1 = new Float64Array(m);
|
|
589
|
+
let a2 = new Float64Array(m);
|
|
590
|
+
let b0 = new Float64Array(m);
|
|
591
|
+
let b1 = new Float64Array(m);
|
|
592
|
+
let b2 = new Float64Array(m);
|
|
593
|
+
for (let q = 0; q < m; ++q) {
|
|
594
|
+
let k = post[q];
|
|
595
|
+
let s0 = 0, s1 = 0, s2 = 0;
|
|
596
|
+
if (kids[k].length === 0) {
|
|
597
|
+
let j = tipNo[k];
|
|
598
|
+
s0 = col0[j];
|
|
599
|
+
s1 = (2.0 * tipDepth[j] * col0[j]) - colInv[j];
|
|
600
|
+
// (2*depth/d - 1)^2 expands to 4*depth^2/d^2 - 4*depth/d + 1: the
|
|
601
|
+
// 1 is owed once per pair actually summed. The desktop adds n - 1,
|
|
602
|
+
// which also counts the pairs at distance 0 (identical sequences)
|
|
603
|
+
// that every other sum skips.
|
|
604
|
+
s2 = (4.0 * tipDepth[j] * tipDepth[j] * col0[j]) - (4.0 * tipDepth[j] * colInv[j]) + partners[j];
|
|
605
|
+
} else {
|
|
606
|
+
for (let c = 0; c < kids[k].length; ++c) {
|
|
607
|
+
s0 += a0[kids[k][c]];
|
|
608
|
+
s1 += a1[kids[k][c]];
|
|
609
|
+
s2 += a2[kids[k][c]];
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
a0[k] = s0;
|
|
613
|
+
a1[k] = s1;
|
|
614
|
+
a2[k] = s2;
|
|
615
|
+
b0[k] = s0 - w0[k];
|
|
616
|
+
b1[k] = s1 - w1[k];
|
|
617
|
+
b2[k] = s2 - w2[k];
|
|
618
|
+
}
|
|
619
|
+
// Pass 3 (pre-order): up[k] = the deviation sum of the pairs whose
|
|
620
|
+
// ancestor lies outside subtree k, by the rerooting recursion, so
|
|
621
|
+
// every branch's total is O(1).
|
|
622
|
+
let up = new Float64Array(m);
|
|
623
|
+
let atParent = new Float64Array(m); // a branch's cross deviation with the root at its parent
|
|
624
|
+
for (let k = 0; k < m; ++k) {
|
|
625
|
+
let ch = kids[k];
|
|
626
|
+
if (ch.length === 0) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
let sumDown = 0;
|
|
630
|
+
let sumAtParent = 0;
|
|
631
|
+
for (let c = 0; c < ch.length; ++c) {
|
|
632
|
+
let x = ch[c];
|
|
633
|
+
sumDown += down[x];
|
|
634
|
+
atParent[x] = madCross(depth[x], b0[x], b1[x], b2[x], madLength(pre[x]));
|
|
635
|
+
sumAtParent += atParent[x];
|
|
636
|
+
}
|
|
637
|
+
// cross deviation among all the groups meeting here: the child subtrees and the complement
|
|
638
|
+
let own = (k === 0) ? 0.0 : madCross(depth[k], b0[k], b1[k], b2[k], 0.0);
|
|
639
|
+
let crossAmong = (sumAtParent + own) / 2.0;
|
|
640
|
+
for (let c = 0; c < ch.length; ++c) {
|
|
641
|
+
let x = ch[c];
|
|
642
|
+
up[x] = up[k] + (sumDown - down[x]) + (crossAmong - atParent[x]);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// The branch and position with the smallest total deviation, and every
|
|
646
|
+
// branch's smallest MAD value keyed by the tips on its far side, so
|
|
647
|
+
// the value finds its branch again after the re-root has turned
|
|
648
|
+
// parents into children. The first minimum in post-order wins, as on
|
|
649
|
+
// the desktop.
|
|
650
|
+
let hash = madTipHashes(n);
|
|
651
|
+
let nPairs = (n * (n - 1.0)) / 2.0;
|
|
652
|
+
let madByKey = new Map();
|
|
653
|
+
let bestSsd = Infinity;
|
|
654
|
+
let best = -1;
|
|
655
|
+
let bestX = 0;
|
|
656
|
+
for (let q = 0; q < m; ++q) {
|
|
657
|
+
let c = post[q];
|
|
658
|
+
if (c === 0) {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
let length = madLength(pre[c]);
|
|
662
|
+
// the optimal root position, as a distance from c toward its parent, kept on the branch
|
|
663
|
+
let x = (b0[c] > MAD_EPSILON) ? (depth[c] - (b1[c] / (2.0 * b0[c]))) : 0.0;
|
|
664
|
+
if (x < 0) {
|
|
665
|
+
x = 0;
|
|
666
|
+
} else if (x > length) {
|
|
667
|
+
x = length;
|
|
668
|
+
}
|
|
669
|
+
let ssd = madCross(depth[c], b0[c], b1[c], b2[c], x) + down[c] + up[c];
|
|
670
|
+
// a sum of squares; clamp a tiny negative left by cancellation, so sqrt is never NaN
|
|
671
|
+
let mad = Math.sqrt(Math.max(0.0, ssd) / nPairs);
|
|
672
|
+
let key = hash.key(hi[c] - lo[c], hash.xa[hi[c]] ^ hash.xa[lo[c]], hash.xb[hi[c]] ^ hash.xb[lo[c]], lo[c] === 0);
|
|
673
|
+
let prev = madByKey.get(key);
|
|
674
|
+
if (prev === undefined || mad < prev) {
|
|
675
|
+
madByKey.set(key, mad); // the two halves of a bifurcating root share a key
|
|
676
|
+
}
|
|
677
|
+
if (ssd < bestSsd) {
|
|
678
|
+
bestSsd = ssd;
|
|
679
|
+
best = c;
|
|
680
|
+
bestX = x;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (best < 0) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
let tipNumber = new Map();
|
|
687
|
+
for (let i = 0; i < n; ++i) {
|
|
688
|
+
tipNumber.set(pre[tipPos[i]], i);
|
|
689
|
+
}
|
|
690
|
+
forester.removeMadConfidences(phy);
|
|
691
|
+
forester.reRoot(phy, pre[best], bestX);
|
|
692
|
+
// annotate the internal branches of the re-rooted tree
|
|
693
|
+
let r = madTraversal(forester.getTreeRoot(phy));
|
|
694
|
+
let count = new Int32Array(r.pre.length);
|
|
695
|
+
let xa = new Int32Array(r.pre.length);
|
|
696
|
+
let xb = new Int32Array(r.pre.length);
|
|
697
|
+
let hasFirst = new Uint8Array(r.pre.length);
|
|
698
|
+
for (let q = 0; q < r.post.length; ++q) {
|
|
699
|
+
let k = r.post[q];
|
|
700
|
+
let ch = r.kids[k];
|
|
701
|
+
if (ch.length === 0) {
|
|
702
|
+
let i = tipNumber.get(r.pre[k]);
|
|
703
|
+
count[k] = 1;
|
|
704
|
+
xa[k] = hash.xa[i + 1] ^ hash.xa[i];
|
|
705
|
+
xb[k] = hash.xb[i + 1] ^ hash.xb[i];
|
|
706
|
+
hasFirst[k] = (i === 0) ? 1 : 0;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
for (let c = 0; c < ch.length; ++c) {
|
|
710
|
+
count[k] += count[ch[c]];
|
|
711
|
+
xa[k] ^= xa[ch[c]];
|
|
712
|
+
xb[k] ^= xb[ch[c]];
|
|
713
|
+
hasFirst[k] |= hasFirst[ch[c]];
|
|
714
|
+
}
|
|
715
|
+
if (k !== 0) {
|
|
716
|
+
let mad = madByKey.get(hash.key(count[k], xa[k], xb[k], hasFirst[k] === 1));
|
|
717
|
+
if (mad !== undefined) {
|
|
718
|
+
let kept = r.pre[k].confidences || [];
|
|
719
|
+
r.pre[k].confidences = kept.concat([{value: mad, type: forester.MAD_CONFIDENCE_TYPE}]);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return true;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Removes every 'MAD' confidence (from madRoot) and keeps all others: a
|
|
728
|
+
* tree rooted any other way no longer has the rooting those values rate.
|
|
729
|
+
*
|
|
730
|
+
* @param phy the tree
|
|
731
|
+
*/
|
|
732
|
+
forester.removeMadConfidences = function (phy) {
|
|
733
|
+
let root = forester.getTreeRoot(phy);
|
|
734
|
+
if (!root) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
madTraversal(root).pre.forEach(function (node) {
|
|
738
|
+
if (node.confidences && node.confidences.some(function (c) { return c.type === forester.MAD_CONFIDENCE_TYPE; })) {
|
|
739
|
+
// a new array: re-rooting can leave two branches sharing one
|
|
740
|
+
let kept = node.confidences.filter(function (c) { return c.type !== forester.MAD_CONFIDENCE_TYPE; });
|
|
741
|
+
if (kept.length > 0) {
|
|
742
|
+
node.confidences = kept;
|
|
743
|
+
} else {
|
|
744
|
+
delete node.confidences;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Whether a node carries data about the node itself, the kind a
|
|
752
|
+
* re-rooting can take the meaning away from: a name, taxonomy, sequence
|
|
753
|
+
* (a domain architecture lives there), events, distribution, date,
|
|
754
|
+
* references, or a property about the node. Not data in this sense: the
|
|
755
|
+
* branch above it -- length, support and MAD values, colour, width, a
|
|
756
|
+
* property applying to the branch -- which re-rooting carries along,
|
|
757
|
+
* visual styling (style: properties), the viewer's own aptx: properties,
|
|
758
|
+
* and an empty taxonomy. The desktop's list is the same.
|
|
759
|
+
*
|
|
760
|
+
* @param node
|
|
761
|
+
* @returns {boolean}
|
|
762
|
+
*/
|
|
763
|
+
forester.nodeHasData = function (node) {
|
|
764
|
+
let filled = function (x) {
|
|
765
|
+
return x !== undefined && x !== null && x !== '' && !(Array.isArray(x) && x.length === 0);
|
|
766
|
+
};
|
|
767
|
+
if ((typeof node.name === 'string' && node.name.length > 0)
|
|
768
|
+
|| (node.taxonomies && node.taxonomies.some(function (t) {
|
|
769
|
+
return t && Object.keys(t).some(function (key) { return filled(t[key]); });
|
|
770
|
+
}))
|
|
771
|
+
|| (node.sequences && node.sequences.length > 0)
|
|
772
|
+
|| node.events
|
|
773
|
+
|| (node.distributions && node.distributions.length > 0)
|
|
774
|
+
|| node.date
|
|
775
|
+
|| (node.references && node.references.length > 0)) {
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
return !!node.properties && node.properties.some(function (p) {
|
|
779
|
+
return p.applies_to !== BRANCH_EVENT_APPLIES_TO
|
|
780
|
+
&& !(typeof p.ref === 'string' && (p.ref.indexOf('style:') === 0 || p.ref.indexOf('aptx:') === 0));
|
|
781
|
+
});
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* What a re-rooting would do to the data on internal nodes, worked out on
|
|
786
|
+
* a bare copy so the tree itself is untouched: `annotated`, the internal
|
|
787
|
+
* nodes carrying data (nodeHasData), and `changed`, those among them whose
|
|
788
|
+
* clade -- the tips below -- the re-rooting changes. Those lie between
|
|
789
|
+
* the old root and the new one (an old two-child root disappears); every
|
|
790
|
+
* other node keeps its tips. Midpoint and MAD rooting find their root on
|
|
791
|
+
* the copy first. Nothing is copied when no internal node carries data.
|
|
792
|
+
*
|
|
793
|
+
* @param phy the tree
|
|
794
|
+
* @param method 'mad', 'midpoint', or 'node': the root on the branch above `node`
|
|
795
|
+
* @param node the node, for 'node'
|
|
796
|
+
* @returns {{annotated: Array, changed: Array}} nodes of `phy`
|
|
797
|
+
*/
|
|
798
|
+
forester.cladesChangedByRerooting = function (phy, method, node) {
|
|
799
|
+
let t = madTraversal(forester.getTreeRoot(phy));
|
|
800
|
+
let annotated = [];
|
|
801
|
+
for (let k = 0; k < t.pre.length; ++k) {
|
|
802
|
+
if (t.kids[k].length > 0 && forester.nodeHasData(t.pre[k])) {
|
|
803
|
+
annotated.push(t.pre[k]);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (annotated.length === 0) {
|
|
807
|
+
return {annotated: annotated, changed: []};
|
|
808
|
+
}
|
|
809
|
+
// the copy: shape and branch lengths only, each copy knowing its original
|
|
810
|
+
let copyOf = new Map();
|
|
811
|
+
let tipNumber = new Map();
|
|
812
|
+
for (let k = 0; k < t.pre.length; ++k) {
|
|
813
|
+
let c = {branch_length: t.pre[k].branch_length, original: t.pre[k]};
|
|
814
|
+
if (t.kids[k].length > 0) {
|
|
815
|
+
c.children = [];
|
|
816
|
+
} else {
|
|
817
|
+
tipNumber.set(t.pre[k], tipNumber.size);
|
|
818
|
+
}
|
|
819
|
+
if (k > 0) {
|
|
820
|
+
c.parent = copyOf.get(t.pre[t.parentOf[k]]);
|
|
821
|
+
c.parent.children.push(c);
|
|
822
|
+
}
|
|
823
|
+
copyOf.set(t.pre[k], c);
|
|
824
|
+
}
|
|
825
|
+
let copy = {children: [copyOf.get(t.pre[0])]};
|
|
826
|
+
copy.children[0].parent = copy;
|
|
827
|
+
if (method === 'mad') {
|
|
828
|
+
forester.madRoot(copy);
|
|
829
|
+
} else if (method === 'midpoint') {
|
|
830
|
+
forester.midpointRoot(copy);
|
|
831
|
+
} else {
|
|
832
|
+
forester.reRoot(copy, copyOf.get(node), -1);
|
|
833
|
+
}
|
|
834
|
+
// the tips below every node, before and after, as a count and two hashes
|
|
835
|
+
let hash = madTipHashes(tipNumber.size);
|
|
836
|
+
let cladeKeys = function (traversal, originalOf) {
|
|
837
|
+
let count = new Int32Array(traversal.pre.length);
|
|
838
|
+
let xa = new Int32Array(traversal.pre.length);
|
|
839
|
+
let xb = new Int32Array(traversal.pre.length);
|
|
840
|
+
let keys = new Map();
|
|
841
|
+
for (let q = 0; q < traversal.post.length; ++q) {
|
|
842
|
+
let k = traversal.post[q];
|
|
843
|
+
let ch = traversal.kids[k];
|
|
844
|
+
if (ch.length === 0) {
|
|
845
|
+
let i = tipNumber.get(originalOf(traversal.pre[k]));
|
|
846
|
+
count[k] = 1;
|
|
847
|
+
xa[k] = hash.xa[i + 1] ^ hash.xa[i];
|
|
848
|
+
xb[k] = hash.xb[i + 1] ^ hash.xb[i];
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
for (let c = 0; c < ch.length; ++c) {
|
|
852
|
+
count[k] += count[ch[c]];
|
|
853
|
+
xa[k] ^= xa[ch[c]];
|
|
854
|
+
xb[k] ^= xb[ch[c]];
|
|
855
|
+
}
|
|
856
|
+
let original = originalOf(traversal.pre[k]);
|
|
857
|
+
if (original) { // not the copy's new root node
|
|
858
|
+
keys.set(original, count[k] + ':' + xa[k] + ':' + xb[k]);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return keys;
|
|
862
|
+
};
|
|
863
|
+
let before = cladeKeys(t, function (n) { return n; });
|
|
864
|
+
let after = cladeKeys(madTraversal(forester.getTreeRoot(copy)), function (c) { return c.original; });
|
|
865
|
+
return {
|
|
866
|
+
annotated: annotated,
|
|
867
|
+
changed: annotated.filter(function (n) { return after.get(n) !== before.get(n); })
|
|
868
|
+
};
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
function madLength(node) {
|
|
872
|
+
return node.branch_length > 0 ? node.branch_length : 0;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// The sum of squared cross-pair deviations between subtree c and its
|
|
876
|
+
// complement, with the root at distance x from c toward its parent. With
|
|
877
|
+
// K = 2*(x - depth[c]) a pair deviates by K/d + (2*depth[j]/d - 1), so the
|
|
878
|
+
// sum is K^2*b0 + 2*K*b1 + b2.
|
|
879
|
+
function madCross(depthC, b0, b1, b2, x) {
|
|
880
|
+
let k = 2.0 * (x - depthC);
|
|
881
|
+
return (k * k * b0) + (2.0 * k * b1) + b2;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Pre-order (the root at 0, parents before children) and post-order,
|
|
885
|
+
// children left to right, as node positions -- without recursion, since
|
|
886
|
+
// a caterpillar tree nests as deep as it has tips.
|
|
887
|
+
function madTraversal(root) {
|
|
888
|
+
let pre = [];
|
|
889
|
+
let parentOf = [];
|
|
890
|
+
let stack = [root];
|
|
891
|
+
let stackParent = [-1];
|
|
892
|
+
while (stack.length > 0) {
|
|
893
|
+
let node = stack.pop();
|
|
894
|
+
let p = stackParent.pop();
|
|
895
|
+
let k = pre.length;
|
|
896
|
+
pre.push(node);
|
|
897
|
+
parentOf.push(p);
|
|
898
|
+
if (node.children) {
|
|
899
|
+
for (let i = node.children.length - 1; i >= 0; --i) {
|
|
900
|
+
stack.push(node.children[i]);
|
|
901
|
+
stackParent.push(k);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
let kids = pre.map(function () {
|
|
906
|
+
return [];
|
|
907
|
+
});
|
|
908
|
+
for (let k = 1; k < pre.length; ++k) {
|
|
909
|
+
kids[parentOf[k]].push(k); // siblings come in left to right
|
|
910
|
+
}
|
|
911
|
+
// the mirrored pre-order, reversed, is the post-order left to right
|
|
912
|
+
let post = [];
|
|
913
|
+
let st = [0];
|
|
914
|
+
while (st.length > 0) {
|
|
915
|
+
let k = st.pop();
|
|
916
|
+
post.push(k);
|
|
917
|
+
for (let c = 0; c < kids[k].length; ++c) {
|
|
918
|
+
st.push(kids[k][c]);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
post.reverse();
|
|
922
|
+
return {pre: pre, parentOf: parentOf, kids: kids, post: post};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// A set of tips as a key that does not depend on the rooting: its size
|
|
926
|
+
// and two 32-bit XOR hashes of its tips, taken on the side WITHOUT tip 0
|
|
927
|
+
// so both sides of a branch give the same key. xa/xb are prefix XORs, so
|
|
928
|
+
// the tips lo..hi-1 hash to xa[hi] ^ xa[lo].
|
|
929
|
+
function madTipHashes(n) {
|
|
930
|
+
let xa = new Int32Array(n + 1);
|
|
931
|
+
let xb = new Int32Array(n + 1);
|
|
932
|
+
let s = 0x2545f491;
|
|
933
|
+
let next = function () { // a fixed seed: the same tree always hashes the same
|
|
934
|
+
s = (s + 0x9e3779b9) | 0;
|
|
935
|
+
let z = s;
|
|
936
|
+
z = Math.imul(z ^ (z >>> 16), 0x85ebca6b);
|
|
937
|
+
z = Math.imul(z ^ (z >>> 13), 0xc2b2ae35);
|
|
938
|
+
return z ^ (z >>> 16);
|
|
939
|
+
};
|
|
940
|
+
for (let i = 0; i < n; ++i) {
|
|
941
|
+
xa[i + 1] = xa[i] ^ next();
|
|
942
|
+
xb[i + 1] = xb[i] ^ next();
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
xa: xa,
|
|
946
|
+
xb: xb,
|
|
947
|
+
key: function (size, ha, hb, holdsFirst) {
|
|
948
|
+
if (holdsFirst) {
|
|
949
|
+
size = n - size;
|
|
950
|
+
ha ^= xa[n];
|
|
951
|
+
hb ^= xb[n];
|
|
952
|
+
}
|
|
953
|
+
return size + ':' + ha + ':' + hb;
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
452
958
|
forester.getFurthestDescendant = function (node) {
|
|
453
959
|
let children = forester.getAllExternalNodes(node);
|
|
454
960
|
let farthest = null;
|
|
@@ -1691,6 +2197,8 @@
|
|
|
1691
2197
|
properties.longestNodeName = 0;
|
|
1692
2198
|
properties.branchLengths = false;
|
|
1693
2199
|
properties.confidences = false;
|
|
2200
|
+
// MAD values (madRoot) rate root positions, not clades: never support
|
|
2201
|
+
properties.madValues = false;
|
|
1694
2202
|
// the largest confidence value seen -- how a caller tells a
|
|
1695
2203
|
// posterior-probability tree (max <= 1) from a bootstrap tree
|
|
1696
2204
|
properties.maxConfidence = 0;
|
|
@@ -1803,8 +2311,12 @@
|
|
|
1803
2311
|
}
|
|
1804
2312
|
}
|
|
1805
2313
|
if (n.confidences && n.confidences.length > 0) {
|
|
1806
|
-
properties.confidences = true;
|
|
1807
2314
|
for (let ci = 0; ci < n.confidences.length; ++ci) {
|
|
2315
|
+
if (n.confidences[ci].type === forester.MAD_CONFIDENCE_TYPE) {
|
|
2316
|
+
properties.madValues = true;
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
properties.confidences = true;
|
|
1808
2320
|
let cv = n.confidences[ci].value;
|
|
1809
2321
|
if (typeof cv === 'number' && isFinite(cv) && cv > properties.maxConfidence) {
|
|
1810
2322
|
properties.maxConfidence = cv;
|
|
@@ -3470,11 +3982,17 @@
|
|
|
3470
3982
|
nh += ":" + node.branch_length;
|
|
3471
3983
|
}
|
|
3472
3984
|
}
|
|
3473
|
-
|
|
3985
|
+
// the support slot holds support: a MAD value (madRoot) never goes
|
|
3986
|
+
// there -- it would read as support, and would crowd out the
|
|
3987
|
+
// bootstrap on a branch carrying both. phyloXML keeps it, typed.
|
|
3988
|
+
let support = writeConfidences && node.confidences
|
|
3989
|
+
? node.confidences.filter(function (c) { return c.type !== forester.MAD_CONFIDENCE_TYPE; })
|
|
3990
|
+
: [];
|
|
3991
|
+
if (support.length === 1 && support[0].value !== undefined && support[0].value !== null) {
|
|
3474
3992
|
if (decPointsMax && decPointsMax > 0) {
|
|
3475
|
-
nh += "[" + forester.roundNumber(
|
|
3993
|
+
nh += "[" + forester.roundNumber(support[0].value, decPointsMax) + "]";
|
|
3476
3994
|
} else {
|
|
3477
|
-
nh += "[" +
|
|
3995
|
+
nh += "[" + support[0].value + "]";
|
|
3478
3996
|
}
|
|
3479
3997
|
}
|
|
3480
3998
|
if (!last) {
|
|
@@ -3978,6 +4496,98 @@
|
|
|
3978
4496
|
// becomes "meta:" plus the header with its whitespace as '_' -- the
|
|
3979
4497
|
// display name prettifies that back to spaces, so "Collection Date"
|
|
3980
4498
|
// stays "Collection Date" in every menu.
|
|
4499
|
+
// The tips' data as a table, one row per tip in the order given, header
|
|
4500
|
+
// first: what the node menu's "Download Ext. Node Data" writes. The
|
|
4501
|
+
// columns and their names are the desktop's (NodeDataExporter.toNodeDataTsv),
|
|
4502
|
+
// so the two programs write the same table: name (always), the first
|
|
4503
|
+
// taxonomy's scientific name, common name, code, id and rank, the first
|
|
4504
|
+
// sequence's name, gene name, symbol, accession and type, the branch
|
|
4505
|
+
// length, then one column per property ref, sorted, holding its first
|
|
4506
|
+
// value. A column no tip has a value for is left out. When the tip names
|
|
4507
|
+
// cannot key the rows (one blank or repeated), a node_id column comes
|
|
4508
|
+
// first, from idOf(tip, index) or the row number. Tabs and line breaks
|
|
4509
|
+
// inside a value become spaces. A property keeps its ref as the header,
|
|
4510
|
+
// so the table joins back onto a tree with joinMetadataTable.
|
|
4511
|
+
forester.externalNodeDataTable = function (tips, idOf) {
|
|
4512
|
+
tips = tips || [];
|
|
4513
|
+
if (tips.length === 0) {
|
|
4514
|
+
return {columns: [], rows: []};
|
|
4515
|
+
}
|
|
4516
|
+
let clean = function (v) {
|
|
4517
|
+
return (v === undefined || v === null) ? '' : String(v).replace(/[\t\n\r]/g, ' ');
|
|
4518
|
+
};
|
|
4519
|
+
let tax = function (n) {
|
|
4520
|
+
return (n.taxonomies && n.taxonomies[0]) || {};
|
|
4521
|
+
};
|
|
4522
|
+
let seq = function (n) {
|
|
4523
|
+
return (n.sequences && n.sequences[0]) || {};
|
|
4524
|
+
};
|
|
4525
|
+
let cols = [];
|
|
4526
|
+
let add = function (name, extract, force) {
|
|
4527
|
+
let vals = tips.map(function (n, i) {
|
|
4528
|
+
return clean(extract(n, i));
|
|
4529
|
+
});
|
|
4530
|
+
if (force || vals.some(function (v) { return v.length > 0; })) {
|
|
4531
|
+
cols.push({name: name, vals: vals});
|
|
4532
|
+
}
|
|
4533
|
+
};
|
|
4534
|
+
let seen = new Set();
|
|
4535
|
+
let unique = tips.every(function (n) {
|
|
4536
|
+
if (!n.name || seen.has(n.name)) {
|
|
4537
|
+
return false;
|
|
4538
|
+
}
|
|
4539
|
+
seen.add(n.name);
|
|
4540
|
+
return true;
|
|
4541
|
+
});
|
|
4542
|
+
if (!unique) {
|
|
4543
|
+
add('node_id', function (n, i) { return idOf ? idOf(n, i) : i + 1; }, true);
|
|
4544
|
+
}
|
|
4545
|
+
add('name', function (n) { return n.name; }, true);
|
|
4546
|
+
add('taxonomy_scientific_name', function (n) { return tax(n).scientific_name; });
|
|
4547
|
+
add('taxonomy_common_name', function (n) { return tax(n).common_name; });
|
|
4548
|
+
add('taxonomy_code', function (n) { return tax(n).code; });
|
|
4549
|
+
add('taxonomy_id', function (n) { return tax(n).id && tax(n).id.value; });
|
|
4550
|
+
add('taxonomy_rank', function (n) { return tax(n).rank; });
|
|
4551
|
+
add('sequence_name', function (n) { return seq(n).name; });
|
|
4552
|
+
add('gene_name', function (n) { return seq(n).gene_name; });
|
|
4553
|
+
add('sequence_symbol', function (n) { return seq(n).symbol; });
|
|
4554
|
+
add('sequence_accession', function (n) { return seq(n).accession && seq(n).accession.value; });
|
|
4555
|
+
add('sequence_type', function (n) { return seq(n).type; });
|
|
4556
|
+
add('branch_length', function (n) { return (typeof n.branch_length === 'number') ? n.branch_length : ''; });
|
|
4557
|
+
let refs = new Set();
|
|
4558
|
+
tips.forEach(function (n) {
|
|
4559
|
+
(n.properties || []).forEach(function (p) {
|
|
4560
|
+
if (p.ref) {
|
|
4561
|
+
refs.add(p.ref);
|
|
4562
|
+
}
|
|
4563
|
+
});
|
|
4564
|
+
});
|
|
4565
|
+
Array.from(refs).sort().forEach(function (ref) {
|
|
4566
|
+
add(ref, function (n) {
|
|
4567
|
+
let p = (n.properties || []).filter(function (q) { return q.ref === ref; })[0];
|
|
4568
|
+
return p ? p.value : '';
|
|
4569
|
+
});
|
|
4570
|
+
});
|
|
4571
|
+
return {
|
|
4572
|
+
columns: cols.map(function (c) { return c.name; }),
|
|
4573
|
+
rows: tips.map(function (n, i) {
|
|
4574
|
+
return cols.map(function (c) { return c.vals[i]; });
|
|
4575
|
+
})
|
|
4576
|
+
};
|
|
4577
|
+
};
|
|
4578
|
+
|
|
4579
|
+
// externalNodeDataTable as tab-separated text, header line first; empty
|
|
4580
|
+
// for no tips.
|
|
4581
|
+
forester.externalNodeDataTsv = function (tips, idOf) {
|
|
4582
|
+
let t = forester.externalNodeDataTable(tips, idOf);
|
|
4583
|
+
if (t.columns.length === 0) {
|
|
4584
|
+
return '';
|
|
4585
|
+
}
|
|
4586
|
+
return [t.columns].concat(t.rows).map(function (r) {
|
|
4587
|
+
return r.join('\t');
|
|
4588
|
+
}).join('\n') + '\n';
|
|
4589
|
+
};
|
|
4590
|
+
|
|
3981
4591
|
forester.metadataColumnRef = function (header, index) {
|
|
3982
4592
|
let h = String(header || '').trim();
|
|
3983
4593
|
if (h.length === 0) {
|
|
@@ -4135,7 +4745,13 @@
|
|
|
4135
4745
|
const SEARCH_DOMAIN = textField('Domain', n => searchSeqs(n).reduce((a, s) => a.concat((s.domain_architecture && s.domain_architecture.domains) ? s.domain_architecture.domains.map(d => d.name) : []), []).filter(Boolean));
|
|
4136
4746
|
const SEARCH_ANNOTATION = textField('Annotation', n => searchSeqs(n).reduce((a, s) => a.concat((s.annotations || []).reduce((b, an) => b.concat([an.desc, an.ref]), [])), []).filter(Boolean), {anyText: true});
|
|
4137
4747
|
const SEARCH_CROSS_REFERENCE = textField('Cross-Reference', n => searchSeqs(n).reduce((a, s) => a.concat((s.cross_references || []).reduce((b, x) => b.concat([x.value, x.source, x.comment]), [])), []).filter(Boolean), {anyText: true});
|
|
4138
|
-
|
|
4748
|
+
// The residues: the phyloXML and Nexus readers give mol_seq as {value,
|
|
4749
|
+
// is_aligned}, and a hand-built tree may carry the plain string. Reading
|
|
4750
|
+
// the object itself compared every query against "[object Object]", so
|
|
4751
|
+
// this field never matched a real file (its test built the string form).
|
|
4752
|
+
const SEARCH_MOLECULAR_SEQUENCE = textField('Molecular Sequence', n => searchSeqs(n).map(function (s) {
|
|
4753
|
+
return (s.mol_seq && typeof s.mol_seq === 'object') ? s.mol_seq.value : s.mol_seq;
|
|
4754
|
+
}).filter(Boolean), {suggest: false});
|
|
4139
4755
|
// in menu order
|
|
4140
4756
|
const SEARCH_TEXT_FIELDS = [
|
|
4141
4757
|
SEARCH_NODE_NAME, SEARCH_TAXONOMY_SCIENTIFIC_NAME, SEARCH_TAXONOMY_COMMON_NAME, SEARCH_TAXONOMY_CODE,
|
|
@@ -4160,7 +4776,8 @@
|
|
|
4160
4776
|
}, {suggest: false});
|
|
4161
4777
|
|
|
4162
4778
|
const SEARCH_BRANCH_LENGTH = numericField('Branch Length', n => (typeof n.branch_length === 'number') ? [n.branch_length] : []);
|
|
4163
|
-
|
|
4779
|
+
// support only: a MAD value rates a root position, not the clade
|
|
4780
|
+
const SEARCH_CONFIDENCE = numericField('Confidence', n => n.confidences ? n.confidences.filter(c => c.type !== forester.MAD_CONFIDENCE_TYPE).map(c => c.value).filter(v => typeof v === 'number') : []);
|
|
4164
4781
|
const SEARCH_CLADE_SIZE = numericField('Clade Size (tips)', n => [n._srchClade], {metrics: true});
|
|
4165
4782
|
const SEARCH_CHILD_COUNT = numericField('Number of Children', n => [n.children ? n.children.length : 0]);
|
|
4166
4783
|
const SEARCH_DEPTH = numericField('Depth from Root', n => [n._srchDepth], {metrics: true});
|
|
@@ -4299,7 +4916,7 @@
|
|
|
4299
4916
|
if (!hasBL && typeof n.branch_length === 'number' && n.branch_length >= 0) hasBL = true;
|
|
4300
4917
|
if (!hasConf && n.confidences) {
|
|
4301
4918
|
for (let i = 0; i < n.confidences.length; ++i) {
|
|
4302
|
-
if (typeof n.confidences[i].value === 'number') { hasConf = true; break; }
|
|
4919
|
+
if (typeof n.confidences[i].value === 'number' && n.confidences[i].type !== forester.MAD_CONFIDENCE_TYPE) { hasConf = true; break; }
|
|
4303
4920
|
}
|
|
4304
4921
|
}
|
|
4305
4922
|
if (n.properties) {
|
|
@@ -4747,6 +5364,32 @@
|
|
|
4747
5364
|
'calendar year': 1, 'calendar years': 1
|
|
4748
5365
|
};
|
|
4749
5366
|
|
|
5367
|
+
/**
|
|
5368
|
+
* Whether the tree's branch lengths are time: most of its internal nodes
|
|
5369
|
+
* carry a date, and at least two do -- BEAST node heights, Nextstrain
|
|
5370
|
+
* dates, phyloXML <date>s on ancestors. Tip dates alone do not make one:
|
|
5371
|
+
* a divergence tree can carry collection dates, and re-rooting it (as a
|
|
5372
|
+
* root-to-tip regression does) is a normal step. A time tree is never
|
|
5373
|
+
* re-rooted: a new root would contradict the ancestors' dates. The
|
|
5374
|
+
* desktop's rule is the same (its detectTimeTree, internal-node half).
|
|
5375
|
+
*
|
|
5376
|
+
* @param phy the tree
|
|
5377
|
+
* @returns {boolean}
|
|
5378
|
+
*/
|
|
5379
|
+
forester.isTimeTree = function (phy) {
|
|
5380
|
+
let internal = 0;
|
|
5381
|
+
let dated = 0;
|
|
5382
|
+
forester.preOrderTraversalAll(forester.getTreeRoot(phy), function (n) {
|
|
5383
|
+
if (n.children && n.children.length > 0) {
|
|
5384
|
+
++internal;
|
|
5385
|
+
if (n.date && typeof n.date.value === 'number' && isFinite(n.date.value)) {
|
|
5386
|
+
++dated;
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
});
|
|
5390
|
+
return dated >= 2 && dated * 2 > internal;
|
|
5391
|
+
};
|
|
5392
|
+
|
|
4750
5393
|
// Everything the viewer needs to decide about and draw a time axis:
|
|
4751
5394
|
// {type: 'geologic'|'calendar'|null, rootAge, presentDate, dated,
|
|
4752
5395
|
// hasInternalIntervals, hasExternalIntervals}. rootAge (geologic) and
|