GenomeTreeTk 0.3.1__py3-none-any.whl
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.
- genometreetk/VERSION +136 -0
- genometreetk/__init__.py +22 -0
- genometreetk/arb.py +126 -0
- genometreetk/blast.py +85 -0
- genometreetk/bootstrap.py +373 -0
- genometreetk/cm_files/README +2 -0
- genometreetk/cm_files/lsu_archaea.cm +20824 -0
- genometreetk/cm_files/lsu_bacteria.cm +20327 -0
- genometreetk/cm_files/lsu_eukaryote.cm +23641 -0
- genometreetk/combine_support.py +151 -0
- genometreetk/common.py +104 -0
- genometreetk/derep_tree.py +236 -0
- genometreetk/fasttree.py +156 -0
- genometreetk/jackknife_markers.py +232 -0
- genometreetk/jackknife_taxa.py +136 -0
- genometreetk/main.py +667 -0
- genometreetk/msa_filter.py +499 -0
- genometreetk/phylogenetic_diversity.py +270 -0
- genometreetk/prune.py +99 -0
- genometreetk/reroot_tree.py +152 -0
- genometreetk/rna_workflow.py +834 -0
- genometreetk/tree_support.py +77 -0
- genometreetk-0.3.1.data/scripts/genometreetk +457 -0
- genometreetk-0.3.1.dist-info/METADATA +22 -0
- genometreetk-0.3.1.dist-info/RECORD +28 -0
- genometreetk-0.3.1.dist-info/WHEEL +5 -0
- genometreetk-0.3.1.dist-info/licenses/LICENSE +675 -0
- genometreetk-0.3.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# #
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify #
|
|
4
|
+
# it under the terms of the GNU General Public License as published by #
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or #
|
|
6
|
+
# (at your option) any later version. #
|
|
7
|
+
# #
|
|
8
|
+
# This program is distributed in the hope that it will be useful, #
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
|
11
|
+
# GNU General Public License for more details. #
|
|
12
|
+
# #
|
|
13
|
+
# You should have received a copy of the GNU General Public License #
|
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
|
15
|
+
# #
|
|
16
|
+
###############################################################################
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import gzip
|
|
21
|
+
import logging
|
|
22
|
+
import multiprocessing as mp
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
from tqdm import tqdm
|
|
27
|
+
|
|
28
|
+
from gtdblib.tree.bootstrap_merge import bootstrap_merge_replicates
|
|
29
|
+
from gtdblib.util.shell.gtdbshutil import make_sure_path_exists
|
|
30
|
+
|
|
31
|
+
from genometreetk.fasttree import FastTreeError, infer_trees, is_complete_tree
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Alignment shared with forked worker processes. It is stored as a module
|
|
35
|
+
# level global so workers inherit it through copy-on-write memory instead of
|
|
36
|
+
# receiving a pickled copy. Since the numpy buffer is never written to and
|
|
37
|
+
# has no per-row Python objects, no pages are duplicated in the workers.
|
|
38
|
+
_SHARED = {}
|
|
39
|
+
|
|
40
|
+
# Number of alignment rows gathered and written at a time by a worker.
|
|
41
|
+
ROW_BLOCK_SIZE = 4096
|
|
42
|
+
|
|
43
|
+
TREE_EXTENSIONS = ('.tree', '.tre', '.treefile')
|
|
44
|
+
MSA_EXTENSIONS = ('.fna', '.faa', '.fasta', '.fa')
|
|
45
|
+
NT_CHARS = b'ACGTUNacgtun'
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BootstrapError(Exception):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def read_msa(msa_file):
|
|
53
|
+
"""Read MSA into a 2D uint8 matrix.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
msa_file : str
|
|
58
|
+
FASTA file with aligned sequences (optionally gzipped).
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
list of bytes
|
|
63
|
+
Sequence IDs in file order.
|
|
64
|
+
np.ndarray
|
|
65
|
+
Matrix of shape (num_seqs, alignment_len) holding one byte per base.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
open_file = gzip.open if msa_file.endswith('.gz') else open
|
|
69
|
+
|
|
70
|
+
# first pass: count sequences and determine alignment length so the
|
|
71
|
+
# matrix can be allocated once (avoids holding two copies in memory)
|
|
72
|
+
num_seqs = 0
|
|
73
|
+
aln_len = 0
|
|
74
|
+
with open_file(msa_file, 'rb') as f:
|
|
75
|
+
for line in f:
|
|
76
|
+
if line[:1] == b'>':
|
|
77
|
+
num_seqs += 1
|
|
78
|
+
elif num_seqs == 1:
|
|
79
|
+
aln_len += len(line.strip())
|
|
80
|
+
|
|
81
|
+
if num_seqs == 0:
|
|
82
|
+
raise BootstrapError(f'No sequences found in {msa_file}.')
|
|
83
|
+
if aln_len == 0:
|
|
84
|
+
raise BootstrapError(f'First sequence in {msa_file} is empty.')
|
|
85
|
+
|
|
86
|
+
msa = np.empty((num_seqs, aln_len), dtype=np.uint8)
|
|
87
|
+
seq_ids = []
|
|
88
|
+
|
|
89
|
+
def store(row_idx, pieces):
|
|
90
|
+
seq = b''.join(pieces)
|
|
91
|
+
if len(seq) != aln_len:
|
|
92
|
+
raise BootstrapError(
|
|
93
|
+
'Sequences in MSA have different lengths: {} has length {:,} but expected {:,}.'.format(
|
|
94
|
+
seq_ids[row_idx].decode(), len(seq), aln_len))
|
|
95
|
+
msa[row_idx] = np.frombuffer(seq, dtype=np.uint8)
|
|
96
|
+
|
|
97
|
+
# second pass: fill matrix
|
|
98
|
+
row_idx = -1
|
|
99
|
+
pieces = []
|
|
100
|
+
with open_file(msa_file, 'rb') as f:
|
|
101
|
+
for line in f:
|
|
102
|
+
if line[:1] == b'>':
|
|
103
|
+
if row_idx >= 0:
|
|
104
|
+
store(row_idx, pieces)
|
|
105
|
+
row_idx += 1
|
|
106
|
+
fields = line[1:].split(None, 1)
|
|
107
|
+
if not fields:
|
|
108
|
+
raise BootstrapError(f'Sequence {row_idx + 1:,} in {msa_file} has an empty header.')
|
|
109
|
+
seq_ids.append(fields[0])
|
|
110
|
+
pieces = []
|
|
111
|
+
else:
|
|
112
|
+
line = line.strip()
|
|
113
|
+
if line:
|
|
114
|
+
pieces.append(line)
|
|
115
|
+
store(row_idx, pieces)
|
|
116
|
+
|
|
117
|
+
if len(set(seq_ids)) != len(seq_ids):
|
|
118
|
+
raise BootstrapError(f'MSA contains duplicate sequence IDs: {msa_file}')
|
|
119
|
+
|
|
120
|
+
return seq_ids, msa
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def nucleotide_fraction(msa, max_rows=1000):
|
|
124
|
+
"""Estimate fraction of non-gap characters that are nucleotides."""
|
|
125
|
+
|
|
126
|
+
counts = np.bincount(msa[:max_rows].ravel(), minlength=256)
|
|
127
|
+
nt = sum(counts[c] for c in NT_CHARS)
|
|
128
|
+
non_gap = counts.sum() - counts[ord('-')] - counts[ord('.')]
|
|
129
|
+
return nt / non_gap if non_gap else 0.0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def replicate_seed(base_seed, rep_idx):
|
|
133
|
+
"""Seed sequence for a replicate (independent of worker scheduling)."""
|
|
134
|
+
|
|
135
|
+
return np.random.SeedSequence(entropy=base_seed, spawn_key=(rep_idx,))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def expected_replicate_size(headers, sample_len):
|
|
139
|
+
"""Exact size in bytes of a complete replicate MSA file."""
|
|
140
|
+
|
|
141
|
+
return sum(len(h) for h in headers) + len(headers) * (sample_len + 1)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def write_replicate(rep_idx, output_msa):
|
|
145
|
+
"""Write a single bootstrap replicate of the shared MSA.
|
|
146
|
+
|
|
147
|
+
Columns are sampled with replacement. The file is written to a temporary
|
|
148
|
+
path and atomically renamed, so an interrupted run never leaves a file
|
|
149
|
+
that looks complete.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
msa = _SHARED['msa']
|
|
153
|
+
headers = _SHARED['headers']
|
|
154
|
+
sample_len = _SHARED['sample_len']
|
|
155
|
+
|
|
156
|
+
rng = np.random.default_rng(replicate_seed(_SHARED['seed'], rep_idx))
|
|
157
|
+
cols = rng.integers(0, msa.shape[1], size=sample_len)
|
|
158
|
+
|
|
159
|
+
num_seqs = msa.shape[0]
|
|
160
|
+
block = np.empty((min(ROW_BLOCK_SIZE, num_seqs), sample_len + 1), dtype=np.uint8)
|
|
161
|
+
block[:, -1] = ord('\n')
|
|
162
|
+
|
|
163
|
+
tmp_file = output_msa + '.tmp'
|
|
164
|
+
with open(tmp_file, 'wb', buffering=8 * 1024 * 1024) as fout:
|
|
165
|
+
for start in range(0, num_seqs, ROW_BLOCK_SIZE):
|
|
166
|
+
end = min(start + ROW_BLOCK_SIZE, num_seqs)
|
|
167
|
+
out = block[:end - start]
|
|
168
|
+
np.take(msa[start:end], cols, axis=1, out=out[:, :-1], mode='clip')
|
|
169
|
+
parts = []
|
|
170
|
+
for i in range(end - start):
|
|
171
|
+
parts.append(headers[start + i])
|
|
172
|
+
parts.append(memoryview(out[i]))
|
|
173
|
+
fout.writelines(parts)
|
|
174
|
+
os.replace(tmp_file, output_msa)
|
|
175
|
+
|
|
176
|
+
return rep_idx
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class Bootstrap(object):
|
|
180
|
+
"""Assess robustness of genome tree by bootstrapping multiple sequence alignment."""
|
|
181
|
+
|
|
182
|
+
def __init__(self, cpus, multiple_processors=False):
|
|
183
|
+
"""Initialization.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
cpus : int
|
|
188
|
+
Number of cpus to use.
|
|
189
|
+
multiple_processors : bool
|
|
190
|
+
Use FastTreeMP instead of FastTree.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
self.logger = logging.getLogger()
|
|
194
|
+
|
|
195
|
+
self.cpus = cpus
|
|
196
|
+
self.mp = multiple_processors
|
|
197
|
+
|
|
198
|
+
def _infer_trees(self, msa_files, tree_files):
|
|
199
|
+
"""Infer trees for replicate MSAs in parallel."""
|
|
200
|
+
|
|
201
|
+
self.logger.info(f'Inferring {len(msa_files):,} bootstrap trees using {self.cpus:,} CPUs:')
|
|
202
|
+
infer_trees(msa_files, tree_files,
|
|
203
|
+
cpus=self.cpus,
|
|
204
|
+
base_type=self.base_type,
|
|
205
|
+
model=self.model,
|
|
206
|
+
gamma=self.gamma,
|
|
207
|
+
multithreaded=self.mp)
|
|
208
|
+
|
|
209
|
+
def _create_msa_replicates(self, msa_file, num_replicates, frac, seed):
|
|
210
|
+
"""Create bootstrap replicates of MSA in parallel."""
|
|
211
|
+
|
|
212
|
+
self.logger.info('Reading MSA:')
|
|
213
|
+
seq_ids, msa = read_msa(msa_file)
|
|
214
|
+
self.logger.info(' - read {:,} sequences with an alignment length of {:,}'.format(
|
|
215
|
+
msa.shape[0], msa.shape[1]))
|
|
216
|
+
|
|
217
|
+
if self.base_type == 'prot' and nucleotide_fraction(msa) > 0.9:
|
|
218
|
+
self.logger.warning('MSA appears to contain nucleotides, but bases are being treated '
|
|
219
|
+
'as amino acids. Use --base_type nt for nucleotide alignments.')
|
|
220
|
+
|
|
221
|
+
sample_len = int(msa.shape[1] * frac)
|
|
222
|
+
if sample_len < 1:
|
|
223
|
+
raise BootstrapError(f'Fraction {frac} of alignment results in no columns being sampled.')
|
|
224
|
+
|
|
225
|
+
headers = [b'>' + sid + b'\n' for sid in seq_ids]
|
|
226
|
+
expected_size = expected_replicate_size(headers, sample_len)
|
|
227
|
+
|
|
228
|
+
msa_files = []
|
|
229
|
+
todo = []
|
|
230
|
+
for rep_idx in range(num_replicates):
|
|
231
|
+
output_msa = os.path.join(self.replicate_dir, f'bootstrap_msa.r_{rep_idx}.fna')
|
|
232
|
+
msa_files.append(output_msa)
|
|
233
|
+
if os.path.exists(output_msa) and os.path.getsize(output_msa) == expected_size:
|
|
234
|
+
continue
|
|
235
|
+
elif os.path.exists(output_msa):
|
|
236
|
+
self.logger.warning(f'Recreating incomplete or mismatched replicate: {output_msa}')
|
|
237
|
+
todo.append((rep_idx, output_msa))
|
|
238
|
+
|
|
239
|
+
if len(todo) < num_replicates:
|
|
240
|
+
self.logger.info(f'Skipping {num_replicates - len(todo):,} MSA replicates which already exist.')
|
|
241
|
+
|
|
242
|
+
if todo:
|
|
243
|
+
self.logger.info('Creating {:,} MSA replicates of length {:,} using {:,} CPUs (seed = {}):'.format(
|
|
244
|
+
len(todo), sample_len, self.cpus, seed))
|
|
245
|
+
|
|
246
|
+
_SHARED.update(msa=msa, headers=headers, sample_len=sample_len, seed=seed)
|
|
247
|
+
try:
|
|
248
|
+
if self.cpus > 1 and len(todo) > 1:
|
|
249
|
+
# fork is used explicitly so workers share the MSA in memory
|
|
250
|
+
# (spawn/forkserver would pickle a full copy to each worker)
|
|
251
|
+
ctx = mp.get_context('fork')
|
|
252
|
+
with ctx.Pool(min(self.cpus, len(todo))) as pool:
|
|
253
|
+
for _ in tqdm(pool.imap_unordered(_write_replicate_task, todo),
|
|
254
|
+
total=len(todo), unit=' replicates'):
|
|
255
|
+
pass
|
|
256
|
+
else:
|
|
257
|
+
for task in tqdm(todo, unit=' replicates'):
|
|
258
|
+
_write_replicate_task(task)
|
|
259
|
+
finally:
|
|
260
|
+
_SHARED.clear()
|
|
261
|
+
|
|
262
|
+
return msa_files
|
|
263
|
+
|
|
264
|
+
def run(self,
|
|
265
|
+
input_tree,
|
|
266
|
+
msa_file,
|
|
267
|
+
num_replicates,
|
|
268
|
+
model,
|
|
269
|
+
gamma,
|
|
270
|
+
base_type,
|
|
271
|
+
frac,
|
|
272
|
+
boot_dir,
|
|
273
|
+
msa_only,
|
|
274
|
+
output_dir,
|
|
275
|
+
seed=None):
|
|
276
|
+
"""Bootstrap multiple sequence alignment.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
input_tree : str
|
|
281
|
+
Tree inferred with all data, or 'none'.
|
|
282
|
+
msa_file : str
|
|
283
|
+
File containing multiple sequence alignment for all taxa.
|
|
284
|
+
num_replicates : int
|
|
285
|
+
Number of replicates to perform.
|
|
286
|
+
model : str
|
|
287
|
+
Desired model of evolution.
|
|
288
|
+
gamma : bool
|
|
289
|
+
Use GAMMA model.
|
|
290
|
+
base_type : str
|
|
291
|
+
Indicates if bases are nucleotides or amino acids.
|
|
292
|
+
frac : float
|
|
293
|
+
Fraction of alignment to subsample.
|
|
294
|
+
boot_dir : str
|
|
295
|
+
Directory with pre-computed bootstrap trees or MSA replicates.
|
|
296
|
+
msa_only : bool
|
|
297
|
+
Only create MSA replicates.
|
|
298
|
+
output_dir : str
|
|
299
|
+
Directory for bootstrap trees.
|
|
300
|
+
seed : int
|
|
301
|
+
Seed for random number generator.
|
|
302
|
+
|
|
303
|
+
Returns
|
|
304
|
+
-------
|
|
305
|
+
str
|
|
306
|
+
Tree decorated with bootstrap support, or None.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
assert(model in ['wag', 'lg', 'jtt'])
|
|
310
|
+
assert(base_type in ['nt', 'prot'])
|
|
311
|
+
|
|
312
|
+
self.model = model
|
|
313
|
+
self.gamma = gamma
|
|
314
|
+
self.base_type = base_type
|
|
315
|
+
|
|
316
|
+
self.replicate_dir = os.path.join(output_dir, 'replicates')
|
|
317
|
+
make_sure_path_exists(self.replicate_dir)
|
|
318
|
+
|
|
319
|
+
rep_tree_files = []
|
|
320
|
+
try:
|
|
321
|
+
if not boot_dir:
|
|
322
|
+
if seed is None:
|
|
323
|
+
seed = np.random.SeedSequence().entropy
|
|
324
|
+
msa_files = self._create_msa_replicates(msa_file, num_replicates, frac, seed)
|
|
325
|
+
|
|
326
|
+
if msa_only:
|
|
327
|
+
self.logger.info(f'MSA replicates written to: {self.replicate_dir}')
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
rep_tree_files = [os.path.join(self.replicate_dir, f'bootstrap_tree.r_{i}.tree')
|
|
331
|
+
for i in range(num_replicates)]
|
|
332
|
+
self._infer_trees(msa_files, rep_tree_files)
|
|
333
|
+
else:
|
|
334
|
+
files = sorted(os.listdir(boot_dir))
|
|
335
|
+
rep_tree_files = [os.path.join(boot_dir, f) for f in files if f.endswith(TREE_EXTENSIONS)]
|
|
336
|
+
msa_files = [os.path.join(boot_dir, f) for f in files if f.endswith(MSA_EXTENSIONS)]
|
|
337
|
+
|
|
338
|
+
if rep_tree_files:
|
|
339
|
+
self.logger.info(f'Read {len(rep_tree_files):,} bootstrap trees from: {boot_dir}')
|
|
340
|
+
incomplete = [t for t in rep_tree_files if not is_complete_tree(t)]
|
|
341
|
+
if incomplete:
|
|
342
|
+
raise BootstrapError('Found {:,} empty or incomplete bootstrap trees, e.g.: {}'.format(
|
|
343
|
+
len(incomplete), incomplete[0]))
|
|
344
|
+
elif msa_files:
|
|
345
|
+
self.logger.info(f'Read {len(msa_files):,} MSA replicates from: {boot_dir}')
|
|
346
|
+
if msa_only:
|
|
347
|
+
self.logger.info('No trees inferred as --msa_only was specified.')
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
rep_tree_files = [os.path.join(self.replicate_dir, Path(m).stem + '.tree')
|
|
351
|
+
for m in msa_files]
|
|
352
|
+
self._infer_trees(msa_files, rep_tree_files)
|
|
353
|
+
else:
|
|
354
|
+
raise BootstrapError(f'No bootstrap trees or MSA replicates found in: {boot_dir}')
|
|
355
|
+
except (BootstrapError, FastTreeError) as e:
|
|
356
|
+
self.logger.error(str(e))
|
|
357
|
+
sys.exit(1)
|
|
358
|
+
|
|
359
|
+
# calculate support values
|
|
360
|
+
output_tree = None
|
|
361
|
+
if input_tree.lower() != 'none' and not msa_only:
|
|
362
|
+
self.logger.info(f'Calculating bootstrap support values from {len(rep_tree_files):,} trees.')
|
|
363
|
+
output_tree = os.path.join(output_dir, Path(input_tree).stem + '.bootstrap.tree')
|
|
364
|
+
bootstrap_merge_replicates(input_tree, output_tree, rep_tree_files, self.cpus)
|
|
365
|
+
|
|
366
|
+
return output_tree
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _write_replicate_task(task):
|
|
370
|
+
"""Pool entry point for writing a replicate."""
|
|
371
|
+
|
|
372
|
+
rep_idx, output_msa = task
|
|
373
|
+
return write_replicate(rep_idx, output_msa)
|