diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml
index a6f62bed..ebf7a97a 100644
--- a/.github/workflows/build_wheels.yml
+++ b/.github/workflows/build_wheels.yml
@@ -61,7 +61,7 @@ jobs:
uses: ilammy/msvc-dev-cmd@v1
- name: Build wheels
- uses: pypa/cibuildwheel@v2.23.1
+ uses: pypa/cibuildwheel@v2.23.3
env: # Can specify per os - e.g. CIBW_BEFORE_ALL_LINUX, CIBW_BEFORE_ALL_MACOS, CIBW_BEFORE_ALL_WINDOWS
CIBW_BEFORE_ALL_LINUX: ./build_tools/before_all_linux.sh
CIBW_BEFORE_ALL_MACOS: ./build_tools/before_all_mac.sh
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index faca30e4..fc4ff196 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -61,7 +61,7 @@ jobs:
uses: ilammy/msvc-dev-cmd@v1
- name: Build wheels
- uses: pypa/cibuildwheel@v2.23.1
+ uses: pypa/cibuildwheel@v2.23.3
env: # Can specify per os - e.g. CIBW_BEFORE_ALL_LINUX, CIBW_BEFORE_ALL_MACOS, CIBW_BEFORE_ALL_WINDOWS
CIBW_BEFORE_ALL_LINUX: ./build_tools/before_all_linux.sh
CIBW_BEFORE_ALL_MACOS: ./build_tools/before_all_mac.sh
diff --git a/build_tools/build_iqtree.sh b/build_tools/build_iqtree.sh
index 5aedccd6..cdc95266 100755
--- a/build_tools/build_iqtree.sh
+++ b/build_tools/build_iqtree.sh
@@ -30,8 +30,8 @@ elif [[ "$OSTYPE" == "msys"* || "$OSTYPE" == "cygwin"* ]]; then
..
make -j
else
- echo "Building for Linux."
- cmake -DBUILD_LIB=ON ..
+ echo "Building for linux."
+ cmake -DBUILD_LIB=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 ..
make -j
fi
diff --git a/changelog.md b/changelog.md
index 5978672c..818884e0 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,24 @@
+
+# Changes in release "0.5.0"
+
+## Contributors
+
+- @GavinHuttley added `nj_tree` as a hook for `cogent3.Alignment.quick_tree`.
+- @YapengLang handled negative branch lengths from the rapidNJ tree.
+- @rmcar17, @thomaskf general maintanence on the piqtree/IQ-TREE sides including work on windows behind the scenes.
+
+## ENH
+
+- Add support for Python 3.13, remove support for 3.10
+- IQ-TREE's rapidNJ implementation can be used as a hook for `quick_tree` on `cogent3` alignment objects. Try `Alignment.quick_tree(use_hook="piqtree")`.
+- `nj_tree` now by default does not allow negative branch lengths. Set `allow_negative=True` if this behaviour is desired.
+- Allow `str` to be used for `model` in `build_tree` and `fit_tree`. The value is automatically coerced into the `Model` class.
+
+## API
+
+- Simplify API for `piqtree_phylo` and `piqtree_fit` apps. Both now take a single parameter for the model, matching the parameter for `model` in `build_tree` and `fit_tree`.
+
# Changes in release "0.4.0"
diff --git a/docs/developers/release.md b/docs/developers/release.md
index 974a38d9..fae6fdd4 100644
--- a/docs/developers/release.md
+++ b/docs/developers/release.md
@@ -8,7 +8,7 @@ The documentation is fetched by readthedocs from the "Build Docs" GitHub Action.
- The `piqtree` version has been correctly bumped.
- The testing, linting and type checking all pass on all supported platforms.
-- The code has been throuroughly tested (check what's been missed in the coverage report).
+- The code has been thoroughly tested (check what's been missed in the coverage report).
- The documentation builds and appears correct.
- The documentation has been updated on readthedocs (this must be triggered from readthedocs).
- The "Release" GitHub Action has correctly uploaded to Test PyPI.
diff --git a/pyproject.toml b/pyproject.toml
index 9460368e..f907df5d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "piqtree"
-dependencies = ["cogent3>=2024.11.29a2", "pyyaml", "requests"]
+dependencies = ["cogent3>=2025.5.8a2", "pyyaml", "requests"]
requires-python = ">=3.11, <3.14"
authors = [{name="Gavin Huttley"}, {name="Robert McArthur"}, {name="Bui Quang Minh "}, {name="Richard Morris"}, {name="Thomas Wong"}]
@@ -28,9 +28,9 @@ classifiers = [
"Programming Language :: C++",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Typing :: Typed"
]
@@ -43,7 +43,7 @@ Documentation = "https://piqtree.readthedocs.io"
[project.optional-dependencies]
dev = ["cibuildwheel", "pybind11", "scriv", "piqtree[test]", "piqtree[lint]", "piqtree[typing]"]
test = ["pytest", "pytest-cov", "nox"]
-lint = ["ruff==0.11.0"]
+lint = ["ruff==0.11.9"]
typing = ["mypy==1.15.0", "piqtree[stubs]", "piqtree[test]"]
stubs = ["types-PyYAML", "types-requests"]
extra = ["cogent3[extra]"]
diff --git a/src/piqtree/__init__.py b/src/piqtree/__init__.py
index 5a91c957..5d7abd5e 100644
--- a/src/piqtree/__init__.py
+++ b/src/piqtree/__init__.py
@@ -35,7 +35,7 @@ def _add_dll_path() -> None:
make_model,
)
-__version__ = "0.4.0"
+__version__ = "0.5.0"
__all__ = [
"Model",
diff --git a/src/piqtree/_app/__init__.py b/src/piqtree/_app/__init__.py
index e70c9de5..32d6cfec 100644
--- a/src/piqtree/_app/__init__.py
+++ b/src/piqtree/_app/__init__.py
@@ -23,21 +23,13 @@ class piqtree_phylo:
@extend_docstring_from(build_tree)
def __init__(
self,
- submod_type: str,
- freq_type: str | None = None,
- rate_model: str | None = None,
+ model: Model | str,
*,
- invariant_sites: bool = False,
rand_seed: int | None = None,
bootstrap_reps: int | None = None,
num_threads: int | None = None,
) -> None:
- self._model = Model(
- submod_type=submod_type,
- invariant_sites=invariant_sites,
- rate_model=rate_model,
- freq_type=freq_type,
- )
+ self._model = model
self._rand_seed = rand_seed
self._bootstrap_reps = bootstrap_reps
self._num_threads = num_threads
@@ -61,21 +53,13 @@ class piqtree_fit:
def __init__(
self,
tree: cogent3.PhyloNode,
- submod_type: str,
- freq_type: str | None = None,
- rate_model: str | None = None,
+ model: Model | str,
*,
rand_seed: int | None = None,
num_threads: int | None = None,
- invariant_sites: bool = False,
) -> None:
self._tree = tree
- self._model = Model(
- submod_type=submod_type,
- invariant_sites=invariant_sites,
- rate_model=rate_model,
- freq_type=freq_type,
- )
+ self._model = model
self._rand_seed = rand_seed
self._num_threads = num_threads
@@ -124,8 +108,12 @@ def main(
@composable.define_app
@extend_docstring_from(nj_tree)
-def piqtree_nj(dists: c3_types.PairwiseDistanceType) -> cogent3.PhyloNode:
- tree = nj_tree(dists)
+def piqtree_nj(
+ dists: c3_types.PairwiseDistanceType,
+ *,
+ allow_negative: bool = False,
+) -> cogent3.PhyloNode:
+ tree = nj_tree(dists, allow_negative=allow_negative)
tree.params |= {"provenance": "piqtree"}
return tree
diff --git a/src/piqtree/iqtree/_tree.py b/src/piqtree/iqtree/_tree.py
index 2485db00..a302e6ea 100644
--- a/src/piqtree/iqtree/_tree.py
+++ b/src/piqtree/iqtree/_tree.py
@@ -11,7 +11,7 @@
from piqtree.exceptions import ParseIqTreeError
from piqtree.iqtree._decorator import iqtree_func
-from piqtree.model import DnaModel, Model
+from piqtree.model import DnaModel, Model, make_model
iq_build_tree = iqtree_func(iq_build_tree, hide_files=True)
iq_fit_tree = iqtree_func(iq_fit_tree, hide_files=True)
@@ -196,7 +196,7 @@ def _process_tree_yaml(
def build_tree(
aln: c3_types.AlignedSeqsType,
- model: Model,
+ model: Model | str,
rand_seed: int | None = None,
bootstrap_replicates: int | None = None,
num_threads: int | None = None,
@@ -209,7 +209,7 @@ def build_tree(
----------
aln : c3_types.AlignedSeqsType
The sequence alignment.
- model : Model
+ model : Model | str
The substitution model with base frequencies and rate heterogeneity.
rand_seed : int | None, optional
The random seed - 0 or None means no seed, by default None.
@@ -227,6 +227,9 @@ def build_tree(
The IQ-TREE maximum likelihood tree from the given alignment.
"""
+ if isinstance(model, str):
+ model = make_model(model)
+
if rand_seed is None:
rand_seed = 0 # The default rand_seed in IQ-TREE
@@ -261,7 +264,7 @@ def build_tree(
def fit_tree(
aln: c3_types.AlignedSeqsType,
tree: cogent3.PhyloNode,
- model: Model,
+ model: Model | str,
rand_seed: int | None = None,
num_threads: int | None = None,
) -> cogent3.PhyloNode:
@@ -276,7 +279,7 @@ def fit_tree(
The sequence alignment.
tree : cogent3.PhyloNode
The topology to fit branch lengths to.
- model : Model
+ model : Model | str
The substitution model with base frequencies and rate heterogeneity.
rand_seed : int | None, optional
The random seed - 0 or None means no seed, by default None.
@@ -290,6 +293,9 @@ def fit_tree(
A phylogenetic tree with same given topology fitted with branch lengths.
"""
+ if isinstance(model, str):
+ model = make_model(model)
+
if rand_seed is None:
rand_seed = 0 # The default rand_seed in IQ-TREE
@@ -312,7 +318,11 @@ def fit_tree(
return tree
-def nj_tree(pairwise_distances: c3_types.PairwiseDistanceType) -> cogent3.PhyloNode:
+def nj_tree(
+ pairwise_distances: c3_types.PairwiseDistanceType,
+ *,
+ allow_negative: bool = False,
+) -> cogent3.PhyloNode:
"""Construct a neighbour joining tree from a pairwise distance matrix.
Parameters
@@ -334,4 +344,11 @@ def nj_tree(pairwise_distances: c3_types.PairwiseDistanceType) -> cogent3.PhyloN
pairwise_distances.keys(),
np.array(pairwise_distances).flatten(),
)
- return make_tree(newick_tree)
+
+ tree = make_tree(newick_tree)
+
+ if not allow_negative:
+ for node in tree.traverse(include_self=False):
+ node.length = max(node.length, 0)
+
+ return tree
diff --git a/src/piqtree/model/_options.py b/src/piqtree/model/_options.py
index ebfa3655..4229ad92 100644
--- a/src/piqtree/model/_options.py
+++ b/src/piqtree/model/_options.py
@@ -3,7 +3,7 @@
import functools
from typing import Literal
-from cogent3 import _Table, make_table
+from cogent3.core.table import Table, make_table
from piqtree.model._freq_type import FreqType
from piqtree.model._rate_type import ALL_BASE_RATE_TYPES, get_description
@@ -40,7 +40,7 @@ def available_models(
model_type: Literal["dna", "protein"] | None = None,
*,
show_all: bool = True,
-) -> _Table:
+) -> Table:
"""Return a table showing available substitution models.
Parameters
@@ -52,7 +52,7 @@ def available_models(
Returns
-------
- _Table
+ Table
Table with all available models.
"""
@@ -78,7 +78,7 @@ def available_models(
return table
-def available_freq_type() -> _Table:
+def available_freq_type() -> Table:
"""Return a table showing available freq type options."""
data: dict[str, list[str]] = {"Freq Type": [], "Description": []}
@@ -89,7 +89,7 @@ def available_freq_type() -> _Table:
return make_table(data=data, title="Available frequency types")
-def available_rate_type() -> _Table:
+def available_rate_type() -> Table:
"""Return a table showing available rate type options."""
data: dict[str, list[str]] = {"Rate Type": [], "Description": []}
diff --git a/tests/conftest.py b/tests/conftest.py
index 76778ade..7d4a1eb7 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,7 @@
import pathlib
import pytest
-from cogent3 import ArrayAlignment, load_aligned_seqs
+from cogent3 import Alignment, load_aligned_seqs
@pytest.fixture(scope="session")
@@ -10,21 +10,27 @@ def DATA_DIR() -> pathlib.Path:
@pytest.fixture
-def three_otu(DATA_DIR: pathlib.Path) -> ArrayAlignment:
+def three_otu(DATA_DIR: pathlib.Path) -> Alignment:
aln = load_aligned_seqs(DATA_DIR / "example.fasta", moltype="dna", new_type=True)
aln = aln.take_seqs(["Human", "Rhesus", "Mouse"])
return aln.omit_gap_pos(allowed_gap_frac=0)
@pytest.fixture
-def four_otu(DATA_DIR: pathlib.Path) -> ArrayAlignment:
+def four_otu(DATA_DIR: pathlib.Path) -> Alignment:
aln = load_aligned_seqs(DATA_DIR / "example.fasta", moltype="dna", new_type=True)
aln = aln.take_seqs(["Human", "Chimpanzee", "Rhesus", "Mouse"])
return aln.omit_gap_pos(allowed_gap_frac=0)
@pytest.fixture
-def five_otu(DATA_DIR: pathlib.Path) -> ArrayAlignment:
+def five_otu(DATA_DIR: pathlib.Path) -> Alignment:
aln = load_aligned_seqs(DATA_DIR / "example.fasta", moltype="dna", new_type=True)
aln = aln.take_seqs(["Human", "Chimpanzee", "Rhesus", "Manatee", "Dugong"])
return aln.omit_gap_pos(allowed_gap_frac=0)
+
+
+@pytest.fixture
+def all_otu(DATA_DIR: pathlib.Path) -> Alignment:
+ aln = load_aligned_seqs(DATA_DIR / "example.fasta", moltype="dna", new_type=True)
+ return aln.omit_gap_pos(allowed_gap_frac=0)
diff --git a/tests/test_app/test_app.py b/tests/test_app/test_app.py
index 7bd3aa2f..6470cb9a 100644
--- a/tests/test_app/test_app.py
+++ b/tests/test_app/test_app.py
@@ -1,21 +1,20 @@
import pytest
-from cogent3 import __version__ as cogent3_vers
from cogent3 import get_app, make_tree
from cogent3.core.new_alignment import Alignment
import piqtree
-from piqtree import jc_distances
+from piqtree import jc_distances, make_model
def test_piqtree_phylo(four_otu: Alignment) -> None:
expected = make_tree("(Human,Chimpanzee,(Rhesus,Mouse));")
- app = get_app("piqtree_phylo", submod_type="JC")
+ app = get_app("piqtree_phylo", model="JC")
got = app(four_otu)
assert expected.same_topology(got)
def test_piqtree_phylo_support(four_otu: Alignment) -> None:
- app = get_app("piqtree_phylo", submod_type="JC", bootstrap_reps=1000)
+ app = get_app("piqtree_phylo", model=make_model("JC"), bootstrap_reps=1000)
got = app(four_otu)
supports = [
node.params.get("support", None)
@@ -29,7 +28,7 @@ def test_piqtree_fit(three_otu: Alignment) -> None:
tree = make_tree(tip_names=three_otu.names)
app = get_app("model", "JC69", tree=tree)
expected = app(three_otu)
- piphylo = get_app("piqtree_fit", tree=tree, submod_type="JC")
+ piphylo = get_app("piqtree_fit", tree=tree, model="JC")
got = piphylo(three_otu)
assert got.params["lnL"] == pytest.approx(expected.lnL)
@@ -112,7 +111,6 @@ def test_mfinder_result_roundtrip(five_otu: Alignment) -> None:
assert str(got.best_aicc) == str(inflated.best_aicc)
-@pytest.mark.skipif(cogent3_vers < "2025.3.1", reason="requires cogent3 >= 2025.3.1")
@pytest.mark.parametrize("use_hook", [None, "piqtree"])
def test_quick_tree_hook(four_otu: Alignment, use_hook: str | None) -> None:
tree = four_otu.quick_tree(use_hook=use_hook)
diff --git a/tests/test_iqtree/test_build_tree.py b/tests/test_iqtree/test_build_tree.py
index 90cbf8ab..250cba32 100644
--- a/tests/test_iqtree/test_build_tree.py
+++ b/tests/test_iqtree/test_build_tree.py
@@ -1,7 +1,7 @@
import re
import pytest
-from cogent3 import ArrayAlignment, make_tree
+from cogent3 import Alignment, make_tree
import piqtree
from piqtree.exceptions import IqTreeError
@@ -16,12 +16,13 @@
def check_build_tree(
- four_otu: ArrayAlignment,
+ four_otu: Alignment,
dna_model: DnaModel,
freq_type: FreqType | None = None,
rate_model: RateModel | None = None,
*,
invariant_sites: bool = False,
+ coerce_str: bool = False,
) -> None:
expected = make_tree("(Human,Chimpanzee,(Rhesus,Mouse));")
@@ -32,7 +33,11 @@ def check_build_tree(
rate_model=rate_model,
)
- got1 = piqtree.build_tree(four_otu, model, rand_seed=1)
+ got1 = piqtree.build_tree(
+ four_otu,
+ str(model) if coerce_str else model,
+ rand_seed=1,
+ )
got1 = got1.unrooted()
# Check topology
assert expected.same_topology(got1.unrooted())
@@ -49,7 +54,7 @@ def check_build_tree(
@pytest.mark.parametrize("dna_model", list(DnaModel)[:22])
@pytest.mark.parametrize("freq_type", list(FreqType))
def test_non_lie_build_tree(
- four_otu: ArrayAlignment,
+ four_otu: Alignment,
dna_model: DnaModel,
freq_type: FreqType,
) -> None:
@@ -57,10 +62,15 @@ def test_non_lie_build_tree(
@pytest.mark.parametrize("dna_model", list(DnaModel)[22:])
-def test_lie_build_tree(four_otu: ArrayAlignment, dna_model: DnaModel) -> None:
+def test_lie_build_tree(four_otu: Alignment, dna_model: DnaModel) -> None:
check_build_tree(four_otu, dna_model)
+@pytest.mark.parametrize("dna_model", list(DnaModel)[-3:])
+def test_str_build_tree(four_otu: Alignment, dna_model: DnaModel) -> None:
+ check_build_tree(four_otu, dna_model, coerce_str=True)
+
+
@pytest.mark.parametrize("dna_model", list(DnaModel)[:5])
@pytest.mark.parametrize("invariant_sites", [False, True])
@pytest.mark.parametrize(
@@ -74,7 +84,7 @@ def test_lie_build_tree(four_otu: ArrayAlignment, dna_model: DnaModel) -> None:
],
)
def test_rate_model_build_tree(
- four_otu: ArrayAlignment,
+ four_otu: Alignment,
dna_model: DnaModel,
invariant_sites: bool,
rate_model: RateModel,
@@ -87,12 +97,12 @@ def test_rate_model_build_tree(
)
-def test_build_tree_inadequate_bootstrapping(four_otu: ArrayAlignment) -> None:
+def test_build_tree_inadequate_bootstrapping(four_otu: Alignment) -> None:
with pytest.raises(IqTreeError, match=re.escape("#replicates must be >= 1000")):
piqtree.build_tree(four_otu, Model(DnaModel.GTR), bootstrap_replicates=10)
-def test_build_tree_bootstrapping(four_otu: ArrayAlignment) -> None:
+def test_build_tree_bootstrapping(four_otu: Alignment) -> None:
tree = piqtree.build_tree(four_otu, Model(DnaModel.GTR), bootstrap_replicates=1000)
supported_node = max(tree.children, key=lambda x: len(x.children))
diff --git a/tests/test_iqtree/test_distance.py b/tests/test_iqtree/test_distance.py
index 38b694b5..b3c18485 100644
--- a/tests/test_iqtree/test_distance.py
+++ b/tests/test_iqtree/test_distance.py
@@ -1,9 +1,9 @@
-from cogent3 import ArrayAlignment
+from cogent3 import Alignment
from piqtree import jc_distances
-def test_jc_distance(five_otu: ArrayAlignment) -> None:
+def test_jc_distance(five_otu: Alignment) -> None:
dists = jc_distances(five_otu)
assert (
diff --git a/tests/test_iqtree/test_fit_tree.py b/tests/test_iqtree/test_fit_tree.py
index 91571a63..cf562eaf 100644
--- a/tests/test_iqtree/test_fit_tree.py
+++ b/tests/test_iqtree/test_fit_tree.py
@@ -1,5 +1,5 @@
import pytest
-from cogent3 import ArrayAlignment, get_app, make_tree
+from cogent3 import Alignment, get_app, make_tree
from cogent3.app.result import model_result
from cogent3.core.tree import PhyloNode
@@ -78,19 +78,57 @@ def check_branch_lengths(got: PhyloNode, expected: PhyloNode) -> None:
(DnaModel.F81, "F81"),
],
)
-def test_fit_tree(three_otu: ArrayAlignment, iq_model: DnaModel, c3_model: str) -> None:
+def test_fit_tree(three_otu: Alignment, iq_model: DnaModel, c3_model: str) -> None:
tree_topology = make_tree(tip_names=three_otu.names)
app = get_app("model", c3_model, tree=tree_topology)
expected = app(three_otu)
- got1 = piqtree.fit_tree(three_otu, tree_topology, Model(iq_model), rand_seed=1)
+ model = Model(iq_model)
+
+ got1 = piqtree.fit_tree(three_otu, tree_topology, model, rand_seed=1)
+ check_likelihood(got1, expected)
+ check_motif_probs(got1, expected.tree)
+ check_rate_parameters(got1, expected.tree)
+ check_branch_lengths(got1, expected.tree)
+
+ # Should be within an approximation for any seed
+ got2 = piqtree.fit_tree(three_otu, tree_topology, model, rand_seed=None)
+ check_likelihood(got2, expected)
+ check_motif_probs(got2, expected.tree)
+ check_rate_parameters(got2, expected.tree)
+ check_branch_lengths(got2, expected.tree)
+
+
+@pytest.mark.parametrize(
+ ("iq_model", "c3_model"),
+ [
+ (DnaModel.JC, "JC69"),
+ (DnaModel.K80, "K80"),
+ (DnaModel.GTR, "GTR"),
+ (DnaModel.TN, "TN93"),
+ (DnaModel.HKY, "HKY85"),
+ (DnaModel.F81, "F81"),
+ ],
+)
+def test_fit_tree_str_model(
+ three_otu: Alignment,
+ iq_model: DnaModel,
+ c3_model: str,
+) -> None:
+ tree_topology = make_tree(tip_names=three_otu.names)
+ app = get_app("model", c3_model, tree=tree_topology)
+ expected = app(three_otu)
+
+ model = str(Model(iq_model))
+
+ got1 = piqtree.fit_tree(three_otu, tree_topology, model, rand_seed=1)
check_likelihood(got1, expected)
check_motif_probs(got1, expected.tree)
check_rate_parameters(got1, expected.tree)
check_branch_lengths(got1, expected.tree)
# Should be within an approximation for any seed
- got2 = piqtree.fit_tree(three_otu, tree_topology, Model(iq_model), rand_seed=None)
+ got2 = piqtree.fit_tree(three_otu, tree_topology, model, rand_seed=None)
check_likelihood(got2, expected)
check_motif_probs(got2, expected.tree)
check_rate_parameters(got2, expected.tree)
diff --git a/tests/test_iqtree/test_model_finder.py b/tests/test_iqtree/test_model_finder.py
index fa98865e..ce0af20f 100644
--- a/tests/test_iqtree/test_model_finder.py
+++ b/tests/test_iqtree/test_model_finder.py
@@ -1,7 +1,7 @@
import multiprocessing
import pytest
-from cogent3 import ArrayAlignment
+from cogent3 import Alignment
from piqtree.iqtree import ModelFinderResult, ModelResultValue, model_finder
@@ -47,7 +47,7 @@ def test_model_finder_result(model: str) -> None:
assert result.model_stats[model].tree_length == 0.678
-def test_model_finder(five_otu: ArrayAlignment) -> None:
+def test_model_finder(five_otu: Alignment) -> None:
result1 = model_finder(five_otu, rand_seed=1)
result2 = model_finder(
five_otu,
@@ -59,7 +59,7 @@ def test_model_finder(five_otu: ArrayAlignment) -> None:
assert str(result1.best_bic) == str(result2.best_bic)
-def test_model_finder_restricted_submod(five_otu: ArrayAlignment) -> None:
+def test_model_finder_restricted_submod(five_otu: Alignment) -> None:
result = model_finder(five_otu, rand_seed=1, model_set={"HKY", "TIM"})
assert str(result.best_aic).startswith(("HKY", "TIM"))
assert str(result.best_aicc).startswith(("HKY", "TIM"))
diff --git a/tests/test_iqtree/test_nj_tree.py b/tests/test_iqtree/test_nj_tree.py
index d3d19495..ae3c68ce 100644
--- a/tests/test_iqtree/test_nj_tree.py
+++ b/tests/test_iqtree/test_nj_tree.py
@@ -1,12 +1,25 @@
-from cogent3 import ArrayAlignment, make_tree
+from cogent3 import Alignment, make_tree
from piqtree import jc_distances, nj_tree
-def test_nj_tree(five_otu: ArrayAlignment) -> None:
+def test_nj_tree(five_otu: Alignment) -> None:
expected = make_tree("(((Human, Chimpanzee), Rhesus), Manatee, Dugong);")
dists = jc_distances(five_otu)
actual = nj_tree(dists)
assert expected.same_topology(actual)
+
+
+def test_nj_tree_allow_negative(all_otu: Alignment) -> None:
+ # a distance matrix can produce trees with negative branch lengths
+ dists = jc_distances(all_otu)
+
+ # check that all branch lengths are non-negative, by default
+ tree1 = nj_tree(dists)
+ assert all(node.length >= 0 for node in tree1.traverse(include_self=False))
+
+ # check that some branch lengths are negative when allow_negative=True
+ tree2 = nj_tree(dists, allow_negative=True)
+ assert any(node.length < 0 for node in tree2.traverse(include_self=False))