@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/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
  }
@@ -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 handles = this.sortedHandles();
542
- const minHandle = handles[0];
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 sample of await this.db.haplotypeSamplesInRange(minHandle, maxHandle)) {
549
- samples.set(posKey(sample), sample);
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
- const first = info.positions[0];
554
- if (first && index !== this.refId) {
555
- starts.set(posKey(first), index);
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 recordAt = this.recordReader(() => {
787
+ const readRecord = this.recordReader(() => {
559
788
  this.stats.identificationFetches += 1;
789
+ identification.graphFetches += 1;
560
790
  });
561
- const sampleAt = async (pos) => {
562
- if (pos.node >= minHandle && pos.node <= maxHandle) {
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
- return this.db.haplotypeSampleAt(pos.node, pos.offset);
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 (const position of info.positions) {
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.positions[info.positions.length - 1];
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 known = starts.get(posKey(pos));
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
- const sample = await sampleAt(pos);
655
- const record = await recordAt(pos.node);
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, record.sequenceLen);
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 * record.sequenceLen) {
927
+ 4 * interval + 4 * node.sequenceLen) {
928
+ record.end = 'bound';
663
929
  break;
664
930
  }
665
- counter += record.sequenceLen;
666
- pos = record.gbwt().lf(pos.offset);
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
- 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
- }
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
- this.align(path.slice(pathOffset, nextPath), ref.slice(refOffset, nextRef), edits);
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
- this.align(path.slice(pathOffset), ref.slice(refOffset), edits);
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 result = [];
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
- const span = {
1195
+ fragments.push({
940
1196
  strand: alongReference ? '+' : '-',
941
1197
  refStart: reference.start + leading,
942
1198
  refEnd: reference.start + refTotal - trailing,
943
- cigar: edits
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.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({
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