@gmod/gbz-base 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +3 -3
  2. package/bin/query.js +3 -1
  3. package/dist/cli.js +26 -7
  4. package/dist/cli.js.map +1 -0
  5. package/dist/db.d.ts +5 -5
  6. package/dist/db.js +35 -14
  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.js +8 -2
  16. package/dist/gbwt/record.js.map +1 -0
  17. package/dist/gbwt/sequence.js +8 -1
  18. package/dist/gbwt/sequence.js.map +1 -0
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +3 -2
  21. package/dist/index.js.map +1 -0
  22. package/dist/lcs.js +32 -10
  23. package/dist/lcs.js.map +1 -0
  24. package/dist/query.d.ts +1 -1
  25. package/dist/query.js +1 -0
  26. package/dist/query.js.map +1 -0
  27. package/dist/sqlite/btree.d.ts +1 -1
  28. package/dist/sqlite/btree.js +17 -5
  29. package/dist/sqlite/btree.js.map +1 -0
  30. package/dist/sqlite/database.d.ts +1 -1
  31. package/dist/sqlite/database.js +12 -2
  32. package/dist/sqlite/database.js.map +1 -0
  33. package/dist/sqlite/pager.d.ts +3 -3
  34. package/dist/sqlite/pager.js +6 -4
  35. package/dist/sqlite/pager.js.map +1 -0
  36. package/dist/sqlite/record.js +3 -1
  37. package/dist/sqlite/record.js.map +1 -0
  38. package/dist/subgraph.d.ts +2 -2
  39. package/dist/subgraph.js +116 -29
  40. package/dist/subgraph.js.map +1 -0
  41. package/package.json +36 -11
  42. package/src/cli.ts +203 -0
  43. package/src/db.ts +322 -0
  44. package/src/filehandle.ts +4 -0
  45. package/src/gbwt/bytecode.ts +87 -0
  46. package/src/gbwt/node.ts +64 -0
  47. package/src/gbwt/record.ts +154 -0
  48. package/src/gbwt/sequence.ts +50 -0
  49. package/src/index.ts +31 -0
  50. package/src/lcs.ts +282 -0
  51. package/src/query.ts +79 -0
  52. package/src/sqlite/btree.ts +255 -0
  53. package/src/sqlite/database.ts +117 -0
  54. package/src/sqlite/pager.ts +78 -0
  55. package/src/sqlite/record.ts +100 -0
  56. package/src/subgraph.ts +1066 -0
@@ -0,0 +1,1066 @@
1
+ import { formatPathName } from './db.ts'
2
+ import {
3
+ ENDMARKER,
4
+ edgeIsCanonical,
5
+ encodeNode,
6
+ entryOrientation,
7
+ entrySide,
8
+ exitOrientation,
9
+ exitSide,
10
+ flipSide,
11
+ isReverse,
12
+ nodeId,
13
+ nodeOrientation,
14
+ pathIsCanonical,
15
+ } from './gbwt/node.ts'
16
+ import { weightedLcs } from './lcs.ts'
17
+
18
+ import type {
19
+ GBZBase,
20
+ GbzPath,
21
+ GbzRecord,
22
+ HaplotypeSample,
23
+ PathName,
24
+ } from './db.ts'
25
+ import type { NodeSide, Orientation } from './gbwt/node.ts'
26
+ import type { Pos } from './gbwt/record.ts'
27
+
28
+ export type HaplotypeOutput = 'all' | 'distinct' | 'reference-only' | 'none'
29
+
30
+ export interface PathPosition {
31
+ seqOffset: number
32
+ handle: number
33
+ nodeOffset: number
34
+ gbwtOffset: number
35
+ }
36
+
37
+ export interface ReferencePath {
38
+ position: PathPosition
39
+ name: PathName
40
+ handle: number
41
+ }
42
+
43
+ export interface PathIdentity {
44
+ pathHandle: number
45
+ name: PathName
46
+ orientation: Orientation
47
+ hapStart: number
48
+ hapEnd: number
49
+ }
50
+
51
+ interface PathInfo {
52
+ path: number[]
53
+ positions: Pos[]
54
+ len: number
55
+ weight: number | undefined
56
+ identity: PathIdentity | undefined
57
+ }
58
+
59
+ type EditOp = 'M' | 'I' | 'D'
60
+ type Edit = [EditOp, number]
61
+
62
+ export interface SubgraphPath {
63
+ name: string
64
+ weight?: number
65
+ cigar?: string
66
+ path: { id: string; is_reverse: boolean }[]
67
+ }
68
+
69
+ export interface SubgraphJson {
70
+ nodes: { id: string; sequence: string }[]
71
+ edges: {
72
+ from: string
73
+ from_is_reverse: boolean
74
+ to: string
75
+ to_is_reverse: boolean
76
+ }[]
77
+ paths: SubgraphPath[]
78
+ }
79
+
80
+ export interface HaplotypeAlignment {
81
+ pathHandle: number | undefined
82
+ name: PathName | undefined
83
+ strand: '+' | '-'
84
+ hapStart: number | undefined
85
+ hapEnd: number | undefined
86
+ refStart: number
87
+ refEnd: number
88
+ cigar: string
89
+ weight: number | undefined
90
+ path: number[]
91
+ start: Pos
92
+ }
93
+
94
+ export interface ToJsonOptions {
95
+ names?: 'anonymous' | 'resolved'
96
+ }
97
+
98
+ class SideQueue {
99
+ private items: [number, number, NodeSide][] = []
100
+
101
+ push(distance: number, node: number, side: NodeSide) {
102
+ this.items.push([distance, node, side])
103
+ }
104
+
105
+ pop() {
106
+ let best = 0
107
+ for (let i = 1; i < this.items.length; i++) {
108
+ const a = this.items[i]!
109
+ const b = this.items[best]!
110
+ if (
111
+ a[0] < b[0] ||
112
+ (a[0] === b[0] && (a[1] < b[1] || (a[1] === b[1] && a[2] < b[2])))
113
+ ) {
114
+ best = i
115
+ }
116
+ }
117
+ const [item] = this.items.splice(best, 1)
118
+ return item
119
+ }
120
+
121
+ get size() {
122
+ return this.items.length
123
+ }
124
+ }
125
+
126
+ function posKey(pos: Pos) {
127
+ return `${pos.node}:${pos.offset}`
128
+ }
129
+
130
+ interface Anchor {
131
+ pathHandle: number
132
+ orientation: Orientation
133
+ base: number
134
+ }
135
+
136
+ export class Subgraph {
137
+ private records = new Map<number, GbzRecord>()
138
+ private paths: PathInfo[] = []
139
+ private refId: number | undefined
140
+ private refPath: PathName | undefined
141
+ private refHandle: number | undefined
142
+ private refInterval: [number, number] | undefined
143
+ private refIndexCache: Map<number, number[]> | undefined
144
+ private refPrefixCache: number[] | undefined
145
+ limit: number | undefined
146
+ readonly stats = {
147
+ orderedAlignments: 0,
148
+ lcsAlignments: 0,
149
+ identificationSteps: 0,
150
+ identificationFetches: 0,
151
+ }
152
+
153
+ private db: GBZBase
154
+
155
+ constructor(db: GBZBase) {
156
+ this.db = db
157
+ }
158
+
159
+ get nodeCount() {
160
+ return this.records.size / 2
161
+ }
162
+
163
+ get pathCount() {
164
+ return this.paths.length
165
+ }
166
+
167
+ get referenceInterval() {
168
+ return this.refInterval && this.refPath
169
+ ? {
170
+ name: this.refPath,
171
+ start: this.refPath.fragment + this.refInterval[0],
172
+ end: this.refPath.fragment + this.refInterval[1],
173
+ }
174
+ : undefined
175
+ }
176
+
177
+ hasNode(id: number) {
178
+ return this.records.has(encodeNode(id, 'forward'))
179
+ }
180
+
181
+ hasHandle(handle: number) {
182
+ return this.records.has(handle)
183
+ }
184
+
185
+ private record(handle: number) {
186
+ const record = this.records.get(handle)
187
+ if (!record) {
188
+ throw new Error(`Subgraph has no record for handle ${handle}`)
189
+ }
190
+ return record
191
+ }
192
+
193
+ private sortedHandles() {
194
+ return [...this.records.keys()].sort((a, b) => a - b)
195
+ }
196
+
197
+ private async addNode(id: number) {
198
+ if (this.limit !== undefined && this.nodeCount >= this.limit) {
199
+ throw new Error(`Subgraph size limit of ${this.limit} nodes exceeded`)
200
+ }
201
+ const forward = await this.db.getRecord(encodeNode(id, 'forward'))
202
+ const reverse = await this.db.getRecord(encodeNode(id, 'reverse'))
203
+ if (!forward || !reverse) {
204
+ throw new Error(`Node ${id} does not exist in the graph`)
205
+ }
206
+ this.records.set(forward.handle, forward)
207
+ this.records.set(reverse.handle, reverse)
208
+ }
209
+
210
+ private async ensureNode(id: number) {
211
+ if (!this.hasNode(id)) {
212
+ await this.addNode(id)
213
+ }
214
+ }
215
+
216
+ private clearPaths() {
217
+ this.paths = []
218
+ this.refId = undefined
219
+ this.refPath = undefined
220
+ this.refHandle = undefined
221
+ this.refInterval = undefined
222
+ this.refIndexCache = undefined
223
+ this.refPrefixCache = undefined
224
+ }
225
+
226
+ async pathPosition(query: PathName): Promise<ReferencePath> {
227
+ const path = await this.db.findPath(query)
228
+ if (!path) {
229
+ throw new Error(
230
+ `Cannot find a path covering ${formatPathName(query, query.fragment)}`,
231
+ )
232
+ }
233
+ if (!path.isIndexed) {
234
+ throw new Error(
235
+ `Path ${formatPathName(path.name, path.name.fragment)} has not been indexed for random access`,
236
+ )
237
+ }
238
+ const queryOffset = query.fragment - path.name.fragment
239
+ const indexed = await this.db.indexedPosition(path.handle, queryOffset)
240
+ if (!indexed) {
241
+ throw new Error(
242
+ `Path ${formatPathName(path.name, path.name.fragment)} has not been indexed for random access`,
243
+ )
244
+ }
245
+ return this.findPathPosition(
246
+ path,
247
+ queryOffset,
248
+ indexed.pathOffset,
249
+ indexed.pos,
250
+ )
251
+ }
252
+
253
+ private async findPathPosition(
254
+ path: GbzPath,
255
+ queryOffset: number,
256
+ startOffset: number,
257
+ start: Pos,
258
+ ): Promise<ReferencePath> {
259
+ let pathOffset = startOffset
260
+ let pos = start
261
+ for (;;) {
262
+ await this.ensureNode(nodeId(pos.node))
263
+ const record = this.record(pos.node)
264
+ if (pathOffset + record.sequenceLen > queryOffset) {
265
+ return {
266
+ position: {
267
+ seqOffset: queryOffset,
268
+ handle: pos.node,
269
+ nodeOffset: queryOffset - pathOffset,
270
+ gbwtOffset: pos.offset,
271
+ },
272
+ name: path.name,
273
+ handle: path.handle,
274
+ }
275
+ }
276
+ pathOffset += record.sequenceLen
277
+ const next = record.gbwt().lf(pos.offset)
278
+ if (!next) {
279
+ throw new Error(
280
+ `Path ${formatPathName(path.name, path.name.fragment)} does not contain offset ${queryOffset}`,
281
+ )
282
+ }
283
+ pos = next
284
+ }
285
+ }
286
+
287
+ async aroundPosition(handle: number, nodeOffset: number, context: number) {
288
+ const id = nodeId(handle)
289
+ await this.ensureNode(id)
290
+ const record = this.record(handle)
291
+ const orientation = nodeOrientation(handle)
292
+ const active = new SideQueue()
293
+ active.push(nodeOffset, id, entrySide(orientation))
294
+ active.push(record.sequenceLen - nodeOffset - 1, id, exitSide(orientation))
295
+ return this.insertContext(active, context)
296
+ }
297
+
298
+ async aroundInterval(start: PathPosition, len: number, context: number) {
299
+ if (len === 0) {
300
+ throw new Error('Interval length must be greater than 0')
301
+ }
302
+ let pos: Pos = { node: start.handle, offset: start.gbwtOffset }
303
+ let offset = start.nodeOffset
304
+ let remaining = len
305
+ const active = new SideQueue()
306
+ for (;;) {
307
+ const id = nodeId(pos.node)
308
+ const orientation = nodeOrientation(pos.node)
309
+ await this.ensureNode(id)
310
+ const record = this.record(pos.node)
311
+ if (offset >= record.sequenceLen) {
312
+ throw new Error(
313
+ `Offset ${offset} in node ${id} of length ${record.sequenceLen}`,
314
+ )
315
+ }
316
+ active.push(offset, id, entrySide(orientation))
317
+ const distanceToNext = record.sequenceLen - offset
318
+ if (remaining <= distanceToNext) {
319
+ active.push(
320
+ remaining === distanceToNext ? 0 : distanceToNext - remaining - 1,
321
+ id,
322
+ exitSide(orientation),
323
+ )
324
+ break
325
+ }
326
+ active.push(0, id, exitSide(orientation))
327
+ const next = record.gbwt().lf(pos.offset)
328
+ if (!next) {
329
+ throw new Error(
330
+ `No successor for GBWT position (${pos.node}, ${pos.offset})`,
331
+ )
332
+ }
333
+ pos = next
334
+ offset = 0
335
+ remaining -= distanceToNext
336
+ }
337
+ return this.insertContext(active, context)
338
+ }
339
+
340
+ async aroundNodes(nodes: Iterable<number>, context: number) {
341
+ const active = new SideQueue()
342
+ for (const id of nodes) {
343
+ await this.ensureNode(id)
344
+ active.push(0, id, 'left')
345
+ active.push(0, id, 'right')
346
+ }
347
+ return this.insertContext(active, context)
348
+ }
349
+
350
+ private async insertContext(active: SideQueue, context: number) {
351
+ this.clearPaths()
352
+ const visited = new Set<string>()
353
+ const toRemove = new Set<number>()
354
+ for (const handle of this.records.keys()) {
355
+ toRemove.add(nodeId(handle))
356
+ }
357
+ let inserted = 0
358
+ while (active.size > 0) {
359
+ const [distance, id, side] = active.pop()!
360
+ const key = `${id}:${side}`
361
+ if (visited.has(key)) {
362
+ continue
363
+ }
364
+ visited.add(key)
365
+ toRemove.delete(id)
366
+ if (!this.hasNode(id)) {
367
+ await this.addNode(id)
368
+ inserted += 1
369
+ }
370
+ const otherSide = flipSide(side)
371
+ if (!visited.has(`${id}:${otherSide}`)) {
372
+ const record = this.record(encodeNode(id, entryOrientation(side)))
373
+ const nextDistance = distance + record.sequenceLen - 1
374
+ if (nextDistance <= context) {
375
+ active.push(nextDistance, id, otherSide)
376
+ }
377
+ }
378
+ const record = this.record(encodeNode(id, exitOrientation(side)))
379
+ const nextDistance = distance + 1
380
+ if (nextDistance <= context) {
381
+ for (const successor of record.successors()) {
382
+ const successorId = nodeId(successor)
383
+ const successorSide = entrySide(nodeOrientation(successor))
384
+ if (!visited.has(`${successorId}:${successorSide}`)) {
385
+ active.push(nextDistance, successorId, successorSide)
386
+ }
387
+ }
388
+ }
389
+ }
390
+ for (const id of toRemove) {
391
+ this.records.delete(encodeNode(id, 'forward'))
392
+ this.records.delete(encodeNode(id, 'reverse'))
393
+ }
394
+ return { inserted, removed: toRemove.size }
395
+ }
396
+
397
+ extractPaths(reference: ReferencePath | undefined, output: HaplotypeOutput) {
398
+ this.clearPaths()
399
+ if (output === 'none') {
400
+ return
401
+ }
402
+ const refPos = reference?.position
403
+ this.refPath = reference?.name
404
+ this.refHandle = reference?.handle
405
+ const handles = this.sortedHandles()
406
+ const successors = new Map<
407
+ number,
408
+ { next: Pos; hasPredecessor: boolean }[]
409
+ >()
410
+ for (const handle of handles) {
411
+ successors.set(
412
+ handle,
413
+ this.record(handle)
414
+ .gbwt()
415
+ .decompress()
416
+ .map(next => ({ next, hasPredecessor: false })),
417
+ )
418
+ }
419
+ for (const handle of handles) {
420
+ for (const { next } of successors.get(handle) as { next: Pos }[]) {
421
+ const entry = successors.get(next.node)?.[next.offset]
422
+ if (entry) {
423
+ entry.hasPredecessor = true
424
+ }
425
+ }
426
+ }
427
+ let refOffset: number | undefined
428
+ for (const handle of handles) {
429
+ const entries = successors.get(handle) as {
430
+ next: Pos
431
+ hasPredecessor: boolean
432
+ }[]
433
+ entries.forEach((entry, offset) => {
434
+ if (entry.hasPredecessor) {
435
+ return
436
+ }
437
+ let curr: Pos | undefined = { node: handle, offset }
438
+ let isRef = false
439
+ const path: number[] = []
440
+ const positions: Pos[] = []
441
+ let len = 0
442
+ while (curr) {
443
+ if (
444
+ curr.node === refPos?.handle &&
445
+ curr.offset === refPos.gbwtOffset
446
+ ) {
447
+ this.refId = this.paths.length
448
+ refOffset = path.length
449
+ isRef = true
450
+ }
451
+ path.push(curr.node)
452
+ positions.push(curr)
453
+ len += this.record(curr.node).sequenceLen
454
+ const step: { next: Pos } | undefined = successors.get(curr.node)?.[
455
+ curr.offset
456
+ ]
457
+ curr =
458
+ step &&
459
+ step.next.node !== ENDMARKER &&
460
+ successors.has(step.next.node)
461
+ ? step.next
462
+ : undefined
463
+ }
464
+ if (isRef || pathIsCanonical(path)) {
465
+ this.paths.push({
466
+ path,
467
+ positions,
468
+ len,
469
+ weight: undefined,
470
+ identity: undefined,
471
+ })
472
+ }
473
+ })
474
+ }
475
+ if (refPos) {
476
+ if (refOffset === undefined || this.refId === undefined) {
477
+ this.clearPaths()
478
+ throw new Error('Could not find the reference path')
479
+ }
480
+ const info = this.paths[this.refId]!
481
+ let before = refPos.nodeOffset
482
+ for (const handle of info.path.slice(0, refOffset)) {
483
+ before += this.record(handle).sequenceLen
484
+ }
485
+ const start = refPos.seqOffset - before
486
+ this.refInterval = [start, start + info.len]
487
+ info.identity = {
488
+ pathHandle: reference.handle,
489
+ name: reference.name,
490
+ orientation: 'forward',
491
+ hapStart: start,
492
+ hapEnd: start + info.len,
493
+ }
494
+ }
495
+ if (output === 'distinct') {
496
+ this.distinctPaths()
497
+ } else if (output === 'reference-only') {
498
+ if (this.refId === undefined) {
499
+ throw new Error('Reference path is required for reference-only output')
500
+ }
501
+ this.paths = [this.paths[this.refId]!]
502
+ this.refId = 0
503
+ }
504
+ }
505
+
506
+ private distinctPaths() {
507
+ const refPath =
508
+ this.refId === undefined ? undefined : this.paths[this.refId]!.path
509
+ this.paths.sort((x, y) => comparePaths(x.path, y.path) || x.len - y.len)
510
+ const merged: PathInfo[] = []
511
+ let refId: number | undefined
512
+ for (const info of this.paths) {
513
+ const last = merged[merged.length - 1]
514
+ if (last && comparePaths(last.path, info.path) === 0) {
515
+ last.weight = (last.weight ?? 0) + 1
516
+ } else {
517
+ if (refPath && comparePaths(info.path, refPath) === 0) {
518
+ refId = merged.length
519
+ }
520
+ merged.push({ ...info, weight: 1 })
521
+ }
522
+ }
523
+ this.paths = merged
524
+ this.refId = refId
525
+ }
526
+
527
+ async identifyPaths() {
528
+ if (!this.db.hasHaplotypeIndex) {
529
+ throw new Error(
530
+ 'The database has no HaplotypeSamples table; run gbz-haplotype-index on it',
531
+ )
532
+ }
533
+ const interval = (await this.db.haplotypeSampleInterval()) ?? 4096
534
+ const handles = this.sortedHandles()
535
+ const minHandle = handles[0]
536
+ const maxHandle = handles[handles.length - 1]
537
+ if (minHandle === undefined || maxHandle === undefined) {
538
+ return
539
+ }
540
+ const samples = new Map<string, HaplotypeSample>()
541
+ for (const sample of await this.db.haplotypeSamplesInRange(
542
+ minHandle,
543
+ maxHandle,
544
+ )) {
545
+ samples.set(posKey(sample), sample)
546
+ }
547
+ const starts = new Map<string, number>()
548
+ this.paths.forEach((info, index) => {
549
+ const first = info.positions[0]
550
+ if (first && index !== this.refId) {
551
+ starts.set(posKey(first), index)
552
+ }
553
+ })
554
+ const outside = new Map<number, GbzRecord>()
555
+ const recordAt = async (handle: number) => {
556
+ const inside = this.records.get(handle)
557
+ if (inside) {
558
+ return inside
559
+ }
560
+ let record = outside.get(handle)
561
+ if (!record) {
562
+ record = await this.db.getRecord(handle)
563
+ this.stats.identificationFetches += 1
564
+ if (!record) {
565
+ throw new Error(`Node record ${handle} is missing from the database`)
566
+ }
567
+ outside.set(handle, record)
568
+ }
569
+ return record
570
+ }
571
+ const sampleAt = async (pos: Pos) => {
572
+ if (pos.node >= minHandle && pos.node <= maxHandle) {
573
+ return samples.get(posKey(pos))
574
+ }
575
+ this.stats.identificationFetches += 1
576
+ return this.db.haplotypeSampleAt(pos.node, pos.offset)
577
+ }
578
+ const names = new Map<number, PathName>()
579
+ const nameOf = async (pathHandle: number) => {
580
+ let name = names.get(pathHandle)
581
+ if (!name) {
582
+ const path = await this.db.getPath(pathHandle)
583
+ if (!path) {
584
+ throw new Error(`Path ${pathHandle} is missing from the database`)
585
+ }
586
+ name = path.name
587
+ names.set(pathHandle, name)
588
+ }
589
+ return name
590
+ }
591
+ const anchorFromSample = (
592
+ sample: HaplotypeSample,
593
+ counter: number,
594
+ nodeLen: number,
595
+ ): Anchor =>
596
+ sample.orientation === 'forward'
597
+ ? {
598
+ pathHandle: sample.pathHandle,
599
+ orientation: 'forward',
600
+ base: sample.pathOffset - counter,
601
+ }
602
+ : {
603
+ pathHandle: sample.pathHandle,
604
+ orientation: 'reverse',
605
+ base: sample.pathOffset + counter + nodeLen,
606
+ }
607
+ const anchorFromIdentity = (
608
+ identity: PathIdentity,
609
+ counter: number,
610
+ ): Anchor =>
611
+ identity.orientation === 'forward'
612
+ ? {
613
+ pathHandle: identity.pathHandle,
614
+ orientation: 'forward',
615
+ base: identity.hapStart - counter,
616
+ }
617
+ : {
618
+ pathHandle: identity.pathHandle,
619
+ orientation: 'reverse',
620
+ base: identity.hapEnd + counter,
621
+ }
622
+
623
+ for (let start = 0; start < this.paths.length; start++) {
624
+ const startInfo = this.paths[start]!
625
+ if (start === this.refId || startInfo.identity) {
626
+ continue
627
+ }
628
+ const chain: { index: number; startBp: number }[] = []
629
+ const visited = new Set<number>()
630
+ let anchor: Anchor | undefined
631
+ let counter = 0
632
+ let current: number | undefined = start
633
+ let pos: Pos | undefined
634
+ while (anchor === undefined) {
635
+ if (current !== undefined) {
636
+ if (visited.has(current)) {
637
+ break
638
+ }
639
+ visited.add(current)
640
+ const info = this.paths[current]!
641
+ chain.push({ index: current, startBp: counter })
642
+ let bp = counter
643
+ for (const position of info.positions) {
644
+ const sample = samples.get(posKey(position))
645
+ const nodeLen = this.record(position.node).sequenceLen
646
+ if (sample) {
647
+ anchor = anchorFromSample(sample, bp, nodeLen)
648
+ break
649
+ }
650
+ bp += nodeLen
651
+ }
652
+ counter += info.len
653
+ if (anchor) {
654
+ break
655
+ }
656
+ const last = info.positions[info.positions.length - 1]!
657
+ pos = this.record(last.node).gbwt().lf(last.offset)
658
+ current = undefined
659
+ }
660
+ if (pos === undefined || pos.node === ENDMARKER) {
661
+ break
662
+ }
663
+ const known = starts.get(posKey(pos))
664
+ if (known !== undefined) {
665
+ const identity = this.paths[known]!.identity
666
+ if (identity) {
667
+ anchor = anchorFromIdentity(identity, counter)
668
+ break
669
+ }
670
+ current = known
671
+ continue
672
+ }
673
+ const sample = await sampleAt(pos)
674
+ const record = await recordAt(pos.node)
675
+ if (sample) {
676
+ anchor = anchorFromSample(sample, counter, record.sequenceLen)
677
+ break
678
+ }
679
+ this.stats.identificationSteps += 1
680
+ if (
681
+ counter - (chain[chain.length - 1] as { startBp: number }).startBp >
682
+ 4 * interval + 4 * record.sequenceLen
683
+ ) {
684
+ break
685
+ }
686
+ counter += record.sequenceLen
687
+ pos = record.gbwt().lf(pos.offset)
688
+ }
689
+ if (anchor) {
690
+ const name = await nameOf(anchor.pathHandle)
691
+ for (const { index, startBp } of chain) {
692
+ const info = this.paths[index]!
693
+ info.identity =
694
+ anchor.orientation === 'forward'
695
+ ? {
696
+ pathHandle: anchor.pathHandle,
697
+ name,
698
+ orientation: 'forward',
699
+ hapStart: anchor.base + startBp,
700
+ hapEnd: anchor.base + startBp + info.len,
701
+ }
702
+ : {
703
+ pathHandle: anchor.pathHandle,
704
+ name,
705
+ orientation: 'reverse',
706
+ hapStart: anchor.base - startBp - info.len,
707
+ hapEnd: anchor.base - startBp,
708
+ }
709
+ }
710
+ }
711
+ }
712
+ }
713
+
714
+ private refIndex(ref: number[]) {
715
+ if (this.refIndexCache === undefined) {
716
+ const index = new Map<number, number[]>()
717
+ ref.forEach((handle, i) => {
718
+ const occurrences = index.get(handle)
719
+ if (occurrences) {
720
+ occurrences.push(i)
721
+ } else {
722
+ index.set(handle, [i])
723
+ }
724
+ })
725
+ this.refIndexCache = index
726
+ }
727
+ return this.refIndexCache
728
+ }
729
+
730
+ private refPrefix(ref: number[]) {
731
+ if (this.refPrefixCache === undefined) {
732
+ const prefix = [0]
733
+ ref.forEach((handle, i) => {
734
+ prefix.push(prefix[i]! + this.record(handle).sequenceLen)
735
+ })
736
+ this.refPrefixCache = prefix
737
+ }
738
+ return this.refPrefixCache
739
+ }
740
+
741
+ private orderedMatches(
742
+ path: number[],
743
+ ref: number[],
744
+ ): [number, number][] | undefined {
745
+ const index = this.refIndex(ref)
746
+ const pairs: [number, number][] = []
747
+ let last = -1
748
+ for (let i = 0; i < path.length; i++) {
749
+ const occurrences = index.get(path[i]!)
750
+ if (occurrences) {
751
+ const j = occurrences.find(x => x > last)
752
+ if (j === undefined) {
753
+ return undefined
754
+ }
755
+ pairs.push([i, j])
756
+ last = j
757
+ }
758
+ }
759
+ return pairs
760
+ }
761
+
762
+ private pathLen(path: number[]) {
763
+ let total = 0
764
+ for (const handle of path) {
765
+ total += this.record(handle).sequenceLen
766
+ }
767
+ return total
768
+ }
769
+
770
+ private prefixMatches(path: number[], ref: number[]) {
771
+ let result = 0
772
+ let pi = 0
773
+ let ri = 0
774
+ let pb = 0
775
+ let rb = 0
776
+ while (pi < path.length && ri < ref.length) {
777
+ const a = this.record(path[pi]!).sequence
778
+ const b = this.record(ref[ri]!).sequence
779
+ while (pb < a.length && rb < b.length) {
780
+ if (a[pb] !== b[rb]) {
781
+ return result
782
+ }
783
+ pb += 1
784
+ rb += 1
785
+ result += 1
786
+ }
787
+ if (pb === a.length) {
788
+ pi += 1
789
+ pb = 0
790
+ }
791
+ if (rb === b.length) {
792
+ ri += 1
793
+ rb = 0
794
+ }
795
+ }
796
+ return result
797
+ }
798
+
799
+ private suffixMatches(path: number[], ref: number[]) {
800
+ let result = 0
801
+ let pi = 0
802
+ let ri = 0
803
+ let pb = 0
804
+ let rb = 0
805
+ while (pi < path.length && ri < ref.length) {
806
+ const a = this.record(path[path.length - pi - 1]!).sequence
807
+ const b = this.record(ref[ref.length - ri - 1]!).sequence
808
+ while (pb < a.length && rb < b.length) {
809
+ if (a[a.length - pb - 1] !== b[b.length - rb - 1]) {
810
+ return result
811
+ }
812
+ pb += 1
813
+ rb += 1
814
+ result += 1
815
+ }
816
+ if (pb === a.length) {
817
+ pi += 1
818
+ pb = 0
819
+ }
820
+ if (rb === b.length) {
821
+ ri += 1
822
+ rb = 0
823
+ }
824
+ }
825
+ return result
826
+ }
827
+
828
+ private align(path: number[], ref: number[], edits: Edit[]) {
829
+ const pathLen = this.pathLen(path)
830
+ const refLen = this.pathLen(ref)
831
+ const prefix = this.prefixMatches(path, ref)
832
+ let suffix = this.suffixMatches(path, ref)
833
+ if (prefix + suffix > pathLen) {
834
+ suffix = pathLen - prefix
835
+ }
836
+ if (prefix + suffix > refLen) {
837
+ suffix = refLen - prefix
838
+ }
839
+ appendEdit(edits, 'M', prefix)
840
+ const pathMiddle = pathLen - prefix - suffix
841
+ const refMiddle = refLen - prefix - suffix
842
+ if (pathMiddle === 0) {
843
+ appendEdit(edits, 'D', refMiddle)
844
+ } else if (refMiddle === 0) {
845
+ appendEdit(edits, 'I', pathMiddle)
846
+ } else {
847
+ const mismatch = Math.min(pathMiddle, refMiddle)
848
+ const mismatchIndel =
849
+ 4 * mismatch +
850
+ gapPenalty(pathMiddle - mismatch) +
851
+ gapPenalty(refMiddle - mismatch)
852
+ const insertionDeletion = gapPenalty(pathMiddle) + gapPenalty(refMiddle)
853
+ if (mismatchIndel <= insertionDeletion) {
854
+ appendEdit(edits, 'M', mismatch)
855
+ appendEdit(edits, 'I', pathMiddle - mismatch)
856
+ appendEdit(edits, 'D', refMiddle - mismatch)
857
+ } else {
858
+ appendEdit(edits, 'I', pathMiddle)
859
+ appendEdit(edits, 'D', refMiddle)
860
+ }
861
+ }
862
+ appendEdit(edits, 'M', suffix)
863
+ }
864
+
865
+ private edits(pathIndex: number): Edit[] | undefined {
866
+ const info = this.paths[pathIndex]
867
+ if (this.refId === undefined || pathIndex === this.refId || !info) {
868
+ return undefined
869
+ }
870
+ const ref = this.paths[this.refId]!.path
871
+ const ordered = this.orderedMatches(info.path, ref)
872
+ if (ordered) {
873
+ this.stats.orderedAlignments += 1
874
+ } else {
875
+ this.stats.lcsAlignments += 1
876
+ }
877
+ const lcs =
878
+ ordered ??
879
+ weightedLcs(info.path, ref, handle => this.record(handle).sequenceLen)[0]
880
+ const edits: Edit[] = []
881
+ let pathOffset = 0
882
+ let refOffset = 0
883
+ for (const [nextPath, nextRef] of lcs) {
884
+ this.align(
885
+ info.path.slice(pathOffset, nextPath),
886
+ ref.slice(refOffset, nextRef),
887
+ edits,
888
+ )
889
+ appendEdit(edits, 'M', this.record(info.path[nextPath]!).sequenceLen)
890
+ pathOffset = nextPath + 1
891
+ refOffset = nextRef + 1
892
+ }
893
+ this.align(info.path.slice(pathOffset), ref.slice(refOffset), edits)
894
+ return edits
895
+ }
896
+
897
+ alignToRef(pathIndex: number) {
898
+ return this.edits(pathIndex)
899
+ ?.map(([op, len]) => `${len}${op}`)
900
+ .join('')
901
+ }
902
+
903
+ alignments(): HaplotypeAlignment[] {
904
+ const reference = this.referenceInterval
905
+ if (this.refId === undefined || !reference) {
906
+ throw new Error('Alignments need a reference path')
907
+ }
908
+ const ref = this.paths[this.refId]!.path
909
+ const refTotal = this.refPrefix(ref)[ref.length]!
910
+ const result: HaplotypeAlignment[] = []
911
+ this.paths.forEach((info, index) => {
912
+ if (index === this.refId) {
913
+ return
914
+ }
915
+ const edits = this.edits(index)!
916
+ let first = 0
917
+ let leading = 0
918
+ while (first < edits.length && edits[first]![0] === 'D') {
919
+ leading += edits[first]![1]
920
+ first += 1
921
+ }
922
+ let last = edits.length
923
+ let trailing = 0
924
+ while (last > first && edits[last - 1]![0] === 'D') {
925
+ trailing += edits[last - 1]![1]
926
+ last -= 1
927
+ }
928
+ const identity = info.identity
929
+ const strand =
930
+ info.path.some(handle => isReverse(handle)) &&
931
+ !info.path.some(handle => !isReverse(handle))
932
+ ? '-'
933
+ : '+'
934
+ result.push({
935
+ pathHandle: identity?.pathHandle,
936
+ name: identity?.name,
937
+ strand: identity
938
+ ? identity.orientation === 'forward'
939
+ ? '+'
940
+ : '-'
941
+ : strand,
942
+ hapStart: identity
943
+ ? identity.name.fragment + identity.hapStart
944
+ : undefined,
945
+ hapEnd: identity ? identity.name.fragment + identity.hapEnd : undefined,
946
+ start: info.positions[0]!,
947
+ refStart: reference.start + leading,
948
+ refEnd: reference.start + refTotal - trailing,
949
+ cigar: edits
950
+ .slice(first, last)
951
+ .map(([op, len]) => `${len}${op}`)
952
+ .join(''),
953
+ weight: info.weight,
954
+ path: info.path,
955
+ })
956
+ })
957
+ return result
958
+ }
959
+
960
+ toJSON(cigar: boolean, opts: ToJsonOptions = {}): SubgraphJson {
961
+ const handles = this.sortedHandles()
962
+ const nodes = handles
963
+ .filter(handle => !isReverse(handle))
964
+ .map(handle => ({
965
+ id: String(nodeId(handle)),
966
+ sequence: this.record(handle).sequence,
967
+ }))
968
+ const edges: SubgraphJson['edges'] = []
969
+ for (const handle of handles) {
970
+ for (const successor of this.record(handle).successors()) {
971
+ if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
972
+ edges.push({
973
+ from: String(nodeId(handle)),
974
+ from_is_reverse: isReverse(handle),
975
+ to: String(nodeId(successor)),
976
+ to_is_reverse: isReverse(successor),
977
+ })
978
+ }
979
+ }
980
+ }
981
+ const paths: SubgraphPath[] = []
982
+ const contig = this.refPath?.contig ?? 'unknown'
983
+ if (this.refId !== undefined && this.refPath && this.refInterval) {
984
+ const info = this.paths[this.refId]!
985
+ const name = {
986
+ ...this.refPath,
987
+ fragment: this.refPath.fragment + this.refInterval[0],
988
+ }
989
+ paths.push(
990
+ jsonPath(
991
+ info,
992
+ formatPathName(name, this.refPath.fragment + this.refInterval[1]),
993
+ undefined,
994
+ ),
995
+ )
996
+ }
997
+ let haplotype = 1
998
+ this.paths.forEach((info, index) => {
999
+ if (index === this.refId) {
1000
+ return
1001
+ }
1002
+ const resolved = opts.names === 'resolved' ? info.identity : undefined
1003
+ const name = resolved
1004
+ ? formatPathName(
1005
+ {
1006
+ ...resolved.name,
1007
+ fragment: resolved.name.fragment + resolved.hapStart,
1008
+ },
1009
+ resolved.name.fragment + resolved.hapEnd,
1010
+ )
1011
+ : formatPathName(
1012
+ { sample: 'unknown', contig, haplotype, fragment: 0 },
1013
+ info.len,
1014
+ )
1015
+ paths.push(
1016
+ jsonPath(info, name, cigar ? this.alignToRef(index) : undefined),
1017
+ )
1018
+ haplotype += 1
1019
+ })
1020
+ return { nodes, edges, paths }
1021
+ }
1022
+ }
1023
+
1024
+ function jsonPath(
1025
+ info: PathInfo,
1026
+ name: string,
1027
+ cigar: string | undefined,
1028
+ ): SubgraphPath {
1029
+ return {
1030
+ name,
1031
+ ...(info.weight === undefined ? {} : { weight: info.weight }),
1032
+ ...(cigar === undefined ? {} : { cigar }),
1033
+ path: info.path.map(handle => ({
1034
+ id: String(nodeId(handle)),
1035
+ is_reverse: isReverse(handle),
1036
+ })),
1037
+ }
1038
+ }
1039
+
1040
+ function comparePaths(a: number[], b: number[]) {
1041
+ const n = Math.min(a.length, b.length)
1042
+ for (let i = 0; i < n; i++) {
1043
+ const x = a[i]!
1044
+ const y = b[i]!
1045
+ if (x !== y) {
1046
+ return x < y ? -1 : 1
1047
+ }
1048
+ }
1049
+ return a.length - b.length
1050
+ }
1051
+
1052
+ function appendEdit(edits: Edit[], op: EditOp, len: number) {
1053
+ if (len === 0) {
1054
+ return
1055
+ }
1056
+ const last = edits[edits.length - 1]
1057
+ if (last?.[0] === op) {
1058
+ last[1] += len
1059
+ } else {
1060
+ edits.push([op, len])
1061
+ }
1062
+ }
1063
+
1064
+ function gapPenalty(len: number) {
1065
+ return len === 0 ? 0 : 6 + (len - 1)
1066
+ }