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