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-cladogramlayout (the default for directed networks) defaults todirection='LR'(root on the left, leaves in a column on the right with horizontal labels) instead of'TD'. Passdirection='TD'for the previous top-down drawing.pz-layeredstays top-down by default.get_edge_attribute(and soget_branch_length,get_bootstrapandget_gamma) onDirectedPhyNetworkandMixedPhyNetworkraisesPhyloZooValueErrorfor an edge that does not exist, as documented, instead of returningNone. Akeythat does not match the single edge between two nodes is no longer ignored. A missing attribute on an existing edge still returnsNone.
Fixed#
binary_resolution()no longer invents gamma values: a hybrid node without gammas is resolved without them, where missing gammas used to be read as0.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-cladogramdrawings, 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:gammaedge fields (with empty fields allowed, as in#H1:0.2::0.3), storing them as the edge attributesbranch_length,bootstrapandgamma. 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-unrootedlayout (compute_pz_unrooted_layout()andcompute_pz_unrooted_layout(), shared core inphylozoo.viz._unrooted), the new default forSemiDirectedPhyNetworkand also available forDirectedPhyNetwork. 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. Optionaldaylightrounds (equal-daylight with exact edge lengths, blobs as rigid pivots) andrefinestress-majorization iterations (theneatoalgorithm) 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-layeredlayout forDirectedPhyNetwork(compute_pz_layered_layout()): a self-contained layered (Sugiyama) layout in the style of Graphvizdot— 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 withpz-cladogram. Optionsalign_leaves,rectangular,sweepsanddirection.pz-radialfor 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-cladogramoptionsalign_leaves(default True: leaves on the bottom layer),rectangular(default True: elbow edges, reticulate edges entering the hybrid sideways) andhorizontal_reticulations(default on withrectangular: 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 theto_preview_string()/preview()methods ofDirectedPhyNetworkandSemiDirectedPhyNetwork: a text drawing of the network (box-drawing tree edges, dotted hybrid edges with arrowheads,◆hybrid nodes,○root) built on the rectangularpz-cladogramplacement, withmax_width,max_leaves,col_width,rows_per_leafand (semi-directed)root_locationoptions. Needs no matplotlib:phylozoo.viz,phylozoo.viz.dnetworkandphylozoo.viz.sdnetworkimport their matplotlib plotters lazily, so the layouts and the preview work with the core install.arrows('all','hybrid','none'orNonefor automatic),label_rotation(Nonealigns labels with their edge, a fixed angle places them beside the node),root_colorandroot_sizeonDNetStyle;label_rotationalso onSDNetStyle.Shared layout utilities
stress_majorization(),count_crossings()andsort_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()replacescompute_pz_dag_layout(moduleviz.dnetwork.layout.cladogramreplaceslayout.dag) andlayout='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),trialsnow counts restarts of the heuristic (default 10), and positions are normalised to unit range like every other layout. Defaultlayer_gapandx_scaleare 1.0.The default layout for semi-directed networks is now pz-unrooted instead of
'neato'; passlayout='neato'for the previous behaviour (needspygraphviz).pz-radialforSemiDirectedPhyNetwork(compute_pz_radial_layout()) accepts any network instead of trees only: the network is rooted (newroot_locationoption) and drawn as a circular cladogram; the sibling order is re-optimised with crossings counted in the circle.pz-layeredstarts its sweeps from thepz-cladogramorder, 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).
DNetStyledefaults now matchSDNetStyle: white tree nodes (node_size80), black leaves (leaf_size100), pink hybrid nodes, red hybrid edges,label_offset0.015,arrow_head_size12. In the layered and radial layouts only hybrid edges carry an arrowhead, and inpz-cladogramand alignedpz-layereddrawings leaf labels hang straight below (or, fordirection='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_rotationsit beside the node on its outward side, andpz-radialleaf 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 onlyphylozoo[viz]; Graphviz layouts ('dot','neato', …) still work whenpygraphvizis installed by hand (see Installation).compute_pz_dag_layoutandlayout='pz-dag'(seepz-cladogramabove), and the trees-only restriction ofpz-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#
copykeyword ondnetwork_from_graph()andsdnetwork_from_graph(). Withcopy=Falsethe network adopts the given graph instead of rebuilding it (the caller must not use the graph afterwards); label validation, leaf auto-labelling andvalidate()run exactly as before. All internal transformations use it, which removes one full graph construction from every conversion, subnetwork and displayed tree.has_node()andall_degrees()onDirectedMultiGraphandMixedMultiGraph(all_degreesreturns every node’s in-/out-/undirected degree in one pass), andhas_updown_path()(linear-time existence test for an up-down path, without enumerating paths).Docstring examples run as doctests in the normal
pytestrun (--doctest-modulesandsrc/phylozoointestpaths); 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_resolutionand the generator enumeration — construct their result insideno_validation(), since it is valid by construction. Constructors, parsers and functions whose result depends on caller input (e.g.to_d_networkwith an explicitroot_location,root_at_outgroup) still validate. Roughly halves the default cost ofto_sd_network,displayed_treesandsuppress_2_blobs.distances()anddisplayed_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-blobdistances2.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_nodeuses dominators;subnetworkcollects ancestors in one traversal;is_ultrametric,is_normal,source_components,orient_away_from_vertexandquartet_distanceavoid repeated scans.Primitives: the combined undirected view and the
nodes/edgesviews are built lazily (constructing a network no longer maintains a second graph),subgraphwalks only the selected nodes’ adjacency (extracting all blobs of a 2 000-leaf network: 1.37 s to 0.03 s), andSplitstores 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/set2are nowfrozenset— equality with plain sets is unchanged, and so is therepr).Generators: R1 is applied to unordered side pairs (it is symmetric), the reachability matrix uses set unions, and
semidirect_generatorsno 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()raisedPhyloZooValueError: Multiple parallel edges existon any network with parallel edges (branch lengths were looked up without the edge key).displayed_quartetson 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
dotformat forMixedMultiGraph, alongside the existingphylozoo-dot(which remains the default). Undirected edges are written asu -> v [dir=none]inside adigraph, so — unlikephylozoo-dot, which mixes->and--in onegraphblock 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 newphylozoo.utils.io.format_utils.dotmodule.SemiDirectedGeneratorandDirectedGeneratornow inheritIOMixin, gainingsave/load/to_string/from_string/convert. They inherit the formats of their underlying graph —phylozoo-dot(default) anddotfor the semi-directed generator,dot(default) andedgelistfor the directed one — so a generator can be serialised and reconstructed directly, without routing throughgenerator.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) andsemidirect_generators()(semi-direct a collection of directed generators + isomorphism deletion). Theall_level_k_generatorsfunctions 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_stepalso 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_hashon 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-dotround-trip forMixedMultiGraph: the writer injected alabel=<node id>attribute on every label-less node, and the reader parsed that unquoted value back as anint. A round-tripped graph therefore carried a spurious non-stringlabelon every node, which broke downstream label validation — e.g.attach_leaves_to_generator()raisedPhyloZooTypeError: ... non-string label. The writer no longer emits the redundant label, and the reader keeps anylabelvalue as a string. Generator graphs (and anyMixedMultiGraph) 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 inattach_leaves_to_generator().hybrid_sidesnow 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 anor-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 = Falseparameter ondisplayed_trees(): whenTrue, each displayed tree is converted to its LSA network before being returned.label_rotation(float | None)attribute onSDNetStyle.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.anchorparameter on the internaldraw_labelrendering 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 inneatoand 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
SemiDirectedPhyNetworkchanged from'twopi'to'neato'(Graphviz spring-embedder). All affected call sites and documentation updated.SDNetStyledefault values updated to better suit theneatolayout: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,
neatolayout, andlabel_rotationparameter.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. Eachsubnetwork()call performs a dictionary lookup instead of re-invokingupdown_path_vertices(). For \(k=5\) on \(n=13\) taxa this reducesupdown_path_verticescalls from \(\binom{13}{5} \cdot \binom{5}{2} = 12{,}870\) to \(\binom{13}{2} = 78\), a \({\approx}165\times\) reduction.subnetwork(): removed a redundantMixedMultiGraph.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 aMixedMultiGraphin-place.subnetworknow calls this helper directly on the working graph before the singlesdnetwork_from_graph()call, eliminating oneMixedMultiGraph.copy()and oneSemiDirectedPhyNetworkconstruction per call whenidentify_parallel_edges=True. The publicidentify_parallel_edgesfunction 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.mdcorrected to display MIT.
Internal#
Distance matrix helper functions in
phylozoo.core.distance.classificationsandphylozoo.core.distance.decompositionrefactored for readability; no API change.Edge routing for the
pz-radialand spring-embedder (neato/fdp) layouts improved inphylozoo.viz.sdnetwork.layout.
0.2.1#
Added#
Split decomposition ([Bandelt and Dress, 1992]) for distance matrices:
isolation_index()— computes the isolation index of a bipartition with respect to a distance matrix.split_decomposition()— canonical decompositiond = d^0 + Σ α_S δ_S, returning aWeightedSplitSystemof all d-splits and the split-prime residual as aDistanceMatrix.Both functions are re-exported from
phylozoo.core.distance.
New classification functions in
phylozoo.core.distance.classifications:is_tree_metric()— four-point condition check (Numba-accelerated, \(O(n^4)\)).is_totally_decomposable()— checks whether the split-prime residual is zero.
distances_from_splitsystem()↔split_decomposition()round-trip tests andtree_from_splitsystem()↔induced_splits()round-trip tests.Documentation: new Split Decomposition section in the distance manual; updated Algorithms section in the split-system manual; new
api/core/distanceentry for the decomposition module. BibTeX entryBandelt1992added tobibliography.bib.
Changed#
_check_kalmanson_conditionsmoved from a nested closure insideis_kalmanson()to a module-level@njitfunction, consistent with the other Numba helpers in the module.
0.2.0#
Added#
New
phylozoo.core.tripletmodule providingTriplet,TripletProfile, andTripletProfileSetfor working with rooted three-taxon trees (characterised by a trivial 1|2 splita|bcor by an unresolved 3-taxon star). All three classes are re-exported from the top-levelphylozoonamespace.New
displayed_triplets()derivation onDirectedPhyNetwork, returning aTripletProfileSetaggregated over the displayed trees.Manual chapter
Triplets(overview + per-class pages) and anapi/core/tripletsAPI page.Manual page
Parallel Executionandapi/utils/parallelAPI page documentingphylozoo.utils.parallelas the standard interface for parallel execution in PhyloZoo. No functions currently expose aparallelparameter; the module is the intended way to introduce parallelization in future implementations.New
CIGitHub Actions workflow (.github/workflows/ci.yml) runningpytest(Python 3.10 and 3.11),ruff, andblack --checkon every push tomasterand every pull request. See the Testing Guide for details. A CI badge has been added to the project README.
Changed#
ParallelBackendclass docstring restructured to plain prose (the enum members are now described inline rather than in a NumPy-styleAttributessection) to avoid duplicate Sphinx object descriptions.Black, Ruff, and Mypy fixes have been made throughout the codebase.
Removed#
phylozoo.core.distance.operations(and the entireoperations.pyfile). 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 packagephysquirrel.phylozoo.core.quartet.qdistance.quartet_distance_with_partitionand 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.