@gmod/gbz-base 2.2.0 → 2.4.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 -19
- package/dist/cli.js +61 -3
- package/dist/cli.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.js +3 -0
- package/dist/db.js.map +1 -1
- package/dist/gbwt/node.d.ts +1 -0
- package/dist/gbwt/node.js +3 -2
- package/dist/gbwt/node.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/query.js +16 -5
- package/dist/query.js.map +1 -1
- package/dist/sqlite/btree.d.ts +4 -0
- package/dist/sqlite/btree.js +66 -0
- package/dist/sqlite/btree.js.map +1 -1
- package/dist/sqlite/database.d.ts +1 -0
- package/dist/sqlite/database.js +3 -0
- package/dist/sqlite/database.js.map +1 -1
- package/dist/subgraph.d.ts +46 -6
- package/dist/subgraph.js +448 -129
- package/dist/subgraph.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +78 -3
- package/src/db.ts +4 -0
- package/src/gbwt/node.ts +7 -2
- package/src/index.ts +4 -1
- package/src/query.ts +19 -9
- package/src/sqlite/btree.ts +72 -0
- package/src/sqlite/database.ts +4 -0
- package/src/subgraph.ts +566 -138
package/dist/subgraph.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import { ENDMARKER, edgeIsCanonical, encodeNode, entryOrientation, entrySide, exitOrientation, exitSide, flipNode, flipSide, isReverse, nodeId, nodeOrientation, pathIsCanonical, } from "./gbwt/node.js";
|
|
1
|
+
import { ENDMARKER, edgeIsCanonical, encodeNode, entryOrientation, entrySide, exitOrientation, exitSide, flipNode, flipSide, isReverse, nodeId, nodeOrientation, pathEndsAreCanonical, pathIsCanonical, } from "./gbwt/node.js";
|
|
2
2
|
import { gfaHeaderLines, sha256Hex, subgraphName } from "./graphName.js";
|
|
3
3
|
import { weightedLcs } from "./lcs.js";
|
|
4
4
|
import { formatPathName } from "./pathName.js";
|
|
5
|
+
export class SubgraphLimitError extends Error {
|
|
6
|
+
name = 'SubgraphLimitError';
|
|
7
|
+
limit;
|
|
8
|
+
windowBp;
|
|
9
|
+
walkedBp;
|
|
10
|
+
constructor(limit, walk) {
|
|
11
|
+
super(walk === undefined
|
|
12
|
+
? `Subgraph size limit of ${limit} nodes exceeded`
|
|
13
|
+
: `Subgraph size limit of ${limit} nodes exceeded ${walk.walkedBp} bp into a ${walk.windowBp} bp window`);
|
|
14
|
+
this.limit = limit;
|
|
15
|
+
this.windowBp = walk?.windowBp;
|
|
16
|
+
this.walkedBp = walk?.walkedBp;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
5
19
|
function sideBefore(a, b) {
|
|
6
20
|
return (a[0] < b[0] ||
|
|
7
21
|
(a[0] === b[0] && (a[1] < b[1] || (a[1] === b[1] && a[2] < b[2]))));
|
|
@@ -58,20 +72,122 @@ class SideQueue {
|
|
|
58
72
|
function posKey(pos) {
|
|
59
73
|
return `${pos.node}:${pos.offset}`;
|
|
60
74
|
}
|
|
75
|
+
function joinable(a, b) {
|
|
76
|
+
const insertion = b.identity.hapStart - a.identity.hapEnd;
|
|
77
|
+
const deletion = a.strand === '+' ? b.refStart - a.refEnd : a.refStart - b.refEnd;
|
|
78
|
+
return a.strand === b.strand && insertion >= 0 && deletion >= 0
|
|
79
|
+
? { insertion, deletion }
|
|
80
|
+
: undefined;
|
|
81
|
+
}
|
|
82
|
+
function joinPair(a, b, gap) {
|
|
83
|
+
const [left, right] = a.strand === '+' ? [a, b] : [b, a];
|
|
84
|
+
const edits = left.edits.map(([op, len]) => [op, len]);
|
|
85
|
+
appendGap(edits, gap.insertion, gap.deletion);
|
|
86
|
+
for (const [op, len] of right.edits) {
|
|
87
|
+
appendEdit(edits, op, len);
|
|
88
|
+
}
|
|
89
|
+
const walkForward = a.identity.walkForward;
|
|
90
|
+
const [first, second] = walkForward ? [a, b] : [b, a];
|
|
91
|
+
return {
|
|
92
|
+
strand: a.strand,
|
|
93
|
+
refStart: left.refStart,
|
|
94
|
+
refEnd: right.refEnd,
|
|
95
|
+
edits,
|
|
96
|
+
weight: undefined,
|
|
97
|
+
path: [...first.path, ...second.path],
|
|
98
|
+
start: first.start,
|
|
99
|
+
identity: {
|
|
100
|
+
pathHandle: a.identity.pathHandle,
|
|
101
|
+
name: a.identity.name,
|
|
102
|
+
hapStart: a.identity.hapStart,
|
|
103
|
+
hapEnd: b.identity.hapEnd,
|
|
104
|
+
walkForward,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function joinSiblings(fragments) {
|
|
109
|
+
if (fragments.some(f => f.weight !== undefined)) {
|
|
110
|
+
return fragments;
|
|
111
|
+
}
|
|
112
|
+
const byPath = new Map();
|
|
113
|
+
fragments.forEach((fragment, index) => {
|
|
114
|
+
if (fragment.identity) {
|
|
115
|
+
const siblings = byPath.get(fragment.identity.pathHandle);
|
|
116
|
+
if (siblings) {
|
|
117
|
+
siblings.push(index);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
byPath.set(fragment.identity.pathHandle, [index]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
const joined = new Map();
|
|
125
|
+
const consumed = new Set();
|
|
126
|
+
for (const siblings of byPath.values()) {
|
|
127
|
+
siblings.sort((x, y) => fragments[x].identity.hapStart - fragments[y].identity.hapStart);
|
|
128
|
+
let head = siblings[0];
|
|
129
|
+
let current = fragments[head];
|
|
130
|
+
for (const index of siblings.slice(1)) {
|
|
131
|
+
const next = fragments[index];
|
|
132
|
+
const gap = joinable(current, next);
|
|
133
|
+
if (gap) {
|
|
134
|
+
current = joinPair(current, next, gap);
|
|
135
|
+
consumed.add(index);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
joined.set(head, current);
|
|
139
|
+
head = index;
|
|
140
|
+
current = next;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
joined.set(head, current);
|
|
144
|
+
}
|
|
145
|
+
return fragments.flatMap((fragment, index) => consumed.has(index) ? [] : [joined.get(index) ?? fragment]);
|
|
146
|
+
}
|
|
147
|
+
function pathPosition(info, k) {
|
|
148
|
+
return { node: info.path[k], offset: info.offsets[k] };
|
|
149
|
+
}
|
|
150
|
+
const SCAN_GAP = 4096;
|
|
151
|
+
function handleRuns(sortedHandles) {
|
|
152
|
+
const runs = [];
|
|
153
|
+
for (const handle of sortedHandles) {
|
|
154
|
+
const last = runs[runs.length - 1];
|
|
155
|
+
if (last && handle - last[1] <= SCAN_GAP) {
|
|
156
|
+
last[1] = handle;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
runs.push([handle, handle]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return runs;
|
|
163
|
+
}
|
|
61
164
|
export class Subgraph {
|
|
62
165
|
records = new Map();
|
|
63
166
|
paths = [];
|
|
167
|
+
twinStarts = new Set();
|
|
64
168
|
refId;
|
|
65
169
|
refPath;
|
|
66
170
|
refHandle;
|
|
67
171
|
refInterval;
|
|
68
172
|
refIndexCache;
|
|
69
173
|
refPrefixCache;
|
|
174
|
+
walkedBp;
|
|
70
175
|
stats = {
|
|
71
176
|
orderedAlignments: 0,
|
|
72
177
|
lcsAlignments: 0,
|
|
73
178
|
identificationSteps: 0,
|
|
74
179
|
identificationFetches: 0,
|
|
180
|
+
identification: {
|
|
181
|
+
interval: 0,
|
|
182
|
+
scans: [],
|
|
183
|
+
windowSamples: 0,
|
|
184
|
+
fragmentLengths: [],
|
|
185
|
+
companionSeeks: 0,
|
|
186
|
+
companionMisses: 0,
|
|
187
|
+
graphLookups: 0,
|
|
188
|
+
graphFetches: 0,
|
|
189
|
+
chains: [],
|
|
190
|
+
},
|
|
75
191
|
};
|
|
76
192
|
db;
|
|
77
193
|
limit;
|
|
@@ -115,7 +231,7 @@ export class Subgraph {
|
|
|
115
231
|
async addNode(id) {
|
|
116
232
|
this.signal?.throwIfAborted();
|
|
117
233
|
if (this.limit !== undefined && this.nodeCount >= this.limit) {
|
|
118
|
-
throw new
|
|
234
|
+
throw new SubgraphLimitError(this.limit);
|
|
119
235
|
}
|
|
120
236
|
const forward = await this.db.getRecord(encodeNode(id, 'forward'));
|
|
121
237
|
const reverse = await this.db.getRecord(encodeNode(id, 'reverse'));
|
|
@@ -132,6 +248,7 @@ export class Subgraph {
|
|
|
132
248
|
}
|
|
133
249
|
clearPaths() {
|
|
134
250
|
this.paths = [];
|
|
251
|
+
this.twinStarts.clear();
|
|
135
252
|
this.refId = undefined;
|
|
136
253
|
this.refPath = undefined;
|
|
137
254
|
this.refHandle = undefined;
|
|
@@ -190,10 +307,35 @@ export class Subgraph {
|
|
|
190
307
|
active.push(record.sequenceLen - nodeOffset - 1, id, exitSide(orientation));
|
|
191
308
|
return this.insertContext(active, context);
|
|
192
309
|
}
|
|
310
|
+
async prefetchReferenceWalk(reference, len) {
|
|
311
|
+
const last = await this.db.indexedPosition(reference.handle, reference.position.seqOffset + len);
|
|
312
|
+
if (last) {
|
|
313
|
+
const a = reference.position.handle;
|
|
314
|
+
const b = last.pos.node;
|
|
315
|
+
await this.db.prefetchRecords(Math.min(a, b), Math.max(a, b) + 1);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
193
318
|
async aroundInterval(start, len, context) {
|
|
194
319
|
if (len === 0) {
|
|
195
320
|
throw new Error('Interval length must be greater than 0');
|
|
196
321
|
}
|
|
322
|
+
this.walkedBp = 0;
|
|
323
|
+
try {
|
|
324
|
+
return await this.walkInterval(start, len, context);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
throw error instanceof SubgraphLimitError && error.windowBp === undefined
|
|
328
|
+
? new SubgraphLimitError(error.limit, {
|
|
329
|
+
windowBp: len,
|
|
330
|
+
walkedBp: this.walkedBp,
|
|
331
|
+
})
|
|
332
|
+
: error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
get referenceWalkedBp() {
|
|
336
|
+
return this.walkedBp;
|
|
337
|
+
}
|
|
338
|
+
async walkInterval(start, len, context) {
|
|
197
339
|
let pos = { node: start.handle, offset: start.gbwtOffset };
|
|
198
340
|
let offset = start.nodeOffset;
|
|
199
341
|
let remaining = len;
|
|
@@ -201,6 +343,7 @@ export class Subgraph {
|
|
|
201
343
|
for (;;) {
|
|
202
344
|
const id = nodeId(pos.node);
|
|
203
345
|
const orientation = nodeOrientation(pos.node);
|
|
346
|
+
this.walkedBp = len - remaining;
|
|
204
347
|
await this.ensureNode(id);
|
|
205
348
|
const record = this.record(pos.node);
|
|
206
349
|
if (offset >= record.sequenceLen) {
|
|
@@ -221,6 +364,7 @@ export class Subgraph {
|
|
|
221
364
|
offset = 0;
|
|
222
365
|
remaining -= distanceToNext;
|
|
223
366
|
}
|
|
367
|
+
this.walkedBp = len;
|
|
224
368
|
return this.insertContext(active, context);
|
|
225
369
|
}
|
|
226
370
|
async aroundNodes(nodes, context) {
|
|
@@ -420,64 +564,126 @@ export class Subgraph {
|
|
|
420
564
|
this.refPath = reference?.name;
|
|
421
565
|
this.refHandle = reference?.handle;
|
|
422
566
|
const handles = this.sortedHandles();
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
567
|
+
const count = handles.length;
|
|
568
|
+
const indexOf = new Map();
|
|
569
|
+
handles.forEach((handle, i) => indexOf.set(handle, i));
|
|
570
|
+
const nextIndex = [];
|
|
571
|
+
const nextOffset = [];
|
|
572
|
+
const hasPredecessor = [];
|
|
573
|
+
const seqLen = new Int32Array(count);
|
|
574
|
+
for (let i = 0; i < count; i++) {
|
|
575
|
+
const record = this.record(handles[i]);
|
|
576
|
+
const { nodes, offsets } = record.gbwt().decompressArrays();
|
|
577
|
+
seqLen[i] = record.sequenceLen;
|
|
578
|
+
nextIndex.push(nodes);
|
|
579
|
+
nextOffset.push(offsets);
|
|
580
|
+
hasPredecessor.push(new Uint8Array(nodes.length));
|
|
431
581
|
}
|
|
432
|
-
for (
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
582
|
+
for (let i = 0; i < count; i++) {
|
|
583
|
+
const nodes = nextIndex[i];
|
|
584
|
+
const offsets = nextOffset[i];
|
|
585
|
+
for (let k = 0; k < nodes.length; k++) {
|
|
586
|
+
const j = indexOf.get(nodes[k]);
|
|
587
|
+
if (j === undefined) {
|
|
588
|
+
nodes[k] = -1;
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
nodes[k] = j;
|
|
592
|
+
hasPredecessor[j][offsets[k]] = 1;
|
|
438
593
|
}
|
|
439
594
|
}
|
|
440
595
|
}
|
|
596
|
+
const refIndex = refPos === undefined ? undefined : indexOf.get(refPos.handle);
|
|
597
|
+
const refGbwtOffset = refPos?.gbwtOffset;
|
|
441
598
|
let refOffset;
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
599
|
+
const walk = (i, offset, path) => {
|
|
600
|
+
const offsets = [];
|
|
601
|
+
let steps = 0;
|
|
602
|
+
let len = 0;
|
|
603
|
+
let refAt = -1;
|
|
604
|
+
let cur = i;
|
|
605
|
+
let off = offset;
|
|
606
|
+
for (;;) {
|
|
607
|
+
if (cur === refIndex && off === refGbwtOffset) {
|
|
608
|
+
refAt = steps;
|
|
609
|
+
}
|
|
610
|
+
if (path) {
|
|
611
|
+
path.push(handles[cur]);
|
|
612
|
+
offsets.push(off);
|
|
613
|
+
}
|
|
614
|
+
steps += 1;
|
|
615
|
+
len += seqLen[cur];
|
|
616
|
+
const next = nextIndex[cur][off];
|
|
617
|
+
if (next < 0) {
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
off = nextOffset[cur][off];
|
|
621
|
+
cur = next;
|
|
622
|
+
}
|
|
623
|
+
return { offsets, len, refAt, last: cur };
|
|
624
|
+
};
|
|
625
|
+
const keep = (path, offsets, len, refAt) => {
|
|
626
|
+
if (refAt >= 0) {
|
|
627
|
+
this.refId = this.paths.length;
|
|
628
|
+
refOffset = refAt;
|
|
629
|
+
}
|
|
630
|
+
this.paths.push({
|
|
631
|
+
path,
|
|
632
|
+
offsets,
|
|
633
|
+
len,
|
|
634
|
+
weight: undefined,
|
|
635
|
+
identity: undefined,
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
const twinsExpected = new Int32Array(count);
|
|
639
|
+
const refMayStartReversed = refIndex !== undefined && isReverse(handles[refIndex]);
|
|
640
|
+
for (let i = 0; i < count; i++) {
|
|
641
|
+
const first = handles[i];
|
|
642
|
+
if (!isReverse(first)) {
|
|
643
|
+
const starts = hasPredecessor[i];
|
|
644
|
+
for (let offset = 0; offset < starts.length; offset++) {
|
|
645
|
+
if (starts[offset] === 0) {
|
|
646
|
+
const path = [];
|
|
647
|
+
const { offsets, len, refAt, last } = walk(i, offset, path);
|
|
648
|
+
const lastHandle = handles[last];
|
|
649
|
+
if (refAt >= 0 || pathEndsAreCanonical(first, lastHandle)) {
|
|
650
|
+
keep(path, offsets, len, refAt);
|
|
651
|
+
if (!isReverse(lastHandle)) {
|
|
652
|
+
const twinRecord = indexOf.get(flipNode(lastHandle));
|
|
653
|
+
twinsExpected[twinRecord] = twinsExpected[twinRecord] + 1;
|
|
654
|
+
}
|
|
468
655
|
}
|
|
469
656
|
else {
|
|
470
|
-
|
|
657
|
+
this.twinStarts.add(posKey({ node: first, offset }));
|
|
471
658
|
}
|
|
472
659
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
for (let i = 0; i < count; i++) {
|
|
664
|
+
const first = handles[i];
|
|
665
|
+
if (isReverse(first)) {
|
|
666
|
+
const starts = hasPredecessor[i];
|
|
667
|
+
let startCount = 0;
|
|
668
|
+
for (const flag of starts) {
|
|
669
|
+
if (flag === 0) {
|
|
670
|
+
startCount += 1;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const allTwins = !refMayStartReversed && startCount === twinsExpected[i];
|
|
674
|
+
for (let offset = 0; offset < starts.length; offset++) {
|
|
675
|
+
if (starts[offset] === 0) {
|
|
676
|
+
const bare = allTwins ? undefined : walk(i, offset, undefined);
|
|
677
|
+
if (bare &&
|
|
678
|
+
(bare.refAt >= 0 ||
|
|
679
|
+
pathEndsAreCanonical(first, handles[bare.last]))) {
|
|
680
|
+
const path = [];
|
|
681
|
+
const { offsets, len, refAt } = walk(i, offset, path);
|
|
682
|
+
keep(path, offsets, len, refAt);
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
this.twinStarts.add(posKey({ node: first, offset }));
|
|
686
|
+
}
|
|
481
687
|
}
|
|
482
688
|
}
|
|
483
689
|
}
|
|
@@ -533,37 +739,93 @@ export class Subgraph {
|
|
|
533
739
|
this.paths = merged;
|
|
534
740
|
this.refId = refId;
|
|
535
741
|
}
|
|
742
|
+
// Narrows an identified subgraph to the reference walk and the named walks
|
|
743
|
+
// `wanted` accepts, and drops every node only the discarded walks visited,
|
|
744
|
+
// so a cut for a chosen set draws that set's private sequence and nothing
|
|
745
|
+
// else's. Unresolved walks are discarded with the rest.
|
|
746
|
+
keepHaplotypes(wanted) {
|
|
747
|
+
const refInfo = this.refId === undefined ? undefined : this.paths[this.refId];
|
|
748
|
+
const kept = this.paths.filter((info, index) => index === this.refId ||
|
|
749
|
+
(info.identity !== undefined && wanted(info.identity.name)));
|
|
750
|
+
const visited = new Set();
|
|
751
|
+
for (const info of kept) {
|
|
752
|
+
for (const handle of info.path) {
|
|
753
|
+
visited.add(nodeId(handle));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
for (const handle of [...this.records.keys()]) {
|
|
757
|
+
if (!visited.has(nodeId(handle))) {
|
|
758
|
+
this.records.delete(handle);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
this.paths = kept;
|
|
762
|
+
this.refId = refInfo === undefined ? undefined : kept.indexOf(refInfo);
|
|
763
|
+
}
|
|
536
764
|
async identifyPaths() {
|
|
537
765
|
if (!this.db.hasHaplotypeIndex) {
|
|
538
766
|
throw new Error('The database has no HaplotypeSamples table; run gbz-haplotype-index on it');
|
|
539
767
|
}
|
|
540
768
|
const interval = (await this.db.haplotypeSampleInterval()) ?? 4096;
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
const maxHandle = handles[handles.length - 1];
|
|
544
|
-
if (minHandle === undefined || maxHandle === undefined) {
|
|
769
|
+
const runs = handleRuns(this.sortedHandles());
|
|
770
|
+
if (runs.length === 0) {
|
|
545
771
|
return;
|
|
546
772
|
}
|
|
547
773
|
const samples = new Map();
|
|
548
|
-
for (const
|
|
549
|
-
|
|
774
|
+
for (const [first, last] of runs) {
|
|
775
|
+
for (const sample of await this.db.haplotypeSamplesInRange(first, last)) {
|
|
776
|
+
samples.set(posKey(sample), sample);
|
|
777
|
+
}
|
|
550
778
|
}
|
|
779
|
+
const scanned = (handle) => {
|
|
780
|
+
let lo = 0;
|
|
781
|
+
let hi = runs.length - 1;
|
|
782
|
+
while (lo < hi) {
|
|
783
|
+
const mid = (lo + hi + 1) >> 1;
|
|
784
|
+
if (runs[mid][0] <= handle) {
|
|
785
|
+
lo = mid;
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
hi = mid - 1;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const run = runs[lo];
|
|
792
|
+
return run[0] <= handle && handle <= run[1];
|
|
793
|
+
};
|
|
551
794
|
const starts = new Map();
|
|
552
795
|
this.paths.forEach((info, index) => {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
796
|
+
if (info.path.length > 0 && index !== this.refId) {
|
|
797
|
+
starts.set(posKey(pathPosition(info, 0)), index);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
const identification = this.stats.identification;
|
|
801
|
+
identification.interval = interval;
|
|
802
|
+
identification.scans = runs;
|
|
803
|
+
identification.windowSamples = samples.size;
|
|
804
|
+
this.paths.forEach((info, index) => {
|
|
805
|
+
if (index !== this.refId) {
|
|
806
|
+
identification.fragmentLengths.push(info.len);
|
|
556
807
|
}
|
|
557
808
|
});
|
|
558
|
-
const
|
|
809
|
+
const readRecord = this.recordReader(() => {
|
|
559
810
|
this.stats.identificationFetches += 1;
|
|
811
|
+
identification.graphFetches += 1;
|
|
560
812
|
});
|
|
561
|
-
const
|
|
562
|
-
|
|
813
|
+
const recordAt = (handle) => {
|
|
814
|
+
identification.graphLookups += 1;
|
|
815
|
+
return readRecord(handle);
|
|
816
|
+
};
|
|
817
|
+
const sampleAt = async (pos, chain) => {
|
|
818
|
+
if (scanned(pos.node)) {
|
|
563
819
|
return samples.get(posKey(pos));
|
|
564
820
|
}
|
|
565
821
|
this.stats.identificationFetches += 1;
|
|
566
|
-
|
|
822
|
+
identification.companionSeeks += 1;
|
|
823
|
+
chain.seeks += 1;
|
|
824
|
+
const sample = await this.db.haplotypeSampleAt(pos.node, pos.offset);
|
|
825
|
+
if (!sample) {
|
|
826
|
+
identification.companionMisses += 1;
|
|
827
|
+
}
|
|
828
|
+
return sample;
|
|
567
829
|
};
|
|
568
830
|
const names = new Map();
|
|
569
831
|
const nameOf = async (pathHandle) => {
|
|
@@ -607,6 +869,16 @@ export class Subgraph {
|
|
|
607
869
|
}
|
|
608
870
|
const chain = [];
|
|
609
871
|
const visited = new Set();
|
|
872
|
+
const record = {
|
|
873
|
+
fragments: 0,
|
|
874
|
+
steps: 0,
|
|
875
|
+
seeks: 0,
|
|
876
|
+
reentries: 0,
|
|
877
|
+
twinLandings: 0,
|
|
878
|
+
end: 'endmarker',
|
|
879
|
+
pathHandle: undefined,
|
|
880
|
+
};
|
|
881
|
+
identification.chains.push(record);
|
|
610
882
|
let anchor;
|
|
611
883
|
let counter = 0;
|
|
612
884
|
let current = start;
|
|
@@ -615,13 +887,16 @@ export class Subgraph {
|
|
|
615
887
|
this.signal?.throwIfAborted();
|
|
616
888
|
if (current !== undefined) {
|
|
617
889
|
if (visited.has(current)) {
|
|
890
|
+
record.end = 'cycle';
|
|
618
891
|
break;
|
|
619
892
|
}
|
|
620
893
|
visited.add(current);
|
|
621
894
|
const info = this.paths[current];
|
|
622
895
|
chain.push({ index: current, startBp: counter });
|
|
896
|
+
record.fragments += 1;
|
|
623
897
|
let bp = counter;
|
|
624
|
-
for (
|
|
898
|
+
for (let k = 0; k < info.path.length; k++) {
|
|
899
|
+
const position = pathPosition(info, k);
|
|
625
900
|
const sample = samples.get(posKey(position));
|
|
626
901
|
const nodeLen = this.record(position.node).sequenceLen;
|
|
627
902
|
if (sample) {
|
|
@@ -632,40 +907,54 @@ export class Subgraph {
|
|
|
632
907
|
}
|
|
633
908
|
counter += info.len;
|
|
634
909
|
if (anchor) {
|
|
910
|
+
record.end = 'in-fragment sample';
|
|
635
911
|
break;
|
|
636
912
|
}
|
|
637
|
-
const last = info
|
|
913
|
+
const last = pathPosition(info, info.path.length - 1);
|
|
638
914
|
pos = this.record(last.node).gbwt().lf(last.offset);
|
|
639
915
|
current = undefined;
|
|
640
916
|
}
|
|
641
917
|
if (pos === undefined || pos.node === ENDMARKER) {
|
|
918
|
+
record.end = 'endmarker';
|
|
642
919
|
break;
|
|
643
920
|
}
|
|
644
|
-
const
|
|
921
|
+
const key = posKey(pos);
|
|
922
|
+
const known = starts.get(key);
|
|
645
923
|
if (known !== undefined) {
|
|
646
924
|
const identity = this.paths[known].identity;
|
|
647
925
|
if (identity) {
|
|
648
926
|
anchor = anchorFromIdentity(identity, counter);
|
|
927
|
+
record.end = 'identified sibling';
|
|
649
928
|
break;
|
|
650
929
|
}
|
|
651
930
|
current = known;
|
|
652
931
|
continue;
|
|
653
932
|
}
|
|
654
|
-
|
|
655
|
-
|
|
933
|
+
if (this.records.has(pos.node)) {
|
|
934
|
+
record.reentries += 1;
|
|
935
|
+
}
|
|
936
|
+
if (this.twinStarts.has(key)) {
|
|
937
|
+
record.twinLandings += 1;
|
|
938
|
+
}
|
|
939
|
+
const sample = await sampleAt(pos, record);
|
|
940
|
+
const node = await recordAt(pos.node);
|
|
656
941
|
if (sample) {
|
|
657
|
-
anchor = anchorFromSample(sample, counter,
|
|
942
|
+
anchor = anchorFromSample(sample, counter, node.sequenceLen);
|
|
943
|
+
record.end = 'out-of-window sample';
|
|
658
944
|
break;
|
|
659
945
|
}
|
|
660
946
|
this.stats.identificationSteps += 1;
|
|
947
|
+
record.steps += 1;
|
|
661
948
|
if (counter - chain[chain.length - 1].startBp >
|
|
662
|
-
4 * interval + 4 *
|
|
949
|
+
4 * interval + 4 * node.sequenceLen) {
|
|
950
|
+
record.end = 'bound';
|
|
663
951
|
break;
|
|
664
952
|
}
|
|
665
|
-
counter +=
|
|
666
|
-
pos =
|
|
953
|
+
counter += node.sequenceLen;
|
|
954
|
+
pos = node.gbwt().lf(pos.offset);
|
|
667
955
|
}
|
|
668
956
|
if (anchor) {
|
|
957
|
+
record.pathHandle = anchor.pathHandle;
|
|
669
958
|
const name = await nameOf(anchor.pathHandle);
|
|
670
959
|
for (const { index, startBp } of chain) {
|
|
671
960
|
const info = this.paths[index];
|
|
@@ -807,30 +1096,7 @@ export class Subgraph {
|
|
|
807
1096
|
suffix = refLen - prefix;
|
|
808
1097
|
}
|
|
809
1098
|
appendEdit(edits, 'M', prefix);
|
|
810
|
-
|
|
811
|
-
const refMiddle = refLen - prefix - suffix;
|
|
812
|
-
if (pathMiddle === 0) {
|
|
813
|
-
appendEdit(edits, 'D', refMiddle);
|
|
814
|
-
}
|
|
815
|
-
else if (refMiddle === 0) {
|
|
816
|
-
appendEdit(edits, 'I', pathMiddle);
|
|
817
|
-
}
|
|
818
|
-
else {
|
|
819
|
-
const mismatch = Math.min(pathMiddle, refMiddle);
|
|
820
|
-
const mismatchIndel = 4 * mismatch +
|
|
821
|
-
gapPenalty(pathMiddle - mismatch) +
|
|
822
|
-
gapPenalty(refMiddle - mismatch);
|
|
823
|
-
const insertionDeletion = gapPenalty(pathMiddle) + gapPenalty(refMiddle);
|
|
824
|
-
if (mismatchIndel <= insertionDeletion) {
|
|
825
|
-
appendEdit(edits, 'M', mismatch);
|
|
826
|
-
appendEdit(edits, 'I', pathMiddle - mismatch);
|
|
827
|
-
appendEdit(edits, 'D', refMiddle - mismatch);
|
|
828
|
-
}
|
|
829
|
-
else {
|
|
830
|
-
appendEdit(edits, 'I', pathMiddle);
|
|
831
|
-
appendEdit(edits, 'D', refMiddle);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
1099
|
+
appendGap(edits, pathLen - prefix - suffix, refLen - prefix - suffix);
|
|
834
1100
|
appendEdit(edits, 'M', suffix);
|
|
835
1101
|
}
|
|
836
1102
|
sharedWeight(path, ref) {
|
|
@@ -854,18 +1120,30 @@ export class Subgraph {
|
|
|
854
1120
|
const lcs = ordered ??
|
|
855
1121
|
weightedLcs(path, ref, handle => this.record(handle).sequenceLen)[0];
|
|
856
1122
|
const edits = [];
|
|
1123
|
+
const refPrefix = this.refPrefix(ref);
|
|
1124
|
+
const alignGap = (pathFrom, pathTo, refFrom, refTo) => {
|
|
1125
|
+
if (pathFrom === pathTo) {
|
|
1126
|
+
appendEdit(edits, 'D', refPrefix[refTo] - refPrefix[refFrom]);
|
|
1127
|
+
}
|
|
1128
|
+
else if (refFrom === refTo) {
|
|
1129
|
+
appendEdit(edits, 'I', this.pathLen(path.slice(pathFrom, pathTo)));
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
this.align(path.slice(pathFrom, pathTo), ref.slice(refFrom, refTo), edits);
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
857
1135
|
let matched = 0;
|
|
858
1136
|
let pathOffset = 0;
|
|
859
1137
|
let refOffset = 0;
|
|
860
1138
|
for (const [nextPath, nextRef] of lcs) {
|
|
861
|
-
|
|
1139
|
+
alignGap(pathOffset, nextPath, refOffset, nextRef);
|
|
862
1140
|
const nodeLen = this.record(path[nextPath]).sequenceLen;
|
|
863
1141
|
appendEdit(edits, 'M', nodeLen);
|
|
864
1142
|
matched += nodeLen;
|
|
865
1143
|
pathOffset = nextPath + 1;
|
|
866
1144
|
refOffset = nextRef + 1;
|
|
867
1145
|
}
|
|
868
|
-
|
|
1146
|
+
alignGap(pathOffset, path.length, refOffset, ref.length);
|
|
869
1147
|
return { edits, matched };
|
|
870
1148
|
}
|
|
871
1149
|
alignment(pathIndex) {
|
|
@@ -914,7 +1192,7 @@ export class Subgraph {
|
|
|
914
1192
|
}
|
|
915
1193
|
const ref = this.paths[this.refId].path;
|
|
916
1194
|
const refTotal = this.refPrefix(ref)[ref.length];
|
|
917
|
-
const
|
|
1195
|
+
const fragments = [];
|
|
918
1196
|
this.paths.forEach((info, index) => {
|
|
919
1197
|
if (index === this.refId) {
|
|
920
1198
|
return;
|
|
@@ -936,36 +1214,40 @@ export class Subgraph {
|
|
|
936
1214
|
const alongReference = identity
|
|
937
1215
|
? (identity.orientation === 'forward') !== flipped
|
|
938
1216
|
: !flipped;
|
|
939
|
-
|
|
1217
|
+
fragments.push({
|
|
940
1218
|
strand: alongReference ? '+' : '-',
|
|
941
1219
|
refStart: reference.start + leading,
|
|
942
1220
|
refEnd: reference.start + refTotal - trailing,
|
|
943
|
-
|
|
944
|
-
.slice(first, last)
|
|
945
|
-
.map(([op, len]) => `${len}${op}`)
|
|
946
|
-
.join(''),
|
|
1221
|
+
edits: edits.slice(first, last),
|
|
947
1222
|
weight: info.weight,
|
|
948
1223
|
path: info.path,
|
|
949
|
-
start: info
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1224
|
+
start: pathPosition(info, 0),
|
|
1225
|
+
identity: identity === undefined
|
|
1226
|
+
? undefined
|
|
1227
|
+
: {
|
|
1228
|
+
pathHandle: identity.pathHandle,
|
|
1229
|
+
name: identity.name,
|
|
1230
|
+
hapStart: identity.name.fragment + identity.hapStart,
|
|
1231
|
+
hapEnd: identity.name.fragment + identity.hapEnd,
|
|
1232
|
+
walkForward: identity.orientation === 'forward',
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
1236
|
+
return joinSiblings(fragments).map(fragment => {
|
|
1237
|
+
const { edits, identity, ...rest } = fragment;
|
|
1238
|
+
const span = { ...rest, cigar: cigarOf(edits) };
|
|
1239
|
+
return identity
|
|
1240
|
+
? {
|
|
955
1241
|
...span,
|
|
956
1242
|
resolved: true,
|
|
957
1243
|
name: identity.name,
|
|
958
|
-
label: formatPathName({ ...identity.name, fragment: hapStart }, hapEnd),
|
|
1244
|
+
label: formatPathName({ ...identity.name, fragment: identity.hapStart }, identity.hapEnd),
|
|
959
1245
|
pathHandle: identity.pathHandle,
|
|
960
|
-
hapStart,
|
|
961
|
-
hapEnd,
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
else {
|
|
965
|
-
result.push({ ...span, resolved: false });
|
|
966
|
-
}
|
|
1246
|
+
hapStart: identity.hapStart,
|
|
1247
|
+
hapEnd: identity.hapEnd,
|
|
1248
|
+
}
|
|
1249
|
+
: { ...span, resolved: false };
|
|
967
1250
|
});
|
|
968
|
-
return result;
|
|
969
1251
|
}
|
|
970
1252
|
canonicalEdges(id) {
|
|
971
1253
|
const edges = [];
|
|
@@ -1022,8 +1304,8 @@ export class Subgraph {
|
|
|
1022
1304
|
}
|
|
1023
1305
|
}
|
|
1024
1306
|
}
|
|
1025
|
-
const walk = (info, name, end, cigarString) => {
|
|
1026
|
-
const steps = info
|
|
1307
|
+
const walk = (info, identity, name, end, cigarString) => {
|
|
1308
|
+
const steps = haplotypeOrderedPath(info, identity)
|
|
1027
1309
|
.map(handle => `${isReverse(handle) ? '<' : '>'}${nodeId(handle)}`)
|
|
1028
1310
|
.join('');
|
|
1029
1311
|
const weight = info.weight === undefined ? '' : `\tWT:i:${info.weight}`;
|
|
@@ -1032,7 +1314,7 @@ export class Subgraph {
|
|
|
1032
1314
|
};
|
|
1033
1315
|
const contig = this.refPath?.contig ?? 'unknown';
|
|
1034
1316
|
if (this.refId !== undefined && this.refPath && this.refInterval) {
|
|
1035
|
-
lines.push(walk(this.paths[this.refId], {
|
|
1317
|
+
lines.push(walk(this.paths[this.refId], undefined, {
|
|
1036
1318
|
...this.refPath,
|
|
1037
1319
|
fragment: this.refPath.fragment + this.refInterval[0],
|
|
1038
1320
|
}, this.refPath.fragment + this.refInterval[1], undefined));
|
|
@@ -1043,11 +1325,11 @@ export class Subgraph {
|
|
|
1043
1325
|
const resolved = opts.names === 'resolved' ? info.identity : undefined;
|
|
1044
1326
|
const cigarString = cigar ? this.alignToRef(index) : undefined;
|
|
1045
1327
|
lines.push(resolved
|
|
1046
|
-
? walk(info, {
|
|
1328
|
+
? walk(info, resolved, {
|
|
1047
1329
|
...resolved.name,
|
|
1048
1330
|
fragment: resolved.name.fragment + resolved.hapStart,
|
|
1049
1331
|
}, resolved.name.fragment + resolved.hapEnd, cigarString)
|
|
1050
|
-
: walk(info, { sample: 'unknown', contig, haplotype, fragment: 0 }, info.len, cigarString));
|
|
1332
|
+
: walk(info, undefined, { sample: 'unknown', contig, haplotype, fragment: 0 }, info.len, cigarString));
|
|
1051
1333
|
haplotype += 1;
|
|
1052
1334
|
}
|
|
1053
1335
|
});
|
|
@@ -1083,7 +1365,7 @@ export class Subgraph {
|
|
|
1083
1365
|
...this.refPath,
|
|
1084
1366
|
fragment: this.refPath.fragment + this.refInterval[0],
|
|
1085
1367
|
};
|
|
1086
|
-
paths.push(jsonPath(info, formatPathName(name, this.refPath.fragment + this.refInterval[1]), undefined));
|
|
1368
|
+
paths.push(jsonPath(info, undefined, formatPathName(name, this.refPath.fragment + this.refInterval[1]), undefined));
|
|
1087
1369
|
}
|
|
1088
1370
|
let haplotype = 1;
|
|
1089
1371
|
this.paths.forEach((info, index) => {
|
|
@@ -1097,18 +1379,28 @@ export class Subgraph {
|
|
|
1097
1379
|
fragment: resolved.name.fragment + resolved.hapStart,
|
|
1098
1380
|
}, resolved.name.fragment + resolved.hapEnd)
|
|
1099
1381
|
: formatPathName({ sample: 'unknown', contig, haplotype, fragment: 0 }, info.len);
|
|
1100
|
-
paths.push(jsonPath(info, name, cigar ? this.alignToRef(index) : undefined));
|
|
1382
|
+
paths.push(jsonPath(info, resolved, name, cigar ? this.alignToRef(index) : undefined));
|
|
1101
1383
|
haplotype += 1;
|
|
1102
1384
|
});
|
|
1103
1385
|
return { nodes, edges, paths };
|
|
1104
1386
|
}
|
|
1105
1387
|
}
|
|
1106
|
-
|
|
1388
|
+
// A named walk lists its steps in the haplotype's own direction, as the W line
|
|
1389
|
+
// spec and the start..end coordinates beside it require. extractPaths keeps
|
|
1390
|
+
// whichever twin of a walk is canonical, which is a property of the handles
|
|
1391
|
+
// and not of the haplotype, so the kept walk runs against the haplotype for
|
|
1392
|
+
// about half of them; identification records which.
|
|
1393
|
+
function haplotypeOrderedPath(info, identity) {
|
|
1394
|
+
return identity?.orientation === 'reverse'
|
|
1395
|
+
? info.path.map(handle => flipNode(handle)).reverse()
|
|
1396
|
+
: info.path;
|
|
1397
|
+
}
|
|
1398
|
+
function jsonPath(info, identity, name, cigar) {
|
|
1107
1399
|
return {
|
|
1108
1400
|
name,
|
|
1109
1401
|
...(info.weight === undefined ? {} : { weight: info.weight }),
|
|
1110
1402
|
...(cigar === undefined ? {} : { cigar }),
|
|
1111
|
-
path: info.
|
|
1403
|
+
path: haplotypeOrderedPath(info, identity).map(handle => ({
|
|
1112
1404
|
id: String(nodeId(handle)),
|
|
1113
1405
|
is_reverse: isReverse(handle),
|
|
1114
1406
|
})),
|
|
@@ -1140,4 +1432,31 @@ function appendEdit(edits, op, len) {
|
|
|
1140
1432
|
function gapPenalty(len) {
|
|
1141
1433
|
return len === 0 ? 0 : 6 + (len - 1);
|
|
1142
1434
|
}
|
|
1435
|
+
function appendGap(edits, pathMiddle, refMiddle) {
|
|
1436
|
+
if (pathMiddle === 0) {
|
|
1437
|
+
appendEdit(edits, 'D', refMiddle);
|
|
1438
|
+
}
|
|
1439
|
+
else if (refMiddle === 0) {
|
|
1440
|
+
appendEdit(edits, 'I', pathMiddle);
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
const mismatch = Math.min(pathMiddle, refMiddle);
|
|
1444
|
+
const mismatchIndel = 4 * mismatch +
|
|
1445
|
+
gapPenalty(pathMiddle - mismatch) +
|
|
1446
|
+
gapPenalty(refMiddle - mismatch);
|
|
1447
|
+
const insertionDeletion = gapPenalty(pathMiddle) + gapPenalty(refMiddle);
|
|
1448
|
+
if (mismatchIndel <= insertionDeletion) {
|
|
1449
|
+
appendEdit(edits, 'M', mismatch);
|
|
1450
|
+
appendEdit(edits, 'I', pathMiddle - mismatch);
|
|
1451
|
+
appendEdit(edits, 'D', refMiddle - mismatch);
|
|
1452
|
+
}
|
|
1453
|
+
else {
|
|
1454
|
+
appendEdit(edits, 'I', pathMiddle);
|
|
1455
|
+
appendEdit(edits, 'D', refMiddle);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
function cigarOf(edits) {
|
|
1460
|
+
return edits.map(([op, len]) => `${len}${op}`).join('');
|
|
1461
|
+
}
|
|
1143
1462
|
//# sourceMappingURL=subgraph.js.map
|