@gmod/bam 8.5.1 → 8.6.0

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 (51) hide show
  1. package/README.md +30 -0
  2. package/dist/bamFile.d.ts +94 -1
  3. package/dist/bamFile.js +94 -3
  4. package/dist/bamFile.js.map +1 -1
  5. package/dist/htsget.d.ts +8 -1
  6. package/dist/htsget.js +4 -1
  7. package/dist/htsget.js.map +1 -1
  8. package/dist/index.d.ts +5 -1
  9. package/dist/index.js +15 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/mismatches.d.ts +114 -0
  12. package/dist/mismatches.js +397 -0
  13. package/dist/mismatches.js.map +1 -0
  14. package/dist/record.d.ts +45 -0
  15. package/dist/record.js +76 -10
  16. package/dist/record.js.map +1 -1
  17. package/dist/reference.d.ts +45 -0
  18. package/dist/reference.js +61 -0
  19. package/dist/reference.js.map +1 -0
  20. package/dist/seqAlphabet.d.ts +3 -0
  21. package/dist/seqAlphabet.js +14 -0
  22. package/dist/seqAlphabet.js.map +1 -0
  23. package/esm/bamFile.d.ts +94 -1
  24. package/esm/bamFile.js +94 -3
  25. package/esm/bamFile.js.map +1 -1
  26. package/esm/htsget.d.ts +8 -1
  27. package/esm/htsget.js +4 -1
  28. package/esm/htsget.js.map +1 -1
  29. package/esm/index.d.ts +5 -1
  30. package/esm/index.js +6 -0
  31. package/esm/index.js.map +1 -1
  32. package/esm/mismatches.d.ts +114 -0
  33. package/esm/mismatches.js +393 -0
  34. package/esm/mismatches.js.map +1 -0
  35. package/esm/record.d.ts +45 -0
  36. package/esm/record.js +69 -3
  37. package/esm/record.js.map +1 -1
  38. package/esm/reference.d.ts +45 -0
  39. package/esm/reference.js +55 -0
  40. package/esm/reference.js.map +1 -0
  41. package/esm/seqAlphabet.d.ts +3 -0
  42. package/esm/seqAlphabet.js +11 -0
  43. package/esm/seqAlphabet.js.map +1 -0
  44. package/package.json +1 -1
  45. package/src/bamFile.ts +178 -1
  46. package/src/htsget.ts +16 -2
  47. package/src/index.ts +26 -1
  48. package/src/mismatches.ts +623 -0
  49. package/src/record.ts +95 -3
  50. package/src/reference.ts +87 -0
  51. package/src/seqAlphabet.ts +10 -0
package/src/record.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import { CIGAR_REF_SKIP, CIGAR_SOFT_CLIP } from './cigar.ts'
2
2
  import Constants from './constants.ts'
3
+ import { forEachMismatchNumeric } from './mismatches.ts'
4
+ import { referenceCovers } from './reference.ts'
5
+ import { SEQRET_CODES, SEQRET_DECODER } from './seqAlphabet.ts'
3
6
 
4
- const SEQRET = '=ACMGRSVTWYHKDBN'
5
- const SEQRET_DECODER = SEQRET.split('')
6
- const SEQRET_CODES = Uint8Array.from(SEQRET, c => c.charCodeAt(0))
7
+ import type {
8
+ Mismatch,
9
+ MismatchCallback,
10
+ MismatchOptions,
11
+ } from './mismatches.ts'
12
+ import type { PackedReference } from './reference.ts'
7
13
 
8
14
  // Both bases of a SEQ byte, precomputed for all 256 bytes so decoding advances a
9
15
  // byte at a time. Two forms because `seq` has two strategies (see below): packed
@@ -371,6 +377,7 @@ export default class BamRecord {
371
377
  private _cachedNumericCigar?: NumericCigar
372
378
  private _cachedNUMERIC_MD?: Uint8Array | null
373
379
  private _cachedSeqStart?: number
380
+ private _reference?: PackedReference
374
381
 
375
382
  // Positional rather than an options object, because every argument is
376
383
  // unpacked into a field immediately and nothing keeps the wrapper. The
@@ -938,6 +945,91 @@ export default class BamRecord {
938
945
  }
939
946
  }
940
947
 
948
+ /**
949
+ * Bind reference bases for this read to resolve its substitutions against,
950
+ * for a read with no MD tag. `BamFile` calls this for you when it was given a
951
+ * `fetchReferenceSequence`; call it yourself when you have the bases from
952
+ * somewhere else.
953
+ *
954
+ * **`ref` must cover the whole read**, and this throws if it does not. The
955
+ * reason is that records are cached and shared between queries (ADR 0006), so
956
+ * a binding that varied by query would make one query's reads answer out of
957
+ * another's region. A region covering the read is the same data whichever
958
+ * query fetched it, so binding it is safe; a partial one is not, and the
959
+ * per-call `forEachMismatch(cb, {ref})` is where that belongs.
960
+ */
961
+ setReference(ref: PackedReference | undefined) {
962
+ if (ref !== undefined && !referenceCovers(ref, this.start, this.end)) {
963
+ throw new Error(
964
+ `reference region ${ref.start}-${ref.start + ref.length} does not cover the record at ${this.start}-${this.end}. Records are shared between queries, so only a region covering the whole read can be bound to one; pass a partial region per call instead, as forEachMismatch(cb, {ref})`,
965
+ )
966
+ }
967
+ this._reference = ref
968
+ }
969
+
970
+ /** the region {@link setReference} bound, if any */
971
+ get reference() {
972
+ return this._reference
973
+ }
974
+
975
+ /**
976
+ * Report each difference between this read and the reference —
977
+ * substitutions, insertions, deletions, reference skips and clips — without
978
+ * allocating an object per difference. This is the intended way to read a
979
+ * record's differences; deriving them from `CIGAR` and `MD` yourself takes
980
+ * rather more of the format to interpret correctly (see {@link Mismatch}).
981
+ *
982
+ * Substitutions come from the MD tag when the read has one, and otherwise
983
+ * from comparing SEQ against reference bases — which have to come from
984
+ * {@link setReference}, `opts.ref`, or the file's `fetchReferenceSequence`.
985
+ * With neither MD nor reference, indels and clips are still reported in full
986
+ * and substitutions are not reported at all: nothing in the record says where
987
+ * they are.
988
+ *
989
+ * @param callback called as
990
+ * `(code, refPos, length, bases, qual, refBaseCode, clipLength)`
991
+ * @param opts optional reference window to restrict to, and an optional
992
+ * per-call reference region; see {@link MismatchOptions}
993
+ */
994
+ forEachMismatch(callback: MismatchCallback, opts?: MismatchOptions) {
995
+ forEachMismatchNumeric(
996
+ this.NUMERIC_CIGAR,
997
+ this.NUMERIC_SEQ,
998
+ this.seq_length,
999
+ this.NUMERIC_MD,
1000
+ this.qual,
1001
+ opts?.ref ?? this._reference,
1002
+ this.start,
1003
+ opts?.start ?? Number.NEGATIVE_INFINITY,
1004
+ opts?.end ?? Number.POSITIVE_INFINITY,
1005
+ callback,
1006
+ )
1007
+ }
1008
+
1009
+ /**
1010
+ * The same differences {@link forEachMismatch} reports, as an array of
1011
+ * {@link Mismatch} objects. Convenient; allocates one object per difference,
1012
+ * so the callback form is the one to reach for on a hot path.
1013
+ */
1014
+ getMismatches(opts?: MismatchOptions) {
1015
+ const out: Mismatch[] = []
1016
+ this.forEachMismatch(
1017
+ (code, refPos, length, bases, qual, refBaseCode, clipLength) => {
1018
+ out.push({
1019
+ code,
1020
+ refPos,
1021
+ length,
1022
+ bases,
1023
+ qual,
1024
+ refBaseCode,
1025
+ clipLength,
1026
+ })
1027
+ },
1028
+ opts,
1029
+ )
1030
+ return out
1031
+ }
1032
+
941
1033
  // Most public BamRecord fields are getters on the prototype, so
942
1034
  // Object.keys(this) wouldn't include them — JSON.stringify needs an explicit
943
1035
  // list. Returns the meaningful BAM-spec fields. Return type is widened so
@@ -0,0 +1,87 @@
1
+ import { SEQRET } from './seqAlphabet.ts'
2
+
3
+ /**
4
+ * A region of reference sequence in BAM's own 4-bit alphabet, so the mismatch
5
+ * walk can compare it against a read's already-packed `NUMERIC_SEQ` a byte —
6
+ * two bases — at a time, instead of unpacking a nibble and reading a string
7
+ * character per base. Only the rare byte that differs gets unpacked.
8
+ *
9
+ * Two packings, because a read's sequence parity and its reference parity need
10
+ * not agree: `even[i]` holds region bases (2i, 2i+1), `odd[i]` holds
11
+ * (2i-1, 2i). The walk picks whichever lines the reference pair up with the
12
+ * read's byte boundary. Together they cost one byte per base, the same as the
13
+ * region string they replace.
14
+ *
15
+ * Build one with {@link packReference}, per region rather than per read: a
16
+ * pileup's reads all resolve against the same region, and packing is the only
17
+ * per-base pass in the whole walk.
18
+ */
19
+ export interface PackedReference {
20
+ /** 0-based reference coordinate of the first base */
21
+ start: number
22
+ /** length in bases, i.e. of the string this was packed from */
23
+ length: number
24
+ even: Uint8Array
25
+ odd: Uint8Array
26
+ }
27
+
28
+ // ASCII -> 4-bit code, case-insensitive. Anything outside the alphabet reads as
29
+ // N, which mismatches every concrete base — what an unrecognized character does
30
+ // when the comparison is done in ASCII.
31
+ const NIBBLE_FROM_CHAR = new Uint8Array(256).fill(15)
32
+ for (let i = 0; i < 16; i++) {
33
+ NIBBLE_FROM_CHAR[SEQRET.charCodeAt(i)] = i
34
+ NIBBLE_FROM_CHAR[SEQRET.charCodeAt(i) | 0x20] = i
35
+ }
36
+
37
+ /**
38
+ * The 4-bit codes back to ASCII, for reporting a mismatch's reference base.
39
+ * Always the uppercase spelling: packing is case-insensitive, so a soft-masked
40
+ * reference reports `A` where the raw string would have said `a`.
41
+ */
42
+ export const CHAR_CODE_FROM_NIBBLE = new Uint8Array(16)
43
+ for (let i = 0; i < 16; i++) {
44
+ CHAR_CODE_FROM_NIBBLE[i] = SEQRET.charCodeAt(i)
45
+ }
46
+
47
+ /**
48
+ * Pack a region of reference sequence for {@link PackedReference}.
49
+ *
50
+ * @param seq the reference bases, case-insensitive
51
+ * @param start the 0-based reference coordinate `seq[0]` sits at
52
+ */
53
+ export function packReference(seq: string, start = 0): PackedReference {
54
+ const n = seq.length
55
+ const even = new Uint8Array((n >> 1) + 1)
56
+ const odd = new Uint8Array((n >> 1) + 1)
57
+ for (let i = 0; i < n; i++) {
58
+ const code = seq.charCodeAt(i)
59
+ const nibble = code < 256 ? NIBBLE_FROM_CHAR[code]! : 15
60
+ if (i & 1) {
61
+ even[i >> 1]! |= nibble
62
+ odd[(i + 1) >> 1] = nibble << 4
63
+ } else {
64
+ even[i >> 1] = nibble << 4
65
+ odd[i >> 1]! |= nibble
66
+ }
67
+ }
68
+ return { start, length: n, even, odd }
69
+ }
70
+
71
+ /**
72
+ * The single reference base at region-relative index `i`, for the ends of an
73
+ * odd-length run. Callers bound `i` to the region themselves — reading past the
74
+ * end would report a base the region does not have.
75
+ */
76
+ export function referenceNibble(ref: PackedReference, i: number) {
77
+ return (ref.even[i >> 1]! >> ((1 - (i & 1)) << 2)) & 0xf
78
+ }
79
+
80
+ /** whether `ref` covers every base of the 0-based half-open span [start, end) */
81
+ export function referenceCovers(
82
+ ref: PackedReference,
83
+ start: number,
84
+ end: number,
85
+ ) {
86
+ return ref.start <= start && ref.start + ref.length >= end
87
+ }
@@ -0,0 +1,10 @@
1
+ // BAM's 4-bit sequence alphabet (SAM spec §4.2, `seq`): nibble value -> base.
2
+ //
3
+ // Shared rather than duplicated because three modules need it for three
4
+ // different shapes — `record.ts` builds pair/quad tables on top of it to decode
5
+ // whole reads, `reference.ts` inverts it to pack a reference the same way, and
6
+ // `mismatches.ts` reads single bases out of it — and a copy that drifted would
7
+ // silently mis-decode one of them.
8
+ export const SEQRET = '=ACMGRSVTWYHKDBN'
9
+ export const SEQRET_DECODER = SEQRET.split('')
10
+ export const SEQRET_CODES = Uint8Array.from(SEQRET, c => c.charCodeAt(0))