@gmod/gbz-base 0.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +45 -4
  2. package/bin/query.js +3 -1
  3. package/dist/cli.js +85 -19
  4. package/dist/cli.js.map +1 -0
  5. package/dist/db.d.ts +14 -6
  6. package/dist/db.js +92 -28
  7. package/dist/db.js.map +1 -0
  8. package/dist/filehandle.js +1 -0
  9. package/dist/filehandle.js.map +1 -0
  10. package/dist/gbwt/bytecode.d.ts +1 -1
  11. package/dist/gbwt/bytecode.js +7 -3
  12. package/dist/gbwt/bytecode.js.map +1 -0
  13. package/dist/gbwt/node.js +7 -2
  14. package/dist/gbwt/node.js.map +1 -0
  15. package/dist/gbwt/record.d.ts +4 -0
  16. package/dist/gbwt/record.js +30 -2
  17. package/dist/gbwt/record.js.map +1 -0
  18. package/dist/gbwt/sequence.js +8 -1
  19. package/dist/gbwt/sequence.js.map +1 -0
  20. package/dist/graphName.d.ts +9 -0
  21. package/dist/graphName.js +83 -0
  22. package/dist/graphName.js.map +1 -0
  23. package/dist/index.d.ts +5 -4
  24. package/dist/index.js +3 -2
  25. package/dist/index.js.map +1 -0
  26. package/dist/lcs.js +32 -10
  27. package/dist/lcs.js.map +1 -0
  28. package/dist/query.d.ts +4 -2
  29. package/dist/query.js +19 -0
  30. package/dist/query.js.map +1 -0
  31. package/dist/sqlite/btree.d.ts +1 -1
  32. package/dist/sqlite/btree.js +24 -7
  33. package/dist/sqlite/btree.js.map +1 -0
  34. package/dist/sqlite/database.d.ts +1 -1
  35. package/dist/sqlite/database.js +12 -2
  36. package/dist/sqlite/database.js.map +1 -0
  37. package/dist/sqlite/pager.d.ts +8 -3
  38. package/dist/sqlite/pager.js +71 -12
  39. package/dist/sqlite/pager.js.map +1 -0
  40. package/dist/sqlite/record.js +3 -1
  41. package/dist/sqlite/record.js.map +1 -0
  42. package/dist/subgraph.d.ts +12 -2
  43. package/dist/subgraph.js +413 -84
  44. package/dist/subgraph.js.map +1 -0
  45. package/package.json +36 -11
  46. package/src/cli.ts +258 -0
  47. package/src/db.ts +374 -0
  48. package/src/filehandle.ts +4 -0
  49. package/src/gbwt/bytecode.ts +87 -0
  50. package/src/gbwt/node.ts +64 -0
  51. package/src/gbwt/record.ts +177 -0
  52. package/src/gbwt/sequence.ts +50 -0
  53. package/src/graphName.ts +94 -0
  54. package/src/index.ts +34 -0
  55. package/src/lcs.ts +282 -0
  56. package/src/query.ts +106 -0
  57. package/src/sqlite/btree.ts +260 -0
  58. package/src/sqlite/database.ts +117 -0
  59. package/src/sqlite/pager.ts +148 -0
  60. package/src/sqlite/record.ts +100 -0
  61. package/src/subgraph.ts +1353 -0
  62. package/tools/haplotype-index/src/main.rs +197 -163
@@ -0,0 +1,1353 @@
1
+ import { formatPathName } from './db.ts'
2
+ import {
3
+ ENDMARKER,
4
+ edgeIsCanonical,
5
+ encodeNode,
6
+ entryOrientation,
7
+ entrySide,
8
+ exitOrientation,
9
+ exitSide,
10
+ flipNode,
11
+ flipSide,
12
+ isReverse,
13
+ nodeId,
14
+ nodeOrientation,
15
+ pathIsCanonical,
16
+ } from './gbwt/node.ts'
17
+ import { gfaHeaderLines, sha256Hex, subgraphName } from './graphName.ts'
18
+ import { weightedLcs } from './lcs.ts'
19
+
20
+ import type {
21
+ GBZBase,
22
+ GbzPath,
23
+ GbzRecord,
24
+ HaplotypeSample,
25
+ PathName,
26
+ } from './db.ts'
27
+ import type { NodeSide, Orientation } from './gbwt/node.ts'
28
+ import type { Pos } from './gbwt/record.ts'
29
+
30
+ export type HaplotypeOutput = 'all' | 'distinct' | 'reference-only' | 'none'
31
+
32
+ export type SnarlOutput = 'none' | 'contained' | 'overlapping'
33
+
34
+ type HandleType =
35
+ | { kind: 'snarl-exit'; snarl: [number, number] }
36
+ | { kind: 'chain' }
37
+ | { kind: 'regular' }
38
+
39
+ export interface PathPosition {
40
+ seqOffset: number
41
+ handle: number
42
+ nodeOffset: number
43
+ gbwtOffset: number
44
+ }
45
+
46
+ export interface ReferencePath {
47
+ position: PathPosition
48
+ name: PathName
49
+ handle: number
50
+ }
51
+
52
+ export interface PathIdentity {
53
+ pathHandle: number
54
+ name: PathName
55
+ orientation: Orientation
56
+ hapStart: number
57
+ hapEnd: number
58
+ }
59
+
60
+ interface PathInfo {
61
+ path: number[]
62
+ positions: Pos[]
63
+ len: number
64
+ weight: number | undefined
65
+ identity: PathIdentity | undefined
66
+ }
67
+
68
+ type EditOp = 'M' | 'I' | 'D'
69
+ type Edit = [EditOp, number]
70
+
71
+ export interface SubgraphPath {
72
+ name: string
73
+ weight?: number
74
+ cigar?: string
75
+ path: { id: string; is_reverse: boolean }[]
76
+ }
77
+
78
+ export interface SubgraphJson {
79
+ nodes: { id: string; sequence: string }[]
80
+ edges: {
81
+ from: string
82
+ from_is_reverse: boolean
83
+ to: string
84
+ to_is_reverse: boolean
85
+ }[]
86
+ paths: SubgraphPath[]
87
+ }
88
+
89
+ export interface HaplotypeAlignment {
90
+ pathHandle: number | undefined
91
+ name: PathName | undefined
92
+ strand: '+' | '-'
93
+ hapStart: number | undefined
94
+ hapEnd: number | undefined
95
+ refStart: number
96
+ refEnd: number
97
+ cigar: string
98
+ weight: number | undefined
99
+ path: number[]
100
+ start: Pos
101
+ }
102
+
103
+ export interface ToJsonOptions {
104
+ names?: 'anonymous' | 'resolved'
105
+ }
106
+
107
+ function sideBefore(
108
+ a: [number, number, NodeSide],
109
+ b: [number, number, NodeSide],
110
+ ) {
111
+ return (
112
+ a[0] < b[0] ||
113
+ (a[0] === b[0] && (a[1] < b[1] || (a[1] === b[1] && a[2] < b[2])))
114
+ )
115
+ }
116
+
117
+ class SideQueue {
118
+ private heap: [number, number, NodeSide][] = []
119
+
120
+ push(distance: number, node: number, side: NodeSide) {
121
+ const heap = this.heap
122
+ heap.push([distance, node, side])
123
+ let i = heap.length - 1
124
+ while (i > 0) {
125
+ const parent = (i - 1) >> 1
126
+ if (sideBefore(heap[i]!, heap[parent]!)) {
127
+ ;[heap[i], heap[parent]] = [heap[parent]!, heap[i]!]
128
+ i = parent
129
+ } else {
130
+ break
131
+ }
132
+ }
133
+ }
134
+
135
+ pop() {
136
+ const heap = this.heap
137
+ const top = heap[0]
138
+ const last = heap.pop()
139
+ if (heap.length > 0 && last !== undefined) {
140
+ heap[0] = last
141
+ let i = 0
142
+ for (;;) {
143
+ const left = 2 * i + 1
144
+ const right = left + 1
145
+ let smallest = i
146
+ if (left < heap.length && sideBefore(heap[left]!, heap[smallest]!)) {
147
+ smallest = left
148
+ }
149
+ if (right < heap.length && sideBefore(heap[right]!, heap[smallest]!)) {
150
+ smallest = right
151
+ }
152
+ if (smallest === i) {
153
+ break
154
+ }
155
+ ;[heap[i], heap[smallest]] = [heap[smallest]!, heap[i]!]
156
+ i = smallest
157
+ }
158
+ }
159
+ return top
160
+ }
161
+
162
+ get size() {
163
+ return this.heap.length
164
+ }
165
+ }
166
+
167
+ function posKey(pos: Pos) {
168
+ return `${pos.node}:${pos.offset}`
169
+ }
170
+
171
+ interface Anchor {
172
+ pathHandle: number
173
+ orientation: Orientation
174
+ base: number
175
+ }
176
+
177
+ export class Subgraph {
178
+ private records = new Map<number, GbzRecord>()
179
+ private paths: PathInfo[] = []
180
+ private refId: number | undefined
181
+ private refPath: PathName | undefined
182
+ private refHandle: number | undefined
183
+ private refInterval: [number, number] | undefined
184
+ private refIndexCache: Map<number, number[]> | undefined
185
+ private refPrefixCache: number[] | undefined
186
+ limit: number | undefined
187
+ readonly stats = {
188
+ orderedAlignments: 0,
189
+ lcsAlignments: 0,
190
+ identificationSteps: 0,
191
+ identificationFetches: 0,
192
+ }
193
+
194
+ private db: GBZBase
195
+
196
+ constructor(db: GBZBase) {
197
+ this.db = db
198
+ }
199
+
200
+ get nodeCount() {
201
+ return this.records.size / 2
202
+ }
203
+
204
+ get pathCount() {
205
+ return this.paths.length
206
+ }
207
+
208
+ get referenceInterval() {
209
+ return this.refInterval && this.refPath
210
+ ? {
211
+ name: this.refPath,
212
+ start: this.refPath.fragment + this.refInterval[0],
213
+ end: this.refPath.fragment + this.refInterval[1],
214
+ }
215
+ : undefined
216
+ }
217
+
218
+ hasNode(id: number) {
219
+ return this.records.has(encodeNode(id, 'forward'))
220
+ }
221
+
222
+ hasHandle(handle: number) {
223
+ return this.records.has(handle)
224
+ }
225
+
226
+ private record(handle: number) {
227
+ const record = this.records.get(handle)
228
+ if (!record) {
229
+ throw new Error(`Subgraph has no record for handle ${handle}`)
230
+ }
231
+ return record
232
+ }
233
+
234
+ private sortedHandles() {
235
+ return [...this.records.keys()].sort((a, b) => a - b)
236
+ }
237
+
238
+ private async addNode(id: number) {
239
+ if (this.limit !== undefined && this.nodeCount >= this.limit) {
240
+ throw new Error(`Subgraph size limit of ${this.limit} nodes exceeded`)
241
+ }
242
+ const forward = await this.db.getRecord(encodeNode(id, 'forward'))
243
+ const reverse = await this.db.getRecord(encodeNode(id, 'reverse'))
244
+ if (!forward || !reverse) {
245
+ throw new Error(`Node ${id} does not exist in the graph`)
246
+ }
247
+ this.records.set(forward.handle, forward)
248
+ this.records.set(reverse.handle, reverse)
249
+ }
250
+
251
+ private async ensureNode(id: number) {
252
+ if (!this.hasNode(id)) {
253
+ await this.addNode(id)
254
+ }
255
+ }
256
+
257
+ private clearPaths() {
258
+ this.paths = []
259
+ this.refId = undefined
260
+ this.refPath = undefined
261
+ this.refHandle = undefined
262
+ this.refInterval = undefined
263
+ this.refIndexCache = undefined
264
+ this.refPrefixCache = undefined
265
+ }
266
+
267
+ async pathPosition(query: PathName): Promise<ReferencePath> {
268
+ const path = await this.db.findPath(query)
269
+ if (!path) {
270
+ throw new Error(
271
+ `Cannot find a path covering ${formatPathName(query, query.fragment)}`,
272
+ )
273
+ }
274
+ if (!path.isIndexed) {
275
+ throw new Error(
276
+ `Path ${formatPathName(path.name, path.name.fragment)} has not been indexed for random access`,
277
+ )
278
+ }
279
+ const queryOffset = query.fragment - path.name.fragment
280
+ const indexed = await this.db.indexedPosition(path.handle, queryOffset)
281
+ if (!indexed) {
282
+ throw new Error(
283
+ `Path ${formatPathName(path.name, path.name.fragment)} has not been indexed for random access`,
284
+ )
285
+ }
286
+ return this.findPathPosition(
287
+ path,
288
+ queryOffset,
289
+ indexed.pathOffset,
290
+ indexed.pos,
291
+ )
292
+ }
293
+
294
+ private async findPathPosition(
295
+ path: GbzPath,
296
+ queryOffset: number,
297
+ startOffset: number,
298
+ start: Pos,
299
+ ): Promise<ReferencePath> {
300
+ let pathOffset = startOffset
301
+ let pos = start
302
+ for (;;) {
303
+ await this.ensureNode(nodeId(pos.node))
304
+ const record = this.record(pos.node)
305
+ if (pathOffset + record.sequenceLen > queryOffset) {
306
+ return {
307
+ position: {
308
+ seqOffset: queryOffset,
309
+ handle: pos.node,
310
+ nodeOffset: queryOffset - pathOffset,
311
+ gbwtOffset: pos.offset,
312
+ },
313
+ name: path.name,
314
+ handle: path.handle,
315
+ }
316
+ }
317
+ pathOffset += record.sequenceLen
318
+ const next = record.gbwt().lf(pos.offset)
319
+ if (!next) {
320
+ throw new Error(
321
+ `Path ${formatPathName(path.name, path.name.fragment)} does not contain offset ${queryOffset}`,
322
+ )
323
+ }
324
+ pos = next
325
+ }
326
+ }
327
+
328
+ async aroundPosition(handle: number, nodeOffset: number, context: number) {
329
+ const id = nodeId(handle)
330
+ await this.ensureNode(id)
331
+ const record = this.record(handle)
332
+ const orientation = nodeOrientation(handle)
333
+ const active = new SideQueue()
334
+ active.push(nodeOffset, id, entrySide(orientation))
335
+ active.push(record.sequenceLen - nodeOffset - 1, id, exitSide(orientation))
336
+ return this.insertContext(active, context)
337
+ }
338
+
339
+ async aroundInterval(start: PathPosition, len: number, context: number) {
340
+ if (len === 0) {
341
+ throw new Error('Interval length must be greater than 0')
342
+ }
343
+ let pos: Pos = { node: start.handle, offset: start.gbwtOffset }
344
+ let offset = start.nodeOffset
345
+ let remaining = len
346
+ const active = new SideQueue()
347
+ for (;;) {
348
+ const id = nodeId(pos.node)
349
+ const orientation = nodeOrientation(pos.node)
350
+ await this.ensureNode(id)
351
+ const record = this.record(pos.node)
352
+ if (offset >= record.sequenceLen) {
353
+ throw new Error(
354
+ `Offset ${offset} in node ${id} of length ${record.sequenceLen}`,
355
+ )
356
+ }
357
+ active.push(offset, id, entrySide(orientation))
358
+ const distanceToNext = record.sequenceLen - offset
359
+ if (remaining <= distanceToNext) {
360
+ active.push(
361
+ remaining === distanceToNext ? 0 : distanceToNext - remaining - 1,
362
+ id,
363
+ exitSide(orientation),
364
+ )
365
+ break
366
+ }
367
+ active.push(0, id, exitSide(orientation))
368
+ const next = record.gbwt().lf(pos.offset)
369
+ if (!next) {
370
+ throw new Error(
371
+ `No successor for GBWT position (${pos.node}, ${pos.offset})`,
372
+ )
373
+ }
374
+ pos = next
375
+ offset = 0
376
+ remaining -= distanceToNext
377
+ }
378
+ return this.insertContext(active, context)
379
+ }
380
+
381
+ async aroundNodes(nodes: Iterable<number>, context: number) {
382
+ const active = new SideQueue()
383
+ for (const id of nodes) {
384
+ await this.ensureNode(id)
385
+ active.push(0, id, 'left')
386
+ active.push(0, id, 'right')
387
+ }
388
+ return this.insertContext(active, context)
389
+ }
390
+
391
+ private async insertContext(active: SideQueue, context: number) {
392
+ this.clearPaths()
393
+ const visited = new Set<string>()
394
+ const toRemove = new Set<number>()
395
+ for (const handle of this.records.keys()) {
396
+ toRemove.add(nodeId(handle))
397
+ }
398
+ let inserted = 0
399
+ while (active.size > 0) {
400
+ const [distance, id, side] = active.pop()!
401
+ const key = `${id}:${side}`
402
+ if (visited.has(key)) {
403
+ continue
404
+ }
405
+ visited.add(key)
406
+ toRemove.delete(id)
407
+ if (!this.hasNode(id)) {
408
+ await this.addNode(id)
409
+ inserted += 1
410
+ }
411
+ const otherSide = flipSide(side)
412
+ if (!visited.has(`${id}:${otherSide}`)) {
413
+ const record = this.record(encodeNode(id, entryOrientation(side)))
414
+ const nextDistance = distance + record.sequenceLen - 1
415
+ if (nextDistance <= context) {
416
+ active.push(nextDistance, id, otherSide)
417
+ }
418
+ }
419
+ const record = this.record(encodeNode(id, exitOrientation(side)))
420
+ const nextDistance = distance + 1
421
+ if (nextDistance <= context) {
422
+ for (const successor of record.successors()) {
423
+ const successorId = nodeId(successor)
424
+ const successorSide = entrySide(nodeOrientation(successor))
425
+ if (!visited.has(`${successorId}:${successorSide}`)) {
426
+ active.push(nextDistance, successorId, successorSide)
427
+ }
428
+ }
429
+ }
430
+ }
431
+ for (const id of toRemove) {
432
+ this.records.delete(encodeNode(id, 'forward'))
433
+ this.records.delete(encodeNode(id, 'reverse'))
434
+ }
435
+ return { inserted, removed: toRemove.size }
436
+ }
437
+
438
+ async betweenNodes(start: number, end: number) {
439
+ this.clearPaths()
440
+ const active = [start, flipNode(end)]
441
+ const visited = new Set([nodeId(start), nodeId(end)])
442
+ let inserted = 0
443
+ while (active.length > 0) {
444
+ const curr = active.pop()!
445
+ const id = nodeId(curr)
446
+ if (!this.hasNode(id)) {
447
+ await this.addNode(id)
448
+ inserted += 1
449
+ }
450
+ for (const successor of this.record(curr).successors()) {
451
+ const successorId = nodeId(successor)
452
+ if (!visited.has(successorId)) {
453
+ active.push(successor, flipNode(successor))
454
+ visited.add(successorId)
455
+ }
456
+ }
457
+ }
458
+ return inserted
459
+ }
460
+
461
+ async extractSnarls(snarls: SnarlOutput) {
462
+ let inserted = 0
463
+ for (const [start, end] of await this.overlappingSnarls(snarls)) {
464
+ inserted += await this.betweenNodes(start, end)
465
+ }
466
+ return inserted
467
+ }
468
+
469
+ private async overlappingSnarls(snarls: SnarlOutput) {
470
+ const result: [number, number][] = []
471
+ if (snarls !== 'none') {
472
+ let foundLink = false
473
+ for (const handle of this.sortedHandles()) {
474
+ const record = this.record(handle)
475
+ const next = record.next
476
+ if (next !== undefined) {
477
+ foundLink = true
478
+ if (this.hasHandle(next)) {
479
+ if (edgeIsCanonical(handle, next)) {
480
+ result.push([handle, next])
481
+ }
482
+ } else if (
483
+ snarls === 'overlapping' &&
484
+ this.isSnarlEntryInSubgraph(record)
485
+ ) {
486
+ result.push([handle, next])
487
+ }
488
+ }
489
+ }
490
+ if (
491
+ !foundLink &&
492
+ snarls === 'overlapping' &&
493
+ (await this.db.hasChainLinks())
494
+ ) {
495
+ const covering = await this.findCoveringSnarl()
496
+ if (covering) {
497
+ result.push(covering)
498
+ }
499
+ }
500
+ }
501
+ return result
502
+ }
503
+
504
+ private isSnarlEntryInSubgraph(record: GbzRecord) {
505
+ const successors = record.successors()
506
+ const first = successors.find(handle => this.hasHandle(handle))
507
+ return first === undefined
508
+ ? false
509
+ : successors.length > 1 ||
510
+ this.record(flipNode(first)).successors().length > 1
511
+ }
512
+
513
+ private recordReader(onFetch?: () => void) {
514
+ const outside = new Map<number, GbzRecord>()
515
+ return async (handle: number) => {
516
+ const inside = this.records.get(handle)
517
+ if (inside) {
518
+ return inside
519
+ }
520
+ let record = outside.get(handle)
521
+ if (!record) {
522
+ record = await this.db.getRecord(handle)
523
+ onFetch?.()
524
+ if (!record) {
525
+ throw new Error(`Node record ${handle} is missing from the database`)
526
+ }
527
+ outside.set(handle, record)
528
+ }
529
+ return record
530
+ }
531
+ }
532
+
533
+ private async findCoveringSnarl() {
534
+ const read = this.recordReader()
535
+ const isSnarlEntry = async (record: GbzRecord) => {
536
+ const successors = record.successors()
537
+ const first = successors[0]
538
+ return first === undefined
539
+ ? false
540
+ : successors.length > 1 ||
541
+ (await read(flipNode(first))).successors().length > 1
542
+ }
543
+ const classify = async (handle: number): Promise<HandleType> => {
544
+ const reverse = await read(flipNode(handle))
545
+ return reverse.next !== undefined
546
+ ? (await isSnarlEntry(reverse))
547
+ ? { kind: 'snarl-exit', snarl: [flipNode(handle), reverse.next] }
548
+ : { kind: 'chain' }
549
+ : (await read(handle)).next !== undefined
550
+ ? { kind: 'chain' }
551
+ : { kind: 'regular' }
552
+ }
553
+ const visited = new Set<number>()
554
+ const queue = this.sortedHandles().flatMap(handle =>
555
+ this.record(handle).successors(),
556
+ )
557
+ let result: [number, number] | undefined
558
+ let done = false
559
+ while (!done && queue.length > 0) {
560
+ const handle = queue.shift()!
561
+ const id = nodeId(handle)
562
+ if (!this.hasHandle(handle) && !visited.has(id)) {
563
+ visited.add(id)
564
+ const type = await classify(handle)
565
+ if (type.kind === 'snarl-exit') {
566
+ result = type.snarl
567
+ done = true
568
+ } else if (type.kind === 'chain') {
569
+ done = true
570
+ } else {
571
+ for (const orientation of ['forward', 'reverse'] as const) {
572
+ queue.push(
573
+ ...(await read(encodeNode(id, orientation))).successors(),
574
+ )
575
+ }
576
+ }
577
+ }
578
+ }
579
+ return result
580
+ }
581
+
582
+ extractPaths(reference: ReferencePath | undefined, output: HaplotypeOutput) {
583
+ this.clearPaths()
584
+ if (output === 'none') {
585
+ return
586
+ }
587
+ const refPos = reference?.position
588
+ this.refPath = reference?.name
589
+ this.refHandle = reference?.handle
590
+ const handles = this.sortedHandles()
591
+ const successors = new Map<
592
+ number,
593
+ { nodes: Int32Array; offsets: Int32Array; hasPredecessor: Uint8Array }
594
+ >()
595
+ for (const handle of handles) {
596
+ const { nodes, offsets } = this.record(handle).gbwt().decompressArrays()
597
+ successors.set(handle, {
598
+ nodes,
599
+ offsets,
600
+ hasPredecessor: new Uint8Array(nodes.length),
601
+ })
602
+ }
603
+ for (const handle of handles) {
604
+ const { nodes, offsets } = successors.get(handle)!
605
+ for (let i = 0; i < nodes.length; i++) {
606
+ const entry = successors.get(nodes[i]!)
607
+ if (entry) {
608
+ entry.hasPredecessor[offsets[i]!] = 1
609
+ }
610
+ }
611
+ }
612
+ let refOffset: number | undefined
613
+ for (const handle of handles) {
614
+ const entries = successors.get(handle)!
615
+ for (let offset = 0; offset < entries.nodes.length; offset++) {
616
+ if (entries.hasPredecessor[offset] === 0) {
617
+ let currNode: number | undefined = handle
618
+ let currOffset = offset
619
+ let isRef = false
620
+ const path: number[] = []
621
+ const positions: Pos[] = []
622
+ let len = 0
623
+ while (currNode !== undefined) {
624
+ if (
625
+ currNode === refPos?.handle &&
626
+ currOffset === refPos.gbwtOffset
627
+ ) {
628
+ this.refId = this.paths.length
629
+ refOffset = path.length
630
+ isRef = true
631
+ }
632
+ path.push(currNode)
633
+ positions.push({ node: currNode, offset: currOffset })
634
+ len += this.record(currNode).sequenceLen
635
+ const step = successors.get(currNode)!
636
+ const nextNode: number = step.nodes[currOffset]!
637
+ const nextOffset: number = step.offsets[currOffset]!
638
+ if (nextNode !== ENDMARKER && successors.has(nextNode)) {
639
+ currNode = nextNode
640
+ currOffset = nextOffset
641
+ } else {
642
+ currNode = undefined
643
+ }
644
+ }
645
+ if (isRef || pathIsCanonical(path)) {
646
+ this.paths.push({
647
+ path,
648
+ positions,
649
+ len,
650
+ weight: undefined,
651
+ identity: undefined,
652
+ })
653
+ }
654
+ }
655
+ }
656
+ }
657
+ if (refPos) {
658
+ if (refOffset === undefined || this.refId === undefined) {
659
+ this.clearPaths()
660
+ throw new Error('Could not find the reference path')
661
+ }
662
+ const info = this.paths[this.refId]!
663
+ let before = refPos.nodeOffset
664
+ for (const handle of info.path.slice(0, refOffset)) {
665
+ before += this.record(handle).sequenceLen
666
+ }
667
+ const start = refPos.seqOffset - before
668
+ this.refInterval = [start, start + info.len]
669
+ info.identity = {
670
+ pathHandle: reference.handle,
671
+ name: reference.name,
672
+ orientation: 'forward',
673
+ hapStart: start,
674
+ hapEnd: start + info.len,
675
+ }
676
+ }
677
+ if (output === 'distinct') {
678
+ this.distinctPaths()
679
+ } else if (output === 'reference-only') {
680
+ if (this.refId === undefined) {
681
+ throw new Error('Reference path is required for reference-only output')
682
+ }
683
+ this.paths = [this.paths[this.refId]!]
684
+ this.refId = 0
685
+ }
686
+ }
687
+
688
+ private distinctPaths() {
689
+ const refPath =
690
+ this.refId === undefined ? undefined : this.paths[this.refId]!.path
691
+ this.paths.sort((x, y) => comparePaths(x.path, y.path) || x.len - y.len)
692
+ const merged: PathInfo[] = []
693
+ let refId: number | undefined
694
+ for (const info of this.paths) {
695
+ const last = merged[merged.length - 1]
696
+ if (last && comparePaths(last.path, info.path) === 0) {
697
+ last.weight = (last.weight ?? 0) + 1
698
+ } else {
699
+ if (refPath && comparePaths(info.path, refPath) === 0) {
700
+ refId = merged.length
701
+ }
702
+ merged.push({ ...info, weight: 1 })
703
+ }
704
+ }
705
+ this.paths = merged
706
+ this.refId = refId
707
+ }
708
+
709
+ async identifyPaths() {
710
+ if (!this.db.hasHaplotypeIndex) {
711
+ throw new Error(
712
+ 'The database has no HaplotypeSamples table; run gbz-haplotype-index on it',
713
+ )
714
+ }
715
+ const interval = (await this.db.haplotypeSampleInterval()) ?? 4096
716
+ const handles = this.sortedHandles()
717
+ const minHandle = handles[0]
718
+ const maxHandle = handles[handles.length - 1]
719
+ if (minHandle === undefined || maxHandle === undefined) {
720
+ return
721
+ }
722
+ const samples = new Map<string, HaplotypeSample>()
723
+ for (const sample of await this.db.haplotypeSamplesInRange(
724
+ minHandle,
725
+ maxHandle,
726
+ )) {
727
+ samples.set(posKey(sample), sample)
728
+ }
729
+ const starts = new Map<string, number>()
730
+ this.paths.forEach((info, index) => {
731
+ const first = info.positions[0]
732
+ if (first && index !== this.refId) {
733
+ starts.set(posKey(first), index)
734
+ }
735
+ })
736
+ const recordAt = this.recordReader(() => {
737
+ this.stats.identificationFetches += 1
738
+ })
739
+ const sampleAt = async (pos: Pos) => {
740
+ if (pos.node >= minHandle && pos.node <= maxHandle) {
741
+ return samples.get(posKey(pos))
742
+ }
743
+ this.stats.identificationFetches += 1
744
+ return this.db.haplotypeSampleAt(pos.node, pos.offset)
745
+ }
746
+ const names = new Map<number, PathName>()
747
+ const nameOf = async (pathHandle: number) => {
748
+ let name = names.get(pathHandle)
749
+ if (!name) {
750
+ const path = await this.db.getPath(pathHandle)
751
+ if (!path) {
752
+ throw new Error(`Path ${pathHandle} is missing from the database`)
753
+ }
754
+ name = path.name
755
+ names.set(pathHandle, name)
756
+ }
757
+ return name
758
+ }
759
+ const anchorFromSample = (
760
+ sample: HaplotypeSample,
761
+ counter: number,
762
+ nodeLen: number,
763
+ ): Anchor =>
764
+ sample.orientation === 'forward'
765
+ ? {
766
+ pathHandle: sample.pathHandle,
767
+ orientation: 'forward',
768
+ base: sample.pathOffset - counter,
769
+ }
770
+ : {
771
+ pathHandle: sample.pathHandle,
772
+ orientation: 'reverse',
773
+ base: sample.pathOffset + counter + nodeLen,
774
+ }
775
+ const anchorFromIdentity = (
776
+ identity: PathIdentity,
777
+ counter: number,
778
+ ): Anchor =>
779
+ identity.orientation === 'forward'
780
+ ? {
781
+ pathHandle: identity.pathHandle,
782
+ orientation: 'forward',
783
+ base: identity.hapStart - counter,
784
+ }
785
+ : {
786
+ pathHandle: identity.pathHandle,
787
+ orientation: 'reverse',
788
+ base: identity.hapEnd + counter,
789
+ }
790
+
791
+ for (let start = 0; start < this.paths.length; start++) {
792
+ const startInfo = this.paths[start]!
793
+ if (start === this.refId || startInfo.identity) {
794
+ continue
795
+ }
796
+ const chain: { index: number; startBp: number }[] = []
797
+ const visited = new Set<number>()
798
+ let anchor: Anchor | undefined
799
+ let counter = 0
800
+ let current: number | undefined = start
801
+ let pos: Pos | undefined
802
+ while (anchor === undefined) {
803
+ if (current !== undefined) {
804
+ if (visited.has(current)) {
805
+ break
806
+ }
807
+ visited.add(current)
808
+ const info = this.paths[current]!
809
+ chain.push({ index: current, startBp: counter })
810
+ let bp = counter
811
+ for (const position of info.positions) {
812
+ const sample = samples.get(posKey(position))
813
+ const nodeLen = this.record(position.node).sequenceLen
814
+ if (sample) {
815
+ anchor = anchorFromSample(sample, bp, nodeLen)
816
+ break
817
+ }
818
+ bp += nodeLen
819
+ }
820
+ counter += info.len
821
+ if (anchor) {
822
+ break
823
+ }
824
+ const last = info.positions[info.positions.length - 1]!
825
+ pos = this.record(last.node).gbwt().lf(last.offset)
826
+ current = undefined
827
+ }
828
+ if (pos === undefined || pos.node === ENDMARKER) {
829
+ break
830
+ }
831
+ const known = starts.get(posKey(pos))
832
+ if (known !== undefined) {
833
+ const identity = this.paths[known]!.identity
834
+ if (identity) {
835
+ anchor = anchorFromIdentity(identity, counter)
836
+ break
837
+ }
838
+ current = known
839
+ continue
840
+ }
841
+ const sample = await sampleAt(pos)
842
+ const record = await recordAt(pos.node)
843
+ if (sample) {
844
+ anchor = anchorFromSample(sample, counter, record.sequenceLen)
845
+ break
846
+ }
847
+ this.stats.identificationSteps += 1
848
+ if (
849
+ counter - (chain[chain.length - 1] as { startBp: number }).startBp >
850
+ 4 * interval + 4 * record.sequenceLen
851
+ ) {
852
+ break
853
+ }
854
+ counter += record.sequenceLen
855
+ pos = record.gbwt().lf(pos.offset)
856
+ }
857
+ if (anchor) {
858
+ const name = await nameOf(anchor.pathHandle)
859
+ for (const { index, startBp } of chain) {
860
+ const info = this.paths[index]!
861
+ info.identity =
862
+ anchor.orientation === 'forward'
863
+ ? {
864
+ pathHandle: anchor.pathHandle,
865
+ name,
866
+ orientation: 'forward',
867
+ hapStart: anchor.base + startBp,
868
+ hapEnd: anchor.base + startBp + info.len,
869
+ }
870
+ : {
871
+ pathHandle: anchor.pathHandle,
872
+ name,
873
+ orientation: 'reverse',
874
+ hapStart: anchor.base - startBp - info.len,
875
+ hapEnd: anchor.base - startBp,
876
+ }
877
+ }
878
+ }
879
+ }
880
+ }
881
+
882
+ private refIndex(ref: number[]) {
883
+ if (this.refIndexCache === undefined) {
884
+ const index = new Map<number, number[]>()
885
+ ref.forEach((handle, i) => {
886
+ const occurrences = index.get(handle)
887
+ if (occurrences) {
888
+ occurrences.push(i)
889
+ } else {
890
+ index.set(handle, [i])
891
+ }
892
+ })
893
+ this.refIndexCache = index
894
+ }
895
+ return this.refIndexCache
896
+ }
897
+
898
+ private refPrefix(ref: number[]) {
899
+ if (this.refPrefixCache === undefined) {
900
+ const prefix = [0]
901
+ ref.forEach((handle, i) => {
902
+ prefix.push(prefix[i]! + this.record(handle).sequenceLen)
903
+ })
904
+ this.refPrefixCache = prefix
905
+ }
906
+ return this.refPrefixCache
907
+ }
908
+
909
+ private orderedMatches(
910
+ path: number[],
911
+ ref: number[],
912
+ ): [number, number][] | undefined {
913
+ const index = this.refIndex(ref)
914
+ const pairs: [number, number][] = []
915
+ let last = -1
916
+ for (let i = 0; i < path.length; i++) {
917
+ const occurrences = index.get(path[i]!)
918
+ if (occurrences) {
919
+ const j = occurrences.find(x => x > last)
920
+ if (j === undefined) {
921
+ return undefined
922
+ }
923
+ pairs.push([i, j])
924
+ last = j
925
+ }
926
+ }
927
+ return pairs
928
+ }
929
+
930
+ private pathLen(path: number[]) {
931
+ let total = 0
932
+ for (const handle of path) {
933
+ total += this.record(handle).sequenceLen
934
+ }
935
+ return total
936
+ }
937
+
938
+ private prefixMatches(path: number[], ref: number[]) {
939
+ let result = 0
940
+ let pi = 0
941
+ let ri = 0
942
+ let pb = 0
943
+ let rb = 0
944
+ while (pi < path.length && ri < ref.length) {
945
+ const a = this.record(path[pi]!).sequence
946
+ const b = this.record(ref[ri]!).sequence
947
+ while (pb < a.length && rb < b.length) {
948
+ if (a[pb] !== b[rb]) {
949
+ return result
950
+ }
951
+ pb += 1
952
+ rb += 1
953
+ result += 1
954
+ }
955
+ if (pb === a.length) {
956
+ pi += 1
957
+ pb = 0
958
+ }
959
+ if (rb === b.length) {
960
+ ri += 1
961
+ rb = 0
962
+ }
963
+ }
964
+ return result
965
+ }
966
+
967
+ private suffixMatches(path: number[], ref: number[]) {
968
+ let result = 0
969
+ let pi = 0
970
+ let ri = 0
971
+ let pb = 0
972
+ let rb = 0
973
+ while (pi < path.length && ri < ref.length) {
974
+ const a = this.record(path[path.length - pi - 1]!).sequence
975
+ const b = this.record(ref[ref.length - ri - 1]!).sequence
976
+ while (pb < a.length && rb < b.length) {
977
+ if (a[a.length - pb - 1] !== b[b.length - rb - 1]) {
978
+ return result
979
+ }
980
+ pb += 1
981
+ rb += 1
982
+ result += 1
983
+ }
984
+ if (pb === a.length) {
985
+ pi += 1
986
+ pb = 0
987
+ }
988
+ if (rb === b.length) {
989
+ ri += 1
990
+ rb = 0
991
+ }
992
+ }
993
+ return result
994
+ }
995
+
996
+ private align(path: number[], ref: number[], edits: Edit[]) {
997
+ const pathLen = this.pathLen(path)
998
+ const refLen = this.pathLen(ref)
999
+ const prefix = this.prefixMatches(path, ref)
1000
+ let suffix = this.suffixMatches(path, ref)
1001
+ if (prefix + suffix > pathLen) {
1002
+ suffix = pathLen - prefix
1003
+ }
1004
+ if (prefix + suffix > refLen) {
1005
+ suffix = refLen - prefix
1006
+ }
1007
+ appendEdit(edits, 'M', prefix)
1008
+ const pathMiddle = pathLen - prefix - suffix
1009
+ const refMiddle = refLen - prefix - suffix
1010
+ if (pathMiddle === 0) {
1011
+ appendEdit(edits, 'D', refMiddle)
1012
+ } else if (refMiddle === 0) {
1013
+ appendEdit(edits, 'I', pathMiddle)
1014
+ } else {
1015
+ const mismatch = Math.min(pathMiddle, refMiddle)
1016
+ const mismatchIndel =
1017
+ 4 * mismatch +
1018
+ gapPenalty(pathMiddle - mismatch) +
1019
+ gapPenalty(refMiddle - mismatch)
1020
+ const insertionDeletion = gapPenalty(pathMiddle) + gapPenalty(refMiddle)
1021
+ if (mismatchIndel <= insertionDeletion) {
1022
+ appendEdit(edits, 'M', mismatch)
1023
+ appendEdit(edits, 'I', pathMiddle - mismatch)
1024
+ appendEdit(edits, 'D', refMiddle - mismatch)
1025
+ } else {
1026
+ appendEdit(edits, 'I', pathMiddle)
1027
+ appendEdit(edits, 'D', refMiddle)
1028
+ }
1029
+ }
1030
+ appendEdit(edits, 'M', suffix)
1031
+ }
1032
+
1033
+ private edits(pathIndex: number): Edit[] | undefined {
1034
+ const info = this.paths[pathIndex]
1035
+ if (this.refId === undefined || pathIndex === this.refId || !info) {
1036
+ return undefined
1037
+ }
1038
+ const ref = this.paths[this.refId]!.path
1039
+ const ordered = this.orderedMatches(info.path, ref)
1040
+ if (ordered) {
1041
+ this.stats.orderedAlignments += 1
1042
+ } else {
1043
+ this.stats.lcsAlignments += 1
1044
+ }
1045
+ const lcs =
1046
+ ordered ??
1047
+ weightedLcs(info.path, ref, handle => this.record(handle).sequenceLen)[0]
1048
+ const edits: Edit[] = []
1049
+ let pathOffset = 0
1050
+ let refOffset = 0
1051
+ for (const [nextPath, nextRef] of lcs) {
1052
+ this.align(
1053
+ info.path.slice(pathOffset, nextPath),
1054
+ ref.slice(refOffset, nextRef),
1055
+ edits,
1056
+ )
1057
+ appendEdit(edits, 'M', this.record(info.path[nextPath]!).sequenceLen)
1058
+ pathOffset = nextPath + 1
1059
+ refOffset = nextRef + 1
1060
+ }
1061
+ this.align(info.path.slice(pathOffset), ref.slice(refOffset), edits)
1062
+ return edits
1063
+ }
1064
+
1065
+ alignToRef(pathIndex: number) {
1066
+ return this.edits(pathIndex)
1067
+ ?.map(([op, len]) => `${len}${op}`)
1068
+ .join('')
1069
+ }
1070
+
1071
+ alignments(): HaplotypeAlignment[] {
1072
+ const reference = this.referenceInterval
1073
+ if (this.refId === undefined || !reference) {
1074
+ throw new Error('Alignments need a reference path')
1075
+ }
1076
+ const ref = this.paths[this.refId]!.path
1077
+ const refTotal = this.refPrefix(ref)[ref.length]!
1078
+ const result: HaplotypeAlignment[] = []
1079
+ this.paths.forEach((info, index) => {
1080
+ if (index === this.refId) {
1081
+ return
1082
+ }
1083
+ const edits = this.edits(index)!
1084
+ let first = 0
1085
+ let leading = 0
1086
+ while (first < edits.length && edits[first]![0] === 'D') {
1087
+ leading += edits[first]![1]
1088
+ first += 1
1089
+ }
1090
+ let last = edits.length
1091
+ let trailing = 0
1092
+ while (last > first && edits[last - 1]![0] === 'D') {
1093
+ trailing += edits[last - 1]![1]
1094
+ last -= 1
1095
+ }
1096
+ const identity = info.identity
1097
+ const strand =
1098
+ info.path.some(handle => isReverse(handle)) &&
1099
+ !info.path.some(handle => !isReverse(handle))
1100
+ ? '-'
1101
+ : '+'
1102
+ result.push({
1103
+ pathHandle: identity?.pathHandle,
1104
+ name: identity?.name,
1105
+ strand: identity
1106
+ ? identity.orientation === 'forward'
1107
+ ? '+'
1108
+ : '-'
1109
+ : strand,
1110
+ hapStart: identity
1111
+ ? identity.name.fragment + identity.hapStart
1112
+ : undefined,
1113
+ hapEnd: identity ? identity.name.fragment + identity.hapEnd : undefined,
1114
+ start: info.positions[0]!,
1115
+ refStart: reference.start + leading,
1116
+ refEnd: reference.start + refTotal - trailing,
1117
+ cigar: edits
1118
+ .slice(first, last)
1119
+ .map(([op, len]) => `${len}${op}`)
1120
+ .join(''),
1121
+ weight: info.weight,
1122
+ path: info.path,
1123
+ })
1124
+ })
1125
+ return result
1126
+ }
1127
+
1128
+ private canonicalEdges(id: number) {
1129
+ const edges: [number, number, number][] = []
1130
+ for (const orientation of ['forward', 'reverse'] as const) {
1131
+ const handle = encodeNode(id, orientation)
1132
+ for (const successor of this.record(handle).successors()) {
1133
+ if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
1134
+ edges.push([
1135
+ orientation === 'reverse' ? 1 : 0,
1136
+ nodeId(successor),
1137
+ isReverse(successor) ? 1 : 0,
1138
+ ])
1139
+ }
1140
+ }
1141
+ }
1142
+ edges.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2])
1143
+ return edges.filter(
1144
+ (edge, i) =>
1145
+ i === 0 ||
1146
+ edge[0] !== edges[i - 1]![0] ||
1147
+ edge[1] !== edges[i - 1]![1] ||
1148
+ edge[2] !== edges[i - 1]![2],
1149
+ )
1150
+ }
1151
+
1152
+ async stableName() {
1153
+ const encoder = new TextEncoder()
1154
+ const chunks: Uint8Array[] = []
1155
+ for (const handle of this.sortedHandles()) {
1156
+ if (!isReverse(handle)) {
1157
+ const id = nodeId(handle)
1158
+ let text = `S\t${id}\t${this.record(handle).sequence}\n`
1159
+ for (const [fromReverse, toId, toReverse] of this.canonicalEdges(id)) {
1160
+ text += `L\t${id}\t${fromReverse ? '-' : '+'}\t${toId}\t${toReverse ? '-' : '+'}\n`
1161
+ }
1162
+ chunks.push(encoder.encode(text))
1163
+ }
1164
+ }
1165
+ return sha256Hex(chunks)
1166
+ }
1167
+
1168
+ async toGFA(cigar: boolean, opts: ToJsonOptions = {}) {
1169
+ const lines = [
1170
+ this.refPath ? `H\tVN:Z:1.1\tRS:Z:${this.refPath.sample}` : 'H\tVN:Z:1.1',
1171
+ ...gfaHeaderLines(
1172
+ subgraphName(await this.stableName(), await this.db.graphName()),
1173
+ ),
1174
+ ]
1175
+ const handles = this.sortedHandles()
1176
+ for (const handle of handles) {
1177
+ if (!isReverse(handle)) {
1178
+ lines.push(`S\t${nodeId(handle)}\t${this.record(handle).sequence}`)
1179
+ }
1180
+ }
1181
+ const sign = (handle: number) => (isReverse(handle) ? '-' : '+')
1182
+ for (const handle of handles) {
1183
+ for (const successor of this.record(handle).successors()) {
1184
+ if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
1185
+ lines.push(
1186
+ `L\t${nodeId(handle)}\t${sign(handle)}\t${nodeId(successor)}\t${sign(successor)}\t0M`,
1187
+ )
1188
+ }
1189
+ }
1190
+ }
1191
+ const walk = (
1192
+ info: PathInfo,
1193
+ name: PathName,
1194
+ end: number,
1195
+ cigarString: string | undefined,
1196
+ ) => {
1197
+ const steps = info.path
1198
+ .map(handle => `${isReverse(handle) ? '<' : '>'}${nodeId(handle)}`)
1199
+ .join('')
1200
+ const weight = info.weight === undefined ? '' : `\tWT:i:${info.weight}`
1201
+ const cg = cigarString === undefined ? '' : `\tCG:Z:${cigarString}`
1202
+ return `W\t${name.sample}\t${name.haplotype}\t${name.contig}\t${name.fragment}\t${end}\t${steps}${weight}${cg}`
1203
+ }
1204
+ const contig = this.refPath?.contig ?? 'unknown'
1205
+ if (this.refId !== undefined && this.refPath && this.refInterval) {
1206
+ lines.push(
1207
+ walk(
1208
+ this.paths[this.refId]!,
1209
+ {
1210
+ ...this.refPath,
1211
+ fragment: this.refPath.fragment + this.refInterval[0],
1212
+ },
1213
+ this.refPath.fragment + this.refInterval[1],
1214
+ undefined,
1215
+ ),
1216
+ )
1217
+ }
1218
+ let haplotype = 1
1219
+ this.paths.forEach((info, index) => {
1220
+ if (index !== this.refId) {
1221
+ const resolved = opts.names === 'resolved' ? info.identity : undefined
1222
+ const cigarString = cigar ? this.alignToRef(index) : undefined
1223
+ lines.push(
1224
+ resolved
1225
+ ? walk(
1226
+ info,
1227
+ {
1228
+ ...resolved.name,
1229
+ fragment: resolved.name.fragment + resolved.hapStart,
1230
+ },
1231
+ resolved.name.fragment + resolved.hapEnd,
1232
+ cigarString,
1233
+ )
1234
+ : walk(
1235
+ info,
1236
+ { sample: 'unknown', contig, haplotype, fragment: 0 },
1237
+ info.len,
1238
+ cigarString,
1239
+ ),
1240
+ )
1241
+ haplotype += 1
1242
+ }
1243
+ })
1244
+ return `${lines.join('\n')}\n`
1245
+ }
1246
+
1247
+ toJSON(cigar: boolean, opts: ToJsonOptions = {}): SubgraphJson {
1248
+ const handles = this.sortedHandles()
1249
+ const nodes = handles
1250
+ .filter(handle => !isReverse(handle))
1251
+ .map(handle => ({
1252
+ id: String(nodeId(handle)),
1253
+ sequence: this.record(handle).sequence,
1254
+ }))
1255
+ const edges: SubgraphJson['edges'] = []
1256
+ for (const handle of handles) {
1257
+ for (const successor of this.record(handle).successors()) {
1258
+ if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
1259
+ edges.push({
1260
+ from: String(nodeId(handle)),
1261
+ from_is_reverse: isReverse(handle),
1262
+ to: String(nodeId(successor)),
1263
+ to_is_reverse: isReverse(successor),
1264
+ })
1265
+ }
1266
+ }
1267
+ }
1268
+ const paths: SubgraphPath[] = []
1269
+ const contig = this.refPath?.contig ?? 'unknown'
1270
+ if (this.refId !== undefined && this.refPath && this.refInterval) {
1271
+ const info = this.paths[this.refId]!
1272
+ const name = {
1273
+ ...this.refPath,
1274
+ fragment: this.refPath.fragment + this.refInterval[0],
1275
+ }
1276
+ paths.push(
1277
+ jsonPath(
1278
+ info,
1279
+ formatPathName(name, this.refPath.fragment + this.refInterval[1]),
1280
+ undefined,
1281
+ ),
1282
+ )
1283
+ }
1284
+ let haplotype = 1
1285
+ this.paths.forEach((info, index) => {
1286
+ if (index === this.refId) {
1287
+ return
1288
+ }
1289
+ const resolved = opts.names === 'resolved' ? info.identity : undefined
1290
+ const name = resolved
1291
+ ? formatPathName(
1292
+ {
1293
+ ...resolved.name,
1294
+ fragment: resolved.name.fragment + resolved.hapStart,
1295
+ },
1296
+ resolved.name.fragment + resolved.hapEnd,
1297
+ )
1298
+ : formatPathName(
1299
+ { sample: 'unknown', contig, haplotype, fragment: 0 },
1300
+ info.len,
1301
+ )
1302
+ paths.push(
1303
+ jsonPath(info, name, cigar ? this.alignToRef(index) : undefined),
1304
+ )
1305
+ haplotype += 1
1306
+ })
1307
+ return { nodes, edges, paths }
1308
+ }
1309
+ }
1310
+
1311
+ function jsonPath(
1312
+ info: PathInfo,
1313
+ name: string,
1314
+ cigar: string | undefined,
1315
+ ): SubgraphPath {
1316
+ return {
1317
+ name,
1318
+ ...(info.weight === undefined ? {} : { weight: info.weight }),
1319
+ ...(cigar === undefined ? {} : { cigar }),
1320
+ path: info.path.map(handle => ({
1321
+ id: String(nodeId(handle)),
1322
+ is_reverse: isReverse(handle),
1323
+ })),
1324
+ }
1325
+ }
1326
+
1327
+ function comparePaths(a: number[], b: number[]) {
1328
+ const n = Math.min(a.length, b.length)
1329
+ for (let i = 0; i < n; i++) {
1330
+ const x = a[i]!
1331
+ const y = b[i]!
1332
+ if (x !== y) {
1333
+ return x < y ? -1 : 1
1334
+ }
1335
+ }
1336
+ return a.length - b.length
1337
+ }
1338
+
1339
+ function appendEdit(edits: Edit[], op: EditOp, len: number) {
1340
+ if (len === 0) {
1341
+ return
1342
+ }
1343
+ const last = edits[edits.length - 1]
1344
+ if (last?.[0] === op) {
1345
+ last[1] += len
1346
+ } else {
1347
+ edits.push([op, len])
1348
+ }
1349
+ }
1350
+
1351
+ function gapPenalty(len: number) {
1352
+ return len === 0 ? 0 : 6 + (len - 1)
1353
+ }