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
12 changes: 7 additions & 5 deletions .github/workflows/R-CMD-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
- name: Set up R dependencies
uses: r-lib/actions/setup-r-dependencies@v2
with:
cache-version: 2-${{ runner.arch }}
cache-version: 3-${{ runner.arch }}
needs: |
check

Expand Down Expand Up @@ -159,15 +159,17 @@ jobs:
run: |
install.packages("remotes")
remotes::install_cran("rcmdcheck")
remotes::install_cran("tinytex")
tinytex::install_tinytex()
if (Sys.info()["machine"] != "x86_64" || Sys.info()["sysname"] != "Darwin") {
remotes::install_cran("tinytex")
tinytex::install_tinytex()
}
shell: Rscript {0}

- name: Set up R dependencies (Windows)
if: runner.os == 'Windows'
uses: r-lib/actions/setup-r-dependencies@v2
with:
cache-version: 2-${{ runner.arch }}
cache-version: 3-${{ runner.arch }}
needs: |
check
coverage
Expand All @@ -176,7 +178,7 @@ jobs:
if: runner.os != 'Windows'
uses: r-lib/actions/setup-r-dependencies@v2
with:
cache-version: 2-${{ runner.arch }}
cache-version: 3-${{ runner.arch }}
needs: |
check

Expand Down
1 change: 0 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,4 @@ importFrom(ape,write.tree)
importFrom(graphics,legend)
importFrom(graphics,par)
importFrom(graphics,plot)
importFrom(viridisLite,viridis)
useDynLib(Quartet, .registration = TRUE)
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
- `TripletDistance()`, `PairsTripletDistance()`, and `AllPairsTripletDistance()`
now use the CPDT algorithm of Jansson & Rajaby (2017) instead of the tqDist
file-based backend, giving a ~100× speedup for triplet-distance calculations.
- Quartet distances no longer abort with "Leaves don't agree" when a tree has
a unifurcating root; the bundled tqDist tree reader treated the redundant
root as a spurious extra leaf
([#64](https://github.com/ms609/Quartet/issues/64)).
- Require R 3.6, dropping dependency on `viridisLite`.

# Quartet v1.3.0 (2026-03-19)
Expand Down
2 changes: 1 addition & 1 deletion man/VisualizeQuartets.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions src/unrooted_tree.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,25 @@ typedef struct UnrootedTree
RootedTree* convertToRootedTree(RootedTreeFactory *oldFactory)
{
UnrootedTree *t = this;
UnrootedTree *avoid = nullptr;

// Make sure the root is not a leaf
// (unless there are only 2 elements, in which case we can't avoid it)
// (unless there are only 2 elements, in which case we can't avoid it).
// A degree-1 root also arises from a unifurcating root, e.g. Newick
// "(((a,b),(c,d)));". We reroot at its single neighbour and record
// the original root in `avoid` so the recursion does not descend back
// into it; otherwise the dummy node would be emitted as a spurious
// extra leaf, triggering "Leaves don't agree" (Quartet issue #64).
if (isLeaf())
{
t = edges.front();
avoid = this;
}

RootedTreeFactory *factory = new RootedTreeFactory(oldFactory);
// Pass nullptr as parent — no writes to shared UnrootedTree nodes,
// Pass `avoid` as parent — no writes to shared UnrootedTree nodes,
// making this function safe to call concurrently on the same tree.
RootedTree *rooted = t->convertToRootedTreeImpl(factory, nullptr);
RootedTree *rooted = t->convertToRootedTreeImpl(factory, avoid);

return rooted;
}
Expand Down
49 changes: 49 additions & 0 deletions tests/testthat/test-1-tqdist.R
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,53 @@ test_that(".TreeToEdge()", {
Quartet:::.TreeToEdge.phylo(RenumberTips(BalancedTree(5), paste0("t", 5:1))),
Quartet:::.TreeToEdge.phylo(BalancedTree(5), paste0("t", 5:1))
)
})

test_that("Unifurcating roots are handled (#64)", {
skip_on_cran()
library("TreeTools", quietly = TRUE, warn.conflicts = FALSE)

writeTree <- function (text) {
file <- tempfile(fileext = ".tree")
writeLines(text, file)
file
}

# Minimum working example from the issue: identical topology, one with a
# unifurcating root. The bundled tqDist reader previously treated the
# redundant root as a spurious extra leaf and aborted.
f1 <- writeTree("((8,3),(5,6));")
f2 <- writeTree("(((3,8),(5,6)));")
expect_equal(QuartetDistance(f1, f2), 0)
expect_equal(QuartetDistance(f2, f1), 0)
expect_equal(QuartetAgreement(f1, f2), c(1, 0))
# The low-level Rcpp wrapper goes straight to the tqDist reader
expect_equal(tqdist_QuartetDistance(f1, f2), 0)
expect_equal(tqdist_TripletDistance(f1, f2), 0)

# A genuinely non-zero distance must match the equivalent bifurcating tree,
# on the file path, the phylo / edge path, and the raw wrappers.
ref <- read.tree(text = "(((1,2),(3,4)),((5,6),(7,8)));")
diff <- read.tree(text = "(((1,3),(2,4)),((5,6),(7,8)));")
uni <- read.tree(text = "((((1,3),(2,4)),((5,6),(7,8))));") # diff + unifurcation
fRef <- writeTree(write.tree(ref))
fDiff <- writeTree(write.tree(diff))
fUni <- writeTree(write.tree(uni))

expect_equal(QuartetDistance(fRef, fUni), QuartetDistance(fRef, fDiff))
expect_equal(TripletDistance(fRef, fUni), TripletDistance(fRef, fDiff))
expect_equal(TripletDistance(ref, uni), TripletDistance(ref, diff))
expect_equal(QuartetStatus(list(ref, uni)), QuartetStatus(list(ref, diff)))
expect_equal(tqdist_QuartetDistance(fRef, fUni),
tqdist_QuartetDistance(fRef, fDiff))

# Arbitrarily-nested singletons are also tolerated by the tqDist reader,
# since a degree-one internal node induces no quartet or triplet statement.
fNested <- writeTree("(((((1,3),(2,4)),((5,6),(7,8)))));")
expect_equal(QuartetDistance(fRef, fNested), QuartetDistance(fRef, fDiff))
expect_equal(TripletDistance(fRef, fNested), TripletDistance(fRef, fDiff))
expect_equal(tqdist_QuartetDistance(fRef, fNested),
tqdist_QuartetDistance(fRef, fDiff))

file.remove(f1, f2, fRef, fDiff, fUni, fNested)
})
Loading