phykit 2.1.84__tar.gz → 2.1.85__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.85}/PKG-INFO +1 -1
  2. {phykit-2.1.84 → phykit-2.1.85}/phykit/cli_registry.py +3 -0
  3. {phykit-2.1.84 → phykit-2.1.85}/phykit/phykit.py +59 -0
  4. {phykit-2.1.84 → phykit-2.1.85}/phykit/service_factories.py +1 -0
  5. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/__init__.py +1 -0
  6. phykit-2.1.85/phykit/services/tree/transfer_annotations.py +166 -0
  7. phykit-2.1.85/phykit/version.py +1 -0
  8. {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/PKG-INFO +1 -1
  9. {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/SOURCES.txt +1 -0
  10. {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/entry_points.txt +3 -0
  11. phykit-2.1.84/phykit/version.py +0 -1
  12. {phykit-2.1.84 → phykit-2.1.85}/LICENSE.md +0 -0
  13. {phykit-2.1.84 → phykit-2.1.85}/README.md +0 -0
  14. {phykit-2.1.84 → phykit-2.1.85}/phykit/__init__.py +0 -0
  15. {phykit-2.1.84 → phykit-2.1.85}/phykit/__main__.py +0 -0
  16. {phykit-2.1.84 → phykit-2.1.85}/phykit/errors.py +0 -0
  17. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/__init__.py +0 -0
  18. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/boolean_argument_parsing.py +0 -0
  19. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/caching.py +0 -0
  20. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/circular_layout.py +0 -0
  21. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/color_annotations.py +0 -0
  22. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/discrete_models.py +0 -0
  23. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/files.py +0 -0
  24. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/json_output.py +0 -0
  25. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/parallel.py +0 -0
  26. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/parsimony_utils.py +0 -0
  27. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/pgls_utils.py +0 -0
  28. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/plot_config.py +0 -0
  29. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/quartet_utils.py +0 -0
  30. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/stats_summary.py +0 -0
  31. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/streaming.py +0 -0
  32. {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/trait_parsing.py +0 -0
  33. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/__init__.py +0 -0
  34. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/__init__.py +0 -0
  35. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_entropy.py +0 -0
  36. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_length.py +0 -0
  37. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  38. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  39. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_recoding.py +0 -0
  40. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_subsample.py +0 -0
  41. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/base.py +0 -0
  42. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/column_score.py +0 -0
  43. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/composition_per_taxon.py +0 -0
  44. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  45. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  46. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/dfoil.py +0 -0
  47. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/dna_threader.py +0 -0
  48. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/dstatistic.py +0 -0
  49. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  50. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/faidx.py +0 -0
  51. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/gc_content.py +0 -0
  52. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/identity_matrix.py +0 -0
  53. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/mask_alignment.py +0 -0
  54. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/occupancy_filter.py +0 -0
  55. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  56. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/pairwise_identity.py +0 -0
  57. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  58. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/phylo_gwas.py +0 -0
  59. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  60. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/rcv.py +0 -0
  61. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/rcvt.py +0 -0
  62. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  63. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  64. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/taxon_groups.py +0 -0
  65. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/variable_sites.py +0 -0
  66. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/base.py +0 -0
  67. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/ancestral_reconstruction.py +0 -0
  68. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/base.py +0 -0
  69. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/bipartition_support_stats.py +0 -0
  70. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/branch_length_multiplier.py +0 -0
  71. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/character_map.py +0 -0
  72. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/collapse_branches.py +0 -0
  73. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/concordance_asr.py +0 -0
  74. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/consensus_network.py +0 -0
  75. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/consensus_tree.py +0 -0
  76. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/cont_map.py +0 -0
  77. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/cophylo.py +0 -0
  78. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  79. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/density_map.py +0 -0
  80. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/discordance_asymmetry.py +0 -0
  81. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/dvmc.py +0 -0
  82. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/evo_tempo_map.py +0 -0
  83. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/evolutionary_rate.py +0 -0
  84. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/fit_continuous.py +0 -0
  85. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/fit_discrete.py +0 -0
  86. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  87. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/hybridization.py +0 -0
  88. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/independent_contrasts.py +0 -0
  89. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/internal_branch_stats.py +0 -0
  90. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/internode_labeler.py +0 -0
  91. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/kf_distance.py +0 -0
  92. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  93. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/lb_score.py +0 -0
  94. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/ltt.py +0 -0
  95. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/monophyly_check.py +0 -0
  96. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  97. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/neighbor_net.py +0 -0
  98. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/network_signal.py +0 -0
  99. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/ou_shift_detection.py +0 -0
  100. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/ouwie.py +0 -0
  101. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/parsimony_score.py +0 -0
  102. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/patristic_distances.py +0 -0
  103. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phenogram.py +0 -0
  104. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylo_anova.py +0 -0
  105. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylo_heatmap.py +0 -0
  106. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylo_impute.py +0 -0
  107. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylo_logistic.py +0 -0
  108. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylo_path.py +0 -0
  109. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylogenetic_glm.py +0 -0
  110. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  111. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylogenetic_regression.py +0 -0
  112. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylogenetic_signal.py +0 -0
  113. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylomorphospace.py +0 -0
  114. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/polytomy_test.py +0 -0
  115. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/print_tree.py +0 -0
  116. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/prune_tree.py +0 -0
  117. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/quartet_network.py +0 -0
  118. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/quartet_pie.py +0 -0
  119. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/rate_heterogeneity.py +0 -0
  120. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/relative_rate_test.py +0 -0
  121. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/rename_tree_tips.py +0 -0
  122. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/rf_distance.py +0 -0
  123. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/root_tree.py +0 -0
  124. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/saturation.py +0 -0
  125. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/simmap_summary.py +0 -0
  126. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/spectral_discordance.py +0 -0
  127. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/spr.py +0 -0
  128. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/spurious_sequence.py +0 -0
  129. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/stochastic_character_map.py +0 -0
  130. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/terminal_branch_stats.py +0 -0
  131. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/threshold_model.py +0 -0
  132. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/tip_labels.py +0 -0
  133. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  134. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  135. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/total_tree_length.py +0 -0
  136. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/trait_correlation.py +0 -0
  137. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/trait_rate_map.py +0 -0
  138. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/tree_space.py +0 -0
  139. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/treeness.py +0 -0
  140. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/treeness_over_rcv.py +0 -0
  141. {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/vcv_utils.py +0 -0
  142. {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/dependency_links.txt +0 -0
  143. {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/requires.txt +0 -0
  144. {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/top_level.txt +0 -0
  145. {phykit-2.1.84 → phykit-2.1.85}/setup.cfg +0 -0
  146. {phykit-2.1.84 → phykit-2.1.85}/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.85
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
@@ -6821,6 +6823,59 @@ class Phykit:
6821
6823
  _add_json_argument(parser)
6822
6824
  _run_service(parser, argv, Spr)
6823
6825
 
6826
+ @staticmethod
6827
+ def transfer_annotations(argv):
6828
+ parser = _new_parser(
6829
+ description=textwrap.dedent(
6830
+ f"""\
6831
+ {help_header}
6832
+
6833
+ Transfer internal node annotations from one tree onto
6834
+ another. Matches nodes by bipartition (descendant taxa
6835
+ set) and copies the annotation labels.
6836
+
6837
+ Typical use case: transfer wASTRAL support annotations
6838
+ (q1/q2/q3, pp1, f1, etc.) from an annotated ASTRAL
6839
+ tree onto a branch-length-optimized topology from
6840
+ RAxML-NG, IQ-TREE, or any other tool. The output tree
6841
+ has the target's branch lengths with the source's
6842
+ annotations.
6843
+
6844
+ Aliases:
6845
+ transfer_annotations, transfer_annot, annotate_tree
6846
+ Command line interfaces:
6847
+ pk_transfer_annotations, pk_transfer_annot, pk_annotate_tree
6848
+
6849
+ Usage:
6850
+ phykit transfer_annotations --source <annotated_tree>
6851
+ --target <branch_length_tree> [-o/--output <file>]
6852
+ [--json]
6853
+
6854
+ Options
6855
+ =====================================================
6856
+ --source annotated tree file (e.g.,
6857
+ wASTRAL output with
6858
+ --support 3)
6859
+
6860
+ --target target tree file with
6861
+ branch lengths to keep
6862
+ (e.g., RAxML-NG or
6863
+ IQ-TREE output)
6864
+
6865
+ -o/--output output file for the
6866
+ annotated tree (default:
6867
+ target file + ".annotated")
6868
+
6869
+ --json output results as JSON
6870
+ """
6871
+ ),
6872
+ )
6873
+ parser.add_argument("--source", type=str, required=True, help=SUPPRESS, metavar="")
6874
+ parser.add_argument("--target", type=str, required=True, help=SUPPRESS, metavar="")
6875
+ parser.add_argument("-o", "--output", type=str, default=None, help=SUPPRESS, metavar="")
6876
+ _add_json_argument(parser)
6877
+ _run_service(parser, argv, TransferAnnotations)
6878
+
6824
6879
  @staticmethod
6825
6880
  def relative_rate_test(argv):
6826
6881
  parser = _new_parser(
@@ -9190,6 +9245,10 @@ def subtree_prune_regraft(argv=None):
9190
9245
  Phykit.subtree_prune_regraft(sys.argv[1:])
9191
9246
 
9192
9247
 
9248
+ def transfer_annotations(argv=None):
9249
+ Phykit.transfer_annotations(sys.argv[1:])
9250
+
9251
+
9193
9252
  def relative_rate_test(argv=None):
9194
9253
  Phykit.relative_rate_test(sys.argv[1:])
9195
9254
 
@@ -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",
@@ -0,0 +1,166 @@
1
+ """
2
+ Transfer annotations between phylogenies.
3
+
4
+ Copies internal node annotations (e.g., wASTRAL q1/q2/q3, pp1, f1)
5
+ from an annotated source tree onto a target tree with optimized branch
6
+ lengths. Nodes are matched by their bipartition (set of descendant taxa).
7
+
8
+ Typical use case: transfer wASTRAL support annotations onto a
9
+ branch-length-optimized topology from RAxML-NG, IQ-TREE, or any
10
+ other tool for use with quartet_pie or other visualization commands.
11
+ """
12
+ from io import StringIO
13
+ from typing import Dict, List, Set, Tuple
14
+
15
+ from Bio import Phylo
16
+
17
+ from .base import Tree
18
+ from ...helpers.json_output import print_json
19
+ from ...errors import PhykitUserError
20
+
21
+
22
+ class TransferAnnotations(Tree):
23
+ def __init__(self, args) -> None:
24
+ parsed = self.process_args(args)
25
+ super().__init__(tree_file_path=parsed["target_path"])
26
+ self.source_path = parsed["source_path"]
27
+ self.output_path = parsed["output_path"]
28
+ self.json_output = parsed["json_output"]
29
+
30
+ def process_args(self, args) -> Dict:
31
+ return dict(
32
+ target_path=args.target,
33
+ source_path=args.source,
34
+ output_path=getattr(args, "output", None),
35
+ json_output=getattr(args, "json", False),
36
+ )
37
+
38
+ def run(self) -> None:
39
+ # Read both trees
40
+ target = self.read_tree_file()
41
+ try:
42
+ source = Phylo.read(self.source_path, "newick")
43
+ except FileNotFoundError:
44
+ raise PhykitUserError(
45
+ [
46
+ f"{self.source_path} corresponds to no such file or directory.",
47
+ "Please check filename and pathing",
48
+ ],
49
+ code=2,
50
+ )
51
+
52
+ # Validate taxa match
53
+ target_tips = {t.name for t in target.get_terminals()}
54
+ source_tips = {t.name for t in source.get_terminals()}
55
+
56
+ if target_tips != source_tips:
57
+ only_target = target_tips - source_tips
58
+ only_source = source_tips - target_tips
59
+ msgs = ["Taxa in source and target trees do not match."]
60
+ if only_target:
61
+ msgs.append(
62
+ f"Only in target: {', '.join(sorted(only_target)[:5])}"
63
+ + (f" ... ({len(only_target)} total)" if len(only_target) > 5 else "")
64
+ )
65
+ if only_source:
66
+ msgs.append(
67
+ f"Only in source: {', '.join(sorted(only_source)[:5])}"
68
+ + (f" ... ({len(only_source)} total)" if len(only_source) > 5 else "")
69
+ )
70
+ raise PhykitUserError(msgs, code=2)
71
+
72
+ # Build bipartition -> annotation map from source tree
73
+ all_taxa = frozenset(source_tips)
74
+ source_annotations = self._extract_annotations(source, all_taxa)
75
+
76
+ # Match and transfer annotations to target tree
77
+ n_transferred, n_unmatched = self._transfer(
78
+ target, source_annotations, all_taxa
79
+ )
80
+
81
+ # Output
82
+ output_path = self.output_path or f"{self.tree_file_path}.annotated"
83
+ self._write_annotated_tree(target, output_path)
84
+
85
+ if self.json_output:
86
+ self._print_json(
87
+ n_transferred, n_unmatched, output_path,
88
+ len(source_annotations), target_tips,
89
+ )
90
+ else:
91
+ try:
92
+ print(f"Annotations transferred: {n_transferred}")
93
+ print(f"Unmatched nodes: {n_unmatched}")
94
+ print(f"Output: {output_path}")
95
+ except BrokenPipeError:
96
+ return
97
+
98
+ @staticmethod
99
+ def _get_bipartition(clade, all_taxa: frozenset) -> frozenset:
100
+ """Get canonical bipartition for a clade (smaller side)."""
101
+ tips = frozenset(t.name for t in clade.get_terminals())
102
+ complement = all_taxa - tips
103
+ # Use the smaller side as the canonical key
104
+ return tips if len(tips) <= len(complement) else complement
105
+
106
+ def _extract_annotations(
107
+ self, tree, all_taxa: frozenset
108
+ ) -> Dict[frozenset, str]:
109
+ """Extract bipartition -> annotation mapping from source tree."""
110
+ annotations = {}
111
+ for clade in tree.find_clades(order="preorder"):
112
+ if clade.is_terminal() or clade == tree.root:
113
+ continue
114
+ annotation = clade.comment or clade.name or ""
115
+ if not annotation:
116
+ continue
117
+ bp = self._get_bipartition(clade, all_taxa)
118
+ if bp:
119
+ annotations[bp] = annotation
120
+ return annotations
121
+
122
+ def _transfer(
123
+ self, target, source_annotations: Dict[frozenset, str],
124
+ all_taxa: frozenset,
125
+ ) -> Tuple[int, int]:
126
+ """Transfer annotations from source to target by bipartition matching."""
127
+ transferred = 0
128
+ unmatched = 0
129
+
130
+ for clade in target.find_clades(order="preorder"):
131
+ if clade.is_terminal() or clade == target.root:
132
+ continue
133
+ bp = self._get_bipartition(clade, all_taxa)
134
+ if bp in source_annotations:
135
+ clade.comment = source_annotations[bp]
136
+ transferred += 1
137
+ else:
138
+ unmatched += 1
139
+
140
+ return transferred, unmatched
141
+
142
+ @staticmethod
143
+ def _write_annotated_tree(tree, output_path: str) -> None:
144
+ """Write tree preserving comment annotations in brackets."""
145
+ Phylo.write(tree, output_path, "newick")
146
+ # BioPython writes comments as [&comment] but wASTRAL uses
147
+ # [comment]. Fix the format.
148
+ with open(output_path) as f:
149
+ content = f.read()
150
+ # Remove the & prefix that BioPython adds
151
+ content = content.replace("[&", "[")
152
+ with open(output_path, "w") as f:
153
+ f.write(content)
154
+
155
+ def _print_json(
156
+ self, n_transferred, n_unmatched, output_path,
157
+ n_source_annotations, target_tips,
158
+ ):
159
+ payload = {
160
+ "annotations_transferred": n_transferred,
161
+ "unmatched_nodes": n_unmatched,
162
+ "source_annotations_available": n_source_annotations,
163
+ "n_taxa": len(target_tips),
164
+ "output": output_path,
165
+ }
166
+ print_json(payload, sort_keys=False)
@@ -0,0 +1 @@
1
+ __version__ = "2.1.85"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.84
3
+ Version: 2.1.85
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -136,6 +136,7 @@ phykit/services/tree/tip_to_tip_node_distance.py
136
136
  phykit/services/tree/total_tree_length.py
137
137
  phykit/services/tree/trait_correlation.py
138
138
  phykit/services/tree/trait_rate_map.py
139
+ phykit/services/tree/transfer_annotations.py
139
140
  phykit/services/tree/tree_space.py
140
141
  phykit/services/tree/treeness.py
141
142
  phykit/services/tree/treeness_over_rcv.py
@@ -9,6 +9,7 @@ pk_aln_recoding = phykit.phykit:alignment_recoding
9
9
  pk_aln_subsample = phykit.phykit:alignment_subsample
10
10
  pk_alng = phykit.phykit:alignment_length_no_gaps
11
11
  pk_anc_recon = phykit.phykit:ancestral_state_reconstruction
12
+ pk_annotate_tree = phykit.phykit:transfer_annotations
12
13
  pk_aot = phykit.phykit:alignment_outlier_taxa
13
14
  pk_asr = phykit.phykit:ancestral_state_reconstruction
14
15
  pk_blm = phykit.phykit:branch_length_multiplier
@@ -207,6 +208,8 @@ pk_trait_corr = phykit.phykit:trait_correlation
207
208
  pk_trait_correlation = phykit.phykit:trait_correlation
208
209
  pk_trait_rate_map = phykit.phykit:trait_rate_map
209
210
  pk_traitgram = phykit.phykit:phenogram
211
+ pk_transfer_annot = phykit.phykit:transfer_annotations
212
+ pk_transfer_annotations = phykit.phykit:transfer_annotations
210
213
  pk_tree_labels = phykit.phykit:tip_labels
211
214
  pk_tree_landscape = phykit.phykit:tree_space
212
215
  pk_tree_len = phykit.phykit:total_tree_length
@@ -1 +0,0 @@
1
- __version__ = "2.1.84"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes