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.
Files changed (121) hide show
  1. {phykit-2.1.46 → phykit-2.1.47}/PKG-INFO +1 -1
  2. {phykit-2.1.46 → phykit-2.1.47}/phykit/cli_registry.py +2 -0
  3. {phykit-2.1.46 → phykit-2.1.47}/phykit/phykit.py +62 -0
  4. {phykit-2.1.46 → phykit-2.1.47}/phykit/service_factories.py +1 -0
  5. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/__init__.py +1 -0
  6. phykit-2.1.47/phykit/services/tree/parsimony_score.py +169 -0
  7. phykit-2.1.47/phykit/version.py +1 -0
  8. {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/PKG-INFO +1 -1
  9. {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/SOURCES.txt +1 -0
  10. {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/entry_points.txt +3 -0
  11. {phykit-2.1.46 → phykit-2.1.47}/setup.py +3 -0
  12. phykit-2.1.46/phykit/version.py +0 -1
  13. {phykit-2.1.46 → phykit-2.1.47}/LICENSE.md +0 -0
  14. {phykit-2.1.46 → phykit-2.1.47}/README.md +0 -0
  15. {phykit-2.1.46 → phykit-2.1.47}/phykit/__init__.py +0 -0
  16. {phykit-2.1.46 → phykit-2.1.47}/phykit/__main__.py +0 -0
  17. {phykit-2.1.46 → phykit-2.1.47}/phykit/errors.py +0 -0
  18. {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/__init__.py +0 -0
  19. {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/boolean_argument_parsing.py +0 -0
  20. {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/caching.py +0 -0
  21. {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/discrete_models.py +0 -0
  22. {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/files.py +0 -0
  23. {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/json_output.py +0 -0
  24. {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/parallel.py +0 -0
  25. {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/plot_config.py +0 -0
  26. {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/quartet_utils.py +0 -0
  27. {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/stats_summary.py +0 -0
  28. {phykit-2.1.46 → phykit-2.1.47}/phykit/helpers/streaming.py +0 -0
  29. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/__init__.py +0 -0
  30. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/__init__.py +0 -0
  31. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/alignment_entropy.py +0 -0
  32. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/alignment_length.py +0 -0
  33. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  34. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  35. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/alignment_recoding.py +0 -0
  36. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/base.py +0 -0
  37. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/column_score.py +0 -0
  38. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/composition_per_taxon.py +0 -0
  39. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  40. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  41. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/dna_threader.py +0 -0
  42. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  43. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/faidx.py +0 -0
  44. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/gc_content.py +0 -0
  45. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/mask_alignment.py +0 -0
  46. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  47. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/pairwise_identity.py +0 -0
  48. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  49. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  50. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/rcv.py +0 -0
  51. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/rcvt.py +0 -0
  52. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  53. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  54. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/alignment/variable_sites.py +0 -0
  55. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/base.py +0 -0
  56. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/ancestral_reconstruction.py +0 -0
  57. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/base.py +0 -0
  58. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/bipartition_support_stats.py +0 -0
  59. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/branch_length_multiplier.py +0 -0
  60. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/collapse_branches.py +0 -0
  61. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/concordance_asr.py +0 -0
  62. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/consensus_network.py +0 -0
  63. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/consensus_tree.py +0 -0
  64. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/cont_map.py +0 -0
  65. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/cophylo.py +0 -0
  66. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  67. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/density_map.py +0 -0
  68. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/discordance_asymmetry.py +0 -0
  69. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/dvmc.py +0 -0
  70. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/evo_tempo_map.py +0 -0
  71. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/evolutionary_rate.py +0 -0
  72. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/fit_continuous.py +0 -0
  73. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/fit_discrete.py +0 -0
  74. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  75. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/independent_contrasts.py +0 -0
  76. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/internal_branch_stats.py +0 -0
  77. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/internode_labeler.py +0 -0
  78. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/kf_distance.py +0 -0
  79. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  80. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/lb_score.py +0 -0
  81. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/ltt.py +0 -0
  82. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/monophyly_check.py +0 -0
  83. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  84. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/network_signal.py +0 -0
  85. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/ou_shift_detection.py +0 -0
  86. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/ouwie.py +0 -0
  87. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/patristic_distances.py +0 -0
  88. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phenogram.py +0 -0
  89. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylo_heatmap.py +0 -0
  90. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylogenetic_glm.py +0 -0
  91. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  92. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylogenetic_regression.py +0 -0
  93. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylogenetic_signal.py +0 -0
  94. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/phylomorphospace.py +0 -0
  95. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/polytomy_test.py +0 -0
  96. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/print_tree.py +0 -0
  97. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/prune_tree.py +0 -0
  98. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/quartet_network.py +0 -0
  99. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/quartet_pie.py +0 -0
  100. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/rate_heterogeneity.py +0 -0
  101. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/relative_rate_test.py +0 -0
  102. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/rename_tree_tips.py +0 -0
  103. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/rf_distance.py +0 -0
  104. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/root_tree.py +0 -0
  105. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/saturation.py +0 -0
  106. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/spectral_discordance.py +0 -0
  107. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/spurious_sequence.py +0 -0
  108. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/stochastic_character_map.py +0 -0
  109. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/terminal_branch_stats.py +0 -0
  110. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/threshold_model.py +0 -0
  111. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/tip_labels.py +0 -0
  112. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  113. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  114. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/total_tree_length.py +0 -0
  115. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/treeness.py +0 -0
  116. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/treeness_over_rcv.py +0 -0
  117. {phykit-2.1.46 → phykit-2.1.47}/phykit/services/tree/vcv_utils.py +0 -0
  118. {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/dependency_links.txt +0 -0
  119. {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/requires.txt +0 -0
  120. {phykit-2.1.46 → phykit-2.1.47}/phykit.egg-info/top_level.txt +0 -0
  121. {phykit-2.1.46 → phykit-2.1.47}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: phykit
3
- Version: 2.1.46
3
+ Version: 2.1.47
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -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")
@@ -27,6 +27,7 @@ _EXPORTS = {
27
27
  "RelativeRateTest": "relative_rate_test",
28
28
  "PolytomyTest": "polytomy_test",
29
29
  "PrintTree": "print_tree",
30
+ "ParsimonyScore": "parsimony_score",
30
31
  "PhyloHeatmap": "phylo_heatmap",
31
32
  "PruneTree": "prune_tree",
32
33
  "QuartetPie": "quartet_pie",
@@ -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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: phykit
3
- Version: 2.1.46
3
+ Version: 2.1.47
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -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",
@@ -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