phykit 2.1.46__tar.gz → 2.1.48__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.46 → phykit-2.1.48}/PKG-INFO +1 -1
- {phykit-2.1.46 → phykit-2.1.48}/phykit/cli_registry.py +2 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/helpers/quartet_utils.py +5 -4
- {phykit-2.1.46 → phykit-2.1.48}/phykit/phykit.py +66 -2
- {phykit-2.1.46 → phykit-2.1.48}/phykit/service_factories.py +1 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/__init__.py +1 -0
- phykit-2.1.48/phykit/services/tree/parsimony_score.py +169 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/quartet_pie.py +3 -2
- phykit-2.1.48/phykit/version.py +1 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit.egg-info/PKG-INFO +1 -1
- {phykit-2.1.46 → phykit-2.1.48}/phykit.egg-info/SOURCES.txt +1 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit.egg-info/entry_points.txt +3 -0
- {phykit-2.1.46 → phykit-2.1.48}/setup.py +3 -0
- phykit-2.1.46/phykit/version.py +0 -1
- {phykit-2.1.46 → phykit-2.1.48}/LICENSE.md +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/README.md +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/__init__.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/__main__.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/errors.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/helpers/__init__.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/helpers/boolean_argument_parsing.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/helpers/caching.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/helpers/discrete_models.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/helpers/files.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/helpers/json_output.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/helpers/parallel.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/helpers/plot_config.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/helpers/stats_summary.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/helpers/streaming.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/__init__.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/__init__.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/alignment_entropy.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/alignment_length.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/alignment_recoding.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/base.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/column_score.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/composition_per_taxon.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/dna_threader.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/faidx.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/gc_content.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/mask_alignment.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/pairwise_identity.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/plot_alignment_qc.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/rcv.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/rcvt.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/rename_fasta_entries.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/alignment/variable_sites.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/base.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/ancestral_reconstruction.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/base.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/bipartition_support_stats.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/branch_length_multiplier.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/collapse_branches.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/concordance_asr.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/consensus_network.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/consensus_tree.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/cont_map.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/cophylo.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/density_map.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/discordance_asymmetry.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/dvmc.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/evo_tempo_map.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/evolutionary_rate.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/fit_continuous.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/fit_discrete.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/hidden_paralogy_check.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/independent_contrasts.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/internal_branch_stats.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/internode_labeler.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/kf_distance.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/lb_score.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/ltt.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/monophyly_check.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/network_signal.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/ou_shift_detection.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/ouwie.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/patristic_distances.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/phenogram.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/phylo_heatmap.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/phylogenetic_glm.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/phylogenetic_ordination.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/phylogenetic_regression.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/phylogenetic_signal.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/phylomorphospace.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/polytomy_test.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/print_tree.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/prune_tree.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/quartet_network.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/rate_heterogeneity.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/relative_rate_test.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/rename_tree_tips.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/rf_distance.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/root_tree.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/saturation.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/spectral_discordance.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/spurious_sequence.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/stochastic_character_map.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/terminal_branch_stats.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/threshold_model.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/tip_labels.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/tip_to_tip_distance.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/total_tree_length.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/treeness.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/treeness_over_rcv.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit/services/tree/vcv_utils.py +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit.egg-info/dependency_links.txt +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit.egg-info/requires.txt +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/phykit.egg-info/top_level.txt +0 -0
- {phykit-2.1.46 → phykit-2.1.48}/setup.cfg +0 -0
|
@@ -88,6 +88,8 @@ ALIAS_TO_HANDLER: Dict[str, str] = {
|
|
|
88
88
|
"pgls": "phylogenetic_regression",
|
|
89
89
|
"phylo_glm": "phylogenetic_glm",
|
|
90
90
|
"pglm": "phylogenetic_glm",
|
|
91
|
+
"parsimony": "parsimony_score",
|
|
92
|
+
"pars": "parsimony_score",
|
|
91
93
|
"pic": "independent_contrasts",
|
|
92
94
|
"phylo_contrasts": "independent_contrasts",
|
|
93
95
|
"asr": "ancestral_state_reconstruction",
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Shared utilities for quartet concordance factor computation and ASTRAL parsing.
|
|
3
3
|
|
|
4
4
|
Provides gene concordance factor (gCF/gDF1/gDF2) computation via the
|
|
5
|
-
four-group bipartition decomposition, and parsing of ASTRAL -t 2
|
|
6
|
-
q1/q2/q3 annotations from Newick node labels.
|
|
5
|
+
four-group bipartition decomposition, and parsing of ASTRAL -t 2 or
|
|
6
|
+
wASTRAL --support 3 q1/q2/q3 annotations from Newick node labels.
|
|
7
7
|
"""
|
|
8
8
|
from typing import Dict, List, Optional, Tuple
|
|
9
9
|
|
|
@@ -93,10 +93,11 @@ def compute_gcf_per_node(
|
|
|
93
93
|
def parse_astral_annotations(
|
|
94
94
|
tree,
|
|
95
95
|
) -> Dict[int, Tuple[float, float, float]]:
|
|
96
|
-
"""Parse q1/q2/q3 annotations from ASTRAL
|
|
96
|
+
"""Parse q1/q2/q3 annotations from ASTRAL/wASTRAL Newick node labels.
|
|
97
97
|
|
|
98
|
-
ASTRAL
|
|
98
|
+
Supports ASTRAL -t 2 and wASTRAL --support 3 output formats:
|
|
99
99
|
'[q1=0.5;q2=0.3;q3=0.2;f1=50;...]'
|
|
100
|
+
'[CULength=2.6;f1=108;...;q1=0.96;q2=0.03;q3=0.01]'
|
|
100
101
|
'q1=0.5;q2=0.3;q3=0.2'
|
|
101
102
|
|
|
102
103
|
Returns dict mapping clade id -> (q1, q2, q3).
|
|
@@ -163,6 +163,8 @@ class Phykit:
|
|
|
163
163
|
===================
|
|
164
164
|
independent_contrasts (alias: pic; phylo_contrasts)
|
|
165
165
|
- Felsenstein's phylogenetically independent contrasts
|
|
166
|
+
parsimony_score (alias: parsimony; pars)
|
|
167
|
+
- Fitch parsimony score of a tree given an alignment
|
|
166
168
|
ancestral_state_reconstruction (alias: asr; anc_recon)
|
|
167
169
|
- estimate ancestral states for continuous traits using
|
|
168
170
|
ML (fast or VCV-based) with optional contMap plot
|
|
@@ -1679,6 +1681,62 @@ class Phykit:
|
|
|
1679
1681
|
_run_service(parser, argv, VariableSites)
|
|
1680
1682
|
|
|
1681
1683
|
## Tree functions
|
|
1684
|
+
@staticmethod
|
|
1685
|
+
def parsimony_score(argv):
|
|
1686
|
+
parser = _new_parser(
|
|
1687
|
+
description=textwrap.dedent(
|
|
1688
|
+
f"""\
|
|
1689
|
+
{help_header}
|
|
1690
|
+
|
|
1691
|
+
Compute the Fitch (1971) maximum parsimony score of a
|
|
1692
|
+
tree given an alignment.
|
|
1693
|
+
|
|
1694
|
+
The parsimony score is the minimum number of character
|
|
1695
|
+
state changes required to explain the alignment on the
|
|
1696
|
+
given tree topology. Each site is scored independently
|
|
1697
|
+
using the Fitch downpass algorithm.
|
|
1698
|
+
|
|
1699
|
+
Gap characters (-, N, X, ?) are treated as wildcards.
|
|
1700
|
+
Multifurcations are automatically resolved.
|
|
1701
|
+
|
|
1702
|
+
Cross-validated against R's phangorn::parsimony().
|
|
1703
|
+
|
|
1704
|
+
Aliases:
|
|
1705
|
+
parsimony_score, parsimony, pars
|
|
1706
|
+
Command line interfaces:
|
|
1707
|
+
pk_parsimony_score, pk_parsimony, pk_pars
|
|
1708
|
+
|
|
1709
|
+
Usage:
|
|
1710
|
+
phykit parsimony_score -t <tree> -a <alignment>
|
|
1711
|
+
[-v/--verbose] [--json]
|
|
1712
|
+
|
|
1713
|
+
Options
|
|
1714
|
+
=====================================================
|
|
1715
|
+
-t/--tree tree file (required)
|
|
1716
|
+
|
|
1717
|
+
-a/--alignment alignment file in FASTA
|
|
1718
|
+
format (required)
|
|
1719
|
+
|
|
1720
|
+
-v/--verbose print per-site parsimony
|
|
1721
|
+
scores
|
|
1722
|
+
|
|
1723
|
+
--json optional argument to output
|
|
1724
|
+
results as JSON
|
|
1725
|
+
"""
|
|
1726
|
+
),
|
|
1727
|
+
)
|
|
1728
|
+
parser.add_argument(
|
|
1729
|
+
"-t", "--tree", type=str, required=True, help=SUPPRESS, metavar=""
|
|
1730
|
+
)
|
|
1731
|
+
parser.add_argument(
|
|
1732
|
+
"-a", "--alignment", type=str, required=True, help=SUPPRESS, metavar=""
|
|
1733
|
+
)
|
|
1734
|
+
parser.add_argument(
|
|
1735
|
+
"-v", "--verbose", action="store_true", required=False, help=SUPPRESS
|
|
1736
|
+
)
|
|
1737
|
+
_add_json_argument(parser)
|
|
1738
|
+
_run_service(parser, argv, ParsimonyScore)
|
|
1739
|
+
|
|
1682
1740
|
@staticmethod
|
|
1683
1741
|
def independent_contrasts(argv):
|
|
1684
1742
|
parser = _new_parser(
|
|
@@ -4689,7 +4747,8 @@ class Phykit:
|
|
|
4689
4747
|
In native mode (-g provided), computes gene concordance
|
|
4690
4748
|
factors (gCF, gDF1, gDF2) from a species tree and gene
|
|
4691
4749
|
trees via bipartition matching. In ASTRAL mode (no -g),
|
|
4692
|
-
parses q1/q2/q3 annotations from ASTRAL -t 2 output
|
|
4750
|
+
parses q1/q2/q3 annotations from ASTRAL -t 2 output or
|
|
4751
|
+
wASTRAL --support 3 output.
|
|
4693
4752
|
|
|
4694
4753
|
Pie slices show: concordant (blue), discordant alt 1
|
|
4695
4754
|
(red), discordant alt 2 (gray).
|
|
@@ -4715,7 +4774,8 @@ class Phykit:
|
|
|
4715
4774
|
|
|
4716
4775
|
-g/--gene-trees gene trees file, one Newick
|
|
4717
4776
|
tree per line (optional;
|
|
4718
|
-
if omitted, ASTRAL -t 2
|
|
4777
|
+
if omitted, ASTRAL -t 2 or
|
|
4778
|
+
wASTRAL --support 3
|
|
4719
4779
|
annotations are parsed)
|
|
4720
4780
|
|
|
4721
4781
|
-o/--output output figure path (required;
|
|
@@ -6725,6 +6785,10 @@ def variable_sites(argv=None):
|
|
|
6725
6785
|
|
|
6726
6786
|
|
|
6727
6787
|
# Tree-based functions
|
|
6788
|
+
def parsimony_score(argv=None):
|
|
6789
|
+
Phykit.parsimony_score(sys.argv[1:])
|
|
6790
|
+
|
|
6791
|
+
|
|
6728
6792
|
def independent_contrasts(argv=None):
|
|
6729
6793
|
Phykit.independent_contrasts(sys.argv[1:])
|
|
6730
6794
|
|
|
@@ -83,6 +83,7 @@ RelativeRateTest = _LazyServiceFactory("phykit.services.tree.relative_rate_test"
|
|
|
83
83
|
ThresholdModel = _LazyServiceFactory("phykit.services.tree.threshold_model", "ThresholdModel")
|
|
84
84
|
PolytomyTest = _LazyServiceFactory("phykit.services.tree.polytomy_test", "PolytomyTest")
|
|
85
85
|
PrintTree = _LazyServiceFactory("phykit.services.tree.print_tree", "PrintTree")
|
|
86
|
+
ParsimonyScore = _LazyServiceFactory("phykit.services.tree.parsimony_score", "ParsimonyScore")
|
|
86
87
|
PhyloHeatmap = _LazyServiceFactory("phykit.services.tree.phylo_heatmap", "PhyloHeatmap")
|
|
87
88
|
PruneTree = _LazyServiceFactory("phykit.services.tree.prune_tree", "PruneTree")
|
|
88
89
|
QuartetPie = _LazyServiceFactory("phykit.services.tree.quartet_pie", "QuartetPie")
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Maximum parsimony score of a tree given an alignment.
|
|
3
|
+
|
|
4
|
+
Computes the Fitch (1971) parsimony score — the minimum number of
|
|
5
|
+
character state changes required to explain the alignment on the
|
|
6
|
+
given tree topology. Each site is scored independently and the
|
|
7
|
+
total is summed.
|
|
8
|
+
|
|
9
|
+
Cross-validated against R's phangorn::parsimony().
|
|
10
|
+
"""
|
|
11
|
+
import copy
|
|
12
|
+
from typing import Dict, List
|
|
13
|
+
|
|
14
|
+
from Bio import SeqIO
|
|
15
|
+
|
|
16
|
+
from .base import Tree
|
|
17
|
+
from ...helpers.json_output import print_json
|
|
18
|
+
from ...errors import PhykitUserError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ParsimonyScore(Tree):
|
|
22
|
+
def __init__(self, args) -> None:
|
|
23
|
+
parsed = self.process_args(args)
|
|
24
|
+
super().__init__(tree_file_path=parsed["tree_file_path"])
|
|
25
|
+
self.alignment_path = parsed["alignment_path"]
|
|
26
|
+
self.verbose = parsed["verbose"]
|
|
27
|
+
self.json_output = parsed["json_output"]
|
|
28
|
+
|
|
29
|
+
def run(self) -> None:
|
|
30
|
+
tree = self.read_tree_file()
|
|
31
|
+
tree = copy.deepcopy(tree)
|
|
32
|
+
self._validate_tree(tree)
|
|
33
|
+
self._resolve_polytomies(tree)
|
|
34
|
+
|
|
35
|
+
sequences = self._parse_alignment(self.alignment_path)
|
|
36
|
+
|
|
37
|
+
# Prune to shared taxa
|
|
38
|
+
tree_tips = set(t.name for t in tree.get_terminals())
|
|
39
|
+
seq_taxa = set(sequences.keys())
|
|
40
|
+
shared = tree_tips & seq_taxa
|
|
41
|
+
if len(shared) < 3:
|
|
42
|
+
raise PhykitUserError(
|
|
43
|
+
[
|
|
44
|
+
f"Only {len(shared)} shared taxa between tree and alignment.",
|
|
45
|
+
"At least 3 shared taxa are required.",
|
|
46
|
+
],
|
|
47
|
+
code=2,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
tips_to_prune = list(tree_tips - shared)
|
|
51
|
+
if tips_to_prune:
|
|
52
|
+
tree = self.prune_tree_using_taxa_list(tree, tips_to_prune)
|
|
53
|
+
self._resolve_polytomies(tree)
|
|
54
|
+
|
|
55
|
+
sequences = {t: sequences[t] for t in shared}
|
|
56
|
+
aln_length = len(next(iter(sequences.values())))
|
|
57
|
+
|
|
58
|
+
total_score, per_site = self._fitch_parsimony(tree, sequences, aln_length)
|
|
59
|
+
|
|
60
|
+
if self.json_output:
|
|
61
|
+
payload = {
|
|
62
|
+
"parsimony_score": total_score,
|
|
63
|
+
"alignment_length": aln_length,
|
|
64
|
+
"n_taxa": len(shared),
|
|
65
|
+
}
|
|
66
|
+
if self.verbose:
|
|
67
|
+
payload["per_site_scores"] = per_site
|
|
68
|
+
print_json(payload)
|
|
69
|
+
else:
|
|
70
|
+
print(total_score)
|
|
71
|
+
if self.verbose:
|
|
72
|
+
print()
|
|
73
|
+
for i, score in enumerate(per_site, 1):
|
|
74
|
+
print(f"{i}\t{score}")
|
|
75
|
+
|
|
76
|
+
def process_args(self, args) -> Dict:
|
|
77
|
+
return dict(
|
|
78
|
+
tree_file_path=args.tree,
|
|
79
|
+
alignment_path=args.alignment,
|
|
80
|
+
verbose=getattr(args, "verbose", False),
|
|
81
|
+
json_output=getattr(args, "json", False),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def _validate_tree(self, tree) -> None:
|
|
85
|
+
tips = list(tree.get_terminals())
|
|
86
|
+
if len(tips) < 3:
|
|
87
|
+
raise PhykitUserError(
|
|
88
|
+
["Tree must have at least 3 tips."], code=2
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _resolve_polytomies(self, tree) -> None:
|
|
92
|
+
from Bio.Phylo import Newick
|
|
93
|
+
for clade in tree.find_clades(order="postorder"):
|
|
94
|
+
while len(clade.clades) > 2:
|
|
95
|
+
child1 = clade.clades.pop()
|
|
96
|
+
child2 = clade.clades.pop()
|
|
97
|
+
new_internal = Newick.Clade(branch_length=0.0)
|
|
98
|
+
new_internal.clades = [child1, child2]
|
|
99
|
+
clade.clades.append(new_internal)
|
|
100
|
+
|
|
101
|
+
def _parse_alignment(self, path: str) -> Dict[str, str]:
|
|
102
|
+
try:
|
|
103
|
+
records = list(SeqIO.parse(path, "fasta"))
|
|
104
|
+
except Exception:
|
|
105
|
+
raise PhykitUserError(
|
|
106
|
+
[f"Could not parse alignment from {path}."], code=2
|
|
107
|
+
)
|
|
108
|
+
if not records:
|
|
109
|
+
raise PhykitUserError(
|
|
110
|
+
["Alignment file is empty."], code=2
|
|
111
|
+
)
|
|
112
|
+
sequences = {r.id: str(r.seq).upper() for r in records}
|
|
113
|
+
|
|
114
|
+
# Validate same length
|
|
115
|
+
lengths = set(len(s) for s in sequences.values())
|
|
116
|
+
if len(lengths) > 1:
|
|
117
|
+
raise PhykitUserError(
|
|
118
|
+
["Sequences have different lengths. Provide an aligned FASTA file."],
|
|
119
|
+
code=2,
|
|
120
|
+
)
|
|
121
|
+
return sequences
|
|
122
|
+
|
|
123
|
+
def _fitch_parsimony(
|
|
124
|
+
self, tree, sequences: Dict[str, str], aln_length: int
|
|
125
|
+
) -> tuple:
|
|
126
|
+
"""Compute Fitch parsimony score.
|
|
127
|
+
|
|
128
|
+
For each site, performs a postorder (downpass) traversal:
|
|
129
|
+
- Terminal nodes: state set = {observed character}
|
|
130
|
+
- Internal nodes: if children share states, intersection;
|
|
131
|
+
otherwise union (and add 1 to score)
|
|
132
|
+
|
|
133
|
+
Gap characters (-) and ambiguous characters (N, X, ?) are
|
|
134
|
+
treated as wildcards (matching any state).
|
|
135
|
+
"""
|
|
136
|
+
wildcard_chars = {"-", "N", "X", "?", "n", "x"}
|
|
137
|
+
all_states = {"A", "C", "G", "T"}
|
|
138
|
+
per_site = []
|
|
139
|
+
total_score = 0
|
|
140
|
+
|
|
141
|
+
for site_idx in range(aln_length):
|
|
142
|
+
site_score = 0
|
|
143
|
+
node_states = {}
|
|
144
|
+
|
|
145
|
+
for clade in tree.find_clades(order="postorder"):
|
|
146
|
+
if clade.is_terminal():
|
|
147
|
+
char = sequences[clade.name][site_idx]
|
|
148
|
+
if char in wildcard_chars:
|
|
149
|
+
node_states[id(clade)] = set(all_states)
|
|
150
|
+
else:
|
|
151
|
+
node_states[id(clade)] = {char}
|
|
152
|
+
else:
|
|
153
|
+
if len(clade.clades) != 2:
|
|
154
|
+
continue
|
|
155
|
+
left, right = clade.clades
|
|
156
|
+
left_states = node_states.get(id(left), set(all_states))
|
|
157
|
+
right_states = node_states.get(id(right), set(all_states))
|
|
158
|
+
|
|
159
|
+
intersection = left_states & right_states
|
|
160
|
+
if intersection:
|
|
161
|
+
node_states[id(clade)] = intersection
|
|
162
|
+
else:
|
|
163
|
+
node_states[id(clade)] = left_states | right_states
|
|
164
|
+
site_score += 1
|
|
165
|
+
|
|
166
|
+
per_site.append(site_score)
|
|
167
|
+
total_score += site_score
|
|
168
|
+
|
|
169
|
+
return total_score, per_site
|
|
@@ -5,7 +5,7 @@ Draws a phylogram with pie charts at internal nodes showing the
|
|
|
5
5
|
proportion of gene trees supporting the species tree topology (gCF)
|
|
6
6
|
versus the two NNI alternative topologies (gDF1, gDF2). Supports
|
|
7
7
|
both native computation from gene trees and parsing of ASTRAL -t 2
|
|
8
|
-
annotations.
|
|
8
|
+
or wASTRAL --support 3 annotations.
|
|
9
9
|
"""
|
|
10
10
|
import sys
|
|
11
11
|
from typing import Dict, List, Tuple
|
|
@@ -50,7 +50,8 @@ class QuartetPie(Tree):
|
|
|
50
50
|
[
|
|
51
51
|
"No ASTRAL q1/q2/q3 annotations found in the tree.",
|
|
52
52
|
"Either provide gene trees with -g, or use an ASTRAL",
|
|
53
|
-
"-t 2 output tree with quartet
|
|
53
|
+
"-t 2 or wASTRAL --support 3 output tree with quartet",
|
|
54
|
+
"annotations.",
|
|
54
55
|
],
|
|
55
56
|
code=2,
|
|
56
57
|
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.48"
|
|
@@ -84,6 +84,7 @@ phykit/services/tree/nearest_neighbor_interchange.py
|
|
|
84
84
|
phykit/services/tree/network_signal.py
|
|
85
85
|
phykit/services/tree/ou_shift_detection.py
|
|
86
86
|
phykit/services/tree/ouwie.py
|
|
87
|
+
phykit/services/tree/parsimony_score.py
|
|
87
88
|
phykit/services/tree/patristic_distances.py
|
|
88
89
|
phykit/services/tree/phenogram.py
|
|
89
90
|
phykit/services/tree/phylo_heatmap.py
|
|
@@ -125,7 +125,10 @@ pk_pairwise_id = phykit.phykit:pairwise_identity
|
|
|
125
125
|
pk_pairwise_identity = phykit.phykit:pairwise_identity
|
|
126
126
|
pk_pal2nal = phykit.phykit:thread_dna
|
|
127
127
|
pk_paqc = phykit.phykit:plot_alignment_qc
|
|
128
|
+
pk_pars = phykit.phykit:parsimony_score
|
|
129
|
+
pk_parsimony = phykit.phykit:parsimony_score
|
|
128
130
|
pk_parsimony_informative_sites = phykit.phykit:parsimony_informative_sites
|
|
131
|
+
pk_parsimony_score = phykit.phykit:parsimony_score
|
|
129
132
|
pk_patristic_distances = phykit.phykit:patristic_distances
|
|
130
133
|
pk_pd = phykit.phykit:patristic_distances
|
|
131
134
|
pk_pdr = phykit.phykit:phylogenetic_ordination
|
|
@@ -101,6 +101,9 @@ setup(
|
|
|
101
101
|
"pk_variable_sites = phykit.phykit:variable_sites",
|
|
102
102
|
"pk_vs = phykit.phykit:variable_sites",
|
|
103
103
|
"pk_ancestral_state_reconstruction = phykit.phykit:ancestral_state_reconstruction", # Tree-based functions
|
|
104
|
+
"pk_parsimony_score = phykit.phykit:parsimony_score",
|
|
105
|
+
"pk_parsimony = phykit.phykit:parsimony_score",
|
|
106
|
+
"pk_pars = phykit.phykit:parsimony_score",
|
|
104
107
|
"pk_independent_contrasts = phykit.phykit:independent_contrasts",
|
|
105
108
|
"pk_pic = phykit.phykit:independent_contrasts",
|
|
106
109
|
"pk_phylo_contrasts = phykit.phykit:independent_contrasts",
|
phykit-2.1.46/phykit/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.1.46"
|
|
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
|