Source code for phylozoo.viz.sdnetwork.layout.unrooted

"""
Tree-of-blobs layout for SemiDirectedPhyNetwork (pz-unrooted).

The placement itself lives in :mod:`phylozoo.viz._unrooted` and is shared with
the directed version.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from phylozoo.viz._unrooted import simple_graph, tree_of_blobs_positions

from .base import SDNetLayout
from .routes import compute_radial_routes

if TYPE_CHECKING:
    from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork


[docs] def compute_pz_unrooted_layout( network: "SemiDirectedPhyNetwork", edge_length: float = 1.0, node_spacing: float = 0.7, inner_scale: float = 0.6, start_angle: float = 0.0, refine: int = 50, daylight: int = 0, ) -> SDNetLayout: """ Compute a tree-of-blobs layout for a SemiDirectedPhyNetwork. This is a custom PhyloZoo layout algorithm (pz-unrooted). It works for any connected semi-directed network; for trees it reduces to the classic equal-angle drawing of an unrooted tree. The algorithm: 1. Decomposes the network into blobs and builds the tree of blobs (one node per blob, one edge per cut edge). 2. Roots that tree at its centroid and assigns every subtree an angular wedge proportional to the number of leaves it contains (equal-angle). 3. Draws each non-trivial blob as a polygon: the nodes on the outer face of a planar embedding lie evenly spaced on a circle, in face order, and the remaining nodes are placed at the barycentre of their neighbours (Tutte embedding). Subtrees attached to interior nodes are drawn inside the blob at a reduced scale. 4. Optionally equalises the "daylight" around every node (``daylight`` rounds, blobs kept rigid) and relaxes the drawing with iterations of stress majorization (the algorithm behind Graphviz ``neato``), started from the crossing-free drawing of step 3, with every blob edge targeting ``node_spacing``. This spreads cramped subtrees and keeps blobs evenly sided, and is deterministic. Parameters ---------- network : SemiDirectedPhyNetwork The network to layout. edge_length : float, optional Length of cut edges (tree edges between blobs and pendant edges). By default 1.0. node_spacing : float, optional Target distance between consecutive nodes on a blob's circle. By default 0.7. inner_scale : float, optional Scale factor for subtrees hanging off interior blob nodes, which are drawn inside the blob. By default 0.6. start_angle : float, optional Rotation of the whole drawing, in radians. By default 0.0. refine : int, optional Number of stress-majorization iterations applied to the tree-of-blobs drawing. Use 0 for the raw geometric drawing (blobs as exact polygons). By default 50. daylight : int, optional Number of equal-daylight rounds applied before the stress refinement: around every tree node and around every blob (as a whole), the subtrees hanging off it through cut edges are rotated so that the empty angular gaps between them become equal (blobs stay rigid). Spreads the drawing while keeping every edge length exact (daylight=8 with refine=0 gives the classic SplitsTree-style unrooted drawing). By default 0. Returns ------- SDNetLayout The computed layout. Positions are centred at the origin and scaled to unit range. Raises ------ PhyloZooLayoutError If the network is empty or not connected. Examples -------- >>> from phylozoo.core.network.sdnetwork import SemiDirectedPhyNetwork >>> from phylozoo.viz.sdnetwork.layout import compute_pz_unrooted_layout >>> >>> net = 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'})] ... ) >>> layout = compute_pz_unrooted_layout(net) >>> len(layout.positions) == net.number_of_nodes() True >>> layout.algorithm 'pz-unrooted' """ positions = tree_of_blobs_positions( simple_graph(network), set(network.leaves), edge_length=edge_length, node_spacing=node_spacing, inner_scale=inner_scale, start_angle=start_angle, refine=refine, daylight=daylight, ) return SDNetLayout( network=network, positions=positions, edge_routes=compute_radial_routes(network, positions), algorithm="pz-unrooted", parameters={ "edge_length": edge_length, "node_spacing": node_spacing, "inner_scale": inner_scale, "start_angle": start_angle, "refine": refine, "daylight": daylight, }, )