From 6d7d8411040e3dff8c74f517b619b7f61c419558 Mon Sep 17 00:00:00 2001 From: Helloworldlty Date: Sun, 4 Feb 2024 12:15:58 -0500 Subject: [PATCH 1/5] integrate citeseq --- docs/test citeseq tutorial.ipynb | 1578 ++++++++++++++++++++++++++++++ scglue/models/sc.py | 42 + scglue/models/scglue.py | 2 +- scglue/utils.py | 63 ++ 4 files changed, 1684 insertions(+), 1 deletion(-) create mode 100644 docs/test citeseq tutorial.ipynb diff --git a/docs/test citeseq tutorial.ipynb b/docs/test citeseq tutorial.ipynb new file mode 100644 index 0000000..3213333 --- /dev/null +++ b/docs/test citeseq tutorial.ipynb @@ -0,0 +1,1578 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f911e97c-aa60-4315-9353-85a8d02eeef4", + "metadata": {}, + "source": [ + "# Prepare the dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "52a69b01-81da-4166-a20f-b2378ef84e1e", + "metadata": {}, + "outputs": [], + "source": [ + "import anndata as ad\n", + "import networkx as nx\n", + "import scanpy as sc\n", + "import scglue\n", + "from matplotlib import rcParams\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0710aacd-19ca-4efb-a98f-f4650af81b63", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "scglue" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ff17ed58-0385-457e-9315-6028723437f2", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/gpfs/gibbs/project/zhao/tl688/conda_envs/scglue/lib/python3.9/site-packages/anndata/_core/anndata.py:1906: UserWarning: Observation names are not unique. To make them unique, call `.obs_names_make_unique`.\n", + " utils.warn_names_duplicates(\"obs\")\n" + ] + } + ], + "source": [ + "adata = sc.read_h5ad(\"/gpfs/gibbs/pi/zhao/tl688/cpsc_finalproject/genept_data/GenePT/pbmc3k5kciteseq.h5ad\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2bf5213e-4668-4df2-b64a-50d6ad0ce452", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AnnData object with n_obs × n_vars = 10849 × 15792\n", + " obs: 'n_genes', 'percent_mito', 'n_counts', 'batch'\n", + " obsm: 'protein_expression'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "adata" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "acf72843-be5d-495d-86e0-40b990575210", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CD3_TotalSeqBCD4_TotalSeqBCD8a_TotalSeqBCD14_TotalSeqBCD15_TotalSeqBCD16_TotalSeqBCD56_TotalSeqBCD19_TotalSeqBCD25_TotalSeqBCD45RA_TotalSeqBCD45RO_TotalSeqBPD-1_TotalSeqBTIGIT_TotalSeqBCD127_TotalSeqB
index
AAACCCAAGATTGTGA-118138134916117173911074947
AAACCCACATCGGTTA-1301191947210215524835125156998
AAACCCAGTACCGCGT-1182071012891287226815526828201112
AAACCCAGTATCGAAA-1181117201241227491515474328255916
AAACCCAGTCGTCATA-151414191561873458416410821287617
.............................................
TTTGGTTGTACGAGTG-1756123726110443302115060
TTTGTTGAGTTAACAG-12121912475941782544712
TTTGTTGCAGCACAAG-169397371561216576934147
TTTGTTGCAGTCTTCC-14021417891425236184112145
TTTGTTGCATTGCCGG-1646338214144468310
\n", + "

10849 rows × 14 columns

\n", + "
" + ], + "text/plain": [ + " CD3_TotalSeqB CD4_TotalSeqB CD8a_TotalSeqB \\\n", + "index \n", + "AAACCCAAGATTGTGA-1 18 138 13 \n", + "AAACCCACATCGGTTA-1 30 119 19 \n", + "AAACCCAGTACCGCGT-1 18 207 10 \n", + "AAACCCAGTATCGAAA-1 18 11 17 \n", + "AAACCCAGTCGTCATA-1 5 14 14 \n", + "... ... ... ... \n", + "TTTGGTTGTACGAGTG-1 756 1237 2 \n", + "TTTGTTGAGTTAACAG-1 21 219 12 \n", + "TTTGTTGCAGCACAAG-1 693 9 737 \n", + "TTTGTTGCAGTCTTCC-1 402 1417 8 \n", + "TTTGTTGCATTGCCGG-1 6 46 3 \n", + "\n", + " CD14_TotalSeqB CD15_TotalSeqB CD16_TotalSeqB \\\n", + "index \n", + "AAACCCAAGATTGTGA-1 491 61 17 \n", + "AAACCCACATCGGTTA-1 472 102 155 \n", + "AAACCCAGTACCGCGT-1 1289 128 72 \n", + "AAACCCAGTATCGAAA-1 20 124 1227 \n", + "AAACCCAGTCGTCATA-1 19 156 1873 \n", + "... ... ... ... \n", + "TTTGGTTGTACGAGTG-1 6 11 0 \n", + "TTTGTTGAGTTAACAG-1 475 9 4 \n", + "TTTGTTGCAGCACAAG-1 15 6 1 \n", + "TTTGTTGCAGTCTTCC-1 9 14 2 \n", + "TTTGTTGCATTGCCGG-1 382 1 4 \n", + "\n", + " CD56_TotalSeqB CD19_TotalSeqB CD25_TotalSeqB \\\n", + "index \n", + "AAACCCAAGATTGTGA-1 17 3 9 \n", + "AAACCCACATCGGTTA-1 248 3 5 \n", + "AAACCCAGTACCGCGT-1 26 8 15 \n", + "AAACCCAGTATCGAAA-1 491 5 15 \n", + "AAACCCAGTCGTCATA-1 458 4 16 \n", + "... ... ... ... \n", + "TTTGGTTGTACGAGTG-1 4 4 3 \n", + "TTTGTTGAGTTAACAG-1 1 7 8 \n", + "TTTGTTGCAGCACAAG-1 2 1 6 \n", + "TTTGTTGCAGTCTTCC-1 5 2 3 \n", + "TTTGTTGCATTGCCGG-1 1 4 4 \n", + "\n", + " CD45RA_TotalSeqB CD45RO_TotalSeqB PD-1_TotalSeqB \\\n", + "index \n", + "AAACCCAAGATTGTGA-1 110 74 9 \n", + "AAACCCACATCGGTTA-1 125 156 9 \n", + "AAACCCAGTACCGCGT-1 5268 28 20 \n", + "AAACCCAGTATCGAAA-1 4743 28 25 \n", + "AAACCCAGTCGTCATA-1 4108 21 28 \n", + "... ... ... ... \n", + "TTTGGTTGTACGAGTG-1 302 11 5 \n", + "TTTGTTGAGTTAACAG-1 25 44 7 \n", + "TTTGTTGCAGCACAAG-1 57 69 34 \n", + "TTTGTTGCAGTCTTCC-1 6 184 11 \n", + "TTTGTTGCATTGCCGG-1 46 8 3 \n", + "\n", + " TIGIT_TotalSeqB CD127_TotalSeqB \n", + "index \n", + "AAACCCAAGATTGTGA-1 4 7 \n", + "AAACCCACATCGGTTA-1 9 8 \n", + "AAACCCAGTACCGCGT-1 11 12 \n", + "AAACCCAGTATCGAAA-1 59 16 \n", + "AAACCCAGTCGTCATA-1 76 17 \n", + "... ... ... \n", + "TTTGGTTGTACGAGTG-1 0 60 \n", + "TTTGTTGAGTTAACAG-1 1 2 \n", + "TTTGTTGCAGCACAAG-1 14 7 \n", + "TTTGTTGCAGTCTTCC-1 2 145 \n", + "TTTGTTGCATTGCCGG-1 1 0 \n", + "\n", + "[10849 rows x 14 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "adata.obsm['protein_expression']" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4fb7ef33-0d47-4b58-85ce-35d736e2a5e3", + "metadata": {}, + "outputs": [], + "source": [ + "# pip install mygene" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77f7d2d8-308e-4a5e-b38b-64854eca7cf3", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f7d41fff-594d-40ba-ae53-a8b101b07cae", + "metadata": {}, + "outputs": [], + "source": [ + "rna = adata" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "36d7c131-4ee9-47c2-a568-3c72a473c3f4", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/gpfs/gibbs/project/zhao/tl688/conda_envs/scglue/lib/python3.9/site-packages/anndata/_core/anndata.py:1906: UserWarning: Observation names are not unique. To make them unique, call `.obs_names_make_unique`.\n", + " utils.warn_names_duplicates(\"obs\")\n" + ] + } + ], + "source": [ + "prot = sc.AnnData(adata.obsm['protein_expression'], obs = adata.obs)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f9208290-1c44-4847-9c53-91d6081d5e58", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['AAACCCAAGATTGTGA-1', 'AAACCCACATCGGTTA-1', 'AAACCCAGTACCGCGT-1',\n", + " 'AAACCCAGTATCGAAA-1', 'AAACCCAGTCGTCATA-1', 'AAACCCAGTCTACACA-1',\n", + " 'AAACCCAGTGCAAGAC-1', 'AAACCCAGTGCATTTG-1', 'AAACCCATCCGATGTA-1',\n", + " 'AAACCCATCTCAACGA-1',\n", + " ...\n", + " 'TTTGGAGCACTCATAG-1', 'TTTGGAGCAGCGGTTC-1', 'TTTGGTTCAAAGCGTG-1',\n", + " 'TTTGGTTGTAATGTGA-1', 'TTTGGTTGTACCTGTA-1', 'TTTGGTTGTACGAGTG-1',\n", + " 'TTTGTTGAGTTAACAG-1', 'TTTGTTGCAGCACAAG-1', 'TTTGTTGCAGTCTTCC-1',\n", + " 'TTTGTTGCATTGCCGG-1'],\n", + " dtype='object', name='index', length=10849)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "prot.obs_names" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2197a5cc-e9e7-40fb-aee2-9b10b49855e8", + "metadata": {}, + "outputs": [], + "source": [ + "prot.var_names = [i.split('_')[0]+'prot' for i in prot.var_names] #rename the protein data to avoid conflict with genes" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "567eda40-65d8-456f-aea4-bdafd26202fc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['CD3prot', 'CD4prot', 'CD8aprot', 'CD14prot', 'CD15prot', 'CD16prot',\n", + " 'CD56prot', 'CD19prot', 'CD25prot', 'CD45RAprot', 'CD45ROprot',\n", + " 'PD-1prot', 'TIGITprot', 'CD127prot'],\n", + " dtype='object')" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "prot.var_names" + ] + }, + { + "cell_type": "markdown", + "id": "e06a964b-6c69-4aac-9532-f07540019a59", + "metadata": {}, + "source": [ + "We provide an option to access the protien-gene network:\n", + "\n", + "The service is based on: https://www.humanmine.org/humanmine/service" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "9b26b4e5-89e9-42f2-a685-4f16cebf5e57", + "metadata": {}, + "outputs": [], + "source": [ + "# pip install intermine" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "26b73d05-9559-4717-8cfc-c9280364364d", + "metadata": {}, + "outputs": [], + "source": [ + "# An option to query the protein-gene matching\n", + "# from intermine.webservice import Service\n", + "# service = Service(\"https://www.humanmine.org/humanmine/service\")\n", + "\n", + "# # Get a new query on the class (table) you will be querying:\n", + "# query = service.new_query(\"Protein\")\n", + "\n", + "# # The view specifies the output columns\n", + "# query.add_view(\n", + "# \"primaryAccession\", \"genes.primaryIdentifier\", \"genes.symbol\",\n", + "# \"genes.chromosome.primaryIdentifier\", \"genes.chromosomeLocation.start\",\n", + "# \"genes.chromosomeLocation.end\", \"genes.chromosomeLocation.strand\",\n", + "# \"genes.length\"\n", + "# )\n", + "\n", + "# # Uncomment and edit the line below (the default) to select a custom sort order:\n", + "# # query.add_sort_order(\"Protein.primaryAccession\", \"ASC\")\n", + "\n", + "# # You can edit the constraint values below\n", + "# query.add_constraint(\"Protein\", \"LOOKUP\", \"CD3\", code=\"A\")\n", + "\n", + "# # Uncomment and edit the code below to specify your own custom logic:\n", + "# # query.set_logic(\"A\")\n", + "\n", + "# for row in query.rows():\n", + "# print(row[\"primaryAccession\"], row[\"genes.primaryIdentifier\"], row[\"genes.symbol\"], \\\n", + "# row[\"genes.chromosome.primaryIdentifier\"], row[\"genes.chromosomeLocation.start\"], \\\n", + "# row[\"genes.chromosomeLocation.end\"], row[\"genes.chromosomeLocation.strand\"], \\\n", + "# row[\"genes.length\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "f445eaf5-3056-4ffa-bbdb-6501186f9ec6", + "metadata": {}, + "outputs": [], + "source": [ + "# for row in query.rows():\n", + "# print(row)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "2fb63955-2b62-49ef-9256-b625d4d95a78", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['CD3prot', 'CD4prot', 'CD8aprot', 'CD14prot', 'CD15prot', 'CD16prot',\n", + " 'CD56prot', 'CD19prot', 'CD25prot', 'CD45RAprot', 'CD45ROprot',\n", + " 'PD-1prot', 'TIGITprot', 'CD127prot'],\n", + " dtype='object')" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "prot.var_names" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "98d557be-6626-413b-8f86-3ad496d95e37", + "metadata": {}, + "outputs": [], + "source": [ + "# Define the protein-gene network\n", + "protein_gene_match = {\n", + " 'CD4prot':'CD4', 'CD8aprot':'CD8A'\n", + " , 'CD14prot':'CD14'\n", + " , 'CD15prot':'FUT4'\n", + " , 'CD16prot':'FCGR3A'\n", + " , 'CD56prot':'NCAM1'\n", + " , 'CD19prot':'CD19'\n", + " , 'CD25prot':'IL2RA'\n", + " ,'CD45RAprot':'PTPRC'\n", + " , 'PD-1prot':'PDCD1'\n", + " , 'TIGITprot':'TIGIT'\n", + " , 'CD127prot':'IL7R'}" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "b8b36a84-fae3-4b0b-a040-d0394e239aaf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'CD4prot': 'CD4',\n", + " 'CD8aprot': 'CD8A',\n", + " 'CD14prot': 'CD14',\n", + " 'CD15prot': 'FUT4',\n", + " 'CD16prot': 'FCGR3A',\n", + " 'CD56prot': 'NCAM1',\n", + " 'CD19prot': 'CD19',\n", + " 'CD25prot': 'IL2RA',\n", + " 'CD45RAprot': 'PTPRC',\n", + " 'PD-1prot': 'PDCD1',\n", + " 'TIGITprot': 'TIGIT',\n", + " 'CD127prot': 'IL7R'}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "protein_gene_match" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "2b38fb91-4ba4-4f8e-b72a-4fbd1664ceac", + "metadata": {}, + "outputs": [], + "source": [ + "rna.layers['counts'] = rna.X.copy()" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "bfd30c9a-cc04-4413-90bb-43baad9101dd", + "metadata": {}, + "outputs": [], + "source": [ + "sc.pp.highly_variable_genes(rna, n_top_genes=2000, flavor=\"seurat_v3\")\n", + "rna = rna[:,rna.var['highly_variable']]" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "11d90f1c-be20-4ea1-a9b5-e582d74ab263", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "View of AnnData object with n_obs × n_vars = 10849 × 2000\n", + " obs: 'n_genes', 'percent_mito', 'n_counts', 'batch'\n", + " var: 'highly_variable', 'highly_variable_rank', 'means', 'variances', 'variances_norm'\n", + " uns: 'hvg'\n", + " obsm: 'protein_expression'\n", + " layers: 'counts'" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rna" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "c96636e5-1fb9-466e-be0a-5949285d0571", + "metadata": {}, + "outputs": [], + "source": [ + "overlap_list = sorted(set(rna.var_names).intersection(prot.var_names))" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "2f0f31c6-d69d-4ac7-82b4-6203df16b0e2", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/gpfs/gibbs/project/zhao/tl688/conda_envs/scglue/lib/python3.9/site-packages/scanpy/preprocessing/_normalization.py:169: UserWarning: Received a view of an AnnData. Making a copy.\n", + " view_to_actual(adata)\n", + "/gpfs/gibbs/project/zhao/tl688/conda_envs/scglue/lib/python3.9/site-packages/anndata/_core/anndata.py:1906: UserWarning: Observation names are not unique. To make them unique, call `.obs_names_make_unique`.\n", + " utils.warn_names_duplicates(\"obs\")\n" + ] + } + ], + "source": [ + "sc.pp.normalize_total(rna)\n", + "sc.pp.log1p(rna)\n", + "sc.pp.scale(rna)\n", + "sc.tl.pca(rna, n_comps=50, svd_solver=\"auto\")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "aade97a5-3bfd-40f5-a6a3-c5e9036e0c8a", + "metadata": {}, + "outputs": [], + "source": [ + "prot.X = prot.X.astype('float')" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "effd1785-9b21-4e6a-8da7-cc36ec8cf864", + "metadata": {}, + "outputs": [], + "source": [ + "prot.layers['counts'] = prot.X" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "7feb2019-a108-44b2-8512-4060ef36ce27", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[1.800e+01, 1.380e+02, 1.300e+01, ..., 9.000e+00, 4.000e+00,\n", + " 7.000e+00],\n", + " [3.000e+01, 1.190e+02, 1.900e+01, ..., 9.000e+00, 9.000e+00,\n", + " 8.000e+00],\n", + " [1.800e+01, 2.070e+02, 1.000e+01, ..., 2.000e+01, 1.100e+01,\n", + " 1.200e+01],\n", + " ...,\n", + " [6.930e+02, 9.000e+00, 7.370e+02, ..., 3.400e+01, 1.400e+01,\n", + " 7.000e+00],\n", + " [4.020e+02, 1.417e+03, 8.000e+00, ..., 1.100e+01, 2.000e+00,\n", + " 1.450e+02],\n", + " [6.000e+00, 4.600e+01, 3.000e+00, ..., 3.000e+00, 1.000e+00,\n", + " 0.000e+00]])" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "prot.layers['counts'] " + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "f6ed0eb6-9012-444d-a45d-c9f352a8a714", + "metadata": {}, + "outputs": [], + "source": [ + "import networkx as nx" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "e44e1361-61a0-40b4-b0ee-beea0924bce9", + "metadata": {}, + "outputs": [], + "source": [ + "guidance = scglue.utils.generate_prot_gudiance_graph(rna, prot, protein_gene_match)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "1fdd06ec-3eb3-48b4-9b27-10a3b4a8ae93", + "metadata": {}, + "outputs": [], + "source": [ + "# clr(prot)\n", + "# sc.pp.scale(prot)\n", + "# sc.tl.pca(prot)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "fe849c51-9254-46cf-abd6-cff27abaffeb", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "OutMultiEdgeView([('CD4prot', 'CD4', 0), ('CD4prot', 'CD4prot', 0), ('CD4', 'CD4prot', 0), ('CD4', 'CD4', 0), ('CD8aprot', 'CD8A', 0), ('CD8aprot', 'CD8aprot', 0), ('CD8A', 'CD8aprot', 0), ('CD8A', 'CD8A', 0), ('CD14prot', 'CD14', 0), ('CD14prot', 'CD14prot', 0), ('CD14', 'CD14prot', 0), ('CD14', 'CD14', 0), ('CD15prot', 'FUT4', 0), ('CD15prot', 'CD15prot', 0), ('FUT4', 'CD15prot', 0), ('CD16prot', 'FCGR3A', 0), ('CD16prot', 'CD16prot', 0), ('FCGR3A', 'CD16prot', 0), ('FCGR3A', 'FCGR3A', 0), ('CD56prot', 'NCAM1', 0), ('CD56prot', 'CD56prot', 0), ('NCAM1', 'CD56prot', 0), ('NCAM1', 'NCAM1', 0), ('CD19prot', 'CD19', 0), ('CD19prot', 'CD19prot', 0), ('CD19', 'CD19prot', 0), ('CD19', 'CD19', 0), ('CD25prot', 'IL2RA', 0), ('CD25prot', 'CD25prot', 0), ('IL2RA', 'CD25prot', 0), ('IL2RA', 'IL2RA', 0), ('CD45RAprot', 'PTPRC', 0), ('CD45RAprot', 'CD45RAprot', 0), ('PTPRC', 'CD45RAprot', 0), ('PD-1prot', 'PDCD1', 0), ('PD-1prot', 'PD-1prot', 0), ('PDCD1', 'PD-1prot', 0), ('TIGITprot', 'TIGIT', 0), ('TIGITprot', 'TIGITprot', 0), ('TIGIT', 'TIGITprot', 0), ('TIGIT', 'TIGIT', 0), ('CD127prot', 'IL7R', 0), ('CD127prot', 'CD127prot', 0), ('IL7R', 'CD127prot', 0), ('IL7R', 'IL7R', 0), ('AL645608.8', 'AL645608.8', 0), ('HES4', 'HES4', 0), ('ISG15', 'ISG15', 0), ('TTLL10', 'TTLL10', 0), ('TNFRSF18', 'TNFRSF18', 0), ('TNFRSF4', 'TNFRSF4', 0), ('AL645728.1', 'AL645728.1', 0), ('MMP23B', 'MMP23B', 0), ('NADK', 'NADK', 0), ('AJAP1', 'AJAP1', 0), ('TNFRSF25', 'TNFRSF25', 0), ('AL034417.3', 'AL034417.3', 0), ('CA6', 'CA6', 0), ('SLC2A5', 'SLC2A5', 0), ('RBP7', 'RBP7', 0), ('PGD', 'PGD', 0), ('AGTRAP', 'AGTRAP', 0), ('TNFRSF8', 'TNFRSF8', 0), ('TNFRSF1B', 'TNFRSF1B', 0), ('EFHD2', 'EFHD2', 0), ('EPHA2', 'EPHA2', 0), ('PADI2', 'PADI2', 0), ('PADI4', 'PADI4', 0), ('ARHGEF10L', 'ARHGEF10L', 0), ('CDA', 'CDA', 0), ('C1QA', 'C1QA', 0), ('C1QC', 'C1QC', 0), ('C1QB', 'C1QB', 0), ('TCEA3', 'TCEA3', 0), ('ID3', 'ID3', 0), ('AL031432.1', 'AL031432.1', 0), ('STMN1', 'STMN1', 0), ('UBXN11', 'UBXN11', 0), ('ZNF683', 'ZNF683', 0), ('FGR', 'FGR', 0), ('IFI6', 'IFI6', 0), ('THEMIS2', 'THEMIS2', 0), ('PTAFR', 'PTAFR', 0), ('AL360012.1', 'AL360012.1', 0), ('MARCKSL1', 'MARCKSL1', 0), ('CLSPN', 'CLSPN', 0), ('AGO4', 'AGO4', 0), ('EVA1B', 'EVA1B', 0), ('CSF3R', 'CSF3R', 0), ('CDCA8', 'CDCA8', 0), ('FHL3', 'FHL3', 0), ('POU3F1', 'POU3F1', 0), ('PABPC4', 'PABPC4', 0), ('MYCL', 'MYCL', 0), ('PPT1', 'PPT1', 0), ('COL9A2', 'COL9A2', 0), ('CDC20', 'CDC20', 0), ('ATP6V0B', 'ATP6V0B', 0), ('ARMH1', 'ARMH1', 0), ('KIF2C', 'KIF2C', 0), ('PLK3', 'PLK3', 0), ('PTCH2', 'PTCH2', 0), ('TTC39A', 'TTC39A', 0), ('SLC1A7', 'SLC1A7', 0), ('AC119428.2', 'AC119428.2', 0), ('ACOT11', 'ACOT11', 0), ('MIR4422HG', 'MIR4422HG', 0), ('JUN', 'JUN', 0), ('IL23R', 'IL23R', 0), ('GADD45A', 'GADD45A', 0), ('WLS', 'WLS', 0), ('AK5', 'AK5', 0), ('NEXN', 'NEXN', 0), ('DNAJB4', 'DNAJB4', 0), ('AC103591.3', 'AC103591.3', 0), ('IFI44L', 'IFI44L', 0), ('IFI44', 'IFI44', 0), ('LINC01781', 'LINC01781', 0), ('LINC01725', 'LINC01725', 0), ('LMO4', 'LMO4', 0), ('GBP1', 'GBP1', 0), ('GBP2', 'GBP2', 0), ('GBP4', 'GBP4', 0), ('GBP5', 'GBP5', 0), ('TGFBR3', 'TGFBR3', 0), ('EVI5', 'EVI5', 0), ('ARHGAP29', 'ARHGAP29', 0), ('DPYD', 'DPYD', 0), ('AC104506.1', 'AC104506.1', 0), ('SORT1', 'SORT1', 0), ('CHI3L2', 'CHI3L2', 0), ('C1ORF162', 'C1ORF162', 0), ('AL603832.1', 'AL603832.1', 0), ('RHOC', 'RHOC', 0), ('PPM1J', 'PPM1J', 0), ('SLC16A1-AS1', 'SLC16A1-AS1', 0), ('BCL2L15', 'BCL2L15', 0), ('HIPK1-AS1', 'HIPK1-AS1', 0), ('CD2', 'CD2', 0), ('TENT5C', 'TENT5C', 0), ('NOTCH2', 'NOTCH2', 0), ('FCGR1B', 'FCGR1B', 0), ('AC245014.3', 'AC245014.3', 0), ('PDZK1', 'PDZK1', 0), ('CD160', 'CD160', 0), ('AC239803.3', 'AC239803.3', 0), ('NBPF14', 'NBPF14', 0), ('NBPF19', 'NBPF19', 0), ('FCGR1A', 'FCGR1A', 0), ('MTMR11', 'MTMR11', 0), ('PLEKHO1', 'PLEKHO1', 0), ('CA14', 'CA14', 0), ('C1ORF54', 'C1ORF54', 0), ('ADAMTSL4', 'ADAMTSL4', 0), ('MCL1', 'MCL1', 0), ('CTSS', 'CTSS', 0), ('TNFAIP8L2', 'TNFAIP8L2', 0), ('SNX27', 'SNX27', 0), ('RORC', 'RORC', 0), ('S100A10', 'S100A10', 0), ('S100A11', 'S100A11', 0), ('S100A9', 'S100A9', 0), ('S100A12', 'S100A12', 0), ('S100A8', 'S100A8', 0), ('S100A6', 'S100A6', 0), ('S100A4', 'S100A4', 0), ('RAB13', 'RAB13', 0), ('IL6R', 'IL6R', 0), ('AL451085.2', 'AL451085.2', 0), ('ADAM15', 'ADAM15', 0), ('LMNA', 'LMNA', 0), ('SEMA4A', 'SEMA4A', 0), ('ETV3', 'ETV3', 0), ('FCRL5', 'FCRL5', 0), ('FCRL3', 'FCRL3', 0), ('FCRL2', 'FCRL2', 0), ('FCRL1', 'FCRL1', 0), ('CD1D', 'CD1D', 0), ('AL138899.1', 'AL138899.1', 0), ('CD1C', 'CD1C', 0), ('CD1B', 'CD1B', 0), ('CD1E', 'CD1E', 0), ('MNDA', 'MNDA', 0), ('PYHIN1', 'PYHIN1', 0), ('AIM2', 'AIM2', 0), ('FCER1A', 'FCER1A', 0), ('FCRL6', 'FCRL6', 0), ('SLAMF8', 'SLAMF8', 0), ('TAGLN2', 'TAGLN2', 0), ('KCNJ10', 'KCNJ10', 0), ('PEA15', 'PEA15', 0), ('SLAMF7', 'SLAMF7', 0), ('FCER1G', 'FCER1G', 0), ('FCGR2A', 'FCGR2A', 0), ('HSPA6', 'HSPA6', 0), ('FCGR2B', 'FCGR2B', 0), ('FCRLA', 'FCRLA', 0), ('SH2D1B', 'SH2D1B', 0), ('NUF2', 'NUF2', 0), ('CD247', 'CD247', 0), ('CREG1', 'CREG1', 0), ('XCL2', 'XCL2', 0), ('XCL1', 'XCL1', 0), ('ATP1B1', 'ATP1B1', 0), ('F5', 'F5', 0), ('SELP', 'SELP', 0), ('C1ORF112', 'C1ORF112', 0), ('FASLG', 'FASLG', 0), ('RALGPS2', 'RALGPS2', 0), ('IER5', 'IER5', 0), ('GLUL', 'GLUL', 0), ('RGS16', 'RGS16', 0), ('NPL', 'NPL', 0), ('NCF2', 'NCF2', 0), ('AL445228.2', 'AL445228.2', 0), ('C1ORF21', 'C1ORF21', 0), ('FAM129A', 'FAM129A', 0), ('PTGS2', 'PTGS2', 0), ('RGS18', 'RGS18', 0), ('AL136987.1', 'AL136987.1', 0), ('RGS1', 'RGS1', 0), ('RGS2', 'RGS2', 0), ('LINC01724', 'LINC01724', 0), ('CFH', 'CFH', 0), ('ASPM', 'ASPM', 0), ('PHLDA3', 'PHLDA3', 0), ('NAV1', 'NAV1', 0), ('BTG2', 'BTG2', 0), ('PPP1R15B', 'PPP1R15B', 0), ('RHEX', 'RHEX', 0), ('AL591846.2', 'AL591846.2', 0), ('MAPKAPK2', 'MAPKAPK2', 0), ('IL10', 'IL10', 0), ('CR1', 'CR1', 0), ('G0S2', 'G0S2', 0), ('HSD11B1', 'HSD11B1', 0), ('SLC30A1', 'SLC30A1', 0), ('DTL', 'DTL', 0), ('ATF3', 'ATF3', 0), ('BATF3', 'BATF3', 0), ('CENPF', 'CENPF', 0), ('MARC1', 'MARC1', 0), ('HLX', 'HLX', 0), ('TLR5', 'TLR5', 0), ('H3F3A', 'H3F3A', 0), ('LINC01132', 'LINC01132', 0), ('LYST', 'LYST', 0), ('NID1', 'NID1', 0), ('CHRM3-AS2', 'CHRM3-AS2', 0), ('RGS7', 'RGS7', 0), ('OPN3', 'OPN3', 0), ('CHML', 'CHML', 0), ('HNRNPU', 'HNRNPU', 0), ('NLRP3', 'NLRP3', 0), ('TRIM58', 'TRIM58', 0), ('CMPK2', 'CMPK2', 0), ('RSAD2', 'RSAD2', 0), ('LINC01871', 'LINC01871', 0), ('ID2', 'ID2', 0), ('ASAP2', 'ASAP2', 0), ('KLF11', 'KLF11', 0), ('RRM2', 'RRM2', 0), ('C2ORF48', 'C2ORF48', 0), ('ODC1', 'ODC1', 0), ('RN7SL832P', 'RN7SL832P', 0), ('MIR3681HG', 'MIR3681HG', 0), ('FAM49A', 'FAM49A', 0), ('RHOB', 'RHOB', 0), ('AC009242.1', 'AC009242.1', 0), ('RAB10', 'RAB10', 0), ('CENPA', 'CENPA', 0), ('FOSL2', 'FOSL2', 0), ('LTBP1', 'LTBP1', 0), ('EIF2AK2', 'EIF2AK2', 0), ('QPCT', 'QPCT', 0), ('CDC42EP3', 'CDC42EP3', 0), ('CYP1B1', 'CYP1B1', 0), ('SLC8A1-AS1', 'SLC8A1-AS1', 0), ('SLC8A1', 'SLC8A1', 0), ('RHOQ', 'RHOQ', 0), ('CALM2', 'CALM2', 0), ('CCDC88A', 'CCDC88A', 0), ('MIR4432HG', 'MIR4432HG', 0), ('BCL11A', 'BCL11A', 0), ('REL', 'REL', 0), ('PELI1', 'PELI1', 0), ('LGALSL', 'LGALSL', 0), ('LINC01800', 'LINC01800', 0), ('ACTR2', 'ACTR2', 0), ('SPRED2', 'SPRED2', 0), ('MEIS1', 'MEIS1', 0), ('PLEK', 'PLEK', 0), ('MXD1', 'MXD1', 0), ('CLEC4F', 'CLEC4F', 0), ('ANKRD53', 'ANKRD53', 0), ('NAGK', 'NAGK', 0), ('DYSF', 'DYSF', 0), ('TET3', 'TET3', 0), ('TRABD2A', 'TRABD2A', 0), ('CAPG', 'CAPG', 0), ('MAT2A', 'MAT2A', 0), ('VAMP5', 'VAMP5', 0), ('GNLY', 'GNLY', 0), ('CD8B', 'CD8B', 0), ('CYTOR', 'CYTOR', 0), ('AC133644.2', 'AC133644.2', 0), ('IGKC', 'IGKC', 0), ('MAL', 'MAL', 0), ('DUSP2', 'DUSP2', 0), ('NCAPH', 'NCAPH', 0), ('AFF3', 'AFF3', 0), ('TBC1D8-AS1', 'TBC1D8-AS1', 0), ('IL1R2', 'IL1R2', 0), ('IL18RAP', 'IL18RAP', 0), ('LIMS1', 'LIMS1', 0), ('BUB1', 'BUB1', 0), ('MIR4435-2HG', 'MIR4435-2HG', 0), ('MERTK', 'MERTK', 0), ('IL1B', 'IL1B', 0), ('IL1RN', 'IL1RN', 0), ('MARCO', 'MARCO', 0), ('PROC', 'PROC', 0), ('LIMS2', 'LIMS2', 0), ('CXCR4', 'CXCR4', 0), ('HNMT', 'HNMT', 0), ('KYNU', 'KYNU', 0), ('ZEB2', 'ZEB2', 0), ('ZEB2-AS1', 'ZEB2-AS1', 0), ('LINC01412', 'LINC01412', 0), ('TNFAIP6', 'TNFAIP6', 0), ('FMNL2', 'FMNL2', 0), ('NR4A2', 'NR4A2', 0), ('BAZ2B', 'BAZ2B', 0), ('CD302', 'CD302', 0), ('SLC4A10', 'SLC4A10', 0), ('DPP4', 'DPP4', 0), ('IFIH1', 'IFIH1', 0), ('GCA', 'GCA', 0), ('COBLL1', 'COBLL1', 0), ('SLC38A11', 'SLC38A11', 0), ('SCN3A', 'SCN3A', 0), ('SCN9A', 'SCN9A', 0), ('DHRS9', 'DHRS9', 0), ('CYBRD1', 'CYBRD1', 0), ('MAP3K20', 'MAP3K20', 0), ('CDCA7', 'CDCA7', 0), ('GPR155', 'GPR155', 0), ('CHRNA1', 'CHRNA1', 0), ('CHN1', 'CHN1', 0), ('TTN', 'TTN', 0), ('LINC01934', 'LINC01934', 0), ('ITGA4', 'ITGA4', 0), ('SSFA2', 'SSFA2', 0), ('AC096667.1', 'AC096667.1', 0), ('SLC40A1', 'SLC40A1', 0), ('C2ORF88', 'C2ORF88', 0), ('STAT1', 'STAT1', 0), ('NABP1', 'NABP1', 0), ('CAVIN2', 'CAVIN2', 0), ('SPATS2L', 'SPATS2L', 0), ('CD28', 'CD28', 0), ('CTLA4', 'CTLA4', 0), ('LINC01857', 'LINC01857', 0), ('IKZF2', 'IKZF2', 0), ('FN1', 'FN1', 0), ('DIRC3', 'DIRC3', 0), ('GPBAR1', 'GPBAR1', 0), ('SLC11A1', 'SLC11A1', 0), ('CYP27A1', 'CYP27A1', 0), ('SLC4A3', 'SLC4A3', 0), ('CCL20', 'CCL20', 0), ('PID1', 'PID1', 0), ('SP140', 'SP140', 0), ('ITM2C', 'ITM2C', 0), ('NMUR1', 'NMUR1', 0), ('SH3BP4', 'SH3BP4', 0), ('RAMP1', 'RAMP1', 0), ('PASK', 'PASK', 0), ('CHL1', 'CHL1', 0), ('BHLHE40', 'BHLHE40', 0), ('CAMK1', 'CAMK1', 0), ('ANKRD28', 'ANKRD28', 0), ('OXNAD1', 'OXNAD1', 0), ('SGO1', 'SGO1', 0), ('UBE2E2', 'UBE2E2', 0), ('THRB', 'THRB', 0), ('EOMES', 'EOMES', 0), ('CMC1', 'CMC1', 0), ('OSBPL10', 'OSBPL10', 0), ('CRTAP', 'CRTAP', 0), ('AC112220.4', 'AC112220.4', 0), ('CTDSPL', 'CTDSPL', 0), ('ACAA1', 'ACAA1', 0), ('MYD88', 'MYD88', 0), ('CX3CR1', 'CX3CR1', 0), ('ABHD5', 'ABHD5', 0), ('TMEM158', 'TMEM158', 0), ('CCR9', 'CCR9', 0), ('CXCR6', 'CXCR6', 0), ('CCR1', 'CCR1', 0), ('CCR2', 'CCR2', 0), ('AC099778.1', 'AC099778.1', 0), ('COL7A1', 'COL7A1', 0), ('CISH', 'CISH', 0), ('MAPKAPK3', 'MAPKAPK3', 0), ('STAB1', 'STAB1', 0), ('NT5DC2', 'NT5DC2', 0), ('PRKCD', 'PRKCD', 0), ('TKT', 'TKT', 0), ('CACNA2D3', 'CACNA2D3', 0), ('DNASE1L3', 'DNASE1L3', 0), ('FHIT', 'FHIT', 0), ('FRMD4B', 'FRMD4B', 0), ('LINC00877', 'LINC00877', 0), ('RYBP', 'RYBP', 0), ('MTRNR2L12', 'MTRNR2L12', 0), ('CLDND1', 'CLDND1', 0), ('ST3GAL6', 'ST3GAL6', 0), ('FILIP1L', 'FILIP1L', 0), ('NFKBIZ', 'NFKBIZ', 0), ('AC106712.1', 'AC106712.1', 0), ('ALCAM', 'ALCAM', 0), ('TRAT1', 'TRAT1', 0), ('DPPA4', 'DPPA4', 0), ('CD200', 'CD200', 0), ('ATG3', 'ATG3', 0), ('ATP6V1A', 'ATP6V1A', 0), ('ZNF80', 'ZNF80', 0), ('ZBTB20-AS4', 'ZBTB20-AS4', 0), ('AC073352.2', 'AC073352.2', 0), ('EAF2', 'EAF2', 0), ('CD86', 'CD86', 0), ('CSTA', 'CSTA', 0), ('PARP9', 'PARP9', 0), ('PARP14', 'PARP14', 0), ('MYLK', 'MYLK', 0), ('ITGB5', 'ITGB5', 0), ('OSBPL11', 'OSBPL11', 0), ('CHST13', 'CHST13', 0), ('TXNRD3', 'TXNRD3', 0), ('MGLL', 'MGLL', 0), ('H1FX', 'H1FX', 0), ('PLXND1', 'PLXND1', 0), ('NUDT16', 'NUDT16', 0), ('ACPP', 'ACPP', 0), ('EPHB1', 'EPHB1', 0), ('ATP1B3', 'ATP1B3', 0), ('CHST2', 'CHST2', 0), ('PLSCR1', 'PLSCR1', 0), ('P2RY14', 'P2RY14', 0), ('P2RY13', 'P2RY13', 0), ('SUCNR1', 'SUCNR1', 0), ('MME', 'MME', 0), ('SSR3', 'SSR3', 0), ('TIPARP', 'TIPARP', 0), ('LINC02029', 'LINC02029', 0), ('CCNL1', 'CCNL1', 0), ('PTX3', 'PTX3', 0), ('MFSD1', 'MFSD1', 0), ('IL12A', 'IL12A', 0), ('SMC4', 'SMC4', 0), ('SPTSSB', 'SPTSSB', 0), ('SKIL', 'SKIL', 0), ('FNDC3B', 'FNDC3B', 0), ('TNFSF10', 'TNFSF10', 0), ('GNB4', 'GNB4', 0), ('BCL6', 'BCL6', 0), ('LPP', 'LPP', 0), ('OSTN-AS1', 'OSTN-AS1', 0), ('CCDC50', 'CCDC50', 0), ('HES1', 'HES1', 0), ('FAM43A', 'FAM43A', 0), ('XXYLT1-AS2', 'XXYLT1-AS2', 0), ('TFRC', 'TFRC', 0), ('ZDHHC19', 'ZDHHC19', 0), ('FGFRL1', 'FGFRL1', 0), ('SPON2', 'SPON2', 0), ('SH3BP2', 'SH3BP2', 0), ('RGS12', 'RGS12', 0), ('LYAR', 'LYAR', 0), ('NSG1', 'NSG1', 0), ('S100P', 'S100P', 0), ('CLNK', 'CLNK', 0), ('AC092546.1', 'AC092546.1', 0), ('FBXL5', 'FBXL5', 0), ('BST1', 'BST1', 0), ('CD38', 'CD38', 0), ('FGFBP2', 'FGFBP2', 0), ('LDB2', 'LDB2', 0), ('LAP3', 'LAP3', 0), ('NCAPG', 'NCAPG', 0), ('SEL1L3', 'SEL1L3', 0), ('RBPJ', 'RBPJ', 0), ('DTHD1', 'DTHD1', 0), ('TLR10', 'TLR10', 0), ('SMIM14', 'SMIM14', 0), ('RBM47', 'RBM47', 0), ('LIMCH1', 'LIMCH1', 0), ('NFXL1', 'NFXL1', 0), ('KIT', 'KIT', 0), ('NMU', 'NMU', 0), ('HOPX', 'HOPX', 0), ('SPINK2', 'SPINK2', 0), ('IGFBP7', 'IGFBP7', 0), ('JCHAIN', 'JCHAIN', 0), ('RUFY3', 'RUFY3', 0), ('CXCL8', 'CXCL8', 0), ('CXCL1', 'CXCL1', 0), ('PF4', 'PF4', 0), ('PPBP', 'PPBP', 0), ('CXCL3', 'CXCL3', 0), ('CXCL2', 'CXCL2', 0), ('EREG', 'EREG', 0), ('AREG', 'AREG', 0), ('PARM1', 'PARM1', 0), ('NAAA', 'NAAA', 0), ('CXCL10', 'CXCL10', 0), ('SCARB2', 'SCARB2', 0), ('SEPT11', 'SEPT11', 0), ('AC098818.2', 'AC098818.2', 0), ('BMP2K', 'BMP2K', 0), ('RASGEF1B', 'RASGEF1B', 0), ('PLAC8', 'PLAC8', 0), ('HPSE', 'HPSE', 0), ('GPAT3', 'GPAT3', 0), ('ARHGAP24', 'ARHGAP24', 0), ('HERC5', 'HERC5', 0), ('FAM13A', 'FAM13A', 0), ('SNCA', 'SNCA', 0), ('CCSER1', 'CCSER1', 0), ('EIF4E', 'EIF4E', 0), ('DAPP1', 'DAPP1', 0), ('BANK1', 'BANK1', 0), ('CENPE', 'CENPE', 0), ('TET2', 'TET2', 0), ('LEF1', 'LEF1', 0), ('PDE5A', 'PDE5A', 0), ('MAD2L1', 'MAD2L1', 0), ('ANXA5', 'ANXA5', 0), ('CCNA2', 'CCNA2', 0), ('IL2', 'IL2', 0), ('SPRY1', 'SPRY1', 0), ('SCLT1', 'SCLT1', 0), ('MGST2', 'MGST2', 0), ('TBC1D9', 'TBC1D9', 0), ('LINC02432', 'LINC02432', 0), ('INPP4B', 'INPP4B', 0), ('PRMT9', 'PRMT9', 0), ('AC097375.1', 'AC097375.1', 0), ('TMEM154', 'TMEM154', 0), ('MND1', 'MND1', 0), ('TLR2', 'TLR2', 0), ('GUCY1A1', 'GUCY1A1', 0), ('GUCY1B1', 'GUCY1B1', 0), ('FAM198B', 'FAM198B', 0), ('FNIP2', 'FNIP2', 0), ('MARCH1', 'MARCH1', 0), ('PALLD', 'PALLD', 0), ('GALNTL6', 'GALNTL6', 0), ('HMGB2', 'HMGB2', 0), ('SAP30', 'SAP30', 0), ('HPGD', 'HPGD', 0), ('CENPU', 'CENPU', 0), ('ACSL1', 'ACSL1', 0), ('OTULINL', 'OTULINL', 0), ('MYO10', 'MYO10', 0), ('BASP1', 'BASP1', 0), ('DAB2', 'DAB2', 0), ('AC025171.3', 'AC025171.3', 0), ('SNX18', 'SNX18', 0), ('ESM1', 'ESM1', 0), ('GZMK', 'GZMK', 0), ('GZMA', 'GZMA', 0), ('MAP3K1', 'MAP3K1', 0), ('PLK2', 'PLK2', 0), ('GAPT', 'GAPT', 0), ('DEPDC1B', 'DEPDC1B', 0), ('ZSWIM6', 'ZSWIM6', 0), ('MAST4-AS1', 'MAST4-AS1', 0), ('CD180', 'CD180', 0), ('CCNB1', 'CCNB1', 0), ('NAIP', 'NAIP', 0), ('ENC1', 'ENC1', 0), ('HEXB', 'HEXB', 0), ('IQGAP2', 'IQGAP2', 0), ('S100Z', 'S100Z', 0), ('LHFPL2', 'LHFPL2', 0), ('ZFYVE16', 'ZFYVE16', 0), ('ANKRD34B', 'ANKRD34B', 0), ('VCAN', 'VCAN', 0), ('MEF2C', 'MEF2C', 0), ('LUCAT1', 'LUCAT1', 0), ('MCTP1', 'MCTP1', 0), ('GLRX', 'GLRX', 0), ('NREP', 'NREP', 0), ('CCDC112', 'CCDC112', 0), ('SNX2', 'SNX2', 0), ('LMNB1', 'LMNB1', 0), ('IRF1', 'IRF1', 0), ('H2AFY', 'H2AFY', 0), ('TGFBI', 'TGFBI', 0), ('SPOCK1', 'SPOCK1', 0), ('FAM53C', 'FAM53C', 0), ('EGR1', 'EGR1', 0), ('CTNNA1', 'CTNNA1', 0), ('MZB1', 'MZB1', 0), ('CXXC5', 'CXXC5', 0), ('HBEGF', 'HBEGF', 0), ('PCDHGB6', 'PCDHGB6', 0), ('PCDH1', 'PCDH1', 0), ('ARHGAP26', 'ARHGAP26', 0), ('ADRB2', 'ADRB2', 0), ('PPARGC1B', 'PPARGC1B', 0), ('CSF1R', 'CSF1R', 0), ('TCOF1', 'TCOF1', 0), ('CD74', 'CD74', 0), ('GM2A', 'GM2A', 0), ('SPARC', 'SPARC', 0), ('TIMD4', 'TIMD4', 0), ('HAVCR2', 'HAVCR2', 0), ('EBF1', 'EBF1', 0), ('PTTG1', 'PTTG1', 0), ('HMMR', 'HMMR', 0), ('KCNMB1', 'KCNMB1', 0), ('DUSP1', 'DUSP1', 0), ('AC008429.1', 'AC008429.1', 0), ('HRH2', 'HRH2', 0), ('HK3', 'HK3', 0), ('PRELID1', 'PRELID1', 0), ('PDLIM7', 'PDLIM7', 0), ('DOK3', 'DOK3', 0), ('RUFY1', 'RUFY1', 0), ('LTC4S', 'LTC4S', 0), ('SQSTM1', 'SQSTM1', 0), ('RNF130', 'RNF130', 0), ('SCGB3A1', 'SCGB3A1', 0), ('IRF4', 'IRF4', 0), ('SERPINB1', 'SERPINB1', 0), ('TUBB2A', 'TUBB2A', 0), ('NRN1', 'NRN1', 0), ('F13A1', 'F13A1', 0), ('LY86-AS1', 'LY86-AS1', 0), ('LY86', 'LY86', 0), ('TXNDC5', 'TXNDC5', 0), ('AL024498.1', 'AL024498.1', 0), ('TMEM170B', 'TMEM170B', 0), ('ADTRP', 'ADTRP', 0), ('PHACTR1', 'PHACTR1', 0), ('CD83', 'CD83', 0), ('MYLIP', 'MYLIP', 0), ('GMPR', 'GMPR', 0), ('KIF13A', 'KIF13A', 0), ('KDM1B', 'KDM1B', 0), ('RNF144B', 'RNF144B', 0), ('SOX4', 'SOX4', 0), ('CASC15', 'CASC15', 0), ('HIST1H1A', 'HIST1H1A', 0), ('HIST1H1C', 'HIST1H1C', 0), ('HIST1H4C', 'HIST1H4C', 0), ('HIST1H2AC', 'HIST1H2AC', 0), ('HIST1H1E', 'HIST1H1E', 0), ('HIST1H2BG', 'HIST1H2BG', 0), ('HIST1H1D', 'HIST1H1D', 0), ('HIST1H2AG', 'HIST1H2AG', 0), ('HIST1H3H', 'HIST1H3H', 0), ('HIST1H2AL', 'HIST1H2AL', 0), ('HIST1H1B', 'HIST1H1B', 0), ('AL121944.1', 'AL121944.1', 0), ('AL358933.1', 'AL358933.1', 0), ('NKAPL', 'NKAPL', 0), ('PPP1R10', 'PPP1R10', 0), ('TUBB', 'TUBB', 0), ('IER3', 'IER3', 0), ('TNF', 'TNF', 0), ('LTB', 'LTB', 0), ('LST1', 'LST1', 0), ('NCR3', 'NCR3', 0), ('AIF1', 'AIF1', 0), ('MPIG6B', 'MPIG6B', 0), ('DDAH2', 'DDAH2', 0), ('HSPA1A', 'HSPA1A', 0), ('HSPA1B', 'HSPA1B', 0), ('C6ORF48', 'C6ORF48', 0), ('NEU1', 'NEU1', 0), ('C2', 'C2', 0), ('NOTCH4', 'NOTCH4', 0), ('HLA-DRA', 'HLA-DRA', 0), ('HLA-DRB5', 'HLA-DRB5', 0), ('HLA-DRB1', 'HLA-DRB1', 0), ('HLA-DQA1', 'HLA-DQA1', 0), ('HLA-DQB1', 'HLA-DQB1', 0), ('HLA-DQA2', 'HLA-DQA2', 0), ('HLA-DOB', 'HLA-DOB', 0), ('HLA-DMB', 'HLA-DMB', 0), ('HLA-DMA', 'HLA-DMA', 0), ('HLA-DOA', 'HLA-DOA', 0), ('HLA-DPA1', 'HLA-DPA1', 0), ('HLA-DPB1', 'HLA-DPB1', 0), ('KIFC1', 'KIFC1', 0), ('PACSIN1', 'PACSIN1', 0), ('ETV7', 'ETV7', 0), ('CDKN1A', 'CDKN1A', 0), ('CPNE5', 'CPNE5', 0), ('PI16', 'PI16', 0), ('FGD2', 'FGD2', 0), ('PIM1', 'PIM1', 0), ('KCNK17', 'KCNK17', 0), ('TREM2', 'TREM2', 0), ('TREM1', 'TREM1', 0), ('PTCRA', 'PTCRA', 0), ('CNPY3', 'CNPY3', 0), ('PTK7', 'PTK7', 0), ('CRIP3', 'CRIP3', 0), ('VEGFA', 'VEGFA', 0), ('NFKBIE', 'NFKBIE', 0), ('RUNX2', 'RUNX2', 0), ('AL096865.1', 'AL096865.1', 0), ('PLA2G7', 'PLA2G7', 0), ('TNFRSF21', 'TNFRSF21', 0), ('GSTA4', 'GSTA4', 0), ('DST', 'DST', 0), ('BEND6', 'BEND6', 0), ('AL391807.1', 'AL391807.1', 0), ('COL19A1', 'COL19A1', 0), ('OGFRL1', 'OGFRL1', 0), ('TTK', 'TTK', 0), ('TENT5A', 'TENT5A', 0), ('UBE2J1', 'UBE2J1', 0), ('BACH2', 'BACH2', 0), ('PRDM1', 'PRDM1', 0), ('CD24', 'CD24', 0), ('AL024507.2', 'AL024507.2', 0), ('MARCKS', 'MARCKS', 0), ('DSE', 'DSE', 0), ('CALHM6', 'CALHM6', 0), ('MAN1A1', 'MAN1A1', 0), ('PKIB', 'PKIB', 0), ('SMPDL3A', 'SMPDL3A', 0), ('CENPW', 'CENPW', 0), ('SAMD3', 'SAMD3', 0), ('EPB41L2', 'EPB41L2', 0), ('ENPP1', 'ENPP1', 0), ('LINC01013', 'LINC01013', 0), ('MOXD1', 'MOXD1', 0), ('STX7', 'STX7', 0), ('VNN1', 'VNN1', 0), ('VNN3', 'VNN3', 0), ('VNN2', 'VNN2', 0), ('SGK1', 'SGK1', 0), ('IFNGR1', 'IFNGR1', 0), ('TNFAIP3', 'TNFAIP3', 0), ('CITED2', 'CITED2', 0), ('STX11', 'STX11', 0), ('UTRN', 'UTRN', 0), ('RAB32', 'RAB32', 0), ('SASH1', 'SASH1', 0), ('ULBP1', 'ULBP1', 0), ('CCDC170', 'CCDC170', 0), ('SOD2', 'SOD2', 0), ('QKI', 'QKI', 0), ('AL022069.1', 'AL022069.1', 0), ('RNASET2', 'RNASET2', 0), ('CCR6', 'CCR6', 0), ('DLL1', 'DLL1', 0), ('PDGFA', 'PDGFA', 0), ('AC147651.1', 'AC147651.1', 0), ('LFNG', 'LFNG', 0), ('TTYH3', 'TTYH3', 0), ('ACTB', 'ACTB', 0), ('FSCN1', 'FSCN1', 0), ('RAC1', 'RAC1', 0), ('ARL4A', 'ARL4A', 0), ('TSPAN13', 'TSPAN13', 0), ('AHR', 'AHR', 0), ('HDAC9', 'HDAC9', 0), ('IL6', 'IL6', 0), ('GPNMB', 'GPNMB', 0), ('IGF2BP3', 'IGF2BP3', 0), ('SNX10', 'SNX10', 0), ('SKAP2', 'SKAP2', 0), ('HOXA9', 'HOXA9', 0), ('CREB5', 'CREB5', 0), ('CPVL', 'CPVL', 0), ('MTURN', 'MTURN', 0), ('PPP1R17', 'PPP1R17', 0), ('AOAH', 'AOAH', 0), ('TRGC2', 'TRGC2', 0), ('TRGC1', 'TRGC1', 0), ('TRG-AS1', 'TRG-AS1', 0), ('TRGV4', 'TRGV4', 0), ('BLVRA', 'BLVRA', 0), ('MRPS24', 'MRPS24', 0), ('PURB', 'PURB', 0), ('IGFBP3', 'IGFBP3', 0), ('TNS3', 'TNS3', 0), ('KCTD7', 'KCTD7', 0), ('LAT2', 'LAT2', 0), ('NCF1', 'NCF1', 0), ('HIP1', 'HIP1', 0), ('FGL2', 'FGL2', 0), ('CD36', 'CD36', 0), ('STEAP4', 'STEAP4', 0), ('FZD1', 'FZD1', 0), ('AKAP9', 'AKAP9', 0), ('CDK6', 'CDK6', 0), ('SAMD9L', 'SAMD9L', 0), ('GNG11', 'GNG11', 0), ('PEG10', 'PEG10', 0), ('PPP1R9A', 'PPP1R9A', 0), ('PON2', 'PON2', 0), ('PDK4', 'PDK4', 0), ('BRI3', 'BRI3', 0), ('MCM7', 'MCM7', 0), ('STAG3', 'STAG3', 0), ('PILRA', 'PILRA', 0), ('SERPINE1', 'SERPINE1', 0), ('CUX1', 'CUX1', 0), ('NAMPT', 'NAMPT', 0), ('AC007032.1', 'AC007032.1', 0), ('PRKAR2B', 'PRKAR2B', 0), ('LRRN3', 'LRRN3', 0), ('IFRD1', 'IFRD1', 0), ('TFEC', 'TFEC', 0), ('IRF5', 'IRF5', 0), ('TSPAN33', 'TSPAN33', 0), ('AC058791.1', 'AC058791.1', 0), ('PLXNA4', 'PLXNA4', 0), ('MTPN', 'MTPN', 0), ('TBXAS1', 'TBXAS1', 0), ('CLEC5A', 'CLEC5A', 0), ('TRBV3-1', 'TRBV3-1', 0), ('TRBV7-3', 'TRBV7-3', 0), ('TRBV14', 'TRBV14', 0), ('TRBV20-1', 'TRBV20-1', 0), ('TRBV21-1', 'TRBV21-1', 0), ('TRBV27', 'TRBV27', 0), ('TRBV28', 'TRBV28', 0), ('TRBC1', 'TRBC1', 0), ('TRBC2', 'TRBC2', 0), ('EPHB6', 'EPHB6', 0), ('ZYX', 'ZYX', 0), ('CNTNAP2', 'CNTNAP2', 0), ('EZH2', 'EZH2', 0), ('GHET1', 'GHET1', 0), ('PDIA4', 'PDIA4', 0), ('ZNF467', 'ZNF467', 0), ('LINC00996', 'LINC00996', 0), ('GIMAP8', 'GIMAP8', 0), ('GIMAP7', 'GIMAP7', 0), ('TMEM176B', 'TMEM176B', 0), ('TMEM176A', 'TMEM176A', 0), ('SMARCD3', 'SMARCD3', 0), ('INSIG1', 'INSIG1', 0), ('RNF32', 'RNF32', 0), ('LINC00685', 'LINC00685', 0), ('CSF2RA', 'CSF2RA', 0), ('IL3RA', 'IL3RA', 0), ('ARHGAP6', 'ARHGAP6', 0), ('TLR7', 'TLR7', 0), ('TLR8', 'TLR8', 0), ('TMSB4X', 'TMSB4X', 0), ('AP1S2', 'AP1S2', 0), ('SCML1', 'SCML1', 0), ('PHEX', 'PHEX', 0), ('SAT1', 'SAT1', 0), ('KLHL15', 'KLHL15', 0), ('CXORF21', 'CXORF21', 0), ('CYBB', 'CYBB', 0), ('MID1IP1', 'MID1IP1', 0), ('MIR222HG', 'MIR222HG', 0), ('AC234772.3', 'AC234772.3', 0), ('TIMP1', 'TIMP1', 0), ('CFP', 'CFP', 0), ('PCSK1N', 'PCSK1N', 0), ('MAGIX', 'MAGIX', 0), ('PLP2', 'PLP2', 0), ('FOXP3', 'FOXP3', 0), ('TSPYL2', 'TSPYL2', 0), ('AL034397.3', 'AL034397.3', 0), ('VSIG4', 'VSIG4', 0), ('AR', 'AR', 0), ('CXCR3', 'CXCR3', 0), ('NAP1L2', 'NAP1L2', 0), ('ITM2A', 'ITM2A', 0), ('DIAPH2', 'DIAPH2', 0), ('BTK', 'BTK', 0), ('BEX3', 'BEX3', 0), ('PAK3', 'PAK3', 0), ('IL13RA1', 'IL13RA1', 0), ('SLC25A5', 'SLC25A5', 0), ('SH2D1A', 'SH2D1A', 0), ('FIRRE', 'FIRRE', 0), ('MIR503HG', 'MIR503HG', 0), ('CD40LG', 'CD40LG', 0), ('HMGB3', 'HMGB3', 0), ('ZNF185', 'ZNF185', 0), ('SLC6A8', 'SLC6A8', 0), ('L1CAM', 'L1CAM', 0), ('TKTL1', 'TKTL1', 0), ('MPP1', 'MPP1', 0), ('CLIC2', 'CLIC2', 0), ('MYOM2', 'MYOM2', 0), ('AC103957.2', 'AC103957.2', 0), ('BLK', 'BLK', 0), ('AC069185.1', 'AC069185.1', 0), ('CTSB', 'CTSB', 0), ('AC123777.1', 'AC123777.1', 0), ('MSR1', 'MSR1', 0), ('ASAH1', 'ASAH1', 0), ('LPL', 'LPL', 0), ('ATP6V1B2', 'ATP6V1B2', 0), ('GFRA2', 'GFRA2', 0), ('EGR3', 'EGR3', 0), ('ADAM28', 'ADAM28', 0), ('ADAMDEC1', 'ADAMDEC1', 0), ('NEFL', 'NEFL', 0), ('DOCK5', 'DOCK5', 0), ('DPYSL2', 'DPYSL2', 0), ('CLU', 'CLU', 0), ('ESCO2', 'ESCO2', 0), ('SCARA5', 'SCARA5', 0), ('PNOC', 'PNOC', 0), ('FZD3', 'FZD3', 0), ('DUSP4', 'DUSP4', 0), ('AC044849.1', 'AC044849.1', 0), ('RBPMS', 'RBPMS', 0), ('NRG1', 'NRG1', 0), ('ZNF703', 'ZNF703', 0), ('RAB11FIP1', 'RAB11FIP1', 0), ('EIF4EBP1', 'EIF4EBP1', 0), ('PLPP5', 'PLPP5', 0), ('IDO1', 'IDO1', 0), ('ZMAT4', 'ZMAT4', 0), ('AC009630.2', 'AC009630.2', 0), ('AC083973.1', 'AC083973.1', 0), ('CEBPD', 'CEBPD', 0), ('MCM4', 'MCM4', 0), ('LYN', 'LYN', 0), ('RPS20', 'RPS20', 0), ('SDCBP', 'SDCBP', 0), ('CA8', 'CA8', 0), ('GGH', 'GGH', 0), ('MYBL1', 'MYBL1', 0), ('SLCO5A1', 'SLCO5A1', 0), ('LY96', 'LY96', 0), ('TPD52', 'TPD52', 0), ('FABP5', 'FABP5', 0), ('LRRCC1', 'LRRCC1', 0), ('CA2', 'CA2', 0), ('GEM', 'GEM', 0), ('KLF10', 'KLF10', 0), ('AP003354.2', 'AP003354.2', 0), ('BAALC', 'BAALC', 0), ('ANGPT1', 'ANGPT1', 0), ('PKHD1L1', 'PKHD1L1', 0), ('TRPS1', 'TRPS1', 0), ('NOV', 'NOV', 0), ('DEPTOR', 'DEPTOR', 0), ('MTSS1', 'MTSS1', 0), ('TRIB1', 'TRIB1', 0), ('MYC', 'MYC', 0), ('CCDC26', 'CCDC26', 0), ('ASAP1', 'ASAP1', 0), ('ZFAT', 'ZFAT', 0), ('DENND3', 'DENND3', 0), ('PTP4A3', 'PTP4A3', 0), ('ARC', 'ARC', 0), ('LYPD2', 'LYPD2', 0), ('LY6E', 'LY6E', 0), ('NAPRT', 'NAPRT', 0), ('GRINA', 'GRINA', 0), ('TONSL', 'TONSL', 0), ('JAK2', 'JAK2', 0), ('CNTLN', 'CNTLN', 0), ('PLIN2', 'PLIN2', 0), ('RPS6', 'RPS6', 0), ('C9ORF72', 'C9ORF72', 0), ('DNAJA1', 'DNAJA1', 0), ('AQP3', 'AQP3', 0), ('ENHO', 'ENHO', 0), ('CD72', 'CD72', 0), ('SIT1', 'SIT1', 0), ('TPM2', 'TPM2', 0), ('TLN1', 'TLN1', 0), ('TMEM8B', 'TMEM8B', 0), ('RECK', 'RECK', 0), ('PAX5', 'PAX5', 0), ('AL161781.2', 'AL161781.2', 0), ('ZFAND5', 'ZFAND5', 0), ('ALDH1A1', 'ALDH1A1', 0), ('ANXA1', 'ANXA1', 0), ('GNAQ', 'GNAQ', 0), ('CEP78', 'CEP78', 0), ('DAPK1', 'DAPK1', 0), ('CTSL', 'CTSL', 0), ('C9ORF47', 'C9ORF47', 0), ('S1PR3', 'S1PR3', 0), ('CKS2', 'CKS2', 0), ('GADD45G', 'GADD45G', 0), ('SYK', 'SYK', 0), ('LINC00484', 'LINC00484', 0), ('NFIL3', 'NFIL3', 0), ('NINJ1', 'NINJ1', 0), ('FBP1', 'FBP1', 0), ('AL590705.1', 'AL590705.1', 0), ('HEMGN', 'HEMGN', 0), ('NR4A3', 'NR4A3', 0), ('SMC2', 'SMC2', 0), ('LINC01505', 'LINC01505', 0), ('KLF4', 'KLF4', 0), ('TXN', 'TXN', 0), ('UGCG', 'UGCG', 0), ('SUSD1', 'SUSD1', 0), ('SLC31A2', 'SLC31A2', 0), ('FKBP15', 'FKBP15', 0), ('ORM1', 'ORM1', 0), ('TLR4', 'TLR4', 0), ('MEGF9', 'MEGF9', 0), ('GSN', 'GSN', 0), ('STRBP', 'STRBP', 0), ('HSPA5', 'HSPA5', 0), ('FAM129B', 'FAM129B', 0), ('TTC16', 'TTC16', 0), ('IER5L', 'IER5L', 0), ('AL158151.3', 'AL158151.3', 0), ('LINC01503', 'LINC01503', 0), ('NUP214', 'NUP214', 0), ('GFI1B', 'GFI1B', 0), ('SLC2A6', 'SLC2A6', 0), ('RXRA', 'RXRA', 0), ('FCN1', 'FCN1', 0), ('OLFM1', 'OLFM1', 0), ('NACC2', 'NACC2', 0), ('CARD9', 'CARD9', 0), ('EGFL7', 'EGFL7', 0), ('LCN12', 'LCN12', 0), ('PTGDS', 'PTGDS', 0), ('LCNL1', 'LCNL1', 0), ('CLIC3', 'CLIC3', 0), ('FUT7', 'FUT7', 0), ('NPDC1', 'NPDC1', 0), ('LRRC26', 'LRRC26', 0), ('TUBB4B', 'TUBB4B', 0), ('NRARP', 'NRARP', 0), ('IFITM3', 'IFITM3', 0), ('RNH1', 'RNH1', 0), ('IRF7', 'IRF7', 0), ('SCT', 'SCT', 0), ('TALDO1', 'TALDO1', 0), ('CEND1', 'CEND1', 0), ('TSPAN4', 'TSPAN4', 0), ('CTSD', 'CTSD', 0), ('TNNI2', 'TNNI2', 0), ('ASCL2', 'ASCL2', 0), ('KCNQ1OT1', 'KCNQ1OT1', 0), ('CDKN1C', 'CDKN1C', 0), ('PHLDA2', 'PHLDA2', 0), ('HBB', 'HBB', 0), ('CAVIN3', 'CAVIN3', 0), ('TPP1', 'TPP1', 0), ('RPL27A', 'RPL27A', 0), ('SWAP70', 'SWAP70', 0), ('AC026250.1', 'AC026250.1', 0), ('SBF2', 'SBF2', 0), ('ADM', 'ADM', 0), ('MTRNR2L8', 'MTRNR2L8', 0), ('RNF141', 'RNF141', 0), ('MICAL2', 'MICAL2', 0), ('FAR1', 'FAR1', 0), ('NUCB2', 'NUCB2', 0), ('CCDC34', 'CCDC34', 0), ('PRRG4', 'PRRG4', 0), ('CD59', 'CD59', 0), ('LMO2', 'LMO2', 0), ('CAT', 'CAT', 0), ('C11ORF96', 'C11ORF96', 0), ('CD82', 'CD82', 0), ('SPI1', 'SPI1', 0), ('SERPING1', 'SERPING1', 0), ('AP001636.3', 'AP001636.3', 0), ('FAM111B', 'FAM111B', 0), ('MPEG1', 'MPEG1', 0), ('MS4A6A', 'MS4A6A', 0), ('MS4A4A', 'MS4A4A', 0), ('MS4A7', 'MS4A7', 0), ('MS4A1', 'MS4A1', 0), ('PTGDR2', 'PTGDR2', 0), ('SLC15A3', 'SLC15A3', 0), ('CD6', 'CD6', 0), ('CD5', 'CD5', 0), ('CYB561A3', 'CYB561A3', 0), ('FEN1', 'FEN1', 0), ('FADS1', 'FADS1', 0), ('FTH1', 'FTH1', 0), ('ASRGL1', 'ASRGL1', 0), ('AHNAK', 'AHNAK', 0), ('GNG3', 'GNG3', 0), ('NXF1', 'NXF1', 0), ('AP001160.1', 'AP001160.1', 0), ('HRASLS2', 'HRASLS2', 0), ('PLA2G16', 'PLA2G16', 0), ('RTN3', 'RTN3', 0), ('PPP1R14B', 'PPP1R14B', 0), ('RPS6KA4', 'RPS6KA4', 0), ('CDCA5', 'CDCA5', 0), ('AP003068.2', 'AP003068.2', 0), ('NEAT1', 'NEAT1', 0), ('EHBP1L1', 'EHBP1L1', 0), ('CTSW', 'CTSW', 0), ('CORO1B', 'CORO1B', 0), ('GSTP1', 'GSTP1', 0), ('ACY3', 'ACY3', 0), ('UNC93B1', 'UNC93B1', 0), ('ALDH3B1', 'ALDH3B1', 0), ('CCND1', 'CCND1', 0), ('FOLR3', 'FOLR3', 0), ('FOLR2', 'FOLR2', 0), ('ARAP1', 'ARAP1', 0), ('FCHSD2', 'FCHSD2', 0), ('P2RY6', 'P2RY6', 0), ('RELT', 'RELT', 0), ('PGM2L1', 'PGM2L1', 0), ('KCNE3', 'KCNE3', 0), ('ARRB1', 'ARRB1', 0), ('ACER3', 'ACER3', 0), ('PAK1', 'PAK1', 0), ('PRCP', 'PRCP', 0), ('PICALM', 'PICALM', 0), ('PRSS23', 'PRSS23', 0), ('CTSC', 'CTSC', 0), ('SMCO4', 'SMCO4', 0), ('BIRC3', 'BIRC3', 0), ('CASP5', 'CASP5', 0), ('CASP1', 'CASP1', 0), ('CARD16', 'CARD16', 0), ('POU2AF1', 'POU2AF1', 0), ('IL18', 'IL18', 0), ('AP002884.1', 'AP002884.1', 0), ('CADM1', 'CADM1', 0), ('SIDT2', 'SIDT2', 0), ('TAGLN', 'TAGLN', 0), ('FXYD2', 'FXYD2', 0), ('JAML', 'JAML', 0), ('CD3D', 'CD3D', 0), ('CD3G', 'CD3G', 0), ('H2AFX', 'H2AFX', 0), ('OAF', 'OAF', 0), ('TMEM136', 'TMEM136', 0), ('NRGN', 'NRGN', 0), ('ESAM', 'ESAM', 0), ('C11ORF45', 'C11ORF45', 0), ('APLP2', 'APLP2', 0), ('ST14', 'ST14', 0), ('KLF6', 'KLF6', 0), ('AKR1C3', 'AKR1C3', 0), ('GDI2', 'GDI2', 0), ('PFKFB3', 'PFKFB3', 0), ('GATA3', 'GATA3', 0), ('VIM', 'VIM', 0), ('HACD1', 'HACD1', 0), ('TMEM236', 'TMEM236', 0), ('MRC1', 'MRC1', 0), ('NSUN6', 'NSUN6', 0), ('ARL5B', 'ARL5B', 0), ('PLXDC2', 'PLXDC2', 0), ('MSRB2', 'MSRB2', 0), ('OTUD1', 'OTUD1', 0), ('ENKUR', 'ENKUR', 0), ('ANKRD26', 'ANKRD26', 0), ('YME1L1', 'YME1L1', 0), ('MPP7', 'MPP7', 0), ('WAC-AS1', 'WAC-AS1', 0), ('BAMBI', 'BAMBI', 0), ('MAP3K8', 'MAP3K8', 0), ('ITGB1', 'ITGB1', 0), ('NRP1', 'NRP1', 0), ('FZD8', 'FZD8', 0), ('RASSF4', 'RASSF4', 0), ('ALOX5', 'ALOX5', 0), ('NCOA4', 'NCOA4', 0), ('WDFY4', 'WDFY4', 0), ('ZWINT', 'ZWINT', 0), ('CDK1', 'CDK1', 0), ('RHOBTB1', 'RHOBTB1', 0), ('ARID5B', 'ARID5B', 0), ('RTKN2', 'RTKN2', 0), ('EGR2', 'EGR2', 0), ('SRGN', 'SRGN', 0), ('PALD1', 'PALD1', 0), ('PRF1', 'PRF1', 0), ('SLC29A3', 'SLC29A3', 0), ('CDH23', 'CDH23', 0), ('VSIR', 'VSIR', 0), ('PSAP', 'PSAP', 0), ('SPOCK2', 'SPOCK2', 0), ('DDIT4', 'DDIT4', 0), ('PLAU', 'PLAU', 0), ('VCL', 'VCL', 0), ('ZNF503', 'ZNF503', 0), ('LRMDA', 'LRMDA', 0), ('KCNMA1', 'KCNMA1', 0), ('ZMIZ1', 'ZMIZ1', 0), ('PPIF', 'PPIF', 0), ('FAM213A', 'FAM213A', 0), ('AL603756.1', 'AL603756.1', 0), ('CDHR1', 'CDHR1', 0), ('ADIRF', 'ADIRF', 0), ('PAPSS2', 'PAPSS2', 0), ('ANKRD22', 'ANKRD22', 0), ('LIPA', 'LIPA', 0), ('IFIT2', 'IFIT2', 0), ('IFIT3', 'IFIT3', 0), ('IFIT1', 'IFIT1', 0), ('KIF20B', 'KIF20B', 0), ('KIF11', 'KIF11', 0), ('HHEX', 'HHEX', 0), ('MYOF', 'MYOF', 0), ('CEP55', 'CEP55', 0), ('HELLS', 'HELLS', 0), ('PDLIM1', 'PDLIM1', 0), ('ENTPD1', 'ENTPD1', 0), ('BLNK', 'BLNK', 0), ('PIK3AP1', 'PIK3AP1', 0), ('FRAT2', 'FRAT2', 0), ('RRP12', 'RRP12', 0), ('TRIM8', 'TRIM8', 0), ('NEURL1', 'NEURL1', 0), ('GSTO1', 'GSTO1', 0), ('DUSP5', 'DUSP5', 0), ('TCF7L2', 'TCF7L2', 0), ('CCDC186', 'CCDC186', 0), ('SHTN1', 'SHTN1', 0), ('SLC18A2', 'SLC18A2', 0), ('CHST15', 'CHST15', 0), ('CTBP2', 'CTBP2', 0), ('FANK1', 'FANK1', 0), ('DOCK1', 'DOCK1', 0), ('PTPRE', 'PTPRE', 0), ('MKI67', 'MKI67', 0), ('DPYSL4', 'DPYSL4', 0), ('UTF1', 'UTF1', 0), ('FUOM', 'FUOM', 0), ('CACNA2D4', 'CACNA2D4', 0), ('KCNA5', 'KCNA5', 0), ('CD9', 'CD9', 0), ('LTBR', 'LTBR', 0), ('CD27', 'CD27', 0), ('GAPDH', 'GAPDH', 0), ('CHD4', 'CHD4', 0), ('ACRBP', 'ACRBP', 0), ('AC125494.2', 'AC125494.2', 0), ('PTMS', 'PTMS', 0), ('CDCA3', 'CDCA3', 0), ('PTPN6', 'PTPN6', 0), ('CD163', 'CD163', 0), ('CLEC4C', 'CLEC4C', 0), ('SLC2A3', 'SLC2A3', 0), ('C3AR1', 'C3AR1', 0), ('CLEC4A', 'CLEC4A', 0), ('LINC00937', 'LINC00937', 0), ('CLEC4D', 'CLEC4D', 0), ('CLEC4E', 'CLEC4E', 0), ('KLRG1', 'KLRG1', 0), ('A2M', 'A2M', 0), ('KLRB1', 'KLRB1', 0), ('CLECL1', 'CLECL1', 0), ('CD69', 'CD69', 0), ('KLRF1', 'KLRF1', 0), ('CLEC12A', 'CLEC12A', 0), ('CLEC7A', 'CLEC7A', 0), ('OLR1', 'OLR1', 0), ('GABARAPL1', 'GABARAPL1', 0), ('KLRD1', 'KLRD1', 0), ('KLRC4', 'KLRC4', 0), ('KLRC2', 'KLRC2', 0), ('KLRC1', 'KLRC1', 0), ('LINC02446', 'LINC02446', 0), ('YBX3', 'YBX3', 0), ('EMP1', 'EMP1', 0), ('PLBD1', 'PLBD1', 0), ('EPS8', 'EPS8', 0), ('MGST1', 'MGST1', 0), ('SOX5', 'SOX5', 0), ('BHLHE41', 'BHLHE41', 0), ('SSPN', 'SSPN', 0), ('STK38L', 'STK38L', 0), ('TMTC1', 'TMTC1', 0), ('DENND5B', 'DENND5B', 0), ('AC023157.3', 'AC023157.3', 0), ('FGD4', 'FGD4', 0), ('LRRK2', 'LRRK2', 0), ('NELL2', 'NELL2', 0), ('FKBP11', 'FKBP11', 0), ('TUBA1B', 'TUBA1B', 0), ('TUBA1A', 'TUBA1A', 0), ('TUBA1C', 'TUBA1C', 0), ('TROAP', 'TROAP', 0), ('METTL7A', 'METTL7A', 0), ('GRASP', 'GRASP', 0), ('NR4A1', 'NR4A1', 0), ('KRT7', 'KRT7', 0), ('KRT86', 'KRT86', 0), ('KRT5', 'KRT5', 0), ('NFE2', 'NFE2', 0), ('ZNF385A', 'ZNF385A', 0), ('CD63', 'CD63', 0), ('IL23A', 'IL23A', 0), ('STAT2', 'STAT2', 0), ('LRP1', 'LRP1', 0), ('NXPH4', 'NXPH4', 0), ('STAC3', 'STAC3', 0), ('DDIT3', 'DDIT3', 0), ('AC135279.3', 'AC135279.3', 0), ('GNS', 'GNS', 0), ('MSRB3', 'MSRB3', 0), ('IRAK3', 'IRAK3', 0), ('GRIP1', 'GRIP1', 0), ('IFNG-AS1', 'IFNG-AS1', 0), ('IFNG', 'IFNG', 0), ('LYZ', 'LYZ', 0), ('AC020656.1', 'AC020656.1', 0), ('AC025263.1', 'AC025263.1', 0), ('AC025569.1', 'AC025569.1', 0), ('GLIPR1', 'GLIPR1', 0), ('PHLDA1', 'PHLDA1', 0), ('NAP1L1', 'NAP1L1', 0), ('ZDHHC17', 'ZDHHC17', 0), ('PAWR', 'PAWR', 0), ('LIN7A', 'LIN7A', 0), ('ACSS3', 'ACSS3', 0), ('TMTC2', 'TMTC2', 0), ('CEP290', 'CEP290', 0), ('DUSP6', 'DUSP6', 0), ('ATP2B1', 'ATP2B1', 0), ('ATP2B1-AS1', 'ATP2B1-AS1', 0), ('AC025164.1', 'AC025164.1', 0), ('LINC02397', 'LINC02397', 0), ('EEA1', 'EEA1', 0), ('PLXNC1', 'PLXNC1', 0), ('HAL', 'HAL', 0), ('LTA4H', 'LTA4H', 0), ('IKBIP', 'IKBIP', 0), ('APAF1', 'APAF1', 0), ('ANKS1B', 'ANKS1B', 0), ('HSP90B1', 'HSP90B1', 0), ('C12ORF45', 'C12ORF45', 0), ('C12ORF75', 'C12ORF75', 0), ('CKAP4', 'CKAP4', 0), ('ASCL4', 'ASCL4', 0), ('CMKLR1', 'CMKLR1', 0), ('CORO1C', 'CORO1C', 0), ('ARPC3', 'ARPC3', 0), ('HVCN1', 'HVCN1', 0), ('CUX2', 'CUX2', 0), ('PHETA1', 'PHETA1', 0), ('SH2B3', 'SH2B3', 0), ('ALDH2', 'ALDH2', 0), ('RPH3A', 'RPH3A', 0), ('OAS1', 'OAS1', 0), ('OAS3', 'OAS3', 0), ('OAS2', 'OAS2', 0), ('SDS', 'SDS', 0), ('HRK', 'HRK', 0), ('TESC', 'TESC', 0), ('CIT', 'CIT', 0), ('DYNLL1', 'DYNLL1', 0), ('MLEC', 'MLEC', 0), ('OASL', 'OASL', 0), ('P2RX7', 'P2RX7', 0), ('CAMKK2', 'CAMKK2', 0), ('BCL7A', 'BCL7A', 0), ('HCAR2', 'HCAR2', 0), ('HCAR3', 'HCAR3', 0), ('ABCB9', 'ABCB9', 0), ('RILPL2', 'RILPL2', 0), ('BRI3BP', 'BRI3BP', 0), ('SLC15A4', 'SLC15A4', 0), ('GLT1D1', 'GLT1D1', 0), ('FLT3', 'FLT3', 0), ('ALOX5AP', 'ALOX5AP', 0), ('HSPH1', 'HSPH1', 0), ('FRY', 'FRY', 0), ('LHFPL6', 'LHFPL6', 0), ('RGCC', 'RGCC', 0), ('EPSTI1', 'EPSTI1', 0), ('TSC22D1', 'TSC22D1', 0), ('LCP1', 'LCP1', 0), ('RUBCNL', 'RUBCNL', 0), ('AL158196.1', 'AL158196.1', 0), ('LPAR6', 'LPAR6', 0), ('RCBTB2', 'RCBTB2', 0), ('ARL11', 'ARL11', 0), ('INTS6', 'INTS6', 0), ('WDFY2', 'WDFY2', 0), ('PCDH9', 'PCDH9', 0), ('DACH1', 'DACH1', 0), ('LMO7-AS1', 'LMO7-AS1', 0), ('KCTD12', 'KCTD12', 0), ('GPR183', 'GPR183', 0), ('CCDC168', 'CCDC168', 0), ('TNFSF13B', 'TNFSF13B', 0), ('IRS2', 'IRS2', 0), ('GAS6', 'GAS6', 0), ('AL355075.4', 'AL355075.4', 0), ('PNP', 'PNP', 0), ('RNASE6', 'RNASE6', 0), ('RNASE1', 'RNASE1', 0), ('RNASE2', 'RNASE2', 0), ('NDRG2', 'NDRG2', 0), ('ARHGEF40', 'ARHGEF40', 0), ('TRAV1-2', 'TRAV1-2', 0), ('TRAV4', 'TRAV4', 0), ('TRAV5', 'TRAV5', 0), ('TRAV6', 'TRAV6', 0), ('TRAV8-3', 'TRAV8-3', 0), ('TRAV8-4', 'TRAV8-4', 0), ('TRAV14DV4', 'TRAV14DV4', 0), ('TRAV17', 'TRAV17', 0), ('TRAV27', 'TRAV27', 0), ('TRAV29DV5', 'TRAV29DV5', 0), ('TRAV36DV7', 'TRAV36DV7', 0), ('TRAV41', 'TRAV41', 0), ('TRDC', 'TRDC', 0), ('TRAC', 'TRAC', 0), ('SLC7A7', 'SLC7A7', 0), ('PSME2', 'PSME2', 0), ('IRF9', 'IRF9', 0), ('GZMH', 'GZMH', 0), ('GZMB', 'GZMB', 0), ('COCH', 'COCH', 0), ('NFKBIA', 'NFKBIA', 0), ('AL162311.3', 'AL162311.3', 0), ('AL132639.2', 'AL132639.2', 0), ('MIS18BP1', 'MIS18BP1', 0), ('AL627171.1', 'AL627171.1', 0), ('LINC01588', 'LINC01588', 0), ('PYGL', 'PYGL', 0), ('PTGDR', 'PTGDR', 0), ('CDKN3', 'CDKN3', 0), ('SAMD4A', 'SAMD4A', 0), ('LGALS3', 'LGALS3', 0), ('DLGAP5', 'DLGAP5', 0), ('DACT1', 'DACT1', 0), ('DAAM1', 'DAAM1', 0), ('RTN1', 'RTN1', 0), ('AL359220.1', 'AL359220.1', 0), ('SYNE2', 'SYNE2', 0), ('HSPA2', 'HSPA2', 0), ('ZFP36L1', 'ZFP36L1', 0), ('AC004817.3', 'AC004817.3', 0), ('AC004846.1', 'AC004846.1', 0), ('NUMB', 'NUMB', 0), ('NPC2', 'NPC2', 0), ('FOS', 'FOS', 0), ('JDP2', 'JDP2', 0), ('SAMD15', 'SAMD15', 0), ('SPTLC2', 'SPTLC2', 0), ('TSHR', 'TSHR', 0), ('STON2', 'STON2', 0), ('KCNK10', 'KCNK10', 0), ('SLC24A4', 'SLC24A4', 0), ('LGMN', 'LGMN', 0), ('OTUB2', 'OTUB2', 0), ('IFI27', 'IFI27', 0), ('SERPINA1', 'SERPINA1', 0), ('CLMN', 'CLMN', 0), ('AL133467.1', 'AL133467.1', 0), ('TCL1B', 'TCL1B', 0), ('TCL1A', 'TCL1A', 0), ('AL139020.1', 'AL139020.1', 0), ('CYP46A1', 'CYP46A1', 0), ('WARS', 'WARS', 0), ('MEG3', 'MEG3', 0), ('HSP90AA1', 'HSP90AA1', 0), ('RCOR1', 'RCOR1', 0), ('TNFAIP2', 'TNFAIP2', 0), ('EIF5', 'EIF5', 0), ('CKB', 'CKB', 0), ('PLD4', 'PLD4', 0), ('JAG2', 'JAG2', 0), ('CRIP2', 'CRIP2', 0), ('CRIP1', 'CRIP1', 0), ('IGHA2', 'IGHA2', 0), ('IGHE', 'IGHE', 0), ('IGHG4', 'IGHG4', 0), ('IGHG2', 'IGHG2', 0), ('IGHA1', 'IGHA1', 0), ('IGHG1', 'IGHG1', 0), ('IGHG3', 'IGHG3', 0), ('IGHD', 'IGHD', 0), ('IGHM', 'IGHM', 0), ('FAM30A', 'FAM30A', 0), ('IGHV3-7', 'IGHV3-7', 0), ('CYFIP1', 'CYFIP1', 0), ('APBA2', 'APBA2', 0), ('AC012236.1', 'AC012236.1', 0), ('LINC02345', 'LINC02345', 0), ('SPRED1', 'SPRED1', 0), ('C15ORF53', 'C15ORF53', 0), ('THBS1', 'THBS1', 0), ('BUB1B', 'BUB1B', 0), ('PLCB2', 'PLCB2', 0), ('AC091045.1', 'AC091045.1', 0), ('KNL1', 'KNL1', 0), ('CHP1', 'CHP1', 0), ('OIP5', 'OIP5', 0), ('NUSAP1', 'NUSAP1', 0), ('LTK', 'LTK', 0), ('AC020659.1', 'AC020659.1', 0), ('ZNF106', 'ZNF106', 0), ('MAP1A', 'MAP1A', 0), ('PATL2', 'PATL2', 0), ('AC025580.3', 'AC025580.3', 0), ('C15ORF48', 'C15ORF48', 0), ('DMXL2', 'DMXL2', 0), ('GNB5', 'GNB5', 0), ('LINC00926', 'LINC00926', 0), ('AQP9', 'AQP9', 0), ('AC090515.2', 'AC090515.2', 0), ('CCNB2', 'CCNB2', 0), ('ANXA2', 'ANXA2', 0), ('RORA', 'RORA', 0), ('LACTB', 'LACTB', 0), ('DAPK2', 'DAPK2', 0), ('SNX22', 'SNX22', 0), ('PPIB', 'PPIB', 0), ('PCLAF', 'PCLAF', 0), ('OAZ2', 'OAZ2', 0), ('PLEKHO2', 'PLEKHO2', 0), ('SMAD6', 'SMAD6', 0), ('KIF23', 'KIF23', 0), ('SCAMP5', 'SCAMP5', 0), ('C15ORF39', 'C15ORF39', 0), ('CIB2', 'CIB2', 0), ('IDH3A', 'IDH3A', 0), ('DNAJA4', 'DNAJA4', 0), ('PSMA4', 'PSMA4', 0), ('CTSH', 'CTSH', 0), ('BCL2A1', 'BCL2A1', 0), ('HOMER2', 'HOMER2', 0), ('TM6SF1', 'TM6SF1', 0), ('ISG20', 'ISG20', 0), ('HAPLN3', 'HAPLN3', 0), ('FANCI', 'FANCI', 0), ('ANPEP', 'ANPEP', 0), ('ARPIN', 'ARPIN', 0), ('FES', 'FES', 0), ('AC106028.4', 'AC106028.4', 0), ('ARRDC4', 'ARRDC4', 0), ('HBA2', 'HBA2', 0), ('HBA1', 'HBA1', 0), ('HBQ1', 'HBQ1', 0), ('CACNA1H', 'CACNA1H', 0), ('MSRB1', 'MSRB1', 0), ('RPS2', 'RPS2', 0), ('IL32', 'IL32', 0), ('AC108134.2', 'AC108134.2', 0), ('MEFV', 'MEFV', 0), ('ROGDI', 'ROGDI', 0), ('TVP23A', 'TVP23A', 0), ('CIITA', 'CIITA', 0), ('SOCS1', 'SOCS1', 0), ('AC007613.1', 'AC007613.1', 0), ('TNFRSF17', 'TNFRSF17', 0), ('CPPED1', 'CPPED1', 0), ('COQ7', 'COQ7', 0), ('ITPRIPL2', 'ITPRIPL2', 0), ('CRYM', 'CRYM', 0), ('IGSF6', 'IGSF6', 0), ('PLK1', 'PLK1', 0), ('AC106739.2', 'AC106739.2', 0), ('IL4R', 'IL4R', 0), ('NPIPB6', 'NPIPB6', 0), ('SULT1A1', 'SULT1A1', 0), ('SPN', 'SPN', 0), ('DOC2A', 'DOC2A', 0), ('PYCARD', 'PYCARD', 0), ('TRIM72', 'TRIM72', 0), ('ITGAM', 'ITGAM', 0), ('ITGAX', 'ITGAX', 0), ('MYLK3', 'MYLK3', 0), ('LPCAT2', 'LPCAT2', 0), ('CES1', 'CES1', 0), ('MT2A', 'MT2A', 0), ('MT1E', 'MT1E', 0), ('SLC12A3', 'SLC12A3', 0), ('HERPUD1', 'HERPUD1', 0), ('ADGRG1', 'ADGRG1', 0), ('AC010542.4', 'AC010542.4', 0), ('CMTM2', 'CMTM2', 0), ('RRAD', 'RRAD', 0), ('TPPP3', 'TPPP3', 0), ('ZDHHC1', 'ZDHHC1', 0), ('ATP6V0D1', 'ATP6V0D1', 0), ('SMPD3', 'SMPD3', 0), ('CDH1', 'CDH1', 0), ('HP', 'HP', 0), ('ZFHX3', 'ZFHX3', 0), ('MAF', 'MAF', 0), ('HSBP1', 'HSBP1', 0), ('COTL1', 'COTL1', 0), ('USP10', 'USP10', 0), ('CRISPLD2', 'CRISPLD2', 0), ('ZDHHC7', 'ZDHHC7', 0), ('KIAA0513', 'KIAA0513', 0), ('GINS2', 'GINS2', 0), ('IRF8', 'IRF8', 0), ('MAP1LC3B', 'MAP1LC3B', 0), ('AC010536.1', 'AC010536.1', 0), ('AC134312.5', 'AC134312.5', 0), ('CYBA', 'CYBA', 0), ('CDT1', 'CDT1', 0), ('CBFA2T3', 'CBFA2T3', 0), ('AC141424.1', 'AC141424.1', 0), ('RFLNB', 'RFLNB', 0), ('VPS53', 'VPS53', 0), ('SLC43A2', 'SLC43A2', 0), ('TLCD2', 'TLCD2', 0), ('MIR22HG', 'MIR22HG', 0), ('SERPINF1', 'SERPINF1', 0), ('HIC1', 'HIC1', 0), ('P2RX5', 'P2RX5', 0), ('P2RX1', 'P2RX1', 0), ('CYB5D2', 'CYB5D2', 0), ('AC091153.3', 'AC091153.3', 0), ('CXCL16', 'CXCL16', 0), ('VMO1', 'VMO1', 0), ('GP1BA', 'GP1BA', 0), ('INCA1', 'INCA1', 0), ('SCIMP', 'SCIMP', 0), ('XAF1', 'XAF1', 0), ('RNASEK', 'RNASEK', 0), ('CLEC10A', 'CLEC10A', 0), ('ASGR2', 'ASGR2', 0), ('ASGR1', 'ASGR1', 0), ('POLR2A', 'POLR2A', 0), ('CD68', 'CD68', 0), ('KDM6B', 'KDM6B', 0), ('PER1', 'PER1', 0), ('AURKB', 'AURKB', 0), ('LINC00324', 'LINC00324', 0), ('GAS7', 'GAS7', 0), ('AC005224.3', 'AC005224.3', 0), ('PMP22', 'PMP22', 0), ('TNFRSF13B', 'TNFRSF13B', 0), ('NT5M', 'NT5M', 0), ('RASD1', 'RASD1', 0), ('LINC02076', 'LINC02076', 0), ('AC007952.4', 'AC007952.4', 0), ('SPECC1', 'SPECC1', 0), ('WSB1', 'WSB1', 0), ('LGALS9', 'LGALS9', 0), ('UNC119', 'UNC119', 0), ('RAB34', 'RAB34', 0), ('RPL23A', 'RPL23A', 0), ('NEK8', 'NEK8', 0), ('TRAF4', 'TRAF4', 0), ('NUFIP2', 'NUFIP2', 0), ('ADAP2', 'ADAP2', 0), ('RNF135', 'RNF135', 0), ('EVI2B', 'EVI2B', 0), ('EVI2A', 'EVI2A', 0), ('CCL2', 'CCL2', 0), ('CCL5', 'CCL5', 0), ('CCL3', 'CCL3', 0), ('CCL4', 'CCL4', 0), ('AC243829.2', 'AC243829.2', 0), ('CCL3L1', 'CCL3L1', 0), ('CCL4L2', 'CCL4L2', 0), ('RPL23', 'RPL23', 0), ('CDK12', 'CDK12', 0), ('RARA', 'RARA', 0), ('RARA-AS1', 'RARA-AS1', 0), ('TOP2A', 'TOP2A', 0), ('AC004585.1', 'AC004585.1', 0), ('CCR7', 'CCR7', 0), ('JUP', 'JUP', 0), ('ATP6V0A1', 'ATP6V0A1', 0), ('CCR10', 'CCR10', 0), ('ARL4D', 'ARL4D', 0), ('MEOX1', 'MEOX1', 0), ('DUSP3', 'DUSP3', 0), ('GRN', 'GRN', 0), ('HEXIM1', 'HEXIM1', 0), ('TBX21', 'TBX21', 0), ('HOXB2', 'HOXB2', 0), ('HOXB7', 'HOXB7', 0), ('ABI3', 'ABI3', 0), ('SAMD14', 'SAMD14', 0), ('ABCC3', 'ABCC3', 0), ('MMD', 'MMD', 0), ('NOG', 'NOG', 0), ('SCPEP1', 'SCPEP1', 0), ('AC091271.1', 'AC091271.1', 0), ('VMP1', 'VMP1', 0), ('MAP3K3', 'MAP3K3', 0), ('CD79B', 'CD79B', 0), ('PECAM1', 'PECAM1', 0), ('MILR1', 'MILR1', 0), ('CD300A', 'CD300A', 0), ('CD300LB', 'CD300LB', 0), ('CD300C', 'CD300C', 0), ('AC064805.1', 'AC064805.1', 0), ('CD300E', 'CD300E', 0), ('CD300LF', 'CD300LF', 0), ('SMIM5', 'SMIM5', 0), ('SMIM6', 'SMIM6', 0), ('FOXJ1', 'FOXJ1', 0), ('ST6GALNAC1', 'ST6GALNAC1', 0), ('AC005837.1', 'AC005837.1', 0), ('SYNGR2', 'SYNGR2', 0), ('TK1', 'TK1', 0), ('BIRC5', 'BIRC5', 0), ('SOCS3', 'SOCS3', 0), ('TIMP2', 'TIMP2', 0), ('GAA', 'GAA', 0), ('EIF4A3', 'EIF4A3', 0), ('ENDOV', 'ENDOV', 0), ('ACTG1', 'ACTG1', 0), ('MAFG', 'MAFG', 0), ('SLC16A3', 'SLC16A3', 0), ('CD7', 'CD7', 0), ('SECTM1', 'SECTM1', 0), ('AC124283.1', 'AC124283.1', 0), ('FN3K', 'FN3K', 0), ('METRNL', 'METRNL', 0), ('TYMS', 'TYMS', 0), ('EMILIN2', 'EMILIN2', 0), ('EPB41L3', 'EPB41L3', 0), ('RAB31', 'RAB31', 0), ('CHMP1B', 'CHMP1B', 0), ('IMPA2', 'IMPA2', 0), ('TUBB6', 'TUBB6', 0), ('AP005482.1', 'AP005482.1', 0), ('RBBP8', 'RBBP8', 0), ('TTC39C-AS1', 'TTC39C-AS1', 0), ('OSBPL1A', 'OSBPL1A', 0), ('KLHL14', 'KLHL14', 0), ('DTNA', 'DTNA', 0), ('AC011815.2', 'AC011815.2', 0), ('PSTPIP2', 'PSTPIP2', 0), ('AC093462.1', 'AC093462.1', 0), ('RAB27B', 'RAB27B', 0), ('TCF4', 'TCF4', 0), ('SEC11C', 'SEC11C', 0), ('PMAIP1', 'PMAIP1', 0), ('CDH20', 'CDH20', 0), ('BCL2', 'BCL2', 0), ('SERPINB2', 'SERPINB2', 0), ('SERPINB8', 'SERPINB8', 0), ('NETO1', 'NETO1', 0), ('PARD6G-AS1', 'PARD6G-AS1', 0), ('FAM110A', 'FAM110A', 0), ('FKBP1A', 'FKBP1A', 0), ('SIRPB2', 'SIRPB2', 0), ('SIRPB1', 'SIRPB1', 0), ('SIRPG', 'SIRPG', 0), ('SIRPA', 'SIRPA', 0), ('SIGLEC1', 'SIGLEC1', 0), ('C20ORF27', 'C20ORF27', 0), ('SMOX', 'SMOX', 0), ('RASSF2', 'RASSF2', 0), ('PCNA', 'PCNA', 0), ('GPCPD1', 'GPCPD1', 0), ('PLCB1', 'PLCB1', 0), ('LAMP5', 'LAMP5', 0), ('RRBP1', 'RRBP1', 0), ('THBD', 'THBD', 0), ('CD93', 'CD93', 0), ('GZF1', 'GZF1', 0), ('CST3', 'CST3', 0), ('CST7', 'CST7', 0), ('HM13-AS1', 'HM13-AS1', 0), ('ID1', 'ID1', 0), ('TPX2', 'TPX2', 0), ('HCK', 'HCK', 0), ('PLAGL2', 'PLAGL2', 0), ('E2F1', 'E2F1', 0), ('ASIP', 'ASIP', 0), ('TP53INP2', 'TP53INP2', 0), ('MYL9', 'MYL9', 0), ('SAMHD1', 'SAMHD1', 0), ('MROH8', 'MROH8', 0), ('TGM2', 'TGM2', 0), ('MAFB', 'MAFB', 0), ('MYBL2', 'MYBL2', 0), ('TOX2', 'TOX2', 0), ('PKIG', 'PKIG', 0), ('ADA', 'ADA', 0), ('UBE2C', 'UBE2C', 0), ('CD40', 'CD40', 0), ('AL031055.1', 'AL031055.1', 0), ('SULF2', 'SULF2', 0), ('SNAI1', 'SNAI1', 0), ('CEBPB', 'CEBPB', 0), ('SMIM25', 'SMIM25', 0), ('KCNG1', 'KCNG1', 0), ('TSHZ2', 'TSHZ2', 0), ('ZNF217', 'ZNF217', 0), ('BCAS1', 'BCAS1', 0), ('PMEPA1', 'PMEPA1', 0), ('CTSZ', 'CTSZ', 0), ('TUBB1', 'TUBB1', 0), ('PHACTR3', 'PHACTR3', 0), ('KCNQ2', 'KCNQ2', 0), ('C20ORF204', 'C20ORF204', 0), ('LKAAEAR1', 'LKAAEAR1', 0), ('GZMM', 'GZMM', 0), ('PRSS57', 'PRSS57', 0), ('CFD', 'CFD', 0), ('ARID3A', 'ARID3A', 0), ('MIDN', 'MIDN', 0), ('JSRP1', 'JSRP1', 0), ('OAZ1', 'OAZ1', 0), ('GADD45B', 'GADD45B', 0), ('GNG7', 'GNG7', 0), ('ZNF556', 'ZNF556', 0), ('AC119403.1', 'AC119403.1', 0), ('GNA15', 'GNA15', 0), ('MATK', 'MATK', 0), ('SHD', 'SHD', 0), ('AC011498.1', 'AC011498.1', 0), ('PLIN5', 'PLIN5', 0), ('LRG1', 'LRG1', 0), ('SEMA6B', 'SEMA6B', 0), ('MYDGF', 'MYDGF', 0), ('UHRF1', 'UHRF1', 0), ('TNFSF9', 'TNFSF9', 0), ('CD70', 'CD70', 0), ('ADGRE1', 'ADGRE1', 0), ('INSR', 'INSR', 0), ('MCOLN1', 'MCOLN1', 0), ('STXBP2', 'STXBP2', 0), ('RETN', 'RETN', 0), ('MCEMP1', 'MCEMP1', 0), ('FCER2', 'FCER2', 0), ('PRAM1', 'PRAM1', 0), ('MYO1F', 'MYO1F', 0), ('COL5A3', 'COL5A3', 0), ('ICAM1', 'ICAM1', 0), ('ICAM4', 'ICAM4', 0), ('S1PR5', 'S1PR5', 0), ('C19ORF38', 'C19ORF38', 0), ('LDLR', 'LDLR', 0), ('SPC24', 'SPC24', 0), ('RAB3D', 'RAB3D', 0), ('TMEM205', 'TMEM205', 0), ('ACP5', 'ACP5', 0), ('ZNF844', 'ZNF844', 0), ('JUNB', 'JUNB', 0), ('DNASE2', 'DNASE2', 0), ('LYL1', 'LYL1', 0), ('IER2', 'IER2', 0), ('AC020916.1', 'AC020916.1', 0), ('ASF1B', 'ASF1B', 0), ('ADGRE5', 'ADGRE5', 0), ('DNAJB1', 'DNAJB1', 0), ('ADGRE2', 'ADGRE2', 0), ('TPM4', 'TPM4', 0), ('AC020911.1', 'AC020911.1', 0), ('PLVAP', 'PLVAP', 0), ('PGLS', 'PGLS', 0), ('FAM129C', 'FAM129C', 0), ('IFI30', 'IFI30', 0), ('LRRC25', 'LRRC25', 0), ('HOMER3', 'HOMER3', 0), ('PLEKHF1', 'PLEKHF1', 0), ('CEBPA', 'CEBPA', 0), ('SCGB1B2P', 'SCGB1B2P', 0), ('SCN1B', 'SCN1B', 0), ('FXYD1', 'FXYD1', 0), ('FXYD7', 'FXYD7', 0), ('HAMP', 'HAMP', 0), ('CD22', 'CD22', 0), ('FFAR2', 'FFAR2', 0), ('ZBTB32', 'ZBTB32', 0), ('NFKBID', 'NFKBID', 0), ('TYROBP', 'TYROBP', 0), ('SPINT2', 'SPINT2', 0), ('PPP1R14A', 'PPP1R14A', 0), ('C19ORF33', 'C19ORF33', 0), ('KCNK6', 'KCNK6', 0), ('RASGRP4', 'RASGRP4', 0), ('ZFP36', 'ZFP36', 0), ('AC005393.1', 'AC005393.1', 0), ('PLD3', 'PLD3', 0), ('SERTAD1', 'SERTAD1', 0), ('BLVRB', 'BLVRB', 0), ('AXL', 'AXL', 0), ('LINC01480', 'LINC01480', 0), ('CEACAM4', 'CEACAM4', 0), ('CEACAM3', 'CEACAM3', 0), ('CD79A', 'CD79A', 0), ('POU2F2', 'POU2F2', 0), ('CNFN', 'CNFN', 0), ('PLAUR', 'PLAUR', 0), ('RELB', 'RELB', 0), ('FOSB', 'FOSB', 0), ('PPM1N', 'PPM1N', 0), ('VASP', 'VASP', 0), ('PTGIR', 'PTGIR', 0), ('DACT3', 'DACT3', 0), ('SLC1A5', 'SLC1A5', 0), ('AP2S1', 'AP2S1', 0), ('C5AR1', 'C5AR1', 0), ('C5AR2', 'C5AR2', 0), ('PPP1R15A', 'PPP1R15A', 0), ('FTL', 'FTL', 0), ('TRPM4', 'TRPM4', 0), ('RPL13A', 'RPL13A', 0), ('RPS11', 'RPS11', 0), ('FCGRT', 'FCGRT', 0), ('RRAS', 'RRAS', 0), ('ATF5', 'ATF5', 0), ('SPIB', 'SPIB', 0), ('KLK1', 'KLK1', 0), ('CD33', 'CD33', 0), ('NKG7', 'NKG7', 0), ('SIGLEC10', 'SIGLEC10', 0), ('SIGLEC6', 'SIGLEC6', 0), ('FPR1', 'FPR1', 0), ('FPR2', 'FPR2', 0), ('FPR3', 'FPR3', 0), ('ZNF331', 'ZNF331', 0), ('AC008753.3', 'AC008753.3', 0), ('NLRP12', 'NLRP12', 0), ('MYADM', 'MYADM', 0), ('VSTM1', 'VSTM1', 0), ('OSCAR', 'OSCAR', 0), ('MBOAT7', 'MBOAT7', 0), ('LILRB3', 'LILRB3', 0), ('LILRA6', 'LILRA6', 0), ('LILRB2', 'LILRB2', 0), ('LILRA5', 'LILRA5', 0), ('LILRA4', 'LILRA4', 0), ('LAIR1', 'LAIR1', 0), ('LAIR2', 'LAIR2', 0), ('LILRA2', 'LILRA2', 0), ('LILRA1', 'LILRA1', 0), ('LILRB1', 'LILRB1', 0), ('LILRB4', 'LILRB4', 0), ('KIR2DL4', 'KIR2DL4', 0), ('KIR3DL1', 'KIR3DL1', 0), ('KIR3DL2', 'KIR3DL2', 0), ('AC245128.3', 'AC245128.3', 0), ('NCR1', 'NCR1', 0), ('NLRP7', 'NLRP7', 0), ('TNNT1', 'TNNT1', 0), ('UBE2S', 'UBE2S', 0), ('MZF1-AS1', 'MZF1-AS1', 0), ('PCDH11Y', 'PCDH11Y', 0), ('IL17RA', 'IL17RA', 0), ('ADA2', 'ADA2', 0), ('BID', 'BID', 0), ('LINC00528', 'LINC00528', 0), ('CDC45', 'CDC45', 0), ('COMT', 'COMT', 0), ('PPM1F', 'PPM1F', 0), ('IGLV6-57', 'IGLV6-57', 0), ('AC245060.5', 'AC245060.5', 0), ('IGLL5', 'IGLL5', 0), ('IGLC2', 'IGLC2', 0), ('IGLC3', 'IGLC3', 0), ('IGLC5', 'IGLC5', 0), ('IGLC6', 'IGLC6', 0), ('IGLC7', 'IGLC7', 0), ('GNAZ', 'GNAZ', 0), ('VPREB3', 'VPREB3', 0), ('DERL3', 'DERL3', 0), ('GRK3', 'GRK3', 0), ('CRYBB1', 'CRYBB1', 0), ('MIAT', 'MIAT', 0), ('XBP1', 'XBP1', 0), ('GAS2L1', 'GAS2L1', 0), ('OSM', 'OSM', 0), ('YWHAH', 'YWHAH', 0), ('HMOX1', 'HMOX1', 0), ('MCM5', 'MCM5', 0), ('NCF4', 'NCF4', 0), ('CSF2RB', 'CSF2RB', 0), ('TST', 'TST', 0), ('IL2RB', 'IL2RB', 0), ('LGALS2', 'LGALS2', 0), ('LGALS1', 'LGALS1', 0), ('H1F0', 'H1F0', 0), ('MAFF', 'MAFF', 0), ('APOBEC3A', 'APOBEC3A', 0), ('APOBEC3G', 'APOBEC3G', 0), ('SYNGR1', 'SYNGR1', 0), ('SHISA8', 'SHISA8', 0), ('TNFRSF13C', 'TNFRSF13C', 0), ('CENPM', 'CENPM', 0), ('NAGA', 'NAGA', 0), ('NFAM1', 'NFAM1', 0), ('Z93241.1', 'Z93241.1', 0), ('BIK', 'BIK', 0), ('TSPO', 'TSPO', 0), ('KIAA0930', 'KIAA0930', 0), ('UPK3A', 'UPK3A', 0), ('TTC38', 'TTC38', 0), ('GTSE1', 'GTSE1', 0), ('LINC01644', 'LINC01644', 0), ('PLXNB2', 'PLXNB2', 0), ('DENND6B', 'DENND6B', 0), ('TYMP', 'TYMP', 0), ('ODF3B', 'ODF3B', 0), ('APP', 'APP', 0), ('CYYR1', 'CYYR1', 0), ('ADAMTS1', 'ADAMTS1', 0), ('ADAMTS5', 'ADAMTS5', 0), ('MAP3K7CL', 'MAP3K7CL', 0), ('BACH1', 'BACH1', 0), ('TIAM1', 'TIAM1', 0), ('OLIG1', 'OLIG1', 0), ('IL10RB-DT', 'IL10RB-DT', 0), ('IFNGR2', 'IFNGR2', 0), ('KCNE1', 'KCNE1', 0), ('AP000692.2', 'AP000692.2', 0), ('ETS2', 'ETS2', 0), ('BACE2', 'BACE2', 0), ('MX2', 'MX2', 0), ('MX1', 'MX1', 0), ('RSPH1', 'RSPH1', 0), ('PDXK', 'PDXK', 0), ('CSTB', 'CSTB', 0), ('AATBC', 'AATBC', 0), ('AIRE', 'AIRE', 0), ('COL6A2', 'COL6A2', 0), ('S100B', 'S100B', 0), ('MT-ND1', 'MT-ND1', 0), ('MT-ND2', 'MT-ND2', 0), ('MT-CO1', 'MT-CO1', 0), ('MT-ATP8', 'MT-ATP8', 0), ('MT-ATP6', 'MT-ATP6', 0), ('MT-CO3', 'MT-CO3', 0), ('MT-ND5', 'MT-ND5', 0), ('CD3prot', 'CD3prot', 0), ('CD45ROprot', 'CD45ROprot', 0)])" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "guidance.edges" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "8151fc57-f9e2-467c-8d8c-da153ad86471", + "metadata": {}, + "outputs": [], + "source": [ + "from itertools import chain\n", + "\n", + "import anndata as ad\n", + "import itertools\n", + "import networkx as nx\n", + "import pandas as pd\n", + "import scanpy as sc\n", + "import scglue\n", + "import seaborn as sns\n", + "from matplotlib import rcParams" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "db335c62-b701-4a7f-8eae-1bddbad038bc", + "metadata": {}, + "outputs": [], + "source": [ + "scglue.plot.set_publication_params()\n", + "rcParams[\"figure.figsize\"] = (4, 4)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "b83d348e-eccc-4f8b-9f58-e7a4d4f5886e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['AAACCCAAGATTGTGA-1', 'AAACCCACATCGGTTA-1', 'AAACCCAGTACCGCGT-1',\n", + " 'AAACCCAGTATCGAAA-1', 'AAACCCAGTCGTCATA-1', 'AAACCCAGTCTACACA-1',\n", + " 'AAACCCAGTGCAAGAC-1', 'AAACCCAGTGCATTTG-1', 'AAACCCATCCGATGTA-1',\n", + " 'AAACCCATCTCAACGA-1',\n", + " ...\n", + " 'TTTGGAGCACTCATAG-1', 'TTTGGAGCAGCGGTTC-1', 'TTTGGTTCAAAGCGTG-1',\n", + " 'TTTGGTTGTAATGTGA-1', 'TTTGGTTGTACCTGTA-1', 'TTTGGTTGTACGAGTG-1',\n", + " 'TTTGTTGAGTTAACAG-1', 'TTTGTTGCAGCACAAG-1', 'TTTGTTGCAGTCTTCC-1',\n", + " 'TTTGTTGCATTGCCGG-1'],\n", + " dtype='object', name='index', length=10849)" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rna.obs_names" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "d34e541b-ef0a-4ca4-ba68-9f466fc7af17", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['AAACCCAAGATTGTGA-1', 'AAACCCACATCGGTTA-1', 'AAACCCAGTACCGCGT-1',\n", + " 'AAACCCAGTATCGAAA-1', 'AAACCCAGTCGTCATA-1', 'AAACCCAGTCTACACA-1',\n", + " 'AAACCCAGTGCAAGAC-1', 'AAACCCAGTGCATTTG-1', 'AAACCCATCCGATGTA-1',\n", + " 'AAACCCATCTCAACGA-1',\n", + " ...\n", + " 'TTTGGAGCACTCATAG-1', 'TTTGGAGCAGCGGTTC-1', 'TTTGGTTCAAAGCGTG-1',\n", + " 'TTTGGTTGTAATGTGA-1', 'TTTGGTTGTACCTGTA-1', 'TTTGGTTGTACGAGTG-1',\n", + " 'TTTGTTGAGTTAACAG-1', 'TTTGTTGCAGCACAAG-1', 'TTTGTTGCAGTCTTCC-1',\n", + " 'TTTGTTGCATTGCCGG-1'],\n", + " dtype='object', name='index', length=10849)" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "prot.obs_names" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "3c36df47-b7f0-49b5-8f94-ba845224ff36", + "metadata": {}, + "outputs": [], + "source": [ + "rna.obs_names_make_unique()" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "c24ccfbb-7a3c-42b8-9f41-9b4d578b8b88", + "metadata": {}, + "outputs": [], + "source": [ + "prot.obs_names_make_unique()" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "3d6f3138-1ac6-464c-b11c-b86979772c08", + "metadata": {}, + "outputs": [], + "source": [ + "scglue.models.configure_dataset(\n", + " rna, \"NB\", use_highly_variable=True,\n", + " use_layer=\"counts\", use_rep=\"X_pca\", use_obs_names=True, use_batch='batch'\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "51cf7b89-180a-4668-b2a4-b48da6667542", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[WARNING] configure_dataset: `configure_dataset` has already been called. Previous configuration will be overwritten!\n" + ] + } + ], + "source": [ + "scglue.models.configure_dataset(\n", + " prot, \"NBMixture\", use_highly_variable=False,use_layer=\"counts\", use_obs_names=True, use_batch='batch'\n", + ") # the default appraoch to model the ADT part of cite-seq is nbmixture model, referring from TOTALVI" + ] + }, + { + "cell_type": "markdown", + "id": "a5c56451-8073-4aa9-93bc-6b566d57caf0", + "metadata": {}, + "source": [ + "We can also model the protein data based on normal distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "ac10194d-20f0-4614-929f-04ef6fca97e8", + "metadata": {}, + "outputs": [], + "source": [ + "# scglue.utils.clr(prot) #need the clr preprocessing\n", + "# sc.pp.scale(prot)\n", + "# sc.tl.pca(prot)\n", + "# scglue.models.configure_dataset(\n", + "# prot, \"Normal\", use_rep=\"X_pca\", use_highly_variable=False, use_obs_names=True, use_batch='batch'\n", + "# )" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "c9f9c5cb-2ac8-44ea-8fad-a8768823f993", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[INFO] fit_SCGLUE: Pretraining SCGLUE model...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[WARNING] PairedSCGLUEModel: It is recommended that `use_rep` dimensionality be equal or larger than `latent_dim`.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Index(['AL645608.8', 'HES4', 'ISG15', 'TTLL10', 'TNFRSF18', 'TNFRSF4',\n", + " 'AL645728.1', 'MMP23B', 'NADK', 'AJAP1',\n", + " ...\n", + " 'AIRE', 'COL6A2', 'S100B', 'MT-ND1', 'MT-ND2', 'MT-CO1', 'MT-ATP8',\n", + " 'MT-ATP6', 'MT-CO3', 'MT-ND5'],\n", + " dtype='object', name='index', length=2000)\n", + "[INFO] autodevice: Using GPU 0 as computation device.\n", + "[INFO] check_graph: Checking variable coverage...\n", + "[INFO] check_graph: Checking edge attributes...\n", + "[INFO] check_graph: Checking self-loops...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/gpfs/gibbs/project/zhao/tl688/conda_envs/scglue/lib/python3.9/abc.py:119: FutureWarning: SparseDataset is deprecated and will be removed in late 2024. It has been replaced by the public classes CSRDataset and CSCDataset.\n", + "\n", + "For instance checks, use `isinstance(X, (anndata.experimental.CSRDataset, anndata.experimental.CSCDataset))` instead.\n", + "\n", + "For creation, use `anndata.experimental.sparse_dataset(X)` instead.\n", + "\n", + " return _abc_instancecheck(cls, instance)\n", + "[WARNING] check_graph: Missing self-loop!\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[INFO] check_graph: Checking graph symmetry...\n", + "[INFO] PairedSCGLUEModel: Setting `graph_batch_size` = 701\n", + "[INFO] PairedSCGLUEModel: Setting `max_epochs` = 315\n", + "[INFO] PairedSCGLUEModel: Setting `patience` = 27\n", + "[INFO] PairedSCGLUEModel: Setting `reduce_lr_patience` = 14\n", + "[INFO] PairedSCGLUETrainer: Using training directory: \"glue_prot/pretrain\"\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/gpfs/gibbs/project/zhao/tl688/conda_envs/scglue/lib/python3.9/abc.py:119: FutureWarning: SparseDataset is deprecated and will be removed in late 2024. It has been replaced by the public classes CSRDataset and CSCDataset.\n", + "\n", + "For instance checks, use `isinstance(X, (anndata.experimental.CSRDataset, anndata.experimental.CSCDataset))` instead.\n", + "\n", + "For creation, use `anndata.experimental.sparse_dataset(X)` instead.\n", + "\n", + " return _abc_instancecheck(cls, instance)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[INFO] PairedSCGLUETrainer: [Epoch 10] train={'g_nll': 0.439, 'g_kl': 0.06, 'g_elbo': 0.499, 'x_rna_nll': 0.421, 'x_rna_kl': 0.012, 'x_rna_elbo': 0.433, 'x_atac_nll': 1.033, 'x_atac_kl': 0.195, 'x_atac_elbo': 1.228, 'dsc_loss': 0.515, 'vae_loss': 1.758, 'gen_loss': 1.732, 'joint_cross_loss': 1.459, 'real_cross_loss': 1.881, 'cos_loss': 0.531}, val={'g_nll': 0.435, 'g_kl': 0.062, 'g_elbo': 0.497, 'x_rna_nll': 0.428, 'x_rna_kl': 0.011, 'x_rna_elbo': 0.439, 'x_atac_nll': 1.014, 'x_atac_kl': 0.17, 'x_atac_elbo': 1.184, 'dsc_loss': 0.497, 'vae_loss': 1.72, 'gen_loss': 1.695, 'joint_cross_loss': 1.447, 'real_cross_loss': 1.847, 'cos_loss': 0.531}, 5.4s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 20] train={'g_nll': 0.39, 'g_kl': 0.083, 'g_elbo': 0.473, 'x_rna_nll': 0.399, 'x_rna_kl': 0.009, 'x_rna_elbo': 0.407, 'x_atac_nll': 0.777, 'x_atac_kl': 0.311, 'x_atac_elbo': 1.088, 'dsc_loss': 0.579, 'vae_loss': 1.578, 'gen_loss': 1.549, 'joint_cross_loss': 1.153, 'real_cross_loss': 1.506, 'cos_loss': 0.527}, val={'g_nll': 0.369, 'g_kl': 0.083, 'g_elbo': 0.452, 'x_rna_nll': 0.396, 'x_rna_kl': 0.009, 'x_rna_elbo': 0.405, 'x_atac_nll': 0.738, 'x_atac_kl': 0.314, 'x_atac_elbo': 1.052, 'dsc_loss': 0.586, 'vae_loss': 1.538, 'gen_loss': 1.508, 'joint_cross_loss': 1.126, 'real_cross_loss': 1.49, 'cos_loss': 0.529}, 5.6s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 30] train={'g_nll': 0.292, 'g_kl': 0.082, 'g_elbo': 0.374, 'x_rna_nll': 0.393, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.402, 'x_atac_nll': 0.713, 'x_atac_kl': 0.351, 'x_atac_elbo': 1.064, 'dsc_loss': 0.6, 'vae_loss': 1.541, 'gen_loss': 1.511, 'joint_cross_loss': 1.074, 'real_cross_loss': 1.391, 'cos_loss': 0.524}, val={'g_nll': 0.287, 'g_kl': 0.082, 'g_elbo': 0.369, 'x_rna_nll': 0.388, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.397, 'x_atac_nll': 0.679, 'x_atac_kl': 0.344, 'x_atac_elbo': 1.022, 'dsc_loss': 0.593, 'vae_loss': 1.492, 'gen_loss': 1.463, 'joint_cross_loss': 1.05, 'real_cross_loss': 1.366, 'cos_loss': 0.525}, 5.5s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 40] train={'g_nll': 0.211, 'g_kl': 0.08, 'g_elbo': 0.291, 'x_rna_nll': 0.391, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.399, 'x_atac_nll': 0.671, 'x_atac_kl': 0.38, 'x_atac_elbo': 1.052, 'dsc_loss': 0.635, 'vae_loss': 1.519, 'gen_loss': 1.487, 'joint_cross_loss': 1.013, 'real_cross_loss': 1.273, 'cos_loss': 0.524}, val={'g_nll': 0.203, 'g_kl': 0.08, 'g_elbo': 0.283, 'x_rna_nll': 0.386, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.638, 'x_atac_kl': 0.373, 'x_atac_elbo': 1.011, 'dsc_loss': 0.626, 'vae_loss': 1.472, 'gen_loss': 1.44, 'joint_cross_loss': 0.98, 'real_cross_loss': 1.23, 'cos_loss': 0.524}, 5.7s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 50] train={'g_nll': 0.156, 'g_kl': 0.08, 'g_elbo': 0.236, 'x_rna_nll': 0.39, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.398, 'x_atac_nll': 0.655, 'x_atac_kl': 0.394, 'x_atac_elbo': 1.049, 'dsc_loss': 0.65, 'vae_loss': 1.51, 'gen_loss': 1.478, 'joint_cross_loss': 0.984, 'real_cross_loss': 1.202, 'cos_loss': 0.522}, val={'g_nll': 0.163, 'g_kl': 0.08, 'g_elbo': 0.242, 'x_rna_nll': 0.383, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.391, 'x_atac_nll': 0.614, 'x_atac_kl': 0.394, 'x_atac_elbo': 1.008, 'dsc_loss': 0.66, 'vae_loss': 1.461, 'gen_loss': 1.428, 'joint_cross_loss': 0.955, 'real_cross_loss': 1.176, 'cos_loss': 0.523}, 5.4s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 60] train={'g_nll': 0.128, 'g_kl': 0.08, 'g_elbo': 0.208, 'x_rna_nll': 0.39, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.397, 'x_atac_nll': 0.641, 'x_atac_kl': 0.4, 'x_atac_elbo': 1.041, 'dsc_loss': 0.667, 'vae_loss': 1.499, 'gen_loss': 1.466, 'joint_cross_loss': 0.965, 'real_cross_loss': 1.166, 'cos_loss': 0.523}, val={'g_nll': 0.123, 'g_kl': 0.08, 'g_elbo': 0.203, 'x_rna_nll': 0.386, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.394, 'x_atac_nll': 0.595, 'x_atac_kl': 0.402, 'x_atac_elbo': 0.997, 'dsc_loss': 0.675, 'vae_loss': 1.451, 'gen_loss': 1.417, 'joint_cross_loss': 0.936, 'real_cross_loss': 1.157, 'cos_loss': 0.525}, 5.5s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 70] train={'g_nll': 0.115, 'g_kl': 0.08, 'g_elbo': 0.195, 'x_rna_nll': 0.389, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.397, 'x_atac_nll': 0.635, 'x_atac_kl': 0.403, 'x_atac_elbo': 1.039, 'dsc_loss': 0.674, 'vae_loss': 1.496, 'gen_loss': 1.462, 'joint_cross_loss': 0.958, 'real_cross_loss': 1.155, 'cos_loss': 0.522}, val={'g_nll': 0.116, 'g_kl': 0.08, 'g_elbo': 0.196, 'x_rna_nll': 0.381, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.389, 'x_atac_nll': 0.595, 'x_atac_kl': 0.403, 'x_atac_elbo': 0.998, 'dsc_loss': 0.669, 'vae_loss': 1.447, 'gen_loss': 1.413, 'joint_cross_loss': 0.926, 'real_cross_loss': 1.133, 'cos_loss': 0.52}, 4.1s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 80] train={'g_nll': 0.104, 'g_kl': 0.08, 'g_elbo': 0.184, 'x_rna_nll': 0.389, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.396, 'x_atac_nll': 0.627, 'x_atac_kl': 0.411, 'x_atac_elbo': 1.039, 'dsc_loss': 0.677, 'vae_loss': 1.495, 'gen_loss': 1.461, 'joint_cross_loss': 0.949, 'real_cross_loss': 1.152, 'cos_loss': 0.521}, val={'g_nll': 0.117, 'g_kl': 0.08, 'g_elbo': 0.196, 'x_rna_nll': 0.383, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.391, 'x_atac_nll': 0.593, 'x_atac_kl': 0.407, 'x_atac_elbo': 1.0, 'dsc_loss': 0.673, 'vae_loss': 1.45, 'gen_loss': 1.416, 'joint_cross_loss': 0.921, 'real_cross_loss': 1.129, 'cos_loss': 0.524}, 4.6s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 90] train={'g_nll': 0.096, 'g_kl': 0.08, 'g_elbo': 0.176, 'x_rna_nll': 0.389, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.397, 'x_atac_nll': 0.624, 'x_atac_kl': 0.412, 'x_atac_elbo': 1.035, 'dsc_loss': 0.674, 'vae_loss': 1.491, 'gen_loss': 1.458, 'joint_cross_loss': 0.947, 'real_cross_loss': 1.154, 'cos_loss': 0.521}, val={'g_nll': 0.094, 'g_kl': 0.08, 'g_elbo': 0.174, 'x_rna_nll': 0.382, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.39, 'x_atac_nll': 0.587, 'x_atac_kl': 0.406, 'x_atac_elbo': 0.993, 'dsc_loss': 0.681, 'vae_loss': 1.441, 'gen_loss': 1.407, 'joint_cross_loss': 0.921, 'real_cross_loss': 1.133, 'cos_loss': 0.523}, 5.5s elapsed\n", + "Epoch 00092: reducing learning rate of group 0 to 2.0000e-04.\n", + "Epoch 00092: reducing learning rate of group 0 to 2.0000e-04.\n", + "[INFO] LRScheduler: Learning rate reduction: step 1\n", + "[INFO] PairedSCGLUETrainer: [Epoch 100] train={'g_nll': 0.097, 'g_kl': 0.08, 'g_elbo': 0.177, 'x_rna_nll': 0.388, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.605, 'x_atac_kl': 0.415, 'x_atac_elbo': 1.02, 'dsc_loss': 0.674, 'vae_loss': 1.474, 'gen_loss': 1.441, 'joint_cross_loss': 0.932, 'real_cross_loss': 1.141, 'cos_loss': 0.52}, val={'g_nll': 0.1, 'g_kl': 0.08, 'g_elbo': 0.18, 'x_rna_nll': 0.382, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.39, 'x_atac_nll': 0.57, 'x_atac_kl': 0.413, 'x_atac_elbo': 0.982, 'dsc_loss': 0.674, 'vae_loss': 1.431, 'gen_loss': 1.397, 'joint_cross_loss': 0.906, 'real_cross_loss': 1.133, 'cos_loss': 0.521}, 5.5s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 110] train={'g_nll': 0.092, 'g_kl': 0.08, 'g_elbo': 0.173, 'x_rna_nll': 0.388, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.396, 'x_atac_nll': 0.603, 'x_atac_kl': 0.421, 'x_atac_elbo': 1.023, 'dsc_loss': 0.676, 'vae_loss': 1.478, 'gen_loss': 1.444, 'joint_cross_loss': 0.93, 'real_cross_loss': 1.141, 'cos_loss': 0.521}, val={'g_nll': 0.09, 'g_kl': 0.08, 'g_elbo': 0.171, 'x_rna_nll': 0.383, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.391, 'x_atac_nll': 0.568, 'x_atac_kl': 0.421, 'x_atac_elbo': 0.989, 'dsc_loss': 0.675, 'vae_loss': 1.437, 'gen_loss': 1.404, 'joint_cross_loss': 0.901, 'real_cross_loss': 1.13, 'cos_loss': 0.523}, 5.6s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 120] train={'g_nll': 0.093, 'g_kl': 0.08, 'g_elbo': 0.173, 'x_rna_nll': 0.387, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.596, 'x_atac_kl': 0.426, 'x_atac_elbo': 1.022, 'dsc_loss': 0.676, 'vae_loss': 1.476, 'gen_loss': 1.442, 'joint_cross_loss': 0.922, 'real_cross_loss': 1.139, 'cos_loss': 0.522}, val={'g_nll': 0.091, 'g_kl': 0.08, 'g_elbo': 0.171, 'x_rna_nll': 0.386, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.394, 'x_atac_nll': 0.558, 'x_atac_kl': 0.421, 'x_atac_elbo': 0.979, 'dsc_loss': 0.674, 'vae_loss': 1.431, 'gen_loss': 1.397, 'joint_cross_loss': 0.903, 'real_cross_loss': 1.138, 'cos_loss': 0.525}, 5.5s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 130] train={'g_nll': 0.094, 'g_kl': 0.08, 'g_elbo': 0.175, 'x_rna_nll': 0.388, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.592, 'x_atac_kl': 0.429, 'x_atac_elbo': 1.021, 'dsc_loss': 0.674, 'vae_loss': 1.475, 'gen_loss': 1.441, 'joint_cross_loss': 0.92, 'real_cross_loss': 1.139, 'cos_loss': 0.522}, val={'g_nll': 0.098, 'g_kl': 0.08, 'g_elbo': 0.178, 'x_rna_nll': 0.384, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.392, 'x_atac_nll': 0.553, 'x_atac_kl': 0.425, 'x_atac_elbo': 0.978, 'dsc_loss': 0.675, 'vae_loss': 1.428, 'gen_loss': 1.394, 'joint_cross_loss': 0.897, 'real_cross_loss': 1.126, 'cos_loss': 0.524}, 5.5s elapsed\n", + "Epoch 00130: reducing learning rate of group 0 to 2.0000e-05.\n", + "Epoch 00130: reducing learning rate of group 0 to 2.0000e-05.\n", + "[INFO] LRScheduler: Learning rate reduction: step 2\n", + "[INFO] PairedSCGLUETrainer: [Epoch 140] train={'g_nll': 0.09, 'g_kl': 0.08, 'g_elbo': 0.171, 'x_rna_nll': 0.388, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.589, 'x_atac_kl': 0.429, 'x_atac_elbo': 1.018, 'dsc_loss': 0.675, 'vae_loss': 1.472, 'gen_loss': 1.438, 'joint_cross_loss': 0.916, 'real_cross_loss': 1.137, 'cos_loss': 0.521}, val={'g_nll': 0.094, 'g_kl': 0.08, 'g_elbo': 0.174, 'x_rna_nll': 0.384, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.392, 'x_atac_nll': 0.539, 'x_atac_kl': 0.427, 'x_atac_elbo': 0.966, 'dsc_loss': 0.669, 'vae_loss': 1.416, 'gen_loss': 1.382, 'joint_cross_loss': 0.888, 'real_cross_loss': 1.123, 'cos_loss': 0.523}, 5.6s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 150] train={'g_nll': 0.091, 'g_kl': 0.08, 'g_elbo': 0.171, 'x_rna_nll': 0.387, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.592, 'x_atac_kl': 0.429, 'x_atac_elbo': 1.021, 'dsc_loss': 0.673, 'vae_loss': 1.474, 'gen_loss': 1.44, 'joint_cross_loss': 0.919, 'real_cross_loss': 1.136, 'cos_loss': 0.521}, val={'g_nll': 0.091, 'g_kl': 0.08, 'g_elbo': 0.172, 'x_rna_nll': 0.385, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.392, 'x_atac_nll': 0.552, 'x_atac_kl': 0.423, 'x_atac_elbo': 0.975, 'dsc_loss': 0.673, 'vae_loss': 1.426, 'gen_loss': 1.392, 'joint_cross_loss': 0.898, 'real_cross_loss': 1.136, 'cos_loss': 0.521}, 5.7s elapsed\n", + "Epoch 00155: reducing learning rate of group 0 to 2.0000e-06.\n", + "Epoch 00155: reducing learning rate of group 0 to 2.0000e-06.\n", + "[INFO] LRScheduler: Learning rate reduction: step 3\n", + "[INFO] PairedSCGLUETrainer: [Epoch 160] train={'g_nll': 0.088, 'g_kl': 0.08, 'g_elbo': 0.169, 'x_rna_nll': 0.388, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.593, 'x_atac_kl': 0.43, 'x_atac_elbo': 1.023, 'dsc_loss': 0.674, 'vae_loss': 1.477, 'gen_loss': 1.443, 'joint_cross_loss': 0.92, 'real_cross_loss': 1.138, 'cos_loss': 0.52}, val={'g_nll': 0.092, 'g_kl': 0.08, 'g_elbo': 0.172, 'x_rna_nll': 0.382, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.389, 'x_atac_nll': 0.545, 'x_atac_kl': 0.431, 'x_atac_elbo': 0.976, 'dsc_loss': 0.673, 'vae_loss': 1.423, 'gen_loss': 1.389, 'joint_cross_loss': 0.887, 'real_cross_loss': 1.125, 'cos_loss': 0.526}, 5.5s elapsed\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-02-04 11:58:21,297 ignite.handlers.early_stopping.EarlyStopping INFO: EarlyStopping: Stop training\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[INFO] EarlyStopping: Restoring checkpoint \"163\"...\n", + "[INFO] fit_SCGLUE: Estimating balancing weight...\n", + "[INFO] estimate_balancing_weight: Clustering cells...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-02-04 11:58:33.201140: I external/local_tsl/tsl/cuda/cudart_stub.cc:31] Could not find cuda drivers on your machine, GPU will not be used.\n", + "2024-02-04 11:58:33.293282: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:9261] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered\n", + "2024-02-04 11:58:33.293330: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:607] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered\n", + "2024-02-04 11:58:33.294468: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1515] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered\n", + "2024-02-04 11:58:33.313569: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.\n", + "To enable the following instructions: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.\n", + "2024-02-04 11:58:41.156705: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[INFO] estimate_balancing_weight: Matching clusters...\n", + "[INFO] estimate_balancing_weight: Matching array shape = (24, 23)...\n", + "[INFO] estimate_balancing_weight: Estimating balancing weight...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/gpfs/gibbs/project/zhao/tl688/conda_envs/scglue/lib/python3.9/site-packages/anndata/_core/anndata.py:183: ImplicitModificationWarning: Transforming to str index.\n", + " warnings.warn(\"Transforming to str index.\", ImplicitModificationWarning)\n", + "/gpfs/gibbs/project/zhao/tl688/conda_envs/scglue/lib/python3.9/site-packages/anndata/_core/anndata.py:183: ImplicitModificationWarning: Transforming to str index.\n", + " warnings.warn(\"Transforming to str index.\", ImplicitModificationWarning)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[INFO] fit_SCGLUE: Fine-tuning SCGLUE model...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[WARNING] PairedSCGLUEModel: It is recommended that `use_rep` dimensionality be equal or larger than `latent_dim`.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[INFO] check_graph: Checking variable coverage...\n", + "[INFO] check_graph: Checking edge attributes...\n", + "[INFO] check_graph: Checking self-loops...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/gpfs/gibbs/project/zhao/tl688/conda_envs/scglue/lib/python3.9/abc.py:119: FutureWarning: SparseDataset is deprecated and will be removed in late 2024. It has been replaced by the public classes CSRDataset and CSCDataset.\n", + "\n", + "For instance checks, use `isinstance(X, (anndata.experimental.CSRDataset, anndata.experimental.CSCDataset))` instead.\n", + "\n", + "For creation, use `anndata.experimental.sparse_dataset(X)` instead.\n", + "\n", + " return _abc_instancecheck(cls, instance)\n", + "[WARNING] check_graph: Missing self-loop!\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[INFO] check_graph: Checking graph symmetry...\n", + "[INFO] PairedSCGLUEModel: Setting `graph_batch_size` = 701\n", + "[INFO] PairedSCGLUEModel: Setting `align_burnin` = 53\n", + "[INFO] PairedSCGLUEModel: Setting `max_epochs` = 315\n", + "[INFO] PairedSCGLUEModel: Setting `patience` = 27\n", + "[INFO] PairedSCGLUEModel: Setting `reduce_lr_patience` = 14\n", + "[INFO] PairedSCGLUETrainer: Using training directory: \"glue_prot/fine-tune\"\n", + "[INFO] PairedSCGLUETrainer: [Epoch 10] train={'g_nll': 0.091, 'g_kl': 0.08, 'g_elbo': 0.171, 'x_rna_nll': 0.387, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.616, 'x_atac_kl': 0.416, 'x_atac_elbo': 1.032, 'dsc_loss': 0.682, 'vae_loss': 1.485, 'gen_loss': 1.451, 'joint_cross_loss': 0.938, 'real_cross_loss': 1.142, 'cos_loss': 0.521}, val={'g_nll': 0.095, 'g_kl': 0.08, 'g_elbo': 0.175, 'x_rna_nll': 0.4, 'x_rna_kl': 0.008, 'x_rna_elbo': 0.408, 'x_atac_nll': 0.599, 'x_atac_kl': 0.405, 'x_atac_elbo': 1.005, 'dsc_loss': 0.676, 'vae_loss': 1.471, 'gen_loss': 1.438, 'joint_cross_loss': 0.941, 'real_cross_loss': 1.141, 'cos_loss': 0.523}, 2.8s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 20] train={'g_nll': 0.092, 'g_kl': 0.08, 'g_elbo': 0.172, 'x_rna_nll': 0.387, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.613, 'x_atac_kl': 0.419, 'x_atac_elbo': 1.032, 'dsc_loss': 0.678, 'vae_loss': 1.485, 'gen_loss': 1.451, 'joint_cross_loss': 0.936, 'real_cross_loss': 1.141, 'cos_loss': 0.522}, val={'g_nll': 0.099, 'g_kl': 0.08, 'g_elbo': 0.178, 'x_rna_nll': 0.4, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.408, 'x_atac_nll': 0.584, 'x_atac_kl': 0.412, 'x_atac_elbo': 0.996, 'dsc_loss': 0.669, 'vae_loss': 1.463, 'gen_loss': 1.43, 'joint_cross_loss': 0.941, 'real_cross_loss': 1.143, 'cos_loss': 0.528}, 2.8s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 30] train={'g_nll': 0.097, 'g_kl': 0.079, 'g_elbo': 0.176, 'x_rna_nll': 0.387, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.612, 'x_atac_kl': 0.421, 'x_atac_elbo': 1.033, 'dsc_loss': 0.673, 'vae_loss': 1.487, 'gen_loss': 1.453, 'joint_cross_loss': 0.939, 'real_cross_loss': 1.145, 'cos_loss': 0.523}, val={'g_nll': 0.101, 'g_kl': 0.079, 'g_elbo': 0.18, 'x_rna_nll': 0.399, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.406, 'x_atac_nll': 0.604, 'x_atac_kl': 0.407, 'x_atac_elbo': 1.011, 'dsc_loss': 0.679, 'vae_loss': 1.477, 'gen_loss': 1.443, 'joint_cross_loss': 0.949, 'real_cross_loss': 1.146, 'cos_loss': 0.531}, 2.8s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 40] train={'g_nll': 0.101, 'g_kl': 0.079, 'g_elbo': 0.18, 'x_rna_nll': 0.388, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.606, 'x_atac_kl': 0.424, 'x_atac_elbo': 1.03, 'dsc_loss': 0.665, 'vae_loss': 1.484, 'gen_loss': 1.451, 'joint_cross_loss': 0.934, 'real_cross_loss': 1.138, 'cos_loss': 0.522}, val={'g_nll': 0.107, 'g_kl': 0.079, 'g_elbo': 0.187, 'x_rna_nll': 0.401, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.408, 'x_atac_nll': 0.577, 'x_atac_kl': 0.416, 'x_atac_elbo': 0.993, 'dsc_loss': 0.66, 'vae_loss': 1.461, 'gen_loss': 1.428, 'joint_cross_loss': 0.937, 'real_cross_loss': 1.148, 'cos_loss': 0.523}, 2.8s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 50] train={'g_nll': 0.099, 'g_kl': 0.079, 'g_elbo': 0.178, 'x_rna_nll': 0.389, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.396, 'x_atac_nll': 0.609, 'x_atac_kl': 0.424, 'x_atac_elbo': 1.033, 'dsc_loss': 0.652, 'vae_loss': 1.489, 'gen_loss': 1.456, 'joint_cross_loss': 0.941, 'real_cross_loss': 1.149, 'cos_loss': 0.525}, val={'g_nll': 0.105, 'g_kl': 0.079, 'g_elbo': 0.184, 'x_rna_nll': 0.402, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.409, 'x_atac_nll': 0.583, 'x_atac_kl': 0.416, 'x_atac_elbo': 0.999, 'dsc_loss': 0.656, 'vae_loss': 1.468, 'gen_loss': 1.435, 'joint_cross_loss': 0.944, 'real_cross_loss': 1.165, 'cos_loss': 0.522}, 2.8s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 60] train={'g_nll': 0.098, 'g_kl': 0.079, 'g_elbo': 0.178, 'x_rna_nll': 0.389, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.397, 'x_atac_nll': 0.604, 'x_atac_kl': 0.426, 'x_atac_elbo': 1.03, 'dsc_loss': 0.655, 'vae_loss': 1.486, 'gen_loss': 1.454, 'joint_cross_loss': 0.94, 'real_cross_loss': 1.152, 'cos_loss': 0.524}, val={'g_nll': 0.097, 'g_kl': 0.079, 'g_elbo': 0.176, 'x_rna_nll': 0.401, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.408, 'x_atac_nll': 0.58, 'x_atac_kl': 0.423, 'x_atac_elbo': 1.003, 'dsc_loss': 0.655, 'vae_loss': 1.471, 'gen_loss': 1.438, 'joint_cross_loss': 0.936, 'real_cross_loss': 1.16, 'cos_loss': 0.523}, 2.7s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 70] train={'g_nll': 0.094, 'g_kl': 0.079, 'g_elbo': 0.173, 'x_rna_nll': 0.389, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.397, 'x_atac_nll': 0.604, 'x_atac_kl': 0.426, 'x_atac_elbo': 1.03, 'dsc_loss': 0.653, 'vae_loss': 1.486, 'gen_loss': 1.453, 'joint_cross_loss': 0.938, 'real_cross_loss': 1.144, 'cos_loss': 0.525}, val={'g_nll': 0.104, 'g_kl': 0.079, 'g_elbo': 0.183, 'x_rna_nll': 0.401, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.409, 'x_atac_nll': 0.58, 'x_atac_kl': 0.415, 'x_atac_elbo': 0.995, 'dsc_loss': 0.648, 'vae_loss': 1.463, 'gen_loss': 1.431, 'joint_cross_loss': 0.938, 'real_cross_loss': 1.158, 'cos_loss': 0.533}, 2.7s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 80] train={'g_nll': 0.095, 'g_kl': 0.079, 'g_elbo': 0.174, 'x_rna_nll': 0.389, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.397, 'x_atac_nll': 0.607, 'x_atac_kl': 0.425, 'x_atac_elbo': 1.032, 'dsc_loss': 0.649, 'vae_loss': 1.488, 'gen_loss': 1.456, 'joint_cross_loss': 0.941, 'real_cross_loss': 1.146, 'cos_loss': 0.524}, val={'g_nll': 0.1, 'g_kl': 0.079, 'g_elbo': 0.18, 'x_rna_nll': 0.404, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.411, 'x_atac_nll': 0.586, 'x_atac_kl': 0.414, 'x_atac_elbo': 1.0, 'dsc_loss': 0.649, 'vae_loss': 1.471, 'gen_loss': 1.439, 'joint_cross_loss': 0.947, 'real_cross_loss': 1.166, 'cos_loss': 0.523}, 2.7s elapsed\n", + "Epoch 00084: reducing learning rate of group 0 to 2.0000e-04.\n", + "Epoch 00084: reducing learning rate of group 0 to 2.0000e-04.\n", + "[INFO] LRScheduler: Learning rate reduction: step 1\n", + "[INFO] PairedSCGLUETrainer: [Epoch 90] train={'g_nll': 0.09, 'g_kl': 0.079, 'g_elbo': 0.169, 'x_rna_nll': 0.388, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.396, 'x_atac_nll': 0.591, 'x_atac_kl': 0.429, 'x_atac_elbo': 1.02, 'dsc_loss': 0.665, 'vae_loss': 1.474, 'gen_loss': 1.441, 'joint_cross_loss': 0.927, 'real_cross_loss': 1.14, 'cos_loss': 0.524}, val={'g_nll': 0.09, 'g_kl': 0.079, 'g_elbo': 0.169, 'x_rna_nll': 0.398, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.406, 'x_atac_nll': 0.593, 'x_atac_kl': 0.42, 'x_atac_elbo': 1.013, 'dsc_loss': 0.644, 'vae_loss': 1.478, 'gen_loss': 1.446, 'joint_cross_loss': 0.951, 'real_cross_loss': 1.169, 'cos_loss': 0.529}, 2.7s elapsed\n", + "[INFO] PairedSCGLUETrainer: [Epoch 100] train={'g_nll': 0.092, 'g_kl': 0.079, 'g_elbo': 0.171, 'x_rna_nll': 0.388, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.581, 'x_atac_kl': 0.433, 'x_atac_elbo': 1.014, 'dsc_loss': 0.663, 'vae_loss': 1.468, 'gen_loss': 1.435, 'joint_cross_loss': 0.919, 'real_cross_loss': 1.136, 'cos_loss': 0.523}, val={'g_nll': 0.09, 'g_kl': 0.079, 'g_elbo': 0.169, 'x_rna_nll': 0.402, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.409, 'x_atac_nll': 0.57, 'x_atac_kl': 0.427, 'x_atac_elbo': 0.997, 'dsc_loss': 0.649, 'vae_loss': 1.466, 'gen_loss': 1.433, 'joint_cross_loss': 0.94, 'real_cross_loss': 1.181, 'cos_loss': 0.527}, 2.7s elapsed\n", + "Epoch 00101: reducing learning rate of group 0 to 2.0000e-05.\n", + "Epoch 00101: reducing learning rate of group 0 to 2.0000e-05.\n", + "[INFO] LRScheduler: Learning rate reduction: step 2\n", + "[INFO] PairedSCGLUETrainer: [Epoch 110] train={'g_nll': 0.089, 'g_kl': 0.079, 'g_elbo': 0.168, 'x_rna_nll': 0.388, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.395, 'x_atac_nll': 0.586, 'x_atac_kl': 0.434, 'x_atac_elbo': 1.02, 'dsc_loss': 0.663, 'vae_loss': 1.473, 'gen_loss': 1.44, 'joint_cross_loss': 0.923, 'real_cross_loss': 1.141, 'cos_loss': 0.526}, val={'g_nll': 0.091, 'g_kl': 0.079, 'g_elbo': 0.17, 'x_rna_nll': 0.403, 'x_rna_kl': 0.007, 'x_rna_elbo': 0.41, 'x_atac_nll': 0.566, 'x_atac_kl': 0.427, 'x_atac_elbo': 0.993, 'dsc_loss': 0.651, 'vae_loss': 1.462, 'gen_loss': 1.43, 'joint_cross_loss': 0.931, 'real_cross_loss': 1.166, 'cos_loss': 0.527}, 2.7s elapsed\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-02-04 12:05:20,260 ignite.handlers.early_stopping.EarlyStopping INFO: EarlyStopping: Stop training\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[INFO] EarlyStopping: Restoring checkpoint \"104\"...\n" + ] + } + ], + "source": [ + "glue = scglue.models.fit_SCGLUE(\n", + " {\"rna\": rna, \"atac\": prot}, guidance,\n", + " model=scglue.models.PairedSCGLUEModel,\n", + " fit_kws={\"directory\": \"glue_prot\"}\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "17d192b5-1e0e-48f1-8a06-4fc6b56dcfbf", + "metadata": {}, + "outputs": [], + "source": [ + "rna.obsm[\"X_glue\"] = glue.encode_data(\"rna\", rna)\n", + "prot.obsm[\"X_glue\"] = glue.encode_data(\"atac\", prot)" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "12ce2848-0a73-4511-8859-389fe21a6fa5", + "metadata": {}, + "outputs": [], + "source": [ + "rna.obsm['X_comb'] = np.concatenate([rna.obsm[\"X_glue\"], prot.obsm[\"X_glue\"]], axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "f794989a-bd71-4451-86e8-730fce4669d1", + "metadata": {}, + "outputs": [], + "source": [ + "sc.pp.neighbors(rna, use_rep=\"X_comb\", metric=\"cosine\")\n", + "sc.tl.umap(rna)" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "b29d2d42-1cbe-466c-8fc3-698c9ecda123", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/gpfs/gibbs/project/zhao/tl688/conda_envs/scglue/lib/python3.9/site-packages/scanpy/plotting/_tools/scatterplots.py:394: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored\n", + " cax = scatter(\n" + ] + }, + { + "data": { + "image/png": "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p2GNNElERPYeBQZy0BjcsRkndU9k6pKtHNc5gVN7tiQ6ysOwLgkYTc4kUtqIp+CL2yB7Bwx7ADocW9MtEhGRvUhViURERKTWUVUikeqnwcciIiIiIqLAQEREREREFBiIiBS3dBIseB8K8mq6JSIiIvuUBh+LiAB8+wj89CzkZbn1pV/C2WNrtEkiIiL7kgIDEZFty+D7x4tvWzO9ZtoiIiJSQ5RKJCKy+gegRMnanufUSFNERERqinoMROTg9uaZsHKqW/b4oNeFcMSFkHRkzbZLRERkH1NgICIHn9S1MPtFSNsYCgoAAvkw+A6IV1l0ERE5+CgwEJGDz9ujYPuyyK81aLVv2yIiIrKf0BgDETn4pK6JvL15dzAm8msiIiIHOAUGInLw6XJa6W1RdeCqb/d9W0RERPYTCgxE5ODTrFPxdW8MXDcDoqJrpj0iIiL7AQUGInLwOfwiaJLslvtcBfdthSYdarZNIiIiNUyDj0Xk4NOgBdw0B/JzwBdb060RERHZL6jHQEQOXgoKREREiigwEBERERERBQYiIiIiIqLAQEREREREUGAgIiIiIiIoMBARERERERQYiIiIiIgICgxERERERAQFBiIiIiIiggIDERERERFBgYGIiIiIiKDAQEREREREUGAgIiIiIiIoMBARERERERQYiIiIiIgICgxERERERAQFBiIiIiIiggIDEREREREBjLW2ptuwzxlj8jwej69FixY13RQRERGpgs2bNxMIBPKttdE13RaRA8XBGhhkAT5ga023RURERKokAci31tar6YaIHCgOysBARERERESK0xgDERERERFRYCAiIiIiIgoMREREREQEBQYiIiIiIoICAxERERERQYGBiIiIiIigwEBERERERFBgICIiIiIiKDAQEREREREUGIiIiIiICAoMREREREQEBQYiIiIiIoICAxERERERQYGBiIiIiIigwEBERERERFBgICIiIiIiKDAQkSBjzBBjjA1+ranksTbsq22J19qWeN0aY16txLmTjDH+EsePrWT73i9xfN9KHBup/eFfmcaYdcaYicaYvxhjGu/hfHWMMf2NMdcbY14xxvxmjMkLO98DlXlvIiIi1SWqphsgIgel0caYW6y1mRXY93L+xEMMY0wz4PQSm68Efq7qOUuoF/xqDZwM3G+Muc5a+14Z+28Ayg0eREREaoICAxHZlwpwv3figHOBV8rb2RhjgMtKHFtZlwDRJbada4y51VqbVYXznVFivT7QC7gYaAo0At4xxmRbaz+PcLy3xPoWIBdoU4W2iIiIVBulEonIvrQUWB5cvrIC+x8PtA0uR7rJrojC6xQAbwSX6+MCk0qz1k4o8fWmtfZ2oDPwa3A3D/CMMSZSIPMZcD9wKtDCWtsCGFuVtoiIiFQnBQYisq+9Fvy3nzGm2x72LbypTwU+ruyFjDEDgU7B1YnAY2EvX1XZ85XHWrsD1ztRqA3QP8J+F1trH7LWTrTWbqnONoiIiPwZCgxEZF8bi3t6D+X0GhhjmhIaG/A2kFOFa4Xf/L9urV0MzA6u9zPGdK/COctkrV0ErAjb1LM6zy8iIrI3KTAQkX0q+JR8YnD1QmNMyfz/QhcRGhtQ7liESIwxDYFRwdWtYdd8PWy3au01CLtWoYZ74fwiIiJ7hQIDEakJheVKmwIjy9jniuC/c6y186twjQuBOsHlt6y1hb0U7wK7C/cxxsRU4dzlSQhbTqvmc4uIiOw1CgxEpCZ8CWwKLpdKJzLGHAUUjj+odG9BhPMW9RJYa9OAT4KrjYGzqnj+UowxnYHksE0LquvcIiIie5sCAxHZ56y1fkKVeI4zxpQs1Vl4U58NvFPZ8xtj+gCHBVd/tdYuLLFLeDpRRaojVeSajSheXWg98FN1nFtERGRf0DwGIlJTXgPuwT2guBz4B4AxJg4YHdznQ2ttehXOXWzQcYTXpwHrgEOAIcaYZGvtigj7lWKMGVliUxwuCLmYUBqRBf4Slr4kIiKy31NgICI1wlq70hjzHXAscKkx5v+stQHgPNzNNlRt0HE9QnMU5ALjI1w7YIwZB9wHGFyvwd0VvMQne3g9A7jBWlvp8qoiIiI1SalEIlKTCgchHwKcEFwuHHS8zFr7YxXOeS5uAjOACdba1DL2G4t7sg9wSRmTkVXEbtx4ia+BO4Fka+2bVTyXiIhIjVGPgYgU8octV/h3gzHGV8559uQj4BmgEXCFMWYD0C/42qtlHlW+8DSisWXtZK1dZYz5ARgMJAIj2HNvANZaU8V2iYiI7NcUGIhIofDSmg0qcVz9Euu7KnqgtTbHGPM2cCNwGpAXfKkAGFeJNgBgjOlBKLAAmGRMhe/jr6QCgYGIiMiBSqlEIlJoc9hyfWNMswoed2jYcqa1NqOS1y3sGYgGzg8uf2GtTankeeDPVRgaboxJ+hPHi4iI1GrqMRARAKy124wxq4D2wU3HAu9X4NDBYcuzqnDdecaYOUDvsM1VGXQci5studATuHKnezIE9x4KqyM9WNlri4iIHAgUGIhIuE+A24PLNxtjPrDW2rJ2NsbUAa4pcXxV/Bu4JbicDkyuwjnOwo1VADdb8l0VOcgY05/QfAOXG2P+GayOJCIiclBRKpGIhHsKyAouDwD+E2FwMVBUFvRNQj0MG3BzE1SatXa8tbZ/8OuE4ARolVWhQccRrj0LWBJcbQMcX4Vri4iI1HrqMRCRItbajcaYy4F3cfX9bwJOM8Z8CCwEMnEDkw8HzgEKxyHkAGdba3P2favBGHMooZSmPCo/W/I44NHg8lXAV9XUtFKMMUOBoSU2DwpbHhqhdOpczYsgIiJ7mwIDESnGWvu+MSYdN2NwIu4p+u3lHLIMON9aO2dftK8M4YOOP7fW7qzk8W8CD+N6UU8zxiRYa7dWW+uKGwT8vZzXBwa/wo0DFBiIiMhepVQiESnFWjsZaAtcius9WIErZ1oApOJSb8YBo4CuNRkUBFOdLgnbNLay57DWbgS+Ca6WPJ+IiMhBwZQzrlBERERERA4S6jEQEREREREFBiIiIiIiosBARERERERQYCAiIiIiIigwEBERERERFBiIiIiIiAgKDEREREREBAUGIiIiIiKCAgMREREREUGBgYiIiIiIoMBARERERERQYCAiIiIiIigwEBERERERFBiIiIiIiAgKDEREREREBAUGIiIiIiKCAgMREREREUGBgYiIiIiIoMBARERERERQYCAiIiIiIigwEBERERER4P8BfkKUF5FW1AcAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": { + "image/png": { + "height": 300, + "width": 387 + } + }, + "output_type": "display_data" + } + ], + "source": [ + "sc.pl.umap(rna, color=[\"batch\"], wspace=0.65)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "d432ae7c-a84f-4d99-8594-b0bc87fa480a", + "metadata": {}, + "outputs": [], + "source": [ + "# sc.pl.umap(rna, color=[\"celltype.l2\", \"orig.ident\"], wspace=0.65)" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "ef997d6e-067a-4049-954f-f87b2d7cbf31", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
n_genespercent_miton_countsbatchbalancing_weight
index
AAACCCAAGATTGTGA-121940.0849036160.0PBMC10k1.141823
AAACCCACATCGGTTA-120930.0618206713.0PBMC10k1.572584
AAACCCAGTACCGCGT-115180.0789113637.0PBMC10k1.581676
AAACCCAGTATCGAAA-17370.0884241244.0PBMC10k0.871529
AAACCCAGTCGTCATA-112400.0597472611.0PBMC10k0.871529
..................
TTTGGTTGTACGAGTG-114500.0648185662.0PBMC5k0.671456
TTTGTTGAGTTAACAG-130650.08774210189.0PBMC5k0.976391
TTTGTTGCAGCACAAG-116410.0985234740.0PBMC5k0.761896
TTTGTTGCAGTCTTCC-119000.0868546367.0PBMC5k0.510280
TTTGTTGCATTGCCGG-134420.10534912207.0PBMC5k1.581676
\n", + "

10849 rows × 5 columns

\n", + "
" + ], + "text/plain": [ + " n_genes percent_mito n_counts batch balancing_weight\n", + "index \n", + "AAACCCAAGATTGTGA-1 2194 0.084903 6160.0 PBMC10k 1.141823\n", + "AAACCCACATCGGTTA-1 2093 0.061820 6713.0 PBMC10k 1.572584\n", + "AAACCCAGTACCGCGT-1 1518 0.078911 3637.0 PBMC10k 1.581676\n", + "AAACCCAGTATCGAAA-1 737 0.088424 1244.0 PBMC10k 0.871529\n", + "AAACCCAGTCGTCATA-1 1240 0.059747 2611.0 PBMC10k 0.871529\n", + "... ... ... ... ... ...\n", + "TTTGGTTGTACGAGTG-1 1450 0.064818 5662.0 PBMC5k 0.671456\n", + "TTTGTTGAGTTAACAG-1 3065 0.087742 10189.0 PBMC5k 0.976391\n", + "TTTGTTGCAGCACAAG-1 1641 0.098523 4740.0 PBMC5k 0.761896\n", + "TTTGTTGCAGTCTTCC-1 1900 0.086854 6367.0 PBMC5k 0.510280\n", + "TTTGTTGCATTGCCGG-1 3442 0.105349 12207.0 PBMC5k 1.581676\n", + "\n", + "[10849 rows x 5 columns]" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rna.obs" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "e60f1a0b-a2a8-48ec-a484-ed2609fcb7cd", + "metadata": {}, + "outputs": [], + "source": [ + "rna.write_h5ad(\"glue_batchonly_prot_normal.h5ad\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "635bc4d6-055f-4580-90f9-3a6eecb8ca42", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "206f8fdb-3436-47b0-acad-8ae657a21adb", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.9.18" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scglue/models/sc.py b/scglue/models/sc.py index 516a0ef..e07ee5a 100644 --- a/scglue/models/sc.py +++ b/scglue/models/sc.py @@ -402,6 +402,48 @@ def forward( logits=(mu + EPS).log() - log_theta ) +class NBMixtureDataDecoder(DataDecoder): + + r""" + Negative binomial data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias1 = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias2 = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.log_theta = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: torch.Tensor # l is sequencing depth + ) -> D.NegativeBinomial: + scale = F.softplus(self.scale_lin[b]) + logit_mu1 = scale * (u @ v.t()) + self.bias1[b] + logit_mu2 = scale * (u @ v.t()) + self.bias2[b] + + mu1 = F.softmax(logit_mu1, dim=1) + mu2 = F.softmax(logit_mu2, dim=1) + + # beta = self.zi_logits[b].expand_as(mu1) # to avoid negative value in the bernoulli distribution, we use l later. + v_s = torch.distributions.Bernoulli(mu1).sample() + # mu_mixture = v_s* l + (1-v_s)*mu2* l + mu_mixture = v_s + (1-v_s)*mu2* l + # print(mu_mixture) + log_theta = self.log_theta[b] + return D.NegativeBinomial( + log_theta.exp(), + logits=(mu_mixture + EPS).log() - log_theta + ) class ZINBDataDecoder(NBDataDecoder): diff --git a/scglue/models/scglue.py b/scglue/models/scglue.py index 9ab8c3a..454be3b 100644 --- a/scglue/models/scglue.py +++ b/scglue/models/scglue.py @@ -56,7 +56,7 @@ def register_prob_model(prob_model: str, encoder: type, decoder: type) -> None: register_prob_model("ZILN", sc.VanillaDataEncoder, sc.ZILNDataDecoder) register_prob_model("NB", sc.NBDataEncoder, sc.NBDataDecoder) register_prob_model("ZINB", sc.NBDataEncoder, sc.ZINBDataDecoder) - +register_prob_model("NBMixture", sc.NBDataEncoder, sc.NBMixtureDataDecoder) #----------------------------- Network definition ------------------------------ diff --git a/scglue/utils.py b/scglue/utils.py index 4c913b8..7166ad1 100644 --- a/scglue/utils.py +++ b/scglue/utils.py @@ -10,10 +10,14 @@ from collections import defaultdict from multiprocessing import Process from typing import Any, List, Mapping, Optional +from warnings import warn +from scipy.sparse import issparse, csc_matrix, csr_matrix +from anndata import AnnData import numpy as np import pandas as pd import torch +import networkx as nx from pybedtools.helpers import set_bedtools_path from .typehint import RandomState, T @@ -671,3 +675,62 @@ def _handle(line): f"{executable} exited with error code: {ret}.{err_message}") if stdout == subprocess.PIPE and not print_output: return output_lines + + +# Function for DIY gudiance graph +def generate_prot_gudiance_graph(rna, + prot, + protein_gene_match): + guidance =nx.MultiDiGraph() + for k, v in protein_gene_match.items(): + guidance.add_edge(k, v, weight=1.0, sign=1) + guidance.add_edge(v, k, weight=1.0, sign=1) + for item in rna.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1) + for item in prot.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1) + + return guidance + + + +def clr(adata:AnnData, inplace= True, axis= 0): + """ + Apply the centered log ratio (CLR) transformation + to normalize counts in adata.X. + + Args: + data: AnnData object with protein expression counts. + inplace: Whether to update adata.X inplace. + axis: Axis across which CLR is performed. + """ + + if axis not in [0, 1]: + raise ValueError("Invalid value for `axis` provided. Admissible options are `0` and `1`.") + + if not inplace: + adata = adata.copy() + + if issparse(adata.X) and axis == 0 and not isinstance(adata.X, csc_matrix): + warn("adata.X is sparse but not in CSC format. Converting to CSC.") + x = csc_matrix(adata.X) + elif issparse(adata.X) and axis == 1 and not isinstance(adata.X, csr_matrix): + warn("adata.X is sparse but not in CSR format. Converting to CSR.") + x = csr_matrix(adata.X) + else: + x = adata.X + + if issparse(x): + x.data /= np.repeat( + np.exp(np.log1p(x).sum(axis=axis).A / x.shape[axis]), x.getnnz(axis=axis) + ) + np.log1p(x.data, out=x.data) + else: + np.log1p( + x / np.exp(np.log1p(x).sum(axis=axis, keepdims=True) / x.shape[axis]), + out=x, + ) + + adata.X = x + + return None if inplace else adata \ No newline at end of file From 55ad68d4cbfe03900c3e6eb79b5bb789d3cbe1b7 Mon Sep 17 00:00:00 2001 From: Helloworldlty Date: Mon, 5 Feb 2024 00:14:11 -0500 Subject: [PATCH 2/5] update p --- scglue/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scglue/utils.py b/scglue/utils.py index 7166ad1..c04fb60 100644 --- a/scglue/utils.py +++ b/scglue/utils.py @@ -683,12 +683,14 @@ def generate_prot_gudiance_graph(rna, protein_gene_match): guidance =nx.MultiDiGraph() for k, v in protein_gene_match.items(): - guidance.add_edge(k, v, weight=1.0, sign=1) - guidance.add_edge(v, k, weight=1.0, sign=1) + guidance.add_edge(k, v, weight=1.0, sign=1, type="rev") + guidance.add_edge(v, k, weight=1.0, sign=1, type="fwd") + for item in rna.var_names: - guidance.add_edge(item, item, weight=1.0, sign=1) + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") for item in prot.var_names: - guidance.add_edge(item, item, weight=1.0, sign=1) + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") + return guidance From 927d77bae0078afd6d68869d7aa3583ecde0e8e6 Mon Sep 17 00:00:00 2001 From: Helloworldlty Date: Mon, 5 Feb 2024 00:41:26 -0500 Subject: [PATCH 3/5] update computation of nbmixture --- scglue/models/sc.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/scglue/models/sc.py b/scglue/models/sc.py index e07ee5a..c3b8ec9 100644 --- a/scglue/models/sc.py +++ b/scglue/models/sc.py @@ -32,17 +32,21 @@ class GraphEncoder(glue.GraphEncoder): """ def __init__( - self, vnum: int, out_features: int + self, vnum: int, out_features: int, init_fea_emb ) -> None: super().__init__() - self.vrepr = torch.nn.Parameter(torch.zeros(vnum, out_features)) self.conv = GraphConv() self.loc = torch.nn.Linear(out_features, out_features) self.std_lin = torch.nn.Linear(out_features, out_features) + if init_fea_emb is None: + self.vrepr = torch.nn.Parameter(torch.zeros(vnum, out_features)) + else: + self.vrepr = torch.nn.Parameter(torch.FloatTensor(init_fea_emb)) def forward( - self, eidx: torch.Tensor, enorm: torch.Tensor, esgn: torch.Tensor + self, eidx: torch.Tensor, enorm: torch.Tensor, esgn: torch.Tensor, init_fea_emb = None ) -> D.Normal: + # self.vrepr = self.vrepr.to(eidx.device) ptr = self.conv(self.vrepr, eidx, enorm, esgn) loc = self.loc(ptr) std = F.softplus(self.std_lin(ptr)) + EPS @@ -401,6 +405,7 @@ def forward( log_theta.exp(), logits=(mu + EPS).log() - log_theta ) + class NBMixtureDataDecoder(DataDecoder): @@ -436,8 +441,8 @@ def forward( # beta = self.zi_logits[b].expand_as(mu1) # to avoid negative value in the bernoulli distribution, we use l later. v_s = torch.distributions.Bernoulli(mu1).sample() - # mu_mixture = v_s* l + (1-v_s)*mu2* l - mu_mixture = v_s + (1-v_s)*mu2* l + mu_mixture = v_s* l + (1-v_s)*mu2* l # keep the same format with TOTALVI + # mu_mixture = v_s + (1-v_s)*mu2* l # print(mu_mixture) log_theta = self.log_theta[b] return D.NegativeBinomial( @@ -445,6 +450,7 @@ def forward( logits=(mu_mixture + EPS).log() - log_theta ) + class ZINBDataDecoder(NBDataDecoder): r""" From 45cada09378282121ca1b977025c5771a7a6fd50 Mon Sep 17 00:00:00 2001 From: Helloworldlty Date: Fri, 23 Feb 2024 08:38:20 -0500 Subject: [PATCH 4/5] modify the codes --- .history/pyproject_20240208235548.toml | 78 ++ .history/pyproject_20240223082542.toml | 78 ++ .history/pyproject_20240223083721.toml | 78 ++ .history/scglue/genomics_20240208225135.py | 943 ++++++++++++++++++ .history/scglue/genomics_20240223082556.py | 943 ++++++++++++++++++ .history/scglue/genomics_20240223082655.py | 943 ++++++++++++++++++ .history/scglue/genomics_20240223082707.py | 943 ++++++++++++++++++ .history/scglue/models/prob_20240208234227.py | 220 ++++ .history/scglue/models/prob_20240223082307.py | 151 +++ .history/scglue/models/sc_20240208224727.py | 638 ++++++++++++ .history/scglue/models/sc_20240223082240.py | 639 ++++++++++++ .history/scglue/models/sc_20240223082318.py | 639 ++++++++++++ docs/test citeseq tutorial.ipynb | 7 +- pyproject.toml | 3 +- scglue/genomics.py | 35 + scglue/models/prob.py | 1 + scglue/models/sc.py | 33 +- scglue/utils.py | 19 - 18 files changed, 6350 insertions(+), 41 deletions(-) create mode 100644 .history/pyproject_20240208235548.toml create mode 100644 .history/pyproject_20240223082542.toml create mode 100644 .history/pyproject_20240223083721.toml create mode 100644 .history/scglue/genomics_20240208225135.py create mode 100644 .history/scglue/genomics_20240223082556.py create mode 100644 .history/scglue/genomics_20240223082655.py create mode 100644 .history/scglue/genomics_20240223082707.py create mode 100644 .history/scglue/models/prob_20240208234227.py create mode 100644 .history/scglue/models/prob_20240223082307.py create mode 100644 .history/scglue/models/sc_20240208224727.py create mode 100644 .history/scglue/models/sc_20240223082240.py create mode 100644 .history/scglue/models/sc_20240223082318.py diff --git a/.history/pyproject_20240208235548.toml b/.history/pyproject_20240208235548.toml new file mode 100644 index 0000000..4606416 --- /dev/null +++ b/.history/pyproject_20240208235548.toml @@ -0,0 +1,78 @@ +[build-system] +requires = ["setuptools", "wheel", "flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "scglue" +version = "0.3.2" +description = "Graph-linked unified embedding for unpaired single-cell multi-omics data integration" +readme = "README.md" +requires-python = ">=3.6" +license = {file = "LICENSE"} +authors = [ + {name = "Zhi-Jie Cao", email = "caozj@mail.cbi.pku.edu.cn"}, + {name = "Xin-Ming Tu", email = "xinmingtu@pku.edu.cn"} +] +keywords = ["bioinformatics", "deep-learning", "single-cell", "single-cell-multiomics"] +classifiers = [ + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering :: Bio-Informatics" +] +dependencies = [ + "numpy>=1.19", + "scipy>=1.3", + "pandas>=1.1", + "matplotlib>=3.1.2", + "seaborn>=0.9", + "dill>=0.2.3", + "tqdm>=4.27", + "scikit-learn>=0.21.2", + "statsmodels>=0.10", + "parse>=1.3.2", + "networkx>=2", + "pynvml>=8.0.1", + "torch>=1.8", + "pytorch-ignite>=0.4.1", + "tensorboardX>=1.4", + "anndata>=0.7", + "scanpy>=1.5", + "pybedtools>=0.8.1", + "h5py>=2.10", + "sparse>=0.3.1", + "packaging>=16.8", + "leidenalg>=0.7", + "muon>=0.1.5" +] + +[project.optional-dependencies] +doc = [ + "sphinx<7", + "sphinx-autodoc-typehints", + "sphinx-copybutton", + "sphinx-intl", + "nbsphinx", + "sphinx-rtd-theme", + "ipython", + "jinja2" +] +test = [ + "plotly", + "pytest", + "pytest-cov" +] + +[project.urls] +Github = "https://github.com/gao-lab/GLUE" + +[tool.flit.sdist] +exclude = [".*", "c*", "d*", "e*", "pa*", "t*", "T*"] diff --git a/.history/pyproject_20240223082542.toml b/.history/pyproject_20240223082542.toml new file mode 100644 index 0000000..4606416 --- /dev/null +++ b/.history/pyproject_20240223082542.toml @@ -0,0 +1,78 @@ +[build-system] +requires = ["setuptools", "wheel", "flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "scglue" +version = "0.3.2" +description = "Graph-linked unified embedding for unpaired single-cell multi-omics data integration" +readme = "README.md" +requires-python = ">=3.6" +license = {file = "LICENSE"} +authors = [ + {name = "Zhi-Jie Cao", email = "caozj@mail.cbi.pku.edu.cn"}, + {name = "Xin-Ming Tu", email = "xinmingtu@pku.edu.cn"} +] +keywords = ["bioinformatics", "deep-learning", "single-cell", "single-cell-multiomics"] +classifiers = [ + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering :: Bio-Informatics" +] +dependencies = [ + "numpy>=1.19", + "scipy>=1.3", + "pandas>=1.1", + "matplotlib>=3.1.2", + "seaborn>=0.9", + "dill>=0.2.3", + "tqdm>=4.27", + "scikit-learn>=0.21.2", + "statsmodels>=0.10", + "parse>=1.3.2", + "networkx>=2", + "pynvml>=8.0.1", + "torch>=1.8", + "pytorch-ignite>=0.4.1", + "tensorboardX>=1.4", + "anndata>=0.7", + "scanpy>=1.5", + "pybedtools>=0.8.1", + "h5py>=2.10", + "sparse>=0.3.1", + "packaging>=16.8", + "leidenalg>=0.7", + "muon>=0.1.5" +] + +[project.optional-dependencies] +doc = [ + "sphinx<7", + "sphinx-autodoc-typehints", + "sphinx-copybutton", + "sphinx-intl", + "nbsphinx", + "sphinx-rtd-theme", + "ipython", + "jinja2" +] +test = [ + "plotly", + "pytest", + "pytest-cov" +] + +[project.urls] +Github = "https://github.com/gao-lab/GLUE" + +[tool.flit.sdist] +exclude = [".*", "c*", "d*", "e*", "pa*", "t*", "T*"] diff --git a/.history/pyproject_20240223083721.toml b/.history/pyproject_20240223083721.toml new file mode 100644 index 0000000..4606416 --- /dev/null +++ b/.history/pyproject_20240223083721.toml @@ -0,0 +1,78 @@ +[build-system] +requires = ["setuptools", "wheel", "flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "scglue" +version = "0.3.2" +description = "Graph-linked unified embedding for unpaired single-cell multi-omics data integration" +readme = "README.md" +requires-python = ">=3.6" +license = {file = "LICENSE"} +authors = [ + {name = "Zhi-Jie Cao", email = "caozj@mail.cbi.pku.edu.cn"}, + {name = "Xin-Ming Tu", email = "xinmingtu@pku.edu.cn"} +] +keywords = ["bioinformatics", "deep-learning", "single-cell", "single-cell-multiomics"] +classifiers = [ + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering :: Bio-Informatics" +] +dependencies = [ + "numpy>=1.19", + "scipy>=1.3", + "pandas>=1.1", + "matplotlib>=3.1.2", + "seaborn>=0.9", + "dill>=0.2.3", + "tqdm>=4.27", + "scikit-learn>=0.21.2", + "statsmodels>=0.10", + "parse>=1.3.2", + "networkx>=2", + "pynvml>=8.0.1", + "torch>=1.8", + "pytorch-ignite>=0.4.1", + "tensorboardX>=1.4", + "anndata>=0.7", + "scanpy>=1.5", + "pybedtools>=0.8.1", + "h5py>=2.10", + "sparse>=0.3.1", + "packaging>=16.8", + "leidenalg>=0.7", + "muon>=0.1.5" +] + +[project.optional-dependencies] +doc = [ + "sphinx<7", + "sphinx-autodoc-typehints", + "sphinx-copybutton", + "sphinx-intl", + "nbsphinx", + "sphinx-rtd-theme", + "ipython", + "jinja2" +] +test = [ + "plotly", + "pytest", + "pytest-cov" +] + +[project.urls] +Github = "https://github.com/gao-lab/GLUE" + +[tool.flit.sdist] +exclude = [".*", "c*", "d*", "e*", "pa*", "t*", "T*"] diff --git a/.history/scglue/genomics_20240208225135.py b/.history/scglue/genomics_20240208225135.py new file mode 100644 index 0000000..8687b24 --- /dev/null +++ b/.history/scglue/genomics_20240208225135.py @@ -0,0 +1,943 @@ +r""" +Genomics operations +""" + +import collections +import os +import re +from ast import literal_eval +from functools import reduce +from itertools import chain, product +from operator import add +from typing import Any, Callable, List, Mapping, Optional, Union + +import networkx as nx +import numpy as np +import pandas as pd +import pybedtools +import scipy.sparse +import scipy.stats +from anndata import AnnData +from networkx.algorithms.bipartite import biadjacency_matrix +from pybedtools import BedTool +from pybedtools.cbedtools import Interval +from statsmodels.stats.multitest import fdrcorrection +from tqdm.auto import tqdm + +from .check import check_deps +from .graph import compose_multigraph, reachable_vertices +from .typehint import RandomState +from .utils import ConstrainedDataFrame, logged, get_rs + + +class Bed(ConstrainedDataFrame): + + r""" + BED format data frame + """ + + COLUMNS = pd.Index([ + "chrom", "chromStart", "chromEnd", "name", "score", + "strand", "thickStart", "thickEnd", "itemRgb", + "blockCount", "blockSizes", "blockStarts" + ]) + + @classmethod + def rectify(cls, df: pd.DataFrame) -> pd.DataFrame: + df = super(Bed, cls).rectify(df) + COLUMNS = cls.COLUMNS.copy(deep=True) + for item in COLUMNS: + if item in df: + if item in ("chromStart", "chromEnd"): + df[item] = df[item].astype(int) + else: + df[item] = df[item].astype(str) + elif item not in ("chrom", "chromStart", "chromEnd"): + df[item] = "." + else: + raise ValueError(f"Required column {item} is missing!") + return df.loc[:, COLUMNS] + + @classmethod + def verify(cls, df: pd.DataFrame) -> None: + super(Bed, cls).verify(df) + if len(df.columns) != len(cls.COLUMNS) or np.any(df.columns != cls.COLUMNS): + raise ValueError("Invalid BED format!") + + @classmethod + def read_bed(cls, fname: os.PathLike) -> "Bed": + r""" + Read BED file + + Parameters + ---------- + fname + BED file + + Returns + ------- + bed + Loaded :class:`Bed` object + """ + COLUMNS = cls.COLUMNS.copy(deep=True) + loaded = pd.read_csv(fname, sep="\t", header=None, comment="#") + loaded.columns = COLUMNS[:loaded.shape[1]] + return cls(loaded) + + def write_bed(self, fname: os.PathLike, ncols: Optional[int] = None) -> None: + r""" + Write BED file + + Parameters + ---------- + fname + BED file + ncols + Number of columns to write (by default write all columns) + """ + if ncols and ncols < 3: + raise ValueError("`ncols` must be larger than 3!") + df = self.df.iloc[:, :ncols] if ncols else self + df.to_csv(fname, sep="\t", header=False, index=False) + + def to_bedtool(self) -> pybedtools.BedTool: + r""" + Convert to a :class:`pybedtools.BedTool` object + + Returns + ------- + bedtool + Converted :class:`pybedtools.BedTool` object + """ + return BedTool(Interval( + row["chrom"], row["chromStart"], row["chromEnd"], + name=row["name"], score=row["score"], strand=row["strand"] + ) for _, row in self.iterrows()) + + def nucleotide_content(self, fasta: os.PathLike) -> pd.DataFrame: + r""" + Compute nucleotide content in the BED regions + + Parameters + ---------- + fasta + Genomic sequence file in FASTA format + + Returns + ------- + nucleotide_stat + Data frame containing nucleotide content statistics for each region + """ + result = self.to_bedtool().nucleotide_content(fi=os.fspath(fasta), s=True) # pylint: disable=unexpected-keyword-arg + result = pd.DataFrame( + np.stack([interval.fields[6:15] for interval in result]), + columns=[ + r"%AT", r"%GC", + r"#A", r"#C", r"#G", r"#T", r"#N", + r"#other", r"length" + ] + ).astype({ + r"%AT": float, r"%GC": float, + r"#A": int, r"#C": int, r"#G": int, r"#T": int, r"#N": int, + r"#other": int, r"length": int + }) + pybedtools.cleanup() + return result + + def strand_specific_start_site(self) -> "Bed": + r""" + Convert to strand-specific start sites of genomic features + + Returns + ------- + start_site_bed + A new :class:`Bed` object, containing strand-specific start sites + of the current :class:`Bed` object + """ + if set(self["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + df = pd.DataFrame(self, copy=True) + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + df.loc[pos_strand, "chromEnd"] = df.loc[pos_strand, "chromStart"] + 1 + df.loc[neg_strand, "chromStart"] = df.loc[neg_strand, "chromEnd"] - 1 + return type(self)(df) + + def strand_specific_end_site(self) -> "Bed": + r""" + Convert to strand-specific end sites of genomic features + + Returns + ------- + end_site_bed + A new :class:`Bed` object, containing strand-specific end sites + of the current :class:`Bed` object + """ + if set(self["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + df = pd.DataFrame(self, copy=True) + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + df.loc[pos_strand, "chromStart"] = df.loc[pos_strand, "chromEnd"] - 1 + df.loc[neg_strand, "chromEnd"] = df.loc[neg_strand, "chromStart"] + 1 + return type(self)(df) + + def expand( + self, upstream: int, downstream: int, + chr_len: Optional[Mapping[str, int]] = None + ) -> "Bed": + r""" + Expand genomic features towards upstream and downstream + + Parameters + ---------- + upstream + Number of bps to expand in the upstream direction + downstream + Number of bps to expand in the downstream direction + chr_len + Length of each chromosome + + Returns + ------- + expanded_bed + A new :class:`Bed` object, containing expanded features + of the current :class:`Bed` object + + Note + ---- + Starting position < 0 after expansion is always trimmed. + Ending position exceeding chromosome length is trimed only if + ``chr_len`` is specified. + """ + if upstream == downstream == 0: + return self + df = pd.DataFrame(self, copy=True) + if upstream == downstream: # symmetric + df["chromStart"] -= upstream + df["chromEnd"] += downstream + else: # asymmetric + if set(df["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + if upstream: + df.loc[pos_strand, "chromStart"] -= upstream + df.loc[neg_strand, "chromEnd"] += upstream + if downstream: + df.loc[pos_strand, "chromEnd"] += downstream + df.loc[neg_strand, "chromStart"] -= downstream + df["chromStart"] = np.maximum(df["chromStart"], 0) + if chr_len: + chr_len = df["chrom"].map(chr_len) + df["chromEnd"] = np.minimum(df["chromEnd"], chr_len) + return type(self)(df) + + +class Gtf(ConstrainedDataFrame): # gffutils is too slow + + r""" + GTF format data frame + """ + + COLUMNS = pd.Index([ + "seqname", "source", "feature", "start", "end", + "score", "strand", "frame", "attribute" + ]) # Additional columns after "attribute" is allowed + + @classmethod + def rectify(cls, df: pd.DataFrame) -> pd.DataFrame: + df = super(Gtf, cls).rectify(df) + COLUMNS = cls.COLUMNS.copy(deep=True) + for item in COLUMNS: + if item in df: + if item in ("start", "end"): + df[item] = df[item].astype(int) + else: + df[item] = df[item].astype(str) + elif item not in ("seqname", "start", "end"): + df[item] = "." + else: + raise ValueError(f"Required column {item} is missing!") + return df.sort_index(axis=1, key=cls._column_key) + + @classmethod + def _column_key(cls, x: pd.Index) -> np.ndarray: + x = cls.COLUMNS.get_indexer(x) + x[x < 0] = x.max() + 1 # Put additional columns after "attribute" + return x + + @classmethod + def verify(cls, df: pd.DataFrame) -> None: + super(Gtf, cls).verify(df) + if len(df.columns) < len(cls.COLUMNS) or \ + np.any(df.columns[:len(cls.COLUMNS)] != cls.COLUMNS): + raise ValueError("Invalid GTF format!") + + @classmethod + def read_gtf(cls, fname: os.PathLike) -> "Gtf": + r""" + Read GTF file + + Parameters + ---------- + fname + GTF file + + Returns + ------- + gtf + Loaded :class:`Gtf` object + """ + COLUMNS = cls.COLUMNS.copy(deep=True) + loaded = pd.read_csv(fname, sep="\t", header=None, comment="#") + loaded.columns = COLUMNS[:loaded.shape[1]] + return cls(loaded) + + def split_attribute(self) -> "Gtf": + r""" + Extract all attributes from the "attribute" column + and append them to existing columns + + Returns + ------- + splitted + Gtf with splitted attribute columns appended + """ + pattern = re.compile(r'([^\s]+) "([^"]+)";') + splitted = pd.DataFrame.from_records(np.vectorize(lambda x: { + key: val for key, val in pattern.findall(x) + })(self["attribute"]), index=self.index) + if set(self.COLUMNS).intersection(splitted.columns): + self.logger.warning( + "Splitted attribute names overlap standard GTF fields! " + "The standard fields are overwritten!" + ) + return self.assign(**splitted) + + def to_bed(self, name: Optional[str] = None) -> Bed: + r""" + Convert GTF to BED format + + Parameters + ---------- + name + Specify a column to be converted to the "name" column in bed format, + otherwise the "name" column would be filled with "." + + Returns + ------- + bed + Converted :class:`Bed` object + """ + bed_df = pd.DataFrame(self, copy=True).loc[ + :, ("seqname", "start", "end", "score", "strand") + ] + bed_df.insert(3, "name", np.repeat( + ".", len(bed_df) + ) if name is None else self[name]) + bed_df["start"] -= 1 # Convert to zero-based + bed_df.columns = ( + "chrom", "chromStart", "chromEnd", "name", "score", "strand" + ) + return Bed(bed_df) + + +def interval_dist(x: Interval, y: Interval) -> int: + r""" + Compute distance and relative position between two bed intervals + + Parameters + ---------- + x + First interval + y + Second interval + + Returns + ------- + dist + Signed distance between ``x`` and ``y`` + """ + if x.chrom != y.chrom: + return np.inf * (-1 if x.chrom < y.chrom else 1) + if x.start < y.stop and y.start < x.stop: + return 0 + if x.stop <= y.start: + return x.stop - y.start - 1 + if y.stop <= x.start: + return x.start - y.stop + 1 + + +def window_graph( + left: Union[Bed, str], right: Union[Bed, str], window_size: int, + left_sorted: bool = False, right_sorted: bool = False, + attr_fn: Optional[Callable[[Interval, Interval, float], Mapping[str, Any]]] = None +) -> nx.MultiDiGraph: + r""" + Construct a window graph between two sets of genomic features, where + features pairs within a window size are connected. + + Parameters + ---------- + left + First feature set, either a :class:`Bed` object or path to a bed file + right + Second feature set, either a :class:`Bed` object or path to a bed file + window_size + Window size (in bp) + left_sorted + Whether ``left`` is already sorted + right_sorted + Whether ``right`` is already sorted + attr_fn + Function to compute edge attributes for connected features, + should accept the following three positional arguments: + + - l: left interval + - r: right interval + - d: signed distance between the intervals + + By default no edge attribute is created. + + Returns + ------- + graph + Window graph + """ + check_deps("bedtools") + if isinstance(left, Bed): + pbar_total = len(left) + left = left.to_bedtool() + else: + pbar_total = None + left = pybedtools.BedTool(left) + if not left_sorted: + left = left.sort(stream=True) + left = iter(left) # Resumable iterator + if isinstance(right, Bed): + right = right.to_bedtool() + else: + right = pybedtools.BedTool(right) + if not right_sorted: + right = right.sort(stream=True) + right = iter(right) # Resumable iterator + + attr_fn = attr_fn or (lambda l, r, d: {}) + if pbar_total is not None: + left = tqdm(left, total=pbar_total, desc="window_graph") + graph = nx.MultiDiGraph() + window = collections.OrderedDict() # Used as ordered set + for l in left: + for r in list(window.keys()): # Allow remove during iteration + d = interval_dist(l, r) + if -window_size <= d <= window_size: + graph.add_edge(l.name, r.name, **attr_fn(l, r, d)) + elif d > window_size: + del window[r] + else: # dist < -window_size + break # No need to expand window + else: + for r in right: # Resume from last break + d = interval_dist(l, r) + if -window_size <= d <= window_size: + graph.add_edge(l.name, r.name, **attr_fn(l, r, d)) + elif d > window_size: + continue + window[r] = None # Placeholder + if d < -window_size: + break + pybedtools.cleanup() + return graph + + +def dist_power_decay(x: int) -> float: + r""" + Distance-based power decay weight, computed as + :math:`w = {\left( \frac {d + 1000} {1000} \right)} ^ {-0.75}` + + Parameters + ---------- + x + Distance (in bp) + + Returns + ------- + weight + Decaying weight + """ + return ((x + 1000) / 1000) ** (-0.75) + + +@logged +def rna_anchored_guidance_graph( + rna: AnnData, *others: AnnData, + gene_region: str = "combined", promoter_len: int = 2000, + extend_range: int = 0, extend_fn: Callable[[int], float] = dist_power_decay, + signs: Optional[List[int]] = None, propagate_highly_variable: bool = True, + corrupt_rate: float = 0.0, random_state: RandomState = None +) -> nx.MultiDiGraph: + r""" + Build guidance graph anchored on RNA genes + + Parameters + ---------- + rna + Anchor RNA dataset + *others + Other datasets + gene_region + Defines the genomic region of genes, must be one of + ``{"gene_body", "promoter", "combined"}``. + promoter_len + Defines the length of gene promoters (bp upstream of TSS) + extend_range + Maximal extend distance beyond gene regions + extend_fn + Distance-decreasing weight function for the extended regions + (by default :func:`dist_power_decay`) + signs + Sign of edges between RNA genes and features in each ``*others`` + dataset, must have the same length as ``*others``. Signs must be + one of ``{-1, 1}``. By default, all edges have positive signs of ``1``. + propagate_highly_variable + Whether to propagate highly variable genes to other datasets, + datasets in ``*others`` would be modified in place. + corrupt_rate + **CAUTION: DO NOT USE**, only for evaluation purpose + random_state + **CAUTION: DO NOT USE**, only for evaluation purpose + + Returns + ------- + graph + Prior regulatory graph + + Note + ---- + In this function, features in the same dataset can only connect to + anchor genes via the same edge sign. For more flexibility, please + construct the guidance graph manually. + """ + signs = signs or [1] * len(others) + if len(others) != len(signs): + raise RuntimeError("Length of ``others`` and ``signs`` must match!") + if set(signs).difference({-1, 1}): + raise RuntimeError("``signs`` can only contain {-1, 1}!") + + rna_bed = Bed(rna.var.assign(name=rna.var_names)) + other_beds = [Bed(other.var.assign(name=other.var_names)) for other in others] + if gene_region == "promoter": + rna_bed = rna_bed.strand_specific_start_site().expand(promoter_len, 0) + elif gene_region == "combined": + rna_bed = rna_bed.expand(promoter_len, 0) + elif gene_region != "gene_body": + raise ValueError("Unrecognized `gene_range`!") + graphs = [window_graph( + rna_bed, other_bed, window_size=extend_range, + attr_fn=lambda l, r, d, s=sign: { + "dist": abs(d), "weight": extend_fn(abs(d)), "sign": s + } + ) for other_bed, sign in zip(other_beds, signs)] + graph = compose_multigraph(*graphs) + + corrupt_num = round(corrupt_rate * graph.number_of_edges()) + if corrupt_num: + rna_anchored_guidance_graph.logger.warning("Corrupting guidance graph!") + rs = get_rs(random_state) + rna_var_names = rna.var_names.tolist() + other_var_names = reduce(add, [other.var_names.tolist() for other in others]) + + corrupt_remove = set(rs.choice(graph.number_of_edges(), corrupt_num, replace=False)) + corrupt_remove = set(edge for i, edge in enumerate(graph.edges) if i in corrupt_remove) + corrupt_add = [] + while len(corrupt_add) < corrupt_num: + corrupt_add += [ + (u, v) for u, v in zip( + rs.choice(rna_var_names, corrupt_num - len(corrupt_add)), + rs.choice(other_var_names, corrupt_num - len(corrupt_add)) + ) if not graph.has_edge(u, v) + ] + + graph.add_edges_from([ + (add[0], add[1], graph.edges[remove]) + for add, remove in zip(corrupt_add, corrupt_remove) + ]) + graph.remove_edges_from(corrupt_remove) + + if propagate_highly_variable: + hvg_reachable = reachable_vertices(graph, rna.var.query("highly_variable").index) + for other in others: + other.var["highly_variable"] = [ + item in hvg_reachable for item in other.var_names + ] + + rgraph = graph.reverse() + nx.set_edge_attributes(graph, "fwd", name="type") + nx.set_edge_attributes(rgraph, "rev", name="type") + graph = compose_multigraph(graph, rgraph) + all_features = set(chain.from_iterable( + map(lambda x: x.var_names, [rna, *others]) + )) + for item in all_features: + graph.add_edge(item, item, weight=1.0, sign=1, type="loop") + return graph + + +@logged +def rna_anchored_prior_graph( + rna: AnnData, *others: AnnData, + gene_region: str = "combined", promoter_len: int = 2000, + extend_range: int = 0, extend_fn: Callable[[int], float] = dist_power_decay, + signs: Optional[List[int]] = None, propagate_highly_variable: bool = True, + corrupt_rate: float = 0.0, random_state: RandomState = None +) -> nx.MultiDiGraph: # pragma: no cover + r""" + Deprecated, please use :func:`rna_anchored_guidance_graph` instead + """ + rna_anchored_prior_graph.logger.warning( + "Deprecated, please use `rna_anchored_guidance_graph` instead!" + ) + return rna_anchored_guidance_graph( + rna, *others, gene_region=gene_region, promoter_len=promoter_len, + extend_range=extend_range, extend_fn=extend_fn, signs=signs, + propagate_highly_variable=propagate_highly_variable, + corrupt_rate=corrupt_rate, random_state=random_state + ) + + +def regulatory_inference( + features: pd.Index, feature_embeddings: Union[np.ndarray, List[np.ndarray]], + skeleton: nx.Graph, alternative: str = "two.sided", + random_state: RandomState = None +) -> nx.Graph: + r""" + Regulatory inference based on feature embeddings + + Parameters + ---------- + features + Feature names + feature_embeddings + List of feature embeddings from 1 or more models + skeleton + Skeleton graph + alternative + Alternative hypothesis, must be one of {"two.sided", "less", "greater"} + random_state + Random state + + Returns + ------- + regulatory_graph + Regulatory graph containing regulatory score ("score"), + *P*-value ("pval"), *Q*-value ("pval") as edge attributes + for feature pairs in the skeleton graph + """ + if isinstance(feature_embeddings, np.ndarray): + feature_embeddings = [feature_embeddings] + n_features = set(item.shape[0] for item in feature_embeddings) + if len(n_features) != 1: + raise ValueError("All feature embeddings must have the same number of rows!") + if n_features.pop() != features.shape[0]: + raise ValueError("Feature embeddings do not match the number of feature names!") + node_idx = features.get_indexer(skeleton.nodes) + features = features[node_idx] + feature_embeddings = [item[node_idx] for item in feature_embeddings] + + rs = get_rs(random_state) + vperm = np.stack([rs.permutation(item) for item in feature_embeddings], axis=1) + vperm = vperm / np.linalg.norm(vperm, axis=-1, keepdims=True) + v = np.stack(feature_embeddings, axis=1) + v = v / np.linalg.norm(v, axis=-1, keepdims=True) + + edgelist = nx.to_pandas_edgelist(skeleton) + source = features.get_indexer(edgelist["source"]) + target = features.get_indexer(edgelist["target"]) + fg, bg = [], [] + + for s, t in tqdm(zip(source, target), total=skeleton.number_of_edges(), desc="regulatory_inference"): + fg.append((v[s] * v[t]).sum(axis=1).mean()) + bg.append((vperm[s] * vperm[t]).sum(axis=1)) + edgelist["score"] = fg + + bg = np.sort(np.concatenate(bg)) + quantile = np.searchsorted(bg, fg) / bg.size + if alternative == "two.sided": + edgelist["pval"] = 2 * np.minimum(quantile, 1 - quantile) + elif alternative == "greater": + edgelist["pval"] = 1 - quantile + elif alternative == "less": + edgelist["pval"] = quantile + else: + raise ValueError("Unrecognized `alternative`!") + edgelist["qval"] = fdrcorrection(edgelist["pval"])[1] + return nx.from_pandas_edgelist(edgelist, edge_attr=True, create_using=type(skeleton)) + + +def write_links( + graph: nx.Graph, source: Bed, target: Bed, file: os.PathLike, + keep_attrs: Optional[List[str]] = None +) -> None: + r""" + Export regulatory graph into a links file + + Parameters + ---------- + graph + Regulatory graph + source + Genomic coordinates of source nodes + target + Genomic coordinates of target nodes + file + Output file + keep_attrs + A list of attributes to keep for each link + """ + nx.to_pandas_edgelist( + graph + ).merge( + source.df.iloc[:, :4], how="left", left_on="source", right_on="name" + ).merge( + target.df.iloc[:, :4], how="left", left_on="target", right_on="name" + ).loc[:, [ + "chrom_x", "chromStart_x", "chromEnd_x", + "chrom_y", "chromStart_y", "chromEnd_y", + *(keep_attrs or []) + ]].to_csv(file, sep="\t", index=False, header=False) + + +def cis_regulatory_ranking( + gene2region: nx.Graph, region2tf: nx.Graph, + genes: List[str], regions: List[str], tfs: List[str], + region_lens: Optional[List[int]] = None, n_samples: int = 1000, + random_state: RandomState = None +) -> pd.DataFrame: + r""" + Generate cis-regulatory ranking between genes and transcription factors + + Parameters + ---------- + gene2region + A graph connecting genes to cis-regulatory regions + region2tf + A graph connecting cis-regulatory regions to transcription factors + genes + A list of genes + tfs + A list of transcription factors + regions + A list of cis-regulatory regions + region_lens + Lengths of cis-regulatory regions + (if not provided, it is assumed that all regions have the same length) + n_samples + Number of random samples used to evaluate regulatory enrichment + (setting this to 0 disables enrichment evaluation) + random_state + Random state + + Returns + ------- + gene2tf_rank + Cis regulatory ranking between genes and transcription factors + """ + gene2region = biadjacency_matrix(gene2region, genes, regions, dtype=np.int16, weight=None) + region2tf = biadjacency_matrix(region2tf, regions, tfs, dtype=np.int16, weight=None) + + if n_samples: + region_lens = [1] * len(regions) if region_lens is None else region_lens + if len(region_lens) != len(regions): + raise ValueError("`region_lens` must have the same length as `regions`!") + region_bins = pd.qcut(region_lens, min(len(set(region_lens)), 500), duplicates="drop") + region_bins_lut = pd.RangeIndex(region_bins.size).groupby(region_bins) + + rs = get_rs(random_state) + row, col_rand, data = [], [], [] + lil = gene2region.tolil() + for r, (c, d) in tqdm( + enumerate(zip(lil.rows, lil.data)), + total=len(lil.rows), desc="cis_reg_ranking.sampling" + ): + if not c: # Empty row + continue + row.append(np.ones_like(c) * r) + col_rand.append(np.stack([ + rs.choice(region_bins_lut[region_bins[c_]], n_samples, replace=True) + for c_ in c + ], axis=0)) + data.append(d) + row = np.concatenate(row) + col_rand = np.concatenate(col_rand) + data = np.concatenate(data) + + gene2tf_obs = (gene2region @ region2tf).toarray() + gene2tf_rand = np.empty((len(genes), len(tfs), n_samples), dtype=np.int16) + for k in tqdm(range(n_samples), desc="cis_reg_ranking.mapping"): + gene2region_rand = scipy.sparse.coo_matrix(( + data, (row, col_rand[:, k]) + ), shape=(len(genes), len(regions))) + gene2tf_rand[:, :, k] = (gene2region_rand @ region2tf).toarray() + gene2tf_rand.sort(axis=2) + + gene2tf_enrich = np.empty_like(gene2tf_obs) + for i, j in product(range(len(genes)), range(len(tfs))): + if gene2tf_obs[i, j] == 0: + gene2tf_enrich[i, j] = 0 + continue + gene2tf_enrich[i, j] = np.searchsorted( + gene2tf_rand[i, j, :], gene2tf_obs[i, j], side="right" + ) + else: + gene2tf_enrich = (gene2region @ region2tf).toarray() + + return pd.DataFrame( + scipy.stats.rankdata(-gene2tf_enrich, axis=0), + index=genes, columns=tfs + ) + + +def write_scenic_feather( + gene2tf_rank: pd.DataFrame, feather: os.PathLike, + version: int = 2 +) -> None: + r""" + Write cis-regulatory ranking to a SCENIC-compatible feather file + + Parameters + ---------- + gene2tf_rank + Cis regulatory ranking between genes and transcription factors, + as generated by :func:`cis_reg_ranking` + feather + Path to the output feather file + version + SCENIC feather version + """ + if version not in {1, 2}: + raise ValueError("Unrecognized SCENIC feather version!") + if version == 2: + suffix = ".genes_vs_tracks.rankings.feather" + if not str(feather).endswith(suffix): + raise ValueError(f"Feather file name must end with `{suffix}`!") + tf2gene_rank = gene2tf_rank.T + tf2gene_rank = tf2gene_rank.loc[ + np.unique(tf2gene_rank.index), np.unique(tf2gene_rank.columns) + ].astype(np.int16) + tf2gene_rank.index.name = "features" if version == 1 else "tracks" + tf2gene_rank.columns.name = None + columns = tf2gene_rank.columns.tolist() + tf2gene_rank = tf2gene_rank.reset_index() + if version == 2: + tf2gene_rank = tf2gene_rank.loc[:, [*columns, "tracks"]] + tf2gene_rank.to_feather(feather) + + +def read_ctx_grn(file: os.PathLike) -> nx.DiGraph: + r""" + Read pruned TF-target GRN as generated by ``pyscenic ctx`` + + Parameters + ---------- + file + Input file (.csv) + + Returns + ------- + grn + Pruned TF-target GRN + + Note + ---- + Node attribute "type" can be used to distinguish TFs and genes + """ + df = pd.read_csv( + file, header=None, skiprows=3, + usecols=[0, 8], names=["TF", "targets"] + ) + df["targets"] = df["targets"].map(lambda x: set(i[0] for i in literal_eval(x))) + df = df.groupby("TF").aggregate({"targets": lambda x: reduce(set.union, x)}) + grn = nx.DiGraph([ + (tf, target) + for tf, row in df.iterrows() + for target in row["targets"]] + ) + nx.set_node_attributes(grn, "target", name="type") + for tf in df.index: + grn.nodes[tf]["target"] = "TF" + return grn + + +def get_chr_len_from_fai(fai: os.PathLike) -> Mapping[str, int]: + r""" + Get chromosome length information from fasta index file + + Parameters + ---------- + fai + Fasta index file + + Returns + ------- + chr_len + Length of each chromosome + """ + return pd.read_table(fai, header=None, index_col=0)[1].to_dict() + + +def ens_trim_version(x: str) -> str: + r""" + Trim version suffix from Ensembl ID + + Parameters + ---------- + x + Ensembl ID + + Returns + ------- + trimmed + Ensembl ID with version suffix trimmed + """ + return re.sub(r"\.[0-9_-]+$", "", x) + +# Function for DIY guidance graph +def generate_prot_guidance_graph(rna: AnnData, + prot: AnnData, + protein_gene_match: Mapping[str, str]): + + r""" + Generate the guidance graph based on CITE-seq datasets. + + Parameters + ---------- + rna + AnnData with gene expression information. + prot + AnnData with protein expression information. + protein_gene_match + The dictionary used to match proteins with genes. + + Returns + ------- + guidance + The guidance map between proteins and genes. + """ + guidance =nx.MultiDiGraph() + for k, v in protein_gene_match.items(): + guidance.add_edge(k, v, weight=1.0, sign=1, type="rev") + guidance.add_edge(v, k, weight=1.0, sign=1, type="fwd") + + for item in rna.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") + for item in prot.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") + + + return guidance + + +# Aliases +read_bed = Bed.read_bed +read_gtf = Gtf.read_gtf diff --git a/.history/scglue/genomics_20240223082556.py b/.history/scglue/genomics_20240223082556.py new file mode 100644 index 0000000..8687b24 --- /dev/null +++ b/.history/scglue/genomics_20240223082556.py @@ -0,0 +1,943 @@ +r""" +Genomics operations +""" + +import collections +import os +import re +from ast import literal_eval +from functools import reduce +from itertools import chain, product +from operator import add +from typing import Any, Callable, List, Mapping, Optional, Union + +import networkx as nx +import numpy as np +import pandas as pd +import pybedtools +import scipy.sparse +import scipy.stats +from anndata import AnnData +from networkx.algorithms.bipartite import biadjacency_matrix +from pybedtools import BedTool +from pybedtools.cbedtools import Interval +from statsmodels.stats.multitest import fdrcorrection +from tqdm.auto import tqdm + +from .check import check_deps +from .graph import compose_multigraph, reachable_vertices +from .typehint import RandomState +from .utils import ConstrainedDataFrame, logged, get_rs + + +class Bed(ConstrainedDataFrame): + + r""" + BED format data frame + """ + + COLUMNS = pd.Index([ + "chrom", "chromStart", "chromEnd", "name", "score", + "strand", "thickStart", "thickEnd", "itemRgb", + "blockCount", "blockSizes", "blockStarts" + ]) + + @classmethod + def rectify(cls, df: pd.DataFrame) -> pd.DataFrame: + df = super(Bed, cls).rectify(df) + COLUMNS = cls.COLUMNS.copy(deep=True) + for item in COLUMNS: + if item in df: + if item in ("chromStart", "chromEnd"): + df[item] = df[item].astype(int) + else: + df[item] = df[item].astype(str) + elif item not in ("chrom", "chromStart", "chromEnd"): + df[item] = "." + else: + raise ValueError(f"Required column {item} is missing!") + return df.loc[:, COLUMNS] + + @classmethod + def verify(cls, df: pd.DataFrame) -> None: + super(Bed, cls).verify(df) + if len(df.columns) != len(cls.COLUMNS) or np.any(df.columns != cls.COLUMNS): + raise ValueError("Invalid BED format!") + + @classmethod + def read_bed(cls, fname: os.PathLike) -> "Bed": + r""" + Read BED file + + Parameters + ---------- + fname + BED file + + Returns + ------- + bed + Loaded :class:`Bed` object + """ + COLUMNS = cls.COLUMNS.copy(deep=True) + loaded = pd.read_csv(fname, sep="\t", header=None, comment="#") + loaded.columns = COLUMNS[:loaded.shape[1]] + return cls(loaded) + + def write_bed(self, fname: os.PathLike, ncols: Optional[int] = None) -> None: + r""" + Write BED file + + Parameters + ---------- + fname + BED file + ncols + Number of columns to write (by default write all columns) + """ + if ncols and ncols < 3: + raise ValueError("`ncols` must be larger than 3!") + df = self.df.iloc[:, :ncols] if ncols else self + df.to_csv(fname, sep="\t", header=False, index=False) + + def to_bedtool(self) -> pybedtools.BedTool: + r""" + Convert to a :class:`pybedtools.BedTool` object + + Returns + ------- + bedtool + Converted :class:`pybedtools.BedTool` object + """ + return BedTool(Interval( + row["chrom"], row["chromStart"], row["chromEnd"], + name=row["name"], score=row["score"], strand=row["strand"] + ) for _, row in self.iterrows()) + + def nucleotide_content(self, fasta: os.PathLike) -> pd.DataFrame: + r""" + Compute nucleotide content in the BED regions + + Parameters + ---------- + fasta + Genomic sequence file in FASTA format + + Returns + ------- + nucleotide_stat + Data frame containing nucleotide content statistics for each region + """ + result = self.to_bedtool().nucleotide_content(fi=os.fspath(fasta), s=True) # pylint: disable=unexpected-keyword-arg + result = pd.DataFrame( + np.stack([interval.fields[6:15] for interval in result]), + columns=[ + r"%AT", r"%GC", + r"#A", r"#C", r"#G", r"#T", r"#N", + r"#other", r"length" + ] + ).astype({ + r"%AT": float, r"%GC": float, + r"#A": int, r"#C": int, r"#G": int, r"#T": int, r"#N": int, + r"#other": int, r"length": int + }) + pybedtools.cleanup() + return result + + def strand_specific_start_site(self) -> "Bed": + r""" + Convert to strand-specific start sites of genomic features + + Returns + ------- + start_site_bed + A new :class:`Bed` object, containing strand-specific start sites + of the current :class:`Bed` object + """ + if set(self["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + df = pd.DataFrame(self, copy=True) + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + df.loc[pos_strand, "chromEnd"] = df.loc[pos_strand, "chromStart"] + 1 + df.loc[neg_strand, "chromStart"] = df.loc[neg_strand, "chromEnd"] - 1 + return type(self)(df) + + def strand_specific_end_site(self) -> "Bed": + r""" + Convert to strand-specific end sites of genomic features + + Returns + ------- + end_site_bed + A new :class:`Bed` object, containing strand-specific end sites + of the current :class:`Bed` object + """ + if set(self["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + df = pd.DataFrame(self, copy=True) + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + df.loc[pos_strand, "chromStart"] = df.loc[pos_strand, "chromEnd"] - 1 + df.loc[neg_strand, "chromEnd"] = df.loc[neg_strand, "chromStart"] + 1 + return type(self)(df) + + def expand( + self, upstream: int, downstream: int, + chr_len: Optional[Mapping[str, int]] = None + ) -> "Bed": + r""" + Expand genomic features towards upstream and downstream + + Parameters + ---------- + upstream + Number of bps to expand in the upstream direction + downstream + Number of bps to expand in the downstream direction + chr_len + Length of each chromosome + + Returns + ------- + expanded_bed + A new :class:`Bed` object, containing expanded features + of the current :class:`Bed` object + + Note + ---- + Starting position < 0 after expansion is always trimmed. + Ending position exceeding chromosome length is trimed only if + ``chr_len`` is specified. + """ + if upstream == downstream == 0: + return self + df = pd.DataFrame(self, copy=True) + if upstream == downstream: # symmetric + df["chromStart"] -= upstream + df["chromEnd"] += downstream + else: # asymmetric + if set(df["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + if upstream: + df.loc[pos_strand, "chromStart"] -= upstream + df.loc[neg_strand, "chromEnd"] += upstream + if downstream: + df.loc[pos_strand, "chromEnd"] += downstream + df.loc[neg_strand, "chromStart"] -= downstream + df["chromStart"] = np.maximum(df["chromStart"], 0) + if chr_len: + chr_len = df["chrom"].map(chr_len) + df["chromEnd"] = np.minimum(df["chromEnd"], chr_len) + return type(self)(df) + + +class Gtf(ConstrainedDataFrame): # gffutils is too slow + + r""" + GTF format data frame + """ + + COLUMNS = pd.Index([ + "seqname", "source", "feature", "start", "end", + "score", "strand", "frame", "attribute" + ]) # Additional columns after "attribute" is allowed + + @classmethod + def rectify(cls, df: pd.DataFrame) -> pd.DataFrame: + df = super(Gtf, cls).rectify(df) + COLUMNS = cls.COLUMNS.copy(deep=True) + for item in COLUMNS: + if item in df: + if item in ("start", "end"): + df[item] = df[item].astype(int) + else: + df[item] = df[item].astype(str) + elif item not in ("seqname", "start", "end"): + df[item] = "." + else: + raise ValueError(f"Required column {item} is missing!") + return df.sort_index(axis=1, key=cls._column_key) + + @classmethod + def _column_key(cls, x: pd.Index) -> np.ndarray: + x = cls.COLUMNS.get_indexer(x) + x[x < 0] = x.max() + 1 # Put additional columns after "attribute" + return x + + @classmethod + def verify(cls, df: pd.DataFrame) -> None: + super(Gtf, cls).verify(df) + if len(df.columns) < len(cls.COLUMNS) or \ + np.any(df.columns[:len(cls.COLUMNS)] != cls.COLUMNS): + raise ValueError("Invalid GTF format!") + + @classmethod + def read_gtf(cls, fname: os.PathLike) -> "Gtf": + r""" + Read GTF file + + Parameters + ---------- + fname + GTF file + + Returns + ------- + gtf + Loaded :class:`Gtf` object + """ + COLUMNS = cls.COLUMNS.copy(deep=True) + loaded = pd.read_csv(fname, sep="\t", header=None, comment="#") + loaded.columns = COLUMNS[:loaded.shape[1]] + return cls(loaded) + + def split_attribute(self) -> "Gtf": + r""" + Extract all attributes from the "attribute" column + and append them to existing columns + + Returns + ------- + splitted + Gtf with splitted attribute columns appended + """ + pattern = re.compile(r'([^\s]+) "([^"]+)";') + splitted = pd.DataFrame.from_records(np.vectorize(lambda x: { + key: val for key, val in pattern.findall(x) + })(self["attribute"]), index=self.index) + if set(self.COLUMNS).intersection(splitted.columns): + self.logger.warning( + "Splitted attribute names overlap standard GTF fields! " + "The standard fields are overwritten!" + ) + return self.assign(**splitted) + + def to_bed(self, name: Optional[str] = None) -> Bed: + r""" + Convert GTF to BED format + + Parameters + ---------- + name + Specify a column to be converted to the "name" column in bed format, + otherwise the "name" column would be filled with "." + + Returns + ------- + bed + Converted :class:`Bed` object + """ + bed_df = pd.DataFrame(self, copy=True).loc[ + :, ("seqname", "start", "end", "score", "strand") + ] + bed_df.insert(3, "name", np.repeat( + ".", len(bed_df) + ) if name is None else self[name]) + bed_df["start"] -= 1 # Convert to zero-based + bed_df.columns = ( + "chrom", "chromStart", "chromEnd", "name", "score", "strand" + ) + return Bed(bed_df) + + +def interval_dist(x: Interval, y: Interval) -> int: + r""" + Compute distance and relative position between two bed intervals + + Parameters + ---------- + x + First interval + y + Second interval + + Returns + ------- + dist + Signed distance between ``x`` and ``y`` + """ + if x.chrom != y.chrom: + return np.inf * (-1 if x.chrom < y.chrom else 1) + if x.start < y.stop and y.start < x.stop: + return 0 + if x.stop <= y.start: + return x.stop - y.start - 1 + if y.stop <= x.start: + return x.start - y.stop + 1 + + +def window_graph( + left: Union[Bed, str], right: Union[Bed, str], window_size: int, + left_sorted: bool = False, right_sorted: bool = False, + attr_fn: Optional[Callable[[Interval, Interval, float], Mapping[str, Any]]] = None +) -> nx.MultiDiGraph: + r""" + Construct a window graph between two sets of genomic features, where + features pairs within a window size are connected. + + Parameters + ---------- + left + First feature set, either a :class:`Bed` object or path to a bed file + right + Second feature set, either a :class:`Bed` object or path to a bed file + window_size + Window size (in bp) + left_sorted + Whether ``left`` is already sorted + right_sorted + Whether ``right`` is already sorted + attr_fn + Function to compute edge attributes for connected features, + should accept the following three positional arguments: + + - l: left interval + - r: right interval + - d: signed distance between the intervals + + By default no edge attribute is created. + + Returns + ------- + graph + Window graph + """ + check_deps("bedtools") + if isinstance(left, Bed): + pbar_total = len(left) + left = left.to_bedtool() + else: + pbar_total = None + left = pybedtools.BedTool(left) + if not left_sorted: + left = left.sort(stream=True) + left = iter(left) # Resumable iterator + if isinstance(right, Bed): + right = right.to_bedtool() + else: + right = pybedtools.BedTool(right) + if not right_sorted: + right = right.sort(stream=True) + right = iter(right) # Resumable iterator + + attr_fn = attr_fn or (lambda l, r, d: {}) + if pbar_total is not None: + left = tqdm(left, total=pbar_total, desc="window_graph") + graph = nx.MultiDiGraph() + window = collections.OrderedDict() # Used as ordered set + for l in left: + for r in list(window.keys()): # Allow remove during iteration + d = interval_dist(l, r) + if -window_size <= d <= window_size: + graph.add_edge(l.name, r.name, **attr_fn(l, r, d)) + elif d > window_size: + del window[r] + else: # dist < -window_size + break # No need to expand window + else: + for r in right: # Resume from last break + d = interval_dist(l, r) + if -window_size <= d <= window_size: + graph.add_edge(l.name, r.name, **attr_fn(l, r, d)) + elif d > window_size: + continue + window[r] = None # Placeholder + if d < -window_size: + break + pybedtools.cleanup() + return graph + + +def dist_power_decay(x: int) -> float: + r""" + Distance-based power decay weight, computed as + :math:`w = {\left( \frac {d + 1000} {1000} \right)} ^ {-0.75}` + + Parameters + ---------- + x + Distance (in bp) + + Returns + ------- + weight + Decaying weight + """ + return ((x + 1000) / 1000) ** (-0.75) + + +@logged +def rna_anchored_guidance_graph( + rna: AnnData, *others: AnnData, + gene_region: str = "combined", promoter_len: int = 2000, + extend_range: int = 0, extend_fn: Callable[[int], float] = dist_power_decay, + signs: Optional[List[int]] = None, propagate_highly_variable: bool = True, + corrupt_rate: float = 0.0, random_state: RandomState = None +) -> nx.MultiDiGraph: + r""" + Build guidance graph anchored on RNA genes + + Parameters + ---------- + rna + Anchor RNA dataset + *others + Other datasets + gene_region + Defines the genomic region of genes, must be one of + ``{"gene_body", "promoter", "combined"}``. + promoter_len + Defines the length of gene promoters (bp upstream of TSS) + extend_range + Maximal extend distance beyond gene regions + extend_fn + Distance-decreasing weight function for the extended regions + (by default :func:`dist_power_decay`) + signs + Sign of edges between RNA genes and features in each ``*others`` + dataset, must have the same length as ``*others``. Signs must be + one of ``{-1, 1}``. By default, all edges have positive signs of ``1``. + propagate_highly_variable + Whether to propagate highly variable genes to other datasets, + datasets in ``*others`` would be modified in place. + corrupt_rate + **CAUTION: DO NOT USE**, only for evaluation purpose + random_state + **CAUTION: DO NOT USE**, only for evaluation purpose + + Returns + ------- + graph + Prior regulatory graph + + Note + ---- + In this function, features in the same dataset can only connect to + anchor genes via the same edge sign. For more flexibility, please + construct the guidance graph manually. + """ + signs = signs or [1] * len(others) + if len(others) != len(signs): + raise RuntimeError("Length of ``others`` and ``signs`` must match!") + if set(signs).difference({-1, 1}): + raise RuntimeError("``signs`` can only contain {-1, 1}!") + + rna_bed = Bed(rna.var.assign(name=rna.var_names)) + other_beds = [Bed(other.var.assign(name=other.var_names)) for other in others] + if gene_region == "promoter": + rna_bed = rna_bed.strand_specific_start_site().expand(promoter_len, 0) + elif gene_region == "combined": + rna_bed = rna_bed.expand(promoter_len, 0) + elif gene_region != "gene_body": + raise ValueError("Unrecognized `gene_range`!") + graphs = [window_graph( + rna_bed, other_bed, window_size=extend_range, + attr_fn=lambda l, r, d, s=sign: { + "dist": abs(d), "weight": extend_fn(abs(d)), "sign": s + } + ) for other_bed, sign in zip(other_beds, signs)] + graph = compose_multigraph(*graphs) + + corrupt_num = round(corrupt_rate * graph.number_of_edges()) + if corrupt_num: + rna_anchored_guidance_graph.logger.warning("Corrupting guidance graph!") + rs = get_rs(random_state) + rna_var_names = rna.var_names.tolist() + other_var_names = reduce(add, [other.var_names.tolist() for other in others]) + + corrupt_remove = set(rs.choice(graph.number_of_edges(), corrupt_num, replace=False)) + corrupt_remove = set(edge for i, edge in enumerate(graph.edges) if i in corrupt_remove) + corrupt_add = [] + while len(corrupt_add) < corrupt_num: + corrupt_add += [ + (u, v) for u, v in zip( + rs.choice(rna_var_names, corrupt_num - len(corrupt_add)), + rs.choice(other_var_names, corrupt_num - len(corrupt_add)) + ) if not graph.has_edge(u, v) + ] + + graph.add_edges_from([ + (add[0], add[1], graph.edges[remove]) + for add, remove in zip(corrupt_add, corrupt_remove) + ]) + graph.remove_edges_from(corrupt_remove) + + if propagate_highly_variable: + hvg_reachable = reachable_vertices(graph, rna.var.query("highly_variable").index) + for other in others: + other.var["highly_variable"] = [ + item in hvg_reachable for item in other.var_names + ] + + rgraph = graph.reverse() + nx.set_edge_attributes(graph, "fwd", name="type") + nx.set_edge_attributes(rgraph, "rev", name="type") + graph = compose_multigraph(graph, rgraph) + all_features = set(chain.from_iterable( + map(lambda x: x.var_names, [rna, *others]) + )) + for item in all_features: + graph.add_edge(item, item, weight=1.0, sign=1, type="loop") + return graph + + +@logged +def rna_anchored_prior_graph( + rna: AnnData, *others: AnnData, + gene_region: str = "combined", promoter_len: int = 2000, + extend_range: int = 0, extend_fn: Callable[[int], float] = dist_power_decay, + signs: Optional[List[int]] = None, propagate_highly_variable: bool = True, + corrupt_rate: float = 0.0, random_state: RandomState = None +) -> nx.MultiDiGraph: # pragma: no cover + r""" + Deprecated, please use :func:`rna_anchored_guidance_graph` instead + """ + rna_anchored_prior_graph.logger.warning( + "Deprecated, please use `rna_anchored_guidance_graph` instead!" + ) + return rna_anchored_guidance_graph( + rna, *others, gene_region=gene_region, promoter_len=promoter_len, + extend_range=extend_range, extend_fn=extend_fn, signs=signs, + propagate_highly_variable=propagate_highly_variable, + corrupt_rate=corrupt_rate, random_state=random_state + ) + + +def regulatory_inference( + features: pd.Index, feature_embeddings: Union[np.ndarray, List[np.ndarray]], + skeleton: nx.Graph, alternative: str = "two.sided", + random_state: RandomState = None +) -> nx.Graph: + r""" + Regulatory inference based on feature embeddings + + Parameters + ---------- + features + Feature names + feature_embeddings + List of feature embeddings from 1 or more models + skeleton + Skeleton graph + alternative + Alternative hypothesis, must be one of {"two.sided", "less", "greater"} + random_state + Random state + + Returns + ------- + regulatory_graph + Regulatory graph containing regulatory score ("score"), + *P*-value ("pval"), *Q*-value ("pval") as edge attributes + for feature pairs in the skeleton graph + """ + if isinstance(feature_embeddings, np.ndarray): + feature_embeddings = [feature_embeddings] + n_features = set(item.shape[0] for item in feature_embeddings) + if len(n_features) != 1: + raise ValueError("All feature embeddings must have the same number of rows!") + if n_features.pop() != features.shape[0]: + raise ValueError("Feature embeddings do not match the number of feature names!") + node_idx = features.get_indexer(skeleton.nodes) + features = features[node_idx] + feature_embeddings = [item[node_idx] for item in feature_embeddings] + + rs = get_rs(random_state) + vperm = np.stack([rs.permutation(item) for item in feature_embeddings], axis=1) + vperm = vperm / np.linalg.norm(vperm, axis=-1, keepdims=True) + v = np.stack(feature_embeddings, axis=1) + v = v / np.linalg.norm(v, axis=-1, keepdims=True) + + edgelist = nx.to_pandas_edgelist(skeleton) + source = features.get_indexer(edgelist["source"]) + target = features.get_indexer(edgelist["target"]) + fg, bg = [], [] + + for s, t in tqdm(zip(source, target), total=skeleton.number_of_edges(), desc="regulatory_inference"): + fg.append((v[s] * v[t]).sum(axis=1).mean()) + bg.append((vperm[s] * vperm[t]).sum(axis=1)) + edgelist["score"] = fg + + bg = np.sort(np.concatenate(bg)) + quantile = np.searchsorted(bg, fg) / bg.size + if alternative == "two.sided": + edgelist["pval"] = 2 * np.minimum(quantile, 1 - quantile) + elif alternative == "greater": + edgelist["pval"] = 1 - quantile + elif alternative == "less": + edgelist["pval"] = quantile + else: + raise ValueError("Unrecognized `alternative`!") + edgelist["qval"] = fdrcorrection(edgelist["pval"])[1] + return nx.from_pandas_edgelist(edgelist, edge_attr=True, create_using=type(skeleton)) + + +def write_links( + graph: nx.Graph, source: Bed, target: Bed, file: os.PathLike, + keep_attrs: Optional[List[str]] = None +) -> None: + r""" + Export regulatory graph into a links file + + Parameters + ---------- + graph + Regulatory graph + source + Genomic coordinates of source nodes + target + Genomic coordinates of target nodes + file + Output file + keep_attrs + A list of attributes to keep for each link + """ + nx.to_pandas_edgelist( + graph + ).merge( + source.df.iloc[:, :4], how="left", left_on="source", right_on="name" + ).merge( + target.df.iloc[:, :4], how="left", left_on="target", right_on="name" + ).loc[:, [ + "chrom_x", "chromStart_x", "chromEnd_x", + "chrom_y", "chromStart_y", "chromEnd_y", + *(keep_attrs or []) + ]].to_csv(file, sep="\t", index=False, header=False) + + +def cis_regulatory_ranking( + gene2region: nx.Graph, region2tf: nx.Graph, + genes: List[str], regions: List[str], tfs: List[str], + region_lens: Optional[List[int]] = None, n_samples: int = 1000, + random_state: RandomState = None +) -> pd.DataFrame: + r""" + Generate cis-regulatory ranking between genes and transcription factors + + Parameters + ---------- + gene2region + A graph connecting genes to cis-regulatory regions + region2tf + A graph connecting cis-regulatory regions to transcription factors + genes + A list of genes + tfs + A list of transcription factors + regions + A list of cis-regulatory regions + region_lens + Lengths of cis-regulatory regions + (if not provided, it is assumed that all regions have the same length) + n_samples + Number of random samples used to evaluate regulatory enrichment + (setting this to 0 disables enrichment evaluation) + random_state + Random state + + Returns + ------- + gene2tf_rank + Cis regulatory ranking between genes and transcription factors + """ + gene2region = biadjacency_matrix(gene2region, genes, regions, dtype=np.int16, weight=None) + region2tf = biadjacency_matrix(region2tf, regions, tfs, dtype=np.int16, weight=None) + + if n_samples: + region_lens = [1] * len(regions) if region_lens is None else region_lens + if len(region_lens) != len(regions): + raise ValueError("`region_lens` must have the same length as `regions`!") + region_bins = pd.qcut(region_lens, min(len(set(region_lens)), 500), duplicates="drop") + region_bins_lut = pd.RangeIndex(region_bins.size).groupby(region_bins) + + rs = get_rs(random_state) + row, col_rand, data = [], [], [] + lil = gene2region.tolil() + for r, (c, d) in tqdm( + enumerate(zip(lil.rows, lil.data)), + total=len(lil.rows), desc="cis_reg_ranking.sampling" + ): + if not c: # Empty row + continue + row.append(np.ones_like(c) * r) + col_rand.append(np.stack([ + rs.choice(region_bins_lut[region_bins[c_]], n_samples, replace=True) + for c_ in c + ], axis=0)) + data.append(d) + row = np.concatenate(row) + col_rand = np.concatenate(col_rand) + data = np.concatenate(data) + + gene2tf_obs = (gene2region @ region2tf).toarray() + gene2tf_rand = np.empty((len(genes), len(tfs), n_samples), dtype=np.int16) + for k in tqdm(range(n_samples), desc="cis_reg_ranking.mapping"): + gene2region_rand = scipy.sparse.coo_matrix(( + data, (row, col_rand[:, k]) + ), shape=(len(genes), len(regions))) + gene2tf_rand[:, :, k] = (gene2region_rand @ region2tf).toarray() + gene2tf_rand.sort(axis=2) + + gene2tf_enrich = np.empty_like(gene2tf_obs) + for i, j in product(range(len(genes)), range(len(tfs))): + if gene2tf_obs[i, j] == 0: + gene2tf_enrich[i, j] = 0 + continue + gene2tf_enrich[i, j] = np.searchsorted( + gene2tf_rand[i, j, :], gene2tf_obs[i, j], side="right" + ) + else: + gene2tf_enrich = (gene2region @ region2tf).toarray() + + return pd.DataFrame( + scipy.stats.rankdata(-gene2tf_enrich, axis=0), + index=genes, columns=tfs + ) + + +def write_scenic_feather( + gene2tf_rank: pd.DataFrame, feather: os.PathLike, + version: int = 2 +) -> None: + r""" + Write cis-regulatory ranking to a SCENIC-compatible feather file + + Parameters + ---------- + gene2tf_rank + Cis regulatory ranking between genes and transcription factors, + as generated by :func:`cis_reg_ranking` + feather + Path to the output feather file + version + SCENIC feather version + """ + if version not in {1, 2}: + raise ValueError("Unrecognized SCENIC feather version!") + if version == 2: + suffix = ".genes_vs_tracks.rankings.feather" + if not str(feather).endswith(suffix): + raise ValueError(f"Feather file name must end with `{suffix}`!") + tf2gene_rank = gene2tf_rank.T + tf2gene_rank = tf2gene_rank.loc[ + np.unique(tf2gene_rank.index), np.unique(tf2gene_rank.columns) + ].astype(np.int16) + tf2gene_rank.index.name = "features" if version == 1 else "tracks" + tf2gene_rank.columns.name = None + columns = tf2gene_rank.columns.tolist() + tf2gene_rank = tf2gene_rank.reset_index() + if version == 2: + tf2gene_rank = tf2gene_rank.loc[:, [*columns, "tracks"]] + tf2gene_rank.to_feather(feather) + + +def read_ctx_grn(file: os.PathLike) -> nx.DiGraph: + r""" + Read pruned TF-target GRN as generated by ``pyscenic ctx`` + + Parameters + ---------- + file + Input file (.csv) + + Returns + ------- + grn + Pruned TF-target GRN + + Note + ---- + Node attribute "type" can be used to distinguish TFs and genes + """ + df = pd.read_csv( + file, header=None, skiprows=3, + usecols=[0, 8], names=["TF", "targets"] + ) + df["targets"] = df["targets"].map(lambda x: set(i[0] for i in literal_eval(x))) + df = df.groupby("TF").aggregate({"targets": lambda x: reduce(set.union, x)}) + grn = nx.DiGraph([ + (tf, target) + for tf, row in df.iterrows() + for target in row["targets"]] + ) + nx.set_node_attributes(grn, "target", name="type") + for tf in df.index: + grn.nodes[tf]["target"] = "TF" + return grn + + +def get_chr_len_from_fai(fai: os.PathLike) -> Mapping[str, int]: + r""" + Get chromosome length information from fasta index file + + Parameters + ---------- + fai + Fasta index file + + Returns + ------- + chr_len + Length of each chromosome + """ + return pd.read_table(fai, header=None, index_col=0)[1].to_dict() + + +def ens_trim_version(x: str) -> str: + r""" + Trim version suffix from Ensembl ID + + Parameters + ---------- + x + Ensembl ID + + Returns + ------- + trimmed + Ensembl ID with version suffix trimmed + """ + return re.sub(r"\.[0-9_-]+$", "", x) + +# Function for DIY guidance graph +def generate_prot_guidance_graph(rna: AnnData, + prot: AnnData, + protein_gene_match: Mapping[str, str]): + + r""" + Generate the guidance graph based on CITE-seq datasets. + + Parameters + ---------- + rna + AnnData with gene expression information. + prot + AnnData with protein expression information. + protein_gene_match + The dictionary used to match proteins with genes. + + Returns + ------- + guidance + The guidance map between proteins and genes. + """ + guidance =nx.MultiDiGraph() + for k, v in protein_gene_match.items(): + guidance.add_edge(k, v, weight=1.0, sign=1, type="rev") + guidance.add_edge(v, k, weight=1.0, sign=1, type="fwd") + + for item in rna.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") + for item in prot.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") + + + return guidance + + +# Aliases +read_bed = Bed.read_bed +read_gtf = Gtf.read_gtf diff --git a/.history/scglue/genomics_20240223082655.py b/.history/scglue/genomics_20240223082655.py new file mode 100644 index 0000000..8687b24 --- /dev/null +++ b/.history/scglue/genomics_20240223082655.py @@ -0,0 +1,943 @@ +r""" +Genomics operations +""" + +import collections +import os +import re +from ast import literal_eval +from functools import reduce +from itertools import chain, product +from operator import add +from typing import Any, Callable, List, Mapping, Optional, Union + +import networkx as nx +import numpy as np +import pandas as pd +import pybedtools +import scipy.sparse +import scipy.stats +from anndata import AnnData +from networkx.algorithms.bipartite import biadjacency_matrix +from pybedtools import BedTool +from pybedtools.cbedtools import Interval +from statsmodels.stats.multitest import fdrcorrection +from tqdm.auto import tqdm + +from .check import check_deps +from .graph import compose_multigraph, reachable_vertices +from .typehint import RandomState +from .utils import ConstrainedDataFrame, logged, get_rs + + +class Bed(ConstrainedDataFrame): + + r""" + BED format data frame + """ + + COLUMNS = pd.Index([ + "chrom", "chromStart", "chromEnd", "name", "score", + "strand", "thickStart", "thickEnd", "itemRgb", + "blockCount", "blockSizes", "blockStarts" + ]) + + @classmethod + def rectify(cls, df: pd.DataFrame) -> pd.DataFrame: + df = super(Bed, cls).rectify(df) + COLUMNS = cls.COLUMNS.copy(deep=True) + for item in COLUMNS: + if item in df: + if item in ("chromStart", "chromEnd"): + df[item] = df[item].astype(int) + else: + df[item] = df[item].astype(str) + elif item not in ("chrom", "chromStart", "chromEnd"): + df[item] = "." + else: + raise ValueError(f"Required column {item} is missing!") + return df.loc[:, COLUMNS] + + @classmethod + def verify(cls, df: pd.DataFrame) -> None: + super(Bed, cls).verify(df) + if len(df.columns) != len(cls.COLUMNS) or np.any(df.columns != cls.COLUMNS): + raise ValueError("Invalid BED format!") + + @classmethod + def read_bed(cls, fname: os.PathLike) -> "Bed": + r""" + Read BED file + + Parameters + ---------- + fname + BED file + + Returns + ------- + bed + Loaded :class:`Bed` object + """ + COLUMNS = cls.COLUMNS.copy(deep=True) + loaded = pd.read_csv(fname, sep="\t", header=None, comment="#") + loaded.columns = COLUMNS[:loaded.shape[1]] + return cls(loaded) + + def write_bed(self, fname: os.PathLike, ncols: Optional[int] = None) -> None: + r""" + Write BED file + + Parameters + ---------- + fname + BED file + ncols + Number of columns to write (by default write all columns) + """ + if ncols and ncols < 3: + raise ValueError("`ncols` must be larger than 3!") + df = self.df.iloc[:, :ncols] if ncols else self + df.to_csv(fname, sep="\t", header=False, index=False) + + def to_bedtool(self) -> pybedtools.BedTool: + r""" + Convert to a :class:`pybedtools.BedTool` object + + Returns + ------- + bedtool + Converted :class:`pybedtools.BedTool` object + """ + return BedTool(Interval( + row["chrom"], row["chromStart"], row["chromEnd"], + name=row["name"], score=row["score"], strand=row["strand"] + ) for _, row in self.iterrows()) + + def nucleotide_content(self, fasta: os.PathLike) -> pd.DataFrame: + r""" + Compute nucleotide content in the BED regions + + Parameters + ---------- + fasta + Genomic sequence file in FASTA format + + Returns + ------- + nucleotide_stat + Data frame containing nucleotide content statistics for each region + """ + result = self.to_bedtool().nucleotide_content(fi=os.fspath(fasta), s=True) # pylint: disable=unexpected-keyword-arg + result = pd.DataFrame( + np.stack([interval.fields[6:15] for interval in result]), + columns=[ + r"%AT", r"%GC", + r"#A", r"#C", r"#G", r"#T", r"#N", + r"#other", r"length" + ] + ).astype({ + r"%AT": float, r"%GC": float, + r"#A": int, r"#C": int, r"#G": int, r"#T": int, r"#N": int, + r"#other": int, r"length": int + }) + pybedtools.cleanup() + return result + + def strand_specific_start_site(self) -> "Bed": + r""" + Convert to strand-specific start sites of genomic features + + Returns + ------- + start_site_bed + A new :class:`Bed` object, containing strand-specific start sites + of the current :class:`Bed` object + """ + if set(self["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + df = pd.DataFrame(self, copy=True) + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + df.loc[pos_strand, "chromEnd"] = df.loc[pos_strand, "chromStart"] + 1 + df.loc[neg_strand, "chromStart"] = df.loc[neg_strand, "chromEnd"] - 1 + return type(self)(df) + + def strand_specific_end_site(self) -> "Bed": + r""" + Convert to strand-specific end sites of genomic features + + Returns + ------- + end_site_bed + A new :class:`Bed` object, containing strand-specific end sites + of the current :class:`Bed` object + """ + if set(self["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + df = pd.DataFrame(self, copy=True) + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + df.loc[pos_strand, "chromStart"] = df.loc[pos_strand, "chromEnd"] - 1 + df.loc[neg_strand, "chromEnd"] = df.loc[neg_strand, "chromStart"] + 1 + return type(self)(df) + + def expand( + self, upstream: int, downstream: int, + chr_len: Optional[Mapping[str, int]] = None + ) -> "Bed": + r""" + Expand genomic features towards upstream and downstream + + Parameters + ---------- + upstream + Number of bps to expand in the upstream direction + downstream + Number of bps to expand in the downstream direction + chr_len + Length of each chromosome + + Returns + ------- + expanded_bed + A new :class:`Bed` object, containing expanded features + of the current :class:`Bed` object + + Note + ---- + Starting position < 0 after expansion is always trimmed. + Ending position exceeding chromosome length is trimed only if + ``chr_len`` is specified. + """ + if upstream == downstream == 0: + return self + df = pd.DataFrame(self, copy=True) + if upstream == downstream: # symmetric + df["chromStart"] -= upstream + df["chromEnd"] += downstream + else: # asymmetric + if set(df["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + if upstream: + df.loc[pos_strand, "chromStart"] -= upstream + df.loc[neg_strand, "chromEnd"] += upstream + if downstream: + df.loc[pos_strand, "chromEnd"] += downstream + df.loc[neg_strand, "chromStart"] -= downstream + df["chromStart"] = np.maximum(df["chromStart"], 0) + if chr_len: + chr_len = df["chrom"].map(chr_len) + df["chromEnd"] = np.minimum(df["chromEnd"], chr_len) + return type(self)(df) + + +class Gtf(ConstrainedDataFrame): # gffutils is too slow + + r""" + GTF format data frame + """ + + COLUMNS = pd.Index([ + "seqname", "source", "feature", "start", "end", + "score", "strand", "frame", "attribute" + ]) # Additional columns after "attribute" is allowed + + @classmethod + def rectify(cls, df: pd.DataFrame) -> pd.DataFrame: + df = super(Gtf, cls).rectify(df) + COLUMNS = cls.COLUMNS.copy(deep=True) + for item in COLUMNS: + if item in df: + if item in ("start", "end"): + df[item] = df[item].astype(int) + else: + df[item] = df[item].astype(str) + elif item not in ("seqname", "start", "end"): + df[item] = "." + else: + raise ValueError(f"Required column {item} is missing!") + return df.sort_index(axis=1, key=cls._column_key) + + @classmethod + def _column_key(cls, x: pd.Index) -> np.ndarray: + x = cls.COLUMNS.get_indexer(x) + x[x < 0] = x.max() + 1 # Put additional columns after "attribute" + return x + + @classmethod + def verify(cls, df: pd.DataFrame) -> None: + super(Gtf, cls).verify(df) + if len(df.columns) < len(cls.COLUMNS) or \ + np.any(df.columns[:len(cls.COLUMNS)] != cls.COLUMNS): + raise ValueError("Invalid GTF format!") + + @classmethod + def read_gtf(cls, fname: os.PathLike) -> "Gtf": + r""" + Read GTF file + + Parameters + ---------- + fname + GTF file + + Returns + ------- + gtf + Loaded :class:`Gtf` object + """ + COLUMNS = cls.COLUMNS.copy(deep=True) + loaded = pd.read_csv(fname, sep="\t", header=None, comment="#") + loaded.columns = COLUMNS[:loaded.shape[1]] + return cls(loaded) + + def split_attribute(self) -> "Gtf": + r""" + Extract all attributes from the "attribute" column + and append them to existing columns + + Returns + ------- + splitted + Gtf with splitted attribute columns appended + """ + pattern = re.compile(r'([^\s]+) "([^"]+)";') + splitted = pd.DataFrame.from_records(np.vectorize(lambda x: { + key: val for key, val in pattern.findall(x) + })(self["attribute"]), index=self.index) + if set(self.COLUMNS).intersection(splitted.columns): + self.logger.warning( + "Splitted attribute names overlap standard GTF fields! " + "The standard fields are overwritten!" + ) + return self.assign(**splitted) + + def to_bed(self, name: Optional[str] = None) -> Bed: + r""" + Convert GTF to BED format + + Parameters + ---------- + name + Specify a column to be converted to the "name" column in bed format, + otherwise the "name" column would be filled with "." + + Returns + ------- + bed + Converted :class:`Bed` object + """ + bed_df = pd.DataFrame(self, copy=True).loc[ + :, ("seqname", "start", "end", "score", "strand") + ] + bed_df.insert(3, "name", np.repeat( + ".", len(bed_df) + ) if name is None else self[name]) + bed_df["start"] -= 1 # Convert to zero-based + bed_df.columns = ( + "chrom", "chromStart", "chromEnd", "name", "score", "strand" + ) + return Bed(bed_df) + + +def interval_dist(x: Interval, y: Interval) -> int: + r""" + Compute distance and relative position between two bed intervals + + Parameters + ---------- + x + First interval + y + Second interval + + Returns + ------- + dist + Signed distance between ``x`` and ``y`` + """ + if x.chrom != y.chrom: + return np.inf * (-1 if x.chrom < y.chrom else 1) + if x.start < y.stop and y.start < x.stop: + return 0 + if x.stop <= y.start: + return x.stop - y.start - 1 + if y.stop <= x.start: + return x.start - y.stop + 1 + + +def window_graph( + left: Union[Bed, str], right: Union[Bed, str], window_size: int, + left_sorted: bool = False, right_sorted: bool = False, + attr_fn: Optional[Callable[[Interval, Interval, float], Mapping[str, Any]]] = None +) -> nx.MultiDiGraph: + r""" + Construct a window graph between two sets of genomic features, where + features pairs within a window size are connected. + + Parameters + ---------- + left + First feature set, either a :class:`Bed` object or path to a bed file + right + Second feature set, either a :class:`Bed` object or path to a bed file + window_size + Window size (in bp) + left_sorted + Whether ``left`` is already sorted + right_sorted + Whether ``right`` is already sorted + attr_fn + Function to compute edge attributes for connected features, + should accept the following three positional arguments: + + - l: left interval + - r: right interval + - d: signed distance between the intervals + + By default no edge attribute is created. + + Returns + ------- + graph + Window graph + """ + check_deps("bedtools") + if isinstance(left, Bed): + pbar_total = len(left) + left = left.to_bedtool() + else: + pbar_total = None + left = pybedtools.BedTool(left) + if not left_sorted: + left = left.sort(stream=True) + left = iter(left) # Resumable iterator + if isinstance(right, Bed): + right = right.to_bedtool() + else: + right = pybedtools.BedTool(right) + if not right_sorted: + right = right.sort(stream=True) + right = iter(right) # Resumable iterator + + attr_fn = attr_fn or (lambda l, r, d: {}) + if pbar_total is not None: + left = tqdm(left, total=pbar_total, desc="window_graph") + graph = nx.MultiDiGraph() + window = collections.OrderedDict() # Used as ordered set + for l in left: + for r in list(window.keys()): # Allow remove during iteration + d = interval_dist(l, r) + if -window_size <= d <= window_size: + graph.add_edge(l.name, r.name, **attr_fn(l, r, d)) + elif d > window_size: + del window[r] + else: # dist < -window_size + break # No need to expand window + else: + for r in right: # Resume from last break + d = interval_dist(l, r) + if -window_size <= d <= window_size: + graph.add_edge(l.name, r.name, **attr_fn(l, r, d)) + elif d > window_size: + continue + window[r] = None # Placeholder + if d < -window_size: + break + pybedtools.cleanup() + return graph + + +def dist_power_decay(x: int) -> float: + r""" + Distance-based power decay weight, computed as + :math:`w = {\left( \frac {d + 1000} {1000} \right)} ^ {-0.75}` + + Parameters + ---------- + x + Distance (in bp) + + Returns + ------- + weight + Decaying weight + """ + return ((x + 1000) / 1000) ** (-0.75) + + +@logged +def rna_anchored_guidance_graph( + rna: AnnData, *others: AnnData, + gene_region: str = "combined", promoter_len: int = 2000, + extend_range: int = 0, extend_fn: Callable[[int], float] = dist_power_decay, + signs: Optional[List[int]] = None, propagate_highly_variable: bool = True, + corrupt_rate: float = 0.0, random_state: RandomState = None +) -> nx.MultiDiGraph: + r""" + Build guidance graph anchored on RNA genes + + Parameters + ---------- + rna + Anchor RNA dataset + *others + Other datasets + gene_region + Defines the genomic region of genes, must be one of + ``{"gene_body", "promoter", "combined"}``. + promoter_len + Defines the length of gene promoters (bp upstream of TSS) + extend_range + Maximal extend distance beyond gene regions + extend_fn + Distance-decreasing weight function for the extended regions + (by default :func:`dist_power_decay`) + signs + Sign of edges between RNA genes and features in each ``*others`` + dataset, must have the same length as ``*others``. Signs must be + one of ``{-1, 1}``. By default, all edges have positive signs of ``1``. + propagate_highly_variable + Whether to propagate highly variable genes to other datasets, + datasets in ``*others`` would be modified in place. + corrupt_rate + **CAUTION: DO NOT USE**, only for evaluation purpose + random_state + **CAUTION: DO NOT USE**, only for evaluation purpose + + Returns + ------- + graph + Prior regulatory graph + + Note + ---- + In this function, features in the same dataset can only connect to + anchor genes via the same edge sign. For more flexibility, please + construct the guidance graph manually. + """ + signs = signs or [1] * len(others) + if len(others) != len(signs): + raise RuntimeError("Length of ``others`` and ``signs`` must match!") + if set(signs).difference({-1, 1}): + raise RuntimeError("``signs`` can only contain {-1, 1}!") + + rna_bed = Bed(rna.var.assign(name=rna.var_names)) + other_beds = [Bed(other.var.assign(name=other.var_names)) for other in others] + if gene_region == "promoter": + rna_bed = rna_bed.strand_specific_start_site().expand(promoter_len, 0) + elif gene_region == "combined": + rna_bed = rna_bed.expand(promoter_len, 0) + elif gene_region != "gene_body": + raise ValueError("Unrecognized `gene_range`!") + graphs = [window_graph( + rna_bed, other_bed, window_size=extend_range, + attr_fn=lambda l, r, d, s=sign: { + "dist": abs(d), "weight": extend_fn(abs(d)), "sign": s + } + ) for other_bed, sign in zip(other_beds, signs)] + graph = compose_multigraph(*graphs) + + corrupt_num = round(corrupt_rate * graph.number_of_edges()) + if corrupt_num: + rna_anchored_guidance_graph.logger.warning("Corrupting guidance graph!") + rs = get_rs(random_state) + rna_var_names = rna.var_names.tolist() + other_var_names = reduce(add, [other.var_names.tolist() for other in others]) + + corrupt_remove = set(rs.choice(graph.number_of_edges(), corrupt_num, replace=False)) + corrupt_remove = set(edge for i, edge in enumerate(graph.edges) if i in corrupt_remove) + corrupt_add = [] + while len(corrupt_add) < corrupt_num: + corrupt_add += [ + (u, v) for u, v in zip( + rs.choice(rna_var_names, corrupt_num - len(corrupt_add)), + rs.choice(other_var_names, corrupt_num - len(corrupt_add)) + ) if not graph.has_edge(u, v) + ] + + graph.add_edges_from([ + (add[0], add[1], graph.edges[remove]) + for add, remove in zip(corrupt_add, corrupt_remove) + ]) + graph.remove_edges_from(corrupt_remove) + + if propagate_highly_variable: + hvg_reachable = reachable_vertices(graph, rna.var.query("highly_variable").index) + for other in others: + other.var["highly_variable"] = [ + item in hvg_reachable for item in other.var_names + ] + + rgraph = graph.reverse() + nx.set_edge_attributes(graph, "fwd", name="type") + nx.set_edge_attributes(rgraph, "rev", name="type") + graph = compose_multigraph(graph, rgraph) + all_features = set(chain.from_iterable( + map(lambda x: x.var_names, [rna, *others]) + )) + for item in all_features: + graph.add_edge(item, item, weight=1.0, sign=1, type="loop") + return graph + + +@logged +def rna_anchored_prior_graph( + rna: AnnData, *others: AnnData, + gene_region: str = "combined", promoter_len: int = 2000, + extend_range: int = 0, extend_fn: Callable[[int], float] = dist_power_decay, + signs: Optional[List[int]] = None, propagate_highly_variable: bool = True, + corrupt_rate: float = 0.0, random_state: RandomState = None +) -> nx.MultiDiGraph: # pragma: no cover + r""" + Deprecated, please use :func:`rna_anchored_guidance_graph` instead + """ + rna_anchored_prior_graph.logger.warning( + "Deprecated, please use `rna_anchored_guidance_graph` instead!" + ) + return rna_anchored_guidance_graph( + rna, *others, gene_region=gene_region, promoter_len=promoter_len, + extend_range=extend_range, extend_fn=extend_fn, signs=signs, + propagate_highly_variable=propagate_highly_variable, + corrupt_rate=corrupt_rate, random_state=random_state + ) + + +def regulatory_inference( + features: pd.Index, feature_embeddings: Union[np.ndarray, List[np.ndarray]], + skeleton: nx.Graph, alternative: str = "two.sided", + random_state: RandomState = None +) -> nx.Graph: + r""" + Regulatory inference based on feature embeddings + + Parameters + ---------- + features + Feature names + feature_embeddings + List of feature embeddings from 1 or more models + skeleton + Skeleton graph + alternative + Alternative hypothesis, must be one of {"two.sided", "less", "greater"} + random_state + Random state + + Returns + ------- + regulatory_graph + Regulatory graph containing regulatory score ("score"), + *P*-value ("pval"), *Q*-value ("pval") as edge attributes + for feature pairs in the skeleton graph + """ + if isinstance(feature_embeddings, np.ndarray): + feature_embeddings = [feature_embeddings] + n_features = set(item.shape[0] for item in feature_embeddings) + if len(n_features) != 1: + raise ValueError("All feature embeddings must have the same number of rows!") + if n_features.pop() != features.shape[0]: + raise ValueError("Feature embeddings do not match the number of feature names!") + node_idx = features.get_indexer(skeleton.nodes) + features = features[node_idx] + feature_embeddings = [item[node_idx] for item in feature_embeddings] + + rs = get_rs(random_state) + vperm = np.stack([rs.permutation(item) for item in feature_embeddings], axis=1) + vperm = vperm / np.linalg.norm(vperm, axis=-1, keepdims=True) + v = np.stack(feature_embeddings, axis=1) + v = v / np.linalg.norm(v, axis=-1, keepdims=True) + + edgelist = nx.to_pandas_edgelist(skeleton) + source = features.get_indexer(edgelist["source"]) + target = features.get_indexer(edgelist["target"]) + fg, bg = [], [] + + for s, t in tqdm(zip(source, target), total=skeleton.number_of_edges(), desc="regulatory_inference"): + fg.append((v[s] * v[t]).sum(axis=1).mean()) + bg.append((vperm[s] * vperm[t]).sum(axis=1)) + edgelist["score"] = fg + + bg = np.sort(np.concatenate(bg)) + quantile = np.searchsorted(bg, fg) / bg.size + if alternative == "two.sided": + edgelist["pval"] = 2 * np.minimum(quantile, 1 - quantile) + elif alternative == "greater": + edgelist["pval"] = 1 - quantile + elif alternative == "less": + edgelist["pval"] = quantile + else: + raise ValueError("Unrecognized `alternative`!") + edgelist["qval"] = fdrcorrection(edgelist["pval"])[1] + return nx.from_pandas_edgelist(edgelist, edge_attr=True, create_using=type(skeleton)) + + +def write_links( + graph: nx.Graph, source: Bed, target: Bed, file: os.PathLike, + keep_attrs: Optional[List[str]] = None +) -> None: + r""" + Export regulatory graph into a links file + + Parameters + ---------- + graph + Regulatory graph + source + Genomic coordinates of source nodes + target + Genomic coordinates of target nodes + file + Output file + keep_attrs + A list of attributes to keep for each link + """ + nx.to_pandas_edgelist( + graph + ).merge( + source.df.iloc[:, :4], how="left", left_on="source", right_on="name" + ).merge( + target.df.iloc[:, :4], how="left", left_on="target", right_on="name" + ).loc[:, [ + "chrom_x", "chromStart_x", "chromEnd_x", + "chrom_y", "chromStart_y", "chromEnd_y", + *(keep_attrs or []) + ]].to_csv(file, sep="\t", index=False, header=False) + + +def cis_regulatory_ranking( + gene2region: nx.Graph, region2tf: nx.Graph, + genes: List[str], regions: List[str], tfs: List[str], + region_lens: Optional[List[int]] = None, n_samples: int = 1000, + random_state: RandomState = None +) -> pd.DataFrame: + r""" + Generate cis-regulatory ranking between genes and transcription factors + + Parameters + ---------- + gene2region + A graph connecting genes to cis-regulatory regions + region2tf + A graph connecting cis-regulatory regions to transcription factors + genes + A list of genes + tfs + A list of transcription factors + regions + A list of cis-regulatory regions + region_lens + Lengths of cis-regulatory regions + (if not provided, it is assumed that all regions have the same length) + n_samples + Number of random samples used to evaluate regulatory enrichment + (setting this to 0 disables enrichment evaluation) + random_state + Random state + + Returns + ------- + gene2tf_rank + Cis regulatory ranking between genes and transcription factors + """ + gene2region = biadjacency_matrix(gene2region, genes, regions, dtype=np.int16, weight=None) + region2tf = biadjacency_matrix(region2tf, regions, tfs, dtype=np.int16, weight=None) + + if n_samples: + region_lens = [1] * len(regions) if region_lens is None else region_lens + if len(region_lens) != len(regions): + raise ValueError("`region_lens` must have the same length as `regions`!") + region_bins = pd.qcut(region_lens, min(len(set(region_lens)), 500), duplicates="drop") + region_bins_lut = pd.RangeIndex(region_bins.size).groupby(region_bins) + + rs = get_rs(random_state) + row, col_rand, data = [], [], [] + lil = gene2region.tolil() + for r, (c, d) in tqdm( + enumerate(zip(lil.rows, lil.data)), + total=len(lil.rows), desc="cis_reg_ranking.sampling" + ): + if not c: # Empty row + continue + row.append(np.ones_like(c) * r) + col_rand.append(np.stack([ + rs.choice(region_bins_lut[region_bins[c_]], n_samples, replace=True) + for c_ in c + ], axis=0)) + data.append(d) + row = np.concatenate(row) + col_rand = np.concatenate(col_rand) + data = np.concatenate(data) + + gene2tf_obs = (gene2region @ region2tf).toarray() + gene2tf_rand = np.empty((len(genes), len(tfs), n_samples), dtype=np.int16) + for k in tqdm(range(n_samples), desc="cis_reg_ranking.mapping"): + gene2region_rand = scipy.sparse.coo_matrix(( + data, (row, col_rand[:, k]) + ), shape=(len(genes), len(regions))) + gene2tf_rand[:, :, k] = (gene2region_rand @ region2tf).toarray() + gene2tf_rand.sort(axis=2) + + gene2tf_enrich = np.empty_like(gene2tf_obs) + for i, j in product(range(len(genes)), range(len(tfs))): + if gene2tf_obs[i, j] == 0: + gene2tf_enrich[i, j] = 0 + continue + gene2tf_enrich[i, j] = np.searchsorted( + gene2tf_rand[i, j, :], gene2tf_obs[i, j], side="right" + ) + else: + gene2tf_enrich = (gene2region @ region2tf).toarray() + + return pd.DataFrame( + scipy.stats.rankdata(-gene2tf_enrich, axis=0), + index=genes, columns=tfs + ) + + +def write_scenic_feather( + gene2tf_rank: pd.DataFrame, feather: os.PathLike, + version: int = 2 +) -> None: + r""" + Write cis-regulatory ranking to a SCENIC-compatible feather file + + Parameters + ---------- + gene2tf_rank + Cis regulatory ranking between genes and transcription factors, + as generated by :func:`cis_reg_ranking` + feather + Path to the output feather file + version + SCENIC feather version + """ + if version not in {1, 2}: + raise ValueError("Unrecognized SCENIC feather version!") + if version == 2: + suffix = ".genes_vs_tracks.rankings.feather" + if not str(feather).endswith(suffix): + raise ValueError(f"Feather file name must end with `{suffix}`!") + tf2gene_rank = gene2tf_rank.T + tf2gene_rank = tf2gene_rank.loc[ + np.unique(tf2gene_rank.index), np.unique(tf2gene_rank.columns) + ].astype(np.int16) + tf2gene_rank.index.name = "features" if version == 1 else "tracks" + tf2gene_rank.columns.name = None + columns = tf2gene_rank.columns.tolist() + tf2gene_rank = tf2gene_rank.reset_index() + if version == 2: + tf2gene_rank = tf2gene_rank.loc[:, [*columns, "tracks"]] + tf2gene_rank.to_feather(feather) + + +def read_ctx_grn(file: os.PathLike) -> nx.DiGraph: + r""" + Read pruned TF-target GRN as generated by ``pyscenic ctx`` + + Parameters + ---------- + file + Input file (.csv) + + Returns + ------- + grn + Pruned TF-target GRN + + Note + ---- + Node attribute "type" can be used to distinguish TFs and genes + """ + df = pd.read_csv( + file, header=None, skiprows=3, + usecols=[0, 8], names=["TF", "targets"] + ) + df["targets"] = df["targets"].map(lambda x: set(i[0] for i in literal_eval(x))) + df = df.groupby("TF").aggregate({"targets": lambda x: reduce(set.union, x)}) + grn = nx.DiGraph([ + (tf, target) + for tf, row in df.iterrows() + for target in row["targets"]] + ) + nx.set_node_attributes(grn, "target", name="type") + for tf in df.index: + grn.nodes[tf]["target"] = "TF" + return grn + + +def get_chr_len_from_fai(fai: os.PathLike) -> Mapping[str, int]: + r""" + Get chromosome length information from fasta index file + + Parameters + ---------- + fai + Fasta index file + + Returns + ------- + chr_len + Length of each chromosome + """ + return pd.read_table(fai, header=None, index_col=0)[1].to_dict() + + +def ens_trim_version(x: str) -> str: + r""" + Trim version suffix from Ensembl ID + + Parameters + ---------- + x + Ensembl ID + + Returns + ------- + trimmed + Ensembl ID with version suffix trimmed + """ + return re.sub(r"\.[0-9_-]+$", "", x) + +# Function for DIY guidance graph +def generate_prot_guidance_graph(rna: AnnData, + prot: AnnData, + protein_gene_match: Mapping[str, str]): + + r""" + Generate the guidance graph based on CITE-seq datasets. + + Parameters + ---------- + rna + AnnData with gene expression information. + prot + AnnData with protein expression information. + protein_gene_match + The dictionary used to match proteins with genes. + + Returns + ------- + guidance + The guidance map between proteins and genes. + """ + guidance =nx.MultiDiGraph() + for k, v in protein_gene_match.items(): + guidance.add_edge(k, v, weight=1.0, sign=1, type="rev") + guidance.add_edge(v, k, weight=1.0, sign=1, type="fwd") + + for item in rna.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") + for item in prot.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") + + + return guidance + + +# Aliases +read_bed = Bed.read_bed +read_gtf = Gtf.read_gtf diff --git a/.history/scglue/genomics_20240223082707.py b/.history/scglue/genomics_20240223082707.py new file mode 100644 index 0000000..8687b24 --- /dev/null +++ b/.history/scglue/genomics_20240223082707.py @@ -0,0 +1,943 @@ +r""" +Genomics operations +""" + +import collections +import os +import re +from ast import literal_eval +from functools import reduce +from itertools import chain, product +from operator import add +from typing import Any, Callable, List, Mapping, Optional, Union + +import networkx as nx +import numpy as np +import pandas as pd +import pybedtools +import scipy.sparse +import scipy.stats +from anndata import AnnData +from networkx.algorithms.bipartite import biadjacency_matrix +from pybedtools import BedTool +from pybedtools.cbedtools import Interval +from statsmodels.stats.multitest import fdrcorrection +from tqdm.auto import tqdm + +from .check import check_deps +from .graph import compose_multigraph, reachable_vertices +from .typehint import RandomState +from .utils import ConstrainedDataFrame, logged, get_rs + + +class Bed(ConstrainedDataFrame): + + r""" + BED format data frame + """ + + COLUMNS = pd.Index([ + "chrom", "chromStart", "chromEnd", "name", "score", + "strand", "thickStart", "thickEnd", "itemRgb", + "blockCount", "blockSizes", "blockStarts" + ]) + + @classmethod + def rectify(cls, df: pd.DataFrame) -> pd.DataFrame: + df = super(Bed, cls).rectify(df) + COLUMNS = cls.COLUMNS.copy(deep=True) + for item in COLUMNS: + if item in df: + if item in ("chromStart", "chromEnd"): + df[item] = df[item].astype(int) + else: + df[item] = df[item].astype(str) + elif item not in ("chrom", "chromStart", "chromEnd"): + df[item] = "." + else: + raise ValueError(f"Required column {item} is missing!") + return df.loc[:, COLUMNS] + + @classmethod + def verify(cls, df: pd.DataFrame) -> None: + super(Bed, cls).verify(df) + if len(df.columns) != len(cls.COLUMNS) or np.any(df.columns != cls.COLUMNS): + raise ValueError("Invalid BED format!") + + @classmethod + def read_bed(cls, fname: os.PathLike) -> "Bed": + r""" + Read BED file + + Parameters + ---------- + fname + BED file + + Returns + ------- + bed + Loaded :class:`Bed` object + """ + COLUMNS = cls.COLUMNS.copy(deep=True) + loaded = pd.read_csv(fname, sep="\t", header=None, comment="#") + loaded.columns = COLUMNS[:loaded.shape[1]] + return cls(loaded) + + def write_bed(self, fname: os.PathLike, ncols: Optional[int] = None) -> None: + r""" + Write BED file + + Parameters + ---------- + fname + BED file + ncols + Number of columns to write (by default write all columns) + """ + if ncols and ncols < 3: + raise ValueError("`ncols` must be larger than 3!") + df = self.df.iloc[:, :ncols] if ncols else self + df.to_csv(fname, sep="\t", header=False, index=False) + + def to_bedtool(self) -> pybedtools.BedTool: + r""" + Convert to a :class:`pybedtools.BedTool` object + + Returns + ------- + bedtool + Converted :class:`pybedtools.BedTool` object + """ + return BedTool(Interval( + row["chrom"], row["chromStart"], row["chromEnd"], + name=row["name"], score=row["score"], strand=row["strand"] + ) for _, row in self.iterrows()) + + def nucleotide_content(self, fasta: os.PathLike) -> pd.DataFrame: + r""" + Compute nucleotide content in the BED regions + + Parameters + ---------- + fasta + Genomic sequence file in FASTA format + + Returns + ------- + nucleotide_stat + Data frame containing nucleotide content statistics for each region + """ + result = self.to_bedtool().nucleotide_content(fi=os.fspath(fasta), s=True) # pylint: disable=unexpected-keyword-arg + result = pd.DataFrame( + np.stack([interval.fields[6:15] for interval in result]), + columns=[ + r"%AT", r"%GC", + r"#A", r"#C", r"#G", r"#T", r"#N", + r"#other", r"length" + ] + ).astype({ + r"%AT": float, r"%GC": float, + r"#A": int, r"#C": int, r"#G": int, r"#T": int, r"#N": int, + r"#other": int, r"length": int + }) + pybedtools.cleanup() + return result + + def strand_specific_start_site(self) -> "Bed": + r""" + Convert to strand-specific start sites of genomic features + + Returns + ------- + start_site_bed + A new :class:`Bed` object, containing strand-specific start sites + of the current :class:`Bed` object + """ + if set(self["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + df = pd.DataFrame(self, copy=True) + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + df.loc[pos_strand, "chromEnd"] = df.loc[pos_strand, "chromStart"] + 1 + df.loc[neg_strand, "chromStart"] = df.loc[neg_strand, "chromEnd"] - 1 + return type(self)(df) + + def strand_specific_end_site(self) -> "Bed": + r""" + Convert to strand-specific end sites of genomic features + + Returns + ------- + end_site_bed + A new :class:`Bed` object, containing strand-specific end sites + of the current :class:`Bed` object + """ + if set(self["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + df = pd.DataFrame(self, copy=True) + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + df.loc[pos_strand, "chromStart"] = df.loc[pos_strand, "chromEnd"] - 1 + df.loc[neg_strand, "chromEnd"] = df.loc[neg_strand, "chromStart"] + 1 + return type(self)(df) + + def expand( + self, upstream: int, downstream: int, + chr_len: Optional[Mapping[str, int]] = None + ) -> "Bed": + r""" + Expand genomic features towards upstream and downstream + + Parameters + ---------- + upstream + Number of bps to expand in the upstream direction + downstream + Number of bps to expand in the downstream direction + chr_len + Length of each chromosome + + Returns + ------- + expanded_bed + A new :class:`Bed` object, containing expanded features + of the current :class:`Bed` object + + Note + ---- + Starting position < 0 after expansion is always trimmed. + Ending position exceeding chromosome length is trimed only if + ``chr_len`` is specified. + """ + if upstream == downstream == 0: + return self + df = pd.DataFrame(self, copy=True) + if upstream == downstream: # symmetric + df["chromStart"] -= upstream + df["chromEnd"] += downstream + else: # asymmetric + if set(df["strand"]) != set(["+", "-"]): + raise ValueError("Not all features are strand specific!") + pos_strand = df.query("strand == '+'").index + neg_strand = df.query("strand == '-'").index + if upstream: + df.loc[pos_strand, "chromStart"] -= upstream + df.loc[neg_strand, "chromEnd"] += upstream + if downstream: + df.loc[pos_strand, "chromEnd"] += downstream + df.loc[neg_strand, "chromStart"] -= downstream + df["chromStart"] = np.maximum(df["chromStart"], 0) + if chr_len: + chr_len = df["chrom"].map(chr_len) + df["chromEnd"] = np.minimum(df["chromEnd"], chr_len) + return type(self)(df) + + +class Gtf(ConstrainedDataFrame): # gffutils is too slow + + r""" + GTF format data frame + """ + + COLUMNS = pd.Index([ + "seqname", "source", "feature", "start", "end", + "score", "strand", "frame", "attribute" + ]) # Additional columns after "attribute" is allowed + + @classmethod + def rectify(cls, df: pd.DataFrame) -> pd.DataFrame: + df = super(Gtf, cls).rectify(df) + COLUMNS = cls.COLUMNS.copy(deep=True) + for item in COLUMNS: + if item in df: + if item in ("start", "end"): + df[item] = df[item].astype(int) + else: + df[item] = df[item].astype(str) + elif item not in ("seqname", "start", "end"): + df[item] = "." + else: + raise ValueError(f"Required column {item} is missing!") + return df.sort_index(axis=1, key=cls._column_key) + + @classmethod + def _column_key(cls, x: pd.Index) -> np.ndarray: + x = cls.COLUMNS.get_indexer(x) + x[x < 0] = x.max() + 1 # Put additional columns after "attribute" + return x + + @classmethod + def verify(cls, df: pd.DataFrame) -> None: + super(Gtf, cls).verify(df) + if len(df.columns) < len(cls.COLUMNS) or \ + np.any(df.columns[:len(cls.COLUMNS)] != cls.COLUMNS): + raise ValueError("Invalid GTF format!") + + @classmethod + def read_gtf(cls, fname: os.PathLike) -> "Gtf": + r""" + Read GTF file + + Parameters + ---------- + fname + GTF file + + Returns + ------- + gtf + Loaded :class:`Gtf` object + """ + COLUMNS = cls.COLUMNS.copy(deep=True) + loaded = pd.read_csv(fname, sep="\t", header=None, comment="#") + loaded.columns = COLUMNS[:loaded.shape[1]] + return cls(loaded) + + def split_attribute(self) -> "Gtf": + r""" + Extract all attributes from the "attribute" column + and append them to existing columns + + Returns + ------- + splitted + Gtf with splitted attribute columns appended + """ + pattern = re.compile(r'([^\s]+) "([^"]+)";') + splitted = pd.DataFrame.from_records(np.vectorize(lambda x: { + key: val for key, val in pattern.findall(x) + })(self["attribute"]), index=self.index) + if set(self.COLUMNS).intersection(splitted.columns): + self.logger.warning( + "Splitted attribute names overlap standard GTF fields! " + "The standard fields are overwritten!" + ) + return self.assign(**splitted) + + def to_bed(self, name: Optional[str] = None) -> Bed: + r""" + Convert GTF to BED format + + Parameters + ---------- + name + Specify a column to be converted to the "name" column in bed format, + otherwise the "name" column would be filled with "." + + Returns + ------- + bed + Converted :class:`Bed` object + """ + bed_df = pd.DataFrame(self, copy=True).loc[ + :, ("seqname", "start", "end", "score", "strand") + ] + bed_df.insert(3, "name", np.repeat( + ".", len(bed_df) + ) if name is None else self[name]) + bed_df["start"] -= 1 # Convert to zero-based + bed_df.columns = ( + "chrom", "chromStart", "chromEnd", "name", "score", "strand" + ) + return Bed(bed_df) + + +def interval_dist(x: Interval, y: Interval) -> int: + r""" + Compute distance and relative position between two bed intervals + + Parameters + ---------- + x + First interval + y + Second interval + + Returns + ------- + dist + Signed distance between ``x`` and ``y`` + """ + if x.chrom != y.chrom: + return np.inf * (-1 if x.chrom < y.chrom else 1) + if x.start < y.stop and y.start < x.stop: + return 0 + if x.stop <= y.start: + return x.stop - y.start - 1 + if y.stop <= x.start: + return x.start - y.stop + 1 + + +def window_graph( + left: Union[Bed, str], right: Union[Bed, str], window_size: int, + left_sorted: bool = False, right_sorted: bool = False, + attr_fn: Optional[Callable[[Interval, Interval, float], Mapping[str, Any]]] = None +) -> nx.MultiDiGraph: + r""" + Construct a window graph between two sets of genomic features, where + features pairs within a window size are connected. + + Parameters + ---------- + left + First feature set, either a :class:`Bed` object or path to a bed file + right + Second feature set, either a :class:`Bed` object or path to a bed file + window_size + Window size (in bp) + left_sorted + Whether ``left`` is already sorted + right_sorted + Whether ``right`` is already sorted + attr_fn + Function to compute edge attributes for connected features, + should accept the following three positional arguments: + + - l: left interval + - r: right interval + - d: signed distance between the intervals + + By default no edge attribute is created. + + Returns + ------- + graph + Window graph + """ + check_deps("bedtools") + if isinstance(left, Bed): + pbar_total = len(left) + left = left.to_bedtool() + else: + pbar_total = None + left = pybedtools.BedTool(left) + if not left_sorted: + left = left.sort(stream=True) + left = iter(left) # Resumable iterator + if isinstance(right, Bed): + right = right.to_bedtool() + else: + right = pybedtools.BedTool(right) + if not right_sorted: + right = right.sort(stream=True) + right = iter(right) # Resumable iterator + + attr_fn = attr_fn or (lambda l, r, d: {}) + if pbar_total is not None: + left = tqdm(left, total=pbar_total, desc="window_graph") + graph = nx.MultiDiGraph() + window = collections.OrderedDict() # Used as ordered set + for l in left: + for r in list(window.keys()): # Allow remove during iteration + d = interval_dist(l, r) + if -window_size <= d <= window_size: + graph.add_edge(l.name, r.name, **attr_fn(l, r, d)) + elif d > window_size: + del window[r] + else: # dist < -window_size + break # No need to expand window + else: + for r in right: # Resume from last break + d = interval_dist(l, r) + if -window_size <= d <= window_size: + graph.add_edge(l.name, r.name, **attr_fn(l, r, d)) + elif d > window_size: + continue + window[r] = None # Placeholder + if d < -window_size: + break + pybedtools.cleanup() + return graph + + +def dist_power_decay(x: int) -> float: + r""" + Distance-based power decay weight, computed as + :math:`w = {\left( \frac {d + 1000} {1000} \right)} ^ {-0.75}` + + Parameters + ---------- + x + Distance (in bp) + + Returns + ------- + weight + Decaying weight + """ + return ((x + 1000) / 1000) ** (-0.75) + + +@logged +def rna_anchored_guidance_graph( + rna: AnnData, *others: AnnData, + gene_region: str = "combined", promoter_len: int = 2000, + extend_range: int = 0, extend_fn: Callable[[int], float] = dist_power_decay, + signs: Optional[List[int]] = None, propagate_highly_variable: bool = True, + corrupt_rate: float = 0.0, random_state: RandomState = None +) -> nx.MultiDiGraph: + r""" + Build guidance graph anchored on RNA genes + + Parameters + ---------- + rna + Anchor RNA dataset + *others + Other datasets + gene_region + Defines the genomic region of genes, must be one of + ``{"gene_body", "promoter", "combined"}``. + promoter_len + Defines the length of gene promoters (bp upstream of TSS) + extend_range + Maximal extend distance beyond gene regions + extend_fn + Distance-decreasing weight function for the extended regions + (by default :func:`dist_power_decay`) + signs + Sign of edges between RNA genes and features in each ``*others`` + dataset, must have the same length as ``*others``. Signs must be + one of ``{-1, 1}``. By default, all edges have positive signs of ``1``. + propagate_highly_variable + Whether to propagate highly variable genes to other datasets, + datasets in ``*others`` would be modified in place. + corrupt_rate + **CAUTION: DO NOT USE**, only for evaluation purpose + random_state + **CAUTION: DO NOT USE**, only for evaluation purpose + + Returns + ------- + graph + Prior regulatory graph + + Note + ---- + In this function, features in the same dataset can only connect to + anchor genes via the same edge sign. For more flexibility, please + construct the guidance graph manually. + """ + signs = signs or [1] * len(others) + if len(others) != len(signs): + raise RuntimeError("Length of ``others`` and ``signs`` must match!") + if set(signs).difference({-1, 1}): + raise RuntimeError("``signs`` can only contain {-1, 1}!") + + rna_bed = Bed(rna.var.assign(name=rna.var_names)) + other_beds = [Bed(other.var.assign(name=other.var_names)) for other in others] + if gene_region == "promoter": + rna_bed = rna_bed.strand_specific_start_site().expand(promoter_len, 0) + elif gene_region == "combined": + rna_bed = rna_bed.expand(promoter_len, 0) + elif gene_region != "gene_body": + raise ValueError("Unrecognized `gene_range`!") + graphs = [window_graph( + rna_bed, other_bed, window_size=extend_range, + attr_fn=lambda l, r, d, s=sign: { + "dist": abs(d), "weight": extend_fn(abs(d)), "sign": s + } + ) for other_bed, sign in zip(other_beds, signs)] + graph = compose_multigraph(*graphs) + + corrupt_num = round(corrupt_rate * graph.number_of_edges()) + if corrupt_num: + rna_anchored_guidance_graph.logger.warning("Corrupting guidance graph!") + rs = get_rs(random_state) + rna_var_names = rna.var_names.tolist() + other_var_names = reduce(add, [other.var_names.tolist() for other in others]) + + corrupt_remove = set(rs.choice(graph.number_of_edges(), corrupt_num, replace=False)) + corrupt_remove = set(edge for i, edge in enumerate(graph.edges) if i in corrupt_remove) + corrupt_add = [] + while len(corrupt_add) < corrupt_num: + corrupt_add += [ + (u, v) for u, v in zip( + rs.choice(rna_var_names, corrupt_num - len(corrupt_add)), + rs.choice(other_var_names, corrupt_num - len(corrupt_add)) + ) if not graph.has_edge(u, v) + ] + + graph.add_edges_from([ + (add[0], add[1], graph.edges[remove]) + for add, remove in zip(corrupt_add, corrupt_remove) + ]) + graph.remove_edges_from(corrupt_remove) + + if propagate_highly_variable: + hvg_reachable = reachable_vertices(graph, rna.var.query("highly_variable").index) + for other in others: + other.var["highly_variable"] = [ + item in hvg_reachable for item in other.var_names + ] + + rgraph = graph.reverse() + nx.set_edge_attributes(graph, "fwd", name="type") + nx.set_edge_attributes(rgraph, "rev", name="type") + graph = compose_multigraph(graph, rgraph) + all_features = set(chain.from_iterable( + map(lambda x: x.var_names, [rna, *others]) + )) + for item in all_features: + graph.add_edge(item, item, weight=1.0, sign=1, type="loop") + return graph + + +@logged +def rna_anchored_prior_graph( + rna: AnnData, *others: AnnData, + gene_region: str = "combined", promoter_len: int = 2000, + extend_range: int = 0, extend_fn: Callable[[int], float] = dist_power_decay, + signs: Optional[List[int]] = None, propagate_highly_variable: bool = True, + corrupt_rate: float = 0.0, random_state: RandomState = None +) -> nx.MultiDiGraph: # pragma: no cover + r""" + Deprecated, please use :func:`rna_anchored_guidance_graph` instead + """ + rna_anchored_prior_graph.logger.warning( + "Deprecated, please use `rna_anchored_guidance_graph` instead!" + ) + return rna_anchored_guidance_graph( + rna, *others, gene_region=gene_region, promoter_len=promoter_len, + extend_range=extend_range, extend_fn=extend_fn, signs=signs, + propagate_highly_variable=propagate_highly_variable, + corrupt_rate=corrupt_rate, random_state=random_state + ) + + +def regulatory_inference( + features: pd.Index, feature_embeddings: Union[np.ndarray, List[np.ndarray]], + skeleton: nx.Graph, alternative: str = "two.sided", + random_state: RandomState = None +) -> nx.Graph: + r""" + Regulatory inference based on feature embeddings + + Parameters + ---------- + features + Feature names + feature_embeddings + List of feature embeddings from 1 or more models + skeleton + Skeleton graph + alternative + Alternative hypothesis, must be one of {"two.sided", "less", "greater"} + random_state + Random state + + Returns + ------- + regulatory_graph + Regulatory graph containing regulatory score ("score"), + *P*-value ("pval"), *Q*-value ("pval") as edge attributes + for feature pairs in the skeleton graph + """ + if isinstance(feature_embeddings, np.ndarray): + feature_embeddings = [feature_embeddings] + n_features = set(item.shape[0] for item in feature_embeddings) + if len(n_features) != 1: + raise ValueError("All feature embeddings must have the same number of rows!") + if n_features.pop() != features.shape[0]: + raise ValueError("Feature embeddings do not match the number of feature names!") + node_idx = features.get_indexer(skeleton.nodes) + features = features[node_idx] + feature_embeddings = [item[node_idx] for item in feature_embeddings] + + rs = get_rs(random_state) + vperm = np.stack([rs.permutation(item) for item in feature_embeddings], axis=1) + vperm = vperm / np.linalg.norm(vperm, axis=-1, keepdims=True) + v = np.stack(feature_embeddings, axis=1) + v = v / np.linalg.norm(v, axis=-1, keepdims=True) + + edgelist = nx.to_pandas_edgelist(skeleton) + source = features.get_indexer(edgelist["source"]) + target = features.get_indexer(edgelist["target"]) + fg, bg = [], [] + + for s, t in tqdm(zip(source, target), total=skeleton.number_of_edges(), desc="regulatory_inference"): + fg.append((v[s] * v[t]).sum(axis=1).mean()) + bg.append((vperm[s] * vperm[t]).sum(axis=1)) + edgelist["score"] = fg + + bg = np.sort(np.concatenate(bg)) + quantile = np.searchsorted(bg, fg) / bg.size + if alternative == "two.sided": + edgelist["pval"] = 2 * np.minimum(quantile, 1 - quantile) + elif alternative == "greater": + edgelist["pval"] = 1 - quantile + elif alternative == "less": + edgelist["pval"] = quantile + else: + raise ValueError("Unrecognized `alternative`!") + edgelist["qval"] = fdrcorrection(edgelist["pval"])[1] + return nx.from_pandas_edgelist(edgelist, edge_attr=True, create_using=type(skeleton)) + + +def write_links( + graph: nx.Graph, source: Bed, target: Bed, file: os.PathLike, + keep_attrs: Optional[List[str]] = None +) -> None: + r""" + Export regulatory graph into a links file + + Parameters + ---------- + graph + Regulatory graph + source + Genomic coordinates of source nodes + target + Genomic coordinates of target nodes + file + Output file + keep_attrs + A list of attributes to keep for each link + """ + nx.to_pandas_edgelist( + graph + ).merge( + source.df.iloc[:, :4], how="left", left_on="source", right_on="name" + ).merge( + target.df.iloc[:, :4], how="left", left_on="target", right_on="name" + ).loc[:, [ + "chrom_x", "chromStart_x", "chromEnd_x", + "chrom_y", "chromStart_y", "chromEnd_y", + *(keep_attrs or []) + ]].to_csv(file, sep="\t", index=False, header=False) + + +def cis_regulatory_ranking( + gene2region: nx.Graph, region2tf: nx.Graph, + genes: List[str], regions: List[str], tfs: List[str], + region_lens: Optional[List[int]] = None, n_samples: int = 1000, + random_state: RandomState = None +) -> pd.DataFrame: + r""" + Generate cis-regulatory ranking between genes and transcription factors + + Parameters + ---------- + gene2region + A graph connecting genes to cis-regulatory regions + region2tf + A graph connecting cis-regulatory regions to transcription factors + genes + A list of genes + tfs + A list of transcription factors + regions + A list of cis-regulatory regions + region_lens + Lengths of cis-regulatory regions + (if not provided, it is assumed that all regions have the same length) + n_samples + Number of random samples used to evaluate regulatory enrichment + (setting this to 0 disables enrichment evaluation) + random_state + Random state + + Returns + ------- + gene2tf_rank + Cis regulatory ranking between genes and transcription factors + """ + gene2region = biadjacency_matrix(gene2region, genes, regions, dtype=np.int16, weight=None) + region2tf = biadjacency_matrix(region2tf, regions, tfs, dtype=np.int16, weight=None) + + if n_samples: + region_lens = [1] * len(regions) if region_lens is None else region_lens + if len(region_lens) != len(regions): + raise ValueError("`region_lens` must have the same length as `regions`!") + region_bins = pd.qcut(region_lens, min(len(set(region_lens)), 500), duplicates="drop") + region_bins_lut = pd.RangeIndex(region_bins.size).groupby(region_bins) + + rs = get_rs(random_state) + row, col_rand, data = [], [], [] + lil = gene2region.tolil() + for r, (c, d) in tqdm( + enumerate(zip(lil.rows, lil.data)), + total=len(lil.rows), desc="cis_reg_ranking.sampling" + ): + if not c: # Empty row + continue + row.append(np.ones_like(c) * r) + col_rand.append(np.stack([ + rs.choice(region_bins_lut[region_bins[c_]], n_samples, replace=True) + for c_ in c + ], axis=0)) + data.append(d) + row = np.concatenate(row) + col_rand = np.concatenate(col_rand) + data = np.concatenate(data) + + gene2tf_obs = (gene2region @ region2tf).toarray() + gene2tf_rand = np.empty((len(genes), len(tfs), n_samples), dtype=np.int16) + for k in tqdm(range(n_samples), desc="cis_reg_ranking.mapping"): + gene2region_rand = scipy.sparse.coo_matrix(( + data, (row, col_rand[:, k]) + ), shape=(len(genes), len(regions))) + gene2tf_rand[:, :, k] = (gene2region_rand @ region2tf).toarray() + gene2tf_rand.sort(axis=2) + + gene2tf_enrich = np.empty_like(gene2tf_obs) + for i, j in product(range(len(genes)), range(len(tfs))): + if gene2tf_obs[i, j] == 0: + gene2tf_enrich[i, j] = 0 + continue + gene2tf_enrich[i, j] = np.searchsorted( + gene2tf_rand[i, j, :], gene2tf_obs[i, j], side="right" + ) + else: + gene2tf_enrich = (gene2region @ region2tf).toarray() + + return pd.DataFrame( + scipy.stats.rankdata(-gene2tf_enrich, axis=0), + index=genes, columns=tfs + ) + + +def write_scenic_feather( + gene2tf_rank: pd.DataFrame, feather: os.PathLike, + version: int = 2 +) -> None: + r""" + Write cis-regulatory ranking to a SCENIC-compatible feather file + + Parameters + ---------- + gene2tf_rank + Cis regulatory ranking between genes and transcription factors, + as generated by :func:`cis_reg_ranking` + feather + Path to the output feather file + version + SCENIC feather version + """ + if version not in {1, 2}: + raise ValueError("Unrecognized SCENIC feather version!") + if version == 2: + suffix = ".genes_vs_tracks.rankings.feather" + if not str(feather).endswith(suffix): + raise ValueError(f"Feather file name must end with `{suffix}`!") + tf2gene_rank = gene2tf_rank.T + tf2gene_rank = tf2gene_rank.loc[ + np.unique(tf2gene_rank.index), np.unique(tf2gene_rank.columns) + ].astype(np.int16) + tf2gene_rank.index.name = "features" if version == 1 else "tracks" + tf2gene_rank.columns.name = None + columns = tf2gene_rank.columns.tolist() + tf2gene_rank = tf2gene_rank.reset_index() + if version == 2: + tf2gene_rank = tf2gene_rank.loc[:, [*columns, "tracks"]] + tf2gene_rank.to_feather(feather) + + +def read_ctx_grn(file: os.PathLike) -> nx.DiGraph: + r""" + Read pruned TF-target GRN as generated by ``pyscenic ctx`` + + Parameters + ---------- + file + Input file (.csv) + + Returns + ------- + grn + Pruned TF-target GRN + + Note + ---- + Node attribute "type" can be used to distinguish TFs and genes + """ + df = pd.read_csv( + file, header=None, skiprows=3, + usecols=[0, 8], names=["TF", "targets"] + ) + df["targets"] = df["targets"].map(lambda x: set(i[0] for i in literal_eval(x))) + df = df.groupby("TF").aggregate({"targets": lambda x: reduce(set.union, x)}) + grn = nx.DiGraph([ + (tf, target) + for tf, row in df.iterrows() + for target in row["targets"]] + ) + nx.set_node_attributes(grn, "target", name="type") + for tf in df.index: + grn.nodes[tf]["target"] = "TF" + return grn + + +def get_chr_len_from_fai(fai: os.PathLike) -> Mapping[str, int]: + r""" + Get chromosome length information from fasta index file + + Parameters + ---------- + fai + Fasta index file + + Returns + ------- + chr_len + Length of each chromosome + """ + return pd.read_table(fai, header=None, index_col=0)[1].to_dict() + + +def ens_trim_version(x: str) -> str: + r""" + Trim version suffix from Ensembl ID + + Parameters + ---------- + x + Ensembl ID + + Returns + ------- + trimmed + Ensembl ID with version suffix trimmed + """ + return re.sub(r"\.[0-9_-]+$", "", x) + +# Function for DIY guidance graph +def generate_prot_guidance_graph(rna: AnnData, + prot: AnnData, + protein_gene_match: Mapping[str, str]): + + r""" + Generate the guidance graph based on CITE-seq datasets. + + Parameters + ---------- + rna + AnnData with gene expression information. + prot + AnnData with protein expression information. + protein_gene_match + The dictionary used to match proteins with genes. + + Returns + ------- + guidance + The guidance map between proteins and genes. + """ + guidance =nx.MultiDiGraph() + for k, v in protein_gene_match.items(): + guidance.add_edge(k, v, weight=1.0, sign=1, type="rev") + guidance.add_edge(v, k, weight=1.0, sign=1, type="fwd") + + for item in rna.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") + for item in prot.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") + + + return guidance + + +# Aliases +read_bed = Bed.read_bed +read_gtf = Gtf.read_gtf diff --git a/.history/scglue/models/prob_20240208234227.py b/.history/scglue/models/prob_20240208234227.py new file mode 100644 index 0000000..737bc0c --- /dev/null +++ b/.history/scglue/models/prob_20240208234227.py @@ -0,0 +1,220 @@ +r""" +Probability distributions +""" + +import torch +import torch.distributions as D +import torch.nn.functional as F + +from ..num import EPS + + +#-------------------------------- Distributions -------------------------------- + +class MSE(D.Distribution): + + r""" + A "sham" distribution that outputs negative MSE on ``log_prob`` + + Parameters + ---------- + loc + Mean of the distribution + """ + + def __init__(self, loc: torch.Tensor) -> None: + super().__init__(validate_args=False) + self.loc = loc + + def log_prob(self, value: torch.Tensor) -> None: + return -F.mse_loss(self.loc, value) + + @property + def mean(self) -> torch.Tensor: + return self.loc + + +class RMSE(MSE): + + r""" + A "sham" distribution that outputs negative RMSE on ``log_prob`` + + Parameters + ---------- + loc + Mean of the distribution + """ + + def log_prob(self, value: torch.Tensor) -> None: + return -F.mse_loss(self.loc, value).sqrt() + + +class ZIN(D.Normal): + + r""" + Zero-inflated normal distribution with subsetting support + + Parameters + ---------- + zi_logits + Zero-inflation logits + loc + Location of the normal distribution + scale + Scale of the normal distribution + """ + + def __init__( + self, zi_logits: torch.Tensor, + loc: torch.Tensor, scale: torch.Tensor + ) -> None: + super().__init__(loc, scale) + self.zi_logits = zi_logits + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + raw_log_prob = super().log_prob(value) + zi_log_prob = torch.empty_like(raw_log_prob) + z_mask = value.abs() < EPS + z_zi_logits, nz_zi_logits = self.zi_logits[z_mask], self.zi_logits[~z_mask] + zi_log_prob[z_mask] = ( + raw_log_prob[z_mask].exp() + z_zi_logits.exp() + EPS + ).log() - F.softplus(z_zi_logits) + zi_log_prob[~z_mask] = raw_log_prob[~z_mask] - F.softplus(nz_zi_logits) + return zi_log_prob + + +class ZILN(D.LogNormal): + + r""" + Zero-inflated log-normal distribution with subsetting support + + Parameters + ---------- + zi_logits + Zero-inflation logits + loc + Location of the log-normal distribution + scale + Scale of the log-normal distribution + """ + + def __init__( + self, zi_logits: torch.Tensor, + loc: torch.Tensor, scale: torch.Tensor + ) -> None: + super().__init__(loc, scale) + self.zi_logits = zi_logits + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + zi_log_prob = torch.empty_like(value) + z_mask = value.abs() < EPS + z_zi_logits, nz_zi_logits = self.zi_logits[z_mask], self.zi_logits[~z_mask] + zi_log_prob[z_mask] = z_zi_logits - F.softplus(z_zi_logits) + zi_log_prob[~z_mask] = D.LogNormal( + self.loc[~z_mask], self.scale[~z_mask] + ).log_prob(value[~z_mask]) - F.softplus(nz_zi_logits) + return zi_log_prob + + +class ZINB(D.NegativeBinomial): + + r""" + Zero-inflated negative binomial distribution + + Parameters + ---------- + zi_logits + Zero-inflation logits + total_count + Total count of the negative binomial distribution + logits + Logits of the negative binomial distribution + """ + + def __init__( + self, zi_logits: torch.Tensor, + total_count: torch.Tensor, logits: torch.Tensor = None + ) -> None: + super().__init__(total_count, logits=logits) + self.zi_logits = zi_logits + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + raw_log_prob = super().log_prob(value) + zi_log_prob = torch.empty_like(raw_log_prob) + z_mask = value.abs() < EPS + z_zi_logits, nz_zi_logits = self.zi_logits[z_mask], self.zi_logits[~z_mask] + zi_log_prob[z_mask] = ( + raw_log_prob[z_mask].exp() + z_zi_logits.exp() + EPS + ).log() - F.softplus(z_zi_logits) + zi_log_prob[~z_mask] = raw_log_prob[~z_mask] - F.softplus(nz_zi_logits) + return zi_log_prob + + +class NBMixture(D.NegativeBinomial): + + r""" + Zero-inflated negative binomial distribution + + Parameters + ---------- + zi_logits + Zero-inflation logits + total_count + Total count of the negative binomial distribution + logits + Logits of the negative binomial distribution + """ + + def __init__( + self, + mu_1: torch.Tensor, + mu_2: torch.Tensor, + theta_1: torch.Tensor, + theta_2: torch.Tensor, + eps=1e-8, + logits: torch.Tensor = None + ) -> None: + super().__init__(logits=logits) + self.mu_1 = mu_1 + self.mu_2 = mu_2 + self.theta_1 = theta_1 + self.theta_2 = theta_2 + self.eps = eps + self.logits = logits + + + def log_prob(self, value: torch.Tensor + ) -> torch.Tensor: + theta = self.theta_1 + if theta.ndimension() == 1: + theta = theta.view( + 1, theta.size(0) + ) # In this case, we reshape theta for broadcasting + + log_theta_mu_1_eps = torch.log(theta + self.mu_1 + self.eps) + log_theta_mu_2_eps = torch.log(theta + self.mu_2 + self.eps) + lgamma_x_theta = torch.lgamma(value + theta) + lgamma_theta = torch.lgamma(theta) + lgamma_x_plus_1 = torch.lgamma(value + 1) + + log_nb_1 = ( + theta * (torch.log(theta + self.eps) - log_theta_mu_1_eps) + + value * (torch.log(self.mu_1 + self.eps) - log_theta_mu_1_eps) + + lgamma_x_theta + - lgamma_theta + - lgamma_x_plus_1 + ) + log_nb_2 = ( + theta * (torch.log(theta + self.eps) - log_theta_mu_2_eps) + + value * (torch.log(self.mu_2 + self.eps) - log_theta_mu_2_eps) + + lgamma_x_theta + - lgamma_theta + - lgamma_x_plus_1 + ) + + logsumexp = torch.logsumexp(torch.stack((log_nb_1, log_nb_2 - self.logits)), dim=0) + softplus_pi = F.softplus(-self.logits) + + log_mixture_nb = logsumexp - softplus_pi + + return log_mixture_nb \ No newline at end of file diff --git a/.history/scglue/models/prob_20240223082307.py b/.history/scglue/models/prob_20240223082307.py new file mode 100644 index 0000000..46a0474 --- /dev/null +++ b/.history/scglue/models/prob_20240223082307.py @@ -0,0 +1,151 @@ +r""" +Probability distributions +""" + +import torch +import torch.distributions as D +import torch.nn.functional as F + +from ..num import EPS + + +#-------------------------------- Distributions -------------------------------- + +class MSE(D.Distribution): + + r""" + A "sham" distribution that outputs negative MSE on ``log_prob`` + + Parameters + ---------- + loc + Mean of the distribution + """ + + def __init__(self, loc: torch.Tensor) -> None: + super().__init__(validate_args=False) + self.loc = loc + + def log_prob(self, value: torch.Tensor) -> None: + return -F.mse_loss(self.loc, value) + + @property + def mean(self) -> torch.Tensor: + return self.loc + + +class RMSE(MSE): + + r""" + A "sham" distribution that outputs negative RMSE on ``log_prob`` + + Parameters + ---------- + loc + Mean of the distribution + """ + + def log_prob(self, value: torch.Tensor) -> None: + return -F.mse_loss(self.loc, value).sqrt() + + +class ZIN(D.Normal): + + r""" + Zero-inflated normal distribution with subsetting support + + Parameters + ---------- + zi_logits + Zero-inflation logits + loc + Location of the normal distribution + scale + Scale of the normal distribution + """ + + def __init__( + self, zi_logits: torch.Tensor, + loc: torch.Tensor, scale: torch.Tensor + ) -> None: + super().__init__(loc, scale) + self.zi_logits = zi_logits + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + raw_log_prob = super().log_prob(value) + zi_log_prob = torch.empty_like(raw_log_prob) + z_mask = value.abs() < EPS + z_zi_logits, nz_zi_logits = self.zi_logits[z_mask], self.zi_logits[~z_mask] + zi_log_prob[z_mask] = ( + raw_log_prob[z_mask].exp() + z_zi_logits.exp() + EPS + ).log() - F.softplus(z_zi_logits) + zi_log_prob[~z_mask] = raw_log_prob[~z_mask] - F.softplus(nz_zi_logits) + return zi_log_prob + + +class ZILN(D.LogNormal): + + r""" + Zero-inflated log-normal distribution with subsetting support + + Parameters + ---------- + zi_logits + Zero-inflation logits + loc + Location of the log-normal distribution + scale + Scale of the log-normal distribution + """ + + def __init__( + self, zi_logits: torch.Tensor, + loc: torch.Tensor, scale: torch.Tensor + ) -> None: + super().__init__(loc, scale) + self.zi_logits = zi_logits + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + zi_log_prob = torch.empty_like(value) + z_mask = value.abs() < EPS + z_zi_logits, nz_zi_logits = self.zi_logits[z_mask], self.zi_logits[~z_mask] + zi_log_prob[z_mask] = z_zi_logits - F.softplus(z_zi_logits) + zi_log_prob[~z_mask] = D.LogNormal( + self.loc[~z_mask], self.scale[~z_mask] + ).log_prob(value[~z_mask]) - F.softplus(nz_zi_logits) + return zi_log_prob + + +class ZINB(D.NegativeBinomial): + + r""" + Zero-inflated negative binomial distribution + + Parameters + ---------- + zi_logits + Zero-inflation logits + total_count + Total count of the negative binomial distribution + logits + Logits of the negative binomial distribution + """ + + def __init__( + self, zi_logits: torch.Tensor, + total_count: torch.Tensor, logits: torch.Tensor = None + ) -> None: + super().__init__(total_count, logits=logits) + self.zi_logits = zi_logits + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + raw_log_prob = super().log_prob(value) + zi_log_prob = torch.empty_like(raw_log_prob) + z_mask = value.abs() < EPS + z_zi_logits, nz_zi_logits = self.zi_logits[z_mask], self.zi_logits[~z_mask] + zi_log_prob[z_mask] = ( + raw_log_prob[z_mask].exp() + z_zi_logits.exp() + EPS + ).log() - F.softplus(z_zi_logits) + zi_log_prob[~z_mask] = raw_log_prob[~z_mask] - F.softplus(nz_zi_logits) + return zi_log_prob + diff --git a/.history/scglue/models/sc_20240208224727.py b/.history/scglue/models/sc_20240208224727.py new file mode 100644 index 0000000..5ce56cb --- /dev/null +++ b/.history/scglue/models/sc_20240208224727.py @@ -0,0 +1,638 @@ +r""" +GLUE component modules for single-cell omics data +""" + +import collections +from abc import abstractmethod +from typing import Optional, Tuple + +import torch +import torch.distributions as D +import torch.nn.functional as F + +from ..num import EPS +from . import glue +from .nn import GraphConv +from .prob import ZILN, ZIN, ZINB + + +#-------------------------- Network modules for GLUE --------------------------- + +class GraphEncoder(glue.GraphEncoder): + + r""" + Graph encoder + + Parameters + ---------- + vnum + Number of vertices + out_features + Output dimensionality + """ + + def __init__( + self, vnum: int, out_features: int + ) -> None: + super().__init__() + self.vrepr = torch.nn.Parameter(torch.zeros(vnum, out_features)) + self.conv = GraphConv() + self.loc = torch.nn.Linear(out_features, out_features) + self.std_lin = torch.nn.Linear(out_features, out_features) + + def forward( + self, eidx: torch.Tensor, enorm: torch.Tensor, esgn: torch.Tensor + ) -> D.Normal: + ptr = self.conv(self.vrepr, eidx, enorm, esgn) + loc = self.loc(ptr) + std = F.softplus(self.std_lin(ptr)) + EPS + return D.Normal(loc, std) + + +class GraphDecoder(glue.GraphDecoder): + + r""" + Graph decoder + """ + + def forward( + self, v: torch.Tensor, eidx: torch.Tensor, esgn: torch.Tensor + ) -> D.Bernoulli: + sidx, tidx = eidx # Source index and target index + logits = esgn * (v[sidx] * v[tidx]).sum(dim=1) + return D.Bernoulli(logits=logits) + + +class DataEncoder(glue.DataEncoder): + + r""" + Abstract data encoder + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + def __init__( + self, in_features: int, out_features: int, + h_depth: int = 2, h_dim: int = 256, + dropout: float = 0.2 + ) -> None: + super().__init__() + self.h_depth = h_depth + ptr_dim = in_features + for layer in range(self.h_depth): + setattr(self, f"linear_{layer}", torch.nn.Linear(ptr_dim, h_dim)) + setattr(self, f"act_{layer}", torch.nn.LeakyReLU(negative_slope=0.2)) + setattr(self, f"bn_{layer}", torch.nn.BatchNorm1d(h_dim)) + setattr(self, f"dropout_{layer}", torch.nn.Dropout(p=dropout)) + ptr_dim = h_dim + self.loc = torch.nn.Linear(ptr_dim, out_features) + self.std_lin = torch.nn.Linear(ptr_dim, out_features) + + @abstractmethod + def compute_l(self, x: torch.Tensor) -> Optional[torch.Tensor]: + r""" + Compute normalizer + + Parameters + ---------- + x + Input data + + Returns + ------- + l + Normalizer + """ + raise NotImplementedError # pragma: no cover + + @abstractmethod + def normalize( + self, x: torch.Tensor, l: Optional[torch.Tensor] + ) -> torch.Tensor: + r""" + Normalize data + + Parameters + ---------- + x + Input data + l + Normalizer + + Returns + ------- + xnorm + Normalized data + """ + raise NotImplementedError # pragma: no cover + + def forward( # pylint: disable=arguments-differ + self, x: torch.Tensor, xrep: torch.Tensor, + lazy_normalizer: bool = True + ) -> Tuple[D.Normal, Optional[torch.Tensor]]: + r""" + Encode data to sample latent distribution + + Parameters + ---------- + x + Input data + xrep + Alternative input data + lazy_normalizer + Whether to skip computing `x` normalizer (just return None) + if `xrep` is non-empty + + Returns + ------- + u + Sample latent distribution + normalizer + Data normalizer + + Note + ---- + Normalization is always computed on `x`. + If xrep is empty, the normalized `x` will be used as input + to the encoder neural network, otherwise xrep is used instead. + """ + if xrep.numel(): + l = None if lazy_normalizer else self.compute_l(x) + ptr = xrep + else: + l = self.compute_l(x) + ptr = self.normalize(x, l) + for layer in range(self.h_depth): + ptr = getattr(self, f"linear_{layer}")(ptr) + ptr = getattr(self, f"act_{layer}")(ptr) + ptr = getattr(self, f"bn_{layer}")(ptr) + ptr = getattr(self, f"dropout_{layer}")(ptr) + loc = self.loc(ptr) + std = F.softplus(self.std_lin(ptr)) + EPS + return D.Normal(loc, std), l + + +class VanillaDataEncoder(DataEncoder): + + r""" + Vanilla data encoder + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + def compute_l(self, x: torch.Tensor) -> Optional[torch.Tensor]: + return None + + def normalize( + self, x: torch.Tensor, l: Optional[torch.Tensor] + ) -> torch.Tensor: + return x + + +class NBDataEncoder(DataEncoder): + + r""" + Data encoder for negative binomial data + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + TOTAL_COUNT = 1e4 + + def compute_l(self, x: torch.Tensor) -> torch.Tensor: + return x.sum(dim=1, keepdim=True) + + def normalize( + self, x: torch.Tensor, l: torch.Tensor + ) -> torch.Tensor: + return (x * (self.TOTAL_COUNT / l)).log1p() + + +class DataDecoder(glue.DataDecoder): + + r""" + Abstract data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: # pylint: disable=unused-argument + super().__init__() + + @abstractmethod + def forward( # pylint: disable=arguments-differ + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> D.Normal: + r""" + Decode data from sample and feature latent + + Parameters + ---------- + u + Sample latent + v + Feature latent + b + Batch index + l + Optional normalizer + + Returns + ------- + recon + Data reconstruction distribution + """ + raise NotImplementedError # pragma: no cover + + +class NormalDataDecoder(DataDecoder): + + r""" + Normal data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.std_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> D.Normal: + scale = F.softplus(self.scale_lin[b]) + loc = scale * (u @ v.t()) + self.bias[b] + std = F.softplus(self.std_lin[b]) + EPS + return D.Normal(loc, std) + + +class ZINDataDecoder(NormalDataDecoder): + + r""" + Zero-inflated normal data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> ZIN: + scale = F.softplus(self.scale_lin[b]) + loc = scale * (u @ v.t()) + self.bias[b] + std = F.softplus(self.std_lin[b]) + EPS + return ZIN(self.zi_logits[b].expand_as(loc), loc, std) + + +class ZILNDataDecoder(DataDecoder): + + r""" + Zero-inflated log-normal data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.std_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> ZILN: + scale = F.softplus(self.scale_lin[b]) + loc = scale * (u @ v.t()) + self.bias[b] + std = F.softplus(self.std_lin[b]) + EPS + return ZILN(self.zi_logits[b].expand_as(loc), loc, std) + + +class NBDataDecoder(DataDecoder): + + r""" + Negative binomial data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.log_theta = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: torch.Tensor + ) -> D.NegativeBinomial: + scale = F.softplus(self.scale_lin[b]) + logit_mu = scale * (u @ v.t()) + self.bias[b] + mu = F.softmax(logit_mu, dim=1) * l + log_theta = self.log_theta[b] + return D.NegativeBinomial( + log_theta.exp(), + logits=(mu + EPS).log() - log_theta + ) + + +class NBMixtureDataDecoder(DataDecoder): + + r""" + Negative binomial data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias1 = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias2 = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.log_theta = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: torch.Tensor # l is sequencing depth + ) -> D.NegativeBinomial: + scale = F.softplus(self.scale_lin[b]) + logit_mu1 = scale * (u @ v.t()) + self.bias1[b] + logit_mu2 = scale * (u @ v.t()) + self.bias2[b] + + mu1 = F.softmax(logit_mu1, dim=1) + mu2 = F.softmax(logit_mu2, dim=1) + + # beta = self.zi_logits[b].expand_as(mu1) # to avoid negative value in the bernoulli distribution, we use l later. + v_s = torch.distributions.Bernoulli(mu1).sample() + mu_mixture = v_s* l + (1-v_s)*mu2* l # keep the same format with TOTALVI + # mu_mixture = v_s + (1-v_s)*mu2* l + # print(mu_mixture) + log_theta = self.log_theta[b] + return D.NegativeBinomial( + log_theta.exp(), + logits=(mu_mixture + EPS).log() - log_theta + ) + + +class ZINBDataDecoder(NBDataDecoder): + + r""" + Zero-inflated negative binomial data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> ZINB: + scale = F.softplus(self.scale_lin[b]) + logit_mu = scale * (u @ v.t()) + self.bias[b] + mu = F.softmax(logit_mu, dim=1) * l + log_theta = self.log_theta[b] + return ZINB( + self.zi_logits[b].expand_as(mu), + log_theta.exp(), + logits=(mu + EPS).log() - log_theta + ) + + +class Discriminator(torch.nn.Sequential, glue.Discriminator): + + r""" + Modality discriminator + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + def __init__( + self, in_features: int, out_features: int, n_batches: int = 0, + h_depth: int = 2, h_dim: Optional[int] = 256, + dropout: float = 0.2 + ) -> None: + self.n_batches = n_batches + od = collections.OrderedDict() + ptr_dim = in_features + self.n_batches + for layer in range(h_depth): + od[f"linear_{layer}"] = torch.nn.Linear(ptr_dim, h_dim) + od[f"act_{layer}"] = torch.nn.LeakyReLU(negative_slope=0.2) + od[f"dropout_{layer}"] = torch.nn.Dropout(p=dropout) + ptr_dim = h_dim + od["pred"] = torch.nn.Linear(ptr_dim, out_features) + super().__init__(od) + + def forward(self, x: torch.Tensor, b: torch.Tensor) -> torch.Tensor: # pylint: disable=arguments-differ + if self.n_batches: + b_one_hot = F.one_hot(b, num_classes=self.n_batches) + x = torch.cat([x, b_one_hot], dim=1) + return super().forward(x) + + +class Classifier(torch.nn.Linear): + + r""" + Linear label classifier + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + """ + + +class Prior(glue.Prior): + + r""" + Prior distribution + + Parameters + ---------- + loc + Mean of the normal distribution + std + Standard deviation of the normal distribution + """ + + def __init__( + self, loc: float = 0.0, std: float = 1.0 + ) -> None: + super().__init__() + loc = torch.as_tensor(loc, dtype=torch.get_default_dtype()) + std = torch.as_tensor(std, dtype=torch.get_default_dtype()) + self.register_buffer("loc", loc) + self.register_buffer("std", std) + + def forward(self) -> D.Normal: + return D.Normal(self.loc, self.std) + + +#-------------------- Network modules for independent GLUE --------------------- + +class IndDataDecoder(DataDecoder): + + r""" + Data decoder mixin that makes decoding independent of feature latent + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__( # pylint: disable=unused-argument + self, in_features: int, out_features: int, n_batches: int = 1 + ) -> None: + super().__init__(out_features, n_batches=n_batches) + self.v = torch.nn.Parameter(torch.zeros(out_features, in_features)) + + def forward( # pylint: disable=arguments-differ + self, u: torch.Tensor, b: torch.Tensor, + l: Optional[torch.Tensor] + ) -> D.Distribution: + r""" + Decode data from sample latent + + Parameters + ---------- + u + Sample latent + b + Batch index + l + Optional normalizer + + Returns + ------- + recon + Data reconstruction distribution + """ + return super().forward(u, self.v, b, l) + + +class IndNormalDataDocoder(IndDataDecoder, NormalDataDecoder): + r""" + Normal data decoder independent of feature latent + """ + + +class IndZINDataDecoder(IndDataDecoder, ZINDataDecoder): + r""" + Zero-inflated normal data decoder independent of feature latent + """ + + +class IndZILNDataDecoder(IndDataDecoder, ZILNDataDecoder): + r""" + Zero-inflated log-normal data decoder independent of feature latent + """ + + +class IndNBDataDecoder(IndDataDecoder, NBDataDecoder): + r""" + Negative binomial data decoder independent of feature latent + """ + + +class IndZINBDataDecoder(IndDataDecoder, ZINBDataDecoder): + r""" + Zero-inflated negative binomial data decoder independent of feature latent + """ diff --git a/.history/scglue/models/sc_20240223082240.py b/.history/scglue/models/sc_20240223082240.py new file mode 100644 index 0000000..aa626f6 --- /dev/null +++ b/.history/scglue/models/sc_20240223082240.py @@ -0,0 +1,639 @@ +r""" +GLUE component modules for single-cell omics data +""" + +import collections +from abc import abstractmethod +from typing import Optional, Tuple + +import torch +import torch.distributions as D +import torch.nn.functional as F + +from ..num import EPS +from . import glue +from .nn import GraphConv +from .prob import ZILN, ZIN, ZINB + + +#-------------------------- Network modules for GLUE --------------------------- + +class GraphEncoder(glue.GraphEncoder): + + r""" + Graph encoder + + Parameters + ---------- + vnum + Number of vertices + out_features + Output dimensionality + """ + + def __init__( + self, vnum: int, out_features: int + ) -> None: + super().__init__() + self.vrepr = torch.nn.Parameter(torch.zeros(vnum, out_features)) + self.conv = GraphConv() + self.loc = torch.nn.Linear(out_features, out_features) + self.std_lin = torch.nn.Linear(out_features, out_features) + + def forward( + self, eidx: torch.Tensor, enorm: torch.Tensor, esgn: torch.Tensor + ) -> D.Normal: + ptr = self.conv(self.vrepr, eidx, enorm, esgn) + loc = self.loc(ptr) + std = F.softplus(self.std_lin(ptr)) + EPS + return D.Normal(loc, std) + + +class GraphDecoder(glue.GraphDecoder): + + r""" + Graph decoder + """ + + def forward( + self, v: torch.Tensor, eidx: torch.Tensor, esgn: torch.Tensor + ) -> D.Bernoulli: + sidx, tidx = eidx # Source index and target index + logits = esgn * (v[sidx] * v[tidx]).sum(dim=1) + return D.Bernoulli(logits=logits) + + +class DataEncoder(glue.DataEncoder): + + r""" + Abstract data encoder + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + def __init__( + self, in_features: int, out_features: int, + h_depth: int = 2, h_dim: int = 256, + dropout: float = 0.2 + ) -> None: + super().__init__() + self.h_depth = h_depth + ptr_dim = in_features + for layer in range(self.h_depth): + setattr(self, f"linear_{layer}", torch.nn.Linear(ptr_dim, h_dim)) + setattr(self, f"act_{layer}", torch.nn.LeakyReLU(negative_slope=0.2)) + setattr(self, f"bn_{layer}", torch.nn.BatchNorm1d(h_dim)) + setattr(self, f"dropout_{layer}", torch.nn.Dropout(p=dropout)) + ptr_dim = h_dim + self.loc = torch.nn.Linear(ptr_dim, out_features) + self.std_lin = torch.nn.Linear(ptr_dim, out_features) + + @abstractmethod + def compute_l(self, x: torch.Tensor) -> Optional[torch.Tensor]: + r""" + Compute normalizer + + Parameters + ---------- + x + Input data + + Returns + ------- + l + Normalizer + """ + raise NotImplementedError # pragma: no cover + + @abstractmethod + def normalize( + self, x: torch.Tensor, l: Optional[torch.Tensor] + ) -> torch.Tensor: + r""" + Normalize data + + Parameters + ---------- + x + Input data + l + Normalizer + + Returns + ------- + xnorm + Normalized data + """ + raise NotImplementedError # pragma: no cover + + def forward( # pylint: disable=arguments-differ + self, x: torch.Tensor, xrep: torch.Tensor, + lazy_normalizer: bool = True + ) -> Tuple[D.Normal, Optional[torch.Tensor]]: + r""" + Encode data to sample latent distribution + + Parameters + ---------- + x + Input data + xrep + Alternative input data + lazy_normalizer + Whether to skip computing `x` normalizer (just return None) + if `xrep` is non-empty + + Returns + ------- + u + Sample latent distribution + normalizer + Data normalizer + + Note + ---- + Normalization is always computed on `x`. + If xrep is empty, the normalized `x` will be used as input + to the encoder neural network, otherwise xrep is used instead. + """ + if xrep.numel(): + l = None if lazy_normalizer else self.compute_l(x) + ptr = xrep + else: + l = self.compute_l(x) + ptr = self.normalize(x, l) + for layer in range(self.h_depth): + ptr = getattr(self, f"linear_{layer}")(ptr) + ptr = getattr(self, f"act_{layer}")(ptr) + ptr = getattr(self, f"bn_{layer}")(ptr) + ptr = getattr(self, f"dropout_{layer}")(ptr) + loc = self.loc(ptr) + std = F.softplus(self.std_lin(ptr)) + EPS + return D.Normal(loc, std), l + + +class VanillaDataEncoder(DataEncoder): + + r""" + Vanilla data encoder + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + def compute_l(self, x: torch.Tensor) -> Optional[torch.Tensor]: + return None + + def normalize( + self, x: torch.Tensor, l: Optional[torch.Tensor] + ) -> torch.Tensor: + return x + + +class NBDataEncoder(DataEncoder): + + r""" + Data encoder for negative binomial data + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + TOTAL_COUNT = 1e4 + + def compute_l(self, x: torch.Tensor) -> torch.Tensor: + return x.sum(dim=1, keepdim=True) + + def normalize( + self, x: torch.Tensor, l: torch.Tensor + ) -> torch.Tensor: + return (x * (self.TOTAL_COUNT / l)).log1p() + + +class DataDecoder(glue.DataDecoder): + + r""" + Abstract data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: # pylint: disable=unused-argument + super().__init__() + + @abstractmethod + def forward( # pylint: disable=arguments-differ + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> D.Normal: + r""" + Decode data from sample and feature latent + + Parameters + ---------- + u + Sample latent + v + Feature latent + b + Batch index + l + Optional normalizer + + Returns + ------- + recon + Data reconstruction distribution + """ + raise NotImplementedError # pragma: no cover + + +class NormalDataDecoder(DataDecoder): + + r""" + Normal data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.std_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> D.Normal: + scale = F.softplus(self.scale_lin[b]) + loc = scale * (u @ v.t()) + self.bias[b] + std = F.softplus(self.std_lin[b]) + EPS + return D.Normal(loc, std) + + +class ZINDataDecoder(NormalDataDecoder): + + r""" + Zero-inflated normal data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> ZIN: + scale = F.softplus(self.scale_lin[b]) + loc = scale * (u @ v.t()) + self.bias[b] + std = F.softplus(self.std_lin[b]) + EPS + return ZIN(self.zi_logits[b].expand_as(loc), loc, std) + + +class ZILNDataDecoder(DataDecoder): + + r""" + Zero-inflated log-normal data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.std_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> ZILN: + scale = F.softplus(self.scale_lin[b]) + loc = scale * (u @ v.t()) + self.bias[b] + std = F.softplus(self.std_lin[b]) + EPS + return ZILN(self.zi_logits[b].expand_as(loc), loc, std) + + +class NBDataDecoder(DataDecoder): + + r""" + Negative binomial data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.log_theta = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: torch.Tensor + ) -> D.NegativeBinomial: + scale = F.softplus(self.scale_lin[b]) + logit_mu = scale * (u @ v.t()) + self.bias[b] + mu = F.softmax(logit_mu, dim=1) * l + log_theta = self.log_theta[b] + return D.NegativeBinomial( + log_theta.exp(), + logits=(mu + EPS).log() - log_theta + ) + + +class NBMixtureDataDecoder(DataDecoder): + + r""" + The Mixture of negative binomial data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias1 = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias2 = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.log_theta = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: torch.Tensor # l is sequencing depth + ) -> D.MixtureSameFamily: + # print(b) + scale = F.softplus(self.scale_lin[b]) + logit_mu1 = scale * (u @ v.t()) + self.bias1[b] + logit_mu2 = scale * (u @ v.t()) + self.bias2[b] + + mu1 = F.softmax(logit_mu1, dim=1) + mu2 = F.softmax(logit_mu2, dim=1) + + log_theta = self.log_theta[b] + log_theta = torch.stack([log_theta,log_theta], axis=-1) + + mix = D.Categorical(logits=torch.stack([logit_mu1, logit_mu2], axis=-1)) + + mu = torch.stack([mu1*l, mu2*l], axis=-1) + + comp = D.NegativeBinomial(log_theta.exp(), logits=(mu + EPS).log() - log_theta) + + return D.MixtureSameFamily(mix, comp) + + +class ZINBDataDecoder(NBDataDecoder): + + r""" + Zero-inflated negative binomial data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> ZINB: + scale = F.softplus(self.scale_lin[b]) + logit_mu = scale * (u @ v.t()) + self.bias[b] + mu = F.softmax(logit_mu, dim=1) * l + log_theta = self.log_theta[b] + return ZINB( + self.zi_logits[b].expand_as(mu), + log_theta.exp(), + logits=(mu + EPS).log() - log_theta + ) + + +class Discriminator(torch.nn.Sequential, glue.Discriminator): + + r""" + Modality discriminator + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + def __init__( + self, in_features: int, out_features: int, n_batches: int = 0, + h_depth: int = 2, h_dim: Optional[int] = 256, + dropout: float = 0.2 + ) -> None: + self.n_batches = n_batches + od = collections.OrderedDict() + ptr_dim = in_features + self.n_batches + for layer in range(h_depth): + od[f"linear_{layer}"] = torch.nn.Linear(ptr_dim, h_dim) + od[f"act_{layer}"] = torch.nn.LeakyReLU(negative_slope=0.2) + od[f"dropout_{layer}"] = torch.nn.Dropout(p=dropout) + ptr_dim = h_dim + od["pred"] = torch.nn.Linear(ptr_dim, out_features) + super().__init__(od) + + def forward(self, x: torch.Tensor, b: torch.Tensor) -> torch.Tensor: # pylint: disable=arguments-differ + if self.n_batches: + b_one_hot = F.one_hot(b, num_classes=self.n_batches) + x = torch.cat([x, b_one_hot], dim=1) + return super().forward(x) + + +class Classifier(torch.nn.Linear): + + r""" + Linear label classifier + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + """ + + +class Prior(glue.Prior): + + r""" + Prior distribution + + Parameters + ---------- + loc + Mean of the normal distribution + std + Standard deviation of the normal distribution + """ + + def __init__( + self, loc: float = 0.0, std: float = 1.0 + ) -> None: + super().__init__() + loc = torch.as_tensor(loc, dtype=torch.get_default_dtype()) + std = torch.as_tensor(std, dtype=torch.get_default_dtype()) + self.register_buffer("loc", loc) + self.register_buffer("std", std) + + def forward(self) -> D.Normal: + return D.Normal(self.loc, self.std) + + +#-------------------- Network modules for independent GLUE --------------------- + +class IndDataDecoder(DataDecoder): + + r""" + Data decoder mixin that makes decoding independent of feature latent + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__( # pylint: disable=unused-argument + self, in_features: int, out_features: int, n_batches: int = 1 + ) -> None: + super().__init__(out_features, n_batches=n_batches) + self.v = torch.nn.Parameter(torch.zeros(out_features, in_features)) + + def forward( # pylint: disable=arguments-differ + self, u: torch.Tensor, b: torch.Tensor, + l: Optional[torch.Tensor] + ) -> D.Distribution: + r""" + Decode data from sample latent + + Parameters + ---------- + u + Sample latent + b + Batch index + l + Optional normalizer + + Returns + ------- + recon + Data reconstruction distribution + """ + return super().forward(u, self.v, b, l) + + +class IndNormalDataDocoder(IndDataDecoder, NormalDataDecoder): + r""" + Normal data decoder independent of feature latent + """ + + +class IndZINDataDecoder(IndDataDecoder, ZINDataDecoder): + r""" + Zero-inflated normal data decoder independent of feature latent + """ + + +class IndZILNDataDecoder(IndDataDecoder, ZILNDataDecoder): + r""" + Zero-inflated log-normal data decoder independent of feature latent + """ + + +class IndNBDataDecoder(IndDataDecoder, NBDataDecoder): + r""" + Negative binomial data decoder independent of feature latent + """ + + +class IndZINBDataDecoder(IndDataDecoder, ZINBDataDecoder): + r""" + Zero-inflated negative binomial data decoder independent of feature latent + """ diff --git a/.history/scglue/models/sc_20240223082318.py b/.history/scglue/models/sc_20240223082318.py new file mode 100644 index 0000000..aa626f6 --- /dev/null +++ b/.history/scglue/models/sc_20240223082318.py @@ -0,0 +1,639 @@ +r""" +GLUE component modules for single-cell omics data +""" + +import collections +from abc import abstractmethod +from typing import Optional, Tuple + +import torch +import torch.distributions as D +import torch.nn.functional as F + +from ..num import EPS +from . import glue +from .nn import GraphConv +from .prob import ZILN, ZIN, ZINB + + +#-------------------------- Network modules for GLUE --------------------------- + +class GraphEncoder(glue.GraphEncoder): + + r""" + Graph encoder + + Parameters + ---------- + vnum + Number of vertices + out_features + Output dimensionality + """ + + def __init__( + self, vnum: int, out_features: int + ) -> None: + super().__init__() + self.vrepr = torch.nn.Parameter(torch.zeros(vnum, out_features)) + self.conv = GraphConv() + self.loc = torch.nn.Linear(out_features, out_features) + self.std_lin = torch.nn.Linear(out_features, out_features) + + def forward( + self, eidx: torch.Tensor, enorm: torch.Tensor, esgn: torch.Tensor + ) -> D.Normal: + ptr = self.conv(self.vrepr, eidx, enorm, esgn) + loc = self.loc(ptr) + std = F.softplus(self.std_lin(ptr)) + EPS + return D.Normal(loc, std) + + +class GraphDecoder(glue.GraphDecoder): + + r""" + Graph decoder + """ + + def forward( + self, v: torch.Tensor, eidx: torch.Tensor, esgn: torch.Tensor + ) -> D.Bernoulli: + sidx, tidx = eidx # Source index and target index + logits = esgn * (v[sidx] * v[tidx]).sum(dim=1) + return D.Bernoulli(logits=logits) + + +class DataEncoder(glue.DataEncoder): + + r""" + Abstract data encoder + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + def __init__( + self, in_features: int, out_features: int, + h_depth: int = 2, h_dim: int = 256, + dropout: float = 0.2 + ) -> None: + super().__init__() + self.h_depth = h_depth + ptr_dim = in_features + for layer in range(self.h_depth): + setattr(self, f"linear_{layer}", torch.nn.Linear(ptr_dim, h_dim)) + setattr(self, f"act_{layer}", torch.nn.LeakyReLU(negative_slope=0.2)) + setattr(self, f"bn_{layer}", torch.nn.BatchNorm1d(h_dim)) + setattr(self, f"dropout_{layer}", torch.nn.Dropout(p=dropout)) + ptr_dim = h_dim + self.loc = torch.nn.Linear(ptr_dim, out_features) + self.std_lin = torch.nn.Linear(ptr_dim, out_features) + + @abstractmethod + def compute_l(self, x: torch.Tensor) -> Optional[torch.Tensor]: + r""" + Compute normalizer + + Parameters + ---------- + x + Input data + + Returns + ------- + l + Normalizer + """ + raise NotImplementedError # pragma: no cover + + @abstractmethod + def normalize( + self, x: torch.Tensor, l: Optional[torch.Tensor] + ) -> torch.Tensor: + r""" + Normalize data + + Parameters + ---------- + x + Input data + l + Normalizer + + Returns + ------- + xnorm + Normalized data + """ + raise NotImplementedError # pragma: no cover + + def forward( # pylint: disable=arguments-differ + self, x: torch.Tensor, xrep: torch.Tensor, + lazy_normalizer: bool = True + ) -> Tuple[D.Normal, Optional[torch.Tensor]]: + r""" + Encode data to sample latent distribution + + Parameters + ---------- + x + Input data + xrep + Alternative input data + lazy_normalizer + Whether to skip computing `x` normalizer (just return None) + if `xrep` is non-empty + + Returns + ------- + u + Sample latent distribution + normalizer + Data normalizer + + Note + ---- + Normalization is always computed on `x`. + If xrep is empty, the normalized `x` will be used as input + to the encoder neural network, otherwise xrep is used instead. + """ + if xrep.numel(): + l = None if lazy_normalizer else self.compute_l(x) + ptr = xrep + else: + l = self.compute_l(x) + ptr = self.normalize(x, l) + for layer in range(self.h_depth): + ptr = getattr(self, f"linear_{layer}")(ptr) + ptr = getattr(self, f"act_{layer}")(ptr) + ptr = getattr(self, f"bn_{layer}")(ptr) + ptr = getattr(self, f"dropout_{layer}")(ptr) + loc = self.loc(ptr) + std = F.softplus(self.std_lin(ptr)) + EPS + return D.Normal(loc, std), l + + +class VanillaDataEncoder(DataEncoder): + + r""" + Vanilla data encoder + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + def compute_l(self, x: torch.Tensor) -> Optional[torch.Tensor]: + return None + + def normalize( + self, x: torch.Tensor, l: Optional[torch.Tensor] + ) -> torch.Tensor: + return x + + +class NBDataEncoder(DataEncoder): + + r""" + Data encoder for negative binomial data + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + TOTAL_COUNT = 1e4 + + def compute_l(self, x: torch.Tensor) -> torch.Tensor: + return x.sum(dim=1, keepdim=True) + + def normalize( + self, x: torch.Tensor, l: torch.Tensor + ) -> torch.Tensor: + return (x * (self.TOTAL_COUNT / l)).log1p() + + +class DataDecoder(glue.DataDecoder): + + r""" + Abstract data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: # pylint: disable=unused-argument + super().__init__() + + @abstractmethod + def forward( # pylint: disable=arguments-differ + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> D.Normal: + r""" + Decode data from sample and feature latent + + Parameters + ---------- + u + Sample latent + v + Feature latent + b + Batch index + l + Optional normalizer + + Returns + ------- + recon + Data reconstruction distribution + """ + raise NotImplementedError # pragma: no cover + + +class NormalDataDecoder(DataDecoder): + + r""" + Normal data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.std_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> D.Normal: + scale = F.softplus(self.scale_lin[b]) + loc = scale * (u @ v.t()) + self.bias[b] + std = F.softplus(self.std_lin[b]) + EPS + return D.Normal(loc, std) + + +class ZINDataDecoder(NormalDataDecoder): + + r""" + Zero-inflated normal data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> ZIN: + scale = F.softplus(self.scale_lin[b]) + loc = scale * (u @ v.t()) + self.bias[b] + std = F.softplus(self.std_lin[b]) + EPS + return ZIN(self.zi_logits[b].expand_as(loc), loc, std) + + +class ZILNDataDecoder(DataDecoder): + + r""" + Zero-inflated log-normal data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.std_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> ZILN: + scale = F.softplus(self.scale_lin[b]) + loc = scale * (u @ v.t()) + self.bias[b] + std = F.softplus(self.std_lin[b]) + EPS + return ZILN(self.zi_logits[b].expand_as(loc), loc, std) + + +class NBDataDecoder(DataDecoder): + + r""" + Negative binomial data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.log_theta = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: torch.Tensor + ) -> D.NegativeBinomial: + scale = F.softplus(self.scale_lin[b]) + logit_mu = scale * (u @ v.t()) + self.bias[b] + mu = F.softmax(logit_mu, dim=1) * l + log_theta = self.log_theta[b] + return D.NegativeBinomial( + log_theta.exp(), + logits=(mu + EPS).log() - log_theta + ) + + +class NBMixtureDataDecoder(DataDecoder): + + r""" + The Mixture of negative binomial data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.scale_lin = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias1 = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.bias2 = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.log_theta = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: torch.Tensor # l is sequencing depth + ) -> D.MixtureSameFamily: + # print(b) + scale = F.softplus(self.scale_lin[b]) + logit_mu1 = scale * (u @ v.t()) + self.bias1[b] + logit_mu2 = scale * (u @ v.t()) + self.bias2[b] + + mu1 = F.softmax(logit_mu1, dim=1) + mu2 = F.softmax(logit_mu2, dim=1) + + log_theta = self.log_theta[b] + log_theta = torch.stack([log_theta,log_theta], axis=-1) + + mix = D.Categorical(logits=torch.stack([logit_mu1, logit_mu2], axis=-1)) + + mu = torch.stack([mu1*l, mu2*l], axis=-1) + + comp = D.NegativeBinomial(log_theta.exp(), logits=(mu + EPS).log() - log_theta) + + return D.MixtureSameFamily(mix, comp) + + +class ZINBDataDecoder(NBDataDecoder): + + r""" + Zero-inflated negative binomial data decoder + + Parameters + ---------- + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__(self, out_features: int, n_batches: int = 1) -> None: + super().__init__(out_features, n_batches=n_batches) + self.zi_logits = torch.nn.Parameter(torch.zeros(n_batches, out_features)) + + def forward( + self, u: torch.Tensor, v: torch.Tensor, + b: torch.Tensor, l: Optional[torch.Tensor] + ) -> ZINB: + scale = F.softplus(self.scale_lin[b]) + logit_mu = scale * (u @ v.t()) + self.bias[b] + mu = F.softmax(logit_mu, dim=1) * l + log_theta = self.log_theta[b] + return ZINB( + self.zi_logits[b].expand_as(mu), + log_theta.exp(), + logits=(mu + EPS).log() - log_theta + ) + + +class Discriminator(torch.nn.Sequential, glue.Discriminator): + + r""" + Modality discriminator + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + h_depth + Hidden layer depth + h_dim + Hidden layer dimensionality + dropout + Dropout rate + """ + + def __init__( + self, in_features: int, out_features: int, n_batches: int = 0, + h_depth: int = 2, h_dim: Optional[int] = 256, + dropout: float = 0.2 + ) -> None: + self.n_batches = n_batches + od = collections.OrderedDict() + ptr_dim = in_features + self.n_batches + for layer in range(h_depth): + od[f"linear_{layer}"] = torch.nn.Linear(ptr_dim, h_dim) + od[f"act_{layer}"] = torch.nn.LeakyReLU(negative_slope=0.2) + od[f"dropout_{layer}"] = torch.nn.Dropout(p=dropout) + ptr_dim = h_dim + od["pred"] = torch.nn.Linear(ptr_dim, out_features) + super().__init__(od) + + def forward(self, x: torch.Tensor, b: torch.Tensor) -> torch.Tensor: # pylint: disable=arguments-differ + if self.n_batches: + b_one_hot = F.one_hot(b, num_classes=self.n_batches) + x = torch.cat([x, b_one_hot], dim=1) + return super().forward(x) + + +class Classifier(torch.nn.Linear): + + r""" + Linear label classifier + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + """ + + +class Prior(glue.Prior): + + r""" + Prior distribution + + Parameters + ---------- + loc + Mean of the normal distribution + std + Standard deviation of the normal distribution + """ + + def __init__( + self, loc: float = 0.0, std: float = 1.0 + ) -> None: + super().__init__() + loc = torch.as_tensor(loc, dtype=torch.get_default_dtype()) + std = torch.as_tensor(std, dtype=torch.get_default_dtype()) + self.register_buffer("loc", loc) + self.register_buffer("std", std) + + def forward(self) -> D.Normal: + return D.Normal(self.loc, self.std) + + +#-------------------- Network modules for independent GLUE --------------------- + +class IndDataDecoder(DataDecoder): + + r""" + Data decoder mixin that makes decoding independent of feature latent + + Parameters + ---------- + in_features + Input dimensionality + out_features + Output dimensionality + n_batches + Number of batches + """ + + def __init__( # pylint: disable=unused-argument + self, in_features: int, out_features: int, n_batches: int = 1 + ) -> None: + super().__init__(out_features, n_batches=n_batches) + self.v = torch.nn.Parameter(torch.zeros(out_features, in_features)) + + def forward( # pylint: disable=arguments-differ + self, u: torch.Tensor, b: torch.Tensor, + l: Optional[torch.Tensor] + ) -> D.Distribution: + r""" + Decode data from sample latent + + Parameters + ---------- + u + Sample latent + b + Batch index + l + Optional normalizer + + Returns + ------- + recon + Data reconstruction distribution + """ + return super().forward(u, self.v, b, l) + + +class IndNormalDataDocoder(IndDataDecoder, NormalDataDecoder): + r""" + Normal data decoder independent of feature latent + """ + + +class IndZINDataDecoder(IndDataDecoder, ZINDataDecoder): + r""" + Zero-inflated normal data decoder independent of feature latent + """ + + +class IndZILNDataDecoder(IndDataDecoder, ZILNDataDecoder): + r""" + Zero-inflated log-normal data decoder independent of feature latent + """ + + +class IndNBDataDecoder(IndDataDecoder, NBDataDecoder): + r""" + Negative binomial data decoder independent of feature latent + """ + + +class IndZINBDataDecoder(IndDataDecoder, ZINBDataDecoder): + r""" + Zero-inflated negative binomial data decoder independent of feature latent + """ diff --git a/docs/test citeseq tutorial.ipynb b/docs/test citeseq tutorial.ipynb index 3213333..34f7131 100644 --- a/docs/test citeseq tutorial.ipynb +++ b/docs/test citeseq tutorial.ipynb @@ -20,7 +20,8 @@ "import scanpy as sc\n", "import scglue\n", "from matplotlib import rcParams\n", - "import numpy as np" + "import numpy as np\n", + "import muon" ] }, { @@ -831,7 +832,7 @@ "metadata": {}, "outputs": [], "source": [ - "guidance = scglue.utils.generate_prot_gudiance_graph(rna, prot, protein_gene_match)" + "guidance = scglue.genomics.generate_prot_guidance_graph(rna, prot, protein_gene_match)" ] }, { @@ -1027,7 +1028,7 @@ "metadata": {}, "outputs": [], "source": [ - "# scglue.utils.clr(prot) #need the clr preprocessing\n", + "# muon.prot.pp.clr(prot) #need the clr preprocessing\n", "# sc.pp.scale(prot)\n", "# sc.tl.pca(prot)\n", "# scglue.models.configure_dataset(\n", diff --git a/pyproject.toml b/pyproject.toml index 42a868f..4606416 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,8 @@ dependencies = [ "h5py>=2.10", "sparse>=0.3.1", "packaging>=16.8", - "leidenalg>=0.7" + "leidenalg>=0.7", + "muon>=0.1.5" ] [project.optional-dependencies] diff --git a/scglue/genomics.py b/scglue/genomics.py index 2aa806e..8687b24 100644 --- a/scglue/genomics.py +++ b/scglue/genomics.py @@ -902,6 +902,41 @@ def ens_trim_version(x: str) -> str: """ return re.sub(r"\.[0-9_-]+$", "", x) +# Function for DIY guidance graph +def generate_prot_guidance_graph(rna: AnnData, + prot: AnnData, + protein_gene_match: Mapping[str, str]): + + r""" + Generate the guidance graph based on CITE-seq datasets. + + Parameters + ---------- + rna + AnnData with gene expression information. + prot + AnnData with protein expression information. + protein_gene_match + The dictionary used to match proteins with genes. + + Returns + ------- + guidance + The guidance map between proteins and genes. + """ + guidance =nx.MultiDiGraph() + for k, v in protein_gene_match.items(): + guidance.add_edge(k, v, weight=1.0, sign=1, type="rev") + guidance.add_edge(v, k, weight=1.0, sign=1, type="fwd") + + for item in rna.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") + for item in prot.var_names: + guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") + + + return guidance + # Aliases read_bed = Bed.read_bed diff --git a/scglue/models/prob.py b/scglue/models/prob.py index aac7825..46a0474 100644 --- a/scglue/models/prob.py +++ b/scglue/models/prob.py @@ -148,3 +148,4 @@ def log_prob(self, value: torch.Tensor) -> torch.Tensor: ).log() - F.softplus(z_zi_logits) zi_log_prob[~z_mask] = raw_log_prob[~z_mask] - F.softplus(nz_zi_logits) return zi_log_prob + diff --git a/scglue/models/sc.py b/scglue/models/sc.py index c3b8ec9..aa626f6 100644 --- a/scglue/models/sc.py +++ b/scglue/models/sc.py @@ -32,21 +32,17 @@ class GraphEncoder(glue.GraphEncoder): """ def __init__( - self, vnum: int, out_features: int, init_fea_emb + self, vnum: int, out_features: int ) -> None: super().__init__() + self.vrepr = torch.nn.Parameter(torch.zeros(vnum, out_features)) self.conv = GraphConv() self.loc = torch.nn.Linear(out_features, out_features) self.std_lin = torch.nn.Linear(out_features, out_features) - if init_fea_emb is None: - self.vrepr = torch.nn.Parameter(torch.zeros(vnum, out_features)) - else: - self.vrepr = torch.nn.Parameter(torch.FloatTensor(init_fea_emb)) def forward( - self, eidx: torch.Tensor, enorm: torch.Tensor, esgn: torch.Tensor, init_fea_emb = None + self, eidx: torch.Tensor, enorm: torch.Tensor, esgn: torch.Tensor ) -> D.Normal: - # self.vrepr = self.vrepr.to(eidx.device) ptr = self.conv(self.vrepr, eidx, enorm, esgn) loc = self.loc(ptr) std = F.softplus(self.std_lin(ptr)) + EPS @@ -410,7 +406,7 @@ def forward( class NBMixtureDataDecoder(DataDecoder): r""" - Negative binomial data decoder + The Mixture of negative binomial data decoder Parameters ---------- @@ -431,7 +427,8 @@ def __init__(self, out_features: int, n_batches: int = 1) -> None: def forward( self, u: torch.Tensor, v: torch.Tensor, b: torch.Tensor, l: torch.Tensor # l is sequencing depth - ) -> D.NegativeBinomial: + ) -> D.MixtureSameFamily: + # print(b) scale = F.softplus(self.scale_lin[b]) logit_mu1 = scale * (u @ v.t()) + self.bias1[b] logit_mu2 = scale * (u @ v.t()) + self.bias2[b] @@ -439,16 +436,16 @@ def forward( mu1 = F.softmax(logit_mu1, dim=1) mu2 = F.softmax(logit_mu2, dim=1) - # beta = self.zi_logits[b].expand_as(mu1) # to avoid negative value in the bernoulli distribution, we use l later. - v_s = torch.distributions.Bernoulli(mu1).sample() - mu_mixture = v_s* l + (1-v_s)*mu2* l # keep the same format with TOTALVI - # mu_mixture = v_s + (1-v_s)*mu2* l - # print(mu_mixture) log_theta = self.log_theta[b] - return D.NegativeBinomial( - log_theta.exp(), - logits=(mu_mixture + EPS).log() - log_theta - ) + log_theta = torch.stack([log_theta,log_theta], axis=-1) + + mix = D.Categorical(logits=torch.stack([logit_mu1, logit_mu2], axis=-1)) + + mu = torch.stack([mu1*l, mu2*l], axis=-1) + + comp = D.NegativeBinomial(log_theta.exp(), logits=(mu + EPS).log() - log_theta) + + return D.MixtureSameFamily(mix, comp) class ZINBDataDecoder(NBDataDecoder): diff --git a/scglue/utils.py b/scglue/utils.py index c04fb60..fbdf8ba 100644 --- a/scglue/utils.py +++ b/scglue/utils.py @@ -677,25 +677,6 @@ def _handle(line): return output_lines -# Function for DIY gudiance graph -def generate_prot_gudiance_graph(rna, - prot, - protein_gene_match): - guidance =nx.MultiDiGraph() - for k, v in protein_gene_match.items(): - guidance.add_edge(k, v, weight=1.0, sign=1, type="rev") - guidance.add_edge(v, k, weight=1.0, sign=1, type="fwd") - - for item in rna.var_names: - guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") - for item in prot.var_names: - guidance.add_edge(item, item, weight=1.0, sign=1, type="loop") - - - return guidance - - - def clr(adata:AnnData, inplace= True, axis= 0): """ Apply the centered log ratio (CLR) transformation From 3923df9c1b5724b9d9a5b31030322c834783a5c1 Mon Sep 17 00:00:00 2001 From: Helloworldlty Date: Fri, 23 Feb 2024 08:42:00 -0500 Subject: [PATCH 5/5] update utils --- .history/pyproject_20240223083840.toml | 78 +++ .history/scglue/utils_20240208225216.py | 719 ++++++++++++++++++++++++ .history/scglue/utils_20240223084149.py | 678 ++++++++++++++++++++++ scglue/utils.py | 41 -- 4 files changed, 1475 insertions(+), 41 deletions(-) create mode 100644 .history/pyproject_20240223083840.toml create mode 100644 .history/scglue/utils_20240208225216.py create mode 100644 .history/scglue/utils_20240223084149.py diff --git a/.history/pyproject_20240223083840.toml b/.history/pyproject_20240223083840.toml new file mode 100644 index 0000000..4606416 --- /dev/null +++ b/.history/pyproject_20240223083840.toml @@ -0,0 +1,78 @@ +[build-system] +requires = ["setuptools", "wheel", "flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "scglue" +version = "0.3.2" +description = "Graph-linked unified embedding for unpaired single-cell multi-omics data integration" +readme = "README.md" +requires-python = ">=3.6" +license = {file = "LICENSE"} +authors = [ + {name = "Zhi-Jie Cao", email = "caozj@mail.cbi.pku.edu.cn"}, + {name = "Xin-Ming Tu", email = "xinmingtu@pku.edu.cn"} +] +keywords = ["bioinformatics", "deep-learning", "single-cell", "single-cell-multiomics"] +classifiers = [ + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering :: Bio-Informatics" +] +dependencies = [ + "numpy>=1.19", + "scipy>=1.3", + "pandas>=1.1", + "matplotlib>=3.1.2", + "seaborn>=0.9", + "dill>=0.2.3", + "tqdm>=4.27", + "scikit-learn>=0.21.2", + "statsmodels>=0.10", + "parse>=1.3.2", + "networkx>=2", + "pynvml>=8.0.1", + "torch>=1.8", + "pytorch-ignite>=0.4.1", + "tensorboardX>=1.4", + "anndata>=0.7", + "scanpy>=1.5", + "pybedtools>=0.8.1", + "h5py>=2.10", + "sparse>=0.3.1", + "packaging>=16.8", + "leidenalg>=0.7", + "muon>=0.1.5" +] + +[project.optional-dependencies] +doc = [ + "sphinx<7", + "sphinx-autodoc-typehints", + "sphinx-copybutton", + "sphinx-intl", + "nbsphinx", + "sphinx-rtd-theme", + "ipython", + "jinja2" +] +test = [ + "plotly", + "pytest", + "pytest-cov" +] + +[project.urls] +Github = "https://github.com/gao-lab/GLUE" + +[tool.flit.sdist] +exclude = [".*", "c*", "d*", "e*", "pa*", "t*", "T*"] diff --git a/.history/scglue/utils_20240208225216.py b/.history/scglue/utils_20240208225216.py new file mode 100644 index 0000000..fbdf8ba --- /dev/null +++ b/.history/scglue/utils_20240208225216.py @@ -0,0 +1,719 @@ +r""" +Miscellaneous utilities +""" + +import os +import logging +import signal +import subprocess +import sys +from collections import defaultdict +from multiprocessing import Process +from typing import Any, List, Mapping, Optional +from warnings import warn + +from scipy.sparse import issparse, csc_matrix, csr_matrix +from anndata import AnnData +import numpy as np +import pandas as pd +import torch +import networkx as nx +from pybedtools.helpers import set_bedtools_path + +from .typehint import RandomState, T + +AUTO = "AUTO" # Flag for using automatically determined hyperparameters + + +#------------------------------ Global containers ------------------------------ + +processes: Mapping[int, Mapping[int, Process]] = defaultdict(dict) # id -> pid -> process + + +#-------------------------------- Meta classes --------------------------------- + +class SingletonMeta(type): + + r""" + Ensure singletons via a meta class + """ + + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + + +#--------------------------------- Log manager --------------------------------- + +class _CriticalFilter(logging.Filter): + + def filter(self, record: logging.LogRecord) -> bool: + return record.levelno >= logging.WARNING + + +class _NonCriticalFilter(logging.Filter): + + def filter(self, record: logging.LogRecord) -> bool: + return record.levelno < logging.WARNING + + +class LogManager(metaclass=SingletonMeta): + + r""" + Manage loggers used in the package + """ + + def __init__(self) -> None: + self._loggers = {} + self._log_file = None + self._console_log_level = logging.INFO + self._file_log_level = logging.DEBUG + self._file_fmt = \ + "%(asctime)s.%(msecs)03d [%(levelname)s] %(name)s: %(message)s" + self._console_fmt = \ + "[%(levelname)s] %(name)s: %(message)s" + self._date_fmt = "%Y-%m-%d %H:%M:%S" + + @property + def log_file(self) -> str: + r""" + Configure log file + """ + return self._log_file + + @property + def file_log_level(self) -> int: + r""" + Configure logging level in the log file + """ + return self._file_log_level + + @property + def console_log_level(self) -> int: + r""" + Configure logging level printed in the console + """ + return self._console_log_level + + def _create_file_handler(self) -> logging.FileHandler: + file_handler = logging.FileHandler(self.log_file) + file_handler.setLevel(self.file_log_level) + file_handler.setFormatter(logging.Formatter( + fmt=self._file_fmt, datefmt=self._date_fmt)) + return file_handler + + def _create_console_handler(self, critical: bool) -> logging.StreamHandler: + if critical: + console_handler = logging.StreamHandler(sys.stderr) + console_handler.addFilter(_CriticalFilter()) + else: + console_handler = logging.StreamHandler(sys.stdout) + console_handler.addFilter(_NonCriticalFilter()) + console_handler.setLevel(self.console_log_level) + console_handler.setFormatter(logging.Formatter(fmt=self._console_fmt)) + return console_handler + + def get_logger(self, name: str) -> logging.Logger: + r""" + Get a logger by name + """ + if name in self._loggers: + return self._loggers[name] + new_logger = logging.getLogger(name) + new_logger.setLevel(logging.DEBUG) # lowest level + new_logger.addHandler(self._create_console_handler(True)) + new_logger.addHandler(self._create_console_handler(False)) + if self.log_file: + new_logger.addHandler(self._create_file_handler()) + self._loggers[name] = new_logger + return new_logger + + @log_file.setter + def log_file(self, file_name: os.PathLike) -> None: + self._log_file = file_name + for logger in self._loggers.values(): + for idx, handler in enumerate(logger.handlers): + if isinstance(handler, logging.FileHandler): + logger.handlers[idx].close() + if self.log_file: + logger.handlers[idx] = self._create_file_handler() + else: + del logger.handlers[idx] + break + else: + if file_name: + logger.addHandler(self._create_file_handler()) + + @file_log_level.setter + def file_log_level(self, log_level: int) -> None: + self._file_log_level = log_level + for logger in self._loggers.values(): + for handler in logger.handlers: + if isinstance(handler, logging.FileHandler): + handler.setLevel(self.file_log_level) + break + + @console_log_level.setter + def console_log_level(self, log_level: int) -> None: + self._console_log_level = log_level + for logger in self._loggers.values(): + for handler in logger.handlers: + if type(handler) is logging.StreamHandler: # pylint: disable=unidiomatic-typecheck + handler.setLevel(self.console_log_level) + + +log = LogManager() + + +def logged(obj: T) -> T: + r""" + Add logger as an attribute + """ + obj.logger = log.get_logger(obj.__name__) + return obj + + +#---------------------------- Configuration Manager ---------------------------- + +@logged +class ConfigManager(metaclass=SingletonMeta): + + r""" + Global configurations + """ + + def __init__(self) -> None: + self.TMP_PREFIX = "GLUETMP" + self.ANNDATA_KEY = "__scglue__" + self.CPU_ONLY = False + self.CUDNN_MODE = "repeatability" + self.MASKED_GPUS = [] + self.ARRAY_SHUFFLE_NUM_WORKERS = 0 + self.GRAPH_SHUFFLE_NUM_WORKERS = 1 + self.FORCE_TERMINATE_WORKER_PATIENCE = 60 + self.DATALOADER_NUM_WORKERS = 0 + self.DATALOADER_FETCHES_PER_WORKER = 4 + self.DATALOADER_PIN_MEMORY = True + self.CHECKPOINT_SAVE_INTERVAL = 10 + self.CHECKPOINT_SAVE_NUMBERS = 3 + self.PRINT_LOSS_INTERVAL = 10 + self.TENSORBOARD_FLUSH_SECS = 5 + self.ALLOW_TRAINING_INTERRUPTION = True + self.BEDTOOLS_PATH = "" + + @property + def TMP_PREFIX(self) -> str: + r""" + Prefix of temporary files and directories created. + Default values is ``"GLUETMP"``. + """ + return self._TMP_PREFIX + + @TMP_PREFIX.setter + def TMP_PREFIX(self, tmp_prefix: str) -> None: + self._TMP_PREFIX = tmp_prefix + + @property + def ANNDATA_KEY(self) -> str: + r""" + Key in ``adata.uns`` for storing dataset configurations. + Default value is ``"__scglue__"`` + """ + return self._ANNDATA_KEY + + @ANNDATA_KEY.setter + def ANNDATA_KEY(self, anndata_key: str) -> None: + self._ANNDATA_KEY = anndata_key + + @property + def CPU_ONLY(self) -> bool: + r""" + Whether computation should use only CPUs. + Default value is ``False``. + """ + return self._CPU_ONLY + + @CPU_ONLY.setter + def CPU_ONLY(self, cpu_only: bool) -> None: + self._CPU_ONLY = cpu_only + if self._CPU_ONLY and self._DATALOADER_NUM_WORKERS: + self.logger.warning( + "It is recommended to set `DATALOADER_NUM_WORKERS` to 0 " + "when using CPU_ONLY mode. Otherwise, deadlocks may happen " + "occationally." + ) + + @property + def CUDNN_MODE(self) -> str: + r""" + CuDNN computation mode, should be one of {"repeatability", "performance"}. + Default value is ``"repeatability"``. + + Note + ---- + As of now, due to the use of :meth:`torch.Tensor.scatter_add_` + operation, the results are not completely reproducible even when + ``CUDNN_MODE`` is set to ``"repeatability"``, if GPU is used as + computation device. Exact repeatability can only be achieved on CPU. + The situtation might change with new releases of :mod:`torch`. + """ + return self._CUDNN_MODE + + @CUDNN_MODE.setter + def CUDNN_MODE(self, cudnn_mode: str) -> None: + if cudnn_mode not in ("repeatability", "performance"): + raise ValueError("Invalid mode!") + self._CUDNN_MODE = cudnn_mode + torch.backends.cudnn.deterministic = self._CUDNN_MODE == "repeatability" + torch.backends.cudnn.benchmark = self._CUDNN_MODE == "performance" + + @property + def MASKED_GPUS(self) -> List[int]: + r""" + A list of GPUs that should not be used when selecting computation device. + This must be set before initializing any model, otherwise would be ineffective. + Default value is ``[]``. + """ + return self._MASKED_GPUS + + @MASKED_GPUS.setter + def MASKED_GPUS(self, masked_gpus: List[int]) -> None: + if masked_gpus: + import pynvml + pynvml.nvmlInit() + device_count = pynvml.nvmlDeviceGetCount() + for item in masked_gpus: + if item >= device_count: + raise ValueError(f"GPU device \"{item}\" is non-existent!") + self._MASKED_GPUS = masked_gpus + + @property + def ARRAY_SHUFFLE_NUM_WORKERS(self) -> int: + r""" + Number of background workers for array data shuffling. + Default value is ``0``. + """ + return self._ARRAY_SHUFFLE_NUM_WORKERS + + @ARRAY_SHUFFLE_NUM_WORKERS.setter + def ARRAY_SHUFFLE_NUM_WORKERS(self, array_shuffle_num_workers: int) -> None: + self._ARRAY_SHUFFLE_NUM_WORKERS = array_shuffle_num_workers + + @property + def GRAPH_SHUFFLE_NUM_WORKERS(self) -> int: + r""" + Number of background workers for graph data shuffling. + Default value is ``1``. + """ + return self._GRAPH_SHUFFLE_NUM_WORKERS + + @GRAPH_SHUFFLE_NUM_WORKERS.setter + def GRAPH_SHUFFLE_NUM_WORKERS(self, graph_shuffle_num_workers: int) -> None: + self._GRAPH_SHUFFLE_NUM_WORKERS = graph_shuffle_num_workers + + @property + def FORCE_TERMINATE_WORKER_PATIENCE(self) -> int: + r""" + Seconds to wait before force terminating unresponsive workers. + Default value is ``60``. + """ + return self._FORCE_TERMINATE_WORKER_PATIENCE + + @FORCE_TERMINATE_WORKER_PATIENCE.setter + def FORCE_TERMINATE_WORKER_PATIENCE(self, force_terminate_worker_patience: int) -> None: + self._FORCE_TERMINATE_WORKER_PATIENCE = force_terminate_worker_patience + + @property + def DATALOADER_NUM_WORKERS(self) -> int: + r""" + Number of worker processes to use in data loader. + Default value is ``0``. + """ + return self._DATALOADER_NUM_WORKERS + + @DATALOADER_NUM_WORKERS.setter + def DATALOADER_NUM_WORKERS(self, dataloader_num_workers: int) -> None: + if dataloader_num_workers > 8: + self.logger.warning( + "Worker number 1-8 is generally sufficient, " + "too many workers might have negative impact on speed." + ) + self._DATALOADER_NUM_WORKERS = dataloader_num_workers + + @property + def DATALOADER_FETCHES_PER_WORKER(self) -> int: + r""" + Number of fetches per worker per batch to use in data loader. + Default value is ``4``. + """ + return self._DATALOADER_FETCHES_PER_WORKER + + @DATALOADER_FETCHES_PER_WORKER.setter + def DATALOADER_FETCHES_PER_WORKER(self, dataloader_fetches_per_worker: int) -> None: + self._DATALOADER_FETCHES_PER_WORKER = dataloader_fetches_per_worker + + @property + def DATALOADER_FETCHES_PER_BATCH(self) -> int: + r""" + Number of fetches per batch in data loader (read-only). + """ + return max(1, self.DATALOADER_NUM_WORKERS) * self.DATALOADER_FETCHES_PER_WORKER + + @property + def DATALOADER_PIN_MEMORY(self) -> bool: + r""" + Whether to use pin memory in data loader. + Default value is ``True``. + """ + return self._DATALOADER_PIN_MEMORY + + @DATALOADER_PIN_MEMORY.setter + def DATALOADER_PIN_MEMORY(self, dataloader_pin_memory: bool): + self._DATALOADER_PIN_MEMORY = dataloader_pin_memory + + @property + def CHECKPOINT_SAVE_INTERVAL(self) -> int: + r""" + Automatically save checkpoints every n epochs. + Default value is ``10``. + """ + return self._CHECKPOINT_SAVE_INTERVAL + + @CHECKPOINT_SAVE_INTERVAL.setter + def CHECKPOINT_SAVE_INTERVAL(self, checkpoint_save_interval: int) -> None: + self._CHECKPOINT_SAVE_INTERVAL = checkpoint_save_interval + + @property + def CHECKPOINT_SAVE_NUMBERS(self) -> int: + r""" + Maximal number of checkpoints to preserve at any point. + Default value is ``3``. + """ + return self._CHECKPOINT_SAVE_NUMBERS + + @CHECKPOINT_SAVE_NUMBERS.setter + def CHECKPOINT_SAVE_NUMBERS(self, checkpoint_save_numbers: int) -> None: + self._CHECKPOINT_SAVE_NUMBERS = checkpoint_save_numbers + + @property + def PRINT_LOSS_INTERVAL(self) -> int: + r""" + Print loss values every n epochs. + Default value is ``10``. + """ + return self._PRINT_LOSS_INTERVAL + + @PRINT_LOSS_INTERVAL.setter + def PRINT_LOSS_INTERVAL(self, print_loss_interval: int) -> None: + self._PRINT_LOSS_INTERVAL = print_loss_interval + + @property + def TENSORBOARD_FLUSH_SECS(self) -> int: + r""" + Flush tensorboard logs to file every n seconds. + Default values is ``5``. + """ + return self._TENSORBOARD_FLUSH_SECS + + @TENSORBOARD_FLUSH_SECS.setter + def TENSORBOARD_FLUSH_SECS(self, tensorboard_flush_secs: int) -> None: + self._TENSORBOARD_FLUSH_SECS = tensorboard_flush_secs + + @property + def ALLOW_TRAINING_INTERRUPTION(self) -> bool: + r""" + Allow interruption before model training converges. + Default values is ``True``. + """ + return self._ALLOW_TRAINING_INTERRUPTION + + @ALLOW_TRAINING_INTERRUPTION.setter + def ALLOW_TRAINING_INTERRUPTION(self, allow_training_interruption: bool) -> None: + self._ALLOW_TRAINING_INTERRUPTION = allow_training_interruption + + @property + def BEDTOOLS_PATH(self) -> str: + r""" + Path to bedtools executable. + Default value is ``bedtools``. + """ + return self._BEDTOOLS_PATH + + @BEDTOOLS_PATH.setter + def BEDTOOLS_PATH(self, bedtools_path: str) -> None: + self._BEDTOOLS_PATH = bedtools_path + set_bedtools_path(bedtools_path) + + +config = ConfigManager() + + +#---------------------------- Interruption handling ---------------------------- + +@logged +class DelayedKeyboardInterrupt: # pragma: no cover + + r""" + Shield a code block from keyboard interruptions, delaying handling + till the block is finished (adapted from + `https://stackoverflow.com/a/21919644 + `__). + """ + + def __init__(self): + self.signal_received = None + self.old_handler = None + + def __enter__(self): + self.signal_received = False + self.old_handler = signal.signal(signal.SIGINT, self._handler) + + def _handler(self, sig, frame): + self.signal_received = (sig, frame) + self.logger.debug("SIGINT received, delaying KeyboardInterrupt...") + + def __exit__(self, exc_type, exc_val, exc_tb): + signal.signal(signal.SIGINT, self.old_handler) + if self.signal_received: + self.old_handler(*self.signal_received) + + +#--------------------------- Constrained data frame ---------------------------- + +@logged +class ConstrainedDataFrame(pd.DataFrame): + + r""" + Data frame with certain format constraints + + Note + ---- + Format constraints are checked and maintained automatically. + """ + + def __init__(self, *args, **kwargs) -> None: + df = pd.DataFrame(*args, **kwargs) + df = self.rectify(df) + self.verify(df) + super().__init__(df) + + def __setitem__(self, key, value) -> None: + super().__setitem__(key, value) + self.verify(self) + + @property + def _constructor(self) -> type: + return type(self) + + @classmethod + def rectify(cls, df: pd.DataFrame) -> pd.DataFrame: + r""" + Rectify data frame for format integrity + + Parameters + ---------- + df + Data frame to be rectified + + Returns + ------- + rectified_df + Rectified data frame + """ + return df + + @classmethod + def verify(cls, df: pd.DataFrame) -> None: + r""" + Verify data frame for format integrity + + Parameters + ---------- + df + Data frame to be verified + """ + + @property + def df(self) -> pd.DataFrame: + r""" + Convert to regular data frame + """ + return pd.DataFrame(self) + + def __repr__(self) -> str: + r""" + Note + ---- + We need to explicitly call :func:`repr` on the regular data frame + to bypass integrity verification, because when the terminal is + too narrow, :mod:`pandas` would split the data frame internally, + causing format verification to fail. + """ + return repr(self.df) + + +#--------------------------- Other utility functions --------------------------- + +def get_chained_attr(x: Any, attr: str) -> Any: + r""" + Get attribute from an object, with support for chained attribute names. + + Parameters + ---------- + x + Object to get attribute from + attr + Attribute name + + Returns + ------- + attr_value + Attribute value + """ + for k in attr.split("."): + if not hasattr(x, k): + raise AttributeError(f"{attr} not found!") + x = getattr(x, k) + return x + + +def get_rs(x: RandomState = None) -> np.random.RandomState: + r""" + Get random state object + + Parameters + ---------- + x + Object that can be converted to a random state object + + Returns + ------- + rs + Random state object + """ + if isinstance(x, int): + return np.random.RandomState(x) + if isinstance(x, np.random.RandomState): + return x + return np.random + + +@logged +def run_command( + command: str, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + log_command: bool = True, print_output: bool = True, + err_message: Optional[Mapping[int, str]] = None, **kwargs +) -> Optional[List[str]]: + r""" + Run an external command and get realtime output + + Parameters + ---------- + command + A string containing the command to be executed + stdout + Where to redirect stdout + stderr + Where to redirect stderr + echo_command + Whether to log the command being printed (log level is INFO) + print_output + Whether to print stdout of the command. + If ``stdout`` is PIPE and ``print_output`` is set to False, + the output will be returned as a list of output lines. + err_message + Look up dict of error message (indexed by error code) + **kwargs + Other keyword arguments to be passed to :class:`subprocess.Popen` + + Returns + ------- + output_lines + A list of output lines (only returned if ``stdout`` is PIPE + and ``print_output`` is False) + """ + if log_command: + run_command.logger.info("Executing external command: %s", command) + executable = command.split(" ")[0] + with subprocess.Popen(command, stdout=stdout, stderr=stderr, + shell=True, **kwargs) as p: + if stdout == subprocess.PIPE: + prompt = f"{executable} ({p.pid}): " + output_lines = [] + + def _handle(line): + line = line.strip().decode() + if print_output: + print(prompt + line) + else: + output_lines.append(line) + + while True: + _handle(p.stdout.readline()) + ret = p.poll() + if ret is not None: + # Handle output between last readlines and successful poll + for line in p.stdout.readlines(): + _handle(line) + break + else: + output_lines = None + ret = p.wait() + if ret != 0: + err_message = err_message or {} + if ret in err_message: + err_message = " " + err_message[ret] + elif "__default__" in err_message: + err_message = " " + err_message["__default__"] + else: + err_message = "" + raise RuntimeError( + f"{executable} exited with error code: {ret}.{err_message}") + if stdout == subprocess.PIPE and not print_output: + return output_lines + + +def clr(adata:AnnData, inplace= True, axis= 0): + """ + Apply the centered log ratio (CLR) transformation + to normalize counts in adata.X. + + Args: + data: AnnData object with protein expression counts. + inplace: Whether to update adata.X inplace. + axis: Axis across which CLR is performed. + """ + + if axis not in [0, 1]: + raise ValueError("Invalid value for `axis` provided. Admissible options are `0` and `1`.") + + if not inplace: + adata = adata.copy() + + if issparse(adata.X) and axis == 0 and not isinstance(adata.X, csc_matrix): + warn("adata.X is sparse but not in CSC format. Converting to CSC.") + x = csc_matrix(adata.X) + elif issparse(adata.X) and axis == 1 and not isinstance(adata.X, csr_matrix): + warn("adata.X is sparse but not in CSR format. Converting to CSR.") + x = csr_matrix(adata.X) + else: + x = adata.X + + if issparse(x): + x.data /= np.repeat( + np.exp(np.log1p(x).sum(axis=axis).A / x.shape[axis]), x.getnnz(axis=axis) + ) + np.log1p(x.data, out=x.data) + else: + np.log1p( + x / np.exp(np.log1p(x).sum(axis=axis, keepdims=True) / x.shape[axis]), + out=x, + ) + + adata.X = x + + return None if inplace else adata \ No newline at end of file diff --git a/.history/scglue/utils_20240223084149.py b/.history/scglue/utils_20240223084149.py new file mode 100644 index 0000000..a3bc480 --- /dev/null +++ b/.history/scglue/utils_20240223084149.py @@ -0,0 +1,678 @@ +r""" +Miscellaneous utilities +""" + +import os +import logging +import signal +import subprocess +import sys +from collections import defaultdict +from multiprocessing import Process +from typing import Any, List, Mapping, Optional +from warnings import warn + +from scipy.sparse import issparse, csc_matrix, csr_matrix +from anndata import AnnData +import numpy as np +import pandas as pd +import torch +import networkx as nx +from pybedtools.helpers import set_bedtools_path + +from .typehint import RandomState, T + +AUTO = "AUTO" # Flag for using automatically determined hyperparameters + + +#------------------------------ Global containers ------------------------------ + +processes: Mapping[int, Mapping[int, Process]] = defaultdict(dict) # id -> pid -> process + + +#-------------------------------- Meta classes --------------------------------- + +class SingletonMeta(type): + + r""" + Ensure singletons via a meta class + """ + + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + + +#--------------------------------- Log manager --------------------------------- + +class _CriticalFilter(logging.Filter): + + def filter(self, record: logging.LogRecord) -> bool: + return record.levelno >= logging.WARNING + + +class _NonCriticalFilter(logging.Filter): + + def filter(self, record: logging.LogRecord) -> bool: + return record.levelno < logging.WARNING + + +class LogManager(metaclass=SingletonMeta): + + r""" + Manage loggers used in the package + """ + + def __init__(self) -> None: + self._loggers = {} + self._log_file = None + self._console_log_level = logging.INFO + self._file_log_level = logging.DEBUG + self._file_fmt = \ + "%(asctime)s.%(msecs)03d [%(levelname)s] %(name)s: %(message)s" + self._console_fmt = \ + "[%(levelname)s] %(name)s: %(message)s" + self._date_fmt = "%Y-%m-%d %H:%M:%S" + + @property + def log_file(self) -> str: + r""" + Configure log file + """ + return self._log_file + + @property + def file_log_level(self) -> int: + r""" + Configure logging level in the log file + """ + return self._file_log_level + + @property + def console_log_level(self) -> int: + r""" + Configure logging level printed in the console + """ + return self._console_log_level + + def _create_file_handler(self) -> logging.FileHandler: + file_handler = logging.FileHandler(self.log_file) + file_handler.setLevel(self.file_log_level) + file_handler.setFormatter(logging.Formatter( + fmt=self._file_fmt, datefmt=self._date_fmt)) + return file_handler + + def _create_console_handler(self, critical: bool) -> logging.StreamHandler: + if critical: + console_handler = logging.StreamHandler(sys.stderr) + console_handler.addFilter(_CriticalFilter()) + else: + console_handler = logging.StreamHandler(sys.stdout) + console_handler.addFilter(_NonCriticalFilter()) + console_handler.setLevel(self.console_log_level) + console_handler.setFormatter(logging.Formatter(fmt=self._console_fmt)) + return console_handler + + def get_logger(self, name: str) -> logging.Logger: + r""" + Get a logger by name + """ + if name in self._loggers: + return self._loggers[name] + new_logger = logging.getLogger(name) + new_logger.setLevel(logging.DEBUG) # lowest level + new_logger.addHandler(self._create_console_handler(True)) + new_logger.addHandler(self._create_console_handler(False)) + if self.log_file: + new_logger.addHandler(self._create_file_handler()) + self._loggers[name] = new_logger + return new_logger + + @log_file.setter + def log_file(self, file_name: os.PathLike) -> None: + self._log_file = file_name + for logger in self._loggers.values(): + for idx, handler in enumerate(logger.handlers): + if isinstance(handler, logging.FileHandler): + logger.handlers[idx].close() + if self.log_file: + logger.handlers[idx] = self._create_file_handler() + else: + del logger.handlers[idx] + break + else: + if file_name: + logger.addHandler(self._create_file_handler()) + + @file_log_level.setter + def file_log_level(self, log_level: int) -> None: + self._file_log_level = log_level + for logger in self._loggers.values(): + for handler in logger.handlers: + if isinstance(handler, logging.FileHandler): + handler.setLevel(self.file_log_level) + break + + @console_log_level.setter + def console_log_level(self, log_level: int) -> None: + self._console_log_level = log_level + for logger in self._loggers.values(): + for handler in logger.handlers: + if type(handler) is logging.StreamHandler: # pylint: disable=unidiomatic-typecheck + handler.setLevel(self.console_log_level) + + +log = LogManager() + + +def logged(obj: T) -> T: + r""" + Add logger as an attribute + """ + obj.logger = log.get_logger(obj.__name__) + return obj + + +#---------------------------- Configuration Manager ---------------------------- + +@logged +class ConfigManager(metaclass=SingletonMeta): + + r""" + Global configurations + """ + + def __init__(self) -> None: + self.TMP_PREFIX = "GLUETMP" + self.ANNDATA_KEY = "__scglue__" + self.CPU_ONLY = False + self.CUDNN_MODE = "repeatability" + self.MASKED_GPUS = [] + self.ARRAY_SHUFFLE_NUM_WORKERS = 0 + self.GRAPH_SHUFFLE_NUM_WORKERS = 1 + self.FORCE_TERMINATE_WORKER_PATIENCE = 60 + self.DATALOADER_NUM_WORKERS = 0 + self.DATALOADER_FETCHES_PER_WORKER = 4 + self.DATALOADER_PIN_MEMORY = True + self.CHECKPOINT_SAVE_INTERVAL = 10 + self.CHECKPOINT_SAVE_NUMBERS = 3 + self.PRINT_LOSS_INTERVAL = 10 + self.TENSORBOARD_FLUSH_SECS = 5 + self.ALLOW_TRAINING_INTERRUPTION = True + self.BEDTOOLS_PATH = "" + + @property + def TMP_PREFIX(self) -> str: + r""" + Prefix of temporary files and directories created. + Default values is ``"GLUETMP"``. + """ + return self._TMP_PREFIX + + @TMP_PREFIX.setter + def TMP_PREFIX(self, tmp_prefix: str) -> None: + self._TMP_PREFIX = tmp_prefix + + @property + def ANNDATA_KEY(self) -> str: + r""" + Key in ``adata.uns`` for storing dataset configurations. + Default value is ``"__scglue__"`` + """ + return self._ANNDATA_KEY + + @ANNDATA_KEY.setter + def ANNDATA_KEY(self, anndata_key: str) -> None: + self._ANNDATA_KEY = anndata_key + + @property + def CPU_ONLY(self) -> bool: + r""" + Whether computation should use only CPUs. + Default value is ``False``. + """ + return self._CPU_ONLY + + @CPU_ONLY.setter + def CPU_ONLY(self, cpu_only: bool) -> None: + self._CPU_ONLY = cpu_only + if self._CPU_ONLY and self._DATALOADER_NUM_WORKERS: + self.logger.warning( + "It is recommended to set `DATALOADER_NUM_WORKERS` to 0 " + "when using CPU_ONLY mode. Otherwise, deadlocks may happen " + "occationally." + ) + + @property + def CUDNN_MODE(self) -> str: + r""" + CuDNN computation mode, should be one of {"repeatability", "performance"}. + Default value is ``"repeatability"``. + + Note + ---- + As of now, due to the use of :meth:`torch.Tensor.scatter_add_` + operation, the results are not completely reproducible even when + ``CUDNN_MODE`` is set to ``"repeatability"``, if GPU is used as + computation device. Exact repeatability can only be achieved on CPU. + The situtation might change with new releases of :mod:`torch`. + """ + return self._CUDNN_MODE + + @CUDNN_MODE.setter + def CUDNN_MODE(self, cudnn_mode: str) -> None: + if cudnn_mode not in ("repeatability", "performance"): + raise ValueError("Invalid mode!") + self._CUDNN_MODE = cudnn_mode + torch.backends.cudnn.deterministic = self._CUDNN_MODE == "repeatability" + torch.backends.cudnn.benchmark = self._CUDNN_MODE == "performance" + + @property + def MASKED_GPUS(self) -> List[int]: + r""" + A list of GPUs that should not be used when selecting computation device. + This must be set before initializing any model, otherwise would be ineffective. + Default value is ``[]``. + """ + return self._MASKED_GPUS + + @MASKED_GPUS.setter + def MASKED_GPUS(self, masked_gpus: List[int]) -> None: + if masked_gpus: + import pynvml + pynvml.nvmlInit() + device_count = pynvml.nvmlDeviceGetCount() + for item in masked_gpus: + if item >= device_count: + raise ValueError(f"GPU device \"{item}\" is non-existent!") + self._MASKED_GPUS = masked_gpus + + @property + def ARRAY_SHUFFLE_NUM_WORKERS(self) -> int: + r""" + Number of background workers for array data shuffling. + Default value is ``0``. + """ + return self._ARRAY_SHUFFLE_NUM_WORKERS + + @ARRAY_SHUFFLE_NUM_WORKERS.setter + def ARRAY_SHUFFLE_NUM_WORKERS(self, array_shuffle_num_workers: int) -> None: + self._ARRAY_SHUFFLE_NUM_WORKERS = array_shuffle_num_workers + + @property + def GRAPH_SHUFFLE_NUM_WORKERS(self) -> int: + r""" + Number of background workers for graph data shuffling. + Default value is ``1``. + """ + return self._GRAPH_SHUFFLE_NUM_WORKERS + + @GRAPH_SHUFFLE_NUM_WORKERS.setter + def GRAPH_SHUFFLE_NUM_WORKERS(self, graph_shuffle_num_workers: int) -> None: + self._GRAPH_SHUFFLE_NUM_WORKERS = graph_shuffle_num_workers + + @property + def FORCE_TERMINATE_WORKER_PATIENCE(self) -> int: + r""" + Seconds to wait before force terminating unresponsive workers. + Default value is ``60``. + """ + return self._FORCE_TERMINATE_WORKER_PATIENCE + + @FORCE_TERMINATE_WORKER_PATIENCE.setter + def FORCE_TERMINATE_WORKER_PATIENCE(self, force_terminate_worker_patience: int) -> None: + self._FORCE_TERMINATE_WORKER_PATIENCE = force_terminate_worker_patience + + @property + def DATALOADER_NUM_WORKERS(self) -> int: + r""" + Number of worker processes to use in data loader. + Default value is ``0``. + """ + return self._DATALOADER_NUM_WORKERS + + @DATALOADER_NUM_WORKERS.setter + def DATALOADER_NUM_WORKERS(self, dataloader_num_workers: int) -> None: + if dataloader_num_workers > 8: + self.logger.warning( + "Worker number 1-8 is generally sufficient, " + "too many workers might have negative impact on speed." + ) + self._DATALOADER_NUM_WORKERS = dataloader_num_workers + + @property + def DATALOADER_FETCHES_PER_WORKER(self) -> int: + r""" + Number of fetches per worker per batch to use in data loader. + Default value is ``4``. + """ + return self._DATALOADER_FETCHES_PER_WORKER + + @DATALOADER_FETCHES_PER_WORKER.setter + def DATALOADER_FETCHES_PER_WORKER(self, dataloader_fetches_per_worker: int) -> None: + self._DATALOADER_FETCHES_PER_WORKER = dataloader_fetches_per_worker + + @property + def DATALOADER_FETCHES_PER_BATCH(self) -> int: + r""" + Number of fetches per batch in data loader (read-only). + """ + return max(1, self.DATALOADER_NUM_WORKERS) * self.DATALOADER_FETCHES_PER_WORKER + + @property + def DATALOADER_PIN_MEMORY(self) -> bool: + r""" + Whether to use pin memory in data loader. + Default value is ``True``. + """ + return self._DATALOADER_PIN_MEMORY + + @DATALOADER_PIN_MEMORY.setter + def DATALOADER_PIN_MEMORY(self, dataloader_pin_memory: bool): + self._DATALOADER_PIN_MEMORY = dataloader_pin_memory + + @property + def CHECKPOINT_SAVE_INTERVAL(self) -> int: + r""" + Automatically save checkpoints every n epochs. + Default value is ``10``. + """ + return self._CHECKPOINT_SAVE_INTERVAL + + @CHECKPOINT_SAVE_INTERVAL.setter + def CHECKPOINT_SAVE_INTERVAL(self, checkpoint_save_interval: int) -> None: + self._CHECKPOINT_SAVE_INTERVAL = checkpoint_save_interval + + @property + def CHECKPOINT_SAVE_NUMBERS(self) -> int: + r""" + Maximal number of checkpoints to preserve at any point. + Default value is ``3``. + """ + return self._CHECKPOINT_SAVE_NUMBERS + + @CHECKPOINT_SAVE_NUMBERS.setter + def CHECKPOINT_SAVE_NUMBERS(self, checkpoint_save_numbers: int) -> None: + self._CHECKPOINT_SAVE_NUMBERS = checkpoint_save_numbers + + @property + def PRINT_LOSS_INTERVAL(self) -> int: + r""" + Print loss values every n epochs. + Default value is ``10``. + """ + return self._PRINT_LOSS_INTERVAL + + @PRINT_LOSS_INTERVAL.setter + def PRINT_LOSS_INTERVAL(self, print_loss_interval: int) -> None: + self._PRINT_LOSS_INTERVAL = print_loss_interval + + @property + def TENSORBOARD_FLUSH_SECS(self) -> int: + r""" + Flush tensorboard logs to file every n seconds. + Default values is ``5``. + """ + return self._TENSORBOARD_FLUSH_SECS + + @TENSORBOARD_FLUSH_SECS.setter + def TENSORBOARD_FLUSH_SECS(self, tensorboard_flush_secs: int) -> None: + self._TENSORBOARD_FLUSH_SECS = tensorboard_flush_secs + + @property + def ALLOW_TRAINING_INTERRUPTION(self) -> bool: + r""" + Allow interruption before model training converges. + Default values is ``True``. + """ + return self._ALLOW_TRAINING_INTERRUPTION + + @ALLOW_TRAINING_INTERRUPTION.setter + def ALLOW_TRAINING_INTERRUPTION(self, allow_training_interruption: bool) -> None: + self._ALLOW_TRAINING_INTERRUPTION = allow_training_interruption + + @property + def BEDTOOLS_PATH(self) -> str: + r""" + Path to bedtools executable. + Default value is ``bedtools``. + """ + return self._BEDTOOLS_PATH + + @BEDTOOLS_PATH.setter + def BEDTOOLS_PATH(self, bedtools_path: str) -> None: + self._BEDTOOLS_PATH = bedtools_path + set_bedtools_path(bedtools_path) + + +config = ConfigManager() + + +#---------------------------- Interruption handling ---------------------------- + +@logged +class DelayedKeyboardInterrupt: # pragma: no cover + + r""" + Shield a code block from keyboard interruptions, delaying handling + till the block is finished (adapted from + `https://stackoverflow.com/a/21919644 + `__). + """ + + def __init__(self): + self.signal_received = None + self.old_handler = None + + def __enter__(self): + self.signal_received = False + self.old_handler = signal.signal(signal.SIGINT, self._handler) + + def _handler(self, sig, frame): + self.signal_received = (sig, frame) + self.logger.debug("SIGINT received, delaying KeyboardInterrupt...") + + def __exit__(self, exc_type, exc_val, exc_tb): + signal.signal(signal.SIGINT, self.old_handler) + if self.signal_received: + self.old_handler(*self.signal_received) + + +#--------------------------- Constrained data frame ---------------------------- + +@logged +class ConstrainedDataFrame(pd.DataFrame): + + r""" + Data frame with certain format constraints + + Note + ---- + Format constraints are checked and maintained automatically. + """ + + def __init__(self, *args, **kwargs) -> None: + df = pd.DataFrame(*args, **kwargs) + df = self.rectify(df) + self.verify(df) + super().__init__(df) + + def __setitem__(self, key, value) -> None: + super().__setitem__(key, value) + self.verify(self) + + @property + def _constructor(self) -> type: + return type(self) + + @classmethod + def rectify(cls, df: pd.DataFrame) -> pd.DataFrame: + r""" + Rectify data frame for format integrity + + Parameters + ---------- + df + Data frame to be rectified + + Returns + ------- + rectified_df + Rectified data frame + """ + return df + + @classmethod + def verify(cls, df: pd.DataFrame) -> None: + r""" + Verify data frame for format integrity + + Parameters + ---------- + df + Data frame to be verified + """ + + @property + def df(self) -> pd.DataFrame: + r""" + Convert to regular data frame + """ + return pd.DataFrame(self) + + def __repr__(self) -> str: + r""" + Note + ---- + We need to explicitly call :func:`repr` on the regular data frame + to bypass integrity verification, because when the terminal is + too narrow, :mod:`pandas` would split the data frame internally, + causing format verification to fail. + """ + return repr(self.df) + + +#--------------------------- Other utility functions --------------------------- + +def get_chained_attr(x: Any, attr: str) -> Any: + r""" + Get attribute from an object, with support for chained attribute names. + + Parameters + ---------- + x + Object to get attribute from + attr + Attribute name + + Returns + ------- + attr_value + Attribute value + """ + for k in attr.split("."): + if not hasattr(x, k): + raise AttributeError(f"{attr} not found!") + x = getattr(x, k) + return x + + +def get_rs(x: RandomState = None) -> np.random.RandomState: + r""" + Get random state object + + Parameters + ---------- + x + Object that can be converted to a random state object + + Returns + ------- + rs + Random state object + """ + if isinstance(x, int): + return np.random.RandomState(x) + if isinstance(x, np.random.RandomState): + return x + return np.random + + +@logged +def run_command( + command: str, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + log_command: bool = True, print_output: bool = True, + err_message: Optional[Mapping[int, str]] = None, **kwargs +) -> Optional[List[str]]: + r""" + Run an external command and get realtime output + + Parameters + ---------- + command + A string containing the command to be executed + stdout + Where to redirect stdout + stderr + Where to redirect stderr + echo_command + Whether to log the command being printed (log level is INFO) + print_output + Whether to print stdout of the command. + If ``stdout`` is PIPE and ``print_output`` is set to False, + the output will be returned as a list of output lines. + err_message + Look up dict of error message (indexed by error code) + **kwargs + Other keyword arguments to be passed to :class:`subprocess.Popen` + + Returns + ------- + output_lines + A list of output lines (only returned if ``stdout`` is PIPE + and ``print_output`` is False) + """ + if log_command: + run_command.logger.info("Executing external command: %s", command) + executable = command.split(" ")[0] + with subprocess.Popen(command, stdout=stdout, stderr=stderr, + shell=True, **kwargs) as p: + if stdout == subprocess.PIPE: + prompt = f"{executable} ({p.pid}): " + output_lines = [] + + def _handle(line): + line = line.strip().decode() + if print_output: + print(prompt + line) + else: + output_lines.append(line) + + while True: + _handle(p.stdout.readline()) + ret = p.poll() + if ret is not None: + # Handle output between last readlines and successful poll + for line in p.stdout.readlines(): + _handle(line) + break + else: + output_lines = None + ret = p.wait() + if ret != 0: + err_message = err_message or {} + if ret in err_message: + err_message = " " + err_message[ret] + elif "__default__" in err_message: + err_message = " " + err_message["__default__"] + else: + err_message = "" + raise RuntimeError( + f"{executable} exited with error code: {ret}.{err_message}") + if stdout == subprocess.PIPE and not print_output: + return output_lines + diff --git a/scglue/utils.py b/scglue/utils.py index fbdf8ba..a3bc480 100644 --- a/scglue/utils.py +++ b/scglue/utils.py @@ -676,44 +676,3 @@ def _handle(line): if stdout == subprocess.PIPE and not print_output: return output_lines - -def clr(adata:AnnData, inplace= True, axis= 0): - """ - Apply the centered log ratio (CLR) transformation - to normalize counts in adata.X. - - Args: - data: AnnData object with protein expression counts. - inplace: Whether to update adata.X inplace. - axis: Axis across which CLR is performed. - """ - - if axis not in [0, 1]: - raise ValueError("Invalid value for `axis` provided. Admissible options are `0` and `1`.") - - if not inplace: - adata = adata.copy() - - if issparse(adata.X) and axis == 0 and not isinstance(adata.X, csc_matrix): - warn("adata.X is sparse but not in CSC format. Converting to CSC.") - x = csc_matrix(adata.X) - elif issparse(adata.X) and axis == 1 and not isinstance(adata.X, csr_matrix): - warn("adata.X is sparse but not in CSR format. Converting to CSR.") - x = csr_matrix(adata.X) - else: - x = adata.X - - if issparse(x): - x.data /= np.repeat( - np.exp(np.log1p(x).sum(axis=axis).A / x.shape[axis]), x.getnnz(axis=axis) - ) - np.log1p(x.data, out=x.data) - else: - np.log1p( - x / np.exp(np.log1p(x).sum(axis=axis, keepdims=True) / x.shape[axis]), - out=x, - ) - - adata.X = x - - return None if inplace else adata \ No newline at end of file