@gmod/bam 8.5.1 → 8.7.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.
- package/README.md +30 -0
- package/dist/bamFile.d.ts +94 -1
- package/dist/bamFile.js +94 -3
- package/dist/bamFile.js.map +1 -1
- package/dist/htsget.d.ts +8 -1
- package/dist/htsget.js +4 -1
- package/dist/htsget.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/mismatches.d.ts +130 -0
- package/dist/mismatches.js +404 -0
- package/dist/mismatches.js.map +1 -0
- package/dist/record.d.ts +46 -0
- package/dist/record.js +77 -10
- package/dist/record.js.map +1 -1
- package/dist/reference.d.ts +45 -0
- package/dist/reference.js +61 -0
- package/dist/reference.js.map +1 -0
- package/dist/seqAlphabet.d.ts +3 -0
- package/dist/seqAlphabet.js +14 -0
- package/dist/seqAlphabet.js.map +1 -0
- package/esm/bamFile.d.ts +94 -1
- package/esm/bamFile.js +94 -3
- package/esm/bamFile.js.map +1 -1
- package/esm/htsget.d.ts +8 -1
- package/esm/htsget.js +4 -1
- package/esm/htsget.js.map +1 -1
- package/esm/index.d.ts +5 -1
- package/esm/index.js +6 -0
- package/esm/index.js.map +1 -1
- package/esm/mismatches.d.ts +130 -0
- package/esm/mismatches.js +400 -0
- package/esm/mismatches.js.map +1 -0
- package/esm/record.d.ts +46 -0
- package/esm/record.js +70 -3
- package/esm/record.js.map +1 -1
- package/esm/reference.d.ts +45 -0
- package/esm/reference.js +55 -0
- package/esm/reference.js.map +1 -0
- package/esm/seqAlphabet.d.ts +3 -0
- package/esm/seqAlphabet.js +11 -0
- package/esm/seqAlphabet.js.map +1 -0
- package/package.json +1 -1
- package/src/bamFile.ts +178 -1
- package/src/htsget.ts +16 -2
- package/src/index.ts +26 -1
- package/src/mismatches.ts +629 -0
- package/src/record.ts +97 -3
- package/src/reference.ts +87 -0
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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,93 @@ 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, an `origin` the
|
|
992
|
+
* reported positions are relative to, and an optional per-call reference
|
|
993
|
+
* region; see {@link MismatchOptions}
|
|
994
|
+
*/
|
|
995
|
+
forEachMismatch(callback: MismatchCallback, opts?: MismatchOptions) {
|
|
996
|
+
forEachMismatchNumeric(
|
|
997
|
+
this.NUMERIC_CIGAR,
|
|
998
|
+
this.NUMERIC_SEQ,
|
|
999
|
+
this.seq_length,
|
|
1000
|
+
this.NUMERIC_MD,
|
|
1001
|
+
this.qual,
|
|
1002
|
+
opts?.ref ?? this._reference,
|
|
1003
|
+
this.start,
|
|
1004
|
+
opts?.start ?? Number.NEGATIVE_INFINITY,
|
|
1005
|
+
opts?.end ?? Number.POSITIVE_INFINITY,
|
|
1006
|
+
opts?.origin ?? 0,
|
|
1007
|
+
callback,
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* The same differences {@link forEachMismatch} reports, as an array of
|
|
1013
|
+
* {@link Mismatch} objects. Convenient; allocates one object per difference,
|
|
1014
|
+
* so the callback form is the one to reach for on a hot path.
|
|
1015
|
+
*/
|
|
1016
|
+
getMismatches(opts?: MismatchOptions) {
|
|
1017
|
+
const out: Mismatch[] = []
|
|
1018
|
+
this.forEachMismatch(
|
|
1019
|
+
(code, refPos, length, bases, qual, refBaseCode, clipLength) => {
|
|
1020
|
+
out.push({
|
|
1021
|
+
code,
|
|
1022
|
+
refPos,
|
|
1023
|
+
length,
|
|
1024
|
+
bases,
|
|
1025
|
+
qual,
|
|
1026
|
+
refBaseCode,
|
|
1027
|
+
clipLength,
|
|
1028
|
+
})
|
|
1029
|
+
},
|
|
1030
|
+
opts,
|
|
1031
|
+
)
|
|
1032
|
+
return out
|
|
1033
|
+
}
|
|
1034
|
+
|
|
941
1035
|
// Most public BamRecord fields are getters on the prototype, so
|
|
942
1036
|
// Object.keys(this) wouldn't include them — JSON.stringify needs an explicit
|
|
943
1037
|
// list. Returns the meaningful BAM-spec fields. Return type is widened so
|
package/src/reference.ts
ADDED
|
@@ -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))
|