@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/subgraph.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { formatPathName } from './db.ts'
|
|
2
1
|
import {
|
|
3
2
|
ENDMARKER,
|
|
4
3
|
edgeIsCanonical,
|
|
@@ -7,26 +6,31 @@ import {
|
|
|
7
6
|
entrySide,
|
|
8
7
|
exitOrientation,
|
|
9
8
|
exitSide,
|
|
9
|
+
flipNode,
|
|
10
10
|
flipSide,
|
|
11
11
|
isReverse,
|
|
12
12
|
nodeId,
|
|
13
13
|
nodeOrientation,
|
|
14
14
|
pathIsCanonical,
|
|
15
15
|
} from './gbwt/node.ts'
|
|
16
|
+
import { gfaHeaderLines, sha256Hex, subgraphName } from './graphName.ts'
|
|
16
17
|
import { weightedLcs } from './lcs.ts'
|
|
18
|
+
import { formatPathName } from './pathName.ts'
|
|
17
19
|
|
|
18
|
-
import type {
|
|
19
|
-
GBZBase,
|
|
20
|
-
GbzPath,
|
|
21
|
-
GbzRecord,
|
|
22
|
-
HaplotypeSample,
|
|
23
|
-
PathName,
|
|
24
|
-
} from './db.ts'
|
|
20
|
+
import type { GBZBase, GbzPath, GbzRecord, HaplotypeSample } from './db.ts'
|
|
25
21
|
import type { NodeSide, Orientation } from './gbwt/node.ts'
|
|
26
22
|
import type { Pos } from './gbwt/record.ts'
|
|
23
|
+
import type { PathName } from './pathName.ts'
|
|
27
24
|
|
|
28
25
|
export type HaplotypeOutput = 'all' | 'distinct' | 'reference-only' | 'none'
|
|
29
26
|
|
|
27
|
+
export type SnarlOutput = 'none' | 'contained' | 'overlapping'
|
|
28
|
+
|
|
29
|
+
type HandleType =
|
|
30
|
+
| { kind: 'snarl-exit'; snarl: [number, number] }
|
|
31
|
+
| { kind: 'chain' }
|
|
32
|
+
| { kind: 'regular' }
|
|
33
|
+
|
|
30
34
|
export interface PathPosition {
|
|
31
35
|
seqOffset: number
|
|
32
36
|
handle: number
|
|
@@ -77,12 +81,8 @@ export interface SubgraphJson {
|
|
|
77
81
|
paths: SubgraphPath[]
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
export interface
|
|
81
|
-
pathHandle: number | undefined
|
|
82
|
-
name: PathName | undefined
|
|
84
|
+
export interface AlignmentSpan {
|
|
83
85
|
strand: '+' | '-'
|
|
84
|
-
hapStart: number | undefined
|
|
85
|
-
hapEnd: number | undefined
|
|
86
86
|
refStart: number
|
|
87
87
|
refEnd: number
|
|
88
88
|
cigar: string
|
|
@@ -91,35 +91,86 @@ export interface HaplotypeAlignment {
|
|
|
91
91
|
start: Pos
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export
|
|
95
|
-
|
|
94
|
+
export type HaplotypeAlignment = AlignmentSpan &
|
|
95
|
+
(
|
|
96
|
+
| {
|
|
97
|
+
resolved: true
|
|
98
|
+
name: PathName
|
|
99
|
+
label: string
|
|
100
|
+
pathHandle: number
|
|
101
|
+
hapStart: number
|
|
102
|
+
hapEnd: number
|
|
103
|
+
}
|
|
104
|
+
| { resolved: false }
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
export interface SubgraphOutputOptions {
|
|
108
|
+
cigar?: boolean | undefined
|
|
109
|
+
names?: 'anonymous' | 'resolved' | undefined
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface SubgraphOptions {
|
|
113
|
+
limit?: number | undefined
|
|
114
|
+
signal?: AbortSignal | undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sideBefore(
|
|
118
|
+
a: [number, number, NodeSide],
|
|
119
|
+
b: [number, number, NodeSide],
|
|
120
|
+
) {
|
|
121
|
+
return (
|
|
122
|
+
a[0] < b[0] ||
|
|
123
|
+
(a[0] === b[0] && (a[1] < b[1] || (a[1] === b[1] && a[2] < b[2])))
|
|
124
|
+
)
|
|
96
125
|
}
|
|
97
126
|
|
|
98
127
|
class SideQueue {
|
|
99
|
-
private
|
|
128
|
+
private heap: [number, number, NodeSide][] = []
|
|
100
129
|
|
|
101
130
|
push(distance: number, node: number, side: NodeSide) {
|
|
102
|
-
this.
|
|
131
|
+
const heap = this.heap
|
|
132
|
+
heap.push([distance, node, side])
|
|
133
|
+
let i = heap.length - 1
|
|
134
|
+
while (i > 0) {
|
|
135
|
+
const parent = (i - 1) >> 1
|
|
136
|
+
if (sideBefore(heap[i]!, heap[parent]!)) {
|
|
137
|
+
;[heap[i], heap[parent]] = [heap[parent]!, heap[i]!]
|
|
138
|
+
i = parent
|
|
139
|
+
} else {
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
}
|
|
103
143
|
}
|
|
104
144
|
|
|
105
145
|
pop() {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
146
|
+
const heap = this.heap
|
|
147
|
+
const top = heap[0]
|
|
148
|
+
const last = heap.pop()
|
|
149
|
+
if (heap.length > 0 && last !== undefined) {
|
|
150
|
+
heap[0] = last
|
|
151
|
+
let i = 0
|
|
152
|
+
for (;;) {
|
|
153
|
+
const left = 2 * i + 1
|
|
154
|
+
const right = left + 1
|
|
155
|
+
let smallest = i
|
|
156
|
+
if (left < heap.length && sideBefore(heap[left]!, heap[smallest]!)) {
|
|
157
|
+
smallest = left
|
|
158
|
+
}
|
|
159
|
+
if (right < heap.length && sideBefore(heap[right]!, heap[smallest]!)) {
|
|
160
|
+
smallest = right
|
|
161
|
+
}
|
|
162
|
+
if (smallest === i) {
|
|
163
|
+
break
|
|
164
|
+
}
|
|
165
|
+
;[heap[i], heap[smallest]] = [heap[smallest]!, heap[i]!]
|
|
166
|
+
i = smallest
|
|
115
167
|
}
|
|
116
168
|
}
|
|
117
|
-
|
|
118
|
-
return item
|
|
169
|
+
return top
|
|
119
170
|
}
|
|
120
171
|
|
|
121
172
|
get size() {
|
|
122
|
-
return this.
|
|
173
|
+
return this.heap.length
|
|
123
174
|
}
|
|
124
175
|
}
|
|
125
176
|
|
|
@@ -142,7 +193,6 @@ export class Subgraph {
|
|
|
142
193
|
private refInterval: [number, number] | undefined
|
|
143
194
|
private refIndexCache: Map<number, number[]> | undefined
|
|
144
195
|
private refPrefixCache: number[] | undefined
|
|
145
|
-
limit: number | undefined
|
|
146
196
|
readonly stats = {
|
|
147
197
|
orderedAlignments: 0,
|
|
148
198
|
lcsAlignments: 0,
|
|
@@ -151,9 +201,13 @@ export class Subgraph {
|
|
|
151
201
|
}
|
|
152
202
|
|
|
153
203
|
private db: GBZBase
|
|
204
|
+
private readonly limit: number | undefined
|
|
205
|
+
private readonly signal: AbortSignal | undefined
|
|
154
206
|
|
|
155
|
-
constructor(db: GBZBase) {
|
|
207
|
+
constructor(db: GBZBase, opts: SubgraphOptions = {}) {
|
|
156
208
|
this.db = db
|
|
209
|
+
this.limit = opts.limit
|
|
210
|
+
this.signal = opts.signal
|
|
157
211
|
}
|
|
158
212
|
|
|
159
213
|
get nodeCount() {
|
|
@@ -195,6 +249,7 @@ export class Subgraph {
|
|
|
195
249
|
}
|
|
196
250
|
|
|
197
251
|
private async addNode(id: number) {
|
|
252
|
+
this.signal?.throwIfAborted()
|
|
198
253
|
if (this.limit !== undefined && this.nodeCount >= this.limit) {
|
|
199
254
|
throw new Error(`Subgraph size limit of ${this.limit} nodes exceeded`)
|
|
200
255
|
}
|
|
@@ -394,6 +449,150 @@ export class Subgraph {
|
|
|
394
449
|
return { inserted, removed: toRemove.size }
|
|
395
450
|
}
|
|
396
451
|
|
|
452
|
+
async betweenNodes(start: number, end: number) {
|
|
453
|
+
this.clearPaths()
|
|
454
|
+
const active = [start, flipNode(end)]
|
|
455
|
+
const visited = new Set([nodeId(start), nodeId(end)])
|
|
456
|
+
let inserted = 0
|
|
457
|
+
while (active.length > 0) {
|
|
458
|
+
const curr = active.pop()!
|
|
459
|
+
const id = nodeId(curr)
|
|
460
|
+
if (!this.hasNode(id)) {
|
|
461
|
+
await this.addNode(id)
|
|
462
|
+
inserted += 1
|
|
463
|
+
}
|
|
464
|
+
for (const successor of this.record(curr).successors()) {
|
|
465
|
+
const successorId = nodeId(successor)
|
|
466
|
+
if (!visited.has(successorId)) {
|
|
467
|
+
active.push(successor, flipNode(successor))
|
|
468
|
+
visited.add(successorId)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return inserted
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async extractSnarls(snarls: SnarlOutput) {
|
|
476
|
+
let inserted = 0
|
|
477
|
+
for (const [start, end] of await this.overlappingSnarls(snarls)) {
|
|
478
|
+
inserted += await this.betweenNodes(start, end)
|
|
479
|
+
}
|
|
480
|
+
return inserted
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private async overlappingSnarls(snarls: SnarlOutput) {
|
|
484
|
+
const result: [number, number][] = []
|
|
485
|
+
if (snarls !== 'none') {
|
|
486
|
+
let foundLink = false
|
|
487
|
+
for (const handle of this.sortedHandles()) {
|
|
488
|
+
const record = this.record(handle)
|
|
489
|
+
const next = record.next
|
|
490
|
+
if (next !== undefined) {
|
|
491
|
+
foundLink = true
|
|
492
|
+
if (this.hasHandle(next)) {
|
|
493
|
+
if (edgeIsCanonical(handle, next)) {
|
|
494
|
+
result.push([handle, next])
|
|
495
|
+
}
|
|
496
|
+
} else if (
|
|
497
|
+
snarls === 'overlapping' &&
|
|
498
|
+
this.isSnarlEntryInSubgraph(record)
|
|
499
|
+
) {
|
|
500
|
+
result.push([handle, next])
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (
|
|
505
|
+
!foundLink &&
|
|
506
|
+
snarls === 'overlapping' &&
|
|
507
|
+
(await this.db.hasChainLinks())
|
|
508
|
+
) {
|
|
509
|
+
const covering = await this.findCoveringSnarl()
|
|
510
|
+
if (covering) {
|
|
511
|
+
result.push(covering)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return result
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private isSnarlEntryInSubgraph(record: GbzRecord) {
|
|
519
|
+
const successors = record.successors()
|
|
520
|
+
const first = successors.find(handle => this.hasHandle(handle))
|
|
521
|
+
return first === undefined
|
|
522
|
+
? false
|
|
523
|
+
: successors.length > 1 ||
|
|
524
|
+
this.record(flipNode(first)).successors().length > 1
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private recordReader(onFetch?: () => void) {
|
|
528
|
+
const outside = new Map<number, GbzRecord>()
|
|
529
|
+
return async (handle: number) => {
|
|
530
|
+
const inside = this.records.get(handle)
|
|
531
|
+
if (inside) {
|
|
532
|
+
return inside
|
|
533
|
+
}
|
|
534
|
+
let record = outside.get(handle)
|
|
535
|
+
if (!record) {
|
|
536
|
+
record = await this.db.getRecord(handle)
|
|
537
|
+
onFetch?.()
|
|
538
|
+
if (!record) {
|
|
539
|
+
throw new Error(`Node record ${handle} is missing from the database`)
|
|
540
|
+
}
|
|
541
|
+
outside.set(handle, record)
|
|
542
|
+
}
|
|
543
|
+
return record
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private async findCoveringSnarl() {
|
|
548
|
+
const read = this.recordReader()
|
|
549
|
+
const isSnarlEntry = async (record: GbzRecord) => {
|
|
550
|
+
const successors = record.successors()
|
|
551
|
+
const first = successors[0]
|
|
552
|
+
return first === undefined
|
|
553
|
+
? false
|
|
554
|
+
: successors.length > 1 ||
|
|
555
|
+
(await read(flipNode(first))).successors().length > 1
|
|
556
|
+
}
|
|
557
|
+
const classify = async (handle: number): Promise<HandleType> => {
|
|
558
|
+
const reverse = await read(flipNode(handle))
|
|
559
|
+
return reverse.next !== undefined
|
|
560
|
+
? (await isSnarlEntry(reverse))
|
|
561
|
+
? { kind: 'snarl-exit', snarl: [flipNode(handle), reverse.next] }
|
|
562
|
+
: { kind: 'chain' }
|
|
563
|
+
: (await read(handle)).next !== undefined
|
|
564
|
+
? { kind: 'chain' }
|
|
565
|
+
: { kind: 'regular' }
|
|
566
|
+
}
|
|
567
|
+
const visited = new Set<number>()
|
|
568
|
+
const queue = this.sortedHandles().flatMap(handle =>
|
|
569
|
+
this.record(handle).successors(),
|
|
570
|
+
)
|
|
571
|
+
let result: [number, number] | undefined
|
|
572
|
+
let done = false
|
|
573
|
+
while (!done && queue.length > 0) {
|
|
574
|
+
const handle = queue.shift()!
|
|
575
|
+
const id = nodeId(handle)
|
|
576
|
+
if (!this.hasHandle(handle) && !visited.has(id)) {
|
|
577
|
+
visited.add(id)
|
|
578
|
+
const type = await classify(handle)
|
|
579
|
+
if (type.kind === 'snarl-exit') {
|
|
580
|
+
result = type.snarl
|
|
581
|
+
done = true
|
|
582
|
+
} else if (type.kind === 'chain') {
|
|
583
|
+
done = true
|
|
584
|
+
} else {
|
|
585
|
+
for (const orientation of ['forward', 'reverse'] as const) {
|
|
586
|
+
queue.push(
|
|
587
|
+
...(await read(encodeNode(id, orientation))).successors(),
|
|
588
|
+
)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return result
|
|
594
|
+
}
|
|
595
|
+
|
|
397
596
|
extractPaths(reference: ReferencePath | undefined, output: HaplotypeOutput) {
|
|
398
597
|
this.clearPaths()
|
|
399
598
|
if (output === 'none') {
|
|
@@ -405,72 +604,69 @@ export class Subgraph {
|
|
|
405
604
|
const handles = this.sortedHandles()
|
|
406
605
|
const successors = new Map<
|
|
407
606
|
number,
|
|
408
|
-
{
|
|
607
|
+
{ nodes: Int32Array; offsets: Int32Array; hasPredecessor: Uint8Array }
|
|
409
608
|
>()
|
|
410
609
|
for (const handle of handles) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
)
|
|
610
|
+
const { nodes, offsets } = this.record(handle).gbwt().decompressArrays()
|
|
611
|
+
successors.set(handle, {
|
|
612
|
+
nodes,
|
|
613
|
+
offsets,
|
|
614
|
+
hasPredecessor: new Uint8Array(nodes.length),
|
|
615
|
+
})
|
|
418
616
|
}
|
|
419
617
|
for (const handle of handles) {
|
|
420
|
-
|
|
421
|
-
|
|
618
|
+
const { nodes, offsets } = successors.get(handle)!
|
|
619
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
620
|
+
const entry = successors.get(nodes[i]!)
|
|
422
621
|
if (entry) {
|
|
423
|
-
entry.hasPredecessor =
|
|
622
|
+
entry.hasPredecessor[offsets[i]!] = 1
|
|
424
623
|
}
|
|
425
624
|
}
|
|
426
625
|
}
|
|
427
626
|
let refOffset: number | undefined
|
|
428
627
|
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
|
-
|
|
628
|
+
const entries = successors.get(handle)!
|
|
629
|
+
for (let offset = 0; offset < entries.nodes.length; offset++) {
|
|
630
|
+
if (entries.hasPredecessor[offset] === 0) {
|
|
631
|
+
let currNode: number | undefined = handle
|
|
632
|
+
let currOffset = offset
|
|
633
|
+
let isRef = false
|
|
634
|
+
const path: number[] = []
|
|
635
|
+
const positions: Pos[] = []
|
|
636
|
+
let len = 0
|
|
637
|
+
while (currNode !== undefined) {
|
|
638
|
+
if (
|
|
639
|
+
currNode === refPos?.handle &&
|
|
640
|
+
currOffset === refPos.gbwtOffset
|
|
641
|
+
) {
|
|
642
|
+
this.refId = this.paths.length
|
|
643
|
+
refOffset = path.length
|
|
644
|
+
isRef = true
|
|
645
|
+
}
|
|
646
|
+
path.push(currNode)
|
|
647
|
+
positions.push({ node: currNode, offset: currOffset })
|
|
648
|
+
len += this.record(currNode).sequenceLen
|
|
649
|
+
const step = successors.get(currNode)!
|
|
650
|
+
const nextNode: number = step.nodes[currOffset]!
|
|
651
|
+
const nextOffset: number = step.offsets[currOffset]!
|
|
652
|
+
if (nextNode !== ENDMARKER && successors.has(nextNode)) {
|
|
653
|
+
currNode = nextNode
|
|
654
|
+
currOffset = nextOffset
|
|
655
|
+
} else {
|
|
656
|
+
currNode = undefined
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (isRef || pathIsCanonical(path)) {
|
|
660
|
+
this.paths.push({
|
|
661
|
+
path,
|
|
662
|
+
positions,
|
|
663
|
+
len,
|
|
664
|
+
weight: undefined,
|
|
665
|
+
identity: undefined,
|
|
666
|
+
})
|
|
450
667
|
}
|
|
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
668
|
}
|
|
473
|
-
}
|
|
669
|
+
}
|
|
474
670
|
}
|
|
475
671
|
if (refPos) {
|
|
476
672
|
if (refOffset === undefined || this.refId === undefined) {
|
|
@@ -551,23 +747,9 @@ export class Subgraph {
|
|
|
551
747
|
starts.set(posKey(first), index)
|
|
552
748
|
}
|
|
553
749
|
})
|
|
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
|
-
}
|
|
750
|
+
const recordAt = this.recordReader(() => {
|
|
751
|
+
this.stats.identificationFetches += 1
|
|
752
|
+
})
|
|
571
753
|
const sampleAt = async (pos: Pos) => {
|
|
572
754
|
if (pos.node >= minHandle && pos.node <= maxHandle) {
|
|
573
755
|
return samples.get(posKey(pos))
|
|
@@ -632,6 +814,7 @@ export class Subgraph {
|
|
|
632
814
|
let current: number | undefined = start
|
|
633
815
|
let pos: Pos | undefined
|
|
634
816
|
while (anchor === undefined) {
|
|
817
|
+
this.signal?.throwIfAborted()
|
|
635
818
|
if (current !== undefined) {
|
|
636
819
|
if (visited.has(current)) {
|
|
637
820
|
break
|
|
@@ -926,24 +1109,17 @@ export class Subgraph {
|
|
|
926
1109
|
last -= 1
|
|
927
1110
|
}
|
|
928
1111
|
const identity = info.identity
|
|
929
|
-
const
|
|
1112
|
+
const walkStrand =
|
|
930
1113
|
info.path.some(handle => isReverse(handle)) &&
|
|
931
1114
|
!info.path.some(handle => !isReverse(handle))
|
|
932
1115
|
? '-'
|
|
933
1116
|
: '+'
|
|
934
|
-
|
|
935
|
-
pathHandle: identity?.pathHandle,
|
|
936
|
-
name: identity?.name,
|
|
1117
|
+
const span: AlignmentSpan = {
|
|
937
1118
|
strand: identity
|
|
938
1119
|
? identity.orientation === 'forward'
|
|
939
1120
|
? '+'
|
|
940
1121
|
: '-'
|
|
941
|
-
:
|
|
942
|
-
hapStart: identity
|
|
943
|
-
? identity.name.fragment + identity.hapStart
|
|
944
|
-
: undefined,
|
|
945
|
-
hapEnd: identity ? identity.name.fragment + identity.hapEnd : undefined,
|
|
946
|
-
start: info.positions[0]!,
|
|
1122
|
+
: walkStrand,
|
|
947
1123
|
refStart: reference.start + leading,
|
|
948
1124
|
refEnd: reference.start + refTotal - trailing,
|
|
949
1125
|
cigar: edits
|
|
@@ -952,12 +1128,152 @@ export class Subgraph {
|
|
|
952
1128
|
.join(''),
|
|
953
1129
|
weight: info.weight,
|
|
954
1130
|
path: info.path,
|
|
955
|
-
|
|
1131
|
+
start: info.positions[0]!,
|
|
1132
|
+
}
|
|
1133
|
+
if (identity) {
|
|
1134
|
+
const hapStart = identity.name.fragment + identity.hapStart
|
|
1135
|
+
const hapEnd = identity.name.fragment + identity.hapEnd
|
|
1136
|
+
result.push({
|
|
1137
|
+
...span,
|
|
1138
|
+
resolved: true,
|
|
1139
|
+
name: identity.name,
|
|
1140
|
+
label: formatPathName(
|
|
1141
|
+
{ ...identity.name, fragment: hapStart },
|
|
1142
|
+
hapEnd,
|
|
1143
|
+
),
|
|
1144
|
+
pathHandle: identity.pathHandle,
|
|
1145
|
+
hapStart,
|
|
1146
|
+
hapEnd,
|
|
1147
|
+
})
|
|
1148
|
+
} else {
|
|
1149
|
+
result.push({ ...span, resolved: false })
|
|
1150
|
+
}
|
|
956
1151
|
})
|
|
957
1152
|
return result
|
|
958
1153
|
}
|
|
959
1154
|
|
|
960
|
-
|
|
1155
|
+
private canonicalEdges(id: number) {
|
|
1156
|
+
const edges: [number, number, number][] = []
|
|
1157
|
+
for (const orientation of ['forward', 'reverse'] as const) {
|
|
1158
|
+
const handle = encodeNode(id, orientation)
|
|
1159
|
+
for (const successor of this.record(handle).successors()) {
|
|
1160
|
+
if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
|
|
1161
|
+
edges.push([
|
|
1162
|
+
orientation === 'reverse' ? 1 : 0,
|
|
1163
|
+
nodeId(successor),
|
|
1164
|
+
isReverse(successor) ? 1 : 0,
|
|
1165
|
+
])
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
edges.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2])
|
|
1170
|
+
return edges.filter(
|
|
1171
|
+
(edge, i) =>
|
|
1172
|
+
i === 0 ||
|
|
1173
|
+
edge[0] !== edges[i - 1]![0] ||
|
|
1174
|
+
edge[1] !== edges[i - 1]![1] ||
|
|
1175
|
+
edge[2] !== edges[i - 1]![2],
|
|
1176
|
+
)
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
async stableName() {
|
|
1180
|
+
const encoder = new TextEncoder()
|
|
1181
|
+
const chunks: Uint8Array[] = []
|
|
1182
|
+
for (const handle of this.sortedHandles()) {
|
|
1183
|
+
if (!isReverse(handle)) {
|
|
1184
|
+
const id = nodeId(handle)
|
|
1185
|
+
let text = `S\t${id}\t${this.record(handle).sequence}\n`
|
|
1186
|
+
for (const [fromReverse, toId, toReverse] of this.canonicalEdges(id)) {
|
|
1187
|
+
text += `L\t${id}\t${fromReverse ? '-' : '+'}\t${toId}\t${toReverse ? '-' : '+'}\n`
|
|
1188
|
+
}
|
|
1189
|
+
chunks.push(encoder.encode(text))
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return sha256Hex(chunks)
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
async toGFA(opts: SubgraphOutputOptions = {}) {
|
|
1196
|
+
const cigar = opts.cigar ?? false
|
|
1197
|
+
const lines = [
|
|
1198
|
+
this.refPath ? `H\tVN:Z:1.1\tRS:Z:${this.refPath.sample}` : 'H\tVN:Z:1.1',
|
|
1199
|
+
...gfaHeaderLines(
|
|
1200
|
+
subgraphName(await this.stableName(), await this.db.graphName()),
|
|
1201
|
+
),
|
|
1202
|
+
]
|
|
1203
|
+
const handles = this.sortedHandles()
|
|
1204
|
+
for (const handle of handles) {
|
|
1205
|
+
if (!isReverse(handle)) {
|
|
1206
|
+
lines.push(`S\t${nodeId(handle)}\t${this.record(handle).sequence}`)
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
const sign = (handle: number) => (isReverse(handle) ? '-' : '+')
|
|
1210
|
+
for (const handle of handles) {
|
|
1211
|
+
for (const successor of this.record(handle).successors()) {
|
|
1212
|
+
if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
|
|
1213
|
+
lines.push(
|
|
1214
|
+
`L\t${nodeId(handle)}\t${sign(handle)}\t${nodeId(successor)}\t${sign(successor)}\t0M`,
|
|
1215
|
+
)
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const walk = (
|
|
1220
|
+
info: PathInfo,
|
|
1221
|
+
name: PathName,
|
|
1222
|
+
end: number,
|
|
1223
|
+
cigarString: string | undefined,
|
|
1224
|
+
) => {
|
|
1225
|
+
const steps = info.path
|
|
1226
|
+
.map(handle => `${isReverse(handle) ? '<' : '>'}${nodeId(handle)}`)
|
|
1227
|
+
.join('')
|
|
1228
|
+
const weight = info.weight === undefined ? '' : `\tWT:i:${info.weight}`
|
|
1229
|
+
const cg = cigarString === undefined ? '' : `\tCG:Z:${cigarString}`
|
|
1230
|
+
return `W\t${name.sample}\t${name.haplotype}\t${name.contig}\t${name.fragment}\t${end}\t${steps}${weight}${cg}`
|
|
1231
|
+
}
|
|
1232
|
+
const contig = this.refPath?.contig ?? 'unknown'
|
|
1233
|
+
if (this.refId !== undefined && this.refPath && this.refInterval) {
|
|
1234
|
+
lines.push(
|
|
1235
|
+
walk(
|
|
1236
|
+
this.paths[this.refId]!,
|
|
1237
|
+
{
|
|
1238
|
+
...this.refPath,
|
|
1239
|
+
fragment: this.refPath.fragment + this.refInterval[0],
|
|
1240
|
+
},
|
|
1241
|
+
this.refPath.fragment + this.refInterval[1],
|
|
1242
|
+
undefined,
|
|
1243
|
+
),
|
|
1244
|
+
)
|
|
1245
|
+
}
|
|
1246
|
+
let haplotype = 1
|
|
1247
|
+
this.paths.forEach((info, index) => {
|
|
1248
|
+
if (index !== this.refId) {
|
|
1249
|
+
const resolved = opts.names === 'resolved' ? info.identity : undefined
|
|
1250
|
+
const cigarString = cigar ? this.alignToRef(index) : undefined
|
|
1251
|
+
lines.push(
|
|
1252
|
+
resolved
|
|
1253
|
+
? walk(
|
|
1254
|
+
info,
|
|
1255
|
+
{
|
|
1256
|
+
...resolved.name,
|
|
1257
|
+
fragment: resolved.name.fragment + resolved.hapStart,
|
|
1258
|
+
},
|
|
1259
|
+
resolved.name.fragment + resolved.hapEnd,
|
|
1260
|
+
cigarString,
|
|
1261
|
+
)
|
|
1262
|
+
: walk(
|
|
1263
|
+
info,
|
|
1264
|
+
{ sample: 'unknown', contig, haplotype, fragment: 0 },
|
|
1265
|
+
info.len,
|
|
1266
|
+
cigarString,
|
|
1267
|
+
),
|
|
1268
|
+
)
|
|
1269
|
+
haplotype += 1
|
|
1270
|
+
}
|
|
1271
|
+
})
|
|
1272
|
+
return `${lines.join('\n')}\n`
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
toSubgraphJson(opts: SubgraphOutputOptions = {}): SubgraphJson {
|
|
1276
|
+
const cigar = opts.cigar ?? false
|
|
961
1277
|
const handles = this.sortedHandles()
|
|
962
1278
|
const nodes = handles
|
|
963
1279
|
.filter(handle => !isReverse(handle))
|