phykit 2.1.84__tar.gz → 2.1.85__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.84 → phykit-2.1.85}/PKG-INFO +1 -1
- {phykit-2.1.84 → phykit-2.1.85}/phykit/cli_registry.py +3 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/phykit.py +59 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/service_factories.py +1 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/__init__.py +1 -0
- phykit-2.1.85/phykit/services/tree/transfer_annotations.py +166 -0
- phykit-2.1.85/phykit/version.py +1 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/PKG-INFO +1 -1
- {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/SOURCES.txt +1 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/entry_points.txt +3 -0
- phykit-2.1.84/phykit/version.py +0 -1
- {phykit-2.1.84 → phykit-2.1.85}/LICENSE.md +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/README.md +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/__init__.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/__main__.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/errors.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/__init__.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/boolean_argument_parsing.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/caching.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/circular_layout.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/color_annotations.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/discrete_models.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/files.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/json_output.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/parallel.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/parsimony_utils.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/pgls_utils.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/plot_config.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/quartet_utils.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/stats_summary.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/streaming.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/helpers/trait_parsing.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/__init__.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/__init__.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_entropy.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_length.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_recoding.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/alignment_subsample.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/base.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/column_score.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/composition_per_taxon.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/dfoil.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/dna_threader.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/dstatistic.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/faidx.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/gc_content.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/identity_matrix.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/mask_alignment.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/occupancy_filter.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/pairwise_identity.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/phylo_gwas.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/plot_alignment_qc.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/rcv.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/rcvt.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/rename_fasta_entries.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/taxon_groups.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/alignment/variable_sites.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/base.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/ancestral_reconstruction.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/base.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/bipartition_support_stats.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/branch_length_multiplier.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/character_map.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/collapse_branches.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/concordance_asr.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/consensus_network.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/consensus_tree.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/cont_map.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/cophylo.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/density_map.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/discordance_asymmetry.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/dvmc.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/evo_tempo_map.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/evolutionary_rate.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/fit_continuous.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/fit_discrete.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/hidden_paralogy_check.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/hybridization.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/independent_contrasts.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/internal_branch_stats.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/internode_labeler.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/kf_distance.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/lb_score.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/ltt.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/monophyly_check.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/neighbor_net.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/network_signal.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/ou_shift_detection.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/ouwie.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/parsimony_score.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/patristic_distances.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phenogram.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylo_anova.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylo_heatmap.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylo_impute.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylo_logistic.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylo_path.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylogenetic_glm.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylogenetic_ordination.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylogenetic_regression.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylogenetic_signal.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/phylomorphospace.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/polytomy_test.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/print_tree.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/prune_tree.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/quartet_network.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/quartet_pie.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/rate_heterogeneity.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/relative_rate_test.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/rename_tree_tips.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/rf_distance.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/root_tree.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/saturation.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/simmap_summary.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/spectral_discordance.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/spr.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/spurious_sequence.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/stochastic_character_map.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/terminal_branch_stats.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/threshold_model.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/tip_labels.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/tip_to_tip_distance.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/total_tree_length.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/trait_correlation.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/trait_rate_map.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/tree_space.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/treeness.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/treeness_over_rcv.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit/services/tree/vcv_utils.py +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/dependency_links.txt +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/requires.txt +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/phykit.egg-info/top_level.txt +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/setup.cfg +0 -0
- {phykit-2.1.84 → phykit-2.1.85}/setup.py +0 -0
|
@@ -215,6 +215,9 @@ ALIAS_TO_HANDLER: Dict[str, str] = {
|
|
|
215
215
|
"tree_space": "tree_space",
|
|
216
216
|
"tspace": "tree_space",
|
|
217
217
|
"tree_landscape": "tree_space",
|
|
218
|
+
"transfer_annotations": "transfer_annotations",
|
|
219
|
+
"transfer_annot": "transfer_annotations",
|
|
220
|
+
"annotate_tree": "transfer_annotations",
|
|
218
221
|
"tgroups": "taxon_groups",
|
|
219
222
|
"shared_taxa": "taxon_groups",
|
|
220
223
|
"occupancy_filter": "occupancy_filter",
|
|
@@ -308,6 +308,8 @@ class Phykit:
|
|
|
308
308
|
- prune taxa from a phylogeny
|
|
309
309
|
subtree_prune_regraft (alias: spr)
|
|
310
310
|
- generate all SPR rearrangements for a specified subtree
|
|
311
|
+
transfer_annotations (alias: transfer_annot; annotate_tree)
|
|
312
|
+
- transfer node annotations between trees (e.g., wASTRAL to RAxML/IQ-TREE)
|
|
311
313
|
relative_rate_test (alias: rrt; tajima_rrt)
|
|
312
314
|
- Tajima's relative rate test for equal evolutionary
|
|
313
315
|
rates between two ingroup lineages
|
|
@@ -6821,6 +6823,59 @@ class Phykit:
|
|
|
6821
6823
|
_add_json_argument(parser)
|
|
6822
6824
|
_run_service(parser, argv, Spr)
|
|
6823
6825
|
|
|
6826
|
+
@staticmethod
|
|
6827
|
+
def transfer_annotations(argv):
|
|
6828
|
+
parser = _new_parser(
|
|
6829
|
+
description=textwrap.dedent(
|
|
6830
|
+
f"""\
|
|
6831
|
+
{help_header}
|
|
6832
|
+
|
|
6833
|
+
Transfer internal node annotations from one tree onto
|
|
6834
|
+
another. Matches nodes by bipartition (descendant taxa
|
|
6835
|
+
set) and copies the annotation labels.
|
|
6836
|
+
|
|
6837
|
+
Typical use case: transfer wASTRAL support annotations
|
|
6838
|
+
(q1/q2/q3, pp1, f1, etc.) from an annotated ASTRAL
|
|
6839
|
+
tree onto a branch-length-optimized topology from
|
|
6840
|
+
RAxML-NG, IQ-TREE, or any other tool. The output tree
|
|
6841
|
+
has the target's branch lengths with the source's
|
|
6842
|
+
annotations.
|
|
6843
|
+
|
|
6844
|
+
Aliases:
|
|
6845
|
+
transfer_annotations, transfer_annot, annotate_tree
|
|
6846
|
+
Command line interfaces:
|
|
6847
|
+
pk_transfer_annotations, pk_transfer_annot, pk_annotate_tree
|
|
6848
|
+
|
|
6849
|
+
Usage:
|
|
6850
|
+
phykit transfer_annotations --source <annotated_tree>
|
|
6851
|
+
--target <branch_length_tree> [-o/--output <file>]
|
|
6852
|
+
[--json]
|
|
6853
|
+
|
|
6854
|
+
Options
|
|
6855
|
+
=====================================================
|
|
6856
|
+
--source annotated tree file (e.g.,
|
|
6857
|
+
wASTRAL output with
|
|
6858
|
+
--support 3)
|
|
6859
|
+
|
|
6860
|
+
--target target tree file with
|
|
6861
|
+
branch lengths to keep
|
|
6862
|
+
(e.g., RAxML-NG or
|
|
6863
|
+
IQ-TREE output)
|
|
6864
|
+
|
|
6865
|
+
-o/--output output file for the
|
|
6866
|
+
annotated tree (default:
|
|
6867
|
+
target file + ".annotated")
|
|
6868
|
+
|
|
6869
|
+
--json output results as JSON
|
|
6870
|
+
"""
|
|
6871
|
+
),
|
|
6872
|
+
)
|
|
6873
|
+
parser.add_argument("--source", type=str, required=True, help=SUPPRESS, metavar="")
|
|
6874
|
+
parser.add_argument("--target", type=str, required=True, help=SUPPRESS, metavar="")
|
|
6875
|
+
parser.add_argument("-o", "--output", type=str, default=None, help=SUPPRESS, metavar="")
|
|
6876
|
+
_add_json_argument(parser)
|
|
6877
|
+
_run_service(parser, argv, TransferAnnotations)
|
|
6878
|
+
|
|
6824
6879
|
@staticmethod
|
|
6825
6880
|
def relative_rate_test(argv):
|
|
6826
6881
|
parser = _new_parser(
|
|
@@ -9190,6 +9245,10 @@ def subtree_prune_regraft(argv=None):
|
|
|
9190
9245
|
Phykit.subtree_prune_regraft(sys.argv[1:])
|
|
9191
9246
|
|
|
9192
9247
|
|
|
9248
|
+
def transfer_annotations(argv=None):
|
|
9249
|
+
Phykit.transfer_annotations(sys.argv[1:])
|
|
9250
|
+
|
|
9251
|
+
|
|
9193
9252
|
def relative_rate_test(argv=None):
|
|
9194
9253
|
Phykit.relative_rate_test(sys.argv[1:])
|
|
9195
9254
|
|
|
@@ -108,6 +108,7 @@ RobinsonFouldsDistance = _LazyServiceFactory("phykit.services.tree.rf_distance",
|
|
|
108
108
|
RootTree = _LazyServiceFactory("phykit.services.tree.root_tree", "RootTree")
|
|
109
109
|
Saturation = _LazyServiceFactory("phykit.services.tree.saturation", "Saturation")
|
|
110
110
|
Spr = _LazyServiceFactory("phykit.services.tree.spr", "Spr")
|
|
111
|
+
TransferAnnotations = _LazyServiceFactory("phykit.services.tree.transfer_annotations", "TransferAnnotations")
|
|
111
112
|
SpuriousSequence = _LazyServiceFactory("phykit.services.tree.spurious_sequence", "SpuriousSequence")
|
|
112
113
|
TerminalBranchStats = _LazyServiceFactory("phykit.services.tree.terminal_branch_stats", "TerminalBranchStats")
|
|
113
114
|
TipLabels = _LazyServiceFactory("phykit.services.tree.tip_labels", "TipLabels")
|
|
@@ -48,6 +48,7 @@ _EXPORTS = {
|
|
|
48
48
|
"TipToTipDistance": "tip_to_tip_distance",
|
|
49
49
|
"TipToTipNodeDistance": "tip_to_tip_node_distance",
|
|
50
50
|
"TotalTreeLength": "total_tree_length",
|
|
51
|
+
"TransferAnnotations": "transfer_annotations",
|
|
51
52
|
"Treeness": "treeness",
|
|
52
53
|
"ThresholdModel": "threshold_model",
|
|
53
54
|
"TreenessOverRCV": "treeness_over_rcv",
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transfer annotations between phylogenies.
|
|
3
|
+
|
|
4
|
+
Copies internal node annotations (e.g., wASTRAL q1/q2/q3, pp1, f1)
|
|
5
|
+
from an annotated source tree onto a target tree with optimized branch
|
|
6
|
+
lengths. Nodes are matched by their bipartition (set of descendant taxa).
|
|
7
|
+
|
|
8
|
+
Typical use case: transfer wASTRAL support annotations onto a
|
|
9
|
+
branch-length-optimized topology from RAxML-NG, IQ-TREE, or any
|
|
10
|
+
other tool for use with quartet_pie or other visualization commands.
|
|
11
|
+
"""
|
|
12
|
+
from io import StringIO
|
|
13
|
+
from typing import Dict, List, Set, Tuple
|
|
14
|
+
|
|
15
|
+
from Bio import Phylo
|
|
16
|
+
|
|
17
|
+
from .base import Tree
|
|
18
|
+
from ...helpers.json_output import print_json
|
|
19
|
+
from ...errors import PhykitUserError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TransferAnnotations(Tree):
|
|
23
|
+
def __init__(self, args) -> None:
|
|
24
|
+
parsed = self.process_args(args)
|
|
25
|
+
super().__init__(tree_file_path=parsed["target_path"])
|
|
26
|
+
self.source_path = parsed["source_path"]
|
|
27
|
+
self.output_path = parsed["output_path"]
|
|
28
|
+
self.json_output = parsed["json_output"]
|
|
29
|
+
|
|
30
|
+
def process_args(self, args) -> Dict:
|
|
31
|
+
return dict(
|
|
32
|
+
target_path=args.target,
|
|
33
|
+
source_path=args.source,
|
|
34
|
+
output_path=getattr(args, "output", None),
|
|
35
|
+
json_output=getattr(args, "json", False),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def run(self) -> None:
|
|
39
|
+
# Read both trees
|
|
40
|
+
target = self.read_tree_file()
|
|
41
|
+
try:
|
|
42
|
+
source = Phylo.read(self.source_path, "newick")
|
|
43
|
+
except FileNotFoundError:
|
|
44
|
+
raise PhykitUserError(
|
|
45
|
+
[
|
|
46
|
+
f"{self.source_path} corresponds to no such file or directory.",
|
|
47
|
+
"Please check filename and pathing",
|
|
48
|
+
],
|
|
49
|
+
code=2,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Validate taxa match
|
|
53
|
+
target_tips = {t.name for t in target.get_terminals()}
|
|
54
|
+
source_tips = {t.name for t in source.get_terminals()}
|
|
55
|
+
|
|
56
|
+
if target_tips != source_tips:
|
|
57
|
+
only_target = target_tips - source_tips
|
|
58
|
+
only_source = source_tips - target_tips
|
|
59
|
+
msgs = ["Taxa in source and target trees do not match."]
|
|
60
|
+
if only_target:
|
|
61
|
+
msgs.append(
|
|
62
|
+
f"Only in target: {', '.join(sorted(only_target)[:5])}"
|
|
63
|
+
+ (f" ... ({len(only_target)} total)" if len(only_target) > 5 else "")
|
|
64
|
+
)
|
|
65
|
+
if only_source:
|
|
66
|
+
msgs.append(
|
|
67
|
+
f"Only in source: {', '.join(sorted(only_source)[:5])}"
|
|
68
|
+
+ (f" ... ({len(only_source)} total)" if len(only_source) > 5 else "")
|
|
69
|
+
)
|
|
70
|
+
raise PhykitUserError(msgs, code=2)
|
|
71
|
+
|
|
72
|
+
# Build bipartition -> annotation map from source tree
|
|
73
|
+
all_taxa = frozenset(source_tips)
|
|
74
|
+
source_annotations = self._extract_annotations(source, all_taxa)
|
|
75
|
+
|
|
76
|
+
# Match and transfer annotations to target tree
|
|
77
|
+
n_transferred, n_unmatched = self._transfer(
|
|
78
|
+
target, source_annotations, all_taxa
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Output
|
|
82
|
+
output_path = self.output_path or f"{self.tree_file_path}.annotated"
|
|
83
|
+
self._write_annotated_tree(target, output_path)
|
|
84
|
+
|
|
85
|
+
if self.json_output:
|
|
86
|
+
self._print_json(
|
|
87
|
+
n_transferred, n_unmatched, output_path,
|
|
88
|
+
len(source_annotations), target_tips,
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
try:
|
|
92
|
+
print(f"Annotations transferred: {n_transferred}")
|
|
93
|
+
print(f"Unmatched nodes: {n_unmatched}")
|
|
94
|
+
print(f"Output: {output_path}")
|
|
95
|
+
except BrokenPipeError:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _get_bipartition(clade, all_taxa: frozenset) -> frozenset:
|
|
100
|
+
"""Get canonical bipartition for a clade (smaller side)."""
|
|
101
|
+
tips = frozenset(t.name for t in clade.get_terminals())
|
|
102
|
+
complement = all_taxa - tips
|
|
103
|
+
# Use the smaller side as the canonical key
|
|
104
|
+
return tips if len(tips) <= len(complement) else complement
|
|
105
|
+
|
|
106
|
+
def _extract_annotations(
|
|
107
|
+
self, tree, all_taxa: frozenset
|
|
108
|
+
) -> Dict[frozenset, str]:
|
|
109
|
+
"""Extract bipartition -> annotation mapping from source tree."""
|
|
110
|
+
annotations = {}
|
|
111
|
+
for clade in tree.find_clades(order="preorder"):
|
|
112
|
+
if clade.is_terminal() or clade == tree.root:
|
|
113
|
+
continue
|
|
114
|
+
annotation = clade.comment or clade.name or ""
|
|
115
|
+
if not annotation:
|
|
116
|
+
continue
|
|
117
|
+
bp = self._get_bipartition(clade, all_taxa)
|
|
118
|
+
if bp:
|
|
119
|
+
annotations[bp] = annotation
|
|
120
|
+
return annotations
|
|
121
|
+
|
|
122
|
+
def _transfer(
|
|
123
|
+
self, target, source_annotations: Dict[frozenset, str],
|
|
124
|
+
all_taxa: frozenset,
|
|
125
|
+
) -> Tuple[int, int]:
|
|
126
|
+
"""Transfer annotations from source to target by bipartition matching."""
|
|
127
|
+
transferred = 0
|
|
128
|
+
unmatched = 0
|
|
129
|
+
|
|
130
|
+
for clade in target.find_clades(order="preorder"):
|
|
131
|
+
if clade.is_terminal() or clade == target.root:
|
|
132
|
+
continue
|
|
133
|
+
bp = self._get_bipartition(clade, all_taxa)
|
|
134
|
+
if bp in source_annotations:
|
|
135
|
+
clade.comment = source_annotations[bp]
|
|
136
|
+
transferred += 1
|
|
137
|
+
else:
|
|
138
|
+
unmatched += 1
|
|
139
|
+
|
|
140
|
+
return transferred, unmatched
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _write_annotated_tree(tree, output_path: str) -> None:
|
|
144
|
+
"""Write tree preserving comment annotations in brackets."""
|
|
145
|
+
Phylo.write(tree, output_path, "newick")
|
|
146
|
+
# BioPython writes comments as [&comment] but wASTRAL uses
|
|
147
|
+
# [comment]. Fix the format.
|
|
148
|
+
with open(output_path) as f:
|
|
149
|
+
content = f.read()
|
|
150
|
+
# Remove the & prefix that BioPython adds
|
|
151
|
+
content = content.replace("[&", "[")
|
|
152
|
+
with open(output_path, "w") as f:
|
|
153
|
+
f.write(content)
|
|
154
|
+
|
|
155
|
+
def _print_json(
|
|
156
|
+
self, n_transferred, n_unmatched, output_path,
|
|
157
|
+
n_source_annotations, target_tips,
|
|
158
|
+
):
|
|
159
|
+
payload = {
|
|
160
|
+
"annotations_transferred": n_transferred,
|
|
161
|
+
"unmatched_nodes": n_unmatched,
|
|
162
|
+
"source_annotations_available": n_source_annotations,
|
|
163
|
+
"n_taxa": len(target_tips),
|
|
164
|
+
"output": output_path,
|
|
165
|
+
}
|
|
166
|
+
print_json(payload, sort_keys=False)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.85"
|
|
@@ -136,6 +136,7 @@ phykit/services/tree/tip_to_tip_node_distance.py
|
|
|
136
136
|
phykit/services/tree/total_tree_length.py
|
|
137
137
|
phykit/services/tree/trait_correlation.py
|
|
138
138
|
phykit/services/tree/trait_rate_map.py
|
|
139
|
+
phykit/services/tree/transfer_annotations.py
|
|
139
140
|
phykit/services/tree/tree_space.py
|
|
140
141
|
phykit/services/tree/treeness.py
|
|
141
142
|
phykit/services/tree/treeness_over_rcv.py
|
|
@@ -9,6 +9,7 @@ pk_aln_recoding = phykit.phykit:alignment_recoding
|
|
|
9
9
|
pk_aln_subsample = phykit.phykit:alignment_subsample
|
|
10
10
|
pk_alng = phykit.phykit:alignment_length_no_gaps
|
|
11
11
|
pk_anc_recon = phykit.phykit:ancestral_state_reconstruction
|
|
12
|
+
pk_annotate_tree = phykit.phykit:transfer_annotations
|
|
12
13
|
pk_aot = phykit.phykit:alignment_outlier_taxa
|
|
13
14
|
pk_asr = phykit.phykit:ancestral_state_reconstruction
|
|
14
15
|
pk_blm = phykit.phykit:branch_length_multiplier
|
|
@@ -207,6 +208,8 @@ pk_trait_corr = phykit.phykit:trait_correlation
|
|
|
207
208
|
pk_trait_correlation = phykit.phykit:trait_correlation
|
|
208
209
|
pk_trait_rate_map = phykit.phykit:trait_rate_map
|
|
209
210
|
pk_traitgram = phykit.phykit:phenogram
|
|
211
|
+
pk_transfer_annot = phykit.phykit:transfer_annotations
|
|
212
|
+
pk_transfer_annotations = phykit.phykit:transfer_annotations
|
|
210
213
|
pk_tree_labels = phykit.phykit:tip_labels
|
|
211
214
|
pk_tree_landscape = phykit.phykit:tree_space
|
|
212
215
|
pk_tree_len = phykit.phykit:total_tree_length
|
phykit-2.1.84/phykit/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.1.84"
|
|
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
|
|
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
|