countmut 0.3.1__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.1/countmut.egg-info → countmut-0.3.3}/PKG-INFO +1 -1
- {countmut-0.3.1 → countmut-0.3.3}/backend/countmut_core.c +28 -18
- {countmut-0.3.1 → countmut-0.3.3}/countmut/_core/countmut_core +0 -0
- {countmut-0.3.1 → countmut-0.3.3/countmut.egg-info}/PKG-INFO +1 -1
- {countmut-0.3.1 → countmut-0.3.3}/pyproject.toml +1 -1
- {countmut-0.3.1 → countmut-0.3.3}/tests/test_correctness.py +30 -1
- {countmut-0.3.1 → countmut-0.3.3}/tests/test_router.py +7 -5
- {countmut-0.3.1 → countmut-0.3.3}/LICENSE +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/MANIFEST.in +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/README.md +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/Makefile +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/bedidx.c +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/countmut_core.h +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/countmut_core_main.c +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/countmut_expr.c +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/countmut_expr.h +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/ketopt.h +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/khash.h +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/kseq.h +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/ksort.h +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/kstdint.h +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/backend/kstring.h +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut/__init__.py +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut/backend.py +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut/bam_tags.py +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut/cli.py +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut/core.py +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut/model.py +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut/utils.py +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut.egg-info/SOURCES.txt +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut.egg-info/dependency_links.txt +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut.egg-info/entry_points.txt +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut.egg-info/requires.txt +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/countmut.egg-info/top_level.txt +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/setup.cfg +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/tests/test_cli.py +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/tests/test_core.py +0 -0
- {countmut-0.3.1 → countmut-0.3.3}/tests/test_unified.py +0 -0
- {countmut-0.3.1 → 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,12 +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 (parity with countmut 0.0.x + pysam pairs). */
|
|
575
|
-
if (s == 1 && 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);
|
|
576
590
|
/* router-assigned category slot (0..CM_CAT_MAX-1); g_a set in
|
|
577
591
|
* the selection loop for every kept candidate. */
|
|
578
592
|
int cat = w->g_a[i];
|
|
@@ -762,12 +776,12 @@ static rw_w *rw_add_base(worker_t *w, const cm_config *cfg, bam_hdr_t *hdr, int
|
|
|
762
776
|
(ref_pos >= 0 && ref_pos < w->chr_len) ? w->chr_seq[ref_pos] : 'N');
|
|
763
777
|
if (rslot < 0) return wins;
|
|
764
778
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
* (
|
|
769
|
-
*
|
|
770
|
-
|
|
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);
|
|
771
785
|
int qual = (int)bam_get_qual(b)[qpos];
|
|
772
786
|
if (direct) {
|
|
773
787
|
int cat = rslot;
|
|
@@ -1038,9 +1052,7 @@ static void count_interval_readwalk(worker_t *w, const cm_config *cfg, bam_hdr_t
|
|
|
1038
1052
|
if (ref_pos < beg || ref_pos >= end) continue;
|
|
1039
1053
|
uint32_t qpos = qcur + (uint32_t)k;
|
|
1040
1054
|
if (qpos >= qlen) break;
|
|
1041
|
-
|
|
1042
|
-
int base_i = nt16_index(nt);
|
|
1043
|
-
if (s == 1 && base_i < 4) base_i = 3 - base_i;
|
|
1055
|
+
int base_i = ref_frame_base(b, qpos);
|
|
1044
1056
|
int cat = 0;
|
|
1045
1057
|
if (w->expr && cm_expr_has_read(w->expr) && !cm_expr_read_constant(w->expr)) {
|
|
1046
1058
|
cat = cm_expr_route(w->expr, b, hdr->target_name[tid],
|
|
@@ -1108,9 +1120,7 @@ static void count_interval_readwalk(worker_t *w, const cm_config *cfg, bam_hdr_t
|
|
|
1108
1120
|
uint32_t qpos = qcur + (uint32_t)k;
|
|
1109
1121
|
if (qpos >= qlen) break;
|
|
1110
1122
|
int in_ovl = ((int)qpos >= olo && (int)qpos < ohi);
|
|
1111
|
-
|
|
1112
|
-
int base_i = nt16_index(nt);
|
|
1113
|
-
if (s == 1 && base_i < 4) base_i = 3 - base_i;
|
|
1123
|
+
int base_i = ref_frame_base(b, qpos);
|
|
1114
1124
|
int cat = 0;
|
|
1115
1125
|
if (w->expr && cm_expr_has_read(w->expr) && !cm_expr_read_constant(w->expr)) {
|
|
1116
1126
|
cat = cm_expr_route(w->expr, b, hdr->target_name[tid],
|
|
Binary file
|
|
@@ -91,6 +91,36 @@ def indel_data(tmp_path_factory):
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
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.
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
def test_reverse_read_not_complemented(tmp_path):
|
|
104
|
+
root = str(tmp_path / "rev")
|
|
105
|
+
os.makedirs(root, exist_ok=True)
|
|
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.
|
|
109
|
+
bam, fa = _write_bam(
|
|
110
|
+
root,
|
|
111
|
+
[
|
|
112
|
+
lambda h: _mk(h, "fwd", 0, 0, "ACGTACGTAC"),
|
|
113
|
+
lambda h: _mk(h, "rev", 16, 0, "ACGTACGTAC"),
|
|
114
|
+
],
|
|
115
|
+
)
|
|
116
|
+
_h, rows = run_c(bam, fa, engine="read-walk", region="chr1:1-5",
|
|
117
|
+
extra=["--strandless"])
|
|
118
|
+
row = [r for r in rows if r[1] == 1][0]
|
|
119
|
+
a, c, g, t = row[4], row[5], row[6], row[7]
|
|
120
|
+
# Both reads carry reference 'A' at 0-based 0 -> A must dominate.
|
|
121
|
+
assert a == 2 and t == 0, (row)
|
|
122
|
+
|
|
123
|
+
|
|
94
124
|
# ---------------------------------------------------------------------------
|
|
95
125
|
# BUG: '-' mutation-row motif reverse-complemented with reference-forward bases
|
|
96
126
|
# ---------------------------------------------------------------------------
|
|
@@ -387,7 +417,6 @@ def _write_reference_read_bam(tmp_path, chrom, length):
|
|
|
387
417
|
return bam, fa
|
|
388
418
|
|
|
389
419
|
|
|
390
|
-
@pytest.mark.xfail(reason="full htslib SAM transcode produces non-BGZF temp BAM; CRAM/BAM paths are covered")
|
|
391
420
|
def test_sam_input_matches_bam(motif_data, tmp_path):
|
|
392
421
|
"""SAM (plain, and gzipped) input must produce byte-identical output to the
|
|
393
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
|