phykit 2.1.85__tar.gz → 2.1.88__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 (148) hide show
  1. {phykit-2.1.85 → phykit-2.1.88}/PKG-INFO +1 -1
  2. {phykit-2.1.85 → phykit-2.1.88}/phykit/cli_registry.py +3 -0
  3. phykit-2.1.88/phykit/helpers/geological_timescale.py +119 -0
  4. {phykit-2.1.85 → phykit-2.1.88}/phykit/phykit.py +84 -0
  5. {phykit-2.1.85 → phykit-2.1.88}/phykit/service_factories.py +1 -0
  6. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/__init__.py +1 -0
  7. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/ancestral_reconstruction.py +3 -3
  8. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/base.py +20 -2
  9. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/branch_length_multiplier.py +2 -2
  10. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/character_map.py +2 -2
  11. phykit-2.1.88/phykit/services/tree/chronogram.py +661 -0
  12. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/collapse_branches.py +2 -2
  13. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/concordance_asr.py +159 -3
  14. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/cont_map.py +2 -2
  15. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/independent_contrasts.py +2 -2
  16. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/internode_labeler.py +2 -2
  17. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/last_common_ancestor_subtree.py +2 -2
  18. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/ou_shift_detection.py +2 -2
  19. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/ouwie.py +2 -2
  20. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/parsimony_score.py +2 -2
  21. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/phenogram.py +2 -2
  22. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/phylo_logistic.py +2 -2
  23. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/phylogenetic_ordination.py +2 -2
  24. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/phylomorphospace.py +2 -2
  25. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/polytomy_test.py +12 -4
  26. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/print_tree.py +2 -2
  27. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/prune_tree.py +2 -2
  28. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/rate_heterogeneity.py +2 -2
  29. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/rename_tree_tips.py +2 -2
  30. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/root_tree.py +2 -2
  31. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/spectral_discordance.py +3 -3
  32. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/spr.py +2 -2
  33. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/trait_rate_map.py +2 -2
  34. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/tree_space.py +2 -2
  35. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/vcv_utils.py +2 -2
  36. phykit-2.1.88/phykit/version.py +1 -0
  37. {phykit-2.1.85 → phykit-2.1.88}/phykit.egg-info/PKG-INFO +1 -1
  38. {phykit-2.1.85 → phykit-2.1.88}/phykit.egg-info/SOURCES.txt +2 -0
  39. {phykit-2.1.85 → phykit-2.1.88}/phykit.egg-info/entry_points.txt +3 -0
  40. phykit-2.1.85/phykit/version.py +0 -1
  41. {phykit-2.1.85 → phykit-2.1.88}/LICENSE.md +0 -0
  42. {phykit-2.1.85 → phykit-2.1.88}/README.md +0 -0
  43. {phykit-2.1.85 → phykit-2.1.88}/phykit/__init__.py +0 -0
  44. {phykit-2.1.85 → phykit-2.1.88}/phykit/__main__.py +0 -0
  45. {phykit-2.1.85 → phykit-2.1.88}/phykit/errors.py +0 -0
  46. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/__init__.py +0 -0
  47. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/boolean_argument_parsing.py +0 -0
  48. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/caching.py +0 -0
  49. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/circular_layout.py +0 -0
  50. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/color_annotations.py +0 -0
  51. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/discrete_models.py +0 -0
  52. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/files.py +0 -0
  53. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/json_output.py +0 -0
  54. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/parallel.py +0 -0
  55. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/parsimony_utils.py +0 -0
  56. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/pgls_utils.py +0 -0
  57. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/plot_config.py +0 -0
  58. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/quartet_utils.py +0 -0
  59. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/stats_summary.py +0 -0
  60. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/streaming.py +0 -0
  61. {phykit-2.1.85 → phykit-2.1.88}/phykit/helpers/trait_parsing.py +0 -0
  62. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/__init__.py +0 -0
  63. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/__init__.py +0 -0
  64. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/alignment_entropy.py +0 -0
  65. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/alignment_length.py +0 -0
  66. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  67. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  68. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/alignment_recoding.py +0 -0
  69. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/alignment_subsample.py +0 -0
  70. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/base.py +0 -0
  71. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/column_score.py +0 -0
  72. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/composition_per_taxon.py +0 -0
  73. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  74. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  75. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/dfoil.py +0 -0
  76. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/dna_threader.py +0 -0
  77. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/dstatistic.py +0 -0
  78. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  79. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/faidx.py +0 -0
  80. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/gc_content.py +0 -0
  81. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/identity_matrix.py +0 -0
  82. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/mask_alignment.py +0 -0
  83. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/occupancy_filter.py +0 -0
  84. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  85. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/pairwise_identity.py +0 -0
  86. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  87. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/phylo_gwas.py +0 -0
  88. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  89. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/rcv.py +0 -0
  90. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/rcvt.py +0 -0
  91. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  92. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  93. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/taxon_groups.py +0 -0
  94. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/alignment/variable_sites.py +0 -0
  95. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/base.py +0 -0
  96. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/bipartition_support_stats.py +0 -0
  97. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/consensus_network.py +0 -0
  98. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/consensus_tree.py +0 -0
  99. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/cophylo.py +0 -0
  100. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  101. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/density_map.py +0 -0
  102. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/discordance_asymmetry.py +0 -0
  103. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/dvmc.py +0 -0
  104. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/evo_tempo_map.py +0 -0
  105. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/evolutionary_rate.py +0 -0
  106. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/fit_continuous.py +0 -0
  107. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/fit_discrete.py +0 -0
  108. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  109. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/hybridization.py +0 -0
  110. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/internal_branch_stats.py +0 -0
  111. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/kf_distance.py +0 -0
  112. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/lb_score.py +0 -0
  113. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/ltt.py +0 -0
  114. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/monophyly_check.py +0 -0
  115. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  116. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/neighbor_net.py +0 -0
  117. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/network_signal.py +0 -0
  118. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/patristic_distances.py +0 -0
  119. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/phylo_anova.py +0 -0
  120. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/phylo_heatmap.py +0 -0
  121. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/phylo_impute.py +0 -0
  122. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/phylo_path.py +0 -0
  123. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/phylogenetic_glm.py +0 -0
  124. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/phylogenetic_regression.py +0 -0
  125. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/phylogenetic_signal.py +0 -0
  126. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/quartet_network.py +0 -0
  127. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/quartet_pie.py +0 -0
  128. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/relative_rate_test.py +0 -0
  129. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/rf_distance.py +0 -0
  130. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/saturation.py +0 -0
  131. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/simmap_summary.py +0 -0
  132. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/spurious_sequence.py +0 -0
  133. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/stochastic_character_map.py +0 -0
  134. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/terminal_branch_stats.py +0 -0
  135. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/threshold_model.py +0 -0
  136. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/tip_labels.py +0 -0
  137. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  138. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  139. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/total_tree_length.py +0 -0
  140. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/trait_correlation.py +0 -0
  141. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/transfer_annotations.py +0 -0
  142. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/treeness.py +0 -0
  143. {phykit-2.1.85 → phykit-2.1.88}/phykit/services/tree/treeness_over_rcv.py +0 -0
  144. {phykit-2.1.85 → phykit-2.1.88}/phykit.egg-info/dependency_links.txt +0 -0
  145. {phykit-2.1.85 → phykit-2.1.88}/phykit.egg-info/requires.txt +0 -0
  146. {phykit-2.1.85 → phykit-2.1.88}/phykit.egg-info/top_level.txt +0 -0
  147. {phykit-2.1.85 → phykit-2.1.88}/setup.cfg +0 -0
  148. {phykit-2.1.85 → phykit-2.1.88}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.85
3
+ Version: 2.1.88
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -70,6 +70,9 @@ ALIAS_TO_HANDLER: Dict[str, str] = {
70
70
  "blm": "branch_length_multiplier",
71
71
  "collapse": "collapse_branches",
72
72
  "cb": "collapse_branches",
73
+ "chronogram": "chronogram",
74
+ "chrono": "chronogram",
75
+ "time_tree": "chronogram",
73
76
  "cover": "covarying_evolutionary_rates",
74
77
  "consnet": "consensus_network",
75
78
  "splitnet": "consensus_network",
@@ -0,0 +1,119 @@
1
+ """
2
+ Geological timescale data (ICS 2024).
3
+
4
+ Provides era, period, and epoch boundaries for plotting chronograms
5
+ with geological time bands.
6
+ """
7
+
8
+ # Muted pastel color palette for geological epochs
9
+ # Inspired by ICS colors but softened for publication figures
10
+ EPOCH_COLORS = {
11
+ "Holocene": "#FEF2CC",
12
+ "Pleistocene": "#FFF2AE",
13
+ "Pliocene": "#FFFF99",
14
+ "Miocene": "#FFFF00",
15
+ "Oligocene": "#FDC07A",
16
+ "Eocene": "#FDB46C",
17
+ "Paleocene": "#FDA75F",
18
+ "Late Cretaceous": "#A6D84A",
19
+ "Early Cretaceous": "#8CCD57",
20
+ "Late Jurassic": "#B3E1EB",
21
+ "Middle Jurassic": "#80CFD8",
22
+ "Early Jurassic": "#42B2D0",
23
+ "Late Triassic": "#BD8CC2",
24
+ "Middle Triassic": "#B07FB7",
25
+ "Early Triassic": "#A372AB",
26
+ }
27
+
28
+ PERIOD_COLORS = {
29
+ "Quaternary": "#F9F97F",
30
+ "Neogene": "#FFE619",
31
+ "Paleogene": "#FD9A52",
32
+ "Cretaceous": "#7FC64E",
33
+ "Jurassic": "#34B2C9",
34
+ "Triassic": "#812B92",
35
+ "Permian": "#F04028",
36
+ "Carboniferous": "#67A599",
37
+ "Devonian": "#CB8C37",
38
+ "Silurian": "#B3E1B6",
39
+ "Ordovician": "#009270",
40
+ "Cambrian": "#7FA056",
41
+ }
42
+
43
+ ERA_COLORS = {
44
+ "Cenozoic": "#F2F91D",
45
+ "Mesozoic": "#67C5CA",
46
+ "Paleozoic": "#99C08D",
47
+ }
48
+
49
+ EPOCHS = [
50
+ ("Holocene", 0.0117, 0),
51
+ ("Pleistocene", 2.58, 0.0117),
52
+ ("Pliocene", 5.333, 2.58),
53
+ ("Miocene", 23.03, 5.333),
54
+ ("Oligocene", 33.9, 23.03),
55
+ ("Eocene", 56.0, 33.9),
56
+ ("Paleocene", 66.0, 56.0),
57
+ ("Late Cretaceous", 100.5, 66.0),
58
+ ("Early Cretaceous", 145.0, 100.5),
59
+ ("Late Jurassic", 163.5, 145.0),
60
+ ("Middle Jurassic", 174.7, 163.5),
61
+ ("Early Jurassic", 201.4, 174.7),
62
+ ("Late Triassic", 237.0, 201.4),
63
+ ("Middle Triassic", 247.2, 237.0),
64
+ ("Early Triassic", 251.902, 247.2),
65
+ ]
66
+
67
+ PERIODS = [
68
+ ("Quaternary", 2.58, 0),
69
+ ("Neogene", 23.03, 2.58),
70
+ ("Paleogene", 66.0, 23.03),
71
+ ("Cretaceous", 145.0, 66.0),
72
+ ("Jurassic", 201.4, 145.0),
73
+ ("Triassic", 251.902, 201.4),
74
+ ("Permian", 298.9, 251.902),
75
+ ("Carboniferous", 358.9, 298.9),
76
+ ("Devonian", 419.2, 358.9),
77
+ ("Silurian", 443.8, 419.2),
78
+ ("Ordovician", 485.4, 443.8),
79
+ ("Cambrian", 538.8, 485.4),
80
+ ]
81
+
82
+ ERAS = [
83
+ ("Cenozoic", 66.0, 0),
84
+ ("Mesozoic", 251.902, 66.0),
85
+ ("Paleozoic", 538.8, 251.902),
86
+ ]
87
+
88
+
89
+ def get_timescale_for_range(root_age, level="auto"):
90
+ """Return appropriate timescale intervals for a given root age.
91
+
92
+ Parameters
93
+ ----------
94
+ root_age : float, Ma
95
+ level : "epoch", "period", "era", or "auto"
96
+
97
+ Returns
98
+ -------
99
+ list of (name, start_ma, end_ma), color_dict
100
+ """
101
+ if level == "auto":
102
+ if root_age <= 66:
103
+ level = "epoch"
104
+ elif root_age <= 252:
105
+ level = "period"
106
+ else:
107
+ level = "era"
108
+
109
+ if level == "epoch":
110
+ intervals = [(n, s, e) for n, s, e in EPOCHS if s > 0 and e < root_age * 1.1]
111
+ # Include any epoch that overlaps the tree's range
112
+ intervals = [(n, s, e) for n, s, e in EPOCHS if e < root_age * 1.05]
113
+ return intervals, EPOCH_COLORS
114
+ elif level == "period":
115
+ intervals = [(n, s, e) for n, s, e in PERIODS if e < root_age * 1.05]
116
+ return intervals, PERIOD_COLORS
117
+ else:
118
+ intervals = [(n, s, e) for n, s, e in ERAS if e < root_age * 1.05]
119
+ return intervals, ERA_COLORS
@@ -192,6 +192,8 @@ class Phykit:
192
192
  concordance_asr (alias: conc_asr; casr)
193
193
  - concordance-aware ancestral state reconstruction
194
194
  incorporating gene tree discordance
195
+ chronogram (alias: chrono; time_tree)
196
+ - plot a time-calibrated tree with geological timescale
195
197
  bipartition_support_stats (alias: bss)
196
198
  - calculates summary statistics for bipartition support
197
199
  branch_length_multiplier (alias: blm)
@@ -2825,6 +2827,14 @@ class Phykit:
2825
2827
  --plot output path for concordance
2826
2828
  ASR plot
2827
2829
 
2830
+ --plot-uncertainty output path for uncertainty
2831
+ plot showing the distribution
2832
+ of ancestral estimates across
2833
+ gene trees (distribution
2834
+ method) or concordance
2835
+ sources (weighted method)
2836
+ as violin + boxplots
2837
+
2828
2838
  --missing-taxa how to handle taxa mismatches:
2829
2839
  shared (default) or error
2830
2840
 
@@ -2902,6 +2912,10 @@ class Phykit:
2902
2912
  "--plot", type=str, required=False, default=None,
2903
2913
  help=SUPPRESS, metavar=""
2904
2914
  )
2915
+ parser.add_argument(
2916
+ "--plot-uncertainty", type=str, required=False, default=None,
2917
+ help=SUPPRESS, metavar=""
2918
+ )
2905
2919
  parser.add_argument(
2906
2920
  "--missing-taxa", type=str, required=False, default="shared",
2907
2921
  choices=["error", "shared"], help=SUPPRESS, metavar=""
@@ -2910,6 +2924,72 @@ class Phykit:
2910
2924
  _add_json_argument(parser)
2911
2925
  _run_service(parser, argv, ConcordanceAsr)
2912
2926
 
2927
+ @staticmethod
2928
+ def chronogram(argv):
2929
+ parser = _new_parser(
2930
+ description=textwrap.dedent(
2931
+ f"""\
2932
+ {help_header}
2933
+
2934
+ Plot a chronogram (time-calibrated phylogeny) with
2935
+ geological timescale bands. Requires an ultrametric
2936
+ tree and the root age in millions of years (Ma).
2937
+
2938
+ Geological epoch/period/era bands are drawn behind
2939
+ the tree as colored stripes, with labels along the
2940
+ top. The time axis runs from past (left) to present
2941
+ (right).
2942
+
2943
+ Aliases:
2944
+ chronogram, chrono, time_tree
2945
+ Command line interfaces:
2946
+ pk_chronogram, pk_chrono, pk_time_tree
2947
+
2948
+ Usage:
2949
+ phykit chronogram -t <tree> --root-age <float>
2950
+ --plot-output <file> [--timescale auto|epoch|period|era]
2951
+ [--node-ages] [--circular] [--ladderize]
2952
+ [--color-file <file>] [--json]
2953
+
2954
+ Options
2955
+ =====================================================
2956
+ -t/--tree ultrametric tree file
2957
+ (required)
2958
+
2959
+ --root-age age of the root in Ma
2960
+ (required)
2961
+
2962
+ --plot-output output figure path
2963
+ (required; .png, .pdf, .svg)
2964
+
2965
+ --timescale timescale level: auto
2966
+ (default), epoch, period,
2967
+ or era. Auto selects based
2968
+ on root age.
2969
+
2970
+ --node-ages label internal nodes with
2971
+ divergence times (Ma)
2972
+
2973
+ --circular draw circular chronogram
2974
+
2975
+ --ladderize ladderize the tree
2976
+
2977
+ --color-file color annotation file
2978
+ (iTOL-inspired TSV)
2979
+
2980
+ --json output node ages as JSON
2981
+ """
2982
+ ),
2983
+ )
2984
+ parser.add_argument("-t", "--tree", type=str, required=True, help=SUPPRESS, metavar="")
2985
+ parser.add_argument("--root-age", type=float, required=True, help=SUPPRESS, metavar="")
2986
+ parser.add_argument("--plot-output", type=str, required=True, help=SUPPRESS, metavar="")
2987
+ parser.add_argument("--timescale", type=str, default="auto", choices=["auto", "epoch", "period", "era"], help=SUPPRESS, metavar="")
2988
+ parser.add_argument("--node-ages", action="store_true", help=SUPPRESS)
2989
+ add_plot_arguments(parser)
2990
+ _add_json_argument(parser)
2991
+ _run_service(parser, argv, Chronogram)
2992
+
2913
2993
  @staticmethod
2914
2994
  def bipartition_support_stats(argv):
2915
2995
  parser = _new_parser(
@@ -9057,6 +9137,10 @@ def concordance_asr(argv=None):
9057
9137
  Phykit.concordance_asr(sys.argv[1:])
9058
9138
 
9059
9139
 
9140
+ def chronogram(argv=None):
9141
+ Phykit.chronogram(sys.argv[1:])
9142
+
9143
+
9060
9144
  def bipartition_support_stats(argv=None):
9061
9145
  Phykit.bipartition_support_stats(sys.argv[1:])
9062
9146
 
@@ -59,6 +59,7 @@ BipartitionSupportStats = _LazyServiceFactory("phykit.services.tree.bipartition_
59
59
  BranchLengthMultiplier = _LazyServiceFactory("phykit.services.tree.branch_length_multiplier", "BranchLengthMultiplier")
60
60
  CollapseBranches = _LazyServiceFactory("phykit.services.tree.collapse_branches", "CollapseBranches")
61
61
  CovaryingEvolutionaryRates = _LazyServiceFactory("phykit.services.tree.covarying_evolutionary_rates", "CovaryingEvolutionaryRates")
62
+ Chronogram = _LazyServiceFactory("phykit.services.tree.chronogram", "Chronogram")
62
63
  ConsensusNetwork = _LazyServiceFactory("phykit.services.tree.consensus_network", "ConsensusNetwork")
63
64
  NeighborNet = _LazyServiceFactory("phykit.services.tree.neighbor_net", "NeighborNet")
64
65
  ConsensusTree = _LazyServiceFactory("phykit.services.tree.consensus_tree", "ConsensusTree")
@@ -5,6 +5,7 @@ _EXPORTS = {
5
5
  "BipartitionSupportStats": "bipartition_support_stats",
6
6
  "BranchLengthMultiplier": "branch_length_multiplier",
7
7
  "CollapseBranches": "collapse_branches",
8
+ "Chronogram": "chronogram",
8
9
  "CovaryingEvolutionaryRates": "covarying_evolutionary_rates",
9
10
  "ConsensusNetwork": "consensus_network",
10
11
  "NeighborNet": "neighbor_net",
@@ -1,5 +1,5 @@
1
- import copy
2
1
  import math
2
+ import pickle
3
3
  import sys
4
4
  from typing import Dict, List, Tuple
5
5
 
@@ -78,7 +78,7 @@ class AncestralReconstruction(Tree):
78
78
  x = np.array([trait_values[name] for name in ordered_names])
79
79
 
80
80
  # Prune tree to shared taxa
81
- tree_copy = copy.deepcopy(tree)
81
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
82
82
  tip_names_in_tree = [t.name for t in tree_copy.get_terminals()]
83
83
  tips_to_prune = [t for t in tip_names_in_tree if t not in trait_values]
84
84
  if tips_to_prune:
@@ -1368,7 +1368,7 @@ class AncestralReconstruction(Tree):
1368
1368
  trait_name = "trait"
1369
1369
 
1370
1370
  # Prune tree to shared taxa
1371
- tree_copy = copy.deepcopy(tree)
1371
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
1372
1372
  tip_names_in_tree = [t.name for t in tree_copy.get_terminals()]
1373
1373
  tips_to_prune = [t for t in tip_names_in_tree if t not in tip_states]
1374
1374
  if tips_to_prune:
@@ -1,4 +1,5 @@
1
1
  import copy
2
+ import pickle
2
3
  from typing import List
3
4
  from functools import lru_cache
4
5
  import os
@@ -80,12 +81,29 @@ class Tree(BaseService):
80
81
  def read_reference_tree_file(self):
81
82
  return self._read_tree_with_error(self.reference, "reference")
82
83
 
84
+ @staticmethod
85
+ def _fast_copy(tree):
86
+ """Copy a tree using pickle instead of deepcopy.
87
+
88
+ Avoids RecursionError on deeply nested trees (e.g., ladder-like
89
+ topologies with hundreds of cascading bifurcations) where
90
+ copy.deepcopy exceeds Python's default recursion limit.
91
+ Falls back to deepcopy for objects that can't be pickled (e.g.,
92
+ mocks in unit tests).
93
+ """
94
+ try:
95
+ return pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
96
+ except (pickle.PicklingError, TypeError, AttributeError):
97
+ return copy.deepcopy(tree)
98
+
83
99
  def _read_tree_with_error(self, tree_path: str, attr_name: str):
84
100
  try:
85
101
  file_hash = self._get_file_hash(tree_path)
86
102
  tree = self._cached_tree_read(tree_path, self.tree_format, file_hash)
87
- # Return a deep copy to prevent modifications to the cached tree
88
- return copy.deepcopy(tree)
103
+ # Return a copy to prevent modifications to the cached tree.
104
+ # Uses pickle instead of deepcopy to avoid RecursionError on
105
+ # deeply nested trees.
106
+ return self._fast_copy(tree)
89
107
  except FileNotFoundError:
90
108
  path = getattr(self, attr_name)
91
109
  raise PhykitUserError(
@@ -1,5 +1,5 @@
1
1
  from typing import Dict
2
- import copy
2
+ import pickle
3
3
 
4
4
  from Bio.Phylo import Newick
5
5
 
@@ -20,7 +20,7 @@ class BranchLengthMultiplier(Tree):
20
20
  def run(self) -> None:
21
21
  tree = self.read_tree_file()
22
22
  # Make a deep copy to avoid modifying the cached tree
23
- tree_copy = copy.deepcopy(tree)
23
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
24
24
  scaled_count = self.multiply_branch_lengths_by_factor(tree_copy, self.factor)
25
25
  self.write_tree_file(tree_copy, self.output_file_path)
26
26
 
@@ -7,7 +7,7 @@ produces a phylogram/cladogram plot with annotated character changes.
7
7
 
8
8
  Uses the generalized parsimony utilities in phykit.helpers.parsimony_utils.
9
9
  """
10
- import copy
10
+ import pickle
11
11
  from collections import Counter
12
12
  from typing import Dict, List, Optional, Set, Tuple
13
13
 
@@ -79,7 +79,7 @@ class CharacterMap(Tree):
79
79
 
80
80
  def run(self) -> None:
81
81
  tree = self.read_tree_file()
82
- tree = copy.deepcopy(tree)
82
+ tree = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
83
83
 
84
84
  char_names, tip_states = self._parse_character_matrix(self.data_path)
85
85
  n_chars = len(char_names)