@jbrowse/cigar-utils 4.1.14

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.
Files changed (47) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +167 -0
  3. package/esm/bamSeqDecoder.d.ts +3 -0
  4. package/esm/bamSeqDecoder.js +10 -0
  5. package/esm/bamSeqEncoder.d.ts +8 -0
  6. package/esm/bamSeqEncoder.js +33 -0
  7. package/esm/buildReadVsRefFeatures.d.ts +57 -0
  8. package/esm/buildReadVsRefFeatures.js +66 -0
  9. package/esm/cigarConstants.d.ts +11 -0
  10. package/esm/cigarConstants.js +15 -0
  11. package/esm/cigarRenderedSegments.d.ts +13 -0
  12. package/esm/cigarRenderedSegments.js +84 -0
  13. package/esm/cigarReorient.d.ts +2 -0
  14. package/esm/cigarReorient.js +29 -0
  15. package/esm/cigarToMismatches2.d.ts +2 -0
  16. package/esm/cigarToMismatches2.js +90 -0
  17. package/esm/cigarWalkStart.d.ts +4 -0
  18. package/esm/cigarWalkStart.js +54 -0
  19. package/esm/csOps.d.ts +47 -0
  20. package/esm/csOps.js +152 -0
  21. package/esm/csToCigar.d.ts +1 -0
  22. package/esm/csToCigar.js +43 -0
  23. package/esm/getNextRefPos.d.ts +31 -0
  24. package/esm/getNextRefPos.js +69 -0
  25. package/esm/index.d.ts +24 -0
  26. package/esm/index.js +24 -0
  27. package/esm/mdToMismatches2.d.ts +2 -0
  28. package/esm/mdToMismatches2.js +108 -0
  29. package/esm/mismatchCallback.d.ts +29 -0
  30. package/esm/mismatchCallback.js +20 -0
  31. package/esm/mismatchParser.d.ts +79 -0
  32. package/esm/mismatchParser.js +238 -0
  33. package/esm/mismatchTypes.d.ts +27 -0
  34. package/esm/mismatchTypes.js +1 -0
  35. package/esm/numericCigarHasSkip.d.ts +1 -0
  36. package/esm/numericCigarHasSkip.js +11 -0
  37. package/esm/numericCigarToString.d.ts +1 -0
  38. package/esm/numericCigarToString.js +13 -0
  39. package/esm/orientAlignment.d.ts +9 -0
  40. package/esm/orientAlignment.js +26 -0
  41. package/esm/pafIdentity.d.ts +20 -0
  42. package/esm/pafIdentity.js +31 -0
  43. package/esm/readEndpoints.d.ts +18 -0
  44. package/esm/readEndpoints.js +69 -0
  45. package/esm/samFlags.d.ts +50 -0
  46. package/esm/samFlags.js +89 -0
  47. package/package.json +42 -0
@@ -0,0 +1,84 @@
1
+ import { CIGAR_D, CIGAR_EQ, CIGAR_I, CIGAR_M, CIGAR_N, CIGAR_X, } from './cigarConstants.js';
2
+ // Indels narrower than this (in pixels) are merged into surrounding match
3
+ // context rather than rendered as separate quads. At 1px an indel resolving to
4
+ // a single pixel still renders — small on-screen detail is intentionally kept.
5
+ // The old 2px floor guarded against 1bp-indel aliasing on noisy long reads, but
6
+ // the synteny/dotplot CIGAR fill now fades sub-pixel indels by their true MSAA
7
+ // coverage instead of aliasing, so a genuinely sub-pixel indel fades honestly
8
+ // rather than flickering. A 1bp indel in a whole-chromosome view is far below
9
+ // 1px and still drops out here, as intended.
10
+ const MIN_INDEL_PX = 1;
11
+ /**
12
+ * Walks pre-parsed (packed int) CIGAR ops in bp-space and fires a callback
13
+ * for each rendered segment. Small indels (width < MIN_INDEL_PX) are merged
14
+ * into surrounding context; tiny M segments (both accumulators advance <
15
+ * bpPerPx) are accumulated before emitting.
16
+ *
17
+ * Used by synteny and dotplot GPU renderers so both stay in sync.
18
+ * Re-exported via @jbrowse/synteny-core for consistent import paths.
19
+ *
20
+ * Callback receives bp-space segment boundaries (cumBp, no inter-region
21
+ * padding). Callers convert to screen positions with hp-math.
22
+ */
23
+ export function visitCigarRenderedSegments(cigar, startBp1, startBp2, bpPerPx0, bpPerPx1, rev1, rev2, callback) {
24
+ let continuingFlag = false;
25
+ let segBp1Start = startBp1;
26
+ let segBp2Start = startBp2;
27
+ let bp1 = startBp1;
28
+ let bp2 = startBp2;
29
+ for (let j = 0; j < cigar.length; j++) {
30
+ const packed = cigar[j];
31
+ const len = packed >>> 4;
32
+ const op = packed & 0xf;
33
+ if (!continuingFlag) {
34
+ segBp1Start = bp1;
35
+ segBp2Start = bp2;
36
+ }
37
+ if (op === CIGAR_M || op === CIGAR_EQ || op === CIGAR_X) {
38
+ bp1 += len * rev1;
39
+ bp2 += len * rev2;
40
+ }
41
+ else if (op === CIGAR_D || op === CIGAR_N) {
42
+ bp1 += len * rev1;
43
+ }
44
+ else if (op === CIGAR_I) {
45
+ bp2 += len * rev2;
46
+ }
47
+ if (op === CIGAR_D || op === CIGAR_N || op === CIGAR_I) {
48
+ const relevantBpPerPx = op === CIGAR_I ? bpPerPx1 : bpPerPx0;
49
+ if (len < relevantBpPerPx * MIN_INDEL_PX) {
50
+ continuingFlag = true;
51
+ continue;
52
+ }
53
+ }
54
+ const isNotLast = j < cigar.length - 1;
55
+ if (Math.abs(bp1 - segBp1Start) <= bpPerPx0 &&
56
+ Math.abs(bp2 - segBp2Start) <= bpPerPx1 &&
57
+ isNotLast) {
58
+ continuingFlag = true;
59
+ }
60
+ else {
61
+ const span1 = Math.abs(bp1 - segBp1Start);
62
+ const span2 = Math.abs(bp2 - segBp2Start);
63
+ const resolvedOp = span1 > bpPerPx0 || span2 > bpPerPx1 ? op : CIGAR_M;
64
+ continuingFlag = false;
65
+ callback(resolvedOp, segBp1Start, bp1, segBp2Start, bp2);
66
+ }
67
+ }
68
+ // The merge above `continue`s PAST the flush, so a sub-pixel indel as the very
69
+ // last op leaves the open segment unvisited: the `isNotLast` escape hatch that
70
+ // forces a final emit is in the branch the `continue` skipped. The tail is
71
+ // usually a base or two, but it is bounded only by how long a run of sub-pixel
72
+ // indels the CIGAR ends with — 1000 trailing 1bp deletions at 100bp/px is 10px
73
+ // of query axis that never reaches the callback, so a clipped block ending in
74
+ // an indel run lost its trailing location markers, and in transparent-indels
75
+ // mode its base tile too (a hole, since a tiled feature draws no full-span
76
+ // base under it).
77
+ //
78
+ // Emitted as CIGAR_M rather than through the resolvedOp expression above: every
79
+ // op merged into this segment was individually judged sub-pixel, so the last
80
+ // one's kind is the wrong label for a span that may be mostly match.
81
+ if (continuingFlag) {
82
+ callback(CIGAR_M, segBp1Start, bp1, segBp2Start, bp2);
83
+ }
84
+ }
@@ -0,0 +1,2 @@
1
+ export declare function flipCigar(cigar: string): string;
2
+ export declare function swapIndelCigar(cigar: string): string;
@@ -0,0 +1,29 @@
1
+ // Reverse a CIGAR's op order and swap insertions<->deletions. Used to view an
2
+ // alignment from the opposite (query<->target) perspective on the reverse
3
+ // strand, where both the direction and the indel sense flip.
4
+ export function flipCigar(cigar) {
5
+ const ops = [];
6
+ let len = 0;
7
+ for (let i = 0, l = cigar.length; i < l; i++) {
8
+ const c = cigar[i];
9
+ if (c >= '0' && c <= '9') {
10
+ len = len * 10 + (c.charCodeAt(0) - 48);
11
+ }
12
+ else {
13
+ ops.push([len, c]);
14
+ len = 0;
15
+ }
16
+ }
17
+ let result = '';
18
+ for (let i = ops.length - 1; i >= 0; i--) {
19
+ const [l, op] = ops[i];
20
+ result += l;
21
+ result += op === 'D' ? 'I' : op === 'I' ? 'D' : op;
22
+ }
23
+ return result;
24
+ }
25
+ // Swap insertions<->deletions in place, leaving op order untouched. This is the
26
+ // +-strand query<->target perspective flip, where only the indel sense reverses.
27
+ export function swapIndelCigar(cigar) {
28
+ return cigar.replaceAll(/[DI]/g, op => (op === 'D' ? 'I' : 'D'));
29
+ }
@@ -0,0 +1,2 @@
1
+ import type { Mismatch } from './mismatchTypes.ts';
2
+ export declare function cigarToMismatches2(ops: ArrayLike<number>, seq?: string, ref?: string, qual?: Uint8Array): Mismatch[];
@@ -0,0 +1,90 @@
1
+ import { CIGAR_D, CIGAR_EQ, CIGAR_H, CIGAR_I, CIGAR_M, CIGAR_N, CIGAR_S, CIGAR_X, } from './cigarConstants.js';
2
+ // Handles packed NUMERIC_CIGAR format from @gmod/bam
3
+ // Format: Uint32Array where each value is (length << 4) | opIndex
4
+ // opIndex is 0-8: M=0, I=1, D=2, N=3, S=4, H=5, P=6, ==7, X=8
5
+ export function cigarToMismatches2(ops, seq, ref, qual) {
6
+ let roffset = 0;
7
+ let soffset = 0;
8
+ const mismatches = [];
9
+ const hasRefAndSeq = ref && seq;
10
+ for (let i = 0, l = ops.length; i < l; i++) {
11
+ const packed = ops[i];
12
+ const len = packed >>> 4;
13
+ const op = packed & 0xf;
14
+ if (op === CIGAR_M || op === CIGAR_EQ) {
15
+ if (hasRefAndSeq) {
16
+ for (let j = 0; j < len; j++) {
17
+ if ((seq.charCodeAt(soffset + j) | 0x20) !==
18
+ (ref.charCodeAt(roffset + j) | 0x20)) {
19
+ mismatches.push({
20
+ start: roffset + j,
21
+ type: 'mismatch',
22
+ base: seq[soffset + j],
23
+ altbase: ref[roffset + j],
24
+ length: 1,
25
+ });
26
+ }
27
+ }
28
+ }
29
+ soffset += len;
30
+ roffset += len;
31
+ }
32
+ else if (op === CIGAR_I) {
33
+ mismatches.push({
34
+ start: roffset,
35
+ type: 'insertion',
36
+ insertlen: len,
37
+ insertedBases: seq?.slice(soffset, soffset + len),
38
+ length: 0,
39
+ });
40
+ soffset += len;
41
+ }
42
+ else if (op === CIGAR_D) {
43
+ mismatches.push({
44
+ start: roffset,
45
+ type: 'deletion',
46
+ length: len,
47
+ });
48
+ roffset += len;
49
+ }
50
+ else if (op === CIGAR_N) {
51
+ mismatches.push({
52
+ start: roffset,
53
+ type: 'skip',
54
+ length: len,
55
+ });
56
+ roffset += len;
57
+ }
58
+ else if (op === CIGAR_X) {
59
+ for (let j = 0; j < len; j++) {
60
+ mismatches.push({
61
+ start: roffset + j,
62
+ type: 'mismatch',
63
+ base: seq?.[soffset + j] || 'X',
64
+ qual: qual?.[soffset + j],
65
+ length: 1,
66
+ });
67
+ }
68
+ soffset += len;
69
+ roffset += len;
70
+ }
71
+ else if (op === CIGAR_H) {
72
+ mismatches.push({
73
+ start: roffset,
74
+ type: 'hardclip',
75
+ cliplen: len,
76
+ length: 1,
77
+ });
78
+ }
79
+ else if (op === CIGAR_S) {
80
+ mismatches.push({
81
+ start: roffset,
82
+ type: 'softclip',
83
+ cliplen: len,
84
+ length: 1,
85
+ });
86
+ soffset += len;
87
+ }
88
+ }
89
+ return mismatches;
90
+ }
@@ -0,0 +1,4 @@
1
+ export declare function cigarWalkBp1(p11: number, p12: number, strand: number): number;
2
+ export declare function cigarWalkBp2(p21: number, p22: number, strand: number): number;
3
+ export declare function cigarWalkRev1(p11: number, p12: number, strand: number): -1 | 1;
4
+ export declare function cigarWalkRev2(p21: number, p22: number, strand: number): number;
@@ -0,0 +1,54 @@
1
+ // Where a comparative feature's CIGAR walk begins, and which way each axis
2
+ // steps — the four arguments `visitCigarRenderedSegments` cannot derive for
3
+ // itself, since it is handed bare coordinates. Named for the parameters they
4
+ // feed: `bp1`/`rev1` are the anchor axis, `bp2`/`rev2` the mate.
5
+ //
6
+ // Both comparative views project a feature into the same four cumBp lanes: p11
7
+ // -> p12 is the anchor's span in the DRAWN direction (their workers swap the
8
+ // two for a reverse-strand feature so a ribbon crosses and a dotplot line runs
9
+ // anti-diagonal), and p21 -> p22 is the mate's, always start -> end.
10
+ //
11
+ // That swap is exactly what makes the walk's start corner not the (p11, p21)
12
+ // one. PAF writes a '-' strand `cg` in ANCHOR-FORWARD order with the mate
13
+ // walking backward, so op 0 sits at (anchor start, mate end) — which after the
14
+ // swap is (p12, p22). Beginning at (p11, p21) and stepping backward instead
15
+ // traverses the same line and lands on the same endpoints, so the drawn shape's
16
+ // extent is right and only the interior is wrong: every indel comes out
17
+ // mirrored through the block's centre. `clipSyntenyFeature` documents the same
18
+ // failure from the other side ("a deletion lands at its mirror-image position"),
19
+ // and the dotplot shipped it for real.
20
+ //
21
+ // Region orientation is read off the endpoints rather than assumed — an axis
22
+ // showing a reversed displayed region (auto-diagonalize flips query regions)
23
+ // lays out with cumBp decreasing, and that is independent of strand. Strand
24
+ // decides only which END the walk starts from.
25
+ //
26
+ // FOUR SCALARS, not one object, and that is measured:
27
+ // `plugins/dotplot-view/benches/cigarWalkStart.bench.ts` A/Bs the two against
28
+ // the loop that calls them. Returning `{bp1, bp2, rev1, rev2}` costs 1.43-1.46x
29
+ // on the derivation in isolation — V8 does not scalar-replace it — and shows as
30
+ // ~1.07x against a 1.02 control on a realistic geometry pass. These come back at
31
+ // or below the control on the same rows. Same shape and same reason as
32
+ // `dotplotProject.ts`'s `cumBpToPxH`/`cumBpToPxV`, which sit in the loop next
33
+ // door. Recomputing the strand test in each is what buys that; it is two
34
+ // compares.
35
+ //
36
+ // **Call them behind the caller's own draw-detail gate**, not above it. A
37
+ // feature too narrow to walk is drawn as one flat line and never asks where its
38
+ // CIGAR would have started, and at whole-genome zoom that is nearly all of them
39
+ // — the worker ships CIGARs within 8x zoom headroom that the geometry builder is
40
+ // then too zoomed out to walk. Hoisting the derivation above the gate, which is
41
+ // how both call sites had it, cost the same bench 1.26x (0.794 the other way) at
42
+ // 98% flat.
43
+ export function cigarWalkBp1(p11, p12, strand) {
44
+ return strand === -1 ? p12 : p11;
45
+ }
46
+ export function cigarWalkBp2(p21, p22, strand) {
47
+ return strand === -1 ? p22 : p21;
48
+ }
49
+ export function cigarWalkRev1(p11, p12, strand) {
50
+ return (strand === -1 ? p12 < p11 : p11 < p12) ? 1 : -1;
51
+ }
52
+ export function cigarWalkRev2(p21, p22, strand) {
53
+ return (p21 < p22 ? 1 : -1) * (strand === -1 ? -1 : 1);
54
+ }
package/esm/csOps.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { MismatchCallback } from './mismatchCallback.ts';
2
+ /** An operation of the minimap2 `cs` grammar. */
3
+ export type CsOp = ':' | '=' | '*' | '+' | '-' | '~';
4
+ /**
5
+ * Walk a minimap2 `cs` difference string
6
+ * (https://github.com/lh3/minimap2#cs), calling `callback` once per operation.
7
+ *
8
+ * Short form is `:N` match run, `*ab` substitution (ref base a, query base b),
9
+ * `+seq` insertion, `-seq` deletion; long form adds `=SEQ` match runs; `~ab12cd`
10
+ * is a 12 bp intron between the given splice motifs. A character belonging to
11
+ * none of those is skipped, so a truncated or extended string still walks to the
12
+ * end rather than throwing.
13
+ *
14
+ * `refLen`/`queryLen` are the bases the operation consumes on each sequence, and
15
+ * `[seqStart, seqEnd)` bounds its operand *within `cs`* — the bases for `=`/`+`/
16
+ * `-`, the two substituted bases for `*`, the digits for `:`, the whole motif
17
+ * for `~`. Offsets rather than a substring because the mismatch walk runs per
18
+ * feature per frame, and only two of six ops ever need the text.
19
+ *
20
+ * **One walker, because four disagreed.** This replaces a copy in each of
21
+ * `csToCigar`, `flipCs`, `forEachCsMismatch` and the (now removed) `visitCsOps`,
22
+ * and every one of the divergences was silent: two of them dropped `~` and then
23
+ * mis-scanned the rest of the string, one ignored `=SEQ` entirely and so never
24
+ * advanced the reference across a long-form match run, and the three that
25
+ * scanned `+`/`-` operands disagreed on where one ends.
26
+ */
27
+ export declare function forEachCsOp(cs: string, callback: (op: CsOp, refLen: number, queryLen: number, seqStart: number, seqEnd: number) => void): void;
28
+ /**
29
+ * Rewrite a cs string from the target perspective to the query perspective (for
30
+ * a synteny view anchored on the query assembly): `*ab` -> `*ba`, `+seq` ->
31
+ * `-seq`, `-seq` -> `+seq`, match runs unchanged.
32
+ *
33
+ * Undefined when the string states something the query perspective cannot: an
34
+ * intron is a gap in the *reference*, and flipped it becomes a gap in the query
35
+ * with no sequence to write for it, which cs has no way to express. Callers fall
36
+ * back to the CIGAR there, the same as they already do for reverse-strand
37
+ * alignments (which additionally need a reverse-complement).
38
+ */
39
+ export declare function flipCs(cs: string): string | undefined;
40
+ /**
41
+ * Walk a cs string and drive a {@link MismatchCallback} (the contract BAM/CRAM
42
+ * features use), emitting per-base substitutions with their real query base plus
43
+ * insertions, deletions and introns. Offsets are relative to the reference
44
+ * (feature) start; `windowStart`/`windowEnd` (also reference-relative) clip the
45
+ * output.
46
+ */
47
+ export declare function forEachCsMismatch(cs: string, callback: MismatchCallback, windowStart?: number, windowEnd?: number): void;
package/esm/csOps.js ADDED
@@ -0,0 +1,152 @@
1
+ import { DELETION_TYPE, INSERTION_TYPE, MISMATCH_TYPE, SKIP_TYPE, } from './mismatchCallback.js';
2
+ function isDigit(code) {
3
+ return code >= 48 && code <= 57;
4
+ }
5
+ function isBaseChar(ch) {
6
+ return ch !== undefined && /[A-Za-z]/.test(ch);
7
+ }
8
+ function scanBases(cs, start) {
9
+ let i = start;
10
+ while (i < cs.length && isBaseChar(cs[i])) {
11
+ i++;
12
+ }
13
+ return i;
14
+ }
15
+ function scanDigits(cs, start) {
16
+ let i = start;
17
+ let value = 0;
18
+ while (i < cs.length && isDigit(cs.charCodeAt(i))) {
19
+ value = value * 10 + (cs.charCodeAt(i) - 48);
20
+ i++;
21
+ }
22
+ return { value, next: i };
23
+ }
24
+ /**
25
+ * Walk a minimap2 `cs` difference string
26
+ * (https://github.com/lh3/minimap2#cs), calling `callback` once per operation.
27
+ *
28
+ * Short form is `:N` match run, `*ab` substitution (ref base a, query base b),
29
+ * `+seq` insertion, `-seq` deletion; long form adds `=SEQ` match runs; `~ab12cd`
30
+ * is a 12 bp intron between the given splice motifs. A character belonging to
31
+ * none of those is skipped, so a truncated or extended string still walks to the
32
+ * end rather than throwing.
33
+ *
34
+ * `refLen`/`queryLen` are the bases the operation consumes on each sequence, and
35
+ * `[seqStart, seqEnd)` bounds its operand *within `cs`* — the bases for `=`/`+`/
36
+ * `-`, the two substituted bases for `*`, the digits for `:`, the whole motif
37
+ * for `~`. Offsets rather than a substring because the mismatch walk runs per
38
+ * feature per frame, and only two of six ops ever need the text.
39
+ *
40
+ * **One walker, because four disagreed.** This replaces a copy in each of
41
+ * `csToCigar`, `flipCs`, `forEachCsMismatch` and the (now removed) `visitCsOps`,
42
+ * and every one of the divergences was silent: two of them dropped `~` and then
43
+ * mis-scanned the rest of the string, one ignored `=SEQ` entirely and so never
44
+ * advanced the reference across a long-form match run, and the three that
45
+ * scanned `+`/`-` operands disagreed on where one ends.
46
+ */
47
+ export function forEachCsOp(cs, callback) {
48
+ let i = 0;
49
+ while (i < cs.length) {
50
+ const c = cs[i];
51
+ if (c === ':') {
52
+ const { value, next } = scanDigits(cs, i + 1);
53
+ callback(':', value, value, i + 1, next);
54
+ i = next;
55
+ }
56
+ else if (c === '=') {
57
+ const next = scanBases(cs, i + 1);
58
+ const len = next - i - 1;
59
+ callback('=', len, len, i + 1, next);
60
+ i = next;
61
+ }
62
+ else if (c === '*') {
63
+ callback('*', 1, 1, i + 1, Math.min(i + 3, cs.length));
64
+ i += 3;
65
+ }
66
+ else if (c === '+' || c === '-') {
67
+ const next = scanBases(cs, i + 1);
68
+ const len = next - i - 1;
69
+ callback(c, c === '-' ? len : 0, c === '+' ? len : 0, i + 1, next);
70
+ i = next;
71
+ }
72
+ else if (c === '~') {
73
+ // the length sits between two 2-base splice motifs
74
+ const { value, next } = scanDigits(cs, i + 3);
75
+ const end = Math.min(next + 2, cs.length);
76
+ callback('~', value, 0, i + 1, end);
77
+ i = end;
78
+ }
79
+ else {
80
+ i++;
81
+ }
82
+ }
83
+ }
84
+ /**
85
+ * Rewrite a cs string from the target perspective to the query perspective (for
86
+ * a synteny view anchored on the query assembly): `*ab` -> `*ba`, `+seq` ->
87
+ * `-seq`, `-seq` -> `+seq`, match runs unchanged.
88
+ *
89
+ * Undefined when the string states something the query perspective cannot: an
90
+ * intron is a gap in the *reference*, and flipped it becomes a gap in the query
91
+ * with no sequence to write for it, which cs has no way to express. Callers fall
92
+ * back to the CIGAR there, the same as they already do for reverse-strand
93
+ * alignments (which additionally need a reverse-complement).
94
+ */
95
+ export function flipCs(cs) {
96
+ // The one op with no query-perspective form, and cs writes it nowhere else —
97
+ // operands are base letters. Answered before the walk so the rewrite below is
98
+ // total.
99
+ if (cs.includes('~')) {
100
+ return undefined;
101
+ }
102
+ let result = '';
103
+ forEachCsOp(cs, (op, refLen, _queryLen, seqStart, seqEnd) => {
104
+ if (op === ':') {
105
+ result += `:${refLen}`;
106
+ }
107
+ else if (op === '=') {
108
+ result += `=${cs.slice(seqStart, seqEnd)}`;
109
+ }
110
+ else if (op === '*') {
111
+ result += `*${cs[seqStart + 1] ?? ''}${cs[seqStart] ?? ''}`;
112
+ }
113
+ else {
114
+ result += `${op === '+' ? '-' : '+'}${cs.slice(seqStart, seqEnd)}`;
115
+ }
116
+ });
117
+ return result;
118
+ }
119
+ /**
120
+ * Walk a cs string and drive a {@link MismatchCallback} (the contract BAM/CRAM
121
+ * features use), emitting per-base substitutions with their real query base plus
122
+ * insertions, deletions and introns. Offsets are relative to the reference
123
+ * (feature) start; `windowStart`/`windowEnd` (also reference-relative) clip the
124
+ * output.
125
+ */
126
+ export function forEachCsMismatch(cs, callback, windowStart, windowEnd) {
127
+ const lo = windowStart ?? Number.NEGATIVE_INFINITY;
128
+ const hi = windowEnd ?? Number.POSITIVE_INFINITY;
129
+ let ref = 0;
130
+ forEachCsOp(cs, (op, refLen, queryLen, seqStart, seqEnd) => {
131
+ if (op === '*') {
132
+ if (ref >= lo && ref < hi) {
133
+ callback(MISMATCH_TYPE, ref, 1, cs[seqStart + 1] ?? '', -1, 0, 0);
134
+ }
135
+ }
136
+ else if (op === '+') {
137
+ if (ref >= lo && ref <= hi) {
138
+ const seq = cs.slice(seqStart, seqEnd);
139
+ callback(INSERTION_TYPE, ref, queryLen, seq, -1, 0, queryLen);
140
+ }
141
+ }
142
+ else if (op === '-' || op === '~') {
143
+ if (ref < hi && ref + refLen > lo) {
144
+ // an intron is a reference gap the read skips rather than deletes, and
145
+ // the two draw differently
146
+ const type = op === '-' ? DELETION_TYPE : SKIP_TYPE;
147
+ callback(type, ref, refLen, '', -1, 0, 0);
148
+ }
149
+ }
150
+ ref += refLen;
151
+ });
152
+ }
@@ -0,0 +1 @@
1
+ export declare function csToCigar(cs: string): string;
@@ -0,0 +1,43 @@
1
+ import { forEachCsOp } from './csOps.js';
2
+ // Convert a minimap2 `cs` difference string to a standard CIGAR: matches -> `=`,
3
+ // substitutions -> `X`, insertions -> `I`, deletions -> `D`, introns -> `N`.
4
+ // Sequence bases are dropped so the result reorients with the same
5
+ // flipCigar/swapIndelCigar helpers as a `cg` CIGAR. The grammar itself is
6
+ // forEachCsOp's; this is only the op mapping.
7
+ export function csToCigar(cs) {
8
+ const ops = [];
9
+ function push(len, op) {
10
+ if (len > 0) {
11
+ const last = ops[ops.length - 1];
12
+ if (last?.[1] === op) {
13
+ last[0] += len;
14
+ }
15
+ else {
16
+ ops.push([len, op]);
17
+ }
18
+ }
19
+ }
20
+ forEachCsOp(cs, (op, refLen, queryLen) => {
21
+ if (op === '*') {
22
+ push(1, 'X');
23
+ }
24
+ else if (op === '+') {
25
+ push(queryLen, 'I');
26
+ }
27
+ else if (op === '-') {
28
+ push(refLen, 'D');
29
+ }
30
+ else if (op === '~') {
31
+ push(refLen, 'N');
32
+ }
33
+ else {
34
+ push(refLen, '=');
35
+ }
36
+ });
37
+ let result = '';
38
+ for (const [len, op] of ops) {
39
+ result += len;
40
+ result += op;
41
+ }
42
+ return result;
43
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * #api
3
+ * Maps read-sequence positions to reference-sequence positions via the CIGAR,
4
+ * invoking the callback for each. Handles both packed Uint32Array and unpacked
5
+ * number[] CIGAR formats.
6
+ *
7
+ * **Driven by the POSITIONS, not by the read bases.** This used to run an inner
8
+ * loop over every base an op spans, testing `positions[currPos] === readPos + j`
9
+ * — so it was O(read length) to find something O(positions) in size. On the full
10
+ * extent of `200x.longread.mod.bam` that is 43.7 Mbp of read sequence scanned to
11
+ * place 0.84M modification calls. Within one op the read-to-reference offset is
12
+ * a constant, so each position's answer is one addition and the ops only have to
13
+ * be walked once: O(positions + ops).
14
+ * `plugins/alignments/benches/cigarWalkShape.bench.ts` prices it at **1.17x on
15
+ * the whole per-read modification pipeline**, of which this walk is 45%.
16
+ *
17
+ * **1.17x is the whole of it, and does not grow on a cleaner alignment.** The
18
+ * first reading of that gap was that the op loop caps it — these reads carry
19
+ * 7,000 ops apiece, and neither shape avoids the op loop. `cigarOpDensity.bench.ts`
20
+ * sweeps op density over a 5,000x range and the ratio stays between 1.10x and
21
+ * 1.18x, so that reading was wrong: what this phase is bound by is the per-CALL
22
+ * work both shapes share — the callback, the ML lookup, the compare, the write —
23
+ * not the traversal that differs. Changing how the scan is shaped therefore has a
24
+ * low ceiling here, however lopsided the iteration counts look.
25
+ *
26
+ * `positions` must be ASCENDING, which every caller's producer guarantees. The
27
+ * one behaviour that changed with the shape: a REPEATED position used to be
28
+ * dropped and to block every position after it (the per-base loop could match at
29
+ * most one position per base offset), and now emits once per occurrence.
30
+ */
31
+ export declare function getNextRefPos(cigarOps: ArrayLike<number>, positions: number[], callback: (ref: number, idx: number) => void): void;
@@ -0,0 +1,69 @@
1
+ // get relative reference sequence positions for positions given relative to
2
+ // the read sequence
3
+ import { CIGAR_D, CIGAR_EQ, CIGAR_I, CIGAR_M, CIGAR_N, CIGAR_S, CIGAR_X, } from './cigarConstants.js';
4
+ /**
5
+ * #api
6
+ * Maps read-sequence positions to reference-sequence positions via the CIGAR,
7
+ * invoking the callback for each. Handles both packed Uint32Array and unpacked
8
+ * number[] CIGAR formats.
9
+ *
10
+ * **Driven by the POSITIONS, not by the read bases.** This used to run an inner
11
+ * loop over every base an op spans, testing `positions[currPos] === readPos + j`
12
+ * — so it was O(read length) to find something O(positions) in size. On the full
13
+ * extent of `200x.longread.mod.bam` that is 43.7 Mbp of read sequence scanned to
14
+ * place 0.84M modification calls. Within one op the read-to-reference offset is
15
+ * a constant, so each position's answer is one addition and the ops only have to
16
+ * be walked once: O(positions + ops).
17
+ * `plugins/alignments/benches/cigarWalkShape.bench.ts` prices it at **1.17x on
18
+ * the whole per-read modification pipeline**, of which this walk is 45%.
19
+ *
20
+ * **1.17x is the whole of it, and does not grow on a cleaner alignment.** The
21
+ * first reading of that gap was that the op loop caps it — these reads carry
22
+ * 7,000 ops apiece, and neither shape avoids the op loop. `cigarOpDensity.bench.ts`
23
+ * sweeps op density over a 5,000x range and the ratio stays between 1.10x and
24
+ * 1.18x, so that reading was wrong: what this phase is bound by is the per-CALL
25
+ * work both shapes share — the callback, the ML lookup, the compare, the write —
26
+ * not the traversal that differs. Changing how the scan is shaped therefore has a
27
+ * low ceiling here, however lopsided the iteration counts look.
28
+ *
29
+ * `positions` must be ASCENDING, which every caller's producer guarantees. The
30
+ * one behaviour that changed with the shape: a REPEATED position used to be
31
+ * dropped and to block every position after it (the per-base loop could match at
32
+ * most one position per base offset), and now emits once per occurrence.
33
+ */
34
+ export function getNextRefPos(cigarOps, positions, callback) {
35
+ const l2 = positions.length;
36
+ if (l2 === 0) {
37
+ return;
38
+ }
39
+ let readPos = 0;
40
+ let refPos = 0;
41
+ let currPos = 0;
42
+ for (let i = 0, l = cigarOps.length; i < l && currPos < l2; i++) {
43
+ const packed = cigarOps[i];
44
+ const len = packed >>> 4;
45
+ const op = packed & 0xf;
46
+ if (op === CIGAR_S || op === CIGAR_I) {
47
+ // Consumes read but not reference: skip the positions that land in it.
48
+ const readEnd = readPos + len;
49
+ while (currPos < l2 && positions[currPos] < readEnd) {
50
+ currPos++;
51
+ }
52
+ readPos = readEnd;
53
+ }
54
+ else if (op === CIGAR_D || op === CIGAR_N) {
55
+ refPos += len;
56
+ }
57
+ else if (op === CIGAR_M || op === CIGAR_X || op === CIGAR_EQ) {
58
+ const readEnd = readPos + len;
59
+ // Constant across the op, so placing a position is one add.
60
+ const delta = refPos - readPos;
61
+ while (currPos < l2 && positions[currPos] < readEnd) {
62
+ callback(positions[currPos] + delta, currPos);
63
+ currPos++;
64
+ }
65
+ readPos = readEnd;
66
+ refPos += len;
67
+ }
68
+ }
69
+ }
package/esm/index.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ export { CIGAR_D, CIGAR_EQ, CIGAR_H, CIGAR_I, CIGAR_INDEL_MASK, CIGAR_M, CIGAR_M_EQ_MASK, CIGAR_N, CIGAR_P, CIGAR_S, CIGAR_X, } from './cigarConstants.ts';
2
+ export { CHAR_FROM_CODE, SEQRET, SEQRET_NUMERIC_DECODER, } from './bamSeqDecoder.ts';
3
+ export { encodeSeqNumeric } from './bamSeqEncoder.ts';
4
+ export { flipCigar, swapIndelCigar } from './cigarReorient.ts';
5
+ export { csToCigar } from './csToCigar.ts';
6
+ export { flipCs, forEachCsMismatch, forEachCsOp } from './csOps.ts';
7
+ export type { CsOp } from './csOps.ts';
8
+ export { orientAlignment } from './orientAlignment.ts';
9
+ export { pafIdentity } from './pafIdentity.ts';
10
+ export { DELETION_TYPE, HARDCLIP_TYPE, INSERTION_TYPE, MISMATCH_TYPE, SKIP_TYPE, SOFTCLIP_TYPE, } from './mismatchCallback.ts';
11
+ export type { MismatchCallback, MismatchWindow } from './mismatchCallback.ts';
12
+ export { visitCigarRenderedSegments } from './cigarRenderedSegments.ts';
13
+ export { cigarWalkBp1, cigarWalkBp2, cigarWalkRev1, cigarWalkRev2, } from './cigarWalkStart.ts';
14
+ export { cigarToMismatches2 } from './cigarToMismatches2.ts';
15
+ export { mdToMismatches2 } from './mdToMismatches2.ts';
16
+ export { getNextRefPos } from './getNextRefPos.ts';
17
+ export { numericCigarHasSkip } from './numericCigarHasSkip.ts';
18
+ export { numericCigarToString } from './numericCigarToString.ts';
19
+ export { connectionEndpointBps, readLeadingBodyDir, readLeadingBp, readTrailingBodyDir, readTrailingBp, } from './readEndpoints.ts';
20
+ export { clipLengthAtStartOfReadNumeric, featurizeSA, featurizeSAEntries, getClip, getLength, getLengthOnRef, getLengthSansClipping, getMismatches, parseCigar2, parseCigar2Typed, parseCigar, splitSA, } from './mismatchParser.ts';
21
+ export type { ClipMismatch, DeletionMismatch, InsertionMismatch, Mismatch, SNPMismatch, SkipMismatch, } from './mismatchTypes.ts';
22
+ export { buildReadVsRefFeatures } from './buildReadVsRefFeatures.ts';
23
+ export type { ReadVsRefFeature, ReadVsRefFeatures, ReadVsRefInput, ReadVsRefMate, } from './buildReadVsRefFeatures.ts';
24
+ export { SAM_FLAG_DUPLICATE, SAM_FLAG_FAILS_QC, SAM_FLAG_FIRST_IN_PAIR, SAM_FLAG_MATE_REVERSE, SAM_FLAG_MATE_UNMAPPED, SAM_FLAG_PAIRED, SAM_FLAG_PROPER_PAIR, SAM_FLAG_REVERSE, SAM_FLAG_SECONDARY, SAM_FLAG_SECOND_IN_PAIR, SAM_FLAG_SUPPLEMENTARY, SAM_FLAG_UNMAPPED, samFlagDescriptions, samFlagLabels, samFlagNames, } from './samFlags.ts';