xengsort 2.0.8__tar.gz → 2.1.0__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.
Files changed (56) hide show
  1. {xengsort-2.0.8/xengsort.egg-info → xengsort-2.1.0}/PKG-INFO +18 -2
  2. {xengsort-2.0.8 → xengsort-2.1.0}/README.md +15 -0
  3. {xengsort-2.0.8 → xengsort-2.1.0}/pyproject.toml +1 -1
  4. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/cptasks_kmers.py +67 -23
  5. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/dnaencode.py +14 -3
  6. xengsort-2.1.0/xengsort/dnaencode_fast.py +156 -0
  7. xengsort-2.1.0/xengsort/fastcash_info.py +156 -0
  8. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/fastcash_main.py +73 -50
  9. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/hash_s3c_fbcbvb.py +18 -2
  10. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/io/fastaio.py +11 -6
  11. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/io/fastqio.py +206 -17
  12. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/io/generalio.py +69 -14
  13. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/io/hashio.py +142 -59
  14. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/io/textio.py +0 -1
  15. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/kmers.py +136 -10
  16. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/lowlevel/bitarray.py +62 -12
  17. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/lowlevel/conpro.py +20 -4
  18. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/lowlevel/debug.py +41 -1
  19. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/lowlevel/libc.py +30 -6
  20. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/lowlevel/llvm.py +192 -1
  21. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/srhash.py +97 -8
  22. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/subtable_hashfunctions.py +12 -6
  23. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/xengsort/config/classify.yaml +16 -1
  24. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/xengsort/xengsort_classify.py +118 -19
  25. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/xengsort/xengsort_index.py +5 -5
  26. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/xengsort/xengsort_main.py +4 -0
  27. {xengsort-2.0.8 → xengsort-2.1.0/xengsort.egg-info}/PKG-INFO +18 -2
  28. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort.egg-info/SOURCES.txt +1 -0
  29. xengsort-2.0.8/xengsort/fastcash_info.py +0 -83
  30. {xengsort-2.0.8 → xengsort-2.1.0}/LICENSE +0 -0
  31. {xengsort-2.0.8 → xengsort-2.1.0}/MANIFEST.in +0 -0
  32. {xengsort-2.0.8 → xengsort-2.1.0}/setup.cfg +0 -0
  33. {xengsort-2.0.8 → xengsort-2.1.0}/tests/test_trimmed_reads.py +0 -0
  34. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/fastcash_optimize.py +0 -0
  35. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/hash_minimizer.py +0 -0
  36. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/hash_srh_fbcbvb.py +0 -0
  37. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/io/binaryio.py +0 -0
  38. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/io/seqio.py +0 -0
  39. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/io/xorio.py +0 -0
  40. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/lowlevel/aligned_arrays.py +0 -0
  41. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/lowlevel/intbitarray.py +0 -0
  42. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/lowlevel/numbautils.py +0 -0
  43. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/lowlevel/packedarray.py +0 -0
  44. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/mask.py +0 -0
  45. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/mathutils.py +0 -0
  46. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/parameters.py +0 -0
  47. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/values/xenograft.py +0 -0
  48. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/xengsort/config/index.yaml +0 -0
  49. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/xengsort/xengsort_info.py +0 -0
  50. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/xengsort/xengsort_load.py +0 -0
  51. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/xengsort/xengsort_remove.py +0 -0
  52. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort/xengsort/xengsort_weak.py +0 -0
  53. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort.egg-info/dependency_links.txt +0 -0
  54. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort.egg-info/entry_points.txt +0 -0
  55. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort.egg-info/requires.txt +0 -0
  56. {xengsort-2.0.8 → xengsort-2.1.0}/xengsort.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: xengsort
3
- Version: 2.0.8
3
+ Version: 2.1.0
4
4
  Summary: xenograft sorting of FASTQ reads into five categories: host, graft, both, neither, ambiguous.
5
5
  Author-email: Jens Zentgraf <zentgraf@cs.uni-saarland.de>, Sven Rahmann <rahmann@cs.uni-saarland.de>
6
6
  License: MIT License
@@ -43,6 +43,7 @@ Requires-Dist: numba
43
43
  Requires-Dist: pytest
44
44
  Requires-Dist: jsonargparse>=4.29.0
45
45
  Requires-Dist: pip
46
+ Dynamic: license-file
46
47
 
47
48
  # xengsort: Fast lightweight accurate xenograft sorting
48
49
 
@@ -286,6 +287,21 @@ Further parameters and options are:
286
287
  - `--filter`: With this flag, only the graft reads are output.
287
288
  - `--count`: With this flag, only the number of reads in each category is counted, but the reads are not sorted.
288
289
 
290
+ ### How to filter bisulfite sequencing data
291
+ In bisulfite sequencing data we have a conversion from C to T and from G to A in the reverse complement. The standard xengsort index classifies most of the reads as `neither`.
292
+ To address this issue, we have created a `--bisulfite` mode.
293
+ This parameter must be set during the index building step.
294
+ If set, all Cs are translated to Ts in the reference and add the $k$-mers are added to the index, the same for G to A.
295
+ This nearly doubles the number of $k$-mers in the index (`-n`)
296
+
297
+ ```
298
+ xengsort index --index myindex -H host.fa.gz -G graft.fa.gz -n 9_000_000_000 --bisulfite [OPTIONS]
299
+ ```
300
+
301
+ If an index is build using the bisulfite parameter, also the read is converted in the classification step.
302
+ It is not necessary to change the parameters of the classification step.
303
+ Currently, it only supports the count-based classification.
304
+
289
305
 
290
306
  Please report a [GitLab issue](https://gitlab.com/genomeinformatics/xengsort/-/issues) in case of problems.
291
307
 
@@ -240,6 +240,21 @@ Further parameters and options are:
240
240
  - `--filter`: With this flag, only the graft reads are output.
241
241
  - `--count`: With this flag, only the number of reads in each category is counted, but the reads are not sorted.
242
242
 
243
+ ### How to filter bisulfite sequencing data
244
+ In bisulfite sequencing data we have a conversion from C to T and from G to A in the reverse complement. The standard xengsort index classifies most of the reads as `neither`.
245
+ To address this issue, we have created a `--bisulfite` mode.
246
+ This parameter must be set during the index building step.
247
+ If set, all Cs are translated to Ts in the reference and add the $k$-mers are added to the index, the same for G to A.
248
+ This nearly doubles the number of $k$-mers in the index (`-n`)
249
+
250
+ ```
251
+ xengsort index --index myindex -H host.fa.gz -G graft.fa.gz -n 9_000_000_000 --bisulfite [OPTIONS]
252
+ ```
253
+
254
+ If an index is build using the bisulfite parameter, also the read is converted in the classification step.
255
+ It is not necessary to change the parameters of the classification step.
256
+ Currently, it only supports the count-based classification.
257
+
243
258
 
244
259
  Please report a [GitLab issue](https://gitlab.com/genomeinformatics/xengsort/-/issues) in case of problems.
245
260
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "xengsort"
8
- version = "2.0.8"
8
+ version = "2.1.0"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -4,15 +4,18 @@ Functions for compiling ConsumerProducerTasks for k-mers
4
4
  """
5
5
 
6
6
  import numpy as np
7
- from numba import njit, int32, int64, uint64
7
+ from numba import njit, uint8, int32, int64, uint64
8
8
 
9
- from .dnaencode import compile_revcomp_and_canonical_code
9
+ from .dnaencode import compile_revcomp_and_canonical_code as compile_revcomp_and_canonical_code_old
10
+ from .dnaencode import _TABLE_DNA_TO_2BITS, compile_quick_dna_to_2bits
11
+ from .dnaencode import quick_dna_to_2bits as quick_dna_to_2bits_old
12
+ from .dnaencode_fast import compile_revcomp_and_canonical_code as compile_revcomp_and_canonical_code_new
13
+ from .dnaencode_fast import quick_dna_to_2bits as quick_dna_to_2bits_new
10
14
  from .lowlevel import debug
11
15
  from .lowlevel.conpro import \
12
16
  find_buffer_for_reading, find_buffer_for_writing, \
13
17
  mark_buffer_for_reading, mark_buffer_for_writing, \
14
18
  mark_my_buffers_failed
15
- from .dnaencode import quick_dna_to_2bits
16
19
 
17
20
 
18
21
  """
@@ -28,7 +31,9 @@ compile_cptask_scatter_kmers_from_buffers():
28
31
 
29
32
  def compile_cptask_scatter_kmers_from_linemarked(
30
33
  mask, rcmode, hf0,
31
- nsubtables, noutbuffers_per_subtable, outbufsize):
34
+ nsubtables, noutbuffers_per_subtable, outbufsize,
35
+ new_encoding=False,
36
+ bisulfite=False):
32
37
  """
33
38
  Compile a cptask function that
34
39
  creates (gapped) k-mers from linemarked buffers.
@@ -38,16 +43,33 @@ def compile_cptask_scatter_kmers_from_linemarked(
38
43
  - nsubtables: the number of subtables
39
44
  - noutbuffers_per_subtable: number of output buffers per subtable
40
45
  - outbufsize: the output buffer size for which to compile this function
46
+ -
41
47
  """
42
48
  debugprint0, debugprint1, debugprint2 = debug.debugprint
43
49
  both = (rcmode[0].lower() == "b")
50
+ if bisulfite:
51
+ if new_encoding:
52
+ raise NotImplementedError('WGBS with new encoding not supported')
53
+
54
+ else:
55
+ ct_table = _TABLE_DNA_TO_2BITS.copy()
56
+ ct_table[99] = 3
57
+ ct_table[67] = 3
58
+ quick_dna_to_2bits_ct = compile_quick_dna_to_2bits(ct_table)
59
+
60
+ ga_table = _TABLE_DNA_TO_2BITS.copy()
61
+ ga_table[103] = 0
62
+ ga_table[71] = 0
63
+ quick_dna_to_2bits_ga = compile_quick_dna_to_2bits(ga_table)
64
+ else:
65
+ quick_dna_to_2bits = quick_dna_to_2bits_new if new_encoding else quick_dna_to_2bits_old
44
66
 
45
67
  k, w, tpl = mask.k, mask.w, mask.tuple
46
68
  assert len(tpl) == k
47
69
  if k < 1 or k > 32:
48
70
  raise ValueError(f"Only 1 <= k <= 32 is supported, but {k=}.")
49
71
  codemask = uint64(4**(k - 1) - 1)
50
- revcomp, ccode = compile_revcomp_and_canonical_code(k, rcmode)
72
+ revcomp, ccode = compile_revcomp_and_canonical_code_new(k, rcmode) if new_encoding else compile_revcomp_and_canonical_code_old(k, rcmode)
51
73
  if mask.is_contiguous:
52
74
  debugprint1(f"- compiling cptask_scatter_kmers for contiguous {k}-mers.")
53
75
  else:
@@ -55,7 +77,7 @@ def compile_cptask_scatter_kmers_from_linemarked(
55
77
 
56
78
  @njit(nogil=True, locals=dict(
57
79
  code=uint64, st=uint64, sk=uint64,
58
- j=int32, stj=int32, p=int32, wait=int64))
80
+ j=int32, stj=int32, p=int32, wait=int64, error=int32))
59
81
  def _store(code, outidx, outpos, outbuffers, outcontrol):
60
82
  st, sk = hf0(code)
61
83
  j = outidx[st]
@@ -180,6 +202,8 @@ def compile_cptask_scatter_kmers_from_linemarked(
180
202
  nactive = -1 # active input buffer
181
203
  outidx = np.full(nsubtables, (-1), dtype=np.int32)
182
204
  outpos = np.full(nsubtables, outbufsize, dtype=np.int32)
205
+ if bisulfite:
206
+ seq_buffer = np.empty(inbuffers[0].size, dtype=np.uint8)
183
207
  while True:
184
208
  nactive, wait = find_buffer_for_reading(incontrol, nactive)
185
209
  wait_read += wait
@@ -193,10 +217,22 @@ def compile_cptask_scatter_kmers_from_linemarked(
193
217
  errorcode = 0
194
218
  linemarks = linemarkbuffers[nactive]
195
219
  for i in range(nseqs):
196
- sq = active_buffer[linemarks[i, 0]:linemarks[i, 1]]
197
- quick_dna_to_2bits(sq)
198
- (wait, _errorcode) = _process(
199
- sq, outidx, outpos, outbuffers, outcontrol)
220
+ if bisulfite:
221
+ seq_len = linemarks[i, 1] - linemarks[i, 0]
222
+ seq_buffer[0:seq_len] = active_buffer[linemarks[i, 0]:linemarks[i, 1]]
223
+ quick_dna_to_2bits_ct(seq_buffer[0:seq_len])
224
+ (wait, _errorcode) = _process(
225
+ seq_buffer[:seq_len], outidx, outpos, outbuffers, outcontrol)
226
+
227
+ seq_buffer[0:seq_len] = active_buffer[linemarks[i, 0]:linemarks[i, 1]]
228
+ quick_dna_to_2bits_ga(seq_buffer[0:seq_len])
229
+ (wait, _errorcode) = _process(
230
+ seq_buffer[:seq_len], outidx, outpos, outbuffers, outcontrol)
231
+ else:
232
+ sq = active_buffer[linemarks[i, 0]:linemarks[i, 1]]
233
+ quick_dna_to_2bits(sq)
234
+ (wait, _errorcode) = _process(
235
+ sq, outidx, outpos, outbuffers, outcontrol)
200
236
  wait_write += wait
201
237
  errorcode |= _errorcode
202
238
  if errorcode:
@@ -229,6 +265,7 @@ def compile_cptask_scatter_kmers_from_linemarked(
229
265
  if j >= 0:
230
266
  # assert outpos[st] > 0
231
267
  outcontrol[j, 7] = outpos[st]
268
+ outcontrol[j, 6] = 1 # buffer type k-mer codes
232
269
  mark_buffer_for_reading(outcontrol, j)
233
270
  debugprint2("- ending: cptask_scatter_kmers", errorcode)
234
271
  return (wait_read, wait_write, -errorcode)
@@ -238,7 +275,7 @@ def compile_cptask_scatter_kmers_from_linemarked(
238
275
 
239
276
  def compile_cptask_scatter_kmers_position_from_linemarked(
240
277
  mask, values, rcmode, hf0,
241
- nsubtables, noutbuffers_per_subtable, outbufsize):
278
+ nsubtables, noutbuffers_per_subtable, outbufsize, new_encoding=False):
242
279
  """
243
280
  Compile a cptask function that
244
281
  creates (gapped) k-mers and value from linemarked buffers.
@@ -251,14 +288,16 @@ def compile_cptask_scatter_kmers_position_from_linemarked(
251
288
  - outbufsize: the output buffer size for which to compile this function
252
289
  """
253
290
  debugprint0, debugprint1, debugprint2 = debug.debugprint
291
+ dassert = debug.dassert
254
292
  both = (rcmode[0].lower() == "b")
293
+ quick_dna_to_2bits = quick_dna_to_2bits_new if new_encoding else quick_dna_to_2bits_old
255
294
 
256
295
  k, w, tpl = mask.k, mask.w, mask.tuple
257
296
  assert len(tpl) == k
258
297
  if k < 1 or k > 32:
259
298
  raise ValueError(f"Only 1 <= k <= 32 is supported, but {k=}.")
260
299
  codemask = uint64(4**(k - 1) - 1)
261
- revcomp, ccode = compile_revcomp_and_canonical_code(k, rcmode)
300
+ revcomp, ccode = compile_revcomp_and_canonical_code_new(k, rcmode) if new_encoding else compile_revcomp_and_canonical_code_old(k, rcmode)
262
301
  if mask.is_contiguous:
263
302
  debugprint1(f"- compiling cptask_scatter_kmers for contiguous {k}-mers.")
264
303
  else:
@@ -281,9 +320,9 @@ def compile_cptask_scatter_kmers_position_from_linemarked(
281
320
  outcontrol[j, 6] = 1 # buffer type k-mer codes
282
321
  mark_buffer_for_reading(outcontrol, j)
283
322
  stj = j - st * noutbuffers_per_subtable # the "local" j for subtable st
284
- assert 0 <= stj < noutbuffers_per_subtable
323
+ dassert(0 <= stj < noutbuffers_per_subtable, __name__, '0 <= stj < noutbuffers_per_subtable', (stj, noutbuffers_per_subtable))
285
324
  else:
286
- assert j == -1
325
+ dassert(j == -1, __name__, 'j == -1', j)
287
326
  stj = -1
288
327
  stoutcontrol = outcontrol[(st * noutbuffers_per_subtable):(st + 1) * noutbuffers_per_subtable]
289
328
  stj, wait = find_buffer_for_writing(stoutcontrol, stj)
@@ -382,14 +421,15 @@ def compile_cptask_scatter_kmers_position_from_linemarked(
382
421
  # ininfos[i]: linemarks for input buffer i
383
422
  # outcontrol[B, 7]: k-mers in buffer B
384
423
  debugprint2("- running: cptask_scatter_kmers; outbuffers shape =", outbuffers.shape)
385
- assert inbuffers.ndim == 2
386
- assert outbuffers.ndim == 2
424
+ dassert(inbuffers.ndim == 2, __name__, 'inbuffers.ndim == 2', inbuffers.ndim, message='check input buffer dimensions')
425
+ dassert(outbuffers.ndim == 2, __name__, 'outbuffers.ndim == 2', outbuffers.ndim, message='check output buffer dimensions')
426
+
387
427
  M, N = ininfos.shape
388
- assert N % 6 == 0 # We have 6 entries per linemark. Start and end of record and read and chromosome and positions
428
+ dassert(N % 6 == 0, __name__, 'N % 6 == 0', N, message='We have 6 entries per linemark. Start and end of record and read and chromosome and positions')
389
429
  linemarkbuffers = ininfos.reshape(M, N // 6, 6)
390
- assert outbuffers.shape[0] == nsubtables * noutbuffers_per_subtable
391
- assert outbuffers.shape[1] >= outbufsize
392
- assert outinfos.shape[1] == 0
430
+ dassert(outbuffers.shape[0] == nsubtables * noutbuffers_per_subtable, __name__, 'outbuffers.shape[0] == nsubtables * noutbuffers_per_subtable', (outbuffers.shape[0], nsubtables, noutbuffers_per_subtable), message='Check number of output buffer per subtable')
431
+ dassert(outbuffers.shape[1] >= outbufsize, __name__, 'outbuffers.shape[1] >= outbufsize', (outbuffers.shape[1], outbufsize))
432
+ dassert(outinfos.shape[1] == 0, __name__, 'outinfos.shape[1] == 0', outinfos.shape[1])
393
433
 
394
434
  wait_read = wait_write = 0
395
435
  nactive = -1 # active input buffer
@@ -404,7 +444,7 @@ def compile_cptask_scatter_kmers_position_from_linemarked(
404
444
  active_buffer = inbuffers[nactive]
405
445
  nseqs = incontrol[nactive, 7]
406
446
  buffer_type = incontrol[nactive, 6]
407
- assert buffer_type == 0 # 0 means sequence buffer
447
+ dassert(buffer_type == 0, __name__, 'buffer_type == 0', buffer_type, message='0 means sequence buffer')
408
448
 
409
449
  errorcode = 0
410
450
  linemarks = linemarkbuffers[nactive]
@@ -430,6 +470,7 @@ def compile_cptask_scatter_kmers_position_from_linemarked(
430
470
  if j >= 0:
431
471
  # assert outpos[st] > 0
432
472
  outcontrol[j, 7] = outpos[st]
473
+ outcontrol[j, 6] = 1 # buffer type k-mer codes
433
474
  mark_buffer_for_reading(outcontrol, j)
434
475
  debugprint2("- ending: cptask_scatter_kmers", errorcode)
435
476
  return (wait_read, wait_write, -errorcode)
@@ -461,7 +502,6 @@ def compile_cptask_insert_subkey_value(
461
502
  assert outbuffers.shape[1] >= maxwalk + 1
462
503
  assert outinfos.shape[1] == 0
463
504
  debugprint2("- running: cptask_insert_subkey_value; subtable", st)
464
-
465
505
  wait_read = wait_write = 0
466
506
  nactive = -1 # active input buffer
467
507
  active_buffer = inbuffers[0] # irrelevant
@@ -506,6 +546,7 @@ def compile_cptask_insert_filtered_subkeys(
506
546
  maxfailures=0,
507
547
  maxwalk=1000):
508
548
  debugprint0, debugprint1, debugprint2 = debug.debugprint
549
+ dassert = debug.dassert
509
550
  if myfilter is not None:
510
551
  wwo = "with"
511
552
  lookup_and_insert_in_subfilter = myfilter.private.lookup_and_insert_in_subfilter
@@ -552,7 +593,10 @@ def compile_cptask_insert_filtered_subkeys(
552
593
  active_buffer = inbuffers[nactive]
553
594
  nsubkeys = incontrol[nactive, 7]
554
595
  buffer_type = incontrol[nactive, 6]
555
- assert buffer_type == 1 # 1 means k-mer buffer (or subkeys)
596
+ if buffer_type != 1:
597
+ print(buffer_type, nsubkeys)
598
+ assert buffer_type == 1
599
+ # dassert(buffer_type == 1, __name__, 'buffer_type == 1', buffer_type, message='0 means sequence buffer')
556
600
 
557
601
  # consume the buffer
558
602
  for subkey in active_buffer[:nsubkeys]:
@@ -34,6 +34,15 @@ def _dna_to_2bits(x, table):
34
34
  x[i] = table[x[i]]
35
35
 
36
36
 
37
+ def compile_quick_dna_to_2bits(table):
38
+ @njit(nogil=True)
39
+ def _quick_dna_to_2bits(x):
40
+ for i in range(len(x)):
41
+ x[i] = table[x[i]]
42
+
43
+ return _quick_dna_to_2bits
44
+
45
+ # this is the one to use!
37
46
  @njit(nogil=True)
38
47
  def quick_dna_to_2bits(x):
39
48
  for i in range(len(x)):
@@ -51,7 +60,7 @@ def parallel_dna_to_2bits(x):
51
60
  def dna_to_2bits(seq, table=_TABLE_DNA_TO_2BITS):
52
61
  # we expect seq to be a bytearray
53
62
  # xx = np.array(seq, dtype=np.uint8)
54
- if type(seq) == bytes:
63
+ if isinstance(seq, bytes):
55
64
  xx = np.frombuffer(bytearray(seq), dtype=np.uint8)
56
65
  else:
57
66
  xx = np.frombuffer(seq, dtype=np.uint8)
@@ -70,7 +79,7 @@ def qcode_to_dnastr(qcode, q, table=_TABLE_BITS_TO_DNASTR):
70
79
  @njit(nogil=True, locals=dict(base=uint64))
71
80
  def write_qcode_to_buffer(qcode, q, buf, start):
72
81
  for i in range(q):
73
- base = qcode >> (2 * (q - i - 1)) & 3
82
+ base = (qcode >> (2 * (q - i - 1))) & 3
74
83
  buf[start + i] = uint8(base)
75
84
 
76
85
 
@@ -245,6 +254,7 @@ def compile_twobit_to_codes(tmask, rcmode, invalid=U64_MINUSONE):
245
254
 
246
255
  @njit(nogil=True, locals=dict(code=uint64))
247
256
  def twobit_to_codes(seq, out, start=0, n=-1):
257
+ """write n (or all) canonical k-mer codes from seq[start...] into out buffer"""
248
258
  if n == -1:
249
259
  n = len(seq) - w + 1 - start
250
260
  for i in range(start, start + n):
@@ -260,7 +270,8 @@ def compile_twobit_to_codes(tmask, rcmode, invalid=U64_MINUSONE):
260
270
  out[i - start] = code
261
271
 
262
272
  @njit(nogil=True, locals=dict(code=uint64))
263
- def twobit_to_code(seq, start):
273
+ def twobit_to_code(seq, start=0):
274
+ """return a single canonical code at seq[start...]"""
264
275
  code = 0
265
276
  for j in tmask:
266
277
  c = seq[start + j]
@@ -0,0 +1,156 @@
1
+ """
2
+ Fast DNA encoding and computation of the reverse complement A=00, C=01, T=10, G=11, U=10
3
+ Other characters are not treated correctly, except N=7>3 and n = 6>3
4
+ """
5
+
6
+ from numba import njit, uint64
7
+ import numpy as np
8
+ from .mask import Mask
9
+
10
+ U64_MINUSONE = uint64(np.iinfo(np.uint64).max)
11
+
12
+
13
+ @njit(nogil=True)
14
+ def quick_dna_to_2bits(x):
15
+ for i in range(len(x)):
16
+ x[i] = (x[i] >> 1) & 7
17
+
18
+
19
+ @njit(nogil=True)
20
+ def dna_to_2bits(x, y):
21
+ for i in range(len(x)):
22
+ y[i] = (x[i] >> 1) & 7
23
+
24
+
25
+ # no need to njit!
26
+ def _get_table_2bits_to_dna():
27
+ b = np.full(256, 35, dtype=np.uint8) # fill with b'#'
28
+ b[0] = 65
29
+ b[1] = 67
30
+ b[2] = 84
31
+ b[3] = 71
32
+ b[6] = 78
33
+ b[7] = 78
34
+ return b
35
+
36
+
37
+ _TABLE_2BITS_TO_DNA = _get_table_2bits_to_dna()
38
+
39
+
40
+ @njit(nogil=True)
41
+ def twobits_to_dna_inplace(buf, start=0, end=0):
42
+ if end <= 0:
43
+ end = len(buf) - end
44
+ for i in range(start, end):
45
+ buf[i] = _TABLE_2BITS_TO_DNA[buf[i]]
46
+
47
+
48
+ _TABLE_BITS_TO_DNASTR = ["A", "C", "T", "G"]
49
+
50
+
51
+ def qcode_to_dnastr(qcode, q, table=_TABLE_BITS_TO_DNASTR):
52
+ qc = int(qcode)
53
+ return "".join([table[((qc >> (2 * (q - i - 1))) & 3)] for i in range(q)])
54
+
55
+
56
+ def compile_revcomp_and_canonical_code(q, rcmode):
57
+ """
58
+ return pair of functions (revcomp_code_q, canonical_code_q)
59
+ specialized for q-gram codes for the given value of q.
60
+ It is expected that LLVM optimization does loop unrolling.
61
+ """
62
+
63
+ mask = uint64(int(q * '10', 2))
64
+ shift = uint64(64 - 2 * q)
65
+
66
+ @njit(nogil=True, locals=dict(value=uint64))
67
+ def reverse_in_pairs(value):
68
+ value = ((value & 0x3333333333333333) << 2) | ((value & 0xCCCCCCCCCCCCCCCC) >> 2)
69
+ value = ((value & 0x0F0F0F0F0F0F0F0F) << 4) | ((value >> 4) & 0x0F0F0F0F0F0F0F0F)
70
+ value = ((value & 0x00FF00FF00FF00FF) << 8) | ((value >> 8) & 0x00FF00FF00FF00FF)
71
+ value = ((value & 0x0000FFFF0000FFFF) << 16) | ((value >> 16) & 0x0000FFFF0000FFFF)
72
+ value = (value << 32) | (value >> 32)
73
+ return value
74
+
75
+ @njit(nogil=True, locals=dict(value=uint64))
76
+ def _rc(value):
77
+ value ^= mask
78
+ value = reverse_in_pairs(value)
79
+ return value >> shift
80
+
81
+ if rcmode == "min":
82
+ @njit(nogil=True, locals=dict(
83
+ code=uint64, rc=uint64))
84
+ def _cc(code):
85
+ rc = _rc(code)
86
+ return code if code <= rc else rc
87
+ elif rcmode == "max":
88
+ @njit(nogil=True, locals=dict(
89
+ code=uint64, rc=uint64))
90
+ def _cc(code):
91
+ rc = _rc(code)
92
+ return code if code >= rc else rc
93
+ elif rcmode == "r":
94
+ @njit(nogil=True, locals=dict(
95
+ code=uint64, rc=uint64))
96
+ def _cc(code):
97
+ rc = _rc(code)
98
+ return rc
99
+ else: # 'f', 'both', ...
100
+ @njit(nogil=True, locals=dict(
101
+ code=uint64, rc=uint64))
102
+ def _cc(code):
103
+ return code
104
+
105
+ return _rc, _cc
106
+
107
+
108
+ # translation of a DNA buffer into codes
109
+
110
+
111
+ def compile_twobit_to_codes(tmask, rcmode, invalid=U64_MINUSONE):
112
+ # tmask should be a mask tuple, but might be an int in some cases
113
+ if isinstance(tmask, int):
114
+ k = w = tmask # be safe here
115
+ tmask = tuple(range(k))
116
+ elif isinstance(tmask, Mask):
117
+ k = tmask.k
118
+ w = tmask.w
119
+ tmask = tmask.tuple
120
+ elif type(tmask) is tuple:
121
+ k, w = len(tmask), tmask[-1] + 1
122
+ else:
123
+ raise ValueError(f"mask type {type(tmask)} is not supported.")
124
+ _, ccc = compile_revcomp_and_canonical_code(k, rcmode)
125
+
126
+ @njit(nogil=True, locals=dict(code=uint64))
127
+ def twobit_to_codes(seq, out, start=0, n=-1):
128
+ """write n (or all) canonical k-mer codes from seq[start...] into out buffer"""
129
+ if n == -1:
130
+ n = len(seq) - w + 1 - start
131
+ for i in range(start, start + n):
132
+ code = 0
133
+ for j in tmask:
134
+ c = seq[i + j]
135
+ if c >= 4:
136
+ out[i - start] = uint64(invalid)
137
+ break
138
+ code = (code << 2) | c
139
+ else:
140
+ code = ccc(code)
141
+ out[i - start] = code
142
+
143
+ @njit(nogil=True, locals=dict(code=uint64))
144
+ def twobit_to_code(seq, start=0):
145
+ """return a single canonical code at seq[start...]"""
146
+ code = 0
147
+ for j in tmask:
148
+ c = seq[start + j]
149
+ if c >= 4:
150
+ return uint64(invalid)
151
+ code = (code << 2) | c
152
+ else:
153
+ code = ccc(code)
154
+ return code
155
+
156
+ return twobit_to_codes, twobit_to_code
@@ -0,0 +1,156 @@
1
+ """
2
+ fastcash_info.py:
3
+ Collect and print information on a hash table stored on disk.
4
+ Select elements according to a filter functions;
5
+ export selected or all elements (keys, values, choices, info).
6
+ """
7
+
8
+ from os import sep as SLASH
9
+ from concurrent.futures import ThreadPoolExecutor, wait
10
+ from importlib.util import spec_from_file_location, module_from_spec
11
+
12
+ from numba import njit
13
+
14
+ from .lowlevel import debug
15
+ from .srhash import print_statistics
16
+ from .io.hashio import load_hash, export_hash
17
+
18
+
19
+ def print_dict(d, title=None):
20
+ if title is not None:
21
+ print(f"\n### {title}")
22
+ for key, value in d.items():
23
+ print(f"- {key}: {value}")
24
+
25
+
26
+ def print_values(values, valuetup, title=None):
27
+ if title is not None:
28
+ print(f"\n### {title}")
29
+ print(f"- value set specification: {valuetup}")
30
+ print(f"- value set name: {values.NAME}")
31
+ print(f"- value set nvalues: {values.NVALUES}")
32
+
33
+
34
+ def compile_filter_function(
35
+ expr=None, compile=None,
36
+ valueset=None, appinfo=None):
37
+ # any filter given?
38
+ if expr is None and compile is None:
39
+ f = (lambda key, choice, value: True)
40
+ ff = njit(nogil=True)(f)
41
+ return ff, False
42
+ # filter by expression in key, choice, value
43
+ elif expr is not None:
44
+ # expr must be an expression using 'key', 'choice', 'value' and constants.
45
+ f = eval(f'lambda key, choice, value: ({expr})')
46
+ ff = njit(nogil=True)(f)
47
+ return ff, True
48
+ # filter by compiling a function, given by name='modulepath::funcname'.
49
+ # compile must be a list: [name, *params] (all strings).
50
+ # name must be of the form "module.function"
51
+ # This function receives the valueset and appinfo as constants,
52
+ # and must return a compiled function f(key, choice, value).
53
+ name, params = compile[0], compile[1:]
54
+ module_path, func = map(str.strip, name.split("::"))
55
+ *_, module_name = module_path.split(SLASH)
56
+ module_path += ".py"
57
+ _spec = spec_from_file_location(module_name, module_path)
58
+ module = module_from_spec(_spec)
59
+ _spec.loader.exec_module(module)
60
+ cf = getattr(module, func)
61
+ ff = cf(valueset=valueset, appinfo=appinfo, params=params)
62
+ return ff, True
63
+
64
+
65
+ def compile_count_filtered(h, filterfunc):
66
+ hp = h.private
67
+ is_slot_empty_at = hp.is_slot_empty_at
68
+ get_item_at = hp.get_item_at
69
+ get_subkey_choice_from_bucket_signature = hp.get_subkey_choice_from_bucket_signature
70
+ get_key_from_subtable_subkey = hp.get_key_from_subtable_subkey
71
+ nbuckets, bucketsize = h.nbuckets, h.bucketsize
72
+
73
+ @njit(nogil=True)
74
+ def count_filtered(ht, st):
75
+ n = total = 0
76
+ for bucket in range(nbuckets):
77
+ for slot in range(bucketsize):
78
+ if is_slot_empty_at(ht, st, bucket, slot):
79
+ break # go to next bucket
80
+ sig, val = get_item_at(ht, st, bucket, slot)
81
+ sbk, chc = get_subkey_choice_from_bucket_signature(bucket, sig)
82
+ key = get_key_from_subtable_subkey(st, sbk)
83
+ n += filterfunc(key, chc, val)
84
+ total += 1
85
+ return n, total
86
+
87
+ return count_filtered
88
+
89
+
90
+ def run_filter(h, filterfunc):
91
+ count_filtered = compile_count_filtered(h, filterfunc)
92
+ nsubtables = h.nsubtables if hasattr(h, "nsubtables") else h.subtables
93
+ with ThreadPoolExecutor(max_workers=nsubtables) as executor:
94
+ futures = [
95
+ executor.submit(count_filtered, h.hashtable, st)
96
+ for st in range(nsubtables)
97
+ ]
98
+ wait(futures)
99
+ filtered = total = 0
100
+ for st, f in enumerate(futures):
101
+ sgood, stotal = f.result() # this may raise errors
102
+ filtered += sgood
103
+ total += stotal
104
+ return filtered, total
105
+
106
+
107
+ def main(args):
108
+ global debugprint0, debugprint1, debugprint2
109
+ global timestamp0, timestamp1, timestamp2
110
+ debugprint0, debugprint1, debugprint2 = debug.debugprint
111
+ timestamp0, timestamp1, timestamp2 = debug.timestamp
112
+ starttime = timestamp0(msg="\n# fastcash info/export : information and export of fastcash hash tables")
113
+
114
+ # load hash table information
115
+ _, values, infotup = load_hash(args.hashname, info_only=True)
116
+ (hashinfo, valueinfo, optinfo, appinfo) = infotup
117
+ print_dict(hashinfo, "Hash table properties")
118
+ print_dict(optinfo)
119
+ print_dict(appinfo)
120
+ print_values(values, valueinfo, "Value set properties")
121
+
122
+ # count number of k-mers passing the filter
123
+ filterfunc, runfilter = compile_filter_function(
124
+ expr=args.filterexpression,
125
+ compile=args.compilefilter,
126
+ valueset=values, appinfo=appinfo)
127
+
128
+ # load the hash table and print statistics
129
+ h, values, infotup = load_hash(args.hashname)
130
+ nvalues = values.NVALUES
131
+ SV = {'all': nvalues, 'none': False}
132
+ sv = args.showvalues
133
+ sv = SV[sv] if sv in SV else int(sv)
134
+ print_statistics(h, level=args.statistics, show_values=sv)
135
+
136
+ # run a compiled filter
137
+ if runfilter:
138
+ filtered, total = run_filter(h, filterfunc)
139
+ percent = filtered / total if total > 0 else 0.0
140
+ debugprint0(f"\n- k-mers passing filter: {filtered} of {total}, or {percent:.2%}")
141
+
142
+ # export data
143
+ fmt = args.format # native, packed, text/txt, dna
144
+ anyformat = (fmt is not None)
145
+ prefix = args.outprefix
146
+ if (prefix is None) and anyformat:
147
+ prefix = args.hashname
148
+ if prefix is not None:
149
+ startout = timestamp0(msg="\n## Data Output")
150
+ fmt = {None: 'native', 'txt': 'text'}.get(fmt, fmt)
151
+ written = export_hash(h, nvalues, infotup, prefix, fmt, filterfunc)
152
+ debugprint0(f"- wrote {written} items")
153
+ timestamp0(startout, msg="- done writing data; writing time")
154
+
155
+ # that's it; successfully exit the program
156
+ timestamp0(starttime, msg="- DONE; total time")