@gmod/gbz-base 1.0.0 → 2.0.0

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