countmut 0.3.1__tar.gz → 0.3.2__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.
Files changed (39) hide show
  1. {countmut-0.3.1/countmut.egg-info → countmut-0.3.2}/PKG-INFO +1 -1
  2. {countmut-0.3.1 → countmut-0.3.2}/backend/countmut_core.c +18 -10
  3. {countmut-0.3.1 → countmut-0.3.2}/countmut/_core/countmut_core +0 -0
  4. {countmut-0.3.1 → countmut-0.3.2/countmut.egg-info}/PKG-INFO +1 -1
  5. {countmut-0.3.1 → countmut-0.3.2}/pyproject.toml +1 -1
  6. {countmut-0.3.1 → countmut-0.3.2}/tests/test_correctness.py +34 -0
  7. {countmut-0.3.1 → countmut-0.3.2}/LICENSE +0 -0
  8. {countmut-0.3.1 → countmut-0.3.2}/MANIFEST.in +0 -0
  9. {countmut-0.3.1 → countmut-0.3.2}/README.md +0 -0
  10. {countmut-0.3.1 → countmut-0.3.2}/backend/Makefile +0 -0
  11. {countmut-0.3.1 → countmut-0.3.2}/backend/bedidx.c +0 -0
  12. {countmut-0.3.1 → countmut-0.3.2}/backend/countmut_core.h +0 -0
  13. {countmut-0.3.1 → countmut-0.3.2}/backend/countmut_core_main.c +0 -0
  14. {countmut-0.3.1 → countmut-0.3.2}/backend/countmut_expr.c +0 -0
  15. {countmut-0.3.1 → countmut-0.3.2}/backend/countmut_expr.h +0 -0
  16. {countmut-0.3.1 → countmut-0.3.2}/backend/ketopt.h +0 -0
  17. {countmut-0.3.1 → countmut-0.3.2}/backend/khash.h +0 -0
  18. {countmut-0.3.1 → countmut-0.3.2}/backend/kseq.h +0 -0
  19. {countmut-0.3.1 → countmut-0.3.2}/backend/ksort.h +0 -0
  20. {countmut-0.3.1 → countmut-0.3.2}/backend/kstdint.h +0 -0
  21. {countmut-0.3.1 → countmut-0.3.2}/backend/kstring.h +0 -0
  22. {countmut-0.3.1 → countmut-0.3.2}/countmut/__init__.py +0 -0
  23. {countmut-0.3.1 → countmut-0.3.2}/countmut/backend.py +0 -0
  24. {countmut-0.3.1 → countmut-0.3.2}/countmut/bam_tags.py +0 -0
  25. {countmut-0.3.1 → countmut-0.3.2}/countmut/cli.py +0 -0
  26. {countmut-0.3.1 → countmut-0.3.2}/countmut/core.py +0 -0
  27. {countmut-0.3.1 → countmut-0.3.2}/countmut/model.py +0 -0
  28. {countmut-0.3.1 → countmut-0.3.2}/countmut/utils.py +0 -0
  29. {countmut-0.3.1 → countmut-0.3.2}/countmut.egg-info/SOURCES.txt +0 -0
  30. {countmut-0.3.1 → countmut-0.3.2}/countmut.egg-info/dependency_links.txt +0 -0
  31. {countmut-0.3.1 → countmut-0.3.2}/countmut.egg-info/entry_points.txt +0 -0
  32. {countmut-0.3.1 → countmut-0.3.2}/countmut.egg-info/requires.txt +0 -0
  33. {countmut-0.3.1 → countmut-0.3.2}/countmut.egg-info/top_level.txt +0 -0
  34. {countmut-0.3.1 → countmut-0.3.2}/setup.cfg +0 -0
  35. {countmut-0.3.1 → countmut-0.3.2}/tests/test_cli.py +0 -0
  36. {countmut-0.3.1 → countmut-0.3.2}/tests/test_core.py +0 -0
  37. {countmut-0.3.1 → countmut-0.3.2}/tests/test_router.py +0 -0
  38. {countmut-0.3.1 → countmut-0.3.2}/tests/test_unified.py +0 -0
  39. {countmut-0.3.1 → countmut-0.3.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: countmut
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Unified ultra-fast strand-aware mutation counter (C backend + samtools-style -e/-p filters)
5
5
  Author-email: Ye Chang <yech1990@gmail.com>
6
6
  License-Expression: MIT
@@ -569,10 +569,11 @@ static void count_interval(worker_t *w, const cm_config *cfg, bam_hdr_t *hdr, FI
569
569
  int s = bio_strand(b);
570
570
  uint8_t nt = bam_seqi(bam_get_seq(b), p->qpos);
571
571
  int base_i = nt16_index(nt);
572
- /* Minus reads: stored SEQ is 5'->3' (SAM spec) but qpos walks
573
- * CIGAR order (left->right); complement the base into the
574
- * reference frame (parity with countmut 0.0.x + pysam pairs). */
575
- if (s == 1 && base_i < 4) base_i = 3 - base_i;
572
+ /* Reverse-oriented reads: stored SEQ is 5'->3' (SAM spec) but qpos
573
+ * walks CIGAR order (left->right); complement the base into the
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;
576
577
  /* router-assigned category slot (0..CM_CAT_MAX-1); g_a set in
577
578
  * the selection loop for every kept candidate. */
578
579
  int cat = w->g_a[i];
@@ -764,10 +765,13 @@ static rw_w *rw_add_base(worker_t *w, const cm_config *cfg, bam_hdr_t *hdr, int
764
765
  }
765
766
  uint8_t nt = bam_seqi(bam_get_seq(b), qpos);
766
767
  int base_i = nt16_index(nt);
767
- /* Minus reads: stored SEQ is 5'->3' (SAM spec) but qpos walks CIGAR order
768
- * (left->right); complement the base into the reference frame (parity
769
- * with countmut 0.0.x + pysam pairs). */
770
- if (s == 1 && base_i < 4) base_i = 3 - base_i;
768
+ /* Reverse-oriented reads: stored SEQ is 5'->3' (SAM spec) but qpos walks
769
+ * CIGAR order (left->right); complement the base into the reference frame
770
+ * (parity with countmut 0.0.x + pysam pairs). This must key on the read's
771
+ * own orientation (bam_is_rev), NOT on the biological strand s: for paired
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;
771
775
  int qual = (int)bam_get_qual(b)[qpos];
772
776
  if (direct) {
773
777
  int cat = rslot;
@@ -1040,7 +1044,9 @@ static void count_interval_readwalk(worker_t *w, const cm_config *cfg, bam_hdr_t
1040
1044
  if (qpos >= qlen) break;
1041
1045
  uint8_t nt = bam_seqi(bam_get_seq(b), qpos);
1042
1046
  int base_i = nt16_index(nt);
1043
- if (s == 1 && base_i < 4) base_i = 3 - base_i;
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;
1044
1050
  int cat = 0;
1045
1051
  if (w->expr && cm_expr_has_read(w->expr) && !cm_expr_read_constant(w->expr)) {
1046
1052
  cat = cm_expr_route(w->expr, b, hdr->target_name[tid],
@@ -1110,7 +1116,9 @@ static void count_interval_readwalk(worker_t *w, const cm_config *cfg, bam_hdr_t
1110
1116
  int in_ovl = ((int)qpos >= olo && (int)qpos < ohi);
1111
1117
  uint8_t nt = bam_seqi(bam_get_seq(b), qpos);
1112
1118
  int base_i = nt16_index(nt);
1113
- if (s == 1 && base_i < 4) base_i = 3 - base_i;
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;
1114
1122
  int cat = 0;
1115
1123
  if (w->expr && cm_expr_has_read(w->expr) && !cm_expr_read_constant(w->expr)) {
1116
1124
  cat = cm_expr_route(w->expr, b, hdr->target_name[tid],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: countmut
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Unified ultra-fast strand-aware mutation counter (C backend + samtools-style -e/-p filters)
5
5
  Author-email: Ye Chang <yech1990@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "countmut"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Unified ultra-fast strand-aware mutation counter (C backend + samtools-style -e/-p filters)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -91,6 +91,40 @@ def indel_data(tmp_path_factory):
91
91
  )
92
92
 
93
93
 
94
+ # ---------------------------------------------------------------------------
95
+ # BUG: base complement keyed on biological strand (s) instead of read
96
+ # orientation (bam_is_rev). For paired data an R2 read mapped forward has
97
+ # bio_strand=1 yet is NOT reverse-oriented, so its bases were wrongly
98
+ # reverse-complemented (A<->T), turning a pure-reference site into a 50/50
99
+ # A/T mix. Regression: R2-forward reads must count their stored bases as-is.
100
+ # ---------------------------------------------------------------------------
101
+ def test_r2_forward_not_complemented(tmp_path):
102
+ root = str(tmp_path / "r2f")
103
+ os.makedirs(root, exist_ok=True)
104
+ # reference base at 0-based 0 is 'A'. An R2 read mapped FORWARD stores the
105
+ # base as-is (A), so a pure-A site must stay A, not flip to T.
106
+ bam, fa = _write_bam(
107
+ root,
108
+ [
109
+ # R1 forward (flag 99) and R2 forward (flag 35) covering 0-based 0.
110
+ lambda h: _mk(h, "frag1", 99, 0, "ACGTACGTAC"),
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"),
116
+ ],
117
+ )
118
+ _h, rows = run_c(bam, fa, engine="read-walk", region="chr1:1-5")
119
+ # rows: [chrom, pos, ref, depth, a, c, g, t, n] (strandless default)
120
+ row = [r for r in rows if r[1] == 1][0]
121
+ a, c, g, t = row[4], row[5], row[6], row[7]
122
+ # All four reads carry reference 'A' at 0-based 0 -> A must dominate.
123
+ assert a > 0 and a >= c + g + t, (row)
124
+ # The bug produced ~50/50 A/T; assert T is a small minority.
125
+ assert t <= a * 0.1, (row)
126
+
127
+
94
128
  # ---------------------------------------------------------------------------
95
129
  # BUG: '-' mutation-row motif reverse-complemented with reference-forward bases
96
130
  # ---------------------------------------------------------------------------
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