Skip to content

Commit ba50596

Browse files
authored
Merge pull request #9 from gofflab/fix/critical-correctness-bugs
Fix critical correctness bugs in complement table and tile overlap
2 parents 97c1b60 + 9ba4536 commit ba50596

6 files changed

Lines changed: 92 additions & 7 deletions

File tree

VERSIONINFO.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## v0.3.5 - 04.11.2026
2+
+ Fixed complement table bug: lowercase 'c' was incorrectly complemented to 't' instead of 'g'
3+
+ Fixed off-by-one in tile overlap detection that caused adjacent non-overlapping tiles to be
4+
incorrectly skipped during probe selection, reducing yield
5+
+ Removed duplicate `__len__` method in Tile class (shadowed definition had off-by-one)
16
## v0.3.4 - 04.10.2026
27
+ Fixed `designProbes`/`designProbesBatch` reading the species registry from the
38
package install directory instead of `~/.hcrprobedesign/HCRconfig.yaml`, which

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[metadata]
22
# replace with your username:
33
name = hcrprobedesign
4-
version = 0.3.4
4+
version = 0.3.5
55
author = Loyal A. Goff
66
author_email = loyalgoff@jhmi.edu
77
description = Probe Design tool for Hybridization Chain Reaction

src/HCRProbeDesign/sequencelib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def complement(s):
5353
:return: A list of the complement bases of the sequence.
5454
'''
5555
comp = {'A': 'T', 'C': 'G', 'G': 'C', 'T': 'A', 'N': 'N',
56-
'a': 't', 'c': 't', 'g': 'c', 't': 'a', 'n': 'n'
56+
'a': 't', 'c': 'g', 'g': 'c', 't': 'a', 'n': 'n'
5757
}
5858
complseq = [comp[base] for base in s]
5959
return complseq

src/HCRProbeDesign/tiles.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,9 @@ def __iter__(self):
6969
"""Iterate over the tile sequence bases."""
7070
return iter(self.sequence)
7171

72-
def __len__(self):
73-
"""Return the length of the tile interval."""
74-
return self.end-self.start+1
75-
7672
def overlaps(self,b):
7773
"""Return true if b overlaps self"""
78-
if (self.start <= b.start and b.start <=self.end) or (self.start >= b.start and self.start <= b.end):
74+
if (self.start < b.end and b.start < self.end):
7975
return True
8076
else:
8177
return False

tests/test_sequencelib.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Tests for sequencelib complement and reverse_complement functions."""
2+
3+
from HCRProbeDesign import sequencelib
4+
5+
6+
def test_complement_uppercase():
7+
assert "".join(sequencelib.complement("ACGT")) == "TGCA"
8+
9+
10+
def test_complement_lowercase():
11+
assert "".join(sequencelib.complement("acgt")) == "tgca"
12+
13+
14+
def test_complement_mixed_case():
15+
assert "".join(sequencelib.complement("AcGt")) == "TgCa"
16+
17+
18+
def test_complement_with_n():
19+
assert "".join(sequencelib.complement("ANn")) == "TNn"
20+
21+
22+
def test_reverse_complement_uppercase():
23+
assert sequencelib.reverse_complement("ACGT") == "ACGT" # palindrome
24+
assert sequencelib.reverse_complement("AACC") == "GGTT"
25+
26+
27+
def test_reverse_complement_lowercase():
28+
assert sequencelib.reverse_complement("acgt") == "acgt" # palindrome
29+
assert sequencelib.reverse_complement("aacc") == "ggtt"
30+
31+
32+
def test_reverse_complement_mixed_case():
33+
assert sequencelib.reverse_complement("AaCc") == "gGtT"
34+
35+
36+
def test_complement_c_to_g_not_t():
37+
"""Regression test: lowercase 'c' must complement to 'g', not 't'."""
38+
result = "".join(sequencelib.complement("c"))
39+
assert result == "g", f"complement('c') returned '{result}', expected 'g'"
40+
result = "".join(sequencelib.complement("C"))
41+
assert result == "G", f"complement('C') returned '{result}', expected 'G'"

tests/test_tiles.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Tests for Tile class overlap detection and __len__."""
2+
3+
from HCRProbeDesign.tiles import Tile
4+
5+
6+
def test_len_equals_sequence_length():
7+
tile = Tile(sequence="ACGT" * 13, seqName="test", startPos=1) # 52 bases
8+
assert len(tile) == 52
9+
10+
11+
def test_overlapping_tiles():
12+
t1 = Tile(sequence="A" * 52, seqName="test", startPos=1)
13+
t2 = Tile(sequence="A" * 52, seqName="test", startPos=26) # overlaps with t1
14+
assert t1.overlaps(t2)
15+
assert t2.overlaps(t1)
16+
17+
18+
def test_adjacent_tiles_do_not_overlap():
19+
"""Regression test: tiles at adjacent non-overlapping positions must not overlap."""
20+
t1 = Tile(sequence="A" * 52, seqName="test", startPos=1) # covers 1..52, end=53
21+
t2 = Tile(sequence="A" * 52, seqName="test", startPos=53) # covers 53..104
22+
assert not t1.overlaps(t2), "Adjacent tiles incorrectly flagged as overlapping"
23+
assert not t2.overlaps(t1), "Adjacent tiles incorrectly flagged as overlapping"
24+
25+
26+
def test_distant_tiles_do_not_overlap():
27+
t1 = Tile(sequence="A" * 52, seqName="test", startPos=1)
28+
t2 = Tile(sequence="A" * 52, seqName="test", startPos=100)
29+
assert not t1.overlaps(t2)
30+
assert not t2.overlaps(t1)
31+
32+
33+
def test_same_start_overlaps():
34+
t1 = Tile(sequence="A" * 52, seqName="test", startPos=10)
35+
t2 = Tile(sequence="A" * 52, seqName="test", startPos=10)
36+
assert t1.overlaps(t2)
37+
38+
39+
def test_one_base_overlap():
40+
t1 = Tile(sequence="A" * 52, seqName="test", startPos=1) # end=53
41+
t2 = Tile(sequence="A" * 52, seqName="test", startPos=52) # starts at 52, within t1
42+
assert t1.overlaps(t2)
43+
assert t2.overlaps(t1)

0 commit comments

Comments
 (0)