phykit 2.1.84__tar.gz → 2.1.87__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 (146) hide show
  1. {phykit-2.1.84 → phykit-2.1.87}/PKG-INFO +1 -1
  2. {phykit-2.1.84 → phykit-2.1.87}/phykit/cli_registry.py +3 -0
  3. {phykit-2.1.84 → phykit-2.1.87}/phykit/phykit.py +71 -0
  4. {phykit-2.1.84 → phykit-2.1.87}/phykit/service_factories.py +1 -0
  5. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/__init__.py +1 -0
  6. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/ancestral_reconstruction.py +3 -3
  7. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/base.py +20 -2
  8. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/branch_length_multiplier.py +2 -2
  9. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/character_map.py +2 -2
  10. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/collapse_branches.py +2 -2
  11. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/concordance_asr.py +159 -3
  12. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/cont_map.py +2 -2
  13. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/independent_contrasts.py +2 -2
  14. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/internode_labeler.py +2 -2
  15. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/last_common_ancestor_subtree.py +2 -2
  16. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/ou_shift_detection.py +2 -2
  17. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/ouwie.py +2 -2
  18. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/parsimony_score.py +2 -2
  19. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/phenogram.py +2 -2
  20. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/phylo_logistic.py +2 -2
  21. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/phylogenetic_ordination.py +2 -2
  22. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/phylomorphospace.py +2 -2
  23. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/polytomy_test.py +12 -4
  24. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/print_tree.py +2 -2
  25. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/prune_tree.py +2 -2
  26. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/rate_heterogeneity.py +2 -2
  27. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/rename_tree_tips.py +2 -2
  28. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/root_tree.py +2 -2
  29. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/spectral_discordance.py +3 -3
  30. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/spr.py +2 -2
  31. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/trait_rate_map.py +2 -2
  32. phykit-2.1.87/phykit/services/tree/transfer_annotations.py +166 -0
  33. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/tree_space.py +2 -2
  34. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/vcv_utils.py +2 -2
  35. phykit-2.1.87/phykit/version.py +1 -0
  36. {phykit-2.1.84 → phykit-2.1.87}/phykit.egg-info/PKG-INFO +1 -1
  37. {phykit-2.1.84 → phykit-2.1.87}/phykit.egg-info/SOURCES.txt +1 -0
  38. {phykit-2.1.84 → phykit-2.1.87}/phykit.egg-info/entry_points.txt +3 -0
  39. phykit-2.1.84/phykit/version.py +0 -1
  40. {phykit-2.1.84 → phykit-2.1.87}/LICENSE.md +0 -0
  41. {phykit-2.1.84 → phykit-2.1.87}/README.md +0 -0
  42. {phykit-2.1.84 → phykit-2.1.87}/phykit/__init__.py +0 -0
  43. {phykit-2.1.84 → phykit-2.1.87}/phykit/__main__.py +0 -0
  44. {phykit-2.1.84 → phykit-2.1.87}/phykit/errors.py +0 -0
  45. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/__init__.py +0 -0
  46. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/boolean_argument_parsing.py +0 -0
  47. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/caching.py +0 -0
  48. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/circular_layout.py +0 -0
  49. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/color_annotations.py +0 -0
  50. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/discrete_models.py +0 -0
  51. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/files.py +0 -0
  52. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/json_output.py +0 -0
  53. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/parallel.py +0 -0
  54. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/parsimony_utils.py +0 -0
  55. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/pgls_utils.py +0 -0
  56. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/plot_config.py +0 -0
  57. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/quartet_utils.py +0 -0
  58. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/stats_summary.py +0 -0
  59. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/streaming.py +0 -0
  60. {phykit-2.1.84 → phykit-2.1.87}/phykit/helpers/trait_parsing.py +0 -0
  61. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/__init__.py +0 -0
  62. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/__init__.py +0 -0
  63. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/alignment_entropy.py +0 -0
  64. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/alignment_length.py +0 -0
  65. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  66. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  67. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/alignment_recoding.py +0 -0
  68. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/alignment_subsample.py +0 -0
  69. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/base.py +0 -0
  70. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/column_score.py +0 -0
  71. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/composition_per_taxon.py +0 -0
  72. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  73. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  74. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/dfoil.py +0 -0
  75. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/dna_threader.py +0 -0
  76. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/dstatistic.py +0 -0
  77. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  78. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/faidx.py +0 -0
  79. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/gc_content.py +0 -0
  80. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/identity_matrix.py +0 -0
  81. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/mask_alignment.py +0 -0
  82. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/occupancy_filter.py +0 -0
  83. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  84. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/pairwise_identity.py +0 -0
  85. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  86. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/phylo_gwas.py +0 -0
  87. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  88. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/rcv.py +0 -0
  89. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/rcvt.py +0 -0
  90. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  91. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  92. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/taxon_groups.py +0 -0
  93. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/alignment/variable_sites.py +0 -0
  94. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/base.py +0 -0
  95. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/bipartition_support_stats.py +0 -0
  96. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/consensus_network.py +0 -0
  97. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/consensus_tree.py +0 -0
  98. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/cophylo.py +0 -0
  99. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  100. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/density_map.py +0 -0
  101. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/discordance_asymmetry.py +0 -0
  102. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/dvmc.py +0 -0
  103. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/evo_tempo_map.py +0 -0
  104. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/evolutionary_rate.py +0 -0
  105. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/fit_continuous.py +0 -0
  106. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/fit_discrete.py +0 -0
  107. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  108. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/hybridization.py +0 -0
  109. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/internal_branch_stats.py +0 -0
  110. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/kf_distance.py +0 -0
  111. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/lb_score.py +0 -0
  112. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/ltt.py +0 -0
  113. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/monophyly_check.py +0 -0
  114. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  115. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/neighbor_net.py +0 -0
  116. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/network_signal.py +0 -0
  117. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/patristic_distances.py +0 -0
  118. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/phylo_anova.py +0 -0
  119. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/phylo_heatmap.py +0 -0
  120. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/phylo_impute.py +0 -0
  121. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/phylo_path.py +0 -0
  122. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/phylogenetic_glm.py +0 -0
  123. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/phylogenetic_regression.py +0 -0
  124. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/phylogenetic_signal.py +0 -0
  125. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/quartet_network.py +0 -0
  126. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/quartet_pie.py +0 -0
  127. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/relative_rate_test.py +0 -0
  128. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/rf_distance.py +0 -0
  129. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/saturation.py +0 -0
  130. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/simmap_summary.py +0 -0
  131. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/spurious_sequence.py +0 -0
  132. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/stochastic_character_map.py +0 -0
  133. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/terminal_branch_stats.py +0 -0
  134. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/threshold_model.py +0 -0
  135. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/tip_labels.py +0 -0
  136. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  137. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  138. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/total_tree_length.py +0 -0
  139. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/trait_correlation.py +0 -0
  140. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/treeness.py +0 -0
  141. {phykit-2.1.84 → phykit-2.1.87}/phykit/services/tree/treeness_over_rcv.py +0 -0
  142. {phykit-2.1.84 → phykit-2.1.87}/phykit.egg-info/dependency_links.txt +0 -0
  143. {phykit-2.1.84 → phykit-2.1.87}/phykit.egg-info/requires.txt +0 -0
  144. {phykit-2.1.84 → phykit-2.1.87}/phykit.egg-info/top_level.txt +0 -0
  145. {phykit-2.1.84 → phykit-2.1.87}/setup.cfg +0 -0
  146. {phykit-2.1.84 → phykit-2.1.87}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.84
3
+ Version: 2.1.87
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -215,6 +215,9 @@ ALIAS_TO_HANDLER: Dict[str, str] = {
215
215
  "tree_space": "tree_space",
216
216
  "tspace": "tree_space",
217
217
  "tree_landscape": "tree_space",
218
+ "transfer_annotations": "transfer_annotations",
219
+ "transfer_annot": "transfer_annotations",
220
+ "annotate_tree": "transfer_annotations",
218
221
  "tgroups": "taxon_groups",
219
222
  "shared_taxa": "taxon_groups",
220
223
  "occupancy_filter": "occupancy_filter",
@@ -308,6 +308,8 @@ class Phykit:
308
308
  - prune taxa from a phylogeny
309
309
  subtree_prune_regraft (alias: spr)
310
310
  - generate all SPR rearrangements for a specified subtree
311
+ transfer_annotations (alias: transfer_annot; annotate_tree)
312
+ - transfer node annotations between trees (e.g., wASTRAL to RAxML/IQ-TREE)
311
313
  relative_rate_test (alias: rrt; tajima_rrt)
312
314
  - Tajima's relative rate test for equal evolutionary
313
315
  rates between two ingroup lineages
@@ -2823,6 +2825,14 @@ class Phykit:
2823
2825
  --plot output path for concordance
2824
2826
  ASR plot
2825
2827
 
2828
+ --plot-uncertainty output path for uncertainty
2829
+ plot showing the distribution
2830
+ of ancestral estimates across
2831
+ gene trees (distribution
2832
+ method) or concordance
2833
+ sources (weighted method)
2834
+ as violin + boxplots
2835
+
2826
2836
  --missing-taxa how to handle taxa mismatches:
2827
2837
  shared (default) or error
2828
2838
 
@@ -2900,6 +2910,10 @@ class Phykit:
2900
2910
  "--plot", type=str, required=False, default=None,
2901
2911
  help=SUPPRESS, metavar=""
2902
2912
  )
2913
+ parser.add_argument(
2914
+ "--plot-uncertainty", type=str, required=False, default=None,
2915
+ help=SUPPRESS, metavar=""
2916
+ )
2903
2917
  parser.add_argument(
2904
2918
  "--missing-taxa", type=str, required=False, default="shared",
2905
2919
  choices=["error", "shared"], help=SUPPRESS, metavar=""
@@ -6821,6 +6835,59 @@ class Phykit:
6821
6835
  _add_json_argument(parser)
6822
6836
  _run_service(parser, argv, Spr)
6823
6837
 
6838
+ @staticmethod
6839
+ def transfer_annotations(argv):
6840
+ parser = _new_parser(
6841
+ description=textwrap.dedent(
6842
+ f"""\
6843
+ {help_header}
6844
+
6845
+ Transfer internal node annotations from one tree onto
6846
+ another. Matches nodes by bipartition (descendant taxa
6847
+ set) and copies the annotation labels.
6848
+
6849
+ Typical use case: transfer wASTRAL support annotations
6850
+ (q1/q2/q3, pp1, f1, etc.) from an annotated ASTRAL
6851
+ tree onto a branch-length-optimized topology from
6852
+ RAxML-NG, IQ-TREE, or any other tool. The output tree
6853
+ has the target's branch lengths with the source's
6854
+ annotations.
6855
+
6856
+ Aliases:
6857
+ transfer_annotations, transfer_annot, annotate_tree
6858
+ Command line interfaces:
6859
+ pk_transfer_annotations, pk_transfer_annot, pk_annotate_tree
6860
+
6861
+ Usage:
6862
+ phykit transfer_annotations --source <annotated_tree>
6863
+ --target <branch_length_tree> [-o/--output <file>]
6864
+ [--json]
6865
+
6866
+ Options
6867
+ =====================================================
6868
+ --source annotated tree file (e.g.,
6869
+ wASTRAL output with
6870
+ --support 3)
6871
+
6872
+ --target target tree file with
6873
+ branch lengths to keep
6874
+ (e.g., RAxML-NG or
6875
+ IQ-TREE output)
6876
+
6877
+ -o/--output output file for the
6878
+ annotated tree (default:
6879
+ target file + ".annotated")
6880
+
6881
+ --json output results as JSON
6882
+ """
6883
+ ),
6884
+ )
6885
+ parser.add_argument("--source", type=str, required=True, help=SUPPRESS, metavar="")
6886
+ parser.add_argument("--target", type=str, required=True, help=SUPPRESS, metavar="")
6887
+ parser.add_argument("-o", "--output", type=str, default=None, help=SUPPRESS, metavar="")
6888
+ _add_json_argument(parser)
6889
+ _run_service(parser, argv, TransferAnnotations)
6890
+
6824
6891
  @staticmethod
6825
6892
  def relative_rate_test(argv):
6826
6893
  parser = _new_parser(
@@ -9190,6 +9257,10 @@ def subtree_prune_regraft(argv=None):
9190
9257
  Phykit.subtree_prune_regraft(sys.argv[1:])
9191
9258
 
9192
9259
 
9260
+ def transfer_annotations(argv=None):
9261
+ Phykit.transfer_annotations(sys.argv[1:])
9262
+
9263
+
9193
9264
  def relative_rate_test(argv=None):
9194
9265
  Phykit.relative_rate_test(sys.argv[1:])
9195
9266
 
@@ -108,6 +108,7 @@ RobinsonFouldsDistance = _LazyServiceFactory("phykit.services.tree.rf_distance",
108
108
  RootTree = _LazyServiceFactory("phykit.services.tree.root_tree", "RootTree")
109
109
  Saturation = _LazyServiceFactory("phykit.services.tree.saturation", "Saturation")
110
110
  Spr = _LazyServiceFactory("phykit.services.tree.spr", "Spr")
111
+ TransferAnnotations = _LazyServiceFactory("phykit.services.tree.transfer_annotations", "TransferAnnotations")
111
112
  SpuriousSequence = _LazyServiceFactory("phykit.services.tree.spurious_sequence", "SpuriousSequence")
112
113
  TerminalBranchStats = _LazyServiceFactory("phykit.services.tree.terminal_branch_stats", "TerminalBranchStats")
113
114
  TipLabels = _LazyServiceFactory("phykit.services.tree.tip_labels", "TipLabels")
@@ -48,6 +48,7 @@ _EXPORTS = {
48
48
  "TipToTipDistance": "tip_to_tip_distance",
49
49
  "TipToTipNodeDistance": "tip_to_tip_node_distance",
50
50
  "TotalTreeLength": "total_tree_length",
51
+ "TransferAnnotations": "transfer_annotations",
51
52
  "Treeness": "treeness",
52
53
  "ThresholdModel": "threshold_model",
53
54
  "TreenessOverRCV": "treeness_over_rcv",
@@ -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)
@@ -1,5 +1,5 @@
1
1
  from typing import Dict
2
- import copy
2
+ import pickle
3
3
 
4
4
  from .base import Tree
5
5
  from ...helpers.json_output import print_json
@@ -18,7 +18,7 @@ class CollapseBranches(Tree):
18
18
  def run(self):
19
19
  tree = self.read_tree_file()
20
20
  # Make a deep copy to avoid modifying the cached tree
21
- tree_copy = copy.deepcopy(tree)
21
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
22
22
  initial_nonterminals = len(tree_copy.get_nonterminals())
23
23
  tree_copy.collapse_all(
24
24
  lambda c: c.confidence and c.confidence < self.support
@@ -1,4 +1,3 @@
1
- import copy
2
1
  import math
3
2
  import pickle
4
3
  from io import StringIO
@@ -37,6 +36,7 @@ class ConcordanceAsr(Tree):
37
36
  self.method = parsed["method"]
38
37
  self.ci = parsed["ci"]
39
38
  self.plot_output = parsed["plot_output"]
39
+ self.plot_uncertainty = parsed["plot_uncertainty"]
40
40
  self.missing_taxa = parsed["missing_taxa"]
41
41
  self.json_output = parsed["json_output"]
42
42
  self.plot_config = parsed["plot_config"]
@@ -50,6 +50,7 @@ class ConcordanceAsr(Tree):
50
50
  method=getattr(args, "method", "weighted"),
51
51
  ci=getattr(args, "ci", False),
52
52
  plot_output=getattr(args, "plot", None),
53
+ plot_uncertainty=getattr(args, "plot_uncertainty", None),
53
54
  missing_taxa=getattr(args, "missing_taxa", "shared"),
54
55
  json_output=getattr(args, "json", False),
55
56
  plot_config=PlotConfig.from_args(args),
@@ -105,7 +106,7 @@ class ConcordanceAsr(Tree):
105
106
  )
106
107
 
107
108
  # Prune species tree to shared taxa with trait data
108
- species_copy = copy.deepcopy(species_tree)
109
+ species_copy = pickle.loads(pickle.dumps(species_tree, protocol=pickle.HIGHEST_PROTOCOL))
109
110
  sp_tips = [t.name for t in species_copy.get_terminals()]
110
111
  tips_to_prune = [t for t in sp_tips if t not in trait_values]
111
112
  if tips_to_prune:
@@ -131,6 +132,12 @@ class ConcordanceAsr(Tree):
131
132
  )
132
133
  result["plot_output"] = self.plot_output
133
134
 
135
+ if self.plot_uncertainty:
136
+ self._plot_uncertainty(
137
+ species_copy, result, self.plot_uncertainty
138
+ )
139
+ result["plot_uncertainty"] = self.plot_uncertainty
140
+
134
141
  if self.json_output:
135
142
  print_json(result)
136
143
  else:
@@ -409,7 +416,7 @@ class ConcordanceAsr(Tree):
409
416
 
410
417
  def _run_asr_on_tree(self, tree, trait_values):
411
418
  """Run _fast_anc on one tree, return results keyed by descendant frozensets."""
412
- tree_copy = copy.deepcopy(tree)
419
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
413
420
 
414
421
  # Prune to trait taxa
415
422
  tip_names = [t.name for t in tree_copy.get_terminals()]
@@ -586,6 +593,8 @@ class ConcordanceAsr(Tree):
586
593
  "gdf2": float(gdf2),
587
594
  "var_topology": float(between_var),
588
595
  "var_parameter": float(within_var),
596
+ "source_estimates": [float(m) for m in means],
597
+ "source_weights": [float(w) for w in weights],
589
598
  }
590
599
 
591
600
  if self.ci:
@@ -658,6 +667,7 @@ class ConcordanceAsr(Tree):
658
667
  "n_gene_trees_with_node": len(estimates_list),
659
668
  "var_topology": float(np.var(estimates_list)),
660
669
  "var_parameter": 0.0,
670
+ "gene_tree_estimates": estimates_list,
661
671
  }
662
672
 
663
673
  if self.ci and len(estimates_list) >= 2:
@@ -961,3 +971,149 @@ class ConcordanceAsr(Tree):
961
971
  fig.savefig(output_path, dpi=config.dpi, bbox_inches="tight")
962
972
  plt.close(fig)
963
973
  print(f"Saved concordance ASR plot: {output_path}")
974
+
975
+ # ------------------------------------------------------------------
976
+ # Uncertainty plot
977
+ # ------------------------------------------------------------------
978
+
979
+ def _plot_uncertainty(self, species_tree, result, output_path) -> None:
980
+ """Violin + boxplot showing distribution of ancestral estimates.
981
+
982
+ For the distribution method: per-gene-tree estimates.
983
+ For the weighted method: concordant + discordant source estimates.
984
+ """
985
+ try:
986
+ import matplotlib
987
+ matplotlib.use("Agg")
988
+ import matplotlib.pyplot as plt
989
+ except ImportError:
990
+ print("matplotlib is required for plotting.")
991
+ return
992
+
993
+ anc = result.get("ancestral_estimates", {})
994
+ if not anc:
995
+ return
996
+
997
+ config = self.plot_config
998
+
999
+ # Collect data: (node_label, estimates_list, gcf, estimate)
1000
+ node_data = []
1001
+ for clade in species_tree.find_clades(order="preorder"):
1002
+ if clade.is_terminal():
1003
+ continue
1004
+ # Match by descendants
1005
+ desc = sorted(t.name for t in clade.get_terminals())
1006
+ for label, entry in anc.items():
1007
+ if entry.get("descendants") == desc:
1008
+ if result["method"] == "distribution":
1009
+ estimates = entry.get("gene_tree_estimates", [])
1010
+ else:
1011
+ estimates = entry.get("source_estimates", [])
1012
+ if len(estimates) >= 2:
1013
+ # Short label for display
1014
+ if len(desc) <= 2:
1015
+ short = f"({', '.join(desc)})"
1016
+ else:
1017
+ short = f"({desc[0]}, ..., {desc[-1]})"
1018
+ node_data.append((
1019
+ short, estimates,
1020
+ entry.get("gcf", 1.0),
1021
+ entry.get("estimate", 0.0),
1022
+ ))
1023
+ break
1024
+
1025
+ if not node_data:
1026
+ print("No nodes with enough estimates for uncertainty plot.")
1027
+ return
1028
+
1029
+ # Sort by weighted/mean estimate
1030
+ node_data.sort(key=lambda x: x[3])
1031
+
1032
+ labels = [d[0] for d in node_data]
1033
+ data = [d[1] for d in node_data]
1034
+ gcfs = [d[2] for d in node_data]
1035
+
1036
+ n_nodes = len(node_data)
1037
+ fig_h = max(4, n_nodes * 0.5)
1038
+ fig_w = config.fig_width or 8
1039
+ if config.fig_height:
1040
+ fig_h = config.fig_height
1041
+ fig, ax = plt.subplots(figsize=(fig_w, fig_h))
1042
+
1043
+ positions = list(range(n_nodes))
1044
+
1045
+ # Color by gCF: high concordance = blue, low = red
1046
+ gcf_colors = []
1047
+ for g in gcfs:
1048
+ if g >= 0.7:
1049
+ gcf_colors.append("#2b8cbe")
1050
+ elif g >= 0.4:
1051
+ gcf_colors.append("#969696")
1052
+ else:
1053
+ gcf_colors.append("#d62728")
1054
+
1055
+ # Violin plots (horizontal)
1056
+ vp = ax.violinplot(
1057
+ data, positions=positions, vert=False,
1058
+ showmeans=False, showmedians=False, showextrema=False,
1059
+ )
1060
+ for i, body in enumerate(vp["bodies"]):
1061
+ body.set_facecolor(gcf_colors[i])
1062
+ body.set_alpha(0.3)
1063
+ body.set_edgecolor(gcf_colors[i])
1064
+
1065
+ # Boxplots
1066
+ bp = ax.boxplot(
1067
+ data, positions=positions, vert=False,
1068
+ widths=0.3, patch_artist=True, showfliers=True, zorder=3,
1069
+ )
1070
+ for i, box in enumerate(bp["boxes"]):
1071
+ box.set_facecolor(gcf_colors[i])
1072
+ box.set_alpha(0.6)
1073
+ for element in ["whiskers", "caps", "medians"]:
1074
+ for line in bp[element]:
1075
+ line.set_color("black")
1076
+
1077
+ # Mean markers
1078
+ for i, d in enumerate(data):
1079
+ mean_val = np.mean(d)
1080
+ ax.plot(
1081
+ mean_val, i, "D", color="white",
1082
+ markeredgecolor="black", markersize=5, zorder=4,
1083
+ )
1084
+
1085
+ ax.set_yticks(positions)
1086
+ ax.set_yticklabels(labels, fontsize=7)
1087
+ xlabel = "Ancestral state estimate"
1088
+ ax.set_xlabel(xlabel)
1089
+
1090
+ ax.spines["top"].set_visible(False)
1091
+ ax.spines["right"].set_visible(False)
1092
+
1093
+ # Legend for gCF colors
1094
+ from matplotlib.patches import Patch
1095
+ legend_handles = [
1096
+ Patch(facecolor="#2b8cbe", alpha=0.6, label="gCF >= 0.7"),
1097
+ Patch(facecolor="#969696", alpha=0.6, label="0.4 <= gCF < 0.7"),
1098
+ Patch(facecolor="#d62728", alpha=0.6, label="gCF < 0.4"),
1099
+ ]
1100
+ ax.legend(
1101
+ handles=legend_handles, loc="lower right",
1102
+ fontsize=7, title="Concordance", title_fontsize=8,
1103
+ )
1104
+
1105
+ method_label = (
1106
+ "gene trees" if result["method"] == "distribution"
1107
+ else "concordance sources"
1108
+ )
1109
+ if config.show_title:
1110
+ ax.set_title(
1111
+ config.title
1112
+ or f"Ancestral state uncertainty across {method_label}",
1113
+ fontsize=config.title_fontsize,
1114
+ )
1115
+
1116
+ fig.tight_layout()
1117
+ fig.savefig(output_path, dpi=config.dpi, bbox_inches="tight")
1118
+ plt.close(fig)
1119
+ print(f"Saved uncertainty plot: {output_path}")
@@ -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
 
@@ -47,7 +47,7 @@ class ContMap(Tree):
47
47
  x = np.array([trait_values[name] for name in ordered_names])
48
48
 
49
49
  # Prune tree to shared taxa
50
- tree_copy = copy.deepcopy(tree)
50
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
51
51
  tip_names_in_tree = [t.name for t in tree_copy.get_terminals()]
52
52
  tips_to_prune = [t for t in tip_names_in_tree if t not in trait_values]
53
53
  if tips_to_prune:
@@ -7,7 +7,7 @@ contrast (standardized difference), producing n-1 contrasts for n tips.
7
7
 
8
8
  Cross-validated against R's ape::pic().
9
9
  """
10
- import copy
10
+ import pickle
11
11
  import sys
12
12
  from typing import Dict, List, Tuple
13
13
 
@@ -27,7 +27,7 @@ class IndependentContrasts(Tree):
27
27
 
28
28
  def run(self) -> None:
29
29
  tree = self.read_tree_file()
30
- tree = copy.deepcopy(tree)
30
+ tree = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
31
31
  self.validate_tree(tree, min_tips=3, assign_default_branch_length=1e-8, context="independent contrasts")
32
32
 
33
33
  tree_tips = [t.name for t in tree.get_terminals()]
@@ -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
 
@@ -19,7 +19,7 @@ class InternodeLabeler(Tree):
19
19
  def run(self):
20
20
  tree = self.read_tree_file()
21
21
  # Make a deep copy to avoid modifying the cached tree
22
- tree_copy = copy.deepcopy(tree)
22
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
23
23
  labeled_count = self.add_labels_to_tree(tree_copy)
24
24
  self.write_tree_file(tree_copy, self.output_file_path)
25
25
 
@@ -1,5 +1,5 @@
1
+ import pickle
1
2
  import sys
2
- import copy
3
3
  from typing import Dict
4
4
 
5
5
  from .base import Tree
@@ -21,7 +21,7 @@ class LastCommonAncestorSubtree(Tree):
21
21
  def run(self):
22
22
  tree = self.read_tree_file()
23
23
  # Make a deep copy to avoid issues with cached tree modifications
24
- tree_copy = copy.deepcopy(tree)
24
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
25
25
  try:
26
26
  taxa = read_single_column_file_to_list(self.list_of_taxa)
27
27
  except FileNotFoundError:
@@ -1,4 +1,4 @@
1
- import copy
1
+ import pickle
2
2
  import sys
3
3
  from typing import Dict, List, Tuple
4
4
 
@@ -61,7 +61,7 @@ class OUShiftDetection(Tree):
61
61
  shared = set(traits.keys())
62
62
 
63
63
  # Prune tree to shared taxa
64
- tree_copy = copy.deepcopy(tree)
64
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
65
65
  tip_names_in_tree = [t.name for t in tree_copy.get_terminals()]
66
66
  to_prune = [t for t in tip_names_in_tree if t not in shared]
67
67
  if to_prune:
@@ -1,4 +1,4 @@
1
- import copy
1
+ import pickle
2
2
  import sys
3
3
  from typing import Dict, List, Tuple
4
4
 
@@ -44,7 +44,7 @@ class OUwie(Tree):
44
44
  traits = {k: traits[k] for k in shared}
45
45
  regime_assignments = {k: regime_assignments[k] for k in shared}
46
46
 
47
- tree_copy = copy.deepcopy(tree)
47
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
48
48
  tip_names_in_tree = [t.name for t in tree_copy.get_terminals()]
49
49
  tips_to_prune = [t for t in tip_names_in_tree if t not in shared]
50
50
  if tips_to_prune:
@@ -8,7 +8,7 @@ total is summed.
8
8
 
9
9
  Cross-validated against R's phangorn::parsimony().
10
10
  """
11
- import copy
11
+ import pickle
12
12
  from typing import Dict, List
13
13
 
14
14
  from Bio import SeqIO
@@ -28,7 +28,7 @@ class ParsimonyScore(Tree):
28
28
 
29
29
  def run(self) -> None:
30
30
  tree = self.read_tree_file()
31
- tree = copy.deepcopy(tree)
31
+ tree = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
32
32
  self.validate_tree(tree, min_tips=3, context="parsimony score")
33
33
  self._resolve_polytomies(tree)
34
34
 
@@ -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
 
@@ -34,7 +34,7 @@ class Phenogram(Tree):
34
34
  x = np.array([trait_values[name] for name in ordered_names])
35
35
 
36
36
  # Prune tree to shared taxa
37
- tree_copy = copy.deepcopy(tree)
37
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
38
38
  tip_names_in_tree = [t.name for t in tree_copy.get_terminals()]
39
39
  tips_to_prune = [t for t in tip_names_in_tree if t not in trait_values]
40
40
  if tips_to_prune:
@@ -1,4 +1,4 @@
1
- import copy
1
+ import pickle
2
2
  from typing import Dict, List, Tuple
3
3
 
4
4
  import numpy as np
@@ -156,7 +156,7 @@ class PhyloLogistic(Tree):
156
156
  return vcv, diag_corr
157
157
 
158
158
  # Transform branch lengths
159
- tree_t = copy.deepcopy(tree)
159
+ tree_t = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
160
160
  for clade in tree_t.find_clades():
161
161
  if clade.branch_length is not None and clade.branch_length > 0:
162
162
  bl = clade.branch_length
@@ -1,5 +1,5 @@
1
- import copy
2
1
  import os
2
+ import pickle
3
3
  from typing import Dict, List, Tuple
4
4
 
5
5
  import numpy as np
@@ -346,7 +346,7 @@ class PhylogeneticOrdination(Tree):
346
346
  def _reconstruct_ancestral_scores(
347
347
  self, tree, scores: np.ndarray, ordered_names: List[str]
348
348
  ) -> Tuple[Dict, Dict]:
349
- tree_copy = copy.deepcopy(tree)
349
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
350
350
 
351
351
  tip_names_in_tree = [t.name for t in tree_copy.get_terminals()]
352
352
  tips_to_prune = [t for t in tip_names_in_tree if t not in ordered_names]
@@ -1,5 +1,5 @@
1
- import copy
2
1
  import os
2
+ import pickle
3
3
  from typing import Dict, List, Tuple
4
4
 
5
5
  import numpy as np
@@ -136,7 +136,7 @@ class Phylomorphospace(Tree):
136
136
  def _reconstruct_ancestral_scores(
137
137
  self, tree, scores: np.ndarray, ordered_names: List[str]
138
138
  ) -> Tuple[Dict, Dict, object]:
139
- tree_copy = copy.deepcopy(tree)
139
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
140
140
 
141
141
  tip_names_in_tree = [t.name for t in tree_copy.get_terminals()]
142
142
  tips_to_prune = [t for t in tip_names_in_tree if t not in ordered_names]