@gmod/tabix 1.5.0 → 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/CHANGELOG.md +20 -0
- package/README.md +3 -2
- package/dist/chunk.js +34 -62
- package/dist/chunk.js.map +1 -0
- package/dist/csi.d.ts +0 -1
- package/dist/csi.js +329 -533
- package/dist/csi.js.map +1 -0
- package/dist/index.js +12 -36
- package/dist/index.js.map +1 -0
- package/dist/indexFile.d.ts +1 -1
- package/dist/indexFile.js +113 -174
- package/dist/indexFile.js.map +1 -0
- package/dist/tabixIndexedFile.d.ts +8 -4
- package/dist/tabixIndexedFile.js +477 -774
- package/dist/tabixIndexedFile.js.map +1 -0
- package/dist/tbi.js +313 -485
- package/dist/tbi.js.map +1 -0
- package/dist/util.js +133 -148
- package/dist/util.js.map +1 -0
- package/dist/virtualOffset.js +45 -68
- package/dist/virtualOffset.js.map +1 -0
- package/esm/chunk.d.ts +18 -0
- package/esm/chunk.js +33 -0
- package/esm/chunk.js.map +1 -0
- package/esm/csi.d.ts +71 -0
- package/esm/csi.js +230 -0
- package/esm/csi.js.map +1 -0
- package/esm/index.d.ts +4 -0
- package/esm/index.js +5 -0
- package/esm/index.js.map +1 -0
- package/esm/indexFile.d.ts +32 -0
- package/esm/indexFile.js +40 -0
- package/esm/indexFile.js.map +1 -0
- package/esm/tabixIndexedFile.d.ts +118 -0
- package/esm/tabixIndexedFile.js +398 -0
- package/esm/tabixIndexedFile.js.map +1 -0
- package/esm/tbi.d.ts +45 -0
- package/esm/tbi.js +216 -0
- package/esm/tbi.js.map +1 -0
- package/esm/util.d.ts +25 -0
- package/esm/util.js +91 -0
- package/esm/util.js.map +1 -0
- package/esm/virtualOffset.d.ts +10 -0
- package/esm/virtualOffset.js +37 -0
- package/esm/virtualOffset.js.map +1 -0
- package/package.json +27 -44
- package/src/chunk.ts +52 -0
- package/src/csi.ts +277 -0
- package/src/declare.d.ts +2 -0
- package/src/index.ts +5 -0
- package/src/indexFile.ts +79 -0
- package/src/tabixIndexedFile.ts +519 -0
- package/src/tbi.ts +250 -0
- package/src/util.ts +103 -0
- package/src/virtualOffset.ts +47 -0
- package/dist/declare.d.js +0 -2
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
|
+
}
|
package/dist/declare.d.js
DELETED