@gmod/gbz-base 2.2.0 → 2.3.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 +37 -19
- package/dist/cli.js +34 -1
- 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 +45 -6
- package/dist/subgraph.js +407 -120
- package/dist/subgraph.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +43 -1
- 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 +511 -135
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
|
}
|
|
@@ -538,32 +744,66 @@ export class Subgraph {
|
|
|
538
744
|
throw new Error('The database has no HaplotypeSamples table; run gbz-haplotype-index on it');
|
|
539
745
|
}
|
|
540
746
|
const interval = (await this.db.haplotypeSampleInterval()) ?? 4096;
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
const maxHandle = handles[handles.length - 1];
|
|
544
|
-
if (minHandle === undefined || maxHandle === undefined) {
|
|
747
|
+
const runs = handleRuns(this.sortedHandles());
|
|
748
|
+
if (runs.length === 0) {
|
|
545
749
|
return;
|
|
546
750
|
}
|
|
547
751
|
const samples = new Map();
|
|
548
|
-
for (const
|
|
549
|
-
|
|
752
|
+
for (const [first, last] of runs) {
|
|
753
|
+
for (const sample of await this.db.haplotypeSamplesInRange(first, last)) {
|
|
754
|
+
samples.set(posKey(sample), sample);
|
|
755
|
+
}
|
|
550
756
|
}
|
|
757
|
+
const scanned = (handle) => {
|
|
758
|
+
let lo = 0;
|
|
759
|
+
let hi = runs.length - 1;
|
|
760
|
+
while (lo < hi) {
|
|
761
|
+
const mid = (lo + hi + 1) >> 1;
|
|
762
|
+
if (runs[mid][0] <= handle) {
|
|
763
|
+
lo = mid;
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
hi = mid - 1;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const run = runs[lo];
|
|
770
|
+
return run[0] <= handle && handle <= run[1];
|
|
771
|
+
};
|
|
551
772
|
const starts = new Map();
|
|
552
773
|
this.paths.forEach((info, index) => {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
774
|
+
if (info.path.length > 0 && index !== this.refId) {
|
|
775
|
+
starts.set(posKey(pathPosition(info, 0)), index);
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
const identification = this.stats.identification;
|
|
779
|
+
identification.interval = interval;
|
|
780
|
+
identification.scans = runs;
|
|
781
|
+
identification.windowSamples = samples.size;
|
|
782
|
+
this.paths.forEach((info, index) => {
|
|
783
|
+
if (index !== this.refId) {
|
|
784
|
+
identification.fragmentLengths.push(info.len);
|
|
556
785
|
}
|
|
557
786
|
});
|
|
558
|
-
const
|
|
787
|
+
const readRecord = this.recordReader(() => {
|
|
559
788
|
this.stats.identificationFetches += 1;
|
|
789
|
+
identification.graphFetches += 1;
|
|
560
790
|
});
|
|
561
|
-
const
|
|
562
|
-
|
|
791
|
+
const recordAt = (handle) => {
|
|
792
|
+
identification.graphLookups += 1;
|
|
793
|
+
return readRecord(handle);
|
|
794
|
+
};
|
|
795
|
+
const sampleAt = async (pos, chain) => {
|
|
796
|
+
if (scanned(pos.node)) {
|
|
563
797
|
return samples.get(posKey(pos));
|
|
564
798
|
}
|
|
565
799
|
this.stats.identificationFetches += 1;
|
|
566
|
-
|
|
800
|
+
identification.companionSeeks += 1;
|
|
801
|
+
chain.seeks += 1;
|
|
802
|
+
const sample = await this.db.haplotypeSampleAt(pos.node, pos.offset);
|
|
803
|
+
if (!sample) {
|
|
804
|
+
identification.companionMisses += 1;
|
|
805
|
+
}
|
|
806
|
+
return sample;
|
|
567
807
|
};
|
|
568
808
|
const names = new Map();
|
|
569
809
|
const nameOf = async (pathHandle) => {
|
|
@@ -607,6 +847,16 @@ export class Subgraph {
|
|
|
607
847
|
}
|
|
608
848
|
const chain = [];
|
|
609
849
|
const visited = new Set();
|
|
850
|
+
const record = {
|
|
851
|
+
fragments: 0,
|
|
852
|
+
steps: 0,
|
|
853
|
+
seeks: 0,
|
|
854
|
+
reentries: 0,
|
|
855
|
+
twinLandings: 0,
|
|
856
|
+
end: 'endmarker',
|
|
857
|
+
pathHandle: undefined,
|
|
858
|
+
};
|
|
859
|
+
identification.chains.push(record);
|
|
610
860
|
let anchor;
|
|
611
861
|
let counter = 0;
|
|
612
862
|
let current = start;
|
|
@@ -615,13 +865,16 @@ export class Subgraph {
|
|
|
615
865
|
this.signal?.throwIfAborted();
|
|
616
866
|
if (current !== undefined) {
|
|
617
867
|
if (visited.has(current)) {
|
|
868
|
+
record.end = 'cycle';
|
|
618
869
|
break;
|
|
619
870
|
}
|
|
620
871
|
visited.add(current);
|
|
621
872
|
const info = this.paths[current];
|
|
622
873
|
chain.push({ index: current, startBp: counter });
|
|
874
|
+
record.fragments += 1;
|
|
623
875
|
let bp = counter;
|
|
624
|
-
for (
|
|
876
|
+
for (let k = 0; k < info.path.length; k++) {
|
|
877
|
+
const position = pathPosition(info, k);
|
|
625
878
|
const sample = samples.get(posKey(position));
|
|
626
879
|
const nodeLen = this.record(position.node).sequenceLen;
|
|
627
880
|
if (sample) {
|
|
@@ -632,40 +885,54 @@ export class Subgraph {
|
|
|
632
885
|
}
|
|
633
886
|
counter += info.len;
|
|
634
887
|
if (anchor) {
|
|
888
|
+
record.end = 'in-fragment sample';
|
|
635
889
|
break;
|
|
636
890
|
}
|
|
637
|
-
const last = info
|
|
891
|
+
const last = pathPosition(info, info.path.length - 1);
|
|
638
892
|
pos = this.record(last.node).gbwt().lf(last.offset);
|
|
639
893
|
current = undefined;
|
|
640
894
|
}
|
|
641
895
|
if (pos === undefined || pos.node === ENDMARKER) {
|
|
896
|
+
record.end = 'endmarker';
|
|
642
897
|
break;
|
|
643
898
|
}
|
|
644
|
-
const
|
|
899
|
+
const key = posKey(pos);
|
|
900
|
+
const known = starts.get(key);
|
|
645
901
|
if (known !== undefined) {
|
|
646
902
|
const identity = this.paths[known].identity;
|
|
647
903
|
if (identity) {
|
|
648
904
|
anchor = anchorFromIdentity(identity, counter);
|
|
905
|
+
record.end = 'identified sibling';
|
|
649
906
|
break;
|
|
650
907
|
}
|
|
651
908
|
current = known;
|
|
652
909
|
continue;
|
|
653
910
|
}
|
|
654
|
-
|
|
655
|
-
|
|
911
|
+
if (this.records.has(pos.node)) {
|
|
912
|
+
record.reentries += 1;
|
|
913
|
+
}
|
|
914
|
+
if (this.twinStarts.has(key)) {
|
|
915
|
+
record.twinLandings += 1;
|
|
916
|
+
}
|
|
917
|
+
const sample = await sampleAt(pos, record);
|
|
918
|
+
const node = await recordAt(pos.node);
|
|
656
919
|
if (sample) {
|
|
657
|
-
anchor = anchorFromSample(sample, counter,
|
|
920
|
+
anchor = anchorFromSample(sample, counter, node.sequenceLen);
|
|
921
|
+
record.end = 'out-of-window sample';
|
|
658
922
|
break;
|
|
659
923
|
}
|
|
660
924
|
this.stats.identificationSteps += 1;
|
|
925
|
+
record.steps += 1;
|
|
661
926
|
if (counter - chain[chain.length - 1].startBp >
|
|
662
|
-
4 * interval + 4 *
|
|
927
|
+
4 * interval + 4 * node.sequenceLen) {
|
|
928
|
+
record.end = 'bound';
|
|
663
929
|
break;
|
|
664
930
|
}
|
|
665
|
-
counter +=
|
|
666
|
-
pos =
|
|
931
|
+
counter += node.sequenceLen;
|
|
932
|
+
pos = node.gbwt().lf(pos.offset);
|
|
667
933
|
}
|
|
668
934
|
if (anchor) {
|
|
935
|
+
record.pathHandle = anchor.pathHandle;
|
|
669
936
|
const name = await nameOf(anchor.pathHandle);
|
|
670
937
|
for (const { index, startBp } of chain) {
|
|
671
938
|
const info = this.paths[index];
|
|
@@ -807,30 +1074,7 @@ export class Subgraph {
|
|
|
807
1074
|
suffix = refLen - prefix;
|
|
808
1075
|
}
|
|
809
1076
|
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
|
-
}
|
|
1077
|
+
appendGap(edits, pathLen - prefix - suffix, refLen - prefix - suffix);
|
|
834
1078
|
appendEdit(edits, 'M', suffix);
|
|
835
1079
|
}
|
|
836
1080
|
sharedWeight(path, ref) {
|
|
@@ -854,18 +1098,30 @@ export class Subgraph {
|
|
|
854
1098
|
const lcs = ordered ??
|
|
855
1099
|
weightedLcs(path, ref, handle => this.record(handle).sequenceLen)[0];
|
|
856
1100
|
const edits = [];
|
|
1101
|
+
const refPrefix = this.refPrefix(ref);
|
|
1102
|
+
const alignGap = (pathFrom, pathTo, refFrom, refTo) => {
|
|
1103
|
+
if (pathFrom === pathTo) {
|
|
1104
|
+
appendEdit(edits, 'D', refPrefix[refTo] - refPrefix[refFrom]);
|
|
1105
|
+
}
|
|
1106
|
+
else if (refFrom === refTo) {
|
|
1107
|
+
appendEdit(edits, 'I', this.pathLen(path.slice(pathFrom, pathTo)));
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
this.align(path.slice(pathFrom, pathTo), ref.slice(refFrom, refTo), edits);
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
857
1113
|
let matched = 0;
|
|
858
1114
|
let pathOffset = 0;
|
|
859
1115
|
let refOffset = 0;
|
|
860
1116
|
for (const [nextPath, nextRef] of lcs) {
|
|
861
|
-
|
|
1117
|
+
alignGap(pathOffset, nextPath, refOffset, nextRef);
|
|
862
1118
|
const nodeLen = this.record(path[nextPath]).sequenceLen;
|
|
863
1119
|
appendEdit(edits, 'M', nodeLen);
|
|
864
1120
|
matched += nodeLen;
|
|
865
1121
|
pathOffset = nextPath + 1;
|
|
866
1122
|
refOffset = nextRef + 1;
|
|
867
1123
|
}
|
|
868
|
-
|
|
1124
|
+
alignGap(pathOffset, path.length, refOffset, ref.length);
|
|
869
1125
|
return { edits, matched };
|
|
870
1126
|
}
|
|
871
1127
|
alignment(pathIndex) {
|
|
@@ -914,7 +1170,7 @@ export class Subgraph {
|
|
|
914
1170
|
}
|
|
915
1171
|
const ref = this.paths[this.refId].path;
|
|
916
1172
|
const refTotal = this.refPrefix(ref)[ref.length];
|
|
917
|
-
const
|
|
1173
|
+
const fragments = [];
|
|
918
1174
|
this.paths.forEach((info, index) => {
|
|
919
1175
|
if (index === this.refId) {
|
|
920
1176
|
return;
|
|
@@ -936,36 +1192,40 @@ export class Subgraph {
|
|
|
936
1192
|
const alongReference = identity
|
|
937
1193
|
? (identity.orientation === 'forward') !== flipped
|
|
938
1194
|
: !flipped;
|
|
939
|
-
|
|
1195
|
+
fragments.push({
|
|
940
1196
|
strand: alongReference ? '+' : '-',
|
|
941
1197
|
refStart: reference.start + leading,
|
|
942
1198
|
refEnd: reference.start + refTotal - trailing,
|
|
943
|
-
|
|
944
|
-
.slice(first, last)
|
|
945
|
-
.map(([op, len]) => `${len}${op}`)
|
|
946
|
-
.join(''),
|
|
1199
|
+
edits: edits.slice(first, last),
|
|
947
1200
|
weight: info.weight,
|
|
948
1201
|
path: info.path,
|
|
949
|
-
start: info
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1202
|
+
start: pathPosition(info, 0),
|
|
1203
|
+
identity: identity === undefined
|
|
1204
|
+
? undefined
|
|
1205
|
+
: {
|
|
1206
|
+
pathHandle: identity.pathHandle,
|
|
1207
|
+
name: identity.name,
|
|
1208
|
+
hapStart: identity.name.fragment + identity.hapStart,
|
|
1209
|
+
hapEnd: identity.name.fragment + identity.hapEnd,
|
|
1210
|
+
walkForward: identity.orientation === 'forward',
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
});
|
|
1214
|
+
return joinSiblings(fragments).map(fragment => {
|
|
1215
|
+
const { edits, identity, ...rest } = fragment;
|
|
1216
|
+
const span = { ...rest, cigar: cigarOf(edits) };
|
|
1217
|
+
return identity
|
|
1218
|
+
? {
|
|
955
1219
|
...span,
|
|
956
1220
|
resolved: true,
|
|
957
1221
|
name: identity.name,
|
|
958
|
-
label: formatPathName({ ...identity.name, fragment: hapStart }, hapEnd),
|
|
1222
|
+
label: formatPathName({ ...identity.name, fragment: identity.hapStart }, identity.hapEnd),
|
|
959
1223
|
pathHandle: identity.pathHandle,
|
|
960
|
-
hapStart,
|
|
961
|
-
hapEnd,
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
else {
|
|
965
|
-
result.push({ ...span, resolved: false });
|
|
966
|
-
}
|
|
1224
|
+
hapStart: identity.hapStart,
|
|
1225
|
+
hapEnd: identity.hapEnd,
|
|
1226
|
+
}
|
|
1227
|
+
: { ...span, resolved: false };
|
|
967
1228
|
});
|
|
968
|
-
return result;
|
|
969
1229
|
}
|
|
970
1230
|
canonicalEdges(id) {
|
|
971
1231
|
const edges = [];
|
|
@@ -1140,4 +1400,31 @@ function appendEdit(edits, op, len) {
|
|
|
1140
1400
|
function gapPenalty(len) {
|
|
1141
1401
|
return len === 0 ? 0 : 6 + (len - 1);
|
|
1142
1402
|
}
|
|
1403
|
+
function appendGap(edits, pathMiddle, refMiddle) {
|
|
1404
|
+
if (pathMiddle === 0) {
|
|
1405
|
+
appendEdit(edits, 'D', refMiddle);
|
|
1406
|
+
}
|
|
1407
|
+
else if (refMiddle === 0) {
|
|
1408
|
+
appendEdit(edits, 'I', pathMiddle);
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
const mismatch = Math.min(pathMiddle, refMiddle);
|
|
1412
|
+
const mismatchIndel = 4 * mismatch +
|
|
1413
|
+
gapPenalty(pathMiddle - mismatch) +
|
|
1414
|
+
gapPenalty(refMiddle - mismatch);
|
|
1415
|
+
const insertionDeletion = gapPenalty(pathMiddle) + gapPenalty(refMiddle);
|
|
1416
|
+
if (mismatchIndel <= insertionDeletion) {
|
|
1417
|
+
appendEdit(edits, 'M', mismatch);
|
|
1418
|
+
appendEdit(edits, 'I', pathMiddle - mismatch);
|
|
1419
|
+
appendEdit(edits, 'D', refMiddle - mismatch);
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
appendEdit(edits, 'I', pathMiddle);
|
|
1423
|
+
appendEdit(edits, 'D', refMiddle);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function cigarOf(edits) {
|
|
1428
|
+
return edits.map(([op, len]) => `${len}${op}`).join('');
|
|
1429
|
+
}
|
|
1143
1430
|
//# sourceMappingURL=subgraph.js.map
|