@gmod/gbz-base 0.0.1
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/LICENSE +25 -0
- package/README.md +125 -0
- package/bin/query.js +7 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +140 -0
- package/dist/db.d.ts +72 -0
- package/dist/db.js +208 -0
- package/dist/filehandle.d.ts +6 -0
- package/dist/filehandle.js +1 -0
- package/dist/gbwt/bytecode.d.ts +20 -0
- package/dist/gbwt/bytecode.js +70 -0
- package/dist/gbwt/node.d.ts +15 -0
- package/dist/gbwt/node.js +44 -0
- package/dist/gbwt/record.d.ts +18 -0
- package/dist/gbwt/record.js +133 -0
- package/dist/gbwt/sequence.d.ts +3 -0
- package/dist/gbwt/sequence.js +40 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +6 -0
- package/dist/lcs.d.ts +1 -0
- package/dist/lcs.js +214 -0
- package/dist/query.d.ts +16 -0
- package/dist/query.js +37 -0
- package/dist/sqlite/btree.d.ts +18 -0
- package/dist/sqlite/btree.js +201 -0
- package/dist/sqlite/database.d.ts +29 -0
- package/dist/sqlite/database.js +71 -0
- package/dist/sqlite/pager.d.ts +19 -0
- package/dist/sqlite/pager.js +55 -0
- package/dist/sqlite/record.d.ts +3 -0
- package/dist/sqlite/record.js +85 -0
- package/dist/subgraph.d.ts +122 -0
- package/dist/subgraph.js +760 -0
- package/package.json +53 -0
- package/tools/haplotype-index/Cargo.toml +15 -0
- package/tools/haplotype-index/src/main.rs +319 -0
package/dist/subgraph.js
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { formatPathName } from "./db.js";
|
|
2
|
+
import { ENDMARKER, edgeIsCanonical, encodeNode, entryOrientation, entrySide, exitOrientation, exitSide, flipSide, isReverse, nodeId, nodeOrientation, pathIsCanonical, } from "./gbwt/node.js";
|
|
3
|
+
import { weightedLcs } from "./lcs.js";
|
|
4
|
+
class SideQueue {
|
|
5
|
+
items = [];
|
|
6
|
+
push(distance, node, side) {
|
|
7
|
+
this.items.push([distance, node, side]);
|
|
8
|
+
}
|
|
9
|
+
pop() {
|
|
10
|
+
let best = 0;
|
|
11
|
+
for (let i = 1; i < this.items.length; i++) {
|
|
12
|
+
const a = this.items[i];
|
|
13
|
+
const b = this.items[best];
|
|
14
|
+
if (a[0] < b[0] || (a[0] === b[0] && (a[1] < b[1] || (a[1] === b[1] && a[2] < b[2])))) {
|
|
15
|
+
best = i;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const [item] = this.items.splice(best, 1);
|
|
19
|
+
return item;
|
|
20
|
+
}
|
|
21
|
+
get size() {
|
|
22
|
+
return this.items.length;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function posKey(pos) {
|
|
26
|
+
return `${pos.node}:${pos.offset}`;
|
|
27
|
+
}
|
|
28
|
+
export class Subgraph {
|
|
29
|
+
db;
|
|
30
|
+
records = new Map();
|
|
31
|
+
paths = [];
|
|
32
|
+
refId;
|
|
33
|
+
refPath;
|
|
34
|
+
refHandle;
|
|
35
|
+
refInterval;
|
|
36
|
+
refIndexCache;
|
|
37
|
+
refPrefixCache;
|
|
38
|
+
limit;
|
|
39
|
+
stats = { orderedAlignments: 0, lcsAlignments: 0, identificationSteps: 0, identificationFetches: 0 };
|
|
40
|
+
constructor(db) {
|
|
41
|
+
this.db = db;
|
|
42
|
+
}
|
|
43
|
+
get nodeCount() {
|
|
44
|
+
return this.records.size / 2;
|
|
45
|
+
}
|
|
46
|
+
get pathCount() {
|
|
47
|
+
return this.paths.length;
|
|
48
|
+
}
|
|
49
|
+
get referenceInterval() {
|
|
50
|
+
return this.refInterval && this.refPath
|
|
51
|
+
? { name: this.refPath, start: this.refPath.fragment + this.refInterval[0], end: this.refPath.fragment + this.refInterval[1] }
|
|
52
|
+
: undefined;
|
|
53
|
+
}
|
|
54
|
+
hasNode(id) {
|
|
55
|
+
return this.records.has(encodeNode(id, 'forward'));
|
|
56
|
+
}
|
|
57
|
+
hasHandle(handle) {
|
|
58
|
+
return this.records.has(handle);
|
|
59
|
+
}
|
|
60
|
+
record(handle) {
|
|
61
|
+
const record = this.records.get(handle);
|
|
62
|
+
if (!record) {
|
|
63
|
+
throw new Error(`Subgraph has no record for handle ${handle}`);
|
|
64
|
+
}
|
|
65
|
+
return record;
|
|
66
|
+
}
|
|
67
|
+
sortedHandles() {
|
|
68
|
+
return [...this.records.keys()].sort((a, b) => a - b);
|
|
69
|
+
}
|
|
70
|
+
async addNode(id) {
|
|
71
|
+
if (this.limit !== undefined && this.nodeCount >= this.limit) {
|
|
72
|
+
throw new Error(`Subgraph size limit of ${this.limit} nodes exceeded`);
|
|
73
|
+
}
|
|
74
|
+
const forward = await this.db.getRecord(encodeNode(id, 'forward'));
|
|
75
|
+
const reverse = await this.db.getRecord(encodeNode(id, 'reverse'));
|
|
76
|
+
if (!forward || !reverse) {
|
|
77
|
+
throw new Error(`Node ${id} does not exist in the graph`);
|
|
78
|
+
}
|
|
79
|
+
this.records.set(forward.handle, forward);
|
|
80
|
+
this.records.set(reverse.handle, reverse);
|
|
81
|
+
}
|
|
82
|
+
async ensureNode(id) {
|
|
83
|
+
if (!this.hasNode(id)) {
|
|
84
|
+
await this.addNode(id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
clearPaths() {
|
|
88
|
+
this.paths = [];
|
|
89
|
+
this.refId = undefined;
|
|
90
|
+
this.refPath = undefined;
|
|
91
|
+
this.refHandle = undefined;
|
|
92
|
+
this.refInterval = undefined;
|
|
93
|
+
this.refIndexCache = undefined;
|
|
94
|
+
this.refPrefixCache = undefined;
|
|
95
|
+
}
|
|
96
|
+
async pathPosition(query) {
|
|
97
|
+
const path = await this.db.findPath(query);
|
|
98
|
+
if (!path) {
|
|
99
|
+
throw new Error(`Cannot find a path covering ${formatPathName(query, query.fragment)}`);
|
|
100
|
+
}
|
|
101
|
+
if (!path.isIndexed) {
|
|
102
|
+
throw new Error(`Path ${formatPathName(path.name, path.name.fragment)} has not been indexed for random access`);
|
|
103
|
+
}
|
|
104
|
+
const queryOffset = query.fragment - path.name.fragment;
|
|
105
|
+
const indexed = await this.db.indexedPosition(path.handle, queryOffset);
|
|
106
|
+
if (!indexed) {
|
|
107
|
+
throw new Error(`Path ${formatPathName(path.name, path.name.fragment)} has not been indexed for random access`);
|
|
108
|
+
}
|
|
109
|
+
return this.findPathPosition(path, queryOffset, indexed.pathOffset, indexed.pos);
|
|
110
|
+
}
|
|
111
|
+
async findPathPosition(path, queryOffset, startOffset, start) {
|
|
112
|
+
let pathOffset = startOffset;
|
|
113
|
+
let pos = start;
|
|
114
|
+
for (;;) {
|
|
115
|
+
await this.ensureNode(nodeId(pos.node));
|
|
116
|
+
const record = this.record(pos.node);
|
|
117
|
+
if (pathOffset + record.sequenceLen > queryOffset) {
|
|
118
|
+
return {
|
|
119
|
+
position: { seqOffset: queryOffset, handle: pos.node, nodeOffset: queryOffset - pathOffset, gbwtOffset: pos.offset },
|
|
120
|
+
name: path.name,
|
|
121
|
+
handle: path.handle,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
pathOffset += record.sequenceLen;
|
|
125
|
+
const next = record.gbwt().lf(pos.offset);
|
|
126
|
+
if (!next) {
|
|
127
|
+
throw new Error(`Path ${formatPathName(path.name, path.name.fragment)} does not contain offset ${queryOffset}`);
|
|
128
|
+
}
|
|
129
|
+
pos = next;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async aroundPosition(handle, nodeOffset, context) {
|
|
133
|
+
const id = nodeId(handle);
|
|
134
|
+
await this.ensureNode(id);
|
|
135
|
+
const record = this.record(handle);
|
|
136
|
+
const orientation = nodeOrientation(handle);
|
|
137
|
+
const active = new SideQueue();
|
|
138
|
+
active.push(nodeOffset, id, entrySide(orientation));
|
|
139
|
+
active.push(record.sequenceLen - nodeOffset - 1, id, exitSide(orientation));
|
|
140
|
+
return this.insertContext(active, context);
|
|
141
|
+
}
|
|
142
|
+
async aroundInterval(start, len, context) {
|
|
143
|
+
if (len === 0) {
|
|
144
|
+
throw new Error('Interval length must be greater than 0');
|
|
145
|
+
}
|
|
146
|
+
let pos = { node: start.handle, offset: start.gbwtOffset };
|
|
147
|
+
let offset = start.nodeOffset;
|
|
148
|
+
let remaining = len;
|
|
149
|
+
const active = new SideQueue();
|
|
150
|
+
for (;;) {
|
|
151
|
+
const id = nodeId(pos.node);
|
|
152
|
+
const orientation = nodeOrientation(pos.node);
|
|
153
|
+
await this.ensureNode(id);
|
|
154
|
+
const record = this.record(pos.node);
|
|
155
|
+
if (offset >= record.sequenceLen) {
|
|
156
|
+
throw new Error(`Offset ${offset} in node ${id} of length ${record.sequenceLen}`);
|
|
157
|
+
}
|
|
158
|
+
active.push(offset, id, entrySide(orientation));
|
|
159
|
+
const distanceToNext = record.sequenceLen - offset;
|
|
160
|
+
if (remaining <= distanceToNext) {
|
|
161
|
+
active.push(remaining === distanceToNext ? 0 : distanceToNext - remaining - 1, id, exitSide(orientation));
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
active.push(0, id, exitSide(orientation));
|
|
165
|
+
const next = record.gbwt().lf(pos.offset);
|
|
166
|
+
if (!next) {
|
|
167
|
+
throw new Error(`No successor for GBWT position (${pos.node}, ${pos.offset})`);
|
|
168
|
+
}
|
|
169
|
+
pos = next;
|
|
170
|
+
offset = 0;
|
|
171
|
+
remaining -= distanceToNext;
|
|
172
|
+
}
|
|
173
|
+
return this.insertContext(active, context);
|
|
174
|
+
}
|
|
175
|
+
async aroundNodes(nodes, context) {
|
|
176
|
+
const active = new SideQueue();
|
|
177
|
+
for (const id of nodes) {
|
|
178
|
+
await this.ensureNode(id);
|
|
179
|
+
active.push(0, id, 'left');
|
|
180
|
+
active.push(0, id, 'right');
|
|
181
|
+
}
|
|
182
|
+
return this.insertContext(active, context);
|
|
183
|
+
}
|
|
184
|
+
async insertContext(active, context) {
|
|
185
|
+
this.clearPaths();
|
|
186
|
+
const visited = new Set();
|
|
187
|
+
const toRemove = new Set();
|
|
188
|
+
for (const handle of this.records.keys()) {
|
|
189
|
+
toRemove.add(nodeId(handle));
|
|
190
|
+
}
|
|
191
|
+
let inserted = 0;
|
|
192
|
+
while (active.size > 0) {
|
|
193
|
+
const [distance, id, side] = active.pop();
|
|
194
|
+
const key = `${id}:${side}`;
|
|
195
|
+
if (visited.has(key)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
visited.add(key);
|
|
199
|
+
toRemove.delete(id);
|
|
200
|
+
if (!this.hasNode(id)) {
|
|
201
|
+
await this.addNode(id);
|
|
202
|
+
inserted += 1;
|
|
203
|
+
}
|
|
204
|
+
const otherSide = flipSide(side);
|
|
205
|
+
if (!visited.has(`${id}:${otherSide}`)) {
|
|
206
|
+
const record = this.record(encodeNode(id, entryOrientation(side)));
|
|
207
|
+
const nextDistance = distance + record.sequenceLen - 1;
|
|
208
|
+
if (nextDistance <= context) {
|
|
209
|
+
active.push(nextDistance, id, otherSide);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const record = this.record(encodeNode(id, exitOrientation(side)));
|
|
213
|
+
const nextDistance = distance + 1;
|
|
214
|
+
if (nextDistance <= context) {
|
|
215
|
+
for (const successor of record.successors()) {
|
|
216
|
+
const successorId = nodeId(successor);
|
|
217
|
+
const successorSide = entrySide(nodeOrientation(successor));
|
|
218
|
+
if (!visited.has(`${successorId}:${successorSide}`)) {
|
|
219
|
+
active.push(nextDistance, successorId, successorSide);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const id of toRemove) {
|
|
225
|
+
this.records.delete(encodeNode(id, 'forward'));
|
|
226
|
+
this.records.delete(encodeNode(id, 'reverse'));
|
|
227
|
+
}
|
|
228
|
+
return { inserted, removed: toRemove.size };
|
|
229
|
+
}
|
|
230
|
+
extractPaths(reference, output) {
|
|
231
|
+
this.clearPaths();
|
|
232
|
+
if (output === 'none') {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const refPos = reference?.position;
|
|
236
|
+
this.refPath = reference?.name;
|
|
237
|
+
this.refHandle = reference?.handle;
|
|
238
|
+
const handles = this.sortedHandles();
|
|
239
|
+
const successors = new Map();
|
|
240
|
+
for (const handle of handles) {
|
|
241
|
+
successors.set(handle, this.record(handle)
|
|
242
|
+
.gbwt()
|
|
243
|
+
.decompress()
|
|
244
|
+
.map(next => ({ next, hasPredecessor: false })));
|
|
245
|
+
}
|
|
246
|
+
for (const handle of handles) {
|
|
247
|
+
for (const { next } of successors.get(handle)) {
|
|
248
|
+
const entry = successors.get(next.node)?.[next.offset];
|
|
249
|
+
if (entry) {
|
|
250
|
+
entry.hasPredecessor = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
let refOffset;
|
|
255
|
+
for (const handle of handles) {
|
|
256
|
+
const entries = successors.get(handle);
|
|
257
|
+
entries.forEach((entry, offset) => {
|
|
258
|
+
if (entry.hasPredecessor) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
let curr = { node: handle, offset };
|
|
262
|
+
let isRef = false;
|
|
263
|
+
const path = [];
|
|
264
|
+
const positions = [];
|
|
265
|
+
let len = 0;
|
|
266
|
+
while (curr) {
|
|
267
|
+
if (refPos && curr.node === refPos.handle && curr.offset === refPos.gbwtOffset) {
|
|
268
|
+
this.refId = this.paths.length;
|
|
269
|
+
refOffset = path.length;
|
|
270
|
+
isRef = true;
|
|
271
|
+
}
|
|
272
|
+
path.push(curr.node);
|
|
273
|
+
positions.push(curr);
|
|
274
|
+
len += this.record(curr.node).sequenceLen;
|
|
275
|
+
const step = successors.get(curr.node)?.[curr.offset];
|
|
276
|
+
curr = step && step.next.node !== ENDMARKER && successors.has(step.next.node) ? step.next : undefined;
|
|
277
|
+
}
|
|
278
|
+
if (isRef || pathIsCanonical(path)) {
|
|
279
|
+
this.paths.push({ path, positions, len, weight: undefined, identity: undefined });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (refPos) {
|
|
284
|
+
if (refOffset === undefined || this.refId === undefined) {
|
|
285
|
+
this.clearPaths();
|
|
286
|
+
throw new Error('Could not find the reference path');
|
|
287
|
+
}
|
|
288
|
+
const info = this.paths[this.refId];
|
|
289
|
+
let before = refPos.nodeOffset;
|
|
290
|
+
for (const handle of info.path.slice(0, refOffset)) {
|
|
291
|
+
before += this.record(handle).sequenceLen;
|
|
292
|
+
}
|
|
293
|
+
const start = refPos.seqOffset - before;
|
|
294
|
+
this.refInterval = [start, start + info.len];
|
|
295
|
+
if (reference) {
|
|
296
|
+
info.identity = { pathHandle: reference.handle, name: reference.name, orientation: 'forward', hapStart: start, hapEnd: start + info.len };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (output === 'distinct') {
|
|
300
|
+
this.distinctPaths();
|
|
301
|
+
}
|
|
302
|
+
else if (output === 'reference-only') {
|
|
303
|
+
if (this.refId === undefined) {
|
|
304
|
+
throw new Error('Reference path is required for reference-only output');
|
|
305
|
+
}
|
|
306
|
+
this.paths = [this.paths[this.refId]];
|
|
307
|
+
this.refId = 0;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
distinctPaths() {
|
|
311
|
+
const refPath = this.refId === undefined ? undefined : this.paths[this.refId].path;
|
|
312
|
+
this.paths.sort((x, y) => comparePaths(x.path, y.path) || x.len - y.len);
|
|
313
|
+
const merged = [];
|
|
314
|
+
let refId;
|
|
315
|
+
for (const info of this.paths) {
|
|
316
|
+
const last = merged[merged.length - 1];
|
|
317
|
+
if (last && comparePaths(last.path, info.path) === 0) {
|
|
318
|
+
last.weight = (last.weight ?? 0) + 1;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
if (refPath && comparePaths(info.path, refPath) === 0) {
|
|
322
|
+
refId = merged.length;
|
|
323
|
+
}
|
|
324
|
+
merged.push({ ...info, weight: 1 });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
this.paths = merged;
|
|
328
|
+
this.refId = refId;
|
|
329
|
+
}
|
|
330
|
+
async identifyPaths() {
|
|
331
|
+
if (!this.db.hasHaplotypeIndex) {
|
|
332
|
+
throw new Error('The database has no HaplotypeSamples table; run gbz-haplotype-index on it');
|
|
333
|
+
}
|
|
334
|
+
const interval = (await this.db.haplotypeSampleInterval()) ?? 4096;
|
|
335
|
+
const handles = this.sortedHandles();
|
|
336
|
+
const minHandle = handles[0];
|
|
337
|
+
const maxHandle = handles[handles.length - 1];
|
|
338
|
+
if (minHandle === undefined || maxHandle === undefined) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const samples = new Map();
|
|
342
|
+
for (const sample of await this.db.haplotypeSamplesInRange(minHandle, maxHandle)) {
|
|
343
|
+
samples.set(posKey(sample), sample);
|
|
344
|
+
}
|
|
345
|
+
const starts = new Map();
|
|
346
|
+
this.paths.forEach((info, index) => {
|
|
347
|
+
const first = info.positions[0];
|
|
348
|
+
if (first && index !== this.refId) {
|
|
349
|
+
starts.set(posKey(first), index);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
const outside = new Map();
|
|
353
|
+
const recordAt = async (handle) => {
|
|
354
|
+
const inside = this.records.get(handle);
|
|
355
|
+
if (inside) {
|
|
356
|
+
return inside;
|
|
357
|
+
}
|
|
358
|
+
let record = outside.get(handle);
|
|
359
|
+
if (!record) {
|
|
360
|
+
record = await this.db.getRecord(handle);
|
|
361
|
+
this.stats.identificationFetches += 1;
|
|
362
|
+
if (!record) {
|
|
363
|
+
throw new Error(`Node record ${handle} is missing from the database`);
|
|
364
|
+
}
|
|
365
|
+
outside.set(handle, record);
|
|
366
|
+
}
|
|
367
|
+
return record;
|
|
368
|
+
};
|
|
369
|
+
const sampleAt = async (pos) => {
|
|
370
|
+
if (pos.node >= minHandle && pos.node <= maxHandle) {
|
|
371
|
+
return samples.get(posKey(pos));
|
|
372
|
+
}
|
|
373
|
+
this.stats.identificationFetches += 1;
|
|
374
|
+
return this.db.haplotypeSampleAt(pos.node, pos.offset);
|
|
375
|
+
};
|
|
376
|
+
const names = new Map();
|
|
377
|
+
const nameOf = async (pathHandle) => {
|
|
378
|
+
let name = names.get(pathHandle);
|
|
379
|
+
if (!name) {
|
|
380
|
+
const path = await this.db.getPath(pathHandle);
|
|
381
|
+
if (!path) {
|
|
382
|
+
throw new Error(`Path ${pathHandle} is missing from the database`);
|
|
383
|
+
}
|
|
384
|
+
name = path.name;
|
|
385
|
+
names.set(pathHandle, name);
|
|
386
|
+
}
|
|
387
|
+
return name;
|
|
388
|
+
};
|
|
389
|
+
const anchorFromSample = (sample, counter, nodeLen) => sample.orientation === 'forward'
|
|
390
|
+
? { pathHandle: sample.pathHandle, orientation: 'forward', base: sample.pathOffset - counter }
|
|
391
|
+
: { pathHandle: sample.pathHandle, orientation: 'reverse', base: sample.pathOffset + counter + nodeLen };
|
|
392
|
+
const anchorFromIdentity = (identity, counter) => identity.orientation === 'forward'
|
|
393
|
+
? { pathHandle: identity.pathHandle, orientation: 'forward', base: identity.hapStart - counter }
|
|
394
|
+
: { pathHandle: identity.pathHandle, orientation: 'reverse', base: identity.hapEnd + counter };
|
|
395
|
+
for (let start = 0; start < this.paths.length; start++) {
|
|
396
|
+
const startInfo = this.paths[start];
|
|
397
|
+
if (start === this.refId || startInfo.identity) {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const chain = [];
|
|
401
|
+
const visited = new Set();
|
|
402
|
+
let anchor;
|
|
403
|
+
let counter = 0;
|
|
404
|
+
let current = start;
|
|
405
|
+
let pos;
|
|
406
|
+
while (anchor === undefined) {
|
|
407
|
+
if (current !== undefined) {
|
|
408
|
+
if (visited.has(current)) {
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
visited.add(current);
|
|
412
|
+
const info = this.paths[current];
|
|
413
|
+
chain.push({ index: current, startBp: counter });
|
|
414
|
+
let bp = counter;
|
|
415
|
+
for (const position of info.positions) {
|
|
416
|
+
const sample = samples.get(posKey(position));
|
|
417
|
+
const nodeLen = this.record(position.node).sequenceLen;
|
|
418
|
+
if (sample) {
|
|
419
|
+
anchor = anchorFromSample(sample, bp, nodeLen);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
bp += nodeLen;
|
|
423
|
+
}
|
|
424
|
+
counter += info.len;
|
|
425
|
+
if (anchor) {
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
const last = info.positions[info.positions.length - 1];
|
|
429
|
+
pos = this.record(last.node).gbwt().lf(last.offset);
|
|
430
|
+
current = undefined;
|
|
431
|
+
}
|
|
432
|
+
if (pos === undefined || pos.node === ENDMARKER) {
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
const known = starts.get(posKey(pos));
|
|
436
|
+
if (known !== undefined) {
|
|
437
|
+
const identity = this.paths[known].identity;
|
|
438
|
+
if (identity) {
|
|
439
|
+
anchor = anchorFromIdentity(identity, counter);
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
current = known;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const sample = await sampleAt(pos);
|
|
446
|
+
const record = await recordAt(pos.node);
|
|
447
|
+
if (sample) {
|
|
448
|
+
anchor = anchorFromSample(sample, counter, record.sequenceLen);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
this.stats.identificationSteps += 1;
|
|
452
|
+
if (counter - chain[chain.length - 1].startBp > 4 * interval + 4 * record.sequenceLen) {
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
counter += record.sequenceLen;
|
|
456
|
+
pos = record.gbwt().lf(pos.offset);
|
|
457
|
+
}
|
|
458
|
+
if (anchor) {
|
|
459
|
+
const name = await nameOf(anchor.pathHandle);
|
|
460
|
+
for (const { index, startBp } of chain) {
|
|
461
|
+
const info = this.paths[index];
|
|
462
|
+
info.identity =
|
|
463
|
+
anchor.orientation === 'forward'
|
|
464
|
+
? { pathHandle: anchor.pathHandle, name, orientation: 'forward', hapStart: anchor.base + startBp, hapEnd: anchor.base + startBp + info.len }
|
|
465
|
+
: { pathHandle: anchor.pathHandle, name, orientation: 'reverse', hapStart: anchor.base - startBp - info.len, hapEnd: anchor.base - startBp };
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
refIndex(ref) {
|
|
471
|
+
if (this.refIndexCache === undefined) {
|
|
472
|
+
const index = new Map();
|
|
473
|
+
ref.forEach((handle, i) => {
|
|
474
|
+
const occurrences = index.get(handle);
|
|
475
|
+
if (occurrences) {
|
|
476
|
+
occurrences.push(i);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
index.set(handle, [i]);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
this.refIndexCache = index;
|
|
483
|
+
}
|
|
484
|
+
return this.refIndexCache;
|
|
485
|
+
}
|
|
486
|
+
refPrefix(ref) {
|
|
487
|
+
if (this.refPrefixCache === undefined) {
|
|
488
|
+
const prefix = [0];
|
|
489
|
+
ref.forEach((handle, i) => {
|
|
490
|
+
prefix.push(prefix[i] + this.record(handle).sequenceLen);
|
|
491
|
+
});
|
|
492
|
+
this.refPrefixCache = prefix;
|
|
493
|
+
}
|
|
494
|
+
return this.refPrefixCache;
|
|
495
|
+
}
|
|
496
|
+
orderedMatches(path, ref) {
|
|
497
|
+
const index = this.refIndex(ref);
|
|
498
|
+
const pairs = [];
|
|
499
|
+
let last = -1;
|
|
500
|
+
for (let i = 0; i < path.length; i++) {
|
|
501
|
+
const occurrences = index.get(path[i]);
|
|
502
|
+
if (occurrences) {
|
|
503
|
+
const j = occurrences.find(x => x > last);
|
|
504
|
+
if (j === undefined) {
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
pairs.push([i, j]);
|
|
508
|
+
last = j;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return pairs;
|
|
512
|
+
}
|
|
513
|
+
pathLen(path) {
|
|
514
|
+
let total = 0;
|
|
515
|
+
for (const handle of path) {
|
|
516
|
+
total += this.record(handle).sequenceLen;
|
|
517
|
+
}
|
|
518
|
+
return total;
|
|
519
|
+
}
|
|
520
|
+
prefixMatches(path, ref) {
|
|
521
|
+
let result = 0;
|
|
522
|
+
let pi = 0;
|
|
523
|
+
let ri = 0;
|
|
524
|
+
let pb = 0;
|
|
525
|
+
let rb = 0;
|
|
526
|
+
while (pi < path.length && ri < ref.length) {
|
|
527
|
+
const a = this.record(path[pi]).sequence;
|
|
528
|
+
const b = this.record(ref[ri]).sequence;
|
|
529
|
+
while (pb < a.length && rb < b.length) {
|
|
530
|
+
if (a[pb] !== b[rb]) {
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
pb += 1;
|
|
534
|
+
rb += 1;
|
|
535
|
+
result += 1;
|
|
536
|
+
}
|
|
537
|
+
if (pb === a.length) {
|
|
538
|
+
pi += 1;
|
|
539
|
+
pb = 0;
|
|
540
|
+
}
|
|
541
|
+
if (rb === b.length) {
|
|
542
|
+
ri += 1;
|
|
543
|
+
rb = 0;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
548
|
+
suffixMatches(path, ref) {
|
|
549
|
+
let result = 0;
|
|
550
|
+
let pi = 0;
|
|
551
|
+
let ri = 0;
|
|
552
|
+
let pb = 0;
|
|
553
|
+
let rb = 0;
|
|
554
|
+
while (pi < path.length && ri < ref.length) {
|
|
555
|
+
const a = this.record(path[path.length - pi - 1]).sequence;
|
|
556
|
+
const b = this.record(ref[ref.length - ri - 1]).sequence;
|
|
557
|
+
while (pb < a.length && rb < b.length) {
|
|
558
|
+
if (a[a.length - pb - 1] !== b[b.length - rb - 1]) {
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
pb += 1;
|
|
562
|
+
rb += 1;
|
|
563
|
+
result += 1;
|
|
564
|
+
}
|
|
565
|
+
if (pb === a.length) {
|
|
566
|
+
pi += 1;
|
|
567
|
+
pb = 0;
|
|
568
|
+
}
|
|
569
|
+
if (rb === b.length) {
|
|
570
|
+
ri += 1;
|
|
571
|
+
rb = 0;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
align(path, ref, edits) {
|
|
577
|
+
const pathLen = this.pathLen(path);
|
|
578
|
+
const refLen = this.pathLen(ref);
|
|
579
|
+
const prefix = this.prefixMatches(path, ref);
|
|
580
|
+
let suffix = this.suffixMatches(path, ref);
|
|
581
|
+
if (prefix + suffix > pathLen) {
|
|
582
|
+
suffix = pathLen - prefix;
|
|
583
|
+
}
|
|
584
|
+
if (prefix + suffix > refLen) {
|
|
585
|
+
suffix = refLen - prefix;
|
|
586
|
+
}
|
|
587
|
+
appendEdit(edits, 'M', prefix);
|
|
588
|
+
const pathMiddle = pathLen - prefix - suffix;
|
|
589
|
+
const refMiddle = refLen - prefix - suffix;
|
|
590
|
+
if (pathMiddle === 0) {
|
|
591
|
+
appendEdit(edits, 'D', refMiddle);
|
|
592
|
+
}
|
|
593
|
+
else if (refMiddle === 0) {
|
|
594
|
+
appendEdit(edits, 'I', pathMiddle);
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
const mismatch = Math.min(pathMiddle, refMiddle);
|
|
598
|
+
const mismatchIndel = 4 * mismatch + gapPenalty(pathMiddle - mismatch) + gapPenalty(refMiddle - mismatch);
|
|
599
|
+
const insertionDeletion = gapPenalty(pathMiddle) + gapPenalty(refMiddle);
|
|
600
|
+
if (mismatchIndel <= insertionDeletion) {
|
|
601
|
+
appendEdit(edits, 'M', mismatch);
|
|
602
|
+
appendEdit(edits, 'I', pathMiddle - mismatch);
|
|
603
|
+
appendEdit(edits, 'D', refMiddle - mismatch);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
appendEdit(edits, 'I', pathMiddle);
|
|
607
|
+
appendEdit(edits, 'D', refMiddle);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
appendEdit(edits, 'M', suffix);
|
|
611
|
+
}
|
|
612
|
+
edits(pathIndex) {
|
|
613
|
+
const info = this.paths[pathIndex];
|
|
614
|
+
if (this.refId === undefined || pathIndex === this.refId || !info) {
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
const ref = this.paths[this.refId].path;
|
|
618
|
+
const ordered = this.orderedMatches(info.path, ref);
|
|
619
|
+
if (ordered) {
|
|
620
|
+
this.stats.orderedAlignments += 1;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
this.stats.lcsAlignments += 1;
|
|
624
|
+
}
|
|
625
|
+
const lcs = ordered ?? weightedLcs(info.path, ref, handle => this.record(handle).sequenceLen)[0];
|
|
626
|
+
const edits = [];
|
|
627
|
+
let pathOffset = 0;
|
|
628
|
+
let refOffset = 0;
|
|
629
|
+
for (const [nextPath, nextRef] of lcs) {
|
|
630
|
+
this.align(info.path.slice(pathOffset, nextPath), ref.slice(refOffset, nextRef), edits);
|
|
631
|
+
appendEdit(edits, 'M', this.record(info.path[nextPath]).sequenceLen);
|
|
632
|
+
pathOffset = nextPath + 1;
|
|
633
|
+
refOffset = nextRef + 1;
|
|
634
|
+
}
|
|
635
|
+
this.align(info.path.slice(pathOffset), ref.slice(refOffset), edits);
|
|
636
|
+
return edits;
|
|
637
|
+
}
|
|
638
|
+
alignToRef(pathIndex) {
|
|
639
|
+
return this.edits(pathIndex)?.map(([op, len]) => `${len}${op}`).join('');
|
|
640
|
+
}
|
|
641
|
+
alignments() {
|
|
642
|
+
const reference = this.referenceInterval;
|
|
643
|
+
if (this.refId === undefined || !reference) {
|
|
644
|
+
throw new Error('Alignments need a reference path');
|
|
645
|
+
}
|
|
646
|
+
const ref = this.paths[this.refId].path;
|
|
647
|
+
const refTotal = this.refPrefix(ref)[ref.length];
|
|
648
|
+
const result = [];
|
|
649
|
+
this.paths.forEach((info, index) => {
|
|
650
|
+
if (index === this.refId) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const edits = this.edits(index);
|
|
654
|
+
let first = 0;
|
|
655
|
+
let leading = 0;
|
|
656
|
+
while (first < edits.length && edits[first][0] === 'D') {
|
|
657
|
+
leading += edits[first][1];
|
|
658
|
+
first += 1;
|
|
659
|
+
}
|
|
660
|
+
let last = edits.length;
|
|
661
|
+
let trailing = 0;
|
|
662
|
+
while (last > first && edits[last - 1][0] === 'D') {
|
|
663
|
+
trailing += edits[last - 1][1];
|
|
664
|
+
last -= 1;
|
|
665
|
+
}
|
|
666
|
+
const identity = info.identity;
|
|
667
|
+
const strand = info.path.some(handle => isReverse(handle)) && !info.path.some(handle => !isReverse(handle)) ? '-' : '+';
|
|
668
|
+
result.push({
|
|
669
|
+
pathHandle: identity?.pathHandle,
|
|
670
|
+
name: identity?.name,
|
|
671
|
+
strand: identity ? (identity.orientation === 'forward' ? '+' : '-') : strand,
|
|
672
|
+
hapStart: identity ? identity.name.fragment + identity.hapStart : undefined,
|
|
673
|
+
hapEnd: identity ? identity.name.fragment + identity.hapEnd : undefined,
|
|
674
|
+
start: info.positions[0],
|
|
675
|
+
refStart: reference.start + leading,
|
|
676
|
+
refEnd: reference.start + refTotal - trailing,
|
|
677
|
+
cigar: edits
|
|
678
|
+
.slice(first, last)
|
|
679
|
+
.map(([op, len]) => `${len}${op}`)
|
|
680
|
+
.join(''),
|
|
681
|
+
weight: info.weight,
|
|
682
|
+
path: info.path,
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
return result;
|
|
686
|
+
}
|
|
687
|
+
toJSON(cigar, opts = {}) {
|
|
688
|
+
const handles = this.sortedHandles();
|
|
689
|
+
const nodes = handles
|
|
690
|
+
.filter(handle => !isReverse(handle))
|
|
691
|
+
.map(handle => ({ id: String(nodeId(handle)), sequence: this.record(handle).sequence }));
|
|
692
|
+
const edges = [];
|
|
693
|
+
for (const handle of handles) {
|
|
694
|
+
for (const successor of this.record(handle).successors()) {
|
|
695
|
+
if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
|
|
696
|
+
edges.push({
|
|
697
|
+
from: String(nodeId(handle)),
|
|
698
|
+
from_is_reverse: isReverse(handle),
|
|
699
|
+
to: String(nodeId(successor)),
|
|
700
|
+
to_is_reverse: isReverse(successor),
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const paths = [];
|
|
706
|
+
const contig = this.refPath?.contig ?? 'unknown';
|
|
707
|
+
if (this.refId !== undefined && this.refPath && this.refInterval) {
|
|
708
|
+
const info = this.paths[this.refId];
|
|
709
|
+
const name = { ...this.refPath, fragment: this.refPath.fragment + this.refInterval[0] };
|
|
710
|
+
paths.push(jsonPath(info, formatPathName(name, this.refPath.fragment + this.refInterval[1]), undefined));
|
|
711
|
+
}
|
|
712
|
+
let haplotype = 1;
|
|
713
|
+
this.paths.forEach((info, index) => {
|
|
714
|
+
if (index === this.refId) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const resolved = opts.names === 'resolved' ? info.identity : undefined;
|
|
718
|
+
const name = resolved
|
|
719
|
+
? formatPathName({ ...resolved.name, fragment: resolved.name.fragment + resolved.hapStart }, resolved.name.fragment + resolved.hapEnd)
|
|
720
|
+
: formatPathName({ sample: 'unknown', contig, haplotype, fragment: 0 }, info.len);
|
|
721
|
+
paths.push(jsonPath(info, name, cigar ? this.alignToRef(index) : undefined));
|
|
722
|
+
haplotype += 1;
|
|
723
|
+
});
|
|
724
|
+
return { nodes, edges, paths };
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function jsonPath(info, name, cigar) {
|
|
728
|
+
return {
|
|
729
|
+
name,
|
|
730
|
+
...(info.weight === undefined ? {} : { weight: info.weight }),
|
|
731
|
+
...(cigar === undefined ? {} : { cigar }),
|
|
732
|
+
path: info.path.map(handle => ({ id: String(nodeId(handle)), is_reverse: isReverse(handle) })),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function comparePaths(a, b) {
|
|
736
|
+
const n = Math.min(a.length, b.length);
|
|
737
|
+
for (let i = 0; i < n; i++) {
|
|
738
|
+
const x = a[i];
|
|
739
|
+
const y = b[i];
|
|
740
|
+
if (x !== y) {
|
|
741
|
+
return x < y ? -1 : 1;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return a.length - b.length;
|
|
745
|
+
}
|
|
746
|
+
function appendEdit(edits, op, len) {
|
|
747
|
+
if (len === 0) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const last = edits[edits.length - 1];
|
|
751
|
+
if (last && last[0] === op) {
|
|
752
|
+
last[1] += len;
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
edits.push([op, len]);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function gapPenalty(len) {
|
|
759
|
+
return len === 0 ? 0 : 6 + (len - 1);
|
|
760
|
+
}
|