phykit 2.1.46__tar.gz → 2.1.47__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.47}/PKG-INFO +1 -1
- {phykit-2.1.46 → phykit-2.1.47}/phykit/cli_registry.py +2 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/phykit.py +62 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/service_factories.py +1 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/__init__.py +1 -0
- phykit-2.1.47/phykit/services/tree/parsimony_score.py +169 -0
- phykit-2.1.47/phykit/version.py +1 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/PKG-INFO +1 -1
- {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/SOURCES.txt +1 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/entry_points.txt +3 -0
- {phykit-2.1.46 → phykit-2.1.47}/setup.py +3 -0
- phykit-2.1.46/phykit/version.py +0 -1
- {phykit-2.1.46 → phykit-2.1.47}/LICENSE.md +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/README.md +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/__init__.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/__main__.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/errors.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/__init__.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/boolean_argument_parsing.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/caching.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/discrete_models.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/files.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/json_output.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/parallel.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/plot_config.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/quartet_utils.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/stats_summary.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/streaming.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/__init__.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/__init__.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/alignment_entropy.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/alignment_length.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/alignment_recoding.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/base.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/column_score.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/composition_per_taxon.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/dna_threader.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/faidx.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/gc_content.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/mask_alignment.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/pairwise_identity.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/plot_alignment_qc.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/rcv.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/rcvt.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/rename_fasta_entries.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/variable_sites.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/base.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/ancestral_reconstruction.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/base.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/bipartition_support_stats.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/branch_length_multiplier.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/collapse_branches.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/concordance_asr.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/consensus_network.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/consensus_tree.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/cont_map.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/cophylo.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/density_map.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/discordance_asymmetry.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/dvmc.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/evo_tempo_map.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/evolutionary_rate.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/fit_continuous.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/fit_discrete.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/hidden_paralogy_check.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/independent_contrasts.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/internal_branch_stats.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/internode_labeler.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/kf_distance.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/lb_score.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/ltt.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/monophyly_check.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/network_signal.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/ou_shift_detection.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/ouwie.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/patristic_distances.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phenogram.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylo_heatmap.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylogenetic_glm.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylogenetic_ordination.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylogenetic_regression.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylogenetic_signal.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylomorphospace.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/polytomy_test.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/print_tree.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/prune_tree.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/quartet_network.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/quartet_pie.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/rate_heterogeneity.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/relative_rate_test.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/rename_tree_tips.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/rf_distance.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/root_tree.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/saturation.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/spectral_discordance.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/spurious_sequence.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/stochastic_character_map.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/terminal_branch_stats.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/threshold_model.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/tip_labels.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/tip_to_tip_distance.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/total_tree_length.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/treeness.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/treeness_over_rcv.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/vcv_utils.py +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/dependency_links.txt +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/requires.txt +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/top_level.txt +0 -0
- {phykit-2.1.46 → phykit-2.1.47}/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",
|
|
@@ -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(
|
|
@@ -6725,6 +6783,10 @@ def variable_sites(argv=None):
|
|
|
6725
6783
|
|
|
6726
6784
|
|
|
6727
6785
|
# Tree-based functions
|
|
6786
|
+
def parsimony_score(argv=None):
|
|
6787
|
+
Phykit.parsimony_score(sys.argv[1:])
|
|
6788
|
+
|
|
6789
|
+
|
|
6728
6790
|
def independent_contrasts(argv=None):
|
|
6729
6791
|
Phykit.independent_contrasts(sys.argv[1:])
|
|
6730
6792
|
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.47"
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|