├── LICENSE ├── README.md ├── codex_spleen ├── .DS_Store ├── FigS7_2021-12-03.ipynb ├── ProspectivePower_2mn.ipynb ├── Spleen_Cohort_Comparison.ipynb ├── Spleen_IST_Generation.ipynb ├── Spleen_NegBinom.ipynb ├── spleen_binning.ipynb ├── spleen_data │ ├── .DS_Store │ └── for_paper │ │ ├── .DS_Store │ │ ├── B_full_image_heuristic_4.npy │ │ ├── C_composite_trim.npy │ │ ├── balbc1_data.pkl │ │ ├── balbc1_enrichments.npy │ │ ├── balbc2_enrichments.npy │ │ ├── balbc3_enrichments.npy │ │ ├── image1_enrichments.npy │ │ ├── image2_enrichments.npy │ │ ├── image3_enrichments.npy │ │ ├── image4_enrichments.npy │ │ └── stitched_graph_noblank.npz └── spleen_multisample.ipynb ├── env.yml ├── hdst_breastcancer ├── .DS_Store ├── BreastCancer_IST_Generation.ipynb ├── BreastCancer_NB.ipynb ├── BreastCancer_multiplesample.ipynb └── HDST_binning.ipynb ├── osmfish_cortex ├── .DS_Store ├── NB_Cell_Discov_clean.ipynb ├── data │ ├── .DS_Store │ ├── osmFISH_SScortex_mouse_all_cells.loom │ └── tiles │ │ ├── A_composite.npz │ │ ├── B_composite.npy │ │ └── C_composite_trim.npy └── osmfish_generation.ipynb ├── sample_results ├── A.npy.gz ├── C.npy ├── vor_test.pdf └── vor_test_heuristic.pdf ├── scripts ├── generate_tiles.py └── random_self_pref_cluster.py ├── simulated_tissue.ipynb ├── simulation ├── FigureS3.ipynb └── FigureS4Heatmaps-2021-12-02.ipynb └── spatialpower ├── .DS_Store ├── __init__.py ├── __pycache__ ├── __init__.cpython-37.pyc └── __init__.cpython-38.pyc ├── main.py ├── neighborhoods ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-37.pyc │ ├── __init__.cpython-38.pyc │ ├── neighborhoods.cpython-37.pyc │ ├── permutationtest.cpython-37.pyc │ └── permutationtest.cpython-38.pyc ├── neighborhoods.py └── permutationtest.py ├── profiling.txt └── tissue_generation ├── __init__.py ├── __pycache__ ├── __init__.cpython-37.pyc ├── __init__.cpython-38.pyc ├── assign_labels.cpython-37.pyc ├── assign_labels.cpython-38.pyc ├── visualization.cpython-37.pyc └── visualization.cpython-38.pyc ├── assign_labels.py ├── random_circle_packing.py └── visualization.py /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Broad Institute 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Power analysis for spatial omics 2 | 3 | This repo contains code related to [_Power analysis for spatial omics_](https://www.biorxiv.org/content/10.1101/2022.01.26.477748v2) 4 | 5 | If you use this in your work, please cite: 6 | **Power analysis for spatial omics.** Ethan Alexander García Baker, Denis Schapiro, Bianca Dumitrascu, Sanja Vickovic, Aviv Regev 7 | *bioRxiv* 2022.01.26.477748; doi: https://doi.org/10.1101/2022.01.26.477748 8 | 9 | ## Contents 10 | 11 | + `codex_spleen/` : 12 | - `Spleen_IST_generation.ipynb` : IST generation for mouse spleen, **Figure 2k** 13 | - `Spleen_NegBinom.ipynb` : Sampling experiments for spleen, **Figure 2l** 14 | - `ProspectivePower_2mn.ipynb`: Prospective power analysis, **Figure 2m-n** 15 | - `FigS7_2021-12-03.ipynb` : FOV size experiments and visualization, **Supplementary Figure 7** 16 | - `Spleen_Cohort_Comparison.ipynb` : Interaction enrichment statistic, **Supplementary Figure 8** 17 | - `spleen_binning.ipynb` : Resolution analysis for spleen dataset, **Figure 3** 18 | - `spleen_multisample.ipynb` : Analysis on the impact of inclusion of multiple samples on power, **Figure 3** 19 | - `spleen_data/` : Contains support files. 20 | + `simulation/` : 21 | - `FigureS3.ipynb` : Sampling experiments for synthetic data. **Supplementary Figure 3** 22 | - `FigureS4Heatmaps-2021-12-02.ipynb`: Clustering experiments for ISTs, **Supplementary Figure 4** 23 | + `osmfish_cortex/` 24 | - `osmfish_generation.ipynb` : IST generation for mouse cortex, **Figure 2g** 25 | - `NB_Cell_Discov_clean.ipynb`: Cell type discovery sampling experiments, **Figure 2h, Supplementary Figure 6d** 26 | - `data/` : Contains support files 27 | + `hdst_breastcancer/` 28 | - `BreastCancer_IST_Generation.ipynb`: IST generation for breast cancer, **Figure 2c** 29 | - `BreastCancer_NB.ipynb` : Cell type discovery sampling experiments, **Figure 2d** 30 | - `HDST_binning.ipynb` : Resolution analysis for HDST breast cancer data, **Figure 3** 31 | - `BreastCancer_multisample.ipynb` : Analysis on the impact of inclusion of multiple samples on power, **Figure 3** 32 | - `data/` : Contains support files 33 | + `spatialpower/` : Package for IST generation and supporting analysis 34 | + `scripts/` : Contains support scripts for other analyses 35 | - `generate_tiles.py` : Generates tiles for shuffling analysis corresponding to Figure 2m-n 36 | - `random_self_pref_cluster.py` : Generates ISTs for clustograms in Supplementary Figure 4. 37 | 38 | ## Requirements 39 | We provide a clone of the conda environment used to generate these results in the env.yml file. To install the environment, use `conda env create -n --file env.yml`. 40 | 41 | ## Generating a tissue _in silico_ 42 | We provide a command line Python tool to generate tissue _in silico_. 43 | 44 | Generating an IST requires knowledge of two parameters: a vector describing the abundance of the _k_ cell types, p, and the _k x k_ matrix describing probability that two cell types are directly adjacent, H. These objects are described in the paper. We suggest that _p_ and _H_ are estimated from pilot data; the IST generation notebooks above illustrate how one might do this. 45 | 46 | The generalized steps for the construction of the IST are: 47 | 1. Generate a tissue scaffold 48 | 2. Estimate _p_ and _H_ 49 | 3. Label the tissue scaffold. 50 | 51 | #### Generate a tissue scaffold 52 | To construct a tissue scaffold, execute `random_circle_packing.py`: 53 | `python spatialpower/tissue_generation/random_circle_packing.py -x 1000 -y 1000 -o sample_results` 54 | 55 | Key arguments that can be adjusted to tune the circle packing are `-x` and `-y`, which control the width and height, respectively, of the rectangular tissue area, and `--rmin` and `--rmax` which control the minimum and maximum radius, respectively, of the circles that are packed within the bounding rectangle to generate the random planar graph. 56 | 57 | A full enumeration of the arguments, including controls for visualization and export, is available using `-h` flag. 58 | 59 | #### Estimate _p_ and _H_ 60 | We suggest obtaining values for _p_ and _H_ from a pilot experiment or estimating them by prior knowledge. 61 | 62 | We provide a function for the efficient computation of _H_ given the adjacency matrix, _A_ and one-hot encoded assignment matrix _B_ for a graph representation of a tissue (real or simulated). To calculate _H_: 63 | 64 | ```python 65 | import spatialpower.neighborhoods.permutationtest as perm_test 66 | H = perm_test.calculate_neighborhood_distribution(A, B) 67 | ``` 68 | 69 | The cell type abundance _p_ can be easily computed using the one-hot encoded assignment matrix _B_: 70 | `p = np.sum(B, axis=0)/np.sum(np.sum(B, axis=0))` 71 | 72 | #### Label the tissue scaffold: 73 | To perform a labeling of the tissue scaffold using the optimization approach: 74 | ```python 75 | import spatialpower.tissue_generation.assign_labels as assign_labels 76 | cell_assignments = assign_labels.optimize(A, p, H, learning_rate=1e-5, iterations = 10) 77 | ``` 78 | 79 | To perform a labeling of the tissue scaffold using the heuristic approach: 80 | 81 | ```python 82 | import networkx as nx 83 | import spatialpower.tissue_generation.assign_labels as assign_labels 84 | 85 | G = nx.from_numpy_array(A) 86 | cell_assignments = assign_labels.heuristic_assignment(G, p, H, mode='graph', dim=1000, position_dict=position_dict, grid_size=50, revision_iters=100, n_swaps=25) 87 | ``` 88 | 89 | ### Example notebooks 90 | We provide `simulated_tissue.ipynb` as an example of how to use our tissue generation method on a theoretical tissue (e.g. for testing methods to recover a specific spatial feature) following this approach above. 91 | 92 | Additionally, we provide example usage of our approach for tissue generation. See the `osmfish_cortex/osmfish_generation.ipynb` file for a complete example with generated tissue from raw osmFISH data. The run time should be below 30 minutes for the full notebook on a modern laptop. 93 | 94 | ## Performing power analysis 95 | Our work provides a general framework for the considerations that should be taken into account in spatial experimental design. In our manuscript, we consider experiments to detect several spatial features, including the discovery of a cell type of interest and the detection of cell-cell interactions. 96 | 97 | We examine experiments to discover these spatial features as illustrative examples of our general framework; we encourage individual users to adapt these approaches for their particular question of interest. 98 | 99 | #### Cell type discovery 100 | 101 | In general, the overall procedure for cell type discovery is: 102 | 1. Obtain pilot data 103 | 2. Estimate model parameters 104 | 3. Calculate probability of detecting cell type of interest given some level of sampling (e.g. number of cells or FOVs sampled) 105 | 106 | We provide notebooks implementing this framework for the three differently-structured data sets discussed in our manuscript: 107 | - `FigureS3.ipynb` : Sampling experiments for synthetic data. **Supplementary Figure 3** 108 | - `Spleen_NegBinom.ipynb` : Sampling experiments for spleen, **Figure 2l** 109 | - `NB_Cell_Discov_clean.ipynb`: Cell type discovery sampling experiments, **Figure 2h, Supplementary Figure 6d** 110 | - `BreastCancer_NB.ipynb` : Cell type discovery sampling experiments, **Figure 2d** 111 | 112 | #### Cell-cell interactions 113 | As discussed in the manuscript, we suggest the detection of cell-cell interactions via a permutation test, which we provide code for in `spatialpower/neighborhoods/permutationtest.py`. 114 | 115 | Additionally, we show an illustrative example of this analysis in: 116 | - `FigS7_2021-12-03.ipynb` : FOV size experiments and visualization, **Supplementary Figure 7** 117 | - `ProspectivePower_2mn.ipynb`: Prospective power analysis, **Figure 2m-n** 118 | 119 | #### Comparing differently structured tissues 120 | We introduce the interaction enrichment statistic, which is implemented in the functions `calculate_enrichment_statistic` and `z_test` in the module `spatialpower/neighborhoods/permutationtest.py`. 121 | 122 | We provide an illustrative example of the IES test as implemented in our manuscript : 123 | - `Spleen_Cohort_Comparison.ipynb` : Interaction enrichment statistic, **Supplementary Figure 8** 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /codex_spleen/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/.DS_Store -------------------------------------------------------------------------------- /codex_spleen/spleen_data/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/.DS_Store -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/.DS_Store -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/B_full_image_heuristic_4.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/B_full_image_heuristic_4.npy -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/C_composite_trim.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/C_composite_trim.npy -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/balbc1_data.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/balbc1_data.pkl -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/balbc1_enrichments.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/balbc1_enrichments.npy -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/balbc2_enrichments.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/balbc2_enrichments.npy -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/balbc3_enrichments.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/balbc3_enrichments.npy -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/image1_enrichments.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/image1_enrichments.npy -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/image2_enrichments.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/image2_enrichments.npy -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/image3_enrichments.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/image3_enrichments.npy -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/image4_enrichments.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/image4_enrichments.npy -------------------------------------------------------------------------------- /codex_spleen/spleen_data/for_paper/stitched_graph_noblank.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/codex_spleen/spleen_data/for_paper/stitched_graph_noblank.npz -------------------------------------------------------------------------------- /env.yml: -------------------------------------------------------------------------------- 1 | # This file may be used to create an environment using: 2 | # $ conda create --name --file 3 | # platform: osx-64 4 | absl-py=1.0.0=pypi_0 5 | appnope=0.1.2=py37hecd8cb5_1001 6 | argon2-cffi=20.1.0=py37h9ed2024_1 7 | async_generator=1.10=py37h28b3542_0 8 | attrs=21.2.0=pyhd3eb1b0_0 9 | backcall=0.2.0=pyhd3eb1b0_0 10 | blas=1.0=mkl 11 | bleach=4.0.0=pyhd3eb1b0_0 12 | bottleneck=1.3.2=py37hf1fa96c_1 13 | brotli=1.0.9=hb1e8313_2 14 | ca-certificates=2021.10.8=h033912b_0 15 | certifi=2021.10.8=py37hf985489_1 16 | cffi=1.14.6=py37h2125817_0 17 | cycler=0.10.0=py37_0 18 | dbus=1.13.18=h18a8e69_0 19 | decorator=5.0.9=pyhd3eb1b0_0 20 | defusedxml=0.7.1=pyhd3eb1b0_0 21 | entrypoints=0.3=py37_0 22 | expat=2.4.1=h23ab428_2 23 | flatbuffers=2.0=pypi_0 24 | fonttools=4.25.0=pyhd3eb1b0_0 25 | freetype=2.10.4=ha233b18_0 26 | gettext=0.21.0=h7535e17_0 27 | glib=2.69.0=hdf23fa2_0 28 | icu=58.2=h0a44026_3 29 | importlib-metadata=3.10.0=py37hecd8cb5_0 30 | importlib_metadata=3.10.0=hd3eb1b0_0 31 | intel-openmp=2021.3.0=hecd8cb5_3375 32 | ipykernel=5.3.4=py37h5ca1d4c_0 33 | ipython=7.26.0=py37h01d92e1_0 34 | ipython_genutils=0.2.0=pyhd3eb1b0_1 35 | ipywidgets=7.6.3=pyhd3eb1b0_1 36 | jax=0.2.27=pypi_0 37 | jaxlib=0.1.75=pypi_0 38 | jedi=0.18.0=py37hecd8cb5_1 39 | jinja2=3.0.1=pyhd3eb1b0_0 40 | joblib=1.0.1=pyhd3eb1b0_0 41 | jpeg=9b=he5867d9_2 42 | jsonschema=3.2.0=py_2 43 | jupyter=1.0.0=py37_7 44 | jupyter_client=6.1.12=pyhd3eb1b0_0 45 | jupyter_console=6.4.0=pyhd3eb1b0_0 46 | jupyter_core=4.7.1=py37hecd8cb5_0 47 | jupyterlab_pygments=0.1.2=py_0 48 | jupyterlab_widgets=1.0.0=pyhd3eb1b0_1 49 | kiwisolver=1.3.1=py37h23ab428_0 50 | lcms2=2.12=hf1fd2bf_0 51 | libcxx=10.0.0=1 52 | libffi=3.3=hb1e8313_2 53 | libgfortran=3.0.1=h93005f0_2 54 | libiconv=1.16=h1de35cc_0 55 | libpng=1.6.37=ha441bb4_0 56 | libsodium=1.0.18=h1de35cc_0 57 | libtiff=4.2.0=h87d7836_0 58 | libwebp-base=1.2.0=h9ed2024_0 59 | libxml2=2.9.12=hcdb78fc_0 60 | llvm-openmp=10.0.0=h28b9765_0 61 | lz4-c=1.9.3=h23ab428_1 62 | markupsafe=2.0.1=py37h9ed2024_0 63 | matplotlib=3.4.2=py37hecd8cb5_0 64 | matplotlib-base=3.4.2=py37h8b3ea08_0 65 | matplotlib-inline=0.1.2=pyhd3eb1b0_2 66 | matplotlib-venn=0.11.6=pyh9f0ad1d_0 67 | mistune=0.8.4=py37h1de35cc_0 68 | mkl=2021.3.0=hecd8cb5_517 69 | mkl-service=2.4.0=py37h9ed2024_0 70 | mkl_fft=1.3.0=py37h4a7008c_2 71 | mkl_random=1.2.2=py37hb2f4e1b_0 72 | munkres=1.1.4=py_0 73 | nbclient=0.5.3=pyhd3eb1b0_0 74 | nbconvert=6.1.0=py37hecd8cb5_0 75 | nbformat=5.1.3=pyhd3eb1b0_0 76 | ncurses=6.2=h0a44026_1 77 | nest-asyncio=1.5.1=pyhd3eb1b0_0 78 | networkx=2.6.2=pyhd3eb1b0_0 79 | notebook=6.4.0=py37hecd8cb5_0 80 | numexpr=2.7.3=py37h5873af2_1 81 | numpy=1.20.3=py37h4b4dc7a_0 82 | numpy-base=1.20.3=py37he0bd621_0 83 | olefile=0.46=py37_0 84 | openjpeg=2.3.0=hb95cd4c_1 85 | openssl=1.1.1l=h0d85af4_0 86 | opt-einsum=3.3.0=pypi_0 87 | packaging=21.0=pyhd3eb1b0_0 88 | pandas=1.3.1=py37h5008ddb_0 89 | pandocfilters=1.4.3=py37hecd8cb5_1 90 | parso=0.8.2=pyhd3eb1b0_0 91 | patsy=0.5.2=py37hecd8cb5_0 92 | pcre=8.45=h23ab428_0 93 | pexpect=4.8.0=pyhd3eb1b0_3 94 | pickleshare=0.7.5=pyhd3eb1b0_1003 95 | pillow=8.3.1=py37ha4cf6ea_0 96 | pip=21.2.2=py37hecd8cb5_0 97 | prometheus_client=0.11.0=pyhd3eb1b0_0 98 | prompt-toolkit=3.0.17=pyh06a4308_0 99 | prompt_toolkit=3.0.17=hd3eb1b0_0 100 | ptyprocess=0.7.0=pyhd3eb1b0_2 101 | pycparser=2.20=py_2 102 | pygments=2.9.0=pyhd3eb1b0_0 103 | pyparsing=2.4.7=pyhd3eb1b0_0 104 | pyqt=5.9.2=py37h655552a_2 105 | pyrsistent=0.17.3=py37haf1e3a3_0 106 | python=3.7.11=h88f2d9e_0 107 | python-dateutil=2.8.2=pyhd3eb1b0_0 108 | python_abi=3.7=2_cp37m 109 | pytz=2021.1=pyhd3eb1b0_0 110 | pyzmq=20.0.0=py37h23ab428_1 111 | qt=5.9.7=h468cd18_1 112 | qtconsole=5.1.0=pyhd3eb1b0_0 113 | qtpy=1.9.0=py_0 114 | readline=8.1=h9ed2024_0 115 | scipy=1.6.2=py37hd5f7400_1 116 | seaborn=0.11.1=pyhd3eb1b0_0 117 | send2trash=1.5.0=pyhd3eb1b0_1 118 | setuptools=52.0.0=py37hecd8cb5_0 119 | sip=4.19.8=py37h0a44026_0 120 | six=1.16.0=pyhd3eb1b0_0 121 | sqlite=3.36.0=hce871da_0 122 | statsmodels=0.12.2=py37h9ed2024_0 123 | terminado=0.9.4=py37hecd8cb5_0 124 | testpath=0.5.0=pyhd3eb1b0_0 125 | tk=8.6.10=hb0a8c7a_0 126 | tornado=6.1=py37h9ed2024_0 127 | tqdm=4.62.3=pyhd3eb1b0_1 128 | traitlets=5.0.5=pyhd3eb1b0_0 129 | typing_extensions=3.10.0.0=pyh06a4308_0 130 | wcwidth=0.2.5=py_0 131 | webencodings=0.5.1=py37_1 132 | wheel=0.36.2=pyhd3eb1b0_0 133 | widgetsnbextension=3.5.1=py37_0 134 | xz=5.2.5=h1de35cc_0 135 | zeromq=4.3.4=h23ab428_0 136 | zipp=3.5.0=pyhd3eb1b0_0 137 | zlib=1.2.11=h1de35cc_3 138 | zstd=1.4.9=h322a384_0 139 | -------------------------------------------------------------------------------- /hdst_breastcancer/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/hdst_breastcancer/.DS_Store -------------------------------------------------------------------------------- /osmfish_cortex/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/osmfish_cortex/.DS_Store -------------------------------------------------------------------------------- /osmfish_cortex/data/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/osmfish_cortex/data/.DS_Store -------------------------------------------------------------------------------- /osmfish_cortex/data/osmFISH_SScortex_mouse_all_cells.loom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/osmfish_cortex/data/osmFISH_SScortex_mouse_all_cells.loom -------------------------------------------------------------------------------- /osmfish_cortex/data/tiles/A_composite.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/osmfish_cortex/data/tiles/A_composite.npz -------------------------------------------------------------------------------- /osmfish_cortex/data/tiles/B_composite.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/osmfish_cortex/data/tiles/B_composite.npy -------------------------------------------------------------------------------- /osmfish_cortex/data/tiles/C_composite_trim.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/osmfish_cortex/data/tiles/C_composite_trim.npy -------------------------------------------------------------------------------- /sample_results/A.npy.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/sample_results/A.npy.gz -------------------------------------------------------------------------------- /sample_results/C.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/sample_results/C.npy -------------------------------------------------------------------------------- /sample_results/vor_test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/sample_results/vor_test.pdf -------------------------------------------------------------------------------- /sample_results/vor_test_heuristic.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/sample_results/vor_test_heuristic.pdf -------------------------------------------------------------------------------- /scripts/generate_tiles.py: -------------------------------------------------------------------------------- 1 | ##Used to generate all of the tiles for the tile shuffling experiement. 2 | import numpy as np 3 | import networkx as nx 4 | from spatialpower.tissue_generation import assign_labels 5 | 6 | 7 | def build_assignment_matrix(attribute_dict, n_cell_types): 8 | data = list(attribute_dict.items()) 9 | data = np.array(data) # Assignment matrix 10 | 11 | B = np.zeros((data.shape[0], n_cell_types)) # Empty matrix 12 | 13 | for i in range(0, data.shape[0]): 14 | t = data[i, 1] 15 | B[i, t] = 1 16 | 17 | return B 18 | 19 | #Set parameters 20 | n_images = 20 21 | b_follicle_count = 51 * n_images 22 | pals_count = 94 * n_images 23 | red_pulp_count = 174 * n_images 24 | marginal_zone_count = 69 * n_images 25 | 26 | #Load relevant data 27 | adjacency_matrix = np.load('./adj_mat_239_cell_tissue_scaffold_tile.npy') 28 | C = np.load('./C_239_cell_tissue_scaffold_tile.npy') 29 | R = np.load('./R_239_cell_tissue_scaffold_tile.npy') 30 | graph = nx.from_numpy_matrix(adjacency_matrix) 31 | 32 | 33 | 34 | #B Follicle 35 | p = np.load('./spleen_data/for_paper/p_bfollicle_balbc1.npy') 36 | H = np.load('./spleen_data/for_paper/H_bfollicle_balbc1.npy') 37 | 38 | position_dict = dict() 39 | for i in range(0, C.shape[0]): 40 | position_dict[i] = C[i, :] 41 | 42 | i=0 43 | while i <= b_follicle_count: 44 | attribute_dict = assign_labels.heuristic_assignment(graph, p, H, 'region', 350, position_dict, 100) 45 | heuristic_B = build_assignment_matrix(attribute_dict, 27) 46 | np.save('./spleen_data/for_paper/tiles/239_cell_tiles/shuffling_experiment/B_hueristic_bfollicle_' + str(i) + '.npy', heuristic_B) 47 | if i % 10 == 0: 48 | print(i) 49 | i += 1 50 | 51 | #PALS Zone 52 | p = np.load('./spleen_data/for_paper/p_pals_balbc1.npy') 53 | H = np.load('./spleen_data/for_paper/H_pals_balbc1.npy') 54 | 55 | n_cell_types = 27 56 | position_dict = dict() 57 | for i in range(0, C.shape[0]): 58 | position_dict[i] = C[i, :] 59 | 60 | i=0 61 | while i <= pals_count: 62 | attribute_dict = assign_labels.heuristic_assignment(graph, p, H, 'region', 350, position_dict, 50) 63 | heuristic_B = build_assignment_matrix(attribute_dict, 27) 64 | np.save('./spleen_data/for_paper/tiles/239_cell_tiles/shuffling_experiment/B_hueristic_pals_' + str(i) + '.npy', heuristic_B) 65 | if i % 10 == 0: 66 | print(i) 67 | i += 1 68 | 69 | #Red Pulp 70 | 71 | p = np.load('./spleen_data/for_paper/p_redpulp_balbc1.npy') 72 | H = np.load('./spleen_data/for_paper/H_redpulp_balbc1.npy') 73 | 74 | n_cell_types = 27 75 | position_dict = dict() 76 | for i in range(0, C.shape[0]): 77 | position_dict[i] = C[i, :] 78 | 79 | i=0 80 | while i <= red_pulp_count: 81 | attribute_dict = assign_labels.heuristic_assignment(graph, p, H, 'region', 350, position_dict, 50) 82 | heuristic_B = build_assignment_matrix(attribute_dict, 27) 83 | np.save('./spleen_data/for_paper/tiles/239_cell_tiles/shuffling_experiment/B_hueristic_redpulp_' + str(i) + '.npy', heuristic_B) 84 | if i % 10 == 0: 85 | print(i) 86 | i += 1 87 | 88 | #Marginal Zone 89 | p = np.load('./spleen_data/for_paper/p_marginalzone_balbc1.npy') 90 | H = np.load('./spleen_data/for_paper/H_marginalzone_balbc1.npy') 91 | 92 | 93 | n_cell_types =27 94 | position_dict = dict() 95 | for i in range(0, C.shape[0]): 96 | position_dict[i] = C[i, :] 97 | 98 | i=0 99 | while i <= marginal_zone_count: 100 | attribute_dict = assign_labels.heuristic_assignment(graph, p, H, 'region', 350, position_dict, 50) 101 | heuristic_B = build_assignment_matrix(attribute_dict, 27) 102 | np.save('./spleen_data/for_paper/tiles/239_cell_tiles/shuffling_experiment/B_hueristic_marginalzone_' + str(i) + '.npy', heuristic_B) 103 | if i % 10 == 0: 104 | print(i) 105 | i += 1 106 | 107 | -------------------------------------------------------------------------------- /scripts/random_self_pref_cluster.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | import numpy as np 3 | import scipy.sparse as sparse 4 | import matplotlib.pyplot as plt 5 | import networkx as nx 6 | import operator 7 | from spatialpower.tissue_generation import assign_labels 8 | from spatialpower.tissue_generation import visualization 9 | 10 | results_dir = './results/motif_detection/' 11 | adj_mat_list = np.sort(glob(results_dir + 'blank_graph_network*.npy')) 12 | pos_mat_list = np.sort(glob(results_dir + 'blank_graph_positions*.npy')) 13 | 14 | dim = 300 15 | 16 | ##RANDOM## 17 | cell_type_probabilities = np.ones(10) * 0.1 18 | neighborhood_probabilities = np.ones((10,10)) * 0.1 19 | n_cell_types = len(cell_type_probabilities) 20 | 21 | for ii in range(0, len(adj_mat_list)): 22 | A = np.load(adj_mat_list[ii]) 23 | C = np.load(pos_mat_list[ii]) 24 | 25 | j = adj_mat_list[ii].split('_')[-1].split('.')[0] 26 | # Blank assignment structure 27 | n_cell_types = len(cell_type_probabilities) 28 | position_dict = dict() 29 | for i in range(0, C.shape[0]): 30 | position_dict[i] = C[i, :] 31 | 32 | graph = nx.from_numpy_matrix(A) 33 | node_id_list = list(graph.nodes) 34 | attribute_dict = dict(zip(node_id_list, [-1 for i in graph.nodes])) 35 | 36 | attribute_dict = assign_labels.heuristic_assignment(graph, cell_type_probabilities, neighborhood_probabilities, 'region', dim, position_dict) 37 | observed_cell_type_dist, kl = assign_labels.check_cell_type_dist(n_cell_types, attribute_dict, cell_type_probabilities) 38 | observed_neighborhood_dist, kl_neighbor = assign_labels.check_neighborhood_dist(n_cell_types, attribute_dict, neighborhood_probabilities, graph, 1) 39 | B = assign_labels.build_assignment_matrix(attribute_dict, n_cell_types) 40 | np.save(results_dir + 'random_B_' + str(j) + '.npy', B) 41 | 42 | visualization.make_vor(dim, attribute_dict, position_dict, n_cell_types, results_dir, 'random_B_' + str(j), node_id_list) 43 | 44 | ## High Self Preference ## 45 | '''cell_type_probabilities = [0.03, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.10, 0.11, 0.10] 46 | neighborhood_probabilities = np.array([[0.50, 0.06, 0.06, 0.06, 0.06, 0.06, 0.05, 0.05, 0.05, 0.05], 47 | [0.06, 0.11, 0.11, 0.11, 0.11, 0.10, 0.10, 0.10, 0.10, 0.10], 48 | [0.06, 0.11, 0.11, 0.11, 0.11, 0.10, 0.10, 0.10, 0.10, 0.10], 49 | [0.06, 0.11, 0.11, 0.11, 0.11, 0.10, 0.10, 0.10, 0.10, 0.10], 50 | [0.06, 0.11, 0.11, 0.11, 0.11, 0.10, 0.10, 0.10, 0.10, 0.10], 51 | [0.06, 0.10, 0.10, 0.10, 0.10, 0.10, 0.11, 0.11, 0.11, 0.11], 52 | [0.05, 0.10, 0.10, 0.10, 0.10, 0.11, 0.11, 0.11, 0.11, 0.11], 53 | [0.05, 0.10, 0.10, 0.10, 0.10, 0.11, 0.11, 0.11, 0.11, 0.11], 54 | [0.05, 0.10, 0.10, 0.10, 0.10, 0.11, 0.11, 0.11, 0.11, 0.11], 55 | [0.05, 0.10, 0.10, 0.10, 0.10, 0.11, 0.11, 0.11, 0.11, 0.11]]) 56 | n_cell_types = len(cell_type_probabilities) 57 | 58 | for ii in range(0, len(adj_mat_list)): 59 | A = np.load(adj_mat_list[ii]) 60 | C = np.load(pos_mat_list[ii]) 61 | j = adj_mat_list[ii].split('_')[-1].split('.')[0] 62 | 63 | # Blank assignment structure 64 | n_cell_types = len(cell_type_probabilities) 65 | position_dict = dict() 66 | for i in range(0, C.shape[0]): 67 | position_dict[i] = C[i, :] 68 | 69 | graph = nx.from_numpy_matrix(A) 70 | node_id_list = list(graph.nodes) 71 | attribute_dict = dict(zip(node_id_list, [-1 for i in graph.nodes])) 72 | 73 | attribute_dict = assign_labels.heuristic_assignment(graph, cell_type_probabilities, neighborhood_probabilities, 'region', dim, position_dict) 74 | 75 | preferred_node_type = 0 76 | for i in list(graph.nodes): 77 | if attribute_dict[i] == preferred_node_type: 78 | #print(i) 79 | graph_distance = 1 80 | neighborhood = nx.ego_graph(graph, i, radius = graph_distance) 81 | neighborhood_nodes = list(neighborhood.nodes) 82 | 83 | # Now set the remaining probabilities in the region. 84 | 85 | for node in neighborhood_nodes: 86 | if node != i: 87 | attribute_dict[node] = assign_labels.sample_cell_type(neighborhood_probabilities[preferred_node_type]) 88 | else: 89 | continue 90 | 91 | observed_cell_type_dist, kl = assign_labels.check_cell_type_dist(n_cell_types, attribute_dict, cell_type_probabilities) 92 | observed_neighborhood_dist, kl_neighbor = assign_labels.check_neighborhood_dist(n_cell_types, attribute_dict, neighborhood_probabilities, graph, 1) 93 | B = assign_labels.build_assignment_matrix(attribute_dict, n_cell_types) 94 | np.save(results_dir + 'selfpref_B_' + str(j) + '.npy', B) 95 | 96 | visualization.make_vor(dim, attribute_dict, position_dict, n_cell_types, results_dir, 'selfpref_B_' + str(j), node_id_list)''' 97 | 98 | ## 3 Cell Motif ## 99 | cell_type_probabilities = [0.04, 0.04, 0.04, 0.13, 0.13, 0.13, 0.12, 0.12, 0.13, 0.12] 100 | neighborhood_probabilities = np.array([[0.15, 0.40, 0.15, 0.05, 0.05, 0.04, 0.04, 0.04, 0.04, 0.04], 101 | [0.40, 0.06, 0.40, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], 102 | [0.15, 0.40, 0.15, 0.05, 0.05, 0.04, 0.04, 0.04, 0.04, 0.04], 103 | [0.05, 0.02, 0.05, 0.13, 0.12, 0.13, 0.13, 0.13, 0.12, 0.12], 104 | [0.05, 0.02, 0.05, 0.12, 0.13, 0.13, 0.12, 0.12, 0.13, 0.13], 105 | [0.04, 0.02, 0.04, 0.13, 0.13, 0.13, 0.12, 0.13, 0.13, 0.13], 106 | [0.04, 0.02, 0.04, 0.13, 0.12, 0.12, 0.13, 0.13, 0.14, 0.13], 107 | [0.04, 0.02, 0.04, 0.13, 0.12, 0.13, 0.13, 0.12, 0.14, 0.13], 108 | [0.04, 0.02, 0.04, 0.12, 0.13, 0.13, 0.14, 0.14, 0.12, 0.12], 109 | [0.04, 0.02, 0.04, 0.12, 0.13, 0.13, 0.13, 0.13, 0.12, 0.14]]) 110 | n_cell_types = len(cell_type_probabilities) 111 | 112 | for ii in range(0, len(adj_mat_list)): 113 | A = np.load(adj_mat_list[ii]) 114 | C = np.load(pos_mat_list[ii]) 115 | j = adj_mat_list[ii].split('_')[-1].split('.')[0] 116 | 117 | # Blank assignment structure 118 | n_cell_types = len(cell_type_probabilities) 119 | position_dict = dict() 120 | for i in range(0, C.shape[0]): 121 | position_dict[i] = C[i, :] 122 | 123 | graph = nx.from_numpy_matrix(A) 124 | node_id_list = list(graph.nodes) 125 | attribute_dict = dict(zip(node_id_list, [-1 for i in graph.nodes])) 126 | 127 | attribute_dict = assign_labels.heuristic_assignment(graph, cell_type_probabilities, neighborhood_probabilities, 'region', dim, position_dict) 128 | 129 | #preferred_node_type = 0 130 | for i in list(graph.nodes): 131 | if ((attribute_dict[i] == 0) or (attribute_dict[i] == 1) or (attribute_dict[i] == 2)): 132 | #print(i) 133 | graph_distance = 1 134 | neighborhood = nx.ego_graph(graph, i, radius = graph_distance) 135 | neighborhood_nodes = list(neighborhood.nodes) 136 | 137 | # Now set the remaining probabilities in the region. 138 | 139 | for node in neighborhood_nodes: 140 | if node != i: 141 | attribute_dict[node] = assign_labels.sample_cell_type(neighborhood_probabilities[attribute_dict[i]]) 142 | else: 143 | continue 144 | 145 | observed_cell_type_dist, kl = assign_labels.check_cell_type_dist(n_cell_types, attribute_dict, cell_type_probabilities) 146 | observed_neighborhood_dist, kl_neighbor = assign_labels.check_neighborhood_dist(n_cell_types, attribute_dict, neighborhood_probabilities, graph, 1) 147 | B = assign_labels.build_assignment_matrix(attribute_dict, n_cell_types) 148 | np.save(results_dir + '3cellmotif_B_' + str(j) + '.npy', B) 149 | 150 | visualization.make_vor(dim, attribute_dict, position_dict, n_cell_types, results_dir, '3cellmotif_B_' + str(j), node_id_list) -------------------------------------------------------------------------------- /simulated_tissue.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 13, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import spatialpower.tissue_generation.assign_labels as assign_labels\n", 11 | "import spatialpower.tissue_generation.visualization as viz\n", 12 | "import spatialpower.neighborhoods.permutationtest as perm_test\n", 13 | "\n", 14 | "import networkx as nx " 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "%%bash \n", 24 | "\n", 25 | "python spatialpower/tissue_generation/random_circle_packing.py -x 1000 -y 1000 -o sample_results \n", 26 | "\n", 27 | "# see python spatialpower/tissue_generation/random_circle_packing.py --help for full options" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "### Load circle packings" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 2, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "A = np.load('./sample_results/A.npy')\n", 44 | "C = np.load('./sample_results/C.npy')" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "### Set parameters" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 3, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "cell_type_probabilities = np.array([0.7, 0.1, 0.1, 0.1])\n", 61 | "\n", 62 | "\n", 63 | "neighborhood_probabilities = np.array(([0.60, 0.13, 0.13, 0.14],\n", 64 | " [0.13, 0.29, 0.29, 0.29],\n", 65 | " [0.13, 0.29, 0.29, 0.29],\n", 66 | " [0.14, 0.29, 0.29, 0.28]))\n" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "### Optimized Assignment\n", 74 | "\n", 75 | "Estimated runtime ~15 mins with no GPU" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 6, 81 | "metadata": {}, 82 | "outputs": [ 83 | { 84 | "name": "stdout", 85 | "output_type": "stream", 86 | "text": [ 87 | "102669.56\n", 88 | "95909.34\n", 89 | "89673.34\n", 90 | "83920.75\n", 91 | "constraint 160566.34\n", 92 | "26444.54\n", 93 | "26393.512\n", 94 | "26347.734\n", 95 | "26306.64\n", 96 | "constraint 24031.094\n", 97 | "44861.234\n", 98 | "44855.277\n", 99 | "44849.32\n", 100 | "44843.383\n", 101 | "constraint 23362.074\n", 102 | "79455.195\n", 103 | "79438.9\n", 104 | "79422.61\n", 105 | "79406.33\n", 106 | "constraint 23038.674\n", 107 | "143482.14\n", 108 | "143433.55\n", 109 | "143385.02\n", 110 | "143336.45\n", 111 | "constraint 22516.639\n", 112 | "261517.2\n", 113 | "261361.42\n", 114 | "261205.9\n", 115 | "261050.56\n", 116 | "constraint 21655.24\n", 117 | "473353.47\n", 118 | "472833.53\n", 119 | "472314.44\n", 120 | "471795.8\n", 121 | "constraint 20221.168\n", 122 | "827157.25\n", 123 | "825413.06\n", 124 | "823671.2\n", 125 | "821932.44\n", 126 | "constraint 17873.21\n", 127 | "1320851.0\n", 128 | "1315265.9\n", 129 | "1309698.6\n", 130 | "1304151.9\n", 131 | "constraint 14255.502\n", 132 | "1705477.9\n", 133 | "1690085.6\n", 134 | "1674816.8\n", 135 | "1659676.5\n", 136 | "constraint 9479.71\n" 137 | ] 138 | } 139 | ], 140 | "source": [ 141 | "cell_assignments = assign_labels.optimize(A, cell_type_probabilities, neighborhood_probabilities, learning_rate=1e-5, iterations = 10)" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 7, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "def build_assignment_matrix(attribute_dict, n_cell_types):\n", 151 | " data = list(attribute_dict.items())\n", 152 | " data = np.array(data) # Assignment matrix\n", 153 | " \n", 154 | " B = np.zeros((data.shape[0],n_cell_types)) # Empty matrix\n", 155 | " \n", 156 | " for i in range(0, data.shape[0]):\n", 157 | " t = data[i,1]\n", 158 | " B[i,t] = 1\n", 159 | " \n", 160 | " return B " 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": 10, 166 | "metadata": {}, 167 | "outputs": [], 168 | "source": [ 169 | "B = cell_assignments.copy()" 170 | ] 171 | }, 172 | { 173 | "cell_type": "markdown", 174 | "metadata": {}, 175 | "source": [ 176 | "### Actual results" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 11, 182 | "metadata": {}, 183 | "outputs": [ 184 | { 185 | "data": { 186 | "text/plain": [ 187 | "array([[0.50926333, 0.16338761, 0.16514334, 0.1644108 ],\n", 188 | " [0.51014847, 0.14834161, 0.15325385, 0.16741487],\n", 189 | " [0.51030556, 0.15429666, 0.16309762, 0.16614074],\n", 190 | " [0.49062609, 0.16007809, 0.16183156, 0.17577564]])" 191 | ] 192 | }, 193 | "execution_count": 11, 194 | "metadata": {}, 195 | "output_type": "execute_result" 196 | } 197 | ], 198 | "source": [ 199 | "perm_test.calculate_neighborhood_distribution(A, B)" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": 12, 205 | "metadata": {}, 206 | "outputs": [ 207 | { 208 | "data": { 209 | "text/plain": [ 210 | "array([0.51183653, 0.16371792, 0.16022925, 0.1642163 ])" 211 | ] 212 | }, 213 | "execution_count": 12, 214 | "metadata": {}, 215 | "output_type": "execute_result" 216 | } 217 | ], 218 | "source": [ 219 | "np.divide(np.sum(B, axis=0), np.sum(B))" 220 | ] 221 | }, 222 | { 223 | "cell_type": "code", 224 | "execution_count": 14, 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "position_dict = dict()\n", 229 | "for i in range(0, C.shape[0]):\n", 230 | " position_dict[i] = C[i, :]" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": {}, 236 | "source": [ 237 | "### Create visualization of tissue" 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": 17, 243 | "metadata": {}, 244 | "outputs": [], 245 | "source": [ 246 | "viz.make_vor(1000, np.argmax(cell_assignments, axis=1), position_dict, 4, './sample_results/', 'test', [x for x in range(0, C.shape[0])])" 247 | ] 248 | }, 249 | { 250 | "cell_type": "markdown", 251 | "metadata": {}, 252 | "source": [ 253 | "### Heuristic assignment\n", 254 | "\n", 255 | "Estimated runtime ~30s" 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": 18, 261 | "metadata": {}, 262 | "outputs": [], 263 | "source": [ 264 | "graph = nx.from_numpy_array(A)\n", 265 | "\n", 266 | "cell_assignments = assign_labels.heuristic_assignment(graph, cell_type_probabilities, neighborhood_probabilities, mode='graph', dim=1000, position_dict=position_dict, grid_size=50, revision_iters=100, n_swaps=25)" 267 | ] 268 | }, 269 | { 270 | "cell_type": "code", 271 | "execution_count": 20, 272 | "metadata": {}, 273 | "outputs": [], 274 | "source": [ 275 | "B = build_assignment_matrix(cell_assignments, n_cell_types=4)" 276 | ] 277 | }, 278 | { 279 | "cell_type": "markdown", 280 | "metadata": {}, 281 | "source": [ 282 | "### observed results\n" 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": 21, 288 | "metadata": {}, 289 | "outputs": [ 290 | { 291 | "data": { 292 | "text/plain": [ 293 | "array([[0.56692352, 0.14563081, 0.13742473, 0.15002095],\n", 294 | " [0.44158258, 0.19238652, 0.18695437, 0.17907653],\n", 295 | " [0.43342818, 0.19954317, 0.17421409, 0.19281456],\n", 296 | " [0.44527197, 0.18153189, 0.17959374, 0.1936024 ]])" 297 | ] 298 | }, 299 | "execution_count": 21, 300 | "metadata": {}, 301 | "output_type": "execute_result" 302 | } 303 | ], 304 | "source": [ 305 | "perm_test.calculate_neighborhood_distribution(A, B)" 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": 22, 311 | "metadata": {}, 312 | "outputs": [ 313 | { 314 | "data": { 315 | "text/plain": [ 316 | "array([0.51264542, 0.16590794, 0.15553869, 0.16590794])" 317 | ] 318 | }, 319 | "execution_count": 22, 320 | "metadata": {}, 321 | "output_type": "execute_result" 322 | } 323 | ], 324 | "source": [ 325 | "np.divide(np.sum(B, axis=0), np.sum(B))" 326 | ] 327 | }, 328 | { 329 | "cell_type": "markdown", 330 | "metadata": {}, 331 | "source": [ 332 | "### Visualize results" 333 | ] 334 | }, 335 | { 336 | "cell_type": "code", 337 | "execution_count": 19, 338 | "metadata": {}, 339 | "outputs": [], 340 | "source": [ 341 | "viz.make_vor(1000, cell_assignments, position_dict, 4, './sample_results/', 'test_heuristic', [x for x in range(0, C.shape[0])])" 342 | ] 343 | } 344 | ], 345 | "metadata": { 346 | "interpreter": { 347 | "hash": "d304a04614eb3bcdf587a00e63943d54bed679b3872ec3347f24b5fb4701eb73" 348 | }, 349 | "kernelspec": { 350 | "display_name": "Python 3.7.11 64-bit ('spleen': conda)", 351 | "language": "python", 352 | "name": "python3" 353 | }, 354 | "language_info": { 355 | "codemirror_mode": { 356 | "name": "ipython", 357 | "version": 3 358 | }, 359 | "file_extension": ".py", 360 | "mimetype": "text/x-python", 361 | "name": "python", 362 | "nbconvert_exporter": "python", 363 | "pygments_lexer": "ipython3", 364 | "version": "3.7.11" 365 | }, 366 | "orig_nbformat": 4 367 | }, 368 | "nbformat": 4, 369 | "nbformat_minor": 2 370 | } 371 | -------------------------------------------------------------------------------- /simulation/FigureS3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 13, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import numpy as np\n", 11 | "import matplotlib.pyplot as plt\n", 12 | "import seaborn as sns\n", 13 | "import networkx as nx\n", 14 | "from scipy.stats import beta\n", 15 | "import matplotlib.pyplot as plt\n", 16 | "from scipy.stats import betabinom, binom\n", 17 | "np.random.seed(512)" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 3, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "def build_assignment_matrix(attribute_dict, n_cell_types):\n", 27 | " data = list(attribute_dict.items())\n", 28 | " data = np.array(data) # Assignment matrix\n", 29 | "\n", 30 | " B = np.zeros((data.shape[0], n_cell_types)) # Empty matrix\n", 31 | "\n", 32 | " for i in range(0, data.shape[0]):\n", 33 | " t = int(data[i, 1])\n", 34 | " B[i, t] = 1\n", 35 | "\n", 36 | " return B\n", 37 | "\n", 38 | "def calculate_FOV_size(sampling_frac, min_x, max_x, min_y, max_y):\n", 39 | " area = (max_x - min_x) * (max_y - min_y)\n", 40 | " sampling_area = sampling_frac * area\n", 41 | " FOV_dim = np.round(np.sqrt(sampling_area))\n", 42 | " return FOV_dim\n", 43 | "\n", 44 | "def random_FOV(FOV_dim, df, min_x, max_x, min_y, max_y):\n", 45 | " x_start = np.random.randint(min_x, max_x - FOV_dim)\n", 46 | " y_start = np.random.randint(min_y, max_y - FOV_dim)\n", 47 | " \n", 48 | " x_filtered = df[(df['segment_px_x'] > x_start) & (df['segment_px_x'] < x_start + FOV_dim)]\n", 49 | " random_FOV = x_filtered[(x_filtered['segment_px_y'] > y_start) & (x_filtered['segment_px_y'] < y_start + FOV_dim)]\n", 50 | " \n", 51 | " return random_FOV\n", 52 | "\n", 53 | "def calculate_p_in_fov(fov, n_cell_types):\n", 54 | " types_in_fov = fov['cell_type_id'].astype(int).tolist()\n", 55 | " #print(types_in_fov)\n", 56 | " attribute_dict = dict(zip(fov.index, types_in_fov))\n", 57 | " B = build_assignment_matrix(attribute_dict, n_cell_types)\n", 58 | " return np.divide(np.sum(B, axis=0), B.shape[0])\n", 59 | "\n", 60 | "def estimate_beta_from_FOV(df, fov_dim, type_of_interest, n_fov, x_min, x_max, y_min, y_max, n_cell_types):\n", 61 | " p_list = []\n", 62 | " i = 0\n", 63 | " ns = []\n", 64 | " while i < n_fov:\n", 65 | " fov = random_FOV(fov_dim, df, x_min, x_max, y_min, y_max)\n", 66 | " if len(fov) > 5:\n", 67 | " # because we don't define the boundary of the TISSUE just the boundary of the image\n", 68 | " # you could draw an fov out of tissue bounds but in the enclosing rectangle\n", 69 | " p_list.append(calculate_p_in_fov(fov, n_cell_types))\n", 70 | " ns.append(len(fov))\n", 71 | " i += 1\n", 72 | " else:\n", 73 | " continue\n", 74 | " print(ns)\n", 75 | " sample_proportions = np.vstack(p_list)\n", 76 | " props_of_interest = sample_proportions[:, type_of_interest]\n", 77 | " \n", 78 | " sample_mean = np.mean(props_of_interest)\n", 79 | " sample_var = np.var(props_of_interest)\n", 80 | " \n", 81 | " #print()\n", 82 | " alpha_hat = sample_mean * (((sample_mean*(1-sample_mean))/sample_var) - 1)\n", 83 | " beta_hat = (1 - sample_mean) * (((sample_mean*(1-sample_mean))/sample_var) - 1)\n", 84 | "\n", 85 | " return alpha_hat, beta_hat, props_of_interest, ns\n", 86 | "\n", 87 | "def estimate_beta_from_FOV_ds(df, fov_dim, type_of_interest, n_fov, x_min, x_max, y_min, y_max, \n", 88 | " n_cell_types, target_size):\n", 89 | " p_list = []\n", 90 | " i = 0\n", 91 | " ns = []\n", 92 | " while i < n_fov:\n", 93 | " fov = random_FOV(fov_dim, df, x_min, x_max, y_min, y_max)\n", 94 | " if len(fov) == target_size:\n", 95 | " # because we don't define the boundary of the TISSUE just the boundary of the image\n", 96 | " # you could draw an fov out of tissue bounds but in the enclosing rectangle\n", 97 | " p_list.append(calculate_p_in_fov(fov, n_cell_types))\n", 98 | " ns.append(len(fov))\n", 99 | " i += 1\n", 100 | " elif len(fov) > target_size:\n", 101 | " #n_to_remove = len(fov) - target_size\n", 102 | " fov = fov.sample(n=target_size, replace=False)\n", 103 | " p_list.append(calculate_p_in_fov(fov, n_cell_types))\n", 104 | " ns.append(len(fov))\n", 105 | " i += 1\n", 106 | " else:\n", 107 | " continue\n", 108 | " #print(ns)\n", 109 | " sample_proportions = np.vstack(p_list)\n", 110 | " props_of_interest = sample_proportions[:, type_of_interest]\n", 111 | " \n", 112 | " sample_mean = np.mean(props_of_interest)\n", 113 | " sample_var = np.var(props_of_interest)\n", 114 | " \n", 115 | " #print()\n", 116 | " alpha_hat = sample_mean * (((sample_mean*(1-sample_mean))/sample_var) - 1)\n", 117 | " beta_hat = (1 - sample_mean) * (((sample_mean*(1-sample_mean))/sample_var) - 1)\n", 118 | "\n", 119 | " return alpha_hat, beta_hat, props_of_interest\n", 120 | "\n", 121 | "def p_fov_with_rarest(a, b, m, N):\n", 122 | " return 1 - np.power((BF(a, b + m)/BF(a, b)), N)\n", 123 | "\n", 124 | "def fov_cell_counts(df, fov_dim, toi, n_fov, x_min, x_max, y_min, y_max, n_cell_types, ret_n = False):\n", 125 | " \n", 126 | " p_list = []\n", 127 | " i = 0\n", 128 | " ns = []\n", 129 | " while i < n_fov:\n", 130 | " fov = random_FOV(fov_dim, df, x_min, x_max, y_min, y_max)\n", 131 | " if len(fov) > 10:\n", 132 | " types_in_fov = fov['cell_type_id'].astype(int).tolist()\n", 133 | " #print(types_in_fov)\n", 134 | " attribute_dict = dict(zip(fov.index, types_in_fov))\n", 135 | " B = build_assignment_matrix(attribute_dict, n_cell_types)\n", 136 | " p_list.append(np.sum(B, axis=0))\n", 137 | " ns.append(len(fov))\n", 138 | " i += 1\n", 139 | " else:\n", 140 | " continue\n", 141 | " \n", 142 | " sample_counts = np.vstack(p_list)\n", 143 | " \n", 144 | " if ret_n == True:\n", 145 | " return sample_counts[:, toi].astype(int), np.sum(sample_counts, axis=1).astype(int)\n", 146 | " else:\n", 147 | " return sample_counts[:, toi].astype(int)\n", 148 | " \n", 149 | "def convert_params(m, k):\n", 150 | " \"\"\" \n", 151 | " Convert mean/dispersion parameterization of a negative binomial to the ones scipy supports\n", 152 | "\n", 153 | " Parameters\n", 154 | " ----------\n", 155 | " m : float \n", 156 | " Mean\n", 157 | " k : float\n", 158 | " Overdispersion parameter. \n", 159 | " \"\"\"\n", 160 | " k = 1/k\n", 161 | " var = m + k * m ** 2\n", 162 | " p = (var - m) / var\n", 163 | " r = m ** 2 / (var - m)\n", 164 | " return r, 1-p\n", 165 | "\n", 166 | "def get_type_from_B(cell_id, B):\n", 167 | " idx, = np.where(B[cell_id])\n", 168 | " cell_type = idx[0]\n", 169 | " return cell_type" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 4, 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "A = np.load('./results/sample_adjmat_20200601.npy')\n", 179 | "C = np.load('./results/sample_positions_20200601.npy')" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": 7, 185 | "metadata": {}, 186 | "outputs": [], 187 | "source": [ 188 | "\n", 189 | "\n", 190 | "#Load data and do calcs\n", 191 | "B = np.load('./results/rarecell_optimized_B.npy')\n", 192 | "for i in range(0, B.shape[0]):\n", 193 | " l, = np.where(B[i,:])\n", 194 | " if len(l) > 1:\n", 195 | " #Randomly assign from the equally likely possibilities\n", 196 | " to_zero = np.random.choice(l, len(l)-1, replace=False)\n", 197 | " for j in to_zero:\n", 198 | " B[i, j] = 0\n", 199 | " \n", 200 | "type_col = [get_type_from_B(i, B) for i in range(0, A.shape[0])]\n", 201 | "df = pd.DataFrame(np.hstack((C, np.array(type_col).reshape(len(type_col),1).astype(int))),columns=['segment_px_x', 'segment_px_y', 'cell_type_id'])\n", 202 | "\n", 203 | "toi = 0\n", 204 | "n_fov = 10\n", 205 | "n_cell_types = 4\n", 206 | "x_min = min(df['segment_px_x'])\n", 207 | "x_max = max(df['segment_px_x'])\n", 208 | "y_min = min(df['segment_px_y'])\n", 209 | "y_max = max(df['segment_px_y'])\n", 210 | "\n", 211 | "fov_size_05r = calculate_FOV_size(0.01, x_min, x_max, y_min, y_max)\n", 212 | "fov_size_1r = calculate_FOV_size(0.01, x_min, x_max, y_min, y_max)\n", 213 | "fov_size_5r = calculate_FOV_size(0.05, x_min, x_max, y_min, y_max)\n", 214 | "fov_size_10r = calculate_FOV_size(0.1, x_min, x_max, y_min, y_max)\n", 215 | "\n", 216 | "\n", 217 | "n_toi_observed, ns = fov_cell_counts(df, fov_size_5r, toi, n_fov, x_min, x_max, y_min, y_max, n_cell_types, ret_n=True)\n", 218 | "props_of_interest = np.divide(n_toi_observed, ns)\n", 219 | "sample_mean = np.mean(props_of_interest)\n", 220 | "sample_var = np.var(props_of_interest)\n", 221 | "alpha_hat_rare = sample_mean * (((sample_mean*(1-sample_mean))/sample_var) - 1)\n", 222 | "beta_hat_rare = (1 - sample_mean) * (((sample_mean*(1-sample_mean))/sample_var) - 1)\n", 223 | "\n", 224 | "B = np.load('./results/negcontrol_optimized_B.npy')\n", 225 | "for i in range(0, B.shape[0]):\n", 226 | " l, = np.where(B[i,:])\n", 227 | " if len(l) > 1:\n", 228 | " #Randomly assign from the equally likely possibilities\n", 229 | " to_zero = np.random.choice(l, len(l)-1, replace=False)\n", 230 | " for j in to_zero:\n", 231 | " B[i, j] = 0\n", 232 | "type_col = [get_type_from_B(i, B) for i in range(0, A.shape[0])]\n", 233 | "df = pd.DataFrame(np.hstack((C, np.array(type_col).reshape(len(type_col),1).astype(int))),columns=['segment_px_x', 'segment_px_y', 'cell_type_id'])\n", 234 | "n_toi_observed, ns = fov_cell_counts(df, fov_size_5r, toi, n_fov, x_min, x_max, y_min, y_max, n_cell_types, ret_n=True)\n", 235 | "props_of_interest = np.divide(n_toi_observed, ns)\n", 236 | "sample_mean = np.mean(props_of_interest)\n", 237 | "sample_var = np.var(props_of_interest)\n", 238 | "alpha_hat_neg = sample_mean * (((sample_mean*(1-sample_mean))/sample_var) - 1)\n", 239 | "beta_hat_neg = (1 - sample_mean) * (((sample_mean*(1-sample_mean))/sample_var) - 1)\n", 240 | "\n", 241 | "B = np.load('./results/self_preference_B_regionheuristic_06082020.npy')\n", 242 | "for i in range(0, B.shape[0]):\n", 243 | " l, = np.where(B[i,:])\n", 244 | " if len(l) > 1:\n", 245 | " #Randomly assign from the equally likely possibilities\n", 246 | " to_zero = np.random.choice(l, len(l)-1, replace=False)\n", 247 | " for j in to_zero:\n", 248 | " B[i, j] = 0\n", 249 | "\n", 250 | "n_toi_observed, ns = fov_cell_counts(df, fov_size_5r, toi, n_fov, x_min, x_max, y_min, y_max, n_cell_types, ret_n=True)\n", 251 | "props_of_interest = np.divide(n_toi_observed, ns)\n", 252 | "sample_mean = np.mean(props_of_interest)\n", 253 | "sample_var = np.var(props_of_interest)\n", 254 | "alpha_hat_sp = sample_mean * (((sample_mean*(1-sample_mean))/sample_var) - 1)\n", 255 | "beta_hat_sp = (1 - sample_mean) * (((sample_mean*(1-sample_mean))/sample_var) - 1)\n", 256 | "\n" 257 | ] 258 | }, 259 | { 260 | "cell_type": "code", 261 | "execution_count": 16, 262 | "metadata": {}, 263 | "outputs": [ 264 | { 265 | "data": { 266 | "text/plain": [ 267 | "
" 268 | ] 269 | }, 270 | "metadata": {}, 271 | "output_type": "display_data" 272 | }, 273 | { 274 | "data": { 275 | "image/png": "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", 276 | "text/plain": [ 277 | "
" 278 | ] 279 | }, 280 | "metadata": {}, 281 | "output_type": "display_data" 282 | } 283 | ], 284 | "source": [ 285 | "import matplotlib\n", 286 | "from matplotlib import cm, colors\n", 287 | "import matplotlib.pyplot as plt\n", 288 | "\n", 289 | "matplotlib.rcParams.update({'axes.linewidth': 0.25,\n", 290 | " 'xtick.major.size': 2,\n", 291 | " 'xtick.major.width': 0.25,\n", 292 | " 'ytick.major.size': 2,\n", 293 | " 'ytick.major.width': 0.25,\n", 294 | " 'pdf.fonttype': 42,\n", 295 | " 'font.sans-serif': 'Arial'})\n", 296 | "\n", 297 | "plt.clf()\n", 298 | "sns.set_style(\"white\")\n", 299 | "sns.set_palette(\"colorblind\")\n", 300 | "fig, ax = plt.subplots(1, 1)\n", 301 | "x = np.arange(0, 100)\n", 302 | "\n", 303 | "ax.plot(x, betabinom.sf(0, x, alpha_hat_rare, beta_hat_rare), lw = 1, label=r\"p ~= 0.03 (rare cell)\", alpha = 0.9, c = 'b')\n", 304 | "ax.plot(x, betabinom.sf(0, x, alpha_hat_sp, beta_hat_sp), lw = 1, label=r\"p ~= 0.19 (self pref)\", alpha = 0.9)\n", 305 | "ax.plot(x, betabinom.sf(0, x, alpha_hat_neg, beta_hat_neg), lw = 1, label=r\"p ~= 0.22 (random)\", alpha = 0.9)\n", 306 | "\n", 307 | "ax.set_xlabel(r'N_cells')\n", 308 | "ax.set_ylabel(r'Probability of discovering rarest cell')\n", 309 | "ax.set_ylim(0,1.05)\n", 310 | "plt.legend()\n", 311 | "#plt.savefig('./spleen_data/figures/FigureS3A.pdf')\n", 312 | "plt.show()" 313 | ] 314 | }, 315 | { 316 | "cell_type": "code", 317 | "execution_count": 24, 318 | "metadata": {}, 319 | "outputs": [ 320 | { 321 | "data": { 322 | "text/plain": [ 323 | "
" 324 | ] 325 | }, 326 | "metadata": {}, 327 | "output_type": "display_data" 328 | }, 329 | { 330 | "data": { 331 | "image/png": "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", 332 | "text/plain": [ 333 | "
" 334 | ] 335 | }, 336 | "metadata": {}, 337 | "output_type": "display_data" 338 | } 339 | ], 340 | "source": [ 341 | "matplotlib.rcParams.update({'axes.linewidth': 0.25,\n", 342 | " 'xtick.major.size': 2,\n", 343 | " 'xtick.major.width': 0.25,\n", 344 | " 'ytick.major.size': 2,\n", 345 | " 'ytick.major.width': 0.25,\n", 346 | " 'pdf.fonttype': 42,\n", 347 | " 'font.sans-serif': 'Arial'})\n", 348 | "\n", 349 | "plt.clf()\n", 350 | "sns.set_style(\"white\")\n", 351 | "sns.set_palette(\"colorblind\")\n", 352 | "fig, ax = plt.subplots(1, 1)\n", 353 | "x = np.arange(0, 100)\n", 354 | "\n", 355 | "ax.plot(x, betabinom.sf(0, x, alpha_hat_rare, beta_hat_rare), lw = 1, label=r\"p ~= 0.03 (rare cell, spatial sampling)\", alpha = 0.9, c = 'g')\n", 356 | "ax.plot(x, binom.sf(0, x, 0.03), lw = 1, linestyle='dashed', c='g', label=r\"p ~= 0.03 (rare cell, random sampling)\", alpha = 0.9)\n", 357 | "ax.plot(x, betabinom.sf(0, x, alpha_hat_sp, beta_hat_sp), lw = 1, label=r\"p ~= 0.19 (self pref, spatial sampling)\", alpha = 0.9, c='b')\n", 358 | "ax.plot(x, binom.sf(0, x, 0.19), lw = 1, label=r\"p ~= 0.19 (self pref, random sampling)\", alpha = 0.9, c='b', linestyle='dashed')\n", 359 | "ax.plot(x, betabinom.sf(0, x, alpha_hat_neg, beta_hat_neg), lw = 1, label=r\"p ~= 0.22 (random, spatial sampling)\", alpha = 0.9, c='k')\n", 360 | "ax.plot(x, binom.sf(0, x, 0.22), lw = 1, label=r\"p ~= 0.22 (random, random sampling)\", alpha = 0.9, c='k', linestyle='dashed')\n", 361 | "plt.legend()\n", 362 | "plt.show()\n" 363 | ] 364 | }, 365 | { 366 | "cell_type": "markdown", 367 | "metadata": {}, 368 | "source": [ 369 | "## S3B" 370 | ] 371 | }, 372 | { 373 | "cell_type": "code", 374 | "execution_count": 67, 375 | "metadata": {}, 376 | "outputs": [], 377 | "source": [ 378 | "from scipy.optimize import fsolve\n", 379 | "from scipy.stats import nbinom" 380 | ] 381 | }, 382 | { 383 | "cell_type": "code", 384 | "execution_count": 117, 385 | "metadata": {}, 386 | "outputs": [], 387 | "source": [ 388 | "def p_discovery_in_n_fov(p0, n):\n", 389 | " return 1 - np.power(p0, n)\n", 390 | " \n", 391 | "def do_model_trials(df, fov_size, toi, n_cell_types, n_fov, n_trials, guess):\n", 392 | " \n", 393 | " x_min = min(df['segment_px_x'])\n", 394 | " x_max = max(df['segment_px_x'])\n", 395 | " y_min = min(df['segment_px_y'])\n", 396 | " y_max = max(df['segment_px_y'])\n", 397 | "\n", 398 | " trial_counter = 0\n", 399 | " \n", 400 | " def f2(k, p0, m):\n", 401 | " return np.power((m/k + 1), -k) - p0\n", 402 | " ns = np.arange(0,10)\n", 403 | " while trial_counter < n_trials:\n", 404 | " n_toi_observed = fov_cell_counts(df, fov_size, toi, n_fov, x_min, x_max, y_min, y_max, n_cell_types)\n", 405 | " values, counts = np.unique(n_toi_observed, return_counts=True)\n", 406 | " v = np.arange(0, max(values) + 1)\n", 407 | " val_count = dict(zip(values, counts))\n", 408 | " c = np.array([val_count[i] if i in values else 0 for i in v])\n", 409 | " \n", 410 | " #Parameter estimation with ZTM method\n", 411 | " n0 = c[0]\n", 412 | " N = np.sum(c)\n", 413 | " p0 = n0/N\n", 414 | " m = np.mean(n_toi_observed)\n", 415 | " k = fsolve(f2, x0=guess, args=(p0, m))\n", 416 | " r, p = convert_params(m, k[0])\n", 417 | " \n", 418 | " x = np.arange(0, 60)\n", 419 | " if trial_counter == 0:\n", 420 | " res = nbinom.pmf(x, r, p)\n", 421 | " fov = p_discovery_in_n_fov(p0, ns)\n", 422 | " else:\n", 423 | " res = np.vstack((res, nbinom.pmf(x, r, p)))\n", 424 | " fov = np.vstack((fov, p_discovery_in_n_fov(p0, ns)))\n", 425 | " trial_counter += 1 \n", 426 | " \n", 427 | " \n", 428 | " return res, fov\n", 429 | "\n", 430 | "def calc_errs(arr, ci=0.95):\n", 431 | " means = np.mean(arr, axis = 0)\n", 432 | " std = np.std(arr, axis = 0)\n", 433 | " ci = stats.norm.ppf(0.95) * (std/np.sqrt(arr.shape[0]))\n", 434 | " return means, ci\n", 435 | "\n" 436 | ] 437 | }, 438 | { 439 | "cell_type": "code", 440 | "execution_count": 98, 441 | "metadata": {}, 442 | "outputs": [], 443 | "source": [ 444 | "B = np.load('./results/rarecell_optimized_B.npy')\n", 445 | "for i in range(0, B.shape[0]):\n", 446 | " l, = np.where(B[i,:])\n", 447 | " if len(l) > 1:\n", 448 | " #Randomly assign from the equally likely possibilities\n", 449 | " to_zero = np.random.choice(l, len(l)-1, replace=False)\n", 450 | " for j in to_zero:\n", 451 | " B[i, j] = 0\n", 452 | " \n", 453 | "type_col = [get_type_from_B(i, B) for i in range(0, A.shape[0])]\n", 454 | "df = pd.DataFrame(np.hstack((C, np.array(type_col).reshape(len(type_col),1).astype(int))),columns=['segment_px_x', 'segment_px_y', 'cell_type_id'])\n", 455 | "n_toi_observed, ns = fov_cell_counts(df, fov_size_5r, toi, n_fov, x_min, x_max, y_min, y_max, n_cell_types, ret_n=True)" 456 | ] 457 | }, 458 | { 459 | "cell_type": "code", 460 | "execution_count": null, 461 | "metadata": { 462 | "scrolled": true 463 | }, 464 | "outputs": [], 465 | "source": [ 466 | "toi = 0\n", 467 | "n_fov = 20\n", 468 | "res_1r, fov_1r = do_model_trials(df, fov_size=fov_size_1r, toi=toi, n_cell_types=n_cell_types, n_fov=n_fov, n_trials = 100,guess = 0.9)\n", 469 | "res_5r, fov_5r = do_model_trials(df, fov_size=fov_size_5r, toi=toi, n_cell_types=n_cell_types, n_fov=n_fov, n_trials = 100, guess = 20)\n", 470 | "res_10r, fov_10r = do_model_trials(df, fov_size=fov_size_10r, toi=toi, n_cell_types=n_cell_types, n_fov=n_fov, n_trials = 100, guess = 500 )\n", 471 | "res_05r, fov_05r = do_model_trials(df, fov_size=fov_size_05r, toi=toi, n_cell_types=n_cell_types, n_fov=n_fov, n_trials = 100, guess = 0.2)\n" 472 | ] 473 | }, 474 | { 475 | "cell_type": "code", 476 | "execution_count": 127, 477 | "metadata": {}, 478 | "outputs": [ 479 | { 480 | "data": { 481 | "image/png": "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", 482 | "text/plain": [ 483 | "
" 484 | ] 485 | }, 486 | "metadata": {}, 487 | "output_type": "display_data" 488 | } 489 | ], 490 | "source": [ 491 | "from scipy import stats\n", 492 | "import matplotlib\n", 493 | "from matplotlib import cm, colors\n", 494 | "import matplotlib.pyplot as plt\n", 495 | "\n", 496 | "ns = np.arange(0,10)\n", 497 | "matplotlib.rcParams.update({'axes.linewidth': 0.25,\n", 498 | " 'xtick.major.size': 2,\n", 499 | " 'xtick.major.width': 0.25,\n", 500 | " 'ytick.major.size': 2,\n", 501 | " 'ytick.major.width': 0.25,\n", 502 | " 'pdf.fonttype': 42,\n", 503 | " 'font.sans-serif': 'Arial'})\n", 504 | "\n", 505 | "labels = ['0.5% FOV, osmFISH', '1% FOV', '5% FOV', '10% FOV',\n", 506 | " '0.5% FOV, IST', '1% FOV, IST', '5% FOV, IST', '10% FOV, IST']\n", 507 | "arrs = [fov_05r, fov_1r, fov_5r, fov_10r]\n", 508 | "sns.set_style('whitegrid')\n", 509 | "sns.set_palette('colorblind')\n", 510 | "for i in range(1, len(arrs)):\n", 511 | " mean, ci = calc_errs(arrs[i], ci=0.95)\n", 512 | " plt.errorbar(ns, mean, yerr=ci, label=str(labels[i]), capsize=4)\n", 513 | "\n", 514 | " _ = plt.xticks(ticks=ns)\n", 515 | " plt.xlabel(r'N FOV')\n", 516 | " plt.ylabel(r'Probability of discovery')\n", 517 | " plt.legend()\n", 518 | " #plt.tight_layout()\n", 519 | " #plt.savefig('../fig/nFOVs_cell'+str(toi)+'discovery_ci95.pdf')\n", 520 | "\n", 521 | "plt.savefig('./spleen_data/figures/FigureS3B.pdf')" 522 | ] 523 | }, 524 | { 525 | "cell_type": "markdown", 526 | "metadata": {}, 527 | "source": [] 528 | }, 529 | { 530 | "cell_type": "code", 531 | "execution_count": 103, 532 | "metadata": {}, 533 | "outputs": [ 534 | { 535 | "data": { 536 | "text/plain": [ 537 | "array([110, 90, 98, 100, 112, 96, 102, 103, 96, 108, 115, 98, 98,\n", 538 | " 112, 107, 92, 98, 114, 122, 110])" 539 | ] 540 | }, 541 | "execution_count": 103, 542 | "metadata": {}, 543 | "output_type": "execute_result" 544 | } 545 | ], 546 | "source": [ 547 | "ns" 548 | ] 549 | }, 550 | { 551 | "cell_type": "code", 552 | "execution_count": 121, 553 | "metadata": {}, 554 | "outputs": [ 555 | { 556 | "data": { 557 | "text/plain": [ 558 | "'ks = np.linspace(1e-9, 5)\\nplt.plot(ks, f2(ks, p0, m))'" 559 | ] 560 | }, 561 | "execution_count": 121, 562 | "metadata": {}, 563 | "output_type": "execute_result" 564 | }, 565 | { 566 | "data": { 567 | "image/png": "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", 568 | "text/plain": [ 569 | "
" 570 | ] 571 | }, 572 | "metadata": {}, 573 | "output_type": "display_data" 574 | } 575 | ], 576 | "source": [ 577 | "def f2(k, p0, m):\n", 578 | " return np.power((m/k + 1), -k) - p0\n", 579 | " \n", 580 | "n_toi_observed,ns = fov_cell_counts(df, fov_size_05r, toi, n_fov, x_min, x_max, y_min, y_max, n_cell_types, ret_n=True)\n", 581 | "values, counts = np.unique(n_toi_observed, return_counts=True)\n", 582 | "v = np.arange(0, max(values) + 1)\n", 583 | "val_count = dict(zip(values, counts))\n", 584 | "c = np.array([val_count[i] if i in values else 0 for i in v])\n", 585 | "'''plt.bar(v, c, color='g')\n", 586 | "_ = plt.xticks(ticks=v)'''\n", 587 | "\n", 588 | "n0 = c[0]\n", 589 | "N = np.sum(c)\n", 590 | "p0 = n0/N\n", 591 | "m = np.mean(n_toi_observed)\n", 592 | "\n", 593 | "ks = np.linspace(1e-9, 1)\n", 594 | "plt.plot(ks, f2(ks, p0, m))\n", 595 | "\n", 596 | "'''ks = np.linspace(1e-9, 5)\n", 597 | "plt.plot(ks, f2(ks, p0, m))'''" 598 | ] 599 | }, 600 | { 601 | "cell_type": "code", 602 | "execution_count": null, 603 | "metadata": {}, 604 | "outputs": [], 605 | "source": [] 606 | } 607 | ], 608 | "metadata": { 609 | "kernelspec": { 610 | "display_name": "Python 3", 611 | "language": "python", 612 | "name": "python3" 613 | }, 614 | "language_info": { 615 | "codemirror_mode": { 616 | "name": "ipython", 617 | "version": 3 618 | }, 619 | "file_extension": ".py", 620 | "mimetype": "text/x-python", 621 | "name": "python", 622 | "nbconvert_exporter": "python", 623 | "pygments_lexer": "ipython3", 624 | "version": "3.8.10" 625 | } 626 | }, 627 | "nbformat": 4, 628 | "nbformat_minor": 4 629 | } 630 | -------------------------------------------------------------------------------- /spatialpower/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/.DS_Store -------------------------------------------------------------------------------- /spatialpower/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/__init__.py -------------------------------------------------------------------------------- /spatialpower/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /spatialpower/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /spatialpower/main.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import networkx as nx 3 | import neighborhoods.permutationtest as perm_test 4 | import neighborhoods.neighborhoods as nbr 5 | from scipy import sparse 6 | from datetime import datetime 7 | #import yappi 8 | 9 | if __name__ == '__main__': 10 | 11 | # Data Loading 12 | 13 | A = np.load('../results/sample_adjmat_20200601.npy') 14 | B = np.load('../results/sample_ass_matrix_4types_20200601.npy') 15 | 16 | # Build required structures 17 | graph = nx.from_numpy_matrix(A) 18 | node_id_list = list(graph.nodes) 19 | attribute_dict = dict(zip(node_id_list, [-1 for i in graph.nodes])) 20 | n_cell_types = B.shape[1] 21 | S = sparse.coo_matrix(A) 22 | S_csc = S.tocsc() # Sparse Adj Mat 23 | 24 | # Configure trial parameters 25 | trials = 1000 26 | max_size = S_csc.shape[0] 27 | size_list = [i for i in range(0, max_size, 50)] 28 | 29 | if max(size_list) != max_size: 30 | size_list.append(max_size) 31 | 32 | # Configure results 33 | out_dir = '../results' 34 | now = datetime.now() 35 | datestamp = date_time = now.strftime("%m%d%Y") 36 | results_path = str(out_dir) + '/neighborhood_perm_test_' + str(datestamp) + '/' 37 | 38 | size = max_size 39 | H_gt = perm_test.calculate_neighborhood_distribution_sparse(S_csc, B) 40 | 41 | nbr.run_test(results_path, S_csc, B, H_gt, size, n_jobs=-1, trials=750, plot=False, graph=graph, graph_id=None, threshold=0.1) 42 | 43 | 44 | frac_list = np.linspace(0, 1, 11) # Measure every 10% 45 | size_list = np.round(frac_list * max_size).astype(int) 46 | 47 | same_size_trials = 1 48 | size = size_list[1] 49 | 50 | '''for size in size_list[1:-1]: 51 | print(size) 52 | for j in range(0, same_size_trials): 53 | # Generate the subgraph 54 | sg = perm_test.create_subgraph(graph, size, 1)[0] 55 | sg_A, sg_B = perm_test.parse_subgraph(sg, graph, B) 56 | 57 | # Get the ground truth distributions for this subgraph. 58 | sg_gt = perm_test.calculate_neighborhood_distribution_sparse(sg_A, sg_B) 59 | 60 | # Now permute this graph 61 | nbr.run_test(results_path, sg_A, sg_B, sg_gt, size, n_jobs=-1, trials=750, plot=False, graph = graph, graph_id=j, 62 | threshold=0.1)''' -------------------------------------------------------------------------------- /spatialpower/neighborhoods/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/neighborhoods/__init__.py -------------------------------------------------------------------------------- /spatialpower/neighborhoods/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/neighborhoods/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /spatialpower/neighborhoods/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/neighborhoods/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /spatialpower/neighborhoods/__pycache__/neighborhoods.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/neighborhoods/__pycache__/neighborhoods.cpython-37.pyc -------------------------------------------------------------------------------- /spatialpower/neighborhoods/__pycache__/permutationtest.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/neighborhoods/__pycache__/permutationtest.cpython-37.pyc -------------------------------------------------------------------------------- /spatialpower/neighborhoods/__pycache__/permutationtest.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/neighborhoods/__pycache__/permutationtest.cpython-38.pyc -------------------------------------------------------------------------------- /spatialpower/neighborhoods/neighborhoods.py: -------------------------------------------------------------------------------- 1 | import neighborhoods.permutationtest as perm_test 2 | import numpy as np 3 | import networkx as nx 4 | import multiprocessing as mp 5 | from datetime import datetime 6 | import errno 7 | from joblib import Parallel, delayed 8 | import os 9 | from glob import glob 10 | 11 | 12 | def build_assignment_matrix(attribute_dict, n_cell_types): 13 | data = list(attribute_dict.items()) 14 | data = np.array(data) # Assignment matrix 15 | 16 | B = np.zeros((data.shape[0], n_cell_types)) # Empty matrix 17 | 18 | for i in range(0, data.shape[0]): 19 | t = data[i, 1] 20 | B[i, t] = 1 21 | 22 | return B 23 | 24 | 25 | def parse_results(results, size, out_dir): 26 | print("Writing results...") 27 | print(str(out_dir)) 28 | print(len(results)) 29 | print(results[0]) 30 | for i in range(0, len(results)): 31 | arr = results[i] 32 | np.save(str(out_dir) + str(size) + 'cells_shuffle' + str(i), arr) 33 | return 34 | 35 | 36 | def run_test(results_path, A, B, H_gt, size, n_jobs, trials, plot, graph, graph_id, threshold): 37 | ''' 38 | Runs the permutation test, and calculates signficant interaction pairs. 39 | 40 | Parameters 41 | ---------- 42 | results_path: str, the root results dir 43 | size : int, size of graph to calculate. 44 | n_jobs: int, number of parallel jobs to spawn 45 | trials: int, number of shuffles in empirical distribution 46 | plot : bool, generate histogram of each pairwise relation if True. 47 | 48 | Returns 49 | ------- 50 | None 51 | ''' 52 | # Make results dir 53 | try: 54 | os.mkdir(results_path) 55 | except OSError as exc: 56 | if exc.errno != errno.EEXIST: 57 | raise 58 | pass 59 | 60 | # Perform calculations. 61 | results = [] 62 | if graph_id == None: 63 | out_dir = results_path + str(size) + '_cells/' 64 | else: 65 | out_dir = results_path + str(size) + '_cells_' + str(graph_id) + '/' 66 | 67 | try: 68 | os.mkdir(out_dir) 69 | except OSError as exc: 70 | if exc.errno != errno.EEXIST: 71 | raise 72 | pass 73 | 74 | n_cell_types = B.shape[1] 75 | args = (A, B, size, graph, n_cell_types) 76 | arg_list = [args for i in range(0, trials)] 77 | results = Parallel(n_jobs=n_jobs, verbose=1, backend="sequential")( 78 | delayed(perm_test.permutation_test_trial_wrapper)(args) for args in arg_list) 79 | #parse_results(results, size, out_dir) 80 | 81 | # Process results 82 | 83 | '''# size_list = [] 84 | result_list = [] 85 | 86 | file_list = glob(out_dir + '*.npy') 87 | for f in file_list: 88 | arr = np.load(f) 89 | # size_list.append(size) 90 | result_list.append(arr)''' 91 | 92 | arr = np.dstack(results) # stack into a 3-D array 93 | n_types = arr.shape[0] 94 | 95 | enriched_pairs = [] 96 | depleted_pairs = [] 97 | 98 | for i in range(0, n_types): 99 | for j in range(0, n_types): 100 | ground_truth_score = H_gt[i, j] 101 | emp_dist = arr[i, j, :] 102 | indices, = np.where(emp_dist < ground_truth_score) 103 | p = (len(emp_dist) - len(indices) + 1) / (len(emp_dist) + 1) 104 | if p <= threshold: 105 | enriched_pairs.append([i, j, p]) 106 | elif p >= 1 - threshold: 107 | depleted_pairs.append([i, j, p]) 108 | 109 | # Visualize empirical distribution 110 | if plot == True: 111 | plt.clf() 112 | # sns.set(style = 'white') 113 | plt.hist(arr[2, 2, :], color='k') 114 | plt.xlim(0, 1) 115 | plt.xlabel("Probability of Interaction between " + str(i) + " and " + str(j)) 116 | plt.ylabel("Count") 117 | plt.savefig(out_dir + "distplot_" + str(i) + "_" + str(j) + ".pdf") 118 | 119 | # Write results matrix. 120 | np.save(out_dir + "enriched_pairs.npy", np.array(enriched_pairs)) 121 | np.save(out_dir + "depleted_pairs.npy", np.array(depleted_pairs)) 122 | 123 | return 124 | 125 | def run_test_nosave(A, B, H_gt, size, n_jobs, trials, graph, threshold): 126 | ''' 127 | Runs the permutation test, and calculates signficant interaction pairs. 128 | 129 | Parameters 130 | ---------- 131 | size : int, size of graph to calculate. 132 | n_jobs: int, number of parallel jobs to spawn 133 | trials: int, number of shuffles in empirical distribution 134 | plot : bool, generate histogram of each pairwise relation if True. 135 | 136 | Returns 137 | ------- 138 | enriched_pairs : array-like 139 | depleted_pairs : array-like 140 | ''' 141 | 142 | n_cell_types = B.shape[1] 143 | args = (A, B, size, graph, n_cell_types) 144 | arg_list = [args for i in range(0, trials)] 145 | results = Parallel(n_jobs=n_jobs, verbose=1, backend="sequential")( 146 | delayed(perm_test.permutation_test_trial_wrapper)(args) for args in arg_list) 147 | #parse_results(results, size, out_dir) 148 | 149 | arr = np.dstack(results) # stack into a 3-D array 150 | n_types = arr.shape[0] 151 | 152 | enriched_pairs = [] 153 | depleted_pairs = [] 154 | 155 | for i in range(0, n_types): 156 | for j in range(0, n_types): 157 | ground_truth_score = H_gt[i, j] 158 | emp_dist = arr[i, j, :] 159 | indices, = np.where(emp_dist < ground_truth_score) 160 | p = (len(emp_dist) - len(indices) + 1) / (len(emp_dist) + 1) 161 | if p <= threshold: 162 | enriched_pairs.append([i, j, p]) 163 | elif p >= 1 - threshold: 164 | depleted_pairs.append([i, j, p]) 165 | 166 | # Write results matrix. 167 | np.save(out_dir + "enriched_pairs.npy", np.array(enriched_pairs)) 168 | np.save(out_dir + "depleted_pairs.npy", np.array(depleted_pairs)) 169 | 170 | return enriched_pairs, depleted_pairs -------------------------------------------------------------------------------- /spatialpower/neighborhoods/permutationtest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import networkx as nx 3 | from scipy import sparse 4 | 5 | def create_subgraph(graph, subgraph_size, n=1): 6 | ''' 7 | Returns the nodes in a breadth-first subgraph. 8 | 9 | Parameters 10 | ---------- 11 | graph : NetworkX.Graph 12 | The graph object of the full tissue. 13 | subgraph_size : int 14 | the size of the subgraph to generate (number of nodes) 15 | n : int 16 | the number of subgraphs to generate. Default = 1 17 | 18 | Returns 19 | ------- 20 | subgraphs : Array-like 21 | list of n lists of nodes contained in subgraph. 22 | ''' 23 | # subgraph_size = 500 24 | counter = 0 25 | subgraphs = [] 26 | 27 | while counter < n: 28 | subgraph_nodes = [] 29 | searched = [] 30 | node_list = list(graph.nodes) 31 | 32 | start_node = np.random.randint(0, max(node_list)) 33 | subgraph_nodes.append(start_node) 34 | 35 | for i in graph.neighbors(start_node): 36 | subgraph_nodes.append(i) 37 | 38 | searched.append(start_node) 39 | 40 | while len(subgraph_nodes) < subgraph_size: 41 | for node in subgraph_nodes: 42 | if node not in searched: 43 | searched.append(node) # Now we're searching that node. 44 | for i in graph.neighbors(node): 45 | if i not in subgraph_nodes: 46 | subgraph_nodes.append(i) 47 | if len(subgraph_nodes) >= subgraph_size: 48 | break 49 | else: 50 | continue 51 | else: 52 | continue 53 | else: 54 | continue 55 | 56 | if len(subgraph_nodes) >= subgraph_size: 57 | break 58 | else: 59 | continue 60 | 61 | subgraphs.append(subgraph_nodes) 62 | counter += 1 63 | 64 | return subgraphs 65 | 66 | def shuffle_labels(ass_matrix, n_cell_types): 67 | ''' 68 | Shuffles the cell assignment matrix. 69 | 70 | Parameters 71 | ---------- 72 | ass_matrix: array_like 73 | The assignment matrix, B. 74 | 75 | Returns 76 | ------- 77 | B: array_like 78 | An assignment matrix with shuffled assignmments. 79 | ''' 80 | node_ids = [i for i in range(0, ass_matrix.shape[0])] # This has to be for the original B. 81 | 82 | assignments = [] 83 | 84 | for i in node_ids: 85 | counter = 0 86 | for j in range(0, n_cell_types): 87 | if ass_matrix[i, j] == 1: 88 | break 89 | else: 90 | counter += 1 91 | assignments.append(j) 92 | 93 | assignments = np.array(assignments) 94 | np.random.shuffle(assignments) # in place shuffling 95 | 96 | shuffled_data = np.vstack((node_ids, assignments)).T 97 | B = np.zeros((shuffled_data.shape[0], n_cell_types)) 98 | 99 | for i in range(0, shuffled_data.shape[0]): 100 | t = int(shuffled_data[i, 1]) 101 | # print(i,t) 102 | B[i, t] = 1 103 | 104 | return B 105 | 106 | 107 | def calculate_neighborhood_distribution(adj_matrix, ass_matrix): 108 | ''' 109 | Calculates the probabilities of cell type adjacencies. 110 | 111 | Parameters 112 | ---------- 113 | adj_matrix: array_like 114 | The N x N adjacency matrix for the graph. 115 | ass_matrix: array-like 116 | The assignment matrix, B. 117 | 118 | Returns 119 | ------- 120 | H: array_like 121 | A K x K matrix where element (i, j) is the fraction 122 | of neighors of type i that are of type j. 123 | ''' 124 | A = adj_matrix 125 | B = ass_matrix 126 | 127 | AB = np.matmul(A, B) # Number of neighbors by type (cols), per node (row) 128 | 129 | edge_count_by_node = np.matmul(A, A.T) # The diagonal of this matrix is the edge count for each node 130 | edge_count_by_node = np.diag(edge_count_by_node) 131 | 132 | aux1 = 1 / edge_count_by_node 133 | aux1[aux1 == np.inf] = 0 134 | aux1 = aux1 * np.identity(A.shape[0]) 135 | 136 | cell_type_counts = np.matmul(B.T, B) # The diagonal of this matrix is the number of cells of each type 137 | cell_type_counts = np.diag(cell_type_counts) 138 | 139 | aux2 = 1 / cell_type_counts 140 | aux2[aux2 == np.inf] = 0 141 | aux2 = aux2 * np.identity(B.shape[1]) 142 | # diag(B.T*B)^-1 143 | 144 | H = np.matmul(np.matmul(aux2, B.T), np.matmul(aux1, AB)) 145 | 146 | return H 147 | 148 | def calculate_neighborhood_distribution_sparse(adj_matrix, ass_matrix): 149 | ''' 150 | Calculates the probabilities of cell type adjacencies. 151 | 152 | Parameters 153 | ---------- 154 | adj_matrix: sparse matrix 155 | The N x N adjacency matrix for the graph in scipy sparse format. 156 | ass_matrix: array-like 157 | The assignment matrix, B. 158 | 159 | Returns 160 | ------- 161 | H: array_like 162 | A K x K matrix where element (i, j) is the fraction 163 | of neighors of type i that are of type j. 164 | ''' 165 | A = adj_matrix 166 | B = ass_matrix 167 | 168 | edge_count_by_node = np.multiply(A, A.T) 169 | edge_count_by_node = edge_count_by_node.diagonal() 170 | aux1 = 1/edge_count_by_node 171 | aux1[aux1 == np.inf] = 0 172 | aux1 = sparse.identity(A.shape[0]).multiply(aux1) 173 | 174 | cell_type_counts = np.matmul(B.T, B) # The diagonal of this matrix is the number of cells of each type 175 | cell_type_counts = np.diag(cell_type_counts) 176 | 177 | aux2 = 1/cell_type_counts 178 | aux2[aux2 == np.inf] = 0 179 | aux2 = aux2 * np.identity(B.shape[1]) 180 | 181 | AB = A*B 182 | 183 | aux3 = np.matmul(aux2, B.T) 184 | aux4 = aux1*AB 185 | 186 | H = np.matmul(aux3, aux4) 187 | 188 | return H 189 | 190 | def calculate_enrichment_statistic(adj_matrix, ass_matrix, type_a, type_b): 191 | ''' 192 | Calculates the probabilities of cell type adjacencies. 193 | 194 | Parameters 195 | ---------- 196 | adj_matrix: sparse matrix 197 | The N x N adjacency matrix for the graph in scipy sparse format. 198 | ass_matrix: array-like 199 | The assignment matrix, B. 200 | type_a : int 201 | index of first type in the interaction pair 202 | type_b : int 203 | index of first type in the interaction pair 204 | 205 | Returns 206 | ------- 207 | X: float 208 | the interaction enrichment statistic, centered at 0. 209 | ''' 210 | A = adj_matrix 211 | B = ass_matrix 212 | 213 | AB = A @ B 214 | C = B.T @ AB 215 | 216 | E = np.sum(np.sum(A, axis=1))/2 # Number of edges 217 | n = A.shape[0] #Number of cells 218 | i = type_a 219 | j = type_b 220 | 221 | f_a = np.sum(B, axis=0)[i]/n #in full graph 222 | f_b = np.sum(B, axis=0)[j]/n 223 | 224 | if i != j: 225 | N_ab = C[i,j] 226 | else: 227 | N_ab = C[i,j] / 2 # When i==j, you're on the diagonal and there's a double count. 228 | 229 | X_ab = N_ab/(2*f_a*f_b*E) - 1 230 | 231 | return X_ab 232 | 233 | def perform_z_test(X1, X2): 234 | ''' 235 | Tests the difference between the distributions of 236 | enrichment statistics. 237 | 238 | Paramters 239 | --------- 240 | X1 : np.array 241 | The array of x statistics from tissue 1 242 | X2 : np.array 243 | The array of x statistics from tissue 2 244 | 245 | Returns 246 | ------- 247 | z : float 248 | The z statistics 249 | p : float 250 | the p-value of the z statistic (1 sided) 251 | 252 | ''' 253 | from scipy import stats 254 | X1_bar = np.mean(X1) 255 | X2_bar = np.mean(X2) 256 | 257 | sigma_1 = np.std(X1) 258 | sigma_2 = np.std(X2) 259 | 260 | n_1 = len(X1) 261 | n_2 = len(X2) 262 | 263 | sem_1 = sigma_1/np.sqrt(n_1) 264 | sem_2 = sigma_2/np.sqrt(n_2) 265 | 266 | z = (np.abs(X1_bar-X2_bar)) / np.sqrt(sem_1 + sem_2) 267 | p = 1 - stats.norm.cdf(z) 268 | 269 | return z, p 270 | 271 | 272 | def parse_subgraph(subgraph_nodes, graph, ass_matrix): 273 | """ 274 | Convert list of nodes to subgraph induced by that list. 275 | 276 | Parameters 277 | ---------- 278 | subgraph_nodes: array_like 279 | The list of nodes on which to induce the subgraph 280 | graph: nx.Graph 281 | The networkx graph of the full network. 282 | ass_matrix: array-like 283 | The assignment matrix, B, of the full network. 284 | 285 | Returns 286 | ------- 287 | sg_adj: CSC Sparse Matrix 288 | The adjacency matrix of the subgraph. 289 | sg_ass: array_like 290 | The assignment matrix of the subgraph. 291 | """ 292 | 293 | sg = graph.subgraph(subgraph_nodes) 294 | sg_adj = nx.to_scipy_sparse_matrix(sg, format='csc') # New adjacency matrix. 295 | sg_ass = ass_matrix[list(sg.nodes)] 296 | 297 | return sg_adj, sg_ass 298 | 299 | 300 | def permutation_test_trial(adj_matrix, ass_matrix, size, graph, n_cell_types): 301 | """ 302 | Conducts a trial of the permutation test. 303 | Builds subgraph, shuffles network, then recalculates H. 304 | 305 | Parameters 306 | ---------- 307 | adj_matrix: array_like 308 | The N x N adjacency matrix for the graph. 309 | ass_matrix: array-like 310 | The assignment matrix, B. 311 | size: int 312 | The size of the subgraph to calculate 313 | graph: nx.Graph 314 | The networkx graph of the full network. 315 | 316 | Returns 317 | ------- 318 | H: array_like 319 | A K x K matrix where element (i, j) is the fraction 320 | of neighbors of type i that are of type j. 321 | """ 322 | 323 | if size == adj_matrix.shape[0]: 324 | shuffled_graph = shuffle_labels(ass_matrix, n_cell_types) 325 | H = calculate_neighborhood_distribution_sparse(adj_matrix, shuffled_graph) 326 | 327 | else: 328 | subgraph_nodes = create_subgraph(graph, size, 1)[0] 329 | sg_adj, sg_ass = parse_subgraph(subgraph_nodes, graph, ass_matrix) 330 | shuffled_graph = shuffle_labels(sg_ass, n_cell_types) 331 | H = calculate_neighborhood_distribution_sparse(sg_adj, shuffled_graph) 332 | 333 | return H 334 | 335 | 336 | def permutation_test_trial_wrapper(args): 337 | """ 338 | Parallelization wrapper for the permutation test trial. 339 | 340 | Parameters 341 | ---------- 342 | args: tuple 343 | Format (adj_matrix, ass_matrix, size, graph, n_cell_types) 344 | 345 | Returns 346 | ------- 347 | H: array_like 348 | the result of the permutation test trial 349 | """ 350 | # print("starting " + str(mp.current_process())) 351 | adj_matrix = args[0] 352 | ass_matrix = args[1] 353 | size = args[2] 354 | graph = args[3] 355 | n_cell_types = args[4] 356 | # H = permumation_test_trial 357 | H = permutation_test_trial(adj_matrix, ass_matrix, size, graph, n_cell_types) 358 | # print("ending " + str(mp.current_process())) 359 | 360 | return H 361 | -------------------------------------------------------------------------------- /spatialpower/tissue_generation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/tissue_generation/__init__.py -------------------------------------------------------------------------------- /spatialpower/tissue_generation/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/tissue_generation/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /spatialpower/tissue_generation/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/tissue_generation/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /spatialpower/tissue_generation/__pycache__/assign_labels.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/tissue_generation/__pycache__/assign_labels.cpython-37.pyc -------------------------------------------------------------------------------- /spatialpower/tissue_generation/__pycache__/assign_labels.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/tissue_generation/__pycache__/assign_labels.cpython-38.pyc -------------------------------------------------------------------------------- /spatialpower/tissue_generation/__pycache__/visualization.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/tissue_generation/__pycache__/visualization.cpython-37.pyc -------------------------------------------------------------------------------- /spatialpower/tissue_generation/__pycache__/visualization.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarman-cell-observatory/PowerAnalysisForSpatialOmics/148719f5a000422b5ba52ad6335573e4c3f9aeeb/spatialpower/tissue_generation/__pycache__/visualization.cpython-38.pyc -------------------------------------------------------------------------------- /spatialpower/tissue_generation/assign_labels.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import networkx as nx 3 | import operator 4 | 5 | def get_unassigned_nodes(node_id_list, attribute_dict): 6 | unassigned = [] 7 | for n in node_id_list: 8 | if attribute_dict[n] == -1: 9 | unassigned.append(n) 10 | return unassigned 11 | 12 | def sample_cell_type(dist): 13 | x_i = np.random.multinomial(1, dist) 14 | for i in range(0, len(x_i)): 15 | if x_i[i] == 1: 16 | return i 17 | 18 | def kl_divergence(p, q): 19 | ''' 20 | Calculate the KL Divergence between two distrbitions 21 | 22 | Parameters 23 | ---------- 24 | p : Array-like, distribution 1 25 | q : Array-like, distribution 2 26 | 27 | Returns 28 | ------- 29 | div : float, KL divergence 30 | ''' 31 | p = np.array(p) 32 | q = np.array(q) 33 | 34 | e = 0.00000001 # this is just to avoid div 0 35 | 36 | p = p + e 37 | q = q + e 38 | 39 | div = np.sum(p*np.log(p/q)) 40 | return div 41 | 42 | def get_type_proportions(n_types, observed): 43 | try: 44 | unique, counts = np.unique(observed, return_counts = True) 45 | except: 46 | unique, counts = onp.unique(observed, return_counts = True) 47 | 48 | if len(unique) == n_types: 49 | return counts/len(observed) 50 | 51 | else: 52 | proportions = [] 53 | for i in range(0, n_types): 54 | counter = 0 55 | for j in observed: 56 | if j == i: 57 | counter += 1 58 | proportions.append(counter/len(observed)) 59 | return proportions 60 | 61 | def check_cell_type_dist(n_cell_types, attribute_dict, cell_type_probabilities, silent=False): 62 | n_cells = len(attribute_dict) 63 | values = list(attribute_dict.values()) 64 | 65 | type_proportions = get_type_proportions(n_cell_types, values) 66 | 67 | try: 68 | type_proportions = np.round(get_type_proportions(n_cell_types, list(attribute_dict.values())), decimals=3) 69 | except: 70 | type_proportions = onp.round(get_type_proportions(n_cell_types, list(attribute_dict.values())), decimals=3) 71 | 72 | 73 | kl = kl_divergence(type_proportions, cell_type_probabilities) 74 | 75 | if silent == False: 76 | print("Expected Cell Type Dist: " + str(cell_type_probabilities)) 77 | print("Observed Cell Type Dist: " + str(type_proportions)) 78 | print("K-L Divergence: " + str(kl)) 79 | 80 | return type_proportions, kl 81 | 82 | def check_neighborhood_dist(n_cell_types, attribute_dict, neighborhood_probabilities, graph, d, silent=False): 83 | values = np.array(list(attribute_dict.values())) 84 | nodes_of_type = [] 85 | results = [[] for i in range(0, n_cell_types)] 86 | 87 | for i in range(0, n_cell_types): 88 | nodes_of_type.append(np.where(values == i)) 89 | 90 | for i in range(0, n_cell_types): 91 | node_arr = nodes_of_type[i][0] 92 | 93 | for node in node_arr: 94 | distance = d 95 | 96 | neighborhood = nx.ego_graph(graph, node, radius = distance) 97 | neighborhood_nodes = list(neighborhood.nodes) 98 | if len(neighborhood_nodes) > 1: 99 | neighborhood_nodes = [n for n in neighborhood_nodes if n != node] 100 | neighborhood_types = [attribute_dict[n] for n in neighborhood_nodes] 101 | neighborhood_proportions = get_type_proportions(n_cell_types, neighborhood_types) 102 | results[i].append(neighborhood_proportions) 103 | 104 | mean_distributions = [] 105 | 106 | for i in range(0, n_cell_types): 107 | trials = results[i] 108 | trials = np.array(trials) 109 | sums = np.sum(trials, axis = 0) 110 | mean = np.divide(sums, len(nodes_of_type[i][0])) 111 | mean = np.round(mean, decimals = 3) 112 | mean_distributions.append(mean) 113 | 114 | mean_distributions = np.array(mean_distributions) 115 | kl = kl_divergence(mean_distributions, neighborhood_probabilities) 116 | 117 | if silent == False: 118 | print("Observed Neighborhood Distribution:") 119 | print(str(mean_distributions)) 120 | print("Expected Neighborhood Distribution:") 121 | print(str(neighborhood_probabilities)) 122 | print("K-L Divergence: " + str(kl)) 123 | 124 | return mean_distributions, kl 125 | 126 | def swap_types(observed_neighborhood, expected_neighborhood, attribute_dict, region_nodes): 127 | dif = observed_neighborhood - expected_neighborhood 128 | max_type = np.argmax(dif) #max difference 129 | min_type = np.argmin(dif) 130 | n_in_region = len(region_nodes) 131 | 132 | n_max_type = n_in_region * np.abs(observed_neighborhood[max_type]) 133 | n_min_type = n_in_region * np.abs(observed_neighborhood[min_type]) 134 | 135 | expected_max_type = np.rint(expected_neighborhood[max_type] * n_in_region) 136 | expected_min_type = np.rint(expected_neighborhood[min_type] * n_in_region) 137 | 138 | if n_min_type <= n_max_type: 139 | expected_to_swap = n_max_type - expected_max_type 140 | 141 | if expected_to_swap > expected_min_type: 142 | n_to_swap = expected_min_type 143 | else: 144 | n_to_swap = expected_to_swap 145 | 146 | # select nodes to swap 147 | max_type_nodes = [] 148 | 149 | for node in region_nodes: 150 | if attribute_dict[node] == max_type: 151 | max_type_nodes.append(node) 152 | 153 | swap_count = 0 154 | if len(max_type_nodes) == 0: 155 | pass 156 | else: 157 | #print('here') 158 | while swap_count < n_to_swap: 159 | node_to_swap = np.random.choice(max_type_nodes) 160 | attribute_dict[node_to_swap] = min_type 161 | swap_count += 1 162 | 163 | return attribute_dict 164 | 165 | def heuristic_assignment(graph, cell_type_probabilities, neighborhood_probabilities, mode, dim, position_dict, grid_size, revision_iters=300, n_swaps = 50): 166 | n_cell_types = neighborhood_probabilities.shape[0] 167 | node_id_list = list(graph.nodes) 168 | attribute_dict = dict(zip(node_id_list, [-1 for i in graph.nodes])) 169 | 170 | x_lims = [z for z in range(0, dim+1, grid_size)] 171 | y_lims = [z for z in range(0, dim+1, grid_size)] 172 | 173 | j = 0 174 | while j < len(x_lims) - 1: 175 | x_min = x_lims[j] 176 | x_max = x_lims[j + 1] 177 | k = 0 178 | while k < len(y_lims) - 1: 179 | y_min = y_lims[k] 180 | y_max = y_lims[k + 1] 181 | 182 | region_nodes = [] 183 | for i in node_id_list: 184 | x = position_dict[i][0] 185 | if x_min <= x < x_max or (x_max == dim and x_min <= x <= x_max): 186 | y = position_dict[i][1] 187 | if y_min <= y < y_max or (y_max == dim and y_min <= y <= y_max): 188 | region_nodes.append(i) 189 | else: 190 | continue 191 | if mode == 'region': 192 | # Pick a starting point by eigenvector centrality and sample its type 193 | '''subgraph = graph.subgraph(region_nodes) 194 | subgraph_centrality = nx.eigenvector_centrality(subgraph, max_iter=1000) 195 | start_node_id = max(subgraph_centrality.items(), key = operator.itemgetter(1))[0]''' 196 | 197 | start_node_id = np.random.choice(region_nodes) 198 | start_node_type = sample_cell_type(cell_type_probabilities) 199 | attribute_dict[start_node_id] = start_node_type # set it in the attributes dict 200 | 201 | # Now set the remaining probabilities in the region. 202 | 203 | for node in region_nodes: 204 | if node != start_node_id: 205 | attribute_dict[node] = sample_cell_type(neighborhood_probabilities[start_node_type]) 206 | else: 207 | continue 208 | 209 | elif mode == 'graph': 210 | # Pick a starting point by at random. 211 | start_node_id = np.random.choice(region_nodes) 212 | start_node_type = sample_cell_type(cell_type_probabilities) 213 | attribute_dict[start_node_id] = start_node_type # set it in the attributes dict 214 | 215 | # Calculate the neighborhood of the start node. 216 | graph_distance = 1 217 | neighborhood = nx.ego_graph(graph, start_node_id, radius = graph_distance) 218 | neighborhood_nodes = list(neighborhood.nodes) 219 | 220 | # Now set the remaining probabilities in the region. 221 | 222 | for node in neighborhood_nodes: 223 | if node != start_node_id: 224 | attribute_dict[node] = sample_cell_type(neighborhood_probabilities[start_node_type]) 225 | else: 226 | continue 227 | k += 1 228 | j += 1 229 | 230 | # Shift and Recalculate 231 | x_lims = [z for z in range(25, dim + 1, grid_size)] 232 | y_lims = [z for z in range(25, dim + 1, grid_size)] 233 | 234 | j = 0 235 | while j < len(x_lims) - 1: 236 | x_min = x_lims[j] 237 | x_max = x_lims[j + 1] 238 | k = 0 239 | while k < len(y_lims) - 1: 240 | y_min = y_lims[k] 241 | y_max = y_lims[k + 1] 242 | 243 | region_nodes = [] 244 | for i in node_id_list: 245 | x = position_dict[i][0] 246 | if x_min <= x < x_max or (x_max == dim and x_min <= x <= x_max): 247 | y = position_dict[i][1] 248 | if y_min <= y < y_max or (y_max == dim and y_min <= y <= y_max): 249 | region_nodes.append(i) 250 | else: 251 | continue 252 | 253 | if mode == 'region': 254 | # Pick a starting point by eigenvector centrality and sample its type 255 | '''subgraph = graph.subgraph(region_nodes) 256 | subgraph_centrality = nx.eigenvector_centrality(subgraph, max_iter = 1000) 257 | start_node_id = max(subgraph_centrality.items(), key = operator.itemgetter(1))[0]''' 258 | start_node_type = np.random.choice(region_nodes) 259 | start_node_type = attribute_dict[start_node_id] 260 | 261 | # Calculate the observed and expected distributions of cell type in the neighborhood 262 | expected_neighborhood = neighborhood_probabilities[start_node_type] 263 | observed_neighborhood = [attribute_dict[x] for x in region_nodes] 264 | observed_neighborhood = get_type_proportions(n_cell_types, observed_neighborhood) 265 | 266 | divergence = kl_divergence(observed_neighborhood, expected_neighborhood) 267 | # print("First divergence: ", divergence) 268 | if divergence > 0.25: 269 | #print(divergence) 270 | div = 100 271 | attempts = 0 272 | while div > 0.25 and attempts < n_swaps: 273 | attribute_dict = swap_types(observed_neighborhood, expected_neighborhood, 274 | attribute_dict, region_nodes) 275 | expected_neighborhood = neighborhood_probabilities[start_node_type] 276 | observed_neighborhood = [attribute_dict[x] for x in region_nodes] 277 | observed_neighborhood = get_type_proportions(n_cell_types, observed_neighborhood) 278 | div = kl_divergence(observed_neighborhood, expected_neighborhood) 279 | attempts += 1 280 | # print(div) 281 | elif mode == 'graph': 282 | # Pick a starting point by at random. 283 | start_node_id = np.random.choice(region_nodes) 284 | start_node_type = sample_cell_type(cell_type_probabilities) 285 | attribute_dict[start_node_id] = start_node_type # set it in the attributes dict 286 | 287 | # Calculate the neighborhood of the start node. 288 | graph_distance = 1 289 | neighborhood = nx.ego_graph(graph, start_node_id, radius = graph_distance) 290 | neighborhood_nodes = list(neighborhood.nodes) 291 | 292 | # Now set the remaining probabilities in the region. 293 | 294 | for node in neighborhood_nodes: 295 | if node != start_node_id: 296 | attribute_dict[node] = sample_cell_type(neighborhood_probabilities[start_node_type]) 297 | else: 298 | continue 299 | k += 1 300 | j += 1 301 | 302 | if mode == 'graph': 303 | unassigned = get_unassigned_nodes(node_id_list, attribute_dict) 304 | 305 | while len(unassigned) > 0: 306 | start_node_id = np.random.choice(unassigned) 307 | start_node_type = sample_cell_type(cell_type_probabilities) 308 | attribute_dict[start_node_id] = start_node_type # set it in the attributes dict 309 | 310 | # Calculate the neighborhood of the start node. 311 | graph_distance = 1 312 | neighborhood = nx.ego_graph(graph, start_node_id, radius = graph_distance) 313 | neighborhood_nodes = list(neighborhood.nodes) 314 | 315 | # Now set the remaining probabilities in the region. 316 | 317 | for node in neighborhood_nodes: 318 | if node != start_node_id: 319 | attribute_dict[node] = sample_cell_type(neighborhood_probabilities[start_node_type]) 320 | else: 321 | continue 322 | 323 | unassigned = get_unassigned_nodes(node_id_list, attribute_dict) 324 | 325 | if mode == 'region': 326 | #extra revision 327 | for xx in range(0, revision_iters): 328 | # Pick a starting point by eigenvector centrality and sample its type 329 | '''subgraph = graph.subgraph(region_nodes) 330 | subgraph_centrality = nx.eigenvector_centrality(subgraph, max_iter = 1000) 331 | start_node_id = max(subgraph_centrality.items(), key = operator.itemgetter(1))[0]''' 332 | start_node_type = np.random.choice(region_nodes) 333 | start_node_type = attribute_dict[start_node_id] 334 | 335 | # Calculate the observed and expected distributions of cell type in the neighborhood 336 | expected_neighborhood = neighborhood_probabilities[start_node_type] 337 | observed_neighborhood = [attribute_dict[x] for x in region_nodes] 338 | observed_neighborhood = get_type_proportions(n_cell_types, observed_neighborhood) 339 | 340 | divergence = kl_divergence(observed_neighborhood, expected_neighborhood) 341 | # print("First divergence: ", divergence) 342 | if divergence > 0.25: 343 | #print(divergence) 344 | div = 100 345 | attempts = 0 346 | while div > 0.25 and attempts < 50: 347 | attribute_dict = swap_types(observed_neighborhood, expected_neighborhood, 348 | attribute_dict, region_nodes) 349 | expected_neighborhood = neighborhood_probabilities[start_node_type] 350 | observed_neighborhood = [attribute_dict[x] for x in region_nodes] 351 | observed_neighborhood = get_type_proportions(n_cell_types, observed_neighborhood) 352 | div = kl_divergence(observed_neighborhood, expected_neighborhood) 353 | attempts += 1 354 | # print(div) 355 | return attribute_dict 356 | 357 | def build_assignment_matrix(attribute_dict, n_cell_types): 358 | data = list(attribute_dict.items()) 359 | data = np.array(data) # Assignment matrix 360 | 361 | B = np.zeros((data.shape[0],n_cell_types)) # Empty matrix 362 | 363 | for i in range(0, data.shape[0]): 364 | t = data[i,1] 365 | B[i,t] = 1 366 | 367 | return B 368 | 369 | def optimize(adj_matrix, cell_type_probabilities, neighborhood_probabilities, 370 | l1 = 1, l2 = 0.5, rho = 1, learning_rate = 1e-3, iterations = 100): 371 | 372 | import random 373 | import itertools 374 | 375 | import jax 376 | import jax.numpy as np 377 | # Current convention is to import original numpy as "onp" 378 | import numpy as onp 379 | 380 | K = len(cell_type_probabilities) 381 | A = adj_matrix 382 | no_cells = A.shape[0 ] 383 | H = neighborhood_probabilities 384 | p = onp.round(no_cells*cell_type_probabilities) 385 | P = np.identity(K) 386 | 387 | def true_objective(B, A, P, H): 388 | #P is wrong it should not be multiplied by!, set P to identity 389 | aux = np.matmul(A.T, A) * np.identity(A.shape[1]) #is this correct? 390 | W = np.matmul(np.linalg.inv( aux ), A) 391 | aux = np.matmul(np.matmul(np.matmul(P,B.T),W),B) 392 | aux /= aux.sum(axis=1)[:,onp.newaxis] 393 | 394 | print("real probability", H) 395 | print("estimated probability given assignment B",aux) 396 | return np.trace(np.matmul(aux, H.T)) 397 | 398 | # objective function and constraints 399 | def objective(B, A, P, H): 400 | aux = np.matmul(A.T, A) * np.identity(A.shape[1]) #is this correct? 401 | W = np.matmul(np.linalg.inv( aux ), A) 402 | aux = np.matmul(np.matmul(np.matmul(P,B.T),W),B) 403 | 404 | #normalize such that it's a probability matrix, get rid of P eventually 405 | aux /= aux.sum(axis=1)[:,onp.newaxis] 406 | return np.trace(np.matmul(aux, H.T)) 407 | 408 | #it's possible to project B to the linear space before 409 | #computing the objective function (TODO) 410 | 411 | def constraint(B,l1,l2): 412 | neg=np.clip(B,a_min=0) 413 | aux1= np.sum(neg-B) 414 | excess=np.clip(B,a_max=1) 415 | aux2=np.sum(B-excess) 416 | 417 | #ps=np.ones(B.shape[0])*p 418 | #p = B.shape[0] * p 419 | #aux3=np.sum((np.sum(B,axis=1)-ps)**2) 420 | #rho = 0.1 421 | #aux3 = np.sum((np.sum(B,axis=0)-p)**2) #columns sum to p (number of cells not frequencies) only enforce this on extreme values 422 | aux3 = np.sum(np.multiply(extreme_values,np.sum(B,axis=0)-p)**2) 423 | ones=np.ones(B.shape[0]) 424 | aux4=np.sum((np.sum(B,axis=1)-ones)**2) # rows sum to 1 425 | 426 | return l1*(aux1+aux2) + l2*(rho*aux3+aux4) 427 | 428 | 429 | def f(B,l1,l2): 430 | return - objective(B, A, P, H) + constraint(B,l1,l2) 431 | 432 | extreme_hi = np.mean(p) + np.std(p) 433 | extreme_low = np.mean(p) - np.std(p) 434 | extreme_values = np.where(((p <= extreme_low) | (p >= extreme_hi)), 1, 0) 435 | 436 | #gradient descent on f= objective + l* constraint 437 | 438 | B = onp.random.randn(no_cells,K) 439 | #B = onp.random.randint(2, size=(no_cells, K)) 440 | 441 | #learning_rate=1e-3 442 | 443 | for n in range(iterations): 444 | 445 | #l1=1 446 | #l2=0.5 447 | 448 | # optimize B for l1, l2 449 | for i in range(50): 450 | loss_grad=jax.grad(f)(B,l1,l2) 451 | # Update parameters via gradient descent 452 | B = B - learning_rate * loss_grad 453 | if i>45: 454 | print(f(B,l1,l2)) 455 | l1=l1*2 456 | l2=l2*1.4 457 | if n%1==0: 458 | print("constraint ", constraint(B,1,1)) 459 | 460 | cell_assignment = B.round(decimals=3) 461 | cell_assignment = 1*(cell_assignment == cell_assignment.max(axis=1)[:,None]) 462 | 463 | #To avoid dual assignment after rounding, randomly correct equal probability assignments 464 | # in future could return a probabalistic assignment. 465 | x, = onp.where(onp.sum(cell_assignment, axis = 1) > 1) 466 | 467 | for i in range(0, len(x)): 468 | choices, = onp.where(cell_assignment[x[i],:] == 1) 469 | cell_assignment = jax.ops.index_update(cell_assignment, x[i], tuple(onp.random.choice(choices, size = len(choices) -1, replace=False)), 0) 470 | 471 | return cell_assignment -------------------------------------------------------------------------------- /spatialpower/tissue_generation/random_circle_packing.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | matplotlib.use('Agg') 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | import random 6 | import argparse 7 | import time 8 | from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas 9 | from matplotlib.figure import Figure 10 | 11 | def sample_line_segment(x_a, x_b, rc_a, rc_b, r_min, r_max): 12 | r = x_b - x_a 13 | l = np.linalg.norm(r, ord=2) # L2 norm 14 | r = r / np.linalg.norm(l) 15 | dr = r_max - r_min 16 | center = x_a 17 | radius = rc_a 18 | while True: 19 | R_new = dr * np.random.rand() + r_min 20 | C_new = center[-1] + r * (radius[-1] + R_new + r_max * np.random.rand()) 21 | d = l - np.linalg.norm(C_new + r * (R_new + rc_b) - x_a, ord=2) 22 | if d < 2 * (r_min + 1e-12): 23 | center = np.vstack((center, x_b)) 24 | radius = np.vstack((radius, rc_b)) 25 | break 26 | else: 27 | center = np.vstack((center, C_new)) 28 | radius = np.vstack((radius, R_new)) 29 | 30 | return center, radius 31 | 32 | 33 | def update_grid(X_new, R_new, G, r_min): 34 | _D = (G - X_new) 35 | _D = np.power(_D, 2) 36 | D = np.sum(_D, axis=1) 37 | thresh = np.power((R_new + r_min + 1e-12), 2) 38 | mask = D < thresh 39 | mask = np.invert(mask) 40 | G = G[mask] 41 | return G, mask 42 | 43 | 44 | def set_color(r_list, max_r, min_r, n_types): 45 | #color_list = ['#24557D', '#156944', '#5EC358', '#EB9420', '#E54116', '#F4BF1F', '#BC1826', '#CCB98E', 46 | # '#90B3B7', '#C4B6C1', '#E6ED40', '#5ECCB7', '#BB4E64', '#8C4FC9', '#E05B7C', '#26A8BD'] 47 | color_list = [(.14,.33,.49), (.08,.41,.27), (.37,.76,.35), (.92,.58,.13), (.90,.25,.09), (.96,.75,.12), (.74,.09,.15), 48 | (0.8,0.76,0.56), (0.56,0.7,0.72), (0.77,0.71,.76)] 49 | random.shuffle(color_list) 50 | assert n_types <= len(color_list) 51 | boundary_list = [] 52 | increment = (max_r - min_r) / n_types 53 | assigned_colors = [] 54 | 55 | for j in range(0, n_types): 56 | b = min_r + j * increment 57 | boundary_list.append(b) 58 | 59 | for r in r_list: 60 | 61 | upper_bound_idx = -1 # void value 62 | for i in range(0, len(boundary_list)): 63 | if r < boundary_list[i]: 64 | upper_bound_idx = i 65 | break 66 | assigned_colors.append(color_list[upper_bound_idx]) 67 | 68 | return assigned_colors 69 | 70 | 71 | def circle_intersect_check(r_0, r_1, x_0, x_1, y_0, y_1): 72 | """ 73 | Check if two circles intersect. 74 | 75 | Parameters 76 | ---------- 77 | r_0 : float 78 | radius of search circle 79 | r_1 : float 80 | radius of check circle 81 | x_0 : float 82 | x coord of search circle center 83 | x_1 : float 84 | x coord of check circle center 85 | y_0 : 86 | y coord of search circle center 87 | y_1 : 88 | x coord of check circle center 89 | Returns 90 | -------- 91 | indices : np.Array 92 | Numpy array of the circle indicies that intersect. 93 | """ 94 | 95 | distance_squared = np.power(x_0 - x_1, 2) + np.power(y_0 - y_1, 2) 96 | dr_squared = np.power(r_0 - r_1, 2) 97 | r_sum_squared = np.power(r_0 + r_1, 2) 98 | 99 | distance_squared = distance_squared.reshape(distance_squared.size, 1) 100 | dr_squared = dr_squared.reshape(dr_squared.size, 1) 101 | r_sum_squared = r_sum_squared.reshape(r_sum_squared.size, 1) 102 | 103 | # print(distance_squared.shape, dr_squared.shape, r_sum_squared.shape) 104 | 105 | mask_a = dr_squared <= distance_squared 106 | mask_b = distance_squared <= r_sum_squared 107 | mask_a = mask_a.astype(int) # 1 if true 108 | mask_b = mask_b.astype(int) # 1 if True 109 | mask = mask_a + mask_b 110 | mask = mask == 2 # Meets both conditions 111 | mask = mask.reshape(mask.size) 112 | mask = mask.astype(int) 113 | indices = mask.nonzero() 114 | 115 | return indices 116 | 117 | 118 | def voronoi_finite_polygons_2d(vor, radius=None): 119 | """ 120 | Reconstruct infinite voronoi regions in a 2D diagram to finite 121 | regions. 122 | 123 | Parameters 124 | ---------- 125 | vor : Voronoi 126 | Input diagram 127 | radius : float, optional 128 | Distance to 'points at infinity'. 129 | 130 | Returns 131 | ------- 132 | regions : list of tuples 133 | Indices of vertices in each revised Voronoi regions. 134 | vertices : list of tuples 135 | Coordinates for revised Voronoi vertices. Same as coordinates 136 | of input vertices, with 'points at infinity' appended to the 137 | end. 138 | 139 | """ 140 | 141 | if vor.points.shape[1] != 2: 142 | raise ValueError("Requires 2D input") 143 | 144 | new_regions = [] 145 | new_vertices = vor.vertices.tolist() 146 | 147 | center = vor.points.mean(axis=0) 148 | if radius is None: 149 | radius = vor.points.ptp(axis=0).max() * 2 150 | 151 | # Construct a map containing all ridges for a given point 152 | all_ridges = {} 153 | for (p1, p2), (v1, v2) in zip(vor.ridge_points, vor.ridge_vertices): 154 | all_ridges.setdefault(p1, []).append((p2, v1, v2)) 155 | all_ridges.setdefault(p2, []).append((p1, v1, v2)) 156 | 157 | # Reconstruct infinite regions 158 | for p1, region in enumerate(vor.point_region): 159 | vertices = vor.regions[region] 160 | 161 | if all([v >= 0 for v in vertices]): 162 | # finite region 163 | new_regions.append(vertices) 164 | continue 165 | 166 | # reconstruct a non-finite region 167 | ridges = all_ridges[p1] 168 | new_region = [v for v in vertices if v >= 0] 169 | 170 | for p2, v1, v2 in ridges: 171 | if v2 < 0: 172 | v1, v2 = v2, v1 173 | if v1 >= 0: 174 | # finite ridge: already in the region 175 | continue 176 | 177 | # Compute the missing endpoint of an infinite ridge 178 | 179 | t = vor.points[p2] - vor.points[p1] # tangent 180 | t /= np.linalg.norm(t) 181 | n = np.array([-t[1], t[0]]) # normal 182 | 183 | midpoint = vor.points[[p1, p2]].mean(axis=0) 184 | direction = np.sign(np.dot(midpoint - center, n)) * n 185 | far_point = vor.vertices[v2] + direction * radius 186 | 187 | new_region.append(len(new_vertices)) 188 | new_vertices.append(far_point.tolist()) 189 | 190 | # sort region counterclockwise 191 | vs = np.asarray([new_vertices[v] for v in new_region]) 192 | c = vs.mean(axis=0) 193 | angles = np.arctan2(vs[:, 1] - c[1], vs[:, 0] - c[0]) 194 | new_region = np.array(new_region)[np.argsort(angles)] 195 | 196 | # finish 197 | new_regions.append(new_region.tolist()) 198 | 199 | return new_regions, np.asarray(new_vertices) 200 | 201 | 202 | if __name__ == '__main__': 203 | 204 | 205 | parser = argparse.ArgumentParser(description="Configuration parameters for circle packing.") 206 | parser.add_argument('-x', '--width', type=float, default=500, help="Width of constraining rectangle") 207 | parser.add_argument('-y', '--height', type=float, default=500, help="Height of the contstraining rectangle") 208 | parser.add_argument('--rmax', type=float, default=25, help="Maximum circle radius.") 209 | parser.add_argument('--rmin', type=float, default=4, help="Minimum circle radius.") 210 | parser.add_argument('--visualization', action="store_true", 211 | help="If present, generate and save a visualization of the circle packing.") 212 | # parser.add_argument('--constrained', action="store_true", 213 | # help="If present, do not allow circle edges past boundary.") 214 | parser.add_argument('-o', '--outdir', type=str, default='./', help="The folder in which to store results.") 215 | parser.add_argument('-n', '--n_colors', type=int, default=10, help='The number of bins to color cell size.') 216 | parser.add_argument('-g', '--graph', default=True, 217 | help='If present, draw the graph representation of the circle packing.', action="store_true") 218 | parser.add_argument('-v', '--voronoi', default=True, action="store_true", help='If present, draw the voronoi.') 219 | args = parser.parse_args() 220 | 221 | ab = np.array([args.width, args.height]) 222 | 223 | r_min = args.rmin 224 | r_max = args.rmax 225 | visualization = args.visualization 226 | constrained = False 227 | 228 | if constrained: 229 | pass 230 | else: 231 | constrained = False 232 | 233 | outdir = args.outdir 234 | 235 | dx = max(min(ab) / 2e3, r_min / 50) # Sets up the increment. 236 | x = np.arange(0, ab[0] + dx, dx) 237 | y = np.arange(0, ab[1] + dx, dx) 238 | 239 | x, y = np.meshgrid(x, y) 240 | 241 | x_col = np.reshape(x, (np.size(x), 1)) 242 | y_col = np.reshape(y, (np.size(y), 1)) 243 | G = np.concatenate((x_col, y_col), axis=1) 244 | 245 | # Start by placing circles around the edges if constrained is false. 246 | 247 | dr = r_max - r_min 248 | corners = np.array([[0, 0], [1, 0], [1, 1], [0, 1]]) 249 | corner_vertices = np.multiply(ab, corners) 250 | 251 | if not constrained: 252 | x_a = corner_vertices 253 | x_b = np.roll(corner_vertices, [-1, -1]) # This command empirically seems equivalent to circshift(X, [-1 0]) 254 | 255 | rc = np.multiply(dr, np.random.rand(4, 1)) + r_min 256 | rc_a = rc 257 | rc_b = np.roll(rc, [-1]) 258 | 259 | C = [] 260 | R = [] 261 | 262 | for i in range(0, 4): 263 | Ci, Ri = sample_line_segment(x_a[i, :], x_b[i, :], rc_a[i], rc_b[i], r_min, r_max) 264 | Ci = Ci[:-1, :] 265 | # C[i] = Ci 266 | C.append(Ci) 267 | Ri = Ri[:-1, :] 268 | # R[i] = Ri 269 | R.append(Ri) 270 | 271 | R = np.vstack((R[0], R[1], R[2], R[3])) 272 | C = np.vstack((C[0], C[1], C[2], C[3])) 273 | 274 | for i in range(0, C.shape[0]): 275 | G, _ = update_grid(C[i, :], R[i], G, r_min) 276 | 277 | else: 278 | G_max = G + r_min + 1e-12 279 | G_min = G - r_min - 1e-12 280 | chk_in = (G_max <= ab) & (G_min >= [0, 0]) 281 | chk_in = chk_in.astype(int) 282 | chk_in = np.sum(chk_in, axis=1) == 2 283 | # keep [T, T] 284 | G = G[chk_in] 285 | C = [] 286 | R = [] 287 | 288 | circle_list = [] 289 | 290 | t = np.linspace(0, 2 * np.pi, 100)[np.newaxis] 291 | t = t.T 292 | sin = np.sin(t) 293 | cos = np.cos(t) 294 | P = np.hstack((sin, cos)) 295 | if np.count_nonzero(C) != 0: # Not empty 296 | for i in range(0, C.shape[0]): 297 | Pm = R[i] * P + C[i, :] 298 | circle_list.append(Pm) 299 | 300 | # Rejection sampling to populate interior of rectangle 301 | flag = True 302 | n = 0 303 | cnt = 0 304 | m = 0 305 | Ng = G.shape[0] 306 | 307 | while (G.shape[0] != 0) and cnt < 3e5: 308 | n += 1 309 | 310 | # New circle 311 | if flag and (cnt > 500 or (G.shape[0] < 0.95 * Ng)): 312 | # print("CONDITION 1") 313 | flag = False 314 | Rg = r_max * np.ones((G.shape[0], 1)) 315 | 316 | i = [] 317 | if cnt <= 500 and flag: 318 | # print("CONDITION 2") 319 | X_new = np.multiply(ab, np.random.rand(1, 2)) 320 | else: 321 | # print("CONDITION 3") 322 | i = np.random.randint(0, G.shape[0]) 323 | X_new = G[i, :] + (dx / 2) * (2 * np.random.rand(1, 2) - 1) 324 | X_new = np.minimum(np.maximum(X_new, np.array([0, 0])), ab) 325 | if cnt > 1e3: 326 | Rg[:] = np.maximum(0.95 * Rg, r_min) 327 | 328 | if np.count_nonzero(i) == 0: 329 | R_new = dr * np.random.rand() + r_min # radius 330 | else: 331 | R_new = (Rg[i] - r_min) * np.random.rand() + r_min 332 | 333 | if constrained: 334 | # print("CONSTRAINED CONDITION") 335 | X_new_max = X_new + R_new + 1e-12 336 | X_new_min = X_new - R_new - 1e-12 337 | mask_max = X_new_max <= ab 338 | mask_min = X_new_min >= np.array([0, 0]) 339 | mask = mask_max & mask_min 340 | mask = mask.astype(int) 341 | if np.sum(mask, axis=1) < 2: 342 | cnt += 1 343 | continue 344 | if np.count_nonzero(C) != 0: # not empty 345 | # print("NON EMPTY C") 346 | _d_in = C - X_new 347 | _d_in = np.power(_d_in, 2) 348 | d_in = np.sqrt(np.sum(_d_in, axis=1)) 349 | v = d_in 350 | v = v.reshape(v.size, 1) 351 | mask = v < (R + R_new) 352 | mask = mask.astype(int) 353 | if np.sum(mask) > 0: 354 | cnt += 1 355 | if np.count_nonzero(i) != 0: # Not empty 356 | Rg[i] = min([0.95 * Rg[i], min([0.99 * (R_new + dx / 2), r_max])]) 357 | Rg[i] = max([Rg[i], r_min]) 358 | continue 359 | 360 | # Accept new circle 361 | cnt = 0 362 | m += 1 363 | C = np.vstack((C, X_new)) 364 | R = np.vstack((R, R_new)) 365 | G, mask = update_grid(X_new, R_new, G, r_min) 366 | 367 | if not flag: 368 | Rg = Rg[mask] # make sure this doesn't need to be flipped. 369 | 370 | if visualization: 371 | Pm = R_new * P + X_new 372 | circle_list.append(Pm) 373 | 374 | time_stamp = str(time.strftime("%Y-%m-%d-%H%M%S")) 375 | assigned_colors = set_color(R, r_max, r_min, args.n_colors) 376 | 377 | if visualization: 378 | plt.clf() 379 | #fig = Figure() 380 | #canvas = FigureCanvas(fig) 381 | 382 | fig, ax = plt.subplots(figsize=(8, 8)) 383 | ax.set(xlim=(0 - 0.05 * ab[0], ab[0] + 0.05 * ab[0]), ylim=(0 - 0.05 * ab[1], ab[1] + 0.05 * ab[1])) 384 | ax.axis('equal') 385 | ax.axis('off') 386 | 387 | for i in range(0, R.shape[0]): 388 | radius = R[i] 389 | circle = circle_list[i] 390 | color = assigned_colors[i] 391 | plt.fill(circle[:, 0], circle[:, 1], facecolor=color, antialiased=False) 392 | 393 | plt.savefig(str(outdir) + 'circle_packing_' + time_stamp + '.png', dpi=350) 394 | 395 | arr = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep='') 396 | arr = arr.reshape(fig.canvas.get_width_height()[::-1] + (3,)) 397 | np.save(str(outdir) + 'circle_packing_' + time_stamp + '.npy', arr) 398 | if args.graph: 399 | import networkx as nx 400 | 401 | # For each circle, check all other circles to see if there is overlap. 402 | # Will need to track which are which, somehow use the indicies for this. 403 | 404 | search_radii = R + r_min 405 | search_array = np.hstack((C, R)) 406 | 407 | empty = np.array([np.nan for i in range(0, C.shape[0])]) 408 | empty = empty.reshape(empty.size, 1) 409 | search_array = np.hstack((search_array, empty)) 410 | 411 | neighbors_list = [] # This is a list of arrays of the neighbors of circles at index i 412 | 413 | for i in range(0, search_array.shape[0]): 414 | search_array[:, 3] = np.nan # Clear the column 415 | search_x = C[i, 0] 416 | search_y = C[i, 1] 417 | search_radius = search_radii[i] 418 | 419 | neighbors = circle_intersect_check(search_radius, search_array[:, 2], search_x, search_array[:, 0], 420 | search_y, search_array[:, 1]) 421 | neighbors_list.append(neighbors) 422 | 423 | adjacency_matrix = np.zeros((search_array.shape[0], search_array.shape[0])) 424 | 425 | for i in range(0, len(neighbors_list)): 426 | neighbors = neighbors_list[i][0] 427 | 428 | for n in neighbors: 429 | if i == n: 430 | continue 431 | 432 | else: 433 | adjacency_matrix[i, n] = 1 434 | adjacency_matrix[n, i] = 1 435 | 436 | position_dict = dict() 437 | 438 | for i in range(0, C.shape[0]): 439 | position_dict[i] = C[i, :] 440 | 441 | graph = nx.from_numpy_matrix(adjacency_matrix) 442 | 443 | plt.clf() 444 | fig, ax = plt.subplots(figsize=(8, 8)) 445 | 446 | nx.draw(graph, pos=position_dict, node_size=17, node_color=assigned_colors, with_labels=False) 447 | plt.savefig(str(outdir) + 'graph_' + time_stamp + '.png', dpi=350) 448 | # Save the adjacency matrix and the positions of the nodes. 449 | np.save(str(outdir) + 'C_' + time_stamp + '.npy', C) 450 | np.save(str(outdir) + 'A_' + time_stamp + '.npy', adjacency_matrix) 451 | 452 | if args.voronoi: 453 | from scipy.spatial import Voronoi, voronoi_plot_2d 454 | 455 | fig, ax = plt.subplots(1, 1, figsize=(8, 8)) 456 | 457 | vor = Voronoi(C) 458 | regions, vertices = voronoi_finite_polygons_2d(vor) 459 | # colorize 460 | for r in range(0, len(regions)): 461 | region = regions[r] 462 | polygon = vertices[region] 463 | plt.fill(*zip(*polygon), alpha=0.7, facecolor=assigned_colors[r], edgecolor='k') 464 | 465 | ax.axis('equal') 466 | ax.axis('off') 467 | ax.set(xlim=(0 - 0.05 * ab[0], ab[0] + 0.05 * ab[0]), ylim=(0 - 0.05 * ab[1], ab[1] + 0.05 * ab[1])) 468 | plt.savefig(str(outdir) + 'voronoi_' + time_stamp + '.png', dpi=350) 469 | -------------------------------------------------------------------------------- /spatialpower/tissue_generation/visualization.py: -------------------------------------------------------------------------------- 1 | from scipy.spatial import Voronoi, voronoi_plot_2d 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | 5 | 6 | def voronoi_finite_polygons_2d(vor, radius=None): 7 | """ 8 | Adapted from: https://stackoverflow.com/questions/36063533/clipping-a-voronoi-diagram-python/43023639#43023639 9 | 10 | Reconstruct infinite voronoi regions in a 2D diagram to finite 11 | regions. 12 | 13 | Parameters 14 | ---------- 15 | vor : Voronoi 16 | Input diagram 17 | radius : float, optional 18 | Distance to 'points at infinity'. 19 | 20 | Returns 21 | ------- 22 | regions : list of tuples 23 | Indices of vertices in each revised Voronoi regions. 24 | vertices : list of tuples 25 | Coordinates for revised Voronoi vertices. Same as coordinates 26 | of input vertices, with 'points at infinity' appended to the 27 | end. 28 | 29 | """ 30 | 31 | if vor.points.shape[1] != 2: 32 | raise ValueError("Requires 2D input") 33 | 34 | new_regions = [] 35 | new_vertices = vor.vertices.tolist() 36 | 37 | center = vor.points.mean(axis=0) 38 | if radius is None: 39 | radius = vor.points.ptp(axis=0).max()*2 40 | 41 | # Construct a map containing all ridges for a given point 42 | all_ridges = {} 43 | for (p1, p2), (v1, v2) in zip(vor.ridge_points, vor.ridge_vertices): 44 | all_ridges.setdefault(p1, []).append((p2, v1, v2)) 45 | all_ridges.setdefault(p2, []).append((p1, v1, v2)) 46 | 47 | # Reconstruct infinite regions 48 | for p1, region in enumerate(vor.point_region): 49 | vertices = vor.regions[region] 50 | 51 | if all([v >= 0 for v in vertices]): 52 | # finite region 53 | new_regions.append(vertices) 54 | continue 55 | 56 | # reconstruct a non-finite region 57 | ridges = all_ridges[p1] 58 | new_region = [v for v in vertices if v >= 0] 59 | 60 | for p2, v1, v2 in ridges: 61 | if v2 < 0: 62 | v1, v2 = v2, v1 63 | if v1 >= 0: 64 | # finite ridge: already in the region 65 | continue 66 | 67 | # Compute the missing endpoint of an infinite ridge 68 | 69 | t = vor.points[p2] - vor.points[p1] # tangent 70 | t /= np.linalg.norm(t) 71 | n = np.array([-t[1], t[0]]) # normal 72 | 73 | midpoint = vor.points[[p1, p2]].mean(axis=0) 74 | direction = np.sign(np.dot(midpoint - center, n)) * n 75 | far_point = vor.vertices[v2] + direction * radius 76 | 77 | new_region.append(len(new_vertices)) 78 | new_vertices.append(far_point.tolist()) 79 | 80 | # sort region counterclockwise 81 | vs = np.asarray([new_vertices[v] for v in new_region]) 82 | c = vs.mean(axis=0) 83 | angles = np.arctan2(vs[:,1] - c[1], vs[:,0] - c[0]) 84 | new_region = np.array(new_region)[np.argsort(angles)] 85 | 86 | # finish 87 | new_regions.append(new_region.tolist()) 88 | 89 | return new_regions, np.asarray(new_vertices) 90 | 91 | def get_cmap(n, name='Spectral'): 92 | '''Returns a function that maps each index in 0, 1, ..., n-1 to a distinct 93 | RGB color; the keyword argument name must be a standard mpl colormap name.''' 94 | return plt.cm.get_cmap(name, n) 95 | 96 | def make_vor(dim, attribute_dict, position_dict, n_cell_types, results_dir, graph_id, node_id_list): 97 | 98 | c = get_cmap(n_cell_types) 99 | colors = [c(attribute_dict[i]) for i in node_id_list] 100 | 101 | ab = [dim, dim] 102 | plt.clf() 103 | fig, ax2 = plt.subplots(1,1,figsize=(8,8)) 104 | 105 | '''# Plot the network 106 | nx.draw(graph, pos = position_dict, ax=ax1, node_color = colors, node_size=17, with_labels = False) 107 | plt.axis('on') 108 | ax1.tick_params(left=True, bottom=True, labelleft=True, labelbottom=True) 109 | ''' 110 | # Now plot the Voronoi Diagram 111 | 112 | point_list = [position_dict[i] for i in node_id_list] 113 | vor = Voronoi(point_list) 114 | regions, vertices = voronoi_finite_polygons_2d(vor) 115 | 116 | for r in range(0, len(regions)): 117 | region = regions[r] 118 | polygon = vertices[region] 119 | ax2.fill(*zip(*polygon), alpha=0.8, facecolor=colors[r], edgecolor = 'k') 120 | 121 | # plt.plot(C[:,0], C[:,1], 'o', markersize=1, color = 'k') 122 | ax2.axis('equal') 123 | ax2.set(xlim=(0 - 0.01*ab[0], ab[0] + 0.01 * ab[0]), ylim=(0 - 0.01*ab[1], ab[1] + 0.01 * ab[1])) 124 | plt.savefig(results_dir + 'vor_' + str(graph_id) + '.pdf') 125 | plt.close() 126 | return 127 | --------------------------------------------------------------------------------