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.
- {phykit-2.1.72 → phykit-2.1.74}/PKG-INFO +1 -1
- {phykit-2.1.72 → phykit-2.1.74}/phykit/phykit.py +9 -2
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/ancestral_reconstruction.py +21 -13
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/concordance_asr.py +10 -6
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/consensus_network.py +91 -11
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/cont_map.py +10 -6
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/density_map.py +10 -6
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/discordance_asymmetry.py +49 -33
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phenogram.py +8 -6
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/rate_heterogeneity.py +10 -6
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/stochastic_character_map.py +10 -6
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/trait_rate_map.py +12 -8
- phykit-2.1.74/phykit/version.py +1 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/PKG-INFO +1 -1
- phykit-2.1.72/phykit/version.py +0 -1
- {phykit-2.1.72 → phykit-2.1.74}/LICENSE.md +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/README.md +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/__init__.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/__main__.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/cli_registry.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/errors.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/__init__.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/boolean_argument_parsing.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/caching.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/circular_layout.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/color_annotations.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/discrete_models.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/files.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/json_output.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/parallel.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/parsimony_utils.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/plot_config.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/quartet_utils.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/stats_summary.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/helpers/streaming.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/service_factories.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/__init__.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/__init__.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_entropy.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_length.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_recoding.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/alignment_subsample.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/base.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/column_score.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/composition_per_taxon.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/dfoil.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/dna_threader.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/dstatistic.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/faidx.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/gc_content.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/identity_matrix.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/mask_alignment.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/pairwise_identity.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/phylo_gwas.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/plot_alignment_qc.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/rcv.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/rcvt.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/rename_fasta_entries.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/alignment/variable_sites.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/base.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/__init__.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/base.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/bipartition_support_stats.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/branch_length_multiplier.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/character_map.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/collapse_branches.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/consensus_tree.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/cophylo.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/dvmc.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/evo_tempo_map.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/evolutionary_rate.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/fit_continuous.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/fit_discrete.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/hidden_paralogy_check.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/independent_contrasts.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/internal_branch_stats.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/internode_labeler.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/kf_distance.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/lb_score.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/ltt.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/monophyly_check.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/network_signal.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/ou_shift_detection.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/ouwie.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/parsimony_score.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/patristic_distances.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylo_heatmap.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylo_impute.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylo_logistic.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylogenetic_glm.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylogenetic_ordination.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylogenetic_regression.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylogenetic_signal.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/phylomorphospace.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/polytomy_test.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/print_tree.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/prune_tree.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/quartet_network.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/quartet_pie.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/relative_rate_test.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/rename_tree_tips.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/rf_distance.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/root_tree.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/saturation.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/spectral_discordance.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/spurious_sequence.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/terminal_branch_stats.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/threshold_model.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/tip_labels.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/tip_to_tip_distance.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/total_tree_length.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/trait_correlation.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/tree_space.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/treeness.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/treeness_over_rcv.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit/services/tree/vcv_utils.py +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/SOURCES.txt +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/dependency_links.txt +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/entry_points.txt +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/requires.txt +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/phykit.egg-info/top_level.txt +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/setup.cfg +0 -0
- {phykit-2.1.72 → phykit-2.1.74}/setup.py +0 -0
|
@@ -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="
|
|
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
|
-
|
|
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
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
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
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
548
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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"
|
phykit-2.1.72/phykit/version.py
DELETED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|