phykit 2.1.72__tar.gz → 2.1.74__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 (135) hide show
  1. {phykit-2.1.72 → phykit-2.1.74}/PKG-INFO +1 -1
  2. {phykit-2.1.72 → phykit-2.1.74}/phykit/phykit.py +9 -2
  3. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/ancestral_reconstruction.py +21 -13
  4. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/concordance_asr.py +10 -6
  5. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/consensus_network.py +91 -11
  6. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/cont_map.py +10 -6
  7. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/density_map.py +10 -6
  8. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/discordance_asymmetry.py +49 -33
  9. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phenogram.py +8 -6
  10. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/rate_heterogeneity.py +10 -6
  11. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/stochastic_character_map.py +10 -6
  12. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/trait_rate_map.py +12 -8
  13. phykit-2.1.74/phykit/version.py +1 -0
  14. {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/PKG-INFO +1 -1
  15. phykit-2.1.72/phykit/version.py +0 -1
  16. {phykit-2.1.72 → phykit-2.1.74}/LICENSE.md +0 -0
  17. {phykit-2.1.72 → phykit-2.1.74}/README.md +0 -0
  18. {phykit-2.1.72 → phykit-2.1.74}/phykit/__init__.py +0 -0
  19. {phykit-2.1.72 → phykit-2.1.74}/phykit/__main__.py +0 -0
  20. {phykit-2.1.72 → phykit-2.1.74}/phykit/cli_registry.py +0 -0
  21. {phykit-2.1.72 → phykit-2.1.74}/phykit/errors.py +0 -0
  22. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/__init__.py +0 -0
  23. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/boolean_argument_parsing.py +0 -0
  24. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/caching.py +0 -0
  25. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/circular_layout.py +0 -0
  26. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/color_annotations.py +0 -0
  27. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/discrete_models.py +0 -0
  28. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/files.py +0 -0
  29. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/json_output.py +0 -0
  30. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/parallel.py +0 -0
  31. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/parsimony_utils.py +0 -0
  32. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/plot_config.py +0 -0
  33. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/quartet_utils.py +0 -0
  34. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/stats_summary.py +0 -0
  35. {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/streaming.py +0 -0
  36. {phykit-2.1.72 → phykit-2.1.74}/phykit/service_factories.py +0 -0
  37. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/__init__.py +0 -0
  38. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/__init__.py +0 -0
  39. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_entropy.py +0 -0
  40. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_length.py +0 -0
  41. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  42. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  43. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_recoding.py +0 -0
  44. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_subsample.py +0 -0
  45. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/base.py +0 -0
  46. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/column_score.py +0 -0
  47. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/composition_per_taxon.py +0 -0
  48. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  49. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  50. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/dfoil.py +0 -0
  51. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/dna_threader.py +0 -0
  52. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/dstatistic.py +0 -0
  53. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  54. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/faidx.py +0 -0
  55. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/gc_content.py +0 -0
  56. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/identity_matrix.py +0 -0
  57. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/mask_alignment.py +0 -0
  58. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  59. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/pairwise_identity.py +0 -0
  60. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  61. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/phylo_gwas.py +0 -0
  62. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  63. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/rcv.py +0 -0
  64. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/rcvt.py +0 -0
  65. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  66. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  67. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/variable_sites.py +0 -0
  68. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/base.py +0 -0
  69. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/__init__.py +0 -0
  70. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/base.py +0 -0
  71. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/bipartition_support_stats.py +0 -0
  72. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/branch_length_multiplier.py +0 -0
  73. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/character_map.py +0 -0
  74. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/collapse_branches.py +0 -0
  75. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/consensus_tree.py +0 -0
  76. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/cophylo.py +0 -0
  77. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  78. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/dvmc.py +0 -0
  79. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/evo_tempo_map.py +0 -0
  80. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/evolutionary_rate.py +0 -0
  81. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/fit_continuous.py +0 -0
  82. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/fit_discrete.py +0 -0
  83. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  84. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/independent_contrasts.py +0 -0
  85. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/internal_branch_stats.py +0 -0
  86. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/internode_labeler.py +0 -0
  87. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/kf_distance.py +0 -0
  88. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  89. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/lb_score.py +0 -0
  90. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/ltt.py +0 -0
  91. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/monophyly_check.py +0 -0
  92. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  93. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/network_signal.py +0 -0
  94. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/ou_shift_detection.py +0 -0
  95. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/ouwie.py +0 -0
  96. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/parsimony_score.py +0 -0
  97. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/patristic_distances.py +0 -0
  98. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylo_heatmap.py +0 -0
  99. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylo_impute.py +0 -0
  100. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylo_logistic.py +0 -0
  101. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylogenetic_glm.py +0 -0
  102. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  103. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylogenetic_regression.py +0 -0
  104. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylogenetic_signal.py +0 -0
  105. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylomorphospace.py +0 -0
  106. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/polytomy_test.py +0 -0
  107. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/print_tree.py +0 -0
  108. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/prune_tree.py +0 -0
  109. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/quartet_network.py +0 -0
  110. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/quartet_pie.py +0 -0
  111. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/relative_rate_test.py +0 -0
  112. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/rename_tree_tips.py +0 -0
  113. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/rf_distance.py +0 -0
  114. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/root_tree.py +0 -0
  115. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/saturation.py +0 -0
  116. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/spectral_discordance.py +0 -0
  117. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/spurious_sequence.py +0 -0
  118. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/terminal_branch_stats.py +0 -0
  119. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/threshold_model.py +0 -0
  120. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/tip_labels.py +0 -0
  121. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  122. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  123. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/total_tree_length.py +0 -0
  124. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/trait_correlation.py +0 -0
  125. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/tree_space.py +0 -0
  126. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/treeness.py +0 -0
  127. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/treeness_over_rcv.py +0 -0
  128. {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/vcv_utils.py +0 -0
  129. {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/SOURCES.txt +0 -0
  130. {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/dependency_links.txt +0 -0
  131. {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/entry_points.txt +0 -0
  132. {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/requires.txt +0 -0
  133. {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/top_level.txt +0 -0
  134. {phykit-2.1.72 → phykit-2.1.74}/setup.cfg +0 -0
  135. {phykit-2.1.72 → phykit-2.1.74}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.72
3
+ Version: 2.1.74
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -5821,8 +5821,8 @@ class Phykit:
5821
5821
  parser.add_argument(
5822
5822
  "--missing-taxa",
5823
5823
  type=str,
5824
- choices=["error", "shared"],
5825
- default="error",
5824
+ choices=["allow", "error", "shared"],
5825
+ default="allow",
5826
5826
  required=False,
5827
5827
  help=SUPPRESS,
5828
5828
  )
@@ -7598,6 +7598,9 @@ class Phykit:
7598
7598
 
7599
7599
  -v/--verbose print per-branch details
7600
7600
 
7601
+ --annotate show gCF values on the
7602
+ plot near each branch
7603
+
7601
7604
  --fig-width figure width in inches
7602
7605
  (auto-scaled if omitted)
7603
7606
 
@@ -7662,6 +7665,10 @@ class Phykit:
7662
7665
  parser.add_argument(
7663
7666
  "-v", "--verbose", action="store_true", required=False, help=SUPPRESS
7664
7667
  )
7668
+ parser.add_argument(
7669
+ "--annotate", action="store_true", required=False, default=False,
7670
+ help=SUPPRESS,
7671
+ )
7665
7672
  add_plot_arguments(parser)
7666
7673
  _add_json_argument(parser)
7667
7674
  _run_service(parser, argv, DiscordanceAsymmetry)
@@ -939,7 +939,9 @@ class AncestralReconstruction(Tree):
939
939
 
940
940
  # Tip labels
941
941
  max_x = max(node_x.values()) if node_x else 1.0
942
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
942
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
943
+ if label_fontsize > 0:
944
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
943
945
 
944
946
  # Apply color annotations (range + label only; branches are trait-colored)
945
947
  if self.plot_config.color_file:
@@ -1012,11 +1014,13 @@ class AncestralReconstruction(Tree):
1012
1014
  # Tip labels
1013
1015
  max_x = max(node_x.values()) if node_x else 0
1014
1016
  offset = max_x * 0.02
1015
- for tip in tips:
1016
- ax.text(
1017
- node_x[id(tip)] + offset, node_y[id(tip)],
1018
- tip.name, va="center", fontsize=9,
1019
- )
1017
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
1018
+ if label_fontsize > 0:
1019
+ for tip in tips:
1020
+ ax.text(
1021
+ node_x[id(tip)] + offset, node_y[id(tip)],
1022
+ tip.name, va="center", fontsize=label_fontsize,
1023
+ )
1020
1024
 
1021
1025
  # Apply color annotations (range + label only; branches are trait-colored)
1022
1026
  if self.plot_config.color_file:
@@ -1655,7 +1659,9 @@ class AncestralReconstruction(Tree):
1655
1659
  start_angle += sweep
1656
1660
 
1657
1661
  # Tip labels
1658
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
1662
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
1663
+ if label_fontsize > 0:
1664
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
1659
1665
 
1660
1666
  # Apply color annotations
1661
1667
  if self.plot_config.color_file:
@@ -1750,12 +1756,14 @@ class AncestralReconstruction(Tree):
1750
1756
  # Tip labels with state color
1751
1757
  max_x_val = max(node_x.values()) if node_x else 0
1752
1758
  offset = max_x_val * 0.02
1753
- for tip in tips:
1754
- color = state_colors.get(tip_states.get(tip.name, ""), "black")
1755
- ax.text(
1756
- node_x[id(tip)] + offset, node_y[id(tip)],
1757
- tip.name, va="center", fontsize=9, color=color,
1758
- )
1759
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
1760
+ if label_fontsize > 0:
1761
+ for tip in tips:
1762
+ color = state_colors.get(tip_states.get(tip.name, ""), "black")
1763
+ ax.text(
1764
+ node_x[id(tip)] + offset, node_y[id(tip)],
1765
+ tip.name, va="center", fontsize=label_fontsize, color=color,
1766
+ )
1759
1767
 
1760
1768
  # Apply color annotations
1761
1769
  if self.plot_config.color_file:
@@ -844,7 +844,9 @@ class ConcordanceAsr(Tree):
844
844
 
845
845
  # Tip labels
846
846
  max_x = max(node_x.values()) if node_x else 1.0
847
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
847
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
848
+ if label_fontsize > 0:
849
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
848
850
 
849
851
  # Apply color annotations (range + label only; branches are trait-colored)
850
852
  if self.plot_config.color_file:
@@ -915,11 +917,13 @@ class ConcordanceAsr(Tree):
915
917
  # Tip labels
916
918
  max_x = max(node_x.values()) if node_x else 0
917
919
  offset = max_x * 0.02
918
- for tip in tips:
919
- ax.text(
920
- node_x[id(tip)] + offset, node_y[id(tip)],
921
- tip.name, va="center", fontsize=9,
922
- )
920
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
921
+ if label_fontsize > 0:
922
+ for tip in tips:
923
+ ax.text(
924
+ node_x[id(tip)] + offset, node_y[id(tip)],
925
+ tip.name, va="center", fontsize=label_fontsize,
926
+ )
923
927
 
924
928
  # Apply color annotations (range + label only; branches are trait-colored)
925
929
  if self.plot_config.color_file:
@@ -103,16 +103,29 @@ class ConsensusNetwork(Tree):
103
103
  raise PhykitUserError(
104
104
  [
105
105
  "Input trees do not share an identical taxon set.",
106
- "Use --missing-taxa shared to prune all trees to their shared taxa.",
106
+ "Use --missing-taxa allow or --missing-taxa shared.",
107
107
  ],
108
108
  code=2,
109
109
  )
110
110
 
111
+ if self.missing_taxa == "allow":
112
+ # Use the union of all taxa; each tree contributes splits
113
+ # using its own taxon set. Split frequencies are normalized
114
+ # by how many trees could contain each split.
115
+ union_taxa = set.union(*tip_sets)
116
+ if len(union_taxa) < 3:
117
+ raise PhykitUserError(
118
+ ["Fewer than 3 taxa found across all trees."], code=2
119
+ )
120
+ return trees, False, union_taxa
121
+
122
+ # shared mode
111
123
  if len(shared_taxa) < 3:
112
124
  raise PhykitUserError(
113
125
  [
114
126
  "Unable to compute network after pruning to shared taxa.",
115
127
  "At least 3 shared taxa are required.",
128
+ "Consider using --missing-taxa allow instead.",
116
129
  ],
117
130
  code=2,
118
131
  )
@@ -159,19 +172,80 @@ class ConsensusNetwork(Tree):
159
172
  return splits
160
173
 
161
174
  @staticmethod
162
- def _count_splits(trees: List, all_taxa: frozenset) -> Counter:
175
+ def _count_splits(trees: List, all_taxa: frozenset,
176
+ allow_mode: bool = False) -> Tuple[Counter, Counter]:
177
+ """Count splits across trees.
178
+
179
+ Returns (split_counts, split_possible) where split_possible[s]
180
+ is the number of trees that contain ALL taxa in split s (and
181
+ its complement). In allow mode, each tree uses its own taxon
182
+ set; in shared mode, all trees use all_taxa.
183
+ """
163
184
  counter = Counter()
164
- for tree in trees:
165
- tree_splits = ConsensusNetwork._extract_splits_from_tree(tree, all_taxa)
166
- for split in tree_splits:
167
- counter[split] += 1
168
- return counter
185
+ possible = Counter()
186
+
187
+ if allow_mode:
188
+ # Precompute taxon sets for all trees
189
+ tree_taxa_list = [
190
+ frozenset(t.name for t in tree.get_terminals())
191
+ for tree in trees
192
+ ]
193
+
194
+ # Extract splits from each tree using its own taxon set
195
+ for tree, tree_taxa in zip(trees, tree_taxa_list):
196
+ tree_splits = ConsensusNetwork._extract_splits_from_tree(
197
+ tree, tree_taxa
198
+ )
199
+ for split in tree_splits:
200
+ counter[split] += 1
201
+
202
+ # For normalization: each split was found in counter[split]
203
+ # trees. The "possible" count is the number of trees that
204
+ # contain ALL taxa on both sides. Since we extracted each
205
+ # split from a tree that had all its taxa, the split count
206
+ # IS the possible count (a tree can only produce a split if
207
+ # it contains all the relevant taxa).
208
+ for split in counter:
209
+ possible[split] = counter[split]
210
+
211
+ # Actually, we should count how many trees COULD have
212
+ # produced the split but didn't. A more accurate approach:
213
+ # possible = number of trees containing all taxa in the
214
+ # split's smaller side. But since splits are defined
215
+ # relative to each tree's own taxon set, the split IS
216
+ # the canonical smaller side from that tree. Different
217
+ # trees may have different "all_taxa" so the same
218
+ # bipartition in two trees means different things.
219
+ #
220
+ # The simplest correct normalization for incomplete
221
+ # taxon sampling: frequency = count / n_trees.
222
+ # This is what most software does.
223
+ for split in counter:
224
+ possible[split] = len(trees)
225
+ else:
226
+ for tree in trees:
227
+ tree_splits = ConsensusNetwork._extract_splits_from_tree(
228
+ tree, all_taxa
229
+ )
230
+ for split in tree_splits:
231
+ counter[split] += 1
232
+ for split in counter:
233
+ possible[split] = len(trees)
234
+
235
+ return counter, possible
169
236
 
170
237
  @staticmethod
171
- def _filter_splits(split_counts: Counter, n_trees: int, threshold: float) -> List[Tuple[frozenset, int, float]]:
238
+ def _filter_splits(
239
+ split_counts: Counter, n_trees: int, threshold: float,
240
+ split_possible: Counter = None,
241
+ ) -> List[Tuple[frozenset, int, float]]:
172
242
  results = []
173
243
  for split, count in split_counts.items():
174
- freq = count / n_trees
244
+ if split_possible and split in split_possible:
245
+ denom = split_possible[split]
246
+ else:
247
+ denom = n_trees
248
+ freq = count / denom if denom > 0 else 0.0
175
249
  if freq >= threshold:
176
250
  results.append((split, count, freq))
177
251
  results.sort(key=lambda x: (-x[2], sorted(x[0])))
@@ -435,8 +509,14 @@ class ConsensusNetwork(Tree):
435
509
  all_taxa = frozenset(all_taxa_set)
436
510
  n_trees = len(trees)
437
511
 
438
- split_counts = self._count_splits(trees, all_taxa)
439
- filtered = self._filter_splits(split_counts, n_trees, self.threshold)
512
+ allow_mode = (self.missing_taxa == "allow")
513
+ split_counts, split_possible = self._count_splits(
514
+ trees, all_taxa, allow_mode=allow_mode
515
+ )
516
+ filtered = self._filter_splits(
517
+ split_counts, n_trees, self.threshold,
518
+ split_possible=split_possible,
519
+ )
440
520
 
441
521
  if self.json_output:
442
522
  splits_list = [
@@ -522,7 +522,9 @@ class ContMap(Tree):
522
522
 
523
523
  # Tip labels
524
524
  max_x = max(node_x.values()) if node_x else 1.0
525
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
525
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
526
+ if label_fontsize > 0:
527
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
526
528
 
527
529
  # Apply color annotations (range + label only; branches are trait-colored)
528
530
  if self.plot_config.color_file:
@@ -592,11 +594,13 @@ class ContMap(Tree):
592
594
  # Tip labels
593
595
  max_x = max(node_x.values()) if node_x else 0
594
596
  offset = max_x * 0.02
595
- for tip in tips:
596
- ax.text(
597
- node_x[id(tip)] + offset, node_y[id(tip)],
598
- tip.name, va="center", fontsize=9,
599
- )
597
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
598
+ if label_fontsize > 0:
599
+ for tip in tips:
600
+ ax.text(
601
+ node_x[id(tip)] + offset, node_y[id(tip)],
602
+ tip.name, va="center", fontsize=label_fontsize,
603
+ )
600
604
 
601
605
  # Apply color annotations (range + label only; branches are trait-colored)
602
606
  if self.plot_config.color_file:
@@ -347,7 +347,9 @@ class DensityMap(Tree):
347
347
 
348
348
  # Tip labels
349
349
  max_x = max(node_x.values()) if node_x else 1.0
350
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
350
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
351
+ if label_fontsize > 0:
352
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
351
353
 
352
354
  # Apply color annotations (range + label only; branches are trait-colored)
353
355
  if self.plot_config.color_file:
@@ -453,11 +455,13 @@ class DensityMap(Tree):
453
455
  # Tip labels
454
456
  max_x = max(node_x.values()) if node_x else 0
455
457
  offset = max_x * 0.02
456
- for tip in tips:
457
- ax.text(
458
- node_x[id(tip)] + offset, node_y[id(tip)],
459
- tip.name, va="center", fontsize=9,
460
- )
458
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
459
+ if label_fontsize > 0:
460
+ for tip in tips:
461
+ ax.text(
462
+ node_x[id(tip)] + offset, node_y[id(tip)],
463
+ tip.name, va="center", fontsize=label_fontsize,
464
+ )
461
465
 
462
466
  # Apply color annotations (range + label only; branches are trait-colored)
463
467
  if self.plot_config.color_file:
@@ -33,6 +33,7 @@ class DiscordanceAsymmetry(Tree):
33
33
  super().__init__(tree_file_path=parsed["tree_file_path"])
34
34
  self.gene_trees_path = parsed["gene_trees_path"]
35
35
  self.verbose = parsed["verbose"]
36
+ self.annotate = parsed["annotate"]
36
37
  self.json_output = parsed["json_output"]
37
38
  self.plot_output = parsed["plot_output"]
38
39
  self.plot_config = parsed["plot_config"]
@@ -42,6 +43,7 @@ class DiscordanceAsymmetry(Tree):
42
43
  tree_file_path=args.tree,
43
44
  gene_trees_path=args.gene_trees,
44
45
  verbose=getattr(args, "verbose", False),
46
+ annotate=getattr(args, "annotate", False),
45
47
  json_output=getattr(args, "json", False),
46
48
  plot_output=getattr(args, "plot_output", None),
47
49
  plot_config=PlotConfig.from_args(args),
@@ -314,7 +316,9 @@ class DiscordanceAsymmetry(Tree):
314
316
 
315
317
  # Tip labels
316
318
  max_x = max(node_x.values()) if node_x else 1.0
317
- draw_circular_tip_labels(ax, species_tree, coords, fontsize=9, offset=max_x * 0.02)
319
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
320
+ if label_fontsize > 0:
321
+ draw_circular_tip_labels(ax, species_tree, coords, fontsize=label_fontsize, offset=max_x * 0.02)
318
322
 
319
323
  # Apply color annotations (range + label only; branches are trait-colored)
320
324
  if self.plot_config.color_file:
@@ -345,23 +349,28 @@ class DiscordanceAsymmetry(Tree):
345
349
 
346
350
  total = entry["n_concordant"] + entry["n_alt1"] + entry["n_alt2"]
347
351
  gcf = entry["n_concordant"] / total if total > 0 else 1.0
348
- ax.annotate(
349
- f"gCF={gcf:.2f}",
350
- (cx, cy),
351
- textcoords="offset points",
352
- xytext=(5, 5),
353
- fontsize=7,
354
- )
352
+
353
+ if self.annotate:
354
+ ax.annotate(
355
+ f"{gcf:.2f}",
356
+ (cx, cy),
357
+ textcoords="offset points",
358
+ xytext=(5, 5),
359
+ fontsize=max(4, 7 - n_tips * 0.01),
360
+ )
355
361
 
356
362
  if (entry["fdr_p"] is not None and entry["fdr_p"] < 0.05
357
363
  and entry["favored_alt"] is not None):
358
364
  ax.scatter(cx, cy, s=100, c="red", marker="*", zorder=5)
359
365
 
360
- # Colorbar
361
- sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
362
- sm.set_array([])
363
- cbar = fig.colorbar(sm, ax=ax, pad=0.15)
364
- cbar.set_label("Asymmetry ratio")
366
+ # Colorbar (hide if legend-position is none)
367
+ legend_loc = config.legend_position or "upper right"
368
+ if legend_loc != "none":
369
+ sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
370
+ sm.set_array([])
371
+ cbar = fig.colorbar(sm, ax=ax, pad=0.15)
372
+ cbar_fontsize = config.axis_fontsize if config.axis_fontsize else 10
373
+ cbar.set_label("Asymmetry ratio", fontsize=cbar_fontsize)
365
374
 
366
375
  if config.show_title:
367
376
  ax.set_title(config.title or "Discordance Asymmetry", fontsize=config.title_fontsize)
@@ -409,16 +418,18 @@ class DiscordanceAsymmetry(Tree):
409
418
  x = node_x.get(id(clade), 0)
410
419
  y = node_y.get(id(clade), 0)
411
420
 
412
- # Show gCF value
421
+ # Show gCF value (only if --annotate)
413
422
  total = entry["n_concordant"] + entry["n_alt1"] + entry["n_alt2"]
414
423
  gcf = entry["n_concordant"] / total if total > 0 else 1.0
415
- ax.annotate(
416
- f"gCF={gcf:.2f}",
417
- (x, y),
418
- textcoords="offset points",
419
- xytext=(5, 5),
420
- fontsize=7,
421
- )
424
+
425
+ if self.annotate:
426
+ ax.annotate(
427
+ f"{gcf:.2f}",
428
+ (x, y),
429
+ textcoords="offset points",
430
+ xytext=(5, 5),
431
+ fontsize=max(4, 7 - n_tips * 0.01),
432
+ )
422
433
 
423
434
  # Mark significant branches (FDR < 0.05)
424
435
  if (entry["fdr_p"] is not None and entry["fdr_p"] < 0.05
@@ -426,13 +437,15 @@ class DiscordanceAsymmetry(Tree):
426
437
  ax.scatter(x, y, s=100, c="red", marker="*", zorder=5)
427
438
 
428
439
  # Tip labels
429
- max_x = max(node_x.values()) if node_x else 0
430
- offset = max_x * 0.02
431
- for tip in tips:
432
- ax.text(
433
- node_x[id(tip)] + offset, node_y[id(tip)],
434
- tip.name, va="center", fontsize=9,
435
- )
440
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
441
+ if label_fontsize > 0:
442
+ max_x = max(node_x.values()) if node_x else 0
443
+ offset = max_x * 0.02
444
+ for tip in tips:
445
+ ax.text(
446
+ node_x[id(tip)] + offset, node_y[id(tip)],
447
+ tip.name, va="center", fontsize=label_fontsize,
448
+ )
436
449
 
437
450
  # Apply color annotations (range + label only; branches are trait-colored)
438
451
  if self.plot_config.color_file:
@@ -450,11 +463,14 @@ class DiscordanceAsymmetry(Tree):
450
463
  if color_legend:
451
464
  ax.legend(handles=color_legend, loc="upper right", fontsize=8, frameon=True)
452
465
 
453
- # Colorbar
454
- sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
455
- sm.set_array([])
456
- cbar = fig.colorbar(sm, ax=ax, pad=0.15)
457
- cbar.set_label("Asymmetry ratio")
466
+ # Colorbar (hide if legend-position is none)
467
+ legend_loc = config.legend_position or "upper right"
468
+ if legend_loc != "none":
469
+ sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
470
+ sm.set_array([])
471
+ cbar = fig.colorbar(sm, ax=ax, pad=0.15)
472
+ cbar_fontsize = config.axis_fontsize if config.axis_fontsize else 10
473
+ cbar.set_label("Asymmetry ratio", fontsize=cbar_fontsize)
458
474
 
459
475
  ax.set_xlabel("Branch length (subs/site)")
460
476
  ax.set_yticks([])
@@ -487,12 +487,14 @@ class Phenogram(Tree):
487
487
  # Label tips with taxon names (offset to the right)
488
488
  max_x = max(node_x.values()) if node_x else 0
489
489
  offset = max_x * 0.02
490
- for tip in tips:
491
- if id(tip) in node_x and tip.name in trait_values:
492
- ax.text(
493
- node_x[id(tip)] + offset, trait_values[tip.name],
494
- tip.name, va="center", fontsize=9,
495
- )
490
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
491
+ if label_fontsize > 0:
492
+ for tip in tips:
493
+ if id(tip) in node_x and tip.name in trait_values:
494
+ ax.text(
495
+ node_x[id(tip)] + offset, trait_values[tip.name],
496
+ tip.name, va="center", fontsize=label_fontsize,
497
+ )
496
498
 
497
499
  ax.set_xlabel("Distance from root")
498
500
  ax.set_ylabel("Trait value")
@@ -759,7 +759,9 @@ class RateHeterogeneity(Tree):
759
759
 
760
760
  # Tip labels
761
761
  max_x = max(node_x.values()) if node_x else 1.0
762
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.02)
762
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
763
+ if label_fontsize > 0:
764
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.02)
763
765
 
764
766
  # Apply color annotations (range + label only; branches are trait-colored)
765
767
  if self.plot_config.color_file:
@@ -825,11 +827,13 @@ class RateHeterogeneity(Tree):
825
827
 
826
828
  max_x = max(node_x.values()) if node_x else 0
827
829
  offset = max_x * 0.02
828
- for tip in tips:
829
- ax.text(
830
- node_x[id(tip)] + offset, node_y[id(tip)],
831
- tip.name, va="center", fontsize=9,
832
- )
830
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
831
+ if label_fontsize > 0:
832
+ for tip in tips:
833
+ ax.text(
834
+ node_x[id(tip)] + offset, node_y[id(tip)],
835
+ tip.name, va="center", fontsize=label_fontsize,
836
+ )
833
837
 
834
838
  # Apply color annotations (range + label only; branches are trait-colored)
835
839
  if self.plot_config.color_file:
@@ -600,7 +600,9 @@ class StochasticCharacterMap(Tree):
600
600
 
601
601
  # Tip labels
602
602
  max_x = max(node_x.values()) if node_x else 1.0
603
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
603
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
604
+ if label_fontsize > 0:
605
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
604
606
 
605
607
  # Apply color annotations (range + label only; branches are trait-colored)
606
608
  if self.plot_config.color_file:
@@ -677,11 +679,13 @@ class StochasticCharacterMap(Tree):
677
679
  # Tip labels
678
680
  max_x = max(node_x.values()) if node_x else 0
679
681
  offset = max_x * 0.02
680
- for tip in tips:
681
- ax.text(
682
- node_x[id(tip)] + offset, node_y[id(tip)],
683
- tip.name, va="center", fontsize=9
684
- )
682
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
683
+ if label_fontsize > 0:
684
+ for tip in tips:
685
+ ax.text(
686
+ node_x[id(tip)] + offset, node_y[id(tip)],
687
+ tip.name, va="center", fontsize=label_fontsize
688
+ )
685
689
 
686
690
  # Apply color annotations (range + label only; branches are trait-colored)
687
691
  if self.plot_config.color_file:
@@ -544,9 +544,11 @@ class TraitRateMap(Tree):
544
544
 
545
545
  # Tip labels
546
546
  max_x = max(node_x.values()) if node_x else 1.0
547
- draw_circular_tip_labels(
548
- ax, tree, coords, fontsize=9, offset=max_x * 0.03
549
- )
547
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
548
+ if label_fontsize > 0:
549
+ draw_circular_tip_labels(
550
+ ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03
551
+ )
550
552
 
551
553
  # Color annotations
552
554
  if self.plot_config.color_file:
@@ -611,11 +613,13 @@ class TraitRateMap(Tree):
611
613
  # Tip labels
612
614
  max_x = max(node_x.values()) if node_x else 0
613
615
  offset = max_x * 0.02
614
- for tip in tips:
615
- ax.text(
616
- node_x[id(tip)] + offset, node_y[id(tip)],
617
- tip.name, va="center", fontsize=9,
618
- )
616
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
617
+ if label_fontsize > 0:
618
+ for tip in tips:
619
+ ax.text(
620
+ node_x[id(tip)] + offset, node_y[id(tip)],
621
+ tip.name, va="center", fontsize=label_fontsize,
622
+ )
619
623
 
620
624
  # Color annotations
621
625
  if self.plot_config.color_file:
@@ -0,0 +1 @@
1
+ __version__ = "2.1.74"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.72
3
+ Version: 2.1.74
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -1 +0,0 @@
1
- __version__ = "2.1.72"
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