PhyNetPy Documentation

Library for the Development and Use of Phylogenetic Network Methods

NetworkMoves Module v0.3.2

Network topology modification operations for MCMC search (add/remove hybrid, NNI, node height).

Author:
Mark Kessler
Last Edit:
5/12/26
Source:
NetworkMoves.py

Contents

Module Functions

add_hybrid(net: Network, source: Edge, destination: Edge, t_src: float = None, t_dest: float = None) -> None

Add a hybrid (reticulation) edge between two existing edges. This is the primary move for *increasing* network complexity. A new directed edge is created that represents a hybridization or introgression event between two lineages. How it works (normal case, source != destination):: BEFORE AFTER a x a x | | | | | | n1 -------> n2 (new hybrid edge) | | | | b y b y Two fresh internal nodes (n1, n2) are inserted by splitting the *source* and *destination* edges respectively. n2 is flagged as a reticulation node, and a directed edge n1 -> n2 is added. **Bubble (genome-duplication) case** -- when *source* and *destination* are the same edge, two parallel edges with equal inheritance probabilities (gamma = 0.5) are created between two new internal nodes, modeling a whole-genome duplication event.

Parameter Type Description
net The network to modify **in place**.
source The edge that will donate the new outgoing lineage.
destination The edge that will receive the incoming hybrid lineage.
t_src Optional divergence time for the new source-side node.
t_dest Optional divergence time for the new reticulation node.
remove_hybrid(net: Network, hybrid_edge: Edge) -> None

Remove a hybrid (reticulation) edge and clean up the two degree-2 nodes. This is the inverse of :func:`add_hybrid`. It lowers the network level by one by deleting the hybrid edge and the two internal nodes that were created when the hybrid was originally added. Expected local topology around the hybrid edge:: a x | | v v n1 -------> n2 <-- hybrid_edge (n1 -> n2, n2 is reticulation) | | v v b y After removal, n1 and n2 are deleted and replaced by two simple edges:: a x | | v v b y **Preconditions** -- the move only makes sense when n1 and n2 were originally inserted by :func:`add_hybrid`, meaning: * n1 has exactly one parent (a) and one non-hybrid child (b). * n2 has exactly one other parent (x) and one child (y).

Parameter Type Description
net The network to modify **in place**.
hybrid_edge The reticulation edge to remove. Its destination (n2) must be flagged as a reticulation node.
Raises: NetworkError: If the edge is not a valid hybrid edge, or if the surrounding topology does not allow clean removal (i.e. the nodes don't satisfy the degree constraints listed above).
nni(net: Network) -> None

Perform a random nearest-neighbor interchange (NNI) on the network. NNI is the workhorse topology-exploration move. It swaps two subtrees across an internal edge, producing a "neighboring" topology that differs by exactly one bipartition. Because it never adds or removes reticulations, the network level is unchanged. Diagram of the swap:: BEFORE AFTER a a / \ / \ c b d b / \ / \ d ... c ... A child *c* of *a* (that is not *b*) and a child *d* of *b* are chosen uniformly at random and exchanged. All edge metadata (length, gamma, weight, tag) travel with their respective subtrees. **Eligible edges** -- only internal edges whose endpoints are both non-reticulation and non-leaf are considered, so the move never disrupts reticulation structure.

Parameter Type Description
net The network to modify **in place**.
Raises: NetworkError: If no eligible internal edge exists, or if the randomly selected edge lacks sufficient children on both sides.
node_height_change(n: Node, net: Network, height: float, extend: bool = False) -> None

Change the divergence time (height) of an internal node. In a time-consistent (ultrametric) network, every node has a "time" value that increases from root toward the tips. This move slides a single node up or down the time axis while preserving the parent > child ordering on both sides. **Simple mode** (``extend=False``, default): Only the time of *n* is changed. Parent and child times are untouched, so the branch lengths into and out of *n* change accordingly. The new height must fall strictly between the nearest parent time and the nearest child time. **Extend mode** (``extend=True``): The node *and all of its immediate children* are shifted by the same delta, preserving the branch lengths between *n* and its children. This is useful when you want to move a speciation event without distorting the subtree below it. The move is rejected if any shifted child would collide with or pass its own grandchildren in time.

Parameter Type Description
n The internal node whose height should change.
net The network containing *n*.
height The proposed new time value for *n*.
extend If True, shift children by the same delta to keep branch lengths within the subtree constant.
Raises: NetworkError: If *n* is a root or leaf (no parents or children), or if the proposed height violates time-ordering constraints.
spr(net: Network) -> None

Perform a random subtree prune-and-regraft (SPR) on a tree. SPR is a larger-radius topology move than NNI: it detaches an entire subtree and reattaches it at a different location in the tree, allowing the search to jump further across tree space in a single step. **Restriction:** currently limited to level-0 networks (i.e. trees with no reticulations). This is checked up front. Algorithm outline:: 1. Pick a random "prune edge" (u, v) where v is not the root and at least one edge exists entirely outside the subtree rooted at v (so there is somewhere to reattach). 2. Remove the prune edge (u, v). 3. If u is now a degree-2 pass-through node, suppress it by merging its parent and child edges (see _suppress_chain_node). 4. Pick a random "host edge" (a, b) that lies completely outside the pruned subtree. 5. Insert a new internal node k on (a, b), splitting it into (a, k) and (k, b), and attach the pruned subtree as (k, v). The host edge's length is split randomly between (a, k) and (k, b), and the original prune-edge length is assigned to (k, v). The result is always a bifurcating tree with the same leaf set.

Parameter Type Description
net The tree to modify **in place**.
Raises: NetworkError: If the network is not level-0, or if no valid prune or reattachment edge can be found.
permute_leaves(net: Network) -> Iterator[Network]

Yield every possible relabeling of the leaf taxa on the network. This is useful for exhaustive searches over small networks: given a fixed internal topology, the function produces one network copy for every permutation of the leaf labels across the existing pendant (leaf-edge) positions. For *n* leaves this yields *n!* networks, so use with caution on networks with more than ~10 leaves. How it works: 1. Record the canonical leaf ordering and each leaf's parent node. 2. For each permutation of the leaf list, make a deep copy of the network, detach all leaves from their parents, and rewire them according to the permutation -- leaf ``perm[i]`` gets attached to the parent that originally held leaf ``i``. 3. Edge metadata (length, gamma, weight, tag) travel with the leaf they were originally attached to, not with the slot. The original network is **never modified**.

Parameter Type Description
net The template network to permute.
Yields A deep copy of *net* with leaves rewired according to one permutation.
Raises: NetworkError: If any leaf has zero or multiple parents (each leaf must have exactly one parent for the rewiring to be well-defined).

Navigation

Modules

This Page