@gmod/gbz-base 1.0.0 → 2.0.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 +166 -26
- package/bin/query.js +0 -0
- package/dist/cli.js +76 -28
- package/dist/cli.js.map +1 -1
- package/dist/db.d.ts +26 -9
- package/dist/db.js +158 -24
- package/dist/db.js.map +1 -1
- package/dist/gbwt/record.d.ts +4 -0
- package/dist/gbwt/record.js +22 -0
- package/dist/gbwt/record.js.map +1 -1
- 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 +8 -5
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/pathName.d.ts +17 -0
- package/dist/pathName.js +33 -0
- package/dist/pathName.js.map +1 -0
- package/dist/query.d.ts +9 -9
- package/dist/query.js +23 -17
- package/dist/query.js.map +1 -1
- package/dist/sqlite/btree.d.ts +2 -0
- package/dist/sqlite/btree.js +55 -20
- package/dist/sqlite/btree.js.map +1 -1
- package/dist/sqlite/pager.d.ts +5 -0
- package/dist/sqlite/pager.js +65 -8
- package/dist/sqlite/pager.js.map +1 -1
- package/dist/subgraph.d.ts +34 -11
- package/dist/subgraph.js +346 -87
- package/dist/subgraph.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +85 -24
- package/src/db.ts +225 -33
- package/src/gbwt/record.ts +23 -0
- package/src/graphName.ts +94 -0
- package/src/index.ts +15 -10
- package/src/pathName.ts +54 -0
- package/src/query.ts +41 -29
- package/src/sqlite/btree.ts +68 -19
- package/src/sqlite/pager.ts +78 -8
- package/src/subgraph.ts +430 -114
- package/tools/haplotype-index/src/main.rs +205 -163
package/src/db.ts
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import { ENDMARKER, nodeId, nodeOrientation } from './gbwt/node.ts'
|
|
2
2
|
import { GbwtRecord, decompressEdges } from './gbwt/record.ts'
|
|
3
3
|
import { decodeSequence, encodedSequenceLength } from './gbwt/sequence.ts'
|
|
4
|
+
import { graphNameFromTags } from './graphName.ts'
|
|
5
|
+
import { formatPathName, pathNameFor, toPathQuery } from './pathName.ts'
|
|
6
|
+
import { subgraphInInterval } from './query.ts'
|
|
4
7
|
import { SqliteDatabase } from './sqlite/database.ts'
|
|
5
8
|
|
|
6
9
|
import type { ByteSource } from './filehandle.ts'
|
|
7
10
|
import type { Pos } from './gbwt/record.ts'
|
|
11
|
+
import type { GraphName } from './graphName.ts'
|
|
12
|
+
import type { PathName, PathRef } from './pathName.ts'
|
|
13
|
+
import type { QueryOptions } from './query.ts'
|
|
8
14
|
import type { PagerOptions } from './sqlite/pager.ts'
|
|
9
15
|
import type { SqlValue } from './sqlite/record.ts'
|
|
16
|
+
import type { HaplotypeAlignment, Subgraph } from './subgraph.ts'
|
|
10
17
|
|
|
11
|
-
export interface PathName {
|
|
12
|
-
sample: string
|
|
13
|
-
contig: string
|
|
14
|
-
haplotype: number
|
|
15
|
-
fragment: number
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const GENERIC_SAMPLE = '_gbwt_ref'
|
|
19
18
|
export const SCHEMA_VERSION = 'GBZ-base version 4'
|
|
20
19
|
|
|
20
|
+
export interface PathFragment {
|
|
21
|
+
path: GbzPath
|
|
22
|
+
start: number
|
|
23
|
+
end: number
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export class SchemaVersionError extends Error {
|
|
22
27
|
override name = 'SchemaVersionError'
|
|
23
28
|
|
|
@@ -33,10 +38,6 @@ export class SchemaVersionError extends Error {
|
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
export function formatPathName(name: PathName, end: number) {
|
|
37
|
-
return `${name.sample}#${name.haplotype}#${name.contig}[${name.fragment}-${end}]`
|
|
38
|
-
}
|
|
39
|
-
|
|
40
41
|
export interface HaplotypeSample {
|
|
41
42
|
node: number
|
|
42
43
|
offset: number
|
|
@@ -144,37 +145,63 @@ function rowToPath(rowid: number, values: SqlValue[]): GbzPath {
|
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
export interface OpenOptions extends PagerOptions {
|
|
149
|
+
haplotypeIndex?: ByteSource
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function readTags(sqlite: SqliteDatabase) {
|
|
153
|
+
const tags = new Map<string, string>()
|
|
154
|
+
for await (const { values } of sqlite.scan('Tags')) {
|
|
155
|
+
tags.set(str(values[0], 'Tags.key'), str(values[1], 'Tags.value'))
|
|
156
|
+
}
|
|
157
|
+
return tags
|
|
158
|
+
}
|
|
159
|
+
|
|
147
160
|
export class GBZBase {
|
|
148
161
|
private tagCache: Promise<Map<string, string>> | undefined
|
|
149
162
|
private pathCache: Promise<GbzPath[]> | undefined
|
|
163
|
+
private indexTags: Map<string, string> | undefined
|
|
150
164
|
|
|
151
165
|
readonly sqlite: SqliteDatabase
|
|
166
|
+
readonly index: SqliteDatabase
|
|
152
167
|
|
|
153
|
-
private constructor(sqlite: SqliteDatabase) {
|
|
168
|
+
private constructor(sqlite: SqliteDatabase, index: SqliteDatabase) {
|
|
154
169
|
this.sqlite = sqlite
|
|
170
|
+
this.index = index
|
|
155
171
|
}
|
|
156
172
|
|
|
157
|
-
static async open(source: ByteSource, opts:
|
|
158
|
-
const
|
|
173
|
+
static async open(source: ByteSource, opts: OpenOptions = {}) {
|
|
174
|
+
const { haplotypeIndex, ...pagerOptions } = opts
|
|
175
|
+
const sqlite = await SqliteDatabase.open(source, pagerOptions)
|
|
159
176
|
for (const table of ['Tags', 'Nodes', 'Paths', 'ReferenceIndex']) {
|
|
160
177
|
sqlite.rootPage(table)
|
|
161
178
|
}
|
|
162
|
-
const
|
|
179
|
+
const index = haplotypeIndex
|
|
180
|
+
? await SqliteDatabase.open(haplotypeIndex, pagerOptions)
|
|
181
|
+
: sqlite
|
|
182
|
+
const db = new GBZBase(sqlite, index)
|
|
163
183
|
const version = await db.tag('version')
|
|
164
184
|
if (version !== SCHEMA_VERSION) {
|
|
165
185
|
throw new SchemaVersionError(version)
|
|
166
186
|
}
|
|
187
|
+
if (haplotypeIndex) {
|
|
188
|
+
for (const table of ['Tags', 'HaplotypeSamples', 'HaplotypeLengths']) {
|
|
189
|
+
index.rootPage(table)
|
|
190
|
+
}
|
|
191
|
+
db.indexTags = await readTags(index)
|
|
192
|
+
const indexed = db.indexTags.get('haplotype_index_paths')
|
|
193
|
+
const paths = await db.tag('paths')
|
|
194
|
+
if (indexed !== paths) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`haplotype index was built for ${indexed ?? 'an unknown number of'} paths but the graph has ${paths}`,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
167
200
|
return db
|
|
168
201
|
}
|
|
169
202
|
|
|
170
203
|
tags() {
|
|
171
|
-
this.tagCache ??= (
|
|
172
|
-
const tags = new Map<string, string>()
|
|
173
|
-
for await (const { values } of this.sqlite.scan('Tags')) {
|
|
174
|
-
tags.set(str(values[0], 'Tags.key'), str(values[1], 'Tags.value'))
|
|
175
|
-
}
|
|
176
|
-
return tags
|
|
177
|
-
})()
|
|
204
|
+
this.tagCache ??= readTags(this.sqlite)
|
|
178
205
|
return this.tagCache
|
|
179
206
|
}
|
|
180
207
|
|
|
@@ -228,14 +255,182 @@ export class GBZBase {
|
|
|
228
255
|
return (await this.paths()).filter(p => p.name.sample === sample)
|
|
229
256
|
}
|
|
230
257
|
|
|
258
|
+
private async pathsNamed(ref: PathRef) {
|
|
259
|
+
const name = pathNameFor(toPathQuery(ref), 0)
|
|
260
|
+
return (await this.paths())
|
|
261
|
+
.filter(
|
|
262
|
+
p =>
|
|
263
|
+
p.name.sample === name.sample &&
|
|
264
|
+
p.name.contig === name.contig &&
|
|
265
|
+
p.name.haplotype === name.haplotype,
|
|
266
|
+
)
|
|
267
|
+
.sort((a, b) => a.name.fragment - b.name.fragment)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async hasPath(ref: PathRef) {
|
|
271
|
+
return (await this.pathsNamed(ref)).length > 0
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private pathLengths = new Map<number, Promise<number>>()
|
|
275
|
+
|
|
276
|
+
pathLength(handle: number) {
|
|
277
|
+
let length = this.pathLengths.get(handle)
|
|
278
|
+
if (!length) {
|
|
279
|
+
length = this.walkPathLength(handle)
|
|
280
|
+
this.pathLengths.set(handle, length)
|
|
281
|
+
length.catch(() => this.pathLengths.delete(handle))
|
|
282
|
+
}
|
|
283
|
+
return length
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private async walkPathLength(handle: number) {
|
|
287
|
+
const indexed = this.hasHaplotypeIndex
|
|
288
|
+
? await this.haplotypeLength(handle)
|
|
289
|
+
: undefined
|
|
290
|
+
if (indexed === undefined) {
|
|
291
|
+
const last = await this.indexedPosition(handle, Number.MAX_SAFE_INTEGER)
|
|
292
|
+
if (!last) {
|
|
293
|
+
const path = await this.getPath(handle)
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Path ${path ? formatPathName(path.name, path.name.fragment) : handle} has not been indexed for random access`,
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
let length = last.pathOffset
|
|
299
|
+
let pos = last.pos
|
|
300
|
+
for (;;) {
|
|
301
|
+
const record = await this.getRecord(pos.node)
|
|
302
|
+
if (!record) {
|
|
303
|
+
throw new Error(`Node ${pos.node} does not exist in the graph`)
|
|
304
|
+
}
|
|
305
|
+
length += record.sequenceLen
|
|
306
|
+
const next = record.gbwt().lf(pos.offset)
|
|
307
|
+
if (!next || next.node === ENDMARKER) {
|
|
308
|
+
return length
|
|
309
|
+
}
|
|
310
|
+
pos = next
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return indexed
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async pathFragmentsForRange(
|
|
317
|
+
ref: PathRef,
|
|
318
|
+
start: number,
|
|
319
|
+
end: number,
|
|
320
|
+
): Promise<PathFragment[]> {
|
|
321
|
+
if (end <= start) {
|
|
322
|
+
return []
|
|
323
|
+
}
|
|
324
|
+
const ordered = await this.pathsNamed(ref)
|
|
325
|
+
const covering = ordered.filter(p => p.name.fragment <= start).slice(-1)
|
|
326
|
+
const within = ordered.filter(
|
|
327
|
+
p => p.name.fragment > start && p.name.fragment < end,
|
|
328
|
+
)
|
|
329
|
+
const fragments = await Promise.all(
|
|
330
|
+
[...covering, ...within].map(async path => ({
|
|
331
|
+
path,
|
|
332
|
+
start: path.name.fragment,
|
|
333
|
+
end: path.name.fragment + (await this.pathLength(path.handle)),
|
|
334
|
+
})),
|
|
335
|
+
)
|
|
336
|
+
return fragments.filter(f => start < f.end)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private async subgraphForFragment(
|
|
340
|
+
fragment: PathFragment,
|
|
341
|
+
start: number,
|
|
342
|
+
end: number,
|
|
343
|
+
opts: QueryOptions,
|
|
344
|
+
) {
|
|
345
|
+
const lo = Math.max(start, fragment.start)
|
|
346
|
+
const hi = Math.min(end, fragment.end)
|
|
347
|
+
let subgraph: Subgraph | undefined
|
|
348
|
+
if (hi > lo) {
|
|
349
|
+
const { sample, contig, haplotype } = fragment.path.name
|
|
350
|
+
subgraph = await subgraphInInterval(
|
|
351
|
+
this,
|
|
352
|
+
{ sample, contig, haplotype },
|
|
353
|
+
lo,
|
|
354
|
+
hi,
|
|
355
|
+
opts,
|
|
356
|
+
)
|
|
357
|
+
const haplotypes = opts.haplotypes ?? 'all'
|
|
358
|
+
const named = haplotypes === 'all' || haplotypes === 'distinct'
|
|
359
|
+
if (this.hasHaplotypeIndex && named) {
|
|
360
|
+
await subgraph.identifyPaths()
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return subgraph
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async getSubgraphForRange(
|
|
367
|
+
ref: PathRef,
|
|
368
|
+
start: number,
|
|
369
|
+
end: number,
|
|
370
|
+
opts: QueryOptions = {},
|
|
371
|
+
) {
|
|
372
|
+
const [fragment] = await this.pathFragmentsForRange(ref, start, end)
|
|
373
|
+
return fragment
|
|
374
|
+
? this.subgraphForFragment(fragment, start, end, opts)
|
|
375
|
+
: undefined
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async getAlignmentsForRange(
|
|
379
|
+
ref: PathRef,
|
|
380
|
+
start: number,
|
|
381
|
+
end: number,
|
|
382
|
+
opts: QueryOptions = {},
|
|
383
|
+
) {
|
|
384
|
+
const result: HaplotypeAlignment[] = []
|
|
385
|
+
for (const fragment of await this.pathFragmentsForRange(ref, start, end)) {
|
|
386
|
+
const subgraph = await this.subgraphForFragment(
|
|
387
|
+
fragment,
|
|
388
|
+
start,
|
|
389
|
+
end,
|
|
390
|
+
opts,
|
|
391
|
+
)
|
|
392
|
+
if (subgraph) {
|
|
393
|
+
result.push(...subgraph.alignments())
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return result
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async graphName() {
|
|
400
|
+
const gbzTags = new Map<string, string>()
|
|
401
|
+
for (const [key, value] of await this.tags()) {
|
|
402
|
+
if (key.startsWith('gbz_')) {
|
|
403
|
+
gbzTags.set(key.slice('gbz_'.length), value)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
let name: GraphName = {
|
|
407
|
+
name: undefined,
|
|
408
|
+
subgraph: new Map(),
|
|
409
|
+
translation: new Map(),
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
name = graphNameFromTags(gbzTags)
|
|
413
|
+
} catch {
|
|
414
|
+
// upstream falls back to an empty name when the tags do not parse
|
|
415
|
+
}
|
|
416
|
+
return name
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async hasChainLinks() {
|
|
420
|
+
const links = await this.tag('chain_links')
|
|
421
|
+
return links !== undefined && Number(links) > 0
|
|
422
|
+
}
|
|
423
|
+
|
|
231
424
|
get hasHaplotypeIndex() {
|
|
232
425
|
return (
|
|
233
|
-
this.
|
|
426
|
+
this.index.has('HaplotypeSamples') && this.index.has('HaplotypeLengths')
|
|
234
427
|
)
|
|
235
428
|
}
|
|
236
429
|
|
|
237
430
|
async haplotypeSampleInterval() {
|
|
238
|
-
const value =
|
|
431
|
+
const value = this.indexTags
|
|
432
|
+
? this.indexTags.get('haplotype_index_interval')
|
|
433
|
+
: await this.tag('haplotype_index_interval')
|
|
239
434
|
return value === undefined ? undefined : Number(value)
|
|
240
435
|
}
|
|
241
436
|
|
|
@@ -254,7 +449,7 @@ export class GBZBase {
|
|
|
254
449
|
|
|
255
450
|
async haplotypeSamplesInRange(minHandle: number, maxHandle: number) {
|
|
256
451
|
const samples: HaplotypeSample[] = []
|
|
257
|
-
for await (const key of this.
|
|
452
|
+
for await (const key of this.index.indexScanFrom('HaplotypeSamples', [
|
|
258
453
|
minHandle,
|
|
259
454
|
0,
|
|
260
455
|
])) {
|
|
@@ -262,7 +457,7 @@ export class GBZBase {
|
|
|
262
457
|
if (node > maxHandle) {
|
|
263
458
|
break
|
|
264
459
|
}
|
|
265
|
-
const row = await this.
|
|
460
|
+
const row = await this.index.byRowid(
|
|
266
461
|
'HaplotypeSamples',
|
|
267
462
|
num(key[2], 'HaplotypeSamples rowid'),
|
|
268
463
|
)
|
|
@@ -274,14 +469,11 @@ export class GBZBase {
|
|
|
274
469
|
}
|
|
275
470
|
|
|
276
471
|
async haplotypeSampleAt(node: number, offset: number) {
|
|
277
|
-
const key = await this.
|
|
278
|
-
node,
|
|
279
|
-
offset,
|
|
280
|
-
])
|
|
472
|
+
const key = await this.index.indexSeekLE('HaplotypeSamples', [node, offset])
|
|
281
473
|
if (key?.[0] !== node || key[1] !== offset) {
|
|
282
474
|
return undefined
|
|
283
475
|
}
|
|
284
|
-
const row = await this.
|
|
476
|
+
const row = await this.index.byRowid(
|
|
285
477
|
'HaplotypeSamples',
|
|
286
478
|
num(key[2], 'HaplotypeSamples rowid'),
|
|
287
479
|
)
|
|
@@ -289,7 +481,7 @@ export class GBZBase {
|
|
|
289
481
|
}
|
|
290
482
|
|
|
291
483
|
async haplotypeLength(pathHandle: number) {
|
|
292
|
-
const row = await this.
|
|
484
|
+
const row = await this.index.byRowid('HaplotypeLengths', pathHandle)
|
|
293
485
|
return row ? num(row[1], 'HaplotypeLengths.length') : undefined
|
|
294
486
|
}
|
|
295
487
|
|
package/src/gbwt/record.ts
CHANGED
|
@@ -139,6 +139,29 @@ export class GbwtRecord {
|
|
|
139
139
|
return undefined
|
|
140
140
|
}
|
|
141
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
|
+
|
|
142
165
|
decompress(): Pos[] {
|
|
143
166
|
const offsets = this.edges.map(e => e.offset)
|
|
144
167
|
const result: Pos[] = []
|
package/src/graphName.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export interface GraphName {
|
|
2
|
+
name: string | undefined
|
|
3
|
+
subgraph: Map<string, Set<string>>
|
|
4
|
+
translation: Map<string, Set<string>>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function parseRelationships(field: string | undefined, what: string) {
|
|
8
|
+
const relationships = new Map<string, Set<string>>()
|
|
9
|
+
if (field !== undefined) {
|
|
10
|
+
for (const rel of field.split(';')) {
|
|
11
|
+
const parts = rel.split(',')
|
|
12
|
+
const [from, to] = parts
|
|
13
|
+
if (parts.length !== 2 || !from || !to) {
|
|
14
|
+
throw new Error(`Invalid ${what} relationship: ${rel}`)
|
|
15
|
+
}
|
|
16
|
+
let targets = relationships.get(from)
|
|
17
|
+
if (!targets) {
|
|
18
|
+
targets = new Set()
|
|
19
|
+
relationships.set(from, targets)
|
|
20
|
+
}
|
|
21
|
+
targets.add(to)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return relationships
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function graphNameFromTags(tags: Map<string, string>): GraphName {
|
|
28
|
+
return {
|
|
29
|
+
name: tags.get('pggname'),
|
|
30
|
+
subgraph: parseRelationships(tags.get('subgraph'), 'subgraph'),
|
|
31
|
+
translation: parseRelationships(tags.get('translation'), 'translation'),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function merge(into: Map<string, Set<string>>, from: Map<string, Set<string>>) {
|
|
36
|
+
for (const [key, values] of from) {
|
|
37
|
+
let targets = into.get(key)
|
|
38
|
+
if (!targets) {
|
|
39
|
+
targets = new Set()
|
|
40
|
+
into.set(key, targets)
|
|
41
|
+
}
|
|
42
|
+
for (const value of values) {
|
|
43
|
+
targets.add(value)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function subgraphName(name: string, parent: GraphName): GraphName {
|
|
49
|
+
const result: GraphName = {
|
|
50
|
+
name,
|
|
51
|
+
subgraph: new Map(),
|
|
52
|
+
translation: new Map(),
|
|
53
|
+
}
|
|
54
|
+
if (parent.name !== undefined) {
|
|
55
|
+
result.subgraph.set(name, new Set([parent.name]))
|
|
56
|
+
merge(result.subgraph, parent.subgraph)
|
|
57
|
+
merge(result.translation, parent.translation)
|
|
58
|
+
}
|
|
59
|
+
return result
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sortedEntries(relationships: Map<string, Set<string>>) {
|
|
63
|
+
return [...relationships.entries()]
|
|
64
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
65
|
+
.flatMap(([from, tos]) => [...tos].sort().map(to => [from, to] as const))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function gfaHeaderLines(graphName: GraphName) {
|
|
69
|
+
const lines: string[] = []
|
|
70
|
+
if (graphName.name !== undefined) {
|
|
71
|
+
lines.push(`H\tNM:Z:${graphName.name}`)
|
|
72
|
+
}
|
|
73
|
+
for (const [subgraph, supergraph] of sortedEntries(graphName.subgraph)) {
|
|
74
|
+
lines.push(`H\tSG:Z:${subgraph},${supergraph}`)
|
|
75
|
+
}
|
|
76
|
+
for (const [from, to] of sortedEntries(graphName.translation)) {
|
|
77
|
+
lines.push(`H\tTL:Z:${from},${to}`)
|
|
78
|
+
}
|
|
79
|
+
return lines
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function sha256Hex(chunks: Uint8Array[]) {
|
|
83
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
|
|
84
|
+
const bytes = new Uint8Array(total)
|
|
85
|
+
let offset = 0
|
|
86
|
+
for (const chunk of chunks) {
|
|
87
|
+
bytes.set(chunk, offset)
|
|
88
|
+
offset += chunk.length
|
|
89
|
+
}
|
|
90
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes)
|
|
91
|
+
return [...new Uint8Array(digest)]
|
|
92
|
+
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
93
|
+
.join('')
|
|
94
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
formatPathName,
|
|
1
|
+
export { GBZBase, GbzRecord, SCHEMA_VERSION, SchemaVersionError } from './db.ts'
|
|
2
|
+
export type {
|
|
3
|
+
GbzPath,
|
|
4
|
+
HaplotypeSample,
|
|
5
|
+
OpenOptions,
|
|
6
|
+
PathFragment,
|
|
8
7
|
} from './db.ts'
|
|
9
|
-
export
|
|
8
|
+
export { GENERIC_SAMPLE, formatPathName, parsePathName } from './pathName.ts'
|
|
9
|
+
export type { PathName, PathQuery, PathRef } from './pathName.ts'
|
|
10
10
|
export type { ByteSource } from './filehandle.ts'
|
|
11
11
|
export type { Pos } from './gbwt/record.ts'
|
|
12
12
|
export { Subgraph } from './subgraph.ts'
|
|
13
13
|
export type {
|
|
14
|
+
AlignmentSpan,
|
|
14
15
|
HaplotypeAlignment,
|
|
15
16
|
HaplotypeOutput,
|
|
16
17
|
PathIdentity,
|
|
17
18
|
PathPosition,
|
|
18
19
|
ReferencePath,
|
|
20
|
+
SnarlOutput,
|
|
19
21
|
SubgraphJson,
|
|
22
|
+
SubgraphOptions,
|
|
23
|
+
SubgraphOutputOptions,
|
|
20
24
|
SubgraphPath,
|
|
21
|
-
ToJsonOptions,
|
|
22
25
|
} from './subgraph.ts'
|
|
23
26
|
export {
|
|
24
27
|
subgraphAroundNodes,
|
|
25
28
|
subgraphAtOffset,
|
|
29
|
+
subgraphBetween,
|
|
26
30
|
subgraphInInterval,
|
|
27
31
|
} from './query.ts'
|
|
28
|
-
export type {
|
|
32
|
+
export type { QueryOptions } from './query.ts'
|
|
29
33
|
export { SqliteDatabase } from './sqlite/database.ts'
|
|
34
|
+
export type { GraphName } from './graphName.ts'
|
|
30
35
|
export { weightedLcs } from './lcs.ts'
|
|
31
36
|
export * as nodes from './gbwt/node.ts'
|
package/src/pathName.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface PathName {
|
|
2
|
+
sample: string
|
|
3
|
+
contig: string
|
|
4
|
+
haplotype: number
|
|
5
|
+
fragment: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PathQuery {
|
|
9
|
+
sample?: string
|
|
10
|
+
contig: string
|
|
11
|
+
haplotype?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type PathRef = string | PathQuery
|
|
15
|
+
|
|
16
|
+
export const GENERIC_SAMPLE = '_gbwt_ref'
|
|
17
|
+
|
|
18
|
+
export function formatPathName(name: PathName, end: number) {
|
|
19
|
+
return `${name.sample}#${name.haplotype}#${name.contig}[${name.fragment}-${end}]`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PAN_SN = /^([^#]+)#(\d+)#([^#]+?)(?:\[\d+-\d+\])?$/
|
|
23
|
+
const BARE_CONTIG = /^([^#]+?)(?:\[\d+-\d+\])?$/
|
|
24
|
+
|
|
25
|
+
export function parsePathName(name: string): PathQuery {
|
|
26
|
+
const panSn = PAN_SN.exec(name)
|
|
27
|
+
if (panSn) {
|
|
28
|
+
return {
|
|
29
|
+
sample: panSn[1]!,
|
|
30
|
+
haplotype: Number(panSn[2]),
|
|
31
|
+
contig: panSn[3]!,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const bare = BARE_CONTIG.exec(name)
|
|
35
|
+
if (bare) {
|
|
36
|
+
return { contig: bare[1]! }
|
|
37
|
+
}
|
|
38
|
+
throw new Error(
|
|
39
|
+
`"${name}" is not a path name; expected "sample#haplotype#contig" or a bare contig`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function toPathQuery(ref: PathRef) {
|
|
44
|
+
return typeof ref === 'string' ? parsePathName(ref) : ref
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function pathNameFor(query: PathQuery, fragment: number): PathName {
|
|
48
|
+
return {
|
|
49
|
+
sample: query.sample ?? GENERIC_SAMPLE,
|
|
50
|
+
contig: query.contig,
|
|
51
|
+
haplotype: query.haplotype ?? 0,
|
|
52
|
+
fragment,
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -1,28 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pathNameFor } from './pathName.ts'
|
|
2
2
|
import { Subgraph } from './subgraph.ts'
|
|
3
3
|
|
|
4
|
-
import type { GBZBase
|
|
5
|
-
import type {
|
|
4
|
+
import type { GBZBase } from './db.ts'
|
|
5
|
+
import type { PathQuery } from './pathName.ts'
|
|
6
|
+
import type { HaplotypeOutput, SnarlOutput } from './subgraph.ts'
|
|
6
7
|
|
|
7
|
-
export
|
|
8
|
-
context?: number
|
|
9
|
-
haplotypes?: HaplotypeOutput
|
|
10
|
-
limit?: number
|
|
11
|
-
}
|
|
8
|
+
export type { PathQuery } from './pathName.ts'
|
|
12
9
|
|
|
13
|
-
export interface
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function pathName(query: PathQuery, fragment: number): PathName {
|
|
20
|
-
return {
|
|
21
|
-
sample: query.sample ?? GENERIC_SAMPLE,
|
|
22
|
-
contig: query.contig,
|
|
23
|
-
haplotype: query.haplotype ?? 0,
|
|
24
|
-
fragment,
|
|
25
|
-
}
|
|
10
|
+
export interface QueryOptions {
|
|
11
|
+
context?: number | undefined
|
|
12
|
+
haplotypes?: HaplotypeOutput | undefined
|
|
13
|
+
snarls?: SnarlOutput | undefined
|
|
14
|
+
limit?: number | undefined
|
|
15
|
+
signal?: AbortSignal | undefined
|
|
26
16
|
}
|
|
27
17
|
|
|
28
18
|
export async function subgraphAtOffset(
|
|
@@ -31,14 +21,14 @@ export async function subgraphAtOffset(
|
|
|
31
21
|
offset: number,
|
|
32
22
|
opts: QueryOptions = {},
|
|
33
23
|
) {
|
|
34
|
-
const subgraph = new Subgraph(db)
|
|
35
|
-
|
|
36
|
-
const reference = await subgraph.pathPosition(pathName(query, offset))
|
|
24
|
+
const subgraph = new Subgraph(db, opts)
|
|
25
|
+
const reference = await subgraph.pathPosition(pathNameFor(query, offset))
|
|
37
26
|
await subgraph.aroundPosition(
|
|
38
27
|
reference.position.handle,
|
|
39
28
|
reference.position.nodeOffset,
|
|
40
29
|
opts.context ?? 100,
|
|
41
30
|
)
|
|
31
|
+
await subgraph.extractSnarls(opts.snarls ?? 'none')
|
|
42
32
|
subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
|
|
43
33
|
return subgraph
|
|
44
34
|
}
|
|
@@ -50,14 +40,14 @@ export async function subgraphInInterval(
|
|
|
50
40
|
end: number,
|
|
51
41
|
opts: QueryOptions = {},
|
|
52
42
|
) {
|
|
53
|
-
const subgraph = new Subgraph(db)
|
|
54
|
-
|
|
55
|
-
const reference = await subgraph.pathPosition(pathName(query, start))
|
|
43
|
+
const subgraph = new Subgraph(db, opts)
|
|
44
|
+
const reference = await subgraph.pathPosition(pathNameFor(query, start))
|
|
56
45
|
await subgraph.aroundInterval(
|
|
57
46
|
reference.position,
|
|
58
47
|
end - start,
|
|
59
48
|
opts.context ?? 100,
|
|
60
49
|
)
|
|
50
|
+
await subgraph.extractSnarls(opts.snarls ?? 'none')
|
|
61
51
|
subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
|
|
62
52
|
return subgraph
|
|
63
53
|
}
|
|
@@ -68,12 +58,34 @@ export async function subgraphAroundNodes(
|
|
|
68
58
|
opts: QueryOptions = {},
|
|
69
59
|
) {
|
|
70
60
|
const haplotypes = opts.haplotypes ?? 'all'
|
|
61
|
+
const snarls = opts.snarls ?? 'none'
|
|
71
62
|
if (haplotypes === 'reference-only') {
|
|
72
63
|
throw new Error('Cannot output a reference path in a node-based query')
|
|
73
64
|
}
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
if (snarls === 'overlapping' && nodes.length > 1) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'Overlapping snarls cannot be extracted for a node-based query with multiple nodes',
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
const subgraph = new Subgraph(db, opts)
|
|
76
71
|
await subgraph.aroundNodes(nodes, opts.context ?? 100)
|
|
72
|
+
await subgraph.extractSnarls(snarls)
|
|
73
|
+
subgraph.extractPaths(undefined, haplotypes)
|
|
74
|
+
return subgraph
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function subgraphBetween(
|
|
78
|
+
db: GBZBase,
|
|
79
|
+
start: number,
|
|
80
|
+
end: number,
|
|
81
|
+
opts: Pick<QueryOptions, 'haplotypes' | 'limit' | 'signal'> = {},
|
|
82
|
+
) {
|
|
83
|
+
const haplotypes = opts.haplotypes ?? 'all'
|
|
84
|
+
if (haplotypes === 'reference-only') {
|
|
85
|
+
throw new Error('Cannot output a reference path in a node-based query')
|
|
86
|
+
}
|
|
87
|
+
const subgraph = new Subgraph(db, opts)
|
|
88
|
+
await subgraph.betweenNodes(start, end)
|
|
77
89
|
subgraph.extractPaths(undefined, haplotypes)
|
|
78
90
|
return subgraph
|
|
79
91
|
}
|