telometer 2.0.0__tar.gz → 2.0.2__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-2.0.2/LICENSE.txt +19 -0
- telometer-2.0.2/PKG-INFO +48 -0
- telometer-2.0.2/pyproject.toml +55 -0
- telometer-2.0.2/telometer/__init__.py +3 -0
- {telometer-2.0.0 → telometer-2.0.2}/telometer/telometer.py +29 -14
- telometer-2.0.2/telometer/telometer_variant_support.py +198 -0
- telometer-2.0.2/telometer/variant_support_v2.py +308 -0
- telometer-2.0.2/telometer.egg-info/PKG-INFO +48 -0
- {telometer-2.0.0 → telometer-2.0.2}/telometer.egg-info/SOURCES.txt +4 -0
- telometer-2.0.2/telometer.egg-info/entry_points.txt +2 -0
- telometer-2.0.2/telometer.egg-info/requires.txt +5 -0
- telometer-2.0.0/PKG-INFO +0 -16
- telometer-2.0.0/pyproject.toml +0 -31
- telometer-2.0.0/telometer/__init__.py +0 -34
- telometer-2.0.0/telometer.egg-info/PKG-INFO +0 -16
- telometer-2.0.0/telometer.egg-info/requires.txt +0 -2
- {telometer-2.0.0 → telometer-2.0.2}/README.md +0 -0
- {telometer-2.0.0 → telometer-2.0.2}/setup.cfg +0 -0
- {telometer-2.0.0 → telometer-2.0.2}/telometer.egg-info/dependency_links.txt +0 -0
- {telometer-2.0.0 → telometer-2.0.2}/telometer.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2024 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
telometer-2.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: telometer
|
|
3
|
+
Version: 2.0.2
|
|
4
|
+
Summary: Quantitative digital telomere measurement from nanopore long reads
|
|
5
|
+
Author-email: "Santiago E. Sanchez" <santy.esanchez@gmail.com>
|
|
6
|
+
License: Copyright (c) 2024 The Python Packaging Authority
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
|
25
|
+
|
|
26
|
+
Classifier: Development Status :: 4 - Beta
|
|
27
|
+
Classifier: Intended Audience :: Science/Research
|
|
28
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
29
|
+
Classifier: Operating System :: OS Independent
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
37
|
+
Requires-Python: >=3.8
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE.txt
|
|
40
|
+
Requires-Dist: numpy
|
|
41
|
+
Requires-Dist: pandas
|
|
42
|
+
Requires-Dist: pysam
|
|
43
|
+
Requires-Dist: regex
|
|
44
|
+
Requires-Dist: scipy
|
|
45
|
+
|
|
46
|
+
A simple tool for measuring chromosome-specific telomeres from long-read alignments.
|
|
47
|
+
|
|
48
|
+
[Telometer Github](https://github.com/santiago-es/Telometer)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=61,<76",
|
|
4
|
+
"wheel"
|
|
5
|
+
]
|
|
6
|
+
build-backend = "setuptools.build_meta"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
[project]
|
|
10
|
+
name = "telometer"
|
|
11
|
+
version = "2.0.2"
|
|
12
|
+
description = "Quantitative digital telomere measurement from nanopore long reads"
|
|
13
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
license = { file = "LICENSE.txt" }
|
|
16
|
+
|
|
17
|
+
authors = [
|
|
18
|
+
{ name = "Santiago E. Sanchez", email = "santy.esanchez@gmail.com" }
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Intended Audience :: Science/Research",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.8",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
dependencies = [
|
|
36
|
+
"numpy",
|
|
37
|
+
"pandas",
|
|
38
|
+
"pysam",
|
|
39
|
+
"regex",
|
|
40
|
+
"scipy"
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
telometer = "telometer.telometer:run_telometer"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
[tool.setuptools]
|
|
49
|
+
include-package-data = true
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
where = ["."]
|
|
54
|
+
include = ["telometer", "telometer.*"]
|
|
55
|
+
exclude = ["build", "build.*", "dist", "dist.*"]
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# Telometer v2.0
|
|
3
|
-
|
|
2
|
+
# Telometer v2.0
|
|
3
|
+
|
|
4
4
|
# Created by: Santiago E Sanchez
|
|
5
5
|
# Artandi Lab, Stanford University, 2024
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
# Measures telomeres from ONT long reads
|
|
8
|
+
|
|
7
9
|
# Simple Usage: telometer -b sorted_t2t.bam -o output.tsv
|
|
8
|
-
# Optional enrichment: add -e to your existing Telometer command.
|
|
9
|
-
# -e adds the sample-level telomeres_per_gb column and console totals. Each QNAME
|
|
10
|
-
# contributes one consistent recorded full length, including soft/hard clips,
|
|
11
|
-
# before any Telometer filter. Missing/conflicting lengths yield NA, not a
|
|
12
|
-
# partial-denominator rate. This measures yield represented by the input BAM;
|
|
13
|
-
# reads/bases removed upstream without retained metadata cannot be recovered.
|
|
14
|
-
# Memory for -e scales with unique read IDs; -l is not a total-RSS limit.
|
|
15
10
|
|
|
16
|
-
__version__ = "2.0"
|
|
11
|
+
__version__ = "2.0.2"
|
|
17
12
|
|
|
18
13
|
import pysam
|
|
19
14
|
from pathlib import Path
|
|
20
|
-
from variant_support_v2 import (BASE_COLUMNS, VARIANT_COLUMNS, TRACT_COLUMNS,
|
|
21
|
-
validate_variant_options, strict_runs, coverage_prefix, combined_window_density, summarize_variant)
|
|
15
|
+
#from variant_support_v2 import (BASE_COLUMNS, VARIANT_COLUMNS, TRACT_COLUMNS,
|
|
16
|
+
# validate_variant_options, strict_runs, coverage_prefix, combined_window_density, summarize_variant)
|
|
22
17
|
import regex as re
|
|
23
18
|
import pandas as pd
|
|
24
19
|
import time
|
|
@@ -27,7 +22,27 @@ import sys
|
|
|
27
22
|
from multiprocessing import Pool, cpu_count
|
|
28
23
|
import numpy as np
|
|
29
24
|
from scipy.signal import savgol_filter
|
|
30
|
-
from telometer_variant_support import alignment_fields, cached_findall, note, reverse_complement_iupac, stable_record_key
|
|
25
|
+
#from telometer_variant_support import alignment_fields, cached_findall, note, reverse_complement_iupac, stable_record_key
|
|
26
|
+
|
|
27
|
+
from .variant_support_v2 import (
|
|
28
|
+
BASE_COLUMNS,
|
|
29
|
+
VARIANT_COLUMNS,
|
|
30
|
+
TRACT_COLUMNS,
|
|
31
|
+
validate_variant_options,
|
|
32
|
+
strict_runs,
|
|
33
|
+
coverage_prefix,
|
|
34
|
+
combined_window_density,
|
|
35
|
+
summarize_variant,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from .telometer_variant_support import (
|
|
39
|
+
alignment_fields,
|
|
40
|
+
cached_findall,
|
|
41
|
+
note,
|
|
42
|
+
reverse_complement_iupac,
|
|
43
|
+
stable_record_key,
|
|
44
|
+
)
|
|
45
|
+
|
|
31
46
|
|
|
32
47
|
def reverse_complement(seq):
|
|
33
48
|
complement = {'A': 'T', 'C': 'G', 'G': 'C', 'T': 'A', 'N': 'N'}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Small, explicit helpers used by isolated Telometer v1.0 patches."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from array import array
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
import hashlib
|
|
8
|
+
import math
|
|
9
|
+
import sqlite3
|
|
10
|
+
import zlib
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import regex
|
|
14
|
+
|
|
15
|
+
_COMPLEMENT = str.maketrans('ACGTRYSWKMBDHVNacgtryswkmbdhvn', 'TGCAYRSWMKVHDBNtgcayrswmkvhdbn')
|
|
16
|
+
_ERROR_PROB = np.power(10.0, -np.arange(256, dtype=float) / 10.0)
|
|
17
|
+
_DOUBLET = regex.compile(r'(?:TTAGGG){2}|(?:CCCTAA){2}')
|
|
18
|
+
NOTES = Counter()
|
|
19
|
+
RECOVERY_COUNTS = Counter()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def reset_notes():
|
|
23
|
+
NOTES.clear()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def note(key, value=1):
|
|
27
|
+
NOTES[key] += value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def reverse_complement_iupac(seq):
|
|
31
|
+
return seq.translate(_COMPLEMENT)[::-1]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def native_read_q(read):
|
|
35
|
+
if not read.has_tag('qs'):
|
|
36
|
+
return 0.0
|
|
37
|
+
try:
|
|
38
|
+
value = float(read.get_tag('qs'))
|
|
39
|
+
except (ValueError, TypeError):
|
|
40
|
+
return 0.0
|
|
41
|
+
return value if math.isfinite(value) else 0.0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def probability_mean_q(qualities):
|
|
45
|
+
if not len(qualities):
|
|
46
|
+
return 0.0
|
|
47
|
+
q = np.asarray(qualities, dtype=np.uint8)
|
|
48
|
+
return float(-10.0 * np.log10(_ERROR_PROB[q].mean()))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@lru_cache(maxsize=16)
|
|
52
|
+
def _compiled(pattern):
|
|
53
|
+
return regex.compile(pattern)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cached_findall(pattern, text):
|
|
57
|
+
return _compiled(pattern).findall(text)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def refine_doublet_boundaries(seq, start, end, radius=120):
|
|
61
|
+
left = max(0, start-radius)
|
|
62
|
+
right = min(len(seq), start+radius+12)
|
|
63
|
+
starts = [left+m.start() for m in _DOUBLET.finditer(seq[left:right], overlapped=True)
|
|
64
|
+
if abs(left+m.start()-start) <= radius]
|
|
65
|
+
left2 = max(0, end-radius-12)
|
|
66
|
+
right2 = min(len(seq), end+radius)
|
|
67
|
+
ends = [left2+m.end() for m in _DOUBLET.finditer(seq[left2:right2], overlapped=True)
|
|
68
|
+
if abs(left2+m.end()-end) <= radius]
|
|
69
|
+
if not starts or not ends:
|
|
70
|
+
note('boundary_unresolved')
|
|
71
|
+
return None
|
|
72
|
+
refined_start = min(starts, key=lambda x:(abs(x-start), x<start, x))
|
|
73
|
+
refined_end = min(ends, key=lambda x:(abs(x-end), x>end, x))
|
|
74
|
+
if refined_start >= refined_end:
|
|
75
|
+
note('boundary_empty_or_reversed')
|
|
76
|
+
return None
|
|
77
|
+
assert abs(refined_start-start) <= radius and abs(refined_end-end) <= radius
|
|
78
|
+
return refined_start, refined_end
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def flank_support(length, start, end, cigar, hardclips_as_soft=False):
|
|
82
|
+
left_terminal, right_terminal = start < 100, end > length-100
|
|
83
|
+
if not left_terminal and not right_terminal:
|
|
84
|
+
return False, 'none', 'not_terminal'
|
|
85
|
+
left_distance, right_distance = start, length-end
|
|
86
|
+
if left_terminal and right_terminal and left_distance == right_distance:
|
|
87
|
+
return False, 'ambiguous', 'equal_terminal_proximity'
|
|
88
|
+
side = 'left' if left_terminal and (not right_terminal or left_distance < right_distance) else 'right'
|
|
89
|
+
lo, hi = (end,end+50) if side == 'left' else (start-50,start)
|
|
90
|
+
if lo < 0 or hi > length:
|
|
91
|
+
return False, side, 'insufficient_proximal_sequence'
|
|
92
|
+
query, covered = 0, 0
|
|
93
|
+
for op, n in (cigar or []):
|
|
94
|
+
if op in (0,7,8):
|
|
95
|
+
covered += max(0, min(query+n,hi)-max(query,lo))
|
|
96
|
+
if op in (0,1,4,7,8) or (op == 5 and hardclips_as_soft):
|
|
97
|
+
query += n
|
|
98
|
+
return covered == 50, side, 'supported' if covered == 50 else 'proximal_flank_not_fully_match_aligned'
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def alignment_fields(read):
|
|
102
|
+
cigar = read.cigartuples or []
|
|
103
|
+
try:
|
|
104
|
+
alignment_score = float(read.get_tag('AS')) if read.has_tag('AS') else None
|
|
105
|
+
if alignment_score is not None and not math.isfinite(alignment_score):
|
|
106
|
+
alignment_score = None
|
|
107
|
+
except (TypeError, ValueError):
|
|
108
|
+
alignment_score = None
|
|
109
|
+
return {'reference_name':read.reference_name or '',
|
|
110
|
+
'reference_start':read.reference_start if read.reference_start is not None else -1,
|
|
111
|
+
'reference_end':read.reference_end if read.reference_end is not None else -1,
|
|
112
|
+
'is_reverse':bool(read.is_reverse), 'flag':int(read.flag), 'cigar':read.cigarstring or '',
|
|
113
|
+
'record_digest':hashlib.sha256(read.to_string().encode()).hexdigest(),
|
|
114
|
+
'mapping_quality':int(read.mapping_quality),
|
|
115
|
+
'alignment_class':2 if read.is_secondary else (1 if read.is_supplementary else 0),
|
|
116
|
+
'aligned_query_bases':sum(n for op,n in cigar if op in (0,7,8)),
|
|
117
|
+
'alignment_score':alignment_score}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def stable_record_key(fields):
|
|
121
|
+
return (fields['reference_name'],fields['reference_start'],fields['reference_end'],
|
|
122
|
+
int(fields['is_reverse']),fields['flag'],fields['cigar'],fields['record_digest'])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def length_neutral_key(fields):
|
|
126
|
+
negative_score = -fields['alignment_score'] if fields['alignment_score'] is not None else math.inf
|
|
127
|
+
return (-fields['mapping_quality'],fields['alignment_class'],-fields['aligned_query_bases'],
|
|
128
|
+
negative_score,stable_record_key(fields))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@lru_cache(maxsize=4)
|
|
132
|
+
def _donor_connection(path):
|
|
133
|
+
return sqlite3.connect(f'file:{path}?mode=ro', uri=True)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class RecoveredRead:
|
|
137
|
+
"""Sequence/quality view retaining the original alignment record and its identity."""
|
|
138
|
+
def __init__(self, original, sequence, qualities):
|
|
139
|
+
self._original = original
|
|
140
|
+
self.query_sequence = sequence
|
|
141
|
+
self.query_qualities = array('B', qualities)
|
|
142
|
+
self.recovered_full_sequence = True
|
|
143
|
+
|
|
144
|
+
def __getattr__(self, name):
|
|
145
|
+
return getattr(self._original, name)
|
|
146
|
+
|
|
147
|
+
def get_forward_sequence(self):
|
|
148
|
+
return reverse_complement_iupac(self.query_sequence) if self.is_reverse else self.query_sequence
|
|
149
|
+
|
|
150
|
+
def get_forward_qualities(self):
|
|
151
|
+
return self.query_qualities[::-1] if self.is_reverse else self.query_qualities
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def recover_record(read, donor_db):
|
|
155
|
+
cigar = read.cigartuples or []
|
|
156
|
+
hard = [(i,n) for i,(op,n) in enumerate(cigar) if op == 5]
|
|
157
|
+
seq, qualities = read.query_sequence, read.query_qualities
|
|
158
|
+
incomplete = bool(hard) or not seq or qualities is None or not len(qualities)
|
|
159
|
+
if not incomplete:
|
|
160
|
+
return read
|
|
161
|
+
RECOVERY_COUNTS['incomplete_records'] += 1
|
|
162
|
+
if not donor_db:
|
|
163
|
+
raise ValueError('Variant 06 needs an explicitly prepared donor database.')
|
|
164
|
+
row = _donor_connection(str(donor_db)).execute(
|
|
165
|
+
'SELECT sequence,qualities,length,conflict FROM donors WHERE read_id=?',(read.query_name,)).fetchone()
|
|
166
|
+
if row is None or row[3]:
|
|
167
|
+
RECOVERY_COUNTS['missing_or_conflicting_donor'] += 1
|
|
168
|
+
return None
|
|
169
|
+
if any(i not in (0,len(cigar)-1) for i,n in hard):
|
|
170
|
+
RECOVERY_COUNTS['internal_hard_clip'] += 1
|
|
171
|
+
return None
|
|
172
|
+
full_length = row[2]
|
|
173
|
+
leading_h = cigar[0][1] if cigar and cigar[0][0]==5 else 0
|
|
174
|
+
trailing_h = cigar[-1][1] if cigar and cigar[-1][0]==5 else 0
|
|
175
|
+
inferred_length = sum(n for op,n in cigar if op in (0,1,4,5,7,8))
|
|
176
|
+
if not cigar and seq:
|
|
177
|
+
inferred_length = len(seq)
|
|
178
|
+
if inferred_length != full_length:
|
|
179
|
+
RECOVERY_COUNTS['inferred_length_conflict'] += 1
|
|
180
|
+
return None
|
|
181
|
+
donor_sequence = zlib.decompress(row[0]).decode('ascii')
|
|
182
|
+
donor_quality = zlib.decompress(row[1])
|
|
183
|
+
if read.is_reverse:
|
|
184
|
+
donor_sequence = reverse_complement_iupac(donor_sequence)
|
|
185
|
+
donor_quality = donor_quality[::-1]
|
|
186
|
+
stop = full_length-trailing_h
|
|
187
|
+
expected_sequence = donor_sequence[leading_h:stop]
|
|
188
|
+
expected_quality = donor_quality[leading_h:stop]
|
|
189
|
+
if seq and seq != expected_sequence:
|
|
190
|
+
RECOVERY_COUNTS['retained_sequence_conflict'] += 1
|
|
191
|
+
return None
|
|
192
|
+
if qualities is not None and len(qualities) and bytes(qualities) != expected_quality:
|
|
193
|
+
RECOVERY_COUNTS['retained_quality_conflict'] += 1
|
|
194
|
+
return None
|
|
195
|
+
if len(donor_sequence) != full_length or len(donor_quality) != full_length:
|
|
196
|
+
raise AssertionError('Invalid donor database SEQ/QUAL length.')
|
|
197
|
+
RECOVERY_COUNTS['recovered_records'] += 1
|
|
198
|
+
return RecoveredRead(read,donor_sequence,donor_quality)
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Exact 6-mer variant support and repeat-screened proximal-flank proxy.
|
|
3
|
+
|
|
4
|
+
Coordinates in computations are relative to available SEQ in original sequencing
|
|
5
|
+
orientation. Public *_forward coordinates add the original-orientation leading
|
|
6
|
+
hard-clip offset, when present. No absent sequence is recovered or invented.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
import re
|
|
11
|
+
import warnings
|
|
12
|
+
from typing import NamedTuple
|
|
13
|
+
import numpy as np
|
|
14
|
+
from .telometer_variant_support import reverse_complement_iupac
|
|
15
|
+
|
|
16
|
+
BASE_COLUMNS = ['chromosome', 'reference_start', 'reference_end', 'telomere_length',
|
|
17
|
+
'read_id', 'mapping_quality', 'read_length', 'arm', 'direction']
|
|
18
|
+
VARIANT_COLUMNS = ['variant_motif', 'variant_present', 'variant_tract_count',
|
|
19
|
+
'variant_supported_bp', 'variant_repeat_equivalents', 'pre_variant_bp',
|
|
20
|
+
'pre_variant_status', 'anchor_status', 'telomeric_query_side',
|
|
21
|
+
'weak_proximal_variant_evidence', 'variant_boundary_truncated', 'variant_overlap_flag']
|
|
22
|
+
TRACT_COLUMNS = ['read_id', 'record_id', 'tract_id', 'query_start_forward', 'query_end_forward',
|
|
23
|
+
'span_bp', 'supported_bp', 'nonoverlapping_units', 'mean_q', 'min_q',
|
|
24
|
+
'boundary_left_truncated', 'boundary_right_truncated', 'touches_telomere_start',
|
|
25
|
+
'touches_telomere_end', 'touches_query_start', 'touches_query_end',
|
|
26
|
+
'sequence_left_missing', 'sequence_right_missing', 'overlap_flag', 'phase_template']
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PeriodicRun(NamedTuple):
|
|
30
|
+
start: int
|
|
31
|
+
end: int
|
|
32
|
+
phase: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@lru_cache(maxsize=32)
|
|
36
|
+
def motif_family(motif):
|
|
37
|
+
"""Deduplicated rotations/RCs; deterministic, cached per process."""
|
|
38
|
+
motif = motif.upper()
|
|
39
|
+
rc = reverse_complement_iupac(motif)
|
|
40
|
+
return tuple(sorted({s[i:] + s[:i] for s in (motif, rc) for i in range(6)}))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def minimum_period(motif):
|
|
44
|
+
return next(p for p in (1, 2, 3, 6) if motif == motif[:p] * (6//p))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def validate_variant_options(motif=None, min_units=None, emit_warnings=True):
|
|
48
|
+
if motif is None:
|
|
49
|
+
if min_units is not None:
|
|
50
|
+
raise ValueError('--min-variant-units requires --variant-motif')
|
|
51
|
+
return None, 2
|
|
52
|
+
motif = str(motif).upper()
|
|
53
|
+
if re.fullmatch('[ACGT]{6}', motif) is None:
|
|
54
|
+
raise ValueError('--variant-motif must be exactly six A/C/G/T bases (not a regex)')
|
|
55
|
+
if motif in motif_family('TTAGGG'):
|
|
56
|
+
raise ValueError('The variant motif must not be canonical-equivalent under rotation/RC')
|
|
57
|
+
units = 2 if min_units is None else min_units
|
|
58
|
+
if isinstance(units, bool) or not isinstance(units, int) or units < 2:
|
|
59
|
+
raise ValueError('--min-variant-units must be an integer at least 2')
|
|
60
|
+
if emit_warnings and (minimum_period(motif) < 6 or len(set(motif)) <= 2):
|
|
61
|
+
warnings.warn('Low-complexity variant motif: periodicity/base diversity may reduce specificity; '
|
|
62
|
+
'exact support is not biological validation.', UserWarning, stacklevel=2)
|
|
63
|
+
return motif, units
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@lru_cache(maxsize=32)
|
|
67
|
+
def _motif_search(motif):
|
|
68
|
+
family = motif_family(motif)
|
|
69
|
+
# Lookahead enumerates overlapping 6-bp hits, never consumes a gap.
|
|
70
|
+
pattern = re.compile('(?=(' + '|'.join(family) + '))')
|
|
71
|
+
# A phase key is the six-base periodic template at absolute query indices mod 6.
|
|
72
|
+
phase_keys = {(m, r): (m[-r:] + m[:-r] if r else m) for m in family for r in range(6)}
|
|
73
|
+
return pattern, phase_keys
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def strict_runs(seq, motif, min_units=2):
|
|
77
|
+
"""Maximal phase-consistent covered runs; no unsupported-gap/phase bridging.
|
|
78
|
+
|
|
79
|
+
Adjacent/overlapping exact hits may join ONLY within the identical global
|
|
80
|
+
periodic template. Distinct templates remain separate, including overlaps.
|
|
81
|
+
Partial end units can be covered by overlapping exact hits; admission still
|
|
82
|
+
requires floor(span/6) >= min_units non-overlapping complete units.
|
|
83
|
+
"""
|
|
84
|
+
if not seq:
|
|
85
|
+
return []
|
|
86
|
+
pattern, phase_keys = _motif_search(motif.upper())
|
|
87
|
+
current, finished = {}, []
|
|
88
|
+
for hit in pattern.finditer(seq):
|
|
89
|
+
start, end = hit.start(), hit.start() + 6
|
|
90
|
+
phase = phase_keys[(hit.group(1), start % 6)]
|
|
91
|
+
previous = current.get(phase)
|
|
92
|
+
if previous is not None and start <= previous[1]:
|
|
93
|
+
current[phase] = (previous[0], max(previous[1], end))
|
|
94
|
+
else:
|
|
95
|
+
if previous is not None:
|
|
96
|
+
finished.append(PeriodicRun(previous[0], previous[1], phase))
|
|
97
|
+
current[phase] = (start, end)
|
|
98
|
+
finished.extend(PeriodicRun(s, e, phase) for phase, (s, e) in current.items())
|
|
99
|
+
# Same-template overlaps/containments already coalesced, distinct phases are not.
|
|
100
|
+
return sorted(set(r for r in finished if r.end-r.start >= 6*min_units))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def union_intervals(intervals):
|
|
104
|
+
merged = []
|
|
105
|
+
for s, e in sorted((int(s), int(e)) for s, e in intervals if e > s):
|
|
106
|
+
if merged and s <= merged[-1][1]:
|
|
107
|
+
merged[-1] = (merged[-1][0], max(e, merged[-1][1]))
|
|
108
|
+
else:
|
|
109
|
+
merged.append((s, e))
|
|
110
|
+
return merged
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def union_bp(intervals):
|
|
114
|
+
return sum(e-s for s, e in union_intervals(intervals))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def overlap_flags(intervals):
|
|
118
|
+
# Sorted sweep: mark every member of a positive-overlap group, not adjacency.
|
|
119
|
+
indexed = sorted((s, e, i) for i, (s, e) in enumerate(intervals))
|
|
120
|
+
flags = [False] * len(indexed)
|
|
121
|
+
active = []
|
|
122
|
+
for s, e, i in indexed:
|
|
123
|
+
active = [(pe, pi) for pe, pi in active if pe > s]
|
|
124
|
+
for _, pi in active:
|
|
125
|
+
flags[pi] = flags[i] = True
|
|
126
|
+
active.append((e, i))
|
|
127
|
+
return flags
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def coverage_prefix(length, runs):
|
|
131
|
+
if not runs:
|
|
132
|
+
return None
|
|
133
|
+
mask = np.zeros(length, dtype=np.uint8)
|
|
134
|
+
for run in runs:
|
|
135
|
+
mask[run.start:run.end] = 1
|
|
136
|
+
prefix = np.empty(length+1, dtype=np.int64)
|
|
137
|
+
prefix[0] = 0
|
|
138
|
+
np.cumsum(mask, out=prefix[1:])
|
|
139
|
+
return prefix
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@lru_cache(maxsize=32)
|
|
143
|
+
def _canonical_pattern(pattern):
|
|
144
|
+
# Standard re and regex have identical semantics for this literal alternation.
|
|
145
|
+
return re.compile(pattern)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def combined_window_density(window_seq, window_start, canonical_pattern, canonical_count, prefix):
|
|
149
|
+
"""Augment the ORIGINAL non-overlapping canonical-window matches, not all registers."""
|
|
150
|
+
if prefix is None:
|
|
151
|
+
return canonical_count * 6 / len(window_seq)
|
|
152
|
+
end = window_start + len(window_seq)
|
|
153
|
+
variant_bp = int(prefix[end]-prefix[window_start])
|
|
154
|
+
if variant_bp == 0:
|
|
155
|
+
return canonical_count * 6 / len(window_seq)
|
|
156
|
+
shared = sum(int(prefix[window_start+m.end()] - prefix[window_start+m.start()])
|
|
157
|
+
for m in _canonical_pattern(canonical_pattern).finditer(window_seq))
|
|
158
|
+
return (canonical_count * 6 + variant_bp - shared) / len(window_seq)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def stored_to_forward(length, start, end, is_reverse, offset=0):
|
|
162
|
+
if not (0 <= start <= end <= length):
|
|
163
|
+
raise ValueError('Query interval is out of bounds')
|
|
164
|
+
s, e = (length-end, length-start) if is_reverse else (start, end)
|
|
165
|
+
return s+offset, e+offset
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def clipping(cigar, is_reverse):
|
|
169
|
+
cigar = cigar or []
|
|
170
|
+
left_h = cigar[0][1] if cigar and cigar[0][0] == 5 else 0
|
|
171
|
+
right_h = cigar[-1][1] if cigar and cigar[-1][0] == 5 else 0
|
|
172
|
+
soft = [n for op, n in cigar if op == 4]
|
|
173
|
+
return {'query_forward_offset': right_h if is_reverse else left_h,
|
|
174
|
+
'sequence_left_missing': bool(right_h if is_reverse else left_h),
|
|
175
|
+
'sequence_right_missing': bool(left_h if is_reverse else right_h),
|
|
176
|
+
'hard_clip_left': left_h, 'hard_clip_right': right_h,
|
|
177
|
+
'hard_clipped_sequence': bool(left_h or right_h), 'soft_clipped_bases': sum(soft)}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def aligned_intervals_forward(length, cigar, is_reverse):
|
|
181
|
+
query, intervals = 0, []
|
|
182
|
+
for op, n in cigar or []:
|
|
183
|
+
if op in (0, 7, 8):
|
|
184
|
+
intervals.append(stored_to_forward(length, query, query+n, is_reverse))
|
|
185
|
+
if op in (0, 1, 4, 7, 8):
|
|
186
|
+
query += n
|
|
187
|
+
if cigar and query != length:
|
|
188
|
+
raise ValueError('CIGAR query-consuming length does not match available SEQ')
|
|
189
|
+
return union_intervals(intervals)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def evaluate_anchor(length, start, end, cigar, is_reverse, repeat_intervals):
|
|
193
|
+
"""Require exactly one terminal side, 50 M/=/X bases, and no strict repeat run."""
|
|
194
|
+
if not (0 <= start < end <= length):
|
|
195
|
+
raise ValueError('Invalid telomere interval')
|
|
196
|
+
left, right = start < 100, end > length-100
|
|
197
|
+
out = {'telomeric_query_side': 'ambiguous' if left and right else ('left' if left else 'right' if right else 'none'),
|
|
198
|
+
'anchor_status': 'ambiguous_terminal_side', 'proximal_boundary_local': None,
|
|
199
|
+
'flank_start_local': None, 'flank_end_local': None, 'flank_aligned_bp': None,
|
|
200
|
+
'flank_repeat_intersection': None}
|
|
201
|
+
if left == right:
|
|
202
|
+
return out
|
|
203
|
+
lo, hi = (end, end+50) if left else (start-50, start)
|
|
204
|
+
out.update(proximal_boundary_local=end if left else start, flank_start_local=lo, flank_end_local=hi)
|
|
205
|
+
if lo < 0 or hi > length:
|
|
206
|
+
out['anchor_status'] = 'insufficient_flank'
|
|
207
|
+
return out
|
|
208
|
+
aligned = aligned_intervals_forward(length, cigar, is_reverse)
|
|
209
|
+
covered = sum(max(0, min(hi, e)-max(lo, s)) for s, e in aligned)
|
|
210
|
+
repeated = any(s < hi and e > lo for s, e in repeat_intervals)
|
|
211
|
+
out.update(flank_aligned_bp=covered, flank_repeat_intersection=bool(repeated))
|
|
212
|
+
out['anchor_status'] = ('unaligned_flank' if covered != 50 else
|
|
213
|
+
'repeat_supported_flank' if repeated else 'supported_proxy')
|
|
214
|
+
return out
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def common_distance(intervals, start, end, anchor):
|
|
218
|
+
if not intervals:
|
|
219
|
+
return None, 'no_supported_tract'
|
|
220
|
+
if anchor['anchor_status'] != 'supported_proxy':
|
|
221
|
+
return None, 'anchor_unresolved'
|
|
222
|
+
if anchor['telomeric_query_side'] == 'right':
|
|
223
|
+
distance = min(s for s, e in intervals)-start
|
|
224
|
+
else:
|
|
225
|
+
distance = end-max(e for s, e in intervals)
|
|
226
|
+
if not 0 <= distance <= end-start:
|
|
227
|
+
raise AssertionError('Invalid pre-variant distance')
|
|
228
|
+
return int(distance), 'estimated_proxy'
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def quality_diagnostics(qualities, start, end):
|
|
232
|
+
if qualities is None or len(qualities) < end:
|
|
233
|
+
return None, None
|
|
234
|
+
values = qualities[start:end]
|
|
235
|
+
return (float(sum(values)/len(values)), int(min(values))) if len(values) else (None, None)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def characterize_intervals(seq, qualities, start, end, intervals, motif, cigar, is_reverse,
|
|
239
|
+
read_id, record_id, min_units=2, phases=None, strict_definition=True):
|
|
240
|
+
"""Annotate supplied intervals, preserving a comparator's intervals unchanged.
|
|
241
|
+
|
|
242
|
+
The flank always screens canonical + variant runs at TWO units, even if the
|
|
243
|
+
chosen variant-admission threshold is higher. It is not a mapping-uniqueness test.
|
|
244
|
+
"""
|
|
245
|
+
length = len(seq)
|
|
246
|
+
if any(not start <= s < e <= end for s, e in intervals):
|
|
247
|
+
raise AssertionError('Tract lies outside the selected telomere interval')
|
|
248
|
+
full_weak = strict_runs(seq, motif, min_units=1)
|
|
249
|
+
strict_repeat_intervals = [(r.start, r.end) for r in full_weak if r.end-r.start >= 12]
|
|
250
|
+
strict_repeat_intervals.extend((r.start, r.end) for r in strict_runs(seq, 'TTAGGG', 2))
|
|
251
|
+
anchor = evaluate_anchor(length, start, end, cigar, is_reverse, strict_repeat_intervals)
|
|
252
|
+
distance, status = common_distance(intervals, start, end, anchor)
|
|
253
|
+
clip = clipping(cigar, is_reverse)
|
|
254
|
+
offset = clip['query_forward_offset']
|
|
255
|
+
overlaps = overlap_flags(intervals)
|
|
256
|
+
phases = phases or ['']*len(intervals)
|
|
257
|
+
rows = []
|
|
258
|
+
for i, ((s, e), phase) in enumerate(zip(intervals, phases), 1):
|
|
259
|
+
left_cut = any(r.start < s == start < r.end for r in full_weak)
|
|
260
|
+
right_cut = any(r.start < e == end < r.end for r in full_weak)
|
|
261
|
+
mean_q, min_q = quality_diagnostics(qualities, s, e)
|
|
262
|
+
rows.append(dict(read_id=read_id, record_id=record_id, tract_id=i,
|
|
263
|
+
query_start_forward=s+offset, query_end_forward=e+offset,
|
|
264
|
+
span_bp=e-s, supported_bp=e-s,
|
|
265
|
+
nonoverlapping_units=(e-s)//6 if strict_definition else None,
|
|
266
|
+
mean_q=mean_q, min_q=min_q, boundary_left_truncated=bool(left_cut),
|
|
267
|
+
boundary_right_truncated=bool(right_cut), touches_telomere_start=s == start,
|
|
268
|
+
touches_telomere_end=e == end, touches_query_start=s == 0, touches_query_end=e == length,
|
|
269
|
+
sequence_left_missing=clip['sequence_left_missing'], sequence_right_missing=clip['sequence_right_missing'],
|
|
270
|
+
overlap_flag=overlaps[i-1], phase_template=phase))
|
|
271
|
+
local_weak = strict_runs(seq[start:end], motif, 1)
|
|
272
|
+
weak_intervals = [(r.start+start, r.end+start) for r in local_weak if r.end-r.start < 6*min_units]
|
|
273
|
+
# Includes extraction-boundary fragments shorter than one complete motif.
|
|
274
|
+
fragments = [(max(start, r.start), min(end, r.end)) for r in full_weak if r.start < end and r.end > start]
|
|
275
|
+
weak_intervals += [(s, e) for s, e in fragments if 0 < e-s < 6*min_units]
|
|
276
|
+
side = anchor['telomeric_query_side']
|
|
277
|
+
proximal_region = ((start, min(s for s, e in intervals)) if intervals else (start, end)) if side == 'right' else (
|
|
278
|
+
(max(e for s, e in intervals), end) if intervals else (start, end))
|
|
279
|
+
weak_flag = (any(s < proximal_region[1] and e > proximal_region[0] for s, e in weak_intervals)
|
|
280
|
+
if side in ('left', 'right') else None)
|
|
281
|
+
boundary_truncated = any(r.start < start < r.end or r.start < end < r.end for r in full_weak)
|
|
282
|
+
boundary_truncated |= any((r.start == 0 and clip['sequence_left_missing']) or
|
|
283
|
+
(r.end == length and clip['sequence_right_missing']) for r in full_weak
|
|
284
|
+
if r.start < end and r.end > start)
|
|
285
|
+
coverage = union_bp(intervals)
|
|
286
|
+
public = dict(variant_motif=motif, variant_present=bool(intervals), variant_tract_count=len(intervals),
|
|
287
|
+
variant_supported_bp=coverage, variant_repeat_equivalents=coverage/6,
|
|
288
|
+
pre_variant_bp=distance, pre_variant_status=status, anchor_status=anchor['anchor_status'],
|
|
289
|
+
telomeric_query_side=side, weak_proximal_variant_evidence=weak_flag,
|
|
290
|
+
variant_boundary_truncated=bool(boundary_truncated), variant_overlap_flag=any(overlaps))
|
|
291
|
+
details = {**clip, **anchor, 'query_start_forward': start+offset, 'query_end_forward': end+offset,
|
|
292
|
+
'query_available_start_forward': offset, 'query_available_end_forward': length+offset,
|
|
293
|
+
'proximal_boundary_forward': (anchor['proximal_boundary_local']+offset
|
|
294
|
+
if anchor['proximal_boundary_local'] is not None else None),
|
|
295
|
+
'local_query_start': start, 'local_query_end': end,
|
|
296
|
+
'missing_sequence': False, 'missing_quality': qualities is None,
|
|
297
|
+
'native_subtelomeric_junction_verified': False}
|
|
298
|
+
return public, rows, details
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def summarize_variant(seq, qualities, start, end, motif, min_units, read_data):
|
|
302
|
+
# Requalification uses only the final extracted telomere, never outside support.
|
|
303
|
+
local = strict_runs(seq[start:end], motif, min_units)
|
|
304
|
+
intervals = [(r.start+start, r.end+start) for r in local]
|
|
305
|
+
return characterize_intervals(seq, qualities, start, end, intervals, motif,
|
|
306
|
+
read_data.get('_variant_cigar', []), read_data['is_reverse'],
|
|
307
|
+
read_data['query_name'], read_data['_ranking_fields']['record_digest'],
|
|
308
|
+
min_units=min_units, phases=[r.phase for r in local], strict_definition=True)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: telometer
|
|
3
|
+
Version: 2.0.2
|
|
4
|
+
Summary: Quantitative digital telomere measurement from nanopore long reads
|
|
5
|
+
Author-email: "Santiago E. Sanchez" <santy.esanchez@gmail.com>
|
|
6
|
+
License: Copyright (c) 2024 The Python Packaging Authority
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
|
25
|
+
|
|
26
|
+
Classifier: Development Status :: 4 - Beta
|
|
27
|
+
Classifier: Intended Audience :: Science/Research
|
|
28
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
29
|
+
Classifier: Operating System :: OS Independent
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
37
|
+
Requires-Python: >=3.8
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE.txt
|
|
40
|
+
Requires-Dist: numpy
|
|
41
|
+
Requires-Dist: pandas
|
|
42
|
+
Requires-Dist: pysam
|
|
43
|
+
Requires-Dist: regex
|
|
44
|
+
Requires-Dist: scipy
|
|
45
|
+
|
|
46
|
+
A simple tool for measuring chromosome-specific telomeres from long-read alignments.
|
|
47
|
+
|
|
48
|
+
[Telometer Github](https://github.com/santiago-es/Telometer)
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
LICENSE.txt
|
|
1
2
|
README.md
|
|
2
3
|
pyproject.toml
|
|
3
4
|
telometer/__init__.py
|
|
4
5
|
telometer/telometer.py
|
|
6
|
+
telometer/telometer_variant_support.py
|
|
7
|
+
telometer/variant_support_v2.py
|
|
5
8
|
telometer.egg-info/PKG-INFO
|
|
6
9
|
telometer.egg-info/SOURCES.txt
|
|
7
10
|
telometer.egg-info/dependency_links.txt
|
|
11
|
+
telometer.egg-info/entry_points.txt
|
|
8
12
|
telometer.egg-info/requires.txt
|
|
9
13
|
telometer.egg-info/top_level.txt
|
telometer-2.0.0/PKG-INFO
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: telometer
|
|
3
|
-
Version: 2.0.0
|
|
4
|
-
Summary: Quantitative digital telomere measurement from nanopore long-reads
|
|
5
|
-
Author-email: "Santiago E. Sanchez" <santy.esanchez@gmail.com>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Operating System :: OS Independent
|
|
9
|
-
Requires-Python: >=3.7
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
Requires-Dist: requests
|
|
12
|
-
Requires-Dist: numpy
|
|
13
|
-
|
|
14
|
-
A simple tool for measuring chromosome-specific telomeres from long-read alignments.
|
|
15
|
-
|
|
16
|
-
[Telometer Github](https://github.com/santiago-es/Telometer)
|
telometer-2.0.0/pyproject.toml
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=77.0.3", "wheel"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "telometer"
|
|
7
|
-
version = "2.0.0"
|
|
8
|
-
description = "Quantitative digital telomere measurement from nanopore long-reads"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.7"
|
|
11
|
-
license = "MIT"
|
|
12
|
-
license-files = ["LICENSE"]
|
|
13
|
-
|
|
14
|
-
authors = [
|
|
15
|
-
{ name = "Santiago E. Sanchez", email = "santy.esanchez@gmail.com" }
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
classifiers = [
|
|
19
|
-
"Programming Language :: Python :: 3",
|
|
20
|
-
"Operating System :: OS Independent"
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
dependencies = [
|
|
24
|
-
"requests",
|
|
25
|
-
"numpy"
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
[tool.setuptools.packages.find]
|
|
29
|
-
where = ["."]
|
|
30
|
-
include = ["telometer*"]
|
|
31
|
-
exclude = ["build*", "dist*"]
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
__version__ = '1.01'
|
|
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
|
-
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: telometer
|
|
3
|
-
Version: 2.0.0
|
|
4
|
-
Summary: Quantitative digital telomere measurement from nanopore long-reads
|
|
5
|
-
Author-email: "Santiago E. Sanchez" <santy.esanchez@gmail.com>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Operating System :: OS Independent
|
|
9
|
-
Requires-Python: >=3.7
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
Requires-Dist: requests
|
|
12
|
-
Requires-Dist: numpy
|
|
13
|
-
|
|
14
|
-
A simple tool for measuring chromosome-specific telomeres from long-read alignments.
|
|
15
|
-
|
|
16
|
-
[Telometer Github](https://github.com/santiago-es/Telometer)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|