phykit 2.1.88__tar.gz → 2.1.90__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 (149) hide show
  1. {phykit-2.1.88 → phykit-2.1.90}/PKG-INFO +1 -1
  2. {phykit-2.1.88 → phykit-2.1.90}/phykit/cli_registry.py +2 -0
  3. {phykit-2.1.88 → phykit-2.1.90}/phykit/phykit.py +77 -0
  4. {phykit-2.1.88 → phykit-2.1.90}/phykit/service_factories.py +1 -0
  5. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/__init__.py +1 -0
  6. phykit-2.1.90/phykit/services/tree/dtt.py +411 -0
  7. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/transfer_annotations.py +8 -0
  8. phykit-2.1.90/phykit/version.py +1 -0
  9. {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/PKG-INFO +1 -1
  10. {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/SOURCES.txt +1 -0
  11. {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/entry_points.txt +2 -0
  12. phykit-2.1.88/phykit/version.py +0 -1
  13. {phykit-2.1.88 → phykit-2.1.90}/LICENSE.md +0 -0
  14. {phykit-2.1.88 → phykit-2.1.90}/README.md +0 -0
  15. {phykit-2.1.88 → phykit-2.1.90}/phykit/__init__.py +0 -0
  16. {phykit-2.1.88 → phykit-2.1.90}/phykit/__main__.py +0 -0
  17. {phykit-2.1.88 → phykit-2.1.90}/phykit/errors.py +0 -0
  18. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/__init__.py +0 -0
  19. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/boolean_argument_parsing.py +0 -0
  20. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/caching.py +0 -0
  21. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/circular_layout.py +0 -0
  22. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/color_annotations.py +0 -0
  23. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/discrete_models.py +0 -0
  24. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/files.py +0 -0
  25. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/geological_timescale.py +0 -0
  26. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/json_output.py +0 -0
  27. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/parallel.py +0 -0
  28. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/parsimony_utils.py +0 -0
  29. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/pgls_utils.py +0 -0
  30. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/plot_config.py +0 -0
  31. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/quartet_utils.py +0 -0
  32. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/stats_summary.py +0 -0
  33. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/streaming.py +0 -0
  34. {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/trait_parsing.py +0 -0
  35. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/__init__.py +0 -0
  36. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/__init__.py +0 -0
  37. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_entropy.py +0 -0
  38. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_length.py +0 -0
  39. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  40. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  41. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_recoding.py +0 -0
  42. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_subsample.py +0 -0
  43. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/base.py +0 -0
  44. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/column_score.py +0 -0
  45. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/composition_per_taxon.py +0 -0
  46. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  47. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  48. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/dfoil.py +0 -0
  49. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/dna_threader.py +0 -0
  50. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/dstatistic.py +0 -0
  51. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  52. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/faidx.py +0 -0
  53. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/gc_content.py +0 -0
  54. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/identity_matrix.py +0 -0
  55. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/mask_alignment.py +0 -0
  56. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/occupancy_filter.py +0 -0
  57. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  58. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/pairwise_identity.py +0 -0
  59. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  60. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/phylo_gwas.py +0 -0
  61. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  62. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/rcv.py +0 -0
  63. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/rcvt.py +0 -0
  64. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  65. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  66. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/taxon_groups.py +0 -0
  67. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/variable_sites.py +0 -0
  68. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/base.py +0 -0
  69. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/ancestral_reconstruction.py +0 -0
  70. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/base.py +0 -0
  71. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/bipartition_support_stats.py +0 -0
  72. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/branch_length_multiplier.py +0 -0
  73. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/character_map.py +0 -0
  74. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/chronogram.py +0 -0
  75. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/collapse_branches.py +0 -0
  76. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/concordance_asr.py +0 -0
  77. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/consensus_network.py +0 -0
  78. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/consensus_tree.py +0 -0
  79. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/cont_map.py +0 -0
  80. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/cophylo.py +0 -0
  81. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  82. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/density_map.py +0 -0
  83. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/discordance_asymmetry.py +0 -0
  84. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/dvmc.py +0 -0
  85. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/evo_tempo_map.py +0 -0
  86. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/evolutionary_rate.py +0 -0
  87. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/fit_continuous.py +0 -0
  88. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/fit_discrete.py +0 -0
  89. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  90. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/hybridization.py +0 -0
  91. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/independent_contrasts.py +0 -0
  92. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/internal_branch_stats.py +0 -0
  93. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/internode_labeler.py +0 -0
  94. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/kf_distance.py +0 -0
  95. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  96. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/lb_score.py +0 -0
  97. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/ltt.py +0 -0
  98. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/monophyly_check.py +0 -0
  99. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  100. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/neighbor_net.py +0 -0
  101. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/network_signal.py +0 -0
  102. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/ou_shift_detection.py +0 -0
  103. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/ouwie.py +0 -0
  104. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/parsimony_score.py +0 -0
  105. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/patristic_distances.py +0 -0
  106. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phenogram.py +0 -0
  107. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylo_anova.py +0 -0
  108. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylo_heatmap.py +0 -0
  109. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylo_impute.py +0 -0
  110. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylo_logistic.py +0 -0
  111. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylo_path.py +0 -0
  112. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylogenetic_glm.py +0 -0
  113. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  114. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylogenetic_regression.py +0 -0
  115. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylogenetic_signal.py +0 -0
  116. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylomorphospace.py +0 -0
  117. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/polytomy_test.py +0 -0
  118. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/print_tree.py +0 -0
  119. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/prune_tree.py +0 -0
  120. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/quartet_network.py +0 -0
  121. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/quartet_pie.py +0 -0
  122. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/rate_heterogeneity.py +0 -0
  123. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/relative_rate_test.py +0 -0
  124. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/rename_tree_tips.py +0 -0
  125. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/rf_distance.py +0 -0
  126. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/root_tree.py +0 -0
  127. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/saturation.py +0 -0
  128. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/simmap_summary.py +0 -0
  129. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/spectral_discordance.py +0 -0
  130. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/spr.py +0 -0
  131. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/spurious_sequence.py +0 -0
  132. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/stochastic_character_map.py +0 -0
  133. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/terminal_branch_stats.py +0 -0
  134. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/threshold_model.py +0 -0
  135. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/tip_labels.py +0 -0
  136. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  137. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  138. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/total_tree_length.py +0 -0
  139. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/trait_correlation.py +0 -0
  140. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/trait_rate_map.py +0 -0
  141. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/tree_space.py +0 -0
  142. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/treeness.py +0 -0
  143. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/treeness_over_rcv.py +0 -0
  144. {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/vcv_utils.py +0 -0
  145. {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/dependency_links.txt +0 -0
  146. {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/requires.txt +0 -0
  147. {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/top_level.txt +0 -0
  148. {phykit-2.1.88 → phykit-2.1.90}/setup.cfg +0 -0
  149. {phykit-2.1.88 → phykit-2.1.90}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.88
3
+ Version: 2.1.90
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -21,6 +21,8 @@ ALIAS_TO_HANDLER: Dict[str, str] = {
21
21
  "recode": "alignment_recoding",
22
22
  "outlier_taxa": "alignment_outlier_taxa",
23
23
  "aot": "alignment_outlier_taxa",
24
+ "dtt": "dtt",
25
+ "disparity_through_time": "dtt",
24
26
  "dstat": "dstatistic",
25
27
  "abba_baba": "dstatistic",
26
28
  "dfoil": "dfoil",
@@ -194,6 +194,8 @@ class Phykit:
194
194
  incorporating gene tree discordance
195
195
  chronogram (alias: chrono; time_tree)
196
196
  - plot a time-calibrated tree with geological timescale
197
+ dtt (alias: disparity_through_time)
198
+ - disparity through time analysis (Harmon et al. 2003)
197
199
  bipartition_support_stats (alias: bss)
198
200
  - calculates summary statistics for bipartition support
199
201
  branch_length_multiplier (alias: blm)
@@ -2990,6 +2992,77 @@ class Phykit:
2990
2992
  _add_json_argument(parser)
2991
2993
  _run_service(parser, argv, Chronogram)
2992
2994
 
2995
+ @staticmethod
2996
+ def dtt(argv):
2997
+ parser = _new_parser(
2998
+ description=textwrap.dedent(
2999
+ f"""\
3000
+ {help_header}
3001
+
3002
+ Disparity through time (DTT) analysis. Computes how
3003
+ morphological disparity partitions among subclades
3004
+ through time (Harmon et al. 2003).
3005
+
3006
+ At each branching time, calculates the mean relative
3007
+ subclade disparity. Under Brownian motion, this declines
3008
+ linearly. The MDI (Morphological Disparity Index) is the
3009
+ area between the observed DTT and the BM null median.
3010
+
3011
+ Positive MDI = late disparity accumulation
3012
+ Negative MDI = early disparity accumulation (radiation)
3013
+
3014
+ Aliases:
3015
+ dtt, disparity_through_time
3016
+ Command line interfaces:
3017
+ pk_dtt, pk_disparity_through_time
3018
+
3019
+ Usage:
3020
+ phykit dtt -t <tree> --traits <traits_file>
3021
+ [--trait <column>] [--index avg_sq|avg_manhattan]
3022
+ [--nsim <int>] [--seed <int>]
3023
+ [--plot-output <file>] [--json]
3024
+
3025
+ Options
3026
+ =====================================================
3027
+ -t/--tree ultrametric tree file
3028
+ (required)
3029
+
3030
+ --traits TSV file with trait data
3031
+ (required)
3032
+
3033
+ --trait specific trait column name
3034
+ (default: all traits)
3035
+
3036
+ --index disparity index: avg_sq
3037
+ (average squared Euclidean
3038
+ distance, default) or
3039
+ avg_manhattan
3040
+
3041
+ --nsim number of BM simulations
3042
+ for null DTT envelope and
3043
+ MDI p-value (default: 0,
3044
+ no simulations)
3045
+
3046
+ --seed random seed for
3047
+ reproducibility
3048
+
3049
+ --plot-output output figure path
3050
+
3051
+ --json output results as JSON
3052
+ """
3053
+ ),
3054
+ )
3055
+ parser.add_argument("-t", "--tree", type=str, required=True, help=SUPPRESS, metavar="")
3056
+ parser.add_argument("--traits", type=str, required=True, help=SUPPRESS, metavar="")
3057
+ parser.add_argument("--trait", type=str, default=None, help=SUPPRESS, metavar="")
3058
+ parser.add_argument("--index", type=str, default="avg_sq", choices=["avg_sq", "avg_manhattan"], help=SUPPRESS, metavar="")
3059
+ parser.add_argument("--nsim", type=int, default=0, help=SUPPRESS, metavar="")
3060
+ parser.add_argument("--seed", type=int, default=None, help=SUPPRESS, metavar="")
3061
+ parser.add_argument("--plot-output", type=str, default=None, help=SUPPRESS, metavar="")
3062
+ add_plot_arguments(parser)
3063
+ _add_json_argument(parser)
3064
+ _run_service(parser, argv, Dtt)
3065
+
2993
3066
  @staticmethod
2994
3067
  def bipartition_support_stats(argv):
2995
3068
  parser = _new_parser(
@@ -9141,6 +9214,10 @@ def chronogram(argv=None):
9141
9214
  Phykit.chronogram(sys.argv[1:])
9142
9215
 
9143
9216
 
9217
+ def dtt(argv=None):
9218
+ Phykit.dtt(sys.argv[1:])
9219
+
9220
+
9144
9221
  def bipartition_support_stats(argv=None):
9145
9222
  Phykit.bipartition_support_stats(sys.argv[1:])
9146
9223
 
@@ -29,6 +29,7 @@ CompositionalBiasPerSite = _LazyServiceFactory("phykit.services.alignment.compos
29
29
  CompositionPerTaxon = _LazyServiceFactory("phykit.services.alignment.composition_per_taxon", "CompositionPerTaxon")
30
30
  CreateConcatenationMatrix = _LazyServiceFactory("phykit.services.alignment.create_concatenation_matrix", "CreateConcatenationMatrix")
31
31
  DNAThreader = _LazyServiceFactory("phykit.services.alignment.dna_threader", "DNAThreader")
32
+ Dtt = _LazyServiceFactory("phykit.services.tree.dtt", "Dtt")
32
33
  Dstatistic = _LazyServiceFactory("phykit.services.alignment.dstatistic", "Dstatistic")
33
34
  Dfoil = _LazyServiceFactory("phykit.services.alignment.dfoil", "Dfoil")
34
35
  EvolutionaryRatePerSite = _LazyServiceFactory("phykit.services.alignment.evolutionary_rate_per_site", "EvolutionaryRatePerSite")
@@ -11,6 +11,7 @@ _EXPORTS = {
11
11
  "NeighborNet": "neighbor_net",
12
12
  "ConsensusTree": "consensus_tree",
13
13
  "DVMC": "dvmc",
14
+ "Dtt": "dtt",
14
15
  "DiscordanceAsymmetry": "discordance_asymmetry",
15
16
  "EvolutionaryRate": "evolutionary_rate",
16
17
  "EvoTempoMap": "evo_tempo_map",
@@ -0,0 +1,411 @@
1
+ """
2
+ Disparity through time (DTT).
3
+
4
+ Calculates and plots disparity-through-time for a phylogenetic tree
5
+ and phenotypic data, following Harmon et al. (2003). Computes the
6
+ Morphological Disparity Index (MDI) comparing observed DTT to a
7
+ Brownian motion null expectation.
8
+ """
9
+ import sys
10
+ from typing import Dict, List, Tuple
11
+
12
+ import numpy as np
13
+
14
+ from .base import Tree
15
+ from ...helpers.json_output import print_json
16
+ from ...helpers.plot_config import PlotConfig
17
+ from ...helpers.trait_parsing import parse_multi_trait_file
18
+ from ...errors import PhykitUserError
19
+
20
+
21
+ class Dtt(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.trait_data_path = parsed["trait_data_path"]
26
+ self.trait_column = parsed["trait_column"]
27
+ self.index = parsed["index"]
28
+ self.nsim = parsed["nsim"]
29
+ self.seed = parsed["seed"]
30
+ self.plot_output = parsed["plot_output"]
31
+ self.json_output = parsed["json_output"]
32
+ self.plot_config = parsed["plot_config"]
33
+
34
+ def process_args(self, args) -> Dict:
35
+ return dict(
36
+ tree_file_path=args.tree,
37
+ trait_data_path=args.traits,
38
+ trait_column=getattr(args, "trait", None),
39
+ index=getattr(args, "index", "avg_sq"),
40
+ nsim=getattr(args, "nsim", 0),
41
+ seed=getattr(args, "seed", None),
42
+ plot_output=getattr(args, "plot_output", None),
43
+ json_output=getattr(args, "json", False),
44
+ plot_config=PlotConfig.from_args(args),
45
+ )
46
+
47
+ def run(self) -> None:
48
+ tree = self.read_tree_file()
49
+ self.validate_tree(
50
+ tree, min_tips=3, require_branch_lengths=True,
51
+ context="disparity through time",
52
+ )
53
+
54
+ tree_tips = self.get_tip_names_from_tree(tree)
55
+ trait_names, traits = parse_multi_trait_file(
56
+ self.trait_data_path, tree_tips
57
+ )
58
+
59
+ # Get trait data
60
+ ordered_names = sorted(traits.keys())
61
+ n = len(ordered_names)
62
+
63
+ if self.trait_column:
64
+ if self.trait_column not in trait_names:
65
+ raise PhykitUserError(
66
+ [
67
+ f"Trait column '{self.trait_column}' not found.",
68
+ f"Available: {', '.join(trait_names)}",
69
+ ],
70
+ code=2,
71
+ )
72
+ col_idx = trait_names.index(self.trait_column)
73
+ data = np.array([traits[name][col_idx] for name in ordered_names])
74
+ data = data.reshape(-1, 1)
75
+ else:
76
+ p = len(trait_names)
77
+ data = np.array([
78
+ [traits[name][j] for j in range(p)]
79
+ for name in ordered_names
80
+ ])
81
+
82
+ # Prune tree to trait taxa
83
+ import pickle
84
+ tree_copy = pickle.loads(pickle.dumps(tree, protocol=pickle.HIGHEST_PROTOCOL))
85
+ tips_in_tree = [t.name for t in tree_copy.get_terminals()]
86
+ tips_to_prune = [t for t in tips_in_tree if t not in traits]
87
+ if tips_to_prune:
88
+ tree_copy = self.prune_tree_using_taxa_list(tree_copy, tips_to_prune)
89
+
90
+ # Compute DTT
91
+ times, dtt_values = self._compute_dtt(tree_copy, data, ordered_names)
92
+
93
+ # Compute MDI and null envelope via simulation
94
+ mdi = None
95
+ mdi_p = None
96
+ sim_dtt = None
97
+ if self.nsim > 0:
98
+ sim_dtt, mdi, mdi_p = self._simulate_null(
99
+ tree_copy, data, ordered_names, times
100
+ )
101
+
102
+ # Output
103
+ if self.plot_output:
104
+ self._plot_dtt(times, dtt_values, sim_dtt, mdi)
105
+
106
+ if self.json_output:
107
+ self._print_json(times, dtt_values, mdi, mdi_p, sim_dtt)
108
+ else:
109
+ self._print_text(times, dtt_values, mdi, mdi_p)
110
+
111
+ def _compute_disparity(self, data: np.ndarray) -> float:
112
+ """Compute disparity (average squared Euclidean distance among all pairs)."""
113
+ n = data.shape[0]
114
+ if n < 2:
115
+ return 0.0
116
+
117
+ if self.index == "avg_manhattan":
118
+ total = 0.0
119
+ count = 0
120
+ for i in range(n):
121
+ for j in range(i + 1, n):
122
+ total += np.sum(np.abs(data[i] - data[j]))
123
+ count += 1
124
+ return total / count if count > 0 else 0.0
125
+ else:
126
+ # avg_sq: average squared Euclidean distance
127
+ total = 0.0
128
+ count = 0
129
+ for i in range(n):
130
+ for j in range(i + 1, n):
131
+ total += np.sum((data[i] - data[j]) ** 2)
132
+ count += 1
133
+ return total / count if count > 0 else 0.0
134
+
135
+ def _compute_dtt(
136
+ self, tree, data: np.ndarray, ordered_names: List[str]
137
+ ) -> Tuple[List[float], List[float]]:
138
+ """Compute DTT curve following Harmon et al. (2003).
139
+
140
+ At each branching time, compute the mean relative disparity of
141
+ subclades with >= 2 species. Times are relative (0 = root, 1 = tips).
142
+ """
143
+ root = tree.root
144
+ tip_heights = [tree.distance(root, t) for t in tree.get_terminals()]
145
+ tree_height = max(tip_heights)
146
+ if tree_height == 0:
147
+ return [0.0, 1.0], [1.0, 0.0]
148
+
149
+ total_disp = self._compute_disparity(data)
150
+ if total_disp == 0:
151
+ return [0.0, 1.0], [1.0, 0.0]
152
+
153
+ name_to_idx = {name: i for i, name in enumerate(ordered_names)}
154
+
155
+ # Precompute disparity for every clade (internal node)
156
+ clade_disp = {}
157
+ for clade in tree.find_clades(order="preorder"):
158
+ tips = [t.name for t in clade.get_terminals()
159
+ if t.name in name_to_idx]
160
+ if len(tips) >= 2:
161
+ indices = [name_to_idx[n] for n in tips]
162
+ clade_disp[id(clade)] = self._compute_disparity(data[indices])
163
+
164
+ # Precompute node times (distance from root, relative)
165
+ node_time = {}
166
+ parent_map = {}
167
+ for clade in tree.find_clades(order="preorder"):
168
+ node_time[id(clade)] = tree.distance(root, clade) / tree_height
169
+ for child in clade.clades:
170
+ parent_map[id(child)] = clade
171
+
172
+ # Branching times (all internal nodes including root, sorted)
173
+ internal_nodes = [
174
+ c for c in tree.find_clades(order="preorder")
175
+ if not c.is_terminal()
176
+ ]
177
+ internal_nodes.sort(key=lambda c: node_time[id(c)])
178
+
179
+ times = [0.0]
180
+ dtt_values = [1.0]
181
+
182
+ for node in internal_nodes:
183
+ t = node_time[id(node)]
184
+
185
+ # At time t (just after this node branches), find all
186
+ # lineages: branches where parent_time <= t < child_tip_time.
187
+ # Each lineage corresponds to the clade below the child end
188
+ # of that branch.
189
+ lineage_disparities = []
190
+
191
+ for clade in tree.find_clades(order="preorder"):
192
+ if clade == root:
193
+ continue
194
+ parent = parent_map.get(id(clade))
195
+ if parent is None:
196
+ continue
197
+
198
+ parent_t = node_time[id(parent)]
199
+ # For a terminal, its "node time" = tree height = 1.0
200
+ if clade.is_terminal():
201
+ child_t = 1.0
202
+ else:
203
+ child_t = node_time[id(clade)]
204
+
205
+ # This branch spans time t if parent_t <= t < child_t
206
+ if parent_t <= t + 1e-10 and child_t > t + 1e-10:
207
+ # The subclade below this branch
208
+ tips = [tip.name for tip in clade.get_terminals()
209
+ if tip.name in name_to_idx]
210
+ if len(tips) >= 2:
211
+ lineage_disparities.append(
212
+ clade_disp.get(id(clade), 0.0)
213
+ )
214
+
215
+ if lineage_disparities:
216
+ rel_disp = float(np.mean(lineage_disparities)) / total_disp
217
+ else:
218
+ rel_disp = 0.0
219
+
220
+ times.append(t)
221
+ dtt_values.append(rel_disp)
222
+
223
+ # Terminal point (only if last entry isn't already at/near 0)
224
+ if dtt_values and dtt_values[-1] > 1e-10:
225
+ times.append(1.0)
226
+ dtt_values.append(0.0)
227
+
228
+ return times, dtt_values
229
+
230
+ def _compute_mdi(
231
+ self, times: List[float], dtt_values: List[float],
232
+ null_median: np.ndarray,
233
+ ) -> float:
234
+ """Compute MDI: area between observed DTT and null median."""
235
+ times_arr = np.array(times)
236
+ dtt_arr = np.array(dtt_values)
237
+
238
+ # Interpolate null median to match observed times
239
+ null_times = np.linspace(0, 1, len(null_median))
240
+ null_interp = np.interp(times_arr, null_times, null_median)
241
+
242
+ # MDI = area between observed and null (trapezoidal integration)
243
+ diff = dtt_arr - null_interp
244
+ mdi = float(np.trapezoid(diff, times_arr))
245
+ return mdi
246
+
247
+ def _simulate_null(
248
+ self, tree, data, ordered_names, obs_times,
249
+ ) -> Tuple[np.ndarray, float, float]:
250
+ """Simulate BM data and compute null DTT distribution."""
251
+ from .vcv_utils import build_vcv_matrix
252
+
253
+ rng = np.random.default_rng(self.seed)
254
+ n = len(ordered_names)
255
+ p = data.shape[1] if data.ndim > 1 else 1
256
+
257
+ vcv = build_vcv_matrix(tree, ordered_names)
258
+
259
+ # Cholesky for simulation
260
+ try:
261
+ L = np.linalg.cholesky(vcv)
262
+ except np.linalg.LinAlgError:
263
+ # Add small diagonal for PD
264
+ vcv += np.eye(n) * 1e-8
265
+ L = np.linalg.cholesky(vcv)
266
+
267
+ # Compute total disparity and mean
268
+ total_disp = self._compute_disparity(data)
269
+
270
+ # Simulate nsim datasets
271
+ n_time_points = len(obs_times)
272
+ sim_dtt_matrix = np.zeros((self.nsim, n_time_points))
273
+
274
+ for s in range(self.nsim):
275
+ # Simulate BM data with same VCV structure
276
+ if p == 1:
277
+ z = rng.standard_normal(n)
278
+ sim_data = (L @ z).reshape(-1, 1)
279
+ else:
280
+ Z = rng.standard_normal((n, p))
281
+ sim_data = L @ Z
282
+
283
+ sim_times, sim_values = self._compute_dtt(
284
+ tree, sim_data, ordered_names
285
+ )
286
+
287
+ # Interpolate to observed time points
288
+ sim_interp = np.interp(obs_times, sim_times, sim_values)
289
+ sim_dtt_matrix[s, :] = sim_interp
290
+
291
+ # Null median
292
+ null_median = np.median(sim_dtt_matrix, axis=0)
293
+
294
+ # MDI
295
+ obs_arr = np.array(
296
+ self._compute_dtt(tree, data, ordered_names)[1]
297
+ )
298
+ # Interpolate observed to consistent times
299
+ obs_times_arr = np.array(obs_times)
300
+ mdi = float(np.trapezoid(
301
+ np.interp(obs_times_arr, obs_times, obs_arr) - null_median,
302
+ obs_times_arr,
303
+ ))
304
+
305
+ # MDI p-value: proportion of simulated MDIs >= observed
306
+ sim_mdis = np.zeros(self.nsim)
307
+ for s in range(self.nsim):
308
+ sim_mdi = float(np.trapezoid(
309
+ sim_dtt_matrix[s, :] - null_median, obs_times_arr
310
+ ))
311
+ sim_mdis[s] = sim_mdi
312
+
313
+ mdi_p = float(np.mean(np.abs(sim_mdis) >= np.abs(mdi)))
314
+
315
+ return sim_dtt_matrix, mdi, mdi_p
316
+
317
+ def _plot_dtt(self, times, dtt_values, sim_dtt, mdi):
318
+ try:
319
+ import matplotlib
320
+ matplotlib.use("Agg")
321
+ import matplotlib.pyplot as plt
322
+ except ImportError:
323
+ print("matplotlib is required for DTT plotting.")
324
+ return
325
+
326
+ config = self.plot_config
327
+ config.resolve(n_rows=10, n_cols=None)
328
+ fig, ax = plt.subplots(figsize=(config.fig_width, config.fig_height))
329
+
330
+ # Null envelope
331
+ if sim_dtt is not None and sim_dtt.shape[0] > 0:
332
+ null_median = np.median(sim_dtt, axis=0)
333
+ null_lo = np.percentile(sim_dtt, 2.5, axis=0)
334
+ null_hi = np.percentile(sim_dtt, 97.5, axis=0)
335
+
336
+ ax.fill_between(
337
+ times, null_lo, null_hi,
338
+ color="#cccccc", alpha=0.4, label="95% BM null envelope",
339
+ zorder=1,
340
+ )
341
+ ax.plot(
342
+ times, null_median,
343
+ color="#888888", lw=1, linestyle="--",
344
+ label="BM null median", zorder=2,
345
+ )
346
+
347
+ # Observed DTT
348
+ ax.plot(
349
+ times, dtt_values,
350
+ color="#2b8cbe", lw=2, solid_capstyle="round",
351
+ label="Observed", zorder=3,
352
+ )
353
+
354
+ # BM expectation line (linear decline from 1 to 0)
355
+ ax.plot(
356
+ [0, 1], [1, 0],
357
+ color="#d62728", lw=1, linestyle=":",
358
+ alpha=0.5, label="BM expectation", zorder=1,
359
+ )
360
+
361
+ ax.set_xlabel("Relative time", fontsize=10)
362
+ ax.set_ylabel("Relative disparity", fontsize=10)
363
+ ax.set_xlim(0, 1)
364
+ ax.set_ylim(bottom=0)
365
+ ax.legend(fontsize=8, loc="upper right")
366
+
367
+ ax.spines["top"].set_visible(False)
368
+ ax.spines["right"].set_visible(False)
369
+
370
+ title = config.title or "Disparity Through Time"
371
+ if mdi is not None:
372
+ title += f" (MDI = {mdi:.4f})"
373
+ if config.show_title:
374
+ ax.set_title(title, fontsize=config.title_fontsize)
375
+
376
+ fig.tight_layout()
377
+ fig.savefig(self.plot_output, dpi=config.dpi, bbox_inches="tight")
378
+ plt.close(fig)
379
+ print(f"DTT plot saved: {self.plot_output}")
380
+
381
+ def _print_text(self, times, dtt_values, mdi, mdi_p):
382
+ try:
383
+ print("Disparity Through Time (DTT)")
384
+ print(f"Index: {self.index}")
385
+ print(f"N time points: {len(times)}")
386
+ if mdi is not None:
387
+ print(f"MDI: {mdi:.6f}")
388
+ if mdi_p is not None:
389
+ print(f"MDI p-value: {mdi_p:.4f}")
390
+ print()
391
+ print(f"{'Time':>10s} {'Relative disparity':>20s}")
392
+ print("-" * 32)
393
+ for t, d in zip(times, dtt_values):
394
+ print(f"{t:>10.6f} {d:>20.6f}")
395
+ except BrokenPipeError:
396
+ return
397
+
398
+ def _print_json(self, times, dtt_values, mdi, mdi_p, sim_dtt):
399
+ payload = {
400
+ "index": self.index,
401
+ "n_time_points": len(times),
402
+ "times": [round(t, 6) for t in times],
403
+ "dtt": [round(d, 6) for d in dtt_values],
404
+ }
405
+ if mdi is not None:
406
+ payload["mdi"] = round(mdi, 6)
407
+ if mdi_p is not None:
408
+ payload["mdi_p_value"] = round(mdi_p, 4)
409
+ if self.plot_output:
410
+ payload["plot_output"] = self.plot_output
411
+ print_json(payload, sort_keys=False)
@@ -114,6 +114,12 @@ class TransferAnnotations(Tree):
114
114
  annotation = clade.comment or clade.name or ""
115
115
  if not annotation:
116
116
  continue
117
+ # wASTRAL --support 3 wraps annotations as '[key=val;...]'
118
+ # which BioPython parses as a quoted node name including the
119
+ # brackets. Strip them so BioPython's comment writer can
120
+ # re-add proper brackets on output.
121
+ if annotation.startswith("[") and annotation.endswith("]"):
122
+ annotation = annotation[1:-1]
117
123
  bp = self._get_bipartition(clade, all_taxa)
118
124
  if bp:
119
125
  annotations[bp] = annotation
@@ -149,6 +155,8 @@ class TransferAnnotations(Tree):
149
155
  content = f.read()
150
156
  # Remove the & prefix that BioPython adds
151
157
  content = content.replace("[&", "[")
158
+ # BioPython may also backslash-escape brackets inside comments
159
+ content = content.replace("\\[", "[").replace("\\]", "]")
152
160
  with open(output_path, "w") as f:
153
161
  f.write(content)
154
162
 
@@ -0,0 +1 @@
1
+ __version__ = "2.1.90"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.88
3
+ Version: 2.1.90
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -81,6 +81,7 @@ phykit/services/tree/cophylo.py
81
81
  phykit/services/tree/covarying_evolutionary_rates.py
82
82
  phykit/services/tree/density_map.py
83
83
  phykit/services/tree/discordance_asymmetry.py
84
+ phykit/services/tree/dtt.py
84
85
  phykit/services/tree/dvmc.py
85
86
  phykit/services/tree/evo_tempo_map.py
86
87
  phykit/services/tree/evolutionary_rate.py
@@ -46,8 +46,10 @@ pk_dfoil = phykit.phykit:dfoil
46
46
  pk_dfoil_test = phykit.phykit:dfoil
47
47
  pk_dimreduce = phykit.phykit:phylogenetic_ordination
48
48
  pk_disc_asym = phykit.phykit:discordance_asymmetry
49
+ pk_disparity_through_time = phykit.phykit:dtt
49
50
  pk_dmap = phykit.phykit:density_map
50
51
  pk_dstat = phykit.phykit:dstatistic
52
+ pk_dtt = phykit.phykit:dtt
51
53
  pk_entropy = phykit.phykit:alignment_entropy
52
54
  pk_erps = phykit.phykit:evolutionary_rate_per_site
53
55
  pk_etm = phykit.phykit:evo_tempo_map
@@ -1 +0,0 @@
1
- __version__ = "2.1.88"
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