Skip to content

Fix MASTSize() under-counting (root row/column of MAST table never filled) - #219

Merged
ms609 merged 1 commit into
mainfrom
fix/mast-unrooted
Oct 16, 2026
Merged

ms609 merged 1 commit into
mainfrom
fix/mast-unrooted

Conversation

@ms609

@ms609 ms609 commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Root cause

cpp_mast() (src/mast.cpp) fills M[node1, node2] for every pair of non-root nodes, then evaluates the root cell M[root1, root2]. The root recurrence reads M[root1, l2], M[root1, r2], M[l1, root2] and M[r1, root2] ("all of one tree vs one child subtree of the other"), but those entries were never filled, so they stayed 0. A MAST confined to one side of either root was therefore missed.

This is a fault in the rooted algorithm, not in the unrooted reduction: rooting tree1 at one fixed edge and maximizing over every rooting of tree2 is correct given a correct rooted MAST. rooted = FALSE under-counted only when no rooting of tree2 hid the defect.

Rooted example: MASTSize(((a,b),(c,(d,e))), (e,((a,b),(c,d)))) returned 3; the MAST has 4 leaves (drop e).

Fix

Before the root cell, fill M[root1, j] for every non-root j of tree 2 (postorder) and M[i, root2] for every non-root i of tree 1. That is two extra O(n) loops; there are no R changes.

Three expectations in the existing "MAST size calculated correctly on small trees" test (ME(2L, 1), ME(2L, 4), ME(2L, 5)) recorded the buggy values. A brute-force rooted MAST gives 3 for each, so they are now ME(3L, …).

Tests

New test "MASTSize() matches brute force" covers the reported reprex (rooted = FALSE, expect 7), the rooted example above in both argument orders (expect 4), and 12 seeded random pairs (n = 5–7) checked against brute force under rooted = TRUE and FALSE. NEWS.md has a new bullet.

Caveat: the compiled package was not built or tested. The sandbox could not run make (msys NtCreateDirectoryObject access denied) and also refuses to dyn.load DLLs from writable paths. CI will run the first real compiled test.

What I did run instead: a line-for-line R port of cpp_mast(), original and patched, used via R/tree_distance_mast.R:

  • Port fidelity: the original port matched compiled TreeDist 2.15.0 MASTSize() on 300/300 cases.
  • 300 cases (n = 5–9): 60 random rooted pairs (tested rooted and unrooted), plus 60 each of random unrooted, NNI and SPR pairs (unrooted). Compiled 2.15.0 was wrong on 32/300: 23/60 rooted; 7, 1, 0 and 1 in the unrooted groups. The patched port was wrong on 0/300 and symmetric in argument order.
  • tests/testthat/test-mast.R run against the emulation: patched gives 0 failures in 50 expectations; original gives 10 failures (3 old expectations + 7 brute-force checks).

cpp_mast() filled M[i, j] only for non-root nodes, then evaluated the root
cell, which reads M[root1, child of root2] and M[child of root1, root2].
Those entries were left at zero, so a MAST confined to one child subtree
of either root was missed. This affects rooted = TRUE directly and
rooted = FALSE via its max over rootings of tree2.

Corrects three per-rooting expectations in test-mast.R that encoded the
bug (checked against brute force) and adds brute-force regression tests.
@codecov

codecov Bot commented Oct 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.57%. Comparing base (5c9e9eb) to head (4ec1c3a).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #219   +/-   ##
=======================================
  Coverage   97.57%   97.57%           
=======================================
  Files          60       60           
  Lines        6065     6069    +4     
=======================================
+ Hits         5918     5922    +4     
  Misses        147      147           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown

Performance benchmark results

Call Status Change Time (ms)
ClusteringInfoDistance(tr280) ⚪ NSD 2.41% 9.61, 9.79 →
9.42, 9.59
ClusteringInfoDistance(tr50) ⚪ NSD 0.18% 6.81, 6.88 →
6.85, 6.81
LAPJV(test2000) 🟣 ~same -1.8% 98.5, 98.4 →
100, 100
LAPJV(test40) ⚪ NSD 4.71% 0.0182, 0.019 →
0.0178, 0.0179
LAPJV(test400) ⚪ NSD -0.03% 3.26, 3.26 →
3.26, 3.27
MutualClusteringInfo(tr280) ⚪ NSD 10.29% 19.6, 20.3 →
17.6, 17.9
MutualClusteringInfo(tr50) ⚪ NSD 7.85% 18.2, 20.1 →
17.9, 19.4
NNIMaxStep(2000, normalize = TRUE) ⚪ NSD 0.42% 0.214, 0.215 →
0.213, 0.213
NNIMaxStep(300, normalize = TRUE) ⚪ NSD 0.51% 0.0316, 0.0315 →
0.0313, 0.0314
NNIMaxStep(300) ⚪ NSD -0.34% 0.00881, 0.00885 →
0.00889, 0.00883
PathDist(postTrees) ⚪ NSD -4.13% 2.4, 2.43 →
2.59, 2.27
PhylogeneticInfoDistance(tr180) ⚪ NSD 0.41% 59.4, 60.1 →
59.3, 59.7
PhylogeneticInfoDistance(tr50) 🟣 ~same 3.46% 44.2, 44.6 →
42.8, 43
RobinsonFoulds(tr280) ⚪ NSD -1.44% 4.79, 4.87 →
4.91, 4.91
RobinsonFoulds(tr280) ⚪ NSD -1.38% 4.63, 4.67 →
4.71, 4.71
RobinsonFoulds(tr50) ⚪ NSD -1.56% 4.62, 4.56 →
4.65, 4.64

@ms609
ms609 merged commit 088b7b0 into main Oct 16, 2026
21 checks passed
@ms609
ms609 deleted the fix/mast-unrooted branch October 16, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants