Manual ====== `physquirrel` reconstructs level-1 phylogenetic networks from quartet data using the Squirrel algorithm. This guide covers the full workflow: preparing input data, running the algorithm, using convenience shortcuts, and working with the output network. Note that physquirrel is built on top of `PhyloZoo`; see the `PhyloZoo documentation `_ for details on working with the core datatypes (:class:`~phylozoo.core.network.sdnetwork.sd_phynetwork.SemiDirectedPhyNetwork`, :class:`~phylozoo.core.split.base.Split`, :class:`~phylozoo.core.quartet.base.Quartet`, :class:`~phylozoo.core.distance.base.DistanceMatrix`, and :class:`~phylozoo.core.sequence.base.MSA`). The main datatypes from phylozoo used in physquirrel are re-exported in the top-level namespace for convenience. The standard import convention used throughout is: .. code-block:: python import physquirrel as psq Input data ---------- physquirrel works with three input types, depending on how far upstream you want to start: .. list-table:: :widths: 30 30 40 :header-rows: 1 * - Input - Type - Entry point * - Quarnets (4-leaf subnetworks) - :class:`~physquirrel.datatypes.sqprofileset.SqQuartetProfileSet` - :func:`~physquirrel.algorithms.squirrel.squirrel` * - Pairwise distance matrix - :class:`~phylozoo.core.distance.base.DistanceMatrix` - :func:`~physquirrel.convenience.squirrel_from_distances` * - Multiple-sequence alignment - :class:`~phylozoo.core.sequence.base.MSA` - :func:`~physquirrel.convenience.squirrel_from_msa` From a set of quarnets ~~~~~~~~~~~~~~~~~~~~~~~ A quarnet is a 4-leaf subnetwork that can be either a split (tree-like) or a 4-cycle (network-like). In physquirrel, quarnets are represented by :class:`~physquirrel.datatypes.sqprofile.SqQuartetProfile` objects, which are then collected into a :class:`~physquirrel.datatypes.sqprofileset.SqQuartetProfileSet`. These objects build on top of phylozoo's :class:`~phylozoo.core.quartet.qprofile.QuartetProfile`: the standard object to represent quartets. For full control, construct an :class:`~physquirrel.datatypes.sqprofileset.SqQuartetProfileSet` from individual :class:`~physquirrel.datatypes.sqprofile.SqQuartetProfile` objects: .. code-block:: python import physquirrel as psq # A split quarnet: AB|CD q = psq.Quartet(psq.Split({'A', 'B'}, {'C', 'D'})) profile = psq.SqQuartetProfile([q]) # A cycle quarnet: two resolved quartets sharing four taxa q1 = psq.Quartet(psq.Split({'A', 'B'}, {'C', 'D'})) q2 = psq.Quartet(psq.Split({'A', 'D'}, {'B', 'C'})) cycle_profile = psq.SqQuartetProfile([q1, q2], reticulation_leaf='A') profileset = psq.SqQuartetProfileSet(profiles=[profile, cycle_profile]) Each :class:`~physquirrel.datatypes.sqprofile.SqQuartetProfile` holds exactly **1 or 2** resolved quartets on the same four taxa. A 2-quartet profile encodes a 4-cycle; an optional ``reticulation_leaf`` identifies which taxon is below the hybrid node. Quarnets can also be weighted by passing a ``profile_weight`` to the constructor; this is used in cycle resolution and scoring to downweight low-confidence profiles. From a distance matrix ~~~~~~~~~~~~~~~~~~~~~~~ The delta heuristic infers a profile set from pairwise distances, represented by a :class:`~phylozoo.core.distance.base.DistanceMatrix`. For each 4-taxon subset it computes a score :math:`\delta \in [0,1]`; below the threshold ``lam`` the quartet is classified as a split, otherwise as a cycle. .. code-block:: python import numpy as np import physquirrel as psq matrix = np.array([ [0.0, 0.1, 0.9, 0.9], [0.1, 0.0, 0.9, 0.9], [0.9, 0.9, 0.0, 0.1], [0.9, 0.9, 0.1, 0.0], ]) dm = psq.DistanceMatrix(matrix, labels=['A', 'B', 'C', 'D']) profileset = psq.delta_heuristic(dm, lam=0.3, weight=True) The ``weight`` parameter controls whether profile confidence (distance to the threshold) is stored as profile-level weights in the returned :class:`~physquirrel.datatypes.sqprofileset.SqQuartetProfileSet`. From an MSA ~~~~~~~~~~~ When starting from aligned sequences, these first need to be converted into a distance matrix. This can be done through phylozoo's :func:`~phylozoo.core.sequence.distances.hamming_distances` function, which computes pairwise Hamming distances between sequences in the MSA. The resulting distance matrix can then be passed to the delta heuristic as described above. Alternatively, physquirrel provides a convenience function that handles this internally: it computes Hamming distances and applies the delta heuristic in one call. Use the ``lam`` parameter to control the split/cycle classification threshold, and the optional ``weight`` parameter to store profile confidence as weights in the returned :class:`~physquirrel.datatypes.sqprofileset.SqQuartetProfileSet`. MSAs are represented by :class:`~phylozoo.core.sequence.base.MSA`. Both FASTA and NEXUS formats are supported. Load from a file with :meth:`~phylozoo.core.sequence.base.MSA.load`, or construct directly from a dictionary of sequences: .. code-block:: python import physquirrel as psq # From a FASTA file (.fasta, .fa, .fas) msa = psq.MSA.load("alignment.fasta") # From a NEXUS file (.nexus, .nex, .nxs) — specify format explicitly msa = psq.MSA.load("alignment.nex", format="nexus") # From a dictionary of sequences msa = psq.MSA({"A": "ACGT", "B": "ACGT", "C": "TGCA", "D": "TGCA"}) profileset = psq.delta_heuristic_from_msa(msa, lam=0.3) Running Squirrel ---------------- The main entry point is the function :func:`~physquirrel.algorithms.squirrel.squirrel`. It accepts an :class:`~physquirrel.datatypes.sqprofileset.SqQuartetProfileSet` (see previous section)and returns a :class:`~phylozoo.core.network.sdnetwork.sd_phynetwork.SemiDirectedPhyNetwork`: .. code-block:: python import physquirrel as psq network = psq.squirrel(profileset) print(f"eNewick string: {network.to_string()}") The pipeline runs internally: 1. **T\* tree** (:func:`~physquirrel.algorithms.tstar_tree.tstar_tree`) — B\*-compatible split system → initial tree. 2. **Adapted quartet joining** (:func:`~physquirrel.algorithms.qjoining.adapted_quartet_joining`) — refines the tree using quartet support. 3. **Unresolve tree** (:func:`~physquirrel.algorithms.unresolve_tree.unresolve_tree`) — iteratively contracts low-support splits, yielding a sequence of contracted trees from fully resolved to star. 4. **Cycle resolution** (:func:`~physquirrel.algorithms.cycle_resolution.resolve_cycles`) — inserts reticulation cycles at internal vertices of each contracted tree using a TSP-based circular ordering. 5. **Scoring** (:func:`~physquirrel.algorithms.qsimilarity.sqprofileset_similarity`) — each candidate network is scored by profile similarity (consistency with the input profile set); the best is returned. Rooting at an outgroup ~~~~~~~~~~~~~~~~~~~~~~ Pass ``outgroup`` to return a :class:`~phylozoo.core.network.dnetwork.base.DirectedPhyNetwork` rooted at the edge incident to the specified leaf: .. code-block:: python import physquirrel as psq rooted = psq.squirrel(profileset, outgroup='A') assert isinstance(rooted, psq.DirectedPhyNetwork) assert rooted.root_node is not None Convenience shortcuts ~~~~~~~~~~~~~~~~~~~~~ For common workflows, physquirrel provides one-call convenience functions that compose :func:`~physquirrel.algorithms.delta_heuristic.delta_heuristic` and :func:`~physquirrel.algorithms.squirrel.squirrel` internally. **From a distance matrix**: .. code-block:: python import physquirrel as psq network = psq.squirrel_from_distances(dm, lam=0.3, outgroup='A') **From an MSA**: .. code-block:: python import physquirrel as psq network = psq.squirrel_from_msa(msa, lam=0.3) All keyword arguments accepted by :func:`~physquirrel.algorithms.squirrel.squirrel` (e.g. ``rho``, ``tsp_threshold``) can be forwarded through both convenience functions. Parallelization ~~~~~~~~~~~~~~~~ The :func:`~physquirrel.algorithms.squirrel.squirrel` algorithm can be parallelized by handling the candidate trees in parallel during cycle resolution and scoring. This is controlled by the optional ``parallel`` argument, which accepts a `ParallelConfig` object from phylozoo. By default, the algorithm runs sequentially. **Threading** (shared memory, lower overhead, good for 2–4 cores): .. code-block:: python from phylozoo.utils.parallel import ParallelConfig, ParallelBackend import physquirrel as psq # Use threading with 4 worker threads parallel_config = ParallelConfig(backend=ParallelBackend.THREADING, n_jobs=4) network = psq.squirrel(profileset, parallel=parallel_config) **Multiprocessing** (true parallelism, higher overhead, good for 4+ cores): .. code-block:: python from phylozoo.utils.parallel import ParallelConfig, ParallelBackend import physquirrel as psq # Use multiprocessing with 8 worker processes parallel_config = ParallelConfig(backend=ParallelBackend.MULTIPROCESSING, n_jobs=8) network = psq.squirrel(profileset, parallel=parallel_config) **Sequential** (default, no parallelization): .. code-block:: python import physquirrel as psq # Omit the parallel argument or pass None network = psq.squirrel(profileset) # or explicitly: network = psq.squirrel(profileset, parallel=None) Notes: * Threading is recommended for most cases due to lower overhead; it works well for 2–4 cores. * Multiprocessing enables true parallelism but incurs higher overhead (serialization, process spawning); use it for large datasets or 4+ cores. * The number of jobs (``n_jobs``) should typically match your CPU core count. Use ``n_jobs=-1`` to automatically detect the number of available cores. I/O and plotting ----------------- Loading and saving quartet profile sets ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :class:`~physquirrel.datatypes.sqprofileset.SqQuartetProfileSet` supports two file formats. The class uses the :class:`~phylozoo.utils.io.IOMixin` interface from phylozoo, which infers the format from the file extension by default. Alternatively, the format can be specified explicitly when loading or saving. The four main PhyloZoo functions for I/O are: `save`, `to_string`, `load`, and `from_string`. **psq format** (``.psq``, default) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``psq`` format is a compact JSON file that stores all information losslessly: the full taxon set, per-profile quartet weights, reticulation leaf assignments, and profile-level confidence weights. It is the recommended format for archiving and exchanging profile sets between physquirrel runs. .. code-block:: python import physquirrel as psq # Infer a profile set from a distance matrix profileset = psq.delta_heuristic(dm, lam=0.3) # Save to file — format inferred from .psq extension profileset.save("profiles.psq") # Load back loaded = psq.SqQuartetProfileSet.load("profiles.psq") A ``profiles.psq`` file looks like: .. code-block:: json { "taxa": ["A", "B", "C", "D"], "profiles": [ { "taxa": ["A", "B", "C", "D"], "quartets": [ {"quartet": {"type": "resolved", "split": {"set1": ["A", "B"], "set2": ["C", "D"]}}, "weight": 1.0} ], "reticulation_leaf": null, "profile_weight": 0.85 } ] } **profile_list format** (``.txt``) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``profile_list`` format is a plain-text file with one profile per line. Each line begins with a type tag followed by four taxon names and an optional weight: .. code-block:: text # physquirrel profile list # Lines starting with '#' are comments and are ignored. SQ A B C D 0.85 # split quarnet: split AB|CD, confidence weight 0.85 4C A B C E 0.60 # 4-cycle: circular order A,B,C,E; A is the reticulation leaf - ``SQ a b c d [weight]`` — a split quarnet with split :math:`ab|cd`. - ``4C a b c d [weight]`` — a 4-cycle with circular order :math:`a,b,c,d` where :math:`a` is the reticulation leaf (below the hybrid node). The weight field is optional and defaults to 1.0. This format is compatible with the quarnet file format used in earlier versions of physquirrel. .. code-block:: python import physquirrel as psq # Save as profile_list profileset.save("profiles.txt", format="profile_list") # Or get the string directly txt = profileset.to_string(format="profile_list") print(txt) # Load back — format must be specified explicitly for .txt loaded = psq.SqQuartetProfileSet.load("profiles.txt", format="profile_list") loaded2 = psq.from_profile_list(txt) Saving networks ~~~~~~~~~~~~~~~ :class:`~phylozoo.core.network.sdnetwork.sd_phynetwork.SemiDirectedPhyNetwork` and :class:`~phylozoo.core.network.dnetwork.base.DirectedPhyNetwork` are provided by phylozoo and support the **eNewick** format (default) via the standard IOMixin interface: .. code-block:: python import physquirrel as psq # Serialize to an eNewick string enewick_str = network.to_string() # eNewick by default # Write directly to file (.nwk, .newick, .enewick, .enw) network.save("network.nwk") See the `PhyloZoo documentation `_ for the full list of supported formats and options. Visualization ~~~~~~~~~~~~~~ physquirrel networks can be plotted directly using phylozoo's unified :func:`~phylozoo.viz.plot` function, which dispatches by network type and uses :mod:`matplotlib`: .. code-block:: python from phylozoo.viz import plot plot(network) # semi-directed network, twopi layout plot(network, layout="pz-radial") # radial layout (trees only) plot(rooted_network) # :class:`~phylozoo.core.network.dnetwork.base.DirectedPhyNetwork`, DAG layout The ``viz`` extra dependency must be installed (``pip install physquirrel[viz]`` or ``pip install physquirrel[graphviz]``); see the :doc:`Installation Guide ` for details. Quick Links ----------- * :doc:`Quickstart ` — a quick walkthrough of the main workflow * :doc:`Installation Guide ` — requirements and install options * :doc:`API Reference ` — complete index of all functions and classes * `PhyloZoo Documentation `_ — core datatypes and utilities for working with networks, splits, quartets, distance matrices, and MSAs.