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.
Files changed (39) hide show
  1. {countmut-0.3.1/countmut.egg-info → countmut-0.3.3}/PKG-INFO +1 -1
  2. {countmut-0.3.1 → countmut-0.3.3}/backend/countmut_core.c +28 -18
  3. {countmut-0.3.1 → countmut-0.3.3}/countmut/_core/countmut_core +0 -0
  4. {countmut-0.3.1 → countmut-0.3.3/countmut.egg-info}/PKG-INFO +1 -1
  5. {countmut-0.3.1 → countmut-0.3.3}/pyproject.toml +1 -1
  6. {countmut-0.3.1 → countmut-0.3.3}/tests/test_correctness.py +30 -1
  7. {countmut-0.3.1 → countmut-0.3.3}/tests/test_router.py +7 -5
  8. {countmut-0.3.1 → countmut-0.3.3}/LICENSE +0 -0
  9. {countmut-0.3.1 → countmut-0.3.3}/MANIFEST.in +0 -0
  10. {countmut-0.3.1 → countmut-0.3.3}/README.md +0 -0
  11. {countmut-0.3.1 → countmut-0.3.3}/backend/Makefile +0 -0
  12. {countmut-0.3.1 → countmut-0.3.3}/backend/bedidx.c +0 -0
  13. {countmut-0.3.1 → countmut-0.3.3}/backend/countmut_core.h +0 -0
  14. {countmut-0.3.1 → countmut-0.3.3}/backend/countmut_core_main.c +0 -0
  15. {countmut-0.3.1 → countmut-0.3.3}/backend/countmut_expr.c +0 -0
  16. {countmut-0.3.1 → countmut-0.3.3}/backend/countmut_expr.h +0 -0
  17. {countmut-0.3.1 → countmut-0.3.3}/backend/ketopt.h +0 -0
  18. {countmut-0.3.1 → countmut-0.3.3}/backend/khash.h +0 -0
  19. {countmut-0.3.1 → countmut-0.3.3}/backend/kseq.h +0 -0
  20. {countmut-0.3.1 → countmut-0.3.3}/backend/ksort.h +0 -0
  21. {countmut-0.3.1 → countmut-0.3.3}/backend/kstdint.h +0 -0
  22. {countmut-0.3.1 → countmut-0.3.3}/backend/kstring.h +0 -0
  23. {countmut-0.3.1 → countmut-0.3.3}/countmut/__init__.py +0 -0
  24. {countmut-0.3.1 → countmut-0.3.3}/countmut/backend.py +0 -0
  25. {countmut-0.3.1 → countmut-0.3.3}/countmut/bam_tags.py +0 -0
  26. {countmut-0.3.1 → countmut-0.3.3}/countmut/cli.py +0 -0
  27. {countmut-0.3.1 → countmut-0.3.3}/countmut/core.py +0 -0
  28. {countmut-0.3.1 → countmut-0.3.3}/countmut/model.py +0 -0
  29. {countmut-0.3.1 → countmut-0.3.3}/countmut/utils.py +0 -0
  30. {countmut-0.3.1 → countmut-0.3.3}/countmut.egg-info/SOURCES.txt +0 -0
  31. {countmut-0.3.1 → countmut-0.3.3}/countmut.egg-info/dependency_links.txt +0 -0
  32. {countmut-0.3.1 → countmut-0.3.3}/countmut.egg-info/entry_points.txt +0 -0
  33. {countmut-0.3.1 → countmut-0.3.3}/countmut.egg-info/requires.txt +0 -0
  34. {countmut-0.3.1 → countmut-0.3.3}/countmut.egg-info/top_level.txt +0 -0
  35. {countmut-0.3.1 → countmut-0.3.3}/setup.cfg +0 -0
  36. {countmut-0.3.1 → countmut-0.3.3}/tests/test_cli.py +0 -0
  37. {countmut-0.3.1 → countmut-0.3.3}/tests/test_core.py +0 -0
  38. {countmut-0.3.1 → countmut-0.3.3}/tests/test_unified.py +0 -0
  39. {countmut-0.3.1 → countmut-0.3.3}/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.3
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
@@ -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
- uint8_t nt = bam_seqi(bam_get_seq(b), p->qpos);
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;
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
- uint8_t nt = bam_seqi(bam_get_seq(b), qpos);
766
- 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;
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
- uint8_t nt = bam_seqi(bam_get_seq(b), qpos);
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
- uint8_t nt = bam_seqi(bam_get_seq(b), qpos);
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],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: countmut
3
- Version: 0.3.1
3
+ Version: 0.3.3
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.3"
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,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; stored T -> reference-frame A.
84
- # qpos = pos - 4, so group 1 (qpos 2..6) = 0-based 6..10.
85
- mk("m_hi", 16, 4, "TTTTTTTTT", Q40, ok),
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, "TTTTTTTTT", Q40, [("NS", 0), ("Yf", 0), ("Zf", 0)]),
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, "TTTTTTTTT", Q40, [("NS", 2), ("Yf", 1), ("Zf", 0)]),
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