Mixed Multi-Graphs#
The phylozoo.core.primitives.m_multigraph module provides the
MixedMultiGraph class, a mixed
multigraph that supports both directed and undirected edges with parallel edges of both
types. It serves as the foundation for SemiDirectedPhyNetwork
and enables representation of phylogenetic networks with both tree-like and reticulate
evolutionary relationships.
All classes and functions on this page can be imported from the mixed multigraph submodule:
from phylozoo.core.primitives.m_multigraph import MixedMultiGraph
Working with Mixed Multi-Graphs#
Creating and modifying mixed multi-graphs#
Mixed multi-graphs support both directed and undirected edges in one structure. You specify directed and undirected edge lists separately; between any given pair of nodes, edges must be either all directed or all undirected (mutual exclusivity). Parallel edges are allowed for both types.
Creating
Use directed_edges and undirected_edges as lists of (u, v) tuples or dicts
with u and v. You can pass only one of them for a graph that is fully directed
or fully undirected.
from phylozoo.core.primitives.m_multigraph import MixedMultiGraph
empty_graph = MixedMultiGraph()
tree_graph = MixedMultiGraph(undirected_edges=[(1, 2), (2, 3), (3, 4)])
network_graph = MixedMultiGraph(directed_edges=[(1, 2), (1, 3), (2, 4), (3, 4)])
mixed_graph = MixedMultiGraph(
directed_edges=[(1, 3), (2, 3)],
undirected_edges=[(3, 4), (4, 5), (4, 6)]
)
Optional nodes and attributes let you attach node or graph-level metadata:
attributed_graph = MixedMultiGraph(
directed_edges=[{'u': 1, 'v': 2, 'weight': 1.0, 'type': 'hybrid'}],
undirected_edges=[{'u': 2, 'v': 3, 'length': 0.5, 'bootstrap': 95}]
)
labeled_graph = MixedMultiGraph(
directed_edges=[(1, 2)],
undirected_edges=[(2, 3)],
nodes=[{'id': 1, 'label': 'A'}, {'id': 2, 'label': 'B'}, {'id': 3, 'label': 'C'}]
)
Tip
For graphs without parallel edges, you can build or manipulate them in NetworkX
(e.g. networkx.Graph or networkx.DiGraph) and then convert to
MixedMultiGraph. See the
NetworkX conversion section below.
Directed edges
Add or remove directed edges with
add_directed_edge() and
remove_directed_edge();
each edge has a unique key.
key1 = mixed_graph.add_directed_edge(1, 2, weight=1.0, type='reticulation')
key2 = mixed_graph.add_directed_edge(1, 2, weight=2.0)
mixed_graph.remove_directed_edge(1, 2, key=key1)
Batch operations for directed edges:
add_directed_edges_from() and
remove_directed_edges_from().
mixed_graph.add_directed_edges_from([(1, 4), (2, 4)])
mixed_graph.remove_directed_edges_from([(1, 4)])
Undirected edges
Undirected edges are added with
add_undirected_edge() and
removed with remove_edge()
(which applies to undirected edges between the given pair). Parallel undirected edges are supported.
key3 = mixed_graph.add_undirected_edge(2, 3, length=0.5, bootstrap=95)
key4 = mixed_graph.add_undirected_edge(2, 3, length=0.7, bootstrap=87)
mixed_graph.remove_edge(2, 3, key=key3)
Batch operations for undirected edges:
add_undirected_edges_from() and
remove_edges_from().
mixed_graph.add_undirected_edges_from([(4, 5), (5, 6)])
mixed_graph.remove_edges_from([(5, 6)])
Mutual exclusivity
Between the same pair of nodes, all edges must be either directed or undirected. Adding a directed edge between a pair that currently has undirected edges (or the reverse) replaces those edges with the new type.
graph = MixedMultiGraph()
graph.add_undirected_edge(1, 2, weight=1.0)
graph.add_directed_edge(1, 2, weight=2.0) # Replaces the undirected edge
assert graph.has_directed_edge(1, 2) and not graph.has_undirected_edge(1, 2)
Node operations
Nodes are added with optional attributes; the nodes
view gives attribute access by node.
mixed_graph.add_node(5, label='taxon_A', type='leaf', support=100)
node_attrs = mixed_graph.nodes[5]
in_degree = mixed_graph.indegree(3)
out_degree = mixed_graph.outdegree(1)
undirected_degree = mixed_graph.undirected_degree(3)
total_degree = mixed_graph.degree(3)
You can add or remove multiple nodes with
add_nodes_from(),
remove_node(), and
remove_nodes_from(), and
generate_node_ids()
returns an iterator of fresh integer IDs.
mixed_graph.add_nodes_from([7, 8, 9], type='internal')
mixed_graph.remove_node(9)
Accessing graph structure
Use number_of_nodes() and
number_of_edges() for
counts. The directed_edges
and undirected_edges
views yield (u, v, key) tuples; edges
returns all edges. neighbors()
returns all nodes adjacent to a vertex (via any edge type).
num_nodes = mixed_graph.number_of_nodes()
num_edges = mixed_graph.number_of_edges()
directed_edges = list(mixed_graph.directed_edges())
undirected_edges = list(mixed_graph.undirected_edges())
all_edges = list(mixed_graph.edges())
neighbors = list(mixed_graph.neighbors(3))
Use has_edge() for
edge membership (optionally with a key). For edges incident to a vertex, use
incident_parent_edges(),
incident_child_edges(), and
incident_undirected_edges().
copy() returns a shallow
copy; clear() removes all
nodes and edges.
mixed_graph.has_edge(1, 3, key=0)
incoming = list(mixed_graph.incident_parent_edges(3, keys=True))
outgoing = list(mixed_graph.incident_child_edges(1, keys=True))
undir_at_3 = list(mixed_graph.incident_undirected_edges(3))
graph_copy = mixed_graph.copy()
graph_copy.clear()
File Input/Output#
Mixed multi-graphs support reading and writing in PhyloZoo DOT format (default), which preserves the directed/undirected edge distinction:
PhyloZoo DOT (default): Extended DOT format for mixed graphs — see DOT format
# Load from file (auto-detects format by extension)
mixed_graph = MixedMultiGraph.load("phylogenetic_network.pzdot")
# Load with explicit format
mixed_graph = MixedMultiGraph.load("network.txt", format="phylozoo-dot")
# Save to file
mixed_graph.save("output.pzdot")
See also
The MixedMultiGraph class uses the
IOMixin interface, providing consistent file handling across PhyloZoo
classes. For details on the I/O system, see the I/O manual.
Graph Analysis Features#
Mixed multi-graphs provide specialized algorithms for phylogenetic network analysis.
Connectivity
The is_connected() function checks weak connectivity (ignoring edge directions).
The number_of_connected_components() function counts the number of connected components.
The connected_components() function returns an iterator over all connected components.
from phylozoo.core.primitives.m_multigraph.features import (
is_connected, number_of_connected_components, connected_components
)
# Check weak connectivity (ignoring edge directions)
connected = is_connected(mixed_graph)
# Count connected components
num_components = number_of_connected_components(mixed_graph)
# Get all connected components
for component in connected_components(mixed_graph):
print(f"Component: {component}")
Source components
The source_components() function finds source components (nodes with no incoming directed edges), which is important for phylogenetic network analysis.
from phylozoo.core.primitives.m_multigraph.features import source_components
# Find source components (no incoming directed edges)
sources = source_components(mixed_graph) # List of (nodes, directed_edges, undirected_edges) tuples
Biconnectivity
The biconnected_components() function returns an iterator over biconnected components.
The bi_edge_connected_components() function returns an iterator over bi-edge-connected components.
from phylozoo.core.primitives.m_multigraph.features import (
biconnected_components, bi_edge_connected_components
)
# Get biconnected components
for component in biconnected_components(mixed_graph):
print(f"Biconnected component: {component}")
# Get bi-edge-connected components
for component in bi_edge_connected_components(mixed_graph):
print(f"Bi-edge-connected component: {component}")
Cut edges and vertices
The cut_edges() function finds all cut edges.
The cut_vertices() function finds all cut vertices.
from phylozoo.core.primitives.m_multigraph.features import cut_edges, cut_vertices
# Find cut edges
cuts = cut_edges(mixed_graph) # Returns list of (u, v, key) tuples
# Find cut vertices
cut_nodes = cut_vertices(mixed_graph) # Returns set of nodes
Up-down paths
The updown_path_vertices() function finds all vertices that lie on an up-down path between two given vertices (a path that first goes up via directed edges, then down via directed edges).
from phylozoo.core.primitives.m_multigraph.features import updown_path_vertices
# Find vertices on up-down paths between x and y
vertices = updown_path_vertices(mixed_graph, x=1, y=5)
Parallel edges and self-loops
The has_parallel_edges() function checks if the graph contains any parallel edges.
The has_self_loops() function checks for edges connecting a node to itself.
from phylozoo.core.primitives.m_multigraph.features import (
has_parallel_edges, has_self_loops
)
# Check for parallel edges
has_parallel = has_parallel_edges(mixed_graph)
# Check for self-loops
has_loops = has_self_loops(mixed_graph)
Isomorphism
The is_isomorphic() function compares graph structures while respecting the mixed edge types, checking if two graphs have the same topology regardless of node labeling.
from phylozoo.core.primitives.m_multigraph.isomorphism import is_isomorphic
graph1 = MixedMultiGraph(
directed_edges=[(1, 3)],
undirected_edges=[(3, 4)]
)
graph2 = MixedMultiGraph(
directed_edges=[(2, 4)],
undirected_edges=[(4, 5)]
)
# Basic isomorphism check
isomorphic = is_isomorphic(graph1, graph2)
# With node attributes
graph1.add_node(1, label='root')
graph2.add_node(2, label='root')
isomorphic_with_attrs = is_isomorphic(graph1, graph2, node_attrs=['label'])
Graph Transformations#
The mixed multi-graph module provides functions for transforming graph structures.
Vertex identification
The identify_vertices() function merges multiple vertices into a single vertex, combining their incident edges.
from phylozoo.core.primitives.m_multigraph.transformations import identify_vertices
# Merge vertices 1, 2, and 3 into a single vertex
identify_vertices(mixed_graph, [1, 2, 3])
Orientation
The orient_away_from_vertex() function orients all undirected edges away from a given root vertex, converting the mixed graph to a directed graph.
from phylozoo.core.primitives.m_multigraph.transformations import orient_away_from_vertex
# Orient all edges away from root vertex
directed_graph = orient_away_from_vertex(mixed_graph, root=1)
Degree-2 node suppression
The suppress_degree2_node() function removes a degree-2 node and connects its neighbors directly.
from phylozoo.core.primitives.m_multigraph.transformations import suppress_degree2_node
# Suppress a degree-2 node
suppress_degree2_node(mixed_graph, node=5)
Parallel edge identification
The identify_parallel_edge() function merges all parallel edges between two nodes into a single edge.
from phylozoo.core.primitives.m_multigraph.transformations import identify_parallel_edge
# Merge all parallel edges between nodes 1 and 2
identify_parallel_edge(mixed_graph, u=1, v=2)
Subgraph extraction
The subgraph() function creates a subgraph containing only the specified nodes and their incident edges.
from phylozoo.core.primitives.m_multigraph.transformations import subgraph
# Extract subgraph with specific nodes
sub = subgraph(mixed_graph, nodes=[1, 2, 3, 4])
NetworkX Conversion#
Convert from various NetworkX graph types. The graph_to_mixedmultigraph() function converts a NetworkX Graph to a MixedMultiGraph, the multigraph_to_mixedmultigraph() function converts a NetworkX MultiGraph to a MixedMultiGraph, and the multidigraph_to_mixedmultigraph() function converts a NetworkX MultiDiGraph to a MixedMultiGraph.
Also, the directedmultigraph_to_mixedmultigraph() function converts a DirectedMultiGraph to a MixedMultiGraph.
import networkx as nx
from phylozoo.core.primitives.m_multigraph.conversions import (
graph_to_mixedmultigraph,
multigraph_to_mixedmultigraph,
multidigraph_to_mixedmultigraph
)
# Convert undirected NetworkX Graph
nx_graph = nx.Graph()
nx_graph.add_edge(1, 2, weight=1.0)
mmg_from_graph = graph_to_mixedmultigraph(nx_graph)
# Convert undirected NetworkX MultiGraph
nx_multigraph = nx.MultiGraph()
nx_multigraph.add_edge(1, 2, key=0, weight=1.0)
mmg_from_multigraph = multigraph_to_mixedmultigraph(nx_multigraph)
# Convert directed NetworkX MultiDiGraph
nx_multidigraph = nx.MultiDiGraph()
nx_multidigraph.add_edge(1, 2, key=0, weight=1.0)
mmg_from_multidigraph = multidigraph_to_mixedmultigraph(nx_multidigraph)
# Convert from DirectedMultiGraph
from phylozoo.core.primitives.d_multigraph import DirectedMultiGraph
from phylozoo.core.primitives.m_multigraph.conversions import directedmultigraph_to_mixedmultigraph
dmg = DirectedMultiGraph(edges=[(1, 2), (2, 3)])
mmg_from_dmg = directedmultigraph_to_mixedmultigraph(dmg)
Tip
The class stores three NetworkX graphs that can be accessed for further use with
NetworkX algorithms. The undirected edges are stored as a networkx.MultiGraph
in the _undirected attribute, the directed edges as a networkx.MultiDiGraph
in the _directed attribute, and the _combined attribute stores a combined
undirected view of the graph as a networkx.MultiGraph for quick connectivity
analyses.
See Also#
API Reference - Complete function signatures and detailed examples
Directed Multi-Graph - Directed-only graphs
Networks - Network classes using mixed multi-graphs