@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,64 @@
1
+ export const ENDMARKER = 0
2
+
3
+ export type Orientation = 'forward' | 'reverse'
4
+
5
+ export function encodeNode(id: number, orientation: Orientation) {
6
+ return 2 * id + (orientation === 'reverse' ? 1 : 0)
7
+ }
8
+
9
+ export function nodeId(handle: number) {
10
+ return Math.floor(handle / 2)
11
+ }
12
+
13
+ export function nodeOrientation(handle: number): Orientation {
14
+ return handle % 2 === 0 ? 'forward' : 'reverse'
15
+ }
16
+
17
+ export function isReverse(handle: number) {
18
+ return handle % 2 === 1
19
+ }
20
+
21
+ export function flipNode(handle: number) {
22
+ return handle % 2 === 0 ? handle + 1 : handle - 1
23
+ }
24
+
25
+ export type NodeSide = 'left' | 'right'
26
+
27
+ export function flipSide(side: NodeSide): NodeSide {
28
+ return side === 'left' ? 'right' : 'left'
29
+ }
30
+
31
+ export function entrySide(orientation: Orientation): NodeSide {
32
+ return orientation === 'forward' ? 'left' : 'right'
33
+ }
34
+
35
+ export function exitSide(orientation: Orientation): NodeSide {
36
+ return orientation === 'forward' ? 'right' : 'left'
37
+ }
38
+
39
+ export function entryOrientation(side: NodeSide): Orientation {
40
+ return side === 'left' ? 'forward' : 'reverse'
41
+ }
42
+
43
+ export function exitOrientation(side: NodeSide): Orientation {
44
+ return side === 'right' ? 'forward' : 'reverse'
45
+ }
46
+
47
+ export function edgeIsCanonical(from: number, to: number) {
48
+ const fromId = nodeId(from)
49
+ const toId = nodeId(to)
50
+ return isReverse(from)
51
+ ? toId > fromId || (toId === fromId && !isReverse(to))
52
+ : toId >= fromId
53
+ }
54
+
55
+ export function pathIsCanonical(path: number[]) {
56
+ const first = path[0]
57
+ const last = path[path.length - 1]
58
+ if (first === undefined || last === undefined) {
59
+ return true
60
+ }
61
+ return isReverse(first) === isReverse(last)
62
+ ? !isReverse(first)
63
+ : edgeIsCanonical(first, last)
64
+ }
@@ -0,0 +1,154 @@
1
+ import { ByteCodeReader, RunReader } from './bytecode.ts'
2
+ import { ENDMARKER, flipNode, nodeId } from './node.ts'
3
+
4
+ export interface Pos {
5
+ node: number
6
+ offset: number
7
+ }
8
+
9
+ export function decompressEdges(bytes: Uint8Array): Pos[] {
10
+ const reader = new ByteCodeReader(bytes)
11
+ const sigma = reader.int()
12
+ if (sigma === undefined || sigma === 0) {
13
+ return []
14
+ }
15
+ const edges: Pos[] = []
16
+ let prev = 0
17
+ for (let i = 0; i < sigma; i++) {
18
+ const delta = reader.int()
19
+ const offset = reader.int()
20
+ if (delta === undefined || offset === undefined) {
21
+ throw new Error('GBWT edge list ends early')
22
+ }
23
+ prev += delta
24
+ edges.push({ node: prev, offset })
25
+ }
26
+ return edges
27
+ }
28
+
29
+ export class GbwtRecord {
30
+ readonly edges: Pos[]
31
+ readonly bwt: Uint8Array
32
+
33
+ constructor(edges: Pos[], bwt: Uint8Array) {
34
+ this.edges = edges
35
+ this.bwt = bwt
36
+ }
37
+
38
+ runs() {
39
+ return new RunReader(this.bwt, this.edges.length)
40
+ }
41
+
42
+ private edge(rank: number) {
43
+ const edge = this.edges[rank]
44
+ if (edge === undefined) {
45
+ throw new Error(
46
+ `GBWT run refers to edge rank ${rank} of ${this.edges.length}`,
47
+ )
48
+ }
49
+ return edge
50
+ }
51
+
52
+ lf(i: number): Pos | undefined {
53
+ const offsets = this.edges.map(e => e.offset)
54
+ let offset = 0
55
+ for (const run of this.runs()) {
56
+ const edge = this.edge(run.value)
57
+ const soFar = offsets[run.value]!
58
+ if (offset + run.len > i) {
59
+ return edge.node === ENDMARKER
60
+ ? undefined
61
+ : { node: edge.node, offset: soFar + (i - offset) }
62
+ }
63
+ offsets[run.value] = soFar + run.len
64
+ offset += run.len
65
+ }
66
+ return undefined
67
+ }
68
+
69
+ private edgeTo(node: number) {
70
+ let low = 0
71
+ let high = this.edges.length
72
+ while (low < high) {
73
+ const mid = (low + high) >> 1
74
+ const edge = this.edges[mid]!
75
+ if (edge.node === node) {
76
+ return mid
77
+ }
78
+ if (edge.node < node) {
79
+ low = mid + 1
80
+ } else {
81
+ high = mid
82
+ }
83
+ }
84
+ return undefined
85
+ }
86
+
87
+ offsetTo(pos: Pos): number | undefined {
88
+ if (pos.node === ENDMARKER) {
89
+ return undefined
90
+ }
91
+ const rank = this.edgeTo(pos.node)
92
+ if (rank === undefined) {
93
+ return undefined
94
+ }
95
+ let succRank = this.edges[rank]!.offset
96
+ if (succRank > pos.offset) {
97
+ return undefined
98
+ }
99
+ let offset = 0
100
+ for (const run of this.runs()) {
101
+ offset += run.len
102
+ if (run.value !== rank) {
103
+ continue
104
+ }
105
+ succRank += run.len
106
+ if (succRank > pos.offset) {
107
+ return offset - (succRank - pos.offset)
108
+ }
109
+ }
110
+ return undefined
111
+ }
112
+
113
+ predecessorAt(i: number): number | undefined {
114
+ const counts = this.edges.map(e => ({
115
+ node: e.node === ENDMARKER ? ENDMARKER : flipNode(e.node),
116
+ count: 0,
117
+ }))
118
+ for (const run of this.runs()) {
119
+ const entry = counts[run.value]
120
+ if (entry) {
121
+ entry.count += run.len
122
+ }
123
+ }
124
+ for (let rank = 1; rank < counts.length; rank++) {
125
+ const prev = counts[rank - 1] as { node: number; count: number }
126
+ const curr = counts[rank] as { node: number; count: number }
127
+ if (nodeId(prev.node) === nodeId(curr.node)) {
128
+ counts[rank - 1] = curr
129
+ counts[rank] = prev
130
+ }
131
+ }
132
+ let offset = 0
133
+ for (const entry of counts) {
134
+ offset += entry.count
135
+ if (offset > i) {
136
+ return entry.node === ENDMARKER ? undefined : entry.node
137
+ }
138
+ }
139
+ return undefined
140
+ }
141
+
142
+ decompress(): Pos[] {
143
+ const offsets = this.edges.map(e => e.offset)
144
+ const result: Pos[] = []
145
+ for (const run of this.runs()) {
146
+ const edge = this.edge(run.value)
147
+ for (let k = 0; k < run.len; k++) {
148
+ result.push({ node: edge.node, offset: offsets[run.value]! })
149
+ offsets[run.value] = offsets[run.value]! + 1
150
+ }
151
+ }
152
+ return result
153
+ }
154
+ }
@@ -0,0 +1,50 @@
1
+ const DECODE = ['', 'A', 'C', 'G', 'T', 'N']
2
+
3
+ export function encodedSequenceLength(encoded: Uint8Array) {
4
+ const last = encoded[encoded.length - 1]
5
+ if (last === undefined) {
6
+ return 0
7
+ }
8
+ let value = last
9
+ let inLast = 0
10
+ for (let i = 0; i < 3; i++) {
11
+ if (value % 6 === 0) {
12
+ break
13
+ }
14
+ value = Math.floor(value / 6)
15
+ inLast += 1
16
+ }
17
+ return 3 * (encoded.length - 1) + inLast
18
+ }
19
+
20
+ export function decodeSequence(encoded: Uint8Array) {
21
+ let result = ''
22
+ for (const byte of encoded) {
23
+ let value = byte
24
+ for (let i = 0; i < 3; i++) {
25
+ const base = DECODE[value % 6]!
26
+ if (base === '') {
27
+ return result
28
+ }
29
+ value = Math.floor(value / 6)
30
+ result += base
31
+ }
32
+ }
33
+ return result
34
+ }
35
+
36
+ const COMPLEMENT: Record<string, string> = {
37
+ A: 'T',
38
+ C: 'G',
39
+ G: 'C',
40
+ T: 'A',
41
+ N: 'N',
42
+ }
43
+
44
+ export function reverseComplement(sequence: string) {
45
+ let result = ''
46
+ for (let i = sequence.length - 1; i >= 0; i--) {
47
+ result += COMPLEMENT[sequence[i]!] ?? 'N'
48
+ }
49
+ return result
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ export {
2
+ GBZBase,
3
+ GENERIC_SAMPLE,
4
+ GbzRecord,
5
+ SCHEMA_VERSION,
6
+ SchemaVersionError,
7
+ formatPathName,
8
+ } from './db.ts'
9
+ export type { GbzPath, HaplotypeSample, PathName } from './db.ts'
10
+ export type { ByteSource } from './filehandle.ts'
11
+ export type { Pos } from './gbwt/record.ts'
12
+ export { Subgraph } from './subgraph.ts'
13
+ export type {
14
+ HaplotypeAlignment,
15
+ HaplotypeOutput,
16
+ PathIdentity,
17
+ PathPosition,
18
+ ReferencePath,
19
+ SubgraphJson,
20
+ SubgraphPath,
21
+ ToJsonOptions,
22
+ } from './subgraph.ts'
23
+ export {
24
+ subgraphAroundNodes,
25
+ subgraphAtOffset,
26
+ subgraphInInterval,
27
+ } from './query.ts'
28
+ export type { PathQuery, QueryOptions } from './query.ts'
29
+ export { SqliteDatabase } from './sqlite/database.ts'
30
+ export { weightedLcs } from './lcs.ts'
31
+ export * as nodes from './gbwt/node.ts'
package/src/lcs.ts ADDED
@@ -0,0 +1,282 @@
1
+ interface Point {
2
+ weight: number
3
+ a: number
4
+ b: number
5
+ matches: number
6
+ }
7
+
8
+ function prefixSums(sequence: number[], weight: (x: number) => number) {
9
+ const sums = [0]
10
+ for (let i = 0; i < sequence.length; i++) {
11
+ sums.push(sums[i]! + weight(sequence[i]!))
12
+ }
13
+ return sums
14
+ }
15
+
16
+ class MinHeap {
17
+ private items: number[] = []
18
+
19
+ push(value: number) {
20
+ const items = this.items
21
+ items.push(value)
22
+ let i = items.length - 1
23
+ while (i > 0) {
24
+ const parent = (i - 1) >> 1
25
+ if (items[parent]! <= value) {
26
+ break
27
+ }
28
+ items[i] = items[parent]!
29
+ i = parent
30
+ }
31
+ items[i] = value
32
+ }
33
+
34
+ peek() {
35
+ return this.items[0]
36
+ }
37
+
38
+ pop() {
39
+ const items = this.items
40
+ const top = items[0]
41
+ const last = items.pop()
42
+ if (items.length > 0 && last !== undefined) {
43
+ let i = 0
44
+ for (;;) {
45
+ const left = 2 * i + 1
46
+ const right = left + 1
47
+ let smallest = i
48
+ let value = last
49
+ if (left < items.length && items[left]! < value) {
50
+ smallest = left
51
+ value = items[left]!
52
+ }
53
+ if (right < items.length && items[right]! < value) {
54
+ smallest = right
55
+ }
56
+ if (smallest === i) {
57
+ break
58
+ }
59
+ items[i] = items[smallest]!
60
+ i = smallest
61
+ }
62
+ items[i] = last
63
+ }
64
+ return top
65
+ }
66
+ }
67
+
68
+ class Matrix {
69
+ readonly aSums: number[]
70
+ readonly bSums: number[]
71
+ readonly a: number[]
72
+ readonly b: number[]
73
+ readonly points = new Map<number, Map<number, Point>>()
74
+ private pendingEdits = new MinHeap()
75
+
76
+ constructor(a: number[], b: number[], weight: (x: number) => number) {
77
+ this.a = a
78
+ this.b = b
79
+ this.aSums = prefixSums(a, weight)
80
+ this.bSums = prefixSums(b, weight)
81
+ this.set(0, 0, { weight: 0, a: 0, b: 0, matches: 0 })
82
+ }
83
+
84
+ private set(edits: number, diagonal: number, point: Point) {
85
+ let row = this.points.get(edits)
86
+ if (!row) {
87
+ row = new Map()
88
+ this.points.set(edits, row)
89
+ this.pendingEdits.push(edits)
90
+ }
91
+ row.set(diagonal, point)
92
+ }
93
+
94
+ get(edits: number, diagonal: number) {
95
+ return this.points.get(edits)?.get(diagonal)
96
+ }
97
+
98
+ private tryInsert(edits: number, diagonal: number, point: Point) {
99
+ const existing = this.get(edits, diagonal)
100
+ if (!existing || point.weight > existing.weight) {
101
+ this.set(edits, diagonal, point)
102
+ }
103
+ }
104
+
105
+ aWeight(offset: number) {
106
+ return this.aSums[offset + 1]! - this.aSums[offset]!
107
+ }
108
+
109
+ bWeight(offset: number) {
110
+ return this.bSums[offset + 1]! - this.bSums[offset]!
111
+ }
112
+
113
+ extend(edits: number): Point | undefined {
114
+ const row = this.points.get(edits)
115
+ if (!row) {
116
+ return undefined
117
+ }
118
+ const diagonals = [...row.keys()].sort((x, y) => x - y)
119
+ for (const diagonal of diagonals) {
120
+ const found = row.get(diagonal)
121
+ if (!found) {
122
+ continue
123
+ }
124
+ const point = { ...found }
125
+ while (
126
+ point.a < this.a.length &&
127
+ point.b < this.b.length &&
128
+ this.a[point.a] === this.b[point.b]
129
+ ) {
130
+ point.weight += 2 * this.aWeight(point.a)
131
+ point.a += 1
132
+ point.b += 1
133
+ point.matches += 1
134
+ }
135
+ if (point.matches > 0) {
136
+ row.set(diagonal, point)
137
+ }
138
+ if (point.a === this.a.length && point.b === this.b.length) {
139
+ return point
140
+ }
141
+ if (point.a < this.a.length) {
142
+ const w = this.aWeight(point.a)
143
+ this.tryInsert(edits + w, diagonal + w, {
144
+ weight: point.weight,
145
+ a: point.a + 1,
146
+ b: point.b,
147
+ matches: 0,
148
+ })
149
+ }
150
+ if (point.b < this.b.length) {
151
+ const w = this.bWeight(point.b)
152
+ this.tryInsert(edits + w, diagonal - w, {
153
+ weight: point.weight,
154
+ a: point.a,
155
+ b: point.b + 1,
156
+ matches: 0,
157
+ })
158
+ }
159
+ }
160
+ return undefined
161
+ }
162
+
163
+ nextEdits(edits: number) {
164
+ while (
165
+ this.pendingEdits.peek() !== undefined &&
166
+ this.pendingEdits.peek()! <= edits
167
+ ) {
168
+ this.pendingEdits.pop()
169
+ }
170
+ return this.pendingEdits.peek()
171
+ }
172
+
173
+ predecessor(
174
+ a: number,
175
+ b: number,
176
+ edits: number,
177
+ ): [Point, number] | undefined {
178
+ const diagonal = this.aSums[a]! - this.bSums[b]!
179
+ const prev =
180
+ a > 0 && this.aWeight(a - 1) <= edits
181
+ ? this.get(edits - this.aWeight(a - 1), diagonal - this.aWeight(a - 1))
182
+ : undefined
183
+ const next =
184
+ b > 0 && this.bWeight(b - 1) <= edits
185
+ ? this.get(edits - this.bWeight(b - 1), diagonal + this.bWeight(b - 1))
186
+ : undefined
187
+ if (prev && next) {
188
+ return prev.weight > next.weight
189
+ ? [prev, edits - this.aWeight(a - 1)]
190
+ : [next, edits - this.bWeight(b - 1)]
191
+ }
192
+ if (prev) {
193
+ return [prev, edits - this.aWeight(a - 1)]
194
+ }
195
+ if (next) {
196
+ return [next, edits - this.bWeight(b - 1)]
197
+ }
198
+ return undefined
199
+ }
200
+ }
201
+
202
+ export function weightedLcs(
203
+ a: number[],
204
+ b: number[],
205
+ weight: (x: number) => number,
206
+ ): [pairs: [number, number][], weight: number] {
207
+ let prefix = 0
208
+ while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix]) {
209
+ prefix += 1
210
+ }
211
+ let suffix = 0
212
+ while (
213
+ suffix < a.length - prefix &&
214
+ suffix < b.length - prefix &&
215
+ a[a.length - 1 - suffix] === b[b.length - 1 - suffix]
216
+ ) {
217
+ suffix += 1
218
+ }
219
+ const pairs: [number, number][] = []
220
+ let total = 0
221
+ for (let i = 0; i < prefix; i++) {
222
+ pairs.push([i, i])
223
+ total += weight(a[i]!)
224
+ }
225
+ const [middle, middleWeight] = weightedLcsCore(
226
+ a.slice(prefix, a.length - suffix),
227
+ b.slice(prefix, b.length - suffix),
228
+ weight,
229
+ )
230
+ for (const [i, j] of middle) {
231
+ pairs.push([i + prefix, j + prefix])
232
+ }
233
+ total += middleWeight
234
+ for (let i = suffix; i > 0; i--) {
235
+ pairs.push([a.length - i, b.length - i])
236
+ total += weight(a[a.length - i]!)
237
+ }
238
+ return [pairs, total]
239
+ }
240
+
241
+ function weightedLcsCore(
242
+ a: number[],
243
+ b: number[],
244
+ weight: (x: number) => number,
245
+ ): [pairs: [number, number][], weight: number] {
246
+ if (a.length === 0 || b.length === 0) {
247
+ return [[], 0]
248
+ }
249
+ const matrix = new Matrix(a, b, weight)
250
+ let edits = 0
251
+ let point: Point = { weight: 0, a: 0, b: 0, matches: 0 }
252
+ for (;;) {
253
+ const end = matrix.extend(edits)
254
+ if (end) {
255
+ point = end
256
+ break
257
+ }
258
+ const next = matrix.nextEdits(edits)
259
+ if (next === undefined) {
260
+ break
261
+ }
262
+ edits = next
263
+ }
264
+ const result: [number, number][] = []
265
+ const finalWeight = point.weight / 2
266
+ point = { ...point }
267
+ for (;;) {
268
+ for (let i = 0; i < point.matches; i++) {
269
+ point.a -= 1
270
+ point.b -= 1
271
+ result.push([point.a, point.b])
272
+ }
273
+ const pred = matrix.predecessor(point.a, point.b, edits)
274
+ if (!pred) {
275
+ break
276
+ }
277
+ point = { ...pred[0] }
278
+ edits = pred[1]
279
+ }
280
+ result.reverse()
281
+ return [result, finalWeight]
282
+ }
package/src/query.ts ADDED
@@ -0,0 +1,79 @@
1
+ import { GENERIC_SAMPLE } from './db.ts'
2
+ import { Subgraph } from './subgraph.ts'
3
+
4
+ import type { GBZBase, PathName } from './db.ts'
5
+ import type { HaplotypeOutput } from './subgraph.ts'
6
+
7
+ export interface QueryOptions {
8
+ context?: number
9
+ haplotypes?: HaplotypeOutput
10
+ limit?: number
11
+ }
12
+
13
+ export interface PathQuery {
14
+ sample?: string
15
+ contig: string
16
+ haplotype?: number
17
+ }
18
+
19
+ function pathName(query: PathQuery, fragment: number): PathName {
20
+ return {
21
+ sample: query.sample ?? GENERIC_SAMPLE,
22
+ contig: query.contig,
23
+ haplotype: query.haplotype ?? 0,
24
+ fragment,
25
+ }
26
+ }
27
+
28
+ export async function subgraphAtOffset(
29
+ db: GBZBase,
30
+ query: PathQuery,
31
+ offset: number,
32
+ opts: QueryOptions = {},
33
+ ) {
34
+ const subgraph = new Subgraph(db)
35
+ subgraph.limit = opts.limit
36
+ const reference = await subgraph.pathPosition(pathName(query, offset))
37
+ await subgraph.aroundPosition(
38
+ reference.position.handle,
39
+ reference.position.nodeOffset,
40
+ opts.context ?? 100,
41
+ )
42
+ subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
43
+ return subgraph
44
+ }
45
+
46
+ export async function subgraphInInterval(
47
+ db: GBZBase,
48
+ query: PathQuery,
49
+ start: number,
50
+ end: number,
51
+ opts: QueryOptions = {},
52
+ ) {
53
+ const subgraph = new Subgraph(db)
54
+ subgraph.limit = opts.limit
55
+ const reference = await subgraph.pathPosition(pathName(query, start))
56
+ await subgraph.aroundInterval(
57
+ reference.position,
58
+ end - start,
59
+ opts.context ?? 100,
60
+ )
61
+ subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
62
+ return subgraph
63
+ }
64
+
65
+ export async function subgraphAroundNodes(
66
+ db: GBZBase,
67
+ nodes: number[],
68
+ opts: QueryOptions = {},
69
+ ) {
70
+ const haplotypes = opts.haplotypes ?? 'all'
71
+ if (haplotypes === 'reference-only') {
72
+ throw new Error('Cannot output a reference path in a node-based query')
73
+ }
74
+ const subgraph = new Subgraph(db)
75
+ subgraph.limit = opts.limit
76
+ await subgraph.aroundNodes(nodes, opts.context ?? 100)
77
+ subgraph.extractPaths(undefined, haplotypes)
78
+ return subgraph
79
+ }