telometer 0.95__tar.gz → 1.0__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.
- {telometer-0.95 → telometer-1.0}/PKG-INFO +1 -1
- {telometer-0.95 → telometer-1.0}/setup.py +1 -1
- telometer-1.0/telometer/__init__.py +34 -0
- telometer-1.0/telometer/telometer.py +292 -0
- {telometer-0.95 → telometer-1.0}/telometer.egg-info/PKG-INFO +1 -1
- telometer-0.95/telometer/__init__.py +0 -19
- telometer-0.95/telometer/telometer.py +0 -207
- {telometer-0.95 → telometer-1.0}/LICENSE.txt +0 -0
- {telometer-0.95 → telometer-1.0}/README.md +0 -0
- {telometer-0.95 → telometer-1.0}/setup.cfg +0 -0
- {telometer-0.95 → telometer-1.0}/telometer.egg-info/SOURCES.txt +0 -0
- {telometer-0.95 → telometer-1.0}/telometer.egg-info/dependency_links.txt +0 -0
- {telometer-0.95 → telometer-1.0}/telometer.egg-info/entry_points.txt +0 -0
- {telometer-0.95 → telometer-1.0}/telometer.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: telometer
|
|
3
|
-
Version: 0
|
|
3
|
+
Version: 1.0
|
|
4
4
|
Summary: a simple regular expression based method for measuring individual, chromosome-specific telomere lengths from long-read sequencing data
|
|
5
5
|
Author: Santiago E Sanchez
|
|
6
6
|
Author-email: ses94@stanford.edu
|
|
@@ -2,7 +2,7 @@ import setuptools
|
|
|
2
2
|
|
|
3
3
|
setuptools.setup(
|
|
4
4
|
name="telometer",
|
|
5
|
-
version="0
|
|
5
|
+
version="1.0",
|
|
6
6
|
author="Santiago E Sanchez",
|
|
7
7
|
author_email="ses94@stanford.edu",
|
|
8
8
|
description="a simple regular expression based method for measuring individual, chromosome-specific telomere lengths from long-read sequencing data",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
__version__ = '1.0'
|
|
2
|
+
__author__ = 'Santiago E Sanchez'
|
|
3
|
+
__email__ = 'santy.esanchez@gmail.com'
|
|
4
|
+
__license__ = 'MIT'
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from .telometer import (
|
|
8
|
+
process_bam_file,
|
|
9
|
+
measure_telomere_length,
|
|
10
|
+
identify_telomere_regions,
|
|
11
|
+
get_telomere_repeats,
|
|
12
|
+
reverse_complement,
|
|
13
|
+
process_read_wrapper,
|
|
14
|
+
process_read,
|
|
15
|
+
detect_discontinuities,
|
|
16
|
+
check_gap,
|
|
17
|
+
run_telometer
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
'process_bam_file',
|
|
24
|
+
'measure_telomere_length',
|
|
25
|
+
'identify_telomere_regions',
|
|
26
|
+
'get_telomere_repeats',
|
|
27
|
+
'reverse_complement',
|
|
28
|
+
'process_read_wrapper',
|
|
29
|
+
'process_read',
|
|
30
|
+
'detect_discontinuities',
|
|
31
|
+
'check_gap',
|
|
32
|
+
'run_telometer'
|
|
33
|
+
]
|
|
34
|
+
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Telometer v1.0
|
|
3
|
+
# Created by: Santiago E Sanchez
|
|
4
|
+
# Artandi Lab, Stanford University, 2024
|
|
5
|
+
# Measures telomeres from ONT or PacBio long reads aligned to a T2T genome assembly
|
|
6
|
+
# Simple Usage: telometer -b sorted_t2t.bam -o output.tsv
|
|
7
|
+
|
|
8
|
+
import pysam
|
|
9
|
+
import regex as re
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import time
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
from multiprocessing import Pool, cpu_count
|
|
15
|
+
import numpy as np
|
|
16
|
+
from scipy.signal import savgol_filter
|
|
17
|
+
|
|
18
|
+
def reverse_complement(seq):
|
|
19
|
+
complement = {'A': 'T', 'C': 'G', 'G': 'C', 'T': 'A', 'N': 'N'}
|
|
20
|
+
return "".join(complement[base] for base in reversed(seq))
|
|
21
|
+
|
|
22
|
+
def get_telomere_repeats():
|
|
23
|
+
telomere_repeats = ['CCCTAA', 'TTAGGG']
|
|
24
|
+
telomere_repeats_rc = [reverse_complement(repeat) for repeat in telomere_repeats]
|
|
25
|
+
return telomere_repeats + telomere_repeats_rc
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def identify_telomere_regions(seq, base_qualities, telomere_motifs, window_size=120, step_size=12,
|
|
29
|
+
density_threshold=0.1, max_gap_length=250, quality_threshold=15):
|
|
30
|
+
telomere_regions = []
|
|
31
|
+
current_start = None
|
|
32
|
+
gap_length = 0
|
|
33
|
+
has_large_gap = False
|
|
34
|
+
densities = []
|
|
35
|
+
|
|
36
|
+
combined_motif_pattern = '|'.join(f'({motif})' for motif in telomere_motifs)
|
|
37
|
+
|
|
38
|
+
for i in range(0, len(seq) - window_size + 1, step_size):
|
|
39
|
+
window_seq = seq[i:i + window_size]
|
|
40
|
+
motif_count = len(re.findall(combined_motif_pattern, window_seq))
|
|
41
|
+
density = motif_count / (window_size / len(telomere_motifs[0])) # Assuming all motifs have similar lengths
|
|
42
|
+
densities.append(density)
|
|
43
|
+
|
|
44
|
+
if density >= density_threshold:
|
|
45
|
+
if current_start is None:
|
|
46
|
+
current_start = i
|
|
47
|
+
gap_length = 0
|
|
48
|
+
else:
|
|
49
|
+
if current_start is not None:
|
|
50
|
+
gap_length += step_size
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
gap_start = i - gap_length
|
|
54
|
+
gap_end = i
|
|
55
|
+
gap_quality = base_qualities[gap_start:gap_end]
|
|
56
|
+
|
|
57
|
+
if len(gap_quality) > 0:
|
|
58
|
+
avg_gap_quality = sum(gap_quality) / len(gap_quality)
|
|
59
|
+
else:
|
|
60
|
+
avg_gap_quality = 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if avg_gap_quality < quality_threshold:
|
|
64
|
+
gap_length = 0
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if gap_length > max_gap_length:
|
|
69
|
+
|
|
70
|
+
mismatch_motif_count = len(re.findall(f"({combined_motif_pattern}){{e<=1}}", window_seq))
|
|
71
|
+
mismatch_density = mismatch_motif_count / (window_size / len(telomere_motifs[0]))
|
|
72
|
+
|
|
73
|
+
if mismatch_density >= density_threshold:
|
|
74
|
+
|
|
75
|
+
gap_length = 0
|
|
76
|
+
else:
|
|
77
|
+
has_large_gap = True
|
|
78
|
+
telomere_regions.append((current_start, i - gap_length + step_size))
|
|
79
|
+
current_start = None
|
|
80
|
+
gap_length = 0
|
|
81
|
+
|
|
82
|
+
if current_start is not None:
|
|
83
|
+
telomere_regions.append((current_start, len(seq)))
|
|
84
|
+
|
|
85
|
+
return telomere_regions, densities, has_large_gap, combined_motif_pattern
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def detect_discontinuities(base_qualities, window_length=11, polyorder=2, gradient_threshold=-1):
|
|
89
|
+
|
|
90
|
+
if len(base_qualities) < window_length:
|
|
91
|
+
window_length = len(base_qualities) if len(base_qualities) % 2 == 1 else len(base_qualities) - 1 # Window length must be odd
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if len(base_qualities) > polyorder:
|
|
95
|
+
smoothed_qualities = savgol_filter(base_qualities, window_length=window_length, polyorder=polyorder)
|
|
96
|
+
else:
|
|
97
|
+
|
|
98
|
+
smoothed_qualities = base_qualities
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
gradient = np.diff(smoothed_qualities)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
discontinuities = np.where(gradient < gradient_threshold)[0]
|
|
105
|
+
|
|
106
|
+
return discontinuities, smoothed_qualities
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def check_gap(seq, base_qualities, telomere_regions, gradient_threshold=-1, quality_threshold=15):
|
|
110
|
+
merged_regions = []
|
|
111
|
+
prev_end = None
|
|
112
|
+
current_start = None
|
|
113
|
+
|
|
114
|
+
for i, (start, end) in enumerate(telomere_regions):
|
|
115
|
+
if current_start is None:
|
|
116
|
+
current_start = start
|
|
117
|
+
|
|
118
|
+
if prev_end is not None:
|
|
119
|
+
gap_start = prev_end
|
|
120
|
+
gap_end = start
|
|
121
|
+
gap_quality = base_qualities[gap_start:gap_end]
|
|
122
|
+
discontinuities, smoothed_qualities = detect_discontinuities(gap_quality, gradient_threshold=gradient_threshold)
|
|
123
|
+
avg_gap_quality = sum(gap_quality) / len(gap_quality) if len(gap_quality) > 0 else 0
|
|
124
|
+
if avg_gap_quality < quality_threshold or len(discontinuities) == 0:
|
|
125
|
+
continue
|
|
126
|
+
merged_regions.append((current_start, end))
|
|
127
|
+
current_start = None
|
|
128
|
+
prev_end = end
|
|
129
|
+
|
|
130
|
+
return merged_regions
|
|
131
|
+
|
|
132
|
+
def measure_telomere_length(seq, telomere_motifs, base_qualities, window_size=120, step_size=12, density_threshold=0.1, max_gap_length=150, quality_threshold=15):
|
|
133
|
+
telomere_regions, densities, has_large_gap, combined_motif_pattern = identify_telomere_regions(
|
|
134
|
+
seq, base_qualities, telomere_motifs, window_size, step_size, density_threshold, max_gap_length, quality_threshold)
|
|
135
|
+
|
|
136
|
+
if not telomere_regions:
|
|
137
|
+
return 0, 0, 0, densities, has_large_gap, combined_motif_pattern
|
|
138
|
+
|
|
139
|
+
merged_telomere_regions = check_gap(seq, base_qualities, telomere_regions, gradient_threshold=-1, quality_threshold=quality_threshold)
|
|
140
|
+
terminus_regions = [region for region in merged_telomere_regions if region[0] < 100 or region[1] > len(seq) - 100]
|
|
141
|
+
|
|
142
|
+
if not terminus_regions:
|
|
143
|
+
return 0, 0, 0, densities, has_large_gap, combined_motif_pattern
|
|
144
|
+
|
|
145
|
+
telomere_start, telomere_end = terminus_regions[0]
|
|
146
|
+
telomere_length = telomere_end - telomere_start
|
|
147
|
+
return telomere_start, telomere_end, telomere_length, densities, has_large_gap, combined_motif_pattern
|
|
148
|
+
|
|
149
|
+
# New function to process a single read
|
|
150
|
+
def process_read(read_data, telomere_motifs, max_gap_length, min_read_len):
|
|
151
|
+
if read_data['is_unmapped'] or read_data['reference_name'] == 'chrM':
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
if read_data['reference_start'] > 30000 and read_data['reference_end'] < read_data['reference_length'] - 30000:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
seq = read_data['query_sequence']
|
|
158
|
+
base_qualities = read_data['query_qualities'] # Extract base qualities here
|
|
159
|
+
if seq is None or len(seq) < min_read_len:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
alignment_start = read_data['reference_start']
|
|
163
|
+
alignment_end = read_data['reference_end']
|
|
164
|
+
reference_genome_length = read_data['reference_length']
|
|
165
|
+
|
|
166
|
+
if alignment_start < 15000 and alignment_end <= reference_genome_length - 30000:
|
|
167
|
+
arm = "p"
|
|
168
|
+
else:
|
|
169
|
+
arm = "q"
|
|
170
|
+
|
|
171
|
+
direction = "rev" if read_data['is_reverse'] else "fwd"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
telomere_start, telomere_end, telomere_length, densities, has_large_gap, combined_motif_pattern = measure_telomere_length(
|
|
175
|
+
seq, telomere_motifs, base_qualities, max_gap_length=max_gap_length)
|
|
176
|
+
|
|
177
|
+
if telomere_length < 1:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
'chromosome': read_data['reference_name'],
|
|
182
|
+
'reference_start': alignment_start,
|
|
183
|
+
'reference_end': alignment_end,
|
|
184
|
+
'telomere_length': telomere_length,
|
|
185
|
+
'read_id': read_data['query_name'],
|
|
186
|
+
'mapping_quality': read_data['mapping_quality'],
|
|
187
|
+
'read_length': len(seq),
|
|
188
|
+
'arm': arm,
|
|
189
|
+
'direction': direction
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def process_read_wrapper(args):
|
|
194
|
+
return process_read(*args)
|
|
195
|
+
|
|
196
|
+
def estimate_memory_usage(reads_list):
|
|
197
|
+
"""
|
|
198
|
+
Estimate the memory usage of the reads in bytes.
|
|
199
|
+
Approximate size based on the size of sequences, base qualities, and metadata.
|
|
200
|
+
"""
|
|
201
|
+
memory_usage = 0
|
|
202
|
+
for read in reads_list:
|
|
203
|
+
seq_len = len(read['query_sequence']) if read['query_sequence'] else 0
|
|
204
|
+
quality_len = len(read['query_qualities']) if read['query_qualities'] else 0
|
|
205
|
+
memory_usage += sys.getsizeof(read['query_name']) + seq_len + quality_len + sys.getsizeof(read)
|
|
206
|
+
return memory_usage
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def process_bam_file(bam_file_path, output_file_path, max_gap_length=150, min_read_len=1000, num_processes=8, memory_limit_gb=8):
|
|
210
|
+
start_time = time.time()
|
|
211
|
+
bam_file = pysam.AlignmentFile(bam_file_path, "rb")
|
|
212
|
+
telomere_motifs = get_telomere_repeats()
|
|
213
|
+
|
|
214
|
+
results = []
|
|
215
|
+
read_results = {}
|
|
216
|
+
total_reads = 0
|
|
217
|
+
memory_limit_bytes = memory_limit_gb * (1024 ** 3)
|
|
218
|
+
|
|
219
|
+
batch = []
|
|
220
|
+
batch_memory = 0
|
|
221
|
+
|
|
222
|
+
def process_batch(batch_data):
|
|
223
|
+
nonlocal total_reads, read_results
|
|
224
|
+
|
|
225
|
+
with Pool(processes=num_processes) as pool:
|
|
226
|
+
for result in pool.imap_unordered(process_read_wrapper, [(rd, telomere_motifs, max_gap_length, min_read_len) for rd in batch_data]):
|
|
227
|
+
if result:
|
|
228
|
+
total_reads += 1
|
|
229
|
+
existing_result = read_results.get(result['read_id'])
|
|
230
|
+
if existing_result:
|
|
231
|
+
if (result['mapping_quality'] > existing_result['mapping_quality'] or
|
|
232
|
+
(result['mapping_quality'] == existing_result['mapping_quality'] and
|
|
233
|
+
result['telomere_length'] > existing_result['telomere_length'])):
|
|
234
|
+
read_results[result['read_id']] = result
|
|
235
|
+
else:
|
|
236
|
+
read_results[result['read_id']] = result
|
|
237
|
+
|
|
238
|
+
batch_data.clear()
|
|
239
|
+
|
|
240
|
+
for read in bam_file:
|
|
241
|
+
if read.reference_name is not None and read.reference_name != 'chrM':
|
|
242
|
+
read_data = {
|
|
243
|
+
'query_name': read.query_name,
|
|
244
|
+
'is_unmapped': read.is_unmapped,
|
|
245
|
+
'is_reverse': read.is_reverse,
|
|
246
|
+
'reference_start': read.reference_start,
|
|
247
|
+
'reference_end': read.reference_end,
|
|
248
|
+
'reference_name': read.reference_name,
|
|
249
|
+
'mapping_quality': read.mapping_quality,
|
|
250
|
+
'query_sequence': read.query_sequence,
|
|
251
|
+
'query_qualities': read.query_qualities,
|
|
252
|
+
'reference_length': bam_file.get_reference_length(read.reference_name) if read.reference_name is not None else None
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
read_memory = estimate_memory_usage([read_data])
|
|
257
|
+
batch_memory += read_memory
|
|
258
|
+
batch.append(read_data)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if batch_memory >= memory_limit_bytes:
|
|
262
|
+
print(f"Processing batch of {len(batch)} reads, approx memory used: {batch_memory / (1024 ** 3):.2f} GB")
|
|
263
|
+
process_batch(batch)
|
|
264
|
+
batch_memory = 0
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
if batch:
|
|
268
|
+
print(f"Processing final batch of {len(batch)} reads")
|
|
269
|
+
process_batch(batch)
|
|
270
|
+
|
|
271
|
+
bam_file.close()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
results_df = pd.DataFrame(list(read_results.values()))
|
|
275
|
+
results_df.to_csv(output_file_path, sep='\t', index=False)
|
|
276
|
+
|
|
277
|
+
print(f"Telometer completed successfully. Total telomeres measured: {len(read_results)}")
|
|
278
|
+
print(f"Total processing time: {time.time() - start_time:.2f} seconds")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def run_telometer():
|
|
282
|
+
parser = argparse.ArgumentParser(description='Calculate telomere length from a BAM file.')
|
|
283
|
+
parser.add_argument('-b', '--bam', help='The path to the sorted BAM file.', required=True)
|
|
284
|
+
parser.add_argument('-o', '--output', help='The path to the output file.', required=True)
|
|
285
|
+
parser.add_argument('-m', '--minreadlen', default=1000, type=int, help='Minimum read length to consider (Default: 1000 for telomere capture, use 4000 for WGS). Optional', required=False)
|
|
286
|
+
parser.add_argument('-g', '--maxgaplen', default=250, type=int, help='Maximum allowed gap length between telomere regions. Optional', required=False)
|
|
287
|
+
parser.add_argument('-t', '--threads', default=cpu_count(), type=int, help='Number of processing threads to use. Optional', required=False)
|
|
288
|
+
args = parser.parse_args()
|
|
289
|
+
process_bam_file(args.bam, args.output, max_gap_length=args.maxgaplen, min_read_len=args.minreadlen, num_processes=args.threads)
|
|
290
|
+
|
|
291
|
+
if __name__ == "__main__":
|
|
292
|
+
run_telometer()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: telometer
|
|
3
|
-
Version: 0
|
|
3
|
+
Version: 1.0
|
|
4
4
|
Summary: a simple regular expression based method for measuring individual, chromosome-specific telomere lengths from long-read sequencing data
|
|
5
5
|
Author: Santiago E Sanchez
|
|
6
6
|
Author-email: ses94@stanford.edu
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
__version__ = '0.95'
|
|
3
|
-
__author__ = 'Santiago E Sanchez'
|
|
4
|
-
__email__ = 'santy.esanchez@gmail.com'
|
|
5
|
-
__license__ = 'MIT'
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
from .telometer import (
|
|
9
|
-
process_bam_file,
|
|
10
|
-
measure_telomere_length,
|
|
11
|
-
identify_telomere_regions,
|
|
12
|
-
get_telomere_repeats,
|
|
13
|
-
reverse_complement,
|
|
14
|
-
process_read_wrapper,
|
|
15
|
-
process_read,
|
|
16
|
-
run_telometer
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# Telometer v0.9
|
|
3
|
-
# Created by: Santiago E Sanchez
|
|
4
|
-
# Artandi Lab, Stanford University, 2024
|
|
5
|
-
# Measures telomeres from ONT or PacBio long reads aligned to a T2T genome assembly
|
|
6
|
-
# Simple Usage: telometer -b sorted_t2t.bam -o output.tsv
|
|
7
|
-
|
|
8
|
-
import pysam
|
|
9
|
-
import regex as re
|
|
10
|
-
import pandas as pd
|
|
11
|
-
import time
|
|
12
|
-
import argparse
|
|
13
|
-
from multiprocessing import Pool, cpu_count
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def reverse_complement(seq):
|
|
17
|
-
complement = {'A': 'T', 'C': 'G', 'G': 'C', 'T': 'A', 'N': 'N'}
|
|
18
|
-
return "".join(complement[base] for base in reversed(seq))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def get_telomere_repeats():
|
|
22
|
-
telomere_repeats = ['CCCTAA', 'TTAGGG']
|
|
23
|
-
telomere_repeats_rc = [reverse_complement(repeat) for repeat in telomere_repeats]
|
|
24
|
-
return telomere_repeats + telomere_repeats_rc
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def identify_telomere_regions(seq, telomere_motifs, window_size=100, step_size=10, density_threshold=0.1, max_gap_length=50):
|
|
28
|
-
telomere_regions = []
|
|
29
|
-
current_start = None
|
|
30
|
-
gap_length = 0
|
|
31
|
-
has_large_gap = False
|
|
32
|
-
densities = []
|
|
33
|
-
|
|
34
|
-
combined_motif_pattern = '|'.join(f'({motif})' for motif in telomere_motifs)
|
|
35
|
-
|
|
36
|
-
for i in range(0, len(seq) - window_size + 1, step_size):
|
|
37
|
-
window_seq = seq[i:i + window_size]
|
|
38
|
-
motif_count = len(re.findall(combined_motif_pattern, window_seq))
|
|
39
|
-
density = motif_count / (window_size / len(telomere_motifs[0])) # Assuming all motifs have similar lengths
|
|
40
|
-
densities.append(density)
|
|
41
|
-
|
|
42
|
-
if density >= density_threshold:
|
|
43
|
-
if current_start is None:
|
|
44
|
-
current_start = i
|
|
45
|
-
gap_length = 0
|
|
46
|
-
else:
|
|
47
|
-
if current_start is not None:
|
|
48
|
-
gap_length += step_size
|
|
49
|
-
if gap_length > max_gap_length:
|
|
50
|
-
|
|
51
|
-
mismatch_motif_count = len(re.findall(f"({combined_motif_pattern}){{e<=1}}", window_seq))
|
|
52
|
-
mismatch_density = mismatch_motif_count / (window_size / len(telomere_motifs[0]))
|
|
53
|
-
|
|
54
|
-
if mismatch_density >= density_threshold:
|
|
55
|
-
|
|
56
|
-
gap_length = 0
|
|
57
|
-
else:
|
|
58
|
-
has_large_gap = True
|
|
59
|
-
telomere_regions.append((current_start, i - gap_length + step_size))
|
|
60
|
-
current_start = None
|
|
61
|
-
gap_length = 0
|
|
62
|
-
|
|
63
|
-
if current_start is not None:
|
|
64
|
-
telomere_regions.append((current_start, len(seq)))
|
|
65
|
-
|
|
66
|
-
return telomere_regions, densities, has_large_gap, combined_motif_pattern
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def measure_telomere_length(seq, telomere_motifs, window_size=100, step_size=10, density_threshold=0.1, max_gap_length=50):
|
|
70
|
-
telomere_regions, densities, has_large_gap, combined_motif_pattern = identify_telomere_regions(seq, telomere_motifs, window_size, step_size, density_threshold, max_gap_length)
|
|
71
|
-
|
|
72
|
-
if not telomere_regions:
|
|
73
|
-
return 0, 0, 0, densities, has_large_gap, combined_motif_pattern
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
terminus_regions = []
|
|
77
|
-
for start, end in telomere_regions:
|
|
78
|
-
if start < 100 or end > len(seq) - 100:
|
|
79
|
-
terminus_regions.append((start, end))
|
|
80
|
-
|
|
81
|
-
if not terminus_regions:
|
|
82
|
-
return 0, 0, 0, densities, has_large_gap, combined_motif_pattern
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
telomere_start, telomere_end = terminus_regions[0]
|
|
86
|
-
telomere_length = telomere_end - telomere_start
|
|
87
|
-
|
|
88
|
-
return telomere_start, telomere_end, telomere_length, densities, has_large_gap, combined_motif_pattern
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def process_read(read_data, telomere_motifs, max_gap_length, min_read_len):
|
|
92
|
-
if read_data['is_unmapped'] or read_data['reference_name'] == 'chrM':
|
|
93
|
-
return None
|
|
94
|
-
|
|
95
|
-
if read_data['reference_start'] > 30000 and read_data['reference_end'] < read_data['reference_length'] - 30000:
|
|
96
|
-
return None
|
|
97
|
-
|
|
98
|
-
seq = read_data['query_sequence']
|
|
99
|
-
if seq is None or len(seq) < min_read_len:
|
|
100
|
-
return None
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
alignment_start = read_data['reference_start']
|
|
104
|
-
alignment_end = read_data['reference_end']
|
|
105
|
-
reference_genome_length = read_data['reference_length']
|
|
106
|
-
|
|
107
|
-
if alignment_start < 15000 and alignment_end <= reference_genome_length - 30000:
|
|
108
|
-
arm = "p"
|
|
109
|
-
else:
|
|
110
|
-
arm = "q"
|
|
111
|
-
|
|
112
|
-
direction = "rev" if read_data['is_reverse'] else "fwd"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
telomere_start, telomere_end, telomere_length, densities, has_large_gap, combined_motif_pattern = measure_telomere_length(seq, telomere_motifs, max_gap_length=max_gap_length)
|
|
116
|
-
if telomere_length < 1:
|
|
117
|
-
return None
|
|
118
|
-
return {
|
|
119
|
-
'chromosome': read_data['reference_name'],
|
|
120
|
-
'reference_start': alignment_start,
|
|
121
|
-
'reference_end': alignment_end,
|
|
122
|
-
'telomere_length': telomere_length,
|
|
123
|
-
#'subtel_boundary_length': telomere_end - telomere_start,
|
|
124
|
-
'read_id': read_data['query_name'],
|
|
125
|
-
'mapping_quality': read_data['mapping_quality'],
|
|
126
|
-
'read_length': len(seq),
|
|
127
|
-
'arm': arm,
|
|
128
|
-
'direction': direction
|
|
129
|
-
#'has_large_gap': has_large_gap
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def process_read_wrapper(args):
|
|
134
|
-
return process_read(*args)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def process_bam_file(bam_file_path, output_file_path, max_gap_length=100, min_read_len=1000, num_processes=8):
|
|
138
|
-
start_time = time.time()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
bam_file = pysam.AlignmentFile(bam_file_path, "rb")
|
|
142
|
-
|
|
143
|
-
telomere_motifs = get_telomere_repeats()
|
|
144
|
-
|
|
145
|
-
results = []
|
|
146
|
-
read_results = {}
|
|
147
|
-
total_reads = 0
|
|
148
|
-
large_gap_count = 0
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
read_data_list = [{
|
|
152
|
-
'query_name': read.query_name,
|
|
153
|
-
'is_unmapped': read.is_unmapped,
|
|
154
|
-
'is_reverse': read.is_reverse,
|
|
155
|
-
'reference_start': read.reference_start,
|
|
156
|
-
'reference_end': read.reference_end,
|
|
157
|
-
'reference_name': read.reference_name,
|
|
158
|
-
'mapping_quality': read.mapping_quality,
|
|
159
|
-
'query_sequence': read.query_sequence,
|
|
160
|
-
'reference_length': bam_file.get_reference_length(read.reference_name) if read.reference_name is not None else None
|
|
161
|
-
} for read in bam_file if read.reference_name is not None and read.reference_name != 'chrM']
|
|
162
|
-
|
|
163
|
-
# Use multiprocessing Pool for parallel processing
|
|
164
|
-
with Pool(processes=num_processes) as pool:
|
|
165
|
-
for result in pool.imap_unordered(process_read_wrapper, [(rd, telomere_motifs, max_gap_length, min_read_len) for rd in read_data_list]):
|
|
166
|
-
if result:
|
|
167
|
-
total_reads += 1
|
|
168
|
-
#if result['has_large_gap']:
|
|
169
|
-
# large_gap_count += 1
|
|
170
|
-
|
|
171
|
-
# Check for existing results with the same read_id
|
|
172
|
-
existing_result = read_results.get(result['read_id'])
|
|
173
|
-
if existing_result:
|
|
174
|
-
if (result['mapping_quality'] > existing_result['mapping_quality'] or
|
|
175
|
-
(result['mapping_quality'] == existing_result['mapping_quality'] and
|
|
176
|
-
result['telomere_length'] > existing_result['telomere_length'])):
|
|
177
|
-
read_results[result['read_id']] = result
|
|
178
|
-
else:
|
|
179
|
-
read_results[result['read_id']] = result
|
|
180
|
-
|
|
181
|
-
bam_file.close()
|
|
182
|
-
|
|
183
|
-
# Convert results to a DataFrame and save to a TSV file
|
|
184
|
-
results_df = pd.DataFrame(list(read_results.values()))
|
|
185
|
-
results_df.to_csv(output_file_path, sep='\t', index=False)
|
|
186
|
-
|
|
187
|
-
# Calculate and print the percentage of telomeres with gaps larger than max_gap_length
|
|
188
|
-
#if total_reads > 0:
|
|
189
|
-
# large_gap_percentage = (large_gap_count / total_reads) * 100
|
|
190
|
-
#else:
|
|
191
|
-
# large_gap_percentage = 0
|
|
192
|
-
|
|
193
|
-
print(f"Telometer completed successfully. Total telomeres measured: {len(read_results)}")
|
|
194
|
-
print(f"Total processing time: {time.time() - start_time:.2f} seconds")
|
|
195
|
-
|
|
196
|
-
def run_telometer():
|
|
197
|
-
parser = argparse.ArgumentParser(description='Calculate telomere length from a BAM file.')
|
|
198
|
-
parser.add_argument('-b', '--bam', help='The path to the sorted BAM file.', required=True)
|
|
199
|
-
parser.add_argument('-o', '--output', help='The path to the output file.', required=True)
|
|
200
|
-
parser.add_argument('-m', '--minreadlen', default=1000, type=int, help='Minimum read length to consider (Default: 1000 for telomere capture, use 4000 for WGS). Optional', required=False)
|
|
201
|
-
parser.add_argument('-g', '--maxgaplen', default=100, type=int, help='Maximum allowed gap length between telomere regions. Optional', required=False)
|
|
202
|
-
parser.add_argument('-t', '--threads', default=cpu_count(), type=int, help='Number of processing threads to use. Optional', required=False)
|
|
203
|
-
args = parser.parse_args()
|
|
204
|
-
process_bam_file(args.bam, args.output, max_gap_length=args.maxgaplen, min_read_len=args.minreadlen, num_processes=args.threads)
|
|
205
|
-
|
|
206
|
-
if __name__ == "__main__":
|
|
207
|
-
run_telometer()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|