phykit 2.1.92__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.92 → phykit-2.1.93}/PKG-INFO +1 -1
  2. {phykit-2.1.92 → phykit-2.1.93}/phykit/phykit.py +53 -9
  3. phykit-2.1.93/phykit/services/tree/nearest_neighbor_interchange.py +313 -0
  4. phykit-2.1.93/phykit/version.py +1 -0
  5. {phykit-2.1.92 → phykit-2.1.93}/phykit.egg-info/PKG-INFO +1 -1
  6. phykit-2.1.92/phykit/services/tree/nearest_neighbor_interchange.py +0 -155
  7. phykit-2.1.92/phykit/version.py +0 -1
  8. {phykit-2.1.92 → phykit-2.1.93}/LICENSE.md +0 -0
  9. {phykit-2.1.92 → phykit-2.1.93}/README.md +0 -0
  10. {phykit-2.1.92 → phykit-2.1.93}/phykit/__init__.py +0 -0
  11. {phykit-2.1.92 → phykit-2.1.93}/phykit/__main__.py +0 -0
  12. {phykit-2.1.92 → phykit-2.1.93}/phykit/cli_registry.py +0 -0
  13. {phykit-2.1.92 → phykit-2.1.93}/phykit/errors.py +0 -0
  14. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/__init__.py +0 -0
  15. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/boolean_argument_parsing.py +0 -0
  16. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/caching.py +0 -0
  17. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/circular_layout.py +0 -0
  18. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/color_annotations.py +0 -0
  19. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/discrete_models.py +0 -0
  20. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/files.py +0 -0
  21. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/geological_timescale.py +0 -0
  22. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/json_output.py +0 -0
  23. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/parallel.py +0 -0
  24. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/parsimony_utils.py +0 -0
  25. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/pgls_utils.py +0 -0
  26. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/plot_config.py +0 -0
  27. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/quartet_utils.py +0 -0
  28. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/stats_summary.py +0 -0
  29. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/streaming.py +0 -0
  30. {phykit-2.1.92 → phykit-2.1.93}/phykit/helpers/trait_parsing.py +0 -0
  31. {phykit-2.1.92 → phykit-2.1.93}/phykit/service_factories.py +0 -0
  32. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/__init__.py +0 -0
  33. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/__init__.py +0 -0
  34. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/alignment_entropy.py +0 -0
  35. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/alignment_length.py +0 -0
  36. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  37. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  38. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/alignment_recoding.py +0 -0
  39. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/alignment_subsample.py +0 -0
  40. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/base.py +0 -0
  41. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/column_score.py +0 -0
  42. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/composition_per_taxon.py +0 -0
  43. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  44. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  45. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/dfoil.py +0 -0
  46. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/dna_threader.py +0 -0
  47. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/dstatistic.py +0 -0
  48. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  49. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/faidx.py +0 -0
  50. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/gc_content.py +0 -0
  51. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/identity_matrix.py +0 -0
  52. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/mask_alignment.py +0 -0
  53. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/occupancy_filter.py +0 -0
  54. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  55. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/pairwise_identity.py +0 -0
  56. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  57. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/phylo_gwas.py +0 -0
  58. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  59. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/rcv.py +0 -0
  60. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/rcvt.py +0 -0
  61. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  62. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  63. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/taxon_groups.py +0 -0
  64. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/alignment/variable_sites.py +0 -0
  65. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/base.py +0 -0
  66. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/__init__.py +0 -0
  67. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/ancestral_reconstruction.py +0 -0
  68. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/base.py +0 -0
  69. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/bipartition_support_stats.py +0 -0
  70. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/branch_length_multiplier.py +0 -0
  71. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/character_map.py +0 -0
  72. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/chronogram.py +0 -0
  73. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/collapse_branches.py +0 -0
  74. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/concordance_asr.py +0 -0
  75. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/consensus_network.py +0 -0
  76. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/consensus_tree.py +0 -0
  77. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/cont_map.py +0 -0
  78. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/cophylo.py +0 -0
  79. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  80. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/density_map.py +0 -0
  81. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/discordance_asymmetry.py +0 -0
  82. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/dtt.py +0 -0
  83. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/dvmc.py +0 -0
  84. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/evo_tempo_map.py +0 -0
  85. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/evolutionary_rate.py +0 -0
  86. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/faiths_pd.py +0 -0
  87. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/fit_continuous.py +0 -0
  88. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/fit_discrete.py +0 -0
  89. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  90. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/hybridization.py +0 -0
  91. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/independent_contrasts.py +0 -0
  92. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/internal_branch_stats.py +0 -0
  93. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/internode_labeler.py +0 -0
  94. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/kf_distance.py +0 -0
  95. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  96. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/lb_score.py +0 -0
  97. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/ltt.py +0 -0
  98. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/monophyly_check.py +0 -0
  99. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/neighbor_net.py +0 -0
  100. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/network_signal.py +0 -0
  101. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/ou_shift_detection.py +0 -0
  102. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/ouwie.py +0 -0
  103. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/parsimony_score.py +0 -0
  104. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/patristic_distances.py +0 -0
  105. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/phenogram.py +0 -0
  106. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/phylo_anova.py +0 -0
  107. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/phylo_heatmap.py +0 -0
  108. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/phylo_impute.py +0 -0
  109. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/phylo_logistic.py +0 -0
  110. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/phylo_path.py +0 -0
  111. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/phylogenetic_glm.py +0 -0
  112. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  113. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/phylogenetic_regression.py +0 -0
  114. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/phylogenetic_signal.py +0 -0
  115. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/phylomorphospace.py +0 -0
  116. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/polytomy_test.py +0 -0
  117. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/print_tree.py +0 -0
  118. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/prune_tree.py +0 -0
  119. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/quartet_network.py +0 -0
  120. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/quartet_pie.py +0 -0
  121. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/rate_heterogeneity.py +0 -0
  122. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/relative_rate_test.py +0 -0
  123. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/rename_tree_tips.py +0 -0
  124. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/rf_distance.py +0 -0
  125. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/root_tree.py +0 -0
  126. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/saturation.py +0 -0
  127. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/simmap_summary.py +0 -0
  128. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/spectral_discordance.py +0 -0
  129. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/spr.py +0 -0
  130. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/spurious_sequence.py +0 -0
  131. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/stochastic_character_map.py +0 -0
  132. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/terminal_branch_stats.py +0 -0
  133. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/threshold_model.py +0 -0
  134. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/tip_labels.py +0 -0
  135. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  136. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  137. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/total_tree_length.py +0 -0
  138. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/trait_correlation.py +0 -0
  139. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/trait_rate_map.py +0 -0
  140. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/transfer_annotations.py +0 -0
  141. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/tree_space.py +0 -0
  142. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/treeness.py +0 -0
  143. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/treeness_over_rcv.py +0 -0
  144. {phykit-2.1.92 → phykit-2.1.93}/phykit/services/tree/vcv_utils.py +0 -0
  145. {phykit-2.1.92 → phykit-2.1.93}/phykit.egg-info/SOURCES.txt +0 -0
  146. {phykit-2.1.92 → phykit-2.1.93}/phykit.egg-info/dependency_links.txt +0 -0
  147. {phykit-2.1.92 → phykit-2.1.93}/phykit.egg-info/entry_points.txt +0 -0
  148. {phykit-2.1.92 → phykit-2.1.93}/phykit.egg-info/requires.txt +0 -0
  149. {phykit-2.1.92 → phykit-2.1.93}/phykit.egg-info/top_level.txt +0 -0
  150. {phykit-2.1.92 → phykit-2.1.93}/setup.cfg +0 -0
  151. {phykit-2.1.92 → phykit-2.1.93}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: phykit
3
- Version: 2.1.92
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
@@ -3742,10 +3742,19 @@ class Phykit:
3742
3742
  f"""\
3743
3743
  {help_header}
3744
3744
 
3745
- Generate all nearest neighbor interchange moves for a binary
3746
- rooted tree.
3745
+ Generate nearest neighbor interchange (NNI) moves for a
3746
+ binary rooted tree.
3747
3747
 
3748
- 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.
3749
3758
 
3750
3759
  Aliases:
3751
3760
  nearest_neighbor_interchange, nni
@@ -3753,22 +3762,49 @@ class Phykit:
3753
3762
  pk_nearest_neighbor_interchange, pk_nni
3754
3763
 
3755
3764
  Usage:
3756
- 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]
3757
3768
 
3758
3769
  Options
3759
3770
  =====================================================
3760
- <tree> first argument after
3771
+ <tree> first argument after
3761
3772
  function name should be
3762
3773
  a tree file
3763
3774
 
3764
3775
  -o/--output name of output file that will
3765
3776
  contain all trees with the
3766
3777
  nearest neighbor interchange
3767
- moves.
3768
- Default output will have
3778
+ moves.
3779
+ Default output will have
3769
3780
  the same name as the input
3770
- file but with the suffix
3771
- ".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.
3772
3808
 
3773
3809
  --json optional argument to output
3774
3810
  results as JSON
@@ -3777,6 +3813,14 @@ class Phykit:
3777
3813
  )
3778
3814
  parser.add_argument("tree", type=str, help=SUPPRESS)
3779
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
+ )
3780
3824
  _add_json_argument(parser)
3781
3825
  _run_service(parser, argv, NearestNeighborInterchange)
3782
3826
 
@@ -0,0 +1,313 @@
1
+ import re
2
+ from typing import Dict, List, Optional, Tuple
3
+ import pickle
4
+
5
+ from Bio import Phylo
6
+ from Bio.Phylo import Newick
7
+
8
+ from .base import Tree
9
+ from ...errors import PhykitUserError
10
+ from ...helpers.json_output import print_json
11
+
12
+
13
+ BranchSpec = Tuple[str, List[str]]
14
+
15
+
16
+ class NearestNeighborInterchange(Tree):
17
+ def __init__(self, args) -> None:
18
+ parsed = self.process_args(args)
19
+ super().__init__(
20
+ tree_file_path=parsed["tree_file_path"],
21
+ output_file_path=parsed["output_file_path"],
22
+ )
23
+ self.json_output = parsed["json_output"]
24
+ self.branch = parsed["branch"]
25
+ self.branches_file = parsed["branches_file"]
26
+ self.no_input_tree = parsed["no_input_tree"]
27
+
28
+ def run(self) -> None:
29
+ tree = self.read_tree_file()
30
+
31
+ branch_specs = self._resolve_branch_specs()
32
+
33
+ if branch_specs:
34
+ output_trees, branch_report = self._generate_targeted_nnis(
35
+ tree, branch_specs
36
+ )
37
+ else:
38
+ output_trees = self.get_neighbors(tree)
39
+ branch_report = None
40
+
41
+ trees_to_write = output_trees if self.no_input_tree else [tree, *output_trees]
42
+ Phylo.write(trees_to_write, self.output_file_path, "newick")
43
+
44
+ if self.json_output:
45
+ payload = dict(
46
+ input_tree=self.tree_file_path,
47
+ total_trees=len(trees_to_write),
48
+ nni_neighbors=len(output_trees),
49
+ output_file=self.output_file_path,
50
+ )
51
+ if branch_report is not None:
52
+ payload["branches"] = branch_report
53
+ print_json(payload)
54
+
55
+ def process_args(self, args) -> Dict[str, str]:
56
+ tree_file_path = args.tree
57
+ output_file_path = \
58
+ f"{args.output}" if args.output else f"{tree_file_path}.nnis"
59
+
60
+ branch = None
61
+ raw_branch = getattr(args, "branch", None)
62
+ if raw_branch:
63
+ parts = [p.strip() for p in re.split(r"[,\t]", raw_branch) if p.strip()]
64
+ if len(parts) != 2:
65
+ raise PhykitUserError(
66
+ [
67
+ "--branch must specify exactly two taxa separated by a "
68
+ "comma (e.g., --branch A,B)"
69
+ ],
70
+ code=2,
71
+ )
72
+ branch = tuple(parts)
73
+
74
+ return dict(
75
+ tree_file_path=tree_file_path,
76
+ output_file_path=output_file_path,
77
+ branch=branch,
78
+ branches_file=getattr(args, "branches", None),
79
+ no_input_tree=getattr(args, "no_input_tree", False),
80
+ json_output=getattr(args, "json", False),
81
+ )
82
+
83
+ def _resolve_branch_specs(self) -> List[BranchSpec]:
84
+ specs: List[BranchSpec] = []
85
+ if self.branch:
86
+ t1, t2 = self.branch
87
+ specs.append((f"{t1}|{t2}", [t1, t2]))
88
+ if self.branches_file:
89
+ with open(self.branches_file) as fh:
90
+ for i, raw in enumerate(fh, start=1):
91
+ line = raw.strip()
92
+ if not line or line.startswith("#"):
93
+ continue
94
+ parts = [p.strip() for p in re.split(r"[,\t]", line) if p.strip()]
95
+ if len(parts) < 2:
96
+ raise PhykitUserError(
97
+ [
98
+ f"--branches file line {i}: need at least two "
99
+ "taxa per line (comma- or tab-separated)"
100
+ ],
101
+ code=2,
102
+ )
103
+ if len(parts) >= 3:
104
+ label, t1, t2 = parts[0], parts[1], parts[2]
105
+ else:
106
+ t1, t2 = parts[0], parts[1]
107
+ label = f"{t1}|{t2}"
108
+ specs.append((label, [t1, t2]))
109
+ return specs
110
+
111
+ def _generate_targeted_nnis(
112
+ self,
113
+ tree: Newick.Tree,
114
+ branch_specs: List[BranchSpec],
115
+ ) -> Tuple[List[Newick.Tree], List[Dict]]:
116
+ tip_names = {term.name for term in tree.get_terminals()}
117
+ output_trees: List[Newick.Tree] = []
118
+ report: List[Dict] = []
119
+
120
+ for label, taxa in branch_specs:
121
+ missing = [t for t in taxa if t not in tip_names]
122
+ if missing:
123
+ raise PhykitUserError(
124
+ [
125
+ f"branch '{label}': taxa not found in tree: "
126
+ + ", ".join(missing)
127
+ ],
128
+ code=2,
129
+ )
130
+
131
+ working = self._fast_tree_copy(tree)
132
+ parents = self._build_parent_map(working)
133
+ target = working.common_ancestor(taxa)
134
+
135
+ if target is working.root:
136
+ raise PhykitUserError(
137
+ [
138
+ f"branch '{label}': MRCA of {taxa[0]} and {taxa[1]} is "
139
+ "the root; no internal branch above to rearrange"
140
+ ],
141
+ code=2,
142
+ )
143
+ if target.is_terminal():
144
+ raise PhykitUserError(
145
+ [
146
+ f"branch '{label}': MRCA of {taxa[0]} and {taxa[1]} is "
147
+ "a terminal; NNI requires an internal branch"
148
+ ],
149
+ code=2,
150
+ )
151
+
152
+ nnis = self._nnis_around_branch(working, target, parents)
153
+ output_trees.extend(nnis)
154
+ report.append(
155
+ dict(label=label, taxa=list(taxa), n_nnis=len(nnis))
156
+ )
157
+
158
+ return output_trees, report
159
+
160
+ def _fast_tree_copy(self, tree: Newick.Tree) -> Newick.Tree:
161
+ """Fast tree copying using pickle instead of deep copy."""
162
+ return pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
163
+
164
+ def _build_parent_map(self, tree: Newick.Tree) -> Dict:
165
+ parents = {}
166
+ for clade in tree.find_clades():
167
+ if clade != tree.root:
168
+ node_path = tree.get_path(clade)
169
+ if len(node_path) == 1:
170
+ parents[clade] = tree.root
171
+ else:
172
+ parents[clade] = node_path[-2]
173
+ return parents
174
+
175
+ def _nnis_around_branch(
176
+ self,
177
+ tree: Newick.Tree,
178
+ clade,
179
+ parents: Dict,
180
+ ) -> List[Newick.Tree]:
181
+ """Generate the 2 NNI rearrangements around the branch leading to clade.
182
+
183
+ Mutates the tree in place but restores the original topology before
184
+ returning so the caller can request additional rearrangements on the
185
+ same tree.
186
+ """
187
+ if clade.is_terminal():
188
+ return []
189
+
190
+ root_children = tree.root.clades
191
+ is_root_child = clade in root_children
192
+ if is_root_child:
193
+ other = (
194
+ root_children[1] if clade is root_children[0] else root_children[0]
195
+ )
196
+ if other.is_terminal():
197
+ # Implicit root edge can only be rearranged if both root
198
+ # children are internal — otherwise there is no subtree on
199
+ # the sister side to swap.
200
+ return []
201
+ return self._nnis_root_edge(tree)
202
+
203
+ parent = parents.get(clade)
204
+ if parent is None:
205
+ return []
206
+ return self._nnis_internal_edge(tree, clade, parent)
207
+
208
+ def _nnis_root_edge(self, tree: Newick.Tree) -> List[Newick.Tree]:
209
+ neighbors: List[Newick.Tree] = []
210
+ left = tree.root.clades[0]
211
+ right = tree.root.clades[1]
212
+ left_right = left.clades[1]
213
+ right_left = right.clades[0]
214
+ right_right = right.clades[1]
215
+
216
+ # neighbor 1: swap left.clades[1] with right.clades[1]
217
+ del left.clades[1]
218
+ del right.clades[1]
219
+ left.clades.append(right_right)
220
+ right.clades.append(left_right)
221
+ neighbors.append(self._fast_tree_copy(tree))
222
+
223
+ # neighbor 2: swap left.clades[1] with right.clades[0]
224
+ del left.clades[1]
225
+ del right.clades[0]
226
+ left.clades.append(right_left)
227
+ right.clades.append(right_right)
228
+ neighbors.append(self._fast_tree_copy(tree))
229
+
230
+ # restore original
231
+ del left.clades[1]
232
+ del right.clades[0]
233
+ left.clades.append(left_right)
234
+ right.clades.insert(0, right_left)
235
+ return neighbors
236
+
237
+ def _nnis_internal_edge(
238
+ self,
239
+ tree: Newick.Tree,
240
+ clade,
241
+ parent,
242
+ ) -> List[Newick.Tree]:
243
+ neighbors: List[Newick.Tree] = []
244
+ left = clade.clades[0]
245
+ right = clade.clades[1]
246
+
247
+ if clade is parent.clades[0]:
248
+ sister = parent.clades[1]
249
+ # neighbor 1: swap clade.clades[1] with sister
250
+ del parent.clades[1]
251
+ del clade.clades[1]
252
+ parent.clades.append(right)
253
+ clade.clades.append(sister)
254
+ neighbors.append(self._fast_tree_copy(tree))
255
+ # neighbor 2: swap clade.clades[0] with sister (current parent.clades[1] is `right`)
256
+ del parent.clades[1]
257
+ del clade.clades[0]
258
+ parent.clades.append(left)
259
+ clade.clades.append(right)
260
+ neighbors.append(self._fast_tree_copy(tree))
261
+ # restore
262
+ del parent.clades[1]
263
+ del clade.clades[0]
264
+ parent.clades.append(sister)
265
+ clade.clades.insert(0, left)
266
+ else:
267
+ sister = parent.clades[0]
268
+ # neighbor 1
269
+ del parent.clades[0]
270
+ del clade.clades[1]
271
+ parent.clades.insert(0, right)
272
+ clade.clades.append(sister)
273
+ neighbors.append(self._fast_tree_copy(tree))
274
+ # neighbor 2
275
+ del parent.clades[0]
276
+ del clade.clades[0]
277
+ parent.clades.insert(0, left)
278
+ clade.clades.append(right)
279
+ neighbors.append(self._fast_tree_copy(tree))
280
+ # restore
281
+ del parent.clades[0]
282
+ del clade.clades[0]
283
+ parent.clades.insert(0, sister)
284
+ clade.clades.insert(0, left)
285
+
286
+ return neighbors
287
+
288
+ def get_neighbors(
289
+ self,
290
+ tree: Newick.Tree
291
+ ) -> List[Newick.Tree]:
292
+ ### This code is from BioPython (so is this comment)
293
+ # Get all neighbor trees of the given tree (PRIVATE).
294
+ # Currently only for binary rooted trees.
295
+ ###
296
+ parents = self._build_parent_map(tree)
297
+ neighbors: List[Newick.Tree] = []
298
+ root_childs = []
299
+ for clade in tree.get_nonterminals(order="level"):
300
+ if clade == tree.root:
301
+ left = clade.clades[0]
302
+ right = clade.clades[1]
303
+ root_childs.append(left)
304
+ root_childs.append(right)
305
+ if not left.is_terminal() and not right.is_terminal():
306
+ neighbors.extend(self._nnis_root_edge(tree))
307
+ elif clade in root_childs:
308
+ continue
309
+ else:
310
+ parent = parents[clade]
311
+ neighbors.extend(self._nnis_internal_edge(tree, clade, parent))
312
+
313
+ return neighbors
@@ -0,0 +1 @@
1
+ __version__ = "2.1.93"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: phykit
3
- Version: 2.1.92
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
@@ -1,155 +0,0 @@
1
- from typing import Dict, List
2
- import pickle
3
-
4
- from Bio import Phylo
5
- from Bio.Phylo import Newick
6
-
7
- from .base import Tree
8
- from ...helpers.json_output import print_json
9
-
10
-
11
- class NearestNeighborInterchange(Tree):
12
- def __init__(self, args) -> None:
13
- parsed = self.process_args(args)
14
- super().__init__(
15
- tree_file_path=parsed["tree_file_path"],
16
- output_file_path=parsed["output_file_path"],
17
- )
18
- self.json_output = parsed["json_output"]
19
-
20
- def run(self) -> None:
21
- tree = self.read_tree_file()
22
-
23
- # Use standard neighbor generation with optimized copying
24
- all_nnis = [tree]
25
- all_nnis.extend(self.get_neighbors(tree))
26
- Phylo.write(all_nnis, self.output_file_path, "newick")
27
- if self.json_output:
28
- print_json(
29
- dict(
30
- input_tree=self.tree_file_path,
31
- total_trees=len(all_nnis),
32
- nni_neighbors=max(0, len(all_nnis) - 1),
33
- output_file=self.output_file_path,
34
- )
35
- )
36
-
37
- def process_args(self, args) -> Dict[str, str]:
38
- tree_file_path = args.tree
39
- output_file_path = \
40
- f"{args.output}" if args.output else f"{tree_file_path}.nnis"
41
-
42
- return dict(
43
- tree_file_path=tree_file_path,
44
- output_file_path=output_file_path,
45
- json_output=getattr(args, "json", False),
46
- )
47
-
48
- def _fast_tree_copy(self, tree: Newick.Tree) -> Newick.Tree:
49
- """Fast tree copying using pickle instead of deep copy."""
50
- return pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
51
-
52
- def get_neighbors(
53
- self,
54
- tree: Newick.Tree
55
- ) -> List[Newick.Tree]:
56
- ### This code is from BioPython (so is this comment)
57
- # Get all neighbor trees of the given tree (PRIVATE).
58
- # Currently only for binary rooted trees.
59
- ###
60
- # make child to parent dict
61
- parents = {}
62
- for clade in tree.find_clades():
63
- if clade != tree.root:
64
- node_path = tree.get_path(clade)
65
- # cannot get the parent if the parent is root. Bug?
66
- if len(node_path) == 1:
67
- parents[clade] = tree.root
68
- else:
69
- parents[clade] = node_path[-2]
70
- neighbors = []
71
- root_childs = []
72
- for clade in tree.get_nonterminals(order="level"):
73
- if clade == tree.root:
74
- left = clade.clades[0]
75
- right = clade.clades[1]
76
- root_childs.append(left)
77
- root_childs.append(right)
78
- if not left.is_terminal() and not right.is_terminal():
79
- # make changes around the left_left clade
80
- # left_left = left.clades[0]
81
- left_right = left.clades[1]
82
- right_left = right.clades[0]
83
- right_right = right.clades[1]
84
- # neighbor 1 (left_left + right_right)
85
- del left.clades[1]
86
- del right.clades[1]
87
- left.clades.append(right_right)
88
- right.clades.append(left_right)
89
- temp_tree = self._fast_tree_copy(tree)
90
- neighbors.append(temp_tree)
91
- # neighbor 2 (left_left + right_left)
92
- del left.clades[1]
93
- del right.clades[0]
94
- left.clades.append(right_left)
95
- right.clades.append(right_right)
96
- temp_tree = self._fast_tree_copy(tree)
97
- neighbors.append(temp_tree)
98
- # change back (left_left + left_right)
99
- del left.clades[1]
100
- del right.clades[0]
101
- left.clades.append(left_right)
102
- right.clades.insert(0, right_left)
103
- elif clade in root_childs:
104
- # skip root child
105
- continue
106
- else:
107
- # method for other clades
108
- # make changes around the parent clade
109
- left = clade.clades[0]
110
- right = clade.clades[1]
111
- parent = parents[clade]
112
- if clade == parent.clades[0]:
113
- sister = parent.clades[1]
114
- # neighbor 1 (parent + right)
115
- del parent.clades[1]
116
- del clade.clades[1]
117
- parent.clades.append(right)
118
- clade.clades.append(sister)
119
- temp_tree = self._fast_tree_copy(tree)
120
- neighbors.append(temp_tree)
121
- # neighbor 2 (parent + left)
122
- del parent.clades[1]
123
- del clade.clades[0]
124
- parent.clades.append(left)
125
- clade.clades.append(right)
126
- temp_tree = self._fast_tree_copy(tree)
127
- neighbors.append(temp_tree)
128
- # change back (parent + sister)
129
- del parent.clades[1]
130
- del clade.clades[0]
131
- parent.clades.append(sister)
132
- clade.clades.insert(0, left)
133
- else:
134
- sister = parent.clades[0]
135
- # neighbor 1 (parent + right)
136
- del parent.clades[0]
137
- del clade.clades[1]
138
- parent.clades.insert(0, right)
139
- clade.clades.append(sister)
140
- temp_tree = self._fast_tree_copy(tree)
141
- neighbors.append(temp_tree)
142
- # neighbor 2 (parent + left)
143
- del parent.clades[0]
144
- del clade.clades[0]
145
- parent.clades.insert(0, left)
146
- clade.clades.append(right)
147
- temp_tree = self._fast_tree_copy(tree)
148
- neighbors.append(temp_tree)
149
- # change back (parent + sister)
150
- del parent.clades[0]
151
- del clade.clades[0]
152
- parent.clades.insert(0, sister)
153
- clade.clades.insert(0, left)
154
-
155
- return neighbors
@@ -1 +0,0 @@
1
- __version__ = "2.1.92"
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
File without changes