@gmod/gbz-base 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -19
- package/dist/cli.js +61 -3
- 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 +46 -6
- package/dist/subgraph.js +448 -129
- package/dist/subgraph.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +78 -3
- 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 +566 -138
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
|
}
|
|
@@ -720,6 +1012,33 @@ export class Subgraph {
|
|
|
720
1012
|
this.refId = refId
|
|
721
1013
|
}
|
|
722
1014
|
|
|
1015
|
+
// Narrows an identified subgraph to the reference walk and the named walks
|
|
1016
|
+
// `wanted` accepts, and drops every node only the discarded walks visited,
|
|
1017
|
+
// so a cut for a chosen set draws that set's private sequence and nothing
|
|
1018
|
+
// else's. Unresolved walks are discarded with the rest.
|
|
1019
|
+
keepHaplotypes(wanted: (name: PathName) => boolean) {
|
|
1020
|
+
const refInfo =
|
|
1021
|
+
this.refId === undefined ? undefined : this.paths[this.refId]
|
|
1022
|
+
const kept = this.paths.filter(
|
|
1023
|
+
(info, index) =>
|
|
1024
|
+
index === this.refId ||
|
|
1025
|
+
(info.identity !== undefined && wanted(info.identity.name)),
|
|
1026
|
+
)
|
|
1027
|
+
const visited = new Set<number>()
|
|
1028
|
+
for (const info of kept) {
|
|
1029
|
+
for (const handle of info.path) {
|
|
1030
|
+
visited.add(nodeId(handle))
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
for (const handle of [...this.records.keys()]) {
|
|
1034
|
+
if (!visited.has(nodeId(handle))) {
|
|
1035
|
+
this.records.delete(handle)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
this.paths = kept
|
|
1039
|
+
this.refId = refInfo === undefined ? undefined : kept.indexOf(refInfo)
|
|
1040
|
+
}
|
|
1041
|
+
|
|
723
1042
|
async identifyPaths() {
|
|
724
1043
|
if (!this.db.hasHaplotypeIndex) {
|
|
725
1044
|
throw new Error(
|
|
@@ -727,35 +1046,65 @@ export class Subgraph {
|
|
|
727
1046
|
)
|
|
728
1047
|
}
|
|
729
1048
|
const interval = (await this.db.haplotypeSampleInterval()) ?? 4096
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
const maxHandle = handles[handles.length - 1]
|
|
733
|
-
if (minHandle === undefined || maxHandle === undefined) {
|
|
1049
|
+
const runs = handleRuns(this.sortedHandles())
|
|
1050
|
+
if (runs.length === 0) {
|
|
734
1051
|
return
|
|
735
1052
|
}
|
|
736
1053
|
const samples = new Map<string, HaplotypeSample>()
|
|
737
|
-
for (const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1054
|
+
for (const [first, last] of runs) {
|
|
1055
|
+
for (const sample of await this.db.haplotypeSamplesInRange(first, last)) {
|
|
1056
|
+
samples.set(posKey(sample), sample)
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const scanned = (handle: number) => {
|
|
1060
|
+
let lo = 0
|
|
1061
|
+
let hi = runs.length - 1
|
|
1062
|
+
while (lo < hi) {
|
|
1063
|
+
const mid = (lo + hi + 1) >> 1
|
|
1064
|
+
if (runs[mid]![0] <= handle) {
|
|
1065
|
+
lo = mid
|
|
1066
|
+
} else {
|
|
1067
|
+
hi = mid - 1
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const run = runs[lo]!
|
|
1071
|
+
return run[0] <= handle && handle <= run[1]
|
|
742
1072
|
}
|
|
743
1073
|
const starts = new Map<string, number>()
|
|
744
1074
|
this.paths.forEach((info, index) => {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
starts.set(posKey(first), index)
|
|
1075
|
+
if (info.path.length > 0 && index !== this.refId) {
|
|
1076
|
+
starts.set(posKey(pathPosition(info, 0)), index)
|
|
748
1077
|
}
|
|
749
1078
|
})
|
|
750
|
-
const
|
|
1079
|
+
const identification = this.stats.identification
|
|
1080
|
+
identification.interval = interval
|
|
1081
|
+
identification.scans = runs
|
|
1082
|
+
identification.windowSamples = samples.size
|
|
1083
|
+
this.paths.forEach((info, index) => {
|
|
1084
|
+
if (index !== this.refId) {
|
|
1085
|
+
identification.fragmentLengths.push(info.len)
|
|
1086
|
+
}
|
|
1087
|
+
})
|
|
1088
|
+
const readRecord = this.recordReader(() => {
|
|
751
1089
|
this.stats.identificationFetches += 1
|
|
1090
|
+
identification.graphFetches += 1
|
|
752
1091
|
})
|
|
753
|
-
const
|
|
754
|
-
|
|
1092
|
+
const recordAt = (handle: number) => {
|
|
1093
|
+
identification.graphLookups += 1
|
|
1094
|
+
return readRecord(handle)
|
|
1095
|
+
}
|
|
1096
|
+
const sampleAt = async (pos: Pos, chain: ChainRecord) => {
|
|
1097
|
+
if (scanned(pos.node)) {
|
|
755
1098
|
return samples.get(posKey(pos))
|
|
756
1099
|
}
|
|
757
1100
|
this.stats.identificationFetches += 1
|
|
758
|
-
|
|
1101
|
+
identification.companionSeeks += 1
|
|
1102
|
+
chain.seeks += 1
|
|
1103
|
+
const sample = await this.db.haplotypeSampleAt(pos.node, pos.offset)
|
|
1104
|
+
if (!sample) {
|
|
1105
|
+
identification.companionMisses += 1
|
|
1106
|
+
}
|
|
1107
|
+
return sample
|
|
759
1108
|
}
|
|
760
1109
|
const names = new Map<number, PathName>()
|
|
761
1110
|
const nameOf = async (pathHandle: number) => {
|
|
@@ -809,6 +1158,16 @@ export class Subgraph {
|
|
|
809
1158
|
}
|
|
810
1159
|
const chain: { index: number; startBp: number }[] = []
|
|
811
1160
|
const visited = new Set<number>()
|
|
1161
|
+
const record: ChainRecord = {
|
|
1162
|
+
fragments: 0,
|
|
1163
|
+
steps: 0,
|
|
1164
|
+
seeks: 0,
|
|
1165
|
+
reentries: 0,
|
|
1166
|
+
twinLandings: 0,
|
|
1167
|
+
end: 'endmarker',
|
|
1168
|
+
pathHandle: undefined,
|
|
1169
|
+
}
|
|
1170
|
+
identification.chains.push(record)
|
|
812
1171
|
let anchor: Anchor | undefined
|
|
813
1172
|
let counter = 0
|
|
814
1173
|
let current: number | undefined = start
|
|
@@ -817,13 +1176,16 @@ export class Subgraph {
|
|
|
817
1176
|
this.signal?.throwIfAborted()
|
|
818
1177
|
if (current !== undefined) {
|
|
819
1178
|
if (visited.has(current)) {
|
|
1179
|
+
record.end = 'cycle'
|
|
820
1180
|
break
|
|
821
1181
|
}
|
|
822
1182
|
visited.add(current)
|
|
823
1183
|
const info = this.paths[current]!
|
|
824
1184
|
chain.push({ index: current, startBp: counter })
|
|
1185
|
+
record.fragments += 1
|
|
825
1186
|
let bp = counter
|
|
826
|
-
for (
|
|
1187
|
+
for (let k = 0; k < info.path.length; k++) {
|
|
1188
|
+
const position = pathPosition(info, k)
|
|
827
1189
|
const sample = samples.get(posKey(position))
|
|
828
1190
|
const nodeLen = this.record(position.node).sequenceLen
|
|
829
1191
|
if (sample) {
|
|
@@ -834,42 +1196,56 @@ export class Subgraph {
|
|
|
834
1196
|
}
|
|
835
1197
|
counter += info.len
|
|
836
1198
|
if (anchor) {
|
|
1199
|
+
record.end = 'in-fragment sample'
|
|
837
1200
|
break
|
|
838
1201
|
}
|
|
839
|
-
const last = info
|
|
1202
|
+
const last = pathPosition(info, info.path.length - 1)
|
|
840
1203
|
pos = this.record(last.node).gbwt().lf(last.offset)
|
|
841
1204
|
current = undefined
|
|
842
1205
|
}
|
|
843
1206
|
if (pos === undefined || pos.node === ENDMARKER) {
|
|
1207
|
+
record.end = 'endmarker'
|
|
844
1208
|
break
|
|
845
1209
|
}
|
|
846
|
-
const
|
|
1210
|
+
const key = posKey(pos)
|
|
1211
|
+
const known = starts.get(key)
|
|
847
1212
|
if (known !== undefined) {
|
|
848
1213
|
const identity = this.paths[known]!.identity
|
|
849
1214
|
if (identity) {
|
|
850
1215
|
anchor = anchorFromIdentity(identity, counter)
|
|
1216
|
+
record.end = 'identified sibling'
|
|
851
1217
|
break
|
|
852
1218
|
}
|
|
853
1219
|
current = known
|
|
854
1220
|
continue
|
|
855
1221
|
}
|
|
856
|
-
|
|
857
|
-
|
|
1222
|
+
if (this.records.has(pos.node)) {
|
|
1223
|
+
record.reentries += 1
|
|
1224
|
+
}
|
|
1225
|
+
if (this.twinStarts.has(key)) {
|
|
1226
|
+
record.twinLandings += 1
|
|
1227
|
+
}
|
|
1228
|
+
const sample = await sampleAt(pos, record)
|
|
1229
|
+
const node = await recordAt(pos.node)
|
|
858
1230
|
if (sample) {
|
|
859
|
-
anchor = anchorFromSample(sample, counter,
|
|
1231
|
+
anchor = anchorFromSample(sample, counter, node.sequenceLen)
|
|
1232
|
+
record.end = 'out-of-window sample'
|
|
860
1233
|
break
|
|
861
1234
|
}
|
|
862
1235
|
this.stats.identificationSteps += 1
|
|
1236
|
+
record.steps += 1
|
|
863
1237
|
if (
|
|
864
1238
|
counter - (chain[chain.length - 1] as { startBp: number }).startBp >
|
|
865
|
-
4 * interval + 4 *
|
|
1239
|
+
4 * interval + 4 * node.sequenceLen
|
|
866
1240
|
) {
|
|
1241
|
+
record.end = 'bound'
|
|
867
1242
|
break
|
|
868
1243
|
}
|
|
869
|
-
counter +=
|
|
870
|
-
pos =
|
|
1244
|
+
counter += node.sequenceLen
|
|
1245
|
+
pos = node.gbwt().lf(pos.offset)
|
|
871
1246
|
}
|
|
872
1247
|
if (anchor) {
|
|
1248
|
+
record.pathHandle = anchor.pathHandle
|
|
873
1249
|
const name = await nameOf(anchor.pathHandle)
|
|
874
1250
|
for (const { index, startBp } of chain) {
|
|
875
1251
|
const info = this.paths[index]!
|
|
@@ -1020,28 +1396,7 @@ export class Subgraph {
|
|
|
1020
1396
|
suffix = refLen - prefix
|
|
1021
1397
|
}
|
|
1022
1398
|
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
|
-
}
|
|
1399
|
+
appendGap(edits, pathLen - prefix - suffix, refLen - prefix - suffix)
|
|
1045
1400
|
appendEdit(edits, 'M', suffix)
|
|
1046
1401
|
}
|
|
1047
1402
|
|
|
@@ -1067,22 +1422,37 @@ export class Subgraph {
|
|
|
1067
1422
|
ordered ??
|
|
1068
1423
|
weightedLcs(path, ref, handle => this.record(handle).sequenceLen)[0]
|
|
1069
1424
|
const edits: Edit[] = []
|
|
1425
|
+
const refPrefix = this.refPrefix(ref)
|
|
1426
|
+
const alignGap = (
|
|
1427
|
+
pathFrom: number,
|
|
1428
|
+
pathTo: number,
|
|
1429
|
+
refFrom: number,
|
|
1430
|
+
refTo: number,
|
|
1431
|
+
) => {
|
|
1432
|
+
if (pathFrom === pathTo) {
|
|
1433
|
+
appendEdit(edits, 'D', refPrefix[refTo]! - refPrefix[refFrom]!)
|
|
1434
|
+
} else if (refFrom === refTo) {
|
|
1435
|
+
appendEdit(edits, 'I', this.pathLen(path.slice(pathFrom, pathTo)))
|
|
1436
|
+
} else {
|
|
1437
|
+
this.align(
|
|
1438
|
+
path.slice(pathFrom, pathTo),
|
|
1439
|
+
ref.slice(refFrom, refTo),
|
|
1440
|
+
edits,
|
|
1441
|
+
)
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1070
1444
|
let matched = 0
|
|
1071
1445
|
let pathOffset = 0
|
|
1072
1446
|
let refOffset = 0
|
|
1073
1447
|
for (const [nextPath, nextRef] of lcs) {
|
|
1074
|
-
|
|
1075
|
-
path.slice(pathOffset, nextPath),
|
|
1076
|
-
ref.slice(refOffset, nextRef),
|
|
1077
|
-
edits,
|
|
1078
|
-
)
|
|
1448
|
+
alignGap(pathOffset, nextPath, refOffset, nextRef)
|
|
1079
1449
|
const nodeLen = this.record(path[nextPath]!).sequenceLen
|
|
1080
1450
|
appendEdit(edits, 'M', nodeLen)
|
|
1081
1451
|
matched += nodeLen
|
|
1082
1452
|
pathOffset = nextPath + 1
|
|
1083
1453
|
refOffset = nextRef + 1
|
|
1084
1454
|
}
|
|
1085
|
-
|
|
1455
|
+
alignGap(pathOffset, path.length, refOffset, ref.length)
|
|
1086
1456
|
return { edits, matched }
|
|
1087
1457
|
}
|
|
1088
1458
|
|
|
@@ -1132,7 +1502,7 @@ export class Subgraph {
|
|
|
1132
1502
|
}
|
|
1133
1503
|
const ref = this.paths[this.refId]!.path
|
|
1134
1504
|
const refTotal = this.refPrefix(ref)[ref.length]!
|
|
1135
|
-
const
|
|
1505
|
+
const fragments: FragmentAlignment[] = []
|
|
1136
1506
|
this.paths.forEach((info, index) => {
|
|
1137
1507
|
if (index === this.refId) {
|
|
1138
1508
|
return
|
|
@@ -1154,38 +1524,44 @@ export class Subgraph {
|
|
|
1154
1524
|
const alongReference = identity
|
|
1155
1525
|
? (identity.orientation === 'forward') !== flipped
|
|
1156
1526
|
: !flipped
|
|
1157
|
-
|
|
1527
|
+
fragments.push({
|
|
1158
1528
|
strand: alongReference ? '+' : '-',
|
|
1159
1529
|
refStart: reference.start + leading,
|
|
1160
1530
|
refEnd: reference.start + refTotal - trailing,
|
|
1161
|
-
|
|
1162
|
-
.slice(first, last)
|
|
1163
|
-
.map(([op, len]) => `${len}${op}`)
|
|
1164
|
-
.join(''),
|
|
1531
|
+
edits: edits.slice(first, last),
|
|
1165
1532
|
weight: info.weight,
|
|
1166
1533
|
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
|
-
|
|
1534
|
+
start: pathPosition(info, 0),
|
|
1535
|
+
identity:
|
|
1536
|
+
identity === undefined
|
|
1537
|
+
? undefined
|
|
1538
|
+
: {
|
|
1539
|
+
pathHandle: identity.pathHandle,
|
|
1540
|
+
name: identity.name,
|
|
1541
|
+
hapStart: identity.name.fragment + identity.hapStart,
|
|
1542
|
+
hapEnd: identity.name.fragment + identity.hapEnd,
|
|
1543
|
+
walkForward: identity.orientation === 'forward',
|
|
1544
|
+
},
|
|
1545
|
+
})
|
|
1546
|
+
})
|
|
1547
|
+
return joinSiblings(fragments).map(fragment => {
|
|
1548
|
+
const { edits, identity, ...rest } = fragment
|
|
1549
|
+
const span: AlignmentSpan = { ...rest, cigar: cigarOf(edits) }
|
|
1550
|
+
return identity
|
|
1551
|
+
? {
|
|
1552
|
+
...span,
|
|
1553
|
+
resolved: true,
|
|
1554
|
+
name: identity.name,
|
|
1555
|
+
label: formatPathName(
|
|
1556
|
+
{ ...identity.name, fragment: identity.hapStart },
|
|
1557
|
+
identity.hapEnd,
|
|
1558
|
+
),
|
|
1559
|
+
pathHandle: identity.pathHandle,
|
|
1560
|
+
hapStart: identity.hapStart,
|
|
1561
|
+
hapEnd: identity.hapEnd,
|
|
1562
|
+
}
|
|
1563
|
+
: { ...span, resolved: false }
|
|
1187
1564
|
})
|
|
1188
|
-
return result
|
|
1189
1565
|
}
|
|
1190
1566
|
|
|
1191
1567
|
private canonicalEdges(id: number) {
|
|
@@ -1254,11 +1630,12 @@ export class Subgraph {
|
|
|
1254
1630
|
}
|
|
1255
1631
|
const walk = (
|
|
1256
1632
|
info: PathInfo,
|
|
1633
|
+
identity: PathIdentity | undefined,
|
|
1257
1634
|
name: PathName,
|
|
1258
1635
|
end: number,
|
|
1259
1636
|
cigarString: string | undefined,
|
|
1260
1637
|
) => {
|
|
1261
|
-
const steps = info
|
|
1638
|
+
const steps = haplotypeOrderedPath(info, identity)
|
|
1262
1639
|
.map(handle => `${isReverse(handle) ? '<' : '>'}${nodeId(handle)}`)
|
|
1263
1640
|
.join('')
|
|
1264
1641
|
const weight = info.weight === undefined ? '' : `\tWT:i:${info.weight}`
|
|
@@ -1270,6 +1647,7 @@ export class Subgraph {
|
|
|
1270
1647
|
lines.push(
|
|
1271
1648
|
walk(
|
|
1272
1649
|
this.paths[this.refId]!,
|
|
1650
|
+
undefined,
|
|
1273
1651
|
{
|
|
1274
1652
|
...this.refPath,
|
|
1275
1653
|
fragment: this.refPath.fragment + this.refInterval[0],
|
|
@@ -1288,6 +1666,7 @@ export class Subgraph {
|
|
|
1288
1666
|
resolved
|
|
1289
1667
|
? walk(
|
|
1290
1668
|
info,
|
|
1669
|
+
resolved,
|
|
1291
1670
|
{
|
|
1292
1671
|
...resolved.name,
|
|
1293
1672
|
fragment: resolved.name.fragment + resolved.hapStart,
|
|
@@ -1297,6 +1676,7 @@ export class Subgraph {
|
|
|
1297
1676
|
)
|
|
1298
1677
|
: walk(
|
|
1299
1678
|
info,
|
|
1679
|
+
undefined,
|
|
1300
1680
|
{ sample: 'unknown', contig, haplotype, fragment: 0 },
|
|
1301
1681
|
info.len,
|
|
1302
1682
|
cigarString,
|
|
@@ -1341,6 +1721,7 @@ export class Subgraph {
|
|
|
1341
1721
|
paths.push(
|
|
1342
1722
|
jsonPath(
|
|
1343
1723
|
info,
|
|
1724
|
+
undefined,
|
|
1344
1725
|
formatPathName(name, this.refPath.fragment + this.refInterval[1]),
|
|
1345
1726
|
undefined,
|
|
1346
1727
|
),
|
|
@@ -1365,7 +1746,12 @@ export class Subgraph {
|
|
|
1365
1746
|
info.len,
|
|
1366
1747
|
)
|
|
1367
1748
|
paths.push(
|
|
1368
|
-
jsonPath(
|
|
1749
|
+
jsonPath(
|
|
1750
|
+
info,
|
|
1751
|
+
resolved,
|
|
1752
|
+
name,
|
|
1753
|
+
cigar ? this.alignToRef(index) : undefined,
|
|
1754
|
+
),
|
|
1369
1755
|
)
|
|
1370
1756
|
haplotype += 1
|
|
1371
1757
|
})
|
|
@@ -1373,8 +1759,23 @@ export class Subgraph {
|
|
|
1373
1759
|
}
|
|
1374
1760
|
}
|
|
1375
1761
|
|
|
1762
|
+
// A named walk lists its steps in the haplotype's own direction, as the W line
|
|
1763
|
+
// spec and the start..end coordinates beside it require. extractPaths keeps
|
|
1764
|
+
// whichever twin of a walk is canonical, which is a property of the handles
|
|
1765
|
+
// and not of the haplotype, so the kept walk runs against the haplotype for
|
|
1766
|
+
// about half of them; identification records which.
|
|
1767
|
+
function haplotypeOrderedPath(
|
|
1768
|
+
info: PathInfo,
|
|
1769
|
+
identity: PathIdentity | undefined,
|
|
1770
|
+
) {
|
|
1771
|
+
return identity?.orientation === 'reverse'
|
|
1772
|
+
? info.path.map(handle => flipNode(handle)).reverse()
|
|
1773
|
+
: info.path
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1376
1776
|
function jsonPath(
|
|
1377
1777
|
info: PathInfo,
|
|
1778
|
+
identity: PathIdentity | undefined,
|
|
1378
1779
|
name: string,
|
|
1379
1780
|
cigar: string | undefined,
|
|
1380
1781
|
): SubgraphPath {
|
|
@@ -1382,7 +1783,7 @@ function jsonPath(
|
|
|
1382
1783
|
name,
|
|
1383
1784
|
...(info.weight === undefined ? {} : { weight: info.weight }),
|
|
1384
1785
|
...(cigar === undefined ? {} : { cigar }),
|
|
1385
|
-
path: info.
|
|
1786
|
+
path: haplotypeOrderedPath(info, identity).map(handle => ({
|
|
1386
1787
|
id: String(nodeId(handle)),
|
|
1387
1788
|
is_reverse: isReverse(handle),
|
|
1388
1789
|
})),
|
|
@@ -1416,3 +1817,30 @@ function appendEdit(edits: Edit[], op: EditOp, len: number) {
|
|
|
1416
1817
|
function gapPenalty(len: number) {
|
|
1417
1818
|
return len === 0 ? 0 : 6 + (len - 1)
|
|
1418
1819
|
}
|
|
1820
|
+
|
|
1821
|
+
function appendGap(edits: Edit[], pathMiddle: number, refMiddle: number) {
|
|
1822
|
+
if (pathMiddle === 0) {
|
|
1823
|
+
appendEdit(edits, 'D', refMiddle)
|
|
1824
|
+
} else if (refMiddle === 0) {
|
|
1825
|
+
appendEdit(edits, 'I', pathMiddle)
|
|
1826
|
+
} else {
|
|
1827
|
+
const mismatch = Math.min(pathMiddle, refMiddle)
|
|
1828
|
+
const mismatchIndel =
|
|
1829
|
+
4 * mismatch +
|
|
1830
|
+
gapPenalty(pathMiddle - mismatch) +
|
|
1831
|
+
gapPenalty(refMiddle - mismatch)
|
|
1832
|
+
const insertionDeletion = gapPenalty(pathMiddle) + gapPenalty(refMiddle)
|
|
1833
|
+
if (mismatchIndel <= insertionDeletion) {
|
|
1834
|
+
appendEdit(edits, 'M', mismatch)
|
|
1835
|
+
appendEdit(edits, 'I', pathMiddle - mismatch)
|
|
1836
|
+
appendEdit(edits, 'D', refMiddle - mismatch)
|
|
1837
|
+
} else {
|
|
1838
|
+
appendEdit(edits, 'I', pathMiddle)
|
|
1839
|
+
appendEdit(edits, 'D', refMiddle)
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function cigarOf(edits: Edit[]) {
|
|
1845
|
+
return edits.map(([op, len]) => `${len}${op}`).join('')
|
|
1846
|
+
}
|