@gmod/gbz-base 1.0.0 → 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.
- package/README.md +43 -2
- package/bin/query.js +0 -0
- package/dist/cli.js +66 -19
- package/dist/cli.js.map +1 -1
- package/dist/db.d.ts +9 -1
- package/dist/db.js +63 -20
- package/dist/db.js.map +1 -1
- package/dist/gbwt/record.d.ts +4 -0
- package/dist/gbwt/record.js +22 -0
- package/dist/gbwt/record.js.map +1 -1
- package/dist/graphName.d.ts +9 -0
- package/dist/graphName.js +83 -0
- package/dist/graphName.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/query.d.ts +3 -1
- package/dist/query.js +18 -0
- package/dist/query.js.map +1 -1
- package/dist/sqlite/btree.js +7 -2
- package/dist/sqlite/btree.js.map +1 -1
- package/dist/sqlite/pager.d.ts +5 -0
- package/dist/sqlite/pager.js +65 -8
- package/dist/sqlite/pager.js.map +1 -1
- package/dist/subgraph.d.ts +10 -0
- package/dist/subgraph.js +314 -72
- package/dist/subgraph.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +69 -14
- package/src/db.ts +73 -21
- package/src/gbwt/record.ts +23 -0
- package/src/graphName.ts +94 -0
- package/src/index.ts +4 -1
- package/src/query.ts +28 -1
- package/src/sqlite/btree.ts +7 -2
- package/src/sqlite/pager.ts +78 -8
- package/src/subgraph.ts +372 -85
- package/tools/haplotype-index/src/main.rs +197 -163
package/src/cli.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { LocalFile, RemoteFile } from 'generic-filehandle2'
|
|
2
2
|
|
|
3
3
|
import { GBZBase, formatPathName } from './db.ts'
|
|
4
|
+
import { encodeNode } from './gbwt/node.ts'
|
|
4
5
|
import {
|
|
5
6
|
subgraphAroundNodes,
|
|
6
7
|
subgraphAtOffset,
|
|
8
|
+
subgraphBetween,
|
|
7
9
|
subgraphInInterval,
|
|
8
10
|
} from './query.ts'
|
|
9
11
|
|
|
10
|
-
import type { HaplotypeOutput } from './subgraph.ts'
|
|
12
|
+
import type { HaplotypeOutput, SnarlOutput } from './subgraph.ts'
|
|
11
13
|
|
|
12
14
|
const USAGE = `Usage: gbz-base-query [options] graph.gbz.db
|
|
13
15
|
|
|
@@ -17,12 +19,17 @@ const USAGE = `Usage: gbz-base-query [options] graph.gbz.db
|
|
|
17
19
|
-o, --offset INT sequence offset
|
|
18
20
|
-i, --interval A..B half-open sequence interval
|
|
19
21
|
-n, --node INT node identifier (may repeat)
|
|
22
|
+
-b, --between A:B subgraph between two chain boundary handles, each INT[+-]
|
|
20
23
|
--context INT context length in bp (default: 100)
|
|
24
|
+
--snarls extend the subgraph with contained top-level snarls
|
|
25
|
+
--extend-snarls extend the subgraph with overlapping top-level snarls
|
|
21
26
|
--limit INT safety limit for the number of nodes
|
|
22
27
|
--haplotypes SEL all, distinct, reference-only or none (default: all)
|
|
23
28
|
--cigar output CIGAR strings for the haplotypes
|
|
29
|
+
--format FMT json (default) or gfa
|
|
24
30
|
--resolve name haplotypes from the HaplotypeSamples table
|
|
25
31
|
--alignments print one alignment record per haplotype fragment instead of the subgraph
|
|
32
|
+
--haplotype-index F companion database written by gbz-haplotype-index --output
|
|
26
33
|
--block-size INT bytes fetched per range request (default: 65536)
|
|
27
34
|
--stats print fetch statistics to stderr
|
|
28
35
|
`
|
|
@@ -35,24 +42,39 @@ interface Args {
|
|
|
35
42
|
offset?: number
|
|
36
43
|
interval?: [number, number]
|
|
37
44
|
nodes: number[]
|
|
45
|
+
between?: [number, number]
|
|
38
46
|
context: number
|
|
47
|
+
snarls: SnarlOutput
|
|
39
48
|
limit?: number
|
|
40
49
|
haplotypes: HaplotypeOutput
|
|
41
50
|
cigar: boolean
|
|
51
|
+
format: 'json' | 'gfa'
|
|
42
52
|
resolve: boolean
|
|
43
53
|
alignments: boolean
|
|
44
54
|
blockSize: number
|
|
55
|
+
haplotypeIndex?: string
|
|
45
56
|
stats: boolean
|
|
46
57
|
}
|
|
47
58
|
|
|
59
|
+
function parseHandle(text: string) {
|
|
60
|
+
const orientation = text.endsWith('-') ? 'reverse' : 'forward'
|
|
61
|
+
const digits = /[+-]$/.test(text) ? text.slice(0, -1) : text
|
|
62
|
+
if (!/^\d+$/.test(digits)) {
|
|
63
|
+
throw new Error(`Failed to parse oriented node ${text}`)
|
|
64
|
+
}
|
|
65
|
+
return encodeNode(Number(digits), orientation)
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
function parseArgs(argv: string[]): Args {
|
|
49
69
|
const args: Args = {
|
|
50
70
|
file: '',
|
|
51
71
|
haplotype: 0,
|
|
52
72
|
nodes: [],
|
|
53
73
|
context: 100,
|
|
74
|
+
snarls: 'none',
|
|
54
75
|
haplotypes: 'all',
|
|
55
76
|
cigar: false,
|
|
77
|
+
format: 'json',
|
|
56
78
|
resolve: false,
|
|
57
79
|
alignments: false,
|
|
58
80
|
blockSize: 65536,
|
|
@@ -91,9 +113,25 @@ function parseArgs(argv: string[]): Args {
|
|
|
91
113
|
case '--node':
|
|
92
114
|
args.nodes.push(Number(next(i++)))
|
|
93
115
|
break
|
|
116
|
+
case '-b':
|
|
117
|
+
case '--between': {
|
|
118
|
+
const [a, b, extra] = next(i++).split(':')
|
|
119
|
+
if (a === undefined || b === undefined || extra !== undefined) {
|
|
120
|
+
throw new Error(`--between needs two oriented nodes, like 14+:17-`)
|
|
121
|
+
}
|
|
122
|
+
args.between = [parseHandle(a), parseHandle(b)]
|
|
123
|
+
break
|
|
124
|
+
}
|
|
94
125
|
case '--context':
|
|
95
126
|
args.context = Number(next(i++))
|
|
96
127
|
break
|
|
128
|
+
case '--snarls':
|
|
129
|
+
args.snarls =
|
|
130
|
+
args.snarls === 'overlapping' ? 'overlapping' : 'contained'
|
|
131
|
+
break
|
|
132
|
+
case '--extend-snarls':
|
|
133
|
+
args.snarls = 'overlapping'
|
|
134
|
+
break
|
|
97
135
|
case '--limit':
|
|
98
136
|
args.limit = Number(next(i++))
|
|
99
137
|
break
|
|
@@ -113,12 +151,20 @@ function parseArgs(argv: string[]): Args {
|
|
|
113
151
|
case '--block-size':
|
|
114
152
|
args.blockSize = Number(next(i++))
|
|
115
153
|
break
|
|
154
|
+
case '--haplotype-index':
|
|
155
|
+
args.haplotypeIndex = next(i++)
|
|
156
|
+
break
|
|
116
157
|
case '--stats':
|
|
117
158
|
args.stats = true
|
|
118
159
|
break
|
|
119
|
-
case '--format':
|
|
120
|
-
next(i++)
|
|
160
|
+
case '--format': {
|
|
161
|
+
const format = next(i++)
|
|
162
|
+
if (format !== 'json' && format !== 'gfa') {
|
|
163
|
+
throw new Error(`Unknown output format ${format}`)
|
|
164
|
+
}
|
|
165
|
+
args.format = format
|
|
121
166
|
break
|
|
167
|
+
}
|
|
122
168
|
case '-h':
|
|
123
169
|
case '--help':
|
|
124
170
|
process.stdout.write(USAGE)
|
|
@@ -139,13 +185,18 @@ function parseArgs(argv: string[]): Args {
|
|
|
139
185
|
|
|
140
186
|
export async function main(argv: string[]) {
|
|
141
187
|
const args = parseArgs(argv)
|
|
142
|
-
const
|
|
143
|
-
? new RemoteFile(
|
|
144
|
-
|
|
145
|
-
|
|
188
|
+
const open = (file: string) =>
|
|
189
|
+
/^https?:\/\//.test(file) ? new RemoteFile(file) : new LocalFile(file)
|
|
190
|
+
const db = await GBZBase.open(open(args.file), {
|
|
191
|
+
blockSize: args.blockSize,
|
|
192
|
+
...(args.haplotypeIndex === undefined
|
|
193
|
+
? {}
|
|
194
|
+
: { haplotypeIndex: open(args.haplotypeIndex) }),
|
|
195
|
+
})
|
|
146
196
|
const opts = {
|
|
147
197
|
context: args.context,
|
|
148
198
|
haplotypes: args.haplotypes,
|
|
199
|
+
snarls: args.snarls,
|
|
149
200
|
...(args.limit === undefined ? {} : { limit: args.limit }),
|
|
150
201
|
}
|
|
151
202
|
const query = {
|
|
@@ -153,8 +204,9 @@ export async function main(argv: string[]) {
|
|
|
153
204
|
haplotype: args.haplotype,
|
|
154
205
|
...(args.sample === undefined ? {} : { sample: args.sample }),
|
|
155
206
|
}
|
|
156
|
-
const subgraph =
|
|
157
|
-
args.
|
|
207
|
+
const subgraph = args.between
|
|
208
|
+
? await subgraphBetween(db, args.between[0], args.between[1], opts)
|
|
209
|
+
: args.nodes.length > 0
|
|
158
210
|
? await subgraphAroundNodes(db, args.nodes, opts)
|
|
159
211
|
: args.interval
|
|
160
212
|
? await subgraphInInterval(
|
|
@@ -169,12 +221,13 @@ export async function main(argv: string[]) {
|
|
|
169
221
|
: undefined
|
|
170
222
|
if (!subgraph) {
|
|
171
223
|
throw new Error(
|
|
172
|
-
'Query type must be specified using --offset, --interval or --
|
|
224
|
+
'Query type must be specified using --offset, --interval, --node or --between',
|
|
173
225
|
)
|
|
174
226
|
}
|
|
175
227
|
if (args.resolve) {
|
|
176
228
|
await subgraph.identifyPaths()
|
|
177
229
|
}
|
|
230
|
+
const names = args.resolve ? 'resolved' : 'anonymous'
|
|
178
231
|
const output = args.alignments
|
|
179
232
|
? subgraph.alignments().map(a => ({
|
|
180
233
|
...a,
|
|
@@ -184,10 +237,12 @@ export async function main(argv: string[]) {
|
|
|
184
237
|
: undefined,
|
|
185
238
|
start: undefined,
|
|
186
239
|
}))
|
|
187
|
-
: subgraph.toJSON(args.cigar, {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
240
|
+
: subgraph.toJSON(args.cigar, { names })
|
|
241
|
+
process.stdout.write(
|
|
242
|
+
args.format === 'gfa' && !args.alignments
|
|
243
|
+
? await subgraph.toGFA(args.cigar, { names })
|
|
244
|
+
: `${JSON.stringify(output)}\n`,
|
|
245
|
+
)
|
|
191
246
|
if (args.stats) {
|
|
192
247
|
const { fetches, bytesFetched } = db.sqlite.pager
|
|
193
248
|
const {
|
package/src/db.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ENDMARKER, nodeId, nodeOrientation } from './gbwt/node.ts'
|
|
2
2
|
import { GbwtRecord, decompressEdges } from './gbwt/record.ts'
|
|
3
3
|
import { decodeSequence, encodedSequenceLength } from './gbwt/sequence.ts'
|
|
4
|
+
import { graphNameFromTags } from './graphName.ts'
|
|
4
5
|
import { SqliteDatabase } from './sqlite/database.ts'
|
|
5
6
|
|
|
6
7
|
import type { ByteSource } from './filehandle.ts'
|
|
7
8
|
import type { Pos } from './gbwt/record.ts'
|
|
9
|
+
import type { GraphName } from './graphName.ts'
|
|
8
10
|
import type { PagerOptions } from './sqlite/pager.ts'
|
|
9
11
|
import type { SqlValue } from './sqlite/record.ts'
|
|
10
12
|
|
|
@@ -144,37 +146,63 @@ function rowToPath(rowid: number, values: SqlValue[]): GbzPath {
|
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
export interface OpenOptions extends PagerOptions {
|
|
150
|
+
haplotypeIndex?: ByteSource
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function readTags(sqlite: SqliteDatabase) {
|
|
154
|
+
const tags = new Map<string, string>()
|
|
155
|
+
for await (const { values } of sqlite.scan('Tags')) {
|
|
156
|
+
tags.set(str(values[0], 'Tags.key'), str(values[1], 'Tags.value'))
|
|
157
|
+
}
|
|
158
|
+
return tags
|
|
159
|
+
}
|
|
160
|
+
|
|
147
161
|
export class GBZBase {
|
|
148
162
|
private tagCache: Promise<Map<string, string>> | undefined
|
|
149
163
|
private pathCache: Promise<GbzPath[]> | undefined
|
|
164
|
+
private indexTags: Map<string, string> | undefined
|
|
150
165
|
|
|
151
166
|
readonly sqlite: SqliteDatabase
|
|
167
|
+
private readonly index: SqliteDatabase
|
|
152
168
|
|
|
153
|
-
private constructor(sqlite: SqliteDatabase) {
|
|
169
|
+
private constructor(sqlite: SqliteDatabase, index: SqliteDatabase) {
|
|
154
170
|
this.sqlite = sqlite
|
|
171
|
+
this.index = index
|
|
155
172
|
}
|
|
156
173
|
|
|
157
|
-
static async open(source: ByteSource, opts:
|
|
158
|
-
const
|
|
174
|
+
static async open(source: ByteSource, opts: OpenOptions = {}) {
|
|
175
|
+
const { haplotypeIndex, ...pagerOptions } = opts
|
|
176
|
+
const sqlite = await SqliteDatabase.open(source, pagerOptions)
|
|
159
177
|
for (const table of ['Tags', 'Nodes', 'Paths', 'ReferenceIndex']) {
|
|
160
178
|
sqlite.rootPage(table)
|
|
161
179
|
}
|
|
162
|
-
const
|
|
180
|
+
const index = haplotypeIndex
|
|
181
|
+
? await SqliteDatabase.open(haplotypeIndex, pagerOptions)
|
|
182
|
+
: sqlite
|
|
183
|
+
const db = new GBZBase(sqlite, index)
|
|
163
184
|
const version = await db.tag('version')
|
|
164
185
|
if (version !== SCHEMA_VERSION) {
|
|
165
186
|
throw new SchemaVersionError(version)
|
|
166
187
|
}
|
|
188
|
+
if (haplotypeIndex) {
|
|
189
|
+
for (const table of ['Tags', 'HaplotypeSamples', 'HaplotypeLengths']) {
|
|
190
|
+
index.rootPage(table)
|
|
191
|
+
}
|
|
192
|
+
db.indexTags = await readTags(index)
|
|
193
|
+
const indexed = db.indexTags.get('haplotype_index_paths')
|
|
194
|
+
const paths = await db.tag('paths')
|
|
195
|
+
if (indexed !== paths) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`haplotype index was built for ${indexed ?? 'an unknown number of'} paths but the graph has ${paths}`,
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
167
201
|
return db
|
|
168
202
|
}
|
|
169
203
|
|
|
170
204
|
tags() {
|
|
171
|
-
this.tagCache ??= (
|
|
172
|
-
const tags = new Map<string, string>()
|
|
173
|
-
for await (const { values } of this.sqlite.scan('Tags')) {
|
|
174
|
-
tags.set(str(values[0], 'Tags.key'), str(values[1], 'Tags.value'))
|
|
175
|
-
}
|
|
176
|
-
return tags
|
|
177
|
-
})()
|
|
205
|
+
this.tagCache ??= readTags(this.sqlite)
|
|
178
206
|
return this.tagCache
|
|
179
207
|
}
|
|
180
208
|
|
|
@@ -228,14 +256,41 @@ export class GBZBase {
|
|
|
228
256
|
return (await this.paths()).filter(p => p.name.sample === sample)
|
|
229
257
|
}
|
|
230
258
|
|
|
259
|
+
async graphName() {
|
|
260
|
+
const gbzTags = new Map<string, string>()
|
|
261
|
+
for (const [key, value] of await this.tags()) {
|
|
262
|
+
if (key.startsWith('gbz_')) {
|
|
263
|
+
gbzTags.set(key.slice('gbz_'.length), value)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
let name: GraphName = {
|
|
267
|
+
name: undefined,
|
|
268
|
+
subgraph: new Map(),
|
|
269
|
+
translation: new Map(),
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
name = graphNameFromTags(gbzTags)
|
|
273
|
+
} catch {
|
|
274
|
+
// upstream falls back to an empty name when the tags do not parse
|
|
275
|
+
}
|
|
276
|
+
return name
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async hasChainLinks() {
|
|
280
|
+
const links = await this.tag('chain_links')
|
|
281
|
+
return links !== undefined && Number(links) > 0
|
|
282
|
+
}
|
|
283
|
+
|
|
231
284
|
get hasHaplotypeIndex() {
|
|
232
285
|
return (
|
|
233
|
-
this.
|
|
286
|
+
this.index.has('HaplotypeSamples') && this.index.has('HaplotypeLengths')
|
|
234
287
|
)
|
|
235
288
|
}
|
|
236
289
|
|
|
237
290
|
async haplotypeSampleInterval() {
|
|
238
|
-
const value =
|
|
291
|
+
const value = this.indexTags
|
|
292
|
+
? this.indexTags.get('haplotype_index_interval')
|
|
293
|
+
: await this.tag('haplotype_index_interval')
|
|
239
294
|
return value === undefined ? undefined : Number(value)
|
|
240
295
|
}
|
|
241
296
|
|
|
@@ -254,7 +309,7 @@ export class GBZBase {
|
|
|
254
309
|
|
|
255
310
|
async haplotypeSamplesInRange(minHandle: number, maxHandle: number) {
|
|
256
311
|
const samples: HaplotypeSample[] = []
|
|
257
|
-
for await (const key of this.
|
|
312
|
+
for await (const key of this.index.indexScanFrom('HaplotypeSamples', [
|
|
258
313
|
minHandle,
|
|
259
314
|
0,
|
|
260
315
|
])) {
|
|
@@ -262,7 +317,7 @@ export class GBZBase {
|
|
|
262
317
|
if (node > maxHandle) {
|
|
263
318
|
break
|
|
264
319
|
}
|
|
265
|
-
const row = await this.
|
|
320
|
+
const row = await this.index.byRowid(
|
|
266
321
|
'HaplotypeSamples',
|
|
267
322
|
num(key[2], 'HaplotypeSamples rowid'),
|
|
268
323
|
)
|
|
@@ -274,14 +329,11 @@ export class GBZBase {
|
|
|
274
329
|
}
|
|
275
330
|
|
|
276
331
|
async haplotypeSampleAt(node: number, offset: number) {
|
|
277
|
-
const key = await this.
|
|
278
|
-
node,
|
|
279
|
-
offset,
|
|
280
|
-
])
|
|
332
|
+
const key = await this.index.indexSeekLE('HaplotypeSamples', [node, offset])
|
|
281
333
|
if (key?.[0] !== node || key[1] !== offset) {
|
|
282
334
|
return undefined
|
|
283
335
|
}
|
|
284
|
-
const row = await this.
|
|
336
|
+
const row = await this.index.byRowid(
|
|
285
337
|
'HaplotypeSamples',
|
|
286
338
|
num(key[2], 'HaplotypeSamples rowid'),
|
|
287
339
|
)
|
|
@@ -289,7 +341,7 @@ export class GBZBase {
|
|
|
289
341
|
}
|
|
290
342
|
|
|
291
343
|
async haplotypeLength(pathHandle: number) {
|
|
292
|
-
const row = await this.
|
|
344
|
+
const row = await this.index.byRowid('HaplotypeLengths', pathHandle)
|
|
293
345
|
return row ? num(row[1], 'HaplotypeLengths.length') : undefined
|
|
294
346
|
}
|
|
295
347
|
|
package/src/gbwt/record.ts
CHANGED
|
@@ -139,6 +139,29 @@ export class GbwtRecord {
|
|
|
139
139
|
return undefined
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
decompressArrays() {
|
|
143
|
+
const offsets = this.edges.map(e => e.offset)
|
|
144
|
+
let total = 0
|
|
145
|
+
for (const run of this.runs()) {
|
|
146
|
+
total += run.len
|
|
147
|
+
}
|
|
148
|
+
const nodes = new Int32Array(total)
|
|
149
|
+
const nextOffsets = new Int32Array(total)
|
|
150
|
+
let i = 0
|
|
151
|
+
for (const run of this.runs()) {
|
|
152
|
+
const edge = this.edge(run.value)
|
|
153
|
+
let offset = offsets[run.value]!
|
|
154
|
+
for (let k = 0; k < run.len; k++) {
|
|
155
|
+
nodes[i] = edge.node
|
|
156
|
+
nextOffsets[i] = offset
|
|
157
|
+
offset += 1
|
|
158
|
+
i += 1
|
|
159
|
+
}
|
|
160
|
+
offsets[run.value] = offset
|
|
161
|
+
}
|
|
162
|
+
return { nodes, offsets: nextOffsets }
|
|
163
|
+
}
|
|
164
|
+
|
|
142
165
|
decompress(): Pos[] {
|
|
143
166
|
const offsets = this.edges.map(e => e.offset)
|
|
144
167
|
const result: Pos[] = []
|
package/src/graphName.ts
ADDED
|
@@ -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
CHANGED
|
@@ -6,7 +6,7 @@ export {
|
|
|
6
6
|
SchemaVersionError,
|
|
7
7
|
formatPathName,
|
|
8
8
|
} from './db.ts'
|
|
9
|
-
export type { GbzPath, HaplotypeSample, PathName } from './db.ts'
|
|
9
|
+
export type { GbzPath, HaplotypeSample, OpenOptions, PathName } from './db.ts'
|
|
10
10
|
export type { ByteSource } from './filehandle.ts'
|
|
11
11
|
export type { Pos } from './gbwt/record.ts'
|
|
12
12
|
export { Subgraph } from './subgraph.ts'
|
|
@@ -16,6 +16,7 @@ export type {
|
|
|
16
16
|
PathIdentity,
|
|
17
17
|
PathPosition,
|
|
18
18
|
ReferencePath,
|
|
19
|
+
SnarlOutput,
|
|
19
20
|
SubgraphJson,
|
|
20
21
|
SubgraphPath,
|
|
21
22
|
ToJsonOptions,
|
|
@@ -23,9 +24,11 @@ export type {
|
|
|
23
24
|
export {
|
|
24
25
|
subgraphAroundNodes,
|
|
25
26
|
subgraphAtOffset,
|
|
27
|
+
subgraphBetween,
|
|
26
28
|
subgraphInInterval,
|
|
27
29
|
} from './query.ts'
|
|
28
30
|
export type { PathQuery, QueryOptions } from './query.ts'
|
|
29
31
|
export { SqliteDatabase } from './sqlite/database.ts'
|
|
32
|
+
export type { GraphName } from './graphName.ts'
|
|
30
33
|
export { weightedLcs } from './lcs.ts'
|
|
31
34
|
export * as nodes from './gbwt/node.ts'
|
package/src/query.ts
CHANGED
|
@@ -2,11 +2,12 @@ import { GENERIC_SAMPLE } from './db.ts'
|
|
|
2
2
|
import { Subgraph } from './subgraph.ts'
|
|
3
3
|
|
|
4
4
|
import type { GBZBase, PathName } from './db.ts'
|
|
5
|
-
import type { HaplotypeOutput } from './subgraph.ts'
|
|
5
|
+
import type { HaplotypeOutput, SnarlOutput } from './subgraph.ts'
|
|
6
6
|
|
|
7
7
|
export interface QueryOptions {
|
|
8
8
|
context?: number
|
|
9
9
|
haplotypes?: HaplotypeOutput
|
|
10
|
+
snarls?: SnarlOutput
|
|
10
11
|
limit?: number
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -39,6 +40,7 @@ export async function subgraphAtOffset(
|
|
|
39
40
|
reference.position.nodeOffset,
|
|
40
41
|
opts.context ?? 100,
|
|
41
42
|
)
|
|
43
|
+
await subgraph.extractSnarls(opts.snarls ?? 'none')
|
|
42
44
|
subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
|
|
43
45
|
return subgraph
|
|
44
46
|
}
|
|
@@ -58,6 +60,7 @@ export async function subgraphInInterval(
|
|
|
58
60
|
end - start,
|
|
59
61
|
opts.context ?? 100,
|
|
60
62
|
)
|
|
63
|
+
await subgraph.extractSnarls(opts.snarls ?? 'none')
|
|
61
64
|
subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
|
|
62
65
|
return subgraph
|
|
63
66
|
}
|
|
@@ -68,12 +71,36 @@ export async function subgraphAroundNodes(
|
|
|
68
71
|
opts: QueryOptions = {},
|
|
69
72
|
) {
|
|
70
73
|
const haplotypes = opts.haplotypes ?? 'all'
|
|
74
|
+
const snarls = opts.snarls ?? 'none'
|
|
71
75
|
if (haplotypes === 'reference-only') {
|
|
72
76
|
throw new Error('Cannot output a reference path in a node-based query')
|
|
73
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
|
+
}
|
|
74
83
|
const subgraph = new Subgraph(db)
|
|
75
84
|
subgraph.limit = opts.limit
|
|
76
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)
|
|
77
104
|
subgraph.extractPaths(undefined, haplotypes)
|
|
78
105
|
return subgraph
|
|
79
106
|
}
|
package/src/sqlite/btree.ts
CHANGED
|
@@ -167,10 +167,15 @@ export class BTree {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
} else {
|
|
170
|
+
const children: number[] = []
|
|
170
171
|
for (let i = 0; i < header.cellCount; i++) {
|
|
171
|
-
|
|
172
|
+
children.push(readUint32(page, cellOffset(page, header, i)))
|
|
173
|
+
}
|
|
174
|
+
children.push(header.rightChild)
|
|
175
|
+
this.pager.prefetch(children)
|
|
176
|
+
for (const child of children) {
|
|
177
|
+
yield* this.tableScan(child)
|
|
172
178
|
}
|
|
173
|
-
yield* this.tableScan(header.rightChild)
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
181
|
|