Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
656bdb2
Modernize NNI c++
ms609 Sep 25, 2025
d9c1ac5
()
ms609 Sep 25, 2025
2359adb
Assertions
ms609 Sep 25, 2025
1e81d27
Initial switch to HybridBuffer
ms609 Sep 25, 2025
da0c646
int32
ms609 Sep 26, 2025
2965a16
max tips 8192
ms609 Sep 26, 2025
b230f7f
maxTips <- 32768
ms609 Sep 26, 2025
437dfd8
Write out macros
ms609 Sep 26, 2025
3b8e0bf
Merge branch 'main' into nni-memcheck
ms609 Sep 26, 2025
c994bb4
pointers to values
ms609 Sep 26, 2025
5f4cd34
ptrdiff safety
ms609 Sep 26, 2025
de9fbc8
Test heap (nocov online)
ms609 Sep 26, 2025
8698116
Update nni_distance.cpp
ms609 Sep 26, 2025
835983a
AllSplitPairings tests
ms609 Sep 26, 2025
8d7040c
Merge branch 'main' into nni-memcheck
ms609 Sep 26, 2025
ad4b150
Nocov start
ms609 Sep 26, 2025
ac3df76
Use memcheck template (only)
ms609 Sep 26, 2025
4a8d3dd
Complier-safe cacheing?
ms609 Sep 26, 2025
bfdc41f
NOT_TRIVIAL to int32
ms609 Sep 26, 2025
8ac10a7
flatten tmp_splits vector
ms609 Sep 26, 2025
aa210ab
cache
ms609 Sep 26, 2025
b608692
explicit!
ms609 Sep 26, 2025
c8e191a
alignas(T) T stack_[StackSize]{};
ms609 Sep 26, 2025
0fd50a4
Try benchmark on CHK completion only
ms609 Sep 26, 2025
b793822
using splitbit = uint64_t;
ms609 Sep 26, 2025
950b48c
expectation order
ms609 Sep 26, 2025
ee12d99
rcout for zero return
ms609 Sep 26, 2025
673104d
Checks
ms609 Sep 26, 2025
8757ae5
Already zero-initialized
ms609 Sep 26, 2025
ef0ce7b
ASSERT((i % SL_BIN_SIZE) < 64); // 1 << 64 = 0
ms609 Sep 26, 2025
ea88cfc
static_cast<int32> / size_t
ms609 Sep 26, 2025
faf547a
use int16s
ms609 Sep 26, 2025
b184ba4
not fast32
ms609 Sep 26, 2025
55b46db
ASSERT
ms609 Sep 26, 2025
515160e
hard-code int32_t
ms609 Sep 26, 2025
f59fab6
Calve out long number vector
ms609 Sep 26, 2025
9fbdef2
casts
ms609 Sep 26, 2025
9da2bfa
supernova fix
ms609 Sep 26, 2025
141be23
rm checks
ms609 Sep 26, 2025
931294b
Rm cluttery asserts
ms609 Sep 26, 2025
edc290d
Version: 2.10.1.9003
ms609 Sep 26, 2025
10d8b06
+rwty
ms609 Sep 26, 2025
d606e94
Merge branch 'main' into nni-memcheck
ms609 Sep 26, 2025
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
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ revdep
^\.travis\.yml$
^\.zenodo\.json$
^\.covrignore$
^\.vs.*$
^\.vscode.*$
^CRAN-SUBMISSION$
6 changes: 5 additions & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ name: Benchmark

on:
workflow_dispatch:
pull_request:
workflow_dispatch:
workflow_run:
workflows: ["R-CMD-check"]
types:
- completed
paths:
- "src/**"
- "R/**"
Expand Down
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: TreeDist
Type: Package
Title: Calculate and Map Distances Between Phylogenetic Trees
Version: 2.10.1.9002
Version: 2.10.1.9003
Authors@R: c(person("Martin R.", "Smith",
email = "martin.smith@durham.ac.uk",
role = c("aut", "cre", "cph", "prg"),
Expand Down
3 changes: 2 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# TreeDist 2.10.1.9002 (development)
# TreeDist 2.10.1.9003 (development)

- `HierarchicalMutualInformation()` calculates the information shared between
pairs of hierarchical partition structures <doi:10.1103/PhysRevE.92.062825>.
- Support larger trees in NNI distance calculations.
- Fix crash in `robinson_foulds_all_pairs()` and `RobinsonFoulds(list)`.
- Fix bug in calculation of `MutualClusteringInfo()`: greedy optimization
was not guaranteed to find globally optimal matching, causing distances to be
Expand Down
8 changes: 3 additions & 5 deletions cran-comments.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@
## Downstream dependencies

["revdepcheck"](https://github.com/ms609/TreeDist/actions/workflows/revdepcheck.yml)
confirmed no changes to worse in the three downstream dependencies:
confirmed no changes to worse in the four downstream dependencies:

'Rogue', 'rwty', 'TBRDist', and 'TreeSearch'

'Rogue', 'TBRDist', and 'TreeSearch'

(all of which I maintain).


## R CMD check results
There were no ERRORs or WARNINGs.
Expand Down
8 changes: 4 additions & 4 deletions src/RcppExports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,14 @@ BEGIN_RCPP
END_RCPP
}
// cpp_nni_distance
IntegerVector cpp_nni_distance(const IntegerMatrix edge1, const IntegerMatrix edge2, const IntegerVector nTip);
IntegerVector cpp_nni_distance(const IntegerMatrix& edge1, const IntegerMatrix& edge2, const IntegerVector& nTip);
RcppExport SEXP _TreeDist_cpp_nni_distance(SEXP edge1SEXP, SEXP edge2SEXP, SEXP nTipSEXP) {
BEGIN_RCPP
Rcpp::RObject rcpp_result_gen;
Rcpp::RNGScope rcpp_rngScope_gen;
Rcpp::traits::input_parameter< const IntegerMatrix >::type edge1(edge1SEXP);
Rcpp::traits::input_parameter< const IntegerMatrix >::type edge2(edge2SEXP);
Rcpp::traits::input_parameter< const IntegerVector >::type nTip(nTipSEXP);
Rcpp::traits::input_parameter< const IntegerMatrix& >::type edge1(edge1SEXP);
Rcpp::traits::input_parameter< const IntegerMatrix& >::type edge2(edge2SEXP);
Rcpp::traits::input_parameter< const IntegerVector& >::type nTip(nTipSEXP);
rcpp_result_gen = Rcpp::wrap(cpp_nni_distance(edge1, edge2, nTip));
return rcpp_result_gen;
END_RCPP
Expand Down
8 changes: 2 additions & 6 deletions src/ints.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@

using int16 = int_fast16_t;
using int32 = int_fast32_t;
using uint16 = uint_fast16_t;
using grf_match = std::vector<int> ;

constexpr uint16 INT_16_MAX = INT_FAST16_MAX;
constexpr uint16 UINT_16_MAX = UINT_FAST16_MAX;
constexpr int16 NA_INT16 = -0x7fff;
using uint32 = uint_fast32_t;
using grf_match = std::vector<int>;

#endif
10 changes: 10 additions & 0 deletions src/li_diameters.h

Large diffs are not rendered by default.

505 changes: 280 additions & 225 deletions src/nni_distance.cpp

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/tree_distances.h
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ namespace TreeDist {

// Returns lg2_unrooted[x] - lg2_trees_matching_split(y, x - y)
[[nodiscard]] inline double mmsi_pair_score(const int16 x, const int16 y) noexcept {
assert(SL_MAX_TIPS + 2 <= INT_16_MAX); // verify int16 ok
assert(SL_MAX_TIPS + 2 <= std::numeric_limits<int16>::max()); // verify int16 ok

return lg2_unrooted[x] - (lg2_rooted[y] + lg2_rooted[x - y]);
}
Expand Down Expand Up @@ -417,7 +417,7 @@ namespace TreeDist {
}

[[nodiscard]] inline double one_overlap(const int16 a, const int16 b, const int16 n) noexcept {
assert(SL_MAX_TIPS + 2 <= INT_16_MAX); // verify int16 ok
assert(SL_MAX_TIPS + 2 <= std::numeric_limits<int16>::max()); // verify int16 ok
if (a == b) {
return lg2_rooted[a] + lg2_rooted[n - a];
} else if (a < b) {
Expand All @@ -428,7 +428,7 @@ namespace TreeDist {
}

[[nodiscard]] inline double one_overlap_notb(const int16 a, const int16 n_minus_b, const int16 n) noexcept {
assert(SL_MAX_TIPS + 2 <= INT_16_MAX); // verify int16 ok
assert(SL_MAX_TIPS + 2 <= std::numeric_limits<int16>::max()); // verify int16 ok
const int16 b = n - n_minus_b;
if (a == b) {
return lg2_rooted[b] + lg2_rooted[n_minus_b];
Expand Down
73 changes: 40 additions & 33 deletions tests/testthat/test-tree_distance_nni.R
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@ test_that("NNIDist() handles exceptions", {
})

test_that("NNIDist() at NNI_MAX_TIPS", {
maxTips <- 2048
maxTips <- 32768
more <- maxTips + 1
expect_error(.NNIDistSingle(PectinateTree(more), BalancedTree(more), more),
"so many tips")
goingQuickly <- TRUE
skip_if(goingQuickly)

heapTips <- 16384 + 1
skip_if_not_installed("testthat", "3.2.2")
expect_no_error(.NNIDistSingle(PectinateTree(heapTips),
BalancedTree(heapTips), heapTips))

n <- .NNIDistSingle(PectinateTree(maxTips), BalancedTree(maxTips),
maxTips)
expect_gt(n[["best_upper"]], n[["best_lower"]])
Expand All @@ -27,9 +38,6 @@ test_that("NNIDist() at NNI_MAX_TIPS", {
expect_gte(n[["loose_upper"]], n[["best_upper"]])
expect_gte(n[["fack_upper"]], n[["best_upper"]])
expect_gte(n[["li_upper"]], n[["best_upper"]])
more <- maxTips + 1
expect_error(.NNIDistSingle(PectinateTree(more), BalancedTree(more), more),
"so many tips")
})

test_that("Simple NNI approximations", {
Expand Down Expand Up @@ -64,7 +72,7 @@ test_that("Simple NNI approximations", {
best_upper = 10L, loose_upper = 18L, fack_upper = 18L,
li_upper = Li(5))

Test <- function(expect, tree) {
Test <- function(tree, expect) {
expectation <- rep(NA_integer_, 7L)
names(expectation) <- c("lower", "best_lower", "tight_upper", "best_upper",
"loose_upper", "fack_upper", "li_upper")
Expand All @@ -76,56 +84,56 @@ test_that("Simple NNI approximations", {
expectation["loose_upper"] <- min(expect[c("fack_upper", "li_upper")])
}

expect_equal(expectation, NNIDist(tree1, tree))
expect_equal(NNIDist(tree1, tree), expectation)
for (i in c(2L, 3L, 4L, 6L)) {
tree1i <- RootOnNode(tree1, i)
j <- 0
for (t2 in unique(lapply(1:9, RootOnNode, tree = tree))) {
expect_equal(expectation, NNIDist(tree1i, t2))
expect_equal(NNIDist(tree1i, t2), expectation)
}
}
}

expect_equal(allMatched, NNIDist(BalancedTree(2), PectinateTree(2)))
expect_equal(NNIDist(BalancedTree(2), PectinateTree(2)), allMatched)

expect_equal(oneUnmatched, cpp_nni_distance(edge1, edge2, NTip(tree1)))
Test(oneUnmatched, PectinateTree(nTip))
expect_equal(cpp_nni_distance(edge1, edge2, NTip(tree1)), oneUnmatched)
Test(PectinateTree(nTip), oneUnmatched)

# Identical trees
tree1 <- Postorder(read.tree(text = "(((a, b), (c, d)), ((e, f), (g, h)));"))
tree2 <- Postorder(read.tree(text = "(((a, b), (d, c)), ((h, g), (f, e)));"))
Test(allMatched, tree1)
Test(allMatched, tree2)
Test(tree1, allMatched)
Test(tree2, allMatched)

# Tree names
output <- NNIDist(list(bal = tree1, pec = tree2),
as.phylo(0:2, tipLabels = letters[1:8]))
expect_equal(rownames(output), c("bal", "pec"))

# Only root edge is different
Test(oneUnmatched,
Postorder(ape::read.tree(text="(((a, b), (e, f)), ((c, d), (g, h)));")))
Test(Postorder(ape::read.tree(text="(((a, b), (e, f)), ((c, d), (g, h)));")),
oneUnmatched)

# Two separate regions of difference one
Test(oneUnmatched * 2,
read.tree(text="((((a, b), c), d), (e, (f, (g, h))));"))
Test(read.tree(text="((((a, b), c), d), (e, (f, (g, h))));"),
oneUnmatched * 2)

# One region of three unmatched edges
Test(c(lower = 3L, tight_upper = 5L, fack_upper = 8L, li_upper = Li(3)),
read.tree(text="(((a, e), (c, d)), ((b, f), (g, h)));"))
Test(read.tree(text="(((a, e), (c, d)), ((b, f), (g, h)));"),
c(lower = 3L, tight_upper = 5L, fack_upper = 8L, li_upper = Li(3)))

# One region of four unmatched edges
Test(c(lower = 4L, tight_upper = 7L, fack_upper = 14L, li_upper = Li(4)),
tree2 <- ape::read.tree(text="(((a, e), (f, d)), ((b, c), (g, h)));"))
Test(ape::read.tree(text="(((a, e), (f, d)), ((b, c), (g, h)));"),
c(lower = 4L, tight_upper = 7L, fack_upper = 14L, li_upper = Li(4)))

# One region of five unmatched edges
Test(fiveUnmatched,
ape::read.tree(text="(((a, e), (f, d)), ((b, g), (c, h)));"))
Test(ape::read.tree(text="(((a, e), (f, d)), ((b, g), (c, h)));"),
fiveUnmatched)

# Trees with different leaves at root
tree1 <- PectinateTree(1:8)
Test(fiveUnmatched,
ape::read.tree(text = "(3, ((5, 6), (7, (1, (2, (4, 8))))));"))
# Trees with different leaves at root.
tree1 <- PectinateTree(1:8) # used in Test()
Test(ape::read.tree(text = "(3, ((5, 6), (7, (1, (2, (4, 8))))));"),
fiveUnmatched)

# Too different for tight upper bound
expect_true(is.na(NNIDist(BalancedTree(100),
Expand Down Expand Up @@ -164,28 +172,27 @@ test_that("NNI with lists of trees", {
test_that("NNIDiameter() is sane", {

exacts <- NNIDiameter(3:12)
expect_equal(exacts, do.call(rbind, NNIDiameter(lapply(3:12, as.integer))))
expect_equal(do.call(rbind, NNIDiameter(lapply(3:12, as.integer))), exacts)
expect_true(all(exacts[, "min"] <= exacts[, "exact"]))
expect_true(all(exacts[, "max"] >= exacts[, "exact"]))
expect_true(is.na(NNIDiameter(13)[, "exact"]))
expect_true(is.na(NNIDiameter(1)[, "exact"]))
expect_equal(c(exact = 10L), NNIDiameter(BalancedTree(8))[, "exact"])
expect_equal(NNIDiameter(BalancedTree(8))[, "exact"], c(exact = 10L))

FackMin <- function(n) ceiling(0.25 * lfactorial(n) / log(2))
exacts <- c(0, 0, 0, 1, 3, 5, 7, 10, 12, 15, 18, 21)
liMaxes <- c(0, 1, 3, 5, 8, 13, 16, 21, 25, 31, 37, 43, 47, 53, 59, 65)
FackMax <- function(n) n*ceiling(log2(n)) + n - (2 * ceiling(log2(n)))
FackMax <- function(n) n * ceiling(log2(n)) + n - (2 * ceiling(log2(n)))
n <- 4:8
expect_equal(cbind(
expect_equal(NNIDiameter(n), cbind(
liMin = n - 3L,
fackMin = FackMin(n - 2L),
min = pmax(n - 3L, FackMin(4:8 - 2L)),
exact = exacts[n],
liMax = liMaxes[n],
fackMax = FackMax(n - 2L),
max = pmin(liMaxes[n], FackMax(n - 2L))
), NNIDiameter(n))
))

expect_equal(NNIDiameter(c(6, 6)), NNIDiameter(as.phylo(0:1, 6)))

expect_equal(NNIDiameter(as.phylo(0:1, 6)), NNIDiameter(c(6, 6)))
})
Loading