Changelog#

Version History#

0.4#

0.4.2#

Directed networks are drawn left to right by default, reticulate edges in cladograms no longer run along tree edges, and edge-attribute getters reject edges that do not exist. The PhyloZoo paper is now published in Bioinformatics.

Changed#
  • The citation in the README and documentation now refers to the published paper (Holtgrefe, Bioinformatics 42(10):btag748, 2026, doi:10.1093/bioinformatics/btag748) instead of the bioRxiv preprint.

  • The pz-cladogram layout (the default for directed networks) defaults to direction='LR' (root on the left, leaves in a column on the right with horizontal labels) instead of 'TD'. Pass direction='TD' for the previous top-down drawing. pz-layered stays top-down by default.

  • get_edge_attribute (and so get_branch_length, get_bootstrap and get_gamma) on DirectedPhyNetwork and MixedPhyNetwork raises PhyloZooValueError for an edge that does not exist, as documented, instead of returning None. A key that does not match the single edge between two nodes is no longer ignored. A missing attribute on an existing edge still returns None.

Fixed#
  • binary_resolution() no longer invents gamma values: a hybrid node without gammas is resolved without them, where missing gammas used to be read as 0.0. When gammas are present, the caterpillar gammas now multiply along each path to the original gamma of that parent edge, so displayed-tree probabilities are preserved (e.g. 0.1/0.2/0.3/0.4 used to come out as 0/0.24/0.36/0.4).

  • In rectangular pz-cladogram drawings, a node whose only backbone child is not the hybrid it also points to (and that cannot be moved onto the hybrid’s layer) is placed half a leaf gap beyond that hybrid. Its reticulate edge then enters the hybrid from the other side instead of running right alongside a tree edge and past other nodes.

0.4.1#

eNewick interoperability: networks written by PhyloNetworks, PhyloNet and the SiPhyNetwork R package now load, and PhyloZoo writes the same edge fields they read.

Changed#
  • to_string("enewick") writes branch lengths, bootstrap and gamma values as the Rich Newick fields :length:support:gamma (empty fields where a value is missing, e.g. #H1:0.5::0.6) instead of [&gamma=...] comments, which no other network tool reads and which PhyloZoo itself could not read back on a hybrid marker. Files written by earlier versions with gamma or bootstrap comments after a hybrid marker are not read.

Fixed#
  • The eNewick parser accepts a hybrid back-reference (#Hk) that appears before the occurrence carrying the label and children, as in (N,((C,(D,(B,#H1))),(A,(E,(F)#H1))));. A hybrid that appears only as bare references now raises an error saying it was never defined.

  • The eNewick parser reads the :length:support:gamma edge fields (with empty fields allowed, as in #H1:0.2::0.3), storing them as the edge attributes branch_length, bootstrap and gamma. Networks simulated with SiPhyNetwork now load directly.

  • to_string("enewick") writes a hybrid node with parallel parent edges once per edge instead of dropping the second edge, so such networks (SiPhyNetwork produces them) survive a write/read round trip with the gamma of every parent edge intact.

0.4.0#

A visualization release: every phylogenetic network is now drawn by PhyloZoo’s own layouts, without Graphviz. Semi-directed networks get an unrooted tree-of-blobs drawing, directed networks a rewritten cladogram plus layered and radial views, and both can be previewed as text in the terminal. The plotting tutorial and the API reference were rewritten around these layouts.

Added#
  • pz-unrooted layout (compute_pz_unrooted_layout() and compute_pz_unrooted_layout(), shared core in phylozoo.viz._unrooted), the new default for SemiDirectedPhyNetwork and also available for DirectedPhyNetwork. The network is decomposed into blobs; the tree of blobs is drawn with the equal-angle algorithm, each blob as a regular polygon from a planar embedding (Tutte embedding for interior nodes), and pendant subtrees leave a blob in the cyclic order of their attachment nodes, so cut edges never cross blob edges. Optional daylight rounds (equal-daylight with exact edge lengths, blobs as rigid pivots) and refine stress-majorization iterations (the neato algorithm) spread the drawing; both are guarded so that they never add edge crossings. On a directed network the root is marked with a larger gold node.

  • pz-layered layout for DirectedPhyNetwork (compute_pz_layered_layout()): a self-contained layered (Sugiyama) layout in the style of Graphviz dot — longest-path layers, bend points for long edges, barycenter sweeps with adjacent swaps, a straightening coordinate pass — without a tree backbone, so heavily reticulate networks get fewer crossings than with pz-cladogram. Options align_leaves, rectangular, sweeps and direction.

  • pz-radial for directed networks (compute_pz_radial_layout()): a circular cladogram with the root at the centre, leaves evenly spaced on the outer circle and reticulate edges as straight chords, in the style of Dendroscope’s radial view.

  • pz-cladogram options align_leaves (default True: leaves on the bottom layer), rectangular (default True: elbow edges, reticulate edges entering the hybrid sideways) and horizontal_reticulations (default on with rectangular: reticulate parents are lowered onto the hybrid’s layer where the network allows it, so those edges are single horizontal segments).

  • to_preview_string() and the to_preview_string() / preview() methods of DirectedPhyNetwork and SemiDirectedPhyNetwork: a text drawing of the network (box-drawing tree edges, dotted hybrid edges with arrowheads, ◆ hybrid nodes, ○ root) built on the rectangular pz-cladogram placement, with max_width, max_leaves, col_width, rows_per_leaf and (semi-directed) root_location options. Needs no matplotlib: phylozoo.viz, phylozoo.viz.dnetwork and phylozoo.viz.sdnetwork import their matplotlib plotters lazily, so the layouts and the preview work with the core install.

  • arrows ('all', 'hybrid', 'none' or None for automatic), label_rotation (None aligns labels with their edge, a fixed angle places them beside the node), root_color and root_size on DNetStyle; label_rotation also on SDNetStyle.

  • Shared layout utilities stress_majorization(), count_crossings() and sort_key().

  • Documentation: the plotting tutorial now walks through both network classes and every PhyloZoo layout with figures (replacing the semi-directed-only tutorial); the plotting and styling manuals describe the PhyloZoo layouts first; the viz API reference lists every layout function with its parameters, the layout classes and the preview; the core manual pages and the README quickstart show the text drawing.

Changed#
  • pz-dag is now pz-cladogram. compute_pz_cladogram_layout() replaces compute_pz_dag_layout (module viz.dnetwork.layout.cladogram replaces layout.dag) and layout='pz-dag' is no longer accepted. The layout itself was rewritten as a layered drawing: nodes hang below their lowest parent, leaves are aligned on the bottom layer, and siblings are ordered by a hybrid-aware barycenter heuristic plus a local search on the exact crossing count instead of 2000 random shuffles. Restarts and the swap search are restricted to the nodes that can change the number of crossings (trees skip them entirely). Deterministic by default (seed=0), trials now counts restarts of the heuristic (default 10), and positions are normalised to unit range like every other layout. Default layer_gap and x_scale are 1.0.

  • The default layout for semi-directed networks is now pz-unrooted instead of 'neato'; pass layout='neato' for the previous behaviour (needs pygraphviz).

  • pz-radial for SemiDirectedPhyNetwork (compute_pz_radial_layout()) accepts any network instead of trees only: the network is rooted (new root_location option) and drawn as a circular cladogram; the sibling order is re-optimised with crossings counted in the circle.

  • pz-layered starts its sweeps from the pz-cladogram order, which gives noticeably fewer crossings on reticulate networks.

  • All PhyloZoo layouts build their working graph from nodes and edges in sorted order, so a drawing depends only on the network and not on the insertion order of its nodes (which for string-labelled networks varied with Python’s hash seed between processes).

  • DNetStyle defaults now match SDNetStyle: white tree nodes (node_size 80), black leaves (leaf_size 100), pink hybrid nodes, red hybrid edges, label_offset 0.015, arrow_head_size 12. In the layered and radial layouts only hybrid edges carry an arrowhead, and in pz-cladogram and aligned pz-layered drawings leaf labels hang straight below (or, for direction='LR', right of) the leaves.

  • Automatically rotated labels continue the line of the leaf’s pendant edge, starting at the node, instead of being offset diagonally; labels with a fixed label_rotation sit beside the node on its outward side, and pz-radial leaf labels read outwards along the radius.

  • The Graphviz layouts are documented as an alternative for the multigraph primitives only; the manuals and tutorial use the PhyloZoo layouts throughout.

Removed#
  • The phylozoo[graphviz] install extra. The PhyloZoo layouts need only phylozoo[viz]; Graphviz layouts ('dot', 'neato', …) still work when pygraphviz is installed by hand (see Installation).

  • compute_pz_dag_layout and layout='pz-dag' (see pz-cladogram above), and the trees-only restriction of pz-radial.

0.3#

0.3.0#

A performance release: the fundamental operations were profiled on networks of up to \(10^5\) leaves and the algorithmic bottlenecks removed, without changing results. Every docstring example is now executed as a test.

Added#
  • copy keyword on dnetwork_from_graph() and sdnetwork_from_graph(). With copy=False the network adopts the given graph instead of rebuilding it (the caller must not use the graph afterwards); label validation, leaf auto-labelling and validate() run exactly as before. All internal transformations use it, which removes one full graph construction from every conversion, subnetwork and displayed tree.

  • has_node() and all_degrees() on DirectedMultiGraph and MixedMultiGraph (all_degrees returns every node’s in-/out-/undirected degree in one pass), and has_updown_path() (linear-time existence test for an up-down path, without enumerating paths).

  • Docstring examples run as doctests in the normal pytest run (--doctest-modules and src/phylozoo in testpaths); see the Testing guide. About 120 stale examples were repaired, and examples that wrote files now write into a temporary directory.

  • README quickstart, and a manual section on validation inside library functions.

Changed#
  • Derived networks are no longer re-validated. Functions that build a new network from an already valid one — subnetwork, to_sd_network, to_d_network (with an automatically chosen root), tree_of_blobs, displayed_trees, to_lsa_network, suppress_2_blobs, identify_parallel_edges, binary_resolution and the generator enumeration — construct their result inside no_validation(), since it is valid by construction. Constructors, parsers and functions whose result depends on caller input (e.g. to_d_network with an explicit root_location, root_at_outgroup) still validate. Roughly halves the default cost of to_sd_network, displayed_trees and suppress_2_blobs.

  • distances() and displayed_splits() (both representations) no longer enumerate all \(\prod_b 2^{r_b}\) switchings. A path’s stretch inside a blob depends only on that blob’s hybrid choices, so only a reference switching plus each blob’s local switchings are evaluated (\(1 + \sum_b 2^{r_b}\)) and aggregated per blob — for all three distance modes, since a sum of independently chosen terms is minimised or maximised term by term. The cost is exponential in the level, not in the number of reticulations: 20 reticulations in 20 blobs take 1.6 s instead of \(2^{20}\) switchings. Both functions share one switching machinery that walks the network’s cached graph with removed edges instead of copying a switching graph per switching (single-blob distances 2.7x faster; the semi-directed matrix computation, which used one shortest-path call per ordered leaf pair, is ~20x faster).

  • is_tree_metric() is \(O(n^2)\) instead of \(O(n^4)\): the tree an additive matrix must come from is reconstructed by inserting taxa one by one, and the matrix is a tree metric exactly when that tree reproduces it (n=500: 4.2 s to 0.26 s). tree_from_splitsystem() inserts compatible splits as clusters, smallest first, instead of searching cut-vertices per split (n=120: 2.1 s to 0.025 s). hamming_distances() counts matches and valid sites with (exact, blocked) matrix products instead of a pair loop (6–31x); is_pairwise_compatible() uses bitmasks.

  • Displayed trees / quartets / triplets: no per-tree graph copies, one shared rooting for semi-directed networks, degree-1 pruning as a worklist, and quartets/triplets read topology off the tree graphs instead of building network objects.

  • Semi-directed validation no longer round-trips through a directed network; lsa_node uses dominators; subnetwork collects ancestors in one traversal; is_ultrametric, is_normal, source_components, orient_away_from_vertex and quartet_distance avoid repeated scans.

  • Primitives: the combined undirected view and the nodes/edges views are built lazily (constructing a network no longer maintains a second graph), subgraph walks only the selected nodes’ adjacency (extracting all blobs of a 2 000-leaf network: 1.37 s to 0.03 s), and Split stores its sides as the partition’s own frozensets and can share one taxon set across a split system (2.9x less memory for large split systems; split.set1/set2 are now frozenset — equality with plain sets is unchanged, and so is the repr).

  • Generators: R1 is applied to unordered side pairs (it is symmetric), the reachability matrix uses set unions, and semidirect_generators no longer re-validates its by-construction-valid results (level-4 semi-direction 8.2 s to 1.2 s). Generator counts are unchanged.

Fixed#
  • distances() raised PhyloZooValueError: Multiple parallel edges exist on any network with parallel edges (branch lengths were looked up without the edge key).

  • displayed_quartets on semi-directed networks crashed because a switching kept the surviving hybrid edges directed; switchings are now undirected before use.

  • The DOT writers dropped non-zero edge keys on non-parallel edges, so such graphs did not round-trip (the cause of an intermittently failing generator I/O test).

  • Nested no_validation() blocks now stack as documented: an inner block adds to the outer block’s suppression instead of replacing it when it names other methods.

  • Documentation: duplicate Squirrel citation removed.

0.2#

0.2.6#

Added#
  • Standard Graphviz dot format for MixedMultiGraph, alongside the existing phylozoo-dot (which remains the default). Undirected edges are written as u -> v [dir=none] inside a digraph, so — unlike phylozoo-dot, which mixes -> and -- in one graph block and is not valid DOT — the output opens in any Graphviz tool and round-trips losslessly, including parallel directed and undirected edges. The DOT scaffolding (escaping, attribute formatting/parsing, node-id coercion and document parsing) is now shared between the directed- and mixed-multigraph handlers in a new phylozoo.utils.io.format_utils.dot module.

  • SemiDirectedGenerator and DirectedGenerator now inherit IOMixin, gaining save/load/to_string/ from_string/convert. They inherit the formats of their underlying graph — phylozoo-dot (default) and dot for the semi-directed generator, dot (default) and edgelist for the directed one — so a generator can be serialised and reconstructed directly, without routing through generator.graph. Reading rebuilds (and validates) the generator from the parsed graph.

  • Generator construction is now exposed as reusable single steps: gambette_step() (apply the Gambette R1/R2 rules + isomorphism deletion to a collection of directed generators) and semidirect_generators() (semi-direct a collection of directed generators + isomorphism deletion). The all_level_k_generators functions are now thin loops over these, and a caller can advance / semi-direct their own (e.g. saved) generator set without rebuilding from level 0. Both accept mixed-level inputs (gambette_step also accepts level-0 generators) and assume their inputs are valid.

Changed#
  • Generator enumeration now deduplicates with a Weisfeiler-Lehman graph hash added to the cheap-invariant key (new helpers _get_graph_wl_hash on the directed and mixed multigraph isomorphism modules; multiplicity- and direction-aware via a simple-graph encoding). This splits isomorphism candidates into far finer groups, so the exact VF2 check runs only within tiny groups — the level-4 directed enumeration drops from ~276 s to ~6 s (~44x), and the full level-4 semi-directed enumeration from ~285 s to ~9 s. Output is identical (1993 directed / 307 semi-directed level-4 generators); exactness is unchanged, since the hash is only a grouping key and VF2 remains the final check.

Fixed#
  • phylozoo-dot round-trip for MixedMultiGraph: the writer injected a label=<node id> attribute on every label-less node, and the reader parsed that unquoted value back as an int. A round-tripped graph therefore carried a spurious non-string label on every node, which broke downstream label validation — e.g. attach_leaves_to_generator() raised PhyloZooTypeError: ... non-string label. The writer no longer emits the redundant label, and the reader keeps any label value as a string. Generator graphs (and any MixedMultiGraph) now round-trip losslessly, including parallel directed and undirected edges, and a loaded generator is directly usable to build networks — without first rebuilding a clean graph from its edge lists.

0.2.5#

Fixed#
  • hybrid_sides: a reticulation whose child slot was already occupied by an undirected (backbone) edge was still reported as a hybrid side. Attaching the required leaf to it then produced an invalid hybrid (total degree = in-degree + 2), raising a degree error in attach_leaves_to_generator(). hybrid_sides now only includes hybrid nodes whose child slot is free (no directed out-edge and no incident undirected edge); such occupied reticulations remain hybrid nodes (the generator’s level is unchanged) and their descendants are reached through the incident undirected edge side. The level-1 bidirected self-loop node is still a hybrid side.

0.2.4#

Fixed#
  • subnetwork(): the updown-path cache lookup used an or-chain (cache.get(key) or ...), which treated an empty cached path set (falsy) as a cache miss and silently recomputed the path. Fixed by using an explicit _MISS_SENTINEL = object() sentinel so empty sets are handled correctly.

0.2.3#

  • Fix PyPi deployment

0.2.2#

Added#
  • make_lsa: bool = False parameter on displayed_trees(): when True, each displayed tree is converted to its LSA network before being returned.

  • label_rotation (float | None) attribute on SDNetStyle. None (default) auto-aligns each taxon label with the direction of the leaf’s connecting edge; a fixed float (e.g. -7) applies a uniform tilt to all labels, which reduces overlap in dense force-directed layouts.

  • anchor parameter on the internal draw_label rendering helper (phylozoo.viz._render): taxon labels are now automatically placed on the side of the node facing away from the connecting edge, preventing labels from crossing edges in neato and related layouts.

  • New tutorials:

    • Plotting Networks — step-by-step guide to plotting a semi-directed phylogenetic network, using the Xiphophorus swordfish example from [Holtgrefe et al., 2025].

    • Tutorial on displayed-tree indistinguishable networks: introduces the concept and provides worked example code.

Changed#
  • Default layout for SemiDirectedPhyNetwork changed from 'twopi' to 'neato' (Graphviz spring-embedder). All affected call sites and documentation updated.

  • SDNetStyle default values updated to better suit the neato layout: node_color='white', node_size=80.0, leaf_color='#0a0a0a', leaf_size=100.0, hybrid_color='#fcc0bc', label_offset=0.01 (previously lighter colours and larger sizes).

  • Semi-directed network visualization tutorial and styling manual updated to reflect the new style defaults, neato layout, and label_rotation parameter.

  • Quickstart tutorial revised: network node accessor methods, hybrid network example, and plotting instructions updated.

  • Citation format in README and documentation updated to a full bibliographic reference for [Holtgrefe et al., 2025].

Performance#
  • k_taxon_subnetworks(): all \(\binom{n}{2}\) pairwise up-down vertex sets are now pre-computed once and cached before the combination loop. Each subnetwork() call performs a dictionary lookup instead of re-invoking updown_path_vertices(). For \(k=5\) on \(n=13\) taxa this reduces updown_path_vertices calls from \(\binom{13}{5} \cdot \binom{5}{2} = 12{,}870\) to \(\binom{13}{2} = 78\), a \({\approx}165\times\) reduction.

  • subnetwork(): removed a redundant MixedMultiGraph.copy() call. subgraph() already returns a fresh independent object; the subsequent .copy() was an unnecessary second allocation.

  • identify_parallel_edges() / subnetwork(): extracted _identify_parallel_edges_inplace(graph, exclude_nodes) as an internal helper that modifies a MixedMultiGraph in-place. subnetwork now calls this helper directly on the working graph before the single sdnetwork_from_graph() call, eliminating one MixedMultiGraph.copy() and one SemiDirectedPhyNetwork construction per call when identify_parallel_edges=True. The public identify_parallel_edges function is an unchanged thin wrapper.

  • Combined effect: on a level-3 network with \(k=11\) backbone leaves (14 taxa, 2,002 quinnets) quinnet extraction time fell from ≈22 s to ≈4 s (≈5× end-to-end speedup).

Fixed#
  • License badge in README.md corrected to display MIT.

Internal#

0.2.1#

Added#
Changed#
  • _check_kalmanson_conditions moved from a nested closure inside is_kalmanson() to a module-level @njit function, consistent with the other Numba helpers in the module.

0.2.0#

Added#
  • New phylozoo.core.triplet module providing Triplet, TripletProfile, and TripletProfileSet for working with rooted three-taxon trees (characterised by a trivial 1|2 split a|bc or by an unresolved 3-taxon star). All three classes are re-exported from the top-level phylozoo namespace.

  • New displayed_triplets() derivation on DirectedPhyNetwork, returning a TripletProfileSet aggregated over the displayed trees.

  • Manual chapter Triplets (overview + per-class pages) and an api/core/triplets API page.

  • Manual page Parallel Execution and api/utils/parallel API page documenting phylozoo.utils.parallel as the standard interface for parallel execution in PhyloZoo. No functions currently expose a parallel parameter; the module is the intended way to introduce parallelization in future implementations.

  • New CI GitHub Actions workflow (.github/workflows/ci.yml) running pytest (Python 3.10 and 3.11), ruff, and black --check on every push to master and every pull request. See the Testing Guide for details. A CI badge has been added to the project README.

Changed#
  • ParallelBackend class docstring restructured to plain prose (the enum members are now described inline rather than in a NumPy-style Attributes section) to avoid duplicate Sphinx object descriptions.

  • Black, Ruff, and Mypy fixes have been made throughout the codebase.

Removed#
  • phylozoo.core.distance.operations (and the entire operations.py file). The Traveling Salesman Problem solvers (optimal_tsp_tour, approximate_tsp_tour) have been removed, together with the corresponding manual section and API entry. Their implementation is now part of the dependent package physquirrel.

  • phylozoo.core.quartet.qdistance.quartet_distance_with_partition and its documentation in the Quartet Profile Sets manual page.

0.1#

0.1.2#

  • Fixed newick parsing branch length order: it must be after the hybrid marker, not before.

0.1.1#

  • Fixed docs build: added matplotlib to docs dependencies.

0.1.0#

  • Initial public release.