phytreon 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. phytreon-0.1.1/LICENSE +21 -0
  2. phytreon-0.1.1/PKG-INFO +438 -0
  3. phytreon-0.1.1/README.md +384 -0
  4. phytreon-0.1.1/phytreon/__init__.py +49 -0
  5. phytreon-0.1.1/phytreon/comparative/__init__.py +5 -0
  6. phytreon-0.1.1/phytreon/comparative/ace.py +223 -0
  7. phytreon-0.1.1/phytreon/comparative/stochastic_mapping.py +134 -0
  8. phytreon-0.1.1/phytreon/core/__init__.py +5 -0
  9. phytreon-0.1.1/phytreon/core/io.py +177 -0
  10. phytreon-0.1.1/phytreon/core/tree.py +243 -0
  11. phytreon-0.1.1/phytreon/datasets.py +48 -0
  12. phytreon-0.1.1/phytreon/infer/__init__.py +27 -0
  13. phytreon-0.1.1/phytreon/infer/_search.py +28 -0
  14. phytreon-0.1.1/phytreon/infer/align.py +299 -0
  15. phytreon-0.1.1/phytreon/infer/bootstrap.py +149 -0
  16. phytreon-0.1.1/phytreon/infer/distance.py +82 -0
  17. phytreon-0.1.1/phytreon/infer/ml.py +68 -0
  18. phytreon-0.1.1/phytreon/infer/ml_native.py +389 -0
  19. phytreon-0.1.1/phytreon/infer/parsimony.py +114 -0
  20. phytreon-0.1.1/phytreon/infer/pipeline.py +122 -0
  21. phytreon-0.1.1/phytreon/infer/trim.py +71 -0
  22. phytreon-0.1.1/phytreon/layout/__init__.py +37 -0
  23. phytreon-0.1.1/phytreon/layout/base.py +99 -0
  24. phytreon-0.1.1/phytreon/layout/circular.py +82 -0
  25. phytreon-0.1.1/phytreon/layout/rectangular.py +78 -0
  26. phytreon-0.1.1/phytreon/layout/unrooted.py +143 -0
  27. phytreon-0.1.1/phytreon/plot/__init__.py +14 -0
  28. phytreon-0.1.1/phytreon/plot/backends.py +403 -0
  29. phytreon-0.1.1/phytreon/plot/elements.py +854 -0
  30. phytreon-0.1.1/phytreon/plot/figure.py +333 -0
  31. phytreon-0.1.1/phytreon/plot/palettes.py +141 -0
  32. phytreon-0.1.1/phytreon/scene.py +147 -0
  33. phytreon-0.1.1/phytreon/treeops.py +318 -0
  34. phytreon-0.1.1/phytreon.egg-info/PKG-INFO +438 -0
  35. phytreon-0.1.1/phytreon.egg-info/SOURCES.txt +40 -0
  36. phytreon-0.1.1/phytreon.egg-info/dependency_links.txt +1 -0
  37. phytreon-0.1.1/phytreon.egg-info/requires.txt +16 -0
  38. phytreon-0.1.1/phytreon.egg-info/top_level.txt +1 -0
  39. phytreon-0.1.1/pyproject.toml +43 -0
  40. phytreon-0.1.1/setup.cfg +4 -0
  41. phytreon-0.1.1/tests/test_pipeline.py +179 -0
  42. phytreon-0.1.1/tests/test_smoke.py +231 -0
phytreon-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yiheng Du
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,438 @@
1
+ Metadata-Version: 2.4
2
+ Name: phytreon
3
+ Version: 0.1.1
4
+ Summary: Phylogenetic trees and publication figures in Python
5
+ Author-email: Yiheng Du <yiheng.du@slu.edu>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Yiheng Du
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/DeweyYihengDu/phytreon
29
+ Project-URL: Repository, https://github.com/DeweyYihengDu/phytreon
30
+ Project-URL: Documentation, https://deweyyihengdu.github.io/phytreon/
31
+ Project-URL: Issues, https://github.com/DeweyYihengDu/phytreon/issues
32
+ Project-URL: Changelog, https://github.com/DeweyYihengDu/phytreon/blob/main/CHANGELOG.md
33
+ Keywords: phylogenetics,phylogenetic-tree,tree,visualization,bioinformatics
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
37
+ Requires-Python: >=3.9
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: numpy>=1.22
41
+ Requires-Dist: scipy>=1.8
42
+ Requires-Dist: pandas>=1.4
43
+ Requires-Dist: matplotlib>=3.5
44
+ Requires-Dist: biopython>=1.79
45
+ Provides-Extra: interactive
46
+ Requires-Dist: plotly>=5.0; extra == "interactive"
47
+ Provides-Extra: dev
48
+ Requires-Dist: pytest>=7; extra == "dev"
49
+ Requires-Dist: plotly>=5.0; extra == "dev"
50
+ Provides-Extra: docs
51
+ Requires-Dist: mkdocs-material; extra == "docs"
52
+ Requires-Dist: mkdocstrings[python]; extra == "docs"
53
+ Dynamic: license-file
54
+
55
+ <p align="center">
56
+ <img src="assets/logo.svg" alt="phytreon logo" width="460">
57
+ </p>
58
+
59
+ <p align="center">
60
+ <b>Phylogenetic trees and publication-quality figures in Python.</b>
61
+ </p>
62
+
63
+ <p align="center">
64
+ phytreon combines tree inference, metadata-aware visualization, and
65
+ static/interactive figure export in a fluent, pure-Python workflow.
66
+ </p>
67
+
68
+ <p align="center">
69
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT license"></a>
70
+ <img src="https://img.shields.io/badge/python-3.9%2B-blue.svg" alt="Python 3.9+">
71
+ <a href="https://github.com/DeweyYihengDu/phytreon/actions/workflows/ci.yml"><img src="https://github.com/DeweyYihengDu/phytreon/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
72
+ <a href="https://github.com/DeweyYihengDu/phytreon/tags"><img src="https://img.shields.io/github/v/tag/DeweyYihengDu/phytreon?label=release&color=1F9E94" alt="Latest release"></a>
73
+ <a href="https://deweyyihengdu.github.io/phytreon/"><img src="https://img.shields.io/badge/docs-mkdocs--material-1F9E94.svg" alt="Documentation"></a>
74
+ <img src="https://img.shields.io/badge/pure-Python-1F9E94.svg" alt="Pure Python">
75
+ <img src="https://img.shields.io/badge/backends-matplotlib%20%2B%20plotly-11557c.svg" alt="matplotlib + plotly backends">
76
+ <img src="https://img.shields.io/badge/domain-bioinformatics-00BA38.svg" alt="Bioinformatics">
77
+ </p>
78
+
79
+ ### Why phytreon?
80
+
81
+ <table>
82
+ <tr>
83
+ <td width="33%"><b>🌿 Fluent <code>TreeFigure</code> builder</b><br><sub>Compose a figure by chaining visual layers onto a tree.</sub></td>
84
+ <td width="33%"><b>🎨 Static + interactive backends</b><br><sub>matplotlib for PDF/SVG/PNG, plotly for interactive HTML.</sub></td>
85
+ <td width="33%"><b>🧬 Sequence-to-tree pipeline</b><br><sub>One call: align → trim → infer → bootstrap.</sub></td>
86
+ </tr>
87
+ <tr>
88
+ <td><b>🧫 Metadata rings / heatmaps / tracks</b><br><sub>Annotate tips with rings, heatmaps, bars, alignments.</sub></td>
89
+ <td><b>🌳 ML / parsimony / NJ inference</b><br><sub>Pure-Python likelihood, parsimony, and distance trees.</sub></td>
90
+ <td><b>📄 Publication-ready exports</b><br><sub>Vector PDF/SVG, raster PNG, or interactive HTML.</sub></td>
91
+ </tr>
92
+ </table>
93
+
94
+ <p align="center">
95
+ <img src="assets/gallery/tree_of_life_circular.png" alt="Circular microbial tree with domain, phylum, and length metadata rings" width="850">
96
+ </p>
97
+
98
+ <p align="center">
99
+ <i>A circular 16S rRNA tree of common microbes (real NCBI data bundled in <code>examples/</code>),
100
+ with domain / phylum / length rings — built and drawn entirely in phytreon.</i>
101
+ </p>
102
+
103
+ ---
104
+
105
+ ## Quickstart
106
+
107
+ ```bash
108
+ pip install -e . # core (numpy, scipy, pandas, matplotlib, biopython)
109
+ pip install -e .[interactive] # + plotly (interactive HTML backend)
110
+ pip install -e .[dev] # + pytest, plotly
111
+ ```
112
+
113
+ ```python
114
+ import phytreon as pt
115
+
116
+ tr = pt.datasets.primates() # a small illustrative toy tree
117
+ meta = pt.datasets.primates_metadata().reset_index()
118
+ tr.join_data(meta, on="name") # attach metadata to tips
119
+
120
+ (pt.TreeFigure(tr) # skeleton drawn for you
121
+ .tip_points(color="habitat", size=9) # color mapped from metadata
122
+ .tip_labels()
123
+ .support_labels() # node support values
124
+ ).save("tree.pdf") # PDF/SVG/PNG -> matplotlib
125
+ # .save("tree.html") # HTML -> plotly (zoom/hover)
126
+ ```
127
+
128
+ The output backend is chosen from the file extension: `.pdf` / `.svg` / `.png`
129
+ render through matplotlib, `.html` renders an interactive plotly figure.
130
+
131
+ ---
132
+
133
+ ## Gallery
134
+
135
+ Every figure below is produced by a script in [`examples/`](examples/)
136
+ (regenerate with `python examples/<name>.py`).
137
+
138
+ <table>
139
+ <tr>
140
+ <td width="50%">
141
+ <img src="assets/gallery/rectangular.png" alt="Rectangular phylogram with domain-colored tips and bootstrap support"><br>
142
+ <b>Rectangular</b><br>
143
+ <sub>16S tree of life, tips colored by domain, bootstrap support (200 reps) — <code>tree_of_life_demo.py</code></sub>
144
+ </td>
145
+ <td width="50%">
146
+ <img src="assets/gallery/annotated_circular.png" alt="Annotated circular tree with lineage-colored branches, shaped tips, and rings"><br>
147
+ <b>Annotated circular</b><br>
148
+ <sub>Lineage-colored branches, shaped tips, tile + bar rings, three legends — <code>showcase_circular.py</code></sub>
149
+ </td>
150
+ </tr>
151
+ <tr>
152
+ <td width="50%">
153
+ <img src="assets/gallery/tracks.png" alt="Tree with stacked categorical tile tracks and a numeric bar track"><br>
154
+ <b>Aligned tracks</b><br>
155
+ <sub>Stacked categorical tile tracks plus a numeric bar track — <code>tracks_demo.py</code></sub>
156
+ </td>
157
+ <td width="50%">
158
+ <img src="assets/gallery/alignment_track.png" alt="Tree beside a residue-colored multiple sequence alignment raster"><br>
159
+ <b>Alignment track</b><br>
160
+ <sub>The multiple-sequence alignment as a residue raster — <code>tracks_demo.py</code></sub>
161
+ </td>
162
+ </tr>
163
+ </table>
164
+
165
+ ---
166
+
167
+ ## From sequences to a tree
168
+
169
+ <p align="center">
170
+ <img src="assets/pipeline.png" alt="phytreon pipeline from FASTA sequences through align, trim, infer, bootstrap, into a TreeFigure, then exported as pdf, svg, png, or html" width="950">
171
+ </p>
172
+
173
+ One configurable call runs **align → trim → infer → bootstrap**, each stage
174
+ opt-in and fully parameterized:
175
+
176
+ ```python
177
+ tree = pt.build_tree(
178
+ "seqs.fasta", # path, list of (name, seq), or Alignment
179
+ aligner="builtin", # pure-Python MSA (or "mafft"/"muscle"/"none")
180
+ align_kw=dict(match=2, gap=-3),
181
+ trim_kw=dict(max_gap=0.4, min_occupancy=0.5, min_conservation=0.3),
182
+ method="nj", # "nj" | "upgma" | "ml" | "parsimony"
183
+ root="midpoint",
184
+ bootstrap=200, # bipartition support
185
+ )
186
+ # distances are JC69-corrected by default (dist_model="jc69"|"k2p"|"raw");
187
+ # negative NJ branch lengths are clamped to 0.
188
+
189
+ # maximum likelihood (pure Python), HKY85 + Γ4 rate variation + NNI:
190
+ ml = pt.build_tree("seqs.fasta", method="ml", ml_model="HKY85", ml_gamma=4,
191
+ bootstrap=100) # bootstrap works for nj/ml/parsimony
192
+ print(ml.data["logL"], ml.data["AIC"], ml.data["gamma_shape"])
193
+ pt.model_finder("seqs.fasta") # rank JC/K80/HKY/GTR ±G by AIC
194
+ # large datasets -> external engines: build_tree(..., method="ml", ml_engine="iqtree")
195
+ ```
196
+
197
+ Each step is also usable on its own: `pt.align`, `pt.trim`,
198
+ `pt.neighbor_joining`, `pt.bootstrap_support`, `pt.infer_ml`,
199
+ `pt.parsimony_tree`.
200
+
201
+ ---
202
+
203
+ ## What phytreon includes
204
+
205
+ | Area | Capabilities |
206
+ |---|---|
207
+ | **I/O & data model** | Newick / Nexus / PhyloXML read-write; metadata joins (`Tree.join_data`) |
208
+ | **Layouts** | rectangular, slanted, dendrogram, circular, fan, radial, inward-circular, unrooted (equal-angle / equal-daylight) |
209
+ | **Inference** | NJ, UPGMA (model-corrected distances), ML (JC69/K80/HKY85/GTR, +Γ, NNI, AIC/BIC), parsimony, bootstrap, built-in MSA, trimming |
210
+ | **Comparative** | ancestral states (parsimony / Mk-ML ER·SYM·ARD / Brownian), stochastic mapping, painted branches, node pies |
211
+ | **Figure tracks** | tip / node / support labels, tip points, metadata rings, heatmaps, bar tracks, alignment rasters |
212
+ | **Tree operations** | rotate, flip, ladderize, collapse, scale clade, midpoint root, cut tree, Robinson-Foulds |
213
+
214
+ ---
215
+
216
+ ## The `TreeFigure` builder
217
+
218
+ `TreeFigure` starts from a tree skeleton and lets you compose visual layers
219
+ fluently — every method returns the figure, so calls chain.
220
+
221
+ | Method | Draws |
222
+ |---|---|
223
+ | `.branches(color=, size=)` | the tree skeleton (e.g. color by lineage) |
224
+ | `.tip_labels()` / `.node_labels()` / `.support_labels()` | text labels |
225
+ | `.tip_points()` / `.node_points()` / `.points()` | markers (color / size / shape mapping) |
226
+ | `.highlight(node=)` / `.clade_label(...)` | shade / bracket a clade |
227
+ | `.heatmap(df)` | a matrix of cells aligned to the tips (rectangular) |
228
+ | `.ring(df, columns=…)` | concentric metadata rings (circular), tile or bar |
229
+ | `.bar_track(df, col)` | a horizontal bar track |
230
+ | `.alignment(aln)` | a residue-colored MSA raster |
231
+ | `.painted_branches()` | branches painted by stochastic-map state |
232
+ | `.node_pies()` | ancestral-state pies at internal nodes |
233
+ | `.time_axis(geo=True)` | a time / geological-period axis |
234
+
235
+ Continuous columns get a colorbar, categorical ones a legend; tracks, labels,
236
+ and legends are placed so nothing overlaps. Layouts: `rectangular`, `slanted`,
237
+ `dendrogram`, `circular`, `fan`, `radial`, `inward_circular`,
238
+ `unrooted` / `daylight` (equal-daylight), `equal_angle`.
239
+
240
+ ---
241
+
242
+ ## Architecture
243
+
244
+ The single design decision that makes the dual backend work: **layout and
245
+ rendering are completely decoupled.** A layout computes *final cartesian
246
+ coordinates* and emits backend-agnostic primitives; matplotlib and plotly are
247
+ "dumb" renderers that only translate those primitives. Adding a backend means
248
+ writing one translator — nothing in the phylogenetic logic changes.
249
+
250
+ | Module | Responsibility |
251
+ |---|---|
252
+ | `core` | `Tree` / `Node` data model + I/O |
253
+ | `layout` | topology → display coordinates |
254
+ | `scene` | `Path` / `Marker` / `Label` / `Polygon` primitives |
255
+ | `plot` | `TreeFigure` builder + matplotlib / plotly backends |
256
+ | `infer` | alignment / trimming / NJ / ML / parsimony / bootstrap |
257
+ | `comparative` | ancestral states + stochastic mapping |
258
+
259
+ ---
260
+
261
+ ## Comparison to other Python tools
262
+
263
+ | | phytreon | ete3 | toytree | Bio.Phylo | dendropy |
264
+ |---|:---:|:---:|:---:|:---:|:---:|
265
+ | Fluent figure builder | ✅ | ✗ | partial | ✗ | ✗ |
266
+ | Static **and** interactive backend | ✅ mpl + plotly | own GUI / SVG | toyplot | basic mpl | ✗ |
267
+ | Annotation tracks (heatmap / rings / MSA / bars) | ✅ | partial | partial | ✗ | ✗ |
268
+ | Built-in ML (+Γ) / parsimony | ✅ pure-Python | ✗ | ✗ | ✗ | ✗ |
269
+ | Comparative (ancestral states / stochastic map) | ✅ | ✗ | ✗ | ✗ | partial |
270
+ | Pure Python, pip-installable | ✅ | ✅ (Qt for GUI) | ✅ | ✅ | ✅ |
271
+
272
+ phytreon's niche is a fluent figure builder plus a self-contained phylogenetics
273
+ stack, with optional external tools for large-scale alignments or rigorous
274
+ large-scale ML (`aligner="mafft"`, `ml_engine="iqtree"`).
275
+
276
+ ---
277
+
278
+ ## Validation
279
+
280
+ `validation/validate.py` checks the core algorithms in **pure Python** (no
281
+ external tools):
282
+
283
+ - the likelihood engine (pattern-compressed, rescaled) matches an independent
284
+ naive re-implementation to **machine precision** (|Δ| ≈ 1e-13);
285
+ - **neighbor-joining recovers a tree exactly from its own additive (patristic)
286
+ distances** (Robinson-Foulds = 0, via `pt.robinson_foulds`) — the defining
287
+ guarantee of NJ;
288
+ - ML recovers a known clade and reports a finite logL / AIC.
289
+
290
+ ---
291
+
292
+ ## More
293
+
294
+ <details>
295
+ <summary><b>Reshaping trees (move branches freely)</b></summary>
296
+
297
+ ```python
298
+ pt.ladderize(tree) # tidy ordering
299
+ pt.rotate(tree, node) # flip a clade's vertical order
300
+ pt.flip(tree, node_a, node_b) # swap two clades' positions
301
+ pt.collapse_low_support(tree, 70) # weak edges -> polytomies
302
+ pt.scale_clade(tree, node, 0.5) # de-emphasize a clade's branch lengths
303
+ pt.midpoint_root(tree) # root an unrooted (NJ) tree
304
+ clusters = pt.cut_tree(tree, k=4) # {tip_name: cluster_id}
305
+ ```
306
+
307
+ Because layout derives tip rows from child order, `rotate` / `flip` are exactly
308
+ how you nudge branches up and down on the plot.
309
+ </details>
310
+
311
+ <details>
312
+ <summary><b>Comparative & time-scaled trees</b></summary>
313
+
314
+ ```python
315
+ pt.stochastic_map(tree, trait, n=200) # stochastic character mapping
316
+ (pt.TreeFigure(tree).painted_branches() # branches painted by inferred state
317
+ .tip_labels())
318
+
319
+ (pt.TreeFigure(dated_tree) # branch lengths = time
320
+ .time_axis(geo=True, gridlines=True, unit="Mya") # geological bands
321
+ .tip_labels())
322
+ ```
323
+ </details>
324
+
325
+ <details>
326
+ <summary><b>Circular tree with metadata rings</b></summary>
327
+
328
+ ```python
329
+ (pt.TreeFigure(tree, layout="circular", extent=320)
330
+ .ring(meta_df, # DataFrame indexed by tip name
331
+ columns=["habitat", "diet", "body_mass_kg"],
332
+ width=0.13, gap=0.03) # each column -> one ring
333
+ .tip_labels()
334
+ ).save("rings.png")
335
+ ```
336
+
337
+ Rings stack outward (categorical → palette, numeric → gradient, each with its
338
+ own legend); tip labels are pushed outside all rings automatically, so the
339
+ tree, rings, labels, and legends never overlap.
340
+ </details>
341
+
342
+ <details>
343
+ <summary><b>Colors</b></summary>
344
+
345
+ Categorical aesthetics use an HCL **hue-wheel** palette by default (for 3
346
+ levels: a balanced `#F8766D #00BA38 #619CFF`); continuous aesthetics use a
347
+ dark-blue → light-blue gradient. Override per element:
348
+
349
+ ```python
350
+ fig.tip_points(color="habitat", palette="dark2") # hue | set2 | dark2 | tab10
351
+ fig.heatmap(mat, cmap="viridis") # name, or ("#fff","#c00") gradient
352
+ ```
353
+
354
+ See `phytreon/plot/palettes.py` (`hue_palette`, `lerp_color`).
355
+ </details>
356
+
357
+ <details>
358
+ <summary><b>Environment notes</b></summary>
359
+
360
+ - **Static figures** use matplotlib (`.save("x.pdf"/".png"/".svg")`) and are
361
+ always reliable. **Interactive** output uses plotly (`.save("x.html")`).
362
+ - plotly's static PNG export (kaleido) is flaky on some Windows setups; pin
363
+ `kaleido==0.2.1` for plotly 5, or just use matplotlib for static figures and
364
+ HTML for interactivity.
365
+ - The plotly backend's legends are click-to-toggle traces and track placement
366
+ is heuristic (it cannot measure text); the matplotlib backend is the reference
367
+ for exact, publication-quality layout.
368
+ </details>
369
+
370
+ <details>
371
+ <summary><b>Caveats</b></summary>
372
+
373
+ - The **built-in aligner** is a single-pass progressive aligner (linear gaps);
374
+ fine for small/medium inputs, but use MAFFT (`aligner="mafft"`) for
375
+ publication alignments.
376
+ - **Native ML / parsimony assume binary trees**; NNI search skips polytomies
377
+ (resolve them first if needed). Pure-Python ML / MSA target tens of taxa —
378
+ see `benchmark/`.
379
+ </details>
380
+
381
+ <details>
382
+ <summary><b>Extending</b></summary>
383
+
384
+ - **New layout**: subclass `phytreon.layout.base.Layout`, implement `compute()`
385
+ (write `node.x` / `node.y`), `branch_path()`, `child_connector()`, and
386
+ register it in `phytreon/layout/__init__.py::LAYOUTS`. The builder and both
387
+ backends pick it up automatically.
388
+ - **New element**: subclass `phytreon.plot.figure._Element`, implement
389
+ `apply(ctx)` to read coordinates and append `scene` primitives (use
390
+ `ctx.resolve_color(...)` for metadata-driven aesthetics + legends), then add
391
+ it with `TreeFigure.add(...)`.
392
+ </details>
393
+
394
+ <details>
395
+ <summary><b>Example data (real, from NCBI)</b></summary>
396
+
397
+ `examples/data/` ships a small **microbial "tree of life" 16S rRNA** set
398
+ downloaded from NCBI — common model organisms and type strains across the
399
+ major bacterial phyla plus four archaea (a natural outgroup) — so the whole
400
+ pipeline runs on real, public sequences:
401
+
402
+ ```
403
+ examples/data/tol_16S.fasta 18 unaligned 16S sequences (mostly RefSeq NR_*)
404
+ examples/data/tol_16S_aligned.fasta built-in MSA of the above (cached)
405
+ examples/data/tol_metadata.csv domain / phylum / organism / accession / length
406
+ examples/data/fetch_example_data.py re-download script (Entrez)
407
+ ```
408
+
409
+ An NJ/ML tree on this set recovers the four archaea as a monophyletic clade
410
+ (deep bacterial splits from a single 16S gene are, as expected, only weakly
411
+ supported). Accession versions may advance over time, so re-downloading need
412
+ not byte-match the shipped snapshot; the cached alignment keeps the examples
413
+ reproducible offline. Full accession list and license: `examples/data/SOURCES.md`.
414
+ </details>
415
+
416
+ ---
417
+
418
+ ## Examples & tests
419
+
420
+ ```bash
421
+ python examples/demo.py # rect / circular / heatmap / nj / ancestral
422
+ python examples/pipeline_demo.py # raw sequences -> align -> trim -> NJ -> bootstrap
423
+ python examples/tree_of_life_demo.py # real 16S -> tree + circular metadata rings
424
+ python examples/showcase_circular.py # lineage colors + tile + bar rings + shapes
425
+ python examples/tracks_demo.py # rectangular tile / bar tracks + alignment track
426
+ python examples/ml_demo.py # native pure-Python ML tree (HKY85)
427
+ python validation/validate.py # pure-Python correctness checks
428
+ python benchmark/benchmark.py # timings + validated-core guidance
429
+ pytest -q # 37 tests
430
+
431
+ # docs: pip install mkdocs-material mkdocstrings[python]; mkdocs serve
432
+ ```
433
+
434
+ ---
435
+
436
+ <p align="center">
437
+ <sub>MIT licensed · Built for reproducible phylogenetic visualization in Python.</sub>
438
+ </p>