phykit 2.1.38__tar.gz → 2.1.40__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 (114) hide show
  1. {phykit-2.1.38 → phykit-2.1.40}/PKG-INFO +1 -1
  2. {phykit-2.1.38 → phykit-2.1.40}/phykit/cli_registry.py +3 -0
  3. phykit-2.1.40/phykit/helpers/plot_config.py +207 -0
  4. {phykit-2.1.38 → phykit-2.1.40}/phykit/phykit.py +1115 -27
  5. {phykit-2.1.38 → phykit-2.1.40}/phykit/service_factories.py +1 -0
  6. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/alignment_entropy.py +19 -5
  7. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/compositional_bias_per_site.py +24 -7
  8. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/create_concatenation_matrix.py +38 -14
  9. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/evolutionary_rate_per_site.py +19 -5
  10. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/pairwise_identity.py +27 -8
  11. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/plot_alignment_qc.py +3 -0
  12. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/rcvt.py +21 -5
  13. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/__init__.py +1 -0
  14. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/ancestral_reconstruction.py +21 -6
  15. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/concordance_asr.py +9 -3
  16. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/consensus_network.py +9 -3
  17. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/cont_map.py +9 -3
  18. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/cophylo.py +10 -4
  19. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/covarying_evolutionary_rates.py +20 -6
  20. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/density_map.py +9 -5
  21. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/discordance_asymmetry.py +11 -3
  22. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/evo_tempo_map.py +13 -4
  23. phykit-2.1.40/phykit/services/tree/kf_distance.py +101 -0
  24. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/ltt.py +17 -7
  25. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/phenogram.py +9 -3
  26. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/phylogenetic_ordination.py +11 -4
  27. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/phylomorphospace.py +7 -2
  28. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/quartet_network.py +9 -2
  29. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/rate_heterogeneity.py +9 -3
  30. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/relative_rate_test.py +15 -4
  31. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/saturation.py +20 -6
  32. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/spectral_discordance.py +23 -6
  33. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/stochastic_character_map.py +9 -3
  34. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/threshold_model.py +11 -6
  35. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/tip_to_tip_distance.py +27 -8
  36. phykit-2.1.40/phykit/version.py +1 -0
  37. {phykit-2.1.38 → phykit-2.1.40}/phykit.egg-info/PKG-INFO +1 -1
  38. {phykit-2.1.38 → phykit-2.1.40}/phykit.egg-info/SOURCES.txt +2 -0
  39. {phykit-2.1.38 → phykit-2.1.40}/phykit.egg-info/entry_points.txt +4 -0
  40. {phykit-2.1.38 → phykit-2.1.40}/setup.py +4 -0
  41. phykit-2.1.38/phykit/version.py +0 -1
  42. {phykit-2.1.38 → phykit-2.1.40}/LICENSE.md +0 -0
  43. {phykit-2.1.38 → phykit-2.1.40}/README.md +0 -0
  44. {phykit-2.1.38 → phykit-2.1.40}/phykit/__init__.py +0 -0
  45. {phykit-2.1.38 → phykit-2.1.40}/phykit/__main__.py +0 -0
  46. {phykit-2.1.38 → phykit-2.1.40}/phykit/errors.py +0 -0
  47. {phykit-2.1.38 → phykit-2.1.40}/phykit/helpers/__init__.py +0 -0
  48. {phykit-2.1.38 → phykit-2.1.40}/phykit/helpers/boolean_argument_parsing.py +0 -0
  49. {phykit-2.1.38 → phykit-2.1.40}/phykit/helpers/caching.py +0 -0
  50. {phykit-2.1.38 → phykit-2.1.40}/phykit/helpers/files.py +0 -0
  51. {phykit-2.1.38 → phykit-2.1.40}/phykit/helpers/json_output.py +0 -0
  52. {phykit-2.1.38 → phykit-2.1.40}/phykit/helpers/parallel.py +0 -0
  53. {phykit-2.1.38 → phykit-2.1.40}/phykit/helpers/stats_summary.py +0 -0
  54. {phykit-2.1.38 → phykit-2.1.40}/phykit/helpers/streaming.py +0 -0
  55. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/__init__.py +0 -0
  56. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/__init__.py +0 -0
  57. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/alignment_length.py +0 -0
  58. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  59. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  60. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/alignment_recoding.py +0 -0
  61. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/base.py +0 -0
  62. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/column_score.py +0 -0
  63. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/composition_per_taxon.py +0 -0
  64. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/dna_threader.py +0 -0
  65. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/faidx.py +0 -0
  66. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/gc_content.py +0 -0
  67. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/mask_alignment.py +0 -0
  68. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  69. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  70. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/rcv.py +0 -0
  71. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  72. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  73. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/alignment/variable_sites.py +0 -0
  74. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/base.py +0 -0
  75. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/base.py +0 -0
  76. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/bipartition_support_stats.py +0 -0
  77. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/branch_length_multiplier.py +0 -0
  78. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/collapse_branches.py +0 -0
  79. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/consensus_tree.py +0 -0
  80. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/dvmc.py +0 -0
  81. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/evolutionary_rate.py +0 -0
  82. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/fit_continuous.py +0 -0
  83. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  84. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/internal_branch_stats.py +0 -0
  85. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/internode_labeler.py +0 -0
  86. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  87. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/lb_score.py +0 -0
  88. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/monophyly_check.py +0 -0
  89. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  90. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/network_signal.py +0 -0
  91. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/ou_shift_detection.py +0 -0
  92. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/ouwie.py +0 -0
  93. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/patristic_distances.py +0 -0
  94. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/phylogenetic_glm.py +0 -0
  95. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/phylogenetic_regression.py +0 -0
  96. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/phylogenetic_signal.py +0 -0
  97. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/polytomy_test.py +0 -0
  98. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/print_tree.py +0 -0
  99. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/prune_tree.py +0 -0
  100. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/rename_tree_tips.py +0 -0
  101. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/rf_distance.py +0 -0
  102. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/root_tree.py +0 -0
  103. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/spurious_sequence.py +0 -0
  104. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/terminal_branch_stats.py +0 -0
  105. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/tip_labels.py +0 -0
  106. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  107. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/total_tree_length.py +0 -0
  108. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/treeness.py +0 -0
  109. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/treeness_over_rcv.py +0 -0
  110. {phykit-2.1.38 → phykit-2.1.40}/phykit/services/tree/vcv_utils.py +0 -0
  111. {phykit-2.1.38 → phykit-2.1.40}/phykit.egg-info/dependency_links.txt +0 -0
  112. {phykit-2.1.38 → phykit-2.1.40}/phykit.egg-info/requires.txt +0 -0
  113. {phykit-2.1.38 → phykit-2.1.40}/phykit.egg-info/top_level.txt +0 -0
  114. {phykit-2.1.38 → phykit-2.1.40}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: phykit
3
- Version: 2.1.38
3
+ Version: 2.1.40
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -129,6 +129,9 @@ ALIAS_TO_HANDLER: Dict[str, str] = {
129
129
  "prune": "prune_tree",
130
130
  "rename_tree": "rename_tree_tips",
131
131
  "rename_tips": "rename_tree_tips",
132
+ "kuhner_felsenstein_distance": "kf_distance",
133
+ "kf_dist": "kf_distance",
134
+ "kf": "kf_distance",
132
135
  "robinson_foulds_distance": "rf_distance",
133
136
  "rf_dist": "rf_distance",
134
137
  "rf": "rf_distance",
@@ -0,0 +1,207 @@
1
+ import sys
2
+ from dataclasses import dataclass, field
3
+ from typing import List, Optional
4
+
5
+
6
+ VALID_LEGEND_POSITIONS = frozenset([
7
+ "best", "upper right", "upper left", "lower left", "lower right",
8
+ "right", "center left", "center right", "lower center", "upper center",
9
+ "center", "none",
10
+ ])
11
+
12
+
13
+ @dataclass
14
+ class PlotConfig:
15
+ fig_width: Optional[float] = None
16
+ fig_height: Optional[float] = None
17
+ dpi: int = 300
18
+ show_title: bool = True
19
+ title: Optional[str] = None
20
+ legend_position: Optional[str] = None
21
+ ylabel_fontsize: Optional[float] = None
22
+ xlabel_fontsize: Optional[float] = None
23
+ title_fontsize: Optional[float] = None
24
+ axis_fontsize: Optional[float] = None
25
+ colors: Optional[List[str]] = None
26
+
27
+ def validate(self) -> None:
28
+ if self.fig_width is not None and self.fig_width <= 0:
29
+ print(f"Error: --fig-width must be positive, got {self.fig_width}")
30
+ sys.exit(2)
31
+ if self.fig_height is not None and self.fig_height <= 0:
32
+ print(f"Error: --fig-height must be positive, got {self.fig_height}")
33
+ sys.exit(2)
34
+ if self.dpi <= 0:
35
+ print(f"Error: --dpi must be positive, got {self.dpi}")
36
+ sys.exit(2)
37
+ for name, val in [
38
+ ("--ylabel-fontsize", self.ylabel_fontsize),
39
+ ("--xlabel-fontsize", self.xlabel_fontsize),
40
+ ("--title-fontsize", self.title_fontsize),
41
+ ("--axis-fontsize", self.axis_fontsize),
42
+ ]:
43
+ if val is not None and val < 0:
44
+ print(f"Error: {name} must be non-negative, got {val}")
45
+ sys.exit(2)
46
+ if (
47
+ self.legend_position is not None
48
+ and self.legend_position not in VALID_LEGEND_POSITIONS
49
+ ):
50
+ print(
51
+ f"Error: --legend-position '{self.legend_position}' is not valid. "
52
+ f"Choose from: {', '.join(sorted(VALID_LEGEND_POSITIONS))}"
53
+ )
54
+ sys.exit(2)
55
+
56
+ @classmethod
57
+ def auto_scale(cls, n_rows=None, n_cols=None) -> "PlotConfig":
58
+ # fig_height
59
+ if n_rows is not None:
60
+ raw_height = 3.0 + n_rows * 0.18
61
+ fig_height = max(5.0, min(200.0, raw_height))
62
+ if raw_height > 200.0:
63
+ print(
64
+ f"Warning: computed figure height ({raw_height:.0f} in) "
65
+ f"exceeds 200 in; capping at 200. Use --fig-height to override.",
66
+ file=sys.stderr,
67
+ )
68
+ else:
69
+ fig_height = 8.0
70
+
71
+ # fig_width
72
+ if n_cols is not None:
73
+ fig_width = max(10.0, min(20.0, 8.0 + n_cols * 0.15))
74
+ else:
75
+ fig_width = 14.0
76
+
77
+ # ylabel_fontsize
78
+ if n_rows is not None:
79
+ if n_rows > 800:
80
+ ylabel_fontsize = 0.0
81
+ else:
82
+ ylabel_fontsize = max(3.0, min(7.0, 7.0 - (n_rows - 50) * 0.008))
83
+ else:
84
+ ylabel_fontsize = 7.0
85
+
86
+ # xlabel_fontsize
87
+ if n_cols is not None:
88
+ if n_cols > 60:
89
+ xlabel_fontsize = 0.0
90
+ else:
91
+ xlabel_fontsize = max(3.0, min(7.0, 7.0 - (n_cols - 20) * 0.1))
92
+ else:
93
+ xlabel_fontsize = 7.0
94
+
95
+ return cls(
96
+ fig_width=fig_width,
97
+ fig_height=fig_height,
98
+ ylabel_fontsize=ylabel_fontsize,
99
+ xlabel_fontsize=xlabel_fontsize,
100
+ title_fontsize=12.0,
101
+ axis_fontsize=10.0,
102
+ )
103
+
104
+ def resolve(self, n_rows=None, n_cols=None) -> "PlotConfig":
105
+ defaults = PlotConfig.auto_scale(n_rows, n_cols)
106
+ for fld in [
107
+ "fig_width", "fig_height", "ylabel_fontsize", "xlabel_fontsize",
108
+ "title_fontsize", "axis_fontsize",
109
+ ]:
110
+ if getattr(self, fld) is None:
111
+ setattr(self, fld, getattr(defaults, fld))
112
+ return self
113
+
114
+ def apply_to_figure(self, fig, ax, default_title, default_colors):
115
+ # Title
116
+ if self.show_title:
117
+ title_text = self.title if self.title is not None else default_title
118
+ ax.set_title(title_text, fontsize=self.title_fontsize)
119
+ else:
120
+ ax.set_title("")
121
+
122
+ # Legend
123
+ if self.legend_position is not None:
124
+ if self.legend_position == "none":
125
+ legend = ax.get_legend()
126
+ if legend is not None:
127
+ legend.set_visible(False)
128
+ else:
129
+ legend = ax.get_legend()
130
+ if legend is not None:
131
+ handles = legend.legend_handles
132
+ labels = [t.get_text() for t in legend.get_texts()]
133
+ legend.remove()
134
+ ax.legend(handles=handles, labels=labels, loc=self.legend_position)
135
+
136
+ # Axis label font sizes
137
+ if self.axis_fontsize is not None:
138
+ ax.xaxis.label.set_fontsize(self.axis_fontsize)
139
+ ax.yaxis.label.set_fontsize(self.axis_fontsize)
140
+
141
+ # Y-axis tick labels
142
+ if self.ylabel_fontsize is not None:
143
+ if self.ylabel_fontsize == 0.0:
144
+ ax.set_yticklabels([])
145
+ else:
146
+ for label in ax.get_yticklabels():
147
+ label.set_fontsize(self.ylabel_fontsize)
148
+
149
+ # X-axis tick labels
150
+ if self.xlabel_fontsize is not None:
151
+ if self.xlabel_fontsize == 0.0:
152
+ ax.set_xticklabels([])
153
+ else:
154
+ for label in ax.get_xticklabels():
155
+ label.set_fontsize(self.xlabel_fontsize)
156
+
157
+ # Colors
158
+ return self.merge_colors(default_colors)
159
+
160
+ def merge_colors(self, defaults: List[str]) -> List[str]:
161
+ if self.colors is None:
162
+ return list(defaults)
163
+ result = list(defaults)
164
+ for i, user_color in enumerate(self.colors[:len(defaults)]):
165
+ if user_color: # non-empty string overrides
166
+ result[i] = user_color
167
+ return result
168
+
169
+ @classmethod
170
+ def from_args(cls, args) -> "PlotConfig":
171
+ colors_str = getattr(args, "colors", None)
172
+ colors = None
173
+ if colors_str is not None:
174
+ colors = [c.strip() for c in colors_str.split(",")]
175
+
176
+ no_title = getattr(args, "no_title", False)
177
+
178
+ config = cls(
179
+ fig_width=getattr(args, "fig_width", None),
180
+ fig_height=getattr(args, "fig_height", None),
181
+ dpi=getattr(args, "dpi", 300),
182
+ show_title=not no_title,
183
+ title=getattr(args, "title", None),
184
+ legend_position=getattr(args, "legend_position", None),
185
+ ylabel_fontsize=getattr(args, "ylabel_fontsize", None),
186
+ xlabel_fontsize=getattr(args, "xlabel_fontsize", None),
187
+ title_fontsize=getattr(args, "title_fontsize", None),
188
+ axis_fontsize=getattr(args, "axis_fontsize", None),
189
+ colors=colors,
190
+ )
191
+ config.validate()
192
+ return config
193
+
194
+
195
+ def add_plot_arguments(parser) -> None:
196
+ group = parser.add_argument_group("plot options")
197
+ group.add_argument("--fig-width", type=float, default=None, help="Figure width in inches (auto-scaled if omitted)")
198
+ group.add_argument("--fig-height", type=float, default=None, help="Figure height in inches (auto-scaled if omitted)")
199
+ group.add_argument("--dpi", type=int, default=300, help="Resolution in DPI (default: 300)")
200
+ group.add_argument("--no-title", action="store_true", default=False, help="Hide the plot title")
201
+ group.add_argument("--title", type=str, default=None, help="Custom title text")
202
+ group.add_argument("--legend-position", type=str, default=None, help="Legend location (e.g., 'upper right', 'none' to hide)")
203
+ group.add_argument("--ylabel-fontsize", type=float, default=None, help="Font size for y-axis labels; 0 to hide")
204
+ group.add_argument("--xlabel-fontsize", type=float, default=None, help="Font size for x-axis labels; 0 to hide")
205
+ group.add_argument("--title-fontsize", type=float, default=None, help="Font size for the title")
206
+ group.add_argument("--axis-fontsize", type=float, default=None, help="Font size for axis labels")
207
+ group.add_argument("--colors", type=str, default=None, help="Comma-separated colors (hex or named)")