dnetwork#

Directed network module.

This module provides the DirectedPhyNetwork class and related functions for working with directed phylogenetic networks. Directed phylogenetic networks are rooted networks where all edges are directed. Internal nodes are either tree nodes (in-degree 1, out-degree >= 2) or hybrid nodes (in-degree >= 2, out-degree 1). The public API is re-exported here; the implementation is split across the base, features, classifications, transformations, derivations, conversions, isomorphism, and io submodules.

Main Class#

Directed network base module.

This module provides the main class for working with directed phylogenetic networks.

class phylozoo.core.network.dnetwork.base.DirectedPhyNetwork(edges: list[tuple[T, T] | tuple[T, T, int] | dict[str, Any]] | None = None, nodes: list[T | tuple[T, dict[str, Any]]] | None = None, attributes: dict[str, Any] | None = None)[source]#

Bases: IOMixin

A directed phylogenetic network.

A DirectedPhyNetwork is a weakly connected, directed acyclic multi-graph (DAG) representing a phylogenetic network structure. It consists of:

  • Root node: Exactly one node with in-degree 0

  • Leaf nodes: Nodes with out-degree 0, each with in-degree 1 and a taxon label

  • Tree nodes: Internal nodes (non-root, non-leaf) with in-degree 1 and out-degree >= 2

  • Hybrid nodes: Internal nodes with in-degree >= 2 and out-degree 1

For technical reasons, an empty network or single-node network (where root and leaf are the same node) is also valid. Internal nodes may be unlabeled.

Parameters:
  • edges (list[tuple[T, T] | tuple[T, T, int] | dict[str, Any]] | None, optional) –

    List of directed edges. Formats: - (u, v) tuples (key auto-generated) - (u, v, key) tuples (explicit key) - Dict with ‘u’, ‘v’ and optional ‘key’ (for parallel edges) plus edge attributes

    Edge attributes (validated): - branch_length (float; for set of parallel edges, all must have equal branch_length) - bootstrap (float in [0.0, 1.0]) - gamma (float in [0.0, 1.0], hybrid edges only; for each hybrid node, all incoming gammas must sum to 1.0) Use a different attribute name (e.g., ‘gamma2’) for non-validated and/or additional attritbutes.

    Can be empty or None for empty/single-node networks. By default None.

  • nodes (list[T | tuple[T, dict[str, Any]]] | None, optional) –

    List of nodes. Formats: - Simple node IDs: 1, “node1”, etc. - Tuples: (node_id, {‘label’: ‘…’,’attr’: …})

    Node attributes (validated): - label: string, unique across all nodes; use another key for non-string data.

    Leaves without labels are auto-labeled. Leaf-labels are referred to as taxa. Use a different attribute name (e.g., ‘label2’) for non-validated and/or additional attritbutes.

    Can be empty or None. By default None.

  • attributes (dict[str, Any] | None, optional) – Optional dictionary of graph-level attributes to store with the network. These attributes are stored in the underlying graph’s .graph attribute and are preserved through copy operations. Can be used to store metadata like provenance, source file, creation date, etc. By default None.

Notes

The class uses composition with DirectedMultiGraph and is immutable after initialization; construct via nodes/edges, from a prebuilt DirectedMultiGraph, or load from a file/eNewick string.

Supported I/O formats:

  • enewick (default): .enewick, .eNewick, .nwk, .newick

  • dot: .dot, .gv

Examples

>>> # Initialize with nodes and labels
>>> net = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> net.taxa
{'A', 'B'}
>>> net.root_node
3
>>> net.is_tree()
True
>>> # Partial labels - uncovered leaves get auto-generated labels
>>> net2 = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2), (3, 4)],
...     nodes=[(1, {'label': 'A'})]
... )
>>> net2.taxa  # 2 and 4 are auto-labeled
{'A', '2', '4'}
>>> # Network with branch lengths and bootstrap support
>>> net3 = DirectedPhyNetwork(
...     edges=[
...         {'u': 3, 'v': 1, 'branch_length': 0.5, 'bootstrap': 0.95},
...         {'u': 3, 'v': 2, 'branch_length': 0.3, 'bootstrap': 0.87}
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> net3.get_branch_length(3, 1)
0.5
>>> net3.get_bootstrap(3, 1)
0.95
>>> # Network with hybrid node and gamma values
>>> net4 = DirectedPhyNetwork(
...     edges=[
...         (7, 5), (7, 6),  # Root to tree nodes
...         {'u': 5, 'v': 4, 'gamma': 0.6},  # Hybrid edge
...         {'u': 6, 'v': 4, 'gamma': 0.4},  # Hybrid edge (Sum = 1.0)
...         (5, 8), (6, 9),  # Tree nodes also have other children
...         (4, 1)  # Hybrid to leaf
...     ],
...     nodes=[(1, {'label': 'A'}), (8, {'label': 'B'}), (9, {'label': 'C'})]
... )
>>> net4.get_gamma(5, 4)
0.6
>>> net4.get_gamma(6, 4)
0.4
property LSA_node: T#

Return the Least Stable Ancestor (LSA) node of the network.

The LSA is the lowest node through which all paths from the root to the leaves pass. In other words, it is the unique node that is an ancestor of all leaves and is the lowest such node (has maximum depth from the root).

Returns:

The LSA node identifier.

Return type:

T

Raises:

PhyloZooValueError – If the network is empty.

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> net.LSA_node
3
__contains__(node_id: T) bool[source]#

Check if node is in the network.

Parameters:

node_id (T) – Node identifier to check.

Returns:

True if node is in the network, False otherwise.

Return type:

bool

__iter__() Iterator[T][source]#

Iterate over nodes.

Returns:

Iterator over node identifiers.

Return type:

Iterator[T]

__len__() int[source]#

Return the number of nodes.

Returns:

Number of nodes.

Return type:

int

__repr__() str[source]#

Return string representation of the network.

Returns:

String representation showing nodes, edges, taxa count, and taxon list.

Return type:

str

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> repr(net)
'DirectedPhyNetwork(nodes=3, edges=2, taxa=2, taxa_list=[A, B])'
children(v: T) Iterator[T][source]#

Return an iterator over child nodes of v.

Parameters:

v (T) – Node identifier.

Returns:

Iterator over child nodes.

Return type:

Iterator[T]

copy() DirectedPhyNetwork[source]#

Create a copy of the network.

Returns a shallow copy of the network. Cached properties are not copied but will be recomputed on first access.

Returns:

A copy of the network.

Return type:

DirectedPhyNetwork

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1)], nodes=[(1, {'label': 'A'})])
>>> net2 = net.copy()
>>> net.number_of_nodes()
2
>>> net2.number_of_nodes()
2
degree(v: T) int[source]#

Return the total degree of node v.

Parameters:

v (T) – Node identifier.

Returns:

Total degree (in-degree + out-degree).

Return type:

int

property edges#

View of all directed edges in the network.

Cached. Same callable/view interface as the underlying graph’s edges (e.g. net.edges(), net.edges(keys=True), net.edges(keys=True, data=True)).

Returns:

Callable view of edges (u, v) or (u, v, key) or with data.

Return type:

DirectedMultiGraph.EdgeView

get_bootstrap(u: T, v: T, key: int | None = None) float | None[source]#

Get bootstrap support for an edge.

Bootstrap values are typically in the range 0.0 to 1.0.

Parameters:
  • u (T) – Edge endpoints.

  • v (T) – Edge endpoints.

  • key (int | None, optional) – Edge key for parallel edges. Required if multiple parallel edges exist.

Returns:

Bootstrap support value (typically 0.0 to 1.0), or None if not set.

Return type:

float | None

Examples

>>> net = DirectedPhyNetwork(
...     edges=[{'u': 3, 'v': 1, 'bootstrap': 0.95}],
...     nodes=[(1, {'label': 'A'})]
... )
>>> net.get_bootstrap(3, 1)
0.95
get_branch_length(u: T, v: T, key: int | None = None) float | None[source]#

Get branch length for an edge.

Parameters:
  • u (T) – Edge endpoints.

  • v (T) – Edge endpoints.

  • key (int | None, optional) – Edge key for parallel edges. Required if multiple parallel edges exist.

Returns:

Branch length, or None if not set.

Return type:

float | None

Examples

>>> net = DirectedPhyNetwork(
...     edges=[{'u': 3, 'v': 1, 'branch_length': 0.5}],
...     nodes=[(1, {'label': 'A'})]
... )
>>> net.get_branch_length(3, 1)
0.5
get_edge_attribute(u: T, v: T, key: int | None = None, attr: str | None = None) dict[str, Any] | Any | None[source]#

Get edge attribute(s).

Parameters:
  • u (T) – Edge endpoints.

  • v (T) – Edge endpoints.

  • key (int | None, optional) – Edge key for parallel edges. If None and multiple parallel edges exist, raises ValueError. Must specify key when parallel edges exist.

  • attr (str | None, optional) – Attribute name. If None, returns all attributes as a dict. If specified, returns the value of that specific attribute. By default None.

Returns:

If attr is None: dict of all edge attributes (empty dict if no attributes). If attr is specified: attribute value, or None if not set.

Return type:

dict[str, Any] | Any | None

Raises:

PhyloZooValueError – If the edge does not exist, or if key is None and multiple parallel edges exist.

Examples

>>> net = DirectedPhyNetwork(
...     edges=[
...         {'u': 3, 'v': 2, 'key': 0, 'branch_length': 0.5, 'bootstrap': 0.95},
...         {'u': 3, 'v': 2, 'key': 1, 'branch_length': 0.7},  # Parallel edge
...         (2, 1)  # Tree node 2 to leaf
...     ],
...     nodes=[(1, {'label': 'A'})]
... )
>>> net.get_edge_attribute(3, 2, key=0, attr='branch_length')
0.5
>>> net.get_edge_attribute(3, 2, key=1, attr='branch_length')
0.7
>>> net.get_edge_attribute(3, 2, key=0)  # Get all attributes
{'branch_length': 0.5, 'bootstrap': 0.95}
>>> net.get_edge_attribute(2, 1)  # Get all attributes
{}
get_gamma(u: T, v: T, key: int | None = None) float | None[source]#

Get gamma value for a hybrid edge.

Gamma values can only be set on hybrid edges (edges pointing into hybrid nodes). If ANY gamma value is specified for edges entering a hybrid node, then ALL edges entering that hybrid node must have gamma values, and they must sum to exactly 1.0.

Parameters:
  • u (T) – Edge endpoints (v must be a hybrid node).

  • v (T) – Edge endpoints (v must be a hybrid node).

  • key (int | None, optional) – Edge key for parallel edges. Required if multiple parallel edges exist.

Returns:

Gamma value, or None if not set.

Return type:

float | None

Examples

>>> net = DirectedPhyNetwork(
...     edges=[
...         (7, 5), (7, 6),  # Root to tree nodes
...         {'u': 5, 'v': 4, 'gamma': 0.6},  # Hybrid edge
...         {'u': 6, 'v': 4, 'gamma': 0.4},  # Hybrid edge (Sum = 1.0)
...         (5, 8), (6, 9),  # Tree nodes also have other children
...         (4, 1)  # Hybrid to leaf
...     ],
...     nodes=[(1, {'label': 'A'}), (8, {'label': 'B'}), (9, {'label': 'C'})]
... )
>>> net.get_gamma(5, 4)
0.6
>>> net.get_gamma(6, 4)
0.4
get_label(node_id: T) str | None[source]#

Get the label for a node.

Parameters:

node_id (T) – Node identifier.

Returns:

Label for the node. Returns None if node has no label. Leaves always have labels (taxa), but internal nodes may be unlabeled.

Return type:

str | None

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1)], nodes=[(1, {'label': 'A'})])
>>> net.get_label(1)
'A'
>>> net.get_label(3) is None
True
get_network_attribute(key: str | None = None) dict[str, Any] | Any | None[source]#

Get network-level attribute(s).

Parameters:

key (str | None, optional) – Attribute key. If None, returns all attributes as a dict. If specified, returns the value of that specific attribute. By default None.

Returns:

If key is None: dict of all network attributes (empty dict if no attributes). If key is specified: attribute value, or None if not set.

Return type:

dict[str, Any] | Any | None

Examples

>>> net = DirectedPhyNetwork(
...     edges=[(3, 1)],
...     nodes=[(1, {'label': 'A'})],
...     attributes={'source': 'file.nex', 'version': '1.0'}
... )
>>> net.get_network_attribute('source')
'file.nex'
>>> net.get_network_attribute('nonexistent') is None
True
>>> net.get_network_attribute()  # Get all attributes
{'source': 'file.nex', 'version': '1.0'}
get_node_attribute(node_id: T, attr: str | None = None) dict[str, Any] | Any | None[source]#

Get node attribute(s).

Parameters:
  • node_id (T) – Node identifier.

  • attr (str | None, optional) – Attribute name. If None, returns all attributes as a dict. If specified, returns the value of that specific attribute. By default None.

Returns:

If attr is None: dict of all node attributes (empty dict if no attributes). If attr is specified: attribute value, or None if not set.

Return type:

dict[str, Any] | Any | None

Raises:

PhyloZooValueError – If the node does not exist in the network.

Examples

>>> net = DirectedPhyNetwork(
...     edges=[(3, 1)],
...     nodes=[(1, {'label': 'A'}), (3, {'label': 'root', 'custom': 42})]
... )
>>> net.get_node_attribute(1, 'label')
'A'
>>> net.get_node_attribute(3, 'label')
'root'
>>> net.get_node_attribute(3, 'custom')
42
>>> net.get_node_attribute(1, 'nonexistent') is None
True
>>> net.get_node_attribute(1)  # Get all attributes
{'label': 'A'}
>>> net.get_node_attribute(3)  # Get all attributes
{'label': 'root', 'custom': 42}
get_node_id(label: str) T | None[source]#

Get the node ID for a label.

Parameters:

label (str) – Node label.

Returns:

Node ID if found, None otherwise.

Return type:

T | None

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1)], nodes=[(1, {'label': 'A'})])
>>> net.get_node_id("A")
1
has_edge(u: T, v: T, key: int | None = None) bool[source]#

Check if edge exists.

Parameters:
  • u (T) – Source node.

  • v (T) – Target node.

  • key (int | None, optional) – Edge key. By default None.

Returns:

True if edge exists, False otherwise.

Return type:

bool

property hybrid_edges: set[tuple[T, T, int]]#

Get the set of all hybrid edges with keys.

Hybrid edges are edges that point into hybrid nodes.

Returns:

Set of (source, target, key) tuples for hybrid edges. Returns a new set (which is mutable).

Return type:

set[tuple[T, T, int]]

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 2), (4, 2)], nodes=[(2, {'label': 'A'})])
>>> net.hybrid_edges
{(3, 2, 0), (4, 2, 0)}
property hybrid_nodes: set[T]#

Get the set of all hybrid nodes.

A hybrid node is a node with in-degree >= 2 and out-degree 1.

Returns:

Set of hybrid node identifiers. Returns a new set (which is mutable).

Return type:

set[T]

Examples

>>> net = DirectedPhyNetwork(edges=[(5, 4), (6, 4), (4, 1)], nodes=[(1, {'label': 'A'})])
>>> net.hybrid_nodes
{4}
incident_child_edges(v: T, keys: bool = False, data: bool = False) Iterator[tuple[T, T] | tuple[T, T, int] | tuple[T, T, int, dict[str, Any]]][source]#

Return an iterator over edges leaving node v (to child nodes).

Parameters:
  • v (T) – Node identifier.

  • keys (bool, optional) – If True, return edge keys. By default False.

  • data (bool, optional) – If True, return edge data. By default False.

Returns:

Iterator over outgoing edges as (v, u) or (v, u, key) or (v, u, key, data).

Return type:

Iterator

Examples

>>> net = DirectedPhyNetwork(
...     edges=[
...         {'u': 1, 'v': 2, 'branch_length': 0.5},
...         {'u': 1, 'v': 3, 'branch_length': 0.3}
...     ],
...     nodes=[(2, {'label': 'A'}), (3, {'label': 'B'})]
... )
>>> list(net.incident_child_edges(1))
[(1, 2), (1, 3)]
>>> list(net.incident_child_edges(1, data=True))
[(1, 2, {'branch_length': 0.5}), (1, 3, {'branch_length': 0.3})]
incident_parent_edges(v: T, keys: bool = False, data: bool = False) Iterator[tuple[T, T] | tuple[T, T, int] | tuple[T, T, int, dict[str, Any]]][source]#

Return an iterator over edges entering node v (from parent nodes).

Parameters:
  • v (T) – Node identifier.

  • keys (bool, optional) – If True, return edge keys. By default False.

  • data (bool, optional) – If True, return edge data. By default False.

Returns:

Iterator over incoming edges as (u, v) or (u, v, key) or (u, v, key, data).

Return type:

Iterator

Examples

>>> net = DirectedPhyNetwork(
...     edges=[
...         {'u': 1, 'v': 2, 'branch_length': 0.5},
...         {'u': 3, 'v': 2, 'branch_length': 0.3}
...     ],
...     nodes=[(2, {'label': 'A'})]
... )
>>> list(net.incident_parent_edges(2))
[(1, 2), (3, 2)]
>>> list(net.incident_parent_edges(2, data=True))
[(1, 2, {'branch_length': 0.5}), (3, 2, {'branch_length': 0.3})]
indegree(v: T) int[source]#

Return the in-degree of node v.

Parameters:

v (T) – Node identifier.

Returns:

In-degree of v.

Return type:

int

property internal_nodes: set[T]#

Get the set of all internal (non-root, non-leaf) nodes.

Returns:

Set of internal node identifiers. Returns a new set (which is mutable).

Return type:

set[T]

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> net.internal_nodes
{3}
is_tree() bool[source]#

Check if the network is a tree.

A tree has no hybrid nodes.

Returns:

True if the network is a tree, False otherwise.

Return type:

bool

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> net.is_tree()
True
property leaves: set[T]#

Get the set of leaf node IDs (nodes with no outgoing edges).

Returns:

Set of leaf node identifiers. Returns a new set (which is mutable).

Return type:

set[T]

neighbors(v: T) Iterator[T][source]#

Return an iterator over neighbors of node v (parents and children).

Parameters:

v (T) – Node identifier.

Returns:

Iterator over neighbors.

Return type:

Iterator[T]

property nodes#

View of all node IDs in the network.

Cached. Same callable/view interface as the underlying graph’s nodes (e.g. net.nodes(), net.nodes(data=True)).

Returns:

Set-like, callable view of node identifiers.

Return type:

DirectedMultiGraph.NodeView

number_of_edges() int[source]#

Return the number of edges.

Returns:

Number of edges.

Return type:

int

number_of_nodes() int[source]#

Return the number of nodes.

Returns:

Number of nodes.

Return type:

int

outdegree(v: T) int[source]#

Return the out-degree of node v.

Parameters:

v (T) – Node identifier.

Returns:

Out-degree of v.

Return type:

int

parents(v: T) Iterator[T][source]#

Return an iterator over parent nodes of v.

Parameters:

v (T) – Node identifier.

Returns:

Iterator over parent nodes.

Return type:

Iterator[T]

property root_node: T#

Return the root node of the network.

The root is the node with in-degree 0.

Returns:

Root node identifier.

Return type:

T

Raises:

PhyloZooValueError – If there is no root node or multiple root nodes.

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> net.root_node
3
property taxa: set[str]#

Get the set of taxon labels (labels of leaves).

Returns:

Set of taxon labels.

Return type:

set[str]

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> net.taxa
{'A', 'B'}
property tree_edges: set[tuple[T, T, int]]#

Get the set of all tree edges with keys.

Tree edges are all edges that are not hybrid edges.

Returns:

Set of (source, target, key) tuples for tree edges. Returns a new set (which is mutable).

Return type:

set[tuple[T, T, int]]

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> len(net.tree_edges)
2
property tree_nodes: set[T]#

Get the set of all tree nodes.

A tree node is an internal node (non-root, non-leaf) with in-degree 1 and out-degree >= 2.

Returns:

Set of tree node identifiers. Returns a new set (which is mutable).

Return type:

set[T]

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> net.tree_nodes
{3}
validate() None[source]#

Validate the network structure and edge attributes.

Checks:

  1. Network is connected (weakly connected)

  2. No self-loops

  3. Directed acyclic graph (no directed cycles)

  4. Single root node (exactly one node with in-degree 0)

  5. Leaf nodes: all have in-degree 1 and out-degree 0

  6. Internal nodes: all have either (in-degree 1 and out-degree >= 2) or (in-degree >= 2 and out-degree 1)

  7. Bootstrap values: all bootstrap values must be in [0.0, 1.0]

  8. Gamma constraints: gamma can only be set on hybrid edges, and if any gamma is specified for a hybrid node, all incoming edges must have gamma values summing to 1.0

  9. Branch length constraints: for each set of parallel edges, if one edge has branch_length, all must have branch_length, and all values must be the same

Raises:

Notes

This method performs comprehensive validation of the network structure and edge attributes. See class docstring for detailed validation rules. Empty networks (no nodes) are considered valid but will raise a warning. Single-node networks (where root and leaf are the same node) are also valid but will raise a warning.

Features#

Network features module.

This module provides functions to extract and identify features of directed phylogenetic networks (e.g., LSA node, blobs, omnians, etc.).

phylozoo.core.network.dnetwork.features.blobs(network: DirectedPhyNetwork, trivial: bool = True, leaves: bool = True) list[set[T]][source]#

Get blobs of the network.

A blob is a maximal subgraph without any cut-edges. This function provides filtering options to control which blobs are returned.

Parameters:
  • network (DirectedPhyNetwork) – The directed phylogenetic network.

  • trivial (bool, optional) – Whether to include trivial (single-node) blobs. By default True.

  • leaves (bool, optional) – Whether to include blobs that contain only leaves. By default True.

Returns:

List of sets of nodes forming each blob.

Return type:

list[set[T]]

Raises:

PhyloZooValueError – If trivial=False and leaves=True (this combination is not possible since leaves are single-node components).

Notes

Blobs are computed as bi-edge connected components (2-edge-connected components). A bi-edge connected component is a maximal subgraph that remains connected after removing any single edge (i.e., has no cut-edges/bridges).

Results are cached using LRU cache with maxsize=128.

Examples

>>> # Network with hybrid node creating a non-trivial blob (cycle)
>>> net = DirectedPhyNetwork(
...     edges=[
...         (8, 5), (8, 6),  # Root to tree nodes
...         (5, 1), (5, 2),  # Tree node 5: in=1, out=2
...         (6, 3), (6, 9),  # Tree node 6: in=1, out=2
...         (5, 4), (6, 4),  # Both lead to hybrid node 4
...         (4, 7),  # Hybrid 4: in=2, out=1
...         (7, 10), (7, 11),  # Tree node 7: in=1, out=2
...     ],
...     nodes=[
...         (1, {'label': 'A'}),
...         (2, {'label': 'B'}),
...         (3, {'label': 'C'}),
...         (9, {'label': 'D'}),
...         (10, {'label': 'E'}),
...         (11, {'label': 'F'}),
...     ]
... )
>>> sorted([sorted(b) for b in blobs(net)])
[[1], [2], [3], [4, 5, 6, 8], [7], [9], [10], [11]]
>>> # Filtering: exclude trivial (single-node) blobs
>>> len([b for b in blobs(net, trivial=False, leaves=False)])
1
>>> # Filtering: exclude blobs containing only leaves
>>> len([b for b in blobs(net, leaves=False)])
2
phylozoo.core.network.dnetwork.features.cut_edges(network: DirectedPhyNetwork) set[tuple[T, T, int]][source]#

Find all cut-edges (bridges) in the network.

A cut-edge is an edge whose removal increases the number of weakly connected components. Results are cached per network instance.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network.

Returns:

Set of cut-edges as 3-tuples (u, v, key).

Return type:

set[tuple[T, T, int]]

Examples

>>> net = DirectedPhyNetwork(edges=[(1, 2), (2, 3)], nodes=[(3, {'label': 'A'})])
>>> edges = cut_edges(net)
>>> (1, 2, 0) in edges and (2, 3, 0) in edges
True

Notes

Results are cached using LRU cache with maxsize=128.

phylozoo.core.network.dnetwork.features.cut_vertices(network: DirectedPhyNetwork) set[T][source]#

Find all cut-vertices (articulation points) in the network.

A cut-vertex is a vertex whose removal increases the number of weakly connected components. Results are cached per network instance.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network.

Returns:

Set of cut-vertices.

Return type:

set[T]

Examples

>>> net = DirectedPhyNetwork(edges=[(1, 2), (2, 3), (2, 4)], nodes=[(3, {'label': 'A'}), (4, {'label': 'B'})])
>>> vertices = cut_vertices(net)
>>> 2 in vertices
True
>>> 1 in vertices
False

Notes

Results are cached using LRU cache with maxsize=128.

phylozoo.core.network.dnetwork.features.k_blobs(network: DirectedPhyNetwork, k: int, trivial: bool = True, leaves: bool = True) list[set[T]][source]#

Get k-blobs of the network.

A k-blob is a blob with exactly k edges incident to it. An incident edge is any edge that connects a node inside the blob to a node outside the blob. Parallel edges are counted separately.

Parameters:
  • network (DirectedPhyNetwork) – The directed phylogenetic network.

  • k (int) – The number of edges that should be incident to each returned blob.

  • trivial (bool, optional) – Whether to include trivial (single-node) blobs. By default True.

  • leaves (bool, optional) – Whether to include blobs that contain only leaves. By default True.

Returns:

List of sets of nodes forming each k-blob.

Return type:

list[set[T]]

Raises:

PhyloZooValueError – If trivial=False and leaves=True (this combination is not possible since leaves are single-node components).

Notes

This function identifies blobs and then filters them based on the number of incident edges. Parallel edges are counted separately, so if there are two parallel edges crossing the blob boundary, they count as two incident edges.

Results are cached using LRU cache with maxsize=128.

Examples

>>> # Tree network: leaves are 1-blobs, internal nodes are 2-blobs or more
>>> net = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> sorted([sorted(b) for b in k_blobs(net, k=1)])
[[1], [2]]
>>> sorted([sorted(b) for b in k_blobs(net, k=2)])
[[3]]
phylozoo.core.network.dnetwork.features.lsa_node(network: DirectedPhyNetwork) T[source]#

Find the Least Stable Ancestor (LSA) node of a directed phylogenetic network.

The LSA is the lowest node through which all paths from the root to the leaves pass. In other words, it is the unique node that is an ancestor of all leaves and is the lowest such node (has maximum depth from the root).

Parameters:

network (DirectedPhyNetwork[T]) – The directed phylogenetic network.

Returns:

The LSA node identifier.

Return type:

T

Raises:

Examples

>>> # LSA below the root (tree node 10 is lowest node on all root-to-leaf paths)
>>> net = DirectedPhyNetwork(
...     edges=[
...         (7, 5), (7, 6),                 # root to tree nodes
...         (5, 4, 0), (5, 4, 1),           # parallel edges keep tree node 5 out-degree >= 2
...         (6, 4, 0), (6, 4, 1),           # parallel edges keep tree node 6 out-degree >= 2
...         (4, 10),                        # hybrid 4 (in-degree 4, out-degree 1) to tree node 10
...         (10, 1), (10, 2)                # tree node 10 splits to leaves
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> lsa_node(net)
10
>>> # In a simple tree, the LSA is just the root
>>> net2 = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> lsa_node(net2)
3
phylozoo.core.network.dnetwork.features.omnians(network: DirectedPhyNetwork) set[T][source]#

Find all omnian nodes in a directed phylogenetic network.

An omnian is an internal node (non-leaf) where all of its children are hybrid nodes. See [Jetten and van Iersel, 2016] for more details.

Parameters:

network (DirectedPhyNetwork[T]) – The directed phylogenetic network.

Returns:

Set of omnian node identifiers.

Return type:

set[T]

Warns:

PhyloZooWarning – If the network contains parallel edges, as omnians are not defined for networks with parallel edges in the original paper. Behavior may be unexpected.

Examples

>>> # Network with omnians (nodes 5 and 8 both have all children as hybrids)
>>> net = DirectedPhyNetwork(
...     edges=[
...         (7, 5), (7, 8), (7, 9),  # Root to tree nodes
...         (5, 4), (5, 6),  # Node 5 to hybrid nodes 4 and 6
...         (8, 4), (8, 6),  # Node 8 to hybrid nodes 4 and 6
...         (9, 4), (9, 6),  # Node 9 to hybrid nodes 4 and 6
...         (4, 1), (6, 2)   # Hybrids to leaves
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> sorted(omnians(net))
[5, 8, 9]
>>> # Network with no omnians
>>> net2 = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> omnians(net2)
set()

Classifications#

Classification functions for directed phylogenetic networks.

This module provides functions to classify and check properties of directed phylogenetic networks (e.g., is_tree, is_binary, level, etc.).

phylozoo.core.network.dnetwork.classifications.has_parallel_edges(network: DirectedPhyNetwork) bool[source]#

Check if the network has any parallel edges.

Parallel edges are multiple edges between the same pair of nodes in the same direction.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to check.

Returns:

True if the network has at least one pair of parallel edges, False otherwise.

Return type:

bool

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> has_parallel_edges(net)
False
phylozoo.core.network.dnetwork.classifications.is_binary(network: DirectedPhyNetwork) bool[source]#

Check if the network is binary.

A network is binary if every internal node has degree exactly 3, except for the root node which must have degree exactly 2.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to check.

Returns:

True if the network is binary, False otherwise.

Return type:

bool

Notes

For empty networks or single-node networks, this function returns True.

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> is_binary(net)
True
phylozoo.core.network.dnetwork.classifications.is_galled(network: DirectedPhyNetwork) bool[source]#

Check if the network is galled.

A network is galled if no hybrid node is ancestral to another hybrid node in the same blob.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to check.

Returns:

True if the network is galled, False otherwise.

Return type:

bool

Notes

For empty networks or networks with no hybrid nodes, this function returns True. All trees are galled networks.

Examples

>>> # Network with no hybrid nodes (galled)
>>> net = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> is_galled(net)
True
>>> # Network with single hybrid in its own blob (galled)
>>> net = DirectedPhyNetwork(
...     edges=[
...         (7, 5), (7, 6),  # Root to tree nodes
...         (5, 4), (6, 4),  # Both lead to hybrid 4
...         (4, 8),  # Hybrid to tree node
...         (8, 1), (8, 2)  # Tree node to leaves
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> is_galled(net)
True
>>> # Network with hybrid ancestral to another hybrid in same blob (not galled)
>>> net = DirectedPhyNetwork(
...     edges=[
...         (9, 5), (9, 6),  # Root to tree nodes
...         (5, 4), (6, 4),  # Both lead to hybrid 4
...         (4, 7), (8, 7),  # Hybrid 4 and tree node 8 lead to hybrid 7
...         (7, 1)  # Hybrid 7 to leaf
...     ],
...     nodes=[(1, {'label': 'A'})]
... )
>>> is_galled(net)
False
phylozoo.core.network.dnetwork.classifications.is_lsa_network(network: DirectedPhyNetwork) bool[source]#

Check whether a directed phylogenetic network is an LSA network.

An LSA (Least Stable Ancestor) network is one where the root node is the LSA node, i.e., the lowest node through which all root-to-leaf paths pass.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to check.

Returns:

True if the network’s root node equals its LSA node, False otherwise.

Return type:

bool

Notes

For empty networks this function returns True.

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> is_lsa_network(net)
True
phylozoo.core.network.dnetwork.classifications.is_normal(network: DirectedPhyNetwork) bool[source]#

Check if the network is normal.

A reticulation arc (u, v) is a shortcut if there is a directed path from u to v that does not traverse (u, v). A normal network is a tree-child network without shortcuts.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to check.

Returns:

True if the network is normal, False otherwise.

Return type:

bool

Notes

For empty networks or single-node networks, this function returns True. All trees are normal networks. Networks with parallel edges are never normal (parallel edges are shortcuts).

Examples

>>> # Tree (normal)
>>> net = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> is_normal(net)
True
>>> # Tree-child network without shortcuts (normal)
>>> net = DirectedPhyNetwork(
...     edges=[
...         (7, 5), (7, 6),  # Root to tree nodes
...         (5, 4), (6, 4),  # Both lead to hybrid 4
...         (4, 8),  # Hybrid to tree node
...         (8, 1), (8, 2)  # Tree node to leaves
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> is_normal(net)
True
>>> # Network with shortcut (not normal)
>>> # Edge (5, 4) is a shortcut because path 5 -> 9 -> 4 exists
>>> net = DirectedPhyNetwork(
...     edges=[
...         (10, 5), (10, 6),  # Root to tree nodes
...         (5, 4), (5, 9), (6, 4),  # Tree nodes lead to hybrid 4
...         (9, 4),  # This creates a shortcut: path 5 -> 9 -> 4 bypasses edge (5, 4)
...         (4, 1)  # Hybrid to leaf
...     ],
...     nodes=[(1, {'label': 'A'})]
... )
>>> is_normal(net)
False
phylozoo.core.network.dnetwork.classifications.is_simple(network: DirectedPhyNetwork) bool[source]#

Check if the network is simple.

A network is simple if it has at most one non-leaf blob.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to check.

Returns:

True if the network has at most one non-leaf blob, False otherwise.

Return type:

bool

Notes

For empty networks, this function returns True.

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> is_simple(net)
True
phylozoo.core.network.dnetwork.classifications.is_stackfree(network: DirectedPhyNetwork) bool[source]#

Check if the network is stack-free.

A network is stack-free if no hybrid node has another hybrid node as its child. In other words, there are no “stacked” hybrid nodes.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to check.

Returns:

True if the network is stack-free (no hybrid has a hybrid child), False otherwise.

Return type:

bool

Examples

>>> # Network with no hybrids (stack-free)
>>> net = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> is_stackfree(net)
True
>>> # Network with hybrid that has tree node child (stack-free)
>>> net = DirectedPhyNetwork(
...     edges=[
...         (7, 5), (7, 6),  # Root to tree nodes
...         (5, 4), (6, 4),  # Both lead to hybrid 4
...         (4, 8),  # Hybrid to tree node
...         (8, 1), (8, 2)  # Tree node to leaves
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> is_stackfree(net)
True
>>> # Network with stacked hybrids (not stack-free)
>>> net = DirectedPhyNetwork(
...     edges=[
...         (9, 5), (9, 6),  # Root to tree nodes
...         (5, 4), (6, 4),  # Both lead to hybrid 4
...         (4, 7), (8, 7),  # Hybrid 4 and tree node 8 lead to hybrid 7
...         (7, 1)  # Hybrid 7 to leaf
...     ],
...     nodes=[(1, {'label': 'A'})]
... )
>>> is_stackfree(net)
False
phylozoo.core.network.dnetwork.classifications.is_tree(network: DirectedPhyNetwork) bool[source]#

Check if the network is a tree.

A network is a tree if it has no hybrid edges.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to check.

Returns:

True if the network is a tree, False otherwise.

Return type:

bool

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> is_tree(net)
True
phylozoo.core.network.dnetwork.classifications.is_treebased(network: DirectedPhyNetwork) bool[source]#

Check if the network is tree-based.

A network is tree-based if it is a base-tree with additional arcs.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to check.

Returns:

True if the network is tree-based, False otherwise.

Return type:

bool

Raises:

PhyloZooNotImplementedError – If the network is non-binary or has parallel edges.

Notes

For empty networks or single-node networks, this function returns True. The implementation uses the omnian characterization of tree-based networks [Jetten and van Iersel, 2016].

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> is_treebased(net)
True
phylozoo.core.network.dnetwork.classifications.is_treechild(network: DirectedPhyNetwork) bool[source]#

Check if the network is tree-child.

A phylogenetic network is tree-child if every internal vertex has at least one child that is not a hybrid node.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to check.

Returns:

True if the network is tree-child, False otherwise.

Return type:

bool

Notes

For empty networks or single-node networks, this function returns True. All trees are tree-child networks.

Examples

>>> # Tree (tree-child)
>>> net = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> is_treechild(net)
True
>>> # Network with hybrid that has tree node child (tree-child)
>>> net = DirectedPhyNetwork(
...     edges=[
...         (7, 5), (7, 6),  # Root to tree nodes
...         (5, 4), (6, 4),  # Both lead to hybrid 4
...         (4, 8),  # Hybrid to tree node
...         (8, 1), (8, 2)  # Tree node to leaves
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> is_treechild(net)
True
>>> # Network where internal node has only hybrid children (not tree-child)
>>> net = DirectedPhyNetwork(
...     edges=[
...         (9, 5), (9, 6),  # Root to tree nodes
...         (5, 4), (6, 4),  # Both lead to hybrid 4
...         (5, 7), (6, 7),  # Both lead to hybrid 7
...         (4, 1), (7, 2)  # Hybrids to leaves
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> is_treechild(net)
False
phylozoo.core.network.dnetwork.classifications.is_ultrametric(network: DirectedPhyNetwork) bool[source]#

Check if the network is ultrametric.

A network is ultrametric if all root-to-leaf distances are equal. All paths from root to each leaf must have the same distance.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to check.

Returns:

True if the network is ultrametric, False otherwise.

Return type:

bool

Raises:

Notes

For empty networks or single-node networks, this function returns True. Every edge must have a branch length specified. If parallel edges have different branch lengths, the network is not ultrametric (returns False). The distance from root to a leaf is the sum of branch lengths along a directed path.

Examples

>>> # Tree with equal distances (ultrametric)
>>> net = DirectedPhyNetwork(
...     edges=[
...         {'u': 3, 'v': 1, 'branch_length': 1.0},
...         {'u': 3, 'v': 2, 'branch_length': 1.0}
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> is_ultrametric(net)
True
>>> # Tree with different distances (not ultrametric)
>>> net = DirectedPhyNetwork(
...     edges=[
...         {'u': 3, 'v': 1, 'branch_length': 1.0},
...         {'u': 3, 'v': 2, 'branch_length': 2.0}
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> is_ultrametric(net)
False
phylozoo.core.network.dnetwork.classifications.level(network: DirectedPhyNetwork) int[source]#

Return the (strict) level of the network.

The level is the maximum over all blobs of (number of hybrid edges minus number of hybrid nodes) in that blob.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network.

Returns:

The level of the network.

Return type:

int

Notes

For empty networks, this function returns 0.

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> level(net)
0
phylozoo.core.network.dnetwork.classifications.reticulation_number(network: DirectedPhyNetwork) int[source]#

Return the reticulation number of the network.

The reticulation number is the total number of hybrid edges minus the total number of hybrid nodes.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network.

Returns:

The reticulation number of the network.

Return type:

int

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> reticulation_number(net)
0
phylozoo.core.network.dnetwork.classifications.vertex_level(network: DirectedPhyNetwork) int[source]#

Return the vertex level of the network.

The vertex level is the maximum over all blobs of the number of hybrid nodes in that blob.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network.

Returns:

The vertex level of the network.

Return type:

int

Notes

For empty networks, this function returns 0.

Examples

>>> net = DirectedPhyNetwork(edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})])
>>> vertex_level(net)
0

Transformations#

Network transformations module.

This module provides functions to transform directed phylogenetic networks (e.g., suppress degree-2 nodes, convert to LSA network, etc.).

phylozoo.core.network.dnetwork.transformations.binary_resolution(network: DirectedPhyNetwork) DirectedPhyNetwork[source]#

Convert a non-binary network to a binary network by resolving high-degree nodes.

This function creates a binary resolution of the network by replacing nodes with in-degree > 2 or out-degree > 2 with caterpillar structures. The first neighbor (incoming or outgoing) is kept, and additional neighbors are connected through a caterpillar chain.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to convert.

Returns:

A new binary network. If the input network is already binary, returns a copy.

Return type:

DirectedPhyNetwork

Raises:

PhyloZooValueError – If the network has parallel edges (binary resolution is not defined for networks with parallel edges).

Notes

Attribute handling:

  • All attributes are removed except branch_length and gamma

  • Branch length handling: If the original network had branch lengths on any edges, new edges in the caterpillar structures are assigned branch_length=0.0. Original edges keep their branch_length values.

  • Gamma handling (for high in-degree nodes only): The top two hybrid edges in the caterpillar maintain the same ratio as the original top two hybrid edges. As we go down the caterpillar, each new hybrid edge’s gamma accounts for the cumulative probability from previous edges. Gamma values are computed using _compute_caterpillar_gammas().

  • Node labels are preserved

  • The network must not have parallel edges

Examples

>>> # Network with high out-degree node
>>> net = DirectedPhyNetwork(
...     edges=[
...         (5, 1), (5, 2), (5, 3), (5, 4)  # Node 5 has out-degree 4
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (3, {'label': 'C'}), (4, {'label': 'D'})]
... )
>>> binary_net = binary_resolution(net)
>>> binary_net.is_binary()
True
phylozoo.core.network.dnetwork.transformations.identify_parallel_edges(network: DirectedPhyNetwork) DirectedPhyNetwork[source]#

Identify all parallel edges and suppress all degree-2 nodes exhaustively.

This function iteratively: 1. Identifies all parallel edges (removes all but one, keeping branch_length) 2. Suppresses all degree-2 nodes (sums branch_lengths, removes other attributes)

The process continues until no more changes occur, as suppression may create new parallel edges, and identification may create new degree-2 nodes.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to transform.

Returns:

A new network with all parallel edges identified and all degree-2 nodes suppressed.

Return type:

DirectedPhyNetwork

Raises:

Notes

  • Branch lengths are preserved: summed when suppressing degree-2 nodes, kept from first edge when identifying parallel edges (all should be same by validation).

  • Gamma values: summed when identifying parallel edges, preserved from edge2 when suppressing degree-2 nodes (if edge2 has gamma, otherwise no gamma is included).

  • All other edge attributes (bootstrap, etc.) are removed.

  • Node labels and other node attributes are preserved.

  • Leaves are never suppressed.

Examples

>>> net = DirectedPhyNetwork(
...     edges=[
...         {'u': 1, 'v': 2, 'branch_length': 0.5},
...         {'u': 1, 'v': 2, 'branch_length': 0.5},  # Parallel edges: node 2 is hybrid (in-degree 2)
...         {'u': 2, 'v': 3, 'branch_length': 0.3},  # Node 2 to tree node 3
...         {'u': 3, 'v': 4, 'branch_length': 0.2},  # Node 3 to leaf
...         {'u': 3, 'v': 5, 'branch_length': 0.1},  # Node 3 to leaf (keeps node 3 valid)
...         {'u': 1, 'v': 6, 'branch_length': 0.1}
...     ],
...     nodes=[(4, {'label': 'A'}), (5, {'label': 'B'}), (6, {'label': 'C'})]
... )
>>> result = identify_parallel_edges(net)
>>> # Step 1: Parallel edges 1->2 identified -> node 2 becomes degree-2 (in-degree 1, out-degree 1)
>>> # Step 2: Degree-2 node 2 suppressed -> edge 1->3 with branch_length=0.8 (0.5+0.3)
>>> # Result: edges 1->3 (0.8), 3->4 (0.2), 3->5 (0.1), 1->6 (0.1)
phylozoo.core.network.dnetwork.transformations.suppress_2_blobs(network: DirectedPhyNetwork) DirectedPhyNetwork[source]#

Suppress all 2-blobs in the network.

A 2-blob is a blob with exactly 2 incident edges. This function:

  1. Finds all 2-blobs using k_blobs

  2. For each 2-blob (except those containing the root), identifies all vertices in the blob with the first vertex (creating a degree-2 node), then suppresses the degree-2 node using proper attribute merging

  3. Returns a new validated network

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to transform.

Returns:

A new network with all 2-blobs suppressed (except root-containing ones). The network is validated before being returned.

Return type:

DirectedPhyNetwork

Raises:

PhyloZooNetworkError – If the transformation creates an invalid network structure.

Notes

  • 2-blobs containing the root node are skipped (special case for directed networks)

  • After identifying vertices in a 2-blob, the kept vertex becomes degree-2 and is then suppressed

  • Edge attributes (branch_length, gamma) are properly merged during suppression

  • The function works on a copy of the graph, so the original network is not modified

Examples

>>> net = DirectedPhyNetwork(
...     edges=[(1, 2), (2, 3), (3, 4), (4, 5), (1, 6)],
...     nodes=[(5, {'label': 'A'}), (6, {'label': 'B'})]
... )
>>> result = suppress_2_blobs(net)
>>> result.validate()  # Should not raise
phylozoo.core.network.dnetwork.transformations.to_lsa_network(network: DirectedPhyNetwork) DirectedPhyNetwork[source]#

Create a new LSA-network by removing everything above the LSA node.

This function finds the LSA (Least Stable Ancestor) node and creates a new network that contains only the LSA node and all nodes/edges below it. The LSA becomes the new root of the resulting network.

Note: All branch lengths, bootstrap values, gamma values, and other edge attributes from edges above the LSA are removed (as those edges are removed). Edge attributes for edges below the LSA are preserved.

Parameters:

network (DirectedPhyNetwork[T]) – The directed phylogenetic network.

Returns:

A new network with the LSA as the root.

Return type:

DirectedPhyNetwork[T]

Raises:

PhyloZooValueError – If the network is empty or has no leaves.

Examples

>>> net = DirectedPhyNetwork(
...     edges=[
...         (7, 5), (7, 6),                 # root to tree nodes
...         (5, 4, 0), (5, 4, 1),           # parallel edges keep tree node 5 out-degree >= 2
...         (6, 4, 0), (6, 4, 1),           # parallel edges keep tree node 6 out-degree >= 2
...         (4, 10),                        # hybrid 4 (in-degree 4, out-degree 1) to tree node 10
...         (10, 1), (10, 2)                # tree node 10 splits to leaves
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> lsa_net = to_lsa_network(net)
>>> lsa_net.root_node
4
>>> sorted(lsa_net.leaves)
[1, 2]

Derivations#

Network derivations module.

This module provides functions to derive other data structures from directed phylogenetic networks (e.g., splits, quartets, distances, blobtrees, subnetworks, etc.).

phylozoo.core.network.dnetwork.derivations.displayed_quartets(network: DirectedPhyNetwork) QuartetProfileSet[source]#

Compute quartet profile set from all displayed trees of the network.

This function converts the directed network to a semi-directed network and then uses the semi-directed displayed_quartets function. This ensures that quartets are unrooted (not rooted quartets).

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network.

Returns:

A quartet profile set where each profile corresponds to a 4-taxon set, and contains quartets from displayed trees weighted by their probabilities.

Return type:

QuartetProfileSet

Examples

>>> net = DirectedPhyNetwork(
...     edges=[(5, 4), (6, 4), (4, 1), (4, 2), (5, 3), (6, 7)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (3, {'label': 'C'}), (7, {'label': 'D'})]
... )
>>> profileset = displayed_quartets(net)
>>> isinstance(profileset, QuartetProfileSet)
True
>>> len(profileset) > 0
True
phylozoo.core.network.dnetwork.derivations.displayed_splits(network: DirectedPhyNetwork) WeightedSplitSystem[source]#

Compute weighted split system from all displayed trees of the network.

This function iterates through all displayed trees of the network and collects their induced splits, weighted by the probability of each displayed tree. If a split appears in multiple displayed trees, their probabilities are summed.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network.

Returns:

A weighted split system where each split’s weight is the sum of probabilities of all displayed trees that contain that split.

Return type:

WeightedSplitSystem

Examples

>>> net = DirectedPhyNetwork(
...     edges=[
...         (10, 5), (10, 6),  # Root to tree nodes
...         (5, 4), (6, 4),    # Both lead to hybrid 4 (in-degree 2)
...         (4, 8),            # Hybrid to tree node
...         (8, 1), (8, 2),    # Tree node to leaves
...         (5, 3), (6, 7)     # Additional leaves to satisfy degree constraints
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (3, {'label': 'C'}), (7, {'label': 'D'})]
... )
>>> splits = displayed_splits(net)
>>> isinstance(splits, WeightedSplitSystem)
True
>>> len(splits) > 0
True
phylozoo.core.network.dnetwork.derivations.displayed_trees(network: DirectedPhyNetwork, probability: bool = False) Iterator[DirectedPhyNetwork][source]#

Generate all displayed trees of a directed phylogenetic network.

A displayed tree is obtained by:

  1. Taking a switching (deleting all but one parent edge per hybrid node)

  2. Exhaustively removing degree-1 nodes that are not leaves or the root

  3. Suppressing all degree-2 nodes

Parameters:
  • network (DirectedPhyNetwork) – The directed phylogenetic network.

  • probability (bool, optional) – If True, store the probability of the displayed tree in the network’s ‘probability’ attribute. The probability is inherited from the switching and equals the product of gamma values for the kept hybrid edges. If a hybrid edge has no gamma value, it is taken to be 1/k where k is the in-degree of the hybrid node. If there are no hybrid nodes, the probability is 1.0. By default False.

Yields:

DirectedPhyNetwork – A displayed tree of the network. If probability=True, the network has a ‘probability’ attribute containing the tree probability.

Examples

>>> net = DirectedPhyNetwork(
...     edges=[
...         (10, 5), (10, 6),
...         (5, 4), (6, 4),
...         (4, 8), (8, 1), (8, 2), (5, 3), (6, 7)
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (3, {'label': 'C'}), (7, {'label': 'D'})]
... )
>>> trees = list(displayed_trees(net))
>>> len(trees)
2  # Two switchings yield two displayed trees
phylozoo.core.network.dnetwork.derivations.distances(network: DirectedPhyNetwork, mode: Literal['shortest', 'longest', 'average'] = 'average') DistanceMatrix[source]#

Compute pairwise distances between taxa based on switchings.

This function computes distances by considering all switchings of the network. For each pair of taxa, the distance is computed in each switching (sum of branch lengths along the unique path), and then aggregated according to the specified mode.

Parameters:
  • network (DirectedPhyNetwork) – The directed phylogenetic network.

  • mode (Literal['shortest', 'longest', 'average'], optional) –

    Distance aggregation mode:

    • ’shortest’: Take the minimum distance across all switchings

    • ’longest’: Take the maximum distance across all switchings

    • ’average’: Take the probability-weighted average across all switchings

    By default ‘average’.

Returns:

A distance matrix with pairwise distances between all taxa.

Return type:

DistanceMatrix

Examples

>>> net = DirectedPhyNetwork(
...     edges=[
...         (10, 5), (10, 6),
...         (5, 4), (6, 4),
...         (4, 8), (8, 1), (8, 2), (5, 3), (6, 7)
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (3, {'label': 'C'}), (7, {'label': 'D'})]
... )
>>> dm = distances(net, mode='shortest')
>>> len(dm)
4
>>> dm.get_distance('A', 'B')
2.0  # Example distance
phylozoo.core.network.dnetwork.derivations.induced_splits(network: DirectedPhyNetwork) SplitSystem[source]#

Extract all splits induced by cut-edges of the network.

This function:

  1. Converts the network to an LSA network

  2. Suppresses all 2-blobs (which don’t influence splits)

  3. Finds all cut-edges

  4. For each cut-edge, computes the split it induces (2-partition of taxa)

The split induced by a cut-edge is the 2-partition of taxa obtained when removing that edge from the network.

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network.

Returns:

A split system containing all splits induced by cut-edges.

Return type:

SplitSystem

Examples

>>> net = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> splits = induced_splits(net)
>>> len(splits) >= 1
True

Notes

The network is first converted to an LSA network and then the tree-of-blobs is computed. Since the tree-of-blobs has the same split system as the original network, we can efficiently compute splits using a single DFS traversal of the tree structure.

phylozoo.core.network.dnetwork.derivations.k_taxon_subnetworks(network: DirectedPhyNetwork, k: int, suppress_2_blobs: bool = False, identify_parallel_edges: bool = False, make_lsa: bool = False) Iterator[DirectedPhyNetwork][source]#

Generate all subnetworks induced by exactly k taxa.

This function yields all possible subnetworks of the network that are induced by exactly k taxon labels. For each combination of k taxa, the corresponding subnetwork is computed using the subnetwork function.

Parameters:
  • network (DirectedPhyNetwork) – Source network.

  • k (int) – Number of taxa to include in each subnetwork. Must be between 0 and the number of taxa in the network (inclusive).

  • suppress_2_blobs (bool, default False) – If True, suppress all 2-blobs in each resulting subnetwork.

  • identify_parallel_edges (bool, default False) – If True, identify/merge parallel edges in each resulting subnetwork.

  • make_lsa (bool, default False) – If True, convert each result to an LSA-network.

Yields:

DirectedPhyNetwork – Subnetworks induced by exactly k taxa. Each subnetwork is generated lazily as the iterator is consumed.

Raises:

PhyloZooValueError – If k < 0 or k > number of taxa in the network.

Examples

>>> net = DirectedPhyNetwork(
...     edges=[(5, 3), (5, 4), (3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})]
... )
>>> # Generate all 2-taxon subnetworks
>>> subnetworks = list(k_taxon_subnetworks(net, k=2))
>>> len(subnetworks)
3  # C(3,2) = 3 combinations
>>> # Each subnetwork has exactly 2 leaves
>>> all(len(subnet.leaves) == 2 for subnet in subnetworks)
True
>>> # Generate all 1-taxon subnetworks
>>> single_taxon_subs = list(k_taxon_subnetworks(net, k=1))
>>> len(single_taxon_subs)
3  # C(3,1) = 3 combinations
phylozoo.core.network.dnetwork.derivations.partition_from_blob(network: DirectedPhyNetwork, blob: set[Any], return_edge_taxa: bool = False) Partition | tuple[Partition, list[tuple[Any, Any, frozenset[str]]]][source]#

Get the partition of taxa induced by removing a blob from the network.

When all nodes in the blob are removed, the network splits into connected components. Each component’s taxa form a part of the partition.

Parameters:
  • network (DirectedPhyNetwork) – The directed phylogenetic network.

  • blob (set[Any]) – Set of nodes forming the blob to remove. All nodes in this set will be removed to compute the partition.

  • return_edge_taxa (bool, optional) – If True, also return a list of tuples (u, v, taxa_set) where u is a node in the component, v is a node in the blob, and taxa_set is the frozenset of taxa in that component. By default False.

Returns:

If return_edge_taxa is False: The partition of taxa induced by removing the blob. If return_edge_taxa is True: A tuple (partition, edge_taxa_list) where edge_taxa_list is a list of (u, v, taxa_set) tuples connecting each component to the blob.

Return type:

Partition | tuple[Partition, list[tuple[Any, Any, frozenset[str]]]]

Raises:
  • PhyloZooValueError – If blob is empty or if blob contains nodes not in the network. If blob is not a non-leaf blob (internal blob).

  • PhyloZooAlgorithmError – If could not find edge connecting component with taxa to blob. If removing the blob does not disconnect the network (blob is not a cut-blob).

Examples

>>> from phylozoo.core.network.dnetwork import DirectedPhyNetwork
>>> net = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2), (3, 4)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})]
... )
>>> partition = partition_from_blob(net, {3})
>>> len(partition)
3
phylozoo.core.network.dnetwork.derivations.split_from_cutedge(network: DirectedPhyNetwork, u: Any, v: Any, key: int | None = None, return_node_taxa: bool = False) Split | tuple[Split, tuple[Any, frozenset[str]], tuple[Any, frozenset[str]]][source]#

Get the split induced by a cut-edge in the network.

This function removes the specified edge from the network and finds the taxa on either side of the resulting partition. If the edge is not a cut-edge (i.e., removing it does not disconnect the graph), an error is raised.

Parameters:
  • network (DirectedPhyNetwork) – The directed phylogenetic network.

  • u (Any) – First node of the edge.

  • v (Any) – Second node of the edge.

  • key (int | None, optional) – Edge key for parallel edges. If None and multiple parallel edges exist, raises PhyloZooValueError. If None and exactly one edge exists, that edge is used. By default None.

  • return_node_taxa (bool, optional) – If True, also returns tuples (u, taxa1) and (v, taxa2) indicating which node is on which side of the split. By default False.

Returns:

If return_node_taxa is False: The split induced by the cut-edge. If return_node_taxa is True: A tuple (split, (u, taxa1), (v, taxa2)) where taxa1 are the taxa on the side of u and taxa2 are the taxa on the side of v.

Return type:

Split | tuple[Split, tuple[Any, frozenset[str]], tuple[Any, frozenset[str]]]

Raises:

PhyloZooValueError – If the edge does not exist, if multiple parallel edges exist and key is None, or if the edge is not a cut-edge (removal does not disconnect the graph).

Examples

>>> from phylozoo.core.network.dnetwork import DirectedPhyNetwork
>>> net = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2), (3, 4)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})]
... )
>>> split = split_from_cutedge(net, 3, 1)
>>> 'A' in split.set1 or 'A' in split.set2
True
phylozoo.core.network.dnetwork.derivations.subnetwork(network: DirectedPhyNetwork, taxa: list[str], suppress_2_blobs: bool = False, identify_parallel_edges: bool = False, make_lsa: bool = False) DirectedPhyNetwork[source]#

Extract the subnetwork induced by a subset of taxa (leaf labels).

The subnetwork is defined as the union of all directed paths from the requested leaves up to the root (i.e., all their ancestors and the leaves themselves). The induced subgraph is taken on the underlying DirectedMultiGraph, then degree-2 internal nodes are suppressed. Optionally, the result can be post-processed by suppressing 2-blobs, identifying parallel edges, and/or converting to an LSA network. After any of these optional steps, degree-2 suppression is applied again to clean up artifacts.

Parameters:
  • network (DirectedPhyNetwork) – Source network.

  • taxa (list[str]) – Subset of taxon labels (leaf labels) to induce the subnetwork on.

  • suppress_2_blobs (bool, default False) – If True, suppress all 2-blobs in the resulting network.

  • identify_parallel_edges (bool, default False) – If True, identify/merge parallel edges in the resulting network.

  • make_lsa (bool, default False) – If True, convert the result to an LSA-network.

Returns:

The derived subnetwork. Returns an empty network if taxa is empty.

Return type:

DirectedPhyNetwork

Raises:

PhyloZooValueError – If any of the provided taxa are not found in the network.

phylozoo.core.network.dnetwork.derivations.to_sd_network(d_network: DirectedPhyNetwork) SemiDirectedPhyNetwork[source]#

Convert a DirectedPhyNetwork to a SemiDirectedPhyNetwork.

Steps:

  1. If the directed network is not an LSA network, replace it by its LSA-network.

  2. Undirect all non-hybrid edges; hybrid edges remain directed.

  3. Suppress any degree-2 node (this stems from a degree-2 root). Suppression may create parallel edges. Suppression connects the two neighbors directly: undirected+undirected -> undirected; directed+directed (u->x, x->v) -> directed (u->v); directed into x and undirected out (u->x, x-v) -> undirected (u-v); undirected into x and directed out (u-x, x->v) -> directed (u->v).

Parameters:

d_network (DirectedPhyNetwork) – The directed phylogenetic network to convert.

Returns:

The corresponding semi-directed phylogenetic network.

Return type:

SemiDirectedPhyNetwork

Examples

>>> # Simple tree (no hybrids) - all edges become undirected
>>> dnet = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> sdnet = to_sd_network(dnet)
>>> sdnet.number_of_directed_edges()
0
>>> sdnet.number_of_undirected_edges()
2
>>> # Network with hybrids - hybrid edges remain directed
>>> dnet = DirectedPhyNetwork(
...     edges=[
...         (4, 1), (4, 2),  # Tree edges from root
...         {'u': 1, 'v': 3, 'gamma': 0.6},  # Hybrid edge
...         {'u': 2, 'v': 3, 'gamma': 0.4}   # Hybrid edge
...     ],
...     nodes=[(3, {'label': 'C'})]
... )
>>> sdnet = to_sd_network(dnet)
>>> sdnet.number_of_directed_edges()  # Hybrid edges
2
>>> sdnet.number_of_undirected_edges()  # Tree edges
2
phylozoo.core.network.dnetwork.derivations.tree_of_blobs(network: DirectedPhyNetwork) DirectedPhyNetwork[source]#

Create a tree of blobs by suppressing all 2-blobs and collapsing internal blobs.

This function:

  1. Suppresses all 2-blobs using suppress_2_blobs

  2. Finds all internal blobs (blobs with more than 1 node, excluding leaves)

  3. For each internal blob, identifies all vertices with a single vertex

  4. Returns a new network representing the tree of blobs

Parameters:

network (DirectedPhyNetwork) – The directed phylogenetic network to transform.

Returns:

A new network where each blob has been collapsed to a single vertex, forming a tree structure.

Return type:

DirectedPhyNetwork

Examples

>>> # Create a directed network with a hybrid
>>> from phylozoo.core.network.dnetwork.classifications import is_tree
>>> dnet = DirectedPhyNetwork(
...     edges=[
...         (10, 5), (10, 6),  # Root to tree nodes
...         (5, 4), (6, 4),    # Both lead to hybrid 4 (in-degree 2)
...         (4, 8),            # Hybrid to tree node
...         (8, 1), (8, 2),    # Tree node to leaves
...         (5, 3), (6, 7)     # Additional leaves to satisfy degree constraints
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (3, {'label': 'C'}), (7, {'label': 'D'})]
... )
>>> tree_net = tree_of_blobs(dnet)
>>> is_tree(tree_net)  # Should be True
True

Conversions#

Network conversion module.

This module provides functions for converting between different graph representations and directed phylogenetic networks.

phylozoo.core.network.dnetwork.conversions.dnetwork_from_graph(graph: nx.DiGraph | nx.MultiDiGraph | DirectedMultiGraph[T]) DirectedPhyNetwork[T][source]#

Create a DirectedPhyNetwork from a NetworkX DiGraph, MultiDiGraph, or phylozoo DirectedMultiGraph.

All edges from the input graph are treated as directed edges. Edge attributes, node attributes, and graph-level attributes are preserved and passed through to the resulting network.

Parameters:

graph (nx.DiGraph | nx.MultiDiGraph | DirectedMultiGraph[T]) – The graph to convert. Can be a NetworkX DiGraph, MultiDiGraph, or a DirectedMultiGraph from the primitives module.

Returns:

A new directed phylogenetic network with edges and labels from the graph.

Return type:

DirectedPhyNetwork[T]

Raises:
  • PhyloZooTypeError – If graph is not one of the supported types (nx.DiGraph, nx.MultiDiGraph, or DirectedMultiGraph).

  • PhyloZooValueError – If the resulting network is invalid according to DirectedPhyNetwork validation rules (e.g., not a DAG, invalid node degrees, etc.).

Notes

  • Edge attributes: All edge attributes (e.g., branch_length, gamma, bootstrap) are preserved and passed through to the network.

  • Node attributes: All node attributes are preserved. The label attribute is used for taxon labels on leaf nodes.

  • Graph attributes: Graph-level attributes are preserved and stored in the network’s attributes dictionary.

  • Validation: The network is validated upon creation. If the graph structure does not meet DirectedPhyNetwork requirements (e.g., must be a DAG, leaves must have in-degree 1, etc.), a ValueError is raised.

Examples

>>> import networkx as nx
>>> G = nx.DiGraph()
>>> G.add_edge(0, 1, branch_length=0.5)
>>> G.add_edge(0, 2, branch_length=0.3)
>>> G.nodes[1]['label'] = 'A'
>>> G.nodes[2]['label'] = 'B'
>>> G.graph['source'] = 'test'
>>> net = dnetwork_from_graph(G)
>>> isinstance(net, DirectedPhyNetwork)
True
>>> net.get_label(1)
'A'
>>> net.get_branch_length(0, 1)
0.5
>>> net.get_network_attribute('source')
'test'

Isomorphism#

Isomorphism module for directed phylogenetic networks.

This module provides functions for checking network isomorphism between DirectedPhyNetwork instances.

phylozoo.core.network.dnetwork.isomorphism.is_isomorphic(net1: DirectedPhyNetwork[T], net2: DirectedPhyNetwork[T], node_attrs: list[str] | None = None, edge_attrs: list[str] | None = None, graph_attrs: list[str] | None = None) bool[source]#

Check if two directed phylogenetic networks are isomorphic.

Two networks are isomorphic if there exists a bijection between their node sets that preserves adjacency, edge direction, parallel edges, and node labels. Labels are always checked (non-optional), and additional node, edge, and graph attributes can be specified.

Parameters:
  • net1 (DirectedPhyNetwork) – First directed phylogenetic network.

  • net2 (DirectedPhyNetwork) – Second directed phylogenetic network.

  • node_attrs (list[str] | None, optional) – List of additional node attribute names to match (beyond ‘label’). If None, only ‘label’ is checked. By default None.

  • edge_attrs (list[str] | None, optional) – List of edge attribute names to match. If None, edge attributes are ignored. By default None.

  • graph_attrs (list[str] | None, optional) – List of graph-level attribute names to match. If None, graph attributes are ignored. By default None.

Returns:

True if the networks are isomorphic, False otherwise.

Return type:

bool

Examples

>>> from phylozoo.core.network.dnetwork import DirectedPhyNetwork
>>> net1 = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> net2 = DirectedPhyNetwork(
...     edges=[(4, 5), (4, 6)],
...     nodes=[(5, {'label': 'A'}), (6, {'label': 'B'})]
... )
>>> is_isomorphic(net1, net2)
True
>>> # Different labels: not isomorphic
>>> net3 = DirectedPhyNetwork(
...     edges=[(4, 5), (4, 6)],
...     nodes=[(5, {'label': 'A'}), (6, {'label': 'C'})]
... )
>>> is_isomorphic(net1, net3)
False
>>> # With additional node attributes
>>> net4 = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A', 'type': 'leaf'}), (2, {'label': 'B', 'type': 'leaf'})]
... )
>>> net5 = DirectedPhyNetwork(
...     edges=[(4, 5), (4, 6)],
...     nodes=[(5, {'label': 'A', 'type': 'leaf'}), (6, {'label': 'B', 'type': 'leaf'})]
... )
>>> is_isomorphic(net4, net5, node_attrs=['type'])
True
>>> # With edge attributes
>>> net6 = DirectedPhyNetwork(
...     edges=[{'u': 3, 'v': 1, 'branch_length': 0.5}],
...     nodes=[(1, {'label': 'A'})]
... )
>>> net7 = DirectedPhyNetwork(
...     edges=[{'u': 4, 'v': 5, 'branch_length': 0.5}],
...     nodes=[(5, {'label': 'A'})]
... )
>>> is_isomorphic(net6, net7, edge_attrs=['branch_length'])
True

Notes

  • Labels are always checked (non-optional) to ensure networks with different taxon labels are not considered isomorphic.

  • The function uses the underlying DirectedMultiGraph isomorphism checking.

  • For node attributes, if a node doesn’t have an attribute, it only matches with nodes that also don’t have that attribute (None matches None).

I/O#

Network I/O module.

This module registers format handlers for DirectedPhyNetwork with FormatRegistry for use with the IOMixin system.

All eNewick functionality (parser, writer, reader) is in the _enewick module. DOT format support uses the underlying DirectedMultiGraph functions, but uses labels as node names where possible.

phylozoo.core.network.dnetwork.io.from_dot(dot_string: str, **kwargs: Any) DirectedPhyNetwork[source]#

Parse a DOT format string and create a DirectedPhyNetwork.

This function parses DOT format and creates a DirectedPhyNetwork. Node names in DOT are used as labels if they are valid labels (strings), otherwise they are used as node IDs.

Parameters:
  • dot_string (str) – DOT format string containing graph data.

  • **kwargs – Additional arguments (currently unused, for compatibility).

Returns:

Parsed directed phylogenetic network.

Return type:

DirectedPhyNetwork

Raises:

PhyloZooFormatError – If the DOT string is malformed or cannot be parsed, or if the resulting network is invalid according to DirectedPhyNetwork validation rules.

Examples

>>> dot_str = '''digraph {
...     A [label="Species A"];
...     B [label="Species B"];
...     root -> A;
...     root -> B;
... }'''
>>> net = from_dot(dot_str)
>>> net.number_of_nodes()
3
>>> 'A' in net.taxa or net.get_label(list(net.leaves)[0]) == 'A'
True

Notes

  • Node names in DOT become labels if they are strings

  • If a node has a ‘label’ attribute in DOT, it overrides the node name

  • Edge attributes are preserved

  • Graph attributes are preserved

  • The network is validated after creation

phylozoo.core.network.dnetwork.io.to_dot(network: DirectedPhyNetwork, **kwargs: Any) str[source]#

Convert a DirectedPhyNetwork to a DOT format string.

This function uses labels as node names in DOT where available, falling back to node IDs if no label is present. This makes the DOT output more readable for phylogenetic networks where leaves have meaningful taxon labels.

Parameters:
  • network (DirectedPhyNetwork) – The directed phylogenetic network to convert.

  • **kwargs – Additional arguments: - graph_name (str): Optional name for the graph (default: ‘’).

Returns:

The DOT format string representation of the network.

Return type:

str

Examples

>>> net = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> dot_str = to_dot(net)
>>> 'digraph' in dot_str
True
>>> 'A ->' in dot_str or '-> A' in dot_str or 'A [' in dot_str
True

Notes

  • Nodes with labels use the label as the node name in DOT

  • Nodes without labels use the node ID as the node name

  • All node and edge attributes are preserved

  • Graph attributes are included

  • Parallel edges are supported

phylozoo.core.network.dnetwork.io.to_enewick(network: DirectedPhyNetwork, **kwargs: Any) str[source]#

Convert a DirectedPhyNetwork to Extended Newick (eNewick) format.

This function serializes a directed phylogenetic network to the Extended Newick format, which supports hybrid nodes (reticulations) using the #H marker notation.

Features: - Branch lengths are encoded as :length (e.g., :0.5) - Gamma and bootstrap values are encoded as comments: [&gamma=0.6,bootstrap=0.95] - Internal node labels are included when present - Hybrid nodes use Extended Newick #Hn markers - Output is deterministic (children sorted by node ID then label)

Parameters:
  • network (DirectedPhyNetwork) – The directed phylogenetic network to serialize.

  • **kwargs – Additional arguments (currently unused, for compatibility).

Returns:

The eNewick string representation of the network.

Return type:

str

Raises:

ValueError – If the network is empty (has no nodes).

Examples

>>> # Simple tree
>>> net = DirectedPhyNetwork(
...     edges=[(3, 1), (3, 2)],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> to_enewick(net)
'(A,B);'
>>> # Tree with branch lengths
>>> net = DirectedPhyNetwork(
...     edges=[
...         {'u': 3, 'v': 1, 'branch_length': 0.5},
...         {'u': 3, 'v': 2, 'branch_length': 0.3}
...     ],
...     nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]
... )
>>> to_enewick(net)
'(A:0.5,B:0.3);'

Notes

  • For parallel edges between the same nodes, only the first edge is used

  • Labels containing special characters (spaces, parentheses, colons, etc.) are automatically quoted with single quotes

  • The output is deterministic: multiple calls on the same network produce the same string

phylozoo.core.network.dnetwork.io.from_enewick(enewick_string: str, **kwargs: Any) DirectedPhyNetwork[source]#

Parse an eNewick string and create a DirectedPhyNetwork.

This function parses an Extended Newick (eNewick) format string and converts it to a DirectedPhyNetwork. It supports: - Branch lengths on edges - Hybrid nodes (reticulations) using #H markers - Gamma and bootstrap values in comments - Node labels (quoted and unquoted) - Internal node labels

Parameters:
  • enewick_string (str) – The eNewick format string to parse. Must end with ‘;’.

  • **kwargs – Additional arguments (currently unused, for compatibility).

Returns:

Parsed directed phylogenetic network.

Return type:

DirectedPhyNetwork

Raises:
  • ENewickParseError – If the eNewick string is malformed or cannot be parsed.

  • PhyloZooValueError – If the parsed network structure is invalid for DirectedPhyNetwork.

Examples

>>> # Simple tree
>>> net = from_enewick("((A,B),C);")
>>> net.number_of_nodes()
4
>>> net.number_of_edges()
3
>>> # Tree with branch lengths
>>> net = from_enewick("((A:0.5,B:0.3):0.1,C:0.2);")
>>> net.get_branch_length(0, 'A')
0.5

Notes

  • Comments starting with ‘&’ are parsed for edge attributes (gamma, bootstrap)

  • Hybrid nodes are identified by #H markers

  • Internal nodes without labels get auto-generated integer IDs

  • Leaves use their label as the node ID