@gmod/gbz-base 1.0.0 → 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 +43 -2
- package/bin/query.js +0 -0
- package/dist/cli.js +66 -19
- package/dist/cli.js.map +1 -1
- package/dist/db.d.ts +9 -1
- package/dist/db.js +63 -20
- 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 +4 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/query.d.ts +3 -1
- package/dist/query.js +18 -0
- package/dist/query.js.map +1 -1
- package/dist/sqlite/btree.js +7 -2
- 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 +10 -0
- package/dist/subgraph.js +314 -72
- package/dist/subgraph.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +69 -14
- package/src/db.ts +73 -21
- package/src/gbwt/record.ts +23 -0
- package/src/graphName.ts +94 -0
- package/src/index.ts +4 -1
- package/src/query.ts +28 -1
- package/src/sqlite/btree.ts +7 -2
- package/src/sqlite/pager.ts +78 -8
- package/src/subgraph.ts +372 -85
- package/tools/haplotype-index/src/main.rs +197 -163
package/src/sqlite/pager.ts
CHANGED
|
@@ -33,6 +33,23 @@ export class Pager {
|
|
|
33
33
|
this.maxBlocks = opts.maxBlocks ?? 256
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
private async read(length: number, start: number) {
|
|
37
|
+
let attempt = 0
|
|
38
|
+
for (;;) {
|
|
39
|
+
try {
|
|
40
|
+
const bytes = await this.source.read(length, start)
|
|
41
|
+
this.bytesFetched += bytes.length
|
|
42
|
+
return bytes
|
|
43
|
+
} catch (error) {
|
|
44
|
+
attempt += 1
|
|
45
|
+
if (attempt >= 3) {
|
|
46
|
+
throw error
|
|
47
|
+
}
|
|
48
|
+
await new Promise(resolve => setTimeout(resolve, 200 * attempt))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
seed(index: number, bytes: Uint8Array) {
|
|
37
54
|
if (
|
|
38
55
|
bytes.length ===
|
|
@@ -51,19 +68,72 @@ export class Pager {
|
|
|
51
68
|
}
|
|
52
69
|
const start = index * this.blockSize
|
|
53
70
|
const length = Math.min(this.blockSize, this.fileSize - start)
|
|
54
|
-
const pending = this.
|
|
55
|
-
this.bytesFetched += bytes.length
|
|
56
|
-
return bytes
|
|
57
|
-
})
|
|
71
|
+
const pending = this.read(length, start)
|
|
58
72
|
this.fetches += 1
|
|
59
73
|
this.blocks.set(index, pending)
|
|
60
|
-
|
|
74
|
+
this.forgetOnFailure(pending, [index])
|
|
75
|
+
this.evict()
|
|
76
|
+
return pending
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
prefetch(pageNumbers: number[]) {
|
|
80
|
+
const wanted = [
|
|
81
|
+
...new Set(
|
|
82
|
+
pageNumbers.map(pageNumber =>
|
|
83
|
+
Math.floor(((pageNumber - 1) * this.pageSize) / this.blockSize),
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
.filter(index => !this.blocks.has(index))
|
|
88
|
+
.sort((a, b) => a - b)
|
|
89
|
+
let runStart = 0
|
|
90
|
+
while (runStart < wanted.length) {
|
|
91
|
+
let runEnd = runStart + 1
|
|
92
|
+
while (
|
|
93
|
+
runEnd < wanted.length &&
|
|
94
|
+
wanted[runEnd] === wanted[runEnd - 1]! + 1
|
|
95
|
+
) {
|
|
96
|
+
runEnd += 1
|
|
97
|
+
}
|
|
98
|
+
this.fetchRun(wanted[runStart]!, runEnd - runStart)
|
|
99
|
+
runStart = runEnd
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fetchRun(firstIndex: number, count: number) {
|
|
104
|
+
const start = firstIndex * this.blockSize
|
|
105
|
+
const length = Math.min(count * this.blockSize, this.fileSize - start)
|
|
106
|
+
const pending = this.read(length, start)
|
|
107
|
+
this.fetches += 1
|
|
108
|
+
const indexes: number[] = []
|
|
109
|
+
for (let i = 0; i < count; i++) {
|
|
110
|
+
const within = i * this.blockSize
|
|
111
|
+
indexes.push(firstIndex + i)
|
|
112
|
+
this.blocks.set(
|
|
113
|
+
firstIndex + i,
|
|
114
|
+
pending.then(bytes => bytes.subarray(within, within + this.blockSize)),
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
this.forgetOnFailure(pending, indexes)
|
|
118
|
+
this.evict()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private forgetOnFailure(pending: Promise<Uint8Array>, indexes: number[]) {
|
|
122
|
+
pending.catch(() => {
|
|
123
|
+
for (const index of indexes) {
|
|
124
|
+
this.blocks.delete(index)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private evict() {
|
|
130
|
+
while (this.blocks.size > this.maxBlocks) {
|
|
61
131
|
const oldest = this.blocks.keys().next().value
|
|
62
|
-
if (oldest
|
|
63
|
-
|
|
132
|
+
if (oldest === undefined) {
|
|
133
|
+
break
|
|
64
134
|
}
|
|
135
|
+
this.blocks.delete(oldest)
|
|
65
136
|
}
|
|
66
|
-
return pending
|
|
67
137
|
}
|
|
68
138
|
|
|
69
139
|
async page(pageNumber: number) {
|
package/src/subgraph.ts
CHANGED
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
entrySide,
|
|
8
8
|
exitOrientation,
|
|
9
9
|
exitSide,
|
|
10
|
+
flipNode,
|
|
10
11
|
flipSide,
|
|
11
12
|
isReverse,
|
|
12
13
|
nodeId,
|
|
13
14
|
nodeOrientation,
|
|
14
15
|
pathIsCanonical,
|
|
15
16
|
} from './gbwt/node.ts'
|
|
17
|
+
import { gfaHeaderLines, sha256Hex, subgraphName } from './graphName.ts'
|
|
16
18
|
import { weightedLcs } from './lcs.ts'
|
|
17
19
|
|
|
18
20
|
import type {
|
|
@@ -27,6 +29,13 @@ import type { Pos } from './gbwt/record.ts'
|
|
|
27
29
|
|
|
28
30
|
export type HaplotypeOutput = 'all' | 'distinct' | 'reference-only' | 'none'
|
|
29
31
|
|
|
32
|
+
export type SnarlOutput = 'none' | 'contained' | 'overlapping'
|
|
33
|
+
|
|
34
|
+
type HandleType =
|
|
35
|
+
| { kind: 'snarl-exit'; snarl: [number, number] }
|
|
36
|
+
| { kind: 'chain' }
|
|
37
|
+
| { kind: 'regular' }
|
|
38
|
+
|
|
30
39
|
export interface PathPosition {
|
|
31
40
|
seqOffset: number
|
|
32
41
|
handle: number
|
|
@@ -95,31 +104,63 @@ export interface ToJsonOptions {
|
|
|
95
104
|
names?: 'anonymous' | 'resolved'
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
function sideBefore(
|
|
108
|
+
a: [number, number, NodeSide],
|
|
109
|
+
b: [number, number, NodeSide],
|
|
110
|
+
) {
|
|
111
|
+
return (
|
|
112
|
+
a[0] < b[0] ||
|
|
113
|
+
(a[0] === b[0] && (a[1] < b[1] || (a[1] === b[1] && a[2] < b[2])))
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
98
117
|
class SideQueue {
|
|
99
|
-
private
|
|
118
|
+
private heap: [number, number, NodeSide][] = []
|
|
100
119
|
|
|
101
120
|
push(distance: number, node: number, side: NodeSide) {
|
|
102
|
-
this.
|
|
121
|
+
const heap = this.heap
|
|
122
|
+
heap.push([distance, node, side])
|
|
123
|
+
let i = heap.length - 1
|
|
124
|
+
while (i > 0) {
|
|
125
|
+
const parent = (i - 1) >> 1
|
|
126
|
+
if (sideBefore(heap[i]!, heap[parent]!)) {
|
|
127
|
+
;[heap[i], heap[parent]] = [heap[parent]!, heap[i]!]
|
|
128
|
+
i = parent
|
|
129
|
+
} else {
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
103
133
|
}
|
|
104
134
|
|
|
105
135
|
pop() {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
136
|
+
const heap = this.heap
|
|
137
|
+
const top = heap[0]
|
|
138
|
+
const last = heap.pop()
|
|
139
|
+
if (heap.length > 0 && last !== undefined) {
|
|
140
|
+
heap[0] = last
|
|
141
|
+
let i = 0
|
|
142
|
+
for (;;) {
|
|
143
|
+
const left = 2 * i + 1
|
|
144
|
+
const right = left + 1
|
|
145
|
+
let smallest = i
|
|
146
|
+
if (left < heap.length && sideBefore(heap[left]!, heap[smallest]!)) {
|
|
147
|
+
smallest = left
|
|
148
|
+
}
|
|
149
|
+
if (right < heap.length && sideBefore(heap[right]!, heap[smallest]!)) {
|
|
150
|
+
smallest = right
|
|
151
|
+
}
|
|
152
|
+
if (smallest === i) {
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
;[heap[i], heap[smallest]] = [heap[smallest]!, heap[i]!]
|
|
156
|
+
i = smallest
|
|
115
157
|
}
|
|
116
158
|
}
|
|
117
|
-
|
|
118
|
-
return item
|
|
159
|
+
return top
|
|
119
160
|
}
|
|
120
161
|
|
|
121
162
|
get size() {
|
|
122
|
-
return this.
|
|
163
|
+
return this.heap.length
|
|
123
164
|
}
|
|
124
165
|
}
|
|
125
166
|
|
|
@@ -394,6 +435,150 @@ export class Subgraph {
|
|
|
394
435
|
return { inserted, removed: toRemove.size }
|
|
395
436
|
}
|
|
396
437
|
|
|
438
|
+
async betweenNodes(start: number, end: number) {
|
|
439
|
+
this.clearPaths()
|
|
440
|
+
const active = [start, flipNode(end)]
|
|
441
|
+
const visited = new Set([nodeId(start), nodeId(end)])
|
|
442
|
+
let inserted = 0
|
|
443
|
+
while (active.length > 0) {
|
|
444
|
+
const curr = active.pop()!
|
|
445
|
+
const id = nodeId(curr)
|
|
446
|
+
if (!this.hasNode(id)) {
|
|
447
|
+
await this.addNode(id)
|
|
448
|
+
inserted += 1
|
|
449
|
+
}
|
|
450
|
+
for (const successor of this.record(curr).successors()) {
|
|
451
|
+
const successorId = nodeId(successor)
|
|
452
|
+
if (!visited.has(successorId)) {
|
|
453
|
+
active.push(successor, flipNode(successor))
|
|
454
|
+
visited.add(successorId)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return inserted
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async extractSnarls(snarls: SnarlOutput) {
|
|
462
|
+
let inserted = 0
|
|
463
|
+
for (const [start, end] of await this.overlappingSnarls(snarls)) {
|
|
464
|
+
inserted += await this.betweenNodes(start, end)
|
|
465
|
+
}
|
|
466
|
+
return inserted
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private async overlappingSnarls(snarls: SnarlOutput) {
|
|
470
|
+
const result: [number, number][] = []
|
|
471
|
+
if (snarls !== 'none') {
|
|
472
|
+
let foundLink = false
|
|
473
|
+
for (const handle of this.sortedHandles()) {
|
|
474
|
+
const record = this.record(handle)
|
|
475
|
+
const next = record.next
|
|
476
|
+
if (next !== undefined) {
|
|
477
|
+
foundLink = true
|
|
478
|
+
if (this.hasHandle(next)) {
|
|
479
|
+
if (edgeIsCanonical(handle, next)) {
|
|
480
|
+
result.push([handle, next])
|
|
481
|
+
}
|
|
482
|
+
} else if (
|
|
483
|
+
snarls === 'overlapping' &&
|
|
484
|
+
this.isSnarlEntryInSubgraph(record)
|
|
485
|
+
) {
|
|
486
|
+
result.push([handle, next])
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (
|
|
491
|
+
!foundLink &&
|
|
492
|
+
snarls === 'overlapping' &&
|
|
493
|
+
(await this.db.hasChainLinks())
|
|
494
|
+
) {
|
|
495
|
+
const covering = await this.findCoveringSnarl()
|
|
496
|
+
if (covering) {
|
|
497
|
+
result.push(covering)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return result
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private isSnarlEntryInSubgraph(record: GbzRecord) {
|
|
505
|
+
const successors = record.successors()
|
|
506
|
+
const first = successors.find(handle => this.hasHandle(handle))
|
|
507
|
+
return first === undefined
|
|
508
|
+
? false
|
|
509
|
+
: successors.length > 1 ||
|
|
510
|
+
this.record(flipNode(first)).successors().length > 1
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private recordReader(onFetch?: () => void) {
|
|
514
|
+
const outside = new Map<number, GbzRecord>()
|
|
515
|
+
return async (handle: number) => {
|
|
516
|
+
const inside = this.records.get(handle)
|
|
517
|
+
if (inside) {
|
|
518
|
+
return inside
|
|
519
|
+
}
|
|
520
|
+
let record = outside.get(handle)
|
|
521
|
+
if (!record) {
|
|
522
|
+
record = await this.db.getRecord(handle)
|
|
523
|
+
onFetch?.()
|
|
524
|
+
if (!record) {
|
|
525
|
+
throw new Error(`Node record ${handle} is missing from the database`)
|
|
526
|
+
}
|
|
527
|
+
outside.set(handle, record)
|
|
528
|
+
}
|
|
529
|
+
return record
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private async findCoveringSnarl() {
|
|
534
|
+
const read = this.recordReader()
|
|
535
|
+
const isSnarlEntry = async (record: GbzRecord) => {
|
|
536
|
+
const successors = record.successors()
|
|
537
|
+
const first = successors[0]
|
|
538
|
+
return first === undefined
|
|
539
|
+
? false
|
|
540
|
+
: successors.length > 1 ||
|
|
541
|
+
(await read(flipNode(first))).successors().length > 1
|
|
542
|
+
}
|
|
543
|
+
const classify = async (handle: number): Promise<HandleType> => {
|
|
544
|
+
const reverse = await read(flipNode(handle))
|
|
545
|
+
return reverse.next !== undefined
|
|
546
|
+
? (await isSnarlEntry(reverse))
|
|
547
|
+
? { kind: 'snarl-exit', snarl: [flipNode(handle), reverse.next] }
|
|
548
|
+
: { kind: 'chain' }
|
|
549
|
+
: (await read(handle)).next !== undefined
|
|
550
|
+
? { kind: 'chain' }
|
|
551
|
+
: { kind: 'regular' }
|
|
552
|
+
}
|
|
553
|
+
const visited = new Set<number>()
|
|
554
|
+
const queue = this.sortedHandles().flatMap(handle =>
|
|
555
|
+
this.record(handle).successors(),
|
|
556
|
+
)
|
|
557
|
+
let result: [number, number] | undefined
|
|
558
|
+
let done = false
|
|
559
|
+
while (!done && queue.length > 0) {
|
|
560
|
+
const handle = queue.shift()!
|
|
561
|
+
const id = nodeId(handle)
|
|
562
|
+
if (!this.hasHandle(handle) && !visited.has(id)) {
|
|
563
|
+
visited.add(id)
|
|
564
|
+
const type = await classify(handle)
|
|
565
|
+
if (type.kind === 'snarl-exit') {
|
|
566
|
+
result = type.snarl
|
|
567
|
+
done = true
|
|
568
|
+
} else if (type.kind === 'chain') {
|
|
569
|
+
done = true
|
|
570
|
+
} else {
|
|
571
|
+
for (const orientation of ['forward', 'reverse'] as const) {
|
|
572
|
+
queue.push(
|
|
573
|
+
...(await read(encodeNode(id, orientation))).successors(),
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return result
|
|
580
|
+
}
|
|
581
|
+
|
|
397
582
|
extractPaths(reference: ReferencePath | undefined, output: HaplotypeOutput) {
|
|
398
583
|
this.clearPaths()
|
|
399
584
|
if (output === 'none') {
|
|
@@ -405,72 +590,69 @@ export class Subgraph {
|
|
|
405
590
|
const handles = this.sortedHandles()
|
|
406
591
|
const successors = new Map<
|
|
407
592
|
number,
|
|
408
|
-
{
|
|
593
|
+
{ nodes: Int32Array; offsets: Int32Array; hasPredecessor: Uint8Array }
|
|
409
594
|
>()
|
|
410
595
|
for (const handle of handles) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
)
|
|
596
|
+
const { nodes, offsets } = this.record(handle).gbwt().decompressArrays()
|
|
597
|
+
successors.set(handle, {
|
|
598
|
+
nodes,
|
|
599
|
+
offsets,
|
|
600
|
+
hasPredecessor: new Uint8Array(nodes.length),
|
|
601
|
+
})
|
|
418
602
|
}
|
|
419
603
|
for (const handle of handles) {
|
|
420
|
-
|
|
421
|
-
|
|
604
|
+
const { nodes, offsets } = successors.get(handle)!
|
|
605
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
606
|
+
const entry = successors.get(nodes[i]!)
|
|
422
607
|
if (entry) {
|
|
423
|
-
entry.hasPredecessor =
|
|
608
|
+
entry.hasPredecessor[offsets[i]!] = 1
|
|
424
609
|
}
|
|
425
610
|
}
|
|
426
611
|
}
|
|
427
612
|
let refOffset: number | undefined
|
|
428
613
|
for (const handle of handles) {
|
|
429
|
-
const entries = successors.get(handle)
|
|
430
|
-
|
|
431
|
-
hasPredecessor
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
614
|
+
const entries = successors.get(handle)!
|
|
615
|
+
for (let offset = 0; offset < entries.nodes.length; offset++) {
|
|
616
|
+
if (entries.hasPredecessor[offset] === 0) {
|
|
617
|
+
let currNode: number | undefined = handle
|
|
618
|
+
let currOffset = offset
|
|
619
|
+
let isRef = false
|
|
620
|
+
const path: number[] = []
|
|
621
|
+
const positions: Pos[] = []
|
|
622
|
+
let len = 0
|
|
623
|
+
while (currNode !== undefined) {
|
|
624
|
+
if (
|
|
625
|
+
currNode === refPos?.handle &&
|
|
626
|
+
currOffset === refPos.gbwtOffset
|
|
627
|
+
) {
|
|
628
|
+
this.refId = this.paths.length
|
|
629
|
+
refOffset = path.length
|
|
630
|
+
isRef = true
|
|
631
|
+
}
|
|
632
|
+
path.push(currNode)
|
|
633
|
+
positions.push({ node: currNode, offset: currOffset })
|
|
634
|
+
len += this.record(currNode).sequenceLen
|
|
635
|
+
const step = successors.get(currNode)!
|
|
636
|
+
const nextNode: number = step.nodes[currOffset]!
|
|
637
|
+
const nextOffset: number = step.offsets[currOffset]!
|
|
638
|
+
if (nextNode !== ENDMARKER && successors.has(nextNode)) {
|
|
639
|
+
currNode = nextNode
|
|
640
|
+
currOffset = nextOffset
|
|
641
|
+
} else {
|
|
642
|
+
currNode = undefined
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (isRef || pathIsCanonical(path)) {
|
|
646
|
+
this.paths.push({
|
|
647
|
+
path,
|
|
648
|
+
positions,
|
|
649
|
+
len,
|
|
650
|
+
weight: undefined,
|
|
651
|
+
identity: undefined,
|
|
652
|
+
})
|
|
450
653
|
}
|
|
451
|
-
path.push(curr.node)
|
|
452
|
-
positions.push(curr)
|
|
453
|
-
len += this.record(curr.node).sequenceLen
|
|
454
|
-
const step: { next: Pos } | undefined = successors.get(curr.node)?.[
|
|
455
|
-
curr.offset
|
|
456
|
-
]
|
|
457
|
-
curr =
|
|
458
|
-
step &&
|
|
459
|
-
step.next.node !== ENDMARKER &&
|
|
460
|
-
successors.has(step.next.node)
|
|
461
|
-
? step.next
|
|
462
|
-
: undefined
|
|
463
|
-
}
|
|
464
|
-
if (isRef || pathIsCanonical(path)) {
|
|
465
|
-
this.paths.push({
|
|
466
|
-
path,
|
|
467
|
-
positions,
|
|
468
|
-
len,
|
|
469
|
-
weight: undefined,
|
|
470
|
-
identity: undefined,
|
|
471
|
-
})
|
|
472
654
|
}
|
|
473
|
-
}
|
|
655
|
+
}
|
|
474
656
|
}
|
|
475
657
|
if (refPos) {
|
|
476
658
|
if (refOffset === undefined || this.refId === undefined) {
|
|
@@ -551,23 +733,9 @@ export class Subgraph {
|
|
|
551
733
|
starts.set(posKey(first), index)
|
|
552
734
|
}
|
|
553
735
|
})
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (inside) {
|
|
558
|
-
return inside
|
|
559
|
-
}
|
|
560
|
-
let record = outside.get(handle)
|
|
561
|
-
if (!record) {
|
|
562
|
-
record = await this.db.getRecord(handle)
|
|
563
|
-
this.stats.identificationFetches += 1
|
|
564
|
-
if (!record) {
|
|
565
|
-
throw new Error(`Node record ${handle} is missing from the database`)
|
|
566
|
-
}
|
|
567
|
-
outside.set(handle, record)
|
|
568
|
-
}
|
|
569
|
-
return record
|
|
570
|
-
}
|
|
736
|
+
const recordAt = this.recordReader(() => {
|
|
737
|
+
this.stats.identificationFetches += 1
|
|
738
|
+
})
|
|
571
739
|
const sampleAt = async (pos: Pos) => {
|
|
572
740
|
if (pos.node >= minHandle && pos.node <= maxHandle) {
|
|
573
741
|
return samples.get(posKey(pos))
|
|
@@ -957,6 +1125,125 @@ export class Subgraph {
|
|
|
957
1125
|
return result
|
|
958
1126
|
}
|
|
959
1127
|
|
|
1128
|
+
private canonicalEdges(id: number) {
|
|
1129
|
+
const edges: [number, number, number][] = []
|
|
1130
|
+
for (const orientation of ['forward', 'reverse'] as const) {
|
|
1131
|
+
const handle = encodeNode(id, orientation)
|
|
1132
|
+
for (const successor of this.record(handle).successors()) {
|
|
1133
|
+
if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
|
|
1134
|
+
edges.push([
|
|
1135
|
+
orientation === 'reverse' ? 1 : 0,
|
|
1136
|
+
nodeId(successor),
|
|
1137
|
+
isReverse(successor) ? 1 : 0,
|
|
1138
|
+
])
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
edges.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2])
|
|
1143
|
+
return edges.filter(
|
|
1144
|
+
(edge, i) =>
|
|
1145
|
+
i === 0 ||
|
|
1146
|
+
edge[0] !== edges[i - 1]![0] ||
|
|
1147
|
+
edge[1] !== edges[i - 1]![1] ||
|
|
1148
|
+
edge[2] !== edges[i - 1]![2],
|
|
1149
|
+
)
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
async stableName() {
|
|
1153
|
+
const encoder = new TextEncoder()
|
|
1154
|
+
const chunks: Uint8Array[] = []
|
|
1155
|
+
for (const handle of this.sortedHandles()) {
|
|
1156
|
+
if (!isReverse(handle)) {
|
|
1157
|
+
const id = nodeId(handle)
|
|
1158
|
+
let text = `S\t${id}\t${this.record(handle).sequence}\n`
|
|
1159
|
+
for (const [fromReverse, toId, toReverse] of this.canonicalEdges(id)) {
|
|
1160
|
+
text += `L\t${id}\t${fromReverse ? '-' : '+'}\t${toId}\t${toReverse ? '-' : '+'}\n`
|
|
1161
|
+
}
|
|
1162
|
+
chunks.push(encoder.encode(text))
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return sha256Hex(chunks)
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async toGFA(cigar: boolean, opts: ToJsonOptions = {}) {
|
|
1169
|
+
const lines = [
|
|
1170
|
+
this.refPath ? `H\tVN:Z:1.1\tRS:Z:${this.refPath.sample}` : 'H\tVN:Z:1.1',
|
|
1171
|
+
...gfaHeaderLines(
|
|
1172
|
+
subgraphName(await this.stableName(), await this.db.graphName()),
|
|
1173
|
+
),
|
|
1174
|
+
]
|
|
1175
|
+
const handles = this.sortedHandles()
|
|
1176
|
+
for (const handle of handles) {
|
|
1177
|
+
if (!isReverse(handle)) {
|
|
1178
|
+
lines.push(`S\t${nodeId(handle)}\t${this.record(handle).sequence}`)
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const sign = (handle: number) => (isReverse(handle) ? '-' : '+')
|
|
1182
|
+
for (const handle of handles) {
|
|
1183
|
+
for (const successor of this.record(handle).successors()) {
|
|
1184
|
+
if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
|
|
1185
|
+
lines.push(
|
|
1186
|
+
`L\t${nodeId(handle)}\t${sign(handle)}\t${nodeId(successor)}\t${sign(successor)}\t0M`,
|
|
1187
|
+
)
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const walk = (
|
|
1192
|
+
info: PathInfo,
|
|
1193
|
+
name: PathName,
|
|
1194
|
+
end: number,
|
|
1195
|
+
cigarString: string | undefined,
|
|
1196
|
+
) => {
|
|
1197
|
+
const steps = info.path
|
|
1198
|
+
.map(handle => `${isReverse(handle) ? '<' : '>'}${nodeId(handle)}`)
|
|
1199
|
+
.join('')
|
|
1200
|
+
const weight = info.weight === undefined ? '' : `\tWT:i:${info.weight}`
|
|
1201
|
+
const cg = cigarString === undefined ? '' : `\tCG:Z:${cigarString}`
|
|
1202
|
+
return `W\t${name.sample}\t${name.haplotype}\t${name.contig}\t${name.fragment}\t${end}\t${steps}${weight}${cg}`
|
|
1203
|
+
}
|
|
1204
|
+
const contig = this.refPath?.contig ?? 'unknown'
|
|
1205
|
+
if (this.refId !== undefined && this.refPath && this.refInterval) {
|
|
1206
|
+
lines.push(
|
|
1207
|
+
walk(
|
|
1208
|
+
this.paths[this.refId]!,
|
|
1209
|
+
{
|
|
1210
|
+
...this.refPath,
|
|
1211
|
+
fragment: this.refPath.fragment + this.refInterval[0],
|
|
1212
|
+
},
|
|
1213
|
+
this.refPath.fragment + this.refInterval[1],
|
|
1214
|
+
undefined,
|
|
1215
|
+
),
|
|
1216
|
+
)
|
|
1217
|
+
}
|
|
1218
|
+
let haplotype = 1
|
|
1219
|
+
this.paths.forEach((info, index) => {
|
|
1220
|
+
if (index !== this.refId) {
|
|
1221
|
+
const resolved = opts.names === 'resolved' ? info.identity : undefined
|
|
1222
|
+
const cigarString = cigar ? this.alignToRef(index) : undefined
|
|
1223
|
+
lines.push(
|
|
1224
|
+
resolved
|
|
1225
|
+
? walk(
|
|
1226
|
+
info,
|
|
1227
|
+
{
|
|
1228
|
+
...resolved.name,
|
|
1229
|
+
fragment: resolved.name.fragment + resolved.hapStart,
|
|
1230
|
+
},
|
|
1231
|
+
resolved.name.fragment + resolved.hapEnd,
|
|
1232
|
+
cigarString,
|
|
1233
|
+
)
|
|
1234
|
+
: walk(
|
|
1235
|
+
info,
|
|
1236
|
+
{ sample: 'unknown', contig, haplotype, fragment: 0 },
|
|
1237
|
+
info.len,
|
|
1238
|
+
cigarString,
|
|
1239
|
+
),
|
|
1240
|
+
)
|
|
1241
|
+
haplotype += 1
|
|
1242
|
+
}
|
|
1243
|
+
})
|
|
1244
|
+
return `${lines.join('\n')}\n`
|
|
1245
|
+
}
|
|
1246
|
+
|
|
960
1247
|
toJSON(cigar: boolean, opts: ToJsonOptions = {}): SubgraphJson {
|
|
961
1248
|
const handles = this.sortedHandles()
|
|
962
1249
|
const nodes = handles
|