@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.
- package/LICENSE +201 -0
- package/README.md +167 -0
- package/esm/bamSeqDecoder.d.ts +3 -0
- package/esm/bamSeqDecoder.js +10 -0
- package/esm/bamSeqEncoder.d.ts +8 -0
- package/esm/bamSeqEncoder.js +33 -0
- package/esm/buildReadVsRefFeatures.d.ts +57 -0
- package/esm/buildReadVsRefFeatures.js +66 -0
- package/esm/cigarConstants.d.ts +11 -0
- package/esm/cigarConstants.js +15 -0
- package/esm/cigarRenderedSegments.d.ts +13 -0
- package/esm/cigarRenderedSegments.js +84 -0
- package/esm/cigarReorient.d.ts +2 -0
- package/esm/cigarReorient.js +29 -0
- package/esm/cigarToMismatches2.d.ts +2 -0
- package/esm/cigarToMismatches2.js +90 -0
- package/esm/cigarWalkStart.d.ts +4 -0
- package/esm/cigarWalkStart.js +54 -0
- package/esm/csOps.d.ts +47 -0
- package/esm/csOps.js +152 -0
- package/esm/csToCigar.d.ts +1 -0
- package/esm/csToCigar.js +43 -0
- package/esm/getNextRefPos.d.ts +31 -0
- package/esm/getNextRefPos.js +69 -0
- package/esm/index.d.ts +24 -0
- package/esm/index.js +24 -0
- package/esm/mdToMismatches2.d.ts +2 -0
- package/esm/mdToMismatches2.js +108 -0
- package/esm/mismatchCallback.d.ts +29 -0
- package/esm/mismatchCallback.js +20 -0
- package/esm/mismatchParser.d.ts +79 -0
- package/esm/mismatchParser.js +238 -0
- package/esm/mismatchTypes.d.ts +27 -0
- package/esm/mismatchTypes.js +1 -0
- package/esm/numericCigarHasSkip.d.ts +1 -0
- package/esm/numericCigarHasSkip.js +11 -0
- package/esm/numericCigarToString.d.ts +1 -0
- package/esm/numericCigarToString.js +13 -0
- package/esm/orientAlignment.d.ts +9 -0
- package/esm/orientAlignment.js +26 -0
- package/esm/pafIdentity.d.ts +20 -0
- package/esm/pafIdentity.js +31 -0
- package/esm/readEndpoints.d.ts +18 -0
- package/esm/readEndpoints.js +69 -0
- package/esm/samFlags.d.ts +50 -0
- package/esm/samFlags.js +89 -0
- 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,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,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;
|
package/esm/csToCigar.js
ADDED
|
@@ -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';
|