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
7 changes: 4 additions & 3 deletions src/piqtree/iqtree/_model_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from piqtree.iqtree._decorator import iqtree_func
from piqtree.model import Model, make_model
from piqtree.util import process_rand_seed_nonzero

iq_model_finder = iqtree_func(iq_model_finder, hide_files=True)

Expand Down Expand Up @@ -140,7 +141,7 @@ def model_finder(
Search space for rate heterogeneity types.
Equivalent to IQ-TREE's mrate parameter, by default None
rand_seed : int | None, optional
The random seed - 0 or None means no seed, by default None.
The random seed - None means no seed is used, by default None.
num_threads: int | None, optional
Number of threads for IQ-TREE to use, by default None (single-threaded).
If 0 is specified, IQ-TREE attempts to find the optimal number of threads.
Expand All @@ -152,8 +153,8 @@ def model_finder(

"""
source = cast("str", aln.info.source)
if rand_seed is None:
rand_seed = 0 # The default rand_seed in IQ-TREE

rand_seed = process_rand_seed_nonzero(rand_seed)

if num_threads is None:
num_threads = 1
Expand Down
6 changes: 3 additions & 3 deletions src/piqtree/iqtree/_random_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from cogent3.core.tree import PhyloNode

from piqtree.iqtree._decorator import iqtree_func
from piqtree.util import process_rand_seed_nonzero

iq_random_tree = iqtree_func(iq_random_tree)

Expand Down Expand Up @@ -38,16 +39,15 @@ def random_tree(
tree_mode : TreeGenMode
How the tree is generated.
rand_seed : int | None, optional
The random seed - 0 or None means no seed, by default None.
The random seed - None means no seed is used, by default None.

Returns
-------
PhyloNode
A random phylogenetic tree.

"""
if rand_seed is None:
rand_seed = 0 # The default rand_seed in IQ-TREE
rand_seed = process_rand_seed_nonzero(rand_seed)

newick = iq_random_tree(num_taxa, tree_mode.name, 1, rand_seed).strip()
return make_tree(newick)
7 changes: 3 additions & 4 deletions src/piqtree/iqtree/_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from piqtree.iqtree._decorator import iqtree_func
from piqtree.iqtree._parse_tree_parameters import parse_model_parameters
from piqtree.model import Model, make_model
from piqtree.util import get_newick
from piqtree.util import get_newick, process_rand_seed_nonzero

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 @@ -99,7 +99,7 @@ def build_tree(
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.
The random seed - None means no seed is used, by default None.
bootstrap_replicates : int, optional
The number of bootstrap replicates to perform, by default None.
If 0 is provided, then no bootstrapping is performed.
Expand All @@ -117,8 +117,7 @@ def build_tree(
if isinstance(model, str):
model = make_model(model)

if rand_seed is None:
rand_seed = 0 # The default rand_seed in IQ-TREE
rand_seed = process_rand_seed_nonzero(rand_seed)

if bootstrap_replicates is None:
bootstrap_replicates = 0
Expand Down
60 changes: 60 additions & 0 deletions src/piqtree/util/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,65 @@
import secrets

from cogent3.core.tree import PhyloNode


def get_newick(tree: PhyloNode) -> str:
return tree.get_newick(with_distances=True, escape_name=False)


def make_rand_seed() -> int:
"""Make a 32-bit random seed.

Returns
-------
int
A 32-bit random seed.
"""
seed = secrets.randbits(32)
return seed if seed < (1 << 31) else seed - (1 << 32)


def make_nonzero_rand_seed() -> int:
"""Make a non-zero 32-bit random seed.

Returns
-------
int
A non-zero 32-bit random seed.
"""
while (seed := make_rand_seed()) == 0:
pass # pragma: nocov
return seed


def process_rand_seed_nonzero(seed: int | None) -> int:
"""Process a random seed before sending to IQ-TREE.

Some functions treat 0 as non-deterministic. For
consistency between methods, 0 is treated as always
deterministic and replaced with a pre-determined
number.

If the seed is None, a random seed is generated.
If the seed is 0, it is replaced with a pre-determined
number (1158426093).
If the seed is anything else, returns that number.

Parameters
----------
seed : int | None
The random seed to process.

Returns
-------
int
The original seed if no processing is required.
Otherwise a random non-zero seed is the original seed
was None. If the original seed was zero, a pre-determined
number is returned.
"""
if seed == 0:
seed = 1074213633 # Randomly chosen once with make_rand_seed
if seed is None:
seed = make_nonzero_rand_seed()
return seed
39 changes: 39 additions & 0 deletions tests/test_iqtree/test_random_tree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pytest

from piqtree import TreeGenMode, random_tree
from piqtree.exceptions import IqTreeError


@pytest.mark.parametrize("num_taxa", [10, 50, 100])
@pytest.mark.parametrize("tree_mode", list(TreeGenMode))
def test_random_tree(num_taxa: int, tree_mode: TreeGenMode) -> None:
tree = random_tree(num_taxa, tree_mode, rand_seed=1)
assert len(tree.tips()) == num_taxa


@pytest.mark.parametrize("rand_seed", [0, 1234])
def test_random_tree_determinism(rand_seed: int) -> None:
tree_1 = random_tree(10, TreeGenMode.YULE_HARDING, rand_seed=rand_seed)
tree_2 = random_tree(10, TreeGenMode.YULE_HARDING, rand_seed=rand_seed)

for node_a, node_b in zip(tree_1.postorder(), tree_2.postorder(), strict=True):
assert node_a.name == node_b.name
assert node_a.length == node_b.length
assert len(node_a.children) == len(node_b.children)


@pytest.mark.parametrize("num_taxa", [10, 50, 100])
@pytest.mark.parametrize("tree_mode", list(TreeGenMode))
def test_random_tree_no_seed(num_taxa: int, tree_mode: TreeGenMode) -> None:
tree = random_tree(num_taxa, tree_mode)
assert len(tree.tips()) == num_taxa


@pytest.mark.parametrize("num_taxa", [-1, 0, 1, 2])
@pytest.mark.parametrize("tree_mode", list(TreeGenMode))
def test_invalid_taxa(
num_taxa: int,
tree_mode: TreeGenMode,
) -> None:
with pytest.raises(IqTreeError):
_ = random_tree(num_taxa, tree_mode, rand_seed=1)
28 changes: 0 additions & 28 deletions tests/test_iqtree/test_random_trees.py

This file was deleted.