@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 +99 -1
- package/esm/cigarConstants.d.ts +1 -0
- package/esm/cigarConstants.js +5 -0
- package/esm/cigarRenderedSegments.d.ts +3 -1
- package/esm/cigarRenderedSegments.js +14 -4
- package/esm/cigarReorient.js +28 -18
- package/esm/coarseCigar.d.ts +54 -0
- package/esm/coarseCigar.js +235 -0
- package/esm/getNextRefPos.d.ts +5 -1
- package/esm/getNextRefPos.js +7 -4
- package/esm/index.d.ts +5 -1
- package/esm/index.js +3 -1
- package/esm/refWindowToRead.d.ts +26 -0
- package/esm/refWindowToRead.js +67 -0
- package/package.json +1 -1
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 -->
|
package/esm/cigarConstants.d.ts
CHANGED
package/esm/cigarConstants.js
CHANGED
|
@@ -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 ===
|
|
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
|
|
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
|
}
|
package/esm/cigarReorient.js
CHANGED
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/esm/getNextRefPos.d.ts
CHANGED
|
@@ -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;
|
package/esm/getNextRefPos.js
CHANGED
|
@@ -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
|
+
}
|