@jbrowse/cigar-utils 5.0.0-beta.1 → 5.0.0-beta.10

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 CHANGED
@@ -32,6 +32,43 @@ a caller may not have the assembly in hand; pass it when you do.
32
32
 
33
33
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/cigar-utils/src/buildReadVsRefFeatures.ts)
34
34
 
35
+ ### coarseCigarOwnAxis
36
+
37
+ The coarse CIGAR as a plain CIGAR over the row's own axis, for a walk that
38
+ places mismatches by reference position: a run keeps only its own length, a run
39
+ with no own length is the insertion it amounts to, and one with no mate length
40
+ the deletion. Kept indels are exact, so they land where the fine tier's would.
41
+
42
+ ```js
43
+ // type signature
44
+ (s: string) => string
45
+ ```
46
+
47
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/cigar-utils/src/coarseCigar.ts)
48
+
49
+ ### coarsenCigar
50
+
51
+ Fold a CIGAR into a coarse CIGAR whose straight lines stay within `gap` of the
52
+ alignment's own path. An indel longer than half the gap keeps its letter and
53
+ length (D/N/I); a shorter one is absorbed into the match run around it, and a
54
+ run whose two axes advanced by different amounts is written `<own>:<mate>M`, a
55
+ square run `<n>M`. `=`/`X` count as M. `ownLen`/`mateLen` are the totals each
56
+ axis consumed, for the caller to check against the row's coordinate columns.
57
+
58
+ The bound: a run is closed before a folded indel would push its running skew
59
+ (own minus mate) past half the gap, and no folded indel is longer than that, so
60
+ inside a run the skew never exceeds half the gap and the line between the run's
61
+ corners is never more than the gap off the path. That is what lets a reader
62
+ interpolate within a run, and it costs a new run only where the two genomes'
63
+ small indels are lopsided.
64
+
65
+ ```js
66
+ // type signature
67
+ (cigar: string, gap: number) => CoarsenedCigar
68
+ ```
69
+
70
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/cigar-utils/src/coarseCigar.ts)
71
+
35
72
  ### featurizeSAEntries
36
73
 
37
74
  featurizeSA over pre-split entries (see splitSA). Lets a caller filter the
@@ -45,6 +82,18 @@ segments — without paying to split and rejoin the tag around the filter.
45
82
 
46
83
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/cigar-utils/src/mismatchParser.ts)
47
84
 
85
+ ### flipCoarseCigar
86
+
87
+ The coarse CIGAR seen from the other axis on the minus strand: swapped as
88
+ `swapCoarseCigar` does, and in reverse op order. The twin of `flipCigar`.
89
+
90
+ ```js
91
+ // type signature
92
+ (s: string) => string
93
+ ```
94
+
95
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/cigar-utils/src/coarseCigar.ts)
96
+
48
97
  ### getLength
49
98
 
50
99
  Length of the read sequence (sum of all ops except D/N).
@@ -109,9 +158,12 @@ behaviour that changed with the shape: a REPEATED position used to be dropped
109
158
  and to block every position after it (the per-base loop could match at most one
110
159
  position per base offset), and now emits once per occurrence.
111
160
 
161
+ `from` starts the walk at a `refWindowToRead` cursor instead of the first op,
162
+ for positions that all lie at or past its `readStart`.
163
+
112
164
  ```js
113
165
  // type signature
114
- (cigarOps: ArrayLike<number>, positions: number[], callback: (ref: number, idx: number) => void) => void
166
+ (cigarOps: ArrayLike<number>, positions: number[], callback: (ref: number, idx: number) => void, from?: CigarCursor | undefined) => void
115
167
  ```
116
168
 
117
169
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/cigar-utils/src/getNextRefPos.ts)
@@ -152,6 +204,39 @@ single typed-array code path.
152
204
 
153
205
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/cigar-utils/src/mismatchParser.ts)
154
206
 
207
+ ### parseCoarseCigar
208
+
209
+ Parse a coarse CIGAR into the packed `(len << 4) | op` form
210
+ `visitCigarRenderedSegments` walks. A square run packs as one `CIGAR_M` word; an
211
+ unequal run as a `CIGAR_RUN` word pair, own axis first.
212
+
213
+ ```js
214
+ // type signature
215
+ (s: string) => Uint32Array<ArrayBuffer>
216
+ ```
217
+
218
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/cigar-utils/src/coarseCigar.ts)
219
+
220
+ ### refWindowToRead
221
+
222
+ The read offsets `[readStart, readEnd)` holding every aligned base whose
223
+ read-relative reference offset lies in `[refStart, refEnd)`, and the op a
224
+ `getNextRefPos` walk over them starts at. Aligned read offsets map to ascending
225
+ reference offsets, so the range holds no other aligned base; a clip or insertion
226
+ inside it maps to no reference offset. The returned `refStart`/`refEnd` are the
227
+ requested ones clipped to the read's span.
228
+
229
+ The op loop adds each op's lengths through the masks above rather than branching
230
+ on the op, because a long read's op sequence is close to random and the branches
231
+ mispredict; only the op that crosses the next edge takes the slow path.
232
+
233
+ ```js
234
+ // type signature
235
+ (cigarOps: ArrayLike<number>, refStart: number, refEnd: number) => ReadWindow
236
+ ```
237
+
238
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/cigar-utils/src/refWindowToRead.ts)
239
+
155
240
  ### splitSA
156
241
 
157
242
  The `;`-separated alignment records of an SA tag, empties dropped — the input
@@ -164,4 +249,17 @@ The `;`-separated alignment records of an SA tag, empties dropped — the input
164
249
 
165
250
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/cigar-utils/src/mismatchParser.ts)
166
251
 
252
+ ### swapCoarseCigar
253
+
254
+ The coarse CIGAR seen from the other axis on the plus strand: the two lengths of
255
+ every run trade places and D<->I swap, op order untouched. The twin of
256
+ `swapIndelCigar`.
257
+
258
+ ```js
259
+ // type signature
260
+ (s: string) => string
261
+ ```
262
+
263
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/cigar-utils/src/coarseCigar.ts)
264
+
167
265
  <!-- API_DOCS_END -->
@@ -9,3 +9,4 @@ export declare const CIGAR_EQ = 7;
9
9
  export declare const CIGAR_X = 8;
10
10
  export declare const CIGAR_M_EQ_MASK = 129;
11
11
  export declare const CIGAR_INDEL_MASK = 14;
12
+ export declare const CIGAR_RUN = 9;
@@ -13,3 +13,8 @@ export const CIGAR_X = 8;
13
13
  export const CIGAR_M_EQ_MASK = 0b10000001; // (1<<0)|(1<<7) = 129
14
14
  // Indel ops (I=1, D=2, N=3) — each consumes only one axis (a gap on the other)
15
15
  export const CIGAR_INDEL_MASK = 0b00001110; // (1<<1)|(1<<2)|(1<<3) = 14
16
+ // Not a BAM op: a match run whose two axes advance by different lengths, the
17
+ // coarse tier's fold of the small indels between two kept gaps. Packed as two
18
+ // consecutive words, `(ownLen << 4) | CIGAR_RUN` then `(mateLen << 4) |
19
+ // CIGAR_RUN`. Only parseCoarseCigar writes it.
20
+ export const CIGAR_RUN = 9;
@@ -2,7 +2,9 @@
2
2
  * Walks pre-parsed (packed int) CIGAR ops in bp-space and fires a callback
3
3
  * for each rendered segment. Small indels (width < MIN_INDEL_PX) are merged
4
4
  * into surrounding context; tiny M segments (both accumulators advance <
5
- * bpPerPx) are accumulated before emitting.
5
+ * bpPerPx) are accumulated before emitting. A `CIGAR_RUN` word pair (the
6
+ * coarse tier's fold of a run with its small indels) advances each axis by its
7
+ * own length and is reported as CIGAR_M.
6
8
  *
7
9
  * Used by synteny and dotplot GPU renderers so both stay in sync.
8
10
  * Re-exported via @jbrowse/synteny-core for consistent import paths.
@@ -1,4 +1,4 @@
1
- import { CIGAR_D, CIGAR_EQ, CIGAR_I, CIGAR_M, CIGAR_N, CIGAR_X, } from './cigarConstants.js';
1
+ import { CIGAR_D, CIGAR_EQ, CIGAR_I, CIGAR_M, CIGAR_N, CIGAR_RUN, CIGAR_X, } from './cigarConstants.js';
2
2
  // Indels narrower than this (in pixels) are merged into surrounding match
3
3
  // context rather than rendered as separate quads. At 1px an indel resolving to
4
4
  // a single pixel still renders — small on-screen detail is intentionally kept.
@@ -12,7 +12,9 @@ const MIN_INDEL_PX = 1;
12
12
  * Walks pre-parsed (packed int) CIGAR ops in bp-space and fires a callback
13
13
  * for each rendered segment. Small indels (width < MIN_INDEL_PX) are merged
14
14
  * into surrounding context; tiny M segments (both accumulators advance <
15
- * bpPerPx) are accumulated before emitting.
15
+ * bpPerPx) are accumulated before emitting. A `CIGAR_RUN` word pair (the
16
+ * coarse tier's fold of a run with its small indels) advances each axis by its
17
+ * own length and is reported as CIGAR_M.
16
18
  *
17
19
  * Used by synteny and dotplot GPU renderers so both stay in sync.
18
20
  * Re-exported via @jbrowse/synteny-core for consistent import paths.
@@ -34,7 +36,11 @@ export function visitCigarRenderedSegments(cigar, startBp1, startBp2, bpPerPx0,
34
36
  segBp1Start = bp1;
35
37
  segBp2Start = bp2;
36
38
  }
37
- if (op === CIGAR_M || op === CIGAR_EQ || op === CIGAR_X) {
39
+ if (op === CIGAR_RUN) {
40
+ bp1 += len * rev1;
41
+ bp2 += (cigar[++j] >>> 4) * rev2;
42
+ }
43
+ else if (op === CIGAR_M || op === CIGAR_EQ || op === CIGAR_X) {
38
44
  bp1 += len * rev1;
39
45
  bp2 += len * rev2;
40
46
  }
@@ -60,7 +66,11 @@ export function visitCigarRenderedSegments(cigar, startBp1, startBp2, bpPerPx0,
60
66
  else {
61
67
  const span1 = Math.abs(bp1 - segBp1Start);
62
68
  const span2 = Math.abs(bp2 - segBp2Start);
63
- const resolvedOp = span1 > bpPerPx0 || span2 > bpPerPx1 ? op : CIGAR_M;
69
+ const resolvedOp = span1 > bpPerPx0 || span2 > bpPerPx1
70
+ ? op === CIGAR_RUN
71
+ ? CIGAR_M
72
+ : op
73
+ : CIGAR_M;
64
74
  continuingFlag = false;
65
75
  callback(resolvedOp, segBp1Start, bp1, segBp2Start, bp2);
66
76
  }
@@ -1,29 +1,39 @@
1
+ // Both re-orientations keep every byte of the CIGAR and change only which
2
+ // letter an indel wears (and, for the flip, the op order), so each is written
3
+ // straight into a byte buffer the size of its input rather than a string built
4
+ // op by op: 2-3x faster on a multi-kb PAF CIGAR, which make-pif does twice a
5
+ // row and orientAlignment does per fetched row.
6
+ function swapIndelByte(code) {
7
+ return code === 68 ? 73 : code === 73 ? 68 : code;
8
+ }
1
9
  // Reverse a CIGAR's op order and swap insertions<->deletions. Used to view an
2
10
  // alignment from the opposite (query<->target) perspective on the reverse
3
11
  // strand, where both the direction and the indel sense flip.
4
12
  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;
13
+ const src = new TextEncoder().encode(cigar);
14
+ const n = src.length;
15
+ const out = new Uint8Array(n);
16
+ let pos = n;
17
+ let start = 0;
18
+ for (let i = 0; i < n; i++) {
19
+ const code = src[i];
20
+ if (code < 48 || code > 57) {
21
+ pos -= i + 1 - start;
22
+ for (let k = start, p = pos; k < i; k++, p++) {
23
+ out[p] = src[k];
24
+ }
25
+ out[pos + i - start] = swapIndelByte(code);
26
+ start = i + 1;
15
27
  }
16
28
  }
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;
29
+ return new TextDecoder().decode(out.subarray(pos));
24
30
  }
25
31
  // Swap insertions<->deletions in place, leaving op order untouched. This is the
26
32
  // +-strand query<->target perspective flip, where only the indel sense reverses.
27
33
  export function swapIndelCigar(cigar) {
28
- return cigar.replaceAll(/[DI]/g, op => (op === 'D' ? 'I' : 'D'));
34
+ const bytes = new TextEncoder().encode(cigar);
35
+ for (let i = 0, n = bytes.length; i < n; i++) {
36
+ bytes[i] = swapIndelByte(bytes[i]);
37
+ }
38
+ return new TextDecoder().decode(bytes);
29
39
  }
@@ -0,0 +1,54 @@
1
+ export interface CoarsenedCigar {
2
+ ops: string;
3
+ ownLen: number;
4
+ mateLen: number;
5
+ gapCount: number;
6
+ opCount: number;
7
+ }
8
+ /**
9
+ * #api
10
+ * Fold a CIGAR into a coarse CIGAR whose straight lines stay within `gap` of
11
+ * the alignment's own path. An indel longer than half the gap keeps its letter
12
+ * and length (D/N/I); a shorter one is absorbed into the match run around it,
13
+ * and a run whose two axes advanced by different amounts is written
14
+ * `<own>:<mate>M`, a square run `<n>M`. `=`/`X` count as M. `ownLen`/`mateLen`
15
+ * are the totals each axis consumed, for the caller to check against the row's
16
+ * coordinate columns.
17
+ *
18
+ * The bound: a run is closed before a folded indel would push its running
19
+ * skew (own minus mate) past half the gap, and no folded indel is longer than
20
+ * that, so inside a run the skew never exceeds half the gap and the line
21
+ * between the run's corners is never more than the gap off the path. That is
22
+ * what lets a reader interpolate within a run, and it costs a new run only
23
+ * where the two genomes' small indels are lopsided.
24
+ */
25
+ export declare function coarsenCigar(cigar: string, gap: number): CoarsenedCigar;
26
+ /**
27
+ * #api
28
+ * The coarse CIGAR seen from the other axis on the plus strand: the two lengths
29
+ * of every run trade places and D<->I swap, op order untouched. The twin of
30
+ * `swapIndelCigar`.
31
+ */
32
+ export declare function swapCoarseCigar(s: string): string;
33
+ /**
34
+ * #api
35
+ * The coarse CIGAR seen from the other axis on the minus strand: swapped as
36
+ * `swapCoarseCigar` does, and in reverse op order. The twin of `flipCigar`.
37
+ */
38
+ export declare function flipCoarseCigar(s: string): string;
39
+ /**
40
+ * #api
41
+ * The coarse CIGAR as a plain CIGAR over the row's own axis, for a walk that
42
+ * places mismatches by reference position: a run keeps only its own length, a
43
+ * run with no own length is the insertion it amounts to, and one with no mate
44
+ * length the deletion. Kept indels are exact, so they land where the fine
45
+ * tier's would.
46
+ */
47
+ export declare function coarseCigarOwnAxis(s: string): string;
48
+ /**
49
+ * #api
50
+ * Parse a coarse CIGAR into the packed `(len << 4) | op` form
51
+ * `visitCigarRenderedSegments` walks. A square run packs as one `CIGAR_M`
52
+ * word; an unequal run as a `CIGAR_RUN` word pair, own axis first.
53
+ */
54
+ export declare function parseCoarseCigar(s: string): Uint32Array<ArrayBuffer>;
@@ -0,0 +1,235 @@
1
+ import { CIGAR_D, CIGAR_I, CIGAR_M, CIGAR_N, CIGAR_RUN, } from './cigarConstants.js';
2
+ /**
3
+ * #api
4
+ * Fold a CIGAR into a coarse CIGAR whose straight lines stay within `gap` of
5
+ * the alignment's own path. An indel longer than half the gap keeps its letter
6
+ * and length (D/N/I); a shorter one is absorbed into the match run around it,
7
+ * and a run whose two axes advanced by different amounts is written
8
+ * `<own>:<mate>M`, a square run `<n>M`. `=`/`X` count as M. `ownLen`/`mateLen`
9
+ * are the totals each axis consumed, for the caller to check against the row's
10
+ * coordinate columns.
11
+ *
12
+ * The bound: a run is closed before a folded indel would push its running
13
+ * skew (own minus mate) past half the gap, and no folded indel is longer than
14
+ * that, so inside a run the skew never exceeds half the gap and the line
15
+ * between the run's corners is never more than the gap off the path. That is
16
+ * what lets a reader interpolate within a run, and it costs a new run only
17
+ * where the two genomes' small indels are lopsided.
18
+ */
19
+ export function coarsenCigar(cigar, gap) {
20
+ const skewBound = gap / 2;
21
+ let ops = '';
22
+ let ownLen = 0;
23
+ let mateLen = 0;
24
+ let gapCount = 0;
25
+ let opCount = 0;
26
+ let runOwn = 0;
27
+ let runMate = 0;
28
+ // a run with nothing on one side is the indel it amounts to, and is written
29
+ // as one, so a reader never meets a zero-sided run
30
+ const flushRun = () => {
31
+ if (runOwn > 0 || runMate > 0) {
32
+ ops +=
33
+ runOwn === runMate
34
+ ? `${runOwn}M`
35
+ : runOwn === 0
36
+ ? `${runMate}I`
37
+ : runMate === 0
38
+ ? `${runOwn}D`
39
+ : `${runOwn}:${runMate}M`;
40
+ opCount++;
41
+ runOwn = 0;
42
+ runMate = 0;
43
+ }
44
+ };
45
+ const keep = (len, op) => {
46
+ flushRun();
47
+ ops += `${len}${op}`;
48
+ gapCount++;
49
+ opCount++;
50
+ };
51
+ const fold = (own, mate) => {
52
+ if (Math.abs(runOwn + own - runMate - mate) > skewBound) {
53
+ flushRun();
54
+ }
55
+ runOwn += own;
56
+ runMate += mate;
57
+ };
58
+ let len = 0;
59
+ for (let i = 0, l = cigar.length; i < l; i++) {
60
+ const code = cigar.charCodeAt(i);
61
+ if (code >= 48 && code <= 57) {
62
+ len = len * 10 + code - 48;
63
+ }
64
+ else {
65
+ const op = cigar[i];
66
+ if (op === 'M' || op === '=' || op === 'X') {
67
+ runOwn += len;
68
+ runMate += len;
69
+ ownLen += len;
70
+ mateLen += len;
71
+ }
72
+ else if (op === 'D' || op === 'N') {
73
+ ownLen += len;
74
+ if (len > skewBound) {
75
+ keep(len, op);
76
+ }
77
+ else {
78
+ fold(len, 0);
79
+ }
80
+ }
81
+ else if (op === 'I') {
82
+ mateLen += len;
83
+ if (len > skewBound) {
84
+ keep(len, op);
85
+ }
86
+ else {
87
+ fold(0, len);
88
+ }
89
+ }
90
+ len = 0;
91
+ }
92
+ }
93
+ flushRun();
94
+ return { ops, ownLen, mateLen, gapCount, opCount };
95
+ }
96
+ function parseCoarseOps(s) {
97
+ const out = [];
98
+ let len = 0;
99
+ let ownBeforeColon;
100
+ for (let i = 0, l = s.length; i < l; i++) {
101
+ const code = s.charCodeAt(i);
102
+ if (code >= 48 && code <= 57) {
103
+ len = len * 10 + code - 48;
104
+ }
105
+ else if (code === 58) {
106
+ ownBeforeColon = len;
107
+ len = 0;
108
+ }
109
+ else {
110
+ // the alphabet is M I D N plus the run; a reader also takes =/X as M and
111
+ // steps over clips and padding, which a writer never emits
112
+ const op = s[i];
113
+ if (op === 'M' || op === '=' || op === 'X') {
114
+ out.push({
115
+ own: ownBeforeColon === undefined ? len : ownBeforeColon,
116
+ mate: len,
117
+ op: 'M',
118
+ });
119
+ }
120
+ else if (op === 'I') {
121
+ out.push({ own: 0, mate: len, op });
122
+ }
123
+ else if (op === 'D' || op === 'N') {
124
+ out.push({ own: len, mate: 0, op });
125
+ }
126
+ ownBeforeColon = undefined;
127
+ len = 0;
128
+ }
129
+ }
130
+ return out;
131
+ }
132
+ function serializeCoarseOps(ops) {
133
+ let s = '';
134
+ for (const { own, mate, op } of ops) {
135
+ s +=
136
+ op === 'M'
137
+ ? own === mate
138
+ ? `${own}M`
139
+ : `${own}:${mate}M`
140
+ : `${op === 'I' ? mate : own}${op}`;
141
+ }
142
+ return s;
143
+ }
144
+ // N keeps its axis as well as its letter, matching `swapIndelCigar`, which
145
+ // swaps only D<->I
146
+ function swapAxes({ own, mate, op }) {
147
+ return op === 'N'
148
+ ? { own, mate, op }
149
+ : {
150
+ own: mate,
151
+ mate: own,
152
+ op: op === 'D' ? 'I' : op === 'I' ? 'D' : op,
153
+ };
154
+ }
155
+ /**
156
+ * #api
157
+ * The coarse CIGAR seen from the other axis on the plus strand: the two lengths
158
+ * of every run trade places and D<->I swap, op order untouched. The twin of
159
+ * `swapIndelCigar`.
160
+ */
161
+ export function swapCoarseCigar(s) {
162
+ return serializeCoarseOps(parseCoarseOps(s).map(swapAxes));
163
+ }
164
+ /**
165
+ * #api
166
+ * The coarse CIGAR seen from the other axis on the minus strand: swapped as
167
+ * `swapCoarseCigar` does, and in reverse op order. The twin of `flipCigar`.
168
+ */
169
+ export function flipCoarseCigar(s) {
170
+ return serializeCoarseOps(parseCoarseOps(s).map(swapAxes).reverse());
171
+ }
172
+ /**
173
+ * #api
174
+ * The coarse CIGAR as a plain CIGAR over the row's own axis, for a walk that
175
+ * places mismatches by reference position: a run keeps only its own length, a
176
+ * run with no own length is the insertion it amounts to, and one with no mate
177
+ * length the deletion. Kept indels are exact, so they land where the fine
178
+ * tier's would.
179
+ */
180
+ export function coarseCigarOwnAxis(s) {
181
+ let out = '';
182
+ for (const { own, mate, op } of parseCoarseOps(s)) {
183
+ out +=
184
+ op === 'M'
185
+ ? own === 0
186
+ ? `${mate}I`
187
+ : mate === 0
188
+ ? `${own}D`
189
+ : `${own}M`
190
+ : `${op === 'I' ? mate : own}${op}`;
191
+ }
192
+ return out;
193
+ }
194
+ // The packed form keeps 28 bits of length, so a longer op is written as several
195
+ // words that sum to it. A run is split with both axes in proportion.
196
+ const MAX_PACKED_LEN = 2 ** 28 - 1;
197
+ function pushPacked(out, len, code) {
198
+ for (let rest = len; rest > 0; rest -= MAX_PACKED_LEN) {
199
+ out.push(((Math.min(rest, MAX_PACKED_LEN) << 4) | code) >>> 0);
200
+ }
201
+ }
202
+ function pushRun(out, own, mate) {
203
+ if (own === mate) {
204
+ pushPacked(out, own, CIGAR_M);
205
+ }
206
+ else {
207
+ const chunks = Math.ceil(Math.max(own, mate) / MAX_PACKED_LEN);
208
+ for (let i = 0; i < chunks; i++) {
209
+ const o = Math.floor((own * (i + 1)) / chunks) - Math.floor((own * i) / chunks);
210
+ const m = Math.floor((mate * (i + 1)) / chunks) - Math.floor((mate * i) / chunks);
211
+ out.push(((o << 4) | CIGAR_RUN) >>> 0, ((m << 4) | CIGAR_RUN) >>> 0);
212
+ }
213
+ }
214
+ }
215
+ /**
216
+ * #api
217
+ * Parse a coarse CIGAR into the packed `(len << 4) | op` form
218
+ * `visitCigarRenderedSegments` walks. A square run packs as one `CIGAR_M`
219
+ * word; an unequal run as a `CIGAR_RUN` word pair, own axis first.
220
+ */
221
+ export function parseCoarseCigar(s) {
222
+ const out = [];
223
+ for (const { own, mate, op } of parseCoarseOps(s)) {
224
+ if (op === 'M') {
225
+ pushRun(out, own, mate);
226
+ }
227
+ else if (op === 'I') {
228
+ pushPacked(out, mate, CIGAR_I);
229
+ }
230
+ else {
231
+ pushPacked(out, own, op === 'N' ? CIGAR_N : CIGAR_D);
232
+ }
233
+ }
234
+ return Uint32Array.from(out);
235
+ }
@@ -1,3 +1,4 @@
1
+ import type { CigarCursor } from './refWindowToRead.ts';
1
2
  /**
2
3
  * #api
3
4
  * Maps read-sequence positions to reference-sequence positions via the CIGAR,
@@ -27,5 +28,8 @@
27
28
  * one behaviour that changed with the shape: a REPEATED position used to be
28
29
  * dropped and to block every position after it (the per-base loop could match at
29
30
  * most one position per base offset), and now emits once per occurrence.
31
+ *
32
+ * `from` starts the walk at a `refWindowToRead` cursor instead of the first op,
33
+ * for positions that all lie at or past its `readStart`.
30
34
  */
31
- export declare function getNextRefPos(cigarOps: ArrayLike<number>, positions: number[], callback: (ref: number, idx: number) => void): void;
35
+ export declare function getNextRefPos(cigarOps: ArrayLike<number>, positions: number[], callback: (ref: number, idx: number) => void, from?: CigarCursor): void;
@@ -30,16 +30,19 @@ import { CIGAR_D, CIGAR_EQ, CIGAR_I, CIGAR_M, CIGAR_N, CIGAR_S, CIGAR_X, } from
30
30
  * one behaviour that changed with the shape: a REPEATED position used to be
31
31
  * dropped and to block every position after it (the per-base loop could match at
32
32
  * most one position per base offset), and now emits once per occurrence.
33
+ *
34
+ * `from` starts the walk at a `refWindowToRead` cursor instead of the first op,
35
+ * for positions that all lie at or past its `readStart`.
33
36
  */
34
- export function getNextRefPos(cigarOps, positions, callback) {
37
+ export function getNextRefPos(cigarOps, positions, callback, from) {
35
38
  const l2 = positions.length;
36
39
  if (l2 === 0) {
37
40
  return;
38
41
  }
39
- let readPos = 0;
40
- let refPos = 0;
42
+ let readPos = from?.opRead ?? 0;
43
+ let refPos = from?.opRef ?? 0;
41
44
  let currPos = 0;
42
- for (let i = 0, l = cigarOps.length; i < l && currPos < l2; i++) {
45
+ for (let i = from?.op ?? 0, l = cigarOps.length; i < l && currPos < l2; i++) {
43
46
  const packed = cigarOps[i];
44
47
  const len = packed >>> 4;
45
48
  const op = packed & 0xf;
package/esm/index.d.ts CHANGED
@@ -1,4 +1,6 @@
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';
1
+ export { CIGAR_D, CIGAR_EQ, CIGAR_H, CIGAR_I, CIGAR_INDEL_MASK, CIGAR_M, CIGAR_M_EQ_MASK, CIGAR_N, CIGAR_P, CIGAR_RUN, CIGAR_S, CIGAR_X, } from './cigarConstants.ts';
2
+ export { coarseCigarOwnAxis, coarsenCigar, flipCoarseCigar, parseCoarseCigar, swapCoarseCigar, } from './coarseCigar.ts';
3
+ export type { CoarsenedCigar } from './coarseCigar.ts';
2
4
  export { CHAR_FROM_CODE, SEQRET, SEQRET_NUMERIC_DECODER, } from './bamSeqDecoder.ts';
3
5
  export { encodeSeqNumeric } from './bamSeqEncoder.ts';
4
6
  export { flipCigar, swapIndelCigar } from './cigarReorient.ts';
@@ -14,6 +16,8 @@ export { cigarWalkBp1, cigarWalkBp2, cigarWalkRev1, cigarWalkRev2, } from './cig
14
16
  export { cigarToMismatches2 } from './cigarToMismatches2.ts';
15
17
  export { mdToMismatches2 } from './mdToMismatches2.ts';
16
18
  export { getNextRefPos } from './getNextRefPos.ts';
19
+ export { refWindowToRead } from './refWindowToRead.ts';
20
+ export type { CigarCursor, ReadWindow } from './refWindowToRead.ts';
17
21
  export { numericCigarHasSkip } from './numericCigarHasSkip.ts';
18
22
  export { numericCigarToString } from './numericCigarToString.ts';
19
23
  export { connectionEndpointBps, readLeadingBodyDir, readLeadingBp, readTrailingBodyDir, readTrailingBp, } from './readEndpoints.ts';
package/esm/index.js CHANGED
@@ -1,4 +1,5 @@
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.js';
1
+ export { CIGAR_D, CIGAR_EQ, CIGAR_H, CIGAR_I, CIGAR_INDEL_MASK, CIGAR_M, CIGAR_M_EQ_MASK, CIGAR_N, CIGAR_P, CIGAR_RUN, CIGAR_S, CIGAR_X, } from './cigarConstants.js';
2
+ export { coarseCigarOwnAxis, coarsenCigar, flipCoarseCigar, parseCoarseCigar, swapCoarseCigar, } from './coarseCigar.js';
2
3
  export { CHAR_FROM_CODE, SEQRET, SEQRET_NUMERIC_DECODER, } from './bamSeqDecoder.js';
3
4
  export { encodeSeqNumeric } from './bamSeqEncoder.js';
4
5
  // The BAM mismatch walk and its packed reference come from `@gmod/bam`:
@@ -16,6 +17,7 @@ export { cigarWalkBp1, cigarWalkBp2, cigarWalkRev1, cigarWalkRev2, } from './cig
16
17
  export { cigarToMismatches2 } from './cigarToMismatches2.js';
17
18
  export { mdToMismatches2 } from './mdToMismatches2.js';
18
19
  export { getNextRefPos } from './getNextRefPos.js';
20
+ export { refWindowToRead } from './refWindowToRead.js';
19
21
  export { numericCigarHasSkip } from './numericCigarHasSkip.js';
20
22
  export { numericCigarToString } from './numericCigarToString.js';
21
23
  export { connectionEndpointBps, readLeadingBodyDir, readLeadingBp, readTrailingBodyDir, readTrailingBp, } from './readEndpoints.js';
@@ -0,0 +1,26 @@
1
+ export interface CigarCursor {
2
+ op: number;
3
+ opRead: number;
4
+ opRef: number;
5
+ }
6
+ export interface ReadWindow extends CigarCursor {
7
+ readStart: number;
8
+ readEnd: number;
9
+ refStart: number;
10
+ refEnd: number;
11
+ }
12
+ /**
13
+ * #api
14
+ * The read offsets `[readStart, readEnd)` holding every aligned base whose
15
+ * read-relative reference offset lies in `[refStart, refEnd)`, and the op a
16
+ * `getNextRefPos` walk over them starts at. Aligned read offsets map to
17
+ * ascending reference offsets, so the range holds no other aligned base; a
18
+ * clip or insertion inside it maps to no reference offset. The returned
19
+ * `refStart`/`refEnd` are the requested ones clipped to the read's span.
20
+ *
21
+ * The op loop adds each op's lengths through the masks above rather than
22
+ * branching on the op, because a long read's op sequence is close to random
23
+ * and the branches mispredict; only the op that crosses the next edge takes
24
+ * the slow path.
25
+ */
26
+ export declare function refWindowToRead(cigarOps: ArrayLike<number>, refStart: number, refEnd: number): ReadWindow;
@@ -0,0 +1,67 @@
1
+ import { CIGAR_D, CIGAR_EQ, CIGAR_I, CIGAR_M, CIGAR_N, CIGAR_S, CIGAR_X, } from './cigarConstants.js';
2
+ const ALIGNED = (1 << CIGAR_M) | (1 << CIGAR_EQ) | (1 << CIGAR_X);
3
+ const CONSUMES_READ = ALIGNED | (1 << CIGAR_I) | (1 << CIGAR_S);
4
+ const CONSUMES_REF = ALIGNED | (1 << CIGAR_D) | (1 << CIGAR_N);
5
+ /**
6
+ * #api
7
+ * The read offsets `[readStart, readEnd)` holding every aligned base whose
8
+ * read-relative reference offset lies in `[refStart, refEnd)`, and the op a
9
+ * `getNextRefPos` walk over them starts at. Aligned read offsets map to
10
+ * ascending reference offsets, so the range holds no other aligned base; a
11
+ * clip or insertion inside it maps to no reference offset. The returned
12
+ * `refStart`/`refEnd` are the requested ones clipped to the read's span.
13
+ *
14
+ * The op loop adds each op's lengths through the masks above rather than
15
+ * branching on the op, because a long read's op sequence is close to random
16
+ * and the branches mispredict; only the op that crosses the next edge takes
17
+ * the slow path.
18
+ */
19
+ export function refWindowToRead(cigarOps, refStart, refEnd) {
20
+ let readPos = 0;
21
+ let refPos = 0;
22
+ let readStart = -1;
23
+ let readEnd = -1;
24
+ let op = cigarOps.length;
25
+ let opRead = 0;
26
+ let opRef = 0;
27
+ let edge = Math.min(refStart + 1, refEnd);
28
+ for (let i = 0, l = cigarOps.length; i < l; i++) {
29
+ const packed = cigarOps[i];
30
+ const len = packed >>> 4;
31
+ const code = packed & 0xf;
32
+ const refLen = len & -((CONSUMES_REF >>> code) & 1);
33
+ if (refPos + refLen >= edge) {
34
+ const aligned = (ALIGNED >>> code) & 1;
35
+ if (readStart < 0 && aligned) {
36
+ readStart = readPos + Math.max(0, refStart - refPos);
37
+ op = i;
38
+ opRead = readPos;
39
+ opRef = refPos;
40
+ edge = refEnd;
41
+ }
42
+ if (refPos + refLen >= refEnd) {
43
+ readEnd = aligned ? readPos + Math.max(0, refEnd - refPos) : readPos;
44
+ refPos += refLen;
45
+ break;
46
+ }
47
+ }
48
+ readPos += len & -((CONSUMES_READ >>> code) & 1);
49
+ refPos += refLen;
50
+ }
51
+ if (readEnd < 0) {
52
+ readEnd = readPos;
53
+ }
54
+ if (readStart < 0 || readStart > readEnd) {
55
+ readStart = readEnd;
56
+ }
57
+ const clippedStart = Math.max(0, refStart);
58
+ return {
59
+ readStart,
60
+ readEnd,
61
+ refStart: clippedStart,
62
+ refEnd: Math.max(clippedStart, Math.min(refEnd, refPos)),
63
+ op,
64
+ opRead,
65
+ opRef,
66
+ };
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/cigar-utils",
3
- "version": "5.0.0-beta.1",
3
+ "version": "5.0.0-beta.10",
4
4
  "type": "module",
5
5
  "description": "Pure CIGAR / MD / mismatch parsers and types — no rendering or framework deps",
6
6
  "keywords": [