phykit 2.1.41__tar.gz → 2.1.43__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.41 → phykit-2.1.43}/PKG-INFO +1 -1
- {phykit-2.1.41 → phykit-2.1.43}/phykit/cli_registry.py +2 -0
- phykit-2.1.43/phykit/helpers/quartet_utils.py +136 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/phykit.py +108 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/service_factories.py +1 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/__init__.py +1 -0
- phykit-2.1.43/phykit/services/tree/quartet_pie.py +305 -0
- phykit-2.1.43/phykit/version.py +1 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit.egg-info/PKG-INFO +1 -1
- {phykit-2.1.41 → phykit-2.1.43}/phykit.egg-info/SOURCES.txt +2 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit.egg-info/entry_points.txt +3 -0
- {phykit-2.1.41 → phykit-2.1.43}/setup.py +3 -0
- phykit-2.1.41/phykit/version.py +0 -1
- {phykit-2.1.41 → phykit-2.1.43}/LICENSE.md +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/README.md +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/__init__.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/__main__.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/errors.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/helpers/__init__.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/helpers/boolean_argument_parsing.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/helpers/caching.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/helpers/discrete_models.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/helpers/files.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/helpers/json_output.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/helpers/parallel.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/helpers/plot_config.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/helpers/stats_summary.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/helpers/streaming.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/__init__.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/__init__.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/alignment_entropy.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/alignment_length.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/alignment_recoding.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/base.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/column_score.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/composition_per_taxon.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/dna_threader.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/faidx.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/gc_content.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/mask_alignment.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/pairwise_identity.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/plot_alignment_qc.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/rcv.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/rcvt.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/rename_fasta_entries.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/alignment/variable_sites.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/base.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/ancestral_reconstruction.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/base.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/bipartition_support_stats.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/branch_length_multiplier.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/collapse_branches.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/concordance_asr.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/consensus_network.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/consensus_tree.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/cont_map.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/cophylo.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/density_map.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/discordance_asymmetry.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/dvmc.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/evo_tempo_map.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/evolutionary_rate.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/fit_continuous.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/fit_discrete.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/hidden_paralogy_check.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/internal_branch_stats.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/internode_labeler.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/kf_distance.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/lb_score.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/ltt.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/monophyly_check.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/network_signal.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/ou_shift_detection.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/ouwie.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/patristic_distances.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/phenogram.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/phylogenetic_glm.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/phylogenetic_ordination.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/phylogenetic_regression.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/phylogenetic_signal.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/phylomorphospace.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/polytomy_test.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/print_tree.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/prune_tree.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/quartet_network.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/rate_heterogeneity.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/relative_rate_test.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/rename_tree_tips.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/rf_distance.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/root_tree.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/saturation.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/spectral_discordance.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/spurious_sequence.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/stochastic_character_map.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/terminal_branch_stats.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/threshold_model.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/tip_labels.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/tip_to_tip_distance.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/total_tree_length.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/treeness.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/treeness_over_rcv.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit/services/tree/vcv_utils.py +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit.egg-info/dependency_links.txt +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit.egg-info/requires.txt +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/phykit.egg-info/top_level.txt +0 -0
- {phykit-2.1.41 → phykit-2.1.43}/setup.cfg +0 -0
|
@@ -116,6 +116,8 @@ ALIAS_TO_HANDLER: Dict[str, str] = {
|
|
|
116
116
|
"quartet_net": "quartet_network",
|
|
117
117
|
"qnet": "quartet_network",
|
|
118
118
|
"nanuq": "quartet_network",
|
|
119
|
+
"qpie": "quartet_pie",
|
|
120
|
+
"quartet_pie_chart": "quartet_pie",
|
|
119
121
|
"ltt": "ltt",
|
|
120
122
|
"gamma_stat": "ltt",
|
|
121
123
|
"gamma": "ltt",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utilities for quartet concordance factor computation and ASTRAL parsing.
|
|
3
|
+
|
|
4
|
+
Provides gene concordance factor (gCF/gDF1/gDF2) computation via the
|
|
5
|
+
four-group bipartition decomposition, and parsing of ASTRAL -t 2
|
|
6
|
+
q1/q2/q3 annotations from Newick node labels.
|
|
7
|
+
"""
|
|
8
|
+
from typing import Dict, List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from ..errors import PhykitUserError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def canonical_split(tips: frozenset, all_taxa: frozenset) -> frozenset:
|
|
14
|
+
"""Normalize a bipartition to a canonical frozenset-of-frozensets."""
|
|
15
|
+
complement = all_taxa - tips
|
|
16
|
+
return frozenset([tips, complement])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compute_gcf_per_node(
|
|
20
|
+
species_tree, gene_trees: list
|
|
21
|
+
) -> Dict[int, Tuple[float, float, float, int, int, int]]:
|
|
22
|
+
"""Compute (gCF, gDF1, gDF2, concordant, disc1, disc2) per internal node.
|
|
23
|
+
|
|
24
|
+
Uses the four-group decomposition (C1, C2, S, D) around each internal
|
|
25
|
+
branch to identify the concordant bipartition and the two NNI alternatives.
|
|
26
|
+
|
|
27
|
+
Returns dict mapping clade id -> (gCF, gDF1, gDF2, n_conc, n_d1, n_d2).
|
|
28
|
+
"""
|
|
29
|
+
all_taxa = frozenset(t.name for t in species_tree.get_terminals())
|
|
30
|
+
|
|
31
|
+
# Build parent map
|
|
32
|
+
parent_map = {}
|
|
33
|
+
for clade in species_tree.find_clades(order="preorder"):
|
|
34
|
+
for child in clade.clades:
|
|
35
|
+
parent_map[id(child)] = clade
|
|
36
|
+
|
|
37
|
+
# Extract bipartitions from all gene trees
|
|
38
|
+
gt_splits = []
|
|
39
|
+
for gt in gene_trees:
|
|
40
|
+
gt_taxa = frozenset(t.name for t in gt.get_terminals())
|
|
41
|
+
shared = gt_taxa & all_taxa
|
|
42
|
+
if len(shared) < 4:
|
|
43
|
+
gt_splits.append(set())
|
|
44
|
+
continue
|
|
45
|
+
splits = set()
|
|
46
|
+
for clade in gt.get_nonterminals():
|
|
47
|
+
tips = frozenset(t.name for t in clade.get_terminals()) & shared
|
|
48
|
+
if len(tips) <= 1 or tips == shared:
|
|
49
|
+
continue
|
|
50
|
+
splits.add(canonical_split(tips, shared))
|
|
51
|
+
gt_splits.append(splits)
|
|
52
|
+
|
|
53
|
+
result = {}
|
|
54
|
+
for clade in species_tree.find_clades(order="preorder"):
|
|
55
|
+
if clade.is_terminal() or clade == species_tree.root:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
children = clade.clades
|
|
59
|
+
if len(children) < 2:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
C1 = frozenset(t.name for t in children[0].get_terminals())
|
|
63
|
+
C2 = frozenset(t.name for t in children[1].get_terminals())
|
|
64
|
+
# Handle polytomies: merge extra children into C2
|
|
65
|
+
for c in children[2:]:
|
|
66
|
+
C2 = C2 | frozenset(t.name for t in c.get_terminals())
|
|
67
|
+
|
|
68
|
+
remaining = all_taxa - C1 - C2
|
|
69
|
+
if not remaining:
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
concordant_bp = canonical_split(C1 | C2, all_taxa)
|
|
73
|
+
nni1_bp = canonical_split(remaining | C2, all_taxa)
|
|
74
|
+
nni2_bp = canonical_split(C1 | remaining, all_taxa)
|
|
75
|
+
|
|
76
|
+
conc = sum(1 for s in gt_splits if concordant_bp in s)
|
|
77
|
+
d1 = sum(1 for s in gt_splits if nni1_bp in s)
|
|
78
|
+
d2 = sum(1 for s in gt_splits if nni2_bp in s)
|
|
79
|
+
|
|
80
|
+
total = conc + d1 + d2
|
|
81
|
+
if total > 0:
|
|
82
|
+
gcf = conc / total
|
|
83
|
+
gdf1 = d1 / total
|
|
84
|
+
gdf2 = d2 / total
|
|
85
|
+
else:
|
|
86
|
+
gcf, gdf1, gdf2 = 1.0, 0.0, 0.0
|
|
87
|
+
|
|
88
|
+
result[id(clade)] = (gcf, gdf1, gdf2, conc, d1, d2)
|
|
89
|
+
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_astral_annotations(
|
|
94
|
+
tree,
|
|
95
|
+
) -> Dict[int, Tuple[float, float, float]]:
|
|
96
|
+
"""Parse q1/q2/q3 annotations from ASTRAL -t 2 Newick node labels.
|
|
97
|
+
|
|
98
|
+
ASTRAL annotates internal nodes with formats like:
|
|
99
|
+
'[q1=0.5;q2=0.3;q3=0.2;f1=50;...]'
|
|
100
|
+
'q1=0.5;q2=0.3;q3=0.2'
|
|
101
|
+
|
|
102
|
+
Returns dict mapping clade id -> (q1, q2, q3).
|
|
103
|
+
Only nodes with valid q1/q2/q3 are included.
|
|
104
|
+
"""
|
|
105
|
+
result = {}
|
|
106
|
+
for clade in tree.find_clades(order="preorder"):
|
|
107
|
+
if clade.is_terminal():
|
|
108
|
+
continue
|
|
109
|
+
label = clade.name or clade.comment or ""
|
|
110
|
+
qs = _parse_qs_from_label(str(label))
|
|
111
|
+
if qs is not None:
|
|
112
|
+
result[id(clade)] = qs
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _parse_qs_from_label(label: str) -> Optional[Tuple[float, float, float]]:
|
|
117
|
+
"""Extract q1, q2, q3 from an ASTRAL node label string."""
|
|
118
|
+
s = label.strip("'\"").strip("[]")
|
|
119
|
+
q1 = q2 = q3 = None
|
|
120
|
+
for part in s.split(";"):
|
|
121
|
+
if "=" not in part:
|
|
122
|
+
continue
|
|
123
|
+
key, val = part.split("=", 1)
|
|
124
|
+
key = key.strip()
|
|
125
|
+
try:
|
|
126
|
+
if key == "q1":
|
|
127
|
+
q1 = float(val)
|
|
128
|
+
elif key == "q2":
|
|
129
|
+
q2 = float(val)
|
|
130
|
+
elif key == "q3":
|
|
131
|
+
q3 = float(val)
|
|
132
|
+
except ValueError:
|
|
133
|
+
continue
|
|
134
|
+
if q1 is not None and q2 is not None and q3 is not None:
|
|
135
|
+
return (q1, q2, q3)
|
|
136
|
+
return None
|
|
@@ -242,6 +242,9 @@ class Phykit:
|
|
|
242
242
|
quartet_network (alias: quartet_net; qnet; nanuq)
|
|
243
243
|
- quartet-based network inference (NANUQ-style)
|
|
244
244
|
distinguishing ILS from hybridization
|
|
245
|
+
quartet_pie (alias: qpie; quartet_pie_chart)
|
|
246
|
+
- phylogram with quartet concordance pie charts
|
|
247
|
+
at internal nodes
|
|
245
248
|
ltt (alias: gamma_stat; gamma)
|
|
246
249
|
- lineage-through-time plot and Pybus & Harvey
|
|
247
250
|
gamma statistic for diversification rate testing
|
|
@@ -4524,6 +4527,107 @@ class Phykit:
|
|
|
4524
4527
|
_add_json_argument(parser)
|
|
4525
4528
|
_run_service(parser, argv, ConsensusNetwork)
|
|
4526
4529
|
|
|
4530
|
+
@staticmethod
|
|
4531
|
+
def quartet_pie(argv):
|
|
4532
|
+
parser = _new_parser(
|
|
4533
|
+
description=textwrap.dedent(
|
|
4534
|
+
f"""\
|
|
4535
|
+
{help_header}
|
|
4536
|
+
|
|
4537
|
+
Draw a phylogram with pie charts at internal nodes showing
|
|
4538
|
+
quartet concordance proportions.
|
|
4539
|
+
|
|
4540
|
+
In native mode (-g provided), computes gene concordance
|
|
4541
|
+
factors (gCF, gDF1, gDF2) from a species tree and gene
|
|
4542
|
+
trees via bipartition matching. In ASTRAL mode (no -g),
|
|
4543
|
+
parses q1/q2/q3 annotations from ASTRAL -t 2 output.
|
|
4544
|
+
|
|
4545
|
+
Pie slices show: concordant (blue), discordant alt 1
|
|
4546
|
+
(red), discordant alt 2 (gray).
|
|
4547
|
+
|
|
4548
|
+
Aliases:
|
|
4549
|
+
quartet_pie, qpie, quartet_pie_chart
|
|
4550
|
+
Command line interfaces:
|
|
4551
|
+
pk_quartet_pie, pk_qpie
|
|
4552
|
+
|
|
4553
|
+
Usage:
|
|
4554
|
+
phykit quartet_pie -t <tree> [-g <gene_trees>] -o <output>
|
|
4555
|
+
[--annotate] [--json]
|
|
4556
|
+
[--fig-width <float>] [--fig-height <float>]
|
|
4557
|
+
[--dpi <int>] [--no-title] [--title <str>]
|
|
4558
|
+
[--legend-position <str>]
|
|
4559
|
+
[--ylabel-fontsize <float>] [--xlabel-fontsize <float>]
|
|
4560
|
+
[--title-fontsize <float>] [--axis-fontsize <float>]
|
|
4561
|
+
[--colors <str>]
|
|
4562
|
+
|
|
4563
|
+
Options
|
|
4564
|
+
=====================================================
|
|
4565
|
+
-t/--tree species tree file (required)
|
|
4566
|
+
|
|
4567
|
+
-g/--gene-trees gene trees file, one Newick
|
|
4568
|
+
tree per line (optional;
|
|
4569
|
+
if omitted, ASTRAL -t 2
|
|
4570
|
+
annotations are parsed)
|
|
4571
|
+
|
|
4572
|
+
-o/--output output figure path (required;
|
|
4573
|
+
supports .png, .pdf, .svg)
|
|
4574
|
+
|
|
4575
|
+
--annotate show gCF/gDF values as text
|
|
4576
|
+
near each pie chart
|
|
4577
|
+
|
|
4578
|
+
--fig-width figure width in inches
|
|
4579
|
+
(auto-scaled if omitted)
|
|
4580
|
+
|
|
4581
|
+
--fig-height figure height in inches
|
|
4582
|
+
(auto-scaled if omitted)
|
|
4583
|
+
|
|
4584
|
+
--dpi resolution in DPI
|
|
4585
|
+
(default: 300)
|
|
4586
|
+
|
|
4587
|
+
--no-title hide the plot title
|
|
4588
|
+
|
|
4589
|
+
--title custom title text
|
|
4590
|
+
|
|
4591
|
+
--legend-position legend location (e.g.,
|
|
4592
|
+
"upper right", "none")
|
|
4593
|
+
|
|
4594
|
+
--ylabel-fontsize font size for tip labels;
|
|
4595
|
+
0 to hide
|
|
4596
|
+
|
|
4597
|
+
--xlabel-fontsize font size for x-axis labels;
|
|
4598
|
+
0 to hide
|
|
4599
|
+
|
|
4600
|
+
--title-fontsize font size for the title
|
|
4601
|
+
|
|
4602
|
+
--axis-fontsize font size for axis labels
|
|
4603
|
+
|
|
4604
|
+
--colors comma-separated colors for
|
|
4605
|
+
concordant, disc1, disc2
|
|
4606
|
+
(default: "#2b8cbe,#d62728,
|
|
4607
|
+
#969696")
|
|
4608
|
+
|
|
4609
|
+
--json optional argument to output
|
|
4610
|
+
per-node concordance as JSON
|
|
4611
|
+
"""
|
|
4612
|
+
),
|
|
4613
|
+
)
|
|
4614
|
+
parser.add_argument(
|
|
4615
|
+
"-t", "--tree", type=str, required=True, help=SUPPRESS, metavar=""
|
|
4616
|
+
)
|
|
4617
|
+
parser.add_argument(
|
|
4618
|
+
"-g", "--gene-trees", type=str, required=False, default=None,
|
|
4619
|
+
help=SUPPRESS, metavar=""
|
|
4620
|
+
)
|
|
4621
|
+
parser.add_argument(
|
|
4622
|
+
"-o", "--output", type=str, required=True, help=SUPPRESS, metavar=""
|
|
4623
|
+
)
|
|
4624
|
+
parser.add_argument(
|
|
4625
|
+
"--annotate", action="store_true", required=False, help=SUPPRESS
|
|
4626
|
+
)
|
|
4627
|
+
add_plot_arguments(parser)
|
|
4628
|
+
_add_json_argument(parser)
|
|
4629
|
+
_run_service(parser, argv, QuartetPie)
|
|
4630
|
+
|
|
4527
4631
|
@staticmethod
|
|
4528
4632
|
def quartet_network(argv):
|
|
4529
4633
|
parser = _new_parser(
|
|
@@ -6620,6 +6724,10 @@ def quartet_network(argv=None):
|
|
|
6620
6724
|
Phykit.quartet_network(sys.argv[1:])
|
|
6621
6725
|
|
|
6622
6726
|
|
|
6727
|
+
def quartet_pie(argv=None):
|
|
6728
|
+
Phykit.quartet_pie(sys.argv[1:])
|
|
6729
|
+
|
|
6730
|
+
|
|
6623
6731
|
def ltt(argv=None):
|
|
6624
6732
|
Phykit.ltt(sys.argv[1:])
|
|
6625
6733
|
|
|
@@ -84,6 +84,7 @@ ThresholdModel = _LazyServiceFactory("phykit.services.tree.threshold_model", "Th
|
|
|
84
84
|
PolytomyTest = _LazyServiceFactory("phykit.services.tree.polytomy_test", "PolytomyTest")
|
|
85
85
|
PrintTree = _LazyServiceFactory("phykit.services.tree.print_tree", "PrintTree")
|
|
86
86
|
PruneTree = _LazyServiceFactory("phykit.services.tree.prune_tree", "PruneTree")
|
|
87
|
+
QuartetPie = _LazyServiceFactory("phykit.services.tree.quartet_pie", "QuartetPie")
|
|
87
88
|
RenameTreeTips = _LazyServiceFactory("phykit.services.tree.rename_tree_tips", "RenameTreeTips")
|
|
88
89
|
FitDiscrete = _LazyServiceFactory("phykit.services.tree.fit_discrete", "FitDiscrete")
|
|
89
90
|
KuhnerFelsensteinDistance = _LazyServiceFactory("phykit.services.tree.kf_distance", "KuhnerFelsensteinDistance")
|
|
@@ -28,6 +28,7 @@ _EXPORTS = {
|
|
|
28
28
|
"PolytomyTest": "polytomy_test",
|
|
29
29
|
"PrintTree": "print_tree",
|
|
30
30
|
"PruneTree": "prune_tree",
|
|
31
|
+
"QuartetPie": "quartet_pie",
|
|
31
32
|
"RenameTreeTips": "rename_tree_tips",
|
|
32
33
|
"KuhnerFelsensteinDistance": "kf_distance",
|
|
33
34
|
"RobinsonFouldsDistance": "rf_distance",
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quartet pie chart visualization of gene tree concordance.
|
|
3
|
+
|
|
4
|
+
Draws a phylogram with pie charts at internal nodes showing the
|
|
5
|
+
proportion of gene trees supporting the species tree topology (gCF)
|
|
6
|
+
versus the two NNI alternative topologies (gDF1, gDF2). Supports
|
|
7
|
+
both native computation from gene trees and parsing of ASTRAL -t 2
|
|
8
|
+
annotations.
|
|
9
|
+
"""
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Dict, List, Tuple
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from .base import Tree
|
|
16
|
+
from ...helpers.json_output import print_json
|
|
17
|
+
from ...helpers.plot_config import PlotConfig
|
|
18
|
+
from ...helpers.quartet_utils import (
|
|
19
|
+
compute_gcf_per_node,
|
|
20
|
+
parse_astral_annotations,
|
|
21
|
+
)
|
|
22
|
+
from ...errors import PhykitUserError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class QuartetPie(Tree):
|
|
26
|
+
def __init__(self, args) -> None:
|
|
27
|
+
parsed = self.process_args(args)
|
|
28
|
+
super().__init__(tree_file_path=parsed["tree_file_path"])
|
|
29
|
+
self.gene_trees_path = parsed["gene_trees_path"]
|
|
30
|
+
self.output_path = parsed["output_path"]
|
|
31
|
+
self.annotate = parsed["annotate"]
|
|
32
|
+
self.json_output = parsed["json_output"]
|
|
33
|
+
self.plot_config = parsed["plot_config"]
|
|
34
|
+
|
|
35
|
+
def run(self) -> None:
|
|
36
|
+
tree = self.read_tree_file()
|
|
37
|
+
self._validate_tree(tree)
|
|
38
|
+
|
|
39
|
+
if self.gene_trees_path:
|
|
40
|
+
# Native mode: compute gCF from gene trees
|
|
41
|
+
gene_trees = self._parse_gene_trees(self.gene_trees_path)
|
|
42
|
+
proportions = compute_gcf_per_node(tree, gene_trees)
|
|
43
|
+
input_mode = "native"
|
|
44
|
+
n_gene_trees = len(gene_trees)
|
|
45
|
+
else:
|
|
46
|
+
# ASTRAL mode: parse q1/q2/q3 from node labels
|
|
47
|
+
astral_props = parse_astral_annotations(tree)
|
|
48
|
+
if not astral_props:
|
|
49
|
+
raise PhykitUserError(
|
|
50
|
+
[
|
|
51
|
+
"No ASTRAL q1/q2/q3 annotations found in the tree.",
|
|
52
|
+
"Either provide gene trees with -g, or use an ASTRAL",
|
|
53
|
+
"-t 2 output tree with quartet annotations.",
|
|
54
|
+
],
|
|
55
|
+
code=2,
|
|
56
|
+
)
|
|
57
|
+
# Convert to same format: (q1, q2, q3, 0, 0, 0) — no raw counts
|
|
58
|
+
proportions = {
|
|
59
|
+
cid: (q1, q2, q3, 0, 0, 0)
|
|
60
|
+
for cid, (q1, q2, q3) in astral_props.items()
|
|
61
|
+
}
|
|
62
|
+
input_mode = "astral"
|
|
63
|
+
n_gene_trees = 0
|
|
64
|
+
|
|
65
|
+
self._plot_quartet_pie(tree, proportions, self.output_path)
|
|
66
|
+
|
|
67
|
+
if self.json_output:
|
|
68
|
+
self._print_json(tree, proportions, input_mode, n_gene_trees)
|
|
69
|
+
else:
|
|
70
|
+
print(f"Quartet pie chart saved: {self.output_path}")
|
|
71
|
+
|
|
72
|
+
def process_args(self, args) -> Dict:
|
|
73
|
+
return dict(
|
|
74
|
+
tree_file_path=args.tree,
|
|
75
|
+
gene_trees_path=getattr(args, "gene_trees", None),
|
|
76
|
+
output_path=args.output,
|
|
77
|
+
annotate=getattr(args, "annotate", False),
|
|
78
|
+
json_output=getattr(args, "json", False),
|
|
79
|
+
plot_config=PlotConfig.from_args(args),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _validate_tree(self, tree) -> None:
|
|
83
|
+
tips = list(tree.get_terminals())
|
|
84
|
+
if len(tips) < 4:
|
|
85
|
+
raise PhykitUserError(
|
|
86
|
+
["Tree must have at least 4 tips for quartet analysis."],
|
|
87
|
+
code=2,
|
|
88
|
+
)
|
|
89
|
+
for clade in tree.find_clades():
|
|
90
|
+
if clade.branch_length is None and clade != tree.root:
|
|
91
|
+
clade.branch_length = 1e-8
|
|
92
|
+
|
|
93
|
+
def _parse_gene_trees(self, path: str) -> list:
|
|
94
|
+
from Bio import Phylo
|
|
95
|
+
try:
|
|
96
|
+
return list(Phylo.parse(path, "newick"))
|
|
97
|
+
except Exception:
|
|
98
|
+
raise PhykitUserError(
|
|
99
|
+
[
|
|
100
|
+
f"Could not parse gene trees from {path}.",
|
|
101
|
+
"File should contain one Newick tree per line.",
|
|
102
|
+
],
|
|
103
|
+
code=2,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _plot_quartet_pie(
|
|
107
|
+
self,
|
|
108
|
+
tree,
|
|
109
|
+
proportions: Dict[int, Tuple],
|
|
110
|
+
output_path: str,
|
|
111
|
+
) -> None:
|
|
112
|
+
try:
|
|
113
|
+
import matplotlib
|
|
114
|
+
matplotlib.use("Agg")
|
|
115
|
+
import matplotlib.pyplot as plt
|
|
116
|
+
from matplotlib.patches import Patch
|
|
117
|
+
except ImportError:
|
|
118
|
+
print("matplotlib is required for quartet_pie. Install matplotlib and retry.")
|
|
119
|
+
raise SystemExit(2)
|
|
120
|
+
|
|
121
|
+
config = self.plot_config
|
|
122
|
+
tips = list(tree.get_terminals())
|
|
123
|
+
config.resolve(n_rows=len(tips), n_cols=None)
|
|
124
|
+
|
|
125
|
+
default_colors = ["#2b8cbe", "#d62728", "#969696"]
|
|
126
|
+
colors = config.merge_colors(default_colors)
|
|
127
|
+
|
|
128
|
+
# Build parent map
|
|
129
|
+
parent_map = {}
|
|
130
|
+
for clade in tree.find_clades(order="preorder"):
|
|
131
|
+
for child in clade.clades:
|
|
132
|
+
parent_map[id(child)] = clade
|
|
133
|
+
|
|
134
|
+
# Compute node positions
|
|
135
|
+
node_x = {}
|
|
136
|
+
node_y = {}
|
|
137
|
+
|
|
138
|
+
for i, tip in enumerate(tips):
|
|
139
|
+
node_y[id(tip)] = i
|
|
140
|
+
|
|
141
|
+
root = tree.root
|
|
142
|
+
for clade in tree.find_clades(order="preorder"):
|
|
143
|
+
if clade == root:
|
|
144
|
+
node_x[id(clade)] = 0.0
|
|
145
|
+
elif id(clade) in parent_map:
|
|
146
|
+
parent = parent_map[id(clade)]
|
|
147
|
+
t = clade.branch_length if clade.branch_length else 0.0
|
|
148
|
+
node_x[id(clade)] = node_x.get(id(parent), 0.0) + t
|
|
149
|
+
|
|
150
|
+
for clade in tree.find_clades(order="postorder"):
|
|
151
|
+
if not clade.is_terminal() and id(clade) not in node_y:
|
|
152
|
+
child_ys = [
|
|
153
|
+
node_y[id(c)] for c in clade.clades if id(c) in node_y
|
|
154
|
+
]
|
|
155
|
+
if child_ys:
|
|
156
|
+
node_y[id(clade)] = np.mean(child_ys)
|
|
157
|
+
else:
|
|
158
|
+
node_y[id(clade)] = 0.0
|
|
159
|
+
|
|
160
|
+
fig, ax = plt.subplots(figsize=(config.fig_width, config.fig_height))
|
|
161
|
+
|
|
162
|
+
# Draw branches
|
|
163
|
+
for clade in tree.find_clades(order="preorder"):
|
|
164
|
+
if clade == root:
|
|
165
|
+
continue
|
|
166
|
+
if id(clade) not in parent_map:
|
|
167
|
+
continue
|
|
168
|
+
parent = parent_map[id(clade)]
|
|
169
|
+
if id(parent) not in node_x or id(clade) not in node_x:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
x0 = node_x[id(parent)]
|
|
173
|
+
x1 = node_x[id(clade)]
|
|
174
|
+
y0 = node_y.get(id(parent), 0)
|
|
175
|
+
y1 = node_y.get(id(clade), 0)
|
|
176
|
+
|
|
177
|
+
ax.plot([x0, x1], [y1, y1], color="black", lw=1.5)
|
|
178
|
+
ax.plot([x0, x0], [y0, y1], color="black", lw=1.5)
|
|
179
|
+
|
|
180
|
+
# Pie charts at internal nodes — rendered as inset axes so they
|
|
181
|
+
# appear as perfect circles regardless of axis scaling, and are
|
|
182
|
+
# drawn above the phylogeny branches.
|
|
183
|
+
max_x = max(node_x.values()) if node_x else 1.0
|
|
184
|
+
n_tips = len(tips)
|
|
185
|
+
# Pie size in figure-fraction units (scales with figure, not data)
|
|
186
|
+
pie_size = min(0.06, 0.8 / max(n_tips, 1))
|
|
187
|
+
|
|
188
|
+
# Force a draw so transData is populated
|
|
189
|
+
fig.canvas.draw()
|
|
190
|
+
|
|
191
|
+
for clade in tree.find_clades(order="preorder"):
|
|
192
|
+
if clade.is_terminal() or clade == root:
|
|
193
|
+
continue
|
|
194
|
+
cid = id(clade)
|
|
195
|
+
if cid not in proportions:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
props = proportions[cid]
|
|
199
|
+
gcf, gdf1, gdf2 = props[0], props[1], props[2]
|
|
200
|
+
cx = node_x.get(cid, 0)
|
|
201
|
+
cy = node_y.get(cid, 0)
|
|
202
|
+
|
|
203
|
+
# Convert data coords to figure-fraction coords for the inset
|
|
204
|
+
disp = ax.transData.transform((cx, cy))
|
|
205
|
+
fig_coord = fig.transFigure.inverted().transform(disp)
|
|
206
|
+
fx, fy = fig_coord
|
|
207
|
+
|
|
208
|
+
# Create a small inset axes centered on the node
|
|
209
|
+
inset = fig.add_axes(
|
|
210
|
+
[fx - pie_size / 2, fy - pie_size / 2, pie_size, pie_size],
|
|
211
|
+
zorder=10,
|
|
212
|
+
)
|
|
213
|
+
wedge_vals = [gcf, gdf1, gdf2]
|
|
214
|
+
wedge_colors = [c for v, c in zip(wedge_vals, colors) if v > 1e-6]
|
|
215
|
+
wedge_vals = [v for v in wedge_vals if v > 1e-6]
|
|
216
|
+
if wedge_vals:
|
|
217
|
+
inset.pie(
|
|
218
|
+
wedge_vals, colors=wedge_colors, startangle=90,
|
|
219
|
+
wedgeprops={"edgecolor": "black", "linewidth": 0.5},
|
|
220
|
+
)
|
|
221
|
+
inset.set_aspect("equal")
|
|
222
|
+
inset.axis("off")
|
|
223
|
+
|
|
224
|
+
# Annotate with values if requested
|
|
225
|
+
if self.annotate:
|
|
226
|
+
ax.annotate(
|
|
227
|
+
f"{gcf:.2f}/{gdf1:.2f}/{gdf2:.2f}",
|
|
228
|
+
(cx, cy),
|
|
229
|
+
textcoords="offset points",
|
|
230
|
+
xytext=(8, 8),
|
|
231
|
+
fontsize=6,
|
|
232
|
+
color="black",
|
|
233
|
+
zorder=11,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Tip labels
|
|
237
|
+
offset = max_x * 0.03
|
|
238
|
+
label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize and config.ylabel_fontsize > 0 else 9
|
|
239
|
+
for tip in tips:
|
|
240
|
+
ax.text(
|
|
241
|
+
node_x[id(tip)] + offset, node_y[id(tip)],
|
|
242
|
+
tip.name, va="center", fontsize=label_fontsize,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Legend
|
|
246
|
+
legend_handles = [
|
|
247
|
+
Patch(facecolor=colors[0], edgecolor="black", linewidth=0.5,
|
|
248
|
+
label="Concordant (gCF / q1)"),
|
|
249
|
+
Patch(facecolor=colors[1], edgecolor="black", linewidth=0.5,
|
|
250
|
+
label="Discordant alt 1 (gDF1 / q2)"),
|
|
251
|
+
Patch(facecolor=colors[2], edgecolor="black", linewidth=0.5,
|
|
252
|
+
label="Discordant alt 2 (gDF2 / q3)"),
|
|
253
|
+
]
|
|
254
|
+
legend_loc = config.legend_position or "upper right"
|
|
255
|
+
if legend_loc != "none":
|
|
256
|
+
ax.legend(handles=legend_handles, loc=legend_loc, fontsize=8, frameon=True)
|
|
257
|
+
|
|
258
|
+
ax.set_xlabel("Branch length (subs/site)")
|
|
259
|
+
ax.set_yticks([])
|
|
260
|
+
ax.spines["top"].set_visible(False)
|
|
261
|
+
ax.spines["right"].set_visible(False)
|
|
262
|
+
ax.spines["left"].set_visible(False)
|
|
263
|
+
|
|
264
|
+
if config.show_title:
|
|
265
|
+
ax.set_title(
|
|
266
|
+
config.title or "Quartet Concordance Pie Chart",
|
|
267
|
+
fontsize=config.title_fontsize,
|
|
268
|
+
)
|
|
269
|
+
if config.axis_fontsize:
|
|
270
|
+
ax.xaxis.label.set_fontsize(config.axis_fontsize)
|
|
271
|
+
|
|
272
|
+
# Use constrained_layout=False since inset pie axes are incompatible
|
|
273
|
+
# with tight_layout; manual padding via subplots_adjust instead
|
|
274
|
+
fig.subplots_adjust(left=0.05, right=0.85, top=0.92, bottom=0.12)
|
|
275
|
+
fig.savefig(output_path, dpi=config.dpi, bbox_inches="tight")
|
|
276
|
+
plt.close(fig)
|
|
277
|
+
|
|
278
|
+
def _print_json(self, tree, proportions, input_mode, n_gene_trees):
|
|
279
|
+
nodes = []
|
|
280
|
+
for clade in tree.find_clades(order="preorder"):
|
|
281
|
+
if clade.is_terminal():
|
|
282
|
+
continue
|
|
283
|
+
cid = id(clade)
|
|
284
|
+
if cid not in proportions:
|
|
285
|
+
continue
|
|
286
|
+
props = proportions[cid]
|
|
287
|
+
tip_names = sorted(t.name for t in clade.get_terminals())
|
|
288
|
+
nodes.append({
|
|
289
|
+
"node_tips": tip_names,
|
|
290
|
+
"gCF": round(props[0], 4),
|
|
291
|
+
"gDF1": round(props[1], 4),
|
|
292
|
+
"gDF2": round(props[2], 4),
|
|
293
|
+
"concordant_count": props[3],
|
|
294
|
+
"disc1_count": props[4],
|
|
295
|
+
"disc2_count": props[5],
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
payload = {
|
|
299
|
+
"n_taxa": tree.count_terminals(),
|
|
300
|
+
"n_gene_trees": n_gene_trees,
|
|
301
|
+
"input_mode": input_mode,
|
|
302
|
+
"nodes": nodes,
|
|
303
|
+
"output_file": self.output_path,
|
|
304
|
+
}
|
|
305
|
+
print_json(payload)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.43"
|
|
@@ -22,6 +22,7 @@ phykit/helpers/files.py
|
|
|
22
22
|
phykit/helpers/json_output.py
|
|
23
23
|
phykit/helpers/parallel.py
|
|
24
24
|
phykit/helpers/plot_config.py
|
|
25
|
+
phykit/helpers/quartet_utils.py
|
|
25
26
|
phykit/helpers/stats_summary.py
|
|
26
27
|
phykit/helpers/streaming.py
|
|
27
28
|
phykit/services/__init__.py
|
|
@@ -93,6 +94,7 @@ phykit/services/tree/polytomy_test.py
|
|
|
93
94
|
phykit/services/tree/print_tree.py
|
|
94
95
|
phykit/services/tree/prune_tree.py
|
|
95
96
|
phykit/services/tree/quartet_network.py
|
|
97
|
+
phykit/services/tree/quartet_pie.py
|
|
96
98
|
phykit/services/tree/rate_heterogeneity.py
|
|
97
99
|
phykit/services/tree/relative_rate_test.py
|
|
98
100
|
phykit/services/tree/rename_tree_tips.py
|
|
@@ -163,8 +163,11 @@ pk_ps = phykit.phykit:phylogenetic_signal
|
|
|
163
163
|
pk_pt = phykit.phykit:print_tree
|
|
164
164
|
pk_ptt = phykit.phykit:polytomy_test
|
|
165
165
|
pk_qnet = phykit.phykit:quartet_network
|
|
166
|
+
pk_qpie = phykit.phykit:quartet_pie
|
|
166
167
|
pk_quartet_net = phykit.phykit:quartet_network
|
|
167
168
|
pk_quartet_network = phykit.phykit:quartet_network
|
|
169
|
+
pk_quartet_pie = phykit.phykit:quartet_pie
|
|
170
|
+
pk_quartet_pie_chart = phykit.phykit:quartet_pie
|
|
168
171
|
pk_rate_heterogeneity = phykit.phykit:rate_heterogeneity
|
|
169
172
|
pk_rcv = phykit.phykit:rcv
|
|
170
173
|
pk_rcvt = phykit.phykit:rcvt
|
|
@@ -202,6 +202,9 @@ setup(
|
|
|
202
202
|
"pk_quartet_net = phykit.phykit:quartet_network",
|
|
203
203
|
"pk_qnet = phykit.phykit:quartet_network",
|
|
204
204
|
"pk_nanuq = phykit.phykit:quartet_network",
|
|
205
|
+
"pk_quartet_pie = phykit.phykit:quartet_pie",
|
|
206
|
+
"pk_qpie = phykit.phykit:quartet_pie",
|
|
207
|
+
"pk_quartet_pie_chart = phykit.phykit:quartet_pie",
|
|
205
208
|
"pk_ltt = phykit.phykit:ltt",
|
|
206
209
|
"pk_gamma_stat = phykit.phykit:ltt",
|
|
207
210
|
"pk_gamma = phykit.phykit:ltt",
|
phykit-2.1.41/phykit/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.1.41"
|
|
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
|