Homology search with MMseqs2#

MMseqs2 is a software suite for fast searching and clustering of large sequence sets. Its interface is the MMseqsApp class from the biotite.application_v2.mmseqs subpackage. In contrast to the MSA programs from the previous chapter, MMseqs2 is not a single command, but a collection of subcommands that read and write databases. Accordingly, MMseqsApp has one method per supported subcommand and each of them returns a Future that resolves to a Database object.

Creating databases#

We start with some homologous sequences to search in: a few globins from different species, plus insulin, which is unrelated to the globins. Furthermore, we need a query sequence, in our case the human hemoglobin subunit delta. Both are fetched from NCBI Entrez as FASTA files.

from tempfile import NamedTemporaryFile, gettempdir
import biotite.database.entrez as entrez

globin_uids = [
    "P69905", "P01942", "P01966",  # hemoglobin subunit alpha
    "P68871", "P02088", "P02070",  # hemoglobin subunit beta
    "P02144", "P02185",            # myoglobin
    "P01308",                      # insulin
]
temp_file = NamedTemporaryFile(suffix=".fasta", delete=False)
target_path = entrez.fetch_single_file(
    globin_uids, temp_file.name, "protein", "fasta"
)
query_path = entrez.fetch("P02042", gettempdir(), "fa", "protein", "fasta")

MMseqs2 does not work on FASTA files directly, but on its own database format. Hence, the first step is to convert the files into a SequenceDatabase via MMseqsApp.create_db().

import biotite.application_v2.mmseqs as mmseqs

app = mmseqs.MMseqsApp()
target_db = app.create_db(target_path).result()
query_db = app.create_db(query_path).result()
print(type(target_db).__name__)
SequenceDatabase

A Database is a directory containing the files MMseqs2 wrote. By default, this directory is temporary and is deleted when the object is discarded. If you want to keep a database, for example to search it again in a later session, use Database.copy() or Database.move() to put it into a persistent directory.

Searching a database#

Now we search the target database for sequences homologous to the query with MMseqsApp.search(). By default, MMseqs2 only computes the score of each hit. The a option enables backtracing, i.e. the alignment path is stored as well, which we will need later on to get the actual alignments. Like in the previous chapter, the command line options are passed as keyword arguments.

alignment_db = app.search(query_db, target_db, a=True).result()
print(type(alignment_db).__name__)
AlignmentDatabase

The hits are again stored in a database, an AlignmentDatabase. To make them human-readable, MMseqs2 provides the subcommand to convert them into a BLAST-like table, available via MMseqsApp.convert_alignments(). If no output path is given, the table is written into a temporary file.

table_file = app.convert_alignments(
    alignment_db,
    format_mode=mmseqs.AlignmentFormatMode.BLAST_TABLE_WITH_HEADERS,
).result()
with open(table_file.name) as file:
    print(file.read())
table_file.close()
query	target	fident	alnlen	mismatch	gapopen	qstart	qend	tstart	tend	evalue	bits
P02042.2	P68871.2	0.931	147	10	0	1	147	1	147	4.966E-95	291
P02042.2	P02070.1	0.833	144	24	0	4	147	2	145	3.184E-82	254
P02042.2	P02088.2	0.782	147	32	0	1	147	1	147	1.770E-79	246
P02042.2	P69905.2	0.434	145	74	3	4	146	3	141	2.229E-34	116
P02042.2	P01942.2	0.400	145	79	2	4	146	3	141	4.727E-32	109
P02042.2	P01966.2	0.420	145	76	3	4	146	3	141	6.477E-32	109

Only the hemoglobin subunits are found: The sequence identity of the query to the beta chains is high, to the alpha chains still moderate, but the myoglobins are too dissimilar for a sequence search with default parameters. Also note that the identifiers are the accessions MMseqs2 parsed from the FASTA headers.

Working with Biotite objects#

So far we have used the subcommands as they are: with files as input and output. However, usually you have Sequence objects at hand and want to get Alignment objects back. The biotite.application_v2.mmseqs subpackage provides utility functions for this purpose. To begin with, create_database_from_sequences() creates a SequenceDatabase from a dictionary of sequences. This way we also get to choose the identifiers ourselves.

import biotite.sequence.io.fasta as fasta

fasta_file = fasta.FastaFile.read(target_path)
target_sequences = {
    # Use the UniProt entry name from the header as identifier,
    # e.g. 'HBA_HUMAN'
    header.split("|")[2].split()[0]: sequence
    for header, sequence in fasta.get_sequences(fasta_file).items()
}
query_sequence = fasta.get_sequence(fasta.FastaFile.read(query_path))

target_db = mmseqs.create_database_from_sequences(app, target_sequences)
query_db = mmseqs.create_database_from_sequences(
    app, {"HBD_HUMAN": query_sequence}
)
alignment_db = app.search(query_db, target_db, a=True).result()

Instead of parsing the BLAST-like table, get_alignment_table() extracts the columns of interest.

table = mmseqs.get_alignment_table(
    app, alignment_db, ["query", "target", "pident", "evalue"]
)
for target, identity, e_value in zip(
    table["target"], table["pident"], table["evalue"]
):
    print(f"{target:12}{identity:>8}{e_value:>12}")
HBB_HUMAN     93.100   4.966E-95
HBB_BOVIN     83.300   3.184E-82
HBB1_MOUSE    78.200   1.770E-79
HBA_HUMAN     43.400   2.229E-34
HBA_MOUSE     40.000   4.727E-32
HBA_BOVIN     42.000   6.477E-32

Finally, get_alignments() reconstructs an Alignment for each hit, which is the reason we enabled backtracing above. The alignments are ordered by increasing E-value, so the first one is the best hit.

alignments = mmseqs.get_alignments(app, alignment_db)
print(list(alignments.keys()))
print()
best_alignment = alignments["HBD_HUMAN", "HBB_HUMAN"]
print(best_alignment)
[('HBD_HUMAN', 'HBB_HUMAN'), ('HBD_HUMAN', 'HBB_BOVIN'), ('HBD_HUMAN', 'HBB1_MOUSE'), ('HBD_HUMAN', 'HBA_HUMAN'), ('HBD_HUMAN', 'HBA_MOUSE'), ('HBD_HUMAN', 'HBA_BOVIN')]

MVHLTPEEKTAVNALWGKVNVDAVGGEALGRLLVVYPWTQRFFESFGDLSSPDAVMGNPKVKAHGKKVLG
MVHLTPEEKSAVTALWGKVNVDEVGGEALGRLLVVYPWTQRFFESFGDLSTPDAVMGNPKVKAHGKKVLG

AFSDGLAHLDNLKGTFSQLSELHCDKLHVDPENFRLLGNVLVCVLARNFGKEFTPQMQAAYQKVVAGVAN
AFSDGLAHLDNLKGTFATLSELHCDKLHVDPENFRLLGNVLVCVLAHHFGKEFTPPVQAAYQKVVAGVAN

ALAHKYH
ALAHKYH

Clustering#

Besides searching, the other main purpose of MMseqs2 is clustering sequences by their similarity, in order to reduce redundancy in a sequence set. This is done by MMseqsApp.cluster(), that returns a ClusterDatabase, and get_clusters(), that maps the representative sequence of each cluster to its members. Here we cluster the globins at a minimum sequence identity of 50 %.

cluster_db = app.cluster(target_db, min_seq_id=0.5).result()
clusters = mmseqs.get_clusters(app, cluster_db)
for representative, members in clusters.items():
    print(f"{representative}: {members}")
HBA_BOVIN: ['HBA_BOVIN', 'HBA_HUMAN', 'HBA_MOUSE']
HBB_HUMAN: ['HBB_HUMAN', 'HBB1_MOUSE', 'HBB_BOVIN']
MYG_PHYMC: ['MYG_PHYMC', 'MYG_HUMAN']
INS_HUMAN: ['INS_HUMAN']

As expected, the alpha chains, the beta chains and the myoglobins form separate clusters, while insulin is on its own.

Increasing the sensitivity with profiles#

Remember that the myoglobins were not found by the sequence search. A common solution for detecting such distant homologs is a profile search: The hits from the first search are summarized into a sequence profile via MMseqsApp.result_to_profile(), that captures the conservation of each position of the query. The resulting ProfileDatabase can be used as query for another search in place of the sequence database. Combined with a high sensitivity, requested via the s option, the myoglobins are found this time.

profile_db = app.result_to_profile(alignment_db).result()
profile_alignment_db = app.search(
    profile_db, target_db, a=True, s=7.5
).result()
table = mmseqs.get_alignment_table(
    app, profile_alignment_db, ["target", "pident", "evalue"]
)
for target, identity, e_value in zip(
    table["target"], table["pident"], table["evalue"]
):
    print(f"{target:12}{identity:>8}{e_value:>12}")
HBB_HUMAN     93.100   3.303E-86
HBB1_MOUSE    78.200   5.220E-84
HBB_BOVIN     83.300   2.321E-82
HBA_MOUSE     40.000   2.403E-72
HBA_HUMAN     42.700   3.295E-72
HBA_BOVIN     41.300   1.062E-70
MYG_HUMAN     26.200   3.773E-15
MYG_PHYMC     24.100   1.205E-11

If you are interested in the profile itself, get_matrix_from_profile() converts it into a position-specific SubstitutionMatrix.

Structure search with Foldseek#

Foldseek applies the same concepts to protein structures: It converts structures into sequences of structural states and then uses the machinery of MMseqs2 to search and cluster them. Consequently, its interface FoldseekApp provides the same methods and databases, only that create_database_from_structures() creates the database from AtomArray objects instead of sequences.