@gmod/tabix 1.5.2 → 1.5.3

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/src/tbi.ts ADDED
@@ -0,0 +1,250 @@
1
+ import Long from 'long'
2
+ import VirtualOffset, { fromBytes } from './virtualOffset'
3
+ import Chunk from './chunk'
4
+ import { unzip } from '@gmod/bgzf-filehandle'
5
+ import { longToNumber, optimizeChunks, checkAbortSignal } from './util'
6
+ import IndexFile, { Options } from './indexFile'
7
+
8
+ const TBI_MAGIC = 21578324 // TBI\1
9
+ const TAD_LIDX_SHIFT = 14
10
+
11
+ /**
12
+ * calculate the list of bins that may overlap with region [beg,end) (zero-based half-open)
13
+ */
14
+ function reg2bins(beg: number, end: number) {
15
+ beg += 1 // < convert to 1-based closed
16
+ end -= 1
17
+ return [
18
+ [0, 0],
19
+ [1 + (beg >> 26), 1 + (end >> 26)],
20
+ [9 + (beg >> 23), 9 + (end >> 23)],
21
+ [73 + (beg >> 20), 73 + (end >> 20)],
22
+ [585 + (beg >> 17), 585 + (end >> 17)],
23
+ [4681 + (beg >> 14), 4681 + (end >> 14)],
24
+ ]
25
+ }
26
+
27
+ export default class TabixIndex extends IndexFile {
28
+ async lineCount(refName: string, opts: Options = {}) {
29
+ const indexData = await this.parse(opts)
30
+ if (!indexData) {
31
+ return -1
32
+ }
33
+ const refId = indexData.refNameToId[refName]
34
+ const idx = indexData.indices[refId]
35
+ if (!idx) {
36
+ return -1
37
+ }
38
+ const { stats } = indexData.indices[refId]
39
+ if (stats) {
40
+ return stats.lineCount
41
+ }
42
+ return -1
43
+ }
44
+
45
+ // memoize
46
+ // fetch and parse the index
47
+ async _parse(opts: Options = {}) {
48
+ const bytes = await unzip((await this.filehandle.readFile(opts)) as Buffer)
49
+ checkAbortSignal(opts.signal)
50
+
51
+ // check TBI magic numbers
52
+ if (bytes.readUInt32LE(0) !== TBI_MAGIC /* "TBI\1" */) {
53
+ throw new Error('Not a TBI file')
54
+ // TODO: do we need to support big-endian TBI files?
55
+ }
56
+
57
+ // number of reference sequences in the index
58
+ const refCount = bytes.readInt32LE(4)
59
+ const formatFlags = bytes.readInt32LE(8)
60
+ const coordinateType =
61
+ formatFlags & 0x10000 ? 'zero-based-half-open' : '1-based-closed'
62
+ const formatOpts: { [key: number]: string } = {
63
+ 0: 'generic',
64
+ 1: 'SAM',
65
+ 2: 'VCF',
66
+ }
67
+ const format = formatOpts[formatFlags & 0xf]
68
+ if (!format) {
69
+ throw new Error(`invalid Tabix preset format flags ${formatFlags}`)
70
+ }
71
+ const columnNumbers = {
72
+ ref: bytes.readInt32LE(12),
73
+ start: bytes.readInt32LE(16),
74
+ end: bytes.readInt32LE(20),
75
+ }
76
+ const metaValue = bytes.readInt32LE(24)
77
+ const depth = 5
78
+ const maxBinNumber = ((1 << ((depth + 1) * 3)) - 1) / 7
79
+ const maxRefLength = 2 ** (14 + depth * 3)
80
+ const metaChar = metaValue ? String.fromCharCode(metaValue) : null
81
+ const skipLines = bytes.readInt32LE(28)
82
+
83
+ // read sequence dictionary
84
+ const nameSectionLength = bytes.readInt32LE(32)
85
+ const { refNameToId, refIdToName } = this._parseNameBytes(
86
+ bytes.slice(36, 36 + nameSectionLength),
87
+ )
88
+
89
+ // read the indexes for each reference sequence
90
+ let currOffset = 36 + nameSectionLength
91
+ let firstDataLine: VirtualOffset | undefined
92
+ const indices = new Array(refCount).fill(0).map(() => {
93
+ // the binning index
94
+ const binCount = bytes.readInt32LE(currOffset)
95
+ currOffset += 4
96
+ const binIndex: { [key: number]: Chunk[] } = {}
97
+ let stats
98
+ for (let j = 0; j < binCount; j += 1) {
99
+ const bin = bytes.readUInt32LE(currOffset)
100
+ currOffset += 4
101
+ if (bin > maxBinNumber + 1) {
102
+ throw new Error(
103
+ 'tabix index contains too many bins, please use a CSI index',
104
+ )
105
+ } else if (bin === maxBinNumber + 1) {
106
+ const chunkCount = bytes.readInt32LE(currOffset)
107
+ currOffset += 4
108
+ if (chunkCount === 2) {
109
+ stats = this.parsePseudoBin(bytes, currOffset)
110
+ }
111
+ currOffset += 16 * chunkCount
112
+ } else {
113
+ const chunkCount = bytes.readInt32LE(currOffset)
114
+ currOffset += 4
115
+ const chunks = new Array(chunkCount)
116
+ for (let k = 0; k < chunkCount; k += 1) {
117
+ const u = fromBytes(bytes, currOffset)
118
+ const v = fromBytes(bytes, currOffset + 8)
119
+ currOffset += 16
120
+ firstDataLine = this._findFirstData(firstDataLine, u)
121
+ chunks[k] = new Chunk(u, v, bin)
122
+ }
123
+ binIndex[bin] = chunks
124
+ }
125
+ }
126
+
127
+ // the linear index
128
+ const linearCount = bytes.readInt32LE(currOffset)
129
+ currOffset += 4
130
+ const linearIndex = new Array(linearCount)
131
+ for (let k = 0; k < linearCount; k += 1) {
132
+ linearIndex[k] = fromBytes(bytes, currOffset)
133
+ currOffset += 8
134
+ firstDataLine = this._findFirstData(firstDataLine, linearIndex[k])
135
+ }
136
+ return { binIndex, linearIndex, stats }
137
+ })
138
+
139
+ return {
140
+ indices,
141
+ metaChar,
142
+ maxBinNumber,
143
+ maxRefLength,
144
+ skipLines,
145
+ firstDataLine,
146
+ columnNumbers,
147
+ coordinateType,
148
+ format,
149
+ refIdToName,
150
+ refNameToId,
151
+ maxBlockSize: 1 << 16,
152
+ }
153
+ }
154
+
155
+ parsePseudoBin(bytes: Buffer, offset: number) {
156
+ const lineCount = longToNumber(
157
+ Long.fromBytesLE(
158
+ bytes.slice(offset + 16, offset + 24) as unknown as number[],
159
+ true,
160
+ ),
161
+ )
162
+ return { lineCount }
163
+ }
164
+
165
+ _parseNameBytes(namesBytes: Buffer) {
166
+ let currRefId = 0
167
+ let currNameStart = 0
168
+ const refIdToName: string[] = []
169
+ const refNameToId: { [key: string]: number } = {}
170
+ for (let i = 0; i < namesBytes.length; i += 1) {
171
+ if (!namesBytes[i]) {
172
+ if (currNameStart < i) {
173
+ let refName = namesBytes.toString('utf8', currNameStart, i)
174
+ refName = this.renameRefSeq(refName)
175
+ refIdToName[currRefId] = refName
176
+ refNameToId[refName] = currRefId
177
+ }
178
+ currNameStart = i + 1
179
+ currRefId += 1
180
+ }
181
+ }
182
+ return { refNameToId, refIdToName }
183
+ }
184
+
185
+ async blocksForRange(
186
+ refName: string,
187
+ min: number,
188
+ max: number,
189
+ opts: Options = {},
190
+ ) {
191
+ if (min < 0) {
192
+ min = 0
193
+ }
194
+
195
+ const indexData = await this.parse(opts)
196
+ if (!indexData) {
197
+ return []
198
+ }
199
+ const refId = indexData.refNameToId[refName]
200
+ const ba = indexData.indices[refId]
201
+ if (!ba) {
202
+ return []
203
+ }
204
+
205
+ const minOffset = ba.linearIndex.length
206
+ ? ba.linearIndex[
207
+ min >> TAD_LIDX_SHIFT >= ba.linearIndex.length
208
+ ? ba.linearIndex.length - 1
209
+ : min >> TAD_LIDX_SHIFT
210
+ ]
211
+ : new VirtualOffset(0, 0)
212
+ if (!minOffset) {
213
+ console.warn('querying outside of possible tabix range')
214
+ }
215
+
216
+ // const { linearIndex, binIndex } = indexes
217
+
218
+ const overlappingBins = reg2bins(min, max) // List of bin #s that overlap min, max
219
+ const chunks: Chunk[] = []
220
+
221
+ // Find chunks in overlapping bins. Leaf bins (< 4681) are not pruned
222
+ for (const [start, end] of overlappingBins) {
223
+ for (let bin = start; bin <= end; bin++) {
224
+ if (ba.binIndex[bin]) {
225
+ const binChunks = ba.binIndex[bin]
226
+ for (let c = 0; c < binChunks.length; ++c) {
227
+ chunks.push(new Chunk(binChunks[c].minv, binChunks[c].maxv, bin))
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ // Use the linear index to find minimum file position of chunks that could
234
+ // contain alignments in the region
235
+ const nintv = ba.linearIndex.length
236
+ let lowest = null
237
+ const minLin = Math.min(min >> 14, nintv - 1)
238
+ const maxLin = Math.min(max >> 14, nintv - 1)
239
+ for (let i = minLin; i <= maxLin; ++i) {
240
+ const vp = ba.linearIndex[i]
241
+ if (vp) {
242
+ if (!lowest || vp.compareTo(lowest) < 0) {
243
+ lowest = vp
244
+ }
245
+ }
246
+ }
247
+
248
+ return optimizeChunks(chunks, lowest)
249
+ }
250
+ }
package/src/util.ts ADDED
@@ -0,0 +1,103 @@
1
+ import Chunk from './chunk'
2
+ import VirtualOffset from './virtualOffset'
3
+
4
+ export function longToNumber(long: Long) {
5
+ if (
6
+ long.greaterThan(Number.MAX_SAFE_INTEGER) ||
7
+ long.lessThan(Number.MIN_SAFE_INTEGER)
8
+ ) {
9
+ throw new Error('integer overflow')
10
+ }
11
+ return long.toNumber()
12
+ }
13
+
14
+ class AbortError extends Error {
15
+ public code: string | undefined
16
+ }
17
+ /**
18
+ * Properly check if the given AbortSignal is aborted.
19
+ * Per the standard, if the signal reads as aborted,
20
+ * this function throws either a DOMException AbortError, or a regular error
21
+ * with a `code` attribute set to `ERR_ABORTED`.
22
+ *
23
+ * For convenience, passing `undefined` is a no-op
24
+ *
25
+ * @param {AbortSignal} [signal] an AbortSignal, or anything with an `aborted` attribute
26
+ * @returns nothing
27
+ */
28
+ export function checkAbortSignal(signal?: AbortSignal) {
29
+ if (!signal) {
30
+ return
31
+ }
32
+
33
+ if (signal.aborted) {
34
+ // console.log('bam aborted!')
35
+ if (typeof DOMException !== 'undefined') {
36
+ // eslint-disable-next-line no-undef
37
+ throw new DOMException('aborted', 'AbortError')
38
+ } else {
39
+ const e = new AbortError('aborted')
40
+ e.code = 'ERR_ABORTED'
41
+ throw e
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Skips to the next tick, then runs `checkAbortSignal`.
48
+ * Await this to inside an otherwise synchronous loop to
49
+ * provide a place to break when an abort signal is received.
50
+ * @param {AbortSignal} signal
51
+ */
52
+ export async function abortBreakPoint(signal?: AbortSignal) {
53
+ await Promise.resolve()
54
+ checkAbortSignal(signal)
55
+ }
56
+
57
+ export function canMergeBlocks(chunk1: Chunk, chunk2: Chunk) {
58
+ return (
59
+ chunk2.minv.blockPosition - chunk1.maxv.blockPosition < 65000 &&
60
+ chunk2.maxv.blockPosition - chunk1.minv.blockPosition < 5000000
61
+ )
62
+ }
63
+
64
+ export function optimizeChunks(chunks: Chunk[], lowest: VirtualOffset) {
65
+ const mergedChunks: Chunk[] = []
66
+ let lastChunk: Chunk | null = null
67
+
68
+ if (chunks.length === 0) {
69
+ return chunks
70
+ }
71
+
72
+ chunks.sort(function (c0, c1) {
73
+ const dif = c0.minv.blockPosition - c1.minv.blockPosition
74
+ if (dif !== 0) {
75
+ return dif
76
+ } else {
77
+ return c0.minv.dataPosition - c1.minv.dataPosition
78
+ }
79
+ })
80
+
81
+ chunks.forEach(chunk => {
82
+ if (!lowest || chunk.maxv.compareTo(lowest) > 0) {
83
+ if (lastChunk === null) {
84
+ mergedChunks.push(chunk)
85
+ lastChunk = chunk
86
+ } else {
87
+ if (canMergeBlocks(lastChunk, chunk)) {
88
+ if (chunk.maxv.compareTo(lastChunk.maxv) > 0) {
89
+ lastChunk.maxv = chunk.maxv
90
+ }
91
+ } else {
92
+ mergedChunks.push(chunk)
93
+ lastChunk = chunk
94
+ }
95
+ }
96
+ }
97
+ // else {
98
+ // console.log(`skipping chunk ${chunk}`)
99
+ // }
100
+ })
101
+
102
+ return mergedChunks
103
+ }
@@ -0,0 +1,47 @@
1
+ export default class VirtualOffset {
2
+ public blockPosition: number
3
+ public dataPosition: number
4
+ constructor(blockPosition: number, dataPosition: number) {
5
+ this.blockPosition = blockPosition // < offset of the compressed data block
6
+ this.dataPosition = dataPosition // < offset into the uncompressed data
7
+ }
8
+
9
+ toString() {
10
+ return `${this.blockPosition}:${this.dataPosition}`
11
+ }
12
+
13
+ compareTo(b: VirtualOffset) {
14
+ return (
15
+ this.blockPosition - b.blockPosition || this.dataPosition - b.dataPosition
16
+ )
17
+ }
18
+
19
+ static min(...args: VirtualOffset[]) {
20
+ let min
21
+ let i = 0
22
+ for (; !min; i += 1) {
23
+ min = args[i]
24
+ }
25
+ for (; i < args.length; i += 1) {
26
+ if (min.compareTo(args[i]) > 0) {
27
+ min = args[i]
28
+ }
29
+ }
30
+ return min
31
+ }
32
+ }
33
+ export function fromBytes(bytes: Buffer, offset = 0, bigendian = false) {
34
+ if (bigendian) {
35
+ throw new Error('big-endian virtual file offsets not implemented')
36
+ }
37
+
38
+ return new VirtualOffset(
39
+ bytes[offset + 7] * 0x10000000000 +
40
+ bytes[offset + 6] * 0x100000000 +
41
+ bytes[offset + 5] * 0x1000000 +
42
+ bytes[offset + 4] * 0x10000 +
43
+ bytes[offset + 3] * 0x100 +
44
+ bytes[offset + 2],
45
+ (bytes[offset + 1] << 8) | bytes[offset],
46
+ )
47
+ }