Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .AGENTS/memory/Optimizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -1020,3 +1020,34 @@ pay; what pays is what the parent overlaps let a candidate *skip*.
double matrix once the fill is no longer dominant.
- Next: depth-2 pair candidates (two leaves) by the same route; the
unbounded MCI could use the Tier 1 form if tests tolerate ulp changes.

## NNIDist() all pairs in one C++ call (2026-10-30)

- `NNIDist(trees)` (one list, all pairs) went through `CompareAll()` ->
`.NNIDistSingle()`, which per pair ran `RenumberTips()`, two `Postorder()`
calls and `cpp_nni_distance()`; on GHA ~85% of the time was this R overhead.
- Now `.NNIDistAllPairs()` renumbers and postorders each tree once and calls
`cpp_nni_distance_all_pairs()`, which computes each tree's splits once
(under its own rootedness) and per pair runs only the matching and scoring.
Shared core: `nni_tree()`, `nni_edge_to_splits()`, `nni_rf_matching()`,
`nni_score()`, also used by `cpp_nni_distance()`.
- Identity: a pair is scored under the *first* tree's rootedness (as before),
so when two trees with equal edge counts differ in rootedness (polytomies),
the second tree's splits are recomputed for that pair. Output `identical()`
to `CompareAll(trees, NNIDist)` on all 945 7-leaf trees, unrooted 7-leaf,
random 60-200 leaves with shuffled labels, 3 leaves, polytomies, mixed
rootedness.
- Release installs, min of N, single thread, R-devel (2026-06-29) on Windows,
same machine/session (main via `git archive origin/main`):

| Workload | main | PR |
|---|---|---|
| 945 × 7 leaves (`as.phylo(0:944, 7)`) | 136.7 s (n = 1) | 0.23 s (n = 3) |
| 100 × 50 leaves | 1.80 s (n = 3) | 0.040 s (n = 10) |
| 40 × 200 leaves | 0.86 s (n = 3) | 0.110 s (n = 10) |

Main's 945-tree time is inflated by `CompareAll()` indexing a multiPhylo
(PR #227); GHA measured 46 s on R 4.6, against ~7 s for the bare C++ calls.
- Next: at 200 leaves the O(s^2 · bins) split matching now dominates.
Canonicalizing splits (tip-1 bit clear) and sorting each tree's splits once
would allow a linear merge per pair.
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@

## Performance

- `NNIDist()` on a single list of trees prepares each tree once and scores
all pairs in one C++ call, rather than renumbering and reordering both
trees for every pair: c. 8–45× faster on 40–100 trees, and 945 seven-leaf
trees take 0.2 s.

- Trees with different leaf sets are pruned to their shared leaves in C++,
without constructing pruned trees in R: `TreeDistance()` on 150 gene trees
is c. 15× faster, with lower memory use (#123).
Expand Down
4 changes: 4 additions & 0 deletions R/RcppExports.R
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ cpp_nni_distance <- function(edge1, edge2, nTip) {
.Call(`_TreeDist_cpp_nni_distance`, edge1, edge2, nTip)
}

cpp_nni_distance_all_pairs <- function(edges, nTip) {
.Call(`_TreeDist_cpp_nni_distance_all_pairs`, edges, nTip)
}

min_subtree_entropy <- function(n) {
.Call(`_TreeDist_min_subtree_entropy`, n)
}
Expand Down
38 changes: 35 additions & 3 deletions R/tree_distance_nni.R
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,48 @@
#' @family tree distances
#' @export
NNIDist <- function(tree1, tree2 = tree1) {
.TreeDistance(.NNIDistSingle, tree1, tree2)
if (!inherits(tree1, "phylo") && length(tree1) > 1 &&
identical(tree1, tree2)) {
.NNIDistAllPairs(tree1)
} else {
.TreeDistance(.NNIDistSingle, tree1, tree2)
}
}

.StopEdgeMismatch <- function() {
stop("Both trees must have the same number of edges. ",
"Is one rooted and the other unrooted?")
}

#' @importFrom TreeTools Postorder RenumberTips TipLabels
#' @importFrom stats setNames
.NNIDistAllPairs <- function(trees) {
labelList <- lapply(trees, TipLabels)
.CheckLabelsSame(labelList)
tipLabels <- as.character(labelList[[1]])
nTip <- length(tipLabels)
.CheckMaxTips(nTip)
edges <- lapply(trees, function(tree) {
Postorder(RenumberTips(tree, tipLabels)[["edge"]])
})
if (length(unique(vapply(edges, nrow, integer(1)))) != 1) {
.StopEdgeMismatch()
}
bounds <- cpp_nni_distance_all_pairs(edges, nTip)

# Return:
lapply(setNames(nm = rownames(bounds)), function(bound) {
structure(bounds[bound, ], Size = length(trees), Labels = names(trees),
Diag = FALSE, Upper = TRUE, class = "dist")
})
}

#' @importFrom TreeTools Postorder RenumberTips
#' @importFrom ape Nnode.phylo
.NNIDistSingle <- function(tree1, tree2, nTip, ...) {
.CheckMaxTips(nTip)
if (nrow(tree1[["edge"]]) != nrow(tree2[["edge"]])) {
stop("Both trees must have the same number of edges. ",
"Is one rooted and the other unrooted?")
.StopEdgeMismatch()
}
tree2 <- RenumberTips(tree2, as.character(tree1[["tip.label"]]))

Expand Down
13 changes: 13 additions & 0 deletions src/RcppExports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,18 @@ BEGIN_RCPP
return rcpp_result_gen;
END_RCPP
}
// cpp_nni_distance_all_pairs
IntegerMatrix cpp_nni_distance_all_pairs(const List& edges, const IntegerVector& nTip);
RcppExport SEXP _TreeDist_cpp_nni_distance_all_pairs(SEXP edgesSEXP, SEXP nTipSEXP) {
BEGIN_RCPP
Rcpp::RObject rcpp_result_gen;
Rcpp::RNGScope rcpp_rngScope_gen;
Rcpp::traits::input_parameter< const List& >::type edges(edgesSEXP);
Rcpp::traits::input_parameter< const IntegerVector& >::type nTip(nTipSEXP);
rcpp_result_gen = Rcpp::wrap(cpp_nni_distance_all_pairs(edges, nTip));
return rcpp_result_gen;
END_RCPP
}
// min_subtree_entropy
Rcpp::List min_subtree_entropy(const int n);
RcppExport SEXP _TreeDist_min_subtree_entropy(SEXP nSEXP) {
Expand Down Expand Up @@ -896,6 +908,7 @@ static const R_CallMethodDef CallEntries[] = {
{"_TreeDist_lapjv", (DL_FUNC) &_TreeDist_lapjv, 2},
{"_TreeDist_cpp_mast", (DL_FUNC) &_TreeDist_cpp_mast, 3},
{"_TreeDist_cpp_nni_distance", (DL_FUNC) &_TreeDist_cpp_nni_distance, 3},
{"_TreeDist_cpp_nni_distance_all_pairs", (DL_FUNC) &_TreeDist_cpp_nni_distance_all_pairs, 2},
{"_TreeDist_min_subtree_entropy", (DL_FUNC) &_TreeDist_min_subtree_entropy, 1},
{"_TreeDist_nni_max_step", (DL_FUNC) &_TreeDist_nni_max_step, 2},
{"_TreeDist_cpp_mutual_clustering_all_pairs", (DL_FUNC) &_TreeDist_cpp_mutual_clustering_all_pairs, 3},
Expand Down
Loading
Loading