Source code for phylozoo.core.primitives.d_multigraph.io

"""
Directed multi-graph I/O module.

This module provides format handlers for reading and writing directed multi-graphs
to/from files. Format handlers are registered with FormatRegistry for use with
the IOMixin system.

The following format handlers are defined and registered:

- **dot**: DOT format (Graphviz) (extensions: .dot, .gv)
  - Writer: `to_dot()` - Converts DirectedMultiGraph to a Graphviz ``digraph``
  - Reader: `from_dot()` - Parses a DOT string to DirectedMultiGraph
- **edgelist**: Edge-list format (extensions: .el)
  - Writer: `to_edgelist()` - Converts DirectedMultiGraph to edge-list string
  - Reader: `from_edgelist()` - Parses edge-list string to DirectedMultiGraph

These handlers are automatically registered when this module is imported.
DirectedMultiGraph inherits from IOMixin, so you can use:

- `graph.save('file.dot')` - Save to file (auto-detects format)
- `graph.load('file.dot')` - Load from file (auto-detects format)
- `graph.to_string(format='dot')` - Convert to string
- `graph.from_string(string, format='edgelist')` - Parse from string
- `DirectedMultiGraph.convert('in.dot', 'out.el')` - Convert between formats

Notes
-----
The DOT scaffolding (escaping, attribute formatting/parsing, node-id coercion and
document parsing) is shared with the mixed-multigraph formats and lives in
:mod:`phylozoo.utils.io.format_utils.dot`.

DOT format supports:
- Node attributes (label, shape, color, etc.)
- Edge attributes (label, weight, color, etc.)
- Graph attributes
- Parallel edges (multigraph support), encoded with an explicit ``key`` attribute

Edge-list format:
- Simple text format: one edge per line
- Format: `u v` or `u v key` or `u v key attr1=value1 attr2=value2`
- Uses node_id as the label/name
"""

from __future__ import annotations

from typing import Any

from phylozoo.utils.exceptions import PhyloZooParseError
from phylozoo.utils.io import FormatRegistry
from phylozoo.utils.io.format_utils.dot import (
    convert_node_id,
    dot_edge_line,
    dot_node_line,
    escape_dot_string,
    parse_dot_document,
)

from .base import DirectedMultiGraph


[docs] def to_dot(graph: DirectedMultiGraph, **kwargs: Any) -> str: """ Convert a DirectedMultiGraph to a DOT format string. Parameters ---------- graph : DirectedMultiGraph The directed multi-graph to convert. **kwargs Additional arguments (``graph_name`` is honoured). Returns ------- str The DOT format string representation of the graph. Examples -------- >>> from phylozoo.core.primitives.d_multigraph import DirectedMultiGraph >>> from phylozoo.core.primitives.d_multigraph.io import to_dot >>> >>> G = DirectedMultiGraph() >>> G.add_edge(1, 2, weight=1.0) 0 >>> G.add_edge(2, 3, weight=2.0) 0 >>> dot_str = to_dot(G) >>> 'digraph' in dot_str True >>> '1 -> 2' in dot_str True Notes ----- The DOT format includes a ``digraph`` declaration, node declarations with attributes, ``->`` edge declarations, graph attributes (if any) and parallel edges (encoded with a ``key`` attribute). """ lines = [] graph_name = kwargs.get("graph_name", "") lines.append(f"digraph {escape_dot_string(graph_name)} {{" if graph_name else "digraph {") # Graph attributes if hasattr(graph, "_graph") and hasattr(graph._graph, "graph"): for key, value in graph._graph.graph.items(): value_str = escape_dot_string(value) if isinstance(value, str) else str(value) lines.append(f" {key}={value_str};") # Node declarations for node in graph.nodes(): node_attrs = dict(graph._graph.nodes[node]) if node in graph._graph else {} lines.append(dot_node_line(node, node_attrs)) # Edge declarations for u, v, key, data in graph.edges_iter(keys=True, data=True): edge_attrs = dict(data) if data else {} if graph._graph.number_of_edges(u, v) > 1: edge_attrs["key"] = key lines.append(dot_edge_line(u, v, edge_attrs, arrow="->")) lines.append("}") return "\n".join(lines) + "\n"
[docs] def from_dot(dot_string: str, **kwargs: Any) -> DirectedMultiGraph: """ Parse a DOT format string and create a DirectedMultiGraph. Parameters ---------- dot_string : str DOT format string containing graph data. **kwargs Additional arguments (currently unused, for compatibility). Returns ------- DirectedMultiGraph Parsed directed multi-graph (every edge is treated as directed). Raises ------ PhyloZooParseError If the DOT string is malformed or cannot be parsed. Examples -------- >>> from phylozoo.core.primitives.d_multigraph.io import from_dot >>> >>> dot_str = '''digraph { ... 1 [label="Node1"]; ... 2 [label="Node2"]; ... 1 -> 2 [weight=1.0]; ... 2 -> 3 [weight=2.0]; ... }''' >>> >>> G = from_dot(dot_str) >>> G.number_of_nodes() 3 >>> G.number_of_edges() 2 """ graph_attrs, nodes_data, edges_data = parse_dot_document(dot_string) graph: Any = DirectedMultiGraph(attributes=graph_attrs if graph_attrs else None) for node_id, attrs in nodes_data.items(): graph.add_node(node_id, **attrs) for u, v, key, attrs, _directed in edges_data: if u not in graph: graph.add_node(u) if v not in graph: graph.add_node(v) graph.add_edge(u, v, key=key, **attrs) return graph # type: ignore[no-any-return]
[docs] def to_edgelist(graph: DirectedMultiGraph, **kwargs: Any) -> str: """ Convert a DirectedMultiGraph to an edge-list format string. Parameters ---------- graph : DirectedMultiGraph The directed multi-graph to convert. **kwargs Additional arguments (currently unused, for compatibility). Returns ------- str The edge-list format string representation of the graph. Examples -------- >>> from phylozoo.core.primitives.d_multigraph import DirectedMultiGraph >>> from phylozoo.core.primitives.d_multigraph.io import to_edgelist >>> >>> G = DirectedMultiGraph() >>> G.add_edge(1, 2, weight=1.0) 0 >>> G.add_edge(2, 3, weight=2.0) 0 >>> el_str = to_edgelist(G) >>> '1 2' in el_str True >>> '2 3' in el_str True Notes ----- The edge-list format: - One edge per line - Format: `u v` or `u v key` or `u v key attr1=value1 attr2=value2` - Uses node_id as the label/name - Includes edge keys for parallel edges - Includes edge attributes if present """ lines = [] for u, v, key, data in graph.edges_iter(keys=True, data=True): line_parts = [str(u), str(v)] # Add key if there are parallel edges if graph._graph.number_of_edges(u, v) > 1: line_parts.append(str(key)) # Add attributes if data: for attr_key, attr_value in data.items(): if isinstance(attr_value, str) and " " in attr_value: attr_value = f'"{attr_value}"' line_parts.append(f"{attr_key}={attr_value}") lines.append(" ".join(line_parts)) return "\n".join(lines) + "\n"
[docs] def from_edgelist(edgelist_string: str, **kwargs: Any) -> DirectedMultiGraph: """ Parse an edge-list format string and create a DirectedMultiGraph. Parameters ---------- edgelist_string : str Edge-list format string containing graph data. **kwargs Additional arguments (currently unused, for compatibility). Returns ------- DirectedMultiGraph Parsed directed multi-graph. Raises ------ PhyloZooParseError If the edge-list string is malformed or cannot be parsed. Examples -------- >>> from phylozoo.core.primitives.d_multigraph.io import from_edgelist >>> >>> el_str = '''1 2 ... 2 3 weight=2.0 ... 3 4 0 key1=value1''' >>> >>> G = from_edgelist(el_str) >>> G.number_of_nodes() 4 >>> G.number_of_edges() 3 Notes ----- This parser expects: - One edge per line - Format: `u v` or `u v key` or `u v key attr1=value1 attr2=value2` - Uses node_id as the label/name """ graph: Any = DirectedMultiGraph() for line in edgelist_string.strip().split("\n"): line = line.strip() if not line or line.startswith("#"): continue parts = line.split() if len(parts) < 2: raise PhyloZooParseError(f"Invalid edge line (need at least 2 values): {line}") u = convert_node_id(parts[0]) v = convert_node_id(parts[1]) key = None attrs: dict[str, Any] = {} if len(parts) > 2: # The third token is a key (a bare integer) or the first attribute. third_part = parts[2] if "=" not in third_part: try: key = int(third_part) start_idx = 3 except ValueError: start_idx = 2 else: start_idx = 2 for part in parts[start_idx:]: if "=" not in part: continue attr_key, attr_value = part.split("=", 1) attr_value = attr_value.strip("\"'") try: attrs[attr_key] = float(attr_value) if "." in attr_value else int(attr_value) except ValueError: attrs[attr_key] = attr_value graph.add_edge(u, v, key=key, **attrs) return graph # type: ignore[no-any-return]
# Register format handlers with FormatRegistry FormatRegistry.register( DirectedMultiGraph, "dot", reader=from_dot, writer=to_dot, extensions=[".dot", ".gv"], default=True, ) FormatRegistry.register( DirectedMultiGraph, "edgelist", reader=from_edgelist, writer=to_edgelist, extensions=[".el"] )