Skip to content

Commit 07801ea

Browse files
committed
Recode getSequenceInRange() to return str directly
Also optimise it to unpack high and low nibbles explicitly.
1 parent 6626415 commit 07801ea

3 files changed

Lines changed: 61 additions & 20 deletions

File tree

pysam/libcalignedsegment.pyx

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ from cpython cimport array as c_array
6565
from cpython.bytearray cimport PyByteArray_FromStringAndSize, PyByteArray_GET_SIZE
6666
from cpython.bytes cimport PyBytes_FromStringAndSize
6767
from cpython.unicode cimport PyUnicode_DATA, PyUnicode_1BYTE_DATA, PyUnicode_1BYTE_KIND, \
68-
PyUnicode_GET_LENGTH, PyUnicode_KIND, PyUnicode_WRITE
68+
PyUnicode_GET_LENGTH, PyUnicode_KIND, PyUnicode_New, PyUnicode_WRITE
6969
from libc.string cimport memset, strchr
7070
from libc.stdint cimport INT8_MIN, INT16_MIN, INT32_MIN, \
7171
INT8_MAX, INT16_MAX, INT32_MAX, \
@@ -632,29 +632,31 @@ cdef inline int32_t getQueryEnd(bam1_t *src) except -1:
632632
return end_offset
633633

634634

635-
cdef inline bytes getSequenceInRange(bam1_t *src,
636-
uint32_t start,
637-
uint32_t end):
635+
cdef inline str getSequenceInRange(const bam1_t *src, uint32_t start, uint32_t end):
638636
"""return python string of the sequence in a bam1_t object.
639637
"""
638+
seq = PyUnicode_New(end - start, 127)
639+
cdef char *dest = <char *> PyUnicode_1BYTE_DATA(seq)
640+
cdef unsigned int di = 0
640641

641-
cdef uint8_t * p
642-
cdef uint32_t k
643-
cdef char * s
642+
cdef const uint8_t *p = bam_get_seq(src)
643+
cdef unsigned int i = start // 2
644644

645-
if not src.core.l_qseq:
646-
return None
645+
if start & 1:
646+
dest[di] = seq_nt16_str[p[i] & 0x0f]
647+
i += 1
648+
di += 1
647649

648-
seq = PyBytes_FromStringAndSize(NULL, end - start)
649-
s = <char*>seq
650-
p = pysam_bam_get_seq(src)
650+
for _ in range(i, end // 2):
651+
dest[di] = seq_nt16_str[p[i] >> 4]
652+
dest[di+1] = seq_nt16_str[p[i] & 0x0f]
653+
i += 1
654+
di += 2
651655

652-
for k from start <= k < end:
653-
# equivalent to seq_nt16_str[bam1_seqi(s, i)] (see bam.c)
654-
# note: do not use string literal as it will be a python string
655-
s[k-start] = seq_nt16_str[p[k//2] >> 4 * (1 - k%2) & 0xf]
656+
if end & 1:
657+
dest[di] = seq_nt16_str[p[i] >> 4]
656658

657-
return charptr_to_bytes(seq)
659+
return seq
658660

659661

660662
#####################################################################
@@ -1530,8 +1532,7 @@ cdef class AlignedSegment:
15301532
self.cache.query_sequence = None
15311533
return None
15321534

1533-
self.cache.query_sequence = force_str(getSequenceInRange(
1534-
src, 0, src.core.l_qseq))
1535+
self.cache.query_sequence = getSequenceInRange(src, 0, src.core.l_qseq)
15351536
return self.cache.query_sequence
15361537

15371538
def __set__(self, seq):
@@ -1898,7 +1899,7 @@ cdef class AlignedSegment:
18981899
start = getQueryStart(src)
18991900
end = getQueryEnd(src)
19001901

1901-
self.cache.query_alignment_sequence = force_str(getSequenceInRange(src, start, end))
1902+
self.cache.query_alignment_sequence = getSequenceInRange(src, start, end)
19021903
return self.cache.query_alignment_sequence
19031904

19041905
property query_alignment_qualities:

tests/AlignedSegment_bench.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,14 @@ def test_reverse_complement(pytestconfig, benchmark):
3535
if pytestconfig.get_verbosity() > 0: print(f"LEN={5 * count} ", end="", flush=True)
3636
result = benchmark(pysam.reverse_complement, "AATGC" * count)
3737
assert result == "GCATT" * count
38+
39+
40+
def test_query_sequence(pytestconfig, benchmark):
41+
seq = "AATGC" * (int(os.environ.get("LEN", 300)) // 5)
42+
if pytestconfig.get_verbosity() > 0: print(f"LEN={len(seq)} ", end="", flush=True)
43+
44+
read = pysam.AlignedSegment()
45+
read.query_sequence = seq
46+
# What we actually need for meaningful benchmarking is a non-caching non-property to call
47+
result = benchmark(lambda : read.query_sequence)
48+
assert result == seq

tests/AlignedSegment_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1951,6 +1951,35 @@ def test_longarray_to_qualstr(self):
19511951
pysam.array_to_qualitystring(qual_array)
19521952

19531953

1954+
@pytest.mark.parametrize("seq", [
1955+
"A",
1956+
"AT",
1957+
"GCA",
1958+
"ATGC",
1959+
"AATTG",
1960+
pytest.param("ABCDGHKMNRSTVWY", id="iupac"),
1961+
])
1962+
def test_sequence_unpacking(seq):
1963+
a = pysam.AlignedSegment()
1964+
a.query_sequence = seq
1965+
assert a.query_sequence == seq
1966+
1967+
1968+
@pytest.mark.parametrize("start,stop", [
1969+
(0, 0), (1, 0), (2, 0), (3, 0),
1970+
(0, 1), (1, 1), (2, 1), (3, 1),
1971+
(0, 2), (1, 2), (2, 2), (3, 2),
1972+
(0, 3), (1, 3), (2, 3), (3, 3),
1973+
])
1974+
def test_subsequence_unpacking(start, stop):
1975+
a = pysam.AlignedSegment()
1976+
a.query_sequence = "ABCDGHKMNRSTVWY"
1977+
start_clip = f"{start}S" if start > 0 else ""
1978+
stop_clip = f"{stop}S" if stop > 0 else ""
1979+
a.cigarstring = f"{start_clip}{a.query_length - start - stop}M{stop_clip}"
1980+
assert a.query_alignment_sequence == a.query_sequence[start : a.query_length - stop]
1981+
1982+
19541983
@pytest.mark.parametrize("seq,revcomp", [
19551984
("", ""),
19561985
("A", "T"),

0 commit comments

Comments
 (0)