Repository: acorg/dark-matter

Issue:
Add a --titleJSONFile option to noninteractive-alignment-panel.py
This will allow you to write out information about matching titles, initially each with a list of their bit scores.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0975edb..2468b6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 3.0.37 October 1, 2018
+
+Add a `--titlesJSONFile` option to `noninteractive-alignment-panel.py`.
+
 ## 3.0.36 September 13, 2018
 
 Added `whitelistFile` and `blacklistFile` to `ReadsAlignmentsFilter` class
diff --git a/bin/noninteractive-alignment-panel.py b/bin/noninteractive-alignment-panel.py
index 9741e9f..7f98ee5 100755
--- a/bin/noninteractive-alignment-panel.py
+++ b/bin/noninteractive-alignment-panel.py
@@ -13,6 +13,7 @@ from __future__ import print_function
 import os
 import sys
 import argparse
+from json import dump
 from collections import defaultdict
 from itertools import chain
 
@@ -292,6 +293,11 @@ if __name__ == '__main__':
               'the results in the files from HTCondor does not match the '
               'order of sequences in the FASTA/Q file.'))
 
+    parser.add_argument(
+        '--titlesJSONFile',
+        help=('Give a file name for JSON holding information about titles to '
+              'be written to'))
+
     args = parser.parse_args()
 
     # Flatten lists of lists that we get from using both nargs='+' and
@@ -383,6 +389,13 @@ if __name__ == '__main__':
     print('Found %d interesting title%s.' %
           (nTitles, '' if nTitles == 1 else 's'), file=sys.stderr)
 
+    # If we've been asked to save the matched title information in JSON,
+    # write it out.
+    if args.titlesJSONFile:
+        with open(args.titlesJSONFile, 'w') as fp:
+            dump(titlesAlignments.toDict(), fp, sort_keys=True, indent=2,
+                 separators=(',', ': '))
+
     if nTitles:
         # Use flush=True on the print of the titles so the output is
         # definitely written out. This is because in some cases the
diff --git a/dark/__init__.py b/dark/__init__.py
index 14d9531..0c505ca 100644
--- a/dark/__init__.py
+++ b/dark/__init__.py
@@ -7,4 +7,4 @@ if sys.version_info < (2, 7):
 # will not be found by the version() function in ../setup.py
 #
 # Remember to update ../CHANGELOG.md describing what's new in each version.
-__version__ = '3.0.36'
+__version__ = '3.0.37'
diff --git a/dark/hsp.py b/dark/hsp.py
index 61dccfd..f25d381 100644
--- a/dark/hsp.py
+++ b/dark/hsp.py
@@ -96,6 +96,27 @@ class _Base(object):
         """
         return self.score.betterThan(score)
 
+    def toDict(self):
+        """
+        Get information about the HSP/LSP as a dictionary.
+
+        @return: A C{dict} representation of the HSP/LSP.
+        """
+        return {
+            'readStart': self.readStart,
+            'readEnd': self.readEnd,
+            'readStartInSubject': self.readStartInSubject,
+            'readEndInSubject': self.readEndInSubject,
+            'readFrame': self.readFrame,
+            'subjectStart': self.subjectStart,
+            'subjectEnd': self.subjectEnd,
+            'subjectFrame': self.subjectFrame,
+            'readMatchedSequence': self.readMatchedSequence,
+            'subjectMatchedSequence': self.subjectMatchedSequence,
+            'identicalCount': self.identicalCount,
+            'positiveCount': self.positiveCount,
+        }
+
 
 class HSP(_Base):
     """
@@ -109,6 +130,16 @@ class HSP(_Base):
         _Base.__init__(self, **kwargs)
         self.score = HigherIsBetterScore(score)
 
+    def toDict(self):
+        """
+        Get information about the HSP as a dictionary.
+
+        @return: A C{dict} representation of the HSP.
+        """
+        result = _Base.toDict(self)
+        result['score'] = self.score.score
+        return result
+
 
 class LSP(_Base):
     """
@@ -121,3 +152,13 @@ class LSP(_Base):
     def __init__(self, score, **kwargs):
         _Base.__init__(self, **kwargs)
         self.score = LowerIsBetterScore(score)
+
+    def toDict(self):
+        """
+        Get information about the LSP as a dictionary.
+
+        @return: A C{dict} representation of the LSP.
+        """
+        result = _Base.toDict(self)
+        result['score'] = self.score.score
+        return result
diff --git a/dark/titles.py b/dark/titles.py
index 8cc659d..cfeb6d5 100644
--- a/dark/titles.py
+++ b/dark/titles.py
@@ -35,6 +35,17 @@ class TitleAlignment(object):
         self.read = read
         self.hsps = hsps
 
+    def toDict(self):
+        """
+        Get information about a title alignment as a dictionary.
+
+        @return: A C{dict} representation of the title aligment.
+        """
+        return {
+            'hsps': [hsp.toDict() for hsp in self.hsps],
+            'read': self.read.toDict(),
+        }
+
 
 class TitleAlignments(list):
     """
@@ -245,6 +256,19 @@ class TitleAlignments(list):
             'subjectTitle': self.subjectTitle,
         }
 
+    def toDict(self):
+        """
+        Get information about the title's alignments as a dictionary.
+
+        @return: A C{dict} representation of the title's aligments.
+        """
+        return {
+            'titleAlignments': [titleAlignment.toDict()
+                                for titleAlignment in self],
+            'subjectTitle': self.subjectTitle,
+            'subjectLength': self.subjectLength,
+        }
+
 
 class TitlesAlignments(dict):
     """
@@ -482,3 +506,15 @@ class TitlesAlignments(dict):
                 '%(subjectTitle)s',
             ]) % titleSummary)
         return '\n'.join(result)
+
+    def toDict(self):
+        """
+        Get information about the titles alignments as a dictionary.
+
+        @return: A C{dict} representation of the titles aligments.
+        """
+        return {
+            'scoreClass': self.scoreClass.__name__,
+            'titles': dict((title, titleAlignments.toDict())
+                           for title, titleAlignments in self.items()),
+        }
diff --git a/requirements.txt b/requirements.txt
index 48defbc..0e7afb8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,16 +1,17 @@
 biopython>=1.71
 bz2file>=0.98
+ete3>=3.1.1
+flake8>=3.5.0
 ipython>=3.1.0
 matplotlib>=1.4.3
 mock>=1.0.1; python_version == '2.7'
+mysql-connector-python>=8.0.11
 numpy>=1.14.2
 pyfaidx>=0.4.8.4
 pysam>=0.14.1
+pysam>=0.14.1
+pytest>=3.8.1
 pyzmq>=14.3.1
 requests>=2.18.4
 simplejson>=3.5.3
 six>=1.11.0
-pysam>=0.14.1
-mysql-connector-python>=8.0.11
-flake8>=3.5.0
-ete3>=3.1.1


Test patch:
diff --git a/test/blast/test_titles.py b/test/blast/test_titles.py
index 3755bd4..1bb56c7 100644
--- a/test/blast/test_titles.py
+++ b/test/blast/test_titles.py
@@ -279,6 +279,210 @@ class TestTitlesAlignments(TestCase):
             self.assertEqual(HSP(5), titleAlignments[1].hsps[1])
             self.assertEqual(HSP(3), titleAlignments[1].hsps[2])
 
+    def testToDict(self):
+        """
+        The toDict method must return the expected value.
+        """
+        mockOpener = mockOpen(read_data=(
+            dumps(PARAMS) + '\n' + dumps(RECORD0) + '\n' +
+            dumps(RECORD1) + '\n' + dumps(RECORD2) + '\n' +
+            dumps(RECORD3) + '\n'))
+        with patch.object(builtins, 'open', mockOpener):
+            reads = Reads()
+            reads.add(Read('id0', 'A' * 70))
+            reads.add(Read('id1', 'C' * 70))
+            reads.add(Read('id2', 'G' * 70))
+            reads.add(Read('id3', 'T' * 70))
+            readsAlignments = BlastReadsAlignments(
+                reads, 'file.json', scoreClass=LowerIsBetterScore)
+            titlesAlignments = TitlesAlignments(readsAlignments)
+
+        self.assertEqual(
+            {
+                'scoreClass': 'LowerIsBetterScore',
+                'titles': {
+                    'gi|887699|gb|DQ37780 Cowpox virus 15': {
+                        'subjectLength': 30000,
+                        'subjectTitle': 'gi|887699|gb|DQ37780 Cowpox virus 15',
+                        'titleAlignments': [
+                            {
+                                'hsps': [
+                                    {
+                                        'identicalCount': None,
+                                        'positiveCount': None,
+                                        'readEnd': 68,
+                                        'readEndInSubject': 1405,
+                                        'readFrame': 1,
+                                        'readMatchedSequence': (
+                                            'TACCCTGCGGCCCGCTACGGCTGG-TCTCCA'),
+                                        'readStart': 27,
+                                        'readStartInSubject': 1334,
+                                        'score': 1e-06,
+                                        'subjectEnd': 1400,
+                                        'subjectFrame': 1,
+                                        'subjectMatchedSequence': (
+                                            'TACCC--CGGCCCGCG-CGGCCGGCTCTCCA'),
+                                        'subjectStart': 1361}],
+                                'read': {
+                                    'id': 'id2',
+                                    'quality': None,
+                                    'sequence': 'G' * 70,
+                                }
+                            },
+                            {
+                                'hsps': [
+                                    {
+                                        'identicalCount': None,
+                                        'positiveCount': None,
+                                        'readEnd': 68,
+                                        'readEndInSubject': 1405,
+                                        'readFrame': 1,
+                                        'readMatchedSequence': (
+                                            'TACCCTGCGGCCCGCTACGGCTGG-TCTCCA'),
+                                        'readStart': 27,
+                                        'readStartInSubject': 1334,
+                                        'score': 1e-05,
+                                        'subjectEnd': 1400,
+                                        'subjectFrame': 1,
+                                        'subjectMatchedSequence': (
+                                            'TACCC--CGGCCCGCG-CGGCCGGCTCTCCA'),
+                                        'subjectStart': 1361}],
+                                'read': {
+                                    'id': 'id3',
+                                    'quality': None,
+                                    'sequence': 'T' * 70,
+                                },
+                            },
+                        ],
+                    },
+                    'gi|887699|gb|DQ37780 Monkeypox virus 456': {
+                        'subjectLength': 35000,
+                        'subjectTitle': (
+                            'gi|887699|gb|DQ37780 Monkeypox virus 456'),
+                        'titleAlignments': [
+                            {
+                                'hsps': [
+                                    {
+                                        'identicalCount': None,
+                                        'positiveCount': None,
+                                        'readEnd': 68,
+                                        'readEndInSubject': 11405,
+                                        'readFrame': 1,
+                                        'readMatchedSequence': (
+                                            'TACCCTGCGGCCCGCTACGGCTGG-TCTCCA'),
+                                        'readStart': 27,
+                                        'readStartInSubject': 11334,
+                                        'score': 1e-08,
+                                        'subjectEnd': 11400,
+                                        'subjectFrame': 1,
+                                        'subjectMatchedSequence': (
+                                            'TACCC--CGGCCCGCG-CGGCCGGCTCTCCA'),
+                                        'subjectStart': 11361}],
+                                'read': {
+                                    'id': 'id1',
+                                    'quality': None,
+                                    'sequence': 'C' * 70,
+                                },
+                            },
+                        ],
+                    },
+                    'gi|887699|gb|DQ37780 Mummypox virus 3000 B.C.': {
+                        'subjectLength': 35000,
+                        'subjectTitle': (
+                            'gi|887699|gb|DQ37780 Mummypox virus 3000 B.C.'),
+                        'titleAlignments': [
+                            {
+                                'hsps': [
+                                    {
+                                        'identicalCount': None,
+                                        'positiveCount': None,
+                                        'readEnd': 68,
+                                        'readEndInSubject': 10405,
+                                        'readFrame': 1,
+                                        'readMatchedSequence': (
+                                            'TACCCTGCGGCCCGCTACGGCTGG-TCTCCA'),
+                                        'readStart': 27,
+                                        'readStartInSubject': 10334,
+                                        'score': 1e-07,
+                                        'subjectEnd': 10400,
+                                        'subjectFrame': 1,
+                                        'subjectMatchedSequence': (
+                                            'TACCC--CGGCCCGCG-CGGCCGGCTCTCCA'),
+                                        'subjectStart': 10361}],
+                                'read': {
+                                    'id': 'id1',
+                                    'quality': None,
+                                    'sequence': 'C' * 70,
+                                },
+                            },
+                        ],
+                    },
+                    'gi|887699|gb|DQ37780 Squirrelpox virus 1296/99': {
+                        'subjectLength': 37000,
+                        'subjectTitle': (
+                            'gi|887699|gb|DQ37780 Squirrelpox virus 1296/99'),
+                        'titleAlignments': [
+                            {
+                                'hsps': [
+                                    {
+                                        'identicalCount': None,
+                                        'positiveCount': None,
+                                        'readEnd': 68,
+                                        'readEndInSubject': 15405,
+                                        'readFrame': 1,
+                                        'readMatchedSequence': (
+                                            'TACCCTGCGGCCCGCTACGGCTGG-TCTCCA'),
+                                        'readStart': 27,
+                                        'readStartInSubject': 15334,
+                                        'score': 1e-11,
+                                        'subjectEnd': 15400,
+                                        'subjectFrame': 1,
+                                        'subjectMatchedSequence': (
+                                            'TACCC--CGGCCCGCG-CGGCCGGCTCTCCA'),
+                                        'subjectStart': 15361}],
+                                'read': {
+                                    'id': 'id0',
+                                    'quality': None,
+                                    'sequence': 'A' * 70,
+                                },
+                            },
+                        ],
+                    },
+                    'gi|887699|gb|DQ37780 Squirrelpox virus 55': {
+                        'subjectLength': 38000,
+                        'subjectTitle': (
+                            'gi|887699|gb|DQ37780 Squirrelpox virus 55'),
+                        'titleAlignments': [
+                            {
+                                'hsps': [
+                                    {
+                                        'identicalCount': None,
+                                        'positiveCount': None,
+                                        'readEnd': 68,
+                                        'readEndInSubject': 12405,
+                                        'readFrame': 1,
+                                        'readMatchedSequence': (
+                                            'TACCCTGCGGCCCGCTACGGCTGG-TCTCCA'),
+                                        'readStart': 27,
+                                        'readStartInSubject': 12334,
+                                        'score': 1e-10,
+                                        'subjectEnd': 12400,
+                                        'subjectFrame': 1,
+                                        'subjectMatchedSequence': (
+                                            'TACCC--CGGCCCGCG-CGGCCGGCTCTCCA'),
+                                        'subjectStart': 12361}],
+                                'read': {
+                                    'id': 'id0',
+                                    'quality': None,
+                                    'sequence': 'A' * 70,
+                                },
+                            },
+                        ],
+                    },
+                },
+            },
+            titlesAlignments.toDict())
+
 
 class TestTitlesAlignmentsFiltering(TestCase):
     """
diff --git a/test/diamond/test_titles.py b/test/diamond/test_titles.py
index 14295f7..7385b55 100644
--- a/test/diamond/test_titles.py
+++ b/test/diamond/test_titles.py
@@ -32,8 +32,7 @@ class TestTitleCounts(TestCase):
         mockOpener = mockOpen(read_data=dumps(PARAMS) + '\n')
         with patch.object(builtins, 'open', mockOpener):
             reads = Reads()
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             self.assertEqual({}, titleCounts(readsAlignments))
 
     def testThreeRecords(self):
@@ -49,8 +48,7 @@ class TestTitleCounts(TestCase):
             reads.add(Read('id0', 'A' * 70))
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             self.assertEqual(
                 {
                     'gi|887699|gb|DQ37780 Squirrelpox virus 1296/99': 1,
@@ -73,8 +71,7 @@ class TestTitleCounts(TestCase):
             reads = Reads()
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             self.assertEqual(
                 {
                     'gi|887699|gb|DQ37780 Cowpox virus 15': 2,
@@ -95,8 +92,7 @@ class TestTitlesAlignments(TestCase):
         mockOpener = mockOpen(read_data=(dumps(PARAMS) + '\n'))
         with patch.object(builtins, 'open', mockOpener):
             reads = Reads()
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             self.assertEqual([], list(titlesAlignments.keys()))
 
@@ -112,8 +108,7 @@ class TestTitlesAlignments(TestCase):
             reads.add(Read('id0', 'A' * 70))
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             self.assertEqual(
                 [
@@ -136,8 +131,7 @@ class TestTitlesAlignments(TestCase):
             reads = Reads()
             read = Read('id0', 'A' * 70)
             reads.add(read)
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
 
             title = 'gi|887699|gb|DQ37780 Squirrelpox virus 1296/99'
@@ -170,8 +164,7 @@ class TestTitlesAlignments(TestCase):
             read3 = Read('id3', 'A' * 70)
             reads.add(read2)
             reads.add(read3)
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
 
             title = 'gi|887699|gb|DQ37780 Cowpox virus 15'
@@ -196,8 +189,7 @@ class TestTitlesAlignments(TestCase):
         with patch.object(builtins, 'open', mockOpener):
             reads = Reads()
             reads.add(Read('id0', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             title = 'gi|887699|gb|DQ37780 Squirrelpox virus 1296/99'
             titleAlignments = TitleAlignments(title, 55)
@@ -217,8 +209,7 @@ class TestTitlesAlignments(TestCase):
         with patch.object(builtins, 'open', mockOpener):
             reads = Reads()
             reads.add(Read('id0', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             title = 'gi|887699|gb|DQ37780 Squirrelpox virus 23'
             titleAlignments = TitleAlignments(title, 55)
@@ -239,8 +230,7 @@ class TestTitlesAlignments(TestCase):
             reads.add(Read('id0', 'A' * 70))
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = list(titlesAlignments.hsps())
             self.assertEqual(
@@ -258,8 +248,7 @@ class TestTitlesAlignments(TestCase):
             reads = Reads()
             reads.add(Read('id0', 'A' * 70))
             reads.add(Read('id1', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             self.assertEqual(
                 [
@@ -315,7 +304,7 @@ class TestTitlesAlignments(TestCase):
         with patch.object(builtins, 'open', mockOpener):
             reads = Reads()
             reads.add(Read('id0', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'f.json', 'db')
+            readsAlignments = DiamondReadsAlignments(reads, 'f.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             summary = titlesAlignments.tabSeparatedSummary(sortOn='title')
             expected = (
@@ -355,8 +344,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id0', 'A' * 70))
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter()
             self.assertEqual(
@@ -384,8 +372,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(minMatchingReads=2)
             self.assertEqual(
@@ -409,8 +396,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(minMedianScore=22)
             self.assertEqual(
@@ -435,8 +421,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
             readsAlignments = DiamondReadsAlignments(
-                reads, 'file.json', 'database.fasta',
-                scoreClass=LowerIsBetterScore)
+                reads, 'file.json', scoreClass=LowerIsBetterScore)
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(minMedianScore=1e-9)
             self.assertEqual(
@@ -461,8 +446,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(withScoreBetterThan=24)
             self.assertEqual(
@@ -487,8 +471,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
             readsAlignments = DiamondReadsAlignments(
-                reads, 'file.json', 'database.fasta',
-                scoreClass=LowerIsBetterScore)
+                reads, 'file.json', scoreClass=LowerIsBetterScore)
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(withScoreBetterThan=1e-10)
             self.assertEqual(
@@ -512,8 +495,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(minNewReads=0.0)
             self.assertEqual(
@@ -541,8 +523,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(minNewReads=1.0)
 
@@ -581,8 +562,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(minCoverage=0.1)
             self.assertEqual(0, len(result))
@@ -602,8 +582,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(minCoverage=0.0)
             self.assertEqual(
@@ -632,8 +611,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             # To understand why the following produces the result it does,
             # you need to look at the HSP coverage in sample_data.py and
@@ -657,8 +635,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
         with patch.object(builtins, 'open', mockOpener):
             reads = Reads()
             reads.add(Read('id0', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             error = '^maxTitles \(-1\) cannot be negative\.$'
             six.assertRaisesRegex(self, ValueError, error,
@@ -674,8 +651,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
         with patch.object(builtins, 'open', mockOpener):
             reads = Reads()
             reads.add(Read('id0', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             error = ('^Sort attribute must be one of "length", "maxScore", '
                      '"medianScore", "readCount", "title"\.$')
@@ -697,8 +673,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(maxTitles=0, sortOn='maxScore')
             self.assertEqual(0, len(result))
@@ -718,8 +693,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(maxTitles=1, sortOn='maxScore')
             self.assertEqual(
@@ -743,8 +717,7 @@ class TestTitlesAlignmentsFiltering(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.filter(maxTitles=2, sortOn='length')
             self.assertEqual(
@@ -767,8 +740,7 @@ class TestTitleSorting(TestCase):
         mockOpener = mockOpen(read_data=dumps(PARAMS) + '\n')
         with patch.object(builtins, 'open', mockOpener):
             reads = Reads()
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             self.assertRaises(ValueError, titlesAlignments.sortTitles, 'xxx')
 
@@ -779,8 +751,7 @@ class TestTitleSorting(TestCase):
         mockOpener = mockOpen(read_data=dumps(PARAMS) + '\n')
         with patch.object(builtins, 'open', mockOpener):
             reads = Reads()
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.sortTitles('title')
             self.assertEqual([], result)
@@ -801,8 +772,7 @@ class TestTitleSorting(TestCase):
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
             reads.add(Read('id4', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.sortTitles('medianScore')
             self.assertEqual([
@@ -830,8 +800,7 @@ class TestTitleSorting(TestCase):
             reads.add(Read('id3', 'A' * 70))
             reads.add(Read('id4', 'A' * 70))
             readsAlignments = DiamondReadsAlignments(
-                reads, 'file.json', 'database.fasta',
-                scoreClass=LowerIsBetterScore)
+                reads, 'file.json', scoreClass=LowerIsBetterScore)
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.sortTitles('medianScore')
             self.assertEqual([
@@ -857,8 +826,7 @@ class TestTitleSorting(TestCase):
             reads.add(Read('id1', 'A' * 70))
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
-            readsAlignments = DiamondReadsAlignments(reads, 'file.json',
-                                                     'database.fasta')
+            readsAlignments = DiamondReadsAlignments(reads, 'file.json')
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.sortTitles('maxScore')
             self.assertEqual([
@@ -885,8 +853,7 @@ class TestTitleSorting(TestCase):
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
             readsAlignments = DiamondReadsAlignments(
-                reads, 'file.json', 'database.fasta',
-                scoreClass=LowerIsBetterScore)
+                reads, 'file.json', scoreClass=LowerIsBetterScore)
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.sortTitles('maxScore')
             # self.assertEqual([
@@ -919,8 +886,7 @@ class TestTitleSorting(TestCase):
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
             readsAlignments = DiamondReadsAlignments(
-                reads, 'file.json', 'database.fasta',
-                scoreClass=LowerIsBetterScore)
+                reads, 'file.json', scoreClass=LowerIsBetterScore)
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.sortTitles('readCount')
             self.assertEqual([
@@ -947,8 +913,7 @@ class TestTitleSorting(TestCase):
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
             readsAlignments = DiamondReadsAlignments(
-                reads, 'file.json', 'database.fasta',
-                scoreClass=LowerIsBetterScore)
+                reads, 'file.json', scoreClass=LowerIsBetterScore)
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.sortTitles('length')
             self.assertEqual([
@@ -974,8 +939,7 @@ class TestTitleSorting(TestCase):
             reads.add(Read('id2', 'A' * 70))
             reads.add(Read('id3', 'A' * 70))
             readsAlignments = DiamondReadsAlignments(
-                reads, 'file.json', 'database.fasta',
-                scoreClass=LowerIsBetterScore)
+                reads, 'file.json', scoreClass=LowerIsBetterScore)
             titlesAlignments = TitlesAlignments(readsAlignments)
             result = titlesAlignments.sortTitles('title')
             self.assertEqual([
diff --git a/test/test_hsp.py b/test/test_hsp.py
index ab24d0b..2494ec3 100644
--- a/test/test_hsp.py
+++ b/test/test_hsp.py
@@ -55,6 +55,35 @@ class TestHSP(TestCase):
         """
         self.assertFalse(HSP(5).betterThan(7))
 
+    def testToDict(self):
+        """
+        The toDict method must return the expected dictionary.
+        """
+        hsp = HSP(0, readStart=1, readEnd=2,
+                  readStartInSubject=3, readEndInSubject=4,
+                  subjectStart=5, subjectEnd=6,
+                  readMatchedSequence='aaa', subjectMatchedSequence='ccc',
+                  readFrame=7, subjectFrame=8, identicalCount=9,
+                  positiveCount=10)
+
+        self.assertEqual(
+            {
+                'score': 0,
+                'readStart': 1,
+                'readEnd': 2,
+                'readStartInSubject': 3,
+                'readEndInSubject': 4,
+                'subjectStart': 5,
+                'subjectEnd': 6,
+                'readFrame': 7,
+                'subjectFrame': 8,
+                'identicalCount': 9,
+                'positiveCount': 10,
+                'readMatchedSequence': 'aaa',
+                'subjectMatchedSequence': 'ccc',
+            },
+            hsp.toDict())
+
 
 class TestLSP(TestCase):
     """
@@ -65,22 +94,22 @@ class TestLSP(TestCase):
         """
         An LSP must have the expected attributes.
         """
-        hsp = LSP(7, readStart=1, readEnd=2,
+        lsp = LSP(7, readStart=1, readEnd=2,
                   readStartInSubject=3, readEndInSubject=4,
                   subjectStart=5, subjectEnd=6,
                   readMatchedSequence='aaa', subjectMatchedSequence='ccc',
                   readFrame=8, subjectFrame=9)
-        self.assertEqual(1, hsp.readStart)
-        self.assertEqual(2, hsp.readEnd)
-        self.assertEqual(3, hsp.readStartInSubject)
-        self.assertEqual(4, hsp.readEndInSubject)
-        self.assertEqual(5, hsp.subjectStart)
-        self.assertEqual(6, hsp.subjectEnd)
-        self.assertEqual('aaa', hsp.readMatchedSequence)
-        self.assertEqual('ccc', hsp.subjectMatchedSequence)
-        self.assertEqual(7, hsp.score.score)
-        self.assertEqual(8, hsp.readFrame)
-        self.assertEqual(9, hsp.subjectFrame)
+        self.assertEqual(1, lsp.readStart)
+        self.assertEqual(2, lsp.readEnd)
+        self.assertEqual(3, lsp.readStartInSubject)
+        self.assertEqual(4, lsp.readEndInSubject)
+        self.assertEqual(5, lsp.subjectStart)
+        self.assertEqual(6, lsp.subjectEnd)
+        self.assertEqual('aaa', lsp.readMatchedSequence)
+        self.assertEqual('ccc', lsp.subjectMatchedSequence)
+        self.assertEqual(7, lsp.score.score)
+        self.assertEqual(8, lsp.readFrame)
+        self.assertEqual(9, lsp.subjectFrame)
 
     def testEqual(self):
         """
@@ -107,3 +136,32 @@ class TestLSP(TestCase):
         score is better than the score of the LSP.
         """
         self.assertFalse(LSP(7).betterThan(5))
+
+    def testToDict(self):
+        """
+        The toDict method must return the expected dictionary.
+        """
+        lsp = LSP(0, readStart=1, readEnd=2,
+                  readStartInSubject=3, readEndInSubject=4,
+                  subjectStart=5, subjectEnd=6,
+                  readMatchedSequence='aaa', subjectMatchedSequence='ccc',
+                  readFrame=7, subjectFrame=8, identicalCount=9,
+                  positiveCount=10)
+
+        self.assertEqual(
+            {
+                'score': 0,
+                'readStart': 1,
+                'readEnd': 2,
+                'readStartInSubject': 3,
+                'readEndInSubject': 4,
+                'subjectStart': 5,
+                'subjectEnd': 6,
+                'readFrame': 7,
+                'subjectFrame': 8,
+                'identicalCount': 9,
+                'positiveCount': 10,
+                'readMatchedSequence': 'aaa',
+                'subjectMatchedSequence': 'ccc',
+            },
+            lsp.toDict())
diff --git a/test/test_titles.py b/test/test_titles.py
index 16d6060..d77f5a3 100644
--- a/test/test_titles.py
+++ b/test/test_titles.py
@@ -48,6 +48,67 @@ class TestTitleAlignment(TestCase):
         self.assertEqual(read, titleAlignment.read)
         self.assertEqual([], titleAlignment.hsps)
 
+    def testToDict(self):
+        """
+        The toDict method must return the expected result.
+        """
+        read = Read('the-id', 'AAA')
+        hsp1 = HSP(0, readStart=1, readEnd=2,
+                   readStartInSubject=3, readEndInSubject=4,
+                   subjectStart=5, subjectEnd=6,
+                   readMatchedSequence='aaa', subjectMatchedSequence='ccc',
+                   readFrame=7, subjectFrame=8, identicalCount=9,
+                   positiveCount=10)
+        hsp2 = HSP(10, readStart=11, readEnd=12,
+                   readStartInSubject=13, readEndInSubject=14,
+                   subjectStart=15, subjectEnd=16,
+                   readMatchedSequence='ggg', subjectMatchedSequence='ttt',
+                   readFrame=17, subjectFrame=18, identicalCount=19,
+                   positiveCount=20)
+        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
+
+        self.assertEqual(
+            {
+                'hsps': [
+                    {
+                        'score': 0,
+                        'readStart': 1,
+                        'readEnd': 2,
+                        'readStartInSubject': 3,
+                        'readEndInSubject': 4,
+                        'subjectStart': 5,
+                        'subjectEnd': 6,
+                        'readFrame': 7,
+                        'subjectFrame': 8,
+                        'identicalCount': 9,
+                        'positiveCount': 10,
+                        'readMatchedSequence': 'aaa',
+                        'subjectMatchedSequence': 'ccc',
+                    },
+                    {
+                        'score': 10,
+                        'readStart': 11,
+                        'readEnd': 12,
+                        'readStartInSubject': 13,
+                        'readEndInSubject': 14,
+                        'subjectStart': 15,
+                        'subjectEnd': 16,
+                        'readFrame': 17,
+                        'subjectFrame': 18,
+                        'identicalCount': 19,
+                        'positiveCount': 20,
+                        'readMatchedSequence': 'ggg',
+                        'subjectMatchedSequence': 'ttt',
+                    },
+                ],
+                'read': {
+                    'id': 'the-id',
+                    'quality': None,
+                    'sequence': 'AAA',
+                },
+            },
+            titleAlignment.toDict())
+
 
 class TestTitleAlignments(WarningTestMixin, TestCase):
     """
@@ -828,6 +889,75 @@ class TestTitleAlignments(WarningTestMixin, TestCase):
             },
             titleAlignments.summary())
 
+    def testToDict(self):
+        """
+        The toDict method must return the expected result.
+        """
+        read = Read('the-id', 'AAA')
+        hsp1 = HSP(0, readStart=1, readEnd=2,
+                   readStartInSubject=3, readEndInSubject=4,
+                   subjectStart=5, subjectEnd=6,
+                   readMatchedSequence='aaa', subjectMatchedSequence='ccc',
+                   readFrame=7, subjectFrame=8, identicalCount=9,
+                   positiveCount=10)
+        hsp2 = HSP(10, readStart=11, readEnd=12,
+                   readStartInSubject=13, readEndInSubject=14,
+                   subjectStart=15, subjectEnd=16,
+                   readMatchedSequence='ggg', subjectMatchedSequence='ttt',
+                   readFrame=17, subjectFrame=18, identicalCount=19,
+                   positiveCount=20)
+        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
+        titleAlignments = TitleAlignments('subject title', 10)
+        titleAlignments.addAlignment(titleAlignment)
+
+        self.assertEqual(
+            {
+                'subjectTitle': 'subject title',
+                'subjectLength': 10,
+                'titleAlignments': [
+                    {
+                        'hsps': [
+                            {
+                                'score': 0,
+                                'readStart': 1,
+                                'readEnd': 2,
+                                'readStartInSubject': 3,
+                                'readEndInSubject': 4,
+                                'subjectStart': 5,
+                                'subjectEnd': 6,
+                                'readFrame': 7,
+                                'subjectFrame': 8,
+                                'identicalCount': 9,
+                                'positiveCount': 10,
+                                'readMatchedSequence': 'aaa',
+                                'subjectMatchedSequence': 'ccc',
+                            },
+                            {
+                                'score': 10,
+                                'readStart': 11,
+                                'readEnd': 12,
+                                'readStartInSubject': 13,
+                                'readEndInSubject': 14,
+                                'subjectStart': 15,
+                                'subjectEnd': 16,
+                                'readFrame': 17,
+                                'subjectFrame': 18,
+                                'identicalCount': 19,
+                                'positiveCount': 20,
+                                'readMatchedSequence': 'ggg',
+                                'subjectMatchedSequence': 'ttt',
+                            },
+                        ],
+                        'read': {
+                            'id': 'the-id',
+                            'quality': None,
+                            'sequence': 'AAA',
+                        },
+                    },
+                ],
+            },
+            titleAlignments.toDict())
+
 
 class TestTitleAlignmentsLSP(TestCase):
     """