├── .gitignore ├── .travis.yml ├── README.md ├── athena ├── __init__.py ├── attributer │ ├── __init__.py │ ├── base_attributer.py │ ├── deep_attributer.py │ ├── mappings.py │ ├── node_features.py │ ├── random_attributer.py │ └── so_attributer.py ├── dataset │ ├── __init__.py │ ├── _utils.py │ └── datasets.py ├── graph_builder │ ├── __init__.py │ ├── base_graph_builder.py │ ├── contact_graph_builder.py │ ├── graphBuilder.py │ ├── knn_graph_builder.py │ ├── mappings.py │ └── radius_graph_builder.py ├── metrics │ ├── __init__.py │ ├── constants.py │ ├── graph │ │ ├── __init__.py │ │ └── graph.py │ ├── heterogeneity │ │ ├── __init__.py │ │ ├── base_metrics.py │ │ └── metrics.py │ └── utils.py ├── neighborhood │ ├── __init__.py │ ├── base_estimators.py │ ├── estimators.py │ └── utils.py ├── plotting │ ├── __init__.py │ ├── matplotlibrc │ ├── utils.py │ └── visualization.py ├── preprocessing │ ├── __init__.py │ ├── _preprocessors.py │ └── preprocess.py └── utils │ ├── __init__.py │ ├── default_configs.py │ ├── docs │ ├── __init__.py │ └── default_docs.py │ ├── general.py │ └── tools │ ├── __init__.py │ ├── graph.py │ ├── image.py │ └── metric.py ├── dev_requirements.txt ├── docs ├── Makefile ├── _templates │ ├── module.rst │ └── package.rst ├── api_overview.rst ├── conf.py ├── index.rst ├── make.bat └── source │ ├── img │ ├── athena_logo.png │ ├── bulk-sc-spatial.jpg │ ├── dilation.png │ ├── entropic-measures.png │ ├── images.png │ ├── imc2.png │ ├── interactions-quant.png │ ├── interactions.png │ ├── interoperability.pdf │ ├── interoperability.png │ ├── local-global.pdf │ ├── local-global.png │ ├── measurement.jpg │ ├── metalabels.png │ ├── metrics-overview.png │ ├── overview.pdf │ ├── overview.png │ ├── overview_old.png │ ├── random.gif │ ├── sample.png │ ├── simulation.png │ ├── single-cell-analysis.png │ ├── single-cell-methods.png │ ├── spatialHeterogeneity.png │ ├── spatialOmics.pdf │ ├── spatialOmics.png │ └── spatialOmics_old.png │ ├── installation.md │ ├── introduction-spatialOmics.md │ ├── introduction-spatialOmics_files │ ├── introduction-spatialOmics_10_1.png │ ├── introduction-spatialOmics_12_1.png │ ├── introduction-spatialOmics_28_1.png │ ├── introduction-spatialOmics_30_0.png │ ├── introduction-spatialOmics_34_0.png │ ├── introduction-spatialOmics_38_1.png │ ├── introduction-spatialOmics_38_2.png │ ├── introduction-spatialOmics_40_0.png │ ├── introduction-spatialOmics_42_0.png │ ├── introduction-spatialOmics_47_1.png │ ├── introduction-spatialOmics_49_0.png │ ├── introduction-spatialOmics_50_1.png │ ├── introduction-spatialOmics_52_2.png │ └── introduction-spatialOmics_56_1.png │ ├── methodology.md │ ├── overview.md │ ├── quickstart.md │ ├── quickstart_files │ ├── quickstart_20_1.png │ ├── quickstart_22_0.png │ ├── quickstart_22_1.png │ └── quickstart_24_0.png │ ├── tutorial.md │ └── tutorial_files │ ├── tutorial_17_0.png │ ├── tutorial_19_0.png │ ├── tutorial_23_0.png │ ├── tutorial_27_0.png │ ├── tutorial_29_0.png │ ├── tutorial_33_0.png │ ├── tutorial_35_0.png │ ├── tutorial_37_0.png │ ├── tutorial_39_0.png │ ├── tutorial_44_0.png │ ├── tutorial_48_1.png │ ├── tutorial_52_0.png │ ├── tutorial_54_0.png │ ├── tutorial_56_0.png │ ├── tutorial_58_0.png │ ├── tutorial_64_1.png │ └── tutorial_68_0.png ├── licence.txt ├── py.typed ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── attributer │ ├── __init__.py │ ├── conftest.py │ ├── integration │ │ └── manual_test.ipynb │ └── unit │ │ ├── __init__.py │ │ └── test_attributer.py ├── graph_builder │ ├── __init__.py │ ├── integration │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── manual_test.ipynb │ │ ├── test_build_and_attribute.py │ │ └── test_isomorphism.py │ └── unit │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_graph_module.py │ │ └── test_subgraph_building.py └── metrics │ ├── __init__.py │ └── test_base_metrics.py └── tutorials ├── ATHENA_Supplementary.pdf ├── documentation.ipynb ├── img ├── athena_logo.png ├── bulk-sc-spatial.jpg ├── dilation.png ├── entropic-measures.png ├── images.png ├── imc2.png ├── interactions-quant.png ├── interactions.png ├── interoperability.pdf ├── interoperability.png ├── local-global.pdf ├── local-global.png ├── measurement.jpg ├── metalabels.png ├── metrics-overview.png ├── overview.pdf ├── overview.png ├── overview_old.png ├── random.gif ├── sample.png ├── simulation.png ├── single-cell-analysis.png ├── single-cell-methods.png ├── spatialHeterogeneity.png ├── spatialOmics.pdf ├── spatialOmics.png └── spatialOmics_old.png ├── introduction-spatialOmics.ipynb ├── overview.ipynb ├── quickstart.ipynb └── tutorial.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | docs/api/* 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | .pytest_cache/ 107 | 108 | # pycharm 109 | .idea 110 | .idea/ 111 | 112 | # files 113 | *.h5py 114 | *.tif? 115 | 116 | # DS_Stores 117 | .DS_Store 118 | */.DS_Store 119 | 120 | # ignore ATHENA doc 121 | #athena.md 122 | #docs/source/img/athena_*.png 123 | data/ 124 | single_cell_values_visium_cellmasks.pkl 125 | /bench 126 | ~/ 127 | /issues 128 | 129 | # Ignore .vscode 130 | .vscode 131 | 132 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | #services: 4 | # - docker 5 | 6 | language: python 7 | 8 | python: 9 | - 3.8 10 | 11 | #before_install: 12 | # - sudo apt-get -y install pandoc 13 | 14 | install: 15 | # install the package 16 | - pip install -U pip 17 | - pip install ai4scr-spatial-omics 18 | # - pip install git+ssh://git@github.ibm.com/AI4SCR-DEV/ATHENA.git 19 | # - pip install --progress-bar off -r requirements.txt 20 | - pip install --progress-bar off -r dev_requirements.txt 21 | - pip install -e . # - pip install git+ssh://git@github.ibm.com/AI4SCR-DEV/ATHENA.git 22 | 23 | script: 24 | # test import 25 | - python -c "import athena" 26 | 27 | jobs: 28 | include: 29 | - stage: Documentation 30 | python: 3.8 31 | script: 32 | # export tutorial as markdown to integrate it in the docs 33 | - jupyter nbconvert --to markdown --output-dir=docs/source --output='introduction-spatialOmics.md' tutorials/introduction-spatialOmics.ipynb 34 | - jupyter nbconvert --to markdown --output-dir=docs/source --output='overview.md' tutorials/overview.ipynb 35 | - jupyter nbconvert --to markdown --output-dir=docs/source --output='tutorial.md' tutorials/tutorial.ipynb 36 | - jupyter nbconvert --to markdown --output-dir=docs/source --output='quickstart.md' tutorials/quickstart.ipynb 37 | # build documentation pages 38 | - cp -r tutorials/img docs/source 39 | - cd docs && make clean && make html && cd .. 40 | deploy: 41 | - provider: pages 42 | skip_cleanup: true 43 | repo: AI4SCR/ATHENA 44 | # github_token: $GITHUB_TOKEN 45 | github_token: $GITHUB_TOKEN 46 | # Set in the settings page of your repository, as a secure variable 47 | # see https://docs.travis-ci.com/user/deployment/pages/#setting-the-github-token 48 | local_dir: docs/_build/html 49 | # github_url: github.ibm.com # defaults to github.com 50 | on: 51 | branch: master 52 | 53 | notifications: 54 | slack: 55 | rooms: 56 | - ibm-research:82jY54xYQAiJOqZ2ycRDM4jo 57 | on_success: always 58 | on_failure: always -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis.ibm.com/art-zurich/spatial-heterogeneity.svg?token=bmUqdLriQp1g3yv7TJC6&branch=master)](https://travis.ibm.com/art-zurich/spatial-heterogeneity) 2 | [![GitHub Pages](https://img.shields.io/badge/docs-sphinx-blue)](https://ai4scr.github.io/ATHENA/) 3 | 4 | ![athena logo](tutorials/img/athena_logo.png) 5 | 6 | ATHENA is an open-source computational framework written in Python that facilitates the visualization, processing and analysis of (spatial) heterogeneity from spatial omics data. ATHENA supports any spatially resolved dataset that contains spatial transcriptomic or proteomic measurements, including Imaging Mass Cytometry (IMC), Multiplexed Ion Beam Imaging (MIBI), multiplexed Immunohistochemisty (mIHC) or Immunofluorescence (mIF), seqFISH, MERFISH, Visium. 7 | 8 | ## Main functionalities 9 | ![overview](tutorials/img/overview.png) 10 | 11 | 1. ATHENA accomodates raw multiplexed images from spatial omics measurements. Together with the images, segmentation masks, cell-level, feature-level and sample-level annotations can be uploaded. 12 | 13 | 2. Based on the cell masks, ATHENA constructs graph representations of the data. The framework currently supports three flavors, namely radius, knn, and contact graphs. 14 | 15 | 3. ATHENA incorporates a variety of methods to quantify heterogeneity, such as global and local entropic scores. Furthermore, cell type interaction strength scores or measures of spatial clustering and dispersion. 16 | 17 | 4. Finally, the large collection of computed scores can be extracted and used as input in downstream machine learning models to perform tasks such as clinical data prediction, patient stratification or discovery of new (spatial) biomarkers. 18 | 19 | ## Manuscript 20 | ATHENA has been published as an Applications Note in _Bioinformatics_ ([Martinelli and Rapsomaniki, 2022](https://academic.oup.com/bioinformatics/article/38/11/3151/6575886)). In our Online [Supplementary material](https://oup.silverchair-cdn.com/oup/backfile/Content_public/Journal/bioinformatics/38/11/10.1093_bioinformatics_btac303/1/btac303_supplementary_data.pdf?Expires=1660121444&Signature=nNOXyBaPzIuE5inrrA97SQMVAlmH~A42Phehnla68hL2-G79c1xzI4OFewWLX~l1R9QMGW-7YNX8isTOMD9xzwH9~xCVJxLuBtcpobKOlx16Ha4tEcdme-LiFM7MC4H3LQrQT~~JRMaTNCN7TSDn8pcfkLsBK1WHbZ9C8qTAwUfJek~tt6fzH~ZwA5dJ0KZ49HzZpwA1DvYU0luxJbgzj3mSs6OczQw3b3B6qm7EV45ijdR447jfCLsz5pbtZ2J6yAuKbsEN5KmkIbUfujMo9vw7YrQOwJjaMWol1Cus5mbebpB6QOfP5jGU7LfiFR1SPQTZ~A0phAssndE~0p1ilQ__&Key-Pair-Id=APKAIE5G5CRDK6RD3PGA) you will find details on the methodology behind ATHENA. If you find ATHENA useful in your research, please consider citing: 21 | ``` 22 | @article{10.1093/bioinformatics/btac303, 23 | author = {Martinelli, Adriano Luca and Rapsomaniki, Maria Anna}, 24 | title = "{ATHENA: analysis of tumor heterogeneity from spatial omics measurements}", 25 | journal = {Bioinformatics}, 26 | volume = {38}, 27 | number = {11}, 28 | pages = {3151-3153}, 29 | year = {2022}, 30 | doi = {10.1093/bioinformatics/btac303}, 31 | } 32 | ``` 33 | 34 | ## Installation and Tutorials 35 | In our detailed [Online Documentation](https://ai4scr.github.io/ATHENA) you'll find: 36 | * Installation [instructions](https://ai4scr.github.io/ATHENA/source/installation.html). 37 | * An overview of ATHENA's [main components](https://ai4scr.github.io/ATHENA/source/overview.html) and [API](https://ai4scr.github.io/ATHENA/api_overview.html) 38 | * An end-to-end [tutorial](https://ai4scr.github.io/ATHENA/source/tutorial.html) using a publicly available Imaging Mass Cytometry dataset. 39 | * A detailed tutorial on ATHENA's `SpatialOmics` data container with examples on how to load your own spatial omics data ([link](https://ai4scr.github.io/ATHENA/source/introduction-spatialOmics.html)). 40 | 41 | -------------------------------------------------------------------------------- /athena/__init__.py: -------------------------------------------------------------------------------- 1 | """Module initialization.""" 2 | 3 | __version__ = "0.1.3" 4 | from . import metrics as metrics 5 | from . import graph_builder as graph 6 | from . import plotting as pl 7 | from . import neighborhood as neigh 8 | from . import preprocessing as pp 9 | from . import dataset 10 | from . import attributer as attributer -------------------------------------------------------------------------------- /athena/attributer/__init__.py: -------------------------------------------------------------------------------- 1 | from .node_features import add_node_features 2 | -------------------------------------------------------------------------------- /athena/attributer/base_attributer.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | import abc 3 | 4 | 5 | class BaseAttributer(ABC): 6 | 7 | def __init__(self, 8 | so, 9 | spl: str, 10 | graph_key: str, 11 | config: dict) -> None: 12 | """Base attributer constructor 13 | """ 14 | 15 | self.so = so 16 | self.spl = spl 17 | self.graph_key = graph_key 18 | self.config = config['attrs_params'] 19 | 20 | @abc.abstractmethod 21 | def __call__(self): 22 | """Attributes graph. Implemented in subclasses. 23 | """ 24 | raise NotImplementedError('Implemented in subclasses.') 25 | 26 | def clear_node_attrs(self) -> None: 27 | # Get key from the first node 28 | any_node = list(self.so.G[self.spl][self.graph_key].nodes)[0] 29 | 30 | # If the any_node dictionary (attrs) is not empty then clear all nodes. 31 | if len(self.so.G[self.spl][self.graph_key].nodes[any_node]) > 0: 32 | for node in self.so.G[self.spl][self.graph_key].nodes: 33 | self.so.G[self.spl][self.graph_key].nodes[node].clear() 34 | -------------------------------------------------------------------------------- /athena/attributer/deep_attributer.py: -------------------------------------------------------------------------------- 1 | from .base_attributer import BaseAttributer 2 | 3 | 4 | class DeepAttributer(BaseAttributer): 5 | 6 | def __init__(self, 7 | so, 8 | spl: str, 9 | graph_key: str, 10 | config: dict) -> None: 11 | """ 12 | Attributer class constructor. TODO: Compleat description, specify config structure. 13 | """ 14 | super().__init__(so, spl, graph_key, config) 15 | 16 | def __call__(self) -> None: 17 | """ 18 | Generates deep features and attributes them to nodes in so.G[spl][graph_key]. 19 | 20 | Returns: 21 | None. Attributes are saved to `so`. 22 | """ 23 | 24 | # TODO: implement this 25 | raise NotImplementedError('Not implemented yet.') 26 | 27 | def check_config(self) -> int: 28 | """ 29 | Check config integrity. TODO: Compleat description. 30 | """ 31 | 32 | # TODO: implement this 33 | raise NotImplementedError('Not implemented yet.') 34 | -------------------------------------------------------------------------------- /athena/attributer/mappings.py: -------------------------------------------------------------------------------- 1 | from .so_attributer import SoAttributer 2 | from .deep_attributer import DeepAttributer 3 | from .random_attributer import RandomAttributer 4 | 5 | GRAPH_ATTRIBUTER = { 6 | 'so': SoAttributer, 7 | 'deep': DeepAttributer, 8 | 'random': RandomAttributer 9 | } 10 | -------------------------------------------------------------------------------- /athena/attributer/node_features.py: -------------------------------------------------------------------------------- 1 | import copy as cp 2 | from ..utils.default_configs import GRAPH_ATTRIBUTER_DEFAULT_PARAMS 3 | from .mappings import GRAPH_ATTRIBUTER 4 | 5 | 6 | def add_node_features(so, 7 | spl: str, 8 | graph_key: str, 9 | features_type: str, 10 | config: dict) -> None: 11 | """ 12 | Maps parameters in `config` to downstream function which 13 | adds node features to graph `so.G[spl][graph_key]`. 14 | 15 | Args: 16 | - `so`: Spatial omics object which contains the graph to be 17 | attributed. 18 | - `spl`: String identifying the sample in `so`. 19 | - `graph_key`: String identifying the graph in `so` to attribute. 20 | - `features_type`: String specifying the type of parameters to assign. 21 | At the moment 3 possibilities. 22 | - 'so_feat'. Features from `so[spl].X` and/or `so[spl].obs` 23 | - 'deep_feat'. TODO: To be implemented. 24 | - 'random_feat' Random uniform [0, 1) features. 25 | - `config`: Parameters of the attribute method to be used downstream. 26 | If none are specified the defaults are used. Default values for each 27 | feature type can be seen in GRAPH_ATTRIBUTER_DEFAULT_PARAMS[features_type] 28 | dictionary in `athena.utils.default_configs`. 29 | 30 | Returns: 31 | - `None`: The changes are saved to the `so.G[spl][graph_key]` 32 | """ 33 | 34 | # Check for Args miss specification. 35 | try: 36 | so.G[spl][graph_key] 37 | except KeyError: 38 | raise KeyError(f'Either spl: "{spl}" or graph_key: "{graph_key}" is invalid.') 39 | 40 | # If no config is specified, use default config. 41 | if config is None: 42 | config = cp.deepcopy(GRAPH_ATTRIBUTER_DEFAULT_PARAMS[features_type]) 43 | 44 | # Instantiate attributer class and call it, thereby attributing `so.G[spl][graph_key]`. 45 | attributer = GRAPH_ATTRIBUTER[features_type](so, spl, graph_key, config) 46 | attributer() 47 | -------------------------------------------------------------------------------- /athena/attributer/random_attributer.py: -------------------------------------------------------------------------------- 1 | from .base_attributer import BaseAttributer 2 | import numpy as np 3 | import pandas as pd 4 | import networkx as nx 5 | 6 | 7 | class RandomAttributer(BaseAttributer): 8 | 9 | def __init__(self, 10 | so, 11 | spl: str, 12 | graph_key: str, 13 | config: dict) -> None: 14 | """ 15 | Attributer class constructor. Assigns uniform [0, 1) random values to each attribute. 16 | `config` must a dict with the following structure; 17 | 18 | `config = {'n_attrs': n_attrs}` 19 | 20 | where `n_attrs` is the number of random attributes to generate. 21 | """ 22 | super().__init__(so, spl, graph_key, config) 23 | 24 | def __call__(self) -> None: 25 | """ 26 | Generates random features and attributes them to nodes in so.G[spl][graph_key] 27 | 28 | Returns: 29 | None. Attributes are saved to `so`. 30 | """ 31 | 32 | # Check config. If no assertions are raised, return number of attrs. 33 | n_attrs = self.check_config() 34 | 35 | # Get index values form so 36 | index = self.so.obs[self.spl].index.values 37 | 38 | # Sample and put values into dict 39 | attrs = pd.DataFrame(np.random.rand(len(index), n_attrs), index=index).to_dict('index') 40 | 41 | # Check if the nodes already have attributes. If yes, clear them. 42 | self.clear_node_attrs() 43 | 44 | # Assign attrs to graph 45 | nx.set_node_attributes(self.so.G[self.spl][self.graph_key], attrs) 46 | 47 | def check_config(self) -> int: 48 | """ 49 | Check config integrity. 50 | 51 | Returns: 52 | Number of attributes to be generated. 53 | """ 54 | assert type(self.config['n_attrs']) is int, "Number of attributes must be an integer." 55 | assert self.config['n_attrs'] > 0, "Number of attributes cannot be zero." 56 | return self.config['n_attrs'] 57 | -------------------------------------------------------------------------------- /athena/attributer/so_attributer.py: -------------------------------------------------------------------------------- 1 | from .base_attributer import BaseAttributer 2 | import numpy as np 3 | import pandas as pd 4 | import networkx as nx 5 | 6 | 7 | class SoAttributer(BaseAttributer): 8 | 9 | def __init__(self, 10 | so, 11 | spl: str, 12 | graph_key: str, 13 | config: dict) -> None: 14 | """ 15 | Attributer class constructor. Gets attributes from `so`. Config must be a dict with 16 | the following structure; 17 | 18 | config = {'from_obs': bool, 19 | 'obs_cols': list, 20 | 'from_X': bool, 21 | 'X_cols': str or list} 22 | 23 | The value corresponding to key 'obs_cols' must be a non empty list of column names 24 | corresponding to so.obs[spl]. The value corresponding to key 'X_cols' must be either 25 | 'all' or a list of column names corresponding to so.X[spl]. These columns represent the 26 | attributes to be included. 27 | """ 28 | super().__init__(so, spl, graph_key, config) 29 | 30 | def __call__(self) -> None: 31 | """ 32 | Generates random features and attributes them to nodes in so.G[spl][graph_key] 33 | 34 | Returns: 35 | None. Attributes are saved to `so`. 36 | """ 37 | 38 | # Check config (that it is well defined). If no error is raised, slice and return data. 39 | self.check_config() 40 | obs_df, X_df = self.extract_data() 41 | 42 | # Join data and transform into dictionary. 43 | attrs = obs_df.merge(X_df, left_index=True, right_index=True, how='inner').to_dict('index') 44 | 45 | # Check if the nodes already have attributes. If yes, clear them. 46 | self.clear_node_attrs() 47 | 48 | # Add to node attributes of so.G[spl][graph_key] 49 | nx.set_node_attributes(self.so.G[self.spl][self.graph_key], attrs) 50 | 51 | def extract_data(self) -> tuple: 52 | """ 53 | Checks whether config is well defined. If no error is raised then the data is sliced 54 | according to the config. 55 | 56 | Returns: 57 | - `obs_df`: sliced so.obs[spl] or empty df with index same as so.obs[spl] 58 | - `X_df`: sliced so.X[spl] or empty df with index same as so.X[spl] 59 | """ 60 | 61 | # Check self.config['from_obs'] if its set to true. 62 | if self.config['from_obs']: 63 | # Subset obs[spl] 64 | obs_df = self.so.obs[self.spl][self.config['obs_cols']] 65 | else: 66 | obs_df = pd.DataFrame(index=self.so.obs[self.spl].index) 67 | 68 | # Check self.config['from_X'] if its set to true. 69 | if self.config['from_X']: 70 | if self.config['X_cols'] != 'all': 71 | # Subset X[spl] 72 | X_df = self.so.X[self.spl][self.config['X_cols']] 73 | else: 74 | # Take all columns 75 | X_df = self.so.X[self.spl] 76 | else: 77 | # Get index 78 | X_df = pd.DataFrame(index=self.so.X[self.spl].index) 79 | 80 | return (obs_df, X_df) 81 | 82 | def check_config(self) -> None: 83 | """ 84 | Checks whether config is well defined. 85 | """ 86 | 87 | # At least one of the config options must be set to true. Raise error otw. 88 | if not (self.config['from_obs'] or self.config['from_X']): 89 | raise NameError('At least one should be true (config["from_obs"] or config["from_X"])') 90 | 91 | # Check self.config['from_obs'] if its set to true. 92 | if self.config['from_obs']: 93 | # Raise error if list is empty 94 | if len(self.config['obs_cols']) == 0: 95 | raise NameError('self.config["obs_cols"] is empty. Please provide list of columns to be included.') 96 | 97 | # Raise error is not all column names given are in so.obs[self.spl].columns 98 | if not np.all(np.isin(self.config['obs_cols'], self.so.obs[self.spl].columns)): 99 | raise NameError('Not all elements provided in list config["obs_cols"] are in so.obs[spl].columns') 100 | 101 | # Check self.config['from_X'] if its set to true. 102 | if self.config['from_X']: 103 | if self.config['X_cols'] != 'all': 104 | # Raise error if list is empty 105 | if len(self.config['X_cols']) == 0: 106 | raise NameError('self.config["X_cols"] is empty. Please provide list of columns to be included.') 107 | 108 | if not np.all(np.isin(self.config['X_cols'], self.so.X[self.spl].columns)): 109 | raise NameError('Not all elements provided in list config["X_cols"] are in so.X[spl].columns') 110 | -------------------------------------------------------------------------------- /athena/dataset/__init__.py: -------------------------------------------------------------------------------- 1 | from .datasets import imc, mibi, imc_sample, imc_quickstart -------------------------------------------------------------------------------- /athena/dataset/_utils.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from abc import ABC 3 | from dataclasses import dataclass, field 4 | import os 5 | from pathlib import Path 6 | from urllib import request 7 | 8 | from spatialOmics import SpatialOmics 9 | from typing import Union 10 | 11 | PathLike = Union[str, Path] 12 | 13 | @dataclass 14 | class DataSet(ABC): 15 | 16 | name: str 17 | url: str 18 | 19 | doc_header: str = field(default=None, repr=False) 20 | path: PathLike = field(default=None, repr=False) 21 | 22 | def __post_init__(self) -> None: 23 | if self.path is None: 24 | object.__setattr__(self, "path", os.path.expanduser(f"~/.cache/athena/{self.name}")) 25 | 26 | @property 27 | def _extension(self) -> str: 28 | return '.h5py' 29 | 30 | def __call__(self, path: PathLike = None, force_download: bool = False): 31 | return self.load(path, force_download) 32 | 33 | def load(self, fpath: PathLike = None, force_download: bool = False): 34 | """Download dataset form url""" 35 | fpath = str(self.path if fpath is None else fpath) 36 | 37 | if not fpath.endswith(self._extension): 38 | fpath += self._extension 39 | 40 | if not os.path.isfile(fpath) or force_download: 41 | # download file 42 | dirname = Path(fpath).parent 43 | if not dirname.is_dir(): 44 | dirname.mkdir(parents=True, exist_ok=True) 45 | 46 | self._download_progress(Path(fpath), self.url) 47 | 48 | return SpatialOmics.from_h5py(fpath) 49 | 50 | def _download_progress(self, fpath: Path, url): 51 | from tqdm import tqdm 52 | from urllib.request import urlopen, Request 53 | 54 | blocksize = 1024 * 8 55 | blocknum = 0 56 | 57 | try: 58 | with urlopen(Request(url, headers={"User-agent":"ATHENA-user"})) as rsp: 59 | total = rsp.info().get("content-length", None) 60 | with tqdm( 61 | unit="B", 62 | unit_scale=True, 63 | miniters=1, 64 | unit_divisor=1024, 65 | total=total if total is None else int(total) 66 | ) as t, fpath.open('wb') as f: 67 | block = rsp.read(blocksize) 68 | while block: 69 | f.write(block) 70 | blocknum += 1 71 | t.update(len(block)) 72 | block = rsp.read(blocksize) 73 | except (KeyboardInterrupt, Exception): 74 | # Make sure file doesn’t exist half-downloaded 75 | if fpath.is_file(): 76 | fpath.unlink() 77 | raise 78 | 79 | 80 | def _download(self, fpath: Path, url) -> None: 81 | try: 82 | path, rsp = request.urlretrieve(url, fpath) 83 | except (KeyboardInterrupt, Exception): 84 | # Make sure file doesn’t exist half-downloaded 85 | if path.is_file(): 86 | path.unlink() 87 | raise 88 | 89 | -------------------------------------------------------------------------------- /athena/dataset/datasets.py: -------------------------------------------------------------------------------- 1 | from ._utils import DataSet 2 | 3 | 4 | def imc(force_download=False): 5 | """Pre-processed zurich cohort IMC dataset from *Jackson, H.W., Fischer, J.R., Zanotelli, V.R.T. et al. The single-cell pathology landscape of breast cancer.* `Nature `_ 6 | """ 7 | print('warning: to get the latest version of this dataset use `so = sh.dataset.imc(force_download=True)`') 8 | so = DataSet( 9 | name='imc', 10 | url='https://figshare.com/ndownloader/files/34877643', 11 | doc_header='Pre-processed subset IMC dataset from `Jackson et al ' 12 | ) 13 | return so(force_download=force_download) 14 | 15 | def imc_sample(force_download=False): 16 | """Pre-processed samples from the cohort IMC dataset from *Jackson, H.W., Fischer, J.R., Zanotelli, V.R.T. et al. The single-cell pathology landscape of breast cancer.* `Nature `_ 17 | """ 18 | print('warning: to get the latest version of this dataset use `so = sh.dataset.imc(force_download=True)`') 19 | so = DataSet( 20 | name='imc_sample', 21 | url='https://figshare.com/ndownloader/files/36150798', 22 | doc_header='Pre-processed subset IMC dataset from `Jackson et al ' 23 | ) 24 | return so(force_download=force_download) 25 | 26 | def imc_quickstart(force_download=False): 27 | """Pre-processed samples from the cohort IMC dataset from *Jackson, H.W., Fischer, J.R., Zanotelli, V.R.T. et al. The single-cell pathology landscape of breast cancer.* `Nature `_ 28 | """ 29 | print('warning: to get the latest version of this dataset use `so = sh.dataset.imc(force_download=True)`') 30 | so = DataSet( 31 | name='imc_quickstart', 32 | url='https://figshare.com/ndownloader/files/36418257', 33 | doc_header='Pre-processed subset IMC dataset from `Jackson et al ' 34 | ) 35 | return so(force_download=force_download) 36 | 37 | # def imc_basel(): 38 | # """Pre-processed subset IMC dataset from *Jackson, H.W., Fischer, J.R., Zanotelli, V.R.T. et al. The single-cell pathology landscape of breast cancer.* `Nature `_ 39 | # """ 40 | # so = DataSet( 41 | # name='imc', 42 | # url='https://figshare.com/ndownloader/files/31750769', 43 | # doc_header='Pre-processed subset IMC dataset from `Jackson et al ' 44 | # ) 45 | # return so() 46 | 47 | 48 | def mibi(): 49 | """ 50 | Processed data from *A Structured Tumor-Immune Microenvironment in Triple Negative Breast Cancer Revealed by Multiplexed Ion Beam Imaging.* `Cell `_. 51 | Normalised expression values of segmented cells and cell masks from `here `_ and tiff stacks from `here `_ 52 | 53 | """ 54 | so = DataSet( 55 | name='mibi', 56 | url='https://figshare.com/ndownloader/files/34148859', 57 | doc_header='Processed data from https://www.angelolab.com/mibi-data and https://mibi-share.ionpath.com/tracker/imageset' 58 | ) 59 | return so() 60 | 61 | 62 | # mibi_pop = DataSet( 63 | # name='mibi', 64 | # url=None, 65 | # doc_header='Processed and populated data (graphs, metrics) from https://www.angelolab.com/mibi-data and https://mibi-share.ionpath.com/tracker/imageset' 66 | # ) 67 | -------------------------------------------------------------------------------- /athena/graph_builder/__init__.py: -------------------------------------------------------------------------------- 1 | from .graphBuilder import build_graph -------------------------------------------------------------------------------- /athena/graph_builder/base_graph_builder.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import networkx as nx 3 | import numpy as np 4 | import pandas as pd 5 | from abc import ABC 6 | from skimage.measure import regionprops_table 7 | 8 | 9 | class BaseGraphBuilder(ABC): 10 | 11 | def __init__(self, config: dict): 12 | """Base-Graph Builder constructor 13 | 14 | Args: 15 | config: Dictionary containing a dict called `builder_params` that provides function call arguments to the build_topology function 16 | key_added: The key associated with the graph in the so object 17 | """ 18 | 19 | self.config = config 20 | self.graph = nx.Graph() 21 | 22 | @abc.abstractmethod 23 | def __call__(self, so, spl): 24 | """Builds graph topology. Implemented in subclasses. 25 | 26 | Args: 27 | **kwargs: 28 | 29 | Returns: 30 | 31 | """ 32 | raise NotImplementedError('Implemented in subclasses.') 33 | 34 | def extract_location(self, mask): 35 | '''Compute centroid from image of labels (mask) and return them as a pandas-compatible table. 36 | ''' 37 | # The table is a dictionary mapping column names to value arrays. 38 | ndata = regionprops_table(mask, properties=['label', 'centroid']) 39 | 40 | # The cell_id is the number that identifies a cell in the mask and is set to be the index of ndata 41 | ndata = pd.DataFrame.from_dict(ndata) 42 | ndata.columns = ['cell_id', 'y', 'x'] # NOTE: axis 0 is y and axis 1 is x 43 | ndata.set_index('cell_id', inplace=True) 44 | ndata.sort_index(axis=0, ascending=True, inplace=True) 45 | 46 | return ndata 47 | 48 | def look_for_miss_specification_error(self, so, spl, filter_col, include_labels): 49 | ''' Looks for a miss specification error in the config 50 | ''' 51 | 52 | # Raise error if either `filter_col` or `include_labels` is specified but not the other. 53 | if (filter_col is None) ^ (include_labels is None): 54 | raise NameError('failed to specify either `filter_col` or `include_labels`') 55 | 56 | # Raise error if `filter_col` is not found in so.obs[spl].columns 57 | if filter_col not in so.obs[spl].columns: 58 | raise NameError(f'{filter_col} is not in so.obs[spl].columns') 59 | 60 | # Raise error if `include_labels` is an empty list 61 | if include_labels == []: 62 | raise NameError('include_labels variable is empty. You need to give a non-empty list') 63 | 64 | # Raise error if not even one of the `include_labels` have a match in `so.obs[spl][filter_col]` 65 | if not np.any(np.isin(include_labels, so.obs[spl][filter_col].values)): 66 | raise NameError( 67 | f''' 68 | None of the specified labels are present in column "{filter_col}". 69 | Labels: {include_labels}. 70 | In {filter_col}: {so.obs[spl][filter_col].unique()} 71 | ''' 72 | ) 73 | -------------------------------------------------------------------------------- /athena/graph_builder/contact_graph_builder.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from skimage.morphology import binary_dilation 3 | from .base_graph_builder import BaseGraphBuilder 4 | from ..utils.default_configs import EDGE_WEIGHT 5 | from ..utils.default_configs import DILATION_KERNELS 6 | from tqdm import tqdm 7 | 8 | 9 | def dilation(args) -> list: 10 | """Compute dilation of a given object in a segmentation mask 11 | 12 | Args: 13 | args: masks, obj and dilation kernel 14 | 15 | Returns: 16 | 17 | """ 18 | mask, obj, kernel = args 19 | dilated_img = binary_dilation(mask == obj, kernel) 20 | cells = np.unique(mask[dilated_img]) 21 | cells = cells[cells != obj] # remove object itself 22 | cells = cells[cells != 0] # remove background 23 | return [(obj, cell, {EDGE_WEIGHT: 1}) for cell in cells] 24 | 25 | 26 | class ContactGraphBuilder(BaseGraphBuilder): 27 | """Contact-Graph class. 28 | 29 | Build contact graph based on pixel expansion of cell masks. 30 | """ 31 | 32 | def __init__(self, config: dict): 33 | """Base-Graph Builder constructor 34 | 35 | Args: 36 | config: Dictionary containing a dict called `builder_params` that provides function call arguments to the build_topology function 37 | 38 | Default config: 39 | config = {'builder_params': 40 | {'dilation_kernel': 'disk', 41 | 'radius': 4, 42 | 'include_self':False}, 43 | 'concept_params': 44 | {'filter_col':None, 45 | 'include_labels':None}, 46 | 'coordinate_keys': ['x', 'y'], 47 | 'mask_key': 'cellmasks'} 48 | """ 49 | self.builder_type = "contact" 50 | super().__init__(config) 51 | 52 | def __call__(self, so, spl): 53 | """Build topology using pixel expansion of segmentation masks provided by topo_data['mask']. Masks that overlap after expansion are connected in the graph. 54 | 55 | Args: 56 | so: spatial omic object 57 | spl: sting identifying the sample in the spatial omics object 58 | 59 | Returns: 60 | Graph and key to graph in the spatial omics object 61 | 62 | """ 63 | 64 | # Unpack parameters for building 65 | if self.config["build_concept_graph"]: 66 | filter_col = self.config["concept_params"]["filter_col"] 67 | include_labels = self.config["concept_params"]["include_labels"] 68 | self.look_for_miss_specification_error(so, spl, filter_col, include_labels) 69 | 70 | mask_key = self.config["mask_key"] 71 | params = self.config["builder_params"] 72 | 73 | # Raise error if mask is not provided 74 | if mask_key is None: 75 | raise ValueError( 76 | "Contact-graph requires segmentation masks. To compute a contact graph please specify `the mask_key` to use in so.masks[spl]" 77 | ) 78 | 79 | # Get masks 80 | mask = so.get_mask(spl, mask_key) 81 | 82 | # If a cell subset is well-specified then simplify the mask 83 | if self.config["build_concept_graph"]: 84 | # Get cell_ids of the cells that are in `include_labels` 85 | cell_ids = so.obs[spl].query(f"{filter_col} in @include_labels").index.values 86 | # Simplify masks filling it with 0s for cells that are not in `include_labels` 87 | mask = np.where(np.isin(mask, cell_ids), mask, 0) 88 | 89 | # If dilation_kernel instantiate kernel object, else raise error 90 | if params["dilation_kernel"] in DILATION_KERNELS: 91 | kernel = DILATION_KERNELS[params["dilation_kernel"]](params["radius"]) 92 | else: 93 | raise ValueError( 94 | f'Specified dilate kernel not available. Please use one of {{{", ".join(DILATION_KERNELS)}}}.' 95 | ) 96 | 97 | # Context: Each pixel that belongs to cell i, was value i in the mask. 98 | # get object ids, 0 is background. 99 | objs = np.unique(mask) 100 | objs = objs[objs != 0] 101 | 102 | # Add nodes to graph 103 | self.graph.add_nodes_from(objs) 104 | 105 | # compute neighbors (object = the mask of a single cell) 106 | edges = [] 107 | for obj in tqdm(objs): 108 | # This creates the augmented object mask in a bool array from 109 | dilated_img = binary_dilation(mask == obj, kernel) 110 | 111 | cells = np.unique(mask[dilated_img]) # This identifies the intersecting objects 112 | cells = cells[cells != obj] # remove object itself 113 | cells = cells[cells != 0] # remove background 114 | 115 | # Appends a list of the edges found at this iteration 116 | edges.extend([(obj, cell, {EDGE_WEIGHT: 1}) for cell in cells]) 117 | 118 | # Adds edges to instance variable graph object 119 | self.graph.add_edges_from(edges) 120 | 121 | # Include self edges if desired 122 | if ( 123 | "include_self" in self.config["builder_params"] 124 | and self.config["builder_params"]["include_self"] 125 | and self.builder_type == "contact" 126 | ): 127 | edge_list = [(i, i) for i in self.graph.nodes] 128 | self.graph.add_edges_from(edge_list) 129 | 130 | return self.graph 131 | -------------------------------------------------------------------------------- /athena/graph_builder/graphBuilder.py: -------------------------------------------------------------------------------- 1 | from ..utils.default_configs import get_default_config 2 | from .mappings import GRAPH_BUILDERS 3 | from ..attributer.node_features import add_node_features 4 | from spatialOmics import SpatialOmics 5 | 6 | 7 | def build_graph(so: SpatialOmics, 8 | spl: str, 9 | builder_type: str = None, 10 | key_added: str = None, 11 | config: dict = None, 12 | inplace: bool = True): 13 | """ 14 | Build graph representation for a sample. A graph is constructed based on the provided segmentation masks 15 | for the sample. For the knn and radius graph representation the centroid of each mask is used. For the contact 16 | graph representation the dilation_ operation on the segmentation masks is performed. The segmentation masks that overlap after 17 | dilation are considered to be in physical contact and connected in the contact graph. 18 | 19 | Args: 20 | so: SpatialOmics object 21 | spl (str): sample name in so.spl.index 22 | builder_type (str): graph type to construct {knn, radius, contact} 23 | config (dict): dict containing a dict 'builder_params' that specifies the graph construction parameters. 24 | Also includes other parameters. See other parameters in config section below. 25 | inplace (bool): whether to return a new SpatialOmics instance 26 | 27 | Other parameters in config: 28 | mask_key (str): key in so.masks[spl] to use as segmentation masks from which the observation 29 | coordinates are extracted, if None, coordinate_keys from obs attribute are used 30 | key_added (str): key added in so.G[spl][key_add] to store the graph. 31 | If not specified it defaults to builder_type. 32 | If the graph is being built on a subset of the nodes 33 | (e.g filter_col and include_labels are not None) then the key is 34 | f'{builder_type} > {filter_col} > {include_labels}' 35 | coordinate_keys (list): column names of the x and y coordinates of a observation 36 | filter_col (str): string of the column in so.obs[spl][filter_col] which has the 37 | labels on which you want to subset the cells. 38 | include_labels (list): list of strings which identify the labels in so.obs[spl][filter_col] 39 | that should be included in the graph. If no list is provided the graph is built 40 | using all the cells/cell labels. 41 | build_and_attribute (bool): bool indicating whether to call the attributer functionality. 42 | 43 | Returns: 44 | None or SpatialOmics if inplace = False 45 | 46 | .. _dilation: https://scikit-image.org/docs/stable/api/skimage.morphology.html#skimage.morphology.binary_dilation 47 | """ 48 | # Check that both config and builder_type are not none 49 | if config is None and builder_type is None: 50 | raise ValueError('Either config or builder_type must be specified. Both are None') 51 | elif config is not None and builder_type is not None: 52 | raise ValueError('Either config or builder_type must be specified, but not both.') 53 | 54 | # If builder_type = None then get builder_type from config 55 | if builder_type is None: 56 | builder_type = config["builder_type"] 57 | 58 | # Raise error is the builder_type is invalid 59 | if builder_type not in GRAPH_BUILDERS: 60 | raise ValueError(f'invalid type {builder_type}. Available types are {GRAPH_BUILDERS.keys()}') 61 | 62 | # Get default building parameters if non are specified 63 | if config is None: 64 | config = get_default_config(builder_type=builder_type) 65 | print("Warning: config not specified, defaulting to predefined configuration.") 66 | 67 | # Instantiate graph builder object 68 | builder = GRAPH_BUILDERS[builder_type](config) 69 | 70 | # Build graph and get key 71 | g = builder(so, spl) 72 | 73 | # If no graph key is provided then use builder_type 74 | if key_added is None: 75 | key_added = builder_type 76 | 77 | # Copies `so` if inplace == True. 78 | so = so if inplace else so.copy() 79 | 80 | # If there already is a graph from spl then add or update the so object with graph at key_added 81 | if spl in so.G: 82 | so.G[spl].update({key_added: g}) 83 | else: 84 | # otherwise initialize new dictionary object at key `spl` 85 | so.G[spl] = {key_added: g} 86 | 87 | # If in config build_and_attribute == True then attribute graph. 88 | if config['build_and_attribute']: 89 | add_node_features(so=so, spl=spl, graph_key=key_added, features_type=config['attrs_type'], config=config) 90 | -------------------------------------------------------------------------------- /athena/graph_builder/knn_graph_builder.py: -------------------------------------------------------------------------------- 1 | from sklearn.neighbors import kneighbors_graph 2 | import networkx as nx 3 | import pandas as pd 4 | import numpy as np 5 | from ..utils.tools.graph import df2node_attr 6 | from .base_graph_builder import BaseGraphBuilder 7 | from ..utils.default_configs import EDGE_WEIGHT 8 | 9 | 10 | class KNNGraphBuilder(BaseGraphBuilder): 11 | '''KNN (K-Nearest Neighbors) class for graph building. 12 | ''' 13 | 14 | def __init__(self, config: dict): 15 | """KNN-Graph Builder constructor 16 | 17 | Args: 18 | config: Dictionary containing `builder_params`. Refer to [1] for possible parameters 19 | 20 | Notes: 21 | [1] https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.kneighbors_graph.html 22 | 23 | """ 24 | self.builder_type = 'knn' 25 | super().__init__(config) 26 | 27 | def __call__(self, so, spl): 28 | '''Build topology using a kNN algorithm based on the distance between the centroid of the nodes. 29 | 30 | Args: 31 | so: spatial omic object 32 | spl: sting identifying the sample in the spatial omics object 33 | 34 | Returns: 35 | Graph and key to graph in the spatial omics object 36 | 37 | ''' 38 | 39 | # Unpack parameters for building 40 | if self.config['build_concept_graph']: 41 | filter_col = self.config['concept_params']['filter_col'] 42 | include_labels = self.config['concept_params']['include_labels'] 43 | self.look_for_miss_specification_error(so, spl, filter_col, include_labels) 44 | 45 | mask_key = self.config['mask_key'] 46 | coordinate_keys = self.config['coordinate_keys'] 47 | 48 | # If no masks are provided build graph with centroids. 49 | if mask_key is None: 50 | # If labels is specified, get rid of coordinates that are in the out-set. Else get all coordinates. 51 | if self.config['build_concept_graph']: 52 | # Get coordinates 53 | ndata = so.obs[spl].query(f'{filter_col} in @include_labels')[[coordinate_keys[0], coordinate_keys[1]]] 54 | else: 55 | ndata = so.obs[spl][[coordinate_keys[0], coordinate_keys[1]]] 56 | 57 | # Else build graph from masks 58 | else: 59 | # Get masks 60 | mask = so.get_mask(spl, mask_key) 61 | 62 | # If a cell subset is well are specified then simplify the mask 63 | if self.config['build_concept_graph']: 64 | # Get cell_ids of the cells that are in `include_labels` 65 | cell_ids = so.obs[spl].query(f'{filter_col} in @include_labels').index.values 66 | # Simplify masks filling it with 0s for cells that are not in `include_labels` 67 | mask = np.where(np.isin(mask, cell_ids), mask, 0) 68 | 69 | # Extract location: 70 | ndata = self.extract_location(mask) 71 | 72 | # compute adjacency matrix 73 | ndata.dropna(inplace=True) 74 | adj = kneighbors_graph(ndata.to_numpy(), **self.config['builder_params']) 75 | df = pd.DataFrame(adj.A, index=ndata.index, columns=ndata.index) 76 | self.graph = nx.from_pandas_adjacency(df) # this does not add the nodes in the same sequence as the index, column 77 | 78 | # Puts node attribute (usually just coordinates) into dictionary 79 | # and then as node attributes in the graph. Set edge weight to 1. 80 | attrs = df2node_attr(ndata) 81 | nx.set_node_attributes(self.graph, attrs) 82 | nx.set_edge_attributes(self.graph, 1, EDGE_WEIGHT) 83 | 84 | return self.graph 85 | -------------------------------------------------------------------------------- /athena/graph_builder/mappings.py: -------------------------------------------------------------------------------- 1 | from .contact_graph_builder import ContactGraphBuilder 2 | from .knn_graph_builder import KNNGraphBuilder 3 | from .radius_graph_builder import RadiusGraphBuilder 4 | 5 | GRAPH_BUILDERS = { 6 | 'knn': KNNGraphBuilder, 7 | 'contact': ContactGraphBuilder, 8 | 'radius': RadiusGraphBuilder 9 | } 10 | -------------------------------------------------------------------------------- /athena/graph_builder/radius_graph_builder.py: -------------------------------------------------------------------------------- 1 | from sklearn.neighbors import radius_neighbors_graph 2 | import networkx as nx 3 | import pandas as pd 4 | import numpy as np 5 | from ..utils.tools.graph import df2node_attr 6 | from .base_graph_builder import BaseGraphBuilder 7 | from ..utils.default_configs import EDGE_WEIGHT 8 | 9 | 10 | class RadiusGraphBuilder(BaseGraphBuilder): 11 | ''' 12 | Radius graph class for graph building. 13 | ''' 14 | 15 | def __init__(self, config: dict): 16 | """Build topology using a radius algorithm based on the distance between the centroid of the nodes. 17 | 18 | Args: 19 | config: dict specifying graph builder params 20 | key_added: string to use as key for the graph in the spatial omics object 21 | """ 22 | self.builder_type = 'radius' 23 | super().__init__(config) 24 | 25 | def __call__(self, so, spl): 26 | ''' 27 | Build topology using a radius algorithm based on the distance between the centroid of the nodes. 28 | 29 | Args: 30 | so: spatial omic object 31 | spl: sting identifying the sample in the spatial omics object 32 | 33 | Returns: 34 | Graph and key to graph in the spatial omics object 35 | 36 | ''' 37 | 38 | # Unpack parameters for building 39 | if self.config['build_concept_graph']: 40 | filter_col = self.config['concept_params']['filter_col'] 41 | include_labels = self.config['concept_params']['include_labels'] 42 | self.look_for_miss_specification_error(so, spl, filter_col, include_labels) 43 | 44 | mask_key = self.config['mask_key'] 45 | coordinate_keys = self.config['coordinate_keys'] 46 | 47 | # If a cell subset is well are specified then simplify the mask 48 | if mask_key is None: 49 | # If the subset is well specified (no error in `look_for_miss_specification_error`), 50 | # get rid of coordinates that are in the out-set. 51 | if self.config['build_concept_graph']: 52 | # Get coordinates 53 | ndata = so.obs[spl].query(f'{filter_col} in @include_labels')[[coordinate_keys[0], coordinate_keys[1]]] 54 | # Else get all coordinates. 55 | else: 56 | ndata = so.obs[spl][[coordinate_keys[0], coordinate_keys[1]]] 57 | 58 | # Else build graph from masks 59 | else: 60 | # Get masks 61 | mask = so.get_mask(spl, mask_key) 62 | 63 | # If include_labels are specified then simplify the mask 64 | if self.config['build_concept_graph']: 65 | # Get cell_ids of the cells that are in `include_labels` 66 | cell_ids = so.obs[spl].query(f'{filter_col} in @include_labels').index.values 67 | # Simplify masks filling it with 0s for cells that are not in `include_labels` 68 | mask = np.where(np.isin(mask, cell_ids), mask, 0) 69 | 70 | # Extract location: 71 | ndata = self.extract_location(mask) 72 | 73 | # compute adjacency matrix, put into df with cell_id for index and columns and ad to graph 74 | ndata.dropna(inplace=True) 75 | adj = radius_neighbors_graph(ndata.to_numpy(), **self.config['builder_params']) 76 | df = pd.DataFrame(adj.A, index=ndata.index, columns=ndata.index) 77 | self.graph = nx.from_pandas_adjacency(df) # this does not add the nodes in the same sequence as the index, column 78 | 79 | # Puts node attribute (usually just coordinates) into dictionary 80 | # and then as node attributes in the graph. Set edge weight to 1. 81 | attrs = df2node_attr(ndata) 82 | nx.set_node_attributes(self.graph, attrs) 83 | nx.set_edge_attributes(self.graph, 1, EDGE_WEIGHT) 84 | 85 | return self.graph 86 | -------------------------------------------------------------------------------- /athena/metrics/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Thu Nov 19 17:07:15 2020 5 | 6 | @author: art 7 | """ 8 | 9 | from .heterogeneity.metrics import richness, \ 10 | abundance, \ 11 | shannon, \ 12 | simpson,\ 13 | renyi_entropy,\ 14 | hill_number, \ 15 | quadratic_entropy 16 | # shannon_evenness,\ 17 | # gini_simpson, \ 18 | # simpson_evenness,\ 19 | # diversity_profile 20 | 21 | from .graph import modularity 22 | -------------------------------------------------------------------------------- /athena/metrics/constants.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/athena/metrics/constants.py -------------------------------------------------------------------------------- /athena/metrics/graph/__init__.py: -------------------------------------------------------------------------------- 1 | from .graph import modularity -------------------------------------------------------------------------------- /athena/metrics/graph/graph.py: -------------------------------------------------------------------------------- 1 | import networkx.algorithms.community as nx_comm 2 | from spatialOmics import SpatialOmics 3 | 4 | 5 | def modularity(so: SpatialOmics, spl: str, community_id: str, 6 | graph_key: str = 'knn', resolution: float = 1, 7 | key_added=None, inplace=True) -> None: 8 | """Computes the modularity of the sample graph. 9 | 10 | Args: 11 | so: SpatialOmics instance 12 | spl: str Spl for which to compute the metric 13 | community_id: str column that specifies the community membership of each observation. Must be categorical. 14 | graph_key: str Specifies the graph representation to use in so.G[spl] 15 | resolution: float 16 | key_added: str Key added to spl 17 | inplace: bool Whether to add the metric to the current SpatialOmics instance or to return a new one. 18 | 19 | Returns: 20 | SpatialOmics if inplace=True, else nothing. 21 | 22 | References: networkx_ 23 | 24 | .. _networkx: https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.community.quality.modularity.html 25 | """ 26 | 27 | so = so if inplace else so.deepcopy() 28 | 29 | if key_added is None: 30 | key_added = f'modularity_{community_id}_res{resolution}' 31 | 32 | if community_id not in so.obs[spl]: 33 | raise ValueError(f'{community_id} not in so.obs[spl]') 34 | elif so.obs[spl][community_id].dtype != 'category': 35 | raise TypeError(f'expected dtype `category` but got {so.obs[spl].meta_id.dtype}') 36 | 37 | # get communities 38 | communities = [] 39 | for _, obs in so.obs[spl].groupby(community_id): 40 | communities.append(set(obs.index)) 41 | 42 | res = nx_comm.modularity(so.G[spl][graph_key], communities) 43 | 44 | so.spl.loc[spl, key_added] = res 45 | 46 | if not inplace: 47 | return so 48 | -------------------------------------------------------------------------------- /athena/metrics/heterogeneity/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/athena/metrics/heterogeneity/__init__.py -------------------------------------------------------------------------------- /athena/metrics/heterogeneity/base_metrics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..utils import _process_input, _validate_counts_vector 3 | # from spatialHeterogeneity.metrics.utils import _process_input, _validate_counts_vector 4 | # from ...utils.general import make_iterable 5 | 6 | # from scipy.spatial.distance import pdist 7 | import pandas as pd 8 | from collections import Counter 9 | from typing import Counter as ct, Iterable, Union 10 | 11 | from sklearn.preprocessing import StandardScaler 12 | from scipy.spatial.distance import cdist 13 | 14 | import warnings 15 | 16 | 17 | # ----- ENTROPYC MEASURES -----# 18 | 19 | def _richness(counts: Union[ct, Iterable]) -> int: 20 | """Compute richness. 21 | 22 | Args: 23 | counts: Either counts or observations 24 | 25 | Returns: 26 | Richness of sample. 27 | 28 | """ 29 | 30 | props = _process_input(counts) 31 | 32 | if np.isnan(props).any(): 33 | # this would correspond to richness == 0 34 | return 0 35 | 36 | return len(props) 37 | 38 | 39 | def _shannon(counts: Union[ct, Iterable], base: float = 2) -> float: 40 | """Compute the shannon entropy of the counts. 41 | 42 | Args: 43 | counts: If a Counter object or an interable with intergers is provided it is assumed that those are counts of the different species. 44 | If the iterable contains dtype `float` it is interpreted as probabilities of the different classes 45 | base: Base of logarithm. Defaults to 2 46 | 47 | Returns: 48 | Shannon entropy of observations. 49 | 50 | """ 51 | 52 | props = _process_input(counts) 53 | 54 | nonzero_props = props[props.nonzero()] # by definition log(0) is 0 for entropy 55 | return -(nonzero_props * np.log(nonzero_props)).sum() / np.log(base) 56 | 57 | 58 | def _shannon_evenness(counts: Union[ct, Iterable], 59 | base: float = 2) -> float: 60 | """Compute shannon evenness. This is a normalised form of the shannon index. 61 | 62 | Args: 63 | counts: If a Counter object or an interable with intergers is provided it is assumed that those are counts of the different species. 64 | If the iterable contains dtype `float` it is interpreted as probabilities of the different classes 65 | base: Base of logarithm. Defaults to 2 66 | 67 | Returns: 68 | A numpy array with the evenness. 69 | 70 | Notes: 71 | The value is 1 in case that all species have the same relative abundances. 72 | .. [1] https://anadat-r.davidzeleny.net/doku.php/en:div-ind#fnt__2 73 | 74 | """ 75 | 76 | H = _shannon(counts, base) 77 | H_max = np.log(_richness(counts)) / np.log(base) 78 | if H == 0 and H_max == 0: 79 | return 0 80 | return H / H_max 81 | 82 | 83 | def _simpson(counts: Union[ct, Iterable]) -> float: 84 | """Compute Simpson Index. 85 | 86 | Args: 87 | counts: If a Counter object or an interable with intergers is provided it is assumed that those are counts of the different species. 88 | If the iterable contains dtype `float` it is interpreted as probabilities of the different classes 89 | 90 | Returns: 91 | Simpson index. 92 | 93 | Notes: 94 | Implementation according to [2]. 95 | Simpson's index is also considering both richness and evenness, 96 | but compared to Shannon it is more influenced by evenness than richness. It represents the probability that two randomly selected individuals will be of the same species. Since this probability decreases with increasing species richness, the Simpson index also decreases with richness, which is not too intuitive. For that reason, more meaningful is to use Gini-Simpson index, which is simply 1-Simpson index, and which with increasing richness increases [2] 97 | 98 | Simpson's index is heavily weighted towards the most abundant species in the sample, while being less sensitive to species richness.[1] 99 | 100 | References: 101 | .. [1]: http://www.pisces-conservation.com/sdrhelp/index.html 102 | .. [2]: https://anadat-r.davidzeleny.net/doku.php/en:div-ind#fnt__2 103 | """ 104 | 105 | props = _process_input(counts) 106 | return (props ** 2).sum() 107 | 108 | 109 | def _simpson_evenness(counts: Union[ct, Iterable]) -> float: 110 | """Compute Simpson Evenness. 111 | 112 | Args: 113 | counts: If a Counter object or an interable with intergers is provided it is assumed that those are counts of the different species. 114 | If the iterable contains dtype `float` it is interpreted as probabilities of the different classes 115 | 116 | Returns: 117 | Simpson evenness 118 | 119 | Notes: 120 | Also called equitability. Is calculated from Simpson’s effective number of species divided by observed number of species. Effective number of species (ENS) is the number of equally abundant species which would need to be in a community so as it has the same Simpson’s index as the one really calculated 121 | 122 | 123 | """ 124 | 125 | D = _simpson(counts) 126 | S = _richness(counts) 127 | return (1 / D) / S 128 | 129 | 130 | def _gini_simpson(counts: Union[ct, Iterable]) -> float: 131 | """Computes the Gini Simpson index 132 | 133 | Args: 134 | counts: If a Counter object or an interable with intergers is provided it is assumed that those are counts of the different species. 135 | If the iterable contains dtype `float` it is interpreted as probabilities of the different classes 136 | 137 | Returns: 138 | Gini-Simpson index. 139 | """ 140 | 141 | return 1 - _simpson(counts) 142 | 143 | 144 | def _hill_number(counts: Union[ct, Iterable], q: float) -> float: 145 | """Compute the hill number. 146 | 147 | Notes: 148 | [1]: https://anadat-r.davidzeleny.net/doku.php/en:div-ind#fnt__2 149 | 150 | Args: 151 | counts: If a Counter object or an interable with intergers is provided it is assumed that those are counts of the different species. 152 | If the iterable contains dtype `float` it is interpreted as probabilities of the different classes 153 | q: Order of hill number. 154 | 155 | Returns: 156 | Hill number. 157 | """ 158 | 159 | if q < 0: 160 | warnings.warn('q is generally limited to non-negative values') 161 | 162 | if q == 1: 163 | # exponential of shannon entropy with base e 164 | return np.exp(_shannon(counts, np.exp(1))) 165 | 166 | props = _process_input(counts) 167 | 168 | if q == np.inf: 169 | return 1 / np.max(props) 170 | 171 | nonzero_props = props[props.nonzero()] 172 | return np.power(np.sum(nonzero_props ** q), (1 / (1 - q))) 173 | 174 | 175 | def _renyi(counts: Union[ct, Iterable], 176 | q: float, 177 | base: float = 2) -> float: 178 | """Computes the Renyi-Entropy of order q. 179 | 180 | Args: 181 | counts: If a Counter object or an interable with intergers is provided it is assumed that those are counts of the different species. 182 | If the iterable contains dtype `float` it is interpreted as probabilities of the different classes 183 | q: Order of Renyi-Entropy. 184 | base: Base of logarithm. 185 | 186 | Returns: 187 | Renyi-Entropy of order q. 188 | """ 189 | 190 | if q < 0: 191 | warnings.warn('q is generally limited to non-negative values') 192 | 193 | props = _process_input(counts) 194 | 195 | if q == 1: 196 | # special case q == 1: shannon entropy 197 | return _shannon(counts, base) 198 | 199 | if q == np.inf: 200 | return - np.log(np.max(props)) / np.log(base) 201 | 202 | return 1 / (1 - q) * np.log(np.sum(props ** q)) / np.log(base) 203 | 204 | 205 | def _quadratic_entropy(counts: ct, features: pd.DataFrame, metric: str = 'minkowski', metric_kwargs: dict = {'p':2}, scale: bool = True) -> float: 206 | """ 207 | 208 | Args: 209 | counts: Counter object with counts of observed species / instances 210 | features: pandas.DataFrame with index representing instances / species and columns features of these instances 211 | metric: metric to compute distance between instances in the features space 212 | metric_kwargs: key word arguments to the metric 213 | scale: whether to scale the features to zero mean and unit variance 214 | 215 | Returns: 216 | float 217 | """ 218 | 219 | # check that all instances in counts are in features 220 | if not np.all([i in features.index for i in counts]): 221 | raise KeyError(f'not all instances in counts are in the features index') 222 | 223 | # order elements in features according to counts and drop excess features 224 | features = features.loc[counts.keys()] 225 | 226 | # scale features 227 | # NOTE: If features are not scaled over-proportional weight might be given to some features 228 | if scale: 229 | features = StandardScaler().fit_transform(features) 230 | 231 | props = _process_input(counts) 232 | D = cdist(features, features, metric, **metric_kwargs) 233 | 234 | return props@D@props 235 | 236 | # ----- MULTIDIMENSIONAL MEASURES -----# 237 | 238 | def _abundance(counts: Union[ct, Iterable], mode='proportion', event_space=None) -> pd.Series: 239 | """Compute abundance of different species in counts 240 | 241 | Args: 242 | counts: If a Counter object or an interable with intergers is provided it is assumed that those are counts of the different species. 243 | mode: Either `proportion` or `counts`. If `proportion` we compute the frequency of the species, else the absolute counts. 244 | event_space: If provided, computes the abundance of all species in the event space. Useful to compute results including all species even of those not present in the current counts. 245 | 246 | Returns: 247 | Abundance of species, either as frequency (proportion) or absolute count. 248 | """ 249 | VALID_MODES = ['proportion', 'count'] 250 | # if a counter is provided we will add zero counts for all the events in the event space 251 | # if an list-like object is provided we compute the counts first 252 | if event_space is not None: 253 | c0 = Counter({i: 0 for i in event_space}) 254 | counts.update(c0) 255 | 256 | if not isinstance(counts, Counter): 257 | counts = Counter({key:val for key, val in zip(range(len(counts)), counts)}) 258 | # counts = Counter(counts) 259 | 260 | index = counts.keys() 261 | if mode == 'proportion': 262 | vals = _process_input(counts) 263 | dtype = float 264 | elif mode == 'count': 265 | vals = np.asarray(list(counts.values())) 266 | _validate_counts_vector(vals) 267 | dtype = int 268 | else: 269 | raise ValueError(f'{mode} is not a valid mode, available are {VALID_MODES}') 270 | 271 | return pd.Series(vals, index=index, dtype=dtype) 272 | 273 | # from string import ascii_lowercase 274 | # n = 10 275 | # counts = Counter({key:1 for key in ascii_lowercase[:n]}) 276 | # feat = pd.DataFrame(np.diag(np.repeat(0.5,n)), index=[i for i in ascii_lowercase[:n]]) 277 | # # res = _quadratic_entropy(Counter({key:1 for key in 'asd'}), pd.DataFrame(np.ones((3,5)), index=[i for i in 'asd'])) 278 | # res = _quadratic_entropy(counts, feat, metric_kwargs={'p':1}) -------------------------------------------------------------------------------- /athena/metrics/heterogeneity/metrics.py: -------------------------------------------------------------------------------- 1 | # %% 2 | from .base_metrics import _shannon, _richness, _simpson, _shannon_evenness, _hill_number, \ 3 | _simpson_evenness, _gini_simpson, _renyi, _abundance, _quadratic_entropy 4 | 5 | from ...utils.general import is_categorical, make_iterable, is_numeric 6 | 7 | import numpy as np 8 | import pandas as pd 9 | from collections import Counter 10 | 11 | from sklearn.preprocessing import StandardScaler 12 | 13 | 14 | # %% 15 | 16 | def richness(so, spl: str, attr: str, *, local=True, key_added=None, graph_key='knn', inplace=True) -> None: 17 | """Computes the richness on the observation or the sample level 18 | 19 | Args: 20 | so: SpatialOmics instance 21 | spl: Spl for which to compute the metric 22 | attr: Categorical feature in SpatialOmics.obs to use for the grouping 23 | local: Whether to compute the metric on the observation or the sample level 24 | key_added: Key added to either obs or spl depending on the choice of `local` 25 | graph_key: Specifies the graph representation to use in so.G[spl] if `local=True`. 26 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 27 | 28 | Examples: 29 | 30 | .. code-block:: python 31 | 32 | so = sh.dataset.imc() 33 | spl = so.spl.index[0] 34 | 35 | sh.metrics.richness(so, spl, 'meta_id', local=False) 36 | sh.metrics.richness(so, spl, 'meta_id', local=True) 37 | """ 38 | 39 | if key_added is None: 40 | key_added = 'richness' 41 | key_added = f'{key_added}_{attr}' 42 | if local: 43 | key_added += f'_{graph_key}' 44 | 45 | metric = _richness 46 | kwargs_metric = {} 47 | 48 | return _compute_metric(so=so, spl=spl, attr=attr, key_added=key_added, graph_key=graph_key, metric=metric, 49 | kwargs_metric=kwargs_metric, 50 | local=local, inplace=inplace) 51 | 52 | 53 | def shannon(so, spl: str, attr: str, *, local=True, key_added=None, graph_key='knn', base=2, inplace=True) -> None: 54 | """Computes the Shannon Index on the observation or the sample level 55 | 56 | Args: 57 | so: SpatialOmics instance 58 | spl: Spl for which to compute the metric 59 | attr: Categorical feature in SpatialOmics.obs to use for the grouping 60 | local: Whether to compute the metric on the observation or the sample level 61 | key_added: Key added to either obs or spl depending on the choice of `local` 62 | graph_key: Specifies the graph representation to use in so.G[spl] if `local=True`. 63 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 64 | 65 | Examples: 66 | 67 | .. code-block:: python 68 | 69 | so = sh.dataset.imc() 70 | spl = so.spl.index[0] 71 | 72 | sh.metrics.shannon(so, spl, 'meta_id', local=False) 73 | sh.metrics.shannon(so, spl, 'meta_id', local=True) 74 | 75 | """ 76 | if key_added is None: 77 | key_added = 'shannon' 78 | key_added = f'{key_added}_{attr}' 79 | if local: 80 | key_added += f'_{graph_key}' 81 | 82 | metric = _shannon 83 | kwargs_metric = {'base': base} 84 | 85 | return _compute_metric(so=so, spl=spl, attr=attr, key_added=key_added, graph_key=graph_key, metric=metric, 86 | kwargs_metric=kwargs_metric, 87 | local=local, inplace=inplace) 88 | 89 | 90 | def simpson(so, spl: str, attr: str, *, local=True, key_added=None, graph_key='knn', inplace=True) -> None: 91 | """Computes the Simpson Index on the observation or the sample level 92 | 93 | Args: 94 | so: SpatialOmics instance 95 | spl: Spl for which to compute the metric 96 | attr: Categorical feature in SpatialOmics.obs to use for the grouping 97 | local: Whether to compute the metric on the observation or the sample level 98 | key_added: Key added to either obs or spl depending on the choice of `local` 99 | graph_key: Specifies the graph representation to use in so.G[spl] if `local=True`. 100 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 101 | 102 | Examples: 103 | 104 | .. code-block:: python 105 | 106 | so = sh.dataset.imc() 107 | spl = so.spl.index[0] 108 | 109 | sh.metrics.simpson(so, spl, 'meta_id', local=False) 110 | sh.metrics.simpson(so, spl, 'meta_id', local=True) 111 | 112 | """ 113 | if key_added is None: 114 | key_added = 'simpson' 115 | key_added = f'{key_added}_{attr}' 116 | if local: 117 | key_added += f'_{graph_key}' 118 | 119 | metric = _simpson 120 | kwargs_metric = {} 121 | 122 | return _compute_metric(so=so, spl=spl, attr=attr, key_added=key_added, graph_key=graph_key, metric=metric, 123 | kwargs_metric=kwargs_metric, 124 | local=local, inplace=inplace) 125 | 126 | 127 | def hill_number(so, spl: str, attr: str, q: float, *, local=True, key_added=None, graph_key='knn', inplace=True): 128 | """Computes the Hill Numbers on the observation or the sample level 129 | 130 | Args: 131 | so: SpatialOmics instance 132 | spl: Spl for which to compute the metric 133 | attr: Categorical feature in SpatialOmics.obs to use for the grouping 134 | q: The hill coefficient as defined here_. 135 | local: Whether to compute the metric on the observation or the sample level 136 | key_added: Key added to either obs or spl depending on the choice of `local` 137 | graph_key: Specifies the graph representation to use in so.G[spl] if `local=True`. 138 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 139 | 140 | Examples: 141 | 142 | .. code-block:: python 143 | 144 | so = sh.dataset.imc() 145 | spl = so.spl.index[0] 146 | 147 | sh.metrics.hill_number(so, spl, 'meta_id', q=2, local=False) 148 | sh.metrics.hill_number(so, spl, 'meta_id', q=2, local=True) 149 | 150 | """ 151 | if key_added is None: 152 | key_added = 'hill_number' 153 | key_added = f'{key_added}_{attr}_q{q}' 154 | if local: 155 | key_added += f'_{graph_key}' 156 | 157 | metric = _hill_number 158 | kwargs_metric = {'q': q} 159 | 160 | return _compute_metric(so=so, spl=spl, attr=attr, key_added=key_added, graph_key=graph_key, metric=metric, 161 | kwargs_metric=kwargs_metric, 162 | local=local, inplace=inplace) 163 | 164 | 165 | def renyi_entropy(so, spl: str, attr: str, q: float, *, local=True, key_added=None, graph_key='knn', base=2, 166 | inplace=True): 167 | """Computes the Renyi-Entropy. 168 | 169 | Args: 170 | so: SpatialOmics instance 171 | spl: Spl for which to compute the metric 172 | attr: Categorical feature in SpatialOmics.obs to use for the grouping 173 | q: The renyi coefficient as defined here_ 174 | local: Whether to compute the metric on the observation or the sample level 175 | key_added: Key added to either obs or spl depending on the choice of `local` 176 | graph_key: Specifies the graph representation to use in so.G[spl] if `local=True`. 177 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 178 | 179 | Examples: 180 | 181 | .. code-block:: python 182 | 183 | so = sh.dataset.imc() 184 | spl = so.spl.index[0] 185 | 186 | sh.metrics.renyi_entropy(so, spl, 'meta_id', q=2, local=False) 187 | sh.metrics.renyi_entropy(so, spl, 'meta_id', q=2, local=True) 188 | 189 | .. _here: https://ai4scr.github.io/ATHENA/source/methodology.html 190 | """ 191 | if key_added is None: 192 | key_added = 'renyi' 193 | key_added = f'{key_added}_{attr}_q{q}' 194 | if local: 195 | key_added += f'_{graph_key}' 196 | 197 | metric = _renyi 198 | kwargs_metric = {'q': q, 199 | 'base': base} 200 | 201 | return _compute_metric(so=so, spl=spl, attr=attr, key_added=key_added, graph_key=graph_key, metric=metric, 202 | kwargs_metric=kwargs_metric, 203 | local=local, inplace=inplace) 204 | 205 | 206 | def quadratic_entropy(so, spl: str, attr: str, *, metric='minkowski', metric_kwargs={}, scale: bool = True, 207 | local=True, key_added=None, graph_key='knn', inplace=True): 208 | """Computes the quadratic entropy, taking relative abundance and similarity between observations into account. 209 | 210 | Args: 211 | so: SpatialOmics instance 212 | spl: Spl for which to compute the metric 213 | attr: Categorical feature in SpatialOmics.obs to use for the grouping 214 | metric: metric used to compute distance of observations in the features space so.X[spl] 215 | metric_kwargs: key word arguments for metric 216 | scale: whether to scale features of observations to unit variance and 0 mean 217 | local: whether to compute the metric on the observation or the sample level 218 | key_added: Key added to either obs or spl depending on the choice of `local` 219 | graph_key: Specifies the graph representation to use in so.G[spl] if `local=True`. 220 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 221 | 222 | Notes: 223 | The implementation computes an average feature vector for each group in attr based on all observations in the 224 | sample. Thus, if staining biases across samples exists this will directly distort this metric. 225 | 226 | Examples: 227 | 228 | .. code-block:: python 229 | 230 | so = sh.dataset.imc() 231 | spl = so.spl.index[0] 232 | 233 | sh.metrics.quadratic_entropy(so, spl, 'meta_id', local=False) 234 | sh.metrics.quadratic_entropy(so, spl, 'meta_id', local=True) 235 | 236 | """ 237 | if key_added is None: 238 | key_added = 'quadratic' 239 | key_added = f'{key_added}_{attr}' 240 | if local: 241 | key_added += f'_{graph_key}' 242 | 243 | # collect feature vectors of all observations and add attr grouping 244 | features: pd.DataFrame = so.X[spl] 245 | features = features.merge(so.obs[spl][attr], right_index=True, left_index=True) 246 | assert len(features) == len(so.X[spl]), 'inner merge resulted in dropped index ids' 247 | 248 | # compute average feature vector for each attr group and standardise 249 | features = features.groupby(attr).mean() 250 | if scale: 251 | tmp = StandardScaler().fit_transform(features) 252 | features = pd.DataFrame(tmp, index=features.index, columns=features.columns) 253 | 254 | base_metric = _quadratic_entropy 255 | kwargs_metric = {'features': features, 256 | 'metric': metric, 257 | 'metric_kwargs': metric_kwargs, 258 | 'scale': False} # we scaled already 259 | 260 | return _compute_metric(so=so, spl=spl, attr=attr, key_added=key_added, graph_key=graph_key, metric=base_metric, 261 | kwargs_metric=kwargs_metric, 262 | local=local, inplace=inplace) 263 | 264 | 265 | def abundance(so, spl: str, attr: str, *, mode='proportion', key_added: str = None, graph_key='knn', 266 | local=False, inplace: bool = True): 267 | """Computes the abundance of species on the observation or the sample level. 268 | 269 | Args: 270 | so: SpatialOmics instance 271 | spl: Spl for which to compute the metric 272 | attr: Categorical feature in SpatialOmics.obs to use for the grouping 273 | local: Whether to compute the metric on the observation or the sample level 274 | key_added: Key added to either uns[spl] or obs depending on the choice of `local` 275 | graph_key: Specifies the graph representation to use in so.G[spl] if `local=True`. 276 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 277 | 278 | Examples: 279 | 280 | .. code-block:: python 281 | 282 | so = sh.dataset.imc() 283 | spl = so.spl.index[0] 284 | 285 | sh.metrics.abundance(so, spl, 'meta_id', local=False) 286 | sh.metrics.abundance(so, spl, 'meta_id', local=True) 287 | 288 | """ 289 | 290 | if key_added is None: 291 | key_added = f'{mode}' 292 | if local: 293 | key_added += f'_{graph_key}' 294 | 295 | event_space = so.obs[spl][attr] 296 | if is_categorical(event_space): 297 | event_space = event_space.dtypes.categories 298 | else: 299 | raise TypeError(f'{attr} is not categorical') 300 | 301 | metric = _abundance 302 | kwargs_metric = {'event_space': event_space, 303 | 'mode': mode} 304 | 305 | return _compute_metric(so=so, spl=spl, attr=attr, key_added=key_added, metric=metric, graph_key=graph_key, 306 | kwargs_metric=kwargs_metric, local=local, inplace=inplace) 307 | 308 | 309 | def _compute_metric(so, spl: str, attr, key_added, graph_key, metric, kwargs_metric, local, inplace=True): 310 | """Computes the given metric for each observation or the sample 311 | """ 312 | 313 | # generate a copy if necessary 314 | so = so if inplace else so.copy() 315 | 316 | # extract relevant categorisation 317 | data = so.obs[spl][attr] 318 | if not is_categorical(data): 319 | raise TypeError('`attr` needs to be categorical') 320 | 321 | if local: 322 | # get graph 323 | g = so.G[spl][graph_key] 324 | 325 | # compute metric for each observation 326 | res = [] 327 | observation_ids = so.obs[spl].index 328 | for observation_id in observation_ids: 329 | n = list(g.neighbors(observation_id)) 330 | if len(n) == 0: 331 | res.append(0) 332 | continue 333 | counts = Counter(data.loc[n].values) 334 | res.append(metric(counts, **kwargs_metric)) 335 | 336 | if np.ndim(res[0]) > 0: 337 | res = pd.DataFrame(res, index=observation_ids) 338 | if spl not in so.obsm: 339 | so.obsm[spl] = {} 340 | so.obsm[spl][key_added] = res 341 | else: 342 | res = pd.DataFrame({key_added: res}, index=observation_ids) 343 | if key_added in so.obs[spl]: # drop previous computation of metric 344 | so.obs[spl].drop(key_added, axis=1, inplace=True) 345 | so.obs[spl] = pd.concat((so.obs[spl], res), axis=1) 346 | else: 347 | res = metric(Counter(data), **kwargs_metric) 348 | 349 | if np.ndim(res) > 0: 350 | if spl not in so.uns: 351 | so.uns[spl] = {} 352 | so.uns[spl][key_added] = res 353 | else: 354 | so.spl.loc[spl, key_added] = res 355 | 356 | if not inplace: 357 | return so 358 | -------------------------------------------------------------------------------- /athena/metrics/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from collections import Counter 3 | from pandas.api.types import is_list_like 4 | import warnings 5 | 6 | def _process_input(counts) -> np.ndarray: 7 | # check that input is not empty 8 | if len(counts) == 0: 9 | warnings.warn('counts is an empty object.') 10 | return np.array([np.nan]) 11 | 12 | # convert to numpy array 13 | if isinstance(counts, Counter): 14 | c = np.fromiter(counts.values(), dtype=int) 15 | elif is_list_like(counts): 16 | c = np.asarray(counts) 17 | else: 18 | raise TypeError(f'counts is neither a counter nor list-like') 19 | 20 | # validate input 21 | if c.dtype == int: 22 | c = _counts_to_props(c) 23 | elif c.dtype == float: 24 | _validate_props_vector(c) 25 | else: 26 | raise TypeError( 27 | f'counts has an invalid type {type(c.dtype)}. Must be `int` for counts and `float` for probabilities.') 28 | 29 | return c 30 | 31 | 32 | def _validate_counts_vector(counts): 33 | '''Validate counts vector. 34 | 35 | Parameters 36 | ---------- 37 | counts: numpy 1d array of type int 38 | ''' 39 | 40 | if not isinstance(counts, np.ndarray): 41 | raise TypeError(f'counts vector is not an numpy.ndarray but {type(counts)}') 42 | if np.isnan(counts).any(): 43 | raise ValueError("counts vector contains nan values.") 44 | if counts.dtype != int: 45 | raise TypeError(f'counts should have type `int`, found invalid type {counts.dtype}') 46 | if counts.ndim != 1: 47 | raise ValueError("Only 1-D vectors are supported.") 48 | if (counts < 0).any(): 49 | raise ValueError("Counts vector cannot contain negative values.") 50 | 51 | 52 | def _counts_to_props(counts): 53 | """Validates and converts counts to probabilities""" 54 | _validate_counts_vector(counts) 55 | props = counts / counts.sum() 56 | _validate_props_vector(props) 57 | return props 58 | 59 | 60 | def _validate_props_vector(props): 61 | '''Validate props vector. 62 | 63 | Parameters 64 | ---------- 65 | props: numpy 1d array of type float 66 | ''' 67 | 68 | if not isinstance(props, np.ndarray): 69 | raise TypeError(f'Probabilities vector is not an numpy.ndarray but {type(props)}') 70 | if props.dtype != float: 71 | raise TypeError(f'Probabilities should have type `float`, found invalid type {props.dtype}') 72 | if props.ndim != 1: 73 | raise ValueError("Only 1-D vectors are supported.") 74 | if (props < 0).any(): 75 | raise ValueError("Probabilities vector cannot contain negative values.") 76 | if (props > 1).any(): 77 | raise ValueError("Probabilities vector cannot contain values larger than 1.") 78 | if np.isnan(props).any(): 79 | raise ValueError("Probabilities vector contains nan values.") 80 | if not np.isclose(props.sum(), 1): 81 | raise ValueError(f'Probabilities do not sum to 1, props.sum = {props.sum()}') -------------------------------------------------------------------------------- /athena/neighborhood/__init__.py: -------------------------------------------------------------------------------- 1 | from .estimators import interactions, infiltration, ripleysK 2 | 3 | 4 | -------------------------------------------------------------------------------- /athena/neighborhood/estimators.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import pandas as pd 3 | import numpy as np 4 | 5 | from .base_estimators import Interactions, _infiltration, RipleysK 6 | 7 | from .utils import get_node_interactions 8 | from ..utils.general import is_categorical 9 | from tqdm import tqdm 10 | # %% 11 | def interactions(so, spl: str, attr: str, mode: str ='classic', prediction_type: str ='observation', *, n_permutations: int =100, 12 | random_seed=None, alpha: float =.01, try_load: bool =True, key_added: str =None, graph_key: str ='knn', 13 | inplace: bool =True) -> None: 14 | """Compute interaction strength between species. This is done by counting the number of interactions (edges in the graph) 15 | between pair-wise observation types as encdoded by `attr`. See notes for more information or the 16 | `methodology `_ section in the docs. 17 | 18 | Args: 19 | so: SpatialOmics instance 20 | spl: Spl for which to compute the metric 21 | attr: Categorical feature in SpatialOmics.obs to use for the grouping 22 | mode: One of {classic, histoCAT, proportion}, see notes 23 | n_permutations: Number of permutations to compute p-values and the interactions strength score (mode diff) 24 | random_seed: Random seed for permutations 25 | alpha: Threshold for significance 26 | prediction_type: One of {observation, pvalue, diff}, see Notes 27 | try_load: load pre-computed permutation results if available 28 | key_added: Key added to SpatialOmics.uns[spl][metric][key_added] 29 | graph_key: Specifies the graph representation to use in so.G[spl] if `local=True`. 30 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 31 | 32 | Notes: 33 | `classic` and `histoCAT` are python implementations of the corresponding methods pubished by the Bodenmiller lab at UZH. 34 | The `proportion` method is similar to the `classic` method but normalises the score by the number of edges and is thus bound [0,1]. 35 | 36 | Returns: 37 | 38 | """ 39 | so = so if inplace else so.copy() 40 | 41 | # NOTE: uns_path = f'{spl}/interactions/' 42 | if key_added is None: 43 | key_added = f'{attr}_{mode}_{prediction_type}_{graph_key}' 44 | 45 | if random_seed is None: 46 | random_seed = so.random_seed 47 | 48 | estimator = Interactions(so=so, spl=spl, attr=attr, mode=mode, n_permutations=n_permutations, 49 | random_seed=random_seed, alpha=alpha, graph_key=graph_key) 50 | 51 | estimator.fit(prediction_type=prediction_type, try_load=try_load) 52 | res = estimator.predict() 53 | 54 | # add result to uns attribute 55 | add2uns(so, res, spl, 'interactions', key_added) 56 | 57 | if not inplace: 58 | return so 59 | 60 | 61 | def infiltration(so, spl: str, attr: str, *, interaction1=('tumor', 'immune'), interaction2=('immune', 'immune'), 62 | add_key='infiltration', inplace=True, graph_key='knn', local=False) -> None: 63 | """Compute infiltration score. Generalises the infiltration score presented in 64 | `A Structured Tumor-Immune Microenvironment in Triple Negative Breast Cancer Revealed by Multiplexed Ion Beam Imaging `_ 65 | The score comptes a ratio between the number of interactions observed between the observation types specified in `interactions1` 66 | and `interaction2` as :math:`\\frac{\\texttt{number of interactions 1}}{\\texttt{number of interactions 2}}`. This ratio can 67 | be undefined. See notes for more information. 68 | 69 | Args: 70 | so: SpatialOmics instance 71 | spl: Spl for which to compute the metric 72 | attr: Categorical feature in SpatialOmics.obs to use for the grouping 73 | interaction1: labels in `attr` of enumerator interaction 74 | interaction2: labels in `attr` of denominator interaction 75 | key_added: Key added to SpatialOmics.uns[spl][metric][key_added] 76 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 77 | graph_key: Specifies the graph representation to use in so.G[spl] if `local=True`. 78 | 79 | Returns: 80 | 81 | Notes: 82 | The default arguments are replicating the `immune infiltration score `_. However, you 83 | can compute any kind of "infiltration" between observation types. The `attr` argument specifies the column 84 | in the `obs` dataframe which encodes different observation types. `interaction{1,2}` argument defines between 85 | which types the score should be computed. 86 | 87 | .. _infiltrationScore: https://pubmed.ncbi.nlm.nih.gov/30193111/ 88 | 89 | """ 90 | so = so if inplace else so.copy() 91 | 92 | data = so.obs[spl][attr] 93 | if isinstance(data, pd.DataFrame): 94 | raise ValueError(f'multidimensional attr ({data.shape}) is not supported.') 95 | 96 | if not is_categorical(data): 97 | raise TypeError('`attr` needs to be categorical') 98 | 99 | if not np.in1d(np.array(interaction1 + interaction2), data.unique()).all(): 100 | mask = np.in1d(np.array(interaction1 + interaction2), data.unique()) 101 | missing = np.array(interaction1 + interaction2)[~mask] 102 | raise ValueError(f'specified interaction categories are not all in `attr`. Missing {missing}') 103 | 104 | G = so.G[spl][graph_key] 105 | if local: 106 | cont = [] 107 | for node in tqdm(G.nodes): 108 | neigh = G[node] 109 | g = G.subgraph(neigh) 110 | nint = get_node_interactions(g, data) 111 | res = _infiltration(node_interactions=nint, interaction1=interaction1, interaction2=interaction2) 112 | cont.append(res) 113 | 114 | res = pd.DataFrame(cont, index=G.nodes, columns=[add_key]) 115 | if add_key in so.obs[spl]: 116 | so.obs[spl] = so.obs[spl].drop(columns=[add_key]) 117 | 118 | so.obs[spl] = pd.concat((so.obs[spl], res), axis=1) 119 | 120 | else: 121 | nint = get_node_interactions(G, data) 122 | 123 | res = _infiltration(node_interactions=nint, interaction1=interaction1, interaction2=interaction2) 124 | 125 | so.spl.loc[spl, add_key] = res 126 | 127 | if not inplace: 128 | return so 129 | 130 | 131 | def ripleysK(so, spl: str, attr: str, id, *, mode='K', radii=None, correction='ripley', inplace=True, key_added=None): 132 | """Compute Ripley's K as implemented by `[1]`_. 133 | 134 | Args: 135 | so: SpatialOmics instance 136 | spl: Spl for which to compute the metric 137 | attr: Categorical feature in SpatialOmics.obs to use for the grouping 138 | id: The category in the categorical feature `attr`, for which Ripley's K should be computed 139 | mode: {K, csr-deviation}. If `K`, Ripley's K is estimated, with `csr-deviation` the deviation from a poission process is computed. 140 | radii: List of radiis for which Ripley's K is computed 141 | correction: Correction method to use to correct for boarder effects, see [1]. 142 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 143 | key_added: Key added to SpatialOmics.uns[spl][metric][key_added] 144 | 145 | Returns: 146 | Ripley's K estimates 147 | 148 | References: 149 | .. _[1]: https://docs.astropy.org/en/stable/stats/ripley.html 150 | 151 | """ 152 | so = so if inplace else so.copy() 153 | 154 | # NOTE: uns_path = f'{spl}/clustering/' 155 | if key_added is None: 156 | key_added = f'{id}_{attr}_{mode}_{correction}' 157 | 158 | estimator = RipleysK(so=so, spl=spl, id=id, attr=attr) 159 | res = estimator.predict(radii=radii, correction=correction, mode=mode) 160 | 161 | # add result to uns attribute 162 | add2uns(so, res, spl, 'ripleysK', key_added) 163 | 164 | if not inplace: 165 | return so 166 | 167 | 168 | def add2uns(so, res, spl: str, parent_key, key_added): 169 | if spl in so.uns: 170 | if parent_key in so.uns[spl]: 171 | so.uns[spl][parent_key][key_added] = res 172 | else: 173 | so.uns[spl].update({parent_key: {key_added: res}}) 174 | else: 175 | so.uns.update({spl: {parent_key: {key_added: res}}}) 176 | -------------------------------------------------------------------------------- /athena/neighborhood/utils.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import networkx as nx 3 | import numpy as np 4 | import pandas as pd 5 | 6 | # %% 7 | def get_edge_interactions(g: nx.Graph, data: pd.Series): 8 | # IMPORTANT: Be aware of the symmetry issues when only looking at edges. This is, two cells A,B that share an edge 9 | # are only represented once, either as A B or B A 10 | 11 | # probably the fasted way to solve this is would be by multidimensional indexing into a numpy array. 12 | # data[edges.T] 13 | # however the api is based on cell_ids that are not sequential, thus we have to index into pd.Series which is fast 14 | # or we convert the non-sequential cell_ids into sequential ones 15 | 16 | # NOTE: The data pd.Series is categorical with globally all categories 17 | 18 | edges = np.array(g.edges) 19 | edge_interactions = pd.DataFrame({'source': edges[:, 0], 'source_label': data.loc[edges[:, 0]].values, 20 | 'target': edges[:, 1], 'target_label': data.loc[edges[:, 1]].values}) 21 | return edge_interactions 22 | 23 | 24 | def get_node_interactions(g: nx.Graph, data: pd.Series = None): 25 | # NOTE: The data pd.Series is categorical with globally all categories 26 | 27 | source, neighs = [], [] 28 | for i in g.nodes: 29 | if len(g[i]) > 0: # some nodes might have no neighbors 30 | source.append(i) 31 | neighs.append(list(g[i])) 32 | 33 | node_interactions = pd.DataFrame({'source': source, 'target': neighs}).explode('target') 34 | if data is not None: 35 | node_interactions['source_label'] = data.loc[node_interactions.source].values 36 | node_interactions['target_label'] = data.loc[node_interactions.target].values 37 | 38 | return node_interactions 39 | 40 | 41 | def get_interaction_score(interactions, relative_freq=False, observed=False): 42 | # NOTE: this is not necessarily len(source_labels) == len(g) since only source nodes with neighbors are included 43 | source_label = interactions[['source', 'source_label']].drop_duplicates().set_index('source') 44 | source_label = source_label.squeeze() 45 | 46 | source2target_label = interactions.groupby(['source', 'target_label'], observed=observed, 47 | as_index=False).size().rename({'size': 'counts'}, axis=1) 48 | source2target_label.loc[:, 'source_label'] = source_label[source2target_label.source].values 49 | 50 | if relative_freq: 51 | tots = source2target_label.groupby('source')['counts'].agg('sum') 52 | source2target_label['n_neigh'] = tots.loc[source2target_label.source].values 53 | source2target_label['relative_freq'] = source2target_label['counts'] / source2target_label['n_neigh'] 54 | label2label = source2target_label\ 55 | .groupby(['source_label', 'target_label'], observed=observed)['relative_freq'] \ 56 | .agg('mean') \ 57 | .rename('score') \ 58 | .fillna(0) \ 59 | .reset_index() 60 | else: 61 | label2label = source2target_label \ 62 | .groupby(['source_label', 'target_label'], observed=observed)['counts'] \ 63 | .agg('mean') \ 64 | .rename('score') \ 65 | .fillna(0) \ 66 | .reset_index() 67 | 68 | return label2label 69 | 70 | 71 | # why is this so slow??? 72 | def permute_labels_deprecate(data, rng: np.random.Generator): 73 | attr_copy = data.copy() 74 | attr_copy[:] = rng.permutation(attr_copy) 75 | return attr_copy 76 | 77 | 78 | def permute_labels(data, rng: np.random.Generator): 79 | return pd.Series(rng.permutation(data), index=data.index) 80 | -------------------------------------------------------------------------------- /athena/plotting/__init__.py: -------------------------------------------------------------------------------- 1 | from .visualization import spatial, napari_viewer, interactions, ripleysK, infiltration 2 | -------------------------------------------------------------------------------- /athena/plotting/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from matplotlib import pyplot as plt 3 | from matplotlib.cm import ScalarMappable 4 | from matplotlib.figure import SubplotParams 5 | import os 6 | 7 | dpi = 300 8 | ax_pad = 10 9 | label_fontdict = {'size': 7} 10 | title_fontdict = {'size': 10} 11 | cbar_inset = [1.02, 0, .0125, .96] 12 | cbar_titel_fontdict = {'size': 7} 13 | cbar_labels_fontdict = {'size': 7} 14 | root_fig = plt.rcParams['savefig.directory'] 15 | 16 | 17 | def make_cbar(ax, title, norm, cmap, cmap_labels, im=None, prefix_labels=True): 18 | """Generate a colorbar for the given axes. 19 | 20 | Parameters 21 | ---------- 22 | ax: Axes 23 | axes for which to plot colorbar 24 | title: str 25 | title of colorbar 26 | norm: 27 | Normalisation instance 28 | cmap: Colormap 29 | Colormap 30 | cmap_labels: dict 31 | colorbar labels 32 | 33 | Returns 34 | ------- 35 | 36 | """ 37 | # NOTE: The Linercolormap ticks can only be set up to the number of colors. Thus if we do not have linear, sequential 38 | # values [0,1,2,3] in the cmap_labels dict this will fail. Solution could be to remap. 39 | 40 | inset = ax.inset_axes(cbar_inset) 41 | fig = ax.get_figure() 42 | if im is None: 43 | cb = fig.colorbar(ScalarMappable(norm=norm, cmap=cmap), cax=inset) 44 | else: 45 | cb = fig.colorbar(im, cax=inset) 46 | 47 | cb.ax.set_title(title, loc='left', fontdict=cbar_titel_fontdict) 48 | if cmap_labels: 49 | if prefix_labels: 50 | labs = [f'{key}, {val}' for key, val in cmap_labels.items()] 51 | else: 52 | labs = list(cmap_labels.values()) 53 | cb.set_ticks(list(cmap_labels.keys())) 54 | cb.ax.set_yticklabels(labs, fontdict=cbar_labels_fontdict) 55 | else: 56 | cb.ax.tick_params(axis='y', labelsize=cbar_labels_fontdict['size']) 57 | 58 | # TODO 59 | def linear_mapping(cmap_labels): 60 | pass 61 | 62 | 63 | def savefig(fig, save): 64 | # if only filename is given, add root_fig, convenient to save plots less verbose. 65 | if save == os.path.basename(save): 66 | save = os.path.join(plt.rcParams['savefig.directory'], save) 67 | fig.savefig(save) 68 | print(f'Figure saved at: {save}') 69 | 70 | 71 | def colormap(cmap): 72 | """Visualise a colormap. 73 | 74 | Parameters 75 | ---------- 76 | cmap: Colormap 77 | 78 | Returns 79 | ------- 80 | 81 | """ 82 | n = len(cmap.colors) 83 | a = np.zeros((1, n, 4)) 84 | 85 | for j in range(n): 86 | a[0, j,] = cmap(j) 87 | 88 | fig, ax = plt.subplots(1, 1) 89 | ax.imshow(a) 90 | ax.tick_params(labelleft=False, left=False) 91 | fig.show() 92 | 93 | # ax.set_aspect(.25) 94 | 95 | 96 | def get_layout(nx, ny=None, max_col=5, max_row=None): 97 | if ny is None: 98 | ny = 0 99 | 100 | ncol = np.min((np.ceil(np.sqrt(nx)), max_col)).astype(int) 101 | nrow = int(np.ceil((nx + ny) / ncol)) 102 | return (nrow, ncol) -------------------------------------------------------------------------------- /athena/preprocessing/__init__.py: -------------------------------------------------------------------------------- 1 | from .preprocess import _extract_image_properties, extract_centroids, arcsinh 2 | 3 | -------------------------------------------------------------------------------- /athena/preprocessing/_preprocessors.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import numpy as np 3 | from scipy.spatial.distance import euclidean, minkowski, mahalanobis, cosine 4 | from sklearn.metrics import mean_squared_error, mean_absolute_error 5 | 6 | 7 | # %% 8 | 9 | # censore data 10 | # NOTE: This function is the python implementation (extended to 2D ndarrays) from the R bbRtools 11 | class CensorData: 12 | def __init__(self, quant=.99, symmetric=False, axis=0): 13 | self.quant = quant 14 | self.symmetric = symmetric 15 | self.axis = axis 16 | self.lower_quant = None 17 | self.upper_val = None 18 | self.fitted = False 19 | 20 | def fit(self, X): 21 | 22 | if self.symmetric: 23 | self.lower_quant = (1 - self.quant) / 2 24 | self.upper_quant = self.lower_quant + self.quant 25 | 26 | self.lower_val = np.quantile(X, self.lower_quant, axis=self.axis) 27 | else: 28 | self.upper_quant = self.quant 29 | 30 | q = np.quantile(X, self.upper_quant, axis=self.axis) 31 | self.upper_val = q 32 | self.fitted = True 33 | 34 | def transform(self, X): 35 | x = X.copy() 36 | 37 | if np.ndim(x) > 1: 38 | for i in range(x.shape[1]): 39 | x[x[:, i] > self.upper_val[i], i] = self.upper_val[i] 40 | if self.symmetric: 41 | x[x[:, i] < self.lower_val[i], i] = self.lower_val[i] 42 | else: 43 | x[x > self.upper_val] = self.upper_val 44 | 45 | if self.symmetric: 46 | x[x < self.lower_val] = self.lower_val 47 | 48 | return x 49 | 50 | def fit_transform(self, X): 51 | self.fit(X) 52 | return self.transform(X) 53 | 54 | 55 | class Arcsinh: 56 | """ 57 | .. [1]: https://support.cytobank.org/hc/en-us/articles/206148057-About-the-Arcsinh-transform 58 | """ 59 | 60 | def __init__(self, cofactor=5): 61 | self.cofactor = cofactor 62 | self.fitted = True 63 | 64 | def fit(self, X, y=None): 65 | pass 66 | 67 | def transform(self, X, y=None): 68 | return np.arcsinh(X / self.cofactor) 69 | 70 | def fit_transform(self, X, y=None): 71 | return self.transform(X) 72 | 73 | 74 | class ReduceLocal: 75 | METRICS = dict(euclidean=euclidean, minkowski=minkowski, mahalanobis=mahalanobis, cosine=cosine, 76 | mse=mean_squared_error, mae=mean_absolute_error) 77 | 78 | def __init__(self, ref=None, mode='reduce', reducer=np.mean, metric='mse', disp_fn=np.std, groupby=None, fillna=0, 79 | kwargs=None): 80 | self.fitted = True 81 | self.groupby = groupby 82 | self.fillna = fillna 83 | self.ref = ref 84 | self.mode = mode 85 | self.reducer = reducer 86 | self.disp_fn = disp_fn 87 | 88 | if callable(metric): 89 | self.metric = metric 90 | elif isinstance(metric, str): 91 | self.metric = self.METRICS[metric] 92 | else: 93 | raise ValueError(f'Invalid metric {metric}. Valid {self.METRICS}') 94 | 95 | self.kwargs = kwargs if kwargs is not None else {} 96 | 97 | def fit(self): 98 | pass 99 | 100 | def transform(self, obs): 101 | if self.mode == 'distance': 102 | if self.ref is None: 103 | self.ref = np.zeros_like(obs) 104 | self.kwargs.update({'ref': self.ref}) 105 | # raise ValueError('Please provide a reference to which to compute the distance.') 106 | if self.groupby is None: 107 | res = self.metric(obs, **self.kwargs) 108 | else: 109 | res = obs.groupby(self.groupby).agg(self.metric, **self.kwargs) 110 | elif self.mode == 'dispersion': 111 | if self.groupby is None: 112 | res = self.disp_fn(obs, **self.kwargs) 113 | else: 114 | res = obs.groupby(self.groupby).agg(self.disp_fn, **self.kwargs) 115 | elif self.mode == 'reduce': 116 | if self.groupby is None: 117 | res = self.reducer(obs, **self.kwargs) 118 | else: 119 | res = obs.groupby(self.groupby).agg(self.reducer, **self.kwargs) 120 | else: 121 | raise ValueError(f'Invalid mode {self.mode}. Select either [distance, dispersion, reduce]') 122 | 123 | res = res.fillna(self.fillna) 124 | 125 | return res 126 | 127 | def fit_transform(self, obs): 128 | return self.transform(obs) 129 | -------------------------------------------------------------------------------- /athena/preprocessing/preprocess.py: -------------------------------------------------------------------------------- 1 | # %% 2 | # from ._preprocessors import CensorData, Arcsinh, ReduceLocal 3 | # from ..utils.general import make_iterable, _check_is_fitted, is_fitted 4 | 5 | import pandas as pd 6 | import numpy as np 7 | import warnings 8 | from skimage.measure import regionprops, regionprops_table 9 | 10 | # from sklearn.preprocessing import StandardScaler 11 | 12 | # %% 13 | 14 | def extract_centroids(so, spl, mask_key='cellmasks', inplace=True): 15 | """Extract centroids from segementation masks. 16 | 17 | Args: 18 | so: SpatialOmics instance 19 | spl: sample for which to extract centroids 20 | mask_key: segmentation masks to use 21 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 22 | 23 | Returns: 24 | 25 | """ 26 | so = so if inplace else so.copy() 27 | 28 | mask = so.get_mask(spl, mask_key) 29 | 30 | ndata = regionprops_table(mask, properties=['label', 'centroid']) 31 | ndata = pd.DataFrame.from_dict(ndata) 32 | ndata.columns = ['cell_id', 'y', 'x'] # NOTE: axis 0 is y and axis 1 is x 33 | ndata.set_index('cell_id', inplace=True) 34 | ndata.sort_index(axis=0, ascending=True, inplace=True) 35 | 36 | if spl in so.obs: 37 | if 'x' in so.obs[spl] and 'y' in so.obs[spl]: 38 | so.obs[spl] = so.obs[spl].drop(columns=['x', 'y']) 39 | so.obs[spl] = pd.concat((so.obs[spl], ndata), axis=1) 40 | else: 41 | so.obs[spl] = ndata 42 | 43 | if not inplace: 44 | return so 45 | 46 | def arcsinh(so, spl, cofactor): 47 | """Computes the arcsinh transformation of the expression values according to: 48 | 49 | .. math:: 50 | 51 | X = \\mathtt{arcsinh}(\\frac{X}{\\mathtt{cofactor}}) 52 | 53 | 54 | Args: 55 | so: spatialOmics instance 56 | spl: sample name 57 | cofactor: cofactor used for transformation 58 | 59 | """ 60 | 61 | X = so.X[spl] 62 | np.divide(X, cofactor, out=X) 63 | np.arcsinh(X, out=X) 64 | 65 | 66 | def _extract_image_properties(so, spl, inplace=True): 67 | """Extract image properties from the high-dimensional images. 68 | 69 | Args: 70 | so: SpatialOmics instance 71 | spl: sample for which to extract centroids 72 | inplace: Whether to add the metric to the current SpatialOmics instance or to return a new one. 73 | 74 | """ 75 | so = so if inplace else so.copy() 76 | 77 | img = so.get_image(spl) 78 | data = list(img.shape[1:]) 79 | data.append(data[0]*data[1]) 80 | 81 | if not np.all([i in so.spl.columns for i in ['height', 'width', 'area']]): 82 | so.spl = pd.concat((so.spl, pd.DataFrame(columns = ['height', 'width', 'area'])), axis=1) 83 | 84 | so.spl.loc[spl, ['height', 'width','area']] = data 85 | 86 | if not inplace: 87 | return so -------------------------------------------------------------------------------- /athena/utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /athena/utils/default_configs.py: -------------------------------------------------------------------------------- 1 | from skimage.morphology import square, diamond, disk 2 | import copy as cp 3 | 4 | DILATION_KERNELS = { 5 | 'disk': disk, 6 | 'square': square, 7 | 'diamond': diamond 8 | } 9 | 10 | EDGE_WEIGHT = 'weight' 11 | 12 | CONCEPT_DEFAULT_PARAMS = { 13 | 'concept_params': { 14 | 'filter_col' : None, 15 | 'include_labels' : None 16 | }, 17 | } 18 | 19 | GRAPH_ATTRIBUTER_DEFAULT_PARAMS = { 20 | 'so': { 21 | 'attrs_type': 'so', 22 | 'attrs_params': { 23 | 'from_obs': True, 24 | 'obs_cols': [ 25 | 'meta_id', 26 | 'cell_type_id', 27 | 'phenograph_cluster', 28 | 'y', 29 | 'x' 30 | ], 31 | 'from_X': True, 32 | 'X_cols': 'all' 33 | } 34 | }, 35 | 'deep': { 36 | 'attrs_type': 'deep', 37 | 'attrs_params': {} 38 | }, 39 | 'random' : { 40 | 'attrs_type': 'random', 41 | 'attrs_params': {'n_attrs': 3} 42 | } 43 | } 44 | 45 | OTHER_PARAMETERS = { 46 | 'coordinate_keys': ['x', 'y'], 47 | 'mask_key': None, 48 | 'build_and_attribute': False, 49 | 'build_concept_graph': False 50 | } 51 | 52 | GRAPH_BUILDER_DEFAULT_PARAMS = { 53 | 'knn': { 54 | 'builder_type': 'knn', 55 | 'builder_params': { 56 | 'n_neighbors': 6, 57 | 'mode': 'connectivity', 58 | 'metric': 'minkowski', 59 | 'p': 2, 60 | 'metric_params': None, 61 | 'include_self': True, 62 | 'n_jobs': -1 63 | } 64 | }, 65 | 'contact': { 66 | 'builder_type': 'contact', 67 | 'builder_params': { 68 | 'dilation_kernel': 'disk', 69 | 'radius': 4, 70 | 'include_self': True 71 | } 72 | }, 73 | 'radius': { 74 | 'builder_type': 'radius', 75 | 'builder_params': { 76 | 'radius': 36, 77 | 'mode': 'connectivity', 78 | 'metric': 'minkowski', 79 | 'p': 2, 80 | 'metric_params': None, 81 | 'include_self': True, 82 | 'n_jobs': -1 83 | } 84 | } 85 | } 86 | 87 | 88 | def get_default_config(builder_type: str, 89 | build_concept_graph: bool = False, 90 | build_and_attribute: bool = False, 91 | attrs_type : str = None) -> dict: 92 | """ 93 | Gets a default configuration of parameters for graph building depending on the desired graph construction. 94 | 95 | Args: 96 | builder_type: string indicating the type of graph to build, namely 'knn', 'contact', or 'radius'. 97 | build_concept_graph: indicates whether to build a concept graph (True) or a graph using all the cells (False) 98 | build_and_attribute: whether to assign attributes to the nodes of the graph. 99 | attrs_type: string indicating which type of attributes to assign. 100 | 101 | Returns: 102 | A dictionary with the default configuration. 103 | """ 104 | 105 | config = cp.deepcopy(GRAPH_BUILDER_DEFAULT_PARAMS[builder_type]) 106 | other_params = cp.deepcopy(OTHER_PARAMETERS) 107 | config = {**config, **other_params} 108 | 109 | if builder_type == "contact": 110 | config["mask_key"] = "cellmasks" 111 | config["coordinate_keys"] = None 112 | 113 | if build_concept_graph: 114 | concept_config = cp.deepcopy(CONCEPT_DEFAULT_PARAMS) 115 | config = {**config, **concept_config} 116 | config['build_concept_graph'] = True 117 | 118 | if build_and_attribute: 119 | assert attrs_type is not None, 'If `build_and_attribute = True`, `attrs_type` must be specified.' 120 | attrs_config = cp.deepcopy(GRAPH_ATTRIBUTER_DEFAULT_PARAMS[attrs_type]) 121 | config = {**config, **attrs_config} 122 | config['build_and_attribute'] = True 123 | 124 | return config 125 | -------------------------------------------------------------------------------- /athena/utils/docs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/athena/utils/docs/__init__.py -------------------------------------------------------------------------------- /athena/utils/docs/default_docs.py: -------------------------------------------------------------------------------- 1 | def ddoc(fun): 2 | key_added = 'DEFAULT' 3 | 4 | doc = fun.__doc__ 5 | 6 | # replace so 7 | doc = doc.replace('so:', 'so: SpatialOmics instance') 8 | 9 | # replace spl 10 | doc = doc.replace('spl:', 'spl: sample for which to apply the function') 11 | 12 | # replace key_added 13 | doc = doc.replace('key_added', f'key_added: key added to {key_added}') 14 | 15 | # replace inplace 16 | doc = doc.replace('inplace:', 'inplace: whether to return a new SpatialOmics instance') 17 | 18 | # replace doc 19 | fun.__doc__ = doc 20 | 21 | return fun 22 | 23 | # from docrep import DocstringProcessor 24 | # docstrings = DocstringProcessor() 25 | # 26 | # @docstrings.get_sections 27 | # def default_doc(so, inplace): 28 | # """ 29 | # 30 | # Args: 31 | # so: SpatialOmics 32 | # inplace: Apply function in place 33 | # 34 | # Returns: 35 | # 36 | # """ 37 | # pass -------------------------------------------------------------------------------- /athena/utils/general.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from collections.abc import Iterable 4 | import numpy as np 5 | from pandas.api.types import is_categorical_dtype, is_numeric_dtype 6 | from sklearn.utils.validation import check_is_fitted as sklearn_check_is_fitted 7 | 8 | # import pandas as pd 9 | def is_numeric(*args, **kwargs): 10 | return is_numeric_dtype(*args, **kwargs) 11 | 12 | def is_categorical(*args, **kwargs): 13 | return is_categorical_dtype(*args, **kwargs) 14 | 15 | def make_iterable(obj: Any): 16 | ''' 17 | 18 | Parameters 19 | ---------- 20 | obj : Any 21 | Any object that you want to make iterable 22 | 23 | Returns 24 | ------- 25 | Packed object, possible to iterate overs 26 | ''' 27 | 28 | # if object is iterable and its not a string return object as is 29 | if isinstance(obj, Iterable) and not isinstance(obj, str): 30 | return obj 31 | else: 32 | return (obj,) 33 | 34 | 35 | def is_seq(x, step=1): 36 | """Checks if the elements in a list-like object are increasing by step 37 | 38 | Parameters 39 | ---------- 40 | x: list-like 41 | step 42 | 43 | Returns 44 | ------- 45 | True if elements increase by step, else false and the index at which the condition is violated. 46 | 47 | """ 48 | for i in range(1, len(x)): 49 | if not x[i] == (x[i - 1] + step): 50 | print('Not seq at: ', i) 51 | return False 52 | return True 53 | 54 | 55 | def order(x, transform=None, decreasing=False): 56 | """\ 57 | Returns the indices of the ordered elements in x. 58 | 59 | Parameters 60 | ---------- 61 | x: list_like 62 | A list_like object 63 | transform: function 64 | a function applied to the elements of x to compute a value based on which the array x should be sorted 65 | decreasing: bool 66 | indicating if the sorting should be in decreasing order 67 | 68 | Returns 69 | ------- 70 | The indices of the sorted array 71 | 72 | Examples 73 | ________ 74 | a = [1,3,2] 75 | order(a) 76 | # [0,2,1] 77 | 78 | a = np.linspace(0,np.pi,5) 79 | order(a, transform = lambda x: np.cos(x)) 80 | # [4,3,2,1,0] 81 | 82 | a = np.linspace(0,np.pi,5) 83 | order(a, transform = lambda x: np.cos(x)**2) 84 | # [2,3,1,0,4] 85 | """ 86 | if transform is None: 87 | transform = lambda a: a 88 | 89 | sign = 1 90 | if decreasing: sign = 1 91 | 92 | _x = [(i, transform(x[i])) for i in range(len(x))] 93 | _x = sorted(_x, key=lambda x: sign * x[1]) 94 | return np.array([_x[i][0] for i in range(len(_x))]) 95 | 96 | def _check_is_fitted(estimator): 97 | if not hasattr(estimator, 'fit'): 98 | raise TypeError("%s is not an estimator instance." % (estimator)) 99 | if hasattr(estimator, 'fitted'): 100 | if not is_fitted(estimator): 101 | raise ValueError('this instance is not fitted.') 102 | else: 103 | sklearn_check_is_fitted(estimator) 104 | 105 | def is_fitted(estimator): 106 | if hasattr(estimator, 'fitted'): 107 | return estimator.fitted 108 | else: 109 | try: 110 | sklearn_check_is_fitted(estimator) 111 | return True 112 | except: 113 | return False 114 | 115 | -------------------------------------------------------------------------------- /athena/utils/tools/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /athena/utils/tools/graph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Wed Nov 25 17:43:48 2020 5 | 6 | @author: art 7 | """ 8 | import pandas as pd 9 | from ..general import make_iterable 10 | #%% 11 | 12 | def df2node_attr(df): 13 | """Convert dataframe to dict keyed by index which can be used to set node attributes with networkx""" 14 | # NOTE: df.index has to be the nodes 15 | return df.T.to_dict() 16 | 17 | def node_attrs2df(g, attrs=None): 18 | """Convert networkx graph node attributes to a dataframe index by node""" 19 | df = pd.DataFrame.from_dict(dict(g.nodes.data()), orient='index') 20 | if attrs is not None: 21 | attrs = list(make_iterable(attrs)) 22 | return df[attrs] 23 | else: 24 | return df 25 | -------------------------------------------------------------------------------- /athena/utils/tools/image.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Sat Nov 28 14:50:24 2020 5 | 6 | @author: art 7 | """ 8 | # %% 9 | import numpy as np 10 | from skimage import io 11 | from skimage.measure import regionprops_table 12 | import os 13 | import pandas as pd 14 | 15 | import warnings 16 | 17 | 18 | # %% 19 | 20 | def get_shape_outline(mask): 21 | ''' 22 | 23 | Parameters 24 | ---------- 25 | mask: 2d array 26 | Array in which objects are marked with labels > 0. Background is 0. 27 | 28 | Returns 29 | ------- 30 | The same as mask but with only the border of the masks, i.e. outline. 31 | 32 | ''' 33 | 34 | mc = mask.copy() 35 | for x in range(1, mask.shape[0] - 1): 36 | for y in range(1, mask.shape[1] - 1): 37 | if mask[x, y] != 0: 38 | if mask[x + 1, y] == mask[x, y] and mask[x - 1, y] == mask[x, y] and mask[x, y + 1] == mask[x, y] and \ 39 | mask[x, y - 1] == mask[ 40 | x, y]: 41 | mc[x, y] = 0 42 | return mc 43 | 44 | 45 | def extract_mask_expr_feat(mask_file: str, img_file: str, norm=None, channels=None, reducer=np.mean, mask_bg: int = 0): 46 | """Extracts the pixel values of the mask objects in the mask_file given the pixel intensities in the image for each channel. 47 | 48 | Parameters 49 | ---------- 50 | mask_file: str 51 | path to cell mask file 52 | img_file: str 53 | path to image file, usually this would be the tiff stack 54 | norm: 55 | a instance that normalises the pixel values 56 | channels: list-like 57 | indicates for which channels in the image the pixel values of the mask should be extracted, defaults to all channels 58 | reducer: 59 | function to summarise the pixel values of a object in the mask, default np.mean 60 | mask_bg: 61 | value which indicates background in the mask_file, defaults to 0 62 | 63 | Returns 64 | ------- 65 | dataframe of shape n_objects x n_channels. 66 | """ 67 | 68 | # load image, image mask 69 | img = io.imread(img_file) 70 | img_mask = io.imread(mask_file) 71 | 72 | if img.shape[1:] != img_mask.shape: 73 | raise (f'Image dimensions {img.shape[1:]} and image mask shape {img_mask.shape} do not match') 74 | 75 | if not channels: 76 | channels = list(range(len(img))) 77 | 78 | if not norm: 79 | norm = lambda x: x 80 | 81 | # extract objects fom mask 82 | objs = np.unique(img_mask) 83 | objs = objs[objs != mask_bg] # remove background label 84 | # print(f'...{len(objs)} cells') 85 | 86 | # initialise expression data frame 87 | expr = pd.DataFrame(np.zeros((len(objs), len(channels)))) 88 | expr = expr.set_index(objs) 89 | expr.index.name = 'cell_id' 90 | expr.columns = channels 91 | expr.columns.name = 'channel' 92 | 93 | for obj in objs: 94 | vals = img[:, img_mask == obj] 95 | # TODO: normalise image pixel intensities, currently identity func 96 | vals = norm(vals) # normalise intensities 97 | vals = reducer(vals, axis=1) # compute single cell intensity 98 | expr.loc[obj, :] = vals 99 | 100 | return expr 101 | 102 | 103 | def extract_mask_morph_feat(mask_file, properties=['label', 'area', 'extent', 'eccentricity'], as_dict=True): 104 | """Extracts morphological features form cell mask files. 105 | 106 | Parameters 107 | ---------- 108 | mask_file: str 109 | path to cell mask file 110 | properties: list of properties, see [1] 111 | as_dict 112 | 113 | Returns 114 | ------- 115 | 116 | References 117 | __________ 118 | .. [1]: https://scikit-image.org/docs/dev/api/skimage.measure.html#skimage.measure.regionprops 119 | """ 120 | 121 | # load image mask 122 | img_mask = io.imread(mask_file) 123 | 124 | res = regionprops_table(img_mask, properties=properties) 125 | res['cell_id'] = res['label'] 126 | del res['label'] 127 | 128 | if as_dict: 129 | return res 130 | else: 131 | return pd.DataFrame.from_dict(res).set_index('cell_id') 132 | -------------------------------------------------------------------------------- /athena/utils/tools/metric.py: -------------------------------------------------------------------------------- 1 | # imports 2 | import numpy as np 3 | import pandas as pd 4 | from typing import Callable, Optional, Union, List, Tuple, Iterable, Counter as type_Counter 5 | from pandas.api.types import is_list_like 6 | 7 | from ..general import make_iterable 8 | 9 | _callList = Iterable[Callable[..., np.ndarray]] 10 | 11 | def compute_metrics(df: Union[pd.DataFrame, pd.Series], 12 | metrics: Union[Callable[...,np.ndarray], _callList], 13 | key: Optional[str] = None, 14 | metrics_kwargs: dict = {}, 15 | name = None) -> pd.DataFrame: 16 | 17 | """ 18 | 19 | Parameters 20 | ---------- 21 | df : pd.DataFrame or pd.Series 22 | DataFrame or Series with the counts of the species in a column defined by key (if DataFrame). 23 | metrics : callable or list of 24 | A list of functions that take counts and output a numpy.ndarray. 25 | key : str or None 26 | If df is a DataFrame the key indicates which column the counts are. 27 | Returns 28 | ------- 29 | 30 | """ 31 | 32 | if isinstance(df, pd.Series): 33 | key = df.name 34 | df = df.to_frame() 35 | elif key is None: 36 | raise TypeError('No look up key specified') 37 | 38 | if not is_list_like(metrics): 39 | metrics = make_iterable(metrics) 40 | 41 | if is_list_like(metrics): 42 | for metric in metrics: 43 | df = df.assign(metric_name = df[key].apply(lambda x: metric(x, **metrics_kwargs))) 44 | if name is not None: 45 | df = df.rename(columns={'metric_name': name}) 46 | elif (metrics_kwargs is None) or ('metric' not in metrics_kwargs): 47 | df = df.rename(columns={'metric_name': metric.__name__}) 48 | else: 49 | df = df.rename(columns={'metric_name': f'{metric.__name__}.{metrics_kwargs["metric"]}'}) 50 | 51 | return df 52 | 53 | def get_group_feature(expr: np.ndarray, 54 | groups: dict, 55 | reducer: Callable[[Union[int, float, Iterable]], np.ndarray], 56 | axis = 0, 57 | **kwargs): 58 | 59 | # cast to int 60 | # np.fromiter(val dtype=int) 61 | grps = {key:np.array(val, dtype=int) for key,val in groups.items()} 62 | 63 | out = np.zeros((len(grps), expr.shape[1])) 64 | for idx, grp in enumerate(grps.values()): 65 | out[idx,:] = reducer(expr[grp, :], axis, **kwargs) 66 | 67 | return pd.DataFrame(out, index=groups.keys()) 68 | 69 | def extract_metric_results(so): 70 | # TODO 71 | '''Extract all the computed results from the spatialOmics instance. 72 | 73 | Args: 74 | so: 75 | 76 | Returns: 77 | 78 | ''' 79 | pass -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | # tests 2 | pytest==6.2.4 3 | pytest-cov==2.11.1 4 | # checks 5 | black==21.5b0 6 | flake8==3.9.1 7 | mypy==0.812 8 | # docs 9 | sphinx==3.5.4 10 | sphinx-autodoc-typehints==1.12.0 11 | better-apidoc==0.3.1 12 | six==1.16.0 13 | sphinx_rtd_theme==0.5.2 14 | myst-parser==0.14 15 | # 16 | nbconvert==6.5.0 17 | Jinja2<3.1 18 | jupyterlab==3.3.4 19 | colorcet==3.0.0 -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | clean: 18 | @-rm -rf $(BUILDDIR)/* 19 | @-rm -rf api/*.rst 20 | 21 | # Catch-all target: route all unknown targets to Sphinx using the new 22 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 23 | %: Makefile 24 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 25 | -------------------------------------------------------------------------------- /docs/_templates/module.rst: -------------------------------------------------------------------------------- 1 | {# The :autogenerated: tag is picked up by breadcrumbs.html to suppress "Edit on Github" link #} 2 | :autogenerated: 3 | 4 | {{ fullname }} module 5 | {% for item in range(7 + fullname|length) -%}={%- endfor %} 6 | 7 | .. currentmodule:: {{ fullname }} 8 | 9 | .. automodule:: {{ fullname }} 10 | {% if members -%} 11 | :members: {{ members|join(", ") }} 12 | :undoc-members: 13 | :show-inheritance: 14 | :member-order: bysource 15 | 16 | Summary 17 | ------- 18 | 19 | {%- if exceptions %} 20 | 21 | Exceptions: 22 | 23 | .. autosummary:: 24 | :nosignatures: 25 | {% for item in exceptions %} 26 | {{ item }} 27 | {%- endfor %} 28 | {%- endif %} 29 | 30 | {%- if classes %} 31 | 32 | Classes: 33 | 34 | .. autosummary:: 35 | :nosignatures: 36 | {% for item in classes %} 37 | {{ item }} 38 | {%- endfor %} 39 | {%- endif %} 40 | 41 | {%- if functions %} 42 | 43 | Functions: 44 | 45 | .. autosummary:: 46 | :nosignatures: 47 | {% for item in functions %} 48 | {{ item }} 49 | {%- endfor %} 50 | {%- endif %} 51 | {%- endif %} 52 | 53 | {% set data = get_members(typ='data', in_list='__all__') %} 54 | {%- if data %} 55 | 56 | Data: 57 | 58 | .. autosummary:: 59 | :nosignatures: 60 | {% for item in data %} 61 | {{ item }} 62 | {%- endfor %} 63 | {%- endif %} 64 | 65 | {% set all_refs = get_members(in_list='__all__', include_imported=True, out_format='refs') %} 66 | {% if all_refs %} 67 | ``__all__``: {{ all_refs|join(", ") }} 68 | {%- endif %} 69 | 70 | 71 | {% if members %} 72 | Reference 73 | --------- 74 | 75 | {%- endif %} 76 | -------------------------------------------------------------------------------- /docs/_templates/package.rst: -------------------------------------------------------------------------------- 1 | {# The :autogenerated: tag is picked up by breadcrumbs.html to suppress "Edit on Github" link #} 2 | :autogenerated: 3 | 4 | {{ fullname }} package 5 | {% for item in range(8 + fullname|length) -%}={%- endfor %} 6 | 7 | .. automodule:: {{ fullname }} 8 | {% if members -%} 9 | :members: {{ members|join(", ") }} 10 | :undoc-members: 11 | :show-inheritance: 12 | {%- endif %} 13 | 14 | {% if submodules %} 15 | Submodules: 16 | 17 | .. toctree:: 18 | :maxdepth: 1 19 | {% for item in submodules %} 20 | {{ fullname }}.{{ item }} 21 | {%- endfor %} 22 | {%- endif -%} 23 | 24 | {% if subpackages %} 25 | 26 | Subpackages: 27 | 28 | .. toctree:: 29 | :maxdepth: 1 30 | {% for item in subpackages %} 31 | {{ fullname }}.{{ item }} 32 | {%- endfor %} 33 | {%- endif %} 34 | 35 | {% set all = get_members(in_list='__all__', include_imported=True) %} 36 | {% if members or all %} 37 | Summary 38 | ------- 39 | 40 | {%- set exceptions = get_members(typ='exception', in_list='__all__', include_imported=True, out_format='table') -%} 41 | {%- set classes = get_members(typ='class', in_list='__all__', include_imported=True, out_format='table') -%} 42 | {%- set functions = get_members(typ='function', in_list='__all__', include_imported=True, out_format='table') -%} 43 | {%- set data = get_members(typ='data', in_list='__all__', include_imported=True, out_format='table') -%} 44 | {%- set private_exceptions = get_members(typ='exception', in_list='__private__', out_format='table') -%} 45 | {%- set private_classes = get_members(typ='class', in_list='__private__', out_format='table') -%} 46 | {%- set private_functions = get_members(typ='function', in_list='__private__', out_format='table') -%} 47 | 48 | {%- if exceptions %} 49 | 50 | ``__all__`` Exceptions: 51 | 52 | {% for line in exceptions %} 53 | {{ line }} 54 | {%- endfor %} 55 | {%- endif %} 56 | {%- if private_exceptions %} 57 | 58 | Private Exceptions: 59 | 60 | {% for line in private_exceptions %} 61 | {{ line }} 62 | {%- endfor %} 63 | {%- endif %} 64 | 65 | {%- if classes %} 66 | 67 | ``__all__`` Classes: 68 | 69 | {% for line in classes %} 70 | {{ line }} 71 | {%- endfor %} 72 | {%- endif %} 73 | {%- if private_classes %} 74 | 75 | Private Classes: 76 | 77 | {% for line in private_classes %} 78 | {{ line }} 79 | {%- endfor %} 80 | {%- endif %} 81 | 82 | {%- if functions %} 83 | 84 | ``__all__`` Functions: 85 | 86 | {% for line in functions %} 87 | {{ line }} 88 | {%- endfor %} 89 | {%- endif %} 90 | {%- if private_functions %} 91 | 92 | Private Functions: 93 | 94 | {% for line in private_functions %} 95 | {{ line }} 96 | {%- endfor %} 97 | {%- endif %} 98 | 99 | {%- if data %} 100 | 101 | ``__all__`` Data: 102 | 103 | {% for line in data %} 104 | {{ line }} 105 | {%- endfor %} 106 | {%- endif %} 107 | 108 | {%- endif %} 109 | 110 | 111 | {% if members %} 112 | Reference 113 | --------- 114 | 115 | {%- endif %} 116 | -------------------------------------------------------------------------------- /docs/api_overview.rst: -------------------------------------------------------------------------------- 1 | API Overview 2 | ============ 3 | Quick overview of the :ref:`methods` and :ref:`datasets` available in ATHENA. 4 | 5 | .. _methods: 6 | 7 | Methods 8 | ------- 9 | Depending on the underlying mathematical foundations, the heterogeneity 10 | scores included in ATHENA can be classified into the following categories: (i) spatial statistics scores that 11 | quantify the degree of clustering or dispersion of each phenotype individually, (ii) graph-theoretic scores that 12 | examine the topology of the tumor graph, (iii) information-theoretic scores that quantify how diverse the 13 | tumor is with respect to different phenotypes present and their relative proportions, and (iv) cell interaction 14 | scores that assess the pairwise connections between different phenotypes in the tumor ecosystem. The interested reader 15 | is advised to read the *Methodology* section. 16 | 17 | Pre-processing 18 | ^^^^^^^^^^^^^^ 19 | Collection of common pre-processing functionalities. 20 | 21 | .. autosummary:: 22 | ~athena.preprocessing.preprocess.extract_centroids 23 | ~athena.preprocessing.preprocess.arcsinh 24 | 25 | 26 | Graph building 27 | ^^^^^^^^^^^^^^ 28 | The :mod:`athena.graph_builder` submodule of ATHENA constructs a graph representation of the tissue using the 29 | cell masks extracted from the high-dimensional images. The graph construction module implements three 30 | different graph flavors that capture different kinds of cell-cell communication: 31 | 32 | - *contact*-graph: juxtacrine signaling, where cells exchange information via membrane receptors, junctions or extracellular matrix glycoproteins 33 | - *radius*-graph: representation mimics paracrine signaling, where signaling molecules that are secreted into the extracellular environment interact with membrane receptors of neighboring cells and induce changes in their cellular state. 34 | - *knn*-graph: common graph topology, successfully used in digital pathology 35 | 36 | .. autosummary:: 37 | ~spatialHeterogeneity.graph_builder.graphBuilder.build_graph 38 | 39 | Visualisation 40 | ^^^^^^^^^^^^^ 41 | The plotting module (:mod:`athena.plotting`) enables the user to visualise the data and provides out-of-the-box plots for some 42 | of the metrics. 43 | 44 | .. autosummary:: 45 | ~athena.plotting.visualization.spatial 46 | ~athena.plotting.visualization.napari_viewer 47 | ~athena.plotting.visualization.interactions 48 | ~athena.plotting.visualization.ripleysK 49 | ~athena.plotting.visualization.infiltration 50 | 51 | 52 | Entropic metrics 53 | ^^^^^^^^^^^^^^^^^^ 54 | ATHENA brings together a number of established as well as novel scores that enable the quantification of 55 | tumor heterogeneity in a spatially-aware manner, borrowing ideas from ecology, information theory, spatial 56 | statistics, and network analysis. 57 | 58 | .. autosummary:: 59 | ~athena.metrics.heterogeneity.metrics.richness 60 | ~athena.metrics.heterogeneity.metrics.abundance 61 | ~athena.metrics.heterogeneity.metrics.shannon 62 | ~athena.metrics.heterogeneity.metrics.simpson 63 | ~athena.metrics.heterogeneity.metrics.renyi_entropy 64 | ~athena.metrics.heterogeneity.metrics.hill_number 65 | ~athena.metrics.heterogeneity.metrics.quadratic_entropy 66 | 67 | Graph metrics 68 | ^^^^^^^^^^^^^^^^^^ 69 | Currently, this module only implements modularity which captures the structure of a graph by quantifying the degree at which it can 70 | be divided into communities of the same label. In the context of tumor heterogeneity, modularity can be 71 | thought of as the degree of self-organization of the cells with the same phenotype into spatially distinct 72 | communities. 73 | 74 | .. autosummary:: 75 | ~athena.metrics.graph.graph.modularity 76 | 77 | Cell-cell interaction metrics 78 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 79 | More sophisticated heterogeneity scores additionally consider cell-cell interactions by exploiting the cell-cell graph, 80 | where nodes encode cells, edges encode interactions, and each node is associated with a label 81 | that encodes the cell’s phenotype. The cell interaction scores implemented in ATHENA’s :mod:`~neighborhood` submodule 82 | include: 83 | 84 | .. autosummary:: 85 | ~athena.neighborhood.estimators.interactions 86 | ~athena.neighborhood.estimators.infiltration 87 | ~athena.neighborhood.estimators.ripleysK 88 | 89 | .. _datasets: 90 | 91 | Datasets 92 | -------- 93 | ATHENA provides two datasets that enables users to explore the implemented functionalities and analytical tools: 94 | 95 | - An image mass cytometry dataset [Jackson]_ 96 | - An multiplexed ion beam imaging dataset [Keren]_ 97 | 98 | .. autosummary:: 99 | ~athena.dataset.datasets.imc 100 | ~athena.dataset.datasets.mibi 101 | 102 | References 103 | ^^^^^^^^^^ 104 | .. [Jackson] Jackson, H. W. et al. The single-cell pathology landscape of breast cancer. 105 | `Nature. `_ 106 | 107 | .. [Keren] Keren, L. et al. A Structured Tumor-Immune Microenvironment in Triple Negative Breast Cancer Revealed by 108 | Multiplexed Ion Beam Imaging. Cell 174, 1373-1387.e19 (2018). `Cell. `_ -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "athena" 22 | copyright = "IBM Corp. 2021" 23 | author = "Adriano Martinelli (art@zurich.ibm.com)" 24 | 25 | # -- Generate API (auto) documentation ------------------------------------------------ 26 | 27 | 28 | def run_apidoc(app): 29 | """Generage API documentation""" 30 | import better_apidoc 31 | 32 | better_apidoc.APP = app 33 | better_apidoc.main( 34 | [ 35 | "better-apidoc", 36 | "-t", 37 | os.path.join(".", "_templates"), 38 | "--force", 39 | "--no-toc", 40 | "--separate", 41 | "-o", 42 | os.path.join(".", "api"), 43 | os.path.join("..", "athena"), 44 | ] 45 | ) 46 | 47 | 48 | # -- General configuration --------------------------------------------------- 49 | 50 | # Add any Sphinx extension module names here, as strings. They can be 51 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 52 | # ones. 53 | extensions = [ 54 | "sphinx.ext.autodoc", 55 | "sphinx.ext.autosummary", 56 | "sphinx.ext.todo", 57 | "sphinx.ext.coverage", 58 | "sphinx.ext.viewcode", 59 | "sphinx.ext.githubpages", 60 | "sphinx.ext.napoleon", 61 | "sphinx_autodoc_typehints", 62 | "sphinx_rtd_theme", 63 | "myst_parser" 64 | ] 65 | 66 | # Add any paths that contain templates here, relative to this directory. 67 | templates_path = ["_templates"] 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This pattern also affects html_static_path and html_extra_path. 72 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 73 | 74 | 75 | # -- Options for HTML output ------------------------------------------------- 76 | 77 | # The theme to use for HTML and HTML Help pages. See the documentation for 78 | # a list of builtin themes. 79 | # 80 | html_theme = "sphinx_rtd_theme" 81 | 82 | # Add any paths that contain custom static files (such as style sheets) here, 83 | # relative to this directory. They are copied after the builtin static files, 84 | # so a file named "default.css" will overwrite the builtin "default.css". 85 | html_static_path = ["_static"] 86 | 87 | 88 | # -- Extension configuration ------------------------------------------------- 89 | add_module_names = False 90 | 91 | 92 | napoleon_google_docstring = True 93 | napoleon_include_init_with_doc = True 94 | 95 | coverage_ignore_modules = [] 96 | coverage_ignore_functions = [] 97 | coverage_ignore_classes = [] 98 | 99 | coverage_show_missing_items = True 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = "sphinx" 103 | 104 | # -- Options for todo extension ---------------------------------------------- 105 | 106 | # If true, `todo` and `todoList` produce output, else they produce nothing. 107 | todo_include_todos = True 108 | 109 | 110 | def setup(app): 111 | app.connect("builder-inited", run_apidoc) 112 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ATHENA - Analysis of Tumor Heterogeneity in Spatial Omics Measurements 2 | ====================================================================== 3 | 4 | ATHENA is an open-source computational framework written in Python that facilitates the visualization, processing and analysis of (spatial) heterogeneity from spatial omics data. ATHENA supports any spatially resolved dataset that contains spatial transcriptomic or proteomic measurements, including Imaging Mass Cytometry (IMC), Multiplexed Ion Beam Imaging (MIBI), multiplexed Immunohistochemisty (mIHC) or Immunofluorescence (mIF), seqFISH, MERFISH, Visium. 5 | 6 | .. note:: 7 | Start by reading the `Quickstart `_ tutorial. 8 | 9 | .. toctree:: 10 | :caption: General 11 | :maxdepth: 1 12 | 13 | Installation 14 | 15 | .. toctree:: 16 | :caption: Gallery 17 | :maxdepth: 2 18 | 19 | Overview of ATHENA 20 | Methodology 21 | source/quickstart.rst 22 | Tutorial 23 | SpatialOmics Tutorial 24 | 25 | .. toctree:: 26 | :caption: API 27 | :maxdepth: 2 28 | 29 | api_overview 30 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/img/athena_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/athena_logo.png -------------------------------------------------------------------------------- /docs/source/img/bulk-sc-spatial.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/bulk-sc-spatial.jpg -------------------------------------------------------------------------------- /docs/source/img/dilation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/dilation.png -------------------------------------------------------------------------------- /docs/source/img/entropic-measures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/entropic-measures.png -------------------------------------------------------------------------------- /docs/source/img/images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/images.png -------------------------------------------------------------------------------- /docs/source/img/imc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/imc2.png -------------------------------------------------------------------------------- /docs/source/img/interactions-quant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/interactions-quant.png -------------------------------------------------------------------------------- /docs/source/img/interactions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/interactions.png -------------------------------------------------------------------------------- /docs/source/img/interoperability.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/interoperability.pdf -------------------------------------------------------------------------------- /docs/source/img/interoperability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/interoperability.png -------------------------------------------------------------------------------- /docs/source/img/local-global.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/local-global.pdf -------------------------------------------------------------------------------- /docs/source/img/local-global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/local-global.png -------------------------------------------------------------------------------- /docs/source/img/measurement.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/measurement.jpg -------------------------------------------------------------------------------- /docs/source/img/metalabels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/metalabels.png -------------------------------------------------------------------------------- /docs/source/img/metrics-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/metrics-overview.png -------------------------------------------------------------------------------- /docs/source/img/overview.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/overview.pdf -------------------------------------------------------------------------------- /docs/source/img/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/overview.png -------------------------------------------------------------------------------- /docs/source/img/overview_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/overview_old.png -------------------------------------------------------------------------------- /docs/source/img/random.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/random.gif -------------------------------------------------------------------------------- /docs/source/img/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/sample.png -------------------------------------------------------------------------------- /docs/source/img/simulation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/simulation.png -------------------------------------------------------------------------------- /docs/source/img/single-cell-analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/single-cell-analysis.png -------------------------------------------------------------------------------- /docs/source/img/single-cell-methods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/single-cell-methods.png -------------------------------------------------------------------------------- /docs/source/img/spatialHeterogeneity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/spatialHeterogeneity.png -------------------------------------------------------------------------------- /docs/source/img/spatialOmics.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/spatialOmics.pdf -------------------------------------------------------------------------------- /docs/source/img/spatialOmics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/spatialOmics.png -------------------------------------------------------------------------------- /docs/source/img/spatialOmics_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/img/spatialOmics_old.png -------------------------------------------------------------------------------- /docs/source/installation.md: -------------------------------------------------------------------------------- 1 | # Install ATHENA 2 | 3 | ```{code-block} 4 | # create a new virtual environment with Python 3.8 5 | conda create -y -n athena python=3.8 6 | conda activate athena 7 | 8 | # install athena, spatial omics 9 | pip install ai4scr-spatial-omics ai4scr-athena 10 | 11 | # install interactive tools 12 | pip install jupyterlab 13 | ``` -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_10_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_10_1.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_12_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_12_1.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_28_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_28_1.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_30_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_30_0.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_34_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_34_0.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_38_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_38_1.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_38_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_38_2.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_40_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_40_0.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_42_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_42_0.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_47_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_47_1.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_49_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_49_0.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_50_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_50_1.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_52_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_52_2.png -------------------------------------------------------------------------------- /docs/source/introduction-spatialOmics_files/introduction-spatialOmics_56_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/introduction-spatialOmics_files/introduction-spatialOmics_56_1.png -------------------------------------------------------------------------------- /docs/source/methodology.md: -------------------------------------------------------------------------------- 1 | # Methodology 2 | 3 | The PDF is available [here](https://github.com/AI4SCR/ATHENA/blob/master/tutorials/ATHENA_Supplementary.pdf) 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/source/overview.md: -------------------------------------------------------------------------------- 1 | ![athena logo](img/athena_logo.png) 2 | # Overview 3 | 4 | ATHENA is an open-source computational framework written in Python that facilitates the visualization, processing and analysis of (spatial) heterogeneity from spatial omics data. ATHENA supports any spatially resolved dataset that contains spatial transcriptomic or proteomic measurements, including Imaging Mass Cytometry (IMC), Multiplexed Ion Beam Imaging (MIBI), multiplexed Immunohistochemisty (mIHC) or Immunofluorescence (mIF), seqFISH, MERFISH, Visium. 5 | 6 | ![overview](img/overview.png) 7 | 8 | 1. ATHENA accomodates raw multiplexed images from spatial omics measurements. Together with the images, segmentation masks, cell-level, feature-level and sample-level annotations can be uploaded. 9 | 10 | 2. Based on the cell masks, ATHENA constructs graph representations of the data. The framework currently supports three flavors, namely radius, knn, and contact graphs. 11 | 12 | 4. ATHENA incorporates a variety of methods to quantify heterogeneity, such as global and local entropic scores. Furthermore, cell type interaction strength scores or measures of spatial clustering and dispersion. 13 | 14 | 5. Finally, the large collection of computed scores can be extracted and used as input in downstream machine learning models to perform tasks such as clinical data prediction, patient stratification or discovery of new (spatial) biomarkers. 15 | 16 | ## Main components 17 | - `SpatialOmics`, a new data structure inspired by [AnnData](https://anndata.readthedocs.io/en/latest/). 18 | - `Athena`, a module that enables the computation of various heterogeneity scores. 19 | 20 | ### `SpatialOmics` Data Structure 21 | The `SpatialOmics` class is designed to accommodate storing and processing spatial omics datasets in a technology-agnostic and memory-efficient way. A `SpatialOmics` instance incorporates multiple attributes that bundle together the multiplexed raw images with the segmentation masks, cell-cell graphs, single-cell values, and sample-, feature- and cell-level annotations, as outlined in the figure below. Since ATHENA works with multiplexed images, memory complexity is a problem. `SpatialOmics` stores data in a HDF5 file and lazily loads the required images on the fly to keep the memory consumption low. The `SpatialOmics` structure is sample-centric, i.e., all samples from a spatial omics experiment are stored separately by heavily using Python dictionaries. 22 | 23 | ![overview](img/spatialOmics.png) 24 | 25 | Specifically, each `SpatialOmics` instance contains the following attributes: 26 | 1. `.images`: A Python dictionary (length: `#samples`) of raw multiplexed images, where each sample is mapped to a [numpy](https://numpy.org/) array of shape: `#features x image_width x image_height`. 27 | 2. `.masks`: A nested Python dictionary (length: `#samples`) supporting different types of segmentation masks (e.g., cell and tissue masks), where each sample is mapped to an inner dictionary (length: `#mask_types`), and each value of the inner dictionary is a binary [numpy](https://numpy.org/) array of shape: `#image_width x image_height`. 28 | 3. `.G`: A nested Python dictionary (length: `#samples`) supporting different topologies of graphs (e.g., knn, contact or radius graph), where each sample is mapped to an inner dictionary (length: `#graph_types`), and each value of the inner dictionary is a [networkx](https://networkx.org/) graph. 29 | 4. `.X`: A Python dictionary of single-cell measurements (length: `#samples`), where each sample is mapped to a [pandas](https://pandas.pydata.org/) dataframe of shape: `#single_cells x #features`. The values in `.X` can either be uploaded or directly computed from `.images` and `.masks`. 30 | 5. `.spl`: A [pandas](https://pandas.pydata.org/) dataframe containing sample-level annotations (e.g., patient clinical data) of shape: `#samples x #annotations`. 31 | 6. `.obs`: A Python dictionary (length: `#samples`) containing single-cell-level annotations (e.g., cluster id, cell type, morphological fatures), where each sample is mapped to a [pandas](https://pandas.pydata.org/) dataframe of shape: `#single_cells x #annotations`. 32 | 7. `.var`: A Python dictionary (length: `#samples`) containing feature-level annotations (e.g., name of protein/transcript), where each sample is mapped to a [pandas](https://pandas.pydata.org/) dataframe of shape: `#features x #annotations`. 33 | 8. `.uns`: A Python dictionary containing unstructed data, e.g. various colormaps, experiment properties etc. 34 | 35 | ### `Athena` 36 | 37 | `Athena` implements all visualization, processing and analysis steps integral to its functionalities. `Athena` consists in the following 5 submodules, each one performing different tasks as outlined below: 38 | ![spatialHeterogeneity](img/spatialHeterogeneity.png) 39 | 40 | `Athena` is tightly interwoven with `SpatialOmics` (see figure below), in the sense that the submodules of `Athena` take as input various aspects of the data as stored in `SpatialOmics` (green arrows) and, at the same time, store computed outputs back into different attributes of `SpatialOmics` (purple arrows). 41 | 42 | ![interoperability](img/interoperability.png) 43 | 44 | 1. `.pp` works with `.images` and .`masks` and facilitates image pre-processing functions, such as extraction of cell centroids. ATHENA requires segmentation masks to be provided by the user. For ideas on how to do that, see Further Resources. 45 | 2. `.pl` supports plotting all aspects of the data, including raw images, masks, graphs and visualizes different annotations as well as results of computed heterogeneity scores. The plots can be either static or interactive, by exploiting the Python image viewer [napari](https://napari.org/#). 46 | 3. `.graph` construct cell-cell graphs from the cell masks using three different graph builders: `kNN` (connects each cell with its _k_ closest neighbors), `radius` (connects each cell to all other cells within a radius $r$), and `contact` (connects cells that physically "touch" by first enlarging each mask by dilation and then connecting it to all other masks if there is overlap). The resulting graphs are saved back in the `.G` attribute of `SpatialOmics`. 47 | 4. `.metrics` uses the cell-cell graphs, the single-cell values (in `.X`) and cell annotations (in `.obs`) to compute a number of diversity scores, including sample richness (number of distinct cell subpopulations/clusters/clones) and abundance (relative proportions of species), and information theoretic scores, (namely Shannon, Simpson, quadratic, or Renyi entropy, Hill numbers), either at a global, sample level (saved in `.spl`), or at a local, single-cell-level (saved in `.obs`) that incorporates the spatial information. 48 | 5. `.neigh` implements a number of neighborhood or spatial statistics methods, namely infiltration score, Ripley's $k$ and neighborhood analysis scores. Results are saved in `.spl` and `.uns`. 49 | 50 | ## Further Resources 51 | 52 | - [Squidpy](https://squidpy.readthedocs.io/en/stable/) 53 | - Jackson, H. W. et al. The single-cell pathology landscape of breast cancer. Nature 578, 615–620 (2020). 54 | -------------------------------------------------------------------------------- /docs/source/quickstart_files/quickstart_20_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/quickstart_files/quickstart_20_1.png -------------------------------------------------------------------------------- /docs/source/quickstart_files/quickstart_22_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/quickstart_files/quickstart_22_0.png -------------------------------------------------------------------------------- /docs/source/quickstart_files/quickstart_22_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/quickstart_files/quickstart_22_1.png -------------------------------------------------------------------------------- /docs/source/quickstart_files/quickstart_24_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/quickstart_files/quickstart_24_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_17_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_17_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_19_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_19_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_23_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_23_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_27_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_27_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_29_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_29_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_33_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_33_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_35_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_35_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_37_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_37_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_39_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_39_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_44_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_44_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_48_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_48_1.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_52_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_52_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_54_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_54_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_56_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_56_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_58_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_58_0.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_64_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_64_1.png -------------------------------------------------------------------------------- /docs/source/tutorial_files/tutorial_68_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/docs/source/tutorial_files/tutorial_68_0.png -------------------------------------------------------------------------------- /licence.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021 Adriano Martinelli 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/py.typed -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | skip-string-normalization = false 4 | target-version = ['py37'] 5 | 6 | [tool.isort] 7 | multi_line_output = 3 8 | include_trailing_comma = true 9 | force_grid_wrap = 0 10 | use_parentheses = true 11 | ensure_newline_before_comments = true 12 | line_length = 88 13 | force_to_top = ["rdkit", "scikit-learn"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | #pip package names required by the package, and exact versions you know to work 2 | scanpy==1.9.1 3 | scikit-image==0.19.2 4 | scikit-learn==1.0.2 5 | scipy==1.8.0 6 | numpy==1.21.6 7 | pandas==1.2 8 | networkx==2.8 9 | h5py==3.6.0 10 | tables==3.7.0 11 | astropy==5.0.4 12 | tqdm==4.64.0 13 | napari[all]==0.4.15 14 | seaborn==0.11.2 15 | squidpy==1.2.0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 80 3 | select = C,E,F,W,B,B950 4 | ignore = E203, E501, W503 5 | 6 | [mypy] 7 | check_untyped_defs = True 8 | 9 | [mypy-pytest.*] 10 | ignore_missing_imports = True -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Install package.""" 2 | import io 3 | import re 4 | 5 | from pkg_resources import parse_requirements 6 | from setuptools import find_packages, setup 7 | 8 | 9 | def read_version(filepath: str) -> str: 10 | """Read the __version__ variable from the file. 11 | 12 | Args: 13 | filepath: probably the path to the root __init__.py 14 | 15 | Returns: 16 | the version 17 | """ 18 | match = re.search( 19 | r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', 20 | io.open(filepath, encoding="utf_8_sig").read(), 21 | ) 22 | if match is None: 23 | raise SystemExit("Version number not found.") 24 | return match.group(1) 25 | 26 | # ease installation during development 27 | vcs = re.compile(r"(git|svn|hg|bzr)\+") 28 | try: 29 | with open("requirements.txt") as fp: 30 | VCS_REQUIREMENTS = [ 31 | str(requirement) 32 | for requirement in parse_requirements(fp) 33 | if vcs.search(str(requirement)) 34 | ] 35 | except FileNotFoundError: 36 | # requires verbose flags to show 37 | print("requirements.txt not found.") 38 | VCS_REQUIREMENTS = [] 39 | 40 | # TODO: Update these values according to the name of the module. 41 | setup( 42 | name="ai4scr-athena", 43 | version=read_version("athena/__init__.py"), # single place for version 44 | description="ATHENA package provides methods to analyse spatial heterogeneity in spatial omics data", 45 | long_description=open("README.md").read(), 46 | url="https://github.com/AI4SCR/ATHENA", 47 | author="Adriano Martinelli", 48 | author_email="art@zurich.ibm.com", 49 | # the following exclusion is to prevent shipping of tests. 50 | # if you do include them, add pytest to the required packages. 51 | packages=find_packages(".", exclude=["*tests*"]), 52 | package_data={"spatialHeterogeneity": ["py.typed"]}, 53 | # entry_points='', 54 | # scripts=["bin/brief_salutation", "bin/a_shell_script"], 55 | extras_require={ 56 | "vcs": VCS_REQUIREMENTS, 57 | "test": ["pytest", "pytest-cov"], 58 | "dev": [ 59 | # tests 60 | 'pytest==6.2.4', 61 | 'pytest-cov==2.11.1', 62 | # checks 63 | 'black==21.5b0', 64 | 'flake8==3.9.1', 65 | 'mypy==0.812', 66 | # docs 67 | 'sphinx==3.5.4', 68 | 'sphinx-autodoc-typehints==1.12.0', 69 | 'better-apidoc==0.3.1', 70 | 'six==1.16.0', 71 | 'sphinx_rtd_theme==0.5.2', 72 | 'myst-parser==0.14', 73 | # 74 | 'nbconvert==6.5.0', 75 | 'Jinja2<3.1', 76 | 'jupyterlab==3.3.4', 77 | 'colorcet==3.0.0', 78 | 'twine' 79 | ] 80 | }, 81 | # versions should be very loose here, just exclude unsuitable versions 82 | # because your dependencies also have dependencies and so on ... 83 | # being too strict here will make dependency resolution harder 84 | install_requires=[ 85 | 'scanpy>=1.9.1', 86 | 'scikit-image>=0.19.2', 87 | 'scikit-learn>=1.0.2', 88 | 'scipy>=1.8.0', 89 | 'numpy>=1.21.6', 90 | 'pandas>=1.2', 91 | 'networkx>=2.8', 92 | 'h5py>=3.6.0', 93 | 'tables>=3.7.0', 94 | 'astropy>=5.0.4', 95 | 'tqdm>=4.64.0', 96 | 'napari[all]>=0.4.15', 97 | 'seaborn>=0.11.2', 98 | 'squidpy>=1.2.0', 99 | ] 100 | ) 101 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tests/__init__.py -------------------------------------------------------------------------------- /tests/attributer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tests/attributer/__init__.py -------------------------------------------------------------------------------- /tests/attributer/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import athena as ath 3 | import copy as cp 4 | from athena.utils.default_configs import GRAPH_ATTRIBUTER_DEFAULT_PARAMS 5 | 6 | @pytest.fixture(scope="module") 7 | def so_fixture(): 8 | # Loead data 9 | so = ath.dataset.imc_sample() 10 | 11 | # Define sample 12 | spl = 'slide_49_By2x5' 13 | 14 | # Extrac centroids 15 | ath.pp.extract_centroids(so, spl, mask_key='cellmasks') 16 | 17 | return (so, spl) 18 | 19 | @pytest.fixture(scope="module") 20 | def default_params(): 21 | return GRAPH_ATTRIBUTER_DEFAULT_PARAMS 22 | -------------------------------------------------------------------------------- /tests/attributer/integration/manual_test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Load dependencies and `so` object" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "tags": [] 15 | }, 16 | "outputs": [ 17 | { 18 | "name": "stderr", 19 | "output_type": "stream", 20 | "text": [ 21 | "INFO:numexpr.utils:NumExpr defaulting to 8 threads.\n" 22 | ] 23 | }, 24 | { 25 | "name": "stdout", 26 | "output_type": "stream", 27 | "text": [ 28 | "warning: to get the latest version of this dataset use `so = sh.dataset.imc(force_download=True)`\n" 29 | ] 30 | } 31 | ], 32 | "source": [ 33 | "# The perpuse of this notebook is to visually show that the new features added to athena, \n", 34 | "# namely the attributer functionality, works appropriately. \n", 35 | "\n", 36 | "# Note: this notebook should be run in an environment where the pubilicly available athena version of the package is not installed. \n", 37 | "# Needles to say all of the other dependencies described in the requirements.txt and dev_requirements.txt should be avilable.\n", 38 | "\n", 39 | "import os\n", 40 | "import sys\n", 41 | "import copy as cp\n", 42 | "import numpy as np\n", 43 | "import pandas as pd\n", 44 | "import networkx as nx\n", 45 | "\n", 46 | "# Add path to the ATHENA beta repository\n", 47 | "module_path = os.path.abspath(os.path.join('../../../'))\n", 48 | "if module_path not in sys.path:\n", 49 | " sys.path.append(module_path)\n", 50 | "\n", 51 | "# Import Local package\n", 52 | "import athena as ath\n", 53 | "from athena.utils.default_configs import GRAPH_ATTRIBUTER_DEFAULT_PARAMS\n", 54 | "\n", 55 | "# Loead data\n", 56 | "so = ath.dataset.imc()\n", 57 | "\n", 58 | "# Define sample\n", 59 | "spl = 'slide_49_By2x5'\n", 60 | "\n", 61 | "# Extrac centroids\n", 62 | "ath.pp.extract_centroids(so, spl, mask_key='cellmasks')" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 2, 68 | "metadata": { 69 | "tags": [] 70 | }, 71 | "outputs": [ 72 | { 73 | "data": { 74 | "text/plain": [ 75 | "{}" 76 | ] 77 | }, 78 | "execution_count": 2, 79 | "metadata": {}, 80 | "output_type": "execute_result" 81 | } 82 | ], 83 | "source": [ 84 | "# No need to make graphs since they are preloaded apparently\n", 85 | "# Show that there is no attributes.\n", 86 | "so.G[spl]['knn'].nodes[1]" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 3, 92 | "metadata": {}, 93 | "outputs": [ 94 | { 95 | "data": { 96 | "text/plain": [ 97 | "{'from_obs': True,\n", 98 | " 'obs_cols': ['meta_id', 'cell_type_id', 'phenograph_cluster', 'y', 'x'],\n", 99 | " 'from_X': True,\n", 100 | " 'X_cols': 'all'}" 101 | ] 102 | }, 103 | "execution_count": 3, 104 | "metadata": {}, 105 | "output_type": "execute_result" 106 | } 107 | ], 108 | "source": [ 109 | "config = cp.deepcopy(GRAPH_ATTRIBUTER_DEFAULT_PARAMS['so'])\n", 110 | "config" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": 4, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "ath.attributer.add_node_features(so, spl, graph_key='knn', features_type='so', config=config)" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": 5, 125 | "metadata": {}, 126 | "outputs": [ 127 | { 128 | "data": { 129 | "text/plain": [ 130 | "{'meta_id': 11,\n", 131 | " 'cell_type_id': 3,\n", 132 | " 'phenograph_cluster': 2,\n", 133 | " 'y': 0.8,\n", 134 | " 'x': 134.1,\n", 135 | " 'H3': 1.31739772604977,\n", 136 | " 'H3K28me3': 0.524900019168854,\n", 137 | " 'Cytokeratin5': 0.319406591490773,\n", 138 | " 'Fibronectin': 35.0405220033211,\n", 139 | " 'Cytokeratin18': 0.0,\n", 140 | " 'Cytokeratin8_18': 0.0611545928983715,\n", 141 | " 'Twist': 0.0,\n", 142 | " 'CD68': 0.623443176899192,\n", 143 | " 'KRT14': 0.246027342461507,\n", 144 | " 'SMA': 0.0385142586096846,\n", 145 | " 'Vimentin': 4.24546156617393,\n", 146 | " 'cMyc': 0.314005477836054,\n", 147 | " 'c_erbB2Her2': 0.0,\n", 148 | " 'CD3epsilon': 0.0837909993025384,\n", 149 | " 'p_H3': 0.0,\n", 150 | " 'Slug': 0.0,\n", 151 | " 'ERa': 0.0726816199277735,\n", 152 | " 'PR_A_B': 0.673637350895625,\n", 153 | " 'p53': 0.0,\n", 154 | " 'CD44': 16.7154655385937,\n", 155 | " 'CD45': 0.481944079970148,\n", 156 | " 'GATA3': 0.266840493572139,\n", 157 | " 'CD20': 0.0,\n", 158 | " 'CarbonicAnhydraseIX': 0.511370712010106,\n", 159 | " 'E_P_Cadherin': 0.475703502084019,\n", 160 | " 'Ki_67': 0.049334585040109,\n", 161 | " 'EGFR': 0.417746155917027,\n", 162 | " 'p_S6': 0.490125682124616,\n", 163 | " 'vWF_CD31': 0.0452369704254316,\n", 164 | " 'p_mTOR': 0.604581735262798,\n", 165 | " 'Cytokeratin7': 0.0,\n", 166 | " 'CytokeratinPan': 0.099983991945649,\n", 167 | " 'PARP': 0.332670051178891,\n", 168 | " 'DNA2': 4.75190019607544}" 169 | ] 170 | }, 171 | "execution_count": 5, 172 | "metadata": {}, 173 | "output_type": "execute_result" 174 | } 175 | ], 176 | "source": [ 177 | "so.G[spl]['knn'].nodes[1]" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": 6, 183 | "metadata": {}, 184 | "outputs": [ 185 | { 186 | "data": { 187 | "text/plain": [ 188 | "{'n_attrs': 3}" 189 | ] 190 | }, 191 | "execution_count": 6, 192 | "metadata": {}, 193 | "output_type": "execute_result" 194 | } 195 | ], 196 | "source": [ 197 | "config = cp.deepcopy(GRAPH_ATTRIBUTER_DEFAULT_PARAMS['random'])\n", 198 | "config" 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": 7, 204 | "metadata": {}, 205 | "outputs": [], 206 | "source": [ 207 | "ath.attributer.add_node_features(so, spl, graph_key='contact', features_type='random', config=config)" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 8, 213 | "metadata": {}, 214 | "outputs": [ 215 | { 216 | "data": { 217 | "text/plain": [ 218 | "{0: 0.2297096372513353, 1: 0.889413959149841, 2: 0.7889544309866718}" 219 | ] 220 | }, 221 | "execution_count": 8, 222 | "metadata": {}, 223 | "output_type": "execute_result" 224 | } 225 | ], 226 | "source": [ 227 | "so.G[spl]['contact'].nodes[1]" 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": null, 233 | "metadata": {}, 234 | "outputs": [], 235 | "source": [] 236 | } 237 | ], 238 | "metadata": { 239 | "kernelspec": { 240 | "display_name": "Python 3 (ipykernel)", 241 | "language": "python", 242 | "name": "python3" 243 | }, 244 | "language_info": { 245 | "codemirror_mode": { 246 | "name": "ipython", 247 | "version": 3 248 | }, 249 | "file_extension": ".py", 250 | "mimetype": "text/x-python", 251 | "name": "python", 252 | "nbconvert_exporter": "python", 253 | "pygments_lexer": "ipython3", 254 | "version": "3.8.15" 255 | }, 256 | "vscode": { 257 | "interpreter": { 258 | "hash": "1a1af0ee75eeea9e2e1ee996c87e7a2b11a0bebd85af04bb136d915cefc0abce" 259 | } 260 | } 261 | }, 262 | "nbformat": 4, 263 | "nbformat_minor": 4 264 | } 265 | -------------------------------------------------------------------------------- /tests/attributer/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tests/attributer/unit/__init__.py -------------------------------------------------------------------------------- /tests/attributer/unit/test_attributer.py: -------------------------------------------------------------------------------- 1 | from athena.attributer import add_node_features 2 | import copy as cp 3 | import pytest 4 | 5 | # Test that error are bieng handleled correctly: 6 | def test_raise_both_false(so_fixture, default_params): 7 | so, spl = so_fixture 8 | config = cp.deepcopy(default_params['so']) 9 | config['attrs_params']['from_obs'] = False 10 | config['attrs_params']['from_X'] = False 11 | 12 | with pytest.raises(NameError): 13 | add_node_features(so=so, spl=spl, graph_key='knn', features_type='so', config=config) 14 | 15 | # Test empty list in obs_cols. 16 | def test_raise_empty_list(so_fixture, default_params): 17 | so, spl = so_fixture 18 | config = cp.deepcopy(default_params['so']) 19 | config['attrs_params']['obs_cols'] = [] 20 | 21 | # Test empty list on obs 22 | with pytest.raises(NameError): 23 | add_node_features(so=so, spl=spl, graph_key='knn', features_type='so', config=config) 24 | 25 | # Test empty list on X 26 | config['attrs_params']['obs_cols'] = ['x', 'y'] 27 | config['attrs_params']['X_cols'] = [] 28 | with pytest.raises(NameError): 29 | add_node_features(so=so, spl=spl, graph_key='knn', features_type='so', config=config) 30 | 31 | # Test. Not all elements provided in list config["obs_cols"] are in so.obs[spl].columns. 32 | def test_invalid_colnames(so_fixture, default_params): 33 | so, spl = so_fixture 34 | config = cp.deepcopy(default_params['so']) 35 | config['attrs_params']['obs_cols'] = ['x', 'not_a_col_name'] 36 | 37 | # Test wrong col name on obs 38 | with pytest.raises(NameError): 39 | add_node_features(so=so, spl=spl, graph_key='knn', features_type='so', config=config) 40 | 41 | config['attrs_params']['obs_cols'] = ['x', 'y'] 42 | config['attrs_params']['X_cols'] = ['Cytokeratin5', 'not_a_col_name'] 43 | 44 | # Test wrong col name on X 45 | with pytest.raises(NameError): 46 | add_node_features(so=so, spl=spl, graph_key='knn', features_type='so', config=config) 47 | 48 | # Test expected behaviour, namely 'so' attributes are in graph. 49 | def test_features_from_so(so_fixture, default_params): 50 | so, spl = so_fixture 51 | config = cp.deepcopy(default_params['so']) 52 | config['attrs_params']['obs_cols'] = ['x'] 53 | config['attrs_params']['from_X'] = False 54 | 55 | # By modifying obs 56 | add_node_features(so=so, spl=spl, graph_key='knn', features_type='so', config=config) 57 | 58 | # Test that 'y' attribute is left out 59 | with pytest.raises(KeyError): 60 | so.G[spl]['knn'].nodes[1]['y'] 61 | 62 | # Check that 'x' is in 63 | assert 'x' in so.G[spl]['knn'].nodes[1].keys(), 'Attribute not found.' 64 | 65 | # By modifying X 66 | config['attrs_params']['from_obs'] = False 67 | config['attrs_params']['from_X'] = True 68 | config['attrs_params']['X_cols'] = ['Cytokeratin5'] 69 | 70 | add_node_features(so=so, spl=spl, graph_key='knn', features_type='so', config=config) 71 | 72 | # Check that 'Cytokeratin5' is in 73 | assert 'Cytokeratin5' in so.G[spl]['knn'].nodes[1].keys(), 'Attribute not found.' 74 | 75 | # Test that random attributes are in graph. 76 | def test_random_features(so_fixture, default_params): 77 | # unpack and change parameters 78 | so, spl = so_fixture 79 | config = cp.deepcopy(default_params['random']) 80 | config['attrs_params']['n_attrs'] = 5 81 | 82 | # Add node features 83 | add_node_features(so=so, spl=spl, graph_key='knn', features_type='random', config=config) 84 | 85 | # This assertion indirectly tests whether the arrase functionality also works properly. 86 | assert len(so.G[spl]['knn'].nodes[1]) == config['attrs_params']['n_attrs'] 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /tests/graph_builder/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tests/graph_builder/__init__.py -------------------------------------------------------------------------------- /tests/graph_builder/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tests/graph_builder/integration/__init__.py -------------------------------------------------------------------------------- /tests/graph_builder/integration/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import athena as ath 3 | 4 | 5 | @pytest.fixture(scope="module") 6 | def so_fixture(): 7 | # %% 8 | # Load data 9 | so = ath.dataset.imc_sample() 10 | 11 | # Define sample 12 | spl = 'slide_7_Cy2x4' 13 | 14 | # Set right index 15 | so.obs[spl].set_index('CellId', inplace=True) 16 | 17 | # Extract centroids 18 | ath.pp.extract_centroids(so, spl, mask_key='cellmasks') 19 | 20 | return (so, spl) 21 | -------------------------------------------------------------------------------- /tests/graph_builder/integration/test_build_and_attribute.py: -------------------------------------------------------------------------------- 1 | import athena as ath 2 | from athena.utils.default_configs import get_default_config 3 | 4 | 5 | def test_build_and_attribute(so_fixture): 6 | # Unpack data 7 | so, spl = so_fixture 8 | 9 | # Build full graph with radius 10 | config = get_default_config( 11 | builder_type='radius', 12 | build_and_attribute=True, 13 | build_concept_graph=False, 14 | attrs_type='random' 15 | ) 16 | 17 | ath.graph.build_graph(so, spl, config=config, key_added='foo') 18 | 19 | assert len(so.G[spl]['foo'].nodes[1]) == config['attrs_params']['n_attrs'] 20 | -------------------------------------------------------------------------------- /tests/graph_builder/integration/test_isomorphism.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import athena as ath 3 | from athena.utils.default_configs import get_default_config 4 | 5 | 6 | # This tests weather a sub set graph is sub graph isomorphic of the full graph. 7 | def test_is_isomorphic(so_fixture): 8 | so, spl = so_fixture 9 | so.G.clear() 10 | 11 | # Build full graph with radius 12 | builder_type = 'radius' 13 | config = get_default_config( 14 | builder_type=builder_type 15 | ) 16 | config['builder_params']['radius'] = 20 # set radius 17 | ath.graph.build_graph(so, spl, config=config) 18 | 19 | # Build concept graph with radius 20 | # Decide on subset 21 | labels = ['endothelial'] 22 | filter_col = 'cell_type' 23 | 24 | # Build subset graphs 25 | # radius graph 26 | config = get_default_config( 27 | builder_type=builder_type, 28 | build_concept_graph=True 29 | ) 30 | config['builder_params']['radius'] = 20 # set radius 31 | config['concept_params']['filter_col'] = filter_col 32 | config['concept_params']['include_labels'] = labels 33 | ath.graph.build_graph(so, spl, config=config, key_added='foo') 34 | 35 | A = so.G[spl][builder_type] 36 | B = so.G[spl]['foo'] 37 | 38 | GM = nx.isomorphism.GraphMatcher(A, B) 39 | assert GM.subgraph_is_isomorphic() 40 | -------------------------------------------------------------------------------- /tests/graph_builder/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tests/graph_builder/unit/__init__.py -------------------------------------------------------------------------------- /tests/graph_builder/unit/conftest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import pytest 4 | from spatialOmics import SpatialOmics 5 | 6 | 7 | @pytest.fixture(scope="module") 8 | def so_object(): 9 | # create empty instance 10 | so = SpatialOmics() 11 | 12 | # populate a sample 13 | data = {'sample': ['a'], 14 | 'anno': [None]} 15 | so.spl = pd.DataFrame(data) 16 | 17 | # Populate so.obs 18 | data = {'cell_id': [1, 2, 3, 4, 5], 19 | 'y': [8, 13, 10, 80, 50], 20 | 'x': [8, 8, 15, 80, 60], 21 | 'cell_type': ['tumor', 'tumor', 'tumor', 'epithilial', 'stromal']} 22 | ndata = pd.DataFrame(data) 23 | ndata.set_index('cell_id', inplace=True) 24 | ndata.sort_index(axis=0, ascending=True, inplace=True) 25 | so.obs['a'] = ndata 26 | 27 | # Populate so.masks 28 | n = 100 29 | r = 2 30 | array = np.zeros((n, n)) 31 | 32 | for i in range(1, 6): 33 | y, x = so.obs['a'].loc[i][['y', 'x']] 34 | array[make_mask(y, x, r, n)] = i 35 | 36 | so.masks['a'] = {'cellmasks' : array.astype(int)} 37 | 38 | return so 39 | 40 | def make_mask(a, b, r, n): 41 | y,x = np.ogrid[-a:n-a, -b:n-b] 42 | mask = x*x + y*y <= r*r 43 | return mask -------------------------------------------------------------------------------- /tests/graph_builder/unit/test_graph_module.py: -------------------------------------------------------------------------------- 1 | from athena.graph_builder.knn_graph_builder import KNNGraphBuilder 2 | from athena.graph_builder.radius_graph_builder import RadiusGraphBuilder 3 | from athena.graph_builder.contact_graph_builder import ContactGraphBuilder 4 | from athena.utils.default_configs import get_default_config 5 | 6 | 7 | def test_knn_graph_builder(so_object): 8 | config = get_default_config(builder_type="knn") 9 | config['builder_params']['n_neighbors'] = 5 10 | builder = KNNGraphBuilder(config) 11 | g = builder(so_object, spl='a') 12 | assert len(g.nodes) == 5 13 | assert len(g.edges) == 15 14 | 15 | 16 | def test_radius_graph_builder(so_object): 17 | config = get_default_config(builder_type="radius") 18 | print(config) 19 | builder = RadiusGraphBuilder(config) 20 | g = builder(so_object, spl='a') 21 | assert len(g.nodes) == 5 22 | assert len(g.edges) == 8 23 | 24 | 25 | def test_contact_graph_builder(so_object): 26 | config = get_default_config(builder_type="contact") 27 | config['builder_params']['radius'] = 15 28 | builder = ContactGraphBuilder(config) 29 | g = builder(so_object, spl='a') 30 | assert len(g.nodes) == 5 31 | assert len(g.edges) == 8 32 | assert (1, 2) in g.edges 33 | -------------------------------------------------------------------------------- /tests/graph_builder/unit/test_subgraph_building.py: -------------------------------------------------------------------------------- 1 | from athena.utils.default_configs import get_default_config 2 | from athena.graph_builder import build_graph 3 | import pytest 4 | 5 | 6 | def test_empty_labels(so_object): 7 | # This tests whether passing the labels variable as an empty list returns and error 8 | config = get_default_config( 9 | builder_type='knn', 10 | build_concept_graph=True 11 | ) 12 | config['builder_params']['n_neighbors'] = 2 # set parameter k 13 | config['concept_params']['filter_col'] = 'cell_type' 14 | config['concept_params']['include_labels'] = [] 15 | with pytest.raises(NameError): 16 | build_graph(so_object, 17 | spl='a', 18 | config=config) 19 | 20 | 21 | def test_incongruent_labels_in_labels(so_object): 22 | # This tests whether passing labels that are not 'filter_col' returns and error 23 | config = get_default_config( 24 | builder_type='knn', 25 | build_concept_graph=True 26 | ) 27 | config['builder_params']['n_neighbors'] = 2 # set parameter k 28 | config['concept_params']['filter_col'] = 'cell_type' 29 | config['concept_params']['include_labels'] = ['tumor', 'stromal', 'not_a_type'] 30 | with pytest.raises(NameError): 31 | build_graph(so_object, 32 | spl='a', 33 | config=config) 34 | 35 | 36 | def test_filter_col_not_in_columns(so_object): 37 | # This tests whether passing an invalid 'filter_col' returns and error 38 | config = get_default_config( 39 | builder_type='knn', 40 | build_concept_graph=True 41 | ) 42 | config['builder_params']['n_neighbors'] = 2 # set parameter k 43 | config['concept_params']['filter_col'] = 'not_a_filter_col' 44 | config['concept_params']['include_labels'] = ['tumor', 'stromal'] 45 | with pytest.raises(NameError): 46 | build_graph(so_object, 47 | spl='a', 48 | config=config) 49 | 50 | 51 | def test_filter_col_or_labels_not_specified(so_object): 52 | # This tests whether filter_col was passed but not labels or the other why around 53 | config = get_default_config( 54 | builder_type='knn', 55 | build_concept_graph=True 56 | ) 57 | config['builder_params']['n_neighbors'] = 2 # set parameter k 58 | config['concept_params']['filter_col'] = 'cell_type' 59 | print(config['concept_params']) 60 | with pytest.raises(NameError): 61 | build_graph(so_object, 62 | spl='a', 63 | config=config) 64 | 65 | 66 | def test_knn(so_object): 67 | config = get_default_config( 68 | builder_type='knn', 69 | build_concept_graph=True 70 | ) 71 | config['builder_params']['n_neighbors'] = 3 72 | config['concept_params']['filter_col'] = 'cell_type' 73 | config['concept_params']['include_labels'] = ['tumor'] 74 | build_graph(so_object, 75 | spl='a', 76 | config=config, 77 | key_added='knn') 78 | assert len(so_object.G['a']['knn'].nodes) == 3 79 | assert len(so_object.G['a']['knn'].edges) == 6 80 | 81 | 82 | def test_radius(so_object): 83 | config = get_default_config( 84 | builder_type='radius', 85 | build_concept_graph=True 86 | ) 87 | config['concept_params']['filter_col'] = 'cell_type' 88 | config['concept_params']['include_labels'] = ['tumor'] 89 | build_graph(so_object, 90 | spl='a', 91 | config=config, 92 | key_added='radius') 93 | assert len(so_object.G['a']['radius'].nodes) == 3 94 | assert len(so_object.G['a']['radius'].edges) == 6 95 | 96 | 97 | def test_contact(so_object): 98 | config = get_default_config( 99 | builder_type='contact', 100 | build_concept_graph=True 101 | ) 102 | config['concept_params']['filter_col'] = 'cell_type' 103 | config['concept_params']['include_labels'] = ['tumor'] 104 | build_graph(so_object, 105 | spl='a', 106 | config=config, 107 | key_added='contact') 108 | assert len(so_object.G['a']['contact'].nodes) == 3 109 | assert len(so_object.G['a']['contact'].edges) == 5 110 | 111 | 112 | def test_name(so_object): 113 | # this testes whether the name gets assigned 114 | config = get_default_config( 115 | builder_type='contact', 116 | build_concept_graph=True 117 | ) 118 | config['concept_params']['filter_col'] = 'cell_type' 119 | config['concept_params']['include_labels'] = ['tumor'] 120 | build_graph(so_object, 121 | spl='a', 122 | config=config, 123 | key_added='foo') 124 | assert "foo" in so_object.G['a'] 125 | -------------------------------------------------------------------------------- /tests/metrics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tests/metrics/__init__.py -------------------------------------------------------------------------------- /tests/metrics/test_base_metrics.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import pytest 3 | import numpy as np 4 | from athena.metrics.heterogeneity.base_metrics import _shannon, _shannon_evenness, _simpson,\ 5 | _simpson_evenness, _gini_simpson, _richness, _hill_number, _renyi, _abundance, _quadratic_entropy 6 | from collections import Counter 7 | 8 | obs = [[1], [1,1], [1,2]] 9 | _counts = [Counter(i) for i in obs] 10 | 11 | _expected = [0, 0, 1] 12 | @pytest.mark.parametrize('input, res', [(c,e) for c,e in zip(_counts, _expected)]) 13 | def test_shannon(input, res): 14 | assert np.isclose(_shannon(input),res) 15 | 16 | _expected = [0, 0, 1] 17 | @pytest.mark.parametrize('input, res', [(c,e) for c,e in zip(_counts, _expected)]) 18 | def test_shannon_evenness(input, res): 19 | assert np.isclose(_shannon_evenness(input), res) 20 | 21 | _expected = [1, 1, 0.5] 22 | @pytest.mark.parametrize('input, res', [(c,e) for c,e in zip(_counts, _expected)]) 23 | def test_simpson(input, res): 24 | assert np.isclose(_simpson(input), res) 25 | 26 | _expected = [1, 1, 1] 27 | @pytest.mark.parametrize('input, res', [(c,e) for c,e in zip(_counts, _expected)]) 28 | def test_simpson_evenness(input, res): 29 | assert np.isclose(_simpson_evenness(input), res) 30 | 31 | _expected = [0, 0, 0.5] 32 | @pytest.mark.parametrize('input, res', [(c,e) for c,e in zip(_counts, _expected)]) 33 | def test_gini_simpson(input, res): 34 | assert np.isclose(_gini_simpson(input), res) 35 | 36 | _expected = [1, 1, 2] 37 | @pytest.mark.parametrize('input, res', [(c,e) for c,e in zip(_counts, _expected)]) 38 | def test_richness(input, res): 39 | assert np.isclose(_richness(input),res) 40 | 41 | _expected = [1, 1, 2] 42 | @pytest.mark.parametrize('input, res', [(c,e) for c,e in zip(_counts, _expected)]) 43 | def test_hill_number(input, res, q=2): 44 | assert _hill_number(input, q=q) == res 45 | 46 | _expected = [0, 0, -1/1*np.log2(0.5)] 47 | @pytest.mark.parametrize('input, res', [(c,e) for c,e in zip(_counts, _expected)]) 48 | def test_renyi(input, res, q=2): 49 | assert np.isclose(_renyi(input, q=q),res) 50 | 51 | _expected = [pd.Series([1], index=[1]), pd.Series([1], index=[1]), pd.Series([0.5,0.5], [1,2])] 52 | @pytest.mark.parametrize('input, res', [(c,e) for c,e in zip(_counts, _expected)]) 53 | def test_abundance(input, res): 54 | a = _abundance(input) 55 | assert np.all(a.eq(res)) 56 | 57 | # 1.) No difference between groups, i.e. entropy should be 0 58 | # 2.) Difference is always 1 between all pair-wise groups and all groups are equally abundant 59 | # entropy should be (1/N)**2 60 | from string import ascii_lowercase 61 | n = 10 62 | _features = [pd.DataFrame(np.ones((n,5)), index=[i for i in ascii_lowercase[:n]]), pd.DataFrame(np.diag(np.repeat(0.5,n)), index=[i for i in ascii_lowercase[:n]])] 63 | _counts = [Counter({key:1 for key in ascii_lowercase[:n]}), Counter({key:1 for key in ascii_lowercase[:n]})] 64 | _input = [(f,c) for f,c in zip(_features, _counts)] 65 | _expected = [0, 1/n ** 2 * 1 * (n ** 2 - n)] 66 | @pytest.mark.parametrize('input, res', [(c,e) for c,e in zip(_input, _expected)]) 67 | def test_quadratic_entropy(input, res): 68 | feat, counts = input 69 | return np.isclose(_quadratic_entropy(counts, feat, metric_kwargs={'p': 1}, scale=False), res) -------------------------------------------------------------------------------- /tutorials/ATHENA_Supplementary.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/ATHENA_Supplementary.pdf -------------------------------------------------------------------------------- /tutorials/documentation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![athena logo](img/athena_logo.png)\n", 8 | "\n", 9 | "# Quantification of heterogeneity\n", 10 | "\n" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "\n", 18 | "### Information-theoretic scores\n", 19 | "\n", 20 | "The quantification of the diversity in an ecosystem or a community is a long-standing problem in ecology and, not surprisingly, a vast body of scientific literature has addressed the problem. The application of the concepts developed in ecology to cancer research is straightforward and there is a direct analogy between species/cell types and ecological niches/tumor micro-environments. In general, the metrics developed in ecology try to describe the number of species and their relative abundance within a ecosystem, weighting both aspects differently depending on the metric. The mathematical foundation of these metrics is rooted in information theory.\n", 21 | "\n", 22 | "![entropic-measures.png](img/entropic-measures.png)\n", 23 | "\n", 24 | "\n", 25 | "### Spatial adaption\n", 26 | "To harness the spatial information about the tumor architecture we adjusted the computation of the diversity indices to consider the phenotype distributions of the single observations (cells). Diversity measures can be computed on a _global_ scope (top) or on a _local_ scope (bottom).\n", 27 | "\n", 28 | "The _global_ scope simply uses the phenotype distribution of the sample and is not\n", 29 | "exploiting the spatial information in the data. The _global_ scope quantifies the diversity only a sample-level.\n", 30 | "This is how traditional diversity scores in ecology work.\n", 31 | "\n", 32 | "In contrast, the _local_ scope exploits the graph representation to compute individual\n", 33 | "phenotype distributions for each single cell based on its neighborhood and enables a cell-level quantification of diversity.\n", 34 | "The resulting diversity score distribution can be aggregated / summarised to obtain a sample-level diversity score.\n", 35 | "\n", 36 | "![local-global.png](img/local-global.png)\n", 37 | "\n", 38 | "\n", 39 | "### Overview\n", 40 | "The result column indicates if the metric is computed on a global (sample) level or on a local (cell or spot) level. The input column specifies the input information used by the metrics. A metric that uses the phenotype distribution does not rely on spatial information. In contrast, metrics that require a graph input use the spatial information encoded in this data representation. Results of some methods depend on hyperparameter choices, as indicated by the last column. Every metric depends on the phenotyping process employed in the experimental setting.\n", 41 | "\n", 42 | "" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "| Metric | Result | Input | Hyperparameter |\n", 80 | "|--------------------|--------|------------------------|------------------------|\n", 81 | "| Shannon index | global | phenotype distribution | -- |\n", 82 | "| Shannon index | local | graph | graph choice |\n", 83 | "| Simpson index | global | phenotype distribution | -- |\n", 84 | "| Simpson index | local | graph | graph choice |\n", 85 | "| Renyi entropy | global | phenotype distribution | $\\alpha$ |\n", 86 | "| Renyi entropy | local | graph | $\\alpha$, graph choice |\n", 87 | "| Hill numbers | global | phenotype distribution | $q$ |\n", 88 | "| Hill numbers | local | graph | $q$, graph choice |\n", 89 | "| Quadratic Entropy | global | phenotype distribution | $D(x,y)$ |\n", 90 | "| Quadratic Entropy | local | phenotype distribution | $D(x,y)$, graph choice |\n", 91 | "| Ripley's K | global | graph | radius, graph choice |\n", 92 | "| Infiltration | global | graph | graph choice |\n", 93 | "| Classic | global | graph | graph choice |\n", 94 | "| HistoCAT | global | graph | graph choice |\n", 95 | "| Proportion | global | graph | graph choice |" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "## Infiltration\n", 103 | "The infiltration score was introduced by Keren _et al._ to measure the degree of immune cell infiltration into the tumor mass.\n", 104 | "\n", 105 | "$\\text{score}=\\frac{N_{it}}{N_{ii}}$\n", 106 | "\n", 107 | "where $N_{it}$ is the number of edges between tumor and immune cells and $N_{ii}$ the number of edges between immune cells." 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": {}, 113 | "source": [ 114 | "## Phenotype interactions\n", 115 | "\n", 116 | "Interaction strength of pairs-wise phenotypes is computed by observing the number or proportion of interactions a given phenotype has with another phenotype on average across a sample. A permutation test is used to determine whether the observed interaction strength is an enrichment or depletion.\n", 117 | "\n", 118 | "![interactions.png](img/interactions.png)\n", 119 | "" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | "The framework implements three different flavours to determine the pair-wise interaction strength between phenotypes.\n", 127 | "\n", 128 | "- classic / [histoCAT](http://www.nature.com/articles/nmeth.4391): Methods developed by the Bodenmiller lab. Estimate the pair-wise interaction strength by counting the number of edges between pair-wise phenotypes.\n", 129 | "- proportion: Flavour of the classic method that normalises the number of edges between phenotypes by the total number of edges present and thus bounds the score [0,1]. \n", 130 | "\n", 131 | "All those methods assess the direction of the interaction (attraction / avoidance) by a permutation test.\n", 132 | "This is, the phenotype labels are randomly permuted and the interaction strength recomputed.\n", 133 | "This is repeated multiple times to generate a null hypothesis against which the observed interaction strength is compared.\n", 134 | "If `prediction_type=pvalue`, we compute P-values for the interaction strength based on the two individual one-tailed permutation tests.\n", 135 | "If `prediction_type=diff` the score is simply the difference of the average interaction strength across all permutations and the observed interaction strength.\n", 136 | "\n", 137 | "In the following cell we compute the interaction strength between the `meta_id` phenotypes." 138 | ] 139 | } 140 | ], 141 | "metadata": { 142 | "kernelspec": { 143 | "display_name": "Python 3 (ipykernel)", 144 | "language": "python", 145 | "name": "python3" 146 | }, 147 | "language_info": { 148 | "codemirror_mode": { 149 | "name": "ipython", 150 | "version": 3 151 | }, 152 | "file_extension": ".py", 153 | "mimetype": "text/x-python", 154 | "name": "python", 155 | "nbconvert_exporter": "python", 156 | "pygments_lexer": "ipython3", 157 | "version": "3.8.13" 158 | }, 159 | "varInspector": { 160 | "cols": { 161 | "lenName": 16, 162 | "lenType": 16, 163 | "lenVar": 40 164 | }, 165 | "kernels_config": { 166 | "python": { 167 | "delete_cmd_postfix": "", 168 | "delete_cmd_prefix": "del ", 169 | "library": "var_list.py", 170 | "varRefreshCmd": "print(var_dic_list())" 171 | }, 172 | "r": { 173 | "delete_cmd_postfix": ") ", 174 | "delete_cmd_prefix": "rm(", 175 | "library": "var_list.r", 176 | "varRefreshCmd": "cat(var_dic_list()) " 177 | } 178 | }, 179 | "types_to_exclude": [ 180 | "module", 181 | "function", 182 | "builtin_function_or_method", 183 | "instance", 184 | "_Feature" 185 | ], 186 | "window_display": false 187 | } 188 | }, 189 | "nbformat": 4, 190 | "nbformat_minor": 4 191 | } 192 | -------------------------------------------------------------------------------- /tutorials/img/athena_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/athena_logo.png -------------------------------------------------------------------------------- /tutorials/img/bulk-sc-spatial.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/bulk-sc-spatial.jpg -------------------------------------------------------------------------------- /tutorials/img/dilation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/dilation.png -------------------------------------------------------------------------------- /tutorials/img/entropic-measures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/entropic-measures.png -------------------------------------------------------------------------------- /tutorials/img/images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/images.png -------------------------------------------------------------------------------- /tutorials/img/imc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/imc2.png -------------------------------------------------------------------------------- /tutorials/img/interactions-quant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/interactions-quant.png -------------------------------------------------------------------------------- /tutorials/img/interactions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/interactions.png -------------------------------------------------------------------------------- /tutorials/img/interoperability.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/interoperability.pdf -------------------------------------------------------------------------------- /tutorials/img/interoperability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/interoperability.png -------------------------------------------------------------------------------- /tutorials/img/local-global.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/local-global.pdf -------------------------------------------------------------------------------- /tutorials/img/local-global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/local-global.png -------------------------------------------------------------------------------- /tutorials/img/measurement.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/measurement.jpg -------------------------------------------------------------------------------- /tutorials/img/metalabels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/metalabels.png -------------------------------------------------------------------------------- /tutorials/img/metrics-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/metrics-overview.png -------------------------------------------------------------------------------- /tutorials/img/overview.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/overview.pdf -------------------------------------------------------------------------------- /tutorials/img/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/overview.png -------------------------------------------------------------------------------- /tutorials/img/overview_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/overview_old.png -------------------------------------------------------------------------------- /tutorials/img/random.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/random.gif -------------------------------------------------------------------------------- /tutorials/img/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/sample.png -------------------------------------------------------------------------------- /tutorials/img/simulation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/simulation.png -------------------------------------------------------------------------------- /tutorials/img/single-cell-analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/single-cell-analysis.png -------------------------------------------------------------------------------- /tutorials/img/single-cell-methods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/single-cell-methods.png -------------------------------------------------------------------------------- /tutorials/img/spatialHeterogeneity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/spatialHeterogeneity.png -------------------------------------------------------------------------------- /tutorials/img/spatialOmics.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/spatialOmics.pdf -------------------------------------------------------------------------------- /tutorials/img/spatialOmics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/spatialOmics.png -------------------------------------------------------------------------------- /tutorials/img/spatialOmics_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI4SCR/ATHENA/bbfc16cec21a4156f6ff1ca5d74dfe3ddba7c30c/tutorials/img/spatialOmics_old.png -------------------------------------------------------------------------------- /tutorials/overview.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![athena logo](img/athena_logo.png)\n", 8 | "# Overview\n", 9 | "\n", 10 | "ATHENA is an open-source computational framework written in Python that facilitates the visualization, processing and analysis of (spatial) heterogeneity from spatial omics data. ATHENA supports any spatially resolved dataset that contains spatial transcriptomic or proteomic measurements, including Imaging Mass Cytometry (IMC), Multiplexed Ion Beam Imaging (MIBI), multiplexed Immunohistochemisty (mIHC) or Immunofluorescence (mIF), seqFISH, MERFISH, Visium.\n", 11 | "\n", 12 | "![overview](img/overview.png)\n", 13 | "\n", 14 | "1. ATHENA accomodates raw multiplexed images from spatial omics measurements. Together with the images, segmentation masks, cell-level, feature-level and sample-level annotations can be uploaded.\n", 15 | "\n", 16 | "2. Based on the cell masks, ATHENA constructs graph representations of the data. The framework currently supports three flavors, namely radius, knn, and contact graphs.\n", 17 | "\n", 18 | "4. ATHENA incorporates a variety of methods to quantify heterogeneity, such as global and local entropic scores. Furthermore, cell type interaction strength scores or measures of spatial clustering and dispersion.\n", 19 | "\n", 20 | "5. Finally, the large collection of computed scores can be extracted and used as input in downstream machine learning models to perform tasks such as clinical data prediction, patient stratification or discovery of new (spatial) biomarkers." 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "## Main components\n", 28 | "- `SpatialOmics`, a new data structure inspired by [AnnData](https://anndata.readthedocs.io/en/latest/). \n", 29 | "- `Athena`, a module that enables the computation of various heterogeneity scores." 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "metadata": {}, 35 | "source": [ 36 | "### `SpatialOmics` Data Structure\n", 37 | "The `SpatialOmics` class is designed to accommodate storing and processing spatial omics datasets in a technology-agnostic and memory-efficient way. A `SpatialOmics` instance incorporates multiple attributes that bundle together the multiplexed raw images with the segmentation masks, cell-cell graphs, single-cell values, and sample-, feature- and cell-level annotations, as outlined in the figure below. Since ATHENA works with multiplexed images, memory complexity is a problem. `SpatialOmics` stores data in a HDF5 file and lazily loads the required images on the fly to keep the memory consumption low. The `SpatialOmics` structure is sample-centric, i.e., all samples from a spatial omics experiment are stored separately by heavily using Python dictionaries. \n", 38 | "\n", 39 | "![overview](img/spatialOmics.png)\n", 40 | "\n", 41 | "Specifically, each `SpatialOmics` instance contains the following attributes:\n", 42 | "1. `.images`: A Python dictionary (length: `#samples`) of raw multiplexed images, where each sample is mapped to a [numpy](https://numpy.org/) array of shape: `#features x image_width x image_height`.\n", 43 | "2. `.masks`: A nested Python dictionary (length: `#samples`) supporting different types of segmentation masks (e.g., cell and tissue masks), where each sample is mapped to an inner dictionary (length: `#mask_types`), and each value of the inner dictionary is a binary [numpy](https://numpy.org/) array of shape: `#image_width x image_height`.\n", 44 | "3. `.G`: A nested Python dictionary (length: `#samples`) supporting different topologies of graphs (e.g., knn, contact or radius graph), where each sample is mapped to an inner dictionary (length: `#graph_types`), and each value of the inner dictionary is a [networkx](https://networkx.org/) graph. \n", 45 | "4. `.X`: A Python dictionary of single-cell measurements (length: `#samples`), where each sample is mapped to a [pandas](https://pandas.pydata.org/) dataframe of shape: `#single_cells x #features`. The values in `.X` can either be uploaded or directly computed from `.images` and `.masks`.\n", 46 | "5. `.spl`: A [pandas](https://pandas.pydata.org/) dataframe containing sample-level annotations (e.g., patient clinical data) of shape: `#samples x #annotations`.\n", 47 | "6. `.obs`: A Python dictionary (length: `#samples`) containing single-cell-level annotations (e.g., cluster id, cell type, morphological fatures), where each sample is mapped to a [pandas](https://pandas.pydata.org/) dataframe of shape: `#single_cells x #annotations`. \n", 48 | "7. `.var`: A Python dictionary (length: `#samples`) containing feature-level annotations (e.g., name of protein/transcript), where each sample is mapped to a [pandas](https://pandas.pydata.org/) dataframe of shape: `#features x #annotations`. \n", 49 | "8. `.uns`: A Python dictionary containing unstructed data, e.g. various colormaps, experiment properties etc." 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "metadata": {}, 55 | "source": [ 56 | "### `Athena`\n", 57 | "\n", 58 | "`Athena` implements all visualization, processing and analysis steps integral to its functionalities. `Athena` consists in the following 5 submodules, each one performing different tasks as outlined below:\n", 59 | "![spatialHeterogeneity](img/spatialHeterogeneity.png)" 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "metadata": {}, 65 | "source": [ 66 | "`Athena` is tightly interwoven with `SpatialOmics` (see figure below), in the sense that the submodules of `Athena` take as input various aspects of the data as stored in `SpatialOmics` (green arrows) and, at the same time, store computed outputs back into different attributes of `SpatialOmics` (purple arrows).\n", 67 | "\n", 68 | "![interoperability](img/interoperability.png)" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "1. `.pp` works with `.images` and .`masks` and facilitates image pre-processing functions, such as extraction of cell centroids. ATHENA requires segmentation masks to be provided by the user. For ideas on how to do that, see Further Resources.\n", 76 | "2. `.pl` supports plotting all aspects of the data, including raw images, masks, graphs and visualizes different annotations as well as results of computed heterogeneity scores. The plots can be either static or interactive, by exploiting the Python image viewer [napari](https://napari.org/#).\n", 77 | "3. `.graph` construct cell-cell graphs from the cell masks using three different graph builders: `kNN` (connects each cell with its _k_ closest neighbors), `radius` (connects each cell to all other cells within a radius $r$), and `contact` (connects cells that physically \"touch\" by first enlarging each mask by dilation and then connecting it to all other masks if there is overlap). The resulting graphs are saved back in the `.G` attribute of `SpatialOmics`.\n", 78 | "4. `.metrics` uses the cell-cell graphs, the single-cell values (in `.X`) and cell annotations (in `.obs`) to compute a number of diversity scores, including sample richness (number of distinct cell subpopulations/clusters/clones) and abundance (relative proportions of species), and information theoretic scores, (namely Shannon, Simpson, quadratic, or Renyi entropy, Hill numbers), either at a global, sample level (saved in `.spl`), or at a local, single-cell-level (saved in `.obs`) that incorporates the spatial information. \n", 79 | "5. `.neigh` implements a number of neighborhood or spatial statistics methods, namely infiltration score, Ripley's $k$ and neighborhood analysis scores. Results are saved in `.spl` and `.uns`.\n", 80 | "\n", 81 | "## Further Resources\n", 82 | "\n", 83 | "- [Squidpy](https://squidpy.readthedocs.io/en/stable/)\n", 84 | "- Jackson, H. W. et al. The single-cell pathology landscape of breast cancer. Nature 578, 615–620 (2020)." 85 | ] 86 | } 87 | ], 88 | "metadata": { 89 | "kernelspec": { 90 | "display_name": "Python 3", 91 | "language": "python", 92 | "name": "python3" 93 | }, 94 | "language_info": { 95 | "codemirror_mode": { 96 | "name": "ipython", 97 | "version": 3 98 | }, 99 | "file_extension": ".py", 100 | "mimetype": "text/x-python", 101 | "name": "python", 102 | "nbconvert_exporter": "python", 103 | "pygments_lexer": "ipython3", 104 | "version": "3.8.10" 105 | }, 106 | "varInspector": { 107 | "cols": { 108 | "lenName": 16, 109 | "lenType": 16, 110 | "lenVar": 40 111 | }, 112 | "kernels_config": { 113 | "python": { 114 | "delete_cmd_postfix": "", 115 | "delete_cmd_prefix": "del ", 116 | "library": "var_list.py", 117 | "varRefreshCmd": "print(var_dic_list())" 118 | }, 119 | "r": { 120 | "delete_cmd_postfix": ") ", 121 | "delete_cmd_prefix": "rm(", 122 | "library": "var_list.r", 123 | "varRefreshCmd": "cat(var_dic_list()) " 124 | } 125 | }, 126 | "types_to_exclude": [ 127 | "module", 128 | "function", 129 | "builtin_function_or_method", 130 | "instance", 131 | "_Feature" 132 | ], 133 | "window_display": false 134 | } 135 | }, 136 | "nbformat": 4, 137 | "nbformat_minor": 4 138 | } --------------------------------------------------------------------------------