Repository: acorg/dark-matter

Issue:
DIAMOND output has qstart > qend when frame < 0
When DIAMOND blastx finds a match in a reverse complemented read, the qstart and qend numbers it emits are decreasing. I.e., they are indices into the read from its end, not its beginning. This is a difference from BLAST, which always has qstart < qend. Our DIAMOND hsp.py normalization code needs to take this into account.


Discussion:

Patch:
diff --git a/dark/blast/hsp.py b/dark/blast/hsp.py
index 70087d8..1c264d6 100644
--- a/dark/blast/hsp.py
+++ b/dark/blast/hsp.py
@@ -6,12 +6,11 @@ def printHSP(hsp, indent=''):
 
 def normalizeHSP(hsp, readLen, blastApplication):
     """
-    Examine the sense of an HSP and return information about where the
-    read and the alignment (match) begin and end.  Return a dict with keys
-    that allow the read and the alignment to be displayed relative to the
-    hit orientation (i.e., with start < stop for both the read and the
-    match). The returned read indices are offsets into the hit. I.e.,
-    they indicate where on the hit the read lies.
+    Examine an HSP and return information about where the query and subject
+    match begins and ends.  Return a dict with keys that allow the query to
+    be displayed against the subject. The returned readStartInSubject and
+    readEndInSubject indices are offsets into the subject. I.e., they
+    indicate where in the subject the query falls.
 
     In the returned object, all indices are suitable for Python string
     slicing etc.  We must be careful to convert from the 1-based offsets
@@ -120,6 +119,16 @@ def normalizeHSP(hsp, readLen, blastApplication):
     subjectLength = subjectEnd - subjectStart
     readLength = readEndInSubject - readStartInSubject
 
+    # NOTE: readLength (above) is a really bad name. It's actually going to
+    # hold the length of the match in the query. I don't know why
+    # readEndInSubject - readStartInSubject is used (I mean why those two
+    # variables are not named readEnd and readStart). Maybe someone made a
+    # find and replace editing error which changed their names. Anyway, the
+    # readLength variable is confusingly named because this function is
+    # passed a 'readLen' argument, which does happen to be the full length
+    # of the read.  This should be cleaned up. See ../diamond/hsp.py for
+    # something cleaner.
+
     hitGaps = hsp['sbjct'].count('-')
     readGaps = hsp['query'].count('-')
 
diff --git a/dark/consensus.py b/dark/consensus.py
index 41817f7..bde8a9b 100644
--- a/dark/consensus.py
+++ b/dark/consensus.py
@@ -1,6 +1,7 @@
 from time import time
 from collections import defaultdict
 
+# TODO: Some of these imported functions are no longer in utils.py!
 from dark.utils import findHits, getSequence, summarizeHits, printHSP, report
 
 
diff --git a/dark/diamond/hsp.py b/dark/diamond/hsp.py
index e5559af..fd49976 100644
--- a/dark/diamond/hsp.py
+++ b/dark/diamond/hsp.py
@@ -1,150 +1,200 @@
-def printHSP(hsp, indent=''):
+from __future__ import division, print_function
+import sys
+
+
+def _debugPrint(hsp, queryLen, localDict, msg=''):
+    """
+    Print debugging information showing the local variables used during
+    a call to normalizeHSP and the hsp and then raise an C{AssertionError}.
+
+    @param hsp: The HSP C{dict} passed to normalizeHSP.
+    @param queryLen: the length of the query sequence.
+    @param localDict: A C{dict} of local variables (as produced by locals()).
+    @param msg: A C{str} message to raise C{AssertionError} with.
+    @raise AssertionError: unconditionally.
+    """
+    print('normalizeHSP error:', file=sys.stderr)
+    print('  queryLen: %d' % queryLen, file=sys.stderr)
+
+    print('  Original HSP:', file=sys.stderr)
     for attr in ['bits', 'expect', 'frame', 'query_end', 'query_start',
                  'sbjct', 'query', 'sbjct_end', 'sbjct_start']:
-        print('%s%s: %s' % (indent, attr, hsp[attr]))
+        print('    %s: %s' % (attr, hsp[attr]), file=sys.stderr)
 
+    print('  Local variables:', file=sys.stderr)
+    for var in sorted(localDict):
+        if var != 'hsp':
+            print('    %s: %s' % (var, localDict[var]), file=sys.stderr)
 
-def normalizeHSP(hsp, readLen, diamondTask):
+    raise AssertionError(msg)
+
+
+def _sanityCheck(subjectStart, subjectEnd, queryStart, queryEnd,
+                 queryStartInSubject, queryEndInSubject, hsp, queryLen,
+                 subjectGaps, queryGaps, localDict):
+    """
+    Perform some sanity checks on an HSP. Call _debugPrint on any error.
+
+    @param subjectStart: The 0-based C{int} start offset of the match in the
+        subject.
+    @param subjectEnd: The 0-based C{int} end offset of the match in the
+        subject.
+    @param queryStart: The 0-based C{int} start offset of the match in the
+        query.
+    @param queryEnd: The 0-based C{int} end offset of the match in the query.
+    @param queryStartInSubject: The 0-based C{int} offset of where the query
+        starts in the subject.
+    @param queryEndInSubject: The 0-based C{int} offset of where the query
+        ends in the subject.
+    @param hsp: The HSP C{dict} passed to normalizeHSP.
+    @param queryLen: the C{int} length of the query sequence.
+    @param subjectGaps: the C{int} number of gaps in the subject.
+    @param queryGaps: the C{int} number of gaps in the query.
+    @param localDict: A C{dict} of local variables from our caller (as
+        produced by locals()).
     """
-    Examine the sense of an HSP and return information about where the
-    read and the alignment (match) begin and end.  Return a dict with keys
-    that allow the read and the alignment to be displayed relative to the
-    hit orientation (i.e., with start < stop for both the read and the
-    match). The returned read indices are offsets into the hit. I.e.,
-    they indicate where on the hit the read lies.
+    # Subject indices must always be ascending.
+    if subjectStart >= subjectEnd:
+        _debugPrint(hsp, queryLen, localDict, 'subjectStart >= subjectEnd')
+
+    subjectMatchLength = subjectEnd - subjectStart
+    queryMatchLength = queryEnd - queryStart
+
+    # Sanity check that the length of the matches in the subject and query
+    # are identical, taking into account gaps in either (indicated by '-'
+    # characters in the match sequences, as returned by DIAMOND).
+    subjectMatchLengthWithGaps = subjectMatchLength + subjectGaps
+    queryMatchLengthWithGaps = queryMatchLength + queryGaps
+    if subjectMatchLengthWithGaps != queryMatchLengthWithGaps:
+        _debugPrint(hsp, queryLen, localDict,
+                    'Including gaps, subject match length (%d) != Query match '
+                    'length (%d)' % (subjectMatchLengthWithGaps,
+                                     queryMatchLengthWithGaps))
+
+    if queryStartInSubject > subjectStart:
+        _debugPrint(hsp, queryLen, localDict,
+                    'queryStartInSubject (%d) > subjectStart (%d)' %
+                    (queryStartInSubject, subjectStart))
+    if queryEndInSubject < subjectEnd:
+        _debugPrint(hsp, queryLen, localDict,
+                    'queryEndInSubject (%d) < subjectEnd (%d)' %
+                    (queryEndInSubject, subjectEnd))
+
+
+def normalizeHSP(hsp, queryLen, diamondTask):
+    """
+    Examine an HSP and return information about where the query and subject
+    match begins and ends.  Return a dict with keys that allow the query to
+    be displayed against the subject. The returned readStartInSubject and
+    readEndInSubject indices are offsets into the subject. I.e., they
+    indicate where in the subject the query falls.
 
     In the returned object, all indices are suitable for Python string
     slicing etc.  We must be careful to convert from the 1-based offsets
     found in DIAMOND output properly.
 
-    hsp['frame'] is a (query, subject) 2-tuple, with both values coming from
-    {-3, -2, -1, 1, 2, 3}. The sign indicates negative or positive sense
-    (i.e., the direction of reading through the query or subject to get the
-    alignment). The value is the nucleotide match offset modulo 3, plus one
-    (i.e., it tells us which of the 3 possible reading frames is used in
-    the match). The value is redundant because that information could also
-    be obtained from the mod 3 value of the match offset.
+    hsp['frame'] is a value from {-3, -2, -1, 1, 2, 3}. The sign indicates
+    negative or positive sense (i.e., the direction of reading through the
+    query to get the alignment). The frame value is the nucleotide match offset
+    modulo 3, plus one (i.e., it tells us which of the 3 possible query reading
+    frames was used in the match).
 
     NOTE: the returned readStartInSubject value may be negative.  We consider
-    the hit sequence to start at offset 0.  So if the read string has
+    the subject sequence to start at offset 0.  So if the query string has
     sufficient additional nucleotides before the start of the alignment
-    match, it may protrude to the left of the hit. Similarly, the returned
+    match, it may protrude to the left of the subject. Similarly, the returned
     readEndInSubject can be greater than the subjectEnd.
 
     @param hsp: an HSP in the form of a C{dict}, built from a DIAMOND record.
-        All passed hsp offsets are 1-based.
-    @param readLen: the length of the read sequence.
-    @param diamondTask: The C{str} command line program that was
-        run (e.g., 'blastp', 'blastx').
-
+        All passed offsets are 1-based.
+    @param queryLen: the length of the query sequence.
+    @param diamondTask: The C{str} command-line matching algorithm that was
+        run (either 'blastx' or 'blastp').
+    @return: A C{dict} with C{str} keys and C{int} offset values. Keys are
+            readStart
+            readEnd
+            readStartInSubject
+            readEndInSubject
+            subjectStart
+            subjectEnd
+        The returned offset values are all zero-based.
     """
 
-    def debugPrint(locals, msg=None):
-        """
-        Print debugging information showing the local variables from
-        a call to normalizeHSP and then raise an C{AssertionError}.
-
-        @param locals: A C{dict} of local variables.
-        @param msg: A C{str} message to raise C{AssertionError} with.
-        """
-        print('normalizeHSP error:')
-        print('  readLen: %d' % readLen)
-        for var in sorted(locals.keys()):
-            if var in ('debugPrint', 'hsp'):
-                continue
-            print('  %s: %s' % (var, locals[var]))
-        print('  Original HSP:')
-        printHSP(hsp, '    ')
-        if msg:
-            raise AssertionError(msg)
+    # TODO: DIAMOND does not show gaps yet. When they start doing that, the
+    # following might have to be changed (or we might have to ask it to
+    # output attributes with different names).
+    subjectGaps = hsp['sbjct'].count('-')
+    queryGaps = hsp['query'].count('-')
+
+    # Make some variables using Python's standard string indexing (start
+    # offset included, end offset not). No calculations in this function
+    # are done with the original 1-based HSP variables.
+    queryStart = hsp['query_start'] - 1
+    queryEnd = hsp['query_end']
+    subjectStart = hsp['sbjct_start'] - 1
+    subjectEnd = hsp['sbjct_end']
+
+    queryReversed = hsp['frame'] < 0
+
+    # Query offsets must be ascending, unless we're looking at blastx output
+    # and the query was reversed for the match.
+    if queryStart >= queryEnd:
+        if diamondTask == 'blastx' and queryReversed:
+            # Compute new query start and end indices, based on their
+            # distance from the end of the string.
+            #
+            # Above we took one off the start index, so we need to undo
+            # that (because the start is actually the end). We didn't take
+            # one off the end index, and need to do that now (because the
+            # end is actually the start).
+            queryStart = queryLen - (queryStart + 1)
+            queryEnd = queryLen - (queryEnd - 1)
         else:
-            raise AssertionError()
-
-    readPositive = hsp['frame'] > 0
-
-    # The following variable names with underscores match the names of
-    # attributes BioPython uses and the values (1-based) match those
-    # reported by DIAMOND.
-    read_start = hsp['query_start']
-    read_end = hsp['query_end']
-    sbjct_start = hsp['sbjct_start']
-    sbjct_end = hsp['sbjct_end']
-
-    # Read offsets should be ascending.
-    if read_start > read_end:
-        debugPrint(locals(),
-                   'Assertion "read_start <= read_end" failed. Read '
-                   'positive is %s. read_start = %d, read_end = %d' %
-                   (readPositive, read_start, read_end))
-
-    # Make sure subject indices are ascending.
-    if sbjct_start > sbjct_end:
-        debugPrint(locals())
-
-    # Now that we have asserted what we can about the original HSP values
-    # and gotten them into ascending order, make some sane 0-based offsets.
-    readStartInSubject = read_start - 1
-    readEndInSubject = read_end
-    subjectStart = sbjct_start - 1
-    subjectEnd = sbjct_end
+            _debugPrint(hsp, queryLen, locals(), 'queryStart >= queryEnd')
 
     if diamondTask == 'blastx':
-        # In Blastx output, hit offsets are based on protein sequence
-        # length but queries (and the reported offsets) are nucleotide.
-        # Convert the read offsets to protein because we will plot against
-        # the hit (protein).
+        # In DIAMOND blastx output, subject offsets are based on protein
+        # sequence length but queries (and the reported offsets) are
+        # nucleotide.  Convert the query offsets to protein because we will
+        # plot against the subject (protein).
         #
-        # Note that readStartInSubject and readEndInSubject may not be 0 mod
-        # 3. They are offsets into the read string giving the position of
-        # the AA, which depends on the translation frame.
-        readStartInSubject = int(readStartInSubject / 3)
-        readEndInSubject = int(readEndInSubject / 3)
-
-    # No operations on original 1-based HSP variables (with underscores)
-    # should appear beyond this point.
+        # Convert queryLen and the query nucleotide start and end offsets
+        # to be valid for the query after translation to AAs. When
+        # translating, DIAMOND may ignore some nucleotides at the start
+        # and/or the end of the original DNA query. At the start this is
+        # due to the frame in use, and at the end it is due to always using
+        # three nucleotides at a time to form codons.
+        #
+        # So, for example, a query of 6 nucleotides that is translated in
+        # frame 2 (i.e., the translation starts from the second nucleotide)
+        # will have length 1 as an AA sequence. The first nucleotide is
+        # ignored due to the frame and the last two due to there not being
+        # enough final nucleotides to make another codon.
+        #
+        # In the following, the subtraction accounts for the first form of
+        # loss and the integer division for the second.
+        initiallyIgnored = abs(hsp['frame']) - 1
+        queryLen = (queryLen - initiallyIgnored) // 3
+        queryStart = (queryStart - initiallyIgnored) // 3
+        queryEnd = (queryEnd - initiallyIgnored) // 3
 
-    subjectLength = subjectEnd - subjectStart
-    readLength = readEndInSubject - readStartInSubject
+    # unmatchedQueryLeft is the number of query bases that will extend
+    # to the left of the start of the subject in our plots.
+    unmatchedQueryLeft = queryStart
 
-    # TODO: DIAMOND output check.
-    hitGaps = hsp['sbjct'].count('-')
-    readGaps = hsp['query'].count('-')
+    # Set the query offsets into the subject.
+    queryStartInSubject = subjectStart - unmatchedQueryLeft
+    queryEndInSubject = queryStartInSubject + queryLen + queryGaps
 
-    # Sanity check that the length of the matches in the hit and read
-    # are identical, taking into account gaps in either (indicated by '-'
-    # characters in the match sequences, as returned by DIAMOND).
-    subjectLengthWithGaps = subjectLength + hitGaps
-    readLengthWithGaps = readLength + readGaps
-    if subjectLengthWithGaps != readLengthWithGaps:
-        debugPrint(locals(),
-                   'Including gaps, hit match length (%d) != Read match '
-                   'length (%d)' % (subjectLengthWithGaps,
-                                    readLengthWithGaps))
-
-    # Calculate read indices. These are indices relative to the hit!
-
-    # unmatchedReadLeft is the number of read bases that will be sticking
-    # out to the left of the start of the subject in our plots.
-    if readPositive:
-        unmatchedReadLeft = readStartInSubject
-    else:
-        unmatchedReadLeft = readLen - readEndInSubject
-
-    # Set the read offsets.
-    readStartInSubject = subjectStart - unmatchedReadLeft
-    readEndInSubject = readStartInSubject + readLen + readGaps
-
-    # Final sanity checks.
-    if readStartInSubject > subjectStart:
-        debugPrint(locals(), 'readStartInSubject > subjectStart')
-    if readEndInSubject < subjectEnd:
-        debugPrint(locals(), 'readEndInSubject < subjectEnd')
+    _sanityCheck(subjectStart, subjectEnd, queryStart, queryEnd,
+                 queryStartInSubject, queryEndInSubject, hsp, queryLen,
+                 subjectGaps, queryGaps, locals())
 
     return {
-        'readStart': read_start - 1,
-        'readEnd': read_end,
-        'readStartInSubject': readStartInSubject,
-        'readEndInSubject': readEndInSubject,
+        'readStart': queryStart,
+        'readEnd': queryEnd,
+        'readStartInSubject': queryStartInSubject,
+        'readEndInSubject': queryEndInSubject,
         'subjectStart': subjectStart,
         'subjectEnd': subjectEnd,
     }
diff --git a/dark/hsp.py b/dark/hsp.py
index 9140466..6a3a019 100644
--- a/dark/hsp.py
+++ b/dark/hsp.py
@@ -91,7 +91,7 @@ class _Base(object):
 class HSP(_Base):
     """
     Holds information about a high-scoring pair from a read alignment.
-    Comparisons are done as for BLAST bit scores (higher is better).
+    Comparisons are done as for BLAST or DIAMOND bit scores (higher is better).
 
     @param score: The numeric score of this HSP.
     """
@@ -104,7 +104,7 @@ class HSP(_Base):
 class LSP(_Base):
     """
     Holds information about a low-scoring pair from a read alignment.
-    Comparisons are done as for BLAST e-values (smaller is better).
+    Comparisons are done as for BLAST or DIAMOND e-values (smaller is better).
 
     @param score: The numeric score of this LSP.
     """
diff --git a/setup.py b/setup.py
index b884bd0..42efba6 100644
--- a/setup.py
+++ b/setup.py
@@ -50,7 +50,7 @@ scripts = [
 ]
 
 setup(name='dark-matter',
-      version='1.0.50',
+      version='1.0.51',
       packages=['dark', 'dark.blast', 'dark.diamond'],
       include_package_data=True,
       url='https://github.com/acorg/dark-matter',


Test patch:
diff --git a/test/diamond/test_hsp.py b/test/diamond/test_hsp.py
index 525a8a3..e479aa8 100644
--- a/test/diamond/test_hsp.py
+++ b/test/diamond/test_hsp.py
@@ -3,153 +3,935 @@ from unittest import TestCase
 from dark.diamond.hsp import normalizeHSP
 
 
-class Frame(object):
-    def __init__(self, read):
-        self.read = read
-
-
 class FakeHSP(dict):
-    def __init__(self, subjectStart, subjectEnd, readStart, readEnd, frame,
-                 hit='', read=''):
+    def __init__(self, subjectStart, subjectEnd, queryStart, queryEnd, frame,
+                 subject='', query='', bitscore=None, evalue=None):
         """
-        A fake HSP class (with 1-based offsets, as are used in DIAMOND).
+        A fake HSP class with 1-based offsets and key names as used by DIAMOND.
         """
+        if frame > 0:
+            if not queryStart < queryEnd:
+                raise ValueError('queryStart (%d) not less than queryEnd '
+                                 '(%d) when frame (%d) is positive.' %
+                                 (queryStart, queryEnd, frame))
+        else:
+            if not queryStart > queryEnd:
+                raise ValueError('queryStart (%d) not greater than queryEnd '
+                                 '(%d) when frame (%d) is negative.' %
+                                 (queryStart, queryEnd, frame))
+
         self['sbjct_start'] = subjectStart
         self['sbjct_end'] = subjectEnd
-        self['query_start'] = readStart
-        self['query_end'] = readEnd
-        self['frame'] = frame.read
-        self['sbjct'] = hit
-        self['query'] = read
-
-        # In case you're thinking of adding it, the following assertion is
-        # not valid:
-        #
-        #   assert abs(subjectEnd - subjectStart) == abs(readEnd - readStart)
-        #
-        # That's because DIAMOND might find a match that requires a gap in
-        # the read or in the hit. The indices that it reports do not
-        # include the gap and so the differences in the lengths of the
-        # sections of the read and hit may not be the same.
-
-
-class Old_ReadPositiveHitPositive(TestCase):
+        self['query_start'] = queryStart
+        self['query_end'] = queryEnd
+        self['frame'] = frame
+        self['sbjct'] = subject
+        self['query'] = query
+        self['bits'] = bitscore
+        self['expect'] = evalue
+
+
+class TestBlastxFramePlus1(TestCase):
     """
-    Tests for normalizeHSP when the hit start is less than the hit end.
+    Tests for normalizeHSP for DIAMOND blastx output when frame=1 (i.e., the
+    query matches in the order it was given to DIAMOND, and the translation
+    frame starts at the first nucleotide.
     """
 
-    frame = Frame(read=1)
+    # All query offsets and lengths must be in terms of nucleotides.
+    # Subject offsets are in terms of protein AA sequences.  This is how
+    # DIAMOND reports those offsets.
+    #
+    # In the little diagrams in the docstrings below, the first line is the
+    # subject and the second the query. Dots indicate where the matched
+    # region is. The queries are shown translated so as to line up properly
+    # with the subjects.
 
     def testIdentical(self):
         """
-        The hit start and end are identical to those of the read.
+        The subject start and end are identical to those of the translated
+        query.
 
-             ssss
-             qqqq
+             ....
+             ....
         """
-        hsp = FakeHSP(subjectStart=1, subjectEnd=4, readStart=3, readEnd=12,
-                      frame=self.frame)
-        normalized = normalizeHSP(hsp, 4, 'blastx')
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=1, queryEnd=12,
+                      frame=1)
+        normalized = normalizeHSP(hsp, 12, 'blastx')
         self.assertEqual({
             'subjectStart': 0,
             'subjectEnd': 4,
-            'readStart': 2,
-            'readEnd': 12,
+            'readStart': 0,
+            'readEnd': 4,
             'readStartInSubject': 0,
             'readEndInSubject': 4,
         }, normalized)
 
-    def testHitExtendsLeft(self):
+    def testSubjectExtendsLeft(self):
         """
-        The hit overlaps the read to the left.
+        The subject overlaps the translated query to the left.
 
-              ssssss
-                qqqq
+              ss....
+                ....
         """
-        hsp = FakeHSP(subjectStart=3, subjectEnd=6, readStart=3, readEnd=12,
-                      frame=self.frame)
-        normalized = normalizeHSP(hsp, 4, 'blastx')
+        hsp = FakeHSP(subjectStart=3, subjectEnd=6, queryStart=1, queryEnd=12,
+                      frame=1)
+        normalized = normalizeHSP(hsp, 12, 'blastx')
         self.assertEqual({
             'subjectStart': 2,
             'subjectEnd': 6,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 2,
+            'readEndInSubject': 6,
+        }, normalized)
+
+    def testQueryExtendsLeft(self):
+        """
+        The translated query extends to the left of the subject.
+
+               ....
+             qq....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=7, queryEnd=18,
+                      frame=1)
+        normalized = normalizeHSP(hsp, 18, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
             'readStart': 2,
-            'readEnd': 12,
+            'readEnd': 6,
+            'readStartInSubject': -2,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testSubjectExtendsRight(self):
+        """
+        The subject extends to the right of the translated query.
+
+               ....ss
+               ....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=1, queryEnd=12,
+                      frame=1)
+        normalized = normalizeHSP(hsp, 12, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testQueryExtendsRight(self):
+        """
+        The translated query extends to the right of the subject.
+
+               ....
+               ....qq
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=1, queryEnd=12,
+                      frame=1)
+        normalized = normalizeHSP(hsp, 18, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 6,
+        }, normalized)
+
+    def testQueryExtendsRightAndLeft(self):
+        """
+        The translated query extends to the right and left of the subject.
+
+                ....
+              qq....q
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=7, queryEnd=18,
+                      frame=1)
+        normalized = normalizeHSP(hsp, 21, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 2,
+            'readEnd': 6,
+            'readStartInSubject': -2,
+            'readEndInSubject': 5,
+        }, normalized)
+
+    def testSubjectExtendsRightAndLeft(self):
+        """
+        The subject extends to the right and left of the translated query.
+
+               s...sss
+                ...q
+        """
+        hsp = FakeHSP(subjectStart=2, subjectEnd=5, queryStart=3, queryEnd=12,
+                      frame=1)
+        normalized = normalizeHSP(hsp, 12, 'blastx')
+        self.assertEqual({
+            'subjectStart': 1,
+            'subjectEnd': 5,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 1,
+            'readEndInSubject': 5,
+        }, normalized)
+
+
+class TestBlastxFramePlus2(TestCase):
+    """
+    Tests for normalizeHSP for DIAMOND blastx output when frame=2 (i.e., the
+    query matches in the order it was given to DIAMOND, and the translation
+    frame starts at the first nucleotide.
+    """
+
+    # All query offsets and lengths must be in terms of nucleotides.
+    # Subject offsets are in terms of protein AA sequences.  This is how
+    # DIAMOND reports those offsets.
+    #
+    # In the little diagrams in the docstrings below, the first line is the
+    # subject and the second the query. Dots indicate where the matched
+    # region is. The queries are shown translated so as to line up properly
+    # with the subjects.
+
+    def testIdentical(self):
+        """
+        The subject start and end are identical to those of the translated
+        query.
+
+             ....
+             ....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=2, queryEnd=13,
+                      frame=2)
+        normalized = normalizeHSP(hsp, 13, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testSubjectExtendsLeft(self):
+        """
+        The subject overlaps the translated query to the left.
+
+              ss....
+                ....
+        """
+        hsp = FakeHSP(subjectStart=3, subjectEnd=6, queryStart=2, queryEnd=13,
+                      frame=2)
+        normalized = normalizeHSP(hsp, 13, 'blastx')
+        self.assertEqual({
+            'subjectStart': 2,
+            'subjectEnd': 6,
+            'readStart': 0,
+            'readEnd': 4,
             'readStartInSubject': 2,
             'readEndInSubject': 6,
         }, normalized)
 
-    def testReadExtendsLeft(self):
+    def testQueryExtendsLeft(self):
         """
-        The read sticks out to the left of the hit.
+        The translated query extends to the left of the subject.
 
-               ssss
-             qqqqqq
+               ....
+             qq....
         """
-        hsp = FakeHSP(subjectStart=1, subjectEnd=4, readStart=9, readEnd=18,
-                      frame=self.frame)
-        normalized = normalizeHSP(hsp, 6, 'blastx')
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=8, queryEnd=19,
+                      frame=2)
+        normalized = normalizeHSP(hsp, 19, 'blastx')
         self.assertEqual({
             'subjectStart': 0,
             'subjectEnd': 4,
-            'readStart': 8,
-            'readEnd': 18,
+            'readStart': 2,
+            'readEnd': 6,
             'readStartInSubject': -2,
             'readEndInSubject': 4,
         }, normalized)
 
-    def testReadExtendsRight(self):
+    def testSubjectExtendsRight(self):
         """
-        The read sticks out to the right of the hit.
+        The subject extends to the right of the translated query.
 
-               ssss
-               qqqqqq
+               ....ss
+               ....
         """
-        hsp = FakeHSP(subjectStart=1, subjectEnd=4, readStart=3, readEnd=12,
-                      frame=self.frame)
-        normalized = normalizeHSP(hsp, 6, 'blastx')
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=2, queryEnd=13,
+                      frame=2)
+        normalized = normalizeHSP(hsp, 13, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testQueryExtendsRight(self):
+        """
+        The translated query extends to the right of the subject.
+
+               ....
+               ....qq
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=2, queryEnd=13,
+                      frame=2)
+        normalized = normalizeHSP(hsp, 19, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 6,
+        }, normalized)
+
+    def testQueryExtendsRightAndLeft(self):
+        """
+        The translated query extends to the right and left of the subject.
+
+                ....
+              qq....q
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=8, queryEnd=19,
+                      frame=2)
+        normalized = normalizeHSP(hsp, 22, 'blastx')
         self.assertEqual({
             'subjectStart': 0,
             'subjectEnd': 4,
             'readStart': 2,
-            'readEnd': 12,
+            'readEnd': 6,
+            'readStartInSubject': -2,
+            'readEndInSubject': 5,
+        }, normalized)
+
+    def testSubjectExtendsRightAndLeft(self):
+        """
+        The subject extends to the right and left of the translated query.
+
+               s...sss
+                ...q
+        """
+        hsp = FakeHSP(subjectStart=2, subjectEnd=5, queryStart=4, queryEnd=13,
+                      frame=2)
+        normalized = normalizeHSP(hsp, 13, 'blastx')
+        self.assertEqual({
+            'subjectStart': 1,
+            'subjectEnd': 5,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 1,
+            'readEndInSubject': 5,
+        }, normalized)
+
+
+class TestBlastxFramePlus3(TestCase):
+    """
+    Tests for normalizeHSP for DIAMOND blastx output when frame=3 (i.e., the
+    query matches in the order it was given to DIAMOND, and the translation
+    frame starts at the first nucleotide.
+    """
+
+    # All query offsets and lengths must be in terms of nucleotides.
+    # Subject offsets are in terms of protein AA sequences.  This is how
+    # DIAMOND reports those offsets.
+    #
+    # In the little diagrams in the docstrings below, the first line is the
+    # subject and the second the query. Dots indicate where the matched
+    # region is. The queries are shown translated so as to line up properly
+    # with the subjects.
+
+    def testIdentical(self):
+        """
+        The subject start and end are identical to those of the translated
+        query.
+
+             ....
+             ....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=3, queryEnd=14,
+                      frame=3)
+        normalized = normalizeHSP(hsp, 14, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
             'readStartInSubject': 0,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testSubjectExtendsLeft(self):
+        """
+        The subject overlaps the translated query to the left.
+
+              ss....
+                ....
+        """
+        hsp = FakeHSP(subjectStart=3, subjectEnd=6, queryStart=3, queryEnd=14,
+                      frame=3)
+        normalized = normalizeHSP(hsp, 14, 'blastx')
+        self.assertEqual({
+            'subjectStart': 2,
+            'subjectEnd': 6,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 2,
             'readEndInSubject': 6,
         }, normalized)
 
-    def testReadExtendsRightAndLeft(self):
+    def testQueryExtendsLeft(self):
         """
-        The read extends to the right and left of the hit.
+        The translated query extends to the left of the subject.
 
-                ssss
-               qqqqqq
+               ....
+             qq....
         """
-        hsp = FakeHSP(subjectStart=1, subjectEnd=4, readStart=6, readEnd=15,
-                      frame=self.frame)
-        normalized = normalizeHSP(hsp, 6, 'blastx')
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=9, queryEnd=20,
+                      frame=3)
+        normalized = normalizeHSP(hsp, 20, 'blastx')
         self.assertEqual({
             'subjectStart': 0,
             'subjectEnd': 4,
-            'readStart': 5,
-            'readEnd': 15,
-            'readStartInSubject': -1,
+            'readStart': 2,
+            'readEnd': 6,
+            'readStartInSubject': -2,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testSubjectExtendsRight(self):
+        """
+        The subject extends to the right of the translated query.
+
+               ....ss
+               ....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=3, queryEnd=14,
+                      frame=3)
+        normalized = normalizeHSP(hsp, 14, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testQueryExtendsRight(self):
+        """
+        The translated query extends to the right of the subject.
+
+               ....
+               ....qq
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=3, queryEnd=14,
+                      frame=3)
+        normalized = normalizeHSP(hsp, 20, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 6,
+        }, normalized)
+
+    def testQueryExtendsRightAndLeft(self):
+        """
+        The translated query extends to the right and left of the subject.
+
+                ....
+              qq....q
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=9, queryEnd=20,
+                      frame=3)
+        normalized = normalizeHSP(hsp, 23, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 2,
+            'readEnd': 6,
+            'readStartInSubject': -2,
             'readEndInSubject': 5,
         }, normalized)
 
-    def testHitExtendsRightAndLeft(self):
+    def testSubjectExtendsRightAndLeft(self):
         """
-        The hit extends to the right and left of the read.
+        The subject extends to the right and left of the translated query.
 
-               sssssss
-                qqqq
+               s...sss
+                ...q
         """
-        hsp = FakeHSP(subjectStart=2, subjectEnd=5, readStart=3, readEnd=12,
-                      frame=self.frame)
-        normalized = normalizeHSP(hsp, 4, 'blastx')
+        hsp = FakeHSP(subjectStart=2, subjectEnd=5, queryStart=5, queryEnd=14,
+                      frame=3)
+        normalized = normalizeHSP(hsp, 14, 'blastx')
         self.assertEqual({
             'subjectStart': 1,
             'subjectEnd': 5,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 1,
+            'readEndInSubject': 5,
+        }, normalized)
+
+
+class TestBlastxFrameMinus1(TestCase):
+    """
+    Tests for normalizeHSP for DIAMOND blastx output when frame=-1 (i.e., the
+    query matches in the reverse (complemented) order it was given to DIAMOND,
+    and the translation frame starts at the last nucleotide.
+    """
+
+    # All query offsets and lengths must be in terms of nucleotides.
+    # Subject offsets are in terms of protein AA sequences.  This is how
+    # DIAMOND reports those offsets.
+    #
+    # In the little diagrams in the docstrings below, the first line is the
+    # subject and the second the query. Dots indicate where the matched
+    # region is. The queries are shown translated so as to line up properly
+    # with the subjects.
+
+    def testIdentical(self):
+        """
+        If the query and subject match completely, the normalized HSP must
+        be correct.
+
+               ....
+               ....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=12, queryEnd=1,
+                      frame=-1)
+        normalized = normalizeHSP(hsp, 12, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testSubjectExtendsLeft(self):
+        """
+        The subject overlaps the translated query to the left.
+
+              ss....
+                ....
+        """
+        hsp = FakeHSP(subjectStart=3, subjectEnd=6, queryStart=12, queryEnd=1,
+                      frame=-1)
+        normalized = normalizeHSP(hsp, 12, 'blastx')
+        self.assertEqual({
+            'subjectStart': 2,
+            'subjectEnd': 6,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 2,
+            'readEndInSubject': 6,
+        }, normalized)
+
+    def testQueryExtendsLeft(self):
+        """
+        The translated query extends to the left of the subject.
+
+               ....
+             qq....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=12, queryEnd=1,
+                      frame=-1)
+        normalized = normalizeHSP(hsp, 18, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 2,
+            'readEnd': 6,
+            'readStartInSubject': -2,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testSubjectExtendsRight(self):
+        """
+        The subject extends to the right of the translated query.
+
+               ....ss
+               ....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=12, queryEnd=1,
+                      frame=-1)
+        normalized = normalizeHSP(hsp, 12, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testQueryExtendsRight(self):
+        """
+        The translated query extends to the right of the subject.
+
+               ....
+               ....qq
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=18, queryEnd=7,
+                      frame=-1)
+        normalized = normalizeHSP(hsp, 18, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 6,
+        }, normalized)
+
+    def testQueryExtendsRightAndLeft(self):
+        """
+        The translated query extends to the right and left of the subject.
+
+                ....
+              qq....q
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=15, queryEnd=4,
+                      frame=-1)
+        normalized = normalizeHSP(hsp, 21, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 2,
+            'readEnd': 6,
+            'readStartInSubject': -2,
+            'readEndInSubject': 5,
+        }, normalized)
+
+    def testSubjectExtendsRightAndLeft(self):
+        """
+        The subject extends to the right and left of the translated query.
+
+               s...sss
+                ...q
+        """
+        hsp = FakeHSP(subjectStart=2, subjectEnd=4, queryStart=12, queryEnd=4,
+                      frame=-1)
+        normalized = normalizeHSP(hsp, 12, 'blastx')
+        self.assertEqual({
+            'subjectStart': 1,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 3,
+            'readStartInSubject': 1,
+            'readEndInSubject': 5,
+        }, normalized)
+
+
+class TestBlastxFrameMinus2(TestCase):
+    """
+    Tests for normalizeHSP for DIAMOND blastx output when frame=-2 (i.e., the
+    query matches in the reverse (complemented) order it was given to DIAMOND,
+    and the translation frame starts at the last nucleotide.
+    """
+
+    # All query offsets and lengths must be in terms of nucleotides.
+    # Subject offsets are in terms of protein AA sequences.  This is how
+    # DIAMOND reports those offsets.
+    #
+    # In the little diagrams in the docstrings below, the first line is the
+    # subject and the second the query. Dots indicate where the matched
+    # region is. The queries are shown translated so as to line up properly
+    # with the subjects.
+
+    def testIdentical(self):
+        """
+        If the query and subject match completely, the normalized HSP must
+        be correct.
+
+               ....
+               ....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=12, queryEnd=1,
+                      frame=-2)
+        normalized = normalizeHSP(hsp, 13, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testSubjectExtendsLeft(self):
+        """
+        The subject overlaps the translated query to the left.
+
+              ss....
+                ....
+        """
+        hsp = FakeHSP(subjectStart=3, subjectEnd=6, queryStart=12, queryEnd=1,
+                      frame=-2)
+        normalized = normalizeHSP(hsp, 13, 'blastx')
+        self.assertEqual({
+            'subjectStart': 2,
+            'subjectEnd': 6,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 2,
+            'readEndInSubject': 6,
+        }, normalized)
+
+    def testQueryExtendsLeft(self):
+        """
+        The translated query extends to the left of the subject.
+
+               ....
+             qq....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=12, queryEnd=1,
+                      frame=-2)
+        normalized = normalizeHSP(hsp, 19, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
             'readStart': 2,
-            'readEnd': 12,
+            'readEnd': 6,
+            'readStartInSubject': -2,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testSubjectExtendsRight(self):
+        """
+        The subject extends to the right of the translated query.
+
+               ....ss
+               ....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=12, queryEnd=1,
+                      frame=-2)
+        normalized = normalizeHSP(hsp, 13, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testQueryExtendsRight(self):
+        """
+        The translated query extends to the right of the subject.
+
+               ....
+               ....qq
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=18, queryEnd=7,
+                      frame=-2)
+        normalized = normalizeHSP(hsp, 19, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 6,
+        }, normalized)
+
+    def testQueryExtendsRightAndLeft(self):
+        """
+        The translated query extends to the right and left of the subject.
+
+                ....
+              qq....q
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=15, queryEnd=4,
+                      frame=-2)
+        normalized = normalizeHSP(hsp, 22, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 2,
+            'readEnd': 6,
+            'readStartInSubject': -2,
+            'readEndInSubject': 5,
+        }, normalized)
+
+    def testSubjectExtendsRightAndLeft(self):
+        """
+        The subject extends to the right and left of the translated query.
+
+               s...sss
+                ...q
+        """
+        hsp = FakeHSP(subjectStart=2, subjectEnd=4, queryStart=12, queryEnd=4,
+                      frame=-2)
+        normalized = normalizeHSP(hsp, 13, 'blastx')
+        self.assertEqual({
+            'subjectStart': 1,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 3,
+            'readStartInSubject': 1,
+            'readEndInSubject': 5,
+        }, normalized)
+
+
+class TestBlastxFrameMinus3(TestCase):
+    """
+    Tests for normalizeHSP for DIAMOND blastx output when frame=-3 (i.e., the
+    query matches in the reverse (complemented) order it was given to DIAMOND,
+    and the translation frame starts at the last nucleotide.
+    """
+
+    # All query offsets and lengths must be in terms of nucleotides.
+    # Subject offsets are in terms of protein AA sequences.  This is how
+    # DIAMOND reports those offsets.
+    #
+    # In the little diagrams in the docstrings below, the first line is the
+    # subject and the second the query. Dots indicate where the matched
+    # region is. The queries are shown translated so as to line up properly
+    # with the subjects.
+
+    def testIdentical(self):
+        """
+        If the query and subject match completely, the normalized HSP must
+        be correct.
+
+               ....
+               ....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=12, queryEnd=1,
+                      frame=-3)
+        normalized = normalizeHSP(hsp, 14, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testSubjectExtendsLeft(self):
+        """
+        The subject overlaps the translated query to the left.
+
+              ss....
+                ....
+        """
+        hsp = FakeHSP(subjectStart=3, subjectEnd=6, queryStart=12, queryEnd=1,
+                      frame=-3)
+        normalized = normalizeHSP(hsp, 14, 'blastx')
+        self.assertEqual({
+            'subjectStart': 2,
+            'subjectEnd': 6,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 2,
+            'readEndInSubject': 6,
+        }, normalized)
+
+    def testQueryExtendsLeft(self):
+        """
+        The translated query extends to the left of the subject.
+
+               ....
+             qq....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=12, queryEnd=1,
+                      frame=-3)
+        normalized = normalizeHSP(hsp, 20, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 2,
+            'readEnd': 6,
+            'readStartInSubject': -2,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testSubjectExtendsRight(self):
+        """
+        The subject extends to the right of the translated query.
+
+               ....ss
+               ....
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=12, queryEnd=1,
+                      frame=-3)
+        normalized = normalizeHSP(hsp, 14, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 4,
+        }, normalized)
+
+    def testQueryExtendsRight(self):
+        """
+        The translated query extends to the right of the subject.
+
+               ....
+               ....qq
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=18, queryEnd=7,
+                      frame=-3)
+        normalized = normalizeHSP(hsp, 20, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 4,
+            'readStartInSubject': 0,
+            'readEndInSubject': 6,
+        }, normalized)
+
+    def testQueryExtendsRightAndLeft(self):
+        """
+        The translated query extends to the right and left of the subject.
+
+                ....
+              qq....q
+        """
+        hsp = FakeHSP(subjectStart=1, subjectEnd=4, queryStart=15, queryEnd=4,
+                      frame=-3)
+        normalized = normalizeHSP(hsp, 23, 'blastx')
+        self.assertEqual({
+            'subjectStart': 0,
+            'subjectEnd': 4,
+            'readStart': 2,
+            'readEnd': 6,
+            'readStartInSubject': -2,
+            'readEndInSubject': 5,
+        }, normalized)
+
+    def testSubjectExtendsRightAndLeft(self):
+        """
+        The subject extends to the right and left of the translated query.
+
+               s...sss
+                ...q
+        """
+        hsp = FakeHSP(subjectStart=2, subjectEnd=4, queryStart=12, queryEnd=4,
+                      frame=-3)
+        normalized = normalizeHSP(hsp, 14, 'blastx')
+        self.assertEqual({
+            'subjectStart': 1,
+            'subjectEnd': 4,
+            'readStart': 0,
+            'readEnd': 3,
             'readStartInSubject': 1,
             'readEndInSubject': 5,
         }, normalized)