Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1ec1429
Add .gitignore for build artifacts
Paururo Apr 4, 2026
57c391e
Refactor into modules and fix critical bugs
Paururo Apr 4, 2026
0c1924e
Bump version to 1.0.1
Paururo Apr 4, 2026
809484b
Fix contributors link to point to distree repo
Paururo Apr 4, 2026
034daf9
Add CITATION.cff with DOI 10.5281/zenodo.16811766
Paururo Apr 4, 2026
27acb9b
Add CHANGELOG.md for v1.0.1
Paururo Apr 4, 2026
442b065
Fix parser crashes and stack overflow on deep trees
Paururo Apr 4, 2026
b229d87
Update CHANGELOG with parser fixes
Paururo Apr 4, 2026
5c42413
Proper error handling, lower triangle, CI workflows, 28 tests
Paururo Apr 4, 2026
44d9486
Fix bugs: empty input, empty branch length, FP clamping, tab in label…
Paururo Apr 4, 2026
6e33916
Integration tests, midpoint expect, LCA tests, tab-in-label guard, RE…
Paururo Apr 4, 2026
f1e70fd
Fix escaped quotes in quoted labels, add tests
Paururo Apr 4, 2026
2205793
Fix --lower to emit proper PHYLIP format with taxa count and row labels
Paururo Apr 4, 2026
ed6c757
Detect unclosed quoted labels with clear error message
Paururo Apr 4, 2026
3f80840
Parse trees that carry comments before a subtree or label
Paururo Jul 26, 2026
56a6c16
Keep non-ASCII leaf labels intact
Paururo Jul 26, 2026
951c9aa
Reject truncated and malformed Newick instead of guessing
Paururo Jul 26, 2026
f942e86
Only treat a quote as opening a label where a label can start
Paururo Jul 26, 2026
99153e5
Drop the parse tree iteratively to stop overflowing the stack
Paururo Jul 26, 2026
2a0d681
Do not create the output file until the tree parses
Paururo Jul 26, 2026
769ba00
Report write failures and stay quiet on a closed pipe
Paururo Jul 26, 2026
879a3ca
Validate --precision and --threads instead of panicking or lying
Paururo Jul 26, 2026
d694f76
Reject newlines in labels and warn about unlabeled leaves
Paururo Jul 26, 2026
b77373e
Stop --midpoint from inflating topological distances
Paururo Jul 26, 2026
7ebc2c9
Add a randomised check that midpoint rooting is sound
Paururo Jul 26, 2026
3d11f57
Halve the memory the LCA table needs
Paururo Jul 26, 2026
7973716
Point the integration tests at the binary Cargo actually built
Paururo Jul 26, 2026
1c63b9f
Keep negative patristic distances instead of rounding them to zero
Paururo Jul 26, 2026
09116a3
Run CI on every branch, and lint the tests too
Paururo Jul 26, 2026
de5c8ce
Fix the output examples in the README
Paururo Jul 26, 2026
8dea63e
Document the stricter parsing and the flag limits
Paururo Jul 26, 2026
2ccdb42
Make the parallelism actually pay: 6x on a 20,000-tip tree
Paururo Jul 26, 2026
10b32d7
Release the parse tree once the flat one is built
Paururo Jul 26, 2026
40ef714
Add the documentation site
Paururo Jul 26, 2026
13488a3
Point the README at the documentation site
Paururo Jul 26, 2026
9f4fa04
Record the performance work in the changelog
Paururo Jul 26, 2026
ce2c8c9
Catch three more ways the output could be quietly wrong
Paururo Jul 26, 2026
b5aecd1
Add a tutorial: a worked outbreak investigation
Paururo Jul 26, 2026
2c32c62
Cross-validate the distances against ape
Paururo Jul 26, 2026
9755cab
Split the tree logic out into a library
Paururo Jul 26, 2026
6ab9649
Reject branch lengths that make the arithmetic meaningless
Paururo Jul 26, 2026
b883c6e
Fuzz the parser and the pipeline
Paururo Jul 26, 2026
1bd7e80
Read gzip directly, and add --taxa and --stats
Paururo Jul 26, 2026
646beff
Add --npy: write the matrix as a NumPy array
Paururo Jul 26, 2026
9e36231
Format the numbers by hand where the answer is not in doubt
Paururo Jul 26, 2026
593627b
Document the new flags and the performance work
Paururo Jul 26, 2026
b836bb5
Cut the README down to a landing page
Paururo Jul 29, 2026
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
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI

on:
push:
branches: ["**"]
pull_request:
branches: ["**"]

env:
CARGO_TERM_COLOR: always

jobs:
check:
name: Check & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Clippy
run: cargo clippy --all-targets -- -D warnings
- name: Test
run: cargo test
- name: Test release
run: cargo test --release
41 changes: 41 additions & 0 deletions .github/workflows/crossvalidate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Cross-validate against ape

# The committed fixtures in tests/fixtures/ let `cargo test` check six trees
# against ape everywhere, with no R needed. This job runs the same comparison
# over 250 freshly generated trees, which is the deeper check but needs an R
# toolchain, so it runs weekly and on demand rather than on every push.

on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
inputs:
trees:
description: "How many trees to generate"
required: false
default: "250"

jobs:
crossvalidate:
name: distree vs ape
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable

- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true

- uses: r-lib/actions/setup-r-dependencies@v2
with:
packages: |
any::ape
any::phangorn

- name: Build
run: cargo build --release

- name: Cross-validate
run: Rscript scripts/crossvalidate.R ${{ inputs.trees || 250 }} target/release/distree
84 changes: 84 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Documentation

# Build the MkDocs Material site and publish it to GitHub Pages (gh-pages
# branch). After the first successful run, set Settings -> Pages -> Source to
# "Deploy from a branch" and pick the gh-pages branch (root).
#
# Deploys on a push to main, so the published site tracks main rather than
# redeploying from every working branch. Pull requests build without deploying,
# which is what catches a dead link before it ships. To publish on demand from
# any branch, use the "Run workflow" button (workflow_dispatch).

on:
push:
branches:
- main
paths:
- "docs/**"
- "mkdocs.yml"
- "CHANGELOG.md"
- "requirements-docs.txt"
- ".github/workflows/docs.yml"
pull_request:
paths:
- "docs/**"
- "mkdocs.yml"
- "CHANGELOG.md"
- "requirements-docs.txt"
- ".github/workflows/docs.yml"
workflow_dispatch:

permissions:
contents: write

concurrency:
group: docs-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Install dependencies
run: pip install -r requirements-docs.txt

# --strict turns broken internal links and missing nav entries into
# errors, so a pull request that breaks one fails here instead of
# shipping a dead link.
- name: Build
run: mkdocs build --strict

deploy:
name: Deploy
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Cache MkDocs build
uses: actions/cache@v4
with:
key: mkdocs-material-${{ github.sha }}
path: .cache
restore-keys: |
mkdocs-material-

- name: Install dependencies
run: pip install -r requirements-docs.txt

- name: Build and deploy
run: mkdocs gh-deploy --force --strict
66 changes: 66 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Build & Release

on:
push:
tags: ["v*"]

permissions:
contents: write

jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
name: distree-linux-x86_64
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
name: distree-linux-aarch64
- target: x86_64-apple-darwin
os: macos-15-intel
name: distree-macos-x86_64
- target: aarch64-apple-darwin
os: macos-latest
name: distree-macos-aarch64
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install cross-compilation tools
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config.toml
echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Package
run: |
cp target/${{ matrix.target }}/release/distree ${{ matrix.name }}
chmod +x ${{ matrix.name }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}
path: ${{ matrix.name }}

release:
name: Create Release
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create release
uses: softprops/action-gh-release@v2
with:
files: artifacts/**/*
generate_release_notes: true
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
target/

# MkDocs build output and the Material plugin cache
site/
.cache/
73 changes: 73 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Changelog

## [1.0.1] - 2026-04-04

### Fixed
- Branch lengths that are not finite are rejected. `A:1e910` parses as infinity rather than failing, and an infinite depth made every distance `inf` with `NaN` down the diagonal, reported with a success exit code. The run also stops when finite depths are large enough that `d_i + d_j` overflows
- A leaf label containing a space is rejected in `--lower` mode: PHYLIP readers treat whitespace as the end of the name, so every row after it landed a column out
- `--lower` with `--lmm` now warns that the omitted diagonal holds each leaf's root-to-tip length rather than zeros
- Parallel computation now speeds the run up instead of slowing it down. One parallel job per row could not pay for synchronising the thread pool, and float formatting sat in the serial write loop; rows are now batched and each worker formats its own. A 20,000-tip matrix went from 10.6 s to 1.6 s, and an 8,000-tip one now scales from 1.26 s on one core to 0.21 s on eight
- Trees prefixed with a `[&R]` / `[&U]` rooting marker, or carrying a comment before a label, no longer fail to parse
- Non-ASCII leaf labels (accents, Greek, CJK) are preserved instead of being mangled into mojibake
- Truncated trees, trailing content, unclosed comments and free-form text are rejected instead of yielding a plausible but wrong matrix
- Files holding more than one tree are rejected instead of silently using the first
- Apostrophes in unquoted labels (`O'Brien`) no longer break parsing, and a doubled quote keeps the whitespace around it
- Deeply nested trees no longer overflow the stack when the parse tree is freed
- `-o FILE` no longer truncates an existing file before the tree has parsed
- Write failures are reported instead of being discarded along with a success exit code; a closed pipe exits quietly
- `--precision` out of range is rejected instead of panicking inside the formatter
- `--threads 0` is rejected instead of quietly starting one thread per core
- Newlines and carriage returns in labels are rejected like tabs, and unlabeled leaves are reported rather than dropped in silence
- `--midpoint` no longer adds a hop to every topological distance crossing the midpoint edge
- Negative patristic distances are reported as computed instead of being rounded up to zero, which claimed distinct taxa were identical
- Midpoint rooting (`--midpoint`) rewritten to fix infinite loop caused by cyclic graph construction
- NHX/bracket comment annotations (e.g., `[&&NHX:S=human]`) no longer crash the parser
- Single-quoted labels (e.g., `'Taxon A'`) now parsed correctly
- Double-quoted labels (e.g., `"Taxon A"`) now parsed correctly
- Whitespace and newlines in Newick strings no longer crash the parser
- Stack overflow on deeply nested trees (>5,000 levels) — parser and flattener rewritten iteratively
- Duplicate leaf names are now detected with a clear error message
- `--lmm --topology` conflict now warns instead of silently choosing LMM
- Topology mode now outputs integers instead of float decimals
- Floating point output uses configurable precision instead of raw representation
- Header row always prints leading tab for R/Python distance matrix compatibility

### Added
- `--precision` / `-p` flag to control decimal places in output (default: 10)
- `-t` / `--threads` flag to control number of parallel threads
- Stdin support: use `-` as the phylogeny argument to read from stdin
- Warning when no branch lengths are detected in patristic mode
- Warning when negative branch lengths are found in the tree
- `--npy` writes the matrix as a NumPy array of 64-bit floats instead of text: exact, half the size, and 2.6x faster. With `--lower` it writes the condensed vector SciPy reads. Labels go to `<FILE>.labels.txt`
- `--taxa FILE` restricts the matrix to a list of leaf labels, keeping the distances the full tree gives rather than pruning it
- `--stats` prints a summary to stderr: leaves, nodes, mode, cells, and the minimum, maximum and mean off the diagonal
- Gzipped input is read directly, detected by content rather than by file name, so it works from stdin too
- CITATION.cff with DOI
- MkDocs Material documentation site with a tutorial, published to GitHub Pages
- Comprehensive test suite (96 tests), including randomised checks of midpoint rooting and of MRCA queries against a brute-force walk

### Changed
- Codebase split into modules: `parser.rs`, `tree.rs`, `lca.rs`, `midpoint.rs`
- Fixed-precision float formatting is done by hand where the rounding is unambiguous and handed to the standard formatter where it is not, which cut a text run by a third to a half with byte-identical output
- Cross-validated against R's ape (`cophenetic.phylo`, `vcv.phylo`) and `phangorn::midpoint` over 250 generated trees; reference matrices for six of them are committed so `cargo test` checks the same thing without R
- The parser and the whole pipeline are fuzzed, in the test suite on every push and with `cargo fuzz` for longer runs
- The tree logic moved into a library target, so it can be fuzzed, documented and used from other Rust code
- LCA binary-lifting table stores plain `usize` rather than `Option<usize>`, halving the memory it needs
- Output buffer raised from 8 KB to 1 MB, so a multi-gigabyte matrix is not hundreds of thousands of write syscalls
- The input text and the recursive parse tree are released once the flat node array is built, rather than held to the end of the run
- Version string now derived from Cargo.toml via `env!("CARGO_PKG_VERSION")`
- Removed unused `--format` flag
- Fixed clippy warnings (`&Vec<Node>` → `&[Node]`)
- Fixed contributors link in README
- README output examples replaced with the real output of a worked example; the previous ones lost their header row to the code fence and the topological matrix was not a realisable tree
- CI runs on every branch, lints the test code, and runs the test suite in release as well as debug

## [1.0.0] - 2025-06-01

### Added
- Initial release
- Patristic distance matrix extraction
- Topological distance computation
- LMM (var-covar) matrix output
- Midpoint rooting option
- Parallel computation with rayon
17 changes: 17 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
cff-version: 1.2.0
message: "If you use this software, please cite it as below."
title: "distree"
version: 1.0.1
doi: 10.5281/zenodo.16811766
date-released: 2026-04-04
url: "https://github.com/PathoGenOmics-Lab/distree"
repository-code: "https://github.com/PathoGenOmics-Lab/distree"
license: GPL-3.0
type: software
authors:
- family-names: Ruiz-Rodriguez
given-names: Paula
affiliation: "I2SysBio, University of Valencia-CSIC, FISABIO Joint Research Unit Infection and Public Health, Valencia, Spain"
- family-names: Coscolla
given-names: Mireia
affiliation: "I2SysBio, University of Valencia-CSIC, FISABIO Joint Research Unit Infection and Public Health, Valencia, Spain"
Loading
Loading