@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/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 Error(`Subgraph size limit of ${this.limit} nodes exceeded`);
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 successors = new Map();
424
- for (const handle of handles) {
425
- const { nodes, offsets } = this.record(handle).gbwt().decompressArrays();
426
- successors.set(handle, {
427
- nodes,
428
- offsets,
429
- hasPredecessor: new Uint8Array(nodes.length),
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 (const handle of handles) {
433
- const { nodes, offsets } = successors.get(handle);
434
- for (let i = 0; i < nodes.length; i++) {
435
- const entry = successors.get(nodes[i]);
436
- if (entry) {
437
- entry.hasPredecessor[offsets[i]] = 1;
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
- for (const handle of handles) {
443
- const entries = successors.get(handle);
444
- for (let offset = 0; offset < entries.nodes.length; offset++) {
445
- if (entries.hasPredecessor[offset] === 0) {
446
- let currNode = handle;
447
- let currOffset = offset;
448
- let isRef = false;
449
- const path = [];
450
- const positions = [];
451
- let len = 0;
452
- while (currNode !== undefined) {
453
- if (currNode === refPos?.handle &&
454
- currOffset === refPos.gbwtOffset) {
455
- this.refId = this.paths.length;
456
- refOffset = path.length;
457
- isRef = true;
458
- }
459
- path.push(currNode);
460
- positions.push({ node: currNode, offset: currOffset });
461
- len += this.record(currNode).sequenceLen;
462
- const step = successors.get(currNode);
463
- const nextNode = step.nodes[currOffset];
464
- const nextOffset = step.offsets[currOffset];
465
- if (nextNode !== ENDMARKER && successors.has(nextNode)) {
466
- currNode = nextNode;
467
- currOffset = nextOffset;
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
- currNode = undefined;
657
+ this.twinStarts.add(posKey({ node: first, offset }));
471
658
  }
472
659
  }
473
- if (isRef || pathIsCanonical(path)) {
474
- this.paths.push({
475
- path,
476
- positions,
477
- len,
478
- weight: undefined,
479
- identity: undefined,
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 handles = this.sortedHandles();
542
- const minHandle = handles[0];
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 sample of await this.db.haplotypeSamplesInRange(minHandle, maxHandle)) {
549
- samples.set(posKey(sample), sample);
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
- const first = info.positions[0];
554
- if (first && index !== this.refId) {
555
- starts.set(posKey(first), index);
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 recordAt = this.recordReader(() => {
809
+ const readRecord = this.recordReader(() => {
559
810
  this.stats.identificationFetches += 1;
811
+ identification.graphFetches += 1;
560
812
  });
561
- const sampleAt = async (pos) => {
562
- if (pos.node >= minHandle && pos.node <= maxHandle) {
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
- return this.db.haplotypeSampleAt(pos.node, pos.offset);
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 (const position of info.positions) {
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.positions[info.positions.length - 1];
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 known = starts.get(posKey(pos));
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
- const sample = await sampleAt(pos);
655
- const record = await recordAt(pos.node);
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, record.sequenceLen);
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 * record.sequenceLen) {
949
+ 4 * interval + 4 * node.sequenceLen) {
950
+ record.end = 'bound';
663
951
  break;
664
952
  }
665
- counter += record.sequenceLen;
666
- pos = record.gbwt().lf(pos.offset);
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
- const pathMiddle = pathLen - prefix - suffix;
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
- this.align(path.slice(pathOffset, nextPath), ref.slice(refOffset, nextRef), edits);
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
- this.align(path.slice(pathOffset), ref.slice(refOffset), edits);
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 result = [];
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
- const span = {
1217
+ fragments.push({
940
1218
  strand: alongReference ? '+' : '-',
941
1219
  refStart: reference.start + leading,
942
1220
  refEnd: reference.start + refTotal - trailing,
943
- cigar: edits
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.positions[0],
950
- };
951
- if (identity) {
952
- const hapStart = identity.name.fragment + identity.hapStart;
953
- const hapEnd = identity.name.fragment + identity.hapEnd;
954
- result.push({
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.path
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
- function jsonPath(info, name, cigar) {
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.path.map(handle => ({
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