@gmod/gbz-base 0.0.1 → 1.1.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 +45 -4
- package/bin/query.js +3 -1
- package/dist/cli.js +85 -19
- package/dist/cli.js.map +1 -0
- package/dist/db.d.ts +14 -6
- package/dist/db.js +92 -28
- package/dist/db.js.map +1 -0
- package/dist/filehandle.js +1 -0
- package/dist/filehandle.js.map +1 -0
- package/dist/gbwt/bytecode.d.ts +1 -1
- package/dist/gbwt/bytecode.js +7 -3
- package/dist/gbwt/bytecode.js.map +1 -0
- package/dist/gbwt/node.js +7 -2
- package/dist/gbwt/node.js.map +1 -0
- package/dist/gbwt/record.d.ts +4 -0
- package/dist/gbwt/record.js +30 -2
- package/dist/gbwt/record.js.map +1 -0
- package/dist/gbwt/sequence.js +8 -1
- package/dist/gbwt/sequence.js.map +1 -0
- package/dist/graphName.d.ts +9 -0
- package/dist/graphName.js +83 -0
- package/dist/graphName.js.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -0
- package/dist/lcs.js +32 -10
- package/dist/lcs.js.map +1 -0
- package/dist/query.d.ts +4 -2
- package/dist/query.js +19 -0
- package/dist/query.js.map +1 -0
- package/dist/sqlite/btree.d.ts +1 -1
- package/dist/sqlite/btree.js +24 -7
- package/dist/sqlite/btree.js.map +1 -0
- package/dist/sqlite/database.d.ts +1 -1
- package/dist/sqlite/database.js +12 -2
- package/dist/sqlite/database.js.map +1 -0
- package/dist/sqlite/pager.d.ts +8 -3
- package/dist/sqlite/pager.js +71 -12
- package/dist/sqlite/pager.js.map +1 -0
- package/dist/sqlite/record.js +3 -1
- package/dist/sqlite/record.js.map +1 -0
- package/dist/subgraph.d.ts +12 -2
- package/dist/subgraph.js +413 -84
- package/dist/subgraph.js.map +1 -0
- package/package.json +36 -11
- package/src/cli.ts +258 -0
- package/src/db.ts +374 -0
- package/src/filehandle.ts +4 -0
- package/src/gbwt/bytecode.ts +87 -0
- package/src/gbwt/node.ts +64 -0
- package/src/gbwt/record.ts +177 -0
- package/src/gbwt/sequence.ts +50 -0
- package/src/graphName.ts +94 -0
- package/src/index.ts +34 -0
- package/src/lcs.ts +282 -0
- package/src/query.ts +106 -0
- package/src/sqlite/btree.ts +260 -0
- package/src/sqlite/database.ts +117 -0
- package/src/sqlite/pager.ts +148 -0
- package/src/sqlite/record.ts +100 -0
- package/src/subgraph.ts +1353 -0
- package/tools/haplotype-index/src/main.rs +197 -163
package/src/db.ts
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { ENDMARKER, nodeId, nodeOrientation } from './gbwt/node.ts'
|
|
2
|
+
import { GbwtRecord, decompressEdges } from './gbwt/record.ts'
|
|
3
|
+
import { decodeSequence, encodedSequenceLength } from './gbwt/sequence.ts'
|
|
4
|
+
import { graphNameFromTags } from './graphName.ts'
|
|
5
|
+
import { SqliteDatabase } from './sqlite/database.ts'
|
|
6
|
+
|
|
7
|
+
import type { ByteSource } from './filehandle.ts'
|
|
8
|
+
import type { Pos } from './gbwt/record.ts'
|
|
9
|
+
import type { GraphName } from './graphName.ts'
|
|
10
|
+
import type { PagerOptions } from './sqlite/pager.ts'
|
|
11
|
+
import type { SqlValue } from './sqlite/record.ts'
|
|
12
|
+
|
|
13
|
+
export interface PathName {
|
|
14
|
+
sample: string
|
|
15
|
+
contig: string
|
|
16
|
+
haplotype: number
|
|
17
|
+
fragment: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const GENERIC_SAMPLE = '_gbwt_ref'
|
|
21
|
+
export const SCHEMA_VERSION = 'GBZ-base version 4'
|
|
22
|
+
|
|
23
|
+
export class SchemaVersionError extends Error {
|
|
24
|
+
override name = 'SchemaVersionError'
|
|
25
|
+
|
|
26
|
+
readonly found: string | undefined
|
|
27
|
+
|
|
28
|
+
constructor(found: string | undefined) {
|
|
29
|
+
super(
|
|
30
|
+
found === undefined
|
|
31
|
+
? `not a gbz-base database: its Tags table has no version`
|
|
32
|
+
: `unsupported database schema "${found}"; this reader understands "${SCHEMA_VERSION}"`,
|
|
33
|
+
)
|
|
34
|
+
this.found = found
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatPathName(name: PathName, end: number) {
|
|
39
|
+
return `${name.sample}#${name.haplotype}#${name.contig}[${name.fragment}-${end}]`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface HaplotypeSample {
|
|
43
|
+
node: number
|
|
44
|
+
offset: number
|
|
45
|
+
pathHandle: number
|
|
46
|
+
orientation: 'forward' | 'reverse'
|
|
47
|
+
pathOffset: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface GbzPath {
|
|
51
|
+
handle: number
|
|
52
|
+
fwStart: Pos
|
|
53
|
+
revStart: Pos
|
|
54
|
+
name: PathName
|
|
55
|
+
isIndexed: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class GbzRecord {
|
|
59
|
+
readonly sequenceLen: number
|
|
60
|
+
readonly handle: number
|
|
61
|
+
readonly edges: Pos[]
|
|
62
|
+
readonly bwt: Uint8Array
|
|
63
|
+
readonly encodedSequence: Uint8Array
|
|
64
|
+
readonly next: number | undefined
|
|
65
|
+
private decoded: string | undefined
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
handle: number,
|
|
69
|
+
edges: Pos[],
|
|
70
|
+
bwt: Uint8Array,
|
|
71
|
+
encodedSequence: Uint8Array,
|
|
72
|
+
next: number | undefined,
|
|
73
|
+
) {
|
|
74
|
+
this.handle = handle
|
|
75
|
+
this.edges = edges
|
|
76
|
+
this.bwt = bwt
|
|
77
|
+
this.encodedSequence = encodedSequence
|
|
78
|
+
this.next = next
|
|
79
|
+
this.sequenceLen = encodedSequenceLength(encodedSequence)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get id() {
|
|
83
|
+
return nodeId(this.handle)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get orientation() {
|
|
87
|
+
return nodeOrientation(this.handle)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get sequence() {
|
|
91
|
+
this.decoded ??= decodeSequence(this.encodedSequence)
|
|
92
|
+
return this.decoded
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
successors() {
|
|
96
|
+
return this.edges.filter(e => e.node !== ENDMARKER).map(e => e.node)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
gbwt() {
|
|
100
|
+
if (this.edges.length === 0) {
|
|
101
|
+
throw new Error(`GBWT record for handle ${this.handle} is empty`)
|
|
102
|
+
}
|
|
103
|
+
return new GbwtRecord(this.edges, this.bwt)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function num(value: SqlValue | undefined, what: string) {
|
|
108
|
+
if (typeof value !== 'number') {
|
|
109
|
+
throw new Error(`${what} is not a number in the database`)
|
|
110
|
+
}
|
|
111
|
+
return value
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function str(value: SqlValue | undefined, what: string) {
|
|
115
|
+
if (typeof value !== 'string') {
|
|
116
|
+
throw new Error(`${what} is not text in the database`)
|
|
117
|
+
}
|
|
118
|
+
return value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function blob(value: SqlValue | undefined, what: string) {
|
|
122
|
+
if (!(value instanceof Uint8Array)) {
|
|
123
|
+
throw new Error(`${what} is not a blob in the database`)
|
|
124
|
+
}
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function rowToPath(rowid: number, values: SqlValue[]): GbzPath {
|
|
129
|
+
return {
|
|
130
|
+
handle: rowid,
|
|
131
|
+
fwStart: {
|
|
132
|
+
node: num(values[1], 'Paths.fw_node'),
|
|
133
|
+
offset: num(values[2], 'Paths.fw_offset'),
|
|
134
|
+
},
|
|
135
|
+
revStart: {
|
|
136
|
+
node: num(values[3], 'Paths.rev_node'),
|
|
137
|
+
offset: num(values[4], 'Paths.rev_offset'),
|
|
138
|
+
},
|
|
139
|
+
name: {
|
|
140
|
+
sample: str(values[5], 'Paths.sample'),
|
|
141
|
+
contig: str(values[6], 'Paths.contig'),
|
|
142
|
+
haplotype: num(values[7], 'Paths.haplotype'),
|
|
143
|
+
fragment: num(values[8], 'Paths.fragment'),
|
|
144
|
+
},
|
|
145
|
+
isIndexed: num(values[9], 'Paths.is_indexed') !== 0,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface OpenOptions extends PagerOptions {
|
|
150
|
+
haplotypeIndex?: ByteSource
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function readTags(sqlite: SqliteDatabase) {
|
|
154
|
+
const tags = new Map<string, string>()
|
|
155
|
+
for await (const { values } of sqlite.scan('Tags')) {
|
|
156
|
+
tags.set(str(values[0], 'Tags.key'), str(values[1], 'Tags.value'))
|
|
157
|
+
}
|
|
158
|
+
return tags
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export class GBZBase {
|
|
162
|
+
private tagCache: Promise<Map<string, string>> | undefined
|
|
163
|
+
private pathCache: Promise<GbzPath[]> | undefined
|
|
164
|
+
private indexTags: Map<string, string> | undefined
|
|
165
|
+
|
|
166
|
+
readonly sqlite: SqliteDatabase
|
|
167
|
+
private readonly index: SqliteDatabase
|
|
168
|
+
|
|
169
|
+
private constructor(sqlite: SqliteDatabase, index: SqliteDatabase) {
|
|
170
|
+
this.sqlite = sqlite
|
|
171
|
+
this.index = index
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static async open(source: ByteSource, opts: OpenOptions = {}) {
|
|
175
|
+
const { haplotypeIndex, ...pagerOptions } = opts
|
|
176
|
+
const sqlite = await SqliteDatabase.open(source, pagerOptions)
|
|
177
|
+
for (const table of ['Tags', 'Nodes', 'Paths', 'ReferenceIndex']) {
|
|
178
|
+
sqlite.rootPage(table)
|
|
179
|
+
}
|
|
180
|
+
const index = haplotypeIndex
|
|
181
|
+
? await SqliteDatabase.open(haplotypeIndex, pagerOptions)
|
|
182
|
+
: sqlite
|
|
183
|
+
const db = new GBZBase(sqlite, index)
|
|
184
|
+
const version = await db.tag('version')
|
|
185
|
+
if (version !== SCHEMA_VERSION) {
|
|
186
|
+
throw new SchemaVersionError(version)
|
|
187
|
+
}
|
|
188
|
+
if (haplotypeIndex) {
|
|
189
|
+
for (const table of ['Tags', 'HaplotypeSamples', 'HaplotypeLengths']) {
|
|
190
|
+
index.rootPage(table)
|
|
191
|
+
}
|
|
192
|
+
db.indexTags = await readTags(index)
|
|
193
|
+
const indexed = db.indexTags.get('haplotype_index_paths')
|
|
194
|
+
const paths = await db.tag('paths')
|
|
195
|
+
if (indexed !== paths) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`haplotype index was built for ${indexed ?? 'an unknown number of'} paths but the graph has ${paths}`,
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return db
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
tags() {
|
|
205
|
+
this.tagCache ??= readTags(this.sqlite)
|
|
206
|
+
return this.tagCache
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async tag(key: string) {
|
|
210
|
+
return (await this.tags()).get(key)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async getRecord(handle: number) {
|
|
214
|
+
const row = await this.sqlite.byRowid('Nodes', handle)
|
|
215
|
+
if (!row) {
|
|
216
|
+
return undefined
|
|
217
|
+
}
|
|
218
|
+
const next = row[4]
|
|
219
|
+
return new GbzRecord(
|
|
220
|
+
handle,
|
|
221
|
+
decompressEdges(blob(row[1], 'Nodes.edges')),
|
|
222
|
+
blob(row[2], 'Nodes.bwt'),
|
|
223
|
+
blob(row[3], 'Nodes.sequence'),
|
|
224
|
+
typeof next === 'number' ? next : undefined,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
paths() {
|
|
229
|
+
this.pathCache ??= (async () => {
|
|
230
|
+
const paths: GbzPath[] = []
|
|
231
|
+
for await (const { rowid, values } of this.sqlite.scan('Paths')) {
|
|
232
|
+
paths.push(rowToPath(rowid, values))
|
|
233
|
+
}
|
|
234
|
+
return paths
|
|
235
|
+
})()
|
|
236
|
+
return this.pathCache
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async getPath(handle: number) {
|
|
240
|
+
const row = await this.sqlite.byRowid('Paths', handle)
|
|
241
|
+
return row ? rowToPath(handle, row) : undefined
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async findPath(name: PathName) {
|
|
245
|
+
const candidates = (await this.paths()).filter(
|
|
246
|
+
p =>
|
|
247
|
+
p.name.sample === name.sample &&
|
|
248
|
+
p.name.contig === name.contig &&
|
|
249
|
+
p.name.haplotype === name.haplotype &&
|
|
250
|
+
p.name.fragment <= name.fragment,
|
|
251
|
+
)
|
|
252
|
+
return candidates.sort((a, b) => b.name.fragment - a.name.fragment)[0]
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async pathsForSample(sample: string) {
|
|
256
|
+
return (await this.paths()).filter(p => p.name.sample === sample)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async graphName() {
|
|
260
|
+
const gbzTags = new Map<string, string>()
|
|
261
|
+
for (const [key, value] of await this.tags()) {
|
|
262
|
+
if (key.startsWith('gbz_')) {
|
|
263
|
+
gbzTags.set(key.slice('gbz_'.length), value)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
let name: GraphName = {
|
|
267
|
+
name: undefined,
|
|
268
|
+
subgraph: new Map(),
|
|
269
|
+
translation: new Map(),
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
name = graphNameFromTags(gbzTags)
|
|
273
|
+
} catch {
|
|
274
|
+
// upstream falls back to an empty name when the tags do not parse
|
|
275
|
+
}
|
|
276
|
+
return name
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async hasChainLinks() {
|
|
280
|
+
const links = await this.tag('chain_links')
|
|
281
|
+
return links !== undefined && Number(links) > 0
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
get hasHaplotypeIndex() {
|
|
285
|
+
return (
|
|
286
|
+
this.index.has('HaplotypeSamples') && this.index.has('HaplotypeLengths')
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async haplotypeSampleInterval() {
|
|
291
|
+
const value = this.indexTags
|
|
292
|
+
? this.indexTags.get('haplotype_index_interval')
|
|
293
|
+
: await this.tag('haplotype_index_interval')
|
|
294
|
+
return value === undefined ? undefined : Number(value)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private sampleFromRow(values: SqlValue[]): HaplotypeSample {
|
|
298
|
+
return {
|
|
299
|
+
node: num(values[0], 'HaplotypeSamples.node_handle'),
|
|
300
|
+
offset: num(values[1], 'HaplotypeSamples.node_offset'),
|
|
301
|
+
pathHandle: num(values[2], 'HaplotypeSamples.path_handle'),
|
|
302
|
+
orientation:
|
|
303
|
+
num(values[3], 'HaplotypeSamples.orientation') === 0
|
|
304
|
+
? 'forward'
|
|
305
|
+
: 'reverse',
|
|
306
|
+
pathOffset: num(values[4], 'HaplotypeSamples.path_offset'),
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async haplotypeSamplesInRange(minHandle: number, maxHandle: number) {
|
|
311
|
+
const samples: HaplotypeSample[] = []
|
|
312
|
+
for await (const key of this.index.indexScanFrom('HaplotypeSamples', [
|
|
313
|
+
minHandle,
|
|
314
|
+
0,
|
|
315
|
+
])) {
|
|
316
|
+
const node = num(key[0], 'HaplotypeSamples.node_handle')
|
|
317
|
+
if (node > maxHandle) {
|
|
318
|
+
break
|
|
319
|
+
}
|
|
320
|
+
const row = await this.index.byRowid(
|
|
321
|
+
'HaplotypeSamples',
|
|
322
|
+
num(key[2], 'HaplotypeSamples rowid'),
|
|
323
|
+
)
|
|
324
|
+
if (row) {
|
|
325
|
+
samples.push(this.sampleFromRow(row))
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return samples
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async haplotypeSampleAt(node: number, offset: number) {
|
|
332
|
+
const key = await this.index.indexSeekLE('HaplotypeSamples', [node, offset])
|
|
333
|
+
if (key?.[0] !== node || key[1] !== offset) {
|
|
334
|
+
return undefined
|
|
335
|
+
}
|
|
336
|
+
const row = await this.index.byRowid(
|
|
337
|
+
'HaplotypeSamples',
|
|
338
|
+
num(key[2], 'HaplotypeSamples rowid'),
|
|
339
|
+
)
|
|
340
|
+
return row ? this.sampleFromRow(row) : undefined
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async haplotypeLength(pathHandle: number) {
|
|
344
|
+
const row = await this.index.byRowid('HaplotypeLengths', pathHandle)
|
|
345
|
+
return row ? num(row[1], 'HaplotypeLengths.length') : undefined
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async indexedPosition(
|
|
349
|
+
pathHandle: number,
|
|
350
|
+
pathOffset: number,
|
|
351
|
+
): Promise<{ pathOffset: number; pos: Pos } | undefined> {
|
|
352
|
+
const key = await this.sqlite.indexSeekLE('ReferenceIndex', [
|
|
353
|
+
pathHandle,
|
|
354
|
+
pathOffset,
|
|
355
|
+
])
|
|
356
|
+
if (key?.[0] !== pathHandle) {
|
|
357
|
+
return undefined
|
|
358
|
+
}
|
|
359
|
+
const row = await this.sqlite.byRowid(
|
|
360
|
+
'ReferenceIndex',
|
|
361
|
+
num(key[2], 'ReferenceIndex rowid'),
|
|
362
|
+
)
|
|
363
|
+
if (!row) {
|
|
364
|
+
throw new Error('ReferenceIndex row referenced by its index is missing')
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
pathOffset: num(row[1], 'ReferenceIndex.path_offset'),
|
|
368
|
+
pos: {
|
|
369
|
+
node: num(row[2], 'ReferenceIndex.node_handle'),
|
|
370
|
+
offset: num(row[3], 'ReferenceIndex.node_offset'),
|
|
371
|
+
},
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export class ByteCodeReader {
|
|
2
|
+
offset = 0
|
|
3
|
+
private bytes: Uint8Array
|
|
4
|
+
|
|
5
|
+
constructor(bytes: Uint8Array) {
|
|
6
|
+
this.bytes = bytes
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get done() {
|
|
10
|
+
return this.offset >= this.bytes.length
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
byte() {
|
|
14
|
+
const value = this.bytes[this.offset]
|
|
15
|
+
if (value === undefined) {
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
this.offset += 1
|
|
19
|
+
return value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
int() {
|
|
23
|
+
let shift = 1
|
|
24
|
+
let result = 0
|
|
25
|
+
while (this.offset < this.bytes.length) {
|
|
26
|
+
const value = this.bytes[this.offset]!
|
|
27
|
+
this.offset += 1
|
|
28
|
+
result += (value & 0x7f) * shift
|
|
29
|
+
shift *= 128
|
|
30
|
+
if ((value & 0x80) === 0) {
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Run {
|
|
39
|
+
value: number
|
|
40
|
+
len: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const RLE_THRESHOLD = 255
|
|
44
|
+
const RLE_UNIVERSE = 256
|
|
45
|
+
|
|
46
|
+
export class RunReader {
|
|
47
|
+
private source: ByteCodeReader
|
|
48
|
+
private sigma: number
|
|
49
|
+
private threshold: number
|
|
50
|
+
|
|
51
|
+
constructor(bytes: Uint8Array, sigma: number) {
|
|
52
|
+
this.source = new ByteCodeReader(bytes)
|
|
53
|
+
this.sigma = sigma === 0 ? Number.MAX_SAFE_INTEGER : sigma
|
|
54
|
+
this.threshold =
|
|
55
|
+
this.sigma < RLE_THRESHOLD ? Math.floor(RLE_UNIVERSE / this.sigma) : 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
next(): Run | undefined {
|
|
59
|
+
if (this.sigma >= RLE_THRESHOLD) {
|
|
60
|
+
const value = this.source.int()
|
|
61
|
+
const len = this.source.int()
|
|
62
|
+
return value === undefined || len === undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: { value, len: len + 1 }
|
|
65
|
+
}
|
|
66
|
+
const byte = this.source.byte()
|
|
67
|
+
if (byte === undefined) {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
const value = byte % this.sigma
|
|
71
|
+
let len = Math.floor(byte / this.sigma) + 1
|
|
72
|
+
if (len === this.threshold) {
|
|
73
|
+
const extra = this.source.int()
|
|
74
|
+
if (extra === undefined) {
|
|
75
|
+
return undefined
|
|
76
|
+
}
|
|
77
|
+
len += extra
|
|
78
|
+
}
|
|
79
|
+
return { value, len }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
*[Symbol.iterator]() {
|
|
83
|
+
for (let run = this.next(); run !== undefined; run = this.next()) {
|
|
84
|
+
yield run
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/gbwt/node.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const ENDMARKER = 0
|
|
2
|
+
|
|
3
|
+
export type Orientation = 'forward' | 'reverse'
|
|
4
|
+
|
|
5
|
+
export function encodeNode(id: number, orientation: Orientation) {
|
|
6
|
+
return 2 * id + (orientation === 'reverse' ? 1 : 0)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function nodeId(handle: number) {
|
|
10
|
+
return Math.floor(handle / 2)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function nodeOrientation(handle: number): Orientation {
|
|
14
|
+
return handle % 2 === 0 ? 'forward' : 'reverse'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isReverse(handle: number) {
|
|
18
|
+
return handle % 2 === 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function flipNode(handle: number) {
|
|
22
|
+
return handle % 2 === 0 ? handle + 1 : handle - 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type NodeSide = 'left' | 'right'
|
|
26
|
+
|
|
27
|
+
export function flipSide(side: NodeSide): NodeSide {
|
|
28
|
+
return side === 'left' ? 'right' : 'left'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function entrySide(orientation: Orientation): NodeSide {
|
|
32
|
+
return orientation === 'forward' ? 'left' : 'right'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function exitSide(orientation: Orientation): NodeSide {
|
|
36
|
+
return orientation === 'forward' ? 'right' : 'left'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function entryOrientation(side: NodeSide): Orientation {
|
|
40
|
+
return side === 'left' ? 'forward' : 'reverse'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function exitOrientation(side: NodeSide): Orientation {
|
|
44
|
+
return side === 'right' ? 'forward' : 'reverse'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function edgeIsCanonical(from: number, to: number) {
|
|
48
|
+
const fromId = nodeId(from)
|
|
49
|
+
const toId = nodeId(to)
|
|
50
|
+
return isReverse(from)
|
|
51
|
+
? toId > fromId || (toId === fromId && !isReverse(to))
|
|
52
|
+
: toId >= fromId
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function pathIsCanonical(path: number[]) {
|
|
56
|
+
const first = path[0]
|
|
57
|
+
const last = path[path.length - 1]
|
|
58
|
+
if (first === undefined || last === undefined) {
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
return isReverse(first) === isReverse(last)
|
|
62
|
+
? !isReverse(first)
|
|
63
|
+
: edgeIsCanonical(first, last)
|
|
64
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { ByteCodeReader, RunReader } from './bytecode.ts'
|
|
2
|
+
import { ENDMARKER, flipNode, nodeId } from './node.ts'
|
|
3
|
+
|
|
4
|
+
export interface Pos {
|
|
5
|
+
node: number
|
|
6
|
+
offset: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function decompressEdges(bytes: Uint8Array): Pos[] {
|
|
10
|
+
const reader = new ByteCodeReader(bytes)
|
|
11
|
+
const sigma = reader.int()
|
|
12
|
+
if (sigma === undefined || sigma === 0) {
|
|
13
|
+
return []
|
|
14
|
+
}
|
|
15
|
+
const edges: Pos[] = []
|
|
16
|
+
let prev = 0
|
|
17
|
+
for (let i = 0; i < sigma; i++) {
|
|
18
|
+
const delta = reader.int()
|
|
19
|
+
const offset = reader.int()
|
|
20
|
+
if (delta === undefined || offset === undefined) {
|
|
21
|
+
throw new Error('GBWT edge list ends early')
|
|
22
|
+
}
|
|
23
|
+
prev += delta
|
|
24
|
+
edges.push({ node: prev, offset })
|
|
25
|
+
}
|
|
26
|
+
return edges
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class GbwtRecord {
|
|
30
|
+
readonly edges: Pos[]
|
|
31
|
+
readonly bwt: Uint8Array
|
|
32
|
+
|
|
33
|
+
constructor(edges: Pos[], bwt: Uint8Array) {
|
|
34
|
+
this.edges = edges
|
|
35
|
+
this.bwt = bwt
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
runs() {
|
|
39
|
+
return new RunReader(this.bwt, this.edges.length)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private edge(rank: number) {
|
|
43
|
+
const edge = this.edges[rank]
|
|
44
|
+
if (edge === undefined) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`GBWT run refers to edge rank ${rank} of ${this.edges.length}`,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
return edge
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lf(i: number): Pos | undefined {
|
|
53
|
+
const offsets = this.edges.map(e => e.offset)
|
|
54
|
+
let offset = 0
|
|
55
|
+
for (const run of this.runs()) {
|
|
56
|
+
const edge = this.edge(run.value)
|
|
57
|
+
const soFar = offsets[run.value]!
|
|
58
|
+
if (offset + run.len > i) {
|
|
59
|
+
return edge.node === ENDMARKER
|
|
60
|
+
? undefined
|
|
61
|
+
: { node: edge.node, offset: soFar + (i - offset) }
|
|
62
|
+
}
|
|
63
|
+
offsets[run.value] = soFar + run.len
|
|
64
|
+
offset += run.len
|
|
65
|
+
}
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private edgeTo(node: number) {
|
|
70
|
+
let low = 0
|
|
71
|
+
let high = this.edges.length
|
|
72
|
+
while (low < high) {
|
|
73
|
+
const mid = (low + high) >> 1
|
|
74
|
+
const edge = this.edges[mid]!
|
|
75
|
+
if (edge.node === node) {
|
|
76
|
+
return mid
|
|
77
|
+
}
|
|
78
|
+
if (edge.node < node) {
|
|
79
|
+
low = mid + 1
|
|
80
|
+
} else {
|
|
81
|
+
high = mid
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
offsetTo(pos: Pos): number | undefined {
|
|
88
|
+
if (pos.node === ENDMARKER) {
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
const rank = this.edgeTo(pos.node)
|
|
92
|
+
if (rank === undefined) {
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
let succRank = this.edges[rank]!.offset
|
|
96
|
+
if (succRank > pos.offset) {
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
let offset = 0
|
|
100
|
+
for (const run of this.runs()) {
|
|
101
|
+
offset += run.len
|
|
102
|
+
if (run.value !== rank) {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
succRank += run.len
|
|
106
|
+
if (succRank > pos.offset) {
|
|
107
|
+
return offset - (succRank - pos.offset)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
predecessorAt(i: number): number | undefined {
|
|
114
|
+
const counts = this.edges.map(e => ({
|
|
115
|
+
node: e.node === ENDMARKER ? ENDMARKER : flipNode(e.node),
|
|
116
|
+
count: 0,
|
|
117
|
+
}))
|
|
118
|
+
for (const run of this.runs()) {
|
|
119
|
+
const entry = counts[run.value]
|
|
120
|
+
if (entry) {
|
|
121
|
+
entry.count += run.len
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (let rank = 1; rank < counts.length; rank++) {
|
|
125
|
+
const prev = counts[rank - 1] as { node: number; count: number }
|
|
126
|
+
const curr = counts[rank] as { node: number; count: number }
|
|
127
|
+
if (nodeId(prev.node) === nodeId(curr.node)) {
|
|
128
|
+
counts[rank - 1] = curr
|
|
129
|
+
counts[rank] = prev
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
let offset = 0
|
|
133
|
+
for (const entry of counts) {
|
|
134
|
+
offset += entry.count
|
|
135
|
+
if (offset > i) {
|
|
136
|
+
return entry.node === ENDMARKER ? undefined : entry.node
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
decompressArrays() {
|
|
143
|
+
const offsets = this.edges.map(e => e.offset)
|
|
144
|
+
let total = 0
|
|
145
|
+
for (const run of this.runs()) {
|
|
146
|
+
total += run.len
|
|
147
|
+
}
|
|
148
|
+
const nodes = new Int32Array(total)
|
|
149
|
+
const nextOffsets = new Int32Array(total)
|
|
150
|
+
let i = 0
|
|
151
|
+
for (const run of this.runs()) {
|
|
152
|
+
const edge = this.edge(run.value)
|
|
153
|
+
let offset = offsets[run.value]!
|
|
154
|
+
for (let k = 0; k < run.len; k++) {
|
|
155
|
+
nodes[i] = edge.node
|
|
156
|
+
nextOffsets[i] = offset
|
|
157
|
+
offset += 1
|
|
158
|
+
i += 1
|
|
159
|
+
}
|
|
160
|
+
offsets[run.value] = offset
|
|
161
|
+
}
|
|
162
|
+
return { nodes, offsets: nextOffsets }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
decompress(): Pos[] {
|
|
166
|
+
const offsets = this.edges.map(e => e.offset)
|
|
167
|
+
const result: Pos[] = []
|
|
168
|
+
for (const run of this.runs()) {
|
|
169
|
+
const edge = this.edge(run.value)
|
|
170
|
+
for (let k = 0; k < run.len; k++) {
|
|
171
|
+
result.push({ node: edge.node, offset: offsets[run.value]! })
|
|
172
|
+
offsets[run.value] = offsets[run.value]! + 1
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result
|
|
176
|
+
}
|
|
177
|
+
}
|