{ "cells": [ { "cell_type": "markdown", "id": "4bf9e5a3-df0f-414b-b0ea-25f8ae4e88cd", "metadata": {}, "source": [ "# Run an iterative search to build a HMM for rhodopsins" ] }, { "cell_type": "markdown", "id": "c0c7cc2b-06b4-41af-a0ff-6fdee1e77b98", "metadata": {}, "source": [ "This examples shows an example workflow for building an HMM from a seed sequence through an iterative search in a sequence database, similar to the JackHMMER methodology. In the second half it will also introduce extra capabilities of the Python wrapper allowing the dynamic modification of the search parameters.\n", "\n", "The goal of this example is to build a HMM specific to [halorhodopsins](https://en.wikipedia.org/wiki/Halorhodopsin), a class of light-activated ion pumps found in Archaea. The difficulty of that task comes from their similarity to [bacteriorhodopsins](https://en.wikipedia.org/wiki/Bacteriorhodopsin), which have a comparable conserved structure, but different activity.\n", "\n", "![Classes of rhodopsins](https://els-jbs-prod-cdn.jbs.elsevierhealth.com/cms/attachment/612410/4909711/gr1.jpg)\n", "\n", "*Figure from* [Zhang et al. (2011)](https://www.cell.com/fulltext/S0092-8674(11)01502-9#gr1).\n", "\n", "In a second time we will then build a broader HMM for any kind of rhodopsin, while introducing how to implement a custom hit selection function. This will allow for the automatic exclusion of false positives coming from other organisms, using an additional filter based on taxonomy.\n", "\n", "
\n", "\n", "References\n", " \n", "* [Johnson, L.S., Eddy, S.R. and Portugaly, E. *Hidden Markov model speed heuristic and iterative HMM search procedure.* BMC Bioinformatics 11, 431 (2010). doi:10.1186/1471-2105-11-431](https://doi.org/10.1186/1471-2105-11-431).\n", "* [Zhang, Feng, Vierock J., Yizhar O., Fenno L. E., Tsunoda S., Kianianmomeni A., Prigge M., et al. *The Microbial Opsin Family of Optogenetic Tools*. Cell 147, no. 7 (23 December 2011): 1446–57. doi:10.1016/j.cell.2011.12.004](https://doi.org/10.1016/j.cell.2011.12.004).\n", " \n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "c059c2f0-67eb-4219-b02a-11825f0f3388", "metadata": {}, "outputs": [], "source": [ "import pyhmmer\n", "pyhmmer.__version__" ] }, { "cell_type": "markdown", "id": "2e6a030d-6673-496d-9bce-c6d1813e7b1a", "metadata": {}, "source": [ "## Downloading sequence data\n", "\n", "For this example workflow we will be using SwissProt, directly downloading the sequences in FASTA format from the EMBL-EBI data server. The remote file is compressed, however we can decompress the sequences on the fly using the `gzip` module of the Python standard library." ] }, { "cell_type": "code", "execution_count": null, "id": "702cee55-e68a-4a81-8432-7b60c030164f", "metadata": {}, "outputs": [], "source": [ "import gzip\n", "import urllib.request\n", "\n", "alphabet = pyhmmer.easel.Alphabet.amino()\n", "\n", "url = \"http://ftp.ebi.ac.uk/pub/databases/uniprot/knowledgebase/uniprot_sprot.fasta.gz\"\n", "with urllib.request.urlopen(url) as response:\n", " reader = gzip.open(response, \"rb\")\n", " with pyhmmer.easel.SequenceFile(reader, digital=True, alphabet=alphabet) as seq_file:\n", " swissprot = seq_file.read_block()" ] }, { "cell_type": "markdown", "id": "e61c9fca-d80c-4075-ac83-be78c90b2067", "metadata": {}, "source": [ "The SwissProt sequences all contain the source organism and NCBI Taxonomy accession in their FASTA header, and we will need these pieces of information later. For now, we can build an index that maps a sequence name to its source organism, and another index for taxonomy:" ] }, { "cell_type": "code", "execution_count": null, "id": "594c1165-8b56-4f11-b06e-21270d29fb5e", "metadata": {}, "outputs": [], "source": [ "import re\n", "\n", "taxonomy_by_name = {}\n", "organism_by_name = {}\n", "description_by_name = {}\n", "\n", "for seq in swissprot:\n", " match = re.search(b\"(.*) OS=(.*) OX=(\\d+)\", seq.description)\n", " taxonomy_by_name[seq.name] = int(match.group(3))\n", " organism_by_name[seq.name] = match.group(2)\n", " description_by_name[seq.name] = match.group(1)" ] }, { "cell_type": "markdown", "id": "585238b8-2eb3-4438-8eeb-49bcc8820f98", "metadata": {}, "source": [ "## Running a simple search\n", "\n", "The sequence we will be using for initial similarity search is the [halorhodopsin](https://www.uniprot.org/uniprot/B0R2U4) from *Halobacterium salinarum ATCC 29341*. First let's extract it from SwissProt:" ] }, { "cell_type": "code", "execution_count": null, "id": "1cfcc4c9-8092-4f1a-a0ba-de6c77544209", "metadata": {}, "outputs": [], "source": [ "hop = next(x for x in swissprot if b'B0R2U4' in x.name)" ] }, { "cell_type": "markdown", "id": "d0469e2e-0bc3-457e-9abe-24a2948f4366", "metadata": {}, "source": [ "Now let's see if we can find close homologs to this one protein without losing the specificity. For this, we first need to create a pipeline and to search for the sequence above:" ] }, { "cell_type": "code", "execution_count": null, "id": "6f8cf129-4159-4f1d-b4d0-197c44079afc", "metadata": {}, "outputs": [], "source": [ "pli = pyhmmer.plan7.Pipeline(alphabet)\n", "hits = pli.search_seq(hop, swissprot)" ] }, { "cell_type": "markdown", "id": "0584dbba-c23a-411f-8741-dbd6e6dc99bd", "metadata": {}, "source": [ "We can visualize the score distribution for the hits to see what we should consider as the inclusion threshold:" ] }, { "cell_type": "code", "execution_count": null, "id": "c66cf473-6b9e-40eb-8ce4-5918e36f394c", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", "plt.plot([hit.score for hit in hits], \"o-\")\n", "plt.xlabel(\"Hit rank\")\n", "plt.ylabel(\"Score (bits)\")\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "f5fa632c-b8d0-437f-914d-cb70288d3feb", "metadata": {}, "source": [ "From this graph we can identify three groups of protein in the hits:\n", "\n", "- The two first hits, with very high scores, that are likely very similar orthologs\n", "- A group of more remote orthologs, with a score above 300 bits\n", "- A group of very remote orthologs, under the 300 bits score mark.\n", "\n", "If we went to run an iterative search with the default `Pipeline` parameters, all of these hits would be included in the next iteration, and we may lose the specifity. Let's check the organisms included with a bitscore cutoff of 300:" ] }, { "cell_type": "code", "execution_count": null, "id": "21cbd587-8aa2-472b-b57a-3a350bb037b1", "metadata": {}, "outputs": [], "source": [ "for hit in hits:\n", " if hit.score >= 100.0:\n", " print(f\"{hit.name.decode():<20}\\t{hit.score:5.1f}\\t{description_by_name[hit.name].decode():34}\\t{organism_by_name[hit.name].decode()}\")" ] }, { "cell_type": "markdown", "id": "c4cfac3f-1210-42a2-b3e4-060837ba0d18", "metadata": {}, "source": [ "Looks like all of these are indeed Halobacteria, so we should be fine running an iterative search with these. However, two hits under the 300 bits are still halorhodopsins according to UniProt annotations, so to keep them included we should lower the inclusion threshold to 250 bits." ] }, { "cell_type": "markdown", "id": "55018b7d-d352-4731-95cb-717da9db1198", "metadata": {}, "source": [ "## Running a conservative iterative search\n", "\n", "Let's run an iterative search until it converges with a bitscore cutoff of 250, as we selected previously. For this we need to create a new pipeline, and use the `Pipeline.iterate_seq` method with our query and target sequences:" ] }, { "cell_type": "code", "execution_count": null, "id": "163f8997-be82-4c3d-8334-7fd433d21d25", "metadata": {}, "outputs": [], "source": [ "pli = pyhmmer.plan7.Pipeline(alphabet, incT=250, incdomT=250)\n", "search = pli.iterate_seq(hop, swissprot)" ] }, { "cell_type": "markdown", "id": "775279dd-ab55-4038-9384-e5391453f27c", "metadata": {}, "source": [ "Now that we have an iterator, we can keep iterating until it converges (in this context, converging means that no new hits are found by the newly created HMM in comparison to the previous one):" ] }, { "cell_type": "code", "execution_count": null, "id": "bd2edf8f-d2e1-4502-96dd-e32d4ef3065c", "metadata": {}, "outputs": [], "source": [ "iterations = []\n", "while not search.converged:\n", " iteration = next(search)\n", " iterations.append(iteration)\n", " print(f\"Iteration={iteration.iteration} Hits={len(iteration.hits):2} Included={len(iteration.hits.included):2} Converged={search.converged}\") " ] }, { "cell_type": "markdown", "id": "879297d8-bfed-45ec-a697-68cddbcee5b6", "metadata": {}, "source": [ "We can plot the score distribution for each iteration:" ] }, { "cell_type": "code", "execution_count": null, "id": "1cc8e48b-ca13-4440-b7c7-6b0d5bb5e361", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", "for iteration in iterations:\n", " plt.plot([hit.score for hit in iteration.hits], \"o-\", label=f\"Iteration {iteration.iteration}\")\n", "\n", "plt.legend()\n", "plt.xlabel(\"Hit rank\")\n", "plt.ylabel(\"Score (bits)\")\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "e032ed20-c629-4d31-bbc5-c27f12fb7228", "metadata": {}, "source": [ "With a high cutoff, the search converges quite quickly, but adds 1 new sequence to the ones that were included in the simple search. Let's see which sequences have been included:" ] }, { "cell_type": "code", "execution_count": null, "id": "2f3b461c-46af-4ab6-b6f3-c85f5be654a7", "metadata": {}, "outputs": [], "source": [ "for hit in iterations[-1].hits:\n", " if hit.included:\n", " print(f\"{hit.name.decode():<20}\\t{hit.score:5.1f}\\t{description_by_name[hit.name].decode():34}\\t{organism_by_name[hit.name][:60].decode()}\")" ] }, { "cell_type": "markdown", "id": "2bcc83f3-2de3-4d65-995a-9d469257e276", "metadata": {}, "source": [ "The last included hit, the Cruxhalorhodopsin-1 fragment, was definitely under inclusion threshold in the simple search. However, the subsequent iterations helped train a new HMM that was able to rescue it, so that it's now included in the alignment. Let's have a look at the excluded hits, to make sure we are not missing anything:" ] }, { "cell_type": "code", "execution_count": null, "id": "e913ba65-55fb-4f00-9163-8bb2aa5b0608", "metadata": {}, "outputs": [], "source": [ "for hit in iterations[-1].hits:\n", " if not hit.included and hit.score > 50.0:\n", " print(f\"{hit.name.decode():<20}\\t{hit.score:5.1f}\\t{description_by_name[hit.name].decode():34}\\t{organism_by_name[hit.name][:60].decode()}\")" ] }, { "cell_type": "markdown", "id": "a2bb8f8f-cbfe-4fe4-827e-22384576c432", "metadata": {}, "source": [ "It looks like we are not missing any halorhodopsin in our hits, so looks like we managed to build a sensitive HMM." ] }, { "cell_type": "markdown", "id": "74982f8a-cddc-414c-b795-f8eabbb20985", "metadata": {}, "source": [ "## Running an iterative search with custom selection\n", "\n", "The previous iterative search helped us create a HMM only covering halorhodopsins. However, let's try to build a very broad HMM for rhodopsin search in Halobacteria. For this round, we do not care about functional specificity, but we care about taxonomy. In particular, looking at the hits from the last iteration, there are some fungal protein among the archeal ones (the [opsin](https://www.uniprot.org/uniprot/Q9UW81) from *Neurospora crassa*, for instance). To build a HMM specific to archeal proteins we cannot rely on a simple score cutoff, so let's instead build a dedicated function to select hits between each iteration based on taxonomy." ] }, { "cell_type": "markdown", "id": "03f4ab7d-7e74-44b9-9250-5d653387b037", "metadata": {}, "source": [ "For handling the taxonomy we will be using [taxopy](https://github.com/apcamargo/taxopy), a Python library for reading NCBI Taxonomy files. Let's start by loading the files so that we have a taxonomy database at hand:" ] }, { "cell_type": "code", "execution_count": null, "id": "0de9ea6b-fde6-49c1-91ac-70bcf65d13d0", "metadata": {}, "outputs": [], "source": [ "import taxopy\n", "taxdb = taxopy.TaxDb(keep_files=False)" ] }, { "cell_type": "markdown", "id": "7a1670ad-f8c7-4cf4-9601-48c0adfecfc1", "metadata": {}, "source": [ "Now let's write a selection function. We still rely on the score inclusion criterion, but we also will want to exclude any hit from an organism that is not in the Halobacteria class." ] }, { "cell_type": "code", "execution_count": null, "id": "ee932652-6d1a-449e-bce1-5a91dc267307", "metadata": {}, "outputs": [], "source": [ "def select_halobacteria_hits(top_hits):\n", " for hit in top_hits:\n", " taxid = taxonomy_by_name[hit.name]\n", " taxon = taxopy.Taxon(taxid, taxdb)\n", " if hit.included and taxon.rank_name_dictionary.get('class') != \"Halobacteria\":\n", " hit.dropped = True" ] }, { "cell_type": "markdown", "id": "1e4aa39c-9b5a-4839-b1e4-3ccd902be377", "metadata": {}, "source": [ "Now run the iterative search with a lower inclusion threshold, and the selection function we just made:" ] }, { "cell_type": "code", "execution_count": null, "id": "9dbed2b9-644d-47f1-abbd-c2f96957dd17", "metadata": {}, "outputs": [], "source": [ "pli = pyhmmer.plan7.Pipeline(alphabet, incT=100, incdomT=100)\n", "search = pli.iterate_seq(hop, swissprot, select_hits=select_halobacteria_hits)" ] }, { "cell_type": "code", "execution_count": null, "id": "8d16a70b-539e-475a-9e55-1ae2360cece4", "metadata": {}, "outputs": [], "source": [ "iterations = []\n", "while not search.converged:\n", " iteration = next(search)\n", " iterations.append(iteration)\n", " worst_score = min(hit.score for hit in iteration.hits if hit.included)\n", " print(f\"Iteration={iteration.iteration} Hits={len(iteration.hits):3} Included={len(iteration.hits.included):3} Converged={search.converged}\") " ] }, { "cell_type": "markdown", "id": "8ce15cde-df16-4756-bccc-b79dac34682c", "metadata": {}, "source": [ "The search converges in 4 iterations. Let's have a look at the score distribution:" ] }, { "cell_type": "code", "execution_count": null, "id": "98283f37-fefd-4ec4-8a52-d7be49b17da9", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", "for iteration in iterations:\n", " plt.plot([hit.score for hit in iteration.hits], \"o-\", label=f\"Iteration {iteration.iteration}\")\n", "\n", "plt.legend()\n", "plt.xlabel(\"Hit rank\")\n", "plt.ylabel(\"Score (bits)\")\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "d2d656c7-000b-47c5-af10-cb35fdd83f02", "metadata": {}, "source": [ "We can make sure that we are not excluding any rhodopsin from Halobacteria by having a look at the excluded hits from the last iteration:" ] }, { "cell_type": "code", "execution_count": null, "id": "8ace1941-c9ec-4c70-8244-832acaf65b4e", "metadata": {}, "outputs": [], "source": [ "for hit in iterations[-1].hits:\n", " if not hit.included:\n", " print(f\"{hit.name.decode():<24}\\t{hit.score:5.1f}\\t{description_by_name[hit.name].decode():64}\\t{organism_by_name[hit.name][:60].decode()}\")" ] }, { "cell_type": "markdown", "id": "dfdc754d-9362-4847-ac07-99038fbf1515", "metadata": {}, "source": [ "Notice that although it is above inclusion threshold, the [opsin](https://www.uniprot.org/uniprot/Q9UW81) from *Neurospora crassa* stays excluded, as expected from our selection function. Regarding the included targets in the final hits, they should only contain rhodopsins from Halobacteria:" ] }, { "cell_type": "code", "execution_count": null, "id": "6b4c2c80-d91a-45ae-b174-ffd454618a52", "metadata": {}, "outputs": [], "source": [ "for hit in iterations[-1].hits:\n", " if hit.included:\n", " print(f\"{hit.name.decode():<24}\\t{hit.score:5.1f}\\t{description_by_name[hit.name].decode():34}\\t{organism_by_name[hit.name].decode()}\")" ] }, { "cell_type": "markdown", "id": "1f308930-2a37-4a56-b6e1-c82100ab24ba", "metadata": {}, "source": [ "## Recording bitscore cutoffs\n", "\n", "A useful feature of HMMER is the possibility to use HMM-specific bitscore cutoffs when searching sequences with a HMM, instead of relying on E-value cutoffs. Three cutoffs categories are available:\n", "\n", "- *Noise cutoffs (NC)*: The least stringent of the 3 cutoffs, normally set as the highest score seen for negative sequences when gathering members of full alignments.\n", "- *Gathering cutoffs (GA)*: More stringent than noise cutoffs, normally computed as the cutoffs used when gathering members of full alignments.\n", "- *Trusted cutoffs (TC)*: The most stringent of the 3 cutoffs, normally set according to the lowest scores seen for true homologous sequences that were above the GA gathering thresholds, when gathering members of full alignments." ] }, { "cell_type": "code", "execution_count": null, "id": "14c5f093-88c7-4522-87cc-0fe5f2980092", "metadata": {}, "outputs": [], "source": [ "hmm = iterations[-1].hmm" ] }, { "cell_type": "markdown", "id": "3872770c-8d25-4e87-85ef-b11685dd4217", "metadata": {}, "source": [ "Let's first build noise cutoffs based on the negative hits: we can use the highest scoring excluded sequence." ] }, { "cell_type": "code", "execution_count": null, "id": "c8b59774-2d6a-4e22-873f-941fcabbca70", "metadata": {}, "outputs": [], "source": [ "from math import ceil\n", "\n", "noise_score = max(hit.score for hit in iterations[-1].hits if not hit.included)\n", "noise_domscore = max(hit.best_domain.score for hit in iterations[-1].hits if not hit.included)\n", "hmm.cutoffs.noise = ceil(noise_score), ceil(noise_domscore)" ] }, { "cell_type": "markdown", "id": "0981a1ca-ae1b-451d-8e89-b941f30ef980", "metadata": {}, "source": [ "For gathering, we can use the lowest scoring included sequence:" ] }, { "cell_type": "code", "execution_count": null, "id": "faf1cdb0-4733-4049-b414-ad7812f4449b", "metadata": {}, "outputs": [], "source": [ "from math import floor\n", "\n", "gathering_score = min(hit.score for hit in iterations[-1].hits if hit.included)\n", "gathering_domscore = min(hit.best_domain.score for hit in iterations[-1].hits if hit.included)\n", "hmm.cutoffs.gathering = floor(gathering_score), floor(gathering_domscore)" ] }, { "cell_type": "markdown", "id": "3f321904-0032-41d6-9bd8-3327054e5b8b", "metadata": {}, "source": [ "For trusted, it's a little bit less obvious. If you look at the bitscore distribution from the last iteration, we can see 3 distinct group of hits, one in the 390-350 range, one in the 315-260 range, and one in the 235-200 range. We may want to set the trusted cutoffs so that the only the hits in the top range can be trusted. " ] }, { "cell_type": "code", "execution_count": null, "id": "692d7c07-4724-45ac-ae87-29b37a723b72", "metadata": {}, "outputs": [], "source": [ "from math import floor\n", "\n", "i = max(i for i, hit in enumerate(iterations[-1].hits) if hit.score > 350)\n", "hmm.cutoffs.trusted = floor(iterations[-1].hits[i].score), floor(iterations[-1].hits[i].best_domain.score)" ] }, { "cell_type": "markdown", "id": "3c945521-7d06-42be-ae0b-f4d801ac3a26", "metadata": {}, "source": [ "Otherwise, for a more unsupervised approach, we could try to extract a cutoffs by selecting the score of the hits where the derivative is at its maximum:" ] }, { "cell_type": "code", "execution_count": null, "id": "275c7bec-4e36-4c5a-ad46-a059b3444db1", "metadata": {}, "outputs": [], "source": [ "diff = [\n", " iterations[-1].hits[i-1].score - iterations[-1].hits[i].score\n", " for i in range(1, len(iterations[-1].hits))\n", " if iterations[-1].hits[i-1].included\n", "]" ] }, { "cell_type": "code", "execution_count": null, "id": "343d6df6-4d15-4650-8548-e2322be81bc7", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", "plt.plot(diff)\n", "plt.ylabel(\"dscore\")\n", "plt.xlabel(\"Hit rank\")\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "id": "abb56159-5b1e-42b3-9e8c-f155a3547fc8", "metadata": {}, "outputs": [], "source": [ "from math import floor\n", "\n", "argmax = max(range(len(diff)), key=diff.__getitem__)\n", "hit = iterations[-1].hits[argmax]\n", "hmm.cutoffs.trusted = floor(hit.score), floor(hit.best_domain.score)" ] }, { "cell_type": "markdown", "id": "608b6e8d-d333-4764-b2df-230a77971d97", "metadata": {}, "source": [ "## Saving the HMM\n", "\n", "Now that we are done creating the HMM, we can save it for later use:" ] }, { "cell_type": "code", "execution_count": null, "id": "35ea77f9-2067-4ca8-907d-c2553913de6c", "metadata": {}, "outputs": [], "source": [ "hmm.name = b\"Rhodopsin\"\n", "hmm.description = None\n", "hmm.accession = None" ] }, { "cell_type": "code", "execution_count": null, "id": "7f6d917d-049d-4260-b390-75766b921fdb", "metadata": {}, "outputs": [], "source": [ "with open(\"rhodopsin.hmm\", \"wb\") as dst_file:\n", " hmm.write(dst_file)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.9" } }, "nbformat": 4, "nbformat_minor": 5 }