phykit 2.1.90__tar.gz → 2.1.93__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 (151) hide show
  1. {phykit-2.1.90 → phykit-2.1.93}/PKG-INFO +2 -18
  2. {phykit-2.1.90 → phykit-2.1.93}/phykit/cli_registry.py +4 -0
  3. {phykit-2.1.90 → phykit-2.1.93}/phykit/phykit.py +142 -15
  4. {phykit-2.1.90 → phykit-2.1.93}/phykit/service_factories.py +1 -0
  5. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/__init__.py +1 -0
  6. phykit-2.1.93/phykit/services/tree/faiths_pd.py +148 -0
  7. phykit-2.1.93/phykit/services/tree/nearest_neighbor_interchange.py +313 -0
  8. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/prune_tree.py +26 -1
  9. phykit-2.1.93/phykit/version.py +1 -0
  10. {phykit-2.1.90 → phykit-2.1.93}/phykit.egg-info/PKG-INFO +2 -18
  11. {phykit-2.1.90 → phykit-2.1.93}/phykit.egg-info/SOURCES.txt +1 -0
  12. {phykit-2.1.90 → phykit-2.1.93}/phykit.egg-info/entry_points.txt +4 -0
  13. phykit-2.1.90/phykit/services/tree/nearest_neighbor_interchange.py +0 -155
  14. phykit-2.1.90/phykit/version.py +0 -1
  15. {phykit-2.1.90 → phykit-2.1.93}/LICENSE.md +0 -0
  16. {phykit-2.1.90 → phykit-2.1.93}/README.md +0 -0
  17. {phykit-2.1.90 → phykit-2.1.93}/phykit/__init__.py +0 -0
  18. {phykit-2.1.90 → phykit-2.1.93}/phykit/__main__.py +0 -0
  19. {phykit-2.1.90 → phykit-2.1.93}/phykit/errors.py +0 -0
  20. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/__init__.py +0 -0
  21. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/boolean_argument_parsing.py +0 -0
  22. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/caching.py +0 -0
  23. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/circular_layout.py +0 -0
  24. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/color_annotations.py +0 -0
  25. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/discrete_models.py +0 -0
  26. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/files.py +0 -0
  27. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/geological_timescale.py +0 -0
  28. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/json_output.py +0 -0
  29. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/parallel.py +0 -0
  30. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/parsimony_utils.py +0 -0
  31. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/pgls_utils.py +0 -0
  32. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/plot_config.py +0 -0
  33. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/quartet_utils.py +0 -0
  34. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/stats_summary.py +0 -0
  35. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/streaming.py +0 -0
  36. {phykit-2.1.90 → phykit-2.1.93}/phykit/helpers/trait_parsing.py +0 -0
  37. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/__init__.py +0 -0
  38. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/__init__.py +0 -0
  39. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/alignment_entropy.py +0 -0
  40. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/alignment_length.py +0 -0
  41. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  42. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  43. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/alignment_recoding.py +0 -0
  44. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/alignment_subsample.py +0 -0
  45. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/base.py +0 -0
  46. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/column_score.py +0 -0
  47. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/composition_per_taxon.py +0 -0
  48. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  49. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  50. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/dfoil.py +0 -0
  51. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/dna_threader.py +0 -0
  52. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/dstatistic.py +0 -0
  53. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  54. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/faidx.py +0 -0
  55. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/gc_content.py +0 -0
  56. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/identity_matrix.py +0 -0
  57. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/mask_alignment.py +0 -0
  58. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/occupancy_filter.py +0 -0
  59. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  60. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/pairwise_identity.py +0 -0
  61. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  62. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/phylo_gwas.py +0 -0
  63. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  64. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/rcv.py +0 -0
  65. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/rcvt.py +0 -0
  66. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  67. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  68. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/taxon_groups.py +0 -0
  69. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/alignment/variable_sites.py +0 -0
  70. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/base.py +0 -0
  71. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/ancestral_reconstruction.py +0 -0
  72. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/base.py +0 -0
  73. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/bipartition_support_stats.py +0 -0
  74. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/branch_length_multiplier.py +0 -0
  75. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/character_map.py +0 -0
  76. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/chronogram.py +0 -0
  77. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/collapse_branches.py +0 -0
  78. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/concordance_asr.py +0 -0
  79. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/consensus_network.py +0 -0
  80. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/consensus_tree.py +0 -0
  81. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/cont_map.py +0 -0
  82. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/cophylo.py +0 -0
  83. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  84. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/density_map.py +0 -0
  85. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/discordance_asymmetry.py +0 -0
  86. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/dtt.py +0 -0
  87. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/dvmc.py +0 -0
  88. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/evo_tempo_map.py +0 -0
  89. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/evolutionary_rate.py +0 -0
  90. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/fit_continuous.py +0 -0
  91. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/fit_discrete.py +0 -0
  92. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  93. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/hybridization.py +0 -0
  94. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/independent_contrasts.py +0 -0
  95. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/internal_branch_stats.py +0 -0
  96. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/internode_labeler.py +0 -0
  97. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/kf_distance.py +0 -0
  98. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  99. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/lb_score.py +0 -0
  100. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/ltt.py +0 -0
  101. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/monophyly_check.py +0 -0
  102. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/neighbor_net.py +0 -0
  103. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/network_signal.py +0 -0
  104. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/ou_shift_detection.py +0 -0
  105. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/ouwie.py +0 -0
  106. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/parsimony_score.py +0 -0
  107. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/patristic_distances.py +0 -0
  108. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/phenogram.py +0 -0
  109. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/phylo_anova.py +0 -0
  110. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/phylo_heatmap.py +0 -0
  111. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/phylo_impute.py +0 -0
  112. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/phylo_logistic.py +0 -0
  113. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/phylo_path.py +0 -0
  114. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/phylogenetic_glm.py +0 -0
  115. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  116. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/phylogenetic_regression.py +0 -0
  117. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/phylogenetic_signal.py +0 -0
  118. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/phylomorphospace.py +0 -0
  119. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/polytomy_test.py +0 -0
  120. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/print_tree.py +0 -0
  121. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/quartet_network.py +0 -0
  122. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/quartet_pie.py +0 -0
  123. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/rate_heterogeneity.py +0 -0
  124. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/relative_rate_test.py +0 -0
  125. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/rename_tree_tips.py +0 -0
  126. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/rf_distance.py +0 -0
  127. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/root_tree.py +0 -0
  128. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/saturation.py +0 -0
  129. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/simmap_summary.py +0 -0
  130. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/spectral_discordance.py +0 -0
  131. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/spr.py +0 -0
  132. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/spurious_sequence.py +0 -0
  133. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/stochastic_character_map.py +0 -0
  134. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/terminal_branch_stats.py +0 -0
  135. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/threshold_model.py +0 -0
  136. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/tip_labels.py +0 -0
  137. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  138. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  139. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/total_tree_length.py +0 -0
  140. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/trait_correlation.py +0 -0
  141. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/trait_rate_map.py +0 -0
  142. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/transfer_annotations.py +0 -0
  143. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/tree_space.py +0 -0
  144. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/treeness.py +0 -0
  145. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/treeness_over_rcv.py +0 -0
  146. {phykit-2.1.90 → phykit-2.1.93}/phykit/services/tree/vcv_utils.py +0 -0
  147. {phykit-2.1.90 → phykit-2.1.93}/phykit.egg-info/dependency_links.txt +0 -0
  148. {phykit-2.1.90 → phykit-2.1.93}/phykit.egg-info/requires.txt +0 -0
  149. {phykit-2.1.90 → phykit-2.1.93}/phykit.egg-info/top_level.txt +0 -0
  150. {phykit-2.1.90 → phykit-2.1.93}/setup.cfg +0 -0
  151. {phykit-2.1.90 → phykit-2.1.93}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: phykit
3
- Version: 2.1.90
3
+ Version: 2.1.93
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -15,22 +15,6 @@ Classifier: Topic :: Scientific/Engineering
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE.md
18
- Requires-Dist: biopython>=1.82
19
- Requires-Dist: matplotlib>=3.7.0
20
- Requires-Dist: numpy>=1.24.0
21
- Requires-Dist: scipy>=1.11.3
22
- Requires-Dist: scikit-learn>=1.4.2
23
- Requires-Dist: umap-learn>=0.5.0
24
- Requires-Dist: tqdm>=4.65.0
25
- Dynamic: author
26
- Dynamic: author-email
27
- Dynamic: classifier
28
- Dynamic: description
29
- Dynamic: description-content-type
30
- Dynamic: home-page
31
- Dynamic: license-file
32
- Dynamic: requires-dist
33
- Dynamic: requires-python
34
18
 
35
19
  <p align="center">
36
20
  <a href="https://github.com/jlsteenwyk/phykit">
@@ -83,6 +83,10 @@ ALIAS_TO_HANDLER: Dict[str, str] = {
83
83
  "ctree": "consensus_tree",
84
84
  "degree_of_violation_of_a_molecular_clock": "dvmc",
85
85
  "evo_rate": "evolutionary_rate",
86
+ "faiths_pd": "faiths_pd",
87
+ "faith_pd": "faiths_pd",
88
+ "fpd": "faiths_pd",
89
+ "phylo_diversity": "faiths_pd",
86
90
  "clan_check": "hidden_paralogy_check",
87
91
  "ibs": "internal_branch_stats",
88
92
  "il": "internode_labeler",
@@ -230,6 +230,9 @@ class Phykit:
230
230
  - determines if a set of tip names are monophyletic
231
231
  nearest_neighbor_interchange (alias: nni)
232
232
  - make nearest neighbor interchange moves on a tree
233
+ faiths_pd (alias: faith_pd; fpd; phylo_diversity)
234
+ - calculate Faith's phylogenetic diversity for a
235
+ community of tips
233
236
  patristic_distances (alias: pd)
234
237
  - calculate all pairwise distances between tips in a tree
235
238
  phylogenetic_signal (alias: phylo_signal; ps)
@@ -3739,10 +3742,19 @@ class Phykit:
3739
3742
  f"""\
3740
3743
  {help_header}
3741
3744
 
3742
- Generate all nearest neighbor interchange moves for a binary
3743
- rooted tree.
3745
+ Generate nearest neighbor interchange (NNI) moves for a
3746
+ binary rooted tree.
3744
3747
 
3745
- The output file will also include the original phylogeny.
3748
+ By default, all NNI rearrangements are emitted. Pass
3749
+ --branch (a comma-separated pair of taxa) or --branches
3750
+ (a file with one pair per line) to restrict output to the
3751
+ two NNI rearrangements around a specific internal branch.
3752
+ The branch is identified as the edge leading to the MRCA
3753
+ of the supplied taxa. Useful for branch-by-branch
3754
+ likelihood comparison in IQ-TREE / RAxML.
3755
+
3756
+ The output file includes the input phylogeny at the top
3757
+ unless --no-input-tree is supplied.
3746
3758
 
3747
3759
  Aliases:
3748
3760
  nearest_neighbor_interchange, nni
@@ -3750,22 +3762,49 @@ class Phykit:
3750
3762
  pk_nearest_neighbor_interchange, pk_nni
3751
3763
 
3752
3764
  Usage:
3753
- phykit nearest_neighbor_interchange <tree> [-o/--output <output_file>] [--json]
3765
+ phykit nearest_neighbor_interchange <tree>
3766
+ [-o/--output <output_file>] [--branch A,B]
3767
+ [--branches <file>] [--no-input-tree] [--json]
3754
3768
 
3755
3769
  Options
3756
3770
  =====================================================
3757
- <tree> first argument after
3771
+ <tree> first argument after
3758
3772
  function name should be
3759
3773
  a tree file
3760
3774
 
3761
3775
  -o/--output name of output file that will
3762
3776
  contain all trees with the
3763
3777
  nearest neighbor interchange
3764
- moves.
3765
- Default output will have
3778
+ moves.
3779
+ Default output will have
3766
3780
  the same name as the input
3767
- file but with the suffix
3768
- ".NNIs"
3781
+ file but with the suffix
3782
+ ".nnis"
3783
+
3784
+ --branch optional argument. Two taxa
3785
+ separated by a comma
3786
+ (e.g., --branch A,B) whose
3787
+ MRCA defines the internal
3788
+ branch to rearrange. Emits
3789
+ the two alternative NNI
3790
+ topologies for that branch.
3791
+
3792
+ --branches optional argument. Path to a
3793
+ file with one taxon pair per
3794
+ line (comma- or tab-
3795
+ separated). Lines may
3796
+ optionally start with a
3797
+ label followed by the two
3798
+ taxa, in which case the
3799
+ label is echoed in the
3800
+ --json report. Lines that
3801
+ are blank or start with #
3802
+ are ignored. Each pair
3803
+ yields two NNI trees.
3804
+
3805
+ --no-input-tree optional argument. Omit the
3806
+ input phylogeny from the
3807
+ top of the output file.
3769
3808
 
3770
3809
  --json optional argument to output
3771
3810
  results as JSON
@@ -3774,9 +3813,78 @@ class Phykit:
3774
3813
  )
3775
3814
  parser.add_argument("tree", type=str, help=SUPPRESS)
3776
3815
  parser.add_argument("-o", "--output", type=str, required=False, help=SUPPRESS)
3816
+ parser.add_argument("--branch", type=str, default=None, help=SUPPRESS)
3817
+ parser.add_argument("--branches", type=str, default=None, help=SUPPRESS)
3818
+ parser.add_argument(
3819
+ "--no-input-tree",
3820
+ action="store_true",
3821
+ default=False,
3822
+ help=SUPPRESS,
3823
+ )
3777
3824
  _add_json_argument(parser)
3778
3825
  _run_service(parser, argv, NearestNeighborInterchange)
3779
3826
 
3827
+ @staticmethod
3828
+ def faiths_pd(argv):
3829
+ parser = _new_parser(
3830
+ description=textwrap.dedent(
3831
+ f"""\
3832
+ {help_header}
3833
+
3834
+ Calculate Faith's phylogenetic diversity (PD) for a
3835
+ community of tips on a phylogeny.
3836
+
3837
+ Faith's PD is the sum of branch lengths in the minimum
3838
+ subtree that connects a set of taxa. By default, the
3839
+ path from the community's most recent common ancestor
3840
+ up to the tree root is included, matching Faith (1992)
3841
+ and picante::pd(..., include.root = TRUE). Use
3842
+ --exclude-root to sum only the branches of the induced
3843
+ subtree rooted at the MRCA, matching
3844
+ picante::pd(..., include.root = FALSE).
3845
+
3846
+ Aliases:
3847
+ faiths_pd, faith_pd, fpd, phylo_diversity
3848
+ Command line interfaces:
3849
+ pk_faiths_pd, pk_faith_pd, pk_fpd, pk_phylo_diversity
3850
+
3851
+ Usage:
3852
+ phykit faiths_pd <tree> -t/--taxa <taxa_file>
3853
+ [--exclude-root] [--json]
3854
+
3855
+ Options
3856
+ =====================================================
3857
+ <tree> first argument after
3858
+ function name should be
3859
+ a tree file
3860
+
3861
+ -t/--taxa file with one tip label per
3862
+ line defining the community
3863
+
3864
+ --exclude-root sum only branches of the
3865
+ induced subtree rooted at
3866
+ the community MRCA; by
3867
+ default the path up to the
3868
+ tree root is included
3869
+
3870
+ --json optional argument to output
3871
+ results as JSON
3872
+ """
3873
+ ),
3874
+ )
3875
+ parser.add_argument("tree", type=str, help=SUPPRESS)
3876
+ parser.add_argument(
3877
+ "-t", "--taxa", type=str, required=True, help=SUPPRESS, metavar=""
3878
+ )
3879
+ parser.add_argument(
3880
+ "--exclude-root",
3881
+ dest="exclude_root",
3882
+ action="store_true",
3883
+ help=SUPPRESS,
3884
+ )
3885
+ _add_json_argument(parser)
3886
+ _run_service(parser, argv, FaithsPD)
3887
+
3780
3888
  @staticmethod
3781
3889
  def patristic_distances(argv):
3782
3890
  parser = _new_parser(
@@ -6887,29 +6995,38 @@ class Phykit:
6887
6995
 
6888
6996
  Usage:
6889
6997
  phykit prune_tree <tree> <list_of_taxa> [-o/--output <output_file>
6890
- -k/--keep] [--json]
6998
+ -k/--keep] [--ignore-branch-labels] [--json]
6891
6999
 
6892
7000
  Options
6893
7001
  =====================================================
6894
- <tree> first argument after
7002
+ <tree> first argument after
6895
7003
  function name should be
6896
7004
  a tree file
6897
7005
 
6898
7006
  <list_of_taxa> single column file with the
6899
7007
  names of the tips to remove
6900
- from the phylogeny
7008
+ from the phylogeny
6901
7009
 
6902
7010
  -o/--output name of output file for the
6903
- pruned phylogeny.
6904
- Default output will have
7011
+ pruned phylogeny.
7012
+ Default output will have
6905
7013
  the same name as the input
6906
- file but with the suffix
7014
+ file but with the suffix
6907
7015
  ".pruned"
6908
7016
 
6909
7017
  -k/--keep optional argument. If used
6910
7018
  instead of pruning taxa in
6911
7019
  <list_of_taxa>, keep them
6912
7020
 
7021
+ --ignore-branch-labels optional argument. Strip
7022
+ HyPhy/aBSREL-style {{...}}
7023
+ branch labels (e.g.,
7024
+ "Hydlep{{FG}}") from tip
7025
+ names when matching
7026
+ against <list_of_taxa>.
7027
+ The labels are preserved
7028
+ in the output tree.
7029
+
6913
7030
  --json optional argument to output
6914
7031
  results as JSON
6915
7032
  """
@@ -6921,6 +7038,12 @@ class Phykit:
6921
7038
  parser.add_argument(
6922
7039
  "-k", "--keep", type=str2bool, nargs="?", default=False, help=SUPPRESS
6923
7040
  )
7041
+ parser.add_argument(
7042
+ "--ignore-branch-labels",
7043
+ action="store_true",
7044
+ default=False,
7045
+ help=SUPPRESS,
7046
+ )
6924
7047
  _add_json_argument(parser)
6925
7048
  _run_service(parser, argv, PruneTree)
6926
7049
 
@@ -9270,6 +9393,10 @@ def nearest_neighbor_interchange(argv=None):
9270
9393
  Phykit.nearest_neighbor_interchange(sys.argv[1:])
9271
9394
 
9272
9395
 
9396
+ def faiths_pd(argv=None):
9397
+ Phykit.faiths_pd(sys.argv[1:])
9398
+
9399
+
9273
9400
  def patristic_distances(argv=None):
9274
9401
  Phykit.patristic_distances(sys.argv[1:])
9275
9402
 
@@ -66,6 +66,7 @@ NeighborNet = _LazyServiceFactory("phykit.services.tree.neighbor_net", "Neighbor
66
66
  ConsensusTree = _LazyServiceFactory("phykit.services.tree.consensus_tree", "ConsensusTree")
67
67
  DVMC = _LazyServiceFactory("phykit.services.tree.dvmc", "DVMC")
68
68
  EvolutionaryRate = _LazyServiceFactory("phykit.services.tree.evolutionary_rate", "EvolutionaryRate")
69
+ FaithsPD = _LazyServiceFactory("phykit.services.tree.faiths_pd", "FaithsPD")
69
70
  HiddenParalogyCheck = _LazyServiceFactory("phykit.services.tree.hidden_paralogy_check", "HiddenParalogyCheck")
70
71
  InternalBranchStats = _LazyServiceFactory("phykit.services.tree.internal_branch_stats", "InternalBranchStats")
71
72
  InternodeLabeler = _LazyServiceFactory("phykit.services.tree.internode_labeler", "InternodeLabeler")
@@ -15,6 +15,7 @@ _EXPORTS = {
15
15
  "DiscordanceAsymmetry": "discordance_asymmetry",
16
16
  "EvolutionaryRate": "evolutionary_rate",
17
17
  "EvoTempoMap": "evo_tempo_map",
18
+ "FaithsPD": "faiths_pd",
18
19
  "FitDiscrete": "fit_discrete",
19
20
  "HiddenParalogyCheck": "hidden_paralogy_check",
20
21
  "Hybridization": "hybridization",
@@ -0,0 +1,148 @@
1
+ """
2
+ Faith's phylogenetic diversity (PD).
3
+
4
+ Given a tree and a community (list of tip labels), sum the branch
5
+ lengths of the minimum subtree connecting the community. When
6
+ ``include_root`` is True (default), the sum includes the path from
7
+ the community's MRCA up to the tree root, matching Faith (1992) and
8
+ ``picante::pd(..., include.root = TRUE)``.
9
+ """
10
+ from typing import Dict, List, Tuple
11
+
12
+ from .base import Tree
13
+ from ...errors import PhykitUserError
14
+ from ...helpers.files import read_single_column_file_to_list
15
+ from ...helpers.json_output import print_json
16
+
17
+
18
+ class FaithsPD(Tree):
19
+ def __init__(self, args) -> None:
20
+ parsed = self.process_args(args)
21
+ super().__init__(tree_file_path=parsed["tree_file_path"])
22
+ self.taxa_file = parsed["taxa_file"]
23
+ self.include_root = parsed["include_root"]
24
+ self.json_output = parsed["json_output"]
25
+
26
+ def process_args(self, args) -> Dict:
27
+ return dict(
28
+ tree_file_path=args.tree,
29
+ taxa_file=args.taxa,
30
+ include_root=not getattr(args, "exclude_root", False),
31
+ json_output=getattr(args, "json", False),
32
+ )
33
+
34
+ def run(self) -> None:
35
+ tree = self.read_tree_file()
36
+ taxa = self._load_taxa(self.taxa_file)
37
+ pd_value, n_tips = self.calculate_faiths_pd(
38
+ tree, taxa, include_root=self.include_root
39
+ )
40
+ pd_rounded = round(pd_value, 4)
41
+
42
+ if self.json_output:
43
+ print_json(
44
+ dict(
45
+ faiths_pd=pd_rounded,
46
+ n_taxa=n_tips,
47
+ include_root=self.include_root,
48
+ )
49
+ )
50
+ return
51
+ print(pd_rounded)
52
+
53
+ @staticmethod
54
+ def _load_taxa(taxa_file: str) -> List[str]:
55
+ raw = read_single_column_file_to_list(taxa_file)
56
+ seen = set()
57
+ taxa: List[str] = []
58
+ for name in raw:
59
+ if not name or name in seen:
60
+ continue
61
+ seen.add(name)
62
+ taxa.append(name)
63
+ if not taxa:
64
+ raise PhykitUserError(
65
+ [f"No taxa found in {taxa_file}."], code=2,
66
+ )
67
+ return taxa
68
+
69
+ def calculate_faiths_pd(
70
+ self, tree, taxa: List[str], include_root: bool = True,
71
+ ) -> Tuple[float, int]:
72
+ """Compute Faith's PD for ``taxa`` on ``tree``.
73
+
74
+ Returns (pd, n_taxa). n_taxa is the deduplicated community size.
75
+ """
76
+ self.validate_tree(tree, min_tips=2, require_branch_lengths=True,
77
+ context="Faith's PD")
78
+
79
+ seen: set = set()
80
+ deduped: List[str] = []
81
+ for name in taxa:
82
+ if name and name not in seen:
83
+ seen.add(name)
84
+ deduped.append(name)
85
+ taxa = deduped
86
+ if not taxa:
87
+ raise PhykitUserError(
88
+ ["Community must contain at least one taxon."], code=2,
89
+ )
90
+
91
+ tip_map = {t.name: t for t in tree.get_terminals()}
92
+ missing = [name for name in taxa if name not in tip_map]
93
+ if missing:
94
+ sample = ", ".join(sorted(missing)[:5])
95
+ suffix = f" ... ({len(missing)} total)" if len(missing) > 5 else ""
96
+ raise PhykitUserError(
97
+ [
98
+ "Taxa not found in tree:",
99
+ sample + suffix,
100
+ ],
101
+ code=2,
102
+ )
103
+
104
+ community_tips = [tip_map[name] for name in taxa]
105
+
106
+ if len(community_tips) == 1:
107
+ # Single-tip community: PD with include_root=True is the path
108
+ # length from the root to that tip; with include_root=False it
109
+ # is 0 (no induced subtree). picante returns NA for the latter;
110
+ # we return 0 for programmatic convenience.
111
+ if not include_root:
112
+ return 0.0, 1
113
+ path = tree.get_path(community_tips[0])
114
+ return (
115
+ sum((c.branch_length or 0.0) for c in path),
116
+ 1,
117
+ )
118
+
119
+ if include_root:
120
+ start_clade = tree.root
121
+ else:
122
+ mrca = tree.common_ancestor(community_tips)
123
+ # If the MRCA is the root, include.root=False is equivalent
124
+ # to include.root=True because there is no branch leading to
125
+ # the MRCA to subtract (matches picante).
126
+ start_clade = mrca
127
+
128
+ total = 0.0
129
+ seen_ids = set()
130
+ for tip in community_tips:
131
+ # Skip clades at or above start_clade. For include_root=True,
132
+ # start_clade is the root, which is never in get_path.
133
+ path = tree.get_path(tip)
134
+ if start_clade is not tree.root:
135
+ try:
136
+ idx = path.index(start_clade)
137
+ path = path[idx + 1:]
138
+ except ValueError:
139
+ # start_clade (MRCA) not on path - defensive; shouldn't happen
140
+ pass
141
+ for clade in path:
142
+ cid = id(clade)
143
+ if cid in seen_ids:
144
+ continue
145
+ seen_ids.add(cid)
146
+ total += clade.branch_length or 0.0
147
+
148
+ return total, len(community_tips)