Indeed, with suffix arrays, you can map a kmer in ~0.0007s. The way to go is to build a suffix array (for instance using PySAIS package) and then do a convoluted binary search of the four possible kmers. A full implementation of such mapper can be found here.
To build suffix array
from Bio import SeqIO
from Bio.Seq import Seq
from PySAIS import sais
from bisect import bisect_right
# load the genome
kmer_genome_file = 'my_genome.fasta'
ffile = SeqIO.parse(kmer_genome_file, "fasta")
sequences = []
scf_names = []
for seq_record in ffile:
scf_names.append(seq_record.name)
sequences.append(str(seq_record.seq).upper())
ffile.close()
# paste all scaffolds into a single string and build a suffix array
genome = '$'.join(sequences)
sa = sais(genome)
To transform genome coordinate to scaffold/position coordinates we define a function evaluated2assembly_position
def evaluated2assembly_position(eval_pos):
scf_index = bisect_right(scf_sizes, eval_pos)
eval_pos - scf_sizes[scf_index - 1]
if scf_index > 0:
return((scf_names[scf_index], eval_pos - scf_sizes[scf_index - 1]))
else:
return((scf_names[scf_index], eval_pos))
and finally the function for a convoluted binary search (assuming all kmers being 21 nt long and having the unknown nucleotide in the middle, on 11th nt). Note that you need to map to both strands of DNA, which is equivalent of mapping kmer and it's reverse complementary sequence
def searchKmer(self, kmer):
reverse_complementary_kmer = str(Seq(kmer).reverse_complement())
alignments = []
for strand in ['+', '-']:
if strand == '-':
kmer = reverse_complementary_kmer
L_kmer = kmer[0:10]
l = 0
r = len(sa) - 1
while l <= r:
m = (l + r) // 2
eval_pos = sa[m]
L_genome_kmer = genome[eval_pos:(eval_pos + 10)]
if L_kmer == L_genome_kmer:
r_L_converged = r
for N in ['A', 'C', 'G', 'T']:
kmer = kmer[0:10] + N + kmer[11:21]
while l <= r:
m = (l + r) // 2
eval_pos = sa[m]
genome_kmer = genome[eval_pos:(eval_pos + 21)]
if kmer == genome_kmer:
m_hit = m
# following ms
while kmer == genome_kmer:
scf, pos = evaluated2assembly_position(eval_pos)
alignments.append([scf, pos, strand, N])
m += 1
eval_pos = sa[m]
genome_kmer = genome[eval_pos:(eval_pos + 21)]
# previous ms
m = m_hit - 1
eval_pos = sa[m]
genome_kmer = genome[eval_pos:(eval_pos + 21)]
while kmer == genome_kmer:
scf, pos = evaluated2assembly_position(eval_pos)
alignments.append([scf, pos, strand, N])
m -= 1
eval_pos = sa[m]
genome_kmer = genome[eval_pos:(eval_pos + 21)]
break
elif genome_kmer < kmer:
l = m + 1
else:
r = m - 1
r = r_L_converged # recover the r after converging on the left sub-kmer
break
elif L_genome_kmer < L_kmer:
l = m + 1
else:
r = m - 1
return(alignments)
Taking one of the fist kmers in the scaffold one to test it
searchKmer('ACGGCCACCCNGTGTCGTTGT')
Out[23]: [['scaf000001', 28, 'G']]
And a softmasked kmer that we can expect more than once
kmer = 'ccgaaccacgNtggaggcctg'.upper()
searchKmer(kmer)
Out[31]:
[['CCGAACCACGATGGAGGCCTG', '1_Tps_b3v07_scaf000306', 5196, 'A'],
['CCGAACCACGCTGGAGGCCTG', '1_Tps_b3v07_scaf008224', 15781, 'C'],
['CCGAACCACGGTGGAGGCCTG', '1_Tps_b3v07_scaf001152', 71261, 'G'],
['CCGAACCACGTTGGAGGCCTG', '1_Tps_b3v07_scaf002780', 1721, 'T']]
So to map all your kmers (assuming a file with a kmer per line)
kmer_file_name = 'my_kmer_file.txt'
with open(kmer_file_name, 'r') as kmer_file:
kmers = [kmer.rstrip() for kmer in kmer_file]
mapping_list = [searchKmer(kmer) for kmer in kmers]
Stuff can be probably done in a more elegant way, but this is a working solution that scales to any genome size.