├── .gitignore ├── GSPA_example.ipynb ├── LICENSE.md ├── Overview.png ├── README.md ├── data └── example.npz ├── gspa ├── __init__.py ├── embedding.py ├── graphs.py ├── gspa.py ├── version.py └── wavelets.py ├── requirements.txt ├── setup.py └── tests └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints/ 2 | __pycache__/ 3 | dist/ 4 | gspa.egg-info/ 5 | gspa/.ipynb_checkpoints/ 6 | gspa/__pycache__/ 7 | -------------------------------------------------------------------------------- /GSPA_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "4bcd03c7-c008-4e71-bcb1-398cfa6a3dca", 7 | "metadata": {}, 8 | "outputs": [ 9 | { 10 | "name": "stderr", 11 | "output_type": "stream", 12 | "text": [ 13 | "2024-06-17 17:53:02.924034: I tensorflow/core/util/port.cc:110] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.\n", 14 | "2024-06-17 17:53:02.925743: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.\n", 15 | "2024-06-17 17:53:02.960350: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.\n", 16 | "2024-06-17 17:53:02.961276: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.\n", 17 | "To enable the following instructions: AVX2 AVX512F AVX512_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.\n", 18 | "2024-06-17 17:53:03.605873: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT\n" 19 | ] 20 | } 21 | ], 22 | "source": [ 23 | "import numpy as np\n", 24 | "import gspa\n", 25 | "import scanpy, phate" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 12, 31 | "id": "ee7d01b2-edd0-4074-bf87-099dffbe42fe", 32 | "metadata": {}, 33 | "outputs": [ 34 | { 35 | "name": "stdout", 36 | "output_type": "stream", 37 | "text": [ 38 | "graphtools 1.5.3\n", 39 | "tensorflow 2.13.0\n", 40 | "keras 2.13.1\n", 41 | "numpy 1.22.4\n", 42 | "sklearn 1.3.2\n", 43 | "scipy 1.10.1\n", 44 | "tqdm 4.66.4\n", 45 | "scanpy 1.9.3\n", 46 | "phate 1.0.11\n" 47 | ] 48 | } 49 | ], 50 | "source": [ 51 | "import graphtools\n", 52 | "print('graphtools', graphtools.__version__)\n", 53 | "import tensorflow\n", 54 | "print('tensorflow', tensorflow.__version__)\n", 55 | "import keras\n", 56 | "print('keras', keras.__version__)\n", 57 | "import numpy as np\n", 58 | "print ('numpy', np.__version__)\n", 59 | "import sklearn\n", 60 | "print ('sklearn', sklearn.__version__)\n", 61 | "import scipy\n", 62 | "print ('scipy', scipy.__version__)\n", 63 | "import tqdm\n", 64 | "print ('tqdm', tqdm.__version__)\n", 65 | "import scanpy\n", 66 | "print ('scanpy', scanpy.__version__)\n", 67 | "import phate\n", 68 | "print ('phate', phate.__version__)" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": 2, 74 | "id": "8ab5ee38-60cc-4b48-aa07-3d3b6cdd552b", 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "data = np.load(f'data/example.npz')" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 3, 84 | "id": "983862b5-c373-4b77-ac46-f8d665abdd16", 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "adata = scanpy.AnnData(data['counts'], obs={'pseudotime': data['pseudotime']}, dtype=np.float64)\n", 89 | "scanpy.pp.highly_variable_genes(adata)\n", 90 | "\n", 91 | "# gene_adata stores genes as observations and cells as variables\n", 92 | "gene_adata = adata[:, adata.var['highly_variable']].copy().T" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 4, 98 | "id": "f4be7185-e42b-4098-910f-09340340ead6", 99 | "metadata": {}, 100 | "outputs": [], 101 | "source": [ 102 | "gspa_op = gspa.GSPA(verbose=False)\n", 103 | "gspa_op.construct_graph(adata.to_df())\n", 104 | "gspa_op.build_diffusion_operator()\n", 105 | "gspa_op.build_wavelet_dictionary()" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 5, 111 | "id": "08da314d-84e6-4565-9ebb-cb34754eaf95", 112 | "metadata": {}, 113 | "outputs": [ 114 | { 115 | "name": "stderr", 116 | "output_type": "stream", 117 | "text": [ 118 | "2024-06-17 17:53:16.410180: E tensorflow/compiler/xla/stream_executor/cuda/cuda_driver.cc:268] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected\n" 119 | ] 120 | }, 121 | { 122 | "data": { 123 | "image/png": "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EZPJxGOPPca2bds+UVu/9rWv8fLLL/Pss88meygAvF4v3/3ud5k4cSJGoxGj0YjT6cTn8x32Pd98802AlFk0EB86czgcvPHGGyn7Z8+ezbhx45LbVquVyZMnpwzrCCE+ORlSEUet7OxsbDbbgA+Op59+Gr/fT319PRdddFHKe6qqsmjRomR3us/n46abbuLvf/87jz/+OF/96leTx55yyin88pe/RFEU7HY7EyZMSMkV6Z6RcumllybzB9xuN6eccgrLli3jN7/5Denp6Yf1+drb29F1fcDhot45KQDNzc2HTFJ97rnnuOKKK7j88su54447yM/Px2g08tBDD32i4OjHP/4xDz/8MI899hjnnntuynuf+9zneOONN7j77rs5/vjjcblcKIrC0qVLCQQCh3W/1tZWjEZjyrAWxHNK8vPzkz+TbllZWf2uYbFYDvv+QoiBScAhjloGg4EzzzyTV199lfr6+pQH87Rp0wAGnGbZl8Ph4M477+Tvf/87mzdvTnnP7XYzf/78Ac/zeDwsW7YMgOOPP37AY55++umUAGYoMjIyUFWV+vr6fu/V1dUB8aALICcnh5qamoNe789//jPl5eX8/e9/T8ktCYVCh9U+iOev3H333dx7773ceOONKe95PB5eeukl7rnnHr73ve+l3K+tre2w75mVlUU0GqW5uTkl6NB1nYaGhgP+WQghhpcMqYij2p133ommadx8881EIpFDHj/QwxtIdu939xwMxtNPP00gEOBHP/oRK1as6PfKzs7+RD0HDoeDE088keeeey7l23gsFuPPf/4zxcXFTJ48GYDzzjuPFStWJIdYBqIoCmazOSXYaGhoGNQslYEsX76cL37xi9x4443cc889A95P1/WUBE6AP/zhD2ialrKv+5jB9DosXrwYiAdQvS1btgyfz5d8Xwjx6ZIeDnFUO/nkk/ntb3/Lrbfeyty5c/nSl77E9OnTkz0D3T0QLpcLgOnTp7N48WLOO+88JkyYQDAY5P333+fnP/85eXl53HTTTYO+92OPPUZGRga33347Vqu13/vXXXcdv/jFL9iwYQPHHXfcYX2+++67j7PPPpszzjiD22+/HbPZzO9+9zs2b97MX//612Tw8MMf/pBXXnmFRYsWcddddzFz5kw6OjpYvnw5t912G1OmTOGCCy7gueee46tf/SqXXXYZ1dXV/OhH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", 124 | "text/plain": [ 125 | "
" 126 | ] 127 | }, 128 | "metadata": {}, 129 | "output_type": "display_data" 130 | } 131 | ], 132 | "source": [ 133 | "gene_adata.obsm['X_AE'], gene_adata.obsm['X_PC'] = gspa_op.get_gene_embeddings(gene_adata.to_df())\n", 134 | "gene_adata.obs['GSPA_localization'] = gspa_op.calculate_localization()\n", 135 | "gene_adata.obsm['X_phate'] = phate.PHATE(verbose=False, random_state=42).fit_transform(gene_adata.obsm['X_AE'])\n", 136 | "scanpy.external.pl.phate(gene_adata, color=['GSPA_localization'], cmap='PuBuGn', sort_order=False,\n", 137 | " vmax=np.percentile(gene_adata.obs['GSPA_localization'], 99.5))" 138 | ] 139 | } 140 | ], 141 | "metadata": { 142 | "kernelspec": { 143 | "display_name": "Python 3 (ipykernel)", 144 | "language": "python", 145 | "name": "python3" 146 | }, 147 | "language_info": { 148 | "codemirror_mode": { 149 | "name": "ipython", 150 | "version": 3 151 | }, 152 | "file_extension": ".py", 153 | "mimetype": "text/x-python", 154 | "name": "python", 155 | "nbconvert_exporter": "python", 156 | "pygments_lexer": "ipython3", 157 | "version": "3.8.18" 158 | } 159 | }, 160 | "nbformat": 4, 161 | "nbformat_minor": 5 162 | } 163 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ---------------------------------- 2 | 3 | Non-Commercial License 4 | Yale Copyright © 2024 Yale University. 5 | 6 | Permission is hereby granted to use, copy, modify, and distribute this Software for any non-commercial purpose. Any distribution or modification or derivations of the Software (together “Derivative Works”) must be made available on GitHub and shall include this copyright notice and this permission notice in all copies or substantial portions of the Software. For the purposes of this license, "non-commercial" means not intended for or directed towards commercial advantage or monetary compensation either via the Software itself or Derivative Works or uses of either which lead to or generate any commercial products. In any event, the use and modification of the Software or Derivative Works shall remain governed by the terms and conditions of this Agreement; Any commercial use of the Software requires a separate commercial license from the copyright holder at Yale University. Direct any requests for commercial licenses to Yale Ventures at yaleventures@yale.edu. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | ---------------------------------- 11 | -------------------------------------------------------------------------------- /Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrishnaswamyLab/Gene-Signal-Pattern-Analysis/163d4bcde8b48fd79051d4a3ec75f585d6a0349a/Overview.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gene Signal Pattern Analysis 2 | ### Mapping the gene space at single-cell resolution 3 | 4 | Gene Signal Pattern Analysis is a Python package for mapping the gene space from single-cell data. For a detailed explanation of GSPA and potential downstream application, see: 5 | 6 | [**Mapping the gene space at single-cell resolution with Gene Signal Pattern Analysis**. Aarthi Venkat, Sam Leone, Scott E. Youlten, Eric Fagerberg, John Attanasio, Nikhil S. Joshi, Michael Perlmutter, Smita Krishnaswamy.](https://www.biorxiv.org/content/10.1101/2023.11.26.568492v1) 7 | 8 | By considering gene expression values as signals on the cell-cell graph, GSPA enables complex analyses of gene-gene relationships, including gene cluster analysis, cell-cell communication, and patient manifold learning from gene-gene graphs. 9 | 10 | ### Installation 11 | 12 | ``` 13 | pip install gspa 14 | ``` 15 | 16 | ### Requirements 17 | 18 | GSPA requires Python >= 3.6. All other requirements are automatically installed by ``pip`` (see also requirements.txt). 19 | 20 | The following have been tested: Python 3.6.15 (graphtools 1.5.3, tensorflow 2.6.2, keras 2.6.0, numpy 1.19.5, sklearn 0.24.2, scipy 1.5.4, tqdm 4.64.1, scanpy 1.7.2, phate 1.0.11) and Python 3.8.18 (graphtools 1.5.3, tensorflow 2.13.0, keras 2.13.1, numpy 1.22.4, sklearn 1.3.2, scipy 1.10.1, tqdm 4.66.4, scanpy 1.9.3, phate 1.0.11) 21 | 22 | ### Usage example 23 | 24 | ``` 25 | import numpy as np 26 | import gspa 27 | 28 | # Create toy data 29 | n_cells = 1000 30 | n_genes = 50 31 | data = np.random.normal(size=(n_cells, n_genes)) 32 | 33 | # GSPA operator constructs wavelet dictionary 34 | gspa_op = gspa.GSPA() 35 | gspa_op.construct_graph(data) 36 | gspa_op.build_diffusion_operator() 37 | gspa_op.build_wavelet_dictionary() 38 | 39 | # Embed gene signals from wavelet dictionary 40 | gene_signals = data.T # embed all measured genes 41 | gene_ae, gene_pc = gspa_op.get_gene_embeddings(gene_signals) 42 | gene_localization = gspa_op.calculate_localization() 43 | ``` 44 | See `GSPA_example.ipynb` [above](https://github.com/KrishnaswamyLab/Gene-Signal-Pattern-Analysis) for test run on simulated single-cell data. More notebooks to generate paper figures available at [https://github.com/KrishnaswamyLab/GSPA-manuscript-analyses](https://github.com/KrishnaswamyLab/GSPA-manuscript-analyses). 45 | 46 | ![](Overview.png) 47 | -------------------------------------------------------------------------------- /data/example.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrishnaswamyLab/Gene-Signal-Pattern-Analysis/163d4bcde8b48fd79051d4a3ec75f585d6a0349a/data/example.npz -------------------------------------------------------------------------------- /gspa/__init__.py: -------------------------------------------------------------------------------- 1 | from .gspa import GSPA 2 | import gspa.embedding 3 | import gspa.graphs 4 | import gspa.wavelets 5 | from .version import __version__ 6 | -------------------------------------------------------------------------------- /gspa/embedding.py: -------------------------------------------------------------------------------- 1 | from sklearn import decomposition 2 | import tensorflow as tf 3 | import keras 4 | import numpy as np 5 | 6 | def project(signals, cell_dictionary): 7 | norms = np.linalg.norm(signals, axis=1).reshape(-1, 1) 8 | norms[norms == 0] = 1 # Avoid division by zero by setting zero norms to one 9 | signals = signals / norms 10 | return np.dot(signals, cell_dictionary) 11 | 12 | def svd(signals, random_state=1234, n_components=2048): 13 | n_components = min(n_components, signals.shape[0], signals.shape[1]) 14 | pc_op = decomposition.PCA(n_components=n_components, random_state=random_state) 15 | data_pc = pc_op.fit_transform(signals) 16 | 17 | # normalize before autoencoder 18 | data_pc_std = data_pc / np.std(data_pc[:, 0]) 19 | 20 | return (data_pc_std) 21 | 22 | def run_ae(data, random_state=1234, act='relu', bias=1, dim=128, num_layers=2, dropout=0.0, lr=0.001, epochs=100, 23 | val_prop=0.05, weight_decay=0, patience=10, verbose=True): 24 | try: 25 | keras.utils.set_random_seed(random_state) 26 | except: 27 | tf.random.set_seed(random_state) # unstable for TF > 2.7 28 | 29 | # encoder 30 | input = keras.Input(shape=(data.shape[1])) 31 | encoded = keras.layers.Dense(dim * 2, activation=act, use_bias=bias)(input) 32 | if dropout > 0: 33 | encoded = keras.layers.Dropout(dropout)(encoded) 34 | for i in range(num_layers - 2): 35 | encoded = keras.layers.Dense(dim * 2, activation=act, use_bias=bias)(encoded) 36 | if dropout > 0: 37 | encoded = keras.layers.Dropout(dropout)(encoded) 38 | 39 | encoded = keras.layers.Dense(dim, activation='linear', use_bias=bias)(encoded) 40 | 41 | # decoder 42 | decoded = keras.layers.Dense(dim * 2, activation=act, use_bias=bias)(encoded) 43 | for i in range(num_layers - 2): 44 | decoded = keras.layers.Dense(dim * 2, activation=act, use_bias=bias)(decoded) 45 | decoded = keras.layers.Dense(data.shape[1], activation='linear', use_bias=bias)(decoded) 46 | 47 | # autoencoder 48 | autoencoder = keras.Model(input, decoded) 49 | encoder = keras.Model(input, encoded) 50 | try: 51 | autoencoder.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=lr, decay=weight_decay), 52 | loss='mean_squared_error') 53 | except ValueError: 54 | 55 | autoencoder.compile(optimizer=tf.keras.optimizers.legacy.Adam(learning_rate=lr, decay=weight_decay), 56 | loss='mean_squared_error') 57 | 58 | callback = keras.callbacks.EarlyStopping(monitor="val_loss", patience=patience) 59 | 60 | history = autoencoder.fit(data, data, 61 | verbose=verbose, 62 | validation_split = val_prop, 63 | epochs=epochs, 64 | shuffle=True, 65 | callbacks=[callback]) 66 | 67 | embedding = encoder(data).numpy() 68 | 69 | return (embedding) 70 | -------------------------------------------------------------------------------- /gspa/graphs.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import multiscale_phate as mp 3 | 4 | def graph_condensation(data, random_state=42, n_jobs=-1, condensation_threshold=10000, n_pca=100): 5 | mp_op = mp.Multiscale_PHATE(random_state=random_state, n_jobs=n_jobs, n_pca=n_pca) 6 | levels = mp_op.fit(data) 7 | number_of_condensed_points = np.array([np.unique(x).shape[0] for x in mp_op.NxTs]) 8 | condensed_level = np.argwhere(number_of_condensed_points <= condensation_threshold)[0][0] 9 | return (np.array(mp_op.NxTs[condensed_level])) 10 | 11 | def aggregate_signals_over_condensed_nodes(data, condensation_groupings): 12 | clust_unique, clust_unique_ids = np.unique(condensation_groupings, return_index=True) 13 | loc = [] 14 | for c in clust_unique: 15 | loc.append(np.where(condensation_groupings == c)[0]) 16 | 17 | counts_condensed = [] 18 | for l in loc: 19 | counts_condensed.append(data[l].mean(axis=0)) 20 | 21 | return (np.array(counts_condensed)) 22 | -------------------------------------------------------------------------------- /gspa/gspa.py: -------------------------------------------------------------------------------- 1 | import tasklogger 2 | import graphtools 3 | from . import graphs, wavelets, embedding 4 | import numpy as np 5 | from tqdm import tqdm 6 | from scipy import sparse, spatial 7 | import os 8 | 9 | os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0' 10 | _logger = tasklogger.get_tasklogger("graphtools") 11 | 12 | class GSPA: 13 | """GSPA operator which performs gene dimensionality reduction. 14 | 15 | Gene Signal Pattern Analysis (GSPA) considers genes as signals on a cell-cell graph, enabling mapping the gene space for complex gene-level analyses, including gene cluster analysis, cell-cell communication, and patient manifold learning from gene-gene graphs (Venkat et al. [1]_). 16 | 17 | Parameters 18 | ---------- 19 | graph : graphtools.Graph, optional, default: None 20 | Cell-cell affinity graph. If None, `construct_graph` function will need to be run to construct graph from data directly. 21 | diffusion_operator : array-like, shape=[n_samples, n_samples], default: None 22 | Cell-cell diffusion operator. If None `build_diffusion_operator` will need to be run based on the cell-cell affinity graph. 23 | qr_decompose : boolean, default: True 24 | If True, composes reduced wavelet dictionary with QR decomposition 25 | qr_epsilon: float, optional, default: 1e-3 26 | If qr_decompose is True, qr_epsilon determines threshold for QR decomposition 27 | wavelet_J: int, optional, default: -1 28 | Maximum number of scales J. If -1, uses J=log(number of cells) based on Tong et al. [2]_. 29 | wavelet_power: int, optional, default: 2 30 | Geometric seqence of ratio wavelet_ower for wavelet transforms. 31 | embedding_dim: int, optional, default: 128 32 | Number of dimensions in which genes will be embedded with autoencoder. 33 | pc_dim: int, optional, default: 2048 34 | Number of dimensions in which genes will be embedded with PCA. 35 | random_state: int, default: 42 36 | Integer random seed for GSPA pipeline. 37 | verbose: boolean, optional, default: True 38 | If True, print status messages 39 | n_jobs: integer, optional, default: -1 40 | The number of jobs to use for computation. If -1 all CPUS are used. 41 | perform_condensation: boolean, optional, default: True 42 | If True, perform graph condensation for large graphs. 43 | condensation_threshold: int, optional, default: 10000 44 | If perform_condensation is True, graph condensation occurrs for graphs with more than condensation_threshold cells. 45 | bc_sample_idx: array-like, shape=[n_samples, 1], default: None 46 | Batch labels. If provided, bc_sample_idx is used to construct mutual nearest neighbors (MNN) graph for batch correction. 47 | bc_theta: float, optional, default: 0.95 48 | If batch labels bc_sample_idx provided, bc_theta is used to parametrize MNN symmetrization. 49 | activation: string, optional, default: relu 50 | Activation function in `keras.activations` between layers for autoencoder. 51 | bias: boolean, optional, default: 1 52 | If 1, autoencoder layers use bias vector. 53 | num_layers: int, optional, default: 2 54 | Number of dense layers within encoder (decoder) of autoencoder. 55 | dropout: float, optional, default: 0.0 56 | If dropout > 0, adds dropout layers between dense layers with `dropout` fraction of input units dropped. 57 | lr: float, optional, default: 0.001 58 | Learning rate for model Adam optimizer. 59 | weight_decay: float, optional, default: 0.0 60 | If set, weight_decay is applied to Adam optimizer. 61 | epochs: int, optional, default: 100 62 | Number of epochs for model training. 63 | val_prop: float, optional, default: 0.05 64 | Proportion of data heldout for validation set used for early stopping. 65 | patience: int, optional, default: 10 66 | Number of epochs with no improvement to validation loss after which training will be stopped. 67 | 68 | Attributes 69 | ---------- 70 | condensation_groupings: array-like, shape=[n_samples, 1] 71 | If perform_condensation is True and graph size is greater than condensation_threshold, MS PHATE (Kuchroo et al. [3]_) computes a new node assignment for each cell, where cells are grouped into nodes based on diffusion condensation. 72 | wavelet_dictionary: array-like, shape=[n_samples, n_wavelet_dictionary_dimensions] 73 | Stores wavelet dictionary vector for each cell after `build_wavelet_dictionary` is run. 74 | signals_projected: array-like, shape=[n_features, n_wavelet_dictionary_dimensions] 75 | Stores gene signals projected onto wavelet dictionary from `get_gene_embeddings`. 76 | 77 | Examples 78 | ---------- 79 | 80 | >>> import gspa 81 | >>> import scprep 82 | >>> import numpy as np 83 | >>> data = np.random.normal(size=(1000, 50)) # fake dataset with 1000 cells and 50 genes 84 | >>> gspa_op = gspa.GSPA() 85 | >>> gspa_op.construct_graph(data) 86 | >>> gspa_op.build_diffusion_operator() 87 | >>> gspa_op.build_wavelet_dictionary() 88 | >>> gene_ae, gene_pc = gspa_op.get_gene_embeddings(data.T) 89 | >>> gene_localization = gspa_op.calculate_localization() 90 | >>> gene_phate = phate.PHATE().fit_transform(gene_ae) 91 | >>> scprep.plot.scatter2d(gene_phate, c=gene_localization, cmap='PuBuGn') 92 | 93 | References 94 | ---------- 95 | .. [1] Venkat A, Leone S, Youlten SE, Fagerberg E, Attansio J, Joshi NS, Perlmutter M, Krishnaswamy S, *Mapping the gene space at single-cell resolution with gene signal pattern analysis* `BioRxiv `_. 96 | .. [2] Tong A, Huguet G, Shung D, Natik A, Kuchroo M, Lajoie G, Wolf G, Krishnaswamy S, *Embedding Signals on Knowledge Graphs with Unbalanced Diffusion Earth Mover's Distance* `arXiv `_. 97 | .. [3] Kuchroo et al, *Multiscale PHATE identifies multimodal signatures of COVID-19* ``_. 98 | """ 99 | 100 | def __init__(self, 101 | graph=None, 102 | diffusion_operator=None, 103 | qr_decompose=True, 104 | qr_epsilon=1e-3, 105 | wavelet_J=-1, 106 | wavelet_power=2, 107 | embedding_dim=128, 108 | pc_dim=2048, 109 | random_state=42, 110 | verbose=True, 111 | n_jobs=-1, 112 | perform_condensation=True, 113 | condensation_threshold=10000, 114 | bc_sample_idx=None, 115 | bc_theta=0.95, 116 | activation='relu', 117 | bias=1, 118 | num_layers=2, 119 | dropout=0.0, 120 | lr=0.001, 121 | weight_decay=0.0, 122 | epochs=100, 123 | val_prop=0.05, 124 | patience=10, 125 | ): 126 | 127 | self.graph = graph 128 | self.diff_op = diffusion_operator 129 | self.qr_decompose = qr_decompose 130 | self.qr_epsilon = qr_epsilon 131 | self.wavelet_J = wavelet_J 132 | self.wavelet_power = wavelet_power 133 | self.embedding_dim = embedding_dim 134 | self.pc_dim = pc_dim 135 | self.random_state = random_state 136 | self.verbose = verbose 137 | self.n_jobs = n_jobs 138 | self.perform_condensation = perform_condensation 139 | self.condensation_threshold = condensation_threshold 140 | self.bc_sample_idx = bc_sample_idx 141 | self.bc_theta = bc_theta 142 | self.activation = activation 143 | self.bias = bias 144 | self.num_layers = num_layers 145 | self.dropout = dropout 146 | self.lr = lr 147 | self.epochs = epochs 148 | self.val_prop = val_prop 149 | self.weight_decay = weight_decay 150 | self.patience = patience 151 | 152 | self.condensation_groupings = None 153 | self.wavelet_dictionary = None 154 | self.wavelet_sizes = None 155 | self.signals_projected = None 156 | 157 | _logger.set_level(self.verbose) 158 | 159 | def construct_graph(self, data): 160 | """Constructs cell-cell affinity graph. 161 | 162 | Parameters 163 | ---------- 164 | data: array-like, shape=[n_samples, n_features] 165 | input data with `n_samples` samples and `n_features` features. Accepted data types: `numpy.ndarray`, `pd.DataFrame`. 166 | 167 | """ 168 | if (data.shape[0] > self.condensation_threshold) & (self.perform_condensation): 169 | _logger.log_info("Dataset is larger than %s cells. Running graph condensation. Set perform_condensation=False to run exact GSPA." % self.condensation_threshold) 170 | self.condensation_groupings = graphs.graph_condensation(data, random_state=self.random_state, 171 | n_jobs=self.n_jobs, 172 | condensation_threshold=self.condensation_threshold, 173 | n_pca=self.pc_dim) 174 | 175 | data = graphs.aggregate_signals_over_condensed_nodes(data, self.condensation_groupings) 176 | if self.bc_sample_idx is None: 177 | self.graph = graphtools.Graph(data, n_pca=100, random_state=self.random_state, verbose=self.verbose, use_pygsp=True) 178 | else: 179 | _logger.log_info(f"bc_sample_idx used for batch correction") 180 | self.graph = graphtools.Graph(data, n_pca=100, sample_idx=self.bc_sample_idx, kernel_symm='mnn', 181 | theta=self.bc_theta, random_state=self.random_state, verbose=self.verbose, use_pygsp=True) 182 | 183 | def build_diffusion_operator(self): 184 | """Constructs diffusion operator from graph. 185 | """ 186 | if self.graph is None: 187 | raise ValueError('Graph not constructed. Run gspa_op.construct_graph(data) or initialize GSPA operator with graph') 188 | else: 189 | self.graph = self.graph.to_pygsp() 190 | 191 | Dmin1 = np.diag([1/np.sum(row) for row in self.graph.A]) 192 | self.diff_op = 1/2 * (np.eye(self.graph.N)+self.graph.A@Dmin1) 193 | 194 | def build_wavelet_dictionary(self): 195 | """Constructs wavelet dictionary from diffusion operator. 196 | """ 197 | if self.diff_op is None: 198 | raise ValueError('Diffusion operator not constructed. Run gspa_op.build_diffusion_operator() or initialize GSPA operator with diffusion_operator') 199 | wavelet_sizes = [] 200 | 201 | if self.graph is not None: 202 | self.graph = self.graph.to_pygsp() 203 | 204 | if sparse.issparse(self.diff_op): 205 | self.diff_op = self.diff_op.toarray() 206 | 207 | N = self.diff_op.shape[0] 208 | if self.wavelet_J == -1: 209 | self.wavelet_J = int(np.log(N)) 210 | I = np.eye(N) 211 | I = wavelets.normalize(I) 212 | wavelet_dictionary = [I] 213 | wavelet_sizes.append(I.shape[1]) 214 | P_j = np.linalg.matrix_power(self.diff_op, self.wavelet_power) 215 | 216 | if self.qr_decompose: 217 | Psi_j_tilde = wavelets.column_subset(I-P_j, epsilon=self.qr_epsilon) 218 | 219 | if Psi_j_tilde.shape[1] == 0: 220 | _logger.log_info(f"Wavelets calculated; J = 1") 221 | self.wavelet_dictionary, self.wavelet_sizes = (wavelets.flatten(wavelet_dictionary, wavelet_sizes)) 222 | 223 | Psi_j_tilde = wavelets.normalize(Psi_j_tilde) 224 | wavelet_sizes.append(Psi_j_tilde.shape[1]) 225 | wavelet_dictionary += [Psi_j_tilde] 226 | 227 | for i in tqdm(range(2,self.wavelet_J), disable=self.verbose==False): 228 | P_j_new = np.linalg.matrix_power(P_j,self.wavelet_power) 229 | Psi_j = P_j - P_j_new 230 | P_j = P_j_new 231 | Psi_j_tilde = wavelets.column_subset(Psi_j, epsilon=self.qr_epsilon) 232 | if Psi_j_tilde.shape[1] == 0: 233 | _logger.log_info("Wavelets calculated; J = %s" %i) 234 | self.wavelet_dictionary, self.wavelet_sizes = (wavelets.flatten(wavelet_dictionary, wavelet_sizes)) 235 | 236 | Psi_j_tilde = wavelets.normalize(Psi_j_tilde) 237 | 238 | wavelet_sizes.append(Psi_j_tilde.shape[1]) 239 | wavelet_dictionary += [Psi_j_tilde] 240 | else: 241 | _logger.log_info("Calculating Wavelets J = %s" % self.wavelet_J) 242 | wavelet_dictionary += [I-P_j] 243 | wavelet_sizes.append((I-P_j).shape[1]) 244 | for i in tqdm(range(2,self.wavelet_J), disable=self.verbose==False): 245 | P_j_new = np.linalg.matrix_power(P_j,self.wavelet_power) 246 | Psi_j = P_j - P_j_new 247 | P_j = P_j_new 248 | Psi_j = wavelets.normalize(Psi_j) 249 | wavelet_sizes.append(Psi_j.shape[1]) 250 | wavelet_dictionary += [Psi_j] 251 | 252 | self.wavelet_dictionary, self.wavelet_sizes = wavelets.flatten(wavelet_dictionary, wavelet_sizes) 253 | 254 | def get_gene_embeddings(self, signals): 255 | """Get gene features embedded in principle component space and autoencoded space. 256 | 257 | Parameters 258 | ---------- 259 | signals: array-like, shape=[n_features, n_samples] 260 | Input signals defined on nodes of cell-cell graph. Accepted data types: `numpy.ndarray`, `pd.DataFrame`. 261 | 262 | Returns 263 | ---------- 264 | signals_ae: array, shape=[n_features, embedding_dim] 265 | Signals embedded with autoencoder into `embedding_dim`-dimensional space. 266 | signals_pc: array, shape=[n_features, pc_dim] 267 | Signals embedded with PCA into `pc_dim`-dimensional space. 268 | """ 269 | 270 | if self.wavelet_dictionary is None: 271 | raise ValueError('Run gspa_op.build_wavelet_dictionary') 272 | 273 | if self.condensation_groupings is not None: 274 | signals = graphs.aggregate_signals_over_condensed_nodes(signals.T, self.condensation_groupings).T 275 | 276 | self.signals_projected = embedding.project(signals, self.wavelet_dictionary) 277 | signals_pc = embedding.svd(self.signals_projected, n_components=self.pc_dim, random_state=self.random_state) 278 | signals_ae = embedding.run_ae(signals_pc, verbose=self.verbose, random_state=self.random_state, act=self.activation, bias=self.bias, 279 | dim=self.embedding_dim, num_layers=self.num_layers, dropout=self.dropout, lr=self.lr, 280 | epochs=self.epochs, val_prop=self.val_prop, weight_decay=self.weight_decay, patience=self.patience) 281 | 282 | return (signals_ae, signals_pc) 283 | 284 | def calculate_localization(self, signals=None): 285 | """Calculates localization for signals. 286 | 287 | Parameters 288 | ---------- 289 | signals: array-like, optional, shape=[n_features, n_samples] 290 | Input signals defined on nodes of cell-cell graph. Accepted data types: `numpy.ndarray`, `pd.DataFrame`. If signals is None, calculates localization for gene signals inputted to `get_gene_embeddings`. 291 | 292 | Returns 293 | ---------- 294 | localization_score: array, shape=[n_features,] 295 | Localization score for each gene, where higher score indicates the gene is more localized on the cell-cell graph. 296 | """ 297 | 298 | if self.wavelet_dictionary is None: 299 | raise ValueError('Run gspa_op.build_wavelet_dictionary') 300 | if signals is not None: 301 | _logger.log_info(f"Computing localization with provided signals.") 302 | if self.condensation_groupings is not None: 303 | signals = graphs.aggregate_signals_over_condensed_nodes(signals.T, self.condensation_groupings).T 304 | signals_projected = embedding.project(signals, self.wavelet_dictionary) 305 | uniform_signal = np.ones((1, self.wavelet_dictionary.shape[0])) 306 | uniform_projected = embedding.project(uniform_signal, self.wavelet_dictionary) 307 | localization_score = spatial.distance.cdist(uniform_projected, signals_projected).reshape(-1,) 308 | 309 | else: 310 | if self.signals_projected is None: 311 | raise ValueError('Provide signals to map to dictionary or run gspa_op.get_gene_embeddings') 312 | else: 313 | _logger.log_info(f"Computing localization with signals used for gene embedding.") 314 | uniform_signal = np.ones((1, self.wavelet_dictionary.shape[0])) 315 | uniform_projected = embedding.project(uniform_signal, self.wavelet_dictionary) 316 | localization_score = spatial.distance.cdist(uniform_projected, self.signals_projected).reshape(-1,) 317 | 318 | return (localization_score) 319 | 320 | def calculate_cell_type_specificity(self, cell_type_assignments, cell_type, signals=None): 321 | """Calculates cell type specificity for each signal to provided cell type of interest. 322 | 323 | Parameters 324 | ---------- 325 | cell_type_assignments: array-like, shape=[n_samples,] 326 | Cluster or cell type assignments to cell nodes. 327 | cell_type: string 328 | Cluster name or cell type of interest. 329 | signals: array-like, optional, shape=[n_features, n_samples] 330 | Input signals defined on nodes of cell-cell graph. Accepted data types: `numpy.ndarray`, `pd.DataFrame`. If signals is None, calculates localization for gene signals inputted to `get_gene_embeddings`. 331 | 332 | Returns 333 | ---------- 334 | specificity_score: array, shape=[n_features,] 335 | Cell type specificity score for each gene, where higher score indicates the gene is more specific to provided cell type. 336 | """ 337 | 338 | cell_type_assignments = np.array(cell_type_assignments) 339 | if cell_type not in cell_type_assignments: 340 | raise ValueError('Cell type not found in cell type assignments') 341 | if self.wavelet_dictionary is None: 342 | raise ValueError('Run gspa_op.build_wavelet_dictionary') 343 | 344 | if signals is not None: 345 | _logger.log_info(f"Computing cell type specificity with provided signals.") 346 | if self.condensation_groupings is not None: 347 | signals = graphs.aggregate_signals_over_condensed_nodes(signals.T, self.condensation_groupings).T 348 | signals_projected = embedding.project(signals, self.wavelet_dictionary) 349 | 350 | cell_type_signal = (cell_type_assignments == cell_type).astype(int).reshape(1, -1) 351 | if self.condensation_groupings is not None: 352 | cell_type_signal = graphs.aggregate_signals_over_condensed_nodes(cell_type_signal.T, self.condensation_groupings).T 353 | cell_type_projected = embedding.project(cell_type_signal, self.wavelet_dictionary) 354 | specificity_score = -1*spatial.distance.cdist(cell_type_projected, signals_projected).reshape(-1,) 355 | 356 | else: 357 | if self.signals_projected is None: 358 | raise ValueError('Provide signals to map to dictionary or run gspa_op.get_gene_embeddings') 359 | else: 360 | _logger.log_info(f"Computing cell type specificity with signals used for gene embedding") 361 | cell_type_signal = (cell_type_assignments == cell_type).astype(int).reshape(1, -1) 362 | if self.condensation_groupings is not None: 363 | cell_type_signal = graphs.aggregate_signals_over_condensed_nodes(cell_type_signal.T, self.condensation_groupings).T 364 | cell_type_projected = embedding.project(cell_type_signal, self.wavelet_dictionary) 365 | specificity_score = -1*spatial.distance.cdist(cell_type_projected, self.signals_projected).reshape(-1,) 366 | 367 | return (specificity_score) 368 | -------------------------------------------------------------------------------- /gspa/version.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 Krishnaswamy Lab, Yale University 2 | 3 | __version__ = "1.1" 4 | -------------------------------------------------------------------------------- /gspa/wavelets.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.linalg import qr 3 | 4 | def normalize(A): 5 | """ 6 | Input: A, an n x m matrix 7 | Output: A with each column divided by its L2 norm 8 | """ 9 | 10 | for i in range(A.shape[1]): 11 | A[:,i]=A[:,i]/np.linalg.norm(A[:,i]) 12 | return A 13 | 14 | def column_subset(A,epsilon): 15 | """ 16 | Input: an m x n matrix A, tolerance epsilon 17 | Output: Subset of A's columns s.t. the projection of A into these columns; 18 | can approximate A with error < epsilon |A|_2 19 | """ 20 | 21 | R,P = qr(A,pivoting=True,mode='r') 22 | A_P = A[:,P] 23 | 24 | A_nrm = np.sum(A*A) 25 | tol = epsilon*A_nrm 26 | R_nrm = 0 27 | 28 | for i in range(0,R.shape[0]): 29 | R_nrm += np.sum(R[i]*R[i]) 30 | err = A_nrm-R_nrm 31 | if err < tol: 32 | return A_P[:,:i] 33 | 34 | return A_P 35 | 36 | def flatten(wavelet_list, size_of_wavelets_per_scale): 37 | N = wavelet_list[0].shape[0] 38 | flat_waves = np.zeros((N,np.sum(size_of_wavelets_per_scale))) 39 | curr = 0 40 | for i,wavelet in enumerate(wavelet_list): 41 | last = curr + size_of_wavelets_per_scale[i] 42 | flat_waves[:,curr:last] = wavelet 43 | curr = last 44 | 45 | return (np.array(flat_waves), np.array(size_of_wavelets_per_scale)) 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | graphtools>=1.5.0 2 | tensorflow>=2.6.0 3 | multiscale_phate==0.0 4 | numpy>=1.14.0 5 | scikit_learn 6 | scipy>=1.1.0 7 | tqdm 8 | scanpy 9 | phate 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | install_requires = [ 5 | "graphtools>=1.5.0", 6 | "tensorflow>=2.6.0", 7 | "multiscale_phate==0.0", 8 | "numpy>=1.14.0", 9 | "scikit-learn", 10 | "scipy>=1.1.0", 11 | "tqdm", 12 | "scanpy", 13 | "phate" 14 | ] 15 | 16 | test_requires = [ 17 | "numpy>=1.14.0", 18 | "phate", 19 | ] 20 | 21 | version_py = os.path.join(os.path.dirname(__file__), "gspa", "version.py") 22 | version = open(version_py).read().strip().split("=")[-1].replace('"', "").strip() 23 | 24 | readme = open("README.md").read() 25 | 26 | setup( 27 | name='gspa', 28 | version=version, 29 | description="Gene Signal Pattern Analysis", 30 | author='Aarthi Venkat, Krishnaswamy Lab, Yale University', 31 | author_email='aarthi.venkat@yale.edu', 32 | packages=find_packages(), 33 | license="GNU General Public License Version 3", 34 | install_requires=install_requires, 35 | python_requires=">=3.6", 36 | extras_require={"test": test_requires}, 37 | long_description=readme, 38 | long_description_content_type="text/markdown", 39 | url="https://github.com/KrishnaswamyLab/Gene-Signal-Pattern-Analysis", 40 | download_url="https://github.com/KrishnaswamyLab/Gene-Signal-Pattern-Analysis/archive/v{}.tar.gz".format(version), 41 | keywords=["big-data", "manifold-learning", "computational-biology", "dimensionality-reduction", "single-cell"], 42 | classifiers=[ 43 | 'Programming Language :: Python :: 3', 44 | 'Programming Language :: Python :: 3.6', 45 | 'Programming Language :: Python :: 3.7', 46 | 'Programming Language :: Python :: 3.8', 47 | 'Programming Language :: Python :: 3.9', 48 | 'Topic :: Scientific/Engineering :: Bio-Informatics', 49 | 'Intended Audience :: Developers', 50 | 'Intended Audience :: Science/Research', 51 | 'Natural Language :: English', 52 | 'Development Status :: 5 - Production/Stable', 53 | 'Operating System :: OS Independent', 54 | ], 55 | ) 56 | 57 | setup_dir = os.path.dirname(os.path.realpath(__file__)) 58 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import gspa 3 | import numpy as np 4 | import phate 5 | 6 | class TestGSPA(unittest.TestCase): 7 | def __init__(self, methodName='runTest'): 8 | super().__init__(methodName) 9 | 10 | # Test input 11 | self.test_data = np.random.random(size=(1000,50)) 12 | phate_op = phate.PHATE(verbose=False) 13 | phate_op.fit(self.test_data) 14 | 15 | self.bc_sample_idx = [1]*500 + [2]*500 16 | self.cell_type_assignments = ['cellA']*250 + ['cellB']*500 + ['cellC']*250 17 | self.cell_type = 'cellC' 18 | 19 | # Test setups 20 | self.gspa = gspa.GSPA(qr_decompose=False, pc_dim=20, embedding_dim=2,wavelet_J=3) 21 | self.gspa_qr = gspa.GSPA(pc_dim=20, embedding_dim=2) 22 | self.condensation = gspa.GSPA(perform_condensation=True, condensation_threshold=200, pc_dim=20, embedding_dim=2) 23 | self.batch_correction = gspa.GSPA(bc_sample_idx=self.bc_sample_idx, pc_dim=20, embedding_dim=2) 24 | self.input_graph = gspa.GSPA(graph=phate_op.graph, qr_decompose=False, pc_dim=20, embedding_dim=2, wavelet_J=3) 25 | self.input_diff_op = gspa.GSPA(diffusion_operator=phate_op.graph.diff_op, qr_decompose=False, pc_dim=20, embedding_dim=2, wavelet_J=3) 26 | 27 | def test_construct_graph(self): 28 | # Positive test case, no MS PHATE 29 | self.gspa.construct_graph(self.test_data) 30 | self.assertEqual(self.gspa.graph.N, self.test_data.shape[0]) 31 | 32 | def test_construct_graph_MS_PHATE(self): 33 | # Positive test case, MS PHATE 34 | self.condensation.construct_graph(self.test_data) 35 | self.assertLessEqual(self.condensation.graph.N, 200) 36 | 37 | def test_construct_graph_BC(self): 38 | # Positive test case, BC 39 | self.batch_correction.construct_graph(self.test_data) 40 | self.assertEqual(self.batch_correction.graph.N, self.test_data.shape[0]) 41 | 42 | def test_construct_diff_op_no_graph(self): 43 | with self.assertRaises(ValueError) as context: 44 | self.gspa.build_diffusion_operator() 45 | self.assertEqual(str(context.exception), "Graph not constructed. Run gspa_op.construct_graph(data) or initialize GSPA operator with graph") 46 | 47 | def test_construct_dict_no_graph(self): 48 | with self.assertRaises(ValueError) as context: 49 | self.gspa.build_wavelet_dictionary() 50 | self.assertEqual(str(context.exception), "Diffusion operator not constructed. Run gspa_op.build_diffusion_operator() or initialize GSPA operator with diffusion_operator") 51 | 52 | def test_dictionary(self): 53 | self.gspa.construct_graph(self.test_data) 54 | self.gspa.build_diffusion_operator() 55 | self.gspa.build_wavelet_dictionary() 56 | self.assertEqual(self.gspa.wavelet_dictionary.shape, (self.test_data.shape[0], self.test_data.shape[0] * self.gspa.wavelet_J)) 57 | 58 | def test_dictionary_qr(self): 59 | self.gspa_qr.construct_graph(self.test_data) 60 | self.gspa_qr.build_diffusion_operator() 61 | self.gspa_qr.build_wavelet_dictionary() 62 | self.assertEqual(self.gspa_qr.wavelet_dictionary.shape[0], self.test_data.shape[0]) 63 | self.assertLessEqual(self.gspa_qr.wavelet_dictionary.shape[1], self.test_data.shape[0] * self.gspa_qr.wavelet_J) 64 | 65 | def test_dictionary_MS_PHATE(self): 66 | self.condensation.construct_graph(self.test_data) 67 | self.condensation.build_diffusion_operator() 68 | self.condensation.build_wavelet_dictionary() 69 | self.assertLessEqual(self.condensation.wavelet_dictionary.shape[0], 200) 70 | 71 | def test_dictionary_input_graph(self): 72 | self.input_graph.build_diffusion_operator() 73 | self.input_graph.build_wavelet_dictionary() 74 | self.assertEqual(self.input_graph.wavelet_dictionary.shape, (self.test_data.shape[0], self.test_data.shape[0] * self.input_graph.wavelet_J)) 75 | 76 | def test_dictionary_input_diff_op(self): 77 | self.input_diff_op.build_wavelet_dictionary() 78 | self.assertEqual(self.input_diff_op.wavelet_dictionary.shape, (self.test_data.shape[0], self.test_data.shape[0] * self.input_graph.wavelet_J)) 79 | 80 | def test_get_gene_embeddings_no_wavelet(self): 81 | with self.assertRaises(ValueError) as context: 82 | self.gspa.get_gene_embeddings(self.test_data.T) 83 | self.assertEqual(str(context.exception), "Run gspa_op.build_wavelet_dictionary") 84 | 85 | with self.assertRaises(ValueError) as context: 86 | self.gspa.calculate_localization(self.test_data.T) 87 | self.assertEqual(str(context.exception), "Run gspa_op.build_wavelet_dictionary") 88 | 89 | with self.assertRaises(ValueError) as context: 90 | self.gspa.calculate_cell_type_specificity(cell_type_assignments=self.cell_type_assignments, cell_type=self.cell_type) 91 | self.assertEqual(str(context.exception), "Run gspa_op.build_wavelet_dictionary") 92 | 93 | def test_loc_no_signals(self): 94 | with self.assertRaises(ValueError) as context: 95 | self.gspa.construct_graph(self.test_data) 96 | self.gspa.build_diffusion_operator() 97 | self.gspa.build_wavelet_dictionary() 98 | self.gspa.calculate_localization() 99 | self.assertEqual(str(context.exception), "Provide signals to map to dictionary or run gspa_op.get_gene_embeddings") 100 | 101 | with self.assertRaises(ValueError) as context: 102 | self.gspa.construct_graph(self.test_data) 103 | self.gspa.build_diffusion_operator() 104 | self.gspa.build_wavelet_dictionary() 105 | self.gspa.calculate_cell_type_specificity(cell_type_assignments=self.cell_type_assignments, cell_type=self.cell_type) 106 | self.assertEqual(str(context.exception), "Provide signals to map to dictionary or run gspa_op.get_gene_embeddings") 107 | 108 | def test_get_gene_embeddings(self): 109 | self.gspa.construct_graph(self.test_data) 110 | self.gspa.build_diffusion_operator() 111 | self.gspa.build_wavelet_dictionary() 112 | out = self.gspa.get_gene_embeddings(self.test_data.T) 113 | self.assertEqual(out[0].shape, (self.test_data.shape[1], 2)) 114 | self.assertEqual(out[1].shape, (self.test_data.shape[1], 20)) 115 | 116 | def test_get_gene_embeddings(self): 117 | self.gspa.construct_graph(self.test_data) 118 | self.gspa.build_diffusion_operator() 119 | self.gspa.build_wavelet_dictionary() 120 | out = self.gspa.get_gene_embeddings(self.test_data.T) 121 | self.assertEqual(out[0].shape, (self.test_data.shape[1], 2)) 122 | self.assertEqual(out[1].shape, (self.test_data.shape[1], 20)) 123 | 124 | def test_get_gene_embeddings_MS_PHATE(self): 125 | self.condensation.construct_graph(self.test_data) 126 | self.condensation.build_diffusion_operator() 127 | self.condensation.build_wavelet_dictionary() 128 | out = self.condensation.get_gene_embeddings(self.test_data.T) 129 | self.assertEqual(out[0].shape, (self.test_data.shape[1], 2)) 130 | self.assertEqual(out[1].shape, (self.test_data.shape[1], 20)) 131 | 132 | def test_localization_with_gene_embeddings(self): 133 | self.gspa.construct_graph(self.test_data) 134 | self.gspa.build_diffusion_operator() 135 | self.gspa.build_wavelet_dictionary() 136 | self.gspa.get_gene_embeddings(self.test_data.T) 137 | out = self.gspa.calculate_localization() 138 | self.assertEqual(out.shape[0], self.test_data.shape[1]) 139 | 140 | def test_localization_with_gene_embeddings_MS_PHATE(self): 141 | self.condensation.construct_graph(self.test_data) 142 | self.condensation.build_diffusion_operator() 143 | self.condensation.build_wavelet_dictionary() 144 | self.condensation.get_gene_embeddings(self.test_data.T) 145 | out = self.condensation.calculate_localization() 146 | self.assertEqual(out.shape[0], self.test_data.shape[1]) 147 | 148 | def test_localization_without_gene_embeddings(self): 149 | self.gspa.construct_graph(self.test_data) 150 | self.gspa.build_diffusion_operator() 151 | self.gspa.build_wavelet_dictionary() 152 | out = self.gspa.calculate_localization(self.test_data.T[:20]) 153 | self.assertEqual(out.shape[0], 20) 154 | 155 | def test_localization_without_gene_embeddings_MS_PHATE(self): 156 | self.condensation.construct_graph(self.test_data) 157 | self.condensation.build_diffusion_operator() 158 | self.condensation.build_wavelet_dictionary() 159 | self.condensation.get_gene_embeddings 160 | out = self.condensation.calculate_localization(self.test_data.T[:20]) 161 | self.assertEqual(out.shape[0], 20) 162 | 163 | def test_cell_type_with_gene_embeddings(self): 164 | self.gspa.construct_graph(self.test_data) 165 | self.gspa.build_diffusion_operator() 166 | self.gspa.build_wavelet_dictionary() 167 | self.gspa.get_gene_embeddings(self.test_data.T) 168 | out = self.gspa.calculate_cell_type_specificity(cell_type_assignments=self.cell_type_assignments, cell_type=self.cell_type) 169 | self.assertEqual(out.shape[0], self.test_data.shape[1]) 170 | 171 | def test_cell_type_with_gene_embeddings_MS_PHATE(self): 172 | self.condensation.construct_graph(self.test_data) 173 | self.condensation.build_diffusion_operator() 174 | self.condensation.build_wavelet_dictionary() 175 | self.condensation.get_gene_embeddings(self.test_data.T) 176 | out = self.condensation.calculate_cell_type_specificity(cell_type_assignments=self.cell_type_assignments, cell_type=self.cell_type) 177 | self.assertEqual(out.shape[0], self.test_data.shape[1]) 178 | 179 | def test_cell_type_without_gene_embeddings(self): 180 | self.gspa.construct_graph(self.test_data) 181 | self.gspa.build_diffusion_operator() 182 | self.gspa.build_wavelet_dictionary() 183 | out = self.gspa.calculate_cell_type_specificity(cell_type_assignments=self.cell_type_assignments, cell_type=self.cell_type, signals=self.test_data.T[:20]) 184 | self.assertEqual(out.shape[0], 20) 185 | 186 | def test_cell_type_without_gene_embeddings_MS_PHATE(self): 187 | self.condensation.construct_graph(self.test_data) 188 | self.condensation.build_diffusion_operator() 189 | self.condensation.build_wavelet_dictionary() 190 | self.condensation.get_gene_embeddings 191 | out = self.condensation.calculate_cell_type_specificity(cell_type_assignments=self.cell_type_assignments, cell_type=self.cell_type, signals=self.test_data.T[:20]) 192 | self.assertEqual(out.shape[0], 20) 193 | 194 | if __name__ == '__main__': 195 | unittest.main() 196 | --------------------------------------------------------------------------------