sdnetwork#
Semi-directed and mixed network module.
This module provides the SemiDirectedPhyNetwork and MixedPhyNetwork classes and related functions for working with semi-directed phylogenetic networks. Semi-directed networks contain both directed edges (reticulation arcs) and undirected edges (tree edges). Internal nodes have degree >= 3, and hybrid nodes have at least one incoming directed edge. The public API is re-exported here; the implementation is split across the base, sd_phynetwork, features, classifications, transformations, derivations, conversions, isomorphism, and io submodules.
Main Classes#
Mixed network module.
This module provides classes and functions for working with mixed phylogenetic networks.
- class phylozoo.core.network.sdnetwork.base.MixedPhyNetwork(directed_edges: list[tuple[T, T] | tuple[T, T, int] | dict[str, Any]] | None = None, undirected_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:
objectA mixed phylogenetic network.
A MixedPhyNetwork is a weakly connected, mixed multigraph (with both directed and undirected edges) representing a phylogenetic network structure. This is an abstract network type which may have undirected cycles, used for canonical forms and to address unidentifiability issues. For semi-directed phylogenetic networks without undirected cycles, use the SemiDirectedPhyNetwork subclass.
It consists of:
Leaf nodes: Nodes with no outgoing directed edges, each with a taxon label
Tree nodes: Internal nodes with in-degree 0 and total degree >= 3
Hybrid nodes: Internal nodes with in-degree >= 2 and total degree = in-degree + 1
A MixedPhyNetwork is obtained from a directed phylogenetic LSA (Least Stable Ancestor) network by undirecting all non-hybrid edges, optionally undirecting all hybrid edges for selected hybrid nodes (if one hybrid edge is undirected, all partner hybrid edges are undirected), and suppressing degree-2 nodes.
- Parameters:
directed_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 attributes.
Can be empty or None for empty/single-node networks. By default None.
undirected_edges (list[tuple[T, T] | tuple[T, T, int] | dict[str, Any]] | None, optional) –
List of undirected 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])
Note: Undirected edges cannot have gamma values.
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 attributes.
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
MixedMultiGraphand is immutable after initialization; construct vianodes/directed_edges/undirected_edges, from a prebuiltMixedMultiGraph, or load from a file/eNewick string.Examples
>>> # Initialize with nodes and labels >>> net = MixedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})] ... ) >>> net.taxa {'A', 'B', 'C'}
>>> # Partial labels - uncovered leaves get auto-generated labels >>> net2 = MixedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4), (3, 5)], ... nodes=[(1, {'label': 'A'})] ... ) >>> net2.taxa # 2, 4, and 5 are auto-labeled {'A', '2', '4', '5'}
>>> # Network with branch lengths and bootstrap support >>> net3 = MixedPhyNetwork( ... undirected_edges=[ ... {'u': 3, 'v': 1, 'branch_length': 0.5, 'bootstrap': 0.95}, ... {'u': 3, 'v': 2, 'branch_length': 0.3, 'bootstrap': 0.87}, ... (3, 4) ... ], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})] ... ) >>> net3.get_branch_length(3, 1) 0.5 >>> net3.get_bootstrap(3, 1) 0.95
>>> # Network with hybrid node and gamma values >>> net4 = MixedPhyNetwork( ... directed_edges=[ ... {'u': 5, 'v': 4, 'gamma': 0.6}, # Hybrid edge ... {'u': 6, 'v': 4, 'gamma': 0.4} # Hybrid edge (Sum = 1.0) ... ], ... undirected_edges=[(4, 1), (4, 2), (4, 3)], # Tree edges ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (3, {'label': 'C'})] ... ) >>> net4.get_gamma(5, 4) 0.6 >>> net4.get_gamma(6, 4) 0.4
- __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:
- __iter__() Iterator[T][source]#
Iterate over nodes.
- Returns:
Iterator over node identifiers.
- Return type:
Iterator[T]
- __repr__() str[source]#
Return string representation of the network.
- Returns:
String representation showing nodes, edges, level, taxa count, and taxon list.
- Return type:
Examples
>>> net = MixedPhyNetwork(undirected_edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]) >>> repr(net) 'MixedPhyNetwork(nodes=3, edges=2, taxa=2, taxa_list=[A, B])'
- copy() MixedPhyNetwork[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:
Examples
>>> net = MixedPhyNetwork(undirected_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 (undirected + in-degree + out-degree).
- Return type:
- property edges#
View of all edges (directed and undirected) 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:
- 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.
- get_branch_length(u: T, v: T, key: int | None = None) float | None[source]#
Get branch length for an edge.
- 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 PhyloZooValueError. 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:
- Raises:
PhyloZooValueError – If the edge does not exist, or if key is None and multiple parallel edges exist.
Notes
Networks will not have undirected and directed edges with the same endpoints, so this method searches both directed and undirected edges (directed first).
- 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 directed hybrid edges (directed edges pointing into hybrid nodes). Undirected edges cannot have gamma values. 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, edge must be directed).
v (T) – Edge endpoints (v must be a hybrid node, edge must be directed).
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
- Raises:
PhyloZooValueError – If the edge is undirected (gamma can only be on directed edges).
- 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 = MixedPhyNetwork(undirected_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:
Examples
>>> net = MixedPhyNetwork( ... undirected_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:
- Raises:
PhyloZooValueError – If the node does not exist in the network.
Examples
>>> net = MixedPhyNetwork( ... undirected_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 = MixedPhyNetwork(undirected_edges=[(3, 1)], nodes=[(1, {'label': 'A'})]) >>> net.get_node_id("A") 1
- has_edge(u: T, v: T, key: int | None = None, directed: bool | None = None) bool[source]#
Check if edge exists.
- Parameters:
- Returns:
True if edge exists, False otherwise.
- Return type:
- property hybrid_edges: set[tuple[T, T, int]]#
Get the set of all hybrid edges with keys.
Hybrid edges are all directed edges.
- Returns:
Set of (source, target, key) tuples for hybrid edges. Returns a new set (which is mutable).
- Return type:
Examples
>>> net = MixedPhyNetwork( ... directed_edges=[(3, 2), (4, 2)], ... undirected_edges=[(2, 1)], ... nodes=[(1, {'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 total degree = in-degree + 1.
- Returns:
Set of hybrid node identifiers. Returns a new set (which is mutable).
- Return type:
set[T]
Examples
>>> net = MixedPhyNetwork( ... directed_edges=[(5, 4), (6, 4)], ... undirected_edges=[(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 directed edges leaving node v (to child nodes).
- 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 directed edges entering node v (from parent nodes).
- incident_undirected_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 undirected edges incident to node v.
- indegree(v: T) int[source]#
Return the in-degree of node v (directed edges only).
- Parameters:
v (T) – Node identifier.
- Returns:
In-degree of v.
- Return type:
- property internal_nodes: set[T]#
Get the set of all internal nodes.
Internal nodes are all nodes that are not leaves.
- Returns:
Set of internal node identifiers. Returns a new set (which is mutable).
- Return type:
set[T]
Examples
>>> net = MixedPhyNetwork( ... directed_edges=[(5, 4), (6, 4)], ... undirected_edges=[(4, 1)], ... nodes=[(1, {'label': 'A'})] ... ) >>> sorted(net.internal_nodes) [4, 5, 6]
- property leaves: set[T]#
Get the set of leaf node IDs (nodes with degree 1, or degree 0 for single-node networks).
- 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.
Neighbors include nodes connected by both directed and undirected edges.
- Parameters:
v (T) – Node identifier.
- Returns:
Iterator over neighbors.
- Return type:
Iterator[T]
Examples
>>> net = MixedPhyNetwork( ... directed_edges=[(1, 2)], ... undirected_edges=[(2, 3)], ... nodes=[(3, {'label': 'A'})] ... ) >>> sorted(net.neighbors(2)) [1, 3]
- 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:
- number_of_edges() int[source]#
Return the number of edges.
- Returns:
Number of edges (directed + undirected).
- Return type:
- outdegree(v: T) int[source]#
Return the out-degree of node v (directed edges only).
- Parameters:
v (T) – Node identifier.
- Returns:
Out-degree of v.
- Return type:
- property taxa: set[str]#
Get the set of taxon labels (labels of leaves).
Examples
>>> net = MixedPhyNetwork(undirected_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 simply all undirected edges.
- Returns:
Set of (source, target, key) tuples for tree edges. Returns a new set (which is mutable).
- Return type:
Examples
>>> net = MixedPhyNetwork( ... directed_edges=[(5, 4), (6, 4)], ... undirected_edges=[(4, 1)], ... nodes=[(1, {'label': 'A'})] ... ) >>> net.tree_edges {(4, 1, 0)}
- property tree_nodes: set[T]#
Get the set of all tree nodes.
A tree node is a node with in-degree 0 and total degree >= 3.
- Returns:
Set of tree node identifiers. Returns a new set (which is mutable).
- Return type:
set[T]
Examples
>>> net = MixedPhyNetwork( ... undirected_edges=[(1, 2), (1, 3), (1, 4)], ... nodes=[(2, {'label': 'A'}), (3, {'label': 'B'}), (4, {'label': 'C'})] ... ) >>> net.tree_nodes {1}
- undirected_degree(v: T) int[source]#
Return the undirected degree of node v.
- Parameters:
v (T) – Node identifier.
- Returns:
Undirected degree of v.
- Return type:
- validate() None[source]#
Validate the network structure and edge attributes.
Checks:
Network is connected (weakly connected)
No self-loops
All internal nodes have degree >= 3
Each node has indegree either 0 or total_degree-1
Mixed network constraint (issues warning that additional checks may be added)
Bootstrap values are in [0.0, 1.0]
Gamma constraints (gamma only on hybrid edges, sum to 1.0 if specified)
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:
PhyloZooNetworkStructureError – If connectivity or self-loop constraints are violated.
PhyloZooNetworkDegreeError – If degree constraints are violated.
PhyloZooNetworkAttributeError – If bootstrap, gamma, or branch length constraints are violated.
PhyloZooEmptyNetworkWarning – If empty network is detected.
PhyloZooSingleNodeNetworkWarning – If single-node network is detected.
- Warns:
UserWarning – Always issued to indicate that additional validation checks may be added later.
Notes
This method performs validation checks but issues a warning that additional checks may be added later. For fully validated networks with additional constraints, use SemiDirectedPhyNetwork. Empty networks (no nodes) are considered valid but will raise a warning. Single-node networks are also valid but will raise a warning.
Semi-directed network module.
This module provides classes and functions for working with semi-directed phylogenetic networks.
- class phylozoo.core.network.sdnetwork.sd_phynetwork.SemiDirectedPhyNetwork(directed_edges: list[tuple[T, T] | tuple[T, T, int] | dict[str, Any]] | None = None, undirected_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:
MixedPhyNetwork,IOMixinA semi-directed phylogenetic network.
A SemiDirectedPhyNetwork is a weakly connected, mixed multigraph (with both directed and undirected edges) representing a phylogenetic network structure. It is a subclass of MixedPhyNetwork with additional constraints: exactly the non-hybrid edges are undirected, and all hybrid edges remain directed. This ensures the network does not have undirected cycles.
It consists of: - Leaf nodes: Nodes with no outgoing directed edges, each with a taxon label - Tree nodes: Internal nodes with in-degree 0 and total degree >= 3 - Hybrid nodes: Internal nodes with in-degree >= 2 and total degree = in-degree + 1
A SemiDirectedPhyNetwork is obtained from a directed phylogenetic LSA (Least Stable Ancestor) network by undirecting all non-hybrid edges and suppressing degree-2 nodes.
- Parameters:
directed_edges (list[tuple[T, T] | tuple[T, T, int] | dict[str, Any]] | None, optional) –
List of directed edges (hybrid edges only). 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) - 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 attributes.
Can be empty or None for empty/single-node networks. By default None.
undirected_edges (list[tuple[T, T] | tuple[T, T, int] | dict[str, Any]] | None, optional) –
List of undirected edges (non-hybrid edges only). 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) - bootstrap (float in [0.0, 1.0])
Note: Undirected edges cannot have gamma values.
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 attributes.
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
MixedMultiGraphand is immutable after initialization; construct vianodes/directed_edges/undirected_edges, from a prebuiltMixedMultiGraph, or load from a file.Supported I/O formats:
enewick(default):.nwk,.newick,.enewick,.eNewick,.enwphylozoo-dot:.pzdot
Examples
>>> # Initialize with nodes and labels >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})] ... ) >>> net.taxa {'A', 'B', 'C'}
>>> # Partial labels - uncovered leaves get auto-generated labels >>> net2 = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4), (3, 5)], ... nodes=[(1, {'label': 'A'})] ... ) >>> net2.taxa # 2, 4, and 5 are auto-labeled {'A', '2', '4', '5'}
>>> # Network with branch lengths and bootstrap support >>> net3 = SemiDirectedPhyNetwork( ... undirected_edges=[ ... {'u': 3, 'v': 1, 'branch_length': 0.5, 'bootstrap': 0.95}, ... {'u': 3, 'v': 2, 'branch_length': 0.3, 'bootstrap': 0.87}, ... (3, 4) ... ], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})] ... ) >>> net3.get_branch_length(3, 1) 0.5 >>> net3.get_bootstrap(3, 1) 0.95
>>> # Network with hybrid node and gamma values >>> net4 = SemiDirectedPhyNetwork( ... directed_edges=[ ... {'u': 5, 'v': 4, 'gamma': 0.6}, # Hybrid edge ... {'u': 6, 'v': 4, 'gamma': 0.4} # Hybrid edge (Sum = 1.0) ... ], ... undirected_edges=[(4, 1), (4, 2), (4, 3)], # Tree edges ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (3, {'label': 'C'})] ... ) >>> net4.get_gamma(5, 4) 0.6 >>> net4.get_gamma(6, 4) 0.4
- _graph#
Internal graph structure using MixedMultiGraph. Warning: Do not modify directly. Use class methods instead.
- Type:
- _node_to_label#
Mapping from node IDs to labels. Only nodes with explicit labels are included. Leaves always have labels (taxa), but internal nodes may be unlabeled.
- __repr__() str[source]#
Return string representation of the network.
- Returns:
String representation showing nodes, edges, taxa count, and taxon list.
- Return type:
Examples
>>> net = SemiDirectedPhyNetwork(undirected_edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]) >>> repr(net) 'SemiDirectedPhyNetwork(nodes=3, edges=2, taxa=2, taxa_list=[A, B])'
- copy() SemiDirectedPhyNetwork[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:
Examples
>>> net = SemiDirectedPhyNetwork(undirected_edges=[(3, 1)], nodes=[(1, {'label': 'A'})]) >>> net2 = net.copy() >>> net.number_of_nodes() 2 >>> net2.number_of_nodes() 2 >>> isinstance(net2, SemiDirectedPhyNetwork) True
- validate() None[source]#
Validate the network structure and edge attributes.
Checks all constraints from MixedPhyNetwork plus additional semi-directed network constraints: - All hybrid edges are directed - All non-hybrid edges are undirected
- Raises:
PhyloZooNetworkStructureError – If connectivity, self-loop, or semi-directed network constraints are violated.
PhyloZooNetworkDegreeError – If degree constraints are violated.
PhyloZooNetworkAttributeError – If bootstrap or gamma constraints are violated.
PhyloZooEmptyNetworkWarning – If empty network is detected.
PhyloZooSingleNodeNetworkWarning – If single-node network is detected.
Notes
This method performs the same validation checks as MixedPhyNetwork, but replaces the mixed network constraint check with a semi-directed network constraint check.
Features#
Network features module.
This module provides functions to extract and identify features of semi-directed and mixed phylogenetic networks (e.g., blobs, omnians, etc.).
- phylozoo.core.network.sdnetwork.features.blobs(network: MixedPhyNetwork, 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 (MixedPhyNetwork) – The mixed 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:
- 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
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> # Network with hybrid node creating a non-trivial blob >>> net = SemiDirectedPhyNetwork( ... directed_edges=[ ... {'u': 6, 'v': 5, 'gamma': 0.6}, # Hybrid edge to node 5 ... {'u': 7, 'v': 5, 'gamma': 0.4}, # Hybrid edge to node 5 ... ], ... undirected_edges=[ ... (5, 1), # Hybrid node 5: in-degree 2, total degree 3 (2+1) ... (6, 2), (6, 3), (6, 7), # Tree node 6 connects to leaves and node 7 ... (7, 8), (7, 9), # Tree node 7 connects to leaves ... ], ... nodes=[ ... (1, {'label': 'A'}), ... (2, {'label': 'B'}), ... (3, {'label': 'C'}), ... (8, {'label': 'D'}), ... (9, {'label': 'E'}), ... ] ... ) >>> sorted([sorted(b) for b in blobs(net)]) [[1], [2], [3], [5, 6, 7], [8], [9]] >>> # 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)]) 1
- phylozoo.core.network.sdnetwork.features.cut_edges(network: MixedPhyNetwork) 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 connected components. Results are cached per network instance.
- Parameters:
network (MixedPhyNetwork) – The mixed phylogenetic network.
- Returns:
Set of cut-edges as 3-tuples (u, v, key).
- Return type:
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork( ... directed_edges=[{'u': 5, 'v': 4, 'gamma': 0.6}, {'u': 6, 'v': 4, 'gamma': 0.4}], ... undirected_edges=[(4, 2), (5, 8), (6, 9), (5, 10), (6, 11)], ... nodes=[(2, {'label': 'A'}), (8, {'label': 'B'}), (9, {'label': 'C'}), (10, {'label': 'D'}), (11, {'label': 'E'})] ... ) >>> edges = cut_edges(net) >>> len(edges) > 0 True
Notes
Results are cached using LRU cache with maxsize=128.
- phylozoo.core.network.sdnetwork.features.cut_vertices(network: MixedPhyNetwork) set[T][source]#
Find all cut-vertices (articulation points) in the network.
A cut-vertex is a vertex whose removal increases the number of connected components. Results are cached per network instance.
- Parameters:
network (MixedPhyNetwork) – The mixed phylogenetic network.
- Returns:
Set of cut-vertices.
- Return type:
set[T]
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork( ... directed_edges=[{'u': 5, 'v': 4, 'gamma': 0.6}, {'u': 6, 'v': 4, 'gamma': 0.4}], ... undirected_edges=[(7, 5), (7, 6), (7, 8), (4, 2), (5, 10), (6, 11)], ... nodes=[(2, {'label': 'A'}), (8, {'label': 'B'}), (10, {'label': 'C'}), (11, {'label': 'D'})] ... ) >>> vertices = cut_vertices(net) >>> len(vertices) > 0 True
Notes
Results are cached using LRU cache with maxsize=128.
- phylozoo.core.network.sdnetwork.features.k_blobs(network: MixedPhyNetwork, 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 (directed or undirected) that connects a node inside the blob to a node outside the blob. Parallel edges are counted separately.
- Parameters:
network (MixedPhyNetwork) – The mixed 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:
- 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. Both directed and undirected edges are counted. 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
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> # Tree network: leaves are 1-blobs, internal nodes are 2-blobs or more >>> net = SemiDirectedPhyNetwork( ... undirected_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.sdnetwork.features.omnians(network: MixedPhyNetwork) set[T][source]#
Stub for omnians function.
- phylozoo.core.network.sdnetwork.features.root_locations(network: SemiDirectedPhyNetwork) tuple[list[T], list[tuple[T, T, int]], list[tuple[T, T, int]]][source]#
Get all valid root locations for a semi-directed network.
Returns three separate lists: 1. Node root locations: non-leaf nodes in the source component 2. Undirected edge root locations: undirected edges in the source component 3. Directed edge root locations: directed edges exiting the source component
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network.
- Raises:
PhyloZooNetworkStructureError – If the network has multiple source components or an empty source component.
- Returns:
A tuple containing: - List of node root locations (non-leaf nodes in source component) - List of undirected edge root locations as (u, v, key) tuples - List of directed edge root locations as (u, v, key) tuples (outgoing from source component)
- Return type:
tuple[list[T], list[tuple[T, T, int]], list[tuple[T, T, int]]]
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})] ... ) >>> node_locs, undir_locs, dir_locs = root_locations(net) >>> 3 in node_locs # Node 3 is a valid root location True >>> (3, 1, 0) in undir_locs # Edge (3, 1) is also a valid root location True
Classifications#
Classification functions for semi-directed and mixed phylogenetic networks.
This module provides functions to classify and check properties of semi-directed and mixed phylogenetic networks (e.g., is_tree, is_binary, level, etc.).
- phylozoo.core.network.sdnetwork.classifications.has_parallel_edges(network: MixedPhyNetwork) bool[source]#
Check if the network has any parallel edges.
Parallel edges are multiple edges between the same pair of nodes, either as multiple directed edges in the same direction or multiple undirected edges.
- Parameters:
network (MixedPhyNetwork) – The mixed phylogenetic network to check.
- Returns:
True if the network has at least one pair of parallel edges, False otherwise.
- Return type:
Examples
>>> net = MixedPhyNetwork(undirected_edges=[(3, 1), (3, 2), (3, 4)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})]) >>> has_parallel_edges(net) False
- phylozoo.core.network.sdnetwork.classifications.is_binary(network: MixedPhyNetwork) bool[source]#
Check if the network is binary.
A network is binary if every internal node has degree exactly 3.
- Parameters:
network (MixedPhyNetwork) – The mixed phylogenetic network to check.
- Returns:
True if the network is binary, False otherwise.
- Return type:
Notes
For empty networks or single-node networks, this function returns True. Unlike directed networks, semi-directed networks have no root node, so all internal nodes must have degree 3.
Examples
>>> net = MixedPhyNetwork(undirected_edges=[(3, 1), (3, 2), (3, 4)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})]) >>> is_binary(net) True
- phylozoo.core.network.sdnetwork.classifications.is_galled(network: SemiDirectedPhyNetwork) 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. A hybrid node is ancestral to another one if there exists an up-down path from one to the other.
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network to check.
- Returns:
True if the network is galled, False otherwise.
- Return type:
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 = SemiDirectedPhyNetwork( ... undirected_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 = SemiDirectedPhyNetwork( ... directed_edges=[ ... (5, 4), (6, 4) # Both lead to hybrid 4 ... ], ... undirected_edges=[ ... (7, 5), (7, 6), # Root to tree nodes ... (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 = SemiDirectedPhyNetwork( ... directed_edges=[ ... (5, 4), (6, 4), # Both lead to hybrid 4 ... (4, 7), (8, 7) # Hybrid 4 and tree node 8 lead to hybrid 7 ... ], ... undirected_edges=[ ... (9, 5), (9, 6), # Root to tree nodes ... (7, 1) # Hybrid 7 to leaf ... ], ... nodes=[(1, {'label': 'A'})] ... ) >>> is_galled(net) False
- phylozoo.core.network.sdnetwork.classifications.is_simple(network: SemiDirectedPhyNetwork) bool[source]#
Check if the network is simple.
A network is simple if it has at most one non-leaf blob.
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network to check.
- Returns:
True if the network has at most one non-leaf blob, False otherwise.
- Return type:
Notes
For empty networks, this function returns True. Some authors call a simple network a “bloblet (network)”.
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork(undirected_edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]) >>> is_simple(net) True
- phylozoo.core.network.sdnetwork.classifications.is_stackfree(network: SemiDirectedPhyNetwork) 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 a semi-directed network, hybrid nodes have exactly one outgoing directed edge, so we check if that child is also a hybrid node.
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network to check.
- Returns:
True if the network is stack-free (no hybrid has a hybrid child), False otherwise.
- Return type:
Examples
>>> # Network with no hybrids (stack-free) >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})] ... ) >>> is_stackfree(net) True
>>> # Network with hybrid that has tree node child (stack-free) >>> net = SemiDirectedPhyNetwork( ... directed_edges=[ ... (5, 4), (6, 4) # Both lead to hybrid 4 ... ], ... undirected_edges=[ ... (7, 5), (7, 6), # Root to tree nodes ... (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 = SemiDirectedPhyNetwork( ... directed_edges=[ ... (5, 4), (6, 4), # Both lead to hybrid 4 ... (4, 7), (8, 7) # Hybrid 4 and tree node 8 lead to hybrid 7 ... ], ... undirected_edges=[ ... (9, 5), (9, 6), # Root to tree nodes ... (7, 1) # Hybrid 7 to leaf ... ], ... nodes=[(1, {'label': 'A'})] ... ) >>> is_stackfree(net) False
- phylozoo.core.network.sdnetwork.classifications.is_strongly_treebased(network: SemiDirectedPhyNetwork) bool[source]#
Check if the network is strongly tree-based.
A semi-directed network is strongly tree-based if every rooting (on an edge) yields a directed network that is tree-based.
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network.
- Returns:
True if every edge rooting is tree-based, False otherwise.
- Return type:
- Raises:
PhyloZooNotImplementedError – If the network is non-binary or has parallel edges.
Notes
Only rootings on edges are considered; node rootings are ignored. For empty or single-node networks, returns True (vacuously).
Examples
>>> from phylozoo.core.network import SemiDirectedPhyNetwork >>> from phylozoo.core.network.sdnetwork.classifications import ( ... is_strongly_treebased, ... ) >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})], ... ) >>> is_strongly_treebased(net) True
- phylozoo.core.network.sdnetwork.classifications.is_strongly_treechild(network: SemiDirectedPhyNetwork) bool[source]#
Check if the network is strongly tree-child.
A semi-directed network is strongly tree-child if every rooting (on an edge) yields a directed network that is tree-child.
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network.
- Returns:
True if every edge rooting is tree-child, False otherwise.
- Return type:
- Raises:
PhyloZooNotImplementedError – If the network is non-binary or has parallel edges.
Notes
Only rootings on edges are considered; node rootings are ignored. For empty or single-node networks, returns True (vacuously).
Examples
>>> from phylozoo.core.network import SemiDirectedPhyNetwork >>> from phylozoo.core.network.sdnetwork.classifications import ( ... is_strongly_treechild, ... ) >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})], ... ) >>> is_strongly_treechild(net) True
- phylozoo.core.network.sdnetwork.classifications.is_tree(network: MixedPhyNetwork) bool[source]#
Check if the network is a tree.
A network is a tree if it has no hybrid edges.
- Parameters:
network (MixedPhyNetwork) – The mixed phylogenetic network to check.
- Returns:
True if the network is a tree, False otherwise.
- Return type:
Examples
>>> net = MixedPhyNetwork(undirected_edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]) >>> is_tree(net) True
- phylozoo.core.network.sdnetwork.classifications.is_weakly_treebased(network: SemiDirectedPhyNetwork) bool[source]#
Check if the network is weakly tree-based.
A semi-directed network is weakly tree-based if there exists at least one rooting (on an edge) that yields a directed network that is tree-based.
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network.
- Returns:
True if some edge rooting is tree-based, False otherwise.
- Return type:
- Raises:
PhyloZooNotImplementedError – If the network is non-binary or has parallel edges.
Notes
Only rootings on edges are considered; node rootings are ignored. For empty or single-node networks, returns True (vacuously).
Examples
>>> from phylozoo.core.network import SemiDirectedPhyNetwork >>> from phylozoo.core.network.sdnetwork.classifications import ( ... is_weakly_treebased, ... ) >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})], ... ) >>> is_weakly_treebased(net) True
- phylozoo.core.network.sdnetwork.classifications.is_weakly_treechild(network: SemiDirectedPhyNetwork) bool[source]#
Check if the network is weakly tree-child.
A semi-directed network is weakly tree-child if there exists at least one rooting (on an edge) that yields a directed network that is tree-child.
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network.
- Returns:
True if some edge rooting is tree-child, False otherwise.
- Return type:
- Raises:
PhyloZooNotImplementedError – If the network is non-binary or has parallel edges.
Notes
Only rootings on edges are considered; node rootings are ignored. For empty or single-node networks, returns True (vacuously).
Examples
>>> from phylozoo.core.network import SemiDirectedPhyNetwork >>> from phylozoo.core.network.sdnetwork.classifications import ( ... is_weakly_treechild, ... ) >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})], ... ) >>> is_weakly_treechild(net) True
- phylozoo.core.network.sdnetwork.classifications.level(network: SemiDirectedPhyNetwork) int[source]#
Return the 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 (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network.
- Returns:
The level of the network.
- Return type:
Notes
For empty networks, this function returns 0.
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork(undirected_edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]) >>> level(net) 0
- phylozoo.core.network.sdnetwork.classifications.reticulation_number(network: SemiDirectedPhyNetwork) 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 (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network.
- Returns:
The reticulation number of the network.
- Return type:
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork(undirected_edges=[(3, 1), (3, 2)], nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})]) >>> reticulation_number(net) 0
- phylozoo.core.network.sdnetwork.classifications.vertex_level(network: SemiDirectedPhyNetwork) 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 (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network.
- Returns:
The vertex level of the network.
- Return type:
Notes
For empty networks, this function returns 0.
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork(undirected_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 semi-directed and mixed phylogenetic networks (e.g., suppress degree-2 nodes/blobs, identify parallel edges, etc.).
- phylozoo.core.network.sdnetwork.transformations.identify_parallel_edges(network: SemiDirectedPhyNetwork) SemiDirectedPhyNetwork[source]#
Identify all parallel edges and suppress all degree-2 nodes exhaustively.
This function iteratively: 1. Identifies all parallel edges 2. Suppresses all degree-2 nodes
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 (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network to transform.
- Returns:
A new network with all parallel edges identified and all degree-2 nodes suppressed.
- Return type:
- Raises:
PhyloZooAlgorithmError – If the algorithm exceeds the maximum number of iterations.
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.
Handles both directed and undirected parallel edges separately.
Examples
>>> net = SemiDirectedPhyNetwork( ... directed_edges=[ ... {'u': 1, 'v': 2, 'branch_length': 0.5}, ... {'u': 1, 'v': 2, 'branch_length': 0.5} # Parallel directed edges: node 2 is hybrid (in-degree 2) ... ], ... undirected_edges=[ ... {'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} # Root to another leaf ... ], ... nodes=[(4, {'label': 'A'}), (5, {'label': 'B'}), (6, {'label': 'C'})] ... ) >>> result = identify_parallel_edges(net) >>> # Step 1: Parallel directed edges 1->2 identified -> node 2 becomes degree-2 (in-degree 1, undirected out) >>> # Step 2: Degree-2 node 2 suppressed (directed_in + undirected -> undirected) -> undirected edge 1-3 with branch_length=0.8 (0.5+0.3) >>> # Result: undirected edges 1-3 (0.8), 3-4 (0.2), 3-5 (0.1), 1-6 (0.1)
- phylozoo.core.network.sdnetwork.transformations.suppress_2_blobs(network: MixedPhyNetwork) MixedPhyNetwork[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, 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 (MixedPhyNetwork) – The mixed phylogenetic network to transform.
- Returns:
A new network with all 2-blobs suppressed. The network is validated before being returned.
- Return type:
Notes
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
For mixed networks, incident edges can be directed or undirected, and the suppression handles both types correctly
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork( ... undirected_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
Derivations#
Network derivations module.
This module provides functions to derive other data structures from semi-directed and mixed phylogenetic networks (e.g., splits, quartets, distances, blobtrees, subnetworks, etc.).
- phylozoo.core.network.sdnetwork.derivations.displayed_quartets(network: SemiDirectedPhyNetwork) QuartetProfileSet[source]#
Compute quartet profile set from all displayed trees of the network.
For each quartet (4-leaf subnetwork), this function: 1. Extracts the subnetwork induced by those 4 taxa 2. Gets all displayed trees of that subnetwork (with probabilities) 3. Converts each displayed tree to a quartet (4-leaf tree) 4. Creates a quartet profile where each quartet’s weight is the probability of the displayed tree that induced it (summing weights if the same quartet appears in multiple displayed trees)
The profiles are then returned as a QuartetProfileSet, where each profile (one per 4-taxon set) has quartet weights that sum to 1.0 (the displayed-tree probabilities). Each profile in the set has default profile weight 1.0.
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-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:
Examples
>>> net = SemiDirectedPhyNetwork( ... directed_edges=[(5, 4), (6, 4)], ... undirected_edges=[ ... (5, 3), (5, 6), (6, 7), # Tree edges ... (4, 8), (8, 1), (8, 2) # Tree edges from hybrid ... ], ... 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.sdnetwork.derivations.displayed_splits(network: SemiDirectedPhyNetwork) 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 (SemiDirectedPhyNetwork) – The semi-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:
Examples
>>> net = SemiDirectedPhyNetwork( ... directed_edges=[(5, 4), (6, 4)], # Hybrid edges to hybrid node 4 ... undirected_edges=[ ... (5, 3), (5, 6), (6, 7), # Tree edges ... (4, 8), (8, 1), (8, 2) # Tree edges from hybrid ... ], ... 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.sdnetwork.derivations.displayed_trees(network: SemiDirectedPhyNetwork, probability: bool = False) Iterator[SemiDirectedPhyNetwork][source]#
Generate all displayed trees of a semi-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 3. Suppressing all degree-2 nodes
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-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:
SemiDirectedPhyNetwork – A displayed tree of the network. If probability=True, the network has a ‘probability’ attribute containing the tree probability.
Examples
>>> net = SemiDirectedPhyNetwork( ... directed_edges=[(5, 4), (6, 4)], ... undirected_edges=[(5, 3), (5, 6), (6, 7), (4, 8), (8, 1), (8, 2)], ... 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.sdnetwork.derivations.distances(network: SemiDirectedPhyNetwork, 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 (SemiDirectedPhyNetwork) – The semi-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:
- Raises:
PhyloZooValueError – If the mode is invalid.
Examples
>>> net = SemiDirectedPhyNetwork( ... directed_edges=[(5, 4), (6, 4)], ... undirected_edges=[(5, 3), (5, 6), (6, 7), (4, 8), (8, 1), (8, 2)], ... 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.sdnetwork.derivations.induced_splits(network: MixedPhyNetwork) SplitSystem[source]#
Extract all splits induced by cut-edges of the network.
This function: 1. Suppresses all 2-blobs (which don’t influence splits) 2. Finds all cut-edges 3. 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 (MixedPhyNetwork) – The mixed phylogenetic network.
- Returns:
A split system containing all splits induced by cut-edges.
- Return type:
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})] ... ) >>> splits = induced_splits(net) >>> len(splits) >= 1 True
Notes
The tree-of-blobs is computed, which 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.sdnetwork.derivations.k_taxon_subnetworks(network: SemiDirectedPhyNetwork, k: int, suppress_2_blobs: bool = False, identify_parallel_edges: bool = False) Iterator[SemiDirectedPhyNetwork][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 (SemiDirectedPhyNetwork) – 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.
- Yields:
SemiDirectedPhyNetwork – 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 = SemiDirectedPhyNetwork( ... undirected_edges=[(5, 3), (5, 4), (5, 6), (3, 1), (3, 2)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'}), (6, {'label': 'D'})] ... ) >>> # Generate all 2-taxon subnetworks >>> subnetworks = list(k_taxon_subnetworks(net, k=2)) >>> len(subnetworks) 6 # C(4,2) = 6 combinations >>> # Each subnetwork has exactly 2 leaves >>> all(len(subnet.taxa) == 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) 4 # C(4,1) = 4 combinations
- phylozoo.core.network.sdnetwork.derivations.partition_from_blob(network: MixedPhyNetwork, 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 (MixedPhyNetwork) – The mixed 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). If removing the blob does not disconnect the network (blob is not a cut-blob).
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork( ... undirected_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.sdnetwork.derivations.root_at_outgroup(network: SemiDirectedPhyNetwork, outgroup: str) DirectedPhyNetwork[source]#
Root a semi-directed network at the edge leading to the specified outgroup taxon.
This is a convenience function that finds the edge incident to the leaf node with the given taxon label, then roots the network at that edge using to_d_network.
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network to root.
outgroup (str) – The taxon label of the outgroup leaf.
- Returns:
A directed phylogenetic network rooted at the edge leading to the outgroup.
- Return type:
- Raises:
PhyloZooValueError – If the outgroup taxon is not found in the network. If no edge is found incident to the outgroup leaf. If the edge is not a valid root location.
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> from phylozoo.core.network.sdnetwork.derivations import root_at_outgroup >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})] ... ) >>> d_net = root_at_outgroup(net, 'A') >>> d_net.root_node is not None True
- phylozoo.core.network.sdnetwork.derivations.split_from_cutedge(network: SemiDirectedPhyNetwork, 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 (SemiDirectedPhyNetwork) – The semi-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 ValueError. 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.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork( ... undirected_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.sdnetwork.derivations.subnetwork(network: SemiDirectedPhyNetwork, taxa: list[str], suppress_2_blobs: bool = False, identify_parallel_edges: bool = False) SemiDirectedPhyNetwork[source]#
Extract the subnetwork induced by a subset of taxa (leaf labels).
The subnetwork is defined as the union of all up-down paths between the requested leaves (i.e., all vertices on up-down paths between any pair of the requested leaves). The induced subgraph is taken on the underlying MixedMultiGraph, then degree-2 internal nodes are suppressed. Optionally, the result can be post-processed by suppressing 2-blobs and/or identifying parallel edges. After any of these optional steps, degree-2 suppression is applied again to clean up artifacts.
An up-down path between two vertices x and y is a path where no two edges are oriented towards each other. Equivalently, it is a path where the first k edges can be oriented towards x (where undirected edges can be oriented in either way) and the remaining l-k edges can be oriented towards y.
- Parameters:
network (SemiDirectedPhyNetwork) – 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.
- Returns:
The derived subnetwork. Returns an empty network if taxa is empty. Returns a network with a single leaf if taxa contains a single leaf.
- Return type:
- Raises:
PhyloZooValueError – If any of the provided taxa are not found in the network.
Examples
>>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})] ... ) >>> subnet = subnetwork(net, ['A', 'B']) >>> sorted(subnet.taxa) ['A', 'B'] >>> # Network with hybrid >>> net2 = SemiDirectedPhyNetwork( ... directed_edges=[(5, 4), (6, 4)], ... undirected_edges=[(5, 3), (5, 6), (6, 7), (4, 8), (8, 1), (8, 2)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (3, {'label': 'C'}), (7, {'label': 'D'})] ... ) >>> subnet2 = subnetwork(net2, ['A', 'B']) >>> sorted(subnet2.taxa) ['A', 'B']
- phylozoo.core.network.sdnetwork.derivations.to_d_network(network: SemiDirectedPhyNetwork, root_location: T | tuple[T, T, int] | None = None) DirectedPhyNetwork[source]#
Convert a semi-directed network to a directed network by rooting it.
The network is rooted at the specified location. If no location is provided, a default location is chosen from the valid root locations.
- Parameters:
network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network to convert.
root_location (RootLocation, optional) – The root location. Can be: - A node (T): non-leaf node in the source component - An edge (tuple[T, T, int]): edge in the source component as (u, v, key) If None, a default location is chosen from valid root locations. By default None.
- Returns:
A directed phylogenetic network rooted at the specified location.
- Return type:
- Raises:
PhyloZooValueError – If the root location is invalid or not in the network’s valid root locations.
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2), (3, 4)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'}), (4, {'label': 'C'})] ... ) >>> d_net = to_d_network(net, root_location=3) >>> d_net.root_node 3
- phylozoo.core.network.sdnetwork.derivations.tree_of_blobs(network: MixedPhyNetwork) MixedPhyNetwork[source]#
Create the 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 (MixedPhyNetwork) – The mixed phylogenetic network to transform.
- Returns:
A new network where each blob has been collapsed to a single vertex, forming a tree structure. Returns a SemiDirectedPhyNetwork if the input is a SemiDirectedPhyNetwork, otherwise returns a MixedPhyNetwork.
- Return type:
Examples
>>> # Create a semi-directed network with a hybrid >>> from phylozoo.core.network.sdnetwork.classifications import is_tree >>> sdnet = SemiDirectedPhyNetwork( ... directed_edges=[ ... (5, 4), ... (6, 4) ... ], ... undirected_edges=[ ... (5, 3), ... (5, 6), ... (6, 7), ... (4, 8), ... (8, 1), ... (8, 2) ... ], ... nodes=[ ... (3, {'label': 'C'}), ... (7, {'label': 'D'}), ... (1, {'label': 'A'}), ... (2, {'label': 'B'}) ... ] ... ) >>> tree_net = tree_of_blobs(sdnet) >>> is_tree(tree_net) True
Conversions#
Network conversion module.
This module provides functions for converting between different graph representations and semi-directed phylogenetic networks.
- phylozoo.core.network.sdnetwork.conversions.sdnetwork_from_graph(graph: nx.Graph | nx.MultiGraph | MixedMultiGraph[T], network_type: Literal['semi-directed', 'mixed'] = 'semi-directed') SemiDirectedPhyNetwork[T] | MixedPhyNetwork[T][source]#
Create a SemiDirectedPhyNetwork or MixedPhyNetwork from a NetworkX Graph, MultiGraph, or phylozoo MixedMultiGraph.
For NetworkX graphs, all edges are treated as undirected edges. Edge attributes, node attributes, and graph-level attributes are preserved and passed through to the resulting network.
- Parameters:
graph (nx.Graph | nx.MultiGraph | MixedMultiGraph[T]) – The graph to convert. Can be a NetworkX Graph, MultiGraph, or a MixedMultiGraph from the primitives module.
network_type (Literal['semi-directed', 'mixed'], default='semi-directed') – Type of network to create. ‘semi-directed’ creates a SemiDirectedPhyNetwork, ‘mixed’ creates a MixedPhyNetwork.
- Returns:
A new phylogenetic network with edges and labels from the graph.
- Return type:
- Raises:
PhyloZooValueError – If the resulting network is invalid according to SemiDirectedPhyNetwork or MixedPhyNetwork validation rules (e.g., invalid node degrees, undirected cycles in semi-directed networks, etc.).
Notes
Edge attributes: All edge attributes (e.g., branch_length, bootstrap, gamma for hybrid edges) 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 network requirements (e.g., leaves must have no outgoing edges, internal nodes must have appropriate degrees, etc.), a ValueError is raised.
Examples
>>> import networkx as nx >>> G = nx.Graph() >>> 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 = sdnetwork_from_graph(G) >>> isinstance(net, SemiDirectedPhyNetwork) True >>> net.get_label(1) 'A' >>> net.get_branch_length(0, 1) 0.5 >>> net.get_network_attribute('source') 'test'
Isomorphism#
Isomorphism module for semi-directed and mixed phylogenetic networks.
This module provides functions for checking network isomorphism between SemiDirectedPhyNetwork and MixedPhyNetwork instances.
- phylozoo.core.network.sdnetwork.isomorphism.is_isomorphic(net1: MixedPhyNetwork[T], net2: MixedPhyNetwork[T], node_attrs: list[str] | None = None, edge_attrs: list[str] | None = None, graph_attrs: list[str] | None = None) bool[source]#
Check if two semi-directed or mixed 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 (MixedPhyNetwork) – First phylogenetic network (SemiDirectedPhyNetwork or MixedPhyNetwork).
net2 (MixedPhyNetwork) – Second phylogenetic network (SemiDirectedPhyNetwork or MixedPhyNetwork).
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:
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> net1 = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})] ... ) >>> net2 = SemiDirectedPhyNetwork( ... undirected_edges=[(4, 5), (4, 6)], ... nodes=[(5, {'label': 'A'}), (6, {'label': 'B'})] ... ) >>> is_isomorphic(net1, net2) True >>> # Different labels: not isomorphic >>> net3 = SemiDirectedPhyNetwork( ... undirected_edges=[(4, 5), (4, 6)], ... nodes=[(5, {'label': 'A'}), (6, {'label': 'C'})] ... ) >>> is_isomorphic(net1, net3) False >>> # With additional node attributes >>> net4 = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2)], ... nodes=[(1, {'label': 'A', 'type': 'leaf'}), (2, {'label': 'B', 'type': 'leaf'})] ... ) >>> net5 = SemiDirectedPhyNetwork( ... undirected_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 = SemiDirectedPhyNetwork( ... undirected_edges=[{'u': 3, 'v': 1, 'branch_length': 0.5}], ... nodes=[(1, {'label': 'A'})] ... ) >>> net7 = SemiDirectedPhyNetwork( ... undirected_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 MixedMultiGraph isomorphism checking, which converts undirected edges to bidirectional directed edges for matching.
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 SemiDirectedPhyNetwork with FormatRegistry for use with the IOMixin system.
All format handlers are defined here: - eNewick format: Uses to_d_network and to_sd_network for conversion - PhyloZoo-DOT format: Uses MixedMultiGraph’s phylozoo-dot I/O functions
- phylozoo.core.network.sdnetwork.io.from_enewick(enewick_string: str, **kwargs: Any) SemiDirectedPhyNetwork[source]#
Parse an eNewick format string and create a SemiDirectedPhyNetwork.
This function parses the eNewick string into a DirectedPhyNetwork, then converts it to a SemiDirectedPhyNetwork using to_sd_network.
- Parameters:
enewick_string (str) – eNewick format string containing network data.
**kwargs – Additional arguments to pass to from_enewick (currently unused, for compatibility).
- Returns:
Parsed semi-directed phylogenetic network.
- Return type:
- Raises:
ENewickParseError – If the eNewick string is malformed or cannot be parsed, or if the resulting network structure is invalid for SemiDirectedPhyNetwork.
Examples
>>> from phylozoo.core.network.sdnetwork.io import from_enewick >>> >>> enewick_str = "((A,B),C);" >>> net = from_enewick(enewick_str) >>> net.number_of_nodes() 4 >>> 'A' in net.taxa True
Notes
The conversion process: 1. Parse eNewick string to DirectedPhyNetwork using from_enewick 2. Convert DirectedPhyNetwork to SemiDirectedPhyNetwork using to_sd_network
- phylozoo.core.network.sdnetwork.io.from_phylozoo_dot(pzdot_string: str, **kwargs: Any) SemiDirectedPhyNetwork[source]#
Parse a PhyloZoo-DOT format string and create a SemiDirectedPhyNetwork.
This function parses the PhyloZoo-DOT string into a MixedMultiGraph, then converts it to a SemiDirectedPhyNetwork.
- Parameters:
pzdot_string (str) – PhyloZoo-DOT format string containing graph data.
**kwargs – Additional arguments (currently unused, for compatibility).
- Returns:
Parsed semi-directed phylogenetic network.
- Return type:
- Raises:
ValueError – If the PhyloZoo-DOT string is malformed or cannot be parsed, or if the resulting network structure is invalid for SemiDirectedPhyNetwork.
Examples
>>> from phylozoo.core.network.sdnetwork.io import from_phylozoo_dot >>> >>> pzdot_str = '''graph { ... 1 [label="A"]; ... 2 [label="B"]; ... 3; ... 1 -- 3; ... 2 -- 3; ... }''' >>> >>> net = from_phylozoo_dot(pzdot_str) >>> net.number_of_nodes() 3
Notes
The conversion process: 1. Parse PhyloZoo-DOT string to MixedMultiGraph using from_phylozoo_dot 2. Convert MixedMultiGraph to SemiDirectedPhyNetwork using sdnetwork_from_graph
- phylozoo.core.network.sdnetwork.io.to_enewick(sd_network: SemiDirectedPhyNetwork, **kwargs: Any) str[source]#
Convert a SemiDirectedPhyNetwork to an eNewick format string.
This function converts the semi-directed network to a directed network using to_d_network, then converts the directed network to eNewick format.
- Parameters:
sd_network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network to convert.
**kwargs – Additional arguments to pass to to_d_network and to_enewick. Use root_location to specify RootLocation when converting to directed network (if None, a default location is chosen). Other arguments are passed to to_enewick.
- Returns:
The eNewick format string representation of the network.
- Return type:
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> from phylozoo.core.network.sdnetwork.io import to_enewick >>> >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})] ... ) >>> enewick_str = to_enewick(net) >>> ';' in enewick_str True >>> 'A' in enewick_str True >>> 'B' in enewick_str True
Notes
The conversion process: 1. Convert SemiDirectedPhyNetwork to DirectedPhyNetwork using to_d_network 2. Convert DirectedPhyNetwork to eNewick string using to_enewick
If no root_location is provided, a default location is chosen automatically.
- phylozoo.core.network.sdnetwork.io.to_phylozoo_dot(sd_network: SemiDirectedPhyNetwork, **kwargs: Any) str[source]#
Convert a SemiDirectedPhyNetwork to a PhyloZoo-DOT format string.
This function delegates to MixedMultiGraph’s to_phylozoo_dot function, using the underlying _graph attribute.
- Parameters:
sd_network (SemiDirectedPhyNetwork) – The semi-directed phylogenetic network to convert.
**kwargs – Additional arguments to pass to the underlying to_phylozoo_dot: - graph_name (str): Optional name for the graph (default: ‘graph’).
- Returns:
The PhyloZoo-DOT format string representation of the network.
- Return type:
Examples
>>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> from phylozoo.core.network.sdnetwork.io import to_phylozoo_dot >>> >>> net = SemiDirectedPhyNetwork( ... undirected_edges=[(3, 1), (3, 2)], ... nodes=[(1, {'label': 'A'}), (2, {'label': 'B'})] ... ) >>> dot_str = to_phylozoo_dot(net) >>> 'graph' in dot_str True >>> '--' in dot_str True
Notes
The PhyloZoo-DOT format uses: - – for undirected edges - -> for directed edges - Supports node, edge, and graph attributes - Supports parallel edges