countmut 0.3.2__tar.gz → 0.3.3__tar.gz
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.
- {countmut-0.3.2/countmut.egg-info → countmut-0.3.3}/PKG-INFO +1 -1
- {countmut-0.3.2 → countmut-0.3.3}/backend/countmut_core.c +28 -26
- {countmut-0.3.2 → countmut-0.3.3}/countmut/_core/countmut_core +0 -0
- {countmut-0.3.2 → countmut-0.3.3/countmut.egg-info}/PKG-INFO +1 -1
- {countmut-0.3.2 → countmut-0.3.3}/pyproject.toml +1 -1
- {countmut-0.3.2 → countmut-0.3.3}/tests/test_correctness.py +18 -23
- {countmut-0.3.2 → countmut-0.3.3}/tests/test_router.py +7 -5
- {countmut-0.3.2 → countmut-0.3.3}/LICENSE +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/MANIFEST.in +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/README.md +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/Makefile +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/bedidx.c +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/countmut_core.h +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/countmut_core_main.c +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/countmut_expr.c +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/countmut_expr.h +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/ketopt.h +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/khash.h +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/kseq.h +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/ksort.h +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/kstdint.h +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/backend/kstring.h +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut/__init__.py +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut/backend.py +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut/bam_tags.py +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut/cli.py +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut/core.py +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut/model.py +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut/utils.py +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut.egg-info/SOURCES.txt +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut.egg-info/dependency_links.txt +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut.egg-info/entry_points.txt +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut.egg-info/requires.txt +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/countmut.egg-info/top_level.txt +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/setup.cfg +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/tests/test_cli.py +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/tests/test_core.py +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/tests/test_unified.py +0 -0
- {countmut-0.3.2 → countmut-0.3.3}/tests/test_utils.py +0 -0
|
@@ -96,6 +96,22 @@ static int base_to_index(char c) {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/* Reference-frame base index for a read at a CIGAR query offset `qpos`.
|
|
100
|
+
*
|
|
101
|
+
* `qpos` is the query offset walked left->right along the CIGAR (reference
|
|
102
|
+
* order). bwa / minibwa write the SEQ of a reverse-strand read already in
|
|
103
|
+
* reference-forward orientation (the stored SEQ is the reverse complement of
|
|
104
|
+
* the original read, matching the reference), so SEQ[qpos] is directly the
|
|
105
|
+
* reference-frame base -- no reverse-complement or index reversal is needed.
|
|
106
|
+
*
|
|
107
|
+
* Returns 0..3 for A/C/G/T, or 4 for any other symbol.
|
|
108
|
+
*/
|
|
109
|
+
static int ref_frame_base(const bam1_t *b, uint32_t qpos) {
|
|
110
|
+
uint32_t qlen = b->core.l_qseq;
|
|
111
|
+
if (qpos >= qlen) return 4;
|
|
112
|
+
return nt16_index(bam_seqi(bam_get_seq(b), qpos));
|
|
113
|
+
}
|
|
114
|
+
|
|
99
115
|
/* Reverse-complement a single uppercase motif base (A<->T, C<->G, else N). */
|
|
100
116
|
static char rc_nt(char c) {
|
|
101
117
|
switch (c) {
|
|
@@ -567,13 +583,10 @@ static void count_interval(worker_t *w, const cm_config *cfg, bam_hdr_t *hdr, FI
|
|
|
567
583
|
const bam_pileup1_t *p = &plp[0][i];
|
|
568
584
|
const bam1_t *b = p->b;
|
|
569
585
|
int s = bio_strand(b);
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
* reference frame. Key on bam_is_rev (read orientation), not the
|
|
575
|
-
* biological strand s (which differs for R2 in paired data). */
|
|
576
|
-
if (bam_is_rev(b) && base_i < 4) base_i = 3 - base_i;
|
|
586
|
+
/* Reference-frame base: for a reverse read the stored SEQ is the
|
|
587
|
+
* reverse complement, so the base lives at SEQ index qlen-1-qpos
|
|
588
|
+
* and must be complemented (see ref_frame_base). */
|
|
589
|
+
int base_i = ref_frame_base(b, (uint32_t)p->qpos);
|
|
577
590
|
/* router-assigned category slot (0..CM_CAT_MAX-1); g_a set in
|
|
578
591
|
* the selection loop for every kept candidate. */
|
|
579
592
|
int cat = w->g_a[i];
|
|
@@ -763,15 +776,12 @@ static rw_w *rw_add_base(worker_t *w, const cm_config *cfg, bam_hdr_t *hdr, int
|
|
|
763
776
|
(ref_pos >= 0 && ref_pos < w->chr_len) ? w->chr_seq[ref_pos] : 'N');
|
|
764
777
|
if (rslot < 0) return wins;
|
|
765
778
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
|
|
772
|
-
* reads an R2 mapped forward has bio_strand=1 yet is NOT reverse-oriented,
|
|
773
|
-
* so complementing on s would wrongly flip its bases (A<->T). */
|
|
774
|
-
if (bam_is_rev(b) && base_i < 4) base_i = 3 - base_i;
|
|
779
|
+
/* Reference-frame base: for a reverse read the stored SEQ is the reverse
|
|
780
|
+
* complement, so the base lives at SEQ index qlen-1-qpos and must be
|
|
781
|
+
* complemented (see ref_frame_base). Keying on the read's own orientation
|
|
782
|
+
* (bam_is_rev) is correct; the biological strand s differs for R2 in
|
|
783
|
+
* paired data. */
|
|
784
|
+
int base_i = ref_frame_base(b, qpos);
|
|
775
785
|
int qual = (int)bam_get_qual(b)[qpos];
|
|
776
786
|
if (direct) {
|
|
777
787
|
int cat = rslot;
|
|
@@ -1042,11 +1052,7 @@ static void count_interval_readwalk(worker_t *w, const cm_config *cfg, bam_hdr_t
|
|
|
1042
1052
|
if (ref_pos < beg || ref_pos >= end) continue;
|
|
1043
1053
|
uint32_t qpos = qcur + (uint32_t)k;
|
|
1044
1054
|
if (qpos >= qlen) break;
|
|
1045
|
-
|
|
1046
|
-
int base_i = nt16_index(nt);
|
|
1047
|
-
/* Complement on read orientation (bam_is_rev), not
|
|
1048
|
-
* the biological strand s (R2 differs). */
|
|
1049
|
-
if (bam_is_rev(b) && base_i < 4) base_i = 3 - base_i;
|
|
1055
|
+
int base_i = ref_frame_base(b, qpos);
|
|
1050
1056
|
int cat = 0;
|
|
1051
1057
|
if (w->expr && cm_expr_has_read(w->expr) && !cm_expr_read_constant(w->expr)) {
|
|
1052
1058
|
cat = cm_expr_route(w->expr, b, hdr->target_name[tid],
|
|
@@ -1114,11 +1120,7 @@ static void count_interval_readwalk(worker_t *w, const cm_config *cfg, bam_hdr_t
|
|
|
1114
1120
|
uint32_t qpos = qcur + (uint32_t)k;
|
|
1115
1121
|
if (qpos >= qlen) break;
|
|
1116
1122
|
int in_ovl = ((int)qpos >= olo && (int)qpos < ohi);
|
|
1117
|
-
|
|
1118
|
-
int base_i = nt16_index(nt);
|
|
1119
|
-
/* Complement on read orientation (bam_is_rev),
|
|
1120
|
-
* not the biological strand s (R2 differs). */
|
|
1121
|
-
if (bam_is_rev(b) && base_i < 4) base_i = 3 - base_i;
|
|
1123
|
+
int base_i = ref_frame_base(b, qpos);
|
|
1122
1124
|
int cat = 0;
|
|
1123
1125
|
if (w->expr && cm_expr_has_read(w->expr) && !cm_expr_read_constant(w->expr)) {
|
|
1124
1126
|
cat = cm_expr_route(w->expr, b, hdr->target_name[tid],
|
|
Binary file
|
|
@@ -92,37 +92,33 @@ def indel_data(tmp_path_factory):
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
# ---------------------------------------------------------------------------
|
|
95
|
-
# BUG: base complement
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
95
|
+
# BUG: base complement applied to reverse-strand reads. bwa / minibwa write
|
|
96
|
+
# the SEQ of a reverse-strand read already in reference-forward orientation
|
|
97
|
+
# (the stored SEQ is the reverse complement of the original read, matching the
|
|
98
|
+
# reference), so the reference-frame base is simply SEQ[qpos]. Complementing
|
|
99
|
+
# or index-reversing it (as an earlier fix did) wrongly flipped A<->T for every
|
|
100
|
+
# reverse read, turning a pure-reference site into a ~50/50 A/T mix.
|
|
101
|
+
# Regression: reverse-strand reads must be counted as-is.
|
|
100
102
|
# ---------------------------------------------------------------------------
|
|
101
|
-
def
|
|
102
|
-
root = str(tmp_path / "
|
|
103
|
+
def test_reverse_read_not_complemented(tmp_path):
|
|
104
|
+
root = str(tmp_path / "rev")
|
|
103
105
|
os.makedirs(root, exist_ok=True)
|
|
104
|
-
# reference base at 0-based 0 is 'A'.
|
|
105
|
-
#
|
|
106
|
+
# reference base at 0-based 0 is 'A'. A reverse-strand read (flag 16) whose
|
|
107
|
+
# stored SEQ is reference-forward (as bwa/minibwa write it) carries 'A' at
|
|
108
|
+
# 0-based 0; it must stay A, not flip to T.
|
|
106
109
|
bam, fa = _write_bam(
|
|
107
110
|
root,
|
|
108
111
|
[
|
|
109
|
-
|
|
110
|
-
lambda h: _mk(h, "
|
|
111
|
-
lambda h: _mk(h, "frag1", 35, 0, "ACGTACGTAC"),
|
|
112
|
-
# R1 reverse (flag 83) and R2 reverse (flag 147): stored SEQ is the
|
|
113
|
-
# reverse complement, so they must be complemented back to A.
|
|
114
|
-
lambda h: _mk(h, "frag2", 83, 0, "ACGTACGTAC"),
|
|
115
|
-
lambda h: _mk(h, "frag2", 147, 0, "ACGTACGTAC"),
|
|
112
|
+
lambda h: _mk(h, "fwd", 0, 0, "ACGTACGTAC"),
|
|
113
|
+
lambda h: _mk(h, "rev", 16, 0, "ACGTACGTAC"),
|
|
116
114
|
],
|
|
117
115
|
)
|
|
118
|
-
_h, rows = run_c(bam, fa, engine="read-walk", region="chr1:1-5"
|
|
119
|
-
|
|
116
|
+
_h, rows = run_c(bam, fa, engine="read-walk", region="chr1:1-5",
|
|
117
|
+
extra=["--strandless"])
|
|
120
118
|
row = [r for r in rows if r[1] == 1][0]
|
|
121
119
|
a, c, g, t = row[4], row[5], row[6], row[7]
|
|
122
|
-
#
|
|
123
|
-
assert a
|
|
124
|
-
# The bug produced ~50/50 A/T; assert T is a small minority.
|
|
125
|
-
assert t <= a * 0.1, (row)
|
|
120
|
+
# Both reads carry reference 'A' at 0-based 0 -> A must dominate.
|
|
121
|
+
assert a == 2 and t == 0, (row)
|
|
126
122
|
|
|
127
123
|
|
|
128
124
|
# ---------------------------------------------------------------------------
|
|
@@ -421,7 +417,6 @@ def _write_reference_read_bam(tmp_path, chrom, length):
|
|
|
421
417
|
return bam, fa
|
|
422
418
|
|
|
423
419
|
|
|
424
|
-
@pytest.mark.xfail(reason="full htslib SAM transcode produces non-BGZF temp BAM; CRAM/BAM paths are covered")
|
|
425
420
|
def test_sam_input_matches_bam(motif_data, tmp_path):
|
|
426
421
|
"""SAM (plain, and gzipped) input must produce byte-identical output to the
|
|
427
422
|
equivalent BAM (it is auto-transcoded to a temp BAM + index)."""
|
|
@@ -80,13 +80,15 @@ def data(tmp_path_factory):
|
|
|
80
80
|
mk("p_g", 0, 0, "GGGGGAATT", Q40, ok),
|
|
81
81
|
# p_x: NS > 1 -> dropped entirely
|
|
82
82
|
mk("p_x", 0, 0, "GGGGAAATT", Q40, [("NS", 2), ("Yf", 1), ("Zf", 0)]),
|
|
83
|
-
# minus reads, 9M at 0-based 4..12
|
|
84
|
-
#
|
|
85
|
-
|
|
83
|
+
# minus reads, 9M at 0-based 4..12. bwa/minibwa store the SEQ of a
|
|
84
|
+
# reverse-strand read in reference-forward orientation, so the stored
|
|
85
|
+
# base equals the reference base (no complement needed). reference
|
|
86
|
+
# [4:13] = AAAACCCC + 1bp past the contig end.
|
|
87
|
+
mk("m_hi", 16, 4, "AAAACCCCC", Q40, ok),
|
|
86
88
|
# m_lo: no Yf -> group 0 at every base
|
|
87
|
-
mk("m_lo", 16, 4, "
|
|
89
|
+
mk("m_lo", 16, 4, "AAAACCCCC", Q40, [("NS", 0), ("Yf", 0), ("Zf", 0)]),
|
|
88
90
|
# m_x: NS > 1 -> dropped entirely
|
|
89
|
-
mk("m_x", 16, 4, "
|
|
91
|
+
mk("m_x", 16, 4, "AAAACCCCC", Q40, [("NS", 2), ("Yf", 1), ("Zf", 0)]),
|
|
90
92
|
]
|
|
91
93
|
reads.sort(key=lambda r: r.reference_start)
|
|
92
94
|
with pysam.AlignmentFile(bam, "wb", header=header) as out:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|