PGAP2 2.2__tar.gz → 2.4__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.
- {pgap2-2.2 → pgap2-2.4/PGAP2.egg-info}/PKG-INFO +1 -1
- {pgap2-2.2/PGAP2.egg-info → pgap2-2.4}/PKG-INFO +1 -1
- {pgap2-2.2 → pgap2-2.4}/pgap2/cli/neighborhood.py +7 -1
- {pgap2-2.2 → pgap2-2.4}/pgap2/lib/synteny.py +3 -2
- {pgap2-2.2 → pgap2-2.4}/pgap2/main.py +1 -1
- {pgap2-2.2 → pgap2-2.4}/pgap2/postprocess/profile.py +1 -1
- {pgap2-2.2 → pgap2-2.4}/pgap2/postprocess/stat.py +235 -37
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/sqlite_builder.py +3 -5
- {pgap2-2.2 → pgap2-2.4}/pyproject.toml +1 -1
- pgap2-2.4/tests/test_post.py +143 -0
- pgap2-2.2/tests/test_post.py +0 -43
- {pgap2-2.2 → pgap2-2.4}/LICENSE +0 -0
- {pgap2-2.2 → pgap2-2.4}/MANIFEST.in +0 -0
- {pgap2-2.2 → pgap2-2.4}/PGAP2.egg-info/SOURCES.txt +0 -0
- {pgap2-2.2 → pgap2-2.4}/PGAP2.egg-info/dependency_links.txt +0 -0
- {pgap2-2.2 → pgap2-2.4}/PGAP2.egg-info/entry_points.txt +0 -0
- {pgap2-2.2 → pgap2-2.4}/PGAP2.egg-info/requires.txt +0 -0
- {pgap2-2.2 → pgap2-2.4}/PGAP2.egg-info/top_level.txt +0 -0
- {pgap2-2.2 → pgap2-2.4}/README.md +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/__init__.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/cli/__init__.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/cli/partition.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/cli/partition_add.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/cli/partition_merge.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/cli/partition_minus.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/cli/postprocess.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/cli/preprocess.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/cli/sqlite_cmd.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/dependencies/draw_post_profile.r +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/dependencies/draw_post_stat.r +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/dependencies/draw_prep.r +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/dependencies/maskrc-svg.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/dependencies/run_fastbaps +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/lib/__init__.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/lib/basic.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/lib/panclust.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/lib/pangenome.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/lib/pangp.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/lib/phylogeny.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/lib/pklcheck.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/lib/species.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/lib/strain.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/lib/tree.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/__init__.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/baps.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/base.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/gene_annotation.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/gene_cluster.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/gene_neighborhood.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/pav.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/phylogeny.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/preprocess.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/project.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/project_file.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/statistics.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/strain.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/models/tajimas_d.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/postprocess/__init__.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/postprocess/baps.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/postprocess/graph.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/postprocess/neighborhood.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/postprocess/reference_layout.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/postprocess/singletree.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/postprocess/tajimas_d.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/toy_input/287_91.fa +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/toy_input/287_91.gff +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/toy_input/AKU_12601.gff +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/toy_input/Readme_input.md +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/toy_input/SC_B67.gbk +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/__init__.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/alignment.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/arrangement_detector.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/data_loader.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/draw.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/gene_retriever.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/generate_tree.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/graph_utils.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/neighborhood.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/partition.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/partition_add.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/partition_merge.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/partition_minus.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/postprocess.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/preprocess.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/supply.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/pgap2/utils/tools.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/setup.cfg +0 -0
- {pgap2-2.2 → pgap2-2.4}/tests/test_main.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/tests/test_pangenome.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/tests/test_prep.py +0 -0
- {pgap2-2.2 → pgap2-2.4}/tests/test_synteny.py +0 -0
|
@@ -4,6 +4,12 @@ CLI definitions for the 'neigh' (conserved gene neighborhood) subcommand.
|
|
|
4
4
|
This subcommand runs the full partition pipeline (identical to 'main')
|
|
5
5
|
followed by a conserved gene neighborhood discovery step inspired by
|
|
6
6
|
spacedust's agglomerative clustering algorithm.
|
|
7
|
+
|
|
8
|
+
spacedust:
|
|
9
|
+
https://github.com/soedinglab/spacedust
|
|
10
|
+
Zhang, R., Mirdita, M. & Söding, J. De novo discovery of conserved gene clusters in microbial genomes with Spacedust.
|
|
11
|
+
Nat Methods 22, 2065–2073 (2025). https://doi.org/10.1038/s41592-025-02816-x
|
|
12
|
+
|
|
7
13
|
"""
|
|
8
14
|
|
|
9
15
|
import os
|
|
@@ -79,7 +85,7 @@ def neighborhood_cmd(subparser: _SubParsersAction):
|
|
|
79
85
|
'neigh',
|
|
80
86
|
help='Conserved gene neighborhood (synteny block) analysis. '
|
|
81
87
|
'Runs the full partition pipeline then discovers conserved '
|
|
82
|
-
'gene neighborhoods across genomes
|
|
88
|
+
'gene neighborhoods across genomes.',
|
|
83
89
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
84
90
|
|
|
85
91
|
# ---- Partition parameters (same as 'main') ----
|
|
@@ -17,8 +17,9 @@ The algorithm works in four steps:
|
|
|
17
17
|
gene neighborhoods.
|
|
18
18
|
|
|
19
19
|
Reference:
|
|
20
|
-
Zhang, R., Mirdita, M
|
|
21
|
-
|
|
20
|
+
Zhang, R., Mirdita, M. & Söding, J. De novo discovery of conserved gene clusters in microbial genomes with Spacedust.
|
|
21
|
+
Nat Methods 22, 2065–2073 (2025).
|
|
22
|
+
https://doi.org/10.1038/s41592-025-02816-x
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
25
|
import math
|
|
@@ -55,7 +55,7 @@ def main(pav_file: str, outdir: str, nodraw: bool, threads: int = 1, disable: bo
|
|
|
55
55
|
group_freq, pan_para_stat = get_pan_group(pav, outdir)
|
|
56
56
|
|
|
57
57
|
logger.info("Starting rarefaction curve fitting.")
|
|
58
|
-
fit_rerefaction(pan_profile, outdir)
|
|
58
|
+
fit_rerefaction(pan_profile, outdir, new_clusters)
|
|
59
59
|
|
|
60
60
|
if nodraw:
|
|
61
61
|
logger.info('Drawing is disabled')
|
|
@@ -56,6 +56,132 @@ def pan_gene_model(x, k, gamma):
|
|
|
56
56
|
return k * x**gamma
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def new_gene_model(x, k, alpha):
|
|
60
|
+
return k * x**(-alpha)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Make sure that only finite values are used in the fitting process.
|
|
64
|
+
def _drop_invalid_fit_points(x_data, y_data):
|
|
65
|
+
x_data = np.asarray(x_data, dtype=float)
|
|
66
|
+
y_data = np.asarray(y_data, dtype=float)
|
|
67
|
+
valid_points = np.isfinite(x_data) & np.isfinite(y_data)
|
|
68
|
+
return x_data[valid_points], y_data[valid_points]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _get_core_fit_initial_params(core_gene_means):
|
|
72
|
+
if len(core_gene_means) == 0:
|
|
73
|
+
raise ValueError("No valid core gene points are available for curve fitting.")
|
|
74
|
+
C_inf = max(float(core_gene_means[-1]), 0.0)
|
|
75
|
+
C0 = max(float(core_gene_means[0]) - C_inf, 0.0)
|
|
76
|
+
return [C0, 0.05, C_inf]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get_pan_fit_initial_params(pan_gene_means):
|
|
80
|
+
if len(pan_gene_means) == 0:
|
|
81
|
+
raise ValueError("No valid pan gene points are available for curve fitting.")
|
|
82
|
+
k = max(float(pan_gene_means[0]), 0.0)
|
|
83
|
+
return [k, 0.5]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _fit_pan_gene_loglog(x_data, pan_gene_means):
|
|
87
|
+
positive_points = (x_data > 0) & (pan_gene_means > 0)
|
|
88
|
+
if np.count_nonzero(positive_points) < 2:
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
"At least two positive points are required for log-log pan gene fitting.")
|
|
91
|
+
|
|
92
|
+
gamma, log_k = np.polyfit(
|
|
93
|
+
np.log(x_data[positive_points]), np.log(pan_gene_means[positive_points]), 1)
|
|
94
|
+
k = np.exp(log_k)
|
|
95
|
+
return np.asarray([k, gamma], dtype=float)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _prepare_new_gene_fit_data(new_clusters):
|
|
99
|
+
if new_clusters is None or len(new_clusters) != 2:
|
|
100
|
+
raise ValueError("New gene cluster data are required for pan-genome state fitting.")
|
|
101
|
+
|
|
102
|
+
x_data = np.asarray(new_clusters[0], dtype=float)
|
|
103
|
+
sample_groups = new_clusters[1]
|
|
104
|
+
if len(x_data) != len(sample_groups):
|
|
105
|
+
raise ValueError("New gene cluster strain counts and samples have different lengths.")
|
|
106
|
+
|
|
107
|
+
means = []
|
|
108
|
+
for samples in sample_groups:
|
|
109
|
+
values = np.asarray(
|
|
110
|
+
[value for value in samples if value != 'NA'], dtype=float)
|
|
111
|
+
values = values[np.isfinite(values)]
|
|
112
|
+
means.append(np.mean(values) if len(values) else np.nan)
|
|
113
|
+
|
|
114
|
+
x_data, means = _drop_invalid_fit_points(x_data, means)
|
|
115
|
+
valid_points = (x_data > 0) & (means >= 0)
|
|
116
|
+
x_data = x_data[valid_points]
|
|
117
|
+
means = means[valid_points]
|
|
118
|
+
if len(np.unique(x_data)) < 3:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
"At least three distinct strain counts are required for pan-genome state fitting.")
|
|
121
|
+
return x_data, means
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _fit_new_gene_decay(new_clusters):
|
|
125
|
+
x_data, new_gene_means = _prepare_new_gene_fit_data(new_clusters)
|
|
126
|
+
if np.all(new_gene_means == 0):
|
|
127
|
+
return np.asarray([0.0, np.inf]), None
|
|
128
|
+
|
|
129
|
+
alpha_initial = 0.5
|
|
130
|
+
k_initial = max(
|
|
131
|
+
float(new_gene_means[0]) * float(x_data[0])**alpha_initial,
|
|
132
|
+
np.finfo(float).eps,
|
|
133
|
+
)
|
|
134
|
+
try:
|
|
135
|
+
return curve_fit(
|
|
136
|
+
new_gene_model,
|
|
137
|
+
x_data,
|
|
138
|
+
new_gene_means,
|
|
139
|
+
p0=[k_initial, alpha_initial],
|
|
140
|
+
bounds=([0, 0], [np.inf, np.inf]),
|
|
141
|
+
maxfev=10000,
|
|
142
|
+
)
|
|
143
|
+
except (RuntimeError, ValueError):
|
|
144
|
+
positive_points = new_gene_means > 0
|
|
145
|
+
if np.count_nonzero(positive_points) < 3:
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
"At least three positive points are required for log-log new gene fitting.")
|
|
148
|
+
|
|
149
|
+
coefficients, log_covariance = np.polyfit(
|
|
150
|
+
np.log(x_data[positive_points]),
|
|
151
|
+
np.log(new_gene_means[positive_points]),
|
|
152
|
+
1,
|
|
153
|
+
cov=True,
|
|
154
|
+
)
|
|
155
|
+
slope, log_k = coefficients
|
|
156
|
+
popt = np.asarray([np.exp(log_k), -slope], dtype=float)
|
|
157
|
+
covariance = np.zeros((2, 2), dtype=float)
|
|
158
|
+
covariance[1, 1] = log_covariance[0, 0]
|
|
159
|
+
return popt, covariance
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _get_alpha_confidence_interval(alpha, covariance):
|
|
163
|
+
if covariance is None or not np.isfinite(alpha):
|
|
164
|
+
return None
|
|
165
|
+
alpha_variance = covariance[1, 1]
|
|
166
|
+
if not np.isfinite(alpha_variance) or alpha_variance < 0:
|
|
167
|
+
return None
|
|
168
|
+
margin = 1.96 * np.sqrt(alpha_variance)
|
|
169
|
+
return alpha - margin, alpha + margin
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _classify_pan_genome(alpha, alpha_interval):
|
|
173
|
+
if np.isposinf(alpha):
|
|
174
|
+
return "closed"
|
|
175
|
+
if alpha_interval is None:
|
|
176
|
+
return "uncertain"
|
|
177
|
+
lower_alpha, upper_alpha = alpha_interval
|
|
178
|
+
if lower_alpha > 1:
|
|
179
|
+
return "closed"
|
|
180
|
+
if upper_alpha <= 1:
|
|
181
|
+
return "open"
|
|
182
|
+
return "uncertain"
|
|
183
|
+
|
|
184
|
+
|
|
59
185
|
def stat_pan_core(sampling_list, pav, i_strain, strain_num):
|
|
60
186
|
core_list = []
|
|
61
187
|
pan_list = []
|
|
@@ -97,32 +223,35 @@ def calculate_rarefaction(i_strain, pangp: PanGP):
|
|
|
97
223
|
return {'strain_num': i_strain, 'profile': record, 'new_clusters': new_clusters, }
|
|
98
224
|
|
|
99
225
|
|
|
100
|
-
def log_pan_genome_state(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
226
|
+
def log_pan_genome_state(alpha, alpha_interval, genome_status, ofh):
|
|
227
|
+
if genome_status == "open":
|
|
228
|
+
status_description = (
|
|
229
|
+
"The pan-genome is open because the 95% confidence interval for alpha is at or below 1."
|
|
230
|
+
)
|
|
231
|
+
elif genome_status == "closed":
|
|
104
232
|
status_description = (
|
|
105
|
-
"The pan-genome is
|
|
233
|
+
"The pan-genome is closed because no new clusters were observed or the 95% confidence interval for alpha is above 1."
|
|
106
234
|
)
|
|
107
235
|
else:
|
|
108
|
-
genome_status = "closed"
|
|
109
236
|
status_description = (
|
|
110
|
-
"The pan-genome is
|
|
237
|
+
"The pan-genome state is uncertain because the alpha confidence interval crosses 1."
|
|
111
238
|
)
|
|
112
239
|
logger.info(
|
|
113
|
-
|
|
114
|
-
ofh.write(
|
|
115
|
-
f"The pan-genome is {genome_status}. {status_description}\n")
|
|
240
|
+
status_description)
|
|
241
|
+
ofh.write(f"{status_description}\n")
|
|
116
242
|
|
|
117
|
-
|
|
118
|
-
f"The
|
|
119
|
-
|
|
120
|
-
|
|
243
|
+
if np.isfinite(alpha):
|
|
244
|
+
ofh.write(f"The new-gene decay exponent alpha is {alpha:.3f}.\n")
|
|
245
|
+
else:
|
|
246
|
+
ofh.write("The new-gene decay exponent alpha is infinite.\n")
|
|
247
|
+
if alpha_interval is not None:
|
|
248
|
+
ofh.write(
|
|
249
|
+
f"The 95% confidence interval for alpha is [{alpha_interval[0]:.3f}, {alpha_interval[1]:.3f}].\n")
|
|
121
250
|
|
|
122
251
|
|
|
123
|
-
def fit_rerefaction(pan_rarefaction, outdir):
|
|
252
|
+
def fit_rerefaction(pan_rarefaction, outdir, new_clusters=None):
|
|
124
253
|
|
|
125
|
-
x_data = pan_rarefaction[0]
|
|
254
|
+
x_data = np.asarray(pan_rarefaction[0], dtype=float)
|
|
126
255
|
core_gene_samples = pan_rarefaction[1]
|
|
127
256
|
pan_gene_samples = pan_rarefaction[2]
|
|
128
257
|
|
|
@@ -134,27 +263,96 @@ def fit_rerefaction(pan_rarefaction, outdir):
|
|
|
134
263
|
pan_gene_samples[i] = [
|
|
135
264
|
int(_) for _ in pan_gene_samples[i] if _ != 'NA']
|
|
136
265
|
|
|
137
|
-
core_gene_means =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
266
|
+
core_gene_means = np.asarray(
|
|
267
|
+
[np.mean(samples) for samples in core_gene_samples], dtype=float)
|
|
268
|
+
pan_gene_means = np.asarray(
|
|
269
|
+
[np.mean(samples) for samples in pan_gene_samples], dtype=float)
|
|
270
|
+
|
|
271
|
+
core_x_data, core_gene_means = _drop_invalid_fit_points(
|
|
272
|
+
x_data, core_gene_means)
|
|
273
|
+
pan_x_data, pan_gene_means = _drop_invalid_fit_points(
|
|
274
|
+
x_data, pan_gene_means)
|
|
275
|
+
|
|
276
|
+
with open(file=f'{outdir}/postprocess.curve_fit.txt', mode='w') as ofh:
|
|
277
|
+
|
|
278
|
+
logger.info("Fitting core gene data.")
|
|
279
|
+
try:
|
|
280
|
+
popt_core, _ = curve_fit(
|
|
281
|
+
core_gene_model,
|
|
282
|
+
core_x_data,
|
|
283
|
+
core_gene_means,
|
|
284
|
+
p0=_get_core_fit_initial_params(core_gene_means),
|
|
285
|
+
bounds=([0, 0, 0], [np.inf, np.inf, np.inf]),
|
|
286
|
+
maxfev=10000,
|
|
287
|
+
)
|
|
288
|
+
logger.info(f"Optimal parameters for core genes: {popt_core}")
|
|
289
|
+
ofh.write(f'Optimal parameters for core genes: {popt_core}\n')
|
|
290
|
+
except (RuntimeError, ValueError) as err:
|
|
291
|
+
popt_core = None
|
|
292
|
+
logger.warning(f"Core gene curve fitting failed: {err}")
|
|
293
|
+
ofh.write('Optimal parameters for core genes: NA\n')
|
|
294
|
+
|
|
295
|
+
logger.info("Fitting pan gene data.")
|
|
296
|
+
try:
|
|
297
|
+
popt_pan, _ = curve_fit(
|
|
298
|
+
pan_gene_model,
|
|
299
|
+
pan_x_data,
|
|
300
|
+
pan_gene_means,
|
|
301
|
+
p0=_get_pan_fit_initial_params(pan_gene_means),
|
|
302
|
+
bounds=([0, 0], [np.inf, np.inf]),
|
|
303
|
+
maxfev=10000,
|
|
304
|
+
)
|
|
305
|
+
logger.info(f"Optimal parameters for pan genes: {popt_pan}")
|
|
306
|
+
ofh.write(f'Optimal parameters for pan genes: {popt_pan}\n')
|
|
307
|
+
except (RuntimeError, ValueError) as err:
|
|
308
|
+
logger.warning(
|
|
309
|
+
f"Pan gene curve fitting failed with curve_fit: {err}. Trying log-log fallback.")
|
|
310
|
+
try:
|
|
311
|
+
popt_pan = _fit_pan_gene_loglog(pan_x_data, pan_gene_means)
|
|
312
|
+
logger.info(
|
|
313
|
+
f"Optimal parameters for pan genes from log-log fallback: {popt_pan}")
|
|
314
|
+
ofh.write(
|
|
315
|
+
f'Optimal parameters for pan genes: {popt_pan} (log-log fallback)\n')
|
|
316
|
+
except (RuntimeError, ValueError) as fallback_err:
|
|
317
|
+
popt_pan = None
|
|
318
|
+
logger.warning(
|
|
319
|
+
f"Pan gene log-log fallback fitting failed: {fallback_err}")
|
|
320
|
+
ofh.write('Optimal parameters for pan genes: NA\n')
|
|
321
|
+
|
|
322
|
+
if popt_core is None:
|
|
323
|
+
ofh.write('Core Gene Fit: NA\n')
|
|
324
|
+
else:
|
|
325
|
+
ofh.write(
|
|
326
|
+
f'Core Gene Fit: y={popt_core[0]:.3f}*exp(-{popt_core[1]:.3f}*x)+ {popt_core[2]:.3f}\n')
|
|
147
327
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
328
|
+
if popt_pan is None:
|
|
329
|
+
ofh.write('Pan Gene Fit: NA\n')
|
|
330
|
+
else:
|
|
331
|
+
ofh.write(
|
|
332
|
+
f'Pan Gene Fit: y={popt_pan[0]:.3f}*x^{popt_pan[1]:.3f} \n')
|
|
333
|
+
|
|
334
|
+
logger.info("Fitting new gene decay data for pan-genome state classification.")
|
|
335
|
+
try:
|
|
336
|
+
popt_new, covariance_new = _fit_new_gene_decay(new_clusters)
|
|
337
|
+
k_new, alpha = popt_new
|
|
338
|
+
alpha_interval = _get_alpha_confidence_interval(
|
|
339
|
+
alpha, covariance_new)
|
|
340
|
+
genome_status = _classify_pan_genome(alpha, alpha_interval)
|
|
341
|
+
logger.info(f"Optimal parameters for new genes: {popt_new}")
|
|
342
|
+
ofh.write(f'Optimal parameters for new genes: {popt_new}\n')
|
|
343
|
+
if np.isfinite(alpha):
|
|
344
|
+
ofh.write(
|
|
345
|
+
f'New Gene Fit: y={k_new:.3f}*x^(-{alpha:.3f})\n')
|
|
346
|
+
else:
|
|
347
|
+
ofh.write('New Gene Fit: y=0\n')
|
|
348
|
+
log_pan_genome_state(
|
|
349
|
+
alpha, alpha_interval, genome_status, ofh)
|
|
350
|
+
except (RuntimeError, ValueError) as err:
|
|
351
|
+
logger.warning(
|
|
352
|
+
f"Pan-genome state fitting could not be completed: {err}")
|
|
353
|
+
ofh.write('Optimal parameters for new genes: NA\n')
|
|
354
|
+
ofh.write('New Gene Fit: NA\n')
|
|
355
|
+
ofh.write('Pan-genome state: NA\n')
|
|
158
356
|
|
|
159
357
|
# plt.figure(figsize=(8, 6))
|
|
160
358
|
|
|
@@ -361,7 +559,7 @@ def main(indir: str, outdir: str, nodraw: bool, threads: int, disable: bool = Fa
|
|
|
361
559
|
pan_profile, new_clusters = get_rarefaction(
|
|
362
560
|
basic.pav, outdir, disable=disable, threads=threads, S=S, N=N, R=R, K=K, method=method)
|
|
363
561
|
logger.info("Starting rarefaction curve fitting.")
|
|
364
|
-
fit_rerefaction(pan_profile, outdir)
|
|
562
|
+
fit_rerefaction(pan_profile, outdir, new_clusters)
|
|
365
563
|
|
|
366
564
|
if nodraw:
|
|
367
565
|
logger.info('Drawing is disabled')
|
|
@@ -714,11 +714,9 @@ def _import_post_results(db: Session, project_id: int, data_path: Path):
|
|
|
714
714
|
if curve_file.exists():
|
|
715
715
|
content = curve_file.read_text().strip()
|
|
716
716
|
if content:
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
elif "closed" in content.lower():
|
|
721
|
-
openness = "closed"
|
|
717
|
+
state_match = re.search(
|
|
718
|
+
r"The pan-genome is (open|closed)\b", content, re.IGNORECASE)
|
|
719
|
+
openness = state_match.group(1).lower() if state_match else None
|
|
722
720
|
db.add(CurveFit(
|
|
723
721
|
project_id=project_id,
|
|
724
722
|
model_name="heaps",
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@pytest.fixture
|
|
6
|
+
def test_basic():
|
|
7
|
+
basic_file = 'tests/output/basic.pkl'
|
|
8
|
+
from pgap2.lib.pklcheck import PklCheck
|
|
9
|
+
from pgap2.lib.basic import Basic
|
|
10
|
+
import pickle
|
|
11
|
+
|
|
12
|
+
with open(f'{basic_file}', 'rb') as fh:
|
|
13
|
+
previous: PklCheck = pickle.load(fh)
|
|
14
|
+
decode_status = previous.decode()
|
|
15
|
+
assert decode_status is True, "decode did not return True for matching parameters"
|
|
16
|
+
basic: Basic = previous.data_dump('basic')
|
|
17
|
+
assert isinstance(
|
|
18
|
+
basic, Basic), "Basic object was not created correctly"
|
|
19
|
+
return basic
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_get_pan_group(test_basic):
|
|
23
|
+
from pgap2.postprocess.stat import get_pan_group
|
|
24
|
+
basic = test_basic
|
|
25
|
+
outdir = 'tests/output'
|
|
26
|
+
basic.load_pav(file=f'{outdir}/pgap2.partition.gene_content.pav')
|
|
27
|
+
pan_group_freq, pan_para_stat = get_pan_group(basic.pav, outdir)
|
|
28
|
+
assert len(
|
|
29
|
+
pan_group_freq) == 3, "get_pan_group did not return the expected number of groups"
|
|
30
|
+
assert len(
|
|
31
|
+
pan_para_stat) == 5, "get_pan_group did not return the expected number of parameters"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_rare(test_basic):
|
|
35
|
+
from pgap2.postprocess.stat import get_rarefaction, fit_rerefaction
|
|
36
|
+
basic = test_basic
|
|
37
|
+
outdir = 'tests/output/'
|
|
38
|
+
basic.load_pav(file=f'{outdir}/pgap2.partition.gene_content.pav')
|
|
39
|
+
pan_profile, new_clusters = get_rarefaction(
|
|
40
|
+
basic.pav, outdir, N=1)
|
|
41
|
+
assert len(
|
|
42
|
+
pan_profile) == 3, "get_rarefaction did not return the expected number of profiles"
|
|
43
|
+
assert len(
|
|
44
|
+
new_clusters) == 2, "get_rarefaction did not return the expected number of clusters"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_fit_rarefaction_keeps_running_when_core_fit_fails(tmp_path, monkeypatch):
|
|
48
|
+
from pgap2.postprocess import stat
|
|
49
|
+
|
|
50
|
+
pan_profile = [
|
|
51
|
+
[1, 2, 3, 4],
|
|
52
|
+
[[3000], [2500], [2300], [2200]],
|
|
53
|
+
[[4000], [4500], [4800], [5100]],
|
|
54
|
+
]
|
|
55
|
+
original_curve_fit = stat.curve_fit
|
|
56
|
+
|
|
57
|
+
def fail_core_fit(model, *args, **kwargs):
|
|
58
|
+
if model is stat.core_gene_model:
|
|
59
|
+
raise RuntimeError("forced core failure")
|
|
60
|
+
return original_curve_fit(model, *args, **kwargs)
|
|
61
|
+
|
|
62
|
+
monkeypatch.setattr(stat, "curve_fit", fail_core_fit)
|
|
63
|
+
|
|
64
|
+
stat.fit_rerefaction(pan_profile, tmp_path)
|
|
65
|
+
|
|
66
|
+
content = (tmp_path / "postprocess.curve_fit.txt").read_text()
|
|
67
|
+
assert "Optimal parameters for core genes: NA" in content
|
|
68
|
+
assert "Pan Gene Fit: y=" in content
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_fit_rarefaction_uses_pan_loglog_fallback(tmp_path, monkeypatch):
|
|
72
|
+
from pgap2.postprocess import stat
|
|
73
|
+
|
|
74
|
+
pan_profile = [
|
|
75
|
+
[1, 2, 4, 8],
|
|
76
|
+
[[3000], [2500], [2300], [2200]],
|
|
77
|
+
[[1000], [1414], [2000], [2828]],
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def fail_pan_fit(model, *args, **kwargs):
|
|
81
|
+
if model is stat.pan_gene_model:
|
|
82
|
+
raise RuntimeError("forced pan failure")
|
|
83
|
+
return (
|
|
84
|
+
np.asarray([800.0, 0.1, 2200.0], dtype=float),
|
|
85
|
+
np.eye(3),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
monkeypatch.setattr(stat, "curve_fit", fail_pan_fit)
|
|
89
|
+
|
|
90
|
+
stat.fit_rerefaction(pan_profile, tmp_path)
|
|
91
|
+
|
|
92
|
+
content = (tmp_path / "postprocess.curve_fit.txt").read_text()
|
|
93
|
+
assert "Optimal parameters for pan genes:" in content
|
|
94
|
+
assert "log-log fallback" in content
|
|
95
|
+
assert "Pan Gene Fit: y=" in content
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize(
|
|
99
|
+
("alpha", "expected_state"),
|
|
100
|
+
[
|
|
101
|
+
(0.5, "open"),
|
|
102
|
+
(1.5, "closed"),
|
|
103
|
+
],
|
|
104
|
+
)
|
|
105
|
+
def test_fit_rarefaction_classifies_state_from_new_genes(
|
|
106
|
+
tmp_path, alpha, expected_state):
|
|
107
|
+
from pgap2.postprocess import stat
|
|
108
|
+
|
|
109
|
+
x_data = np.asarray([1, 2, 4, 8, 16, 32], dtype=float)
|
|
110
|
+
pan_profile = [
|
|
111
|
+
x_data.tolist(),
|
|
112
|
+
[[3000 - 20 * i] for i in range(len(x_data))],
|
|
113
|
+
[[4000 + 300 * np.sqrt(x)] for x in x_data],
|
|
114
|
+
]
|
|
115
|
+
new_x_data = x_data[1:]
|
|
116
|
+
new_gene_means = 1000 * new_x_data**(-alpha)
|
|
117
|
+
new_clusters = [
|
|
118
|
+
new_x_data.tolist(),
|
|
119
|
+
[[value] for value in new_gene_means],
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
stat.fit_rerefaction(pan_profile, tmp_path, new_clusters)
|
|
123
|
+
|
|
124
|
+
content = (tmp_path / "postprocess.curve_fit.txt").read_text()
|
|
125
|
+
assert "New Gene Fit: y=" in content
|
|
126
|
+
assert f"The pan-genome is {expected_state}" in content
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_fit_rarefaction_reports_na_with_too_few_new_gene_points(tmp_path):
|
|
130
|
+
from pgap2.postprocess import stat
|
|
131
|
+
|
|
132
|
+
pan_profile = [
|
|
133
|
+
[1, 2, 3, 4],
|
|
134
|
+
[[3000], [2500], [2300], [2200]],
|
|
135
|
+
[[4000], [4500], [4800], [5100]],
|
|
136
|
+
]
|
|
137
|
+
new_clusters = [[2, 3], [[500], [200]]]
|
|
138
|
+
|
|
139
|
+
stat.fit_rerefaction(pan_profile, tmp_path, new_clusters)
|
|
140
|
+
|
|
141
|
+
content = (tmp_path / "postprocess.curve_fit.txt").read_text()
|
|
142
|
+
assert "Optimal parameters for new genes: NA" in content
|
|
143
|
+
assert "Pan-genome state: NA" in content
|
pgap2-2.2/tests/test_post.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
@pytest.fixture
|
|
5
|
-
def test_basic():
|
|
6
|
-
basic_file = 'tests/output/basic.pkl'
|
|
7
|
-
from pgap2.lib.pklcheck import PklCheck
|
|
8
|
-
from pgap2.lib.basic import Basic
|
|
9
|
-
import pickle
|
|
10
|
-
|
|
11
|
-
with open(f'{basic_file}', 'rb') as fh:
|
|
12
|
-
previous: PklCheck = pickle.load(fh)
|
|
13
|
-
decode_status = previous.decode()
|
|
14
|
-
assert decode_status is True, "decode did not return True for matching parameters"
|
|
15
|
-
basic: Basic = previous.data_dump('basic')
|
|
16
|
-
assert isinstance(
|
|
17
|
-
basic, Basic), "Basic object was not created correctly"
|
|
18
|
-
return basic
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_get_pan_group(test_basic):
|
|
22
|
-
from pgap2.postprocess.stat import get_pan_group
|
|
23
|
-
basic = test_basic
|
|
24
|
-
outdir = 'tests/output'
|
|
25
|
-
basic.load_pav(file=f'{outdir}/pgap2.partition.gene_content.pav')
|
|
26
|
-
pan_group_freq, pan_para_stat = get_pan_group(basic.pav, outdir)
|
|
27
|
-
assert len(
|
|
28
|
-
pan_group_freq) == 3, "get_pan_group did not return the expected number of groups"
|
|
29
|
-
assert len(
|
|
30
|
-
pan_para_stat) == 5, "get_pan_group did not return the expected number of parameters"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_rare(test_basic):
|
|
34
|
-
from pgap2.postprocess.stat import get_rarefaction, fit_rerefaction
|
|
35
|
-
basic = test_basic
|
|
36
|
-
outdir = 'tests/output/'
|
|
37
|
-
basic.load_pav(file=f'{outdir}/pgap2.partition.gene_content.pav')
|
|
38
|
-
pan_profile, new_clusters = get_rarefaction(
|
|
39
|
-
basic.pav, outdir, N=1)
|
|
40
|
-
assert len(
|
|
41
|
-
pan_profile) == 3, "get_rarefaction did not return the expected number of profiles"
|
|
42
|
-
assert len(
|
|
43
|
-
new_clusters) == 2, "get_rarefaction did not return the expected number of clusters"
|
{pgap2-2.2 → pgap2-2.4}/LICENSE
RENAMED
|
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
|