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.
- {phykit-2.1.88 → phykit-2.1.90}/PKG-INFO +1 -1
- {phykit-2.1.88 → phykit-2.1.90}/phykit/cli_registry.py +2 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/phykit.py +77 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/service_factories.py +1 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/__init__.py +1 -0
- phykit-2.1.90/phykit/services/tree/dtt.py +411 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/transfer_annotations.py +8 -0
- phykit-2.1.90/phykit/version.py +1 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/PKG-INFO +1 -1
- {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/SOURCES.txt +1 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/entry_points.txt +2 -0
- phykit-2.1.88/phykit/version.py +0 -1
- {phykit-2.1.88 → phykit-2.1.90}/LICENSE.md +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/README.md +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/__init__.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/__main__.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/errors.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/__init__.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/boolean_argument_parsing.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/caching.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/circular_layout.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/color_annotations.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/discrete_models.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/files.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/geological_timescale.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/json_output.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/parallel.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/parsimony_utils.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/pgls_utils.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/plot_config.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/quartet_utils.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/stats_summary.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/streaming.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/helpers/trait_parsing.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/__init__.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/__init__.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_entropy.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_length.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_recoding.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/alignment_subsample.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/base.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/column_score.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/composition_per_taxon.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/dfoil.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/dna_threader.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/dstatistic.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/faidx.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/gc_content.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/identity_matrix.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/mask_alignment.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/occupancy_filter.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/pairwise_identity.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/phylo_gwas.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/plot_alignment_qc.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/rcv.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/rcvt.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/rename_fasta_entries.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/taxon_groups.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/alignment/variable_sites.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/base.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/ancestral_reconstruction.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/base.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/bipartition_support_stats.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/branch_length_multiplier.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/character_map.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/chronogram.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/collapse_branches.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/concordance_asr.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/consensus_network.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/consensus_tree.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/cont_map.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/cophylo.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/density_map.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/discordance_asymmetry.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/dvmc.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/evo_tempo_map.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/evolutionary_rate.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/fit_continuous.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/fit_discrete.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/hidden_paralogy_check.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/hybridization.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/independent_contrasts.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/internal_branch_stats.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/internode_labeler.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/kf_distance.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/lb_score.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/ltt.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/monophyly_check.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/neighbor_net.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/network_signal.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/ou_shift_detection.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/ouwie.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/parsimony_score.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/patristic_distances.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phenogram.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylo_anova.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylo_heatmap.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylo_impute.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylo_logistic.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylo_path.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylogenetic_glm.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylogenetic_ordination.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylogenetic_regression.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylogenetic_signal.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/phylomorphospace.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/polytomy_test.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/print_tree.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/prune_tree.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/quartet_network.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/quartet_pie.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/rate_heterogeneity.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/relative_rate_test.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/rename_tree_tips.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/rf_distance.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/root_tree.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/saturation.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/simmap_summary.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/spectral_discordance.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/spr.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/spurious_sequence.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/stochastic_character_map.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/terminal_branch_stats.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/threshold_model.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/tip_labels.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/tip_to_tip_distance.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/total_tree_length.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/trait_correlation.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/trait_rate_map.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/tree_space.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/treeness.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/treeness_over_rcv.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit/services/tree/vcv_utils.py +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/dependency_links.txt +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/requires.txt +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/phykit.egg-info/top_level.txt +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/setup.cfg +0 -0
- {phykit-2.1.88 → phykit-2.1.90}/setup.py +0 -0
|
@@ -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")
|
|
@@ -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"
|
|
@@ -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
|
phykit-2.1.88/phykit/version.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|