@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,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
+ }
@@ -0,0 +1,94 @@
1
+ export interface GraphName {
2
+ name: string | undefined
3
+ subgraph: Map<string, Set<string>>
4
+ translation: Map<string, Set<string>>
5
+ }
6
+
7
+ function parseRelationships(field: string | undefined, what: string) {
8
+ const relationships = new Map<string, Set<string>>()
9
+ if (field !== undefined) {
10
+ for (const rel of field.split(';')) {
11
+ const parts = rel.split(',')
12
+ const [from, to] = parts
13
+ if (parts.length !== 2 || !from || !to) {
14
+ throw new Error(`Invalid ${what} relationship: ${rel}`)
15
+ }
16
+ let targets = relationships.get(from)
17
+ if (!targets) {
18
+ targets = new Set()
19
+ relationships.set(from, targets)
20
+ }
21
+ targets.add(to)
22
+ }
23
+ }
24
+ return relationships
25
+ }
26
+
27
+ export function graphNameFromTags(tags: Map<string, string>): GraphName {
28
+ return {
29
+ name: tags.get('pggname'),
30
+ subgraph: parseRelationships(tags.get('subgraph'), 'subgraph'),
31
+ translation: parseRelationships(tags.get('translation'), 'translation'),
32
+ }
33
+ }
34
+
35
+ function merge(into: Map<string, Set<string>>, from: Map<string, Set<string>>) {
36
+ for (const [key, values] of from) {
37
+ let targets = into.get(key)
38
+ if (!targets) {
39
+ targets = new Set()
40
+ into.set(key, targets)
41
+ }
42
+ for (const value of values) {
43
+ targets.add(value)
44
+ }
45
+ }
46
+ }
47
+
48
+ export function subgraphName(name: string, parent: GraphName): GraphName {
49
+ const result: GraphName = {
50
+ name,
51
+ subgraph: new Map(),
52
+ translation: new Map(),
53
+ }
54
+ if (parent.name !== undefined) {
55
+ result.subgraph.set(name, new Set([parent.name]))
56
+ merge(result.subgraph, parent.subgraph)
57
+ merge(result.translation, parent.translation)
58
+ }
59
+ return result
60
+ }
61
+
62
+ function sortedEntries(relationships: Map<string, Set<string>>) {
63
+ return [...relationships.entries()]
64
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
65
+ .flatMap(([from, tos]) => [...tos].sort().map(to => [from, to] as const))
66
+ }
67
+
68
+ export function gfaHeaderLines(graphName: GraphName) {
69
+ const lines: string[] = []
70
+ if (graphName.name !== undefined) {
71
+ lines.push(`H\tNM:Z:${graphName.name}`)
72
+ }
73
+ for (const [subgraph, supergraph] of sortedEntries(graphName.subgraph)) {
74
+ lines.push(`H\tSG:Z:${subgraph},${supergraph}`)
75
+ }
76
+ for (const [from, to] of sortedEntries(graphName.translation)) {
77
+ lines.push(`H\tTL:Z:${from},${to}`)
78
+ }
79
+ return lines
80
+ }
81
+
82
+ export async function sha256Hex(chunks: Uint8Array[]) {
83
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
84
+ const bytes = new Uint8Array(total)
85
+ let offset = 0
86
+ for (const chunk of chunks) {
87
+ bytes.set(chunk, offset)
88
+ offset += chunk.length
89
+ }
90
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes)
91
+ return [...new Uint8Array(digest)]
92
+ .map(byte => byte.toString(16).padStart(2, '0'))
93
+ .join('')
94
+ }
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
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, OpenOptions, 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
+ SnarlOutput,
20
+ SubgraphJson,
21
+ SubgraphPath,
22
+ ToJsonOptions,
23
+ } from './subgraph.ts'
24
+ export {
25
+ subgraphAroundNodes,
26
+ subgraphAtOffset,
27
+ subgraphBetween,
28
+ subgraphInInterval,
29
+ } from './query.ts'
30
+ export type { PathQuery, QueryOptions } from './query.ts'
31
+ export { SqliteDatabase } from './sqlite/database.ts'
32
+ export type { GraphName } from './graphName.ts'
33
+ export { weightedLcs } from './lcs.ts'
34
+ 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,106 @@
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, SnarlOutput } from './subgraph.ts'
6
+
7
+ export interface QueryOptions {
8
+ context?: number
9
+ haplotypes?: HaplotypeOutput
10
+ snarls?: SnarlOutput
11
+ limit?: number
12
+ }
13
+
14
+ export interface PathQuery {
15
+ sample?: string
16
+ contig: string
17
+ haplotype?: number
18
+ }
19
+
20
+ function pathName(query: PathQuery, fragment: number): PathName {
21
+ return {
22
+ sample: query.sample ?? GENERIC_SAMPLE,
23
+ contig: query.contig,
24
+ haplotype: query.haplotype ?? 0,
25
+ fragment,
26
+ }
27
+ }
28
+
29
+ export async function subgraphAtOffset(
30
+ db: GBZBase,
31
+ query: PathQuery,
32
+ offset: number,
33
+ opts: QueryOptions = {},
34
+ ) {
35
+ const subgraph = new Subgraph(db)
36
+ subgraph.limit = opts.limit
37
+ const reference = await subgraph.pathPosition(pathName(query, offset))
38
+ await subgraph.aroundPosition(
39
+ reference.position.handle,
40
+ reference.position.nodeOffset,
41
+ opts.context ?? 100,
42
+ )
43
+ await subgraph.extractSnarls(opts.snarls ?? 'none')
44
+ subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
45
+ return subgraph
46
+ }
47
+
48
+ export async function subgraphInInterval(
49
+ db: GBZBase,
50
+ query: PathQuery,
51
+ start: number,
52
+ end: number,
53
+ opts: QueryOptions = {},
54
+ ) {
55
+ const subgraph = new Subgraph(db)
56
+ subgraph.limit = opts.limit
57
+ const reference = await subgraph.pathPosition(pathName(query, start))
58
+ await subgraph.aroundInterval(
59
+ reference.position,
60
+ end - start,
61
+ opts.context ?? 100,
62
+ )
63
+ await subgraph.extractSnarls(opts.snarls ?? 'none')
64
+ subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
65
+ return subgraph
66
+ }
67
+
68
+ export async function subgraphAroundNodes(
69
+ db: GBZBase,
70
+ nodes: number[],
71
+ opts: QueryOptions = {},
72
+ ) {
73
+ const haplotypes = opts.haplotypes ?? 'all'
74
+ const snarls = opts.snarls ?? 'none'
75
+ if (haplotypes === 'reference-only') {
76
+ throw new Error('Cannot output a reference path in a node-based query')
77
+ }
78
+ if (snarls === 'overlapping' && nodes.length > 1) {
79
+ throw new Error(
80
+ 'Overlapping snarls cannot be extracted for a node-based query with multiple nodes',
81
+ )
82
+ }
83
+ const subgraph = new Subgraph(db)
84
+ subgraph.limit = opts.limit
85
+ await subgraph.aroundNodes(nodes, opts.context ?? 100)
86
+ await subgraph.extractSnarls(snarls)
87
+ subgraph.extractPaths(undefined, haplotypes)
88
+ return subgraph
89
+ }
90
+
91
+ export async function subgraphBetween(
92
+ db: GBZBase,
93
+ start: number,
94
+ end: number,
95
+ opts: Pick<QueryOptions, 'haplotypes' | 'limit'> = {},
96
+ ) {
97
+ const haplotypes = opts.haplotypes ?? 'all'
98
+ if (haplotypes === 'reference-only') {
99
+ throw new Error('Cannot output a reference path in a node-based query')
100
+ }
101
+ const subgraph = new Subgraph(db)
102
+ subgraph.limit = opts.limit
103
+ await subgraph.betweenNodes(start, end)
104
+ subgraph.extractPaths(undefined, haplotypes)
105
+ return subgraph
106
+ }