@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.
- package/README.md +45 -4
- package/bin/query.js +3 -1
- package/dist/cli.js +85 -19
- package/dist/cli.js.map +1 -0
- package/dist/db.d.ts +14 -6
- package/dist/db.js +92 -28
- package/dist/db.js.map +1 -0
- package/dist/filehandle.js +1 -0
- package/dist/filehandle.js.map +1 -0
- package/dist/gbwt/bytecode.d.ts +1 -1
- package/dist/gbwt/bytecode.js +7 -3
- package/dist/gbwt/bytecode.js.map +1 -0
- package/dist/gbwt/node.js +7 -2
- package/dist/gbwt/node.js.map +1 -0
- package/dist/gbwt/record.d.ts +4 -0
- package/dist/gbwt/record.js +30 -2
- package/dist/gbwt/record.js.map +1 -0
- package/dist/gbwt/sequence.js +8 -1
- package/dist/gbwt/sequence.js.map +1 -0
- 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 +5 -4
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -0
- package/dist/lcs.js +32 -10
- package/dist/lcs.js.map +1 -0
- package/dist/query.d.ts +4 -2
- package/dist/query.js +19 -0
- package/dist/query.js.map +1 -0
- package/dist/sqlite/btree.d.ts +1 -1
- package/dist/sqlite/btree.js +24 -7
- package/dist/sqlite/btree.js.map +1 -0
- package/dist/sqlite/database.d.ts +1 -1
- package/dist/sqlite/database.js +12 -2
- package/dist/sqlite/database.js.map +1 -0
- package/dist/sqlite/pager.d.ts +8 -3
- package/dist/sqlite/pager.js +71 -12
- package/dist/sqlite/pager.js.map +1 -0
- package/dist/sqlite/record.js +3 -1
- package/dist/sqlite/record.js.map +1 -0
- package/dist/subgraph.d.ts +12 -2
- package/dist/subgraph.js +413 -84
- package/dist/subgraph.js.map +1 -0
- package/package.json +36 -11
- package/src/cli.ts +258 -0
- package/src/db.ts +374 -0
- package/src/filehandle.ts +4 -0
- package/src/gbwt/bytecode.ts +87 -0
- package/src/gbwt/node.ts +64 -0
- package/src/gbwt/record.ts +177 -0
- package/src/gbwt/sequence.ts +50 -0
- package/src/graphName.ts +94 -0
- package/src/index.ts +34 -0
- package/src/lcs.ts +282 -0
- package/src/query.ts +106 -0
- package/src/sqlite/btree.ts +260 -0
- package/src/sqlite/database.ts +117 -0
- package/src/sqlite/pager.ts +148 -0
- package/src/sqlite/record.ts +100 -0
- package/src/subgraph.ts +1353 -0
- package/tools/haplotype-index/src/main.rs +197 -163
package/src/subgraph.ts
ADDED
|
@@ -0,0 +1,1353 @@
|
|
|
1
|
+
import { formatPathName } from './db.ts'
|
|
2
|
+
import {
|
|
3
|
+
ENDMARKER,
|
|
4
|
+
edgeIsCanonical,
|
|
5
|
+
encodeNode,
|
|
6
|
+
entryOrientation,
|
|
7
|
+
entrySide,
|
|
8
|
+
exitOrientation,
|
|
9
|
+
exitSide,
|
|
10
|
+
flipNode,
|
|
11
|
+
flipSide,
|
|
12
|
+
isReverse,
|
|
13
|
+
nodeId,
|
|
14
|
+
nodeOrientation,
|
|
15
|
+
pathIsCanonical,
|
|
16
|
+
} from './gbwt/node.ts'
|
|
17
|
+
import { gfaHeaderLines, sha256Hex, subgraphName } from './graphName.ts'
|
|
18
|
+
import { weightedLcs } from './lcs.ts'
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
GBZBase,
|
|
22
|
+
GbzPath,
|
|
23
|
+
GbzRecord,
|
|
24
|
+
HaplotypeSample,
|
|
25
|
+
PathName,
|
|
26
|
+
} from './db.ts'
|
|
27
|
+
import type { NodeSide, Orientation } from './gbwt/node.ts'
|
|
28
|
+
import type { Pos } from './gbwt/record.ts'
|
|
29
|
+
|
|
30
|
+
export type HaplotypeOutput = 'all' | 'distinct' | 'reference-only' | 'none'
|
|
31
|
+
|
|
32
|
+
export type SnarlOutput = 'none' | 'contained' | 'overlapping'
|
|
33
|
+
|
|
34
|
+
type HandleType =
|
|
35
|
+
| { kind: 'snarl-exit'; snarl: [number, number] }
|
|
36
|
+
| { kind: 'chain' }
|
|
37
|
+
| { kind: 'regular' }
|
|
38
|
+
|
|
39
|
+
export interface PathPosition {
|
|
40
|
+
seqOffset: number
|
|
41
|
+
handle: number
|
|
42
|
+
nodeOffset: number
|
|
43
|
+
gbwtOffset: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ReferencePath {
|
|
47
|
+
position: PathPosition
|
|
48
|
+
name: PathName
|
|
49
|
+
handle: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PathIdentity {
|
|
53
|
+
pathHandle: number
|
|
54
|
+
name: PathName
|
|
55
|
+
orientation: Orientation
|
|
56
|
+
hapStart: number
|
|
57
|
+
hapEnd: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface PathInfo {
|
|
61
|
+
path: number[]
|
|
62
|
+
positions: Pos[]
|
|
63
|
+
len: number
|
|
64
|
+
weight: number | undefined
|
|
65
|
+
identity: PathIdentity | undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type EditOp = 'M' | 'I' | 'D'
|
|
69
|
+
type Edit = [EditOp, number]
|
|
70
|
+
|
|
71
|
+
export interface SubgraphPath {
|
|
72
|
+
name: string
|
|
73
|
+
weight?: number
|
|
74
|
+
cigar?: string
|
|
75
|
+
path: { id: string; is_reverse: boolean }[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SubgraphJson {
|
|
79
|
+
nodes: { id: string; sequence: string }[]
|
|
80
|
+
edges: {
|
|
81
|
+
from: string
|
|
82
|
+
from_is_reverse: boolean
|
|
83
|
+
to: string
|
|
84
|
+
to_is_reverse: boolean
|
|
85
|
+
}[]
|
|
86
|
+
paths: SubgraphPath[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface HaplotypeAlignment {
|
|
90
|
+
pathHandle: number | undefined
|
|
91
|
+
name: PathName | undefined
|
|
92
|
+
strand: '+' | '-'
|
|
93
|
+
hapStart: number | undefined
|
|
94
|
+
hapEnd: number | undefined
|
|
95
|
+
refStart: number
|
|
96
|
+
refEnd: number
|
|
97
|
+
cigar: string
|
|
98
|
+
weight: number | undefined
|
|
99
|
+
path: number[]
|
|
100
|
+
start: Pos
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ToJsonOptions {
|
|
104
|
+
names?: 'anonymous' | 'resolved'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sideBefore(
|
|
108
|
+
a: [number, number, NodeSide],
|
|
109
|
+
b: [number, number, NodeSide],
|
|
110
|
+
) {
|
|
111
|
+
return (
|
|
112
|
+
a[0] < b[0] ||
|
|
113
|
+
(a[0] === b[0] && (a[1] < b[1] || (a[1] === b[1] && a[2] < b[2])))
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
class SideQueue {
|
|
118
|
+
private heap: [number, number, NodeSide][] = []
|
|
119
|
+
|
|
120
|
+
push(distance: number, node: number, side: NodeSide) {
|
|
121
|
+
const heap = this.heap
|
|
122
|
+
heap.push([distance, node, side])
|
|
123
|
+
let i = heap.length - 1
|
|
124
|
+
while (i > 0) {
|
|
125
|
+
const parent = (i - 1) >> 1
|
|
126
|
+
if (sideBefore(heap[i]!, heap[parent]!)) {
|
|
127
|
+
;[heap[i], heap[parent]] = [heap[parent]!, heap[i]!]
|
|
128
|
+
i = parent
|
|
129
|
+
} else {
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
pop() {
|
|
136
|
+
const heap = this.heap
|
|
137
|
+
const top = heap[0]
|
|
138
|
+
const last = heap.pop()
|
|
139
|
+
if (heap.length > 0 && last !== undefined) {
|
|
140
|
+
heap[0] = last
|
|
141
|
+
let i = 0
|
|
142
|
+
for (;;) {
|
|
143
|
+
const left = 2 * i + 1
|
|
144
|
+
const right = left + 1
|
|
145
|
+
let smallest = i
|
|
146
|
+
if (left < heap.length && sideBefore(heap[left]!, heap[smallest]!)) {
|
|
147
|
+
smallest = left
|
|
148
|
+
}
|
|
149
|
+
if (right < heap.length && sideBefore(heap[right]!, heap[smallest]!)) {
|
|
150
|
+
smallest = right
|
|
151
|
+
}
|
|
152
|
+
if (smallest === i) {
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
;[heap[i], heap[smallest]] = [heap[smallest]!, heap[i]!]
|
|
156
|
+
i = smallest
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return top
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get size() {
|
|
163
|
+
return this.heap.length
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function posKey(pos: Pos) {
|
|
168
|
+
return `${pos.node}:${pos.offset}`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface Anchor {
|
|
172
|
+
pathHandle: number
|
|
173
|
+
orientation: Orientation
|
|
174
|
+
base: number
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export class Subgraph {
|
|
178
|
+
private records = new Map<number, GbzRecord>()
|
|
179
|
+
private paths: PathInfo[] = []
|
|
180
|
+
private refId: number | undefined
|
|
181
|
+
private refPath: PathName | undefined
|
|
182
|
+
private refHandle: number | undefined
|
|
183
|
+
private refInterval: [number, number] | undefined
|
|
184
|
+
private refIndexCache: Map<number, number[]> | undefined
|
|
185
|
+
private refPrefixCache: number[] | undefined
|
|
186
|
+
limit: number | undefined
|
|
187
|
+
readonly stats = {
|
|
188
|
+
orderedAlignments: 0,
|
|
189
|
+
lcsAlignments: 0,
|
|
190
|
+
identificationSteps: 0,
|
|
191
|
+
identificationFetches: 0,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private db: GBZBase
|
|
195
|
+
|
|
196
|
+
constructor(db: GBZBase) {
|
|
197
|
+
this.db = db
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get nodeCount() {
|
|
201
|
+
return this.records.size / 2
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
get pathCount() {
|
|
205
|
+
return this.paths.length
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
get referenceInterval() {
|
|
209
|
+
return this.refInterval && this.refPath
|
|
210
|
+
? {
|
|
211
|
+
name: this.refPath,
|
|
212
|
+
start: this.refPath.fragment + this.refInterval[0],
|
|
213
|
+
end: this.refPath.fragment + this.refInterval[1],
|
|
214
|
+
}
|
|
215
|
+
: undefined
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
hasNode(id: number) {
|
|
219
|
+
return this.records.has(encodeNode(id, 'forward'))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
hasHandle(handle: number) {
|
|
223
|
+
return this.records.has(handle)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private record(handle: number) {
|
|
227
|
+
const record = this.records.get(handle)
|
|
228
|
+
if (!record) {
|
|
229
|
+
throw new Error(`Subgraph has no record for handle ${handle}`)
|
|
230
|
+
}
|
|
231
|
+
return record
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private sortedHandles() {
|
|
235
|
+
return [...this.records.keys()].sort((a, b) => a - b)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private async addNode(id: number) {
|
|
239
|
+
if (this.limit !== undefined && this.nodeCount >= this.limit) {
|
|
240
|
+
throw new Error(`Subgraph size limit of ${this.limit} nodes exceeded`)
|
|
241
|
+
}
|
|
242
|
+
const forward = await this.db.getRecord(encodeNode(id, 'forward'))
|
|
243
|
+
const reverse = await this.db.getRecord(encodeNode(id, 'reverse'))
|
|
244
|
+
if (!forward || !reverse) {
|
|
245
|
+
throw new Error(`Node ${id} does not exist in the graph`)
|
|
246
|
+
}
|
|
247
|
+
this.records.set(forward.handle, forward)
|
|
248
|
+
this.records.set(reverse.handle, reverse)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private async ensureNode(id: number) {
|
|
252
|
+
if (!this.hasNode(id)) {
|
|
253
|
+
await this.addNode(id)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private clearPaths() {
|
|
258
|
+
this.paths = []
|
|
259
|
+
this.refId = undefined
|
|
260
|
+
this.refPath = undefined
|
|
261
|
+
this.refHandle = undefined
|
|
262
|
+
this.refInterval = undefined
|
|
263
|
+
this.refIndexCache = undefined
|
|
264
|
+
this.refPrefixCache = undefined
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async pathPosition(query: PathName): Promise<ReferencePath> {
|
|
268
|
+
const path = await this.db.findPath(query)
|
|
269
|
+
if (!path) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Cannot find a path covering ${formatPathName(query, query.fragment)}`,
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
if (!path.isIndexed) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Path ${formatPathName(path.name, path.name.fragment)} has not been indexed for random access`,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
const queryOffset = query.fragment - path.name.fragment
|
|
280
|
+
const indexed = await this.db.indexedPosition(path.handle, queryOffset)
|
|
281
|
+
if (!indexed) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Path ${formatPathName(path.name, path.name.fragment)} has not been indexed for random access`,
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
return this.findPathPosition(
|
|
287
|
+
path,
|
|
288
|
+
queryOffset,
|
|
289
|
+
indexed.pathOffset,
|
|
290
|
+
indexed.pos,
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private async findPathPosition(
|
|
295
|
+
path: GbzPath,
|
|
296
|
+
queryOffset: number,
|
|
297
|
+
startOffset: number,
|
|
298
|
+
start: Pos,
|
|
299
|
+
): Promise<ReferencePath> {
|
|
300
|
+
let pathOffset = startOffset
|
|
301
|
+
let pos = start
|
|
302
|
+
for (;;) {
|
|
303
|
+
await this.ensureNode(nodeId(pos.node))
|
|
304
|
+
const record = this.record(pos.node)
|
|
305
|
+
if (pathOffset + record.sequenceLen > queryOffset) {
|
|
306
|
+
return {
|
|
307
|
+
position: {
|
|
308
|
+
seqOffset: queryOffset,
|
|
309
|
+
handle: pos.node,
|
|
310
|
+
nodeOffset: queryOffset - pathOffset,
|
|
311
|
+
gbwtOffset: pos.offset,
|
|
312
|
+
},
|
|
313
|
+
name: path.name,
|
|
314
|
+
handle: path.handle,
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
pathOffset += record.sequenceLen
|
|
318
|
+
const next = record.gbwt().lf(pos.offset)
|
|
319
|
+
if (!next) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Path ${formatPathName(path.name, path.name.fragment)} does not contain offset ${queryOffset}`,
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
pos = next
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async aroundPosition(handle: number, nodeOffset: number, context: number) {
|
|
329
|
+
const id = nodeId(handle)
|
|
330
|
+
await this.ensureNode(id)
|
|
331
|
+
const record = this.record(handle)
|
|
332
|
+
const orientation = nodeOrientation(handle)
|
|
333
|
+
const active = new SideQueue()
|
|
334
|
+
active.push(nodeOffset, id, entrySide(orientation))
|
|
335
|
+
active.push(record.sequenceLen - nodeOffset - 1, id, exitSide(orientation))
|
|
336
|
+
return this.insertContext(active, context)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async aroundInterval(start: PathPosition, len: number, context: number) {
|
|
340
|
+
if (len === 0) {
|
|
341
|
+
throw new Error('Interval length must be greater than 0')
|
|
342
|
+
}
|
|
343
|
+
let pos: Pos = { node: start.handle, offset: start.gbwtOffset }
|
|
344
|
+
let offset = start.nodeOffset
|
|
345
|
+
let remaining = len
|
|
346
|
+
const active = new SideQueue()
|
|
347
|
+
for (;;) {
|
|
348
|
+
const id = nodeId(pos.node)
|
|
349
|
+
const orientation = nodeOrientation(pos.node)
|
|
350
|
+
await this.ensureNode(id)
|
|
351
|
+
const record = this.record(pos.node)
|
|
352
|
+
if (offset >= record.sequenceLen) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Offset ${offset} in node ${id} of length ${record.sequenceLen}`,
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
active.push(offset, id, entrySide(orientation))
|
|
358
|
+
const distanceToNext = record.sequenceLen - offset
|
|
359
|
+
if (remaining <= distanceToNext) {
|
|
360
|
+
active.push(
|
|
361
|
+
remaining === distanceToNext ? 0 : distanceToNext - remaining - 1,
|
|
362
|
+
id,
|
|
363
|
+
exitSide(orientation),
|
|
364
|
+
)
|
|
365
|
+
break
|
|
366
|
+
}
|
|
367
|
+
active.push(0, id, exitSide(orientation))
|
|
368
|
+
const next = record.gbwt().lf(pos.offset)
|
|
369
|
+
if (!next) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
`No successor for GBWT position (${pos.node}, ${pos.offset})`,
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
pos = next
|
|
375
|
+
offset = 0
|
|
376
|
+
remaining -= distanceToNext
|
|
377
|
+
}
|
|
378
|
+
return this.insertContext(active, context)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async aroundNodes(nodes: Iterable<number>, context: number) {
|
|
382
|
+
const active = new SideQueue()
|
|
383
|
+
for (const id of nodes) {
|
|
384
|
+
await this.ensureNode(id)
|
|
385
|
+
active.push(0, id, 'left')
|
|
386
|
+
active.push(0, id, 'right')
|
|
387
|
+
}
|
|
388
|
+
return this.insertContext(active, context)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private async insertContext(active: SideQueue, context: number) {
|
|
392
|
+
this.clearPaths()
|
|
393
|
+
const visited = new Set<string>()
|
|
394
|
+
const toRemove = new Set<number>()
|
|
395
|
+
for (const handle of this.records.keys()) {
|
|
396
|
+
toRemove.add(nodeId(handle))
|
|
397
|
+
}
|
|
398
|
+
let inserted = 0
|
|
399
|
+
while (active.size > 0) {
|
|
400
|
+
const [distance, id, side] = active.pop()!
|
|
401
|
+
const key = `${id}:${side}`
|
|
402
|
+
if (visited.has(key)) {
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
405
|
+
visited.add(key)
|
|
406
|
+
toRemove.delete(id)
|
|
407
|
+
if (!this.hasNode(id)) {
|
|
408
|
+
await this.addNode(id)
|
|
409
|
+
inserted += 1
|
|
410
|
+
}
|
|
411
|
+
const otherSide = flipSide(side)
|
|
412
|
+
if (!visited.has(`${id}:${otherSide}`)) {
|
|
413
|
+
const record = this.record(encodeNode(id, entryOrientation(side)))
|
|
414
|
+
const nextDistance = distance + record.sequenceLen - 1
|
|
415
|
+
if (nextDistance <= context) {
|
|
416
|
+
active.push(nextDistance, id, otherSide)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const record = this.record(encodeNode(id, exitOrientation(side)))
|
|
420
|
+
const nextDistance = distance + 1
|
|
421
|
+
if (nextDistance <= context) {
|
|
422
|
+
for (const successor of record.successors()) {
|
|
423
|
+
const successorId = nodeId(successor)
|
|
424
|
+
const successorSide = entrySide(nodeOrientation(successor))
|
|
425
|
+
if (!visited.has(`${successorId}:${successorSide}`)) {
|
|
426
|
+
active.push(nextDistance, successorId, successorSide)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const id of toRemove) {
|
|
432
|
+
this.records.delete(encodeNode(id, 'forward'))
|
|
433
|
+
this.records.delete(encodeNode(id, 'reverse'))
|
|
434
|
+
}
|
|
435
|
+
return { inserted, removed: toRemove.size }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async betweenNodes(start: number, end: number) {
|
|
439
|
+
this.clearPaths()
|
|
440
|
+
const active = [start, flipNode(end)]
|
|
441
|
+
const visited = new Set([nodeId(start), nodeId(end)])
|
|
442
|
+
let inserted = 0
|
|
443
|
+
while (active.length > 0) {
|
|
444
|
+
const curr = active.pop()!
|
|
445
|
+
const id = nodeId(curr)
|
|
446
|
+
if (!this.hasNode(id)) {
|
|
447
|
+
await this.addNode(id)
|
|
448
|
+
inserted += 1
|
|
449
|
+
}
|
|
450
|
+
for (const successor of this.record(curr).successors()) {
|
|
451
|
+
const successorId = nodeId(successor)
|
|
452
|
+
if (!visited.has(successorId)) {
|
|
453
|
+
active.push(successor, flipNode(successor))
|
|
454
|
+
visited.add(successorId)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return inserted
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async extractSnarls(snarls: SnarlOutput) {
|
|
462
|
+
let inserted = 0
|
|
463
|
+
for (const [start, end] of await this.overlappingSnarls(snarls)) {
|
|
464
|
+
inserted += await this.betweenNodes(start, end)
|
|
465
|
+
}
|
|
466
|
+
return inserted
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private async overlappingSnarls(snarls: SnarlOutput) {
|
|
470
|
+
const result: [number, number][] = []
|
|
471
|
+
if (snarls !== 'none') {
|
|
472
|
+
let foundLink = false
|
|
473
|
+
for (const handle of this.sortedHandles()) {
|
|
474
|
+
const record = this.record(handle)
|
|
475
|
+
const next = record.next
|
|
476
|
+
if (next !== undefined) {
|
|
477
|
+
foundLink = true
|
|
478
|
+
if (this.hasHandle(next)) {
|
|
479
|
+
if (edgeIsCanonical(handle, next)) {
|
|
480
|
+
result.push([handle, next])
|
|
481
|
+
}
|
|
482
|
+
} else if (
|
|
483
|
+
snarls === 'overlapping' &&
|
|
484
|
+
this.isSnarlEntryInSubgraph(record)
|
|
485
|
+
) {
|
|
486
|
+
result.push([handle, next])
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (
|
|
491
|
+
!foundLink &&
|
|
492
|
+
snarls === 'overlapping' &&
|
|
493
|
+
(await this.db.hasChainLinks())
|
|
494
|
+
) {
|
|
495
|
+
const covering = await this.findCoveringSnarl()
|
|
496
|
+
if (covering) {
|
|
497
|
+
result.push(covering)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return result
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private isSnarlEntryInSubgraph(record: GbzRecord) {
|
|
505
|
+
const successors = record.successors()
|
|
506
|
+
const first = successors.find(handle => this.hasHandle(handle))
|
|
507
|
+
return first === undefined
|
|
508
|
+
? false
|
|
509
|
+
: successors.length > 1 ||
|
|
510
|
+
this.record(flipNode(first)).successors().length > 1
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private recordReader(onFetch?: () => void) {
|
|
514
|
+
const outside = new Map<number, GbzRecord>()
|
|
515
|
+
return async (handle: number) => {
|
|
516
|
+
const inside = this.records.get(handle)
|
|
517
|
+
if (inside) {
|
|
518
|
+
return inside
|
|
519
|
+
}
|
|
520
|
+
let record = outside.get(handle)
|
|
521
|
+
if (!record) {
|
|
522
|
+
record = await this.db.getRecord(handle)
|
|
523
|
+
onFetch?.()
|
|
524
|
+
if (!record) {
|
|
525
|
+
throw new Error(`Node record ${handle} is missing from the database`)
|
|
526
|
+
}
|
|
527
|
+
outside.set(handle, record)
|
|
528
|
+
}
|
|
529
|
+
return record
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private async findCoveringSnarl() {
|
|
534
|
+
const read = this.recordReader()
|
|
535
|
+
const isSnarlEntry = async (record: GbzRecord) => {
|
|
536
|
+
const successors = record.successors()
|
|
537
|
+
const first = successors[0]
|
|
538
|
+
return first === undefined
|
|
539
|
+
? false
|
|
540
|
+
: successors.length > 1 ||
|
|
541
|
+
(await read(flipNode(first))).successors().length > 1
|
|
542
|
+
}
|
|
543
|
+
const classify = async (handle: number): Promise<HandleType> => {
|
|
544
|
+
const reverse = await read(flipNode(handle))
|
|
545
|
+
return reverse.next !== undefined
|
|
546
|
+
? (await isSnarlEntry(reverse))
|
|
547
|
+
? { kind: 'snarl-exit', snarl: [flipNode(handle), reverse.next] }
|
|
548
|
+
: { kind: 'chain' }
|
|
549
|
+
: (await read(handle)).next !== undefined
|
|
550
|
+
? { kind: 'chain' }
|
|
551
|
+
: { kind: 'regular' }
|
|
552
|
+
}
|
|
553
|
+
const visited = new Set<number>()
|
|
554
|
+
const queue = this.sortedHandles().flatMap(handle =>
|
|
555
|
+
this.record(handle).successors(),
|
|
556
|
+
)
|
|
557
|
+
let result: [number, number] | undefined
|
|
558
|
+
let done = false
|
|
559
|
+
while (!done && queue.length > 0) {
|
|
560
|
+
const handle = queue.shift()!
|
|
561
|
+
const id = nodeId(handle)
|
|
562
|
+
if (!this.hasHandle(handle) && !visited.has(id)) {
|
|
563
|
+
visited.add(id)
|
|
564
|
+
const type = await classify(handle)
|
|
565
|
+
if (type.kind === 'snarl-exit') {
|
|
566
|
+
result = type.snarl
|
|
567
|
+
done = true
|
|
568
|
+
} else if (type.kind === 'chain') {
|
|
569
|
+
done = true
|
|
570
|
+
} else {
|
|
571
|
+
for (const orientation of ['forward', 'reverse'] as const) {
|
|
572
|
+
queue.push(
|
|
573
|
+
...(await read(encodeNode(id, orientation))).successors(),
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return result
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
extractPaths(reference: ReferencePath | undefined, output: HaplotypeOutput) {
|
|
583
|
+
this.clearPaths()
|
|
584
|
+
if (output === 'none') {
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
const refPos = reference?.position
|
|
588
|
+
this.refPath = reference?.name
|
|
589
|
+
this.refHandle = reference?.handle
|
|
590
|
+
const handles = this.sortedHandles()
|
|
591
|
+
const successors = new Map<
|
|
592
|
+
number,
|
|
593
|
+
{ nodes: Int32Array; offsets: Int32Array; hasPredecessor: Uint8Array }
|
|
594
|
+
>()
|
|
595
|
+
for (const handle of handles) {
|
|
596
|
+
const { nodes, offsets } = this.record(handle).gbwt().decompressArrays()
|
|
597
|
+
successors.set(handle, {
|
|
598
|
+
nodes,
|
|
599
|
+
offsets,
|
|
600
|
+
hasPredecessor: new Uint8Array(nodes.length),
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
for (const handle of handles) {
|
|
604
|
+
const { nodes, offsets } = successors.get(handle)!
|
|
605
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
606
|
+
const entry = successors.get(nodes[i]!)
|
|
607
|
+
if (entry) {
|
|
608
|
+
entry.hasPredecessor[offsets[i]!] = 1
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
let refOffset: number | undefined
|
|
613
|
+
for (const handle of handles) {
|
|
614
|
+
const entries = successors.get(handle)!
|
|
615
|
+
for (let offset = 0; offset < entries.nodes.length; offset++) {
|
|
616
|
+
if (entries.hasPredecessor[offset] === 0) {
|
|
617
|
+
let currNode: number | undefined = handle
|
|
618
|
+
let currOffset = offset
|
|
619
|
+
let isRef = false
|
|
620
|
+
const path: number[] = []
|
|
621
|
+
const positions: Pos[] = []
|
|
622
|
+
let len = 0
|
|
623
|
+
while (currNode !== undefined) {
|
|
624
|
+
if (
|
|
625
|
+
currNode === refPos?.handle &&
|
|
626
|
+
currOffset === refPos.gbwtOffset
|
|
627
|
+
) {
|
|
628
|
+
this.refId = this.paths.length
|
|
629
|
+
refOffset = path.length
|
|
630
|
+
isRef = true
|
|
631
|
+
}
|
|
632
|
+
path.push(currNode)
|
|
633
|
+
positions.push({ node: currNode, offset: currOffset })
|
|
634
|
+
len += this.record(currNode).sequenceLen
|
|
635
|
+
const step = successors.get(currNode)!
|
|
636
|
+
const nextNode: number = step.nodes[currOffset]!
|
|
637
|
+
const nextOffset: number = step.offsets[currOffset]!
|
|
638
|
+
if (nextNode !== ENDMARKER && successors.has(nextNode)) {
|
|
639
|
+
currNode = nextNode
|
|
640
|
+
currOffset = nextOffset
|
|
641
|
+
} else {
|
|
642
|
+
currNode = undefined
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (isRef || pathIsCanonical(path)) {
|
|
646
|
+
this.paths.push({
|
|
647
|
+
path,
|
|
648
|
+
positions,
|
|
649
|
+
len,
|
|
650
|
+
weight: undefined,
|
|
651
|
+
identity: undefined,
|
|
652
|
+
})
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (refPos) {
|
|
658
|
+
if (refOffset === undefined || this.refId === undefined) {
|
|
659
|
+
this.clearPaths()
|
|
660
|
+
throw new Error('Could not find the reference path')
|
|
661
|
+
}
|
|
662
|
+
const info = this.paths[this.refId]!
|
|
663
|
+
let before = refPos.nodeOffset
|
|
664
|
+
for (const handle of info.path.slice(0, refOffset)) {
|
|
665
|
+
before += this.record(handle).sequenceLen
|
|
666
|
+
}
|
|
667
|
+
const start = refPos.seqOffset - before
|
|
668
|
+
this.refInterval = [start, start + info.len]
|
|
669
|
+
info.identity = {
|
|
670
|
+
pathHandle: reference.handle,
|
|
671
|
+
name: reference.name,
|
|
672
|
+
orientation: 'forward',
|
|
673
|
+
hapStart: start,
|
|
674
|
+
hapEnd: start + info.len,
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (output === 'distinct') {
|
|
678
|
+
this.distinctPaths()
|
|
679
|
+
} else if (output === 'reference-only') {
|
|
680
|
+
if (this.refId === undefined) {
|
|
681
|
+
throw new Error('Reference path is required for reference-only output')
|
|
682
|
+
}
|
|
683
|
+
this.paths = [this.paths[this.refId]!]
|
|
684
|
+
this.refId = 0
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private distinctPaths() {
|
|
689
|
+
const refPath =
|
|
690
|
+
this.refId === undefined ? undefined : this.paths[this.refId]!.path
|
|
691
|
+
this.paths.sort((x, y) => comparePaths(x.path, y.path) || x.len - y.len)
|
|
692
|
+
const merged: PathInfo[] = []
|
|
693
|
+
let refId: number | undefined
|
|
694
|
+
for (const info of this.paths) {
|
|
695
|
+
const last = merged[merged.length - 1]
|
|
696
|
+
if (last && comparePaths(last.path, info.path) === 0) {
|
|
697
|
+
last.weight = (last.weight ?? 0) + 1
|
|
698
|
+
} else {
|
|
699
|
+
if (refPath && comparePaths(info.path, refPath) === 0) {
|
|
700
|
+
refId = merged.length
|
|
701
|
+
}
|
|
702
|
+
merged.push({ ...info, weight: 1 })
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
this.paths = merged
|
|
706
|
+
this.refId = refId
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async identifyPaths() {
|
|
710
|
+
if (!this.db.hasHaplotypeIndex) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
'The database has no HaplotypeSamples table; run gbz-haplotype-index on it',
|
|
713
|
+
)
|
|
714
|
+
}
|
|
715
|
+
const interval = (await this.db.haplotypeSampleInterval()) ?? 4096
|
|
716
|
+
const handles = this.sortedHandles()
|
|
717
|
+
const minHandle = handles[0]
|
|
718
|
+
const maxHandle = handles[handles.length - 1]
|
|
719
|
+
if (minHandle === undefined || maxHandle === undefined) {
|
|
720
|
+
return
|
|
721
|
+
}
|
|
722
|
+
const samples = new Map<string, HaplotypeSample>()
|
|
723
|
+
for (const sample of await this.db.haplotypeSamplesInRange(
|
|
724
|
+
minHandle,
|
|
725
|
+
maxHandle,
|
|
726
|
+
)) {
|
|
727
|
+
samples.set(posKey(sample), sample)
|
|
728
|
+
}
|
|
729
|
+
const starts = new Map<string, number>()
|
|
730
|
+
this.paths.forEach((info, index) => {
|
|
731
|
+
const first = info.positions[0]
|
|
732
|
+
if (first && index !== this.refId) {
|
|
733
|
+
starts.set(posKey(first), index)
|
|
734
|
+
}
|
|
735
|
+
})
|
|
736
|
+
const recordAt = this.recordReader(() => {
|
|
737
|
+
this.stats.identificationFetches += 1
|
|
738
|
+
})
|
|
739
|
+
const sampleAt = async (pos: Pos) => {
|
|
740
|
+
if (pos.node >= minHandle && pos.node <= maxHandle) {
|
|
741
|
+
return samples.get(posKey(pos))
|
|
742
|
+
}
|
|
743
|
+
this.stats.identificationFetches += 1
|
|
744
|
+
return this.db.haplotypeSampleAt(pos.node, pos.offset)
|
|
745
|
+
}
|
|
746
|
+
const names = new Map<number, PathName>()
|
|
747
|
+
const nameOf = async (pathHandle: number) => {
|
|
748
|
+
let name = names.get(pathHandle)
|
|
749
|
+
if (!name) {
|
|
750
|
+
const path = await this.db.getPath(pathHandle)
|
|
751
|
+
if (!path) {
|
|
752
|
+
throw new Error(`Path ${pathHandle} is missing from the database`)
|
|
753
|
+
}
|
|
754
|
+
name = path.name
|
|
755
|
+
names.set(pathHandle, name)
|
|
756
|
+
}
|
|
757
|
+
return name
|
|
758
|
+
}
|
|
759
|
+
const anchorFromSample = (
|
|
760
|
+
sample: HaplotypeSample,
|
|
761
|
+
counter: number,
|
|
762
|
+
nodeLen: number,
|
|
763
|
+
): Anchor =>
|
|
764
|
+
sample.orientation === 'forward'
|
|
765
|
+
? {
|
|
766
|
+
pathHandle: sample.pathHandle,
|
|
767
|
+
orientation: 'forward',
|
|
768
|
+
base: sample.pathOffset - counter,
|
|
769
|
+
}
|
|
770
|
+
: {
|
|
771
|
+
pathHandle: sample.pathHandle,
|
|
772
|
+
orientation: 'reverse',
|
|
773
|
+
base: sample.pathOffset + counter + nodeLen,
|
|
774
|
+
}
|
|
775
|
+
const anchorFromIdentity = (
|
|
776
|
+
identity: PathIdentity,
|
|
777
|
+
counter: number,
|
|
778
|
+
): Anchor =>
|
|
779
|
+
identity.orientation === 'forward'
|
|
780
|
+
? {
|
|
781
|
+
pathHandle: identity.pathHandle,
|
|
782
|
+
orientation: 'forward',
|
|
783
|
+
base: identity.hapStart - counter,
|
|
784
|
+
}
|
|
785
|
+
: {
|
|
786
|
+
pathHandle: identity.pathHandle,
|
|
787
|
+
orientation: 'reverse',
|
|
788
|
+
base: identity.hapEnd + counter,
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
for (let start = 0; start < this.paths.length; start++) {
|
|
792
|
+
const startInfo = this.paths[start]!
|
|
793
|
+
if (start === this.refId || startInfo.identity) {
|
|
794
|
+
continue
|
|
795
|
+
}
|
|
796
|
+
const chain: { index: number; startBp: number }[] = []
|
|
797
|
+
const visited = new Set<number>()
|
|
798
|
+
let anchor: Anchor | undefined
|
|
799
|
+
let counter = 0
|
|
800
|
+
let current: number | undefined = start
|
|
801
|
+
let pos: Pos | undefined
|
|
802
|
+
while (anchor === undefined) {
|
|
803
|
+
if (current !== undefined) {
|
|
804
|
+
if (visited.has(current)) {
|
|
805
|
+
break
|
|
806
|
+
}
|
|
807
|
+
visited.add(current)
|
|
808
|
+
const info = this.paths[current]!
|
|
809
|
+
chain.push({ index: current, startBp: counter })
|
|
810
|
+
let bp = counter
|
|
811
|
+
for (const position of info.positions) {
|
|
812
|
+
const sample = samples.get(posKey(position))
|
|
813
|
+
const nodeLen = this.record(position.node).sequenceLen
|
|
814
|
+
if (sample) {
|
|
815
|
+
anchor = anchorFromSample(sample, bp, nodeLen)
|
|
816
|
+
break
|
|
817
|
+
}
|
|
818
|
+
bp += nodeLen
|
|
819
|
+
}
|
|
820
|
+
counter += info.len
|
|
821
|
+
if (anchor) {
|
|
822
|
+
break
|
|
823
|
+
}
|
|
824
|
+
const last = info.positions[info.positions.length - 1]!
|
|
825
|
+
pos = this.record(last.node).gbwt().lf(last.offset)
|
|
826
|
+
current = undefined
|
|
827
|
+
}
|
|
828
|
+
if (pos === undefined || pos.node === ENDMARKER) {
|
|
829
|
+
break
|
|
830
|
+
}
|
|
831
|
+
const known = starts.get(posKey(pos))
|
|
832
|
+
if (known !== undefined) {
|
|
833
|
+
const identity = this.paths[known]!.identity
|
|
834
|
+
if (identity) {
|
|
835
|
+
anchor = anchorFromIdentity(identity, counter)
|
|
836
|
+
break
|
|
837
|
+
}
|
|
838
|
+
current = known
|
|
839
|
+
continue
|
|
840
|
+
}
|
|
841
|
+
const sample = await sampleAt(pos)
|
|
842
|
+
const record = await recordAt(pos.node)
|
|
843
|
+
if (sample) {
|
|
844
|
+
anchor = anchorFromSample(sample, counter, record.sequenceLen)
|
|
845
|
+
break
|
|
846
|
+
}
|
|
847
|
+
this.stats.identificationSteps += 1
|
|
848
|
+
if (
|
|
849
|
+
counter - (chain[chain.length - 1] as { startBp: number }).startBp >
|
|
850
|
+
4 * interval + 4 * record.sequenceLen
|
|
851
|
+
) {
|
|
852
|
+
break
|
|
853
|
+
}
|
|
854
|
+
counter += record.sequenceLen
|
|
855
|
+
pos = record.gbwt().lf(pos.offset)
|
|
856
|
+
}
|
|
857
|
+
if (anchor) {
|
|
858
|
+
const name = await nameOf(anchor.pathHandle)
|
|
859
|
+
for (const { index, startBp } of chain) {
|
|
860
|
+
const info = this.paths[index]!
|
|
861
|
+
info.identity =
|
|
862
|
+
anchor.orientation === 'forward'
|
|
863
|
+
? {
|
|
864
|
+
pathHandle: anchor.pathHandle,
|
|
865
|
+
name,
|
|
866
|
+
orientation: 'forward',
|
|
867
|
+
hapStart: anchor.base + startBp,
|
|
868
|
+
hapEnd: anchor.base + startBp + info.len,
|
|
869
|
+
}
|
|
870
|
+
: {
|
|
871
|
+
pathHandle: anchor.pathHandle,
|
|
872
|
+
name,
|
|
873
|
+
orientation: 'reverse',
|
|
874
|
+
hapStart: anchor.base - startBp - info.len,
|
|
875
|
+
hapEnd: anchor.base - startBp,
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private refIndex(ref: number[]) {
|
|
883
|
+
if (this.refIndexCache === undefined) {
|
|
884
|
+
const index = new Map<number, number[]>()
|
|
885
|
+
ref.forEach((handle, i) => {
|
|
886
|
+
const occurrences = index.get(handle)
|
|
887
|
+
if (occurrences) {
|
|
888
|
+
occurrences.push(i)
|
|
889
|
+
} else {
|
|
890
|
+
index.set(handle, [i])
|
|
891
|
+
}
|
|
892
|
+
})
|
|
893
|
+
this.refIndexCache = index
|
|
894
|
+
}
|
|
895
|
+
return this.refIndexCache
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private refPrefix(ref: number[]) {
|
|
899
|
+
if (this.refPrefixCache === undefined) {
|
|
900
|
+
const prefix = [0]
|
|
901
|
+
ref.forEach((handle, i) => {
|
|
902
|
+
prefix.push(prefix[i]! + this.record(handle).sequenceLen)
|
|
903
|
+
})
|
|
904
|
+
this.refPrefixCache = prefix
|
|
905
|
+
}
|
|
906
|
+
return this.refPrefixCache
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private orderedMatches(
|
|
910
|
+
path: number[],
|
|
911
|
+
ref: number[],
|
|
912
|
+
): [number, number][] | undefined {
|
|
913
|
+
const index = this.refIndex(ref)
|
|
914
|
+
const pairs: [number, number][] = []
|
|
915
|
+
let last = -1
|
|
916
|
+
for (let i = 0; i < path.length; i++) {
|
|
917
|
+
const occurrences = index.get(path[i]!)
|
|
918
|
+
if (occurrences) {
|
|
919
|
+
const j = occurrences.find(x => x > last)
|
|
920
|
+
if (j === undefined) {
|
|
921
|
+
return undefined
|
|
922
|
+
}
|
|
923
|
+
pairs.push([i, j])
|
|
924
|
+
last = j
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return pairs
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
private pathLen(path: number[]) {
|
|
931
|
+
let total = 0
|
|
932
|
+
for (const handle of path) {
|
|
933
|
+
total += this.record(handle).sequenceLen
|
|
934
|
+
}
|
|
935
|
+
return total
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private prefixMatches(path: number[], ref: number[]) {
|
|
939
|
+
let result = 0
|
|
940
|
+
let pi = 0
|
|
941
|
+
let ri = 0
|
|
942
|
+
let pb = 0
|
|
943
|
+
let rb = 0
|
|
944
|
+
while (pi < path.length && ri < ref.length) {
|
|
945
|
+
const a = this.record(path[pi]!).sequence
|
|
946
|
+
const b = this.record(ref[ri]!).sequence
|
|
947
|
+
while (pb < a.length && rb < b.length) {
|
|
948
|
+
if (a[pb] !== b[rb]) {
|
|
949
|
+
return result
|
|
950
|
+
}
|
|
951
|
+
pb += 1
|
|
952
|
+
rb += 1
|
|
953
|
+
result += 1
|
|
954
|
+
}
|
|
955
|
+
if (pb === a.length) {
|
|
956
|
+
pi += 1
|
|
957
|
+
pb = 0
|
|
958
|
+
}
|
|
959
|
+
if (rb === b.length) {
|
|
960
|
+
ri += 1
|
|
961
|
+
rb = 0
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return result
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
private suffixMatches(path: number[], ref: number[]) {
|
|
968
|
+
let result = 0
|
|
969
|
+
let pi = 0
|
|
970
|
+
let ri = 0
|
|
971
|
+
let pb = 0
|
|
972
|
+
let rb = 0
|
|
973
|
+
while (pi < path.length && ri < ref.length) {
|
|
974
|
+
const a = this.record(path[path.length - pi - 1]!).sequence
|
|
975
|
+
const b = this.record(ref[ref.length - ri - 1]!).sequence
|
|
976
|
+
while (pb < a.length && rb < b.length) {
|
|
977
|
+
if (a[a.length - pb - 1] !== b[b.length - rb - 1]) {
|
|
978
|
+
return result
|
|
979
|
+
}
|
|
980
|
+
pb += 1
|
|
981
|
+
rb += 1
|
|
982
|
+
result += 1
|
|
983
|
+
}
|
|
984
|
+
if (pb === a.length) {
|
|
985
|
+
pi += 1
|
|
986
|
+
pb = 0
|
|
987
|
+
}
|
|
988
|
+
if (rb === b.length) {
|
|
989
|
+
ri += 1
|
|
990
|
+
rb = 0
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return result
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private align(path: number[], ref: number[], edits: Edit[]) {
|
|
997
|
+
const pathLen = this.pathLen(path)
|
|
998
|
+
const refLen = this.pathLen(ref)
|
|
999
|
+
const prefix = this.prefixMatches(path, ref)
|
|
1000
|
+
let suffix = this.suffixMatches(path, ref)
|
|
1001
|
+
if (prefix + suffix > pathLen) {
|
|
1002
|
+
suffix = pathLen - prefix
|
|
1003
|
+
}
|
|
1004
|
+
if (prefix + suffix > refLen) {
|
|
1005
|
+
suffix = refLen - prefix
|
|
1006
|
+
}
|
|
1007
|
+
appendEdit(edits, 'M', prefix)
|
|
1008
|
+
const pathMiddle = pathLen - prefix - suffix
|
|
1009
|
+
const refMiddle = refLen - prefix - suffix
|
|
1010
|
+
if (pathMiddle === 0) {
|
|
1011
|
+
appendEdit(edits, 'D', refMiddle)
|
|
1012
|
+
} else if (refMiddle === 0) {
|
|
1013
|
+
appendEdit(edits, 'I', pathMiddle)
|
|
1014
|
+
} else {
|
|
1015
|
+
const mismatch = Math.min(pathMiddle, refMiddle)
|
|
1016
|
+
const mismatchIndel =
|
|
1017
|
+
4 * mismatch +
|
|
1018
|
+
gapPenalty(pathMiddle - mismatch) +
|
|
1019
|
+
gapPenalty(refMiddle - mismatch)
|
|
1020
|
+
const insertionDeletion = gapPenalty(pathMiddle) + gapPenalty(refMiddle)
|
|
1021
|
+
if (mismatchIndel <= insertionDeletion) {
|
|
1022
|
+
appendEdit(edits, 'M', mismatch)
|
|
1023
|
+
appendEdit(edits, 'I', pathMiddle - mismatch)
|
|
1024
|
+
appendEdit(edits, 'D', refMiddle - mismatch)
|
|
1025
|
+
} else {
|
|
1026
|
+
appendEdit(edits, 'I', pathMiddle)
|
|
1027
|
+
appendEdit(edits, 'D', refMiddle)
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
appendEdit(edits, 'M', suffix)
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
private edits(pathIndex: number): Edit[] | undefined {
|
|
1034
|
+
const info = this.paths[pathIndex]
|
|
1035
|
+
if (this.refId === undefined || pathIndex === this.refId || !info) {
|
|
1036
|
+
return undefined
|
|
1037
|
+
}
|
|
1038
|
+
const ref = this.paths[this.refId]!.path
|
|
1039
|
+
const ordered = this.orderedMatches(info.path, ref)
|
|
1040
|
+
if (ordered) {
|
|
1041
|
+
this.stats.orderedAlignments += 1
|
|
1042
|
+
} else {
|
|
1043
|
+
this.stats.lcsAlignments += 1
|
|
1044
|
+
}
|
|
1045
|
+
const lcs =
|
|
1046
|
+
ordered ??
|
|
1047
|
+
weightedLcs(info.path, ref, handle => this.record(handle).sequenceLen)[0]
|
|
1048
|
+
const edits: Edit[] = []
|
|
1049
|
+
let pathOffset = 0
|
|
1050
|
+
let refOffset = 0
|
|
1051
|
+
for (const [nextPath, nextRef] of lcs) {
|
|
1052
|
+
this.align(
|
|
1053
|
+
info.path.slice(pathOffset, nextPath),
|
|
1054
|
+
ref.slice(refOffset, nextRef),
|
|
1055
|
+
edits,
|
|
1056
|
+
)
|
|
1057
|
+
appendEdit(edits, 'M', this.record(info.path[nextPath]!).sequenceLen)
|
|
1058
|
+
pathOffset = nextPath + 1
|
|
1059
|
+
refOffset = nextRef + 1
|
|
1060
|
+
}
|
|
1061
|
+
this.align(info.path.slice(pathOffset), ref.slice(refOffset), edits)
|
|
1062
|
+
return edits
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
alignToRef(pathIndex: number) {
|
|
1066
|
+
return this.edits(pathIndex)
|
|
1067
|
+
?.map(([op, len]) => `${len}${op}`)
|
|
1068
|
+
.join('')
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
alignments(): HaplotypeAlignment[] {
|
|
1072
|
+
const reference = this.referenceInterval
|
|
1073
|
+
if (this.refId === undefined || !reference) {
|
|
1074
|
+
throw new Error('Alignments need a reference path')
|
|
1075
|
+
}
|
|
1076
|
+
const ref = this.paths[this.refId]!.path
|
|
1077
|
+
const refTotal = this.refPrefix(ref)[ref.length]!
|
|
1078
|
+
const result: HaplotypeAlignment[] = []
|
|
1079
|
+
this.paths.forEach((info, index) => {
|
|
1080
|
+
if (index === this.refId) {
|
|
1081
|
+
return
|
|
1082
|
+
}
|
|
1083
|
+
const edits = this.edits(index)!
|
|
1084
|
+
let first = 0
|
|
1085
|
+
let leading = 0
|
|
1086
|
+
while (first < edits.length && edits[first]![0] === 'D') {
|
|
1087
|
+
leading += edits[first]![1]
|
|
1088
|
+
first += 1
|
|
1089
|
+
}
|
|
1090
|
+
let last = edits.length
|
|
1091
|
+
let trailing = 0
|
|
1092
|
+
while (last > first && edits[last - 1]![0] === 'D') {
|
|
1093
|
+
trailing += edits[last - 1]![1]
|
|
1094
|
+
last -= 1
|
|
1095
|
+
}
|
|
1096
|
+
const identity = info.identity
|
|
1097
|
+
const strand =
|
|
1098
|
+
info.path.some(handle => isReverse(handle)) &&
|
|
1099
|
+
!info.path.some(handle => !isReverse(handle))
|
|
1100
|
+
? '-'
|
|
1101
|
+
: '+'
|
|
1102
|
+
result.push({
|
|
1103
|
+
pathHandle: identity?.pathHandle,
|
|
1104
|
+
name: identity?.name,
|
|
1105
|
+
strand: identity
|
|
1106
|
+
? identity.orientation === 'forward'
|
|
1107
|
+
? '+'
|
|
1108
|
+
: '-'
|
|
1109
|
+
: strand,
|
|
1110
|
+
hapStart: identity
|
|
1111
|
+
? identity.name.fragment + identity.hapStart
|
|
1112
|
+
: undefined,
|
|
1113
|
+
hapEnd: identity ? identity.name.fragment + identity.hapEnd : undefined,
|
|
1114
|
+
start: info.positions[0]!,
|
|
1115
|
+
refStart: reference.start + leading,
|
|
1116
|
+
refEnd: reference.start + refTotal - trailing,
|
|
1117
|
+
cigar: edits
|
|
1118
|
+
.slice(first, last)
|
|
1119
|
+
.map(([op, len]) => `${len}${op}`)
|
|
1120
|
+
.join(''),
|
|
1121
|
+
weight: info.weight,
|
|
1122
|
+
path: info.path,
|
|
1123
|
+
})
|
|
1124
|
+
})
|
|
1125
|
+
return result
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
private canonicalEdges(id: number) {
|
|
1129
|
+
const edges: [number, number, number][] = []
|
|
1130
|
+
for (const orientation of ['forward', 'reverse'] as const) {
|
|
1131
|
+
const handle = encodeNode(id, orientation)
|
|
1132
|
+
for (const successor of this.record(handle).successors()) {
|
|
1133
|
+
if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
|
|
1134
|
+
edges.push([
|
|
1135
|
+
orientation === 'reverse' ? 1 : 0,
|
|
1136
|
+
nodeId(successor),
|
|
1137
|
+
isReverse(successor) ? 1 : 0,
|
|
1138
|
+
])
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
edges.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2])
|
|
1143
|
+
return edges.filter(
|
|
1144
|
+
(edge, i) =>
|
|
1145
|
+
i === 0 ||
|
|
1146
|
+
edge[0] !== edges[i - 1]![0] ||
|
|
1147
|
+
edge[1] !== edges[i - 1]![1] ||
|
|
1148
|
+
edge[2] !== edges[i - 1]![2],
|
|
1149
|
+
)
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
async stableName() {
|
|
1153
|
+
const encoder = new TextEncoder()
|
|
1154
|
+
const chunks: Uint8Array[] = []
|
|
1155
|
+
for (const handle of this.sortedHandles()) {
|
|
1156
|
+
if (!isReverse(handle)) {
|
|
1157
|
+
const id = nodeId(handle)
|
|
1158
|
+
let text = `S\t${id}\t${this.record(handle).sequence}\n`
|
|
1159
|
+
for (const [fromReverse, toId, toReverse] of this.canonicalEdges(id)) {
|
|
1160
|
+
text += `L\t${id}\t${fromReverse ? '-' : '+'}\t${toId}\t${toReverse ? '-' : '+'}\n`
|
|
1161
|
+
}
|
|
1162
|
+
chunks.push(encoder.encode(text))
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return sha256Hex(chunks)
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async toGFA(cigar: boolean, opts: ToJsonOptions = {}) {
|
|
1169
|
+
const lines = [
|
|
1170
|
+
this.refPath ? `H\tVN:Z:1.1\tRS:Z:${this.refPath.sample}` : 'H\tVN:Z:1.1',
|
|
1171
|
+
...gfaHeaderLines(
|
|
1172
|
+
subgraphName(await this.stableName(), await this.db.graphName()),
|
|
1173
|
+
),
|
|
1174
|
+
]
|
|
1175
|
+
const handles = this.sortedHandles()
|
|
1176
|
+
for (const handle of handles) {
|
|
1177
|
+
if (!isReverse(handle)) {
|
|
1178
|
+
lines.push(`S\t${nodeId(handle)}\t${this.record(handle).sequence}`)
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const sign = (handle: number) => (isReverse(handle) ? '-' : '+')
|
|
1182
|
+
for (const handle of handles) {
|
|
1183
|
+
for (const successor of this.record(handle).successors()) {
|
|
1184
|
+
if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
|
|
1185
|
+
lines.push(
|
|
1186
|
+
`L\t${nodeId(handle)}\t${sign(handle)}\t${nodeId(successor)}\t${sign(successor)}\t0M`,
|
|
1187
|
+
)
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const walk = (
|
|
1192
|
+
info: PathInfo,
|
|
1193
|
+
name: PathName,
|
|
1194
|
+
end: number,
|
|
1195
|
+
cigarString: string | undefined,
|
|
1196
|
+
) => {
|
|
1197
|
+
const steps = info.path
|
|
1198
|
+
.map(handle => `${isReverse(handle) ? '<' : '>'}${nodeId(handle)}`)
|
|
1199
|
+
.join('')
|
|
1200
|
+
const weight = info.weight === undefined ? '' : `\tWT:i:${info.weight}`
|
|
1201
|
+
const cg = cigarString === undefined ? '' : `\tCG:Z:${cigarString}`
|
|
1202
|
+
return `W\t${name.sample}\t${name.haplotype}\t${name.contig}\t${name.fragment}\t${end}\t${steps}${weight}${cg}`
|
|
1203
|
+
}
|
|
1204
|
+
const contig = this.refPath?.contig ?? 'unknown'
|
|
1205
|
+
if (this.refId !== undefined && this.refPath && this.refInterval) {
|
|
1206
|
+
lines.push(
|
|
1207
|
+
walk(
|
|
1208
|
+
this.paths[this.refId]!,
|
|
1209
|
+
{
|
|
1210
|
+
...this.refPath,
|
|
1211
|
+
fragment: this.refPath.fragment + this.refInterval[0],
|
|
1212
|
+
},
|
|
1213
|
+
this.refPath.fragment + this.refInterval[1],
|
|
1214
|
+
undefined,
|
|
1215
|
+
),
|
|
1216
|
+
)
|
|
1217
|
+
}
|
|
1218
|
+
let haplotype = 1
|
|
1219
|
+
this.paths.forEach((info, index) => {
|
|
1220
|
+
if (index !== this.refId) {
|
|
1221
|
+
const resolved = opts.names === 'resolved' ? info.identity : undefined
|
|
1222
|
+
const cigarString = cigar ? this.alignToRef(index) : undefined
|
|
1223
|
+
lines.push(
|
|
1224
|
+
resolved
|
|
1225
|
+
? walk(
|
|
1226
|
+
info,
|
|
1227
|
+
{
|
|
1228
|
+
...resolved.name,
|
|
1229
|
+
fragment: resolved.name.fragment + resolved.hapStart,
|
|
1230
|
+
},
|
|
1231
|
+
resolved.name.fragment + resolved.hapEnd,
|
|
1232
|
+
cigarString,
|
|
1233
|
+
)
|
|
1234
|
+
: walk(
|
|
1235
|
+
info,
|
|
1236
|
+
{ sample: 'unknown', contig, haplotype, fragment: 0 },
|
|
1237
|
+
info.len,
|
|
1238
|
+
cigarString,
|
|
1239
|
+
),
|
|
1240
|
+
)
|
|
1241
|
+
haplotype += 1
|
|
1242
|
+
}
|
|
1243
|
+
})
|
|
1244
|
+
return `${lines.join('\n')}\n`
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
toJSON(cigar: boolean, opts: ToJsonOptions = {}): SubgraphJson {
|
|
1248
|
+
const handles = this.sortedHandles()
|
|
1249
|
+
const nodes = handles
|
|
1250
|
+
.filter(handle => !isReverse(handle))
|
|
1251
|
+
.map(handle => ({
|
|
1252
|
+
id: String(nodeId(handle)),
|
|
1253
|
+
sequence: this.record(handle).sequence,
|
|
1254
|
+
}))
|
|
1255
|
+
const edges: SubgraphJson['edges'] = []
|
|
1256
|
+
for (const handle of handles) {
|
|
1257
|
+
for (const successor of this.record(handle).successors()) {
|
|
1258
|
+
if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
|
|
1259
|
+
edges.push({
|
|
1260
|
+
from: String(nodeId(handle)),
|
|
1261
|
+
from_is_reverse: isReverse(handle),
|
|
1262
|
+
to: String(nodeId(successor)),
|
|
1263
|
+
to_is_reverse: isReverse(successor),
|
|
1264
|
+
})
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const paths: SubgraphPath[] = []
|
|
1269
|
+
const contig = this.refPath?.contig ?? 'unknown'
|
|
1270
|
+
if (this.refId !== undefined && this.refPath && this.refInterval) {
|
|
1271
|
+
const info = this.paths[this.refId]!
|
|
1272
|
+
const name = {
|
|
1273
|
+
...this.refPath,
|
|
1274
|
+
fragment: this.refPath.fragment + this.refInterval[0],
|
|
1275
|
+
}
|
|
1276
|
+
paths.push(
|
|
1277
|
+
jsonPath(
|
|
1278
|
+
info,
|
|
1279
|
+
formatPathName(name, this.refPath.fragment + this.refInterval[1]),
|
|
1280
|
+
undefined,
|
|
1281
|
+
),
|
|
1282
|
+
)
|
|
1283
|
+
}
|
|
1284
|
+
let haplotype = 1
|
|
1285
|
+
this.paths.forEach((info, index) => {
|
|
1286
|
+
if (index === this.refId) {
|
|
1287
|
+
return
|
|
1288
|
+
}
|
|
1289
|
+
const resolved = opts.names === 'resolved' ? info.identity : undefined
|
|
1290
|
+
const name = resolved
|
|
1291
|
+
? formatPathName(
|
|
1292
|
+
{
|
|
1293
|
+
...resolved.name,
|
|
1294
|
+
fragment: resolved.name.fragment + resolved.hapStart,
|
|
1295
|
+
},
|
|
1296
|
+
resolved.name.fragment + resolved.hapEnd,
|
|
1297
|
+
)
|
|
1298
|
+
: formatPathName(
|
|
1299
|
+
{ sample: 'unknown', contig, haplotype, fragment: 0 },
|
|
1300
|
+
info.len,
|
|
1301
|
+
)
|
|
1302
|
+
paths.push(
|
|
1303
|
+
jsonPath(info, name, cigar ? this.alignToRef(index) : undefined),
|
|
1304
|
+
)
|
|
1305
|
+
haplotype += 1
|
|
1306
|
+
})
|
|
1307
|
+
return { nodes, edges, paths }
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function jsonPath(
|
|
1312
|
+
info: PathInfo,
|
|
1313
|
+
name: string,
|
|
1314
|
+
cigar: string | undefined,
|
|
1315
|
+
): SubgraphPath {
|
|
1316
|
+
return {
|
|
1317
|
+
name,
|
|
1318
|
+
...(info.weight === undefined ? {} : { weight: info.weight }),
|
|
1319
|
+
...(cigar === undefined ? {} : { cigar }),
|
|
1320
|
+
path: info.path.map(handle => ({
|
|
1321
|
+
id: String(nodeId(handle)),
|
|
1322
|
+
is_reverse: isReverse(handle),
|
|
1323
|
+
})),
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function comparePaths(a: number[], b: number[]) {
|
|
1328
|
+
const n = Math.min(a.length, b.length)
|
|
1329
|
+
for (let i = 0; i < n; i++) {
|
|
1330
|
+
const x = a[i]!
|
|
1331
|
+
const y = b[i]!
|
|
1332
|
+
if (x !== y) {
|
|
1333
|
+
return x < y ? -1 : 1
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return a.length - b.length
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function appendEdit(edits: Edit[], op: EditOp, len: number) {
|
|
1340
|
+
if (len === 0) {
|
|
1341
|
+
return
|
|
1342
|
+
}
|
|
1343
|
+
const last = edits[edits.length - 1]
|
|
1344
|
+
if (last?.[0] === op) {
|
|
1345
|
+
last[1] += len
|
|
1346
|
+
} else {
|
|
1347
|
+
edits.push([op, len])
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function gapPenalty(len: number) {
|
|
1352
|
+
return len === 0 ? 0 : 6 + (len - 1)
|
|
1353
|
+
}
|