diff --git a/NEWS.rst b/NEWS.rst index 1b60918..fdc4fde 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,14 @@ Release Notes ================================================================================ +Version 0.7.9dev +------------------------------------------------------------------------------- + +MaskPrimers: + ++ Fixed a bug in MaskPrimers align involving the reverse primer position offset +calculation. + Version 0.7.8: January 28, 2026 ------------------------------------------------------------------------------- diff --git a/presto/Sequence.py b/presto/Sequence.py index 995b616..8c33c2d 100644 --- a/presto/Sequence.py +++ b/presto/Sequence.py @@ -903,7 +903,7 @@ def localAlignment(seq_record, primers, primers_regex=None, max_error=default_as align.end = align_coord[0][-1] else: # Count position from tail and end gaps - rev_pos = rec_len - align_coord[0][-1] + rev_pos = rec_len - max_len if rec_len > max_len else 0 align.start = rev_pos + align_coord[0][0] align.end = rev_pos + align_coord[0][-1] diff --git a/presto/Version.py b/presto/Version.py index cd8ca6f..8c4de7f 100644 --- a/presto/Version.py +++ b/presto/Version.py @@ -5,5 +5,5 @@ __author__ = 'Jason Anthony Vander Heiden' __copyright__ = 'Copyright 2026 Kleinstein Lab, Yale University. All rights reserved.' __license__ = 'GNU Affero General Public License 3 (AGPL-3)' -__version__ = '0.7.8' +__version__ = '0.7.9dev' __date__ = '2026.01.28' diff --git a/tests/test_MaskPrimers.py b/tests/test_MaskPrimers.py index 85e2512..e6d9f66 100644 --- a/tests/test_MaskPrimers.py +++ b/tests/test_MaskPrimers.py @@ -200,6 +200,45 @@ def test_localAlignment(self): self.assertListEqual([(x, round(y, 4)) for x, y in self.align_indel], [(x.primer, round(x.error, 4)) for x in align]) + def test_issue110_revprimer_cut(self): + """Test reverse primer alignment with maxlen parameter (issue #110)""" + # Test case from issue #110: reverse primer alignment + # This is equivalent to: + # MaskPrimers.py align --mode cut --revpr --skiprc --maxerror 0.2 --maxlen 800 + + # Input sequence + seq = Seq('ATGTCAGTTAGCTCGTTCAGGTAATAGTTGCCCACACAACGTCAAAATAAGAGAACGGTCGTAACATTATCCGTGATTTTCTCACTACTATCAGTACTCACGACTCGACTCTGCCGCAGCCACGTATCGCCTGAAAGCCAGTCAGCGTTAAGGAGTGCTCTGAGCAGGACAACTCGCGTAGTGAGAGTTACATGTTCGTTGGGCTCTTCCGACACGGACCTGAGTTGGCCAACGTCCCACCTGAGGTCTGTGCCCCGGTGATGAGAAGTGTGCATCTCGTTCTTGCAGCTCGTCAGTACTTTCAGAATCATGGCGTGCATGGTAGAATGACCCTTATAACGGACTTCGACATGGCAATAACCCCCCGTTTCTACTTCTAGAGGAGAAAAGTATTGACATGAGCGCTCCCGGCACAAGGGCCAAAGAAGTCTCCAATTTCTTATTTCCGAATGACATGCGTCTCCTTGCGGGTAAATCACCGACCGCAATTCATAGAAGCCTGGGGGAACAGATAGGTCTAATTAGCTTAAGAGAGTAAATCCTGGGATCATCGATACTAGTTGTTTTATATTTGTTGTAAAAAGTAGTAATTACTTCAGTAGTAACCATAAACTTACGCTGGGGCTTCTTCGGCGGATTTTTACAGTTACCAACCAGGAGATTTGAAGTAAATCAGTTGAGGATTTAGCCGCGCTATCCGGTAATCTCCAAATTAAAACATACCGTTCCATGAAGGCTAGAATTAC') + record = SeqRecord(seq, id='sample4') + + # PGK1 reverse complement primer - looking for it at the end of the sequence + primers = OrderedDict([('PGK1', 'CGATACTAGTTGTTTTATATTTGTTGTAAAAAGTAGATAATTACT')]) + + # Test with maxlen parameter, rev_primer=True, skip_rc=True + score_dict = getDNAScoreDict(mask_score=(0, 1), gap_score=(0, 0)) + result = localAlignment(record, primers, max_error=0.2, max_len=800, + rev_primer=True, skip_rc=True, score_dict=score_dict) + + print('\nTEST issue #110 reverse primer>') + print(' ID> %s' % result.seq.id) + print(' PRIMER> %s' % result.primer) + print(' REV_PRIM> %s' % result.rev_primer) + print(' PRSTART> %d' % result.start) + print(' ERROR> %.4f' % result.error) + + # Verify the alignment found the reverse primer + # Position should be 551 (1-based, as shown in the log output) + self.assertEqual(result.primer, 'PGK1') + self.assertTrue(result.rev_primer) + self.assertEqual(result.start, 551) + self.assertAlmostEqual(result.error, 0.0444, places=3) + self.assertTrue(result.valid) + + # Test the cut operation - verify the sequence is trimmed correctly + cut_seq = maskSeq(result, mode='cut', barcode=False) + expected_cut = 'ATGTCAGTTAGCTCGTTCAGGTAATAGTTGCCCACACAACGTCAAAATAAGAGAACGGTCGTAACATTATCCGTGATTTTCTCACTACTATCAGTACTCACGACTCGACTCTGCCGCAGCCACGTATCGCCTGAAAGCCAGTCAGCGTTAAGGAGTGCTCTGAGCAGGACAACTCGCGTAGTGAGAGTTACATGTTCGTTGGGCTCTTCCGACACGGACCTGAGTTGGCCAACGTCCCACCTGAGGTCTGTGCCCCGGTGATGAGAAGTGTGCATCTCGTTCTTGCAGCTCGTCAGTACTTTCAGAATCATGGCGTGCATGGTAGAATGACCCTTATAACGGACTTCGACATGGCAATAACCCCCCGTTTCTACTTCTAGAGGAGAAAAGTATTGACATGAGCGCTCCCGGCACAAGGGCCAAAGAAGTCTCCAATTTCTTATTTCCGAATGACATGCGTCTCCTTGCGGGTAAATCACCGACCGCAATTCATAGAAGCCTGGGGGAACAGATAGGTCTAATTAGCTTAAGAGAGTAAATCCTGGGATCAT' + print(' CUT_SEQ> %s' % cut_seq.seq) + self.assertEqual(str(cut_seq.seq), expected_cut) + #@unittest.skip('-> maskSeq() skipped\n') def test_maskSeq(self): print('TEST CUT>')