telometer 0.80__tar.gz → 0.90__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: telometer
3
- Version: 0.80
3
+ Version: 0.90
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.80",
5
+ version="0.90",
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",
@@ -17,7 +17,7 @@ setuptools.setup(
17
17
  python_requires='>=3.7',
18
18
  entry_points={
19
19
  'console_scripts': [
20
- 'telometer=telometer:calculate_telomere_length', # 'telometer' is the command, 'telometer:main' means the main function in telometer.py
20
+ 'telometer=telometer:process_bam_file', # 'telometer' is the command, 'telometer:main' means the main function in telometer.py
21
21
  ],
22
22
  },
23
23
  )
@@ -0,0 +1,207 @@
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
+ from multiprocessing import Pool, cpu_count
13
+
14
+
15
+ def reverse_complement(seq):
16
+ complement = {'A': 'T', 'C': 'G', 'G': 'C', 'T': 'A', 'N': 'N'}
17
+ return "".join(complement[base] for base in reversed(seq))
18
+
19
+
20
+ def get_telomere_repeats():
21
+ telomere_repeats = ['CCCTAA', 'TTAGGG']
22
+ telomere_repeats_rc = [reverse_complement(repeat) for repeat in telomere_repeats]
23
+ return telomere_repeats + telomere_repeats_rc
24
+
25
+
26
+ def identify_telomere_regions(seq, telomere_motifs, window_size=100, step_size=10, density_threshold=0.1, max_gap_length=50):
27
+ telomere_regions = []
28
+ current_start = None
29
+ gap_length = 0
30
+ has_large_gap = False
31
+ densities = []
32
+
33
+ combined_motif_pattern = '|'.join(f'({motif})' for motif in telomere_motifs)
34
+
35
+ for i in range(0, len(seq) - window_size + 1, step_size):
36
+ window_seq = seq[i:i + window_size]
37
+ motif_count = len(re.findall(combined_motif_pattern, window_seq))
38
+ density = motif_count / (window_size / len(telomere_motifs[0])) # Assuming all motifs have similar lengths
39
+ densities.append(density)
40
+
41
+ if density >= density_threshold:
42
+ if current_start is None:
43
+ current_start = i
44
+ gap_length = 0
45
+ else:
46
+ if current_start is not None:
47
+ gap_length += step_size
48
+ if gap_length > max_gap_length:
49
+
50
+ mismatch_motif_count = len(re.findall(f"({combined_motif_pattern}){{e<=1}}", window_seq))
51
+ mismatch_density = mismatch_motif_count / (window_size / len(telomere_motifs[0]))
52
+
53
+ if mismatch_density >= density_threshold:
54
+
55
+ gap_length = 0
56
+ else:
57
+ has_large_gap = True
58
+ telomere_regions.append((current_start, i - gap_length + step_size))
59
+ current_start = None
60
+ gap_length = 0
61
+
62
+ if current_start is not None:
63
+ telomere_regions.append((current_start, len(seq)))
64
+
65
+ return telomere_regions, densities, has_large_gap, combined_motif_pattern
66
+
67
+
68
+ def measure_telomere_length(seq, telomere_motifs, window_size=100, step_size=10, density_threshold=0.1, max_gap_length=50):
69
+ telomere_regions, densities, has_large_gap, combined_motif_pattern = identify_telomere_regions(seq, telomere_motifs, window_size, step_size, density_threshold, max_gap_length)
70
+
71
+ if not telomere_regions:
72
+ return 0, 0, 0, densities, has_large_gap, combined_motif_pattern
73
+
74
+
75
+ terminus_regions = []
76
+ for start, end in telomere_regions:
77
+ if start < 100 or end > len(seq) - 100:
78
+ terminus_regions.append((start, end))
79
+
80
+ if not terminus_regions:
81
+ return 0, 0, 0, densities, has_large_gap, combined_motif_pattern
82
+
83
+
84
+ telomere_start, telomere_end = terminus_regions[0]
85
+ telomere_length = telomere_end - telomere_start
86
+
87
+ return telomere_start, telomere_end, telomere_length, densities, has_large_gap, combined_motif_pattern
88
+
89
+
90
+ def process_read(read_data, telomere_motifs, max_gap_length, min_read_len):
91
+ if read_data['is_unmapped'] or read_data['reference_name'] == 'chrM':
92
+ return None
93
+
94
+ if read_data['reference_start'] > 30000 and read_data['reference_end'] < read_data['reference_length'] - 30000:
95
+ return None
96
+
97
+ seq = read_data['query_sequence']
98
+ if seq is None or len(seq) < min_read_len:
99
+ return None
100
+
101
+
102
+ alignment_start = read_data['reference_start']
103
+ alignment_end = read_data['reference_end']
104
+ reference_genome_length = read_data['reference_length']
105
+
106
+ if alignment_start < 15000 and alignment_end <= reference_genome_length - 30000:
107
+ arm = "p"
108
+ else:
109
+ arm = "q"
110
+
111
+ direction = "rev" if read_data['is_reverse'] else "fwd"
112
+
113
+
114
+ 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)
115
+ if telomere_length < 1:
116
+ return None
117
+ return {
118
+ 'chromosome': read_data['reference_name'],
119
+ 'reference_start': alignment_start,
120
+ 'reference_end': alignment_end,
121
+ 'telomere_length': telomere_length,
122
+ #'subtel_boundary_length': telomere_end - telomere_start,
123
+ 'read_id': read_data['query_name'],
124
+ 'mapping_quality': read_data['mapping_quality'],
125
+ 'read_length': len(seq),
126
+ 'arm': arm,
127
+ 'direction': direction
128
+ #'has_large_gap': has_large_gap
129
+ }
130
+
131
+
132
+ def process_read_wrapper(args):
133
+ return process_read(*args)
134
+
135
+
136
+ def process_bam_file(bam_file_path, output_file_path, max_gap_length=100, min_read_len=1000, num_processes=8):
137
+ start_time = time.time()
138
+
139
+
140
+ bam_file = pysam.AlignmentFile(bam_file_path, "rb")
141
+
142
+ telomere_motifs = get_telomere_repeats()
143
+
144
+ results = []
145
+ read_results = {}
146
+ total_reads = 0
147
+ large_gap_count = 0
148
+
149
+
150
+ read_data_list = [{
151
+ 'query_name': read.query_name,
152
+ 'is_unmapped': read.is_unmapped,
153
+ 'is_reverse': read.is_reverse,
154
+ 'reference_start': read.reference_start,
155
+ 'reference_end': read.reference_end,
156
+ 'reference_name': read.reference_name,
157
+ 'mapping_quality': read.mapping_quality,
158
+ 'query_sequence': read.query_sequence,
159
+ 'reference_length': bam_file.get_reference_length(read.reference_name) if read.reference_name is not None else None
160
+ } for read in bam_file if read.reference_name is not None and read.reference_name != 'chrM']
161
+
162
+ # Use multiprocessing Pool for parallel processing
163
+ with Pool(processes=num_processes) as pool:
164
+ for result in pool.imap_unordered(process_read_wrapper, [(rd, telomere_motifs, max_gap_length, min_read_len) for rd in read_data_list]):
165
+ if result:
166
+ total_reads += 1
167
+ #if result['has_large_gap']:
168
+ # large_gap_count += 1
169
+
170
+ # Check for existing results with the same read_id
171
+ existing_result = read_results.get(result['read_id'])
172
+ if existing_result:
173
+ if (result['mapping_quality'] > existing_result['mapping_quality'] or
174
+ (result['mapping_quality'] == existing_result['mapping_quality'] and
175
+ result['telomere_length'] > existing_result['telomere_length'])):
176
+ read_results[result['read_id']] = result
177
+ else:
178
+ read_results[result['read_id']] = result
179
+
180
+ bam_file.close()
181
+
182
+ # Convert results to a DataFrame and save to a TSV file
183
+ results_df = pd.DataFrame(list(read_results.values()))
184
+ results_df.to_csv(output_file_path, sep='\t', index=False)
185
+
186
+ # Calculate and print the percentage of telomeres with gaps larger than max_gap_length
187
+ #if total_reads > 0:
188
+ # large_gap_percentage = (large_gap_count / total_reads) * 100
189
+ #else:
190
+ # large_gap_percentage = 0
191
+
192
+ print(f"Telometer completed successfully. Total telomeres measured: {len(read_results)}")
193
+ print(f"Total processing time: {time.time() - start_time:.2f} seconds")
194
+
195
+ if __name__ == "__main__":
196
+ import argparse
197
+
198
+ parser = argparse.ArgumentParser(description='Calculate telomere length from a BAM file.')
199
+ parser.add_argument('-b', '--bam', help='The path to the sorted BAM file.', required=True)
200
+ parser.add_argument('-o', '--output', help='The path to the output file.', required=True)
201
+ 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)
202
+ parser.add_argument('-g', '--maxgaplen', default=100, type=int, help='Maximum allowed gap length between telomere regions. Optional', required=False)
203
+ parser.add_argument('-t', '--threads', default=cpu_count(), type=int, help='Number of processing threads to use. Optional', required=False)
204
+
205
+ args = parser.parse_args()
206
+
207
+ process_bam_file(args.bam, args.output, max_gap_length=args.maxgaplen, min_read_len=args.minreadlen, num_processes=args.threads)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: telometer
3
- Version: 0.80
3
+ Version: 0.90
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
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ telometer = telometer:process_bam_file
@@ -1,171 +0,0 @@
1
- #!/usr/bin/env python3
2
- # Telometer v0.79
3
- # Created by: Santiago E Sanche8
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.ts8
7
- import pysam
8
- import re
9
- import regex as re
10
- import csv
11
- import argparse
12
- from multiprocessing import Pool, cpu_count
13
-
14
- def reverse_complement(seq):
15
- """Returns the reverse complement of a DNA sequence."""
16
- complement = {'A': 'T', 'C': 'G', 'G': 'C', 'T': 'A', 'N': 'N'}
17
- return "".join(complement[base] for base in reversed(seq))
18
-
19
- def get_adapters(chemistry):
20
- """Returns the adapter sequences based on the sequencing chemistry."""
21
- if chemistry == 'r10':
22
- adapters = ['TTTTTTTTCCTGTACTTCGTTCAGTTACGTATTGCT', 'GCAATACGTAACTGAACGAAGTACAGG']
23
- else:
24
- adapters = ['TTTTTTTTTTTAATGTACTTCGTTCAGTTACGTATTGCT', 'GCAATACGTAACTGAACGAAGT']
25
-
26
- adapters_rc = [reverse_complement(adapter) for adapter in adapters]
27
- return adapters + adapters_rc
28
-
29
- def get_flexible_telomere_patterns():
30
- """Returns flexible telomere repeat patterns for both G-rich and C-rich strands."""
31
- g_rich_telomere_pattern = r'(T{1,2}T{1,2}A{1,2}G{1,2}G{1,2}G{1,2}){2,}'
32
- c_rich_telomere_pattern = r'(C{1,2}C{1,2}C{1,2}T{1,2}A{1,2}A{1,2}){2,}'
33
- return g_rich_telomere_pattern, c_rich_telomere_pattern
34
-
35
- def find_initial_boundary_region(sequence, patterns, max_mismatches):
36
- """Finds the initial boundary region with allowed mismatches."""
37
- boundary_length = 0
38
- combined_pattern = '|'.join(f'({pattern})' for pattern in patterns)
39
- regex_pattern = f'({combined_pattern}){{2,}}'
40
-
41
- for match in re.finditer(f'({regex_pattern}){{e<={max_mismatches}}}', sequence, re.BESTMATCH):
42
- boundary_length = max(boundary_length, len(match.group(0)))
43
- return boundary_length
44
-
45
- def determine_arm(reference_name, alignment_start, reference_length):
46
- """Determine the chromosome arm based on the alignment start position and reference name."""
47
- if alignment_start < 15000 and "q" not in reference_name:
48
- return "p"
49
- return "q"
50
-
51
- def process_read(args):
52
- read_data, g_rich_telomere_pattern, c_rich_telomere_pattern, adapters, minreadlen = args
53
-
54
- if read_data['is_unmapped'] or read_data['query_sequence'] is None or len(read_data['query_sequence']) < minreadlen:
55
- print(f"Skipping read {read_data['read_id']}: unmapped or too short")
56
- return None
57
-
58
- alignment_start = read_data['reference_start']
59
- alignment_end = read_data['reference_end']
60
- seq_to_check = read_data['query_sequence']
61
-
62
- if read_data['is_reverse']:
63
- direction = "rev"
64
- seq_to_check = reverse_complement(seq_to_check)
65
- else:
66
- direction = "fwd"
67
-
68
- reference_genome_length = read_data['reference_length']
69
- if alignment_start >= 15000 and alignment_start <= reference_genome_length - 30000:
70
- print(f"Skipping read {read_data['read_id']}: alignment start {alignment_start} within filtered range")
71
- return None
72
-
73
- arm = determine_arm(read_data['reference_name'], alignment_start, reference_genome_length)
74
-
75
- telomere_starts = [m.start() for m in re.finditer(g_rich_telomere_pattern, seq_to_check)]
76
- if not telomere_starts:
77
- telomere_starts = [m.start() for m in re.finditer(c_rich_telomere_pattern, seq_to_check)]
78
-
79
- if telomere_starts:
80
- telomere_start = telomere_starts[0]
81
-
82
- # Find the end of the telomere region
83
- telomere_end = min((pos for pos in (seq_to_check.find(adapter) for adapter in adapters) if pos != -1), default=len(seq_to_check))
84
-
85
- telomere_region = seq_to_check[telomere_start:telomere_end]
86
- telomere_repeat = [m.group() for m in re.finditer(g_rich_telomere_pattern, telomere_region)]
87
- if not telomere_repeat:
88
- telomere_repeat = [m.group() for m in re.finditer(c_rich_telomere_pattern, telomere_region)]
89
-
90
- telomere_length = len(''.join(telomere_repeat))
91
- if telomere_length > 0:
92
- # Find the region immediately adjacent to the telomere region
93
- boundary_mm1_region = seq_to_check[telomere_end:]
94
- boundary_mm1_length = find_initial_boundary_region(boundary_mm1_region, g_rich_telomere_pattern.split('|') + c_rich_telomere_pattern.split('|'), max_mismatches=2)
95
- result = {
96
- 'chromosome': read_data['reference_name'],
97
- 'arm': arm,
98
- 'telomere_start': telomere_start,
99
- 'telomere_end': telomere_end,
100
- 'telomere_length': telomere_length,
101
- 'subtel_boundary_length': boundary_mm1_length,
102
- 'read_id': read_data['read_id'],
103
- 'mapping_quality': read_data['mapping_quality'],
104
- 'direction': direction
105
- }
106
- return result
107
- else:
108
- return None
109
-
110
-
111
- def calculate_telomere_length():
112
- parser = argparse.ArgumentParser(description='Calculate telomere length from a BAM file.')
113
- parser.add_argument('-b', '--bam', help='The path to the sorted BAM file.', required=True)
114
- parser.add_argument('-o', '--output', help='The path to the output file.', required=True)
115
- parser.add_argument('-c', '--chemistry', default="r10", help="Sequencing chemistry (r9 or r10, default=r10). Optional", required=False)
116
- 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)
117
- args = parser.parse_args()
118
-
119
- adapters = get_adapters(args.chemistry)
120
- g_rich_telomere_pattern, c_rich_telomere_pattern = get_flexible_telomere_patterns()
121
-
122
- bam_file = pysam.AlignmentFile(args.bam, "rb")
123
- read_data_list = [{
124
- 'read_id': read.query_name,
125
- 'is_unmapped': read.is_unmapped,
126
- 'reference_start': read.reference_start,
127
- 'reference_end': read.reference_end,
128
- 'reference_name': read.reference_name,
129
- 'mapping_quality': read.mapping_quality,
130
- 'query_sequence': read.query_sequence,
131
- 'reference_length': bam_file.get_reference_length(read.reference_name) if read.reference_name is not None else None,
132
- 'is_reverse': read.is_reverse
133
- } for read in bam_file if read.reference_name is not None and read.reference_name != 'chrM']
134
-
135
- with Pool(processes=cpu_count()) as pool:
136
- results = pool.map(
137
- process_read,
138
- [(read_data, g_rich_telomere_pattern, c_rich_telomere_pattern, adapters, args.minreadlen) for read_data in read_data_list]
139
- )
140
-
141
- results = [result for result in results if result]
142
-
143
- # Ensure only the best result per read_id is saved
144
- best_results = {}
145
- for result in results:
146
- read_id = result['read_id']
147
- if read_id not in best_results:
148
- best_results[read_id] = result
149
- else:
150
- existing_result = best_results[read_id]
151
- if result['mapping_quality'] > existing_result['mapping_quality']:
152
- best_results[read_id] = result
153
- elif result['mapping_quality'] == existing_result['mapping_quality']:
154
- if result['telomere_length'] > existing_result['telomere_length']:
155
- best_results[read_id] = result
156
- elif result['telomere_length'] == existing_result['telomere_length']:
157
- # Arbitrarily keep the existing one if both telomere_length and mapping_quality are the same
158
- pass
159
-
160
- final_results = list(best_results.values())
161
-
162
- if final_results:
163
- with open(args.output, 'w', newline='') as output_file:
164
- writer = csv.DictWriter(output_file, fieldnames=final_results[0].keys(), delimiter='\t')
165
- writer.writeheader()
166
- writer.writerows(final_results)
167
-
168
- print(f"Telometer completed successfully. Total telomeres measured: {len(final_results)}")
169
-
170
- if __name__ == "__main__":
171
- calculate_telomere_length()
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- telometer = telometer:calculate_telomere_length
File without changes
File without changes
File without changes
File without changes