phykit 2.1.64__tar.gz → 2.1.66__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 (131) hide show
  1. {phykit-2.1.64 → phykit-2.1.66}/PKG-INFO +3 -2
  2. {phykit-2.1.64 → phykit-2.1.66}/phykit/phykit.py +49 -16
  3. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/dstatistic.py +200 -4
  4. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/quartet_pie.py +4 -2
  5. phykit-2.1.66/phykit/version.py +1 -0
  6. {phykit-2.1.64 → phykit-2.1.66}/phykit.egg-info/PKG-INFO +3 -2
  7. {phykit-2.1.64 → phykit-2.1.66}/setup.py +2 -1
  8. phykit-2.1.64/phykit/version.py +0 -1
  9. {phykit-2.1.64 → phykit-2.1.66}/LICENSE.md +0 -0
  10. {phykit-2.1.64 → phykit-2.1.66}/README.md +0 -0
  11. {phykit-2.1.64 → phykit-2.1.66}/phykit/__init__.py +0 -0
  12. {phykit-2.1.64 → phykit-2.1.66}/phykit/__main__.py +0 -0
  13. {phykit-2.1.64 → phykit-2.1.66}/phykit/cli_registry.py +0 -0
  14. {phykit-2.1.64 → phykit-2.1.66}/phykit/errors.py +0 -0
  15. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/__init__.py +0 -0
  16. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/boolean_argument_parsing.py +0 -0
  17. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/caching.py +0 -0
  18. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/circular_layout.py +0 -0
  19. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/color_annotations.py +0 -0
  20. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/discrete_models.py +0 -0
  21. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/files.py +0 -0
  22. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/json_output.py +0 -0
  23. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/parallel.py +0 -0
  24. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/parsimony_utils.py +0 -0
  25. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/plot_config.py +0 -0
  26. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/quartet_utils.py +0 -0
  27. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/stats_summary.py +0 -0
  28. {phykit-2.1.64 → phykit-2.1.66}/phykit/helpers/streaming.py +0 -0
  29. {phykit-2.1.64 → phykit-2.1.66}/phykit/service_factories.py +0 -0
  30. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/__init__.py +0 -0
  31. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/__init__.py +0 -0
  32. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/alignment_entropy.py +0 -0
  33. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/alignment_length.py +0 -0
  34. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  35. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  36. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/alignment_recoding.py +0 -0
  37. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/alignment_subsample.py +0 -0
  38. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/base.py +0 -0
  39. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/column_score.py +0 -0
  40. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/composition_per_taxon.py +0 -0
  41. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  42. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  43. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/dna_threader.py +0 -0
  44. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  45. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/faidx.py +0 -0
  46. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/gc_content.py +0 -0
  47. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/identity_matrix.py +0 -0
  48. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/mask_alignment.py +0 -0
  49. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  50. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/pairwise_identity.py +0 -0
  51. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  52. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  53. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/rcv.py +0 -0
  54. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/rcvt.py +0 -0
  55. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  56. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  57. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/alignment/variable_sites.py +0 -0
  58. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/base.py +0 -0
  59. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/__init__.py +0 -0
  60. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/ancestral_reconstruction.py +0 -0
  61. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/base.py +0 -0
  62. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/bipartition_support_stats.py +0 -0
  63. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/branch_length_multiplier.py +0 -0
  64. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/character_map.py +0 -0
  65. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/collapse_branches.py +0 -0
  66. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/concordance_asr.py +0 -0
  67. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/consensus_network.py +0 -0
  68. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/consensus_tree.py +0 -0
  69. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/cont_map.py +0 -0
  70. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/cophylo.py +0 -0
  71. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  72. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/density_map.py +0 -0
  73. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/discordance_asymmetry.py +0 -0
  74. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/dvmc.py +0 -0
  75. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/evo_tempo_map.py +0 -0
  76. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/evolutionary_rate.py +0 -0
  77. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/fit_continuous.py +0 -0
  78. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/fit_discrete.py +0 -0
  79. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  80. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/independent_contrasts.py +0 -0
  81. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/internal_branch_stats.py +0 -0
  82. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/internode_labeler.py +0 -0
  83. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/kf_distance.py +0 -0
  84. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  85. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/lb_score.py +0 -0
  86. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/ltt.py +0 -0
  87. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/monophyly_check.py +0 -0
  88. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  89. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/network_signal.py +0 -0
  90. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/ou_shift_detection.py +0 -0
  91. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/ouwie.py +0 -0
  92. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/parsimony_score.py +0 -0
  93. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/patristic_distances.py +0 -0
  94. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/phenogram.py +0 -0
  95. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/phylo_heatmap.py +0 -0
  96. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/phylogenetic_glm.py +0 -0
  97. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  98. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/phylogenetic_regression.py +0 -0
  99. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/phylogenetic_signal.py +0 -0
  100. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/phylomorphospace.py +0 -0
  101. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/polytomy_test.py +0 -0
  102. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/print_tree.py +0 -0
  103. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/prune_tree.py +0 -0
  104. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/quartet_network.py +0 -0
  105. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/rate_heterogeneity.py +0 -0
  106. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/relative_rate_test.py +0 -0
  107. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/rename_tree_tips.py +0 -0
  108. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/rf_distance.py +0 -0
  109. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/root_tree.py +0 -0
  110. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/saturation.py +0 -0
  111. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/spectral_discordance.py +0 -0
  112. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/spurious_sequence.py +0 -0
  113. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/stochastic_character_map.py +0 -0
  114. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/terminal_branch_stats.py +0 -0
  115. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/threshold_model.py +0 -0
  116. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/tip_labels.py +0 -0
  117. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  118. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  119. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/total_tree_length.py +0 -0
  120. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/trait_correlation.py +0 -0
  121. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/trait_rate_map.py +0 -0
  122. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/tree_space.py +0 -0
  123. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/treeness.py +0 -0
  124. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/treeness_over_rcv.py +0 -0
  125. {phykit-2.1.64 → phykit-2.1.66}/phykit/services/tree/vcv_utils.py +0 -0
  126. {phykit-2.1.64 → phykit-2.1.66}/phykit.egg-info/SOURCES.txt +0 -0
  127. {phykit-2.1.64 → phykit-2.1.66}/phykit.egg-info/dependency_links.txt +0 -0
  128. {phykit-2.1.64 → phykit-2.1.66}/phykit.egg-info/entry_points.txt +0 -0
  129. {phykit-2.1.64 → phykit-2.1.66}/phykit.egg-info/requires.txt +0 -0
  130. {phykit-2.1.64 → phykit-2.1.66}/phykit.egg-info/top_level.txt +0 -0
  131. {phykit-2.1.64 → phykit-2.1.66}/setup.cfg +0 -0
@@ -1,17 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.64
3
+ Version: 2.1.66
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
7
7
  Classifier: Operating System :: OS Independent
8
8
  Classifier: Intended Audience :: Science/Research
9
9
  Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3.10
10
11
  Classifier: Programming Language :: Python :: 3.11
11
12
  Classifier: Programming Language :: Python :: 3.12
12
13
  Classifier: Programming Language :: Python :: 3.13
13
14
  Classifier: Topic :: Scientific/Engineering
14
- Requires-Python: >=3.11
15
+ Requires-Python: >=3.10
15
16
  Description-Content-Type: text/markdown
16
17
  License-File: LICENSE.md
17
18
  Requires-Dist: biopython>=1.82
@@ -1956,21 +1956,26 @@ class Phykit:
1956
1956
  {help_header}
1957
1957
 
1958
1958
  Compute Patterson's D-statistic (ABBA-BABA test) for
1959
- detecting introgression or gene flow from a four-taxon
1960
- alignment.
1959
+ detecting introgression or gene flow.
1961
1960
 
1962
- The assumed topology is (((P1, P2), P3), Outgroup).
1963
- Under incomplete lineage sorting (ILS) alone, ABBA and
1964
- BABA site patterns should be equally frequent. A
1965
- significant excess of either pattern indicates gene
1966
- flow.
1961
+ Two input modes:
1962
+ 1) Site patterns from an alignment (-a)
1963
+ 2) Quartet topologies from gene trees (-g)
1967
1964
 
1968
- D > 0 suggests gene flow between P2 and P3.
1969
- D < 0 suggests gene flow between P1 and P3.
1970
- D = 0 is consistent with ILS alone.
1965
+ Species topology: (((P1, P2), P3), Outgroup).
1966
+ Under ILS alone, ABBA and BABA patterns (or
1967
+ discordant topologies) are equally frequent. A
1968
+ significant excess indicates introgression.
1971
1969
 
1972
- Significance is assessed via block jackknife (Green
1973
- et al. 2010; Patterson et al. 2012).
1970
+ D > 0: introgression between P2 and P3.
1971
+ D < 0: introgression between P1 and P3.
1972
+ D = 0: consistent with ILS alone.
1973
+ Note: D identifies which lineages exchanged genes
1974
+ but cannot determine direction of flow.
1975
+
1976
+ Gene trees can have any number of taxa; only the
1977
+ quartet induced by the four specified taxa is
1978
+ evaluated from each tree.
1974
1979
 
1975
1980
  Aliases:
1976
1981
  dstatistic, dstat, abba_baba
@@ -1978,13 +1983,22 @@ class Phykit:
1978
1983
  pk_dstatistic, pk_dstat, pk_abba_baba
1979
1984
 
1980
1985
  Usage:
1981
- phykit dstatistic -a <alignment> --p1 <taxon> --p2 <taxon>
1982
- --p3 <taxon> --outgroup <taxon>
1986
+ phykit dstatistic -a <alignment> --p1 <taxon>
1987
+ --p2 <taxon> --p3 <taxon> --outgroup <taxon>
1983
1988
  [--block-size 100] [--json]
1989
+ phykit dstatistic -g <gene_trees> --p1 <taxon>
1990
+ --p2 <taxon> --p3 <taxon> --outgroup <taxon>
1991
+ [--json]
1984
1992
 
1985
1993
  Options
1986
1994
  =====================================================
1987
1995
  -a/--alignment FASTA alignment file
1996
+ (site-pattern mode)
1997
+
1998
+ -g/--gene-trees gene trees file, one
1999
+ Newick per line (gene-
2000
+ tree mode; trees can
2001
+ have any number of taxa)
1988
2002
 
1989
2003
  --p1 taxon name for P1
1990
2004
  (sister to P2)
@@ -2000,18 +2014,28 @@ class Phykit:
2000
2014
 
2001
2015
  --block-size block size for jackknife
2002
2016
  estimation of standard
2003
- error (default: 100)
2017
+ error (default: 100;
2018
+ alignment mode only)
2019
+
2020
+ --support minimum branch support
2021
+ threshold for gene trees;
2022
+ branches below this value
2023
+ are collapsed (treated as
2024
+ unresolved). Gene-tree
2025
+ mode only.
2004
2026
 
2005
2027
  --json output results as JSON
2006
2028
  """
2007
2029
  ),
2008
2030
  )
2009
- parser.add_argument("-a", "--alignment", type=str, required=True, help=SUPPRESS, metavar="")
2031
+ parser.add_argument("-a", "--alignment", type=str, required=False, default=None, help=SUPPRESS, metavar="")
2032
+ parser.add_argument("-g", "--gene-trees", type=str, required=False, default=None, help=SUPPRESS, metavar="")
2010
2033
  parser.add_argument("--p1", type=str, required=True, help=SUPPRESS, metavar="")
2011
2034
  parser.add_argument("--p2", type=str, required=True, help=SUPPRESS, metavar="")
2012
2035
  parser.add_argument("--p3", type=str, required=True, help=SUPPRESS, metavar="")
2013
2036
  parser.add_argument("--outgroup", type=str, required=True, help=SUPPRESS, metavar="")
2014
2037
  parser.add_argument("--block-size", type=int, default=100, help=SUPPRESS, metavar="")
2038
+ parser.add_argument("--support", type=float, default=None, help=SUPPRESS, metavar="")
2015
2039
  _add_json_argument(parser)
2016
2040
  _run_service(parser, argv, Dstatistic)
2017
2041
 
@@ -5556,6 +5580,11 @@ class Phykit:
5556
5580
  --csv output per-branch concordance
5557
5581
  values as a CSV file
5558
5582
 
5583
+ --pie-size scale factor for pie chart
5584
+ size (default: 1.0; use
5585
+ 2.0 for double, 0.5 for
5586
+ half, etc.)
5587
+
5559
5588
  --json optional argument to output
5560
5589
  per-node concordance as JSON
5561
5590
  """
@@ -5578,6 +5607,10 @@ class Phykit:
5578
5607
  "--csv", type=str, required=False, default=None,
5579
5608
  help=SUPPRESS, metavar=""
5580
5609
  )
5610
+ parser.add_argument(
5611
+ "--pie-size", type=float, required=False, default=1.0,
5612
+ help=SUPPRESS, metavar=""
5613
+ )
5581
5614
  add_plot_arguments(parser)
5582
5615
  _add_json_argument(parser)
5583
5616
  _run_service(parser, argv, QuartetPie)
@@ -1,9 +1,15 @@
1
- """Patterson's D-statistic (ABBA-BABA test) for detecting introgression."""
1
+ """Patterson's D-statistic (ABBA-BABA test) for detecting introgression.
2
2
 
3
- from typing import Dict
3
+ Supports two modes:
4
+ 1) Site patterns from a FASTA alignment (-a)
5
+ 2) Quartet topologies from gene trees (-g)
6
+ """
7
+
8
+ from io import StringIO
9
+ from typing import Dict, List, Optional, Tuple
4
10
 
5
11
  import numpy as np
6
- from Bio import SeqIO
12
+ from Bio import Phylo, SeqIO
7
13
 
8
14
  from .base import Alignment
9
15
  from ...helpers.json_output import print_json
@@ -14,25 +20,215 @@ class Dstatistic(Alignment):
14
20
  def __init__(self, args) -> None:
15
21
  parsed = self.process_args(args)
16
22
  super().__init__(alignment_file_path=parsed["alignment_path"])
23
+ self.gene_trees_path = parsed["gene_trees_path"]
17
24
  self.p1 = parsed["p1"]
18
25
  self.p2 = parsed["p2"]
19
26
  self.p3 = parsed["p3"]
20
27
  self.outgroup = parsed["outgroup"]
21
28
  self.block_size = parsed["block_size"]
29
+ self.support_threshold = parsed["support_threshold"]
22
30
  self.json_output = parsed["json_output"]
23
31
 
24
32
  def process_args(self, args) -> Dict[str, object]:
33
+ aln = getattr(args, "alignment", None)
34
+ gt = getattr(args, "gene_trees", None)
35
+ if aln is None and gt is None:
36
+ raise PhykitUserError(
37
+ ["Either -a/--alignment or -g/--gene-trees is required."],
38
+ code=2,
39
+ )
40
+ if aln is not None and gt is not None:
41
+ raise PhykitUserError(
42
+ ["-a/--alignment and -g/--gene-trees are mutually exclusive."],
43
+ code=2,
44
+ )
25
45
  return dict(
26
- alignment_path=args.alignment,
46
+ alignment_path=aln,
47
+ gene_trees_path=gt,
27
48
  p1=args.p1,
28
49
  p2=args.p2,
29
50
  p3=args.p3,
30
51
  outgroup=args.outgroup,
31
52
  block_size=getattr(args, "block_size", 100),
53
+ support_threshold=getattr(args, "support", None),
32
54
  json_output=getattr(args, "json", False),
33
55
  )
34
56
 
35
57
  def run(self):
58
+ if self.gene_trees_path:
59
+ self._run_gene_tree_mode()
60
+ else:
61
+ self._run_alignment_mode()
62
+
63
+ # ------------------------------------------------------------------
64
+ # Gene tree mode
65
+ # ------------------------------------------------------------------
66
+
67
+ def _run_gene_tree_mode(self):
68
+ """Count quartet topologies from gene trees."""
69
+ gene_trees = self._parse_gene_trees(self.gene_trees_path)
70
+ quartet = (self.p1, self.p2, self.p3, self.outgroup)
71
+
72
+ # For each gene tree, determine the quartet topology
73
+ # Species tree: (((P1,P2),P3),O)
74
+ # Concordant: P1+P2 together → ((P1,P2),(P3,O))
75
+ # ABBA: P2+P3 together → ((P2,P3),(P1,O))
76
+ # BABA: P1+P3 together → ((P1,P3),(P2,O))
77
+ concordant = 0
78
+ abba_count = 0
79
+ baba_count = 0
80
+ unresolved = 0
81
+
82
+ for gt in gene_trees:
83
+ topo = self._get_quartet_topology(gt, quartet)
84
+ if topo == "concordant":
85
+ concordant += 1
86
+ elif topo == "abba":
87
+ abba_count += 1
88
+ elif topo == "baba":
89
+ baba_count += 1
90
+ else:
91
+ unresolved += 1
92
+
93
+ n_informative = abba_count + baba_count
94
+ n_total = len(gene_trees)
95
+
96
+ # D-statistic
97
+ if n_informative == 0:
98
+ d_stat = 0.0
99
+ else:
100
+ d_stat = (abba_count - baba_count) / n_informative
101
+
102
+ # Chi-squared test: are ABBA and BABA equally frequent?
103
+ from scipy.stats import chi2
104
+
105
+ p_value = None
106
+ chi2_stat = None
107
+ if n_informative > 0:
108
+ expected = n_informative / 2.0
109
+ chi2_stat = ((abba_count - expected) ** 2 + (baba_count - expected) ** 2) / expected
110
+ p_value = float(chi2.sf(chi2_stat, df=1))
111
+
112
+ # Output
113
+ if self.json_output:
114
+ payload = {
115
+ "mode": "gene_trees",
116
+ "p1": self.p1,
117
+ "p2": self.p2,
118
+ "p3": self.p3,
119
+ "outgroup": self.outgroup,
120
+ "n_gene_trees": n_total,
121
+ "concordant": concordant,
122
+ "abba_count": abba_count,
123
+ "baba_count": baba_count,
124
+ "unresolved": unresolved,
125
+ "d_statistic": round(d_stat, 4),
126
+ "support_threshold": self.support_threshold,
127
+ "chi2_statistic": round(chi2_stat, 4) if chi2_stat is not None else None,
128
+ "p_value": round(p_value, 6) if p_value is not None else None,
129
+ }
130
+ print_json(payload, sort_keys=False)
131
+ return
132
+
133
+ try:
134
+ print("Patterson's D-statistic (Gene Tree Mode)")
135
+ print("=========================================")
136
+ print(f"Topology: ((({self.p1}, {self.p2}), {self.p3}), {self.outgroup})")
137
+ print(f"P1: {self.p1}")
138
+ print(f"P2: {self.p2}")
139
+ print(f"P3: {self.p3}")
140
+ print(f"Outgroup: {self.outgroup}")
141
+ print()
142
+ print(f"Gene trees: {n_total}")
143
+ if self.support_threshold is not None:
144
+ print(f"Support threshold: {self.support_threshold}")
145
+ print(f"Concordant ((P1,P2),P3): {concordant}")
146
+ print(f"ABBA ((P2,P3),P1): {abba_count}")
147
+ print(f"BABA ((P1,P3),P2): {baba_count}")
148
+ print(f"Unresolved: {unresolved}")
149
+ print(f"D-statistic: {d_stat:.4f}")
150
+ if chi2_stat is not None:
151
+ print(f"Chi-squared: {chi2_stat:.4f}")
152
+ print(f"p-value: {p_value:.6f}")
153
+ print()
154
+ print(f"Interpretation: {self._interpret(d_stat, p_value)}")
155
+ else:
156
+ print()
157
+ print("No informative (discordant) gene trees found.")
158
+ except BrokenPipeError:
159
+ pass
160
+
161
+ def _parse_gene_trees(self, path: str) -> list:
162
+ """Parse gene trees from a file (one Newick per line)."""
163
+ try:
164
+ return list(Phylo.parse(path, "newick"))
165
+ except Exception:
166
+ raise PhykitUserError(
167
+ [f"Could not parse gene trees from {path}."],
168
+ code=2,
169
+ )
170
+
171
+ def _get_quartet_topology(self, tree, quartet) -> str:
172
+ """Determine quartet topology from a (possibly multi-taxon) gene tree.
173
+
174
+ If support_threshold is set, branches with support below the
175
+ threshold are excluded (treated as collapsed/unresolved).
176
+
177
+ Returns 'concordant', 'abba', 'baba', or 'unresolved'.
178
+ """
179
+ p1, p2, p3, outgroup = quartet
180
+
181
+ # Get all taxa in the tree
182
+ tree_taxa = {t.name for t in tree.get_terminals()}
183
+
184
+ # Check all four taxa are present
185
+ if not all(t in tree_taxa for t in quartet):
186
+ return "unresolved"
187
+
188
+ # Extract bipartitions from the gene tree
189
+ # Skip branches with support below threshold
190
+ all_taxa = frozenset(tree_taxa)
191
+ bipartitions = []
192
+ for clade in tree.get_nonterminals():
193
+ # Check support threshold
194
+ if self.support_threshold is not None:
195
+ support = clade.confidence
196
+ if support is not None and support < self.support_threshold:
197
+ continue # collapse this branch (skip its bipartition)
198
+
199
+ tips = frozenset(t.name for t in clade.get_terminals())
200
+ if len(tips) <= 1 or tips == all_taxa:
201
+ continue
202
+ complement = all_taxa - tips
203
+ if len(complement) <= 0:
204
+ continue
205
+ bipartitions.append((tips, complement))
206
+
207
+ # Check which quartet topology the bipartitions support
208
+ quartet_set = {p1, p2, p3, outgroup}
209
+ for side_a, side_b in bipartitions:
210
+ in_a = quartet_set & side_a
211
+ in_b = quartet_set & side_b
212
+ if len(in_a) == 2 and len(in_b) == 2:
213
+ pair = frozenset(in_a)
214
+ # Concordant: P1+P2 on one side
215
+ if pair == frozenset({p1, p2}) or pair == frozenset({p3, outgroup}):
216
+ return "concordant"
217
+ # ABBA: P2+P3 on one side
218
+ if pair == frozenset({p2, p3}) or pair == frozenset({p1, outgroup}):
219
+ return "abba"
220
+ # BABA: P1+P3 on one side
221
+ if pair == frozenset({p1, p3}) or pair == frozenset({p2, outgroup}):
222
+ return "baba"
223
+
224
+ return "unresolved"
225
+
226
+ # ------------------------------------------------------------------
227
+ # Alignment mode
228
+ # ------------------------------------------------------------------
229
+
230
+ def _run_alignment_mode(self):
231
+ """Count ABBA/BABA site patterns from an alignment."""
36
232
  # Read alignment sequences
37
233
  sequences = {}
38
234
  for record in SeqIO.parse(self.alignment_file_path, "fasta"):
@@ -48,6 +48,7 @@ class QuartetPie(Tree):
48
48
  self.annotate = parsed["annotate"]
49
49
  self.json_output = parsed["json_output"]
50
50
  self.csv_output = parsed["csv_output"]
51
+ self.pie_size = parsed["pie_size"]
51
52
  self.plot_config = parsed["plot_config"]
52
53
 
53
54
  def run(self) -> None:
@@ -104,6 +105,7 @@ class QuartetPie(Tree):
104
105
  annotate=getattr(args, "annotate", False),
105
106
  json_output=getattr(args, "json", False),
106
107
  csv_output=getattr(args, "csv", None),
108
+ pie_size=getattr(args, "pie_size", 1.0),
107
109
  plot_config=PlotConfig.from_args(args),
108
110
  )
109
111
 
@@ -253,7 +255,7 @@ class QuartetPie(Tree):
253
255
  # Circular mode: scale with n_tips but stay larger than rectangular
254
256
  # since radial spacing gives more room between nodes
255
257
  n_tips = len(tips)
256
- pie_size = min(0.05, 0.6 / max(n_tips, 1))
258
+ pie_size = min(0.05, 0.6 / max(n_tips, 1)) * self.pie_size
257
259
 
258
260
  for clade in tree.find_clades(order="preorder"):
259
261
  if clade.is_terminal() or clade == root:
@@ -400,7 +402,7 @@ class QuartetPie(Tree):
400
402
  # appear as perfect circles regardless of axis scaling, and are
401
403
  # drawn above the phylogeny branches.
402
404
  n_tips = len(tips)
403
- pie_size = min(0.06, 0.8 / max(n_tips, 1))
405
+ pie_size = min(0.06, 0.8 / max(n_tips, 1)) * self.pie_size
404
406
 
405
407
  for clade in tree.find_clades(order="preorder"):
406
408
  if clade.is_terminal() or clade == root:
@@ -0,0 +1 @@
1
+ __version__ = "2.1.66"
@@ -1,17 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.64
3
+ Version: 2.1.66
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
7
7
  Classifier: Operating System :: OS Independent
8
8
  Classifier: Intended Audience :: Science/Research
9
9
  Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3.10
10
11
  Classifier: Programming Language :: Python :: 3.11
11
12
  Classifier: Programming Language :: Python :: 3.12
12
13
  Classifier: Programming Language :: Python :: 3.13
13
14
  Classifier: Topic :: Scientific/Engineering
14
- Requires-Python: >=3.11
15
+ Requires-Python: >=3.10
15
16
  Description-Content-Type: text/markdown
16
17
  License-File: LICENSE.md
17
18
  Requires-Dist: biopython>=1.82
@@ -12,6 +12,7 @@ CLASSIFIERS = [
12
12
  'Operating System :: OS Independent',
13
13
  'Intended Audience :: Science/Research',
14
14
  'Programming Language :: Python',
15
+ 'Programming Language :: Python :: 3.10',
15
16
  'Programming Language :: Python :: 3.11',
16
17
  'Programming Language :: Python :: 3.12',
17
18
  'Programming Language :: Python :: 3.13',
@@ -37,7 +38,7 @@ setup(
37
38
  author_email="jlsteenwyk@gmail.com",
38
39
  url="https://github.com/jlsteenwyk/phykit",
39
40
  packages=find_packages(),
40
- python_requires=">=3.11",
41
+ python_requires=">=3.10",
41
42
  classifiers=CLASSIFIERS,
42
43
  entry_points={
43
44
  "console_scripts": [
@@ -1 +0,0 @@
1
- __version__ = "2.1.64"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes