Skip to content

DEV: rebase windows-support on main (#5) #207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 12, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
uses: ilammy/msvc-dev-cmd@v1

- name: Build wheels
uses: pypa/[email protected].1
uses: pypa/[email protected].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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
uses: ilammy/msvc-dev-cmd@v1

- name: Build wheels
uses: pypa/[email protected].1
uses: pypa/[email protected].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
Expand Down
4 changes: 2 additions & 2 deletions build_tools/build_iqtree.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 20 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@

<a id='changelog-0.5.0'></a>
# 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`.

<a id='changelog-0.4.0'></a>
# Changes in release "0.4.0"

Expand Down
2 changes: 1 addition & 1 deletion docs/developers/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]
Expand All @@ -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"
]
Expand All @@ -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]"]
Expand Down
2 changes: 1 addition & 1 deletion src/piqtree/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def _add_dll_path() -> None:
make_model,
)

__version__ = "0.4.0"
__version__ = "0.5.0"

__all__ = [
"Model",
Expand Down
32 changes: 10 additions & 22 deletions src/piqtree/_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
31 changes: 24 additions & 7 deletions src/piqtree/iqtree/_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
10 changes: 5 additions & 5 deletions src/piqtree/model/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -52,7 +52,7 @@ def available_models(

Returns
-------
_Table
Table
Table with all available models.

"""
Expand All @@ -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": []}

Expand All @@ -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": []}

Expand Down
14 changes: 10 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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)
10 changes: 4 additions & 6 deletions tests/test_app/test_app.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading