telometer 0.74__tar.gz → 0.76__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.74 → telometer-0.76}/PKG-INFO +1 -1
- {telometer-0.74 → telometer-0.76}/setup.py +1 -1
- telometer-0.76/telometer/telometer.py +163 -0
- {telometer-0.74 → telometer-0.76}/telometer.egg-info/PKG-INFO +1 -1
- telometer-0.74/telometer/telometer.py +0 -159
- {telometer-0.74 → telometer-0.76}/LICENSE.txt +0 -0
- {telometer-0.74 → telometer-0.76}/README.md +0 -0
- {telometer-0.74 → telometer-0.76}/setup.cfg +0 -0
- {telometer-0.74 → telometer-0.76}/telometer/__init__.py +0 -0
- {telometer-0.74 → telometer-0.76}/telometer.egg-info/SOURCES.txt +0 -0
- {telometer-0.74 → telometer-0.76}/telometer.egg-info/dependency_links.txt +0 -0
- {telometer-0.74 → telometer-0.76}/telometer.egg-info/entry_points.txt +0 -0
- {telometer-0.74 → telometer-0.76}/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: 0.76
|
|
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="0.76",
|
|
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,163 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Telometer v0.76
|
|
3
|
+
# Created by: Santiago E Sanchez
|
|
4
|
+
# Artandi Lab, Stanford University, 2023
|
|
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 re
|
|
10
|
+
import regex
|
|
11
|
+
import csv
|
|
12
|
+
import argparse
|
|
13
|
+
from multiprocessing import Pool, cpu_count
|
|
14
|
+
|
|
15
|
+
def reverse_complement(seq):
|
|
16
|
+
"""Returns the reverse complement of a DNA sequence."""
|
|
17
|
+
complement = {'A': 'T', 'C': 'G', 'G': 'C', 'T': 'A', 'N': 'N'}
|
|
18
|
+
return "".join(complement[base] for base in reversed(seq))
|
|
19
|
+
|
|
20
|
+
def get_adapters(chemistry):
|
|
21
|
+
"""Returns the adapter sequences based on the sequencing chemistry."""
|
|
22
|
+
if chemistry == 'r10':
|
|
23
|
+
adapters = ['TTTTTTTTCCTGTACTTCGTTCAGTTACGTATTGCT', 'GCAATACGTAACTGAACGAAGTACAGG']
|
|
24
|
+
else:
|
|
25
|
+
adapters = ['TTTTTTTTTTTAATGTACTTCGTTCAGTTACGTATTGCT', 'GCAATACGTAACTGAACGAAGT']
|
|
26
|
+
|
|
27
|
+
adapters_rc = [reverse_complement(adapter) for adapter in adapters]
|
|
28
|
+
return adapters + adapters_rc
|
|
29
|
+
|
|
30
|
+
def get_telomere_repeats():
|
|
31
|
+
"""Returns the telomere repeat sequences."""
|
|
32
|
+
telomere_repeats = ['GGCCA', 'CCCTAA', 'TTAGGG', 'CCCTGG', 'CTTCTT', 'TTAAAA', 'CCTGG']
|
|
33
|
+
telomere_repeats_rc = [reverse_complement(repeat) for repeat in telomere_repeats]
|
|
34
|
+
return telomere_repeats + telomere_repeats_rc
|
|
35
|
+
|
|
36
|
+
def find_initial_boundary_region(sequence, patterns, max_mismatches):
|
|
37
|
+
"""Finds the initial boundary region with allowed mismatches."""
|
|
38
|
+
boundary_length = 0
|
|
39
|
+
combined_pattern = '|'.join(f'({pattern})' for pattern in patterns)
|
|
40
|
+
regex_pattern = f'({combined_pattern}){{2,}}'
|
|
41
|
+
|
|
42
|
+
for match in regex.finditer(f'({regex_pattern}){{e<={max_mismatches}}}', sequence, regex.BESTMATCH):
|
|
43
|
+
boundary_length = max(boundary_length, len(match.group(0)))
|
|
44
|
+
return boundary_length
|
|
45
|
+
|
|
46
|
+
def process_read(args):
|
|
47
|
+
read_data, telomere_repeats_re, adapters, minreadlen = args
|
|
48
|
+
|
|
49
|
+
if read_data['is_unmapped'] or read_data['query_sequence'] is None or len(read_data['query_sequence']) < minreadlen:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
alignment_start = read_data['reference_start']
|
|
53
|
+
alignment_end = read_data['reference_end']
|
|
54
|
+
seq = read_data['query_sequence']
|
|
55
|
+
|
|
56
|
+
if read_data['is_reverse']:
|
|
57
|
+
direction = "rev"
|
|
58
|
+
seq = reverse_complement(seq)
|
|
59
|
+
else:
|
|
60
|
+
direction = "fwd"
|
|
61
|
+
|
|
62
|
+
reference_genome_length = read_data['reference_length']
|
|
63
|
+
|
|
64
|
+
if alignment_start >= 15000 and alignment_start <= reference_genome_length - 30000:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
if alignment_start < 15000 and "q" not in read_data['reference_name']:
|
|
68
|
+
arm = "p"
|
|
69
|
+
else:
|
|
70
|
+
arm = "q"
|
|
71
|
+
|
|
72
|
+
telomere_start = [m.start() for m in re.finditer(telomere_repeats_re, seq)]
|
|
73
|
+
if telomere_start:
|
|
74
|
+
telomere_start = telomere_start[0]
|
|
75
|
+
if telomere_start > 100 and (len(seq) - telomere_start > 200):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
telomere_end = min((seq.find(adapter) for adapter in adapters), default=-1)
|
|
79
|
+
if telomere_end == -1:
|
|
80
|
+
telomere_end = len(seq)
|
|
81
|
+
|
|
82
|
+
telomere_region = seq[telomere_start:telomere_end]
|
|
83
|
+
telomere_repeat = [m.group() for m in re.finditer('|'.join(telomere_repeats_re.split('|')), telomere_region)]
|
|
84
|
+
telomere_length = len(''.join(telomere_repeat))
|
|
85
|
+
|
|
86
|
+
boundary_mm1_length = find_initial_boundary_region(telomere_region, telomere_repeats_re.split('|'), max_mismatches=1)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
'chromosome': read_data['reference_name'],
|
|
90
|
+
'reference_start': alignment_start,
|
|
91
|
+
'reference_end': alignment_end,
|
|
92
|
+
'telomere_length': telomere_length,
|
|
93
|
+
'subtel_boundary_length': boundary_mm1_length,
|
|
94
|
+
'read_id': read_data['query_name'],
|
|
95
|
+
'mapping_quality': read_data['mapping_quality'],
|
|
96
|
+
'read_length': len(seq),
|
|
97
|
+
'arm': arm,
|
|
98
|
+
'direction': direction
|
|
99
|
+
}
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def calculate_telomere_length():
|
|
103
|
+
parser = argparse.ArgumentParser(description='Calculate telomere length from a BAM file.')
|
|
104
|
+
parser.add_argument('-b', '--bam', help='The path to the sorted BAM file.', required=True)
|
|
105
|
+
parser.add_argument('-o', '--output', help='The path to the output file.', required=True)
|
|
106
|
+
parser.add_argument('-c', '--chemistry', default="r10", help="Sequencing chemistry (r9 or r10, default=r10). Optional", required=False)
|
|
107
|
+
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)
|
|
108
|
+
args = parser.parse_args()
|
|
109
|
+
bam_file = pysam.AlignmentFile(args.bam, "rb")
|
|
110
|
+
|
|
111
|
+
adapters = get_adapters(args.chemistry)
|
|
112
|
+
telomere_repeats = get_telomere_repeats()
|
|
113
|
+
telomere_repeats_re = "|".join(f'({repeat}){{2,}}' for repeat in telomere_repeats)
|
|
114
|
+
|
|
115
|
+
read_data_list = [{
|
|
116
|
+
'query_name': read.query_name,
|
|
117
|
+
'is_unmapped': read.is_unmapped,
|
|
118
|
+
'is_reverse': read.is_reverse,
|
|
119
|
+
'reference_start': read.reference_start,
|
|
120
|
+
'reference_end': read.reference_end,
|
|
121
|
+
'reference_name': read.reference_name,
|
|
122
|
+
'mapping_quality': read.mapping_quality,
|
|
123
|
+
'query_sequence': read.query_sequence,
|
|
124
|
+
'reference_length': bam_file.get_reference_length(read.reference_name) if read.reference_name is not None else None
|
|
125
|
+
} for read in bam_file if read.reference_name is not None and read.reference_name != 'chrM']
|
|
126
|
+
|
|
127
|
+
with Pool(processes=cpu_count()) as pool:
|
|
128
|
+
results = pool.map(
|
|
129
|
+
process_read,
|
|
130
|
+
[(read_data, telomere_repeats_re, adapters, args.minreadlen) for read_data in read_data_list]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
results = [result for result in results if result]
|
|
134
|
+
|
|
135
|
+
# Ensure only the best result per read_id is saved
|
|
136
|
+
best_results = {}
|
|
137
|
+
for result in results:
|
|
138
|
+
read_id = result['read_id']
|
|
139
|
+
if read_id not in best_results:
|
|
140
|
+
best_results[read_id] = result
|
|
141
|
+
else:
|
|
142
|
+
existing_result = best_results[read_id]
|
|
143
|
+
if result['mapping_quality'] > existing_result['mapping_quality']:
|
|
144
|
+
best_results[read_id] = result
|
|
145
|
+
elif result['mapping_quality'] == existing_result['mapping_quality']:
|
|
146
|
+
if result['telomere_length'] > existing_result['telomere_length']:
|
|
147
|
+
best_results[read_id] = result
|
|
148
|
+
elif result['telomere_length'] == existing_result['telomere_length']:
|
|
149
|
+
# Arbitrarily keep the existing one if both telomere_length and mapping_quality are the same
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
final_results = list(best_results.values())
|
|
153
|
+
|
|
154
|
+
if final_results:
|
|
155
|
+
with open(args.output, 'w', newline='') as output_file:
|
|
156
|
+
writer = csv.DictWriter(output_file, fieldnames=final_results[0].keys(), delimiter='\t')
|
|
157
|
+
writer.writeheader()
|
|
158
|
+
writer.writerows(final_results)
|
|
159
|
+
|
|
160
|
+
print(f"Telometer completed successfully. Total telomeres measured: {len(final_results)}")
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
calculate_telomere_length()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: telometer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.76
|
|
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,159 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# Telometer v0.74
|
|
3
|
-
# Created by: Santiago E Sanchez
|
|
4
|
-
# Artandi Lab, Stanford University, 2023
|
|
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 re
|
|
10
|
-
import regex
|
|
11
|
-
import csv
|
|
12
|
-
import argparse
|
|
13
|
-
import pandas as pd
|
|
14
|
-
|
|
15
|
-
def reverse_complement(seq):
|
|
16
|
-
"""Returns the reverse complement of a DNA sequence."""
|
|
17
|
-
complement = {'A': 'T', 'C': 'G', 'G': 'C', 'T': 'A', 'N': 'N'}
|
|
18
|
-
return "".join(complement[base] for base in reversed(seq))
|
|
19
|
-
|
|
20
|
-
def get_adapters(chemistry):
|
|
21
|
-
"""Returns the adapter sequences based on the sequencing chemistry."""
|
|
22
|
-
if chemistry == 'r10':
|
|
23
|
-
adapters = ['TTTTTTTTCCTGTACTTCGTTCAGTTACGTATTGCT', 'GCAATACGTAACTGAACGAAGTACAGG']
|
|
24
|
-
else:
|
|
25
|
-
adapters = ['TTTTTTTTTTTAATGTACTTCGTTCAGTTACGTATTGCT', 'GCAATACGTAACTGAACGAAGT']
|
|
26
|
-
|
|
27
|
-
adapters_rc = [reverse_complement(adapter) for adapter in adapters]
|
|
28
|
-
return adapters + adapters_rc
|
|
29
|
-
|
|
30
|
-
def get_telomere_repeats():
|
|
31
|
-
"""Returns the telomere repeat sequences."""
|
|
32
|
-
telomere_repeats = ['GGCCA', 'CCCTAA', 'TTAGGG', 'CCCTGG', 'CTTCTT', 'TTAAAA', 'CCTGG']
|
|
33
|
-
telomere_repeats_rc = [reverse_complement(repeat) for repeat in telomere_repeats]
|
|
34
|
-
return telomere_repeats + telomere_repeats_rc
|
|
35
|
-
|
|
36
|
-
def find_initial_boundary_region(sequence, patterns, max_mismatches):
|
|
37
|
-
"""Finds the initial boundary region with allowed mismatches."""
|
|
38
|
-
boundary_length = 0
|
|
39
|
-
combined_pattern = '|'.join(f'({pattern})' for pattern in patterns)
|
|
40
|
-
regex_pattern = f'({combined_pattern}){{2,}}'
|
|
41
|
-
|
|
42
|
-
for match in regex.finditer(f'({regex_pattern}){{e<={max_mismatches}}}', sequence, regex.BESTMATCH):
|
|
43
|
-
boundary_length = max(boundary_length, len(match.group(0)))
|
|
44
|
-
return boundary_length
|
|
45
|
-
|
|
46
|
-
def extend_boundary_region(sequence, start, end, patterns, window_size, mismatch_threshold):
|
|
47
|
-
"""Extends the boundary region around the initial match, allowing some mismatches."""
|
|
48
|
-
extended_start = max(0, start - window_size)
|
|
49
|
-
extended_end = min(len(sequence), end + window_size)
|
|
50
|
-
extended_seq = sequence[extended_start:extended_end]
|
|
51
|
-
|
|
52
|
-
combined_pattern = ''.join(patterns)
|
|
53
|
-
mismatches = sum(1 for base in extended_seq if base not in combined_pattern)
|
|
54
|
-
|
|
55
|
-
if mismatches / len(extended_seq) <= mismatch_threshold:
|
|
56
|
-
return len(extended_seq) - (end - start) # Additional length
|
|
57
|
-
return 0
|
|
58
|
-
|
|
59
|
-
def calculate_telomere_length():
|
|
60
|
-
# required inputs: bam_file_path, output_file_path, chemistry
|
|
61
|
-
parser = argparse.ArgumentParser(description='Calculate telomere length from a BAM file.')
|
|
62
|
-
parser.add_argument('-b', '--bam', help='The path to the sorted BAM file.', required=True)
|
|
63
|
-
parser.add_argument('-o', '--output', help='The path to the output file.', required=True)
|
|
64
|
-
parser.add_argument('-c', '--chemistry', default="r10", help="Sequencing chemistry (r9 or r10, default=r10). Optional", required=False)
|
|
65
|
-
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)
|
|
66
|
-
args = parser.parse_args()
|
|
67
|
-
bam_file = pysam.AlignmentFile(args.bam, "rb")
|
|
68
|
-
|
|
69
|
-
adapters = get_adapters(args.chemistry)
|
|
70
|
-
telomere_repeats = get_telomere_repeats()
|
|
71
|
-
telomere_repeats_re = "|".join(f'({repeat}){{2,}}' for repeat in telomere_repeats)
|
|
72
|
-
|
|
73
|
-
highest_mapping_quality = {}
|
|
74
|
-
results = []
|
|
75
|
-
p_count = 0
|
|
76
|
-
q_count = 0
|
|
77
|
-
rev_count = 0
|
|
78
|
-
fwd_count = 0
|
|
79
|
-
p_tel = 0
|
|
80
|
-
q_tel = 0
|
|
81
|
-
|
|
82
|
-
for read in bam_file:
|
|
83
|
-
if read.is_unmapped or read.query_sequence is None or len(read.query_sequence) < args.minreadlen:
|
|
84
|
-
continue
|
|
85
|
-
|
|
86
|
-
alignment_start = read.reference_start
|
|
87
|
-
alignment_end = read.reference_end
|
|
88
|
-
seq = read.query_sequence
|
|
89
|
-
|
|
90
|
-
if read.is_reverse:
|
|
91
|
-
rev_count += 1
|
|
92
|
-
direction = "rev"
|
|
93
|
-
seq = reverse_complement(seq)
|
|
94
|
-
else:
|
|
95
|
-
fwd_count += 1
|
|
96
|
-
direction = "fwd"
|
|
97
|
-
|
|
98
|
-
reference_genome_length = bam_file.get_reference_length(read.reference_name)
|
|
99
|
-
|
|
100
|
-
if alignment_start < 15000 or alignment_start > reference_genome_length - 30000:
|
|
101
|
-
if alignment_start < 15000 and "q" not in read.reference_name:
|
|
102
|
-
arm = "p"
|
|
103
|
-
p_count += 1
|
|
104
|
-
else:
|
|
105
|
-
arm = "q"
|
|
106
|
-
q_count += 1
|
|
107
|
-
|
|
108
|
-
telomere_start = [m.start() for m in re.finditer(telomere_repeats_re, seq)]
|
|
109
|
-
if telomere_start:
|
|
110
|
-
telomere_start = telomere_start[0]
|
|
111
|
-
if telomere_start > 100 and (len(seq) - telomere_start > 200):
|
|
112
|
-
continue
|
|
113
|
-
|
|
114
|
-
telomere_end = min((seq.find(adapter) for adapter in adapters), default=-1)
|
|
115
|
-
if telomere_end == -1:
|
|
116
|
-
telomere_end = len(seq)
|
|
117
|
-
|
|
118
|
-
telomere_region = seq[telomere_start:telomere_end]
|
|
119
|
-
telomere_repeat = [m.group() for m in re.finditer('|'.join(telomere_repeats), telomere_region)]
|
|
120
|
-
telomere_length = len(''.join(telomere_repeat))
|
|
121
|
-
|
|
122
|
-
boundary_mm1_length = find_initial_boundary_region(telomere_region, telomere_repeats, max_mismatches=1)
|
|
123
|
-
|
|
124
|
-
if telomere_length < boundary_mm1_length:
|
|
125
|
-
telomere_length = boundary_mm1_length
|
|
126
|
-
if telomere_length < boundary_mm2_length:
|
|
127
|
-
telomere_length = boundary_mm2_length
|
|
128
|
-
|
|
129
|
-
if read.query_name not in highest_mapping_quality or read.mapping_quality > highest_mapping_quality[read.query_name]:
|
|
130
|
-
if arm == "p":
|
|
131
|
-
p_tel += 1
|
|
132
|
-
else:
|
|
133
|
-
q_tel += 1
|
|
134
|
-
highest_mapping_quality[read.query_name] = read.mapping_quality
|
|
135
|
-
results.append({
|
|
136
|
-
'chromosome': read.reference_name,
|
|
137
|
-
'reference_start': alignment_start,
|
|
138
|
-
'reference_end': alignment_end,
|
|
139
|
-
'telomere_length': telomere_length,
|
|
140
|
-
'subtel_boundary_length': boundary_mm1_length,
|
|
141
|
-
'read_id': read.query_name,
|
|
142
|
-
'mapping_quality': read.mapping_quality,
|
|
143
|
-
'read_length': len(seq),
|
|
144
|
-
'arm': arm,
|
|
145
|
-
'direction': direction
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
bam_file.close()
|
|
149
|
-
|
|
150
|
-
with open(args.output, 'w', newline='') as output_file:
|
|
151
|
-
writer = csv.DictWriter(output_file, fieldnames=results[0].keys(), delimiter='\t')
|
|
152
|
-
writer.writeheader()
|
|
153
|
-
writer.writerows(results)
|
|
154
|
-
|
|
155
|
-
# Print the total number of telomeres measured
|
|
156
|
-
print(f"Telometer completed successfully. Total telomeres measured: {len(results)}")
|
|
157
|
-
|
|
158
|
-
if __name__ == "__main__":
|
|
159
|
-
calculate_telomere_length()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|