archaeopteryx 3.5.0 → 3.5.1

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 CHANGED
@@ -296,8 +296,11 @@ so every branch and position is scored by how far the tip pairs' ancestors
296
296
  fall from that halfway point, and the root goes where that deviation is
297
297
  smallest [1]. It has been compared with other rooting methods on prokaryotic
298
298
  gene families [2]. It needs branch lengths and at least three tips, and is
299
- offered only then. The algorithm is the desktop Archaeopteryx's, and gives
300
- the same roots; it runs in O(n²) time and O(n) memory (the 13,246-tip H5N1
299
+ offered only then. Tip pairs closer than 1/100,000 of the tree's diameter
300
+ (identical sequences, or FastTree's 5 × 10⁻⁹ stand-in for a zero branch) are
301
+ left out of the deviation sums: their deviation measures noise, and summing
302
+ it would swamp the precision of all the others. The algorithm is the desktop
303
+ Archaeopteryx's, and gives the same roots; it runs in O(n²) time and O(n) memory (the 13,246-tip H5N1
301
304
  demo tree roots in 0.4 s, measured in Node).
302
305
 
303
306
  Every internal branch then carries its **MAD value**: the root-mean-square
package/archaeopteryx.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.5.0
23
+ // v 3.5.1
24
24
  // 2026-09-10
25
25
  //
26
26
  // Archaeopteryx.js is a software tool for the visualization and
@@ -103,7 +103,7 @@ function (root, d3, forester, phyloXml) {
103
103
  // IIFE's own function name -- a plain object says what it is.)
104
104
  let archaeopteryx = {};
105
105
 
106
- const VERSION = '3.5.0';
106
+ const VERSION = '3.5.1';
107
107
  const WEBSITE = 'https://cmzmasek.github.io/archaeopteryx-js/';
108
108
  const DESKTOP_WEBSITE = 'https://cmzmasek.github.io/archaeopteryx/';
109
109
  const SOURCE_WEBSITE = 'https://github.com/cmzmasek/archaeopteryx-js';
package/forester.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.5.0
23
+ // v 3.5.1
24
24
  // 2026-09-10
25
25
  //
26
26
  // forester.js is a general suite for dealing with phylogenetic trees.
@@ -455,6 +455,14 @@
455
455
  forester.MAD_CONFIDENCE_TYPE = 'MAD';
456
456
 
457
457
  const MAD_EPSILON = 1e-9;
458
+ // Tip pairs closer than this share of the tree's diameter are left out of
459
+ // every sum, like pairs at distance 0. Their 1/d^2 terms (tips 1e-8 apart,
460
+ // as FastTree's 5e-9 "zero" branches make them, reach 1e16) cancel
461
+ // catastrophically in the subtractions below and swamp every other pair:
462
+ // MAD then answered differently from every rooting, and re-rooting moved
463
+ // the root back and forth. The diameter does not depend on the rooting, so
464
+ // neither does the cut. Joint with the desktop (Christian, 2026-09-14).
465
+ const MAD_NEAR_FRACTION = 1e-5;
458
466
 
459
467
  /**
460
468
  * Roots the tree by Minimal Ancestor Deviation (Tria, Landan & Dagan,
@@ -472,6 +480,11 @@
472
480
  * and the root's branch carries the smallest. Pendant branches get none,
473
481
  * and MAD values from an earlier run are replaced.
474
482
  *
483
+ * Tip pairs closer than 1e-5 of the tree's diameter (identical sequences,
484
+ * FastTree's 5e-9 "zero" branches) are left out of the deviation sums:
485
+ * their deviation measures noise, and their terms would swamp the
486
+ * precision of all the others.
487
+ *
475
488
  * A no-op for fewer than three tips or a tree without branch lengths.
476
489
  * O(n^2) time and O(n) memory: the desktop fills an n x n distance matrix
477
490
  * only to sum its columns (1.4 GB at 13,000 tips), so the column sums are
@@ -513,6 +526,27 @@
513
526
  if (maxDepth <= 0) {
514
527
  return false; // no usable branch lengths
515
528
  }
529
+ // the diameter, the longest tip-to-tip path: at every node, its two
530
+ // longest paths down through different children
531
+ let longestDown = new Float64Array(m);
532
+ let diameter = 0;
533
+ for (let q = 0; q < m; ++q) {
534
+ let k = post[q];
535
+ let first = 0;
536
+ let second = 0;
537
+ for (let c = 0; c < kids[k].length; ++c) {
538
+ let path = longestDown[kids[k][c]] + madLength(pre[kids[k][c]]);
539
+ if (path > first) {
540
+ second = first;
541
+ first = path;
542
+ } else if (path > second) {
543
+ second = path;
544
+ }
545
+ }
546
+ longestDown[k] = first;
547
+ diameter = Math.max(diameter, first + second);
548
+ }
549
+ let nearEps = Math.max(MAD_EPSILON, MAD_NEAR_FRACTION * diameter);
516
550
  let tipDepth = new Float64Array(n);
517
551
  for (let i = 0; i < n; ++i) {
518
552
  tipDepth[i] = depth[tipPos[i]];
@@ -529,7 +563,7 @@
529
563
  let w2 = new Float64Array(m); // sum_{i!=j in subtree} (2*depth[j]/d - 1)^2 (j second)
530
564
  let col0 = new Float64Array(n);
531
565
  let colInv = new Float64Array(n);
532
- let partners = new Int32Array(n); // the tips each tip is apart from (d > MAD_EPSILON)
566
+ let partners = new Int32Array(n); // the tips each tip is counted against (d > nearEps)
533
567
  for (let q = 0; q < m; ++q) {
534
568
  let k = post[q];
535
569
  let ch = kids[k];
@@ -554,7 +588,7 @@
554
588
  for (let j = lo[ch[b]]; j < hi[ch[b]]; ++j) {
555
589
  let dj = tipDepth[j];
556
590
  let dij = (di - dm) + (dj - dm);
557
- if (dij > MAD_EPSILON) {
591
+ if (dij > nearEps) {
558
592
  let inv = 1.0 / dij;
559
593
  let inv2 = inv * inv;
560
594
  let dev = (2.0 * (di - dm) * inv) - 1.0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archaeopteryx",
3
- "version": "3.5.0",
3
+ "version": "3.5.1",
4
4
  "description": "Archaeopteryx.js is a software tool for the visualization and analysis of highly annotated phylogenetic trees.",
5
5
  "main": "archaeopteryx.js",
6
6
  "types": "archaeopteryx.d.ts",