@gmod/gbz-base 2.2.0 → 2.3.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 +37 -19
- package/dist/cli.js +34 -1
- package/dist/cli.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.js +3 -0
- package/dist/db.js.map +1 -1
- package/dist/gbwt/node.d.ts +1 -0
- package/dist/gbwt/node.js +3 -2
- package/dist/gbwt/node.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/query.js +16 -5
- package/dist/query.js.map +1 -1
- package/dist/sqlite/btree.d.ts +4 -0
- package/dist/sqlite/btree.js +66 -0
- package/dist/sqlite/btree.js.map +1 -1
- package/dist/sqlite/database.d.ts +1 -0
- package/dist/sqlite/database.js +3 -0
- package/dist/sqlite/database.js.map +1 -1
- package/dist/subgraph.d.ts +45 -6
- package/dist/subgraph.js +407 -120
- package/dist/subgraph.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +43 -1
- package/src/db.ts +4 -0
- package/src/gbwt/node.ts +7 -2
- package/src/index.ts +4 -1
- package/src/query.ts +19 -9
- package/src/sqlite/btree.ts +72 -0
- package/src/sqlite/database.ts +4 -0
- package/src/subgraph.ts +511 -135
package/src/subgraph.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
isReverse,
|
|
12
12
|
nodeId,
|
|
13
13
|
nodeOrientation,
|
|
14
|
+
pathEndsAreCanonical,
|
|
14
15
|
pathIsCanonical,
|
|
15
16
|
} from './gbwt/node.ts'
|
|
16
17
|
import { gfaHeaderLines, sha256Hex, subgraphName } from './graphName.ts'
|
|
@@ -54,7 +55,7 @@ export interface PathIdentity {
|
|
|
54
55
|
|
|
55
56
|
interface PathInfo {
|
|
56
57
|
path: number[]
|
|
57
|
-
|
|
58
|
+
offsets: number[]
|
|
58
59
|
len: number
|
|
59
60
|
weight: number | undefined
|
|
60
61
|
identity: PathIdentity | undefined
|
|
@@ -114,6 +115,24 @@ export interface SubgraphOptions {
|
|
|
114
115
|
signal?: AbortSignal | undefined
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
export class SubgraphLimitError extends Error {
|
|
119
|
+
override name = 'SubgraphLimitError'
|
|
120
|
+
readonly limit: number
|
|
121
|
+
readonly windowBp: number | undefined
|
|
122
|
+
readonly walkedBp: number | undefined
|
|
123
|
+
|
|
124
|
+
constructor(limit: number, walk?: { windowBp: number; walkedBp: number }) {
|
|
125
|
+
super(
|
|
126
|
+
walk === undefined
|
|
127
|
+
? `Subgraph size limit of ${limit} nodes exceeded`
|
|
128
|
+
: `Subgraph size limit of ${limit} nodes exceeded ${walk.walkedBp} bp into a ${walk.windowBp} bp window`,
|
|
129
|
+
)
|
|
130
|
+
this.limit = limit
|
|
131
|
+
this.windowBp = walk?.windowBp
|
|
132
|
+
this.walkedBp = walk?.walkedBp
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
117
136
|
function sideBefore(
|
|
118
137
|
a: [number, number, NodeSide],
|
|
119
138
|
b: [number, number, NodeSide],
|
|
@@ -178,26 +197,198 @@ function posKey(pos: Pos) {
|
|
|
178
197
|
return `${pos.node}:${pos.offset}`
|
|
179
198
|
}
|
|
180
199
|
|
|
200
|
+
interface FragmentAlignment {
|
|
201
|
+
strand: '+' | '-'
|
|
202
|
+
refStart: number
|
|
203
|
+
refEnd: number
|
|
204
|
+
edits: Edit[]
|
|
205
|
+
weight: number | undefined
|
|
206
|
+
path: number[]
|
|
207
|
+
start: Pos
|
|
208
|
+
identity:
|
|
209
|
+
| {
|
|
210
|
+
pathHandle: number
|
|
211
|
+
name: PathName
|
|
212
|
+
hapStart: number
|
|
213
|
+
hapEnd: number
|
|
214
|
+
walkForward: boolean
|
|
215
|
+
}
|
|
216
|
+
| undefined
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function joinable(a: FragmentAlignment, b: FragmentAlignment) {
|
|
220
|
+
const insertion = b.identity!.hapStart - a.identity!.hapEnd
|
|
221
|
+
const deletion =
|
|
222
|
+
a.strand === '+' ? b.refStart - a.refEnd : a.refStart - b.refEnd
|
|
223
|
+
return a.strand === b.strand && insertion >= 0 && deletion >= 0
|
|
224
|
+
? { insertion, deletion }
|
|
225
|
+
: undefined
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function joinPair(
|
|
229
|
+
a: FragmentAlignment,
|
|
230
|
+
b: FragmentAlignment,
|
|
231
|
+
gap: { insertion: number; deletion: number },
|
|
232
|
+
): FragmentAlignment {
|
|
233
|
+
const [left, right] = a.strand === '+' ? [a, b] : [b, a]
|
|
234
|
+
const edits = left.edits.map(([op, len]): Edit => [op, len])
|
|
235
|
+
appendGap(edits, gap.insertion, gap.deletion)
|
|
236
|
+
for (const [op, len] of right.edits) {
|
|
237
|
+
appendEdit(edits, op, len)
|
|
238
|
+
}
|
|
239
|
+
const walkForward = a.identity!.walkForward
|
|
240
|
+
const [first, second] = walkForward ? [a, b] : [b, a]
|
|
241
|
+
return {
|
|
242
|
+
strand: a.strand,
|
|
243
|
+
refStart: left.refStart,
|
|
244
|
+
refEnd: right.refEnd,
|
|
245
|
+
edits,
|
|
246
|
+
weight: undefined,
|
|
247
|
+
path: [...first.path, ...second.path],
|
|
248
|
+
start: first.start,
|
|
249
|
+
identity: {
|
|
250
|
+
pathHandle: a.identity!.pathHandle,
|
|
251
|
+
name: a.identity!.name,
|
|
252
|
+
hapStart: a.identity!.hapStart,
|
|
253
|
+
hapEnd: b.identity!.hapEnd,
|
|
254
|
+
walkForward,
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function joinSiblings(fragments: FragmentAlignment[]) {
|
|
260
|
+
if (fragments.some(f => f.weight !== undefined)) {
|
|
261
|
+
return fragments
|
|
262
|
+
}
|
|
263
|
+
const byPath = new Map<number, number[]>()
|
|
264
|
+
fragments.forEach((fragment, index) => {
|
|
265
|
+
if (fragment.identity) {
|
|
266
|
+
const siblings = byPath.get(fragment.identity.pathHandle)
|
|
267
|
+
if (siblings) {
|
|
268
|
+
siblings.push(index)
|
|
269
|
+
} else {
|
|
270
|
+
byPath.set(fragment.identity.pathHandle, [index])
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
const joined = new Map<number, FragmentAlignment>()
|
|
275
|
+
const consumed = new Set<number>()
|
|
276
|
+
for (const siblings of byPath.values()) {
|
|
277
|
+
siblings.sort(
|
|
278
|
+
(x, y) =>
|
|
279
|
+
fragments[x]!.identity!.hapStart - fragments[y]!.identity!.hapStart,
|
|
280
|
+
)
|
|
281
|
+
let head = siblings[0]!
|
|
282
|
+
let current = fragments[head]!
|
|
283
|
+
for (const index of siblings.slice(1)) {
|
|
284
|
+
const next = fragments[index]!
|
|
285
|
+
const gap = joinable(current, next)
|
|
286
|
+
if (gap) {
|
|
287
|
+
current = joinPair(current, next, gap)
|
|
288
|
+
consumed.add(index)
|
|
289
|
+
} else {
|
|
290
|
+
joined.set(head, current)
|
|
291
|
+
head = index
|
|
292
|
+
current = next
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
joined.set(head, current)
|
|
296
|
+
}
|
|
297
|
+
return fragments.flatMap((fragment, index) =>
|
|
298
|
+
consumed.has(index) ? [] : [joined.get(index) ?? fragment],
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function pathPosition(info: PathInfo, k: number): Pos {
|
|
303
|
+
return { node: info.path[k]!, offset: info.offsets[k]! }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const SCAN_GAP = 4096
|
|
307
|
+
|
|
308
|
+
function handleRuns(sortedHandles: number[]) {
|
|
309
|
+
const runs: [number, number][] = []
|
|
310
|
+
for (const handle of sortedHandles) {
|
|
311
|
+
const last = runs[runs.length - 1]
|
|
312
|
+
if (last && handle - last[1] <= SCAN_GAP) {
|
|
313
|
+
last[1] = handle
|
|
314
|
+
} else {
|
|
315
|
+
runs.push([handle, handle])
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return runs
|
|
319
|
+
}
|
|
320
|
+
|
|
181
321
|
interface Anchor {
|
|
182
322
|
pathHandle: number
|
|
183
323
|
orientation: Orientation
|
|
184
324
|
base: number
|
|
185
325
|
}
|
|
186
326
|
|
|
327
|
+
export type ChainEnd =
|
|
328
|
+
| 'in-fragment sample'
|
|
329
|
+
| 'identified sibling'
|
|
330
|
+
| 'out-of-window sample'
|
|
331
|
+
| 'bound'
|
|
332
|
+
| 'endmarker'
|
|
333
|
+
| 'cycle'
|
|
334
|
+
|
|
335
|
+
export interface ChainRecord {
|
|
336
|
+
fragments: number
|
|
337
|
+
steps: number
|
|
338
|
+
seeks: number
|
|
339
|
+
reentries: number
|
|
340
|
+
twinLandings: number
|
|
341
|
+
end: ChainEnd
|
|
342
|
+
pathHandle: number | undefined
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface IdentificationStats {
|
|
346
|
+
interval: number
|
|
347
|
+
scans: [number, number][]
|
|
348
|
+
windowSamples: number
|
|
349
|
+
fragmentLengths: number[]
|
|
350
|
+
companionSeeks: number
|
|
351
|
+
companionMisses: number
|
|
352
|
+
graphLookups: number
|
|
353
|
+
graphFetches: number
|
|
354
|
+
chains: ChainRecord[]
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
interface SubgraphStats {
|
|
358
|
+
orderedAlignments: number
|
|
359
|
+
lcsAlignments: number
|
|
360
|
+
identificationSteps: number
|
|
361
|
+
identificationFetches: number
|
|
362
|
+
identification: IdentificationStats
|
|
363
|
+
}
|
|
364
|
+
|
|
187
365
|
export class Subgraph {
|
|
188
366
|
private records = new Map<number, GbzRecord>()
|
|
189
367
|
private paths: PathInfo[] = []
|
|
368
|
+
private twinStarts = new Set<string>()
|
|
190
369
|
private refId: number | undefined
|
|
191
370
|
private refPath: PathName | undefined
|
|
192
371
|
private refHandle: number | undefined
|
|
193
372
|
private refInterval: [number, number] | undefined
|
|
194
373
|
private refIndexCache: Map<number, number[]> | undefined
|
|
195
374
|
private refPrefixCache: number[] | undefined
|
|
196
|
-
|
|
375
|
+
private walkedBp: number | undefined
|
|
376
|
+
readonly stats: SubgraphStats = {
|
|
197
377
|
orderedAlignments: 0,
|
|
198
378
|
lcsAlignments: 0,
|
|
199
379
|
identificationSteps: 0,
|
|
200
380
|
identificationFetches: 0,
|
|
381
|
+
identification: {
|
|
382
|
+
interval: 0,
|
|
383
|
+
scans: [],
|
|
384
|
+
windowSamples: 0,
|
|
385
|
+
fragmentLengths: [],
|
|
386
|
+
companionSeeks: 0,
|
|
387
|
+
companionMisses: 0,
|
|
388
|
+
graphLookups: 0,
|
|
389
|
+
graphFetches: 0,
|
|
390
|
+
chains: [],
|
|
391
|
+
},
|
|
201
392
|
}
|
|
202
393
|
|
|
203
394
|
private db: GBZBase
|
|
@@ -251,7 +442,7 @@ export class Subgraph {
|
|
|
251
442
|
private async addNode(id: number) {
|
|
252
443
|
this.signal?.throwIfAborted()
|
|
253
444
|
if (this.limit !== undefined && this.nodeCount >= this.limit) {
|
|
254
|
-
throw new
|
|
445
|
+
throw new SubgraphLimitError(this.limit)
|
|
255
446
|
}
|
|
256
447
|
const forward = await this.db.getRecord(encodeNode(id, 'forward'))
|
|
257
448
|
const reverse = await this.db.getRecord(encodeNode(id, 'reverse'))
|
|
@@ -270,6 +461,7 @@ export class Subgraph {
|
|
|
270
461
|
|
|
271
462
|
private clearPaths() {
|
|
272
463
|
this.paths = []
|
|
464
|
+
this.twinStarts.clear()
|
|
273
465
|
this.refId = undefined
|
|
274
466
|
this.refPath = undefined
|
|
275
467
|
this.refHandle = undefined
|
|
@@ -350,10 +542,44 @@ export class Subgraph {
|
|
|
350
542
|
return this.insertContext(active, context)
|
|
351
543
|
}
|
|
352
544
|
|
|
545
|
+
async prefetchReferenceWalk(reference: ReferencePath, len: number) {
|
|
546
|
+
const last = await this.db.indexedPosition(
|
|
547
|
+
reference.handle,
|
|
548
|
+
reference.position.seqOffset + len,
|
|
549
|
+
)
|
|
550
|
+
if (last) {
|
|
551
|
+
const a = reference.position.handle
|
|
552
|
+
const b = last.pos.node
|
|
553
|
+
await this.db.prefetchRecords(Math.min(a, b), Math.max(a, b) + 1)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
353
557
|
async aroundInterval(start: PathPosition, len: number, context: number) {
|
|
354
558
|
if (len === 0) {
|
|
355
559
|
throw new Error('Interval length must be greater than 0')
|
|
356
560
|
}
|
|
561
|
+
this.walkedBp = 0
|
|
562
|
+
try {
|
|
563
|
+
return await this.walkInterval(start, len, context)
|
|
564
|
+
} catch (error) {
|
|
565
|
+
throw error instanceof SubgraphLimitError && error.windowBp === undefined
|
|
566
|
+
? new SubgraphLimitError(error.limit, {
|
|
567
|
+
windowBp: len,
|
|
568
|
+
walkedBp: this.walkedBp,
|
|
569
|
+
})
|
|
570
|
+
: error
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
get referenceWalkedBp() {
|
|
575
|
+
return this.walkedBp
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private async walkInterval(
|
|
579
|
+
start: PathPosition,
|
|
580
|
+
len: number,
|
|
581
|
+
context: number,
|
|
582
|
+
) {
|
|
357
583
|
let pos: Pos = { node: start.handle, offset: start.gbwtOffset }
|
|
358
584
|
let offset = start.nodeOffset
|
|
359
585
|
let remaining = len
|
|
@@ -361,6 +587,7 @@ export class Subgraph {
|
|
|
361
587
|
for (;;) {
|
|
362
588
|
const id = nodeId(pos.node)
|
|
363
589
|
const orientation = nodeOrientation(pos.node)
|
|
590
|
+
this.walkedBp = len - remaining
|
|
364
591
|
await this.ensureNode(id)
|
|
365
592
|
const record = this.record(pos.node)
|
|
366
593
|
if (offset >= record.sequenceLen) {
|
|
@@ -389,6 +616,7 @@ export class Subgraph {
|
|
|
389
616
|
offset = 0
|
|
390
617
|
remaining -= distanceToNext
|
|
391
618
|
}
|
|
619
|
+
this.walkedBp = len
|
|
392
620
|
return this.insertContext(active, context)
|
|
393
621
|
}
|
|
394
622
|
|
|
@@ -602,69 +830,133 @@ export class Subgraph {
|
|
|
602
830
|
this.refPath = reference?.name
|
|
603
831
|
this.refHandle = reference?.handle
|
|
604
832
|
const handles = this.sortedHandles()
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
833
|
+
const count = handles.length
|
|
834
|
+
const indexOf = new Map<number, number>()
|
|
835
|
+
handles.forEach((handle, i) => indexOf.set(handle, i))
|
|
836
|
+
const nextIndex: Int32Array[] = []
|
|
837
|
+
const nextOffset: Int32Array[] = []
|
|
838
|
+
const hasPredecessor: Uint8Array[] = []
|
|
839
|
+
const seqLen = new Int32Array(count)
|
|
840
|
+
for (let i = 0; i < count; i++) {
|
|
841
|
+
const record = this.record(handles[i]!)
|
|
842
|
+
const { nodes, offsets } = record.gbwt().decompressArrays()
|
|
843
|
+
seqLen[i] = record.sequenceLen
|
|
844
|
+
nextIndex.push(nodes)
|
|
845
|
+
nextOffset.push(offsets)
|
|
846
|
+
hasPredecessor.push(new Uint8Array(nodes.length))
|
|
847
|
+
}
|
|
848
|
+
for (let i = 0; i < count; i++) {
|
|
849
|
+
const nodes = nextIndex[i]!
|
|
850
|
+
const offsets = nextOffset[i]!
|
|
851
|
+
for (let k = 0; k < nodes.length; k++) {
|
|
852
|
+
const j = indexOf.get(nodes[k]!)
|
|
853
|
+
if (j === undefined) {
|
|
854
|
+
nodes[k] = -1
|
|
855
|
+
} else {
|
|
856
|
+
nodes[k] = j
|
|
857
|
+
hasPredecessor[j]![offsets[k]!] = 1
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const refIndex =
|
|
862
|
+
refPos === undefined ? undefined : indexOf.get(refPos.handle)
|
|
863
|
+
const refGbwtOffset = refPos?.gbwtOffset
|
|
864
|
+
let refOffset: number | undefined
|
|
865
|
+
const walk = (i: number, offset: number, path: number[] | undefined) => {
|
|
866
|
+
const offsets: number[] = []
|
|
867
|
+
let steps = 0
|
|
868
|
+
let len = 0
|
|
869
|
+
let refAt = -1
|
|
870
|
+
let cur = i
|
|
871
|
+
let off = offset
|
|
872
|
+
for (;;) {
|
|
873
|
+
if (cur === refIndex && off === refGbwtOffset) {
|
|
874
|
+
refAt = steps
|
|
875
|
+
}
|
|
876
|
+
if (path) {
|
|
877
|
+
path.push(handles[cur]!)
|
|
878
|
+
offsets.push(off)
|
|
879
|
+
}
|
|
880
|
+
steps += 1
|
|
881
|
+
len += seqLen[cur]!
|
|
882
|
+
const next = nextIndex[cur]![off]!
|
|
883
|
+
if (next < 0) {
|
|
884
|
+
break
|
|
885
|
+
}
|
|
886
|
+
off = nextOffset[cur]![off]!
|
|
887
|
+
cur = next
|
|
888
|
+
}
|
|
889
|
+
return { offsets, len, refAt, last: cur }
|
|
890
|
+
}
|
|
891
|
+
const keep = (
|
|
892
|
+
path: number[],
|
|
893
|
+
offsets: number[],
|
|
894
|
+
len: number,
|
|
895
|
+
refAt: number,
|
|
896
|
+
) => {
|
|
897
|
+
if (refAt >= 0) {
|
|
898
|
+
this.refId = this.paths.length
|
|
899
|
+
refOffset = refAt
|
|
900
|
+
}
|
|
901
|
+
this.paths.push({
|
|
902
|
+
path,
|
|
613
903
|
offsets,
|
|
614
|
-
|
|
904
|
+
len,
|
|
905
|
+
weight: undefined,
|
|
906
|
+
identity: undefined,
|
|
615
907
|
})
|
|
616
908
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
909
|
+
const twinsExpected = new Int32Array(count)
|
|
910
|
+
const refMayStartReversed =
|
|
911
|
+
refIndex !== undefined && isReverse(handles[refIndex]!)
|
|
912
|
+
for (let i = 0; i < count; i++) {
|
|
913
|
+
const first = handles[i]!
|
|
914
|
+
if (!isReverse(first)) {
|
|
915
|
+
const starts = hasPredecessor[i]!
|
|
916
|
+
for (let offset = 0; offset < starts.length; offset++) {
|
|
917
|
+
if (starts[offset] === 0) {
|
|
918
|
+
const path: number[] = []
|
|
919
|
+
const { offsets, len, refAt, last } = walk(i, offset, path)
|
|
920
|
+
const lastHandle = handles[last]!
|
|
921
|
+
if (refAt >= 0 || pathEndsAreCanonical(first, lastHandle)) {
|
|
922
|
+
keep(path, offsets, len, refAt)
|
|
923
|
+
if (!isReverse(lastHandle)) {
|
|
924
|
+
const twinRecord = indexOf.get(flipNode(lastHandle))!
|
|
925
|
+
twinsExpected[twinRecord] = twinsExpected[twinRecord]! + 1
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
this.twinStarts.add(posKey({ node: first, offset }))
|
|
929
|
+
}
|
|
930
|
+
}
|
|
623
931
|
}
|
|
624
932
|
}
|
|
625
933
|
}
|
|
626
|
-
let
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
934
|
+
for (let i = 0; i < count; i++) {
|
|
935
|
+
const first = handles[i]!
|
|
936
|
+
if (isReverse(first)) {
|
|
937
|
+
const starts = hasPredecessor[i]!
|
|
938
|
+
let startCount = 0
|
|
939
|
+
for (const flag of starts) {
|
|
940
|
+
if (flag === 0) {
|
|
941
|
+
startCount += 1
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const allTwins = !refMayStartReversed && startCount === twinsExpected[i]
|
|
945
|
+
for (let offset = 0; offset < starts.length; offset++) {
|
|
946
|
+
if (starts[offset] === 0) {
|
|
947
|
+
const bare = allTwins ? undefined : walk(i, offset, undefined)
|
|
638
948
|
if (
|
|
639
|
-
|
|
640
|
-
|
|
949
|
+
bare &&
|
|
950
|
+
(bare.refAt >= 0 ||
|
|
951
|
+
pathEndsAreCanonical(first, handles[bare.last]))
|
|
641
952
|
) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
|
953
|
+
const path: number[] = []
|
|
954
|
+
const { offsets, len, refAt } = walk(i, offset, path)
|
|
955
|
+
keep(path, offsets, len, refAt)
|
|
655
956
|
} else {
|
|
656
|
-
|
|
957
|
+
this.twinStarts.add(posKey({ node: first, offset }))
|
|
657
958
|
}
|
|
658
959
|
}
|
|
659
|
-
if (isRef || pathIsCanonical(path)) {
|
|
660
|
-
this.paths.push({
|
|
661
|
-
path,
|
|
662
|
-
positions,
|
|
663
|
-
len,
|
|
664
|
-
weight: undefined,
|
|
665
|
-
identity: undefined,
|
|
666
|
-
})
|
|
667
|
-
}
|
|
668
960
|
}
|
|
669
961
|
}
|
|
670
962
|
}
|
|
@@ -727,35 +1019,65 @@ export class Subgraph {
|
|
|
727
1019
|
)
|
|
728
1020
|
}
|
|
729
1021
|
const interval = (await this.db.haplotypeSampleInterval()) ?? 4096
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
const maxHandle = handles[handles.length - 1]
|
|
733
|
-
if (minHandle === undefined || maxHandle === undefined) {
|
|
1022
|
+
const runs = handleRuns(this.sortedHandles())
|
|
1023
|
+
if (runs.length === 0) {
|
|
734
1024
|
return
|
|
735
1025
|
}
|
|
736
1026
|
const samples = new Map<string, HaplotypeSample>()
|
|
737
|
-
for (const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1027
|
+
for (const [first, last] of runs) {
|
|
1028
|
+
for (const sample of await this.db.haplotypeSamplesInRange(first, last)) {
|
|
1029
|
+
samples.set(posKey(sample), sample)
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
const scanned = (handle: number) => {
|
|
1033
|
+
let lo = 0
|
|
1034
|
+
let hi = runs.length - 1
|
|
1035
|
+
while (lo < hi) {
|
|
1036
|
+
const mid = (lo + hi + 1) >> 1
|
|
1037
|
+
if (runs[mid]![0] <= handle) {
|
|
1038
|
+
lo = mid
|
|
1039
|
+
} else {
|
|
1040
|
+
hi = mid - 1
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const run = runs[lo]!
|
|
1044
|
+
return run[0] <= handle && handle <= run[1]
|
|
742
1045
|
}
|
|
743
1046
|
const starts = new Map<string, number>()
|
|
744
1047
|
this.paths.forEach((info, index) => {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1048
|
+
if (info.path.length > 0 && index !== this.refId) {
|
|
1049
|
+
starts.set(posKey(pathPosition(info, 0)), index)
|
|
1050
|
+
}
|
|
1051
|
+
})
|
|
1052
|
+
const identification = this.stats.identification
|
|
1053
|
+
identification.interval = interval
|
|
1054
|
+
identification.scans = runs
|
|
1055
|
+
identification.windowSamples = samples.size
|
|
1056
|
+
this.paths.forEach((info, index) => {
|
|
1057
|
+
if (index !== this.refId) {
|
|
1058
|
+
identification.fragmentLengths.push(info.len)
|
|
748
1059
|
}
|
|
749
1060
|
})
|
|
750
|
-
const
|
|
1061
|
+
const readRecord = this.recordReader(() => {
|
|
751
1062
|
this.stats.identificationFetches += 1
|
|
1063
|
+
identification.graphFetches += 1
|
|
752
1064
|
})
|
|
753
|
-
const
|
|
754
|
-
|
|
1065
|
+
const recordAt = (handle: number) => {
|
|
1066
|
+
identification.graphLookups += 1
|
|
1067
|
+
return readRecord(handle)
|
|
1068
|
+
}
|
|
1069
|
+
const sampleAt = async (pos: Pos, chain: ChainRecord) => {
|
|
1070
|
+
if (scanned(pos.node)) {
|
|
755
1071
|
return samples.get(posKey(pos))
|
|
756
1072
|
}
|
|
757
1073
|
this.stats.identificationFetches += 1
|
|
758
|
-
|
|
1074
|
+
identification.companionSeeks += 1
|
|
1075
|
+
chain.seeks += 1
|
|
1076
|
+
const sample = await this.db.haplotypeSampleAt(pos.node, pos.offset)
|
|
1077
|
+
if (!sample) {
|
|
1078
|
+
identification.companionMisses += 1
|
|
1079
|
+
}
|
|
1080
|
+
return sample
|
|
759
1081
|
}
|
|
760
1082
|
const names = new Map<number, PathName>()
|
|
761
1083
|
const nameOf = async (pathHandle: number) => {
|
|
@@ -809,6 +1131,16 @@ export class Subgraph {
|
|
|
809
1131
|
}
|
|
810
1132
|
const chain: { index: number; startBp: number }[] = []
|
|
811
1133
|
const visited = new Set<number>()
|
|
1134
|
+
const record: ChainRecord = {
|
|
1135
|
+
fragments: 0,
|
|
1136
|
+
steps: 0,
|
|
1137
|
+
seeks: 0,
|
|
1138
|
+
reentries: 0,
|
|
1139
|
+
twinLandings: 0,
|
|
1140
|
+
end: 'endmarker',
|
|
1141
|
+
pathHandle: undefined,
|
|
1142
|
+
}
|
|
1143
|
+
identification.chains.push(record)
|
|
812
1144
|
let anchor: Anchor | undefined
|
|
813
1145
|
let counter = 0
|
|
814
1146
|
let current: number | undefined = start
|
|
@@ -817,13 +1149,16 @@ export class Subgraph {
|
|
|
817
1149
|
this.signal?.throwIfAborted()
|
|
818
1150
|
if (current !== undefined) {
|
|
819
1151
|
if (visited.has(current)) {
|
|
1152
|
+
record.end = 'cycle'
|
|
820
1153
|
break
|
|
821
1154
|
}
|
|
822
1155
|
visited.add(current)
|
|
823
1156
|
const info = this.paths[current]!
|
|
824
1157
|
chain.push({ index: current, startBp: counter })
|
|
1158
|
+
record.fragments += 1
|
|
825
1159
|
let bp = counter
|
|
826
|
-
for (
|
|
1160
|
+
for (let k = 0; k < info.path.length; k++) {
|
|
1161
|
+
const position = pathPosition(info, k)
|
|
827
1162
|
const sample = samples.get(posKey(position))
|
|
828
1163
|
const nodeLen = this.record(position.node).sequenceLen
|
|
829
1164
|
if (sample) {
|
|
@@ -834,42 +1169,56 @@ export class Subgraph {
|
|
|
834
1169
|
}
|
|
835
1170
|
counter += info.len
|
|
836
1171
|
if (anchor) {
|
|
1172
|
+
record.end = 'in-fragment sample'
|
|
837
1173
|
break
|
|
838
1174
|
}
|
|
839
|
-
const last = info
|
|
1175
|
+
const last = pathPosition(info, info.path.length - 1)
|
|
840
1176
|
pos = this.record(last.node).gbwt().lf(last.offset)
|
|
841
1177
|
current = undefined
|
|
842
1178
|
}
|
|
843
1179
|
if (pos === undefined || pos.node === ENDMARKER) {
|
|
1180
|
+
record.end = 'endmarker'
|
|
844
1181
|
break
|
|
845
1182
|
}
|
|
846
|
-
const
|
|
1183
|
+
const key = posKey(pos)
|
|
1184
|
+
const known = starts.get(key)
|
|
847
1185
|
if (known !== undefined) {
|
|
848
1186
|
const identity = this.paths[known]!.identity
|
|
849
1187
|
if (identity) {
|
|
850
1188
|
anchor = anchorFromIdentity(identity, counter)
|
|
1189
|
+
record.end = 'identified sibling'
|
|
851
1190
|
break
|
|
852
1191
|
}
|
|
853
1192
|
current = known
|
|
854
1193
|
continue
|
|
855
1194
|
}
|
|
856
|
-
|
|
857
|
-
|
|
1195
|
+
if (this.records.has(pos.node)) {
|
|
1196
|
+
record.reentries += 1
|
|
1197
|
+
}
|
|
1198
|
+
if (this.twinStarts.has(key)) {
|
|
1199
|
+
record.twinLandings += 1
|
|
1200
|
+
}
|
|
1201
|
+
const sample = await sampleAt(pos, record)
|
|
1202
|
+
const node = await recordAt(pos.node)
|
|
858
1203
|
if (sample) {
|
|
859
|
-
anchor = anchorFromSample(sample, counter,
|
|
1204
|
+
anchor = anchorFromSample(sample, counter, node.sequenceLen)
|
|
1205
|
+
record.end = 'out-of-window sample'
|
|
860
1206
|
break
|
|
861
1207
|
}
|
|
862
1208
|
this.stats.identificationSteps += 1
|
|
1209
|
+
record.steps += 1
|
|
863
1210
|
if (
|
|
864
1211
|
counter - (chain[chain.length - 1] as { startBp: number }).startBp >
|
|
865
|
-
4 * interval + 4 *
|
|
1212
|
+
4 * interval + 4 * node.sequenceLen
|
|
866
1213
|
) {
|
|
1214
|
+
record.end = 'bound'
|
|
867
1215
|
break
|
|
868
1216
|
}
|
|
869
|
-
counter +=
|
|
870
|
-
pos =
|
|
1217
|
+
counter += node.sequenceLen
|
|
1218
|
+
pos = node.gbwt().lf(pos.offset)
|
|
871
1219
|
}
|
|
872
1220
|
if (anchor) {
|
|
1221
|
+
record.pathHandle = anchor.pathHandle
|
|
873
1222
|
const name = await nameOf(anchor.pathHandle)
|
|
874
1223
|
for (const { index, startBp } of chain) {
|
|
875
1224
|
const info = this.paths[index]!
|
|
@@ -1020,28 +1369,7 @@ export class Subgraph {
|
|
|
1020
1369
|
suffix = refLen - prefix
|
|
1021
1370
|
}
|
|
1022
1371
|
appendEdit(edits, 'M', prefix)
|
|
1023
|
-
|
|
1024
|
-
const refMiddle = refLen - prefix - suffix
|
|
1025
|
-
if (pathMiddle === 0) {
|
|
1026
|
-
appendEdit(edits, 'D', refMiddle)
|
|
1027
|
-
} else if (refMiddle === 0) {
|
|
1028
|
-
appendEdit(edits, 'I', pathMiddle)
|
|
1029
|
-
} else {
|
|
1030
|
-
const mismatch = Math.min(pathMiddle, refMiddle)
|
|
1031
|
-
const mismatchIndel =
|
|
1032
|
-
4 * mismatch +
|
|
1033
|
-
gapPenalty(pathMiddle - mismatch) +
|
|
1034
|
-
gapPenalty(refMiddle - mismatch)
|
|
1035
|
-
const insertionDeletion = gapPenalty(pathMiddle) + gapPenalty(refMiddle)
|
|
1036
|
-
if (mismatchIndel <= insertionDeletion) {
|
|
1037
|
-
appendEdit(edits, 'M', mismatch)
|
|
1038
|
-
appendEdit(edits, 'I', pathMiddle - mismatch)
|
|
1039
|
-
appendEdit(edits, 'D', refMiddle - mismatch)
|
|
1040
|
-
} else {
|
|
1041
|
-
appendEdit(edits, 'I', pathMiddle)
|
|
1042
|
-
appendEdit(edits, 'D', refMiddle)
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1372
|
+
appendGap(edits, pathLen - prefix - suffix, refLen - prefix - suffix)
|
|
1045
1373
|
appendEdit(edits, 'M', suffix)
|
|
1046
1374
|
}
|
|
1047
1375
|
|
|
@@ -1067,22 +1395,37 @@ export class Subgraph {
|
|
|
1067
1395
|
ordered ??
|
|
1068
1396
|
weightedLcs(path, ref, handle => this.record(handle).sequenceLen)[0]
|
|
1069
1397
|
const edits: Edit[] = []
|
|
1398
|
+
const refPrefix = this.refPrefix(ref)
|
|
1399
|
+
const alignGap = (
|
|
1400
|
+
pathFrom: number,
|
|
1401
|
+
pathTo: number,
|
|
1402
|
+
refFrom: number,
|
|
1403
|
+
refTo: number,
|
|
1404
|
+
) => {
|
|
1405
|
+
if (pathFrom === pathTo) {
|
|
1406
|
+
appendEdit(edits, 'D', refPrefix[refTo]! - refPrefix[refFrom]!)
|
|
1407
|
+
} else if (refFrom === refTo) {
|
|
1408
|
+
appendEdit(edits, 'I', this.pathLen(path.slice(pathFrom, pathTo)))
|
|
1409
|
+
} else {
|
|
1410
|
+
this.align(
|
|
1411
|
+
path.slice(pathFrom, pathTo),
|
|
1412
|
+
ref.slice(refFrom, refTo),
|
|
1413
|
+
edits,
|
|
1414
|
+
)
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1070
1417
|
let matched = 0
|
|
1071
1418
|
let pathOffset = 0
|
|
1072
1419
|
let refOffset = 0
|
|
1073
1420
|
for (const [nextPath, nextRef] of lcs) {
|
|
1074
|
-
|
|
1075
|
-
path.slice(pathOffset, nextPath),
|
|
1076
|
-
ref.slice(refOffset, nextRef),
|
|
1077
|
-
edits,
|
|
1078
|
-
)
|
|
1421
|
+
alignGap(pathOffset, nextPath, refOffset, nextRef)
|
|
1079
1422
|
const nodeLen = this.record(path[nextPath]!).sequenceLen
|
|
1080
1423
|
appendEdit(edits, 'M', nodeLen)
|
|
1081
1424
|
matched += nodeLen
|
|
1082
1425
|
pathOffset = nextPath + 1
|
|
1083
1426
|
refOffset = nextRef + 1
|
|
1084
1427
|
}
|
|
1085
|
-
|
|
1428
|
+
alignGap(pathOffset, path.length, refOffset, ref.length)
|
|
1086
1429
|
return { edits, matched }
|
|
1087
1430
|
}
|
|
1088
1431
|
|
|
@@ -1132,7 +1475,7 @@ export class Subgraph {
|
|
|
1132
1475
|
}
|
|
1133
1476
|
const ref = this.paths[this.refId]!.path
|
|
1134
1477
|
const refTotal = this.refPrefix(ref)[ref.length]!
|
|
1135
|
-
const
|
|
1478
|
+
const fragments: FragmentAlignment[] = []
|
|
1136
1479
|
this.paths.forEach((info, index) => {
|
|
1137
1480
|
if (index === this.refId) {
|
|
1138
1481
|
return
|
|
@@ -1154,38 +1497,44 @@ export class Subgraph {
|
|
|
1154
1497
|
const alongReference = identity
|
|
1155
1498
|
? (identity.orientation === 'forward') !== flipped
|
|
1156
1499
|
: !flipped
|
|
1157
|
-
|
|
1500
|
+
fragments.push({
|
|
1158
1501
|
strand: alongReference ? '+' : '-',
|
|
1159
1502
|
refStart: reference.start + leading,
|
|
1160
1503
|
refEnd: reference.start + refTotal - trailing,
|
|
1161
|
-
|
|
1162
|
-
.slice(first, last)
|
|
1163
|
-
.map(([op, len]) => `${len}${op}`)
|
|
1164
|
-
.join(''),
|
|
1504
|
+
edits: edits.slice(first, last),
|
|
1165
1505
|
weight: info.weight,
|
|
1166
1506
|
path: info.path,
|
|
1167
|
-
start: info
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1507
|
+
start: pathPosition(info, 0),
|
|
1508
|
+
identity:
|
|
1509
|
+
identity === undefined
|
|
1510
|
+
? undefined
|
|
1511
|
+
: {
|
|
1512
|
+
pathHandle: identity.pathHandle,
|
|
1513
|
+
name: identity.name,
|
|
1514
|
+
hapStart: identity.name.fragment + identity.hapStart,
|
|
1515
|
+
hapEnd: identity.name.fragment + identity.hapEnd,
|
|
1516
|
+
walkForward: identity.orientation === 'forward',
|
|
1517
|
+
},
|
|
1518
|
+
})
|
|
1519
|
+
})
|
|
1520
|
+
return joinSiblings(fragments).map(fragment => {
|
|
1521
|
+
const { edits, identity, ...rest } = fragment
|
|
1522
|
+
const span: AlignmentSpan = { ...rest, cigar: cigarOf(edits) }
|
|
1523
|
+
return identity
|
|
1524
|
+
? {
|
|
1525
|
+
...span,
|
|
1526
|
+
resolved: true,
|
|
1527
|
+
name: identity.name,
|
|
1528
|
+
label: formatPathName(
|
|
1529
|
+
{ ...identity.name, fragment: identity.hapStart },
|
|
1530
|
+
identity.hapEnd,
|
|
1531
|
+
),
|
|
1532
|
+
pathHandle: identity.pathHandle,
|
|
1533
|
+
hapStart: identity.hapStart,
|
|
1534
|
+
hapEnd: identity.hapEnd,
|
|
1535
|
+
}
|
|
1536
|
+
: { ...span, resolved: false }
|
|
1187
1537
|
})
|
|
1188
|
-
return result
|
|
1189
1538
|
}
|
|
1190
1539
|
|
|
1191
1540
|
private canonicalEdges(id: number) {
|
|
@@ -1416,3 +1765,30 @@ function appendEdit(edits: Edit[], op: EditOp, len: number) {
|
|
|
1416
1765
|
function gapPenalty(len: number) {
|
|
1417
1766
|
return len === 0 ? 0 : 6 + (len - 1)
|
|
1418
1767
|
}
|
|
1768
|
+
|
|
1769
|
+
function appendGap(edits: Edit[], pathMiddle: number, refMiddle: number) {
|
|
1770
|
+
if (pathMiddle === 0) {
|
|
1771
|
+
appendEdit(edits, 'D', refMiddle)
|
|
1772
|
+
} else if (refMiddle === 0) {
|
|
1773
|
+
appendEdit(edits, 'I', pathMiddle)
|
|
1774
|
+
} else {
|
|
1775
|
+
const mismatch = Math.min(pathMiddle, refMiddle)
|
|
1776
|
+
const mismatchIndel =
|
|
1777
|
+
4 * mismatch +
|
|
1778
|
+
gapPenalty(pathMiddle - mismatch) +
|
|
1779
|
+
gapPenalty(refMiddle - mismatch)
|
|
1780
|
+
const insertionDeletion = gapPenalty(pathMiddle) + gapPenalty(refMiddle)
|
|
1781
|
+
if (mismatchIndel <= insertionDeletion) {
|
|
1782
|
+
appendEdit(edits, 'M', mismatch)
|
|
1783
|
+
appendEdit(edits, 'I', pathMiddle - mismatch)
|
|
1784
|
+
appendEdit(edits, 'D', refMiddle - mismatch)
|
|
1785
|
+
} else {
|
|
1786
|
+
appendEdit(edits, 'I', pathMiddle)
|
|
1787
|
+
appendEdit(edits, 'D', refMiddle)
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function cigarOf(edits: Edit[]) {
|
|
1793
|
+
return edits.map(([op, len]) => `${len}${op}`).join('')
|
|
1794
|
+
}
|