From 21043e0726ab0aa432b0ee37e531e14ef5f712e4 Mon Sep 17 00:00:00 2001 From: Andrew Riha Date: Mon, 5 Apr 2021 23:18:46 -0700 Subject: [PATCH 1/3] Mock downloads --- .github/workflows/ci.yml | 17 +--- CONTRIBUTING.rst | 6 ++ tests/__init__.py | 13 +++ tests/test_lineage.py | 198 +++++++++++++++++++++++++++++++++++++-- tests/test_resources.py | 158 +++++++++++++++++++++++++++++-- 5 files changed, 361 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d182bea..f7eb3a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,9 +74,6 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Set default for downloads - shell: bash - run: echo "DOWNLOADS_ENABLED=false" >> $GITHUB_ENV - name: Determine if downloads are enabled for this job # for testing, limit downloads from the resource servers to only the selected job for # PRs and the master branch; note that the master branch is tested weekly via `cron`, @@ -94,21 +91,9 @@ jobs: echo "DOWNLOADS_ENABLED=true" >> $GITHUB_ENV fi - name: Install dependencies - shell: bash - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: | - pip install pytest-cov awscli + pip install pytest-cov pip install . - if [[ $DOWNLOADS_ENABLED == "false" ]]; then - # use cached resources on Amazon S3 - aws s3 cp s3://lineage-resources/resources.tar.gz resources.tar.gz - if [[ -f resources.tar.gz ]]; then - tar -xzf resources.tar.gz - rm resources.tar.gz - fi - fi - name: Ensure Python and source code are on same drive (Windows) if: ${{ matrix.os == 'windows-latest' }} shell: cmd diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ab648a6..9130af0 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -56,6 +56,12 @@ To set up ``lineage`` for local development: $ pipenv run pytest --cov-report=html --cov=lineage tests + .. note:: Downloads during tests are disabled by default. To enable downloads, set + the environment variable ``DOWNLOADS_ENABLED=true``. + + .. note:: If you receive errors when running the tests, you may need to specify the temporary + directory with an environment variable, e.g., ``TMPDIR="/path/to/tmp/dir"``. + .. note:: After running the tests, a coverage report can be viewed by opening ``htmlcov/index.html`` in a browser. diff --git a/tests/__init__.py b/tests/__init__.py index db467d2..f420367 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -142,3 +142,16 @@ def generic_snps(self): pos=list(range(101, 109)), genotype=["AA", "CC", "GG", "TT", np.nan, "GC", "TC", "AT"], ) + + @property + def downloads_enabled(self): + """ Property indicating if downloads are enabled. + + Only download from external resources when an environment variable named + "DOWNLOADS_ENABLED" is set to "true". + + Returns + ------- + bool + """ + return True if os.getenv("DOWNLOADS_ENABLED") == "true" else False diff --git a/tests/test_lineage.py b/tests/test_lineage.py index 4be13f3..4792e96 100644 --- a/tests/test_lineage.py +++ b/tests/test_lineage.py @@ -24,6 +24,8 @@ """ import os +import tempfile +from unittest.mock import Mock, mock_open, patch import warnings import numpy as np @@ -43,15 +45,142 @@ def get_discordant_snps(self, ind, df): return ind - def test_download_example_datasets(self): - paths = self.l.download_example_datasets() + def _generate_test_genetic_map_HapMapII_GRCh37( + self, + chrom="1", + pos=(1, 111700001), + rate=(140.443968 / (111700001 / 1e6), 0), + map_cMs=(0.000000, 140.443968), + **kwargs, + ): + return { + chrom: pd.DataFrame({"pos": pos, "rate": rate, "map": map_cMs,}), + } + + def _generate_test_cytoBand_hg19(self): + return pd.DataFrame( + { + "chrom": ["1"], + "start": [0], + "end": [111800001], + "name": ["test"], + "gie_stain": ["gneg"], + } + ) + + def _generate_test_gene_dfs( + self, + chrom="1", + len1=3811, + len2=4000, + txStart1=1000000, + txEnd1=2000000, + txStart2=111600000, + txEnd2=111800000, + **kwargs, + ): + diff = len2 - len1 + txStart = [txStart1] * len1 + txStart.extend([txStart2] * diff) + txEnd = [txEnd1] * len1 + txEnd.extend([txEnd2] * diff) + + kg = pd.DataFrame( + { + "name": [f"g{i}" for i in range(len2)], + "chrom": [chrom] * len2, + "strand": ["+"] * len2, + "txStart": txStart, + "txEnd": txEnd, + "proteinID": ["g"] * len2, + } + ) + kg.set_index("name", inplace=True) + + kgXref = pd.DataFrame( + { + "kgID": [f"g{i}" for i in range(len2)], + "geneSymbol": ["s"] * len2, + "refseq": ["s"] * len2, + "description": ["s"] * len2, + } + ) + kgXref.set_index("kgID", inplace=True) + return kg, kgXref + + def run_find_shared_dna_test_X(self, f): + self.run_find_shared_dna_test( + f, + chrom="X", + pos=(1, 2695340, 154929412, 155270560), + rate=( + 20.837792 / (2695340 / 1e6), + 180.837755 / ((154929412 - 2695340) / 1e6), + 0.347344 / ((155270560 - 154929412) / 1e6), + 0, + ), + map_cMs=( + 0.000000, + 20.837792, + 20.837792 + 180.837755, + 20.837792 + 180.837755 + 0.347344, + ), + len1=54, + len2=3022, + txStart1=2400000, + txEnd1=2600000, + txStart2=150000000, + txEnd2=155000000, + ) + + def run_find_shared_dna_test(self, f, **kwargs): + if self.downloads_enabled: + f() + else: + genetic_map = self._generate_test_genetic_map_HapMapII_GRCh37(**kwargs) + cytoband = self._generate_test_cytoBand_hg19() + kg, kgXref = self._generate_test_gene_dfs(**kwargs) + + with patch( + "lineage.resources.Resources.get_genetic_map_HapMapII_GRCh37", + Mock(return_value=genetic_map), + ): + with patch( + "lineage.resources.Resources.get_cytoBand_hg19", + Mock(return_value=cytoband), + ): + with patch( + "lineage.resources.Resources.get_knownGene_hg19", + Mock(return_value=kg), + ): + with patch( + "lineage.resources.Resources.get_kgXref_hg19", + Mock(return_value=kgXref), + ): + f() + + def _check_example_paths(self, paths): for path in paths: if path is None or not os.path.exists(path): warnings.warn("Example dataset(s) not currently available") return - assert True + def test_download_example_datasets(self): + if self.downloads_enabled: + paths = self.l.download_example_datasets() + self._check_example_paths(paths) + else: + with tempfile.TemporaryDirectory() as tmpdir: + self.l._resources._resources_dir = tmpdir + + # use a temporary directory for test resource data + with patch("urllib.request.urlopen", mock_open(read_data=b"")): + paths = self.l.download_example_datasets() + + self._check_example_paths(paths) + + self.l._resources._resources_dir = "resources" def test_find_discordant_snps(self): df = pd.read_csv( @@ -142,6 +271,9 @@ def test_find_discordant_snps(self): assert os.path.exists("output/discordant_snps_ind1_ind2_ind3_GRCh37.csv") def test_find_shared_dna_one_ind(self): + self.run_find_shared_dna_test(self._test_find_shared_dna_one_ind) + + def _test_find_shared_dna_one_ind(self): ind1 = self.simulate_snps(self.l.create_individual("ind1")) d = self.l.find_shared_dna([ind1], shared_genes=True) @@ -159,6 +291,9 @@ def test_find_shared_dna_one_ind(self): assert not os.path.exists("output/shared_dna_ind1.png") def test_find_shared_dna_two_chrom_shared(self): + self.run_find_shared_dna_test(self._test_find_shared_dna_two_chrom_shared) + + def _test_find_shared_dna_two_chrom_shared(self): ind1 = self.simulate_snps(self.l.create_individual("ind1")) ind2 = self.simulate_snps(self.l.create_individual("ind2")) @@ -179,6 +314,11 @@ def test_find_shared_dna_two_chrom_shared(self): assert os.path.exists("output/shared_dna_ind1_ind2.png") def test_find_shared_dna_two_chrom_shared_three_ind(self): + self.run_find_shared_dna_test( + self._test_find_shared_dna_two_chrom_shared_three_ind + ) + + def _test_find_shared_dna_two_chrom_shared_three_ind(self): ind1 = self.simulate_snps(self.l.create_individual("ind1")) ind2 = self.simulate_snps(self.l.create_individual("ind2")) ind3 = self.simulate_snps(self.l.create_individual("ind3")) @@ -202,6 +342,11 @@ def test_find_shared_dna_two_chrom_shared_three_ind(self): assert os.path.exists("output/shared_dna_ind1_ind2_ind3.png") def test_find_shared_dna_two_chrom_shared_no_output(self): + self.run_find_shared_dna_test( + self._test_find_shared_dna_two_chrom_shared_no_output + ) + + def _test_find_shared_dna_two_chrom_shared_no_output(self): ind1 = self.simulate_snps(self.l.create_individual("ind1")) ind2 = self.simulate_snps(self.l.create_individual("ind2")) @@ -222,6 +367,9 @@ def test_find_shared_dna_two_chrom_shared_no_output(self): assert not os.path.exists("output/shared_dna_ind1_ind2.png") def test_find_shared_dna_one_chrom_shared(self): + self.run_find_shared_dna_test(self._test_find_shared_dna_one_chrom_shared) + + def _test_find_shared_dna_one_chrom_shared(self): ind1 = self.simulate_snps(self.l.create_individual("ind1")) ind2 = self.simulate_snps( self.l.create_individual("ind2"), complement_genotype_one_chrom=True @@ -243,6 +391,11 @@ def test_find_shared_dna_one_chrom_shared(self): assert os.path.exists("output/shared_dna_ind1_ind2.png") def test_find_shared_dna_one_chrom_shared_three_ind(self): + self.run_find_shared_dna_test( + self._test_find_shared_dna_one_chrom_shared_three_ind + ) + + def _test_find_shared_dna_one_chrom_shared_three_ind(self): ind1 = self.simulate_snps(self.l.create_individual("ind1")) ind2 = self.simulate_snps( self.l.create_individual("ind2"), complement_genotype_one_chrom=True @@ -269,6 +422,11 @@ def test_find_shared_dna_one_chrom_shared_three_ind(self): assert os.path.exists("output/shared_dna_ind1_ind2_ind3.png") def test_find_shared_dna_X_chrom_two_individuals_male(self): + self.run_find_shared_dna_test_X( + self._test_find_shared_dna_X_chrom_two_individuals_male + ) + + def _test_find_shared_dna_X_chrom_two_individuals_male(self): ind1 = self.simulate_snps( self.l.create_individual("ind1"), chrom="X", @@ -292,8 +450,12 @@ def test_find_shared_dna_X_chrom_two_individuals_male(self): assert len(d["two_chrom_shared_genes"]) == 54 assert len(d["one_chrom_discrepant_snps"]) == 0 assert len(d["two_chrom_discrepant_snps"]) == 0 - np.testing.assert_allclose(d["one_chrom_shared_dna"].loc[1]["cMs"], 202.022891) - np.testing.assert_allclose(d["two_chrom_shared_dna"].loc[1]["cMs"], 20.837792) + np.testing.assert_allclose( + d["one_chrom_shared_dna"].loc[1]["cMs"], 202.022891, rtol=1e-5, + ) + np.testing.assert_allclose( + d["two_chrom_shared_dna"].loc[1]["cMs"], 20.837792, rtol=1e-3, + ) assert os.path.exists("output/shared_dna_one_chrom_ind1_ind2_GRCh37.csv") assert os.path.exists("output/shared_dna_two_chroms_ind1_ind2_GRCh37.csv") assert os.path.exists("output/shared_genes_one_chrom_ind1_ind2_GRCh37.csv") @@ -301,6 +463,11 @@ def test_find_shared_dna_X_chrom_two_individuals_male(self): assert os.path.exists("output/shared_dna_ind1_ind2.png") def test_find_shared_dna_X_chrom_two_individuals_female(self): + self.run_find_shared_dna_test_X( + self._test_find_shared_dna_X_chrom_two_individuals_female + ) + + def _test_find_shared_dna_X_chrom_two_individuals_female(self): ind1 = self.simulate_snps( self.l.create_individual("ind1"), chrom="X", @@ -322,8 +489,12 @@ def test_find_shared_dna_X_chrom_two_individuals_female(self): assert len(d["two_chrom_shared_genes"]) == 3022 assert len(d["one_chrom_discrepant_snps"]) == 0 assert len(d["two_chrom_discrepant_snps"]) == 0 - np.testing.assert_allclose(d["one_chrom_shared_dna"].loc[1]["cMs"], 202.022891) - np.testing.assert_allclose(d["two_chrom_shared_dna"].loc[1]["cMs"], 202.022891) + np.testing.assert_allclose( + d["one_chrom_shared_dna"].loc[1]["cMs"], 202.022891, rtol=1e-5, + ) + np.testing.assert_allclose( + d["two_chrom_shared_dna"].loc[1]["cMs"], 202.022891, rtol=1e-5, + ) assert os.path.exists("output/shared_dna_one_chrom_ind1_ind2_GRCh37.csv") assert os.path.exists("output/shared_dna_two_chroms_ind1_ind2_GRCh37.csv") assert os.path.exists("output/shared_genes_one_chrom_ind1_ind2_GRCh37.csv") @@ -331,6 +502,11 @@ def test_find_shared_dna_X_chrom_two_individuals_female(self): assert os.path.exists("output/shared_dna_ind1_ind2.png") def test_find_shared_dna_two_chrom_shared_discrepant_snps(self): + self.run_find_shared_dna_test( + self._test_find_shared_dna_two_chrom_shared_discrepant_snps + ) + + def _test_find_shared_dna_two_chrom_shared_discrepant_snps(self): # simulate discrepant SNPs so that stitching of adjacent shared DNA segments is performed ind1 = self.simulate_snps(self.l.create_individual("ind1")) ind2 = self.simulate_snps( @@ -356,6 +532,9 @@ def test_find_shared_dna_two_chrom_shared_discrepant_snps(self): assert os.path.exists("output/shared_dna_ind1_ind2.png") def test_find_shared_dna_no_shared_dna(self): + self.run_find_shared_dna_test(self._test_find_shared_dna_no_shared_dna) + + def _test_find_shared_dna_no_shared_dna(self): ind1 = self.simulate_snps(self.l.create_individual("ind1")) ind2 = self.simulate_snps( self.l.create_individual("ind2"), complement_genotype_two_chroms=True @@ -376,6 +555,11 @@ def test_find_shared_dna_no_shared_dna(self): assert os.path.exists("output/shared_dna_ind1_ind2.png") def test_find_shared_dna_no_shared_dna_three_ind(self): + self.run_find_shared_dna_test( + self._test_find_shared_dna_no_shared_dna_three_ind + ) + + def _test_find_shared_dna_no_shared_dna_three_ind(self): ind1 = self.simulate_snps(self.l.create_individual("ind1")) ind2 = self.simulate_snps( self.l.create_individual("ind2"), complement_genotype_two_chroms=True diff --git a/tests/test_resources.py b/tests/test_resources.py index b59a6a3..c4ff75d 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -23,47 +23,189 @@ """ +import gzip +import io import os +import tarfile +import tempfile +from unittest.mock import mock_open, patch import warnings +import pandas as pd + from lineage.resources import Resources from tests import BaseLineageTestCase class TestResources(BaseLineageTestCase): def setUp(self): - self.resource = Resources(resources_dir="resources") self.del_output_dir_helper() + def _reset_resource(self): + self.resource._genetic_map_HapMapII_GRCh37 = {} + self.resource._cytoBand_hg19 = pd.DataFrame() + self.resource._knownGene_hg19 = pd.DataFrame() + self.resource._kgXref_hg19 = pd.DataFrame() + + def run(self, result=None): + # set resources directory based on if downloads are being performed + # https://stackoverflow.com/a/11180583 + + self.resource = Resources() + self._reset_resource() + if self.downloads_enabled: + self.resource._resources_dir = "resources" + super().run(result) + else: + # use a temporary directory for test resource data + with tempfile.TemporaryDirectory() as tmpdir: + self.resource._resources_dir = tmpdir + super().run(result) + self.resource._resources_dir = "resources" + + def _generate_test_genetic_map_HapMapII_GRCh37_resource(self): + + filenames = [f"genetic_map_GRCh37_chr{chrom}.txt" for chrom in range(1, 23)] + filenames.extend( + [ + "genetic_map_GRCh37_chrX.txt", + "genetic_map_GRCh37_chrX_par1.txt", + "genetic_map_GRCh37_chrX_par2.txt", + "README.txt", + ] + ) + + # create compressed tar in memory + tar_file = io.BytesIO() + with tarfile.open(fileobj=tar_file, mode="w:gz") as out_tar: + for filename in filenames: + if filename != "README.txt": + chrom = filename[filename.find("chr") :].split(".")[0] + s = "Chromosome\tPosition(bp)\tRate(cM/Mb)\tMap(cM)\n" + s += f"{chrom}\t0\t0.0\t0.0\n" + else: + s = "test" + + # add file to tar; https://stackoverflow.com/a/40392022 + data = s.encode() + file = io.BytesIO(data) + tar_info = tarfile.TarInfo(name=filename) + tar_info.size = len(data) + out_tar.addfile(tar_info, fileobj=file) + + mock = mock_open(read_data=tar_file.getvalue()) + with patch("urllib.request.urlopen", mock): + self.resource._get_path_genetic_map_HapMapII_GRCh37() + def test_get_genetic_map_HapMapII_GRCh37(self): + def f(): + # mock download of test data + self._generate_test_genetic_map_HapMapII_GRCh37_resource() + return self.resource.get_genetic_map_HapMapII_GRCh37() + + genetic_map_HapMapII_GRCh37 = ( + self.resource.get_genetic_map_HapMapII_GRCh37() + if self.downloads_enabled + else f() + ) + + assert len(genetic_map_HapMapII_GRCh37) == 23 + + # get already loaded resource genetic_map_HapMapII_GRCh37 = self.resource.get_genetic_map_HapMapII_GRCh37() assert len(genetic_map_HapMapII_GRCh37) == 23 + def _generate_test_cytoBand_hg19_resource(self): + s = f"s\t0\t0\ts\ts\n" * 862 + mock = mock_open(read_data=gzip.compress(s.encode())) + with patch("urllib.request.urlopen", mock): + self.resource._get_path_cytoBand_hg19() + def test_get_cytoBand_hg19(self): + def f(): + # mock download of test data + self._generate_test_cytoBand_hg19_resource() + return self.resource.get_cytoBand_hg19() + + cytoBand_hg19 = ( + self.resource.get_cytoBand_hg19() if self.downloads_enabled else f() + ) + + assert len(cytoBand_hg19) == 862 + + # get already loaded resource cytoBand_hg19 = self.resource.get_cytoBand_hg19() assert len(cytoBand_hg19) == 862 + def _generate_test_knownGene_hg19_resource(self): + s = "s\ts\ts\t0\t0\t0\t0\t0\ts\ts\ts\ts\n" * 82960 + + mock = mock_open(read_data=gzip.compress(s.encode())) + with patch("urllib.request.urlopen", mock): + self.resource._get_path_knownGene_hg19() + def test_get_knownGene_hg19(self): + def f(): + # mock download of test data + self._generate_test_knownGene_hg19_resource() + return self.resource.get_knownGene_hg19() + + knownGene_hg19 = ( + self.resource.get_knownGene_hg19() if self.downloads_enabled else f() + ) + + assert len(knownGene_hg19) == 82960 + + # get already loaded resource knownGene_hg19 = self.resource.get_knownGene_hg19() assert len(knownGene_hg19) == 82960 + def _generate_test_kgXref_hg19_resource(self): + s = "s\ts\ts\ts\ts\ts\ts\ts\n" * 82960 + + mock = mock_open(read_data=gzip.compress(s.encode())) + with patch("urllib.request.urlopen", mock): + self.resource._get_path_kgXref_hg19() + def test_get_kgXref_hg19(self): + def f(): + # mock download of test data + self._generate_test_kgXref_hg19_resource() + return self.resource.get_kgXref_hg19() + + kgXref_hg19 = self.resource.get_kgXref_hg19() if self.downloads_enabled else f() + + assert len(kgXref_hg19) == 82960 + + # get already loaded resource kgXref_hg19 = self.resource.get_kgXref_hg19() assert len(kgXref_hg19) == 82960 def test_get_all_resources(self): - resources = self.resource.get_all_resources() + def f(): + # mock download of test data for each resource + self._generate_test_genetic_map_HapMapII_GRCh37_resource() + self._generate_test_cytoBand_hg19_resource() + self._generate_test_knownGene_hg19_resource() + self._generate_test_kgXref_hg19_resource() + + return self.resource.get_all_resources() + + resources = self.resource.get_all_resources() if self.downloads_enabled else f() + for k, v in resources.items(): - if v is None: - assert False - assert True + self.assertGreater(len(v), 0) def test_download_example_datasets(self): - paths = self.resource.download_example_datasets() + def f(): + with patch("urllib.request.urlopen", mock_open(read_data=b"")): + return self.resource.download_example_datasets() + + paths = ( + self.resource.download_example_datasets() if self.downloads_enabled else f() + ) for path in paths: if path is None or not os.path.exists(path): warnings.warn("Example dataset(s) not currently available") return - - assert True From fef6e09229da65ebbb347b16bf07fc9a94f31f18 Mon Sep 17 00:00:00 2001 From: Andrew Riha Date: Tue, 13 Apr 2021 22:07:59 -0700 Subject: [PATCH 2/3] Support 1000 Genomes Project genetic maps --- src/lineage/__init__.py | 54 +++++++++--- src/lineage/resources.py | 175 +++++++++++++++++++++++++++++++++++-- tests/test_lineage.py | 180 +++++++++++++++++++++++---------------- tests/test_resources.py | 60 ++++++++++++- 4 files changed, 372 insertions(+), 97 deletions(-) diff --git a/src/lineage/__init__.py b/src/lineage/__init__.py index 0e078fd..9ca6d78 100644 --- a/src/lineage/__init__.py +++ b/src/lineage/__init__.py @@ -280,12 +280,13 @@ def find_shared_dna( snp_threshold=1100, shared_genes=False, save_output=True, + genetic_map="HapMap2", ): """ Find the shared DNA between individuals. - Computes the genetic distance in centiMorgans (cMs) between SNPs using the HapMap Phase II - GRCh37 genetic map. Applies thresholds to determine the shared DNA. Plots shared DNA. - Optionally determines shared genes (i.e., genes that are transcribed from the shared DNA). + Computes the genetic distance in centiMorgans (cMs) between SNPs using the specified genetic + map. Applies thresholds to determine the shared DNA. Plots shared DNA. Optionally determines + shared genes (i.e., genes transcribed from the shared DNA). All output is saved to the output directory as `CSV` or `PNG` files. @@ -300,6 +301,16 @@ def find_shared_dna( determine shared genes save_output : bool specifies whether to save output files in the output directory + genetic_map : {'HapMap2', 'ACB', 'ASW', 'CDX', 'CEU', 'CHB', 'CHS', 'CLM', 'FIN', 'GBR', 'GIH', 'IBS', 'JPT', 'KHV', 'LWK', 'MKK', 'MXL', 'PEL', 'PUR', 'TSI', 'YRI'} + genetic map to use for computation of shared DNA; `HapMap2` corresponds to the HapMap + Phase II genetic map from the + `International HapMap Project `_ + and all others correspond to the + `population-specific `_ + genetic maps generated from the + `1000 Genomes Project `_ phased OMNI data. + Note that shared DNA is not computed on the X chromosome with the 1000 Genomes + Project genetic maps since the X chromosome is not included in these genetic maps. Returns ------- @@ -339,6 +350,18 @@ def find_shared_dna( two_chrom_discrepant_snps, ) + genetic_map_dfs = self._resources.get_genetic_map(genetic_map) + + if len(genetic_map_dfs) == 0: + return self._find_shared_dna_return_helper( + one_chrom_shared_dna, + two_chrom_shared_dna, + one_chrom_shared_genes, + two_chrom_shared_genes, + one_chrom_discrepant_snps, + two_chrom_discrepant_snps, + ) + cols = ["genotype{}".format(str(i)) for i in range(len(individuals))] df = individuals[0].snps @@ -351,19 +374,17 @@ def find_shared_dna( one_x_chrom = self._is_one_individual_male(individuals) - genetic_map = self._resources.get_genetic_map_HapMapII_GRCh37() - tasks = [] chroms_to_drop = [] for chrom in df["chrom"].unique(): - if chrom not in genetic_map.keys(): + if chrom not in genetic_map_dfs.keys(): chroms_to_drop.append(chrom) continue tasks.append( { - "genetic_map": genetic_map[chrom], + "genetic_map": genetic_map_dfs[chrom], # get positions for the current chromosome "snps": pd.DataFrame(df.loc[(df["chrom"] == chrom)]["pos"]), } @@ -433,6 +454,7 @@ def find_shared_dna( two_chrom_shared_dna, one_chrom_shared_genes, two_chrom_shared_genes, + genetic_map, ) return self._find_shared_dna_return_helper( @@ -479,6 +501,7 @@ def _find_shared_dna_output_helper( two_chrom_shared_dna, one_chrom_shared_genes, two_chrom_shared_genes, + genetic_map, ): cytobands = self._resources.get_cytoBand_hg19() @@ -498,14 +521,17 @@ def _find_shared_dna_output_helper( two_chrom_shared_dna, cytobands, os.path.join( - self._output_dir, "shared_dna_{}.png".format(individuals_filename) + self._output_dir, + f"shared_dna_{individuals_filename}_{genetic_map}.png", ), - "{} shared DNA".format(individuals_plot_title), + f"{individuals_plot_title} shared DNA", 37, ) if len(one_chrom_shared_dna) > 0: - file = "shared_dna_one_chrom_{}_GRCh37.csv".format(individuals_filename) + file = ( + f"shared_dna_one_chrom_{individuals_filename}_GRCh37_{genetic_map}.csv" + ) save_df_as_csv( one_chrom_shared_dna, self._output_dir, @@ -516,7 +542,9 @@ def _find_shared_dna_output_helper( ) if len(two_chrom_shared_dna) > 0: - file = "shared_dna_two_chroms_{}_GRCh37.csv".format(individuals_filename) + file = ( + f"shared_dna_two_chroms_{individuals_filename}_GRCh37_{genetic_map}.csv" + ) save_df_as_csv( two_chrom_shared_dna, self._output_dir, @@ -527,7 +555,7 @@ def _find_shared_dna_output_helper( ) if len(one_chrom_shared_genes) > 0: - file = "shared_genes_one_chrom_{}_GRCh37.csv".format(individuals_filename) + file = f"shared_genes_one_chrom_{individuals_filename}_GRCh37_{genetic_map}.csv" save_df_as_csv( one_chrom_shared_genes, self._output_dir, @@ -537,7 +565,7 @@ def _find_shared_dna_output_helper( ) if len(two_chrom_shared_genes) > 0: - file = "shared_genes_two_chroms_{}_GRCh37.csv".format(individuals_filename) + file = f"shared_genes_two_chroms_{individuals_filename}_GRCh37_{genetic_map}.csv" save_df_as_csv( two_chrom_shared_genes, self._output_dir, diff --git a/src/lineage/resources.py b/src/lineage/resources.py index b74ee3a..c624120 100644 --- a/src/lineage/resources.py +++ b/src/lineage/resources.py @@ -76,11 +76,61 @@ def __init__(self, resources_dir="resources"): """ super().__init__(resources_dir=resources_dir) - self._genetic_map_HapMapII_GRCh37 = {} + self._genetic_map = {} + self._genetic_map_name = "" self._cytoBand_hg19 = pd.DataFrame() self._knownGene_hg19 = pd.DataFrame() self._kgXref_hg19 = pd.DataFrame() + def get_genetic_map(self, genetic_map): + """ Get specified genetic map. + + Parameters + ---------- + genetic_map : {'HapMap2', 'ACB', 'ASW', 'CDX', 'CEU', 'CHB', 'CHS', 'CLM', 'FIN', 'GBR', 'GIH', 'IBS', 'JPT', 'KHV', 'LWK', 'MKK', 'MXL', 'PEL', 'PUR', 'TSI', 'YRI'} + `HapMap2` corresponds to the HapMap Phase II genetic map from the + `International HapMap Project `_ + and all others correspond to the + `population-specific `_ + genetic maps generated from the + `1000 Genomes Project `_ phased OMNI data. + + Returns + ------- + dict + dict of pandas.DataFrame genetic maps if loading was successful, else {} + """ + if genetic_map not in [ + "HapMap2", + "ACB", + "ASW", + "CDX", + "CEU", + "CHB", + "CHS", + "CLM", + "FIN", + "GBR", + "GIH", + "IBS", + "JPT", + "KHV", + "LWK", + "MKK", + "MXL", + "PEL", + "PUR", + "TSI", + "YRI", + ]: + logger.warning("Invalid genetic map") + return {} + + if genetic_map == "HapMap2": + return self.get_genetic_map_HapMapII_GRCh37() + else: + return self.get_genetic_map_1000G_GRCh37(genetic_map) + def get_genetic_map_HapMapII_GRCh37(self): """ Get International HapMap Consortium HapMap Phase II genetic map for Build 37. @@ -98,12 +148,67 @@ def get_genetic_map_HapMapII_GRCh37(self): paper (Nature, 18th Sept 2007). The conversion from b35 to GRCh37 was achieved using the UCSC liftOver tool. Adam Auton, 08/12/2010" """ - if not self._genetic_map_HapMapII_GRCh37: - self._genetic_map_HapMapII_GRCh37 = self._load_genetic_map( + if self._genetic_map_name != "HapMap2": + self._genetic_map = self._load_genetic_map_HapMapII_GRCh37( self._get_path_genetic_map_HapMapII_GRCh37() ) + self._genetic_map_name = "HapMap2" + + return self._genetic_map + + def get_genetic_map_1000G_GRCh37(self, pop): + """ Get population-specific 1000 Genomes Project genetic map. + + Notes + ----- + From `README_omni_recombination_20130507 `_: + + Genetic maps generated from the 1000G phased OMNI data. + + [Build 37] OMNI haplotypes were obtained from the Phase 1 dataset + (`/vol1/ftp/phase1/analysis_results/supporting/omni_haplotypes/ `_). - return self._genetic_map_HapMapII_GRCh37 + Genetic maps were generated for each population separately using LDhat + (http://ldhat.sourceforge.net/). Haplotypes were split into 2000 SNP windows + with an overlap of 200 SNPs between each window. The recombination rate was + estimated for each window independently, using a block penalty of 5 for a + total of 22.5 million iterations with a sample being taken from the MCMC + chain every 15,000 iterations. The first 7.5 million iterations were + discarded as burn in. Once rates were estimated, windows were merged by + splicing the estimates at the mid-point of the overlapping regions. + + LDhat estimates the population genetic recombination rate, rho = 4Ner. In + order to convert to per-generation rates (measured in cM/Mb), the LDhat + rates were compared to pedigree-based rates from Kong et al. (2010). + Specifically, rates were binned at the 5Mb scale, and a linear regression + performed between the two datasets. The gradient of this line gives an + estimate of 4Ne, allowing the population based rates to be converted to + per-generation rates. + + Adam Auton (adam.auton@einstein.yu.edu) + May 7th, 2013 + + Returns + ------- + dict + dict of pandas.DataFrame population-specific 1000 Genomes Project genetic maps if + loading was successful, else {} + + References + ---------- + 1. Adam Auton (adam.auton@einstein.yu.edu), May 7th, 2013 + 2. The 1000 Genomes Project Consortium., Corresponding authors., Auton, A. et al. A + global reference for human genetic variation. Nature 526, 68–74 (2015). + https://doi.org/10.1038/nature15393 + 3. https://github.com/joepickrell/1000-genomes-genetic-maps + """ + if self._genetic_map_name != pop: + self._genetic_map = self._load_genetic_map_1000G_GRCh37( + self._get_path_genetic_map_1000G_GRCh37(pop) + ) + self._genetic_map_name = pop + + return self._genetic_map def get_cytoBand_hg19(self): """ Get UCSC cytoBand table for Build 37. @@ -210,8 +315,8 @@ def get_all_resources(self): return resources @staticmethod - def _load_genetic_map(filename): - """ Load genetic map (e.g. HapMapII). + def _load_genetic_map_HapMapII_GRCh37(filename): + """ Load HapMapII genetic map. Parameters ---------- @@ -229,7 +334,7 @@ def _load_genetic_map(filename): """ genetic_map = {} - with tarfile.open(filename, "r") as tar: + with tarfile.open(filename, "r:gz") as tar: # http://stackoverflow.com/a/2018576 for member in tar.getmembers(): if "genetic_map" in member.name: @@ -255,6 +360,47 @@ def _load_genetic_map(filename): return genetic_map + @staticmethod + def _load_genetic_map_1000G_GRCh37(filename): + """ Load 1000 Genomes Project genetic map. + + Parameters + ---------- + filename : str + path to archive with compressed genetic map data + + Returns + ------- + genetic_map : dict + dict of pandas.DataFrame genetic maps + + Notes + ----- + Keys of returned dict are chromosomes and values are the corresponding genetic map. + """ + genetic_map = {} + + with tarfile.open(filename, "r") as tar: + # http://stackoverflow.com/a/2018576 + for member in tar.getmembers(): + df = pd.read_csv( + tar.extractfile(member), + compression="gzip", + sep="\s+", + usecols=["Position(bp)", "Rate(cM/Mb)", "Map(cM)"], + ) + df = df.rename( + columns={ + "Position(bp)": "pos", + "Rate(cM/Mb)": "rate", + "Map(cM)": "map", + } + ) + chrom = member.name.split("-")[1] + genetic_map[chrom] = df + + return genetic_map + @staticmethod def _load_cytoBand(filename): """ Load UCSC cytoBand table. @@ -374,6 +520,21 @@ def _get_path_genetic_map_HapMapII_GRCh37(self): "genetic_map_HapMapII_GRCh37.tar.gz", ) + def _get_path_genetic_map_1000G_GRCh37(self, pop): + """ Get local path to population-specific 1000 Genomes Project genetic map, + downloading if necessary. + + Returns + ------- + str + path to {pop}_omni_recombination_20130507.tar + """ + filename = f"{pop}_omni_recombination_20130507.tar" + return self._download_file( + f"ftp://ftp.1000genomes.ebi.ac.uk/vol1/ftp/technical/working/20130507_omni_recombination_rates/{filename}", + filename, + ) + def _get_path_knownGene_hg19(self): """ Get local path to knownGene file for hg19 / GRCh37 from UCSC, downloading if necessary. diff --git a/tests/test_lineage.py b/tests/test_lineage.py index 4792e96..d329d84 100644 --- a/tests/test_lineage.py +++ b/tests/test_lineage.py @@ -45,7 +45,7 @@ def get_discordant_snps(self, ind, df): return ind - def _generate_test_genetic_map_HapMapII_GRCh37( + def _generate_test_genetic_map( self, chrom="1", pos=(1, 111700001), @@ -134,17 +134,31 @@ def run_find_shared_dna_test_X(self, f): txEnd2=155000000, ) - def run_find_shared_dna_test(self, f, **kwargs): + def run_find_shared_dna_test_1000G(self, f): + self.run_find_shared_dna_test( + f, + HapMap2=False, + pos=(1, 43800001), + rate=(63.0402663602 / (43800001 / 1e6), 0), + map_cMs=(0.0, 63.0402663602), + len1=2188, + ) + + def run_find_shared_dna_test(self, f, HapMap2=True, **kwargs): if self.downloads_enabled: f() else: - genetic_map = self._generate_test_genetic_map_HapMapII_GRCh37(**kwargs) + genetic_map_patch = ( + "lineage.resources.Resources.get_genetic_map_HapMapII_GRCh37" + if HapMap2 + else "lineage.resources.Resources.get_genetic_map_1000G_GRCh37" + ) + genetic_map = self._generate_test_genetic_map(**kwargs) cytoband = self._generate_test_cytoBand_hg19() kg, kgXref = self._generate_test_gene_dfs(**kwargs) with patch( - "lineage.resources.Resources.get_genetic_map_HapMapII_GRCh37", - Mock(return_value=genetic_map), + genetic_map_patch, Mock(return_value=genetic_map), ): with patch( "lineage.resources.Resources.get_cytoBand_hg19", @@ -160,6 +174,36 @@ def run_find_shared_dna_test(self, f, **kwargs): ): f() + def _assert_exists(self, files, idx): + for i, file in enumerate(files): + if i in idx: + self.assertTrue(os.path.exists(file)) + + def _assert_does_not_exist(self, files, idx): + for i, file in enumerate(files): + if i in idx: + self.assertFalse(os.path.exists(file)) + + def _make_file_exist_assertions(self, inds, exist="all", genetic_map="HapMap2"): + files = [ + f"output/shared_dna_one_chrom_{inds}_GRCh37_{genetic_map}.csv", + f"output/shared_dna_two_chroms_{inds}_GRCh37_{genetic_map}.csv", + f"output/shared_genes_one_chrom_{inds}_GRCh37_{genetic_map}.csv", + f"output/shared_genes_two_chroms_{inds}_GRCh37_{genetic_map}.csv", + f"output/shared_dna_{inds}_{genetic_map}.png", + ] + + if exist == "all": + self._assert_exists(files, list(range(5))) + elif exist == "none": + self._assert_does_not_exist(files, list(range(5))) + elif exist == "one_chrom": + self._assert_exists(files, [0, 2, 4]) + self._assert_does_not_exist(files, [1, 3]) + elif exist == "plots": + self._assert_exists(files, [4]) + self._assert_does_not_exist(files, list(range(4))) + def _check_example_paths(self, paths): for path in paths: if path is None or not os.path.exists(path): @@ -284,11 +328,24 @@ def _test_find_shared_dna_one_ind(self): assert len(d["two_chrom_shared_genes"]) == 0 assert len(d["one_chrom_discrepant_snps"]) == 0 assert len(d["two_chrom_discrepant_snps"]) == 0 - assert not os.path.exists("output/shared_dna_one_chrom_ind1_GRCh37.csv") - assert not os.path.exists("output/shared_dna_two_chroms_ind1_GRCh37.csv") - assert not os.path.exists("output/shared_genes_one_chrom_ind1_GRCh37.csv") - assert not os.path.exists("output/shared_genes_two_chroms_ind1_GRCh37.csv") - assert not os.path.exists("output/shared_dna_ind1.png") + self._make_file_exist_assertions("ind1", exist="none") + + def test_find_shared_dna_invalid_genetic_map(self): + self.run_find_shared_dna_test(self._test_find_shared_dna_invalid_genetic_map) + + def _test_find_shared_dna_invalid_genetic_map(self): + ind1 = self.simulate_snps(self.l.create_individual("ind1")) + ind2 = self.simulate_snps(self.l.create_individual("ind2")) + + d = self.l.find_shared_dna([ind1, ind2], genetic_map="test") + + assert len(d["one_chrom_shared_dna"]) == 0 + assert len(d["two_chrom_shared_dna"]) == 0 + assert len(d["one_chrom_shared_genes"]) == 0 + assert len(d["two_chrom_shared_genes"]) == 0 + assert len(d["one_chrom_discrepant_snps"]) == 0 + assert len(d["two_chrom_discrepant_snps"]) == 0 + self._make_file_exist_assertions("ind1_ind2", exist="none") def test_find_shared_dna_two_chrom_shared(self): self.run_find_shared_dna_test(self._test_find_shared_dna_two_chrom_shared) @@ -297,7 +354,9 @@ def _test_find_shared_dna_two_chrom_shared(self): ind1 = self.simulate_snps(self.l.create_individual("ind1")) ind2 = self.simulate_snps(self.l.create_individual("ind2")) - d = self.l.find_shared_dna([ind1, ind2], shared_genes=True) + d = self.l.find_shared_dna( + [ind1, ind2], shared_genes=True, genetic_map="HapMap2" + ) assert len(d["one_chrom_shared_dna"]) == 1 assert len(d["two_chrom_shared_dna"]) == 1 @@ -307,11 +366,32 @@ def _test_find_shared_dna_two_chrom_shared(self): assert len(d["two_chrom_discrepant_snps"]) == 0 np.testing.assert_allclose(d["one_chrom_shared_dna"].loc[1]["cMs"], 140.443968) np.testing.assert_allclose(d["two_chrom_shared_dna"].loc[1]["cMs"], 140.443968) - assert os.path.exists("output/shared_dna_one_chrom_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_dna_two_chroms_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_genes_one_chrom_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_genes_two_chroms_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_dna_ind1_ind2.png") + self._make_file_exist_assertions("ind1_ind2") + + def test_find_shared_dna_two_chrom_shared_1000G(self): + self.run_find_shared_dna_test_1000G( + self._test_find_shared_dna_two_chrom_shared_1000G + ) + + def _test_find_shared_dna_two_chrom_shared_1000G(self): + ind1 = self.simulate_snps(self.l.create_individual("ind1"), pos_max=43800002) + ind2 = self.simulate_snps(self.l.create_individual("ind2"), pos_max=43800002) + + d = self.l.find_shared_dna([ind1, ind2], shared_genes=True, genetic_map="CEU") + + assert len(d["one_chrom_shared_dna"]) == 1 + assert len(d["two_chrom_shared_dna"]) == 1 + assert len(d["one_chrom_shared_genes"]) == 2188 + assert len(d["two_chrom_shared_genes"]) == 2188 + assert len(d["one_chrom_discrepant_snps"]) == 0 + assert len(d["two_chrom_discrepant_snps"]) == 0 + np.testing.assert_allclose( + d["one_chrom_shared_dna"].loc[1]["cMs"], 63.0402663602 + ) + np.testing.assert_allclose( + d["two_chrom_shared_dna"].loc[1]["cMs"], 63.0402663602 + ) + self._make_file_exist_assertions("ind1_ind2", genetic_map="CEU") def test_find_shared_dna_two_chrom_shared_three_ind(self): self.run_find_shared_dna_test( @@ -333,13 +413,7 @@ def _test_find_shared_dna_two_chrom_shared_three_ind(self): assert len(d["two_chrom_discrepant_snps"]) == 0 np.testing.assert_allclose(d["one_chrom_shared_dna"].loc[1]["cMs"], 140.443968) np.testing.assert_allclose(d["two_chrom_shared_dna"].loc[1]["cMs"], 140.443968) - assert os.path.exists("output/shared_dna_one_chrom_ind1_ind2_ind3_GRCh37.csv") - assert os.path.exists("output/shared_dna_two_chroms_ind1_ind2_ind3_GRCh37.csv") - assert os.path.exists("output/shared_genes_one_chrom_ind1_ind2_ind3_GRCh37.csv") - assert os.path.exists( - "output/shared_genes_two_chroms_ind1_ind2_ind3_GRCh37.csv" - ) - assert os.path.exists("output/shared_dna_ind1_ind2_ind3.png") + self._make_file_exist_assertions("ind1_ind2_ind3") def test_find_shared_dna_two_chrom_shared_no_output(self): self.run_find_shared_dna_test( @@ -360,11 +434,7 @@ def _test_find_shared_dna_two_chrom_shared_no_output(self): assert len(d["two_chrom_discrepant_snps"]) == 0 np.testing.assert_allclose(d["one_chrom_shared_dna"].loc[1]["cMs"], 140.443968) np.testing.assert_allclose(d["two_chrom_shared_dna"].loc[1]["cMs"], 140.443968) - assert not os.path.exists("output/shared_dna_one_chrom_ind1_ind2_GRCh37.csv") - assert not os.path.exists("output/shared_dna_two_chroms_ind1_ind2_GRCh37.csv") - assert not os.path.exists("output/shared_genes_one_chrom_ind1_ind2_GRCh37.csv") - assert not os.path.exists("output/shared_genes_two_chroms_ind1_ind2_GRCh37.csv") - assert not os.path.exists("output/shared_dna_ind1_ind2.png") + self._make_file_exist_assertions("ind1_ind2", exist="none") def test_find_shared_dna_one_chrom_shared(self): self.run_find_shared_dna_test(self._test_find_shared_dna_one_chrom_shared) @@ -384,11 +454,7 @@ def _test_find_shared_dna_one_chrom_shared(self): assert len(d["one_chrom_discrepant_snps"]) == 0 assert len(d["two_chrom_discrepant_snps"]) == 0 np.testing.assert_allclose(d["one_chrom_shared_dna"].loc[1]["cMs"], 140.443968) - assert os.path.exists("output/shared_dna_one_chrom_ind1_ind2_GRCh37.csv") - assert not os.path.exists("output/shared_dna_two_chroms_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_genes_one_chrom_ind1_ind2_GRCh37.csv") - assert not os.path.exists("output/shared_genes_two_chroms_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_dna_ind1_ind2.png") + self._make_file_exist_assertions("ind1_ind2", exist="one_chrom") def test_find_shared_dna_one_chrom_shared_three_ind(self): self.run_find_shared_dna_test( @@ -411,15 +477,7 @@ def _test_find_shared_dna_one_chrom_shared_three_ind(self): assert len(d["one_chrom_discrepant_snps"]) == 0 assert len(d["two_chrom_discrepant_snps"]) == 0 np.testing.assert_allclose(d["one_chrom_shared_dna"].loc[1]["cMs"], 140.443968) - assert os.path.exists("output/shared_dna_one_chrom_ind1_ind2_ind3_GRCh37.csv") - assert not os.path.exists( - "output/shared_dna_two_chroms_ind1_ind2_ind3_GRCh37.csv" - ) - assert os.path.exists("output/shared_genes_one_chrom_ind1_ind2_ind3_GRCh37.csv") - assert not os.path.exists( - "output/shared_genes_two_chroms_ind1_ind2_ind3_GRCh37.csv" - ) - assert os.path.exists("output/shared_dna_ind1_ind2_ind3.png") + self._make_file_exist_assertions("ind1_ind2_ind3", exist="one_chrom") def test_find_shared_dna_X_chrom_two_individuals_male(self): self.run_find_shared_dna_test_X( @@ -456,11 +514,7 @@ def _test_find_shared_dna_X_chrom_two_individuals_male(self): np.testing.assert_allclose( d["two_chrom_shared_dna"].loc[1]["cMs"], 20.837792, rtol=1e-3, ) - assert os.path.exists("output/shared_dna_one_chrom_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_dna_two_chroms_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_genes_one_chrom_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_genes_two_chroms_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_dna_ind1_ind2.png") + self._make_file_exist_assertions("ind1_ind2") def test_find_shared_dna_X_chrom_two_individuals_female(self): self.run_find_shared_dna_test_X( @@ -495,11 +549,7 @@ def _test_find_shared_dna_X_chrom_two_individuals_female(self): np.testing.assert_allclose( d["two_chrom_shared_dna"].loc[1]["cMs"], 202.022891, rtol=1e-5, ) - assert os.path.exists("output/shared_dna_one_chrom_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_dna_two_chroms_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_genes_one_chrom_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_genes_two_chroms_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_dna_ind1_ind2.png") + self._make_file_exist_assertions("ind1_ind2") def test_find_shared_dna_two_chrom_shared_discrepant_snps(self): self.run_find_shared_dna_test( @@ -525,11 +575,7 @@ def _test_find_shared_dna_two_chrom_shared_discrepant_snps(self): assert len(d["two_chrom_discrepant_snps"]) == 2 np.testing.assert_allclose(d["one_chrom_shared_dna"].loc[1]["cMs"], 140.443968) np.testing.assert_allclose(d["two_chrom_shared_dna"].loc[1]["cMs"], 140.443968) - assert os.path.exists("output/shared_dna_one_chrom_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_dna_two_chroms_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_genes_one_chrom_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_genes_two_chroms_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_dna_ind1_ind2.png") + self._make_file_exist_assertions("ind1_ind2") def test_find_shared_dna_no_shared_dna(self): self.run_find_shared_dna_test(self._test_find_shared_dna_no_shared_dna) @@ -548,11 +594,7 @@ def _test_find_shared_dna_no_shared_dna(self): assert len(d["two_chrom_shared_genes"]) == 0 assert len(d["one_chrom_discrepant_snps"]) == 0 assert len(d["two_chrom_discrepant_snps"]) == 0 - assert not os.path.exists("output/shared_dna_one_chrom_ind1_ind2_GRCh37.csv") - assert not os.path.exists("output/shared_dna_two_chroms_ind1_ind2_GRCh37.csv") - assert not os.path.exists("output/shared_genes_one_chrom_ind1_ind2_GRCh37.csv") - assert not os.path.exists("output/shared_genes_two_chroms_ind1_ind2_GRCh37.csv") - assert os.path.exists("output/shared_dna_ind1_ind2.png") + self._make_file_exist_assertions("ind1_ind2", exist="plots") def test_find_shared_dna_no_shared_dna_three_ind(self): self.run_find_shared_dna_test( @@ -574,16 +616,4 @@ def _test_find_shared_dna_no_shared_dna_three_ind(self): assert len(d["two_chrom_shared_genes"]) == 0 assert len(d["one_chrom_discrepant_snps"]) == 0 assert len(d["two_chrom_discrepant_snps"]) == 0 - assert not os.path.exists( - "output/shared_dna_one_chrom_ind1_ind2_ind3_GRCh37.csv" - ) - assert not os.path.exists( - "output/shared_dna_two_chroms_ind1_ind2_ind3_GRCh37.csv" - ) - assert not os.path.exists( - "output/shared_genes_one_chrom_ind1_ind2_ind3_GRCh37.csv" - ) - assert not os.path.exists( - "output/shared_genes_two_chroms_ind1_ind2_ind3_GRCh37.csv" - ) - assert os.path.exists("output/shared_dna_ind1_ind2_ind3.png") + self._make_file_exist_assertions("ind1_ind2_ind3", exist="plots") diff --git a/tests/test_resources.py b/tests/test_resources.py index c4ff75d..a052f4b 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -42,7 +42,8 @@ def setUp(self): self.del_output_dir_helper() def _reset_resource(self): - self.resource._genetic_map_HapMapII_GRCh37 = {} + self.resource._genetic_map = {} + self.resource._genetic_map_name = "" self.resource._cytoBand_hg19 = pd.DataFrame() self.resource._knownGene_hg19 = pd.DataFrame() self.resource._kgXref_hg19 = pd.DataFrame() @@ -64,7 +65,6 @@ def run(self, result=None): self.resource._resources_dir = "resources" def _generate_test_genetic_map_HapMapII_GRCh37_resource(self): - filenames = [f"genetic_map_GRCh37_chr{chrom}.txt" for chrom in range(1, 23)] filenames.extend( [ @@ -115,6 +115,62 @@ def f(): genetic_map_HapMapII_GRCh37 = self.resource.get_genetic_map_HapMapII_GRCh37() assert len(genetic_map_HapMapII_GRCh37) == 23 + # get already loaded resource + genetic_map = self.resource.get_genetic_map("HapMap2") + assert len(genetic_map) == 23 + + def _generate_test_genetic_map_1000G_GRCh37_resource(self): + filenames = [f"CEU-{chrom}-final.txt.gz" for chrom in range(1, 23)] + + # create tar in memory + tar_file = io.BytesIO() + with tarfile.open(fileobj=tar_file, mode="w") as out_tar: + for filename in filenames: + s = "Position(bp)\tRate(cM/Mb)\tMap(cM)\tFiltered\n" + s += f" 0\t0.0\t0.0\t0\n" + + # add file to tar; https://stackoverflow.com/a/40392022 + data = gzip.compress(s.encode()) + file = io.BytesIO(data) + tar_info = tarfile.TarInfo(name=filename) + tar_info.size = len(data) + out_tar.addfile(tar_info, fileobj=file) + + mock = mock_open(read_data=tar_file.getvalue()) + with patch("urllib.request.urlopen", mock): + self.resource._get_path_genetic_map_1000G_GRCh37("CEU") + + def test_get_genetic_map_1000G_GRCh37(self): + def f(): + # mock download of test data + self._generate_test_genetic_map_1000G_GRCh37_resource() + return self.resource.get_genetic_map_1000G_GRCh37("CEU") + + genetic_map = ( + self.resource.get_genetic_map_1000G_GRCh37("CEU") + if self.downloads_enabled + else f() + ) + + assert len(genetic_map) == 22 + + # get already loaded resource + genetic_map = self.resource.get_genetic_map_1000G_GRCh37("CEU") + assert len(genetic_map) == 22 + + # get already loaded resource + genetic_map = self.resource.get_genetic_map("CEU") + assert len(genetic_map) == 22 + + def test_invalid_genetic_map(self): + # https://stackoverflow.com/a/46767037 + with self.assertLogs() as log: + genetic_map = self.resource.get_genetic_map("test") + self.assertEqual(len(log.output), 1) + self.assertEqual(len(log.records), 1) + self.assertIn("Invalid genetic map", log.output[0]) + self.assertEqual(len(genetic_map), 0) + def _generate_test_cytoBand_hg19_resource(self): s = f"s\t0\t0\ts\ts\n" * 862 mock = mock_open(read_data=gzip.compress(s.encode())) From 9fafc6c4909a69331358771e433e51b6d22d000a Mon Sep 17 00:00:00 2001 From: Andrew Riha Date: Tue, 13 Apr 2021 22:08:25 -0700 Subject: [PATCH 3/3] Update docs --- README.rst | 45 ++++++++++-------- docs/images/shared_dna_User4583_User4584.png | Bin 46142 -> 0 bytes .../shared_dna_User4583_User4584_CEU.png | Bin 0 -> 46091 bytes ...=> shared_dna_User662_User663_HapMap2.png} | Bin docs/output_files.rst | 33 ++++++------- 5 files changed, 43 insertions(+), 35 deletions(-) delete mode 100644 docs/images/shared_dna_User4583_User4584.png create mode 100644 docs/images/shared_dna_User4583_User4584_CEU.png rename docs/images/{shared_dna_User662_User663.png => shared_dna_User662_User663_HapMap2.png} (100%) diff --git a/README.rst b/README.rst index 28ab04e..1b791a6 100644 --- a/README.rst +++ b/README.rst @@ -9,9 +9,9 @@ lineage Capabilities ------------ -- Compute centiMorgans (cMs) of shared DNA between individuals using the HapMap Phase II genetic map +- Find shared DNA and genes between individuals +- Compute centiMorgans (cMs) of shared DNA using a variety of genetic maps (e.g., HapMap Phase II, 1000 Genomes Project) - Plot shared DNA between individuals -- Determine genes shared between individuals (i.e., genes transcribed from shared DNA segments) - Find discordant SNPs between child and parent(s) - Read, write, merge, and remaps SNPs for an individual via the `snps `_ package @@ -139,11 +139,12 @@ Not counting mtDNA SNPs, there are 37 discordant SNPs between these two datasets Find Shared DNA ''''''''''''''' ``lineage`` uses the probabilistic recombination rates throughout the human genome from the -`International HapMap Project `_ to -compute the shared DNA (in centiMorgans) between two individuals. Additionally, ``lineage`` -denotes when the shared DNA is shared on either one or both chromosomes in a pair. For example, -when siblings share a segment of DNA on both chromosomes, they inherited the same DNA from their -mother and father for that segment. +`International HapMap Project `_ +and the `1000 Genomes Project `_ to compute the shared DNA +(in centiMorgans) between two individuals. Additionally, ``lineage`` denotes when the shared DNA +is shared on either one or both chromosomes in a pair. For example, when siblings share a segment +of DNA on both chromosomes, they inherited the same DNA from their mother and father for that +segment. With that background, let's find the shared DNA between the ``User662`` and ``User663`` datasets, calculating the centiMorgans of shared DNA and plotting the results: @@ -151,8 +152,8 @@ calculating the centiMorgans of shared DNA and plotting the results: >>> results = l.find_shared_dna([user662, user663], cM_threshold=0.75, snp_threshold=1100) Downloading resources/genetic_map_HapMapII_GRCh37.tar.gz Downloading resources/cytoBand_hg19.txt.gz -Saving output/shared_dna_User662_User663.png -Saving output/shared_dna_one_chrom_User662_User663_GRCh37.csv +Saving output/shared_dna_User662_User663_HapMap2.png +Saving output/shared_dna_one_chrom_User662_User663_GRCh37_HapMap2.csv Notice that the centiMorgan and SNP thresholds for each DNA segment can be tuned. Additionally, notice that two files were downloaded to facilitate the analysis and plotting - future analyses @@ -177,11 +178,11 @@ created; these files are detailed in the documentation and their generation can ``save_output=False`` argument. In this example, the output files consist of a CSV file that details the shared segments of DNA on one chromosome and a plot that illustrates the shared DNA: -.. image:: https://raw.githubusercontent.com/apriha/lineage/master/docs/images/shared_dna_User662_User663.png +.. image:: https://raw.githubusercontent.com/apriha/lineage/master/docs/images/shared_dna_User662_User663_HapMap2.png Find Shared Genes ''''''''''''''''' -The `Central Dogma of Molecular Biology `_ +The `Central Dogma of Molecular Biology `_ states that genetic information flows from DNA to mRNA to proteins: DNA is transcribed into mRNA, and mRNA is translated into a protein. It's more complicated than this (it's biology after all), but generally, one mRNA produces one protein, and the mRNA / protein is considered a @@ -205,21 +206,27 @@ Loading SNPs('resources/4583.ftdna-illumina.3482.csv.gz') >>> user4584 = l.create_individual('User4584', 'resources/4584.ftdna-illumina.3483.csv.gz') Loading SNPs('resources/4584.ftdna-illumina.3483.csv.gz') -Now let's find the shared genes: +Now let's find the shared genes, specifying a +`population-specific `_ +1000 Genomes Project genetic map (e.g., as predicted by `ezancestry `_!): ->>> results = l.find_shared_dna([user4583, user4584], shared_genes=True) +>>> results = l.find_shared_dna([user4583, user4584], shared_genes=True, genetic_map="CEU") +Downloading resources/CEU_omni_recombination_20130507.tar Downloading resources/knownGene_hg19.txt.gz Downloading resources/kgXref_hg19.txt.gz -Saving output/shared_dna_User4583_User4584.png -Saving output/shared_dna_one_chrom_User4583_User4584_GRCh37.csv -Saving output/shared_dna_two_chroms_User4583_User4584_GRCh37.csv -Saving output/shared_genes_one_chrom_User4583_User4584_GRCh37.csv -Saving output/shared_genes_two_chroms_User4583_User4584_GRCh37.csv +Saving output/shared_dna_User4583_User4584_CEU.png +Saving output/shared_dna_one_chrom_User4583_User4584_GRCh37_CEU.csv +Saving output/shared_dna_two_chroms_User4583_User4584_GRCh37_CEU.csv +Saving output/shared_genes_one_chrom_User4583_User4584_GRCh37_CEU.csv +Saving output/shared_genes_two_chroms_User4583_User4584_GRCh37_CEU.csv The plot that illustrates the shared DNA is shown below. Note that in addition to outputting the shared DNA segments on either one or both chromosomes, the shared genes on either one or both chromosomes are also output. +.. note:: Shared DNA is not computed on the X chromosome with the 1000 Genomes Project genetic + maps since the X chromosome is not included in these genetic maps. + In this example, there are 15,976 shared genes on both chromosomes transcribed from 36 segments of shared DNA: @@ -228,7 +235,7 @@ of shared DNA: >>> len(results['two_chrom_shared_dna']) 36 -.. image:: https://raw.githubusercontent.com/apriha/lineage/master/docs/images/shared_dna_User4583_User4584.png +.. image:: https://raw.githubusercontent.com/apriha/lineage/master/docs/images/shared_dna_User4583_User4584_CEU.png Documentation ------------- diff --git a/docs/images/shared_dna_User4583_User4584.png b/docs/images/shared_dna_User4583_User4584.png deleted file mode 100644 index 0b9cac81d53a1afa5f85ec3c0422d87525d1223b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46142 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 A_W%F@ diff --git a/docs/images/shared_dna_User4583_User4584_CEU.png b/docs/images/shared_dna_User4583_User4584_CEU.png new file mode 100644 index 0000000000000000000000000000000000000000..d9f12c9af02ab06b720d021477da0332cde4e982 GIT binary patch literal 46091 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 P4Rz|I8Kx;F3pf8Ck>yCJ literal 0 HcmV?d00001 diff --git a/docs/images/shared_dna_User662_User663.png b/docs/images/shared_dna_User662_User663_HapMap2.png similarity index 100% rename from docs/images/shared_dna_User662_User663.png rename to docs/images/shared_dna_User662_User663_HapMap2.png diff --git a/docs/output_files.rst b/docs/output_files.rst index 5008a71..9360b68 100644 --- a/docs/output_files.rst +++ b/docs/output_files.rst @@ -57,25 +57,26 @@ In the filenames below, ``name1`` is the name of the first :class:`~lineage.individual.Individual` and ``name2`` is the name of the second :class:`~lineage.individual.Individual`. (If more individuals are compared, all :class:`~lineage.individual.Individual` names will be included in the filenames and plot titles -using the same conventions.) +using the same conventions.) Additionally, ``genetic_map`` corresponds to the genetic map used +in the calculations of shared DNA, specified as a parameter to :meth:`~lineage.Lineage.find_shared_dna`. .. note:: Genetic maps do not have recombination rates for the Y chromosome since the Y chromosome does not recombine. Therefore, shared DNA will not be shown on the Y chromosome. -shared_dna__.png -`````````````````````````````` +shared_dna___.png +```````````````````````````````````````````` This plot illustrates shared DNA (i.e., no shared DNA, shared DNA on one chromosome, and shared DNA on both chromosomes). The centromere for each chromosome is also detailed. Two examples of this plot are shown below. -.. image:: https://raw.githubusercontent.com/apriha/lineage/master/docs/images/shared_dna_User662_User663.png +.. image:: https://raw.githubusercontent.com/apriha/lineage/master/docs/images/shared_dna_User662_User663_HapMap2.png In the above plot, note that the two individuals only share DNA on one chromosome. In this plot, the larger regions where "No shared DNA" is indicated are due to SNPs not being available in those regions (i.e., SNPs were not tested in those regions). -.. image:: https://raw.githubusercontent.com/apriha/lineage/master/docs/images/shared_dna_User4583_User4584.png +.. image:: https://raw.githubusercontent.com/apriha/lineage/master/docs/images/shared_dna_User4583_User4584_CEU.png In the above plot, the areas where "No shared DNA" is indicated are the regions where SNPs were not tested or where DNA is not shared. The areas where "One chromosome shared" is indicated are @@ -85,8 +86,8 @@ shared" is indicated are regions where the individuals share DNA on both chromos Note that the regions where DNA is shared on both chromosomes is a subset of the regions where one chromosome is shared. -shared_dna_one_chrom___GRCh37.csv -``````````````````````````````````````````````` +shared_dna_one_chrom___GRCh37_.csv +````````````````````````````````````````````````````````````` If DNA is shared on one chromosome, a CSV file details the shared segments of DNA. ======= =========== @@ -100,8 +101,8 @@ cMs CentiMorgans of matching DNA segment snps Number of SNPs in matching DNA segment ======= =========== -shared_dna_two_chroms___GRCh37.csv -```````````````````````````````````````````````` +shared_dna_two_chroms___GRCh37_.csv +`````````````````````````````````````````````````````````````` If DNA is shared on two chromosomes, a CSV file details the shared segments of DNA. ======= =========== @@ -128,11 +129,11 @@ In the filenames below, ``name1`` is the name of the first :class:`~lineage.individual.Individual` names will be included in the filenames using the same convention.) -shared_genes_one_chrom___GRCh37.csv -````````````````````````````````````````````````` +shared_genes_one_chrom___GRCh37_.csv +``````````````````````````````````````````````````````````````` If DNA is shared on one chromosome, this file details the genes shared between the individuals on at least one chromosome; these genes are located in the shared DNA segments specified in -`shared_dna_one_chrom___GRCh37.csv`_. +`shared_dna_one_chrom___GRCh37_.csv`_. =========== ============ Column* Description* @@ -151,10 +152,10 @@ description Description \* `UCSC Genome Browser `_ / `UCSC Table Browser `_ -shared_genes_two_chroms___GRCh37.csv -`````````````````````````````````````````````````` +shared_genes_two_chroms___GRCh37_.csv +```````````````````````````````````````````````````````````````` If DNA is shared on both chromosomes in a pair, this file details the genes shared between the individuals on both chromosomes; these genes are located in the shared DNA segments specified in -`shared_dna_two_chroms___GRCh37.csv`_. +`shared_dna_two_chroms___GRCh37_.csv`_. -The file has the same columns as `shared_genes_one_chrom___GRCh37.csv`_. +The file has the same columns as `shared_genes_one_chrom___GRCh37_.csv`_.