├── .bumpversion.cfg ├── .cruft.json ├── .editorconfig ├── .github ├── .codecov.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md ├── release.yml └── workflows │ ├── build.yml │ ├── deployment.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .mypy.ini ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── .scripts └── ci │ ├── download_data.py │ └── install_dependencies.sh ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── README_pypi.rst ├── docs ├── Makefile ├── _ext │ └── typed_returns.py ├── _static │ ├── css │ │ ├── custom.css │ │ ├── dataframe.css │ │ ├── nbsphinx.css │ │ └── sphinx_gallery.css │ └── img │ │ ├── figure1.png │ │ ├── squidpy_horizontal.png │ │ └── squidpy_vertical.png ├── _templates │ ├── autosummary │ │ └── class.rst │ └── breadcrumbs.html ├── api.rst ├── classes.rst ├── conf.py ├── index.rst ├── installation.rst ├── make.bat ├── references.bib ├── references.rst ├── release │ ├── notes-1.0.0.rst │ ├── notes-1.0.1.rst │ ├── notes-1.1.0.rst │ ├── notes-1.1.1.rst │ ├── notes-1.1.2.rst │ ├── notes-1.2.0.rst │ ├── notes-1.2.1.rst │ ├── notes-1.2.2.rst │ ├── notes-1.2.3.rst │ ├── notes-1.3.0.rst │ ├── notes-1.3.1.rst │ ├── notes-1.4.0.rst │ ├── notes-1.4.1.rst │ ├── notes-1.5.0.rst │ ├── notes-1.6.0.rst │ ├── notes-1.6.1.rst │ ├── notes-1.6.2.rst │ └── notes-dev.rst ├── release_notes.rst ├── spelling_wordlist.txt └── utils.py ├── pyproject.toml ├── src └── squidpy │ ├── __init__.py │ ├── _constants │ ├── __init__.py │ ├── _constants.py │ ├── _pkg_constants.py │ └── _utils.py │ ├── _docs.py │ ├── _utils.py │ ├── datasets │ ├── _10x_datasets.py │ ├── __init__.py │ ├── _dataset.py │ ├── _image.py │ └── _utils.py │ ├── gr │ ├── __init__.py │ ├── _build.py │ ├── _ligrec.py │ ├── _nhood.py │ ├── _niche.py │ ├── _ppatterns.py │ ├── _ripley.py │ ├── _sepal.py │ └── _utils.py │ ├── im │ ├── __init__.py │ ├── _container.py │ ├── _coords.py │ ├── _feature.py │ ├── _feature_mixin.py │ ├── _io.py │ ├── _process.py │ └── _segment.py │ ├── pl │ ├── __init__.py │ ├── _color_utils.py │ ├── _graph.py │ ├── _interactive │ │ ├── __init__.py │ │ ├── _controller.py │ │ ├── _model.py │ │ ├── _utils.py │ │ ├── _view.py │ │ ├── _widgets.py │ │ └── interactive.py │ ├── _ligrec.py │ ├── _spatial.py │ ├── _spatial_utils.py │ ├── _utils.py │ └── _var_by_distance.py │ ├── read │ ├── __init__.py │ ├── _read.py │ └── _utils.py │ └── tl │ ├── __init__.py │ ├── _sliding_window.py │ └── _var_by_distance.py ├── tests ├── __init__.py ├── _data │ ├── filtered_feature_bc_matrix.h5 │ ├── ligrec_no_numba.pickle │ ├── paul15_means.pickle │ ├── spatial │ │ ├── scalefactors_json.json │ │ ├── tissue_hires_image.png │ │ ├── tissue_lowres_image.png │ │ └── tissue_positions_list.csv │ ├── test_data.h5ad │ └── test_img.jpg ├── _images │ ├── ContainerShow_axis.png │ ├── ContainerShow_channel.png │ ├── ContainerShow_channelwise.png │ ├── ContainerShow_channelwise_segmentation.png │ ├── ContainerShow_imshow_kwargs.png │ ├── ContainerShow_library_id.png │ ├── ContainerShow_scale_mask_circle_crop.png │ ├── ContainerShow_segmentation.png │ ├── ContainerShow_transpose_channelwise_False_False.png │ ├── ContainerShow_transpose_channelwise_False_True.png │ ├── ContainerShow_transpose_channelwise_True_False.png │ ├── ContainerShow_transpose_channelwise_True_True.png │ ├── Graph_centrality_scores.png │ ├── Graph_centrality_scores_single.png │ ├── Graph_co_occurrence.png │ ├── Graph_co_occurrence_palette.png │ ├── Graph_interaction.png │ ├── Graph_interaction_dendro.png │ ├── Graph_nhood_enrichment.png │ ├── Graph_nhood_enrichment_ax.png │ ├── Graph_nhood_enrichment_dendro.png │ ├── Graph_ripley_f.png │ ├── Graph_ripley_f_nopalette.png │ ├── Graph_ripley_g.png │ ├── Graph_ripley_l.png │ ├── Heatmap_cbar_kwargs.png │ ├── Heatmap_cbar_vmin_vmax.png │ ├── Ligrec_alpha.png │ ├── Ligrec_alpha_none.png │ ├── Ligrec_cmap.png │ ├── Ligrec_dendrogram_both.png │ ├── Ligrec_dendrogram_clusters.png │ ├── Ligrec_dendrogram_pairs.png │ ├── Ligrec_kwargs.png │ ├── Ligrec_means_range.png │ ├── Ligrec_no_remove_empty_interactions.png │ ├── Ligrec_pvalue_threshold.png │ ├── Ligrec_remove_empty_interactions.png │ ├── Ligrec_remove_nonsig_interactions.png │ ├── Ligrec_source_clusters.png │ ├── Ligrec_swap_axes.png │ ├── Ligrec_swap_axes_dedrogram.png │ ├── Ligrec_target_clusters.png │ ├── Napari_add_image.png │ ├── Napari_blending.png │ ├── Napari_cat_cmap.png │ ├── Napari_cont_cmap.png │ ├── Napari_corner_case_-200_-200_600_800.png │ ├── Napari_corner_case_-200_-200_800_600.png │ ├── Napari_corner_case_-200_-200_800_800.png │ ├── Napari_corner_case_-200_200_600_800.png │ ├── Napari_corner_case_-200_200_800_600.png │ ├── Napari_corner_case_-200_200_800_800.png │ ├── Napari_corner_case_200_-200_600_800.png │ ├── Napari_corner_case_200_-200_800_600.png │ ├── Napari_corner_case_200_-200_800_800.png │ ├── Napari_corner_case_200_200_600_800.png │ ├── Napari_corner_case_200_200_800_600.png │ ├── Napari_corner_case_200_200_800_800.png │ ├── Napari_crop_center.png │ ├── Napari_crop_corner.png │ ├── Napari_gene_X.png │ ├── Napari_obs_categorical.png │ ├── Napari_obs_continuous.png │ ├── Napari_scalefactor.png │ ├── Napari_simple_canvas.png │ ├── Napari_symbol.png │ ├── Napari_viewer_canvas.png │ ├── SpatialStatic_palette_listed_cmap.png │ ├── SpatialStatic_spatial_scatter_axfig.png │ ├── SpatialStatic_spatial_scatter_categorical_alpha.png │ ├── SpatialStatic_spatial_scatter_crop.png │ ├── SpatialStatic_spatial_scatter_crop_graph.png │ ├── SpatialStatic_spatial_scatter_crop_noorigin.png │ ├── SpatialStatic_spatial_scatter_group.png │ ├── SpatialStatic_spatial_scatter_group_multi.png │ ├── SpatialStatic_spatial_scatter_group_outline.png │ ├── SpatialStatic_spatial_scatter_image.png │ ├── SpatialStatic_spatial_scatter_noimage.png │ ├── SpatialStatic_spatial_scatter_non_unique_colors.png │ ├── SpatialStatic_spatial_scatter_nospatial.png │ ├── SpatialStatic_spatial_scatter_novisium.png │ ├── SpatialStatic_spatial_scatter_title_single.png │ ├── SpatialStatic_spatial_segment.png │ ├── SpatialStatic_spatial_segment_crop.png │ ├── SpatialStatic_spatial_segment_group.png │ ├── var_by_distance_single_anchor_and_gene.png │ ├── var_by_distance_single_anchor_and_gene_two_categories.png │ ├── var_by_distance_single_anchor_four_genes_two_categories_two_palettes.png │ └── var_by_distance_single_anchor_one_gene_two_categories_without_scatter.png ├── conftest.py ├── datasets │ ├── __init__.py │ ├── test_dataset.py │ └── test_download_visium_dataset.py ├── graph │ ├── __init__.py │ ├── test_ligrec.py │ ├── test_nhood.py │ ├── test_niche.py │ ├── test_ppatterns.py │ ├── test_ripley.py │ ├── test_sepal.py │ ├── test_spatial_neighbors.py │ └── test_utils.py ├── image │ ├── __init__.py │ ├── test_container.py │ ├── test_features.py │ ├── test_io.py │ ├── test_processing.py │ └── test_segmentation.py ├── plotting │ ├── __init__.py │ ├── test_graph.py │ ├── test_image.py │ ├── test_interactive.py │ ├── test_spatial_static.py │ └── test_var_by_distance_plot.py ├── read │ ├── __init__.py │ └── test_visium.py └── tools │ ├── __init__.py │ ├── test_sliding_window.py │ └── test_var_by_distance.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.6.2 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+) 6 | serialize = 7 | {major}.{minor}.{patch} 8 | -------------------------------------------------------------------------------- /.cruft.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "https://github.com/scverse/cookiecutter-scverse", 3 | "commit": "87a407a65408d75a949c0b54b19fd287475a56f8", 4 | "checkout": "v0.4.0", 5 | "context": { 6 | "cookiecutter": { 7 | "project_name": "squidpy", 8 | "package_name": "squidpy", 9 | "project_description": "Spatial Single Cell Analysis in Python", 10 | "author_full_name": "Giovanni Palla", 11 | "author_email": "giovanni.palla@helmholtz-muenchen.de", 12 | "github_user": "giovp", 13 | "project_repo": "https://github.com/scverse/squidpy", 14 | "license": "BSD 3-Clause License", 15 | "_copy_without_render": [ 16 | ".github/workflows/build.yaml", 17 | ".github/workflows/test.yaml", 18 | "docs/_templates/autosummary/**.rst" 19 | ], 20 | "_render_devdocs": false, 21 | "_jinja2_env_vars": { 22 | "lstrip_blocks": true, 23 | "trim_blocks": true 24 | }, 25 | "_template": "https://github.com/scverse/cookiecutter-scverse" 26 | } 27 | }, 28 | "directory": null 29 | } 30 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [Makefile] 12 | indent_style = tab 13 | -------------------------------------------------------------------------------- /.github/.codecov.yml: -------------------------------------------------------------------------------- 1 | # Based on pydata/xarray 2 | codecov: 3 | require_ci_to_pass: false 4 | 5 | coverage: 6 | status: 7 | project: 8 | default: 9 | # Require 1% coverage, i.e., always succeed 10 | target: 1 11 | patch: false 12 | changes: false 13 | 14 | comment: 15 | layout: "diff, flags, files" 16 | behavior: once 17 | require_base: false 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug to help us improve Squidpy 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | ## Description 10 | 11 | 12 | 13 | ... 14 | 15 | ## Minimal reproducible example 16 | 17 | 18 | 19 | ```python 20 | ... 21 | ``` 22 | 23 | ## Traceback 24 | 25 | 26 | 27 |
28 | 29 | ```pytb 30 | ... 31 | ``` 32 | 33 |
34 | 35 | ## Version 36 | 37 | 38 | 39 | ... 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Request a new feature addition 4 | title: "" 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | ## Description 10 | 11 | 12 | 13 | ... 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about Squidpy 4 | title: 5 | labels: question 6 | assignees: 7 | --- 8 | 9 | 10 | 11 | ... 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **IMPORTANT: Please search among the [Pull requests](../pulls) before creating one.** 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## How has this been tested? 8 | 9 | 10 | 11 | ## Closes 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - release-ignore 5 | authors: 6 | - pre-commit-ci 7 | categories: 8 | - title: Added 9 | labels: 10 | - "release-added" 11 | - title: Changed 12 | labels: 13 | - "release-changed" 14 | - title: Deprecated 15 | labels: 16 | - "release-deprecated" 17 | - title: Removed 18 | labels: 19 | - "release-removed" 20 | - title: Fixed 21 | labels: 22 | - "release-fixed" 23 | - title: Security 24 | labels: 25 | - "release-security" 26 | - title: Other Changes 27 | labels: 28 | - "*" 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | package: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python 3.10 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.10" 22 | cache: "pip" 23 | cache-dependency-path: "**/pyproject.toml" 24 | - name: Install build dependencies 25 | run: python -m pip install --upgrade pip wheel twine build 26 | - name: Build package 27 | run: python -m build 28 | - name: Check package 29 | run: twine check --strict dist/*.whl 30 | -------------------------------------------------------------------------------- /.github/workflows/deployment.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: [v*] 7 | workflow_dispatch: 8 | inputs: 9 | reason: 10 | description: Reason for the workflow dispatch. Only "release" is valid. 11 | required: true 12 | default: release 13 | 14 | jobs: 15 | deploy: 16 | if: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.reason == 'release') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags')) }} 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Set up Python 3.10 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: "3.10" 28 | 29 | - name: Install hatch 30 | run: pip install hatch 31 | 32 | # this will fail if the last commit is not tagged 33 | - name: Build project for distribution 34 | run: hatch build 35 | 36 | - name: Publish on PyPI 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | user: __token__ 40 | password: ${{ secrets.PYPI_TOKEN }} 41 | skip_existing: true 42 | verbose: true 43 | 44 | sync-branches: 45 | if: ${{ github.event_name == 'workflow_dispatch' }} 46 | needs: deploy 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v3 50 | - name: Extract branch name 51 | id: vars 52 | run: | 53 | echo ::set-output name=branch::${GITHUB_REF#refs/*/} 54 | 55 | - name: Merge release into main 56 | uses: everlytic/branch-merge@1.1.2 57 | with: 58 | github_token: ${{ secrets.RELEASE_DISPATCH_TOKEN }} 59 | target_branch: main 60 | commit_message_template: ${{ format('[auto][ci skip] Merge branch ''{0}'' into main', steps.vars.outputs.branch) }} 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | package_and_release: 9 | runs-on: ubuntu-latest 10 | if: startsWith(github.ref, 'refs/tags/v') 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python 3.12 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.12" 17 | cache: pip 18 | - name: Install build dependencies 19 | run: python -m pip install --upgrade pip wheel twine build 20 | - name: Build package 21 | run: python -m build 22 | - name: Check package 23 | run: twine check --strict dist/*.whl 24 | - name: Install hatch 25 | run: pip install hatch 26 | - name: Build project for distribution 27 | run: hatch build 28 | - name: Publish a Python distribution to PyPI 29 | uses: pypa/gh-action-pypi-publish@release/v1 30 | with: 31 | password: ${{ secrets.PYPI_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | schedule: 5 | - cron: 00 00 * * 1 # every Monday at 00:00 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | workflow_dispatch: 11 | inputs: 12 | reason: 13 | description: Reason for the workflow dispatch. Only "release" is valid. 14 | required: true 15 | default: release 16 | 17 | jobs: 18 | test: 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | python: ["3.10", "3.11", "3.12"] 24 | os: [ubuntu-latest] 25 | include: 26 | - python: "3.12" 27 | os: macos-latest 28 | pip-flags: "--pre" 29 | name: "Python 3.12 (pre-release)" 30 | 31 | env: 32 | OS: ${{ matrix.os }} 33 | PYTHON: ${{ matrix.python }} 34 | 35 | steps: 36 | - uses: actions/checkout@v3 37 | - name: Set up Python ${{ matrix.python }} 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: ${{ matrix.python }} 41 | 42 | - name: Get pip cache dir 43 | id: pip-cache-dir 44 | run: | 45 | echo "::set-output name=dir::$(pip cache dir)" 46 | 47 | - name: Restore pip cache 48 | uses: actions/cache@v3 49 | with: 50 | path: ${{ steps.pip-cache-dir.outputs.dir }} 51 | key: pip-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('**/requirements.txt') }} 52 | restore-keys: | 53 | pip-${{ runner.os }}-${{ env.pythonLocation }}- 54 | 55 | - name: Install dependencies 56 | run: | 57 | ./.scripts/ci/install_dependencies.sh 58 | 59 | - name: Install pip dependencies 60 | run: | 61 | python -m pip install --upgrade pip 62 | pip install tox tox-gh-actions 63 | 64 | - name: Restore data cache 65 | id: data-cache 66 | uses: actions/cache@v3 67 | with: 68 | path: | 69 | ~/.cache/squidpy/*.h5ad 70 | key: data-${{ hashFiles('**/download_data.py') }} 71 | 72 | - name: Download datasets 73 | if: steps.data-cache.outputs.cache-hit != 'true' 74 | run: | 75 | tox -e download-data 76 | 77 | # caching .tox is not encouraged, but since we're private and this shaves off ~1min from the step 78 | # if any problems occur and/or once the package is public, this can be removed 79 | - name: Restore tox cache 80 | uses: actions/cache@v3 81 | with: 82 | path: .tox 83 | key: tox-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('**/requirements.txt', '**/tox.ini') }} 84 | 85 | - name: Test 86 | timeout-minutes: 60 87 | env: 88 | MPLBACKEND: agg 89 | PLATFORM: ${{ matrix.os }} 90 | DISPLAY: :42 91 | run: | 92 | tox -vv 93 | # check if this can be deprecated 94 | #- name: List figures for potential debugging 95 | # ls -alh /home/runner/work/squidpy/squidpy/tests/figures 96 | 97 | - name: Archive figures generated during testing 98 | if: always() 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: visual_test_results_${{ matrix.os }}-python${{ matrix.python }} 102 | path: /home/runner/work/squidpy/squidpy/tests/figures/* 103 | 104 | - name: Upload coverage to Codecov 105 | uses: codecov/codecov-action@v4 106 | with: 107 | name: coverage 108 | verbose: true 109 | token: ${{ secrets.CODECOV_TOKEN }} # required 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | *.pyc 9 | .DS_Store 10 | */.DS_Store 11 | .idea 12 | notebooks/data 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | data/ 58 | tests/figures/ 59 | tests/_images/*-failed-diff.png 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/build/ 80 | docs/api 81 | docs/classes 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 104 | __pypackages__/ 105 | 106 | # Celery stuff 107 | celerybeat-schedule 108 | celerybeat.pid 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | 140 | # editors 141 | .idea 142 | 143 | # data 144 | data 145 | 146 | # docs 147 | docs/source/api/*rst 148 | docs/source/classes/*rst 149 | docs/source/auto_* 150 | docs/source/gen_modules 151 | docs/source/external_tutorials 152 | 153 | # Vim .swp 154 | *.swp 155 | 156 | # vscode 157 | .vscode 158 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/notebooks"] 2 | path = docs/notebooks 3 | url = https://github.com/scverse/squidpy_notebooks 4 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | mypy_path = squidpy 3 | python_version = 3.10 4 | plugins = numpy.typing.mypy_plugin 5 | 6 | ignore_errors = False 7 | warn_redundant_casts = True 8 | warn_unused_configs = True 9 | warn_unused_ignores = True 10 | 11 | disallow_untyped_calls = False 12 | disallow_untyped_defs = True 13 | disallow_incomplete_defs = True 14 | disallow_any_generics = True 15 | 16 | strict_optional = True 17 | strict_equality = True 18 | warn_return_any = True 19 | warn_unreachable = False 20 | check_untyped_defs = True 21 | ; because of docrep 22 | allow_untyped_decorators = True 23 | no_implicit_optional = True 24 | no_implicit_reexport = True 25 | no_warn_no_return = True 26 | 27 | show_error_codes = True 28 | show_column_numbers = True 29 | error_summary = True 30 | 31 | no_namespace_packages = True 32 | 33 | [mypy-tests.*] 34 | ignore_errors = True 35 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: false 2 | default_language_version: 3 | python: python3 4 | default_stages: 5 | - pre-commit 6 | - pre-push 7 | minimum_pre_commit_version: 2.16.0 8 | repos: 9 | - repo: https://github.com/rbubley/mirrors-prettier 10 | rev: v3.5.3 11 | hooks: 12 | - id: prettier 13 | - repo: https://github.com/astral-sh/ruff-pre-commit 14 | rev: v0.11.7 15 | hooks: 16 | - id: ruff 17 | types_or: [python, pyi, jupyter] 18 | args: [--fix, --exit-non-zero-on-fix] 19 | - id: ruff-format 20 | types_or: [python, pyi, jupyter] 21 | - repo: https://github.com/pre-commit/mirrors-mypy 22 | rev: v1.15.0 23 | hooks: 24 | - id: mypy 25 | additional_dependencies: [numpy, types-requests] 26 | exclude: tests/|docs/ 27 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-24.04 5 | tools: 6 | python: "3.12" 7 | commands: 8 | - asdf plugin add uv 9 | - asdf install uv latest 10 | - asdf global uv latest 11 | - uv venv 12 | - uv pip install .[docs,pre] 13 | - .venv/bin/python -m sphinx -T -b html -d docs/_build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html 14 | 15 | sphinx: 16 | builder: html 17 | configuration: docs/conf.py 18 | fail_on_warning: false 19 | 20 | submodules: 21 | include: [docs/notebooks] 22 | recursive: true 23 | -------------------------------------------------------------------------------- /.scripts/ci/download_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import argparse 5 | from pathlib import Path 6 | from typing import Any 7 | 8 | from squidpy.datasets import visium_hne_sdata 9 | 10 | _CNT = 0 # increment this when you want to rebuild the CI cache 11 | _ROOT = Path.home() / ".cache" / "squidpy" 12 | 13 | 14 | def _print_message(func_name: str, path: Path, *, dry_run: bool = False) -> None: 15 | prefix = "[DRY RUN]" if dry_run else "" 16 | if path.is_file(): 17 | print(f"{prefix}[Loading] {func_name:>25} <- {str(path):>25}") 18 | else: 19 | print(f"{prefix}[Downloading] {func_name:>25} -> {str(path):>25}") 20 | 21 | 22 | def _maybe_download_data(func_name: str, path: Path) -> Any: 23 | import squidpy as sq 24 | 25 | try: 26 | return getattr(sq.datasets, func_name)(path=path) 27 | except Exception as e: # noqa: BLE001 28 | print(f"File {str(path):>25} seems to be corrupted: {e}. Removing and retrying") 29 | path.unlink() 30 | 31 | return getattr(sq.datasets, func_name)(path=path) 32 | 33 | 34 | def main(args: argparse.Namespace) -> None: 35 | from anndata import AnnData 36 | 37 | import squidpy as sq 38 | 39 | all_datasets = sq.datasets._dataset.__all__ + sq.datasets._image.__all__ 40 | all_extensions = ["h5ad"] * len(sq.datasets._dataset.__all__) + ["tiff"] * len(sq.datasets._image.__all__) 41 | 42 | if args.dry_run: 43 | for func_name, ext in zip(all_datasets, all_extensions): 44 | if func_name == "visium_hne_sdata": 45 | ext = "zarr" 46 | path = _ROOT / f"{func_name}.{ext}" 47 | _print_message(func_name, path, dry_run=True) 48 | return 49 | 50 | # could be parallelized, but on CI it largely does not matter (usually limited to 2 cores + bandwidth limit) 51 | for func_name, ext in zip(all_datasets, all_extensions): 52 | if func_name == "visium_hne_sdata": 53 | ext = "zarr" 54 | path = _ROOT / f"{func_name}.{ext}" 55 | 56 | _print_message(func_name, path) 57 | obj = visium_hne_sdata(_ROOT) 58 | 59 | assert path.is_dir(), f"Expected a .zarr folder at {path}" 60 | continue 61 | 62 | path = _ROOT / f"{func_name}.{ext}" 63 | _print_message(func_name, path) 64 | obj = _maybe_download_data(func_name, path) 65 | 66 | # we could do without the AnnData check as well (1 less req. in tox.ini), but it's better to be safe 67 | assert isinstance(obj, AnnData | sq.im.ImageContainer), type(obj) 68 | assert path.is_file(), path 69 | 70 | 71 | if __name__ == "__main__": 72 | parser = argparse.ArgumentParser(description="Download data used for tutorials/examples.") 73 | parser.add_argument( 74 | "--dry-run", action="store_true", help="Do not download any data, just print what would be downloaded." 75 | ) 76 | 77 | main(parser.parse_args()) 78 | -------------------------------------------------------------------------------- /.scripts/ci/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | if [[ "$OS" == "ubuntu-latest" ]]; then 6 | echo "Installing APT dependencies" 7 | 8 | sudo apt-get update -y 9 | sudo apt-get install automake -y 10 | 11 | # PyQt5 related 12 | sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 -y 13 | sudo Xvfb :42 -screen 0 1920x1080x24 -ac +extension GLX `_. 39 | 40 | .. |PyPI| image:: https://img.shields.io/pypi/v/squidpy.svg 41 | :target: https://pypi.org/project/squidpy/ 42 | :alt: PyPI 43 | 44 | .. |CI| image:: https://img.shields.io/github/actions/workflow/status/scverse/squidpy/test.yml?branch=main 45 | :target: https://github.com/scverse/squidpy/actions 46 | :alt: CI 47 | 48 | .. |Pre-commit| image:: https://results.pre-commit.ci/badge/github/scverse/squidpy/main.svg 49 | :target: https://results.pre-commit.ci/latest/github/scverse/squidpy/main 50 | :alt: pre-commit.ci status 51 | 52 | .. |Docs| image:: https://img.shields.io/readthedocs/squidpy 53 | :target: https://squidpy.readthedocs.io/en/stable/ 54 | :alt: Documentation 55 | 56 | .. |Coverage| image:: https://codecov.io/gh/scverse/squidpy/branch/main/graph/badge.svg 57 | :target: https://codecov.io/gh/scverse/squidpy 58 | :alt: Coverage 59 | 60 | .. |Downloads| image:: https://pepy.tech/badge/squidpy 61 | :target: https://pepy.tech/project/squidpy 62 | :alt: Downloads 63 | 64 | .. |Discourse| image:: https://img.shields.io/discourse/posts?color=yellow&logo=discourse&server=https%3A%2F%2Fdiscourse.scverse.org 65 | :target: https://discourse.scverse.org/ 66 | :alt: Discourse 67 | 68 | .. |Zulip| image:: https://img.shields.io/badge/zulip-join_chat-%2367b08f.svg 69 | :target: https://scverse.zulipchat.com 70 | :alt: Zulip 71 | 72 | .. |NumFOCUS| image:: https://img.shields.io/badge/powered%20by-NumFOCUS-orange.svg?style=flat&colorA=E1523D&colorB=007D8A 73 | :target: http://numfocus.org 74 | :alt: NumFOCUS 75 | 76 | .. |NumFOCUS logo| image:: https://raw.githubusercontent.com/numfocus/templates/master/images/numfocus-logo.png 77 | :target: https://numfocus.org/project/scverse 78 | :width: 200 79 | 80 | 81 | .. _Palla, Spitzer et al. (2022): https://doi.org/10.1038/s41592-021-01358-2 82 | .. _scanpy: https://scanpy.readthedocs.io/en/stable/ 83 | .. _anndata: https://anndata.readthedocs.io/en/stable/ 84 | .. _napari: https://napari.org/ 85 | .. _skimage: https://scikit-image.org/ 86 | .. _documentation: https://squidpy.readthedocs.io/en/stable/ 87 | .. _website: https://scverse.org/ 88 | .. _governance: https://scverse.org/about/roles/ 89 | .. _NumFOCUS: https://numfocus.org/ 90 | .. _donation: https://numfocus.org/donate-to-scverse/ 91 | 92 | Squidpy is part of the scverse® project (`website`_, `governance`_) and is fiscally sponsored by `NumFOCUS`_. 93 | Please consider making a tax-deductible `donation`_ to help the project pay for developer time, professional services, travel, workshops, and a variety of other needs. 94 | 95 | .. image:: https://raw.githubusercontent.com/numfocus/templates/master/images/numfocus-logo.png 96 | :width: 200 97 | :target: https://numfocus.org/project/scverse 98 | :align: center 99 | 100 | -------------------------------------------------------------------------------- /README_pypi.rst: -------------------------------------------------------------------------------- 1 | |PyPI| |Downloads| |CI| |Docs| |Coverage| |Discourse| |Zulip| 2 | 3 | Squidpy - Spatial Single Cell Analysis in Python 4 | ================================================ 5 | 6 | **Squidpy** is a tool for the analysis and visualization of spatial molecular data. 7 | It builds on top of `scanpy`_ and `anndata`_, from which it inherits modularity and scalability. 8 | It provides analysis tools that leverages the spatial coordinates of the data, as well as 9 | tissue images if available. 10 | 11 | Visit our `documentation`_ for installation, tutorials, examples and more. 12 | 13 | Manuscript 14 | ---------- 15 | Please see our manuscript `Palla, Spitzer et al. (2022)`_ in **Nature Methods** to learn more. 16 | 17 | Squidpy's key applications 18 | -------------------------- 19 | - Build and analyze the neighborhood graph from spatial coordinates. 20 | - Compute spatial statistics for cell-types and genes. 21 | - Efficiently store, analyze and visualize large tissue images, leveraging `skimage`_. 22 | - Interactively explore `anndata`_ and large tissue images in `napari`_. 23 | 24 | Installation 25 | ------------ 26 | Install Squidpy via PyPI by running:: 27 | 28 | pip install squidpy 29 | # or with napari included 30 | pip install 'squidpy[interactive]' 31 | 32 | or via Conda as:: 33 | 34 | conda install -c conda-forge squidpy 35 | 36 | Contributing to Squidpy 37 | ----------------------- 38 | We are happy about any contributions! Before you start, check out our `contributing guide `_. 39 | 40 | .. |PyPI| image:: https://img.shields.io/pypi/v/squidpy.svg 41 | :target: https://pypi.org/project/squidpy/ 42 | :alt: PyPI 43 | 44 | .. |CI| image:: https://img.shields.io/github/workflow/status/scverse/squidpy/Test/main 45 | :target: https://github.com/scverse/squidpy/actions 46 | :alt: CI 47 | 48 | .. |Pre-commit| image:: https://results.pre-commit.ci/badge/github/scverse/squidpy/main.svg 49 | :target: https://results.pre-commit.ci/latest/github/scverse/squidpy/main 50 | :alt: pre-commit.ci status 51 | 52 | .. |Docs| image:: https://img.shields.io/readthedocs/squidpy 53 | :target: https://squidpy.readthedocs.io/en/stable/ 54 | :alt: Documentation 55 | 56 | .. |Coverage| image:: https://codecov.io/gh/scverse/squidpy/branch/main/graph/badge.svg 57 | :target: https://codecov.io/gh/scverse/squidpy 58 | :alt: Coverage 59 | 60 | .. |Downloads| image:: https://pepy.tech/badge/squidpy 61 | :target: https://pepy.tech/project/squidpy 62 | :alt: Downloads 63 | 64 | .. |Discourse| image:: https://img.shields.io/discourse/posts?color=yellow&logo=discourse&server=https%3A%2F%2Fdiscourse.scverse.org 65 | :target: https://discourse.scverse.org/ 66 | :alt: Discourse 67 | 68 | .. |Zulip| image:: https://img.shields.io/badge/zulip-join_chat-%2367b08f.svg 69 | :target: https://scverse.zulipchat.com 70 | :alt: Zulip 71 | 72 | .. _Palla, Spitzer et al. (2022): https://doi.org/10.1038/s41592-021-01358-2 73 | .. _scanpy: https://scanpy.readthedocs.io/en/stable/ 74 | .. _anndata: https://anndata.readthedocs.io/en/stable/ 75 | .. _napari: https://napari.org/ 76 | .. _skimage: https://scikit-image.org/ 77 | .. _documentation: https://squidpy.readthedocs.io/en/stable/ 78 | -------------------------------------------------------------------------------- /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 ?= python -msphinx 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 clean 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | clean: 23 | @rm -rf "$(BUILDDIR)" 24 | @rm -rf "generated" 25 | -------------------------------------------------------------------------------- /docs/_ext/typed_returns.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from collections.abc import Iterable, Iterator 5 | 6 | from sphinx.application import Sphinx 7 | from sphinx.ext.napoleon import NumpyDocstring 8 | 9 | 10 | def _process_return(lines: Iterable[str]) -> Iterator[str]: 11 | for line in lines: 12 | m = re.fullmatch(r"(?P\w+)\s+:\s+(?P[\w.]+)", line) 13 | if m: 14 | # Once this is in scanpydoc, we can use the fancy hover stuff 15 | yield f"**{m['param']}** : :class:`~{m['type']}`" 16 | else: 17 | yield line 18 | 19 | 20 | def _parse_returns_section(self: NumpyDocstring, section: str) -> list[str]: 21 | lines_raw = list(_process_return(self._dedent(self._consume_to_next_section()))) 22 | lines: list[str] = self._format_block(":returns: ", lines_raw) 23 | if lines and lines[-1]: 24 | lines.append("") 25 | return lines 26 | 27 | 28 | def setup(app: Sphinx) -> None: 29 | NumpyDocstring._parse_returns_section = _parse_returns_section 30 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .small { 2 | font-size: 55%; 3 | } 4 | 5 | div.version { 6 | color: #ffd92c !important; 7 | } 8 | 9 | .wy-nav-side { 10 | background: #242335; 11 | } 12 | 13 | .wy-side-nav-search { 14 | background-color: #242335; 15 | } 16 | 17 | .wy-side-nav-search input[type="text"] { 18 | border-radius: 6px !important; 19 | } 20 | 21 | .wy-nav-content { 22 | max-width: 950px; 23 | } 24 | 25 | .wy-menu-vertical a { 26 | color: #eceef4; 27 | } 28 | 29 | .wy-menu-vertical li.current { 30 | background: #f1f5fb; 31 | } 32 | 33 | .wy-menu-vertical li.toctree-l2.current > a { 34 | background: #34377d2e; 35 | } 36 | 37 | .wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { 38 | background: #34377d4a; 39 | } 40 | 41 | .wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a { 42 | background: #34377d7d; 43 | } 44 | 45 | .wy-menu-vertical a:hover { 46 | background-color: #6b86b0; 47 | } 48 | 49 | .wy-menu-vertical li.current a:hover { 50 | background: #bdcde6a3; 51 | } 52 | 53 | a { 54 | color: #5b64b1; 55 | } 56 | 57 | .rst-content .viewcode-link { 58 | color: #7013e1d9; 59 | } 60 | 61 | .highlight { 62 | background: #f1f5fb !important; 63 | } 64 | 65 | .rst-content div[class^="highlight"] { 66 | border: 1px solid #e4eaf2; 67 | } 68 | 69 | .wy-menu-vertical p.caption { 70 | color: #ffd92c; 71 | } 72 | 73 | div.output_subarea.output_html.rendered_html.output_result { 74 | overflow: auto; 75 | } 76 | 77 | /* function/class top bar */ 78 | html.writer-html5 79 | .rst-content 80 | dl[class]:not(.option-list):not(.field-list):not(.footnote):not( 81 | .glossary 82 | ):not(.simple) 83 | > dt { 84 | color: #404040; 85 | border-top: solid 4px #7013e1d9; 86 | background: #ffd833a8; 87 | } 88 | 89 | /* class params */ 90 | html.writer-html5 91 | .rst-content 92 | dl[class]:not(.option-list):not(.field-list):not(.footnote):not( 93 | .glossary 94 | ):not(.simple) 95 | dl:not(.field-list) 96 | > dt { 97 | color: #404040; 98 | border-left: solid 4px #7013e1d9; 99 | background: #ffd8338f; 100 | } 101 | 102 | /* the other elements, but more specific - leave them be */ 103 | code.docutils.literal.notranslate > span[class="pre"] { 104 | font-weight: bold; 105 | color: #404040; 106 | } 107 | 108 | /* odd rows in API */ 109 | .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { 110 | background-color: #f6f6f3; 111 | } 112 | 113 | .rst-content div[class^="highlight"] pre { 114 | padding: 8px; 115 | } 116 | 117 | .rst-content .seealso { 118 | background: #fafae2 !important; 119 | } 120 | 121 | .rst-content .seealso .admonition-title { 122 | background: #7013e1d9 !important; 123 | } 124 | -------------------------------------------------------------------------------- /docs/_static/css/dataframe.css: -------------------------------------------------------------------------------- 1 | /* Pandas dataframe css */ 2 | /* Taken from: https://github.com/spatialaudio/nbsphinx/blob/fb3ba670fc1ba5f54d4c487573dbc1b4ecf7e9ff/src/nbsphinx.py#L587-L619 */ 3 | /* modified margin-left */ 4 | 5 | table.dataframe { 6 | border: none !important; 7 | border-collapse: collapse; 8 | border-spacing: 0; 9 | border-color: transparent; 10 | color: black; 11 | font-size: 12px; 12 | table-layout: fixed; 13 | margin-left: 0 !important; 14 | } 15 | 16 | table.dataframe thead { 17 | border-bottom: 1px solid black; 18 | vertical-align: bottom; 19 | } 20 | 21 | table.dataframe tr, 22 | table.dataframe th, 23 | table.dataframe td { 24 | text-align: right; 25 | vertical-align: middle; 26 | padding: 0.5em 0.5em; 27 | line-height: normal; 28 | white-space: normal; 29 | max-width: none; 30 | border: none; 31 | } 32 | 33 | table.dataframe th { 34 | font-weight: bold; 35 | } 36 | 37 | table.dataframe tbody tr:nth-child(odd) { 38 | background: #f5f5f5; 39 | } 40 | 41 | table.dataframe tbody tr:hover { 42 | background: rgba(66, 165, 245, 0.2); 43 | } 44 | -------------------------------------------------------------------------------- /docs/_static/css/nbsphinx.css: -------------------------------------------------------------------------------- 1 | div.nbinput.container div.prompt, 2 | div.nboutput.container div.prompt { 3 | display: none; 4 | } 5 | 6 | div.nbinput.container div.prompt > div.highlight, 7 | div.nboutput.container div.prompt > div.highlight { 8 | display: none; 9 | } 10 | 11 | div.nbinput.container div.input_area div[class*="highlight"] > pre, 12 | div.nboutput.container div.output_area div[class*="highlight"] > pre { 13 | padding: 8px !important; 14 | } 15 | 16 | div.nboutput.container div.output_area > div[class^="highlight"] { 17 | background-color: #fafae2 !important; 18 | } 19 | 20 | .rst-content .output_area img { 21 | max-width: unset; 22 | width: 100% !important; 23 | height: auto !important; 24 | } 25 | -------------------------------------------------------------------------------- /docs/_static/css/sphinx_gallery.css: -------------------------------------------------------------------------------- 1 | #graph, 2 | #image, 3 | #core-tutorials, 4 | #external-tutorials, 5 | #gallery { 6 | margin-bottom: 1em; 7 | } 8 | 9 | div.sphx-glr-download a { 10 | background-color: #ffd92c9e !important; 11 | background-image: none !important; 12 | border-radius: 2px !important; 13 | border: 1px solid #f4c200 !important; 14 | color: #404040 !important; 15 | font-weight: bold !important; 16 | padding: 0.1cm !important; 17 | text-align: center !important; 18 | } 19 | 20 | div.sphx-glr-download a[href$=".py"] { 21 | display: none !important; 22 | } 23 | 24 | div.sphx-glr-example-title div[class="highlight"] { 25 | background-color: #f5f5f5; 26 | border: none; 27 | } 28 | 29 | / * notebook output cell */ .sphx-glr-script-out .highlight pre { 30 | background: #fdffd9 !important; 31 | } 32 | 33 | p.sphx-glr-script-out { 34 | display: none !important; 35 | } 36 | 37 | div.sphx-glr-download p { 38 | margin: 0 !important; 39 | width: auto !important; 40 | } 41 | 42 | .sphx-glr-script-out { 43 | color: #404040 !important; 44 | margin: -24px 0px 0px 0px !important; 45 | } 46 | 47 | p.sphx-glr-signature { 48 | display: none !important; 49 | } 50 | 51 | div.sphx-glr-download-link-note { 52 | display: none !important; 53 | } 54 | 55 | /* this gets rid of uneven vertical padding */ 56 | div.sphx-glr-download code.download { 57 | display: block !important; 58 | } 59 | 60 | .sphx-glr-thumbcontainer { 61 | background: none !important; 62 | border: 1px solid #7013e1d9 !important; 63 | text-align: center !important; 64 | min-height: 220px !important; 65 | } 66 | 67 | .sphx-glr-thumbcontainer a.internal:hover { 68 | color: #7013e1d9 !important; 69 | } 70 | 71 | .sphx-glr-thumbcontainer .headerlink { 72 | display: none !important; 73 | } 74 | 75 | div.sphx-glr-thumbcontainer span { 76 | font-style: normal !important; 77 | } 78 | 79 | p.sphx-glr-timing { 80 | margin: 0 !important; 81 | padding-top: 24px; 82 | border-top: 1px solid #000; 83 | } 84 | 85 | .sphx-glr-thumbcontainer:hover { 86 | box-shadow: 0 0 10px #7013e1d9 !important; 87 | } 88 | 89 | /* sphinx-gallery inserts 2
after_repr_html_, ignore the 1st one */ 90 | div[class="rendered_html"] + br { 91 | display: none !important; 92 | } 93 | 94 | /* remove `Jupyter notebook: ` from `Download Jupyter notebook: `*/ 95 | div.sphx-glr-download-jupyter 96 | code.xref.download.docutils.literal.notranslate 97 | > span:nth-child(2), 98 | div.sphx-glr-download-jupyter 99 | code.xref.download.docutils.literal.notranslate 100 | > span:nth-child(3) { 101 | display: none !important; 102 | } 103 | 104 | .sphx-glr-thumbcontainer a.internal { 105 | padding: 140px 10px 0 !important; 106 | } 107 | 108 | div.binder-badge img { 109 | width: 120px; 110 | } 111 | -------------------------------------------------------------------------------- /docs/_static/img/figure1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/docs/_static/img/figure1.png -------------------------------------------------------------------------------- /docs/_static/img/squidpy_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/docs/_static/img/squidpy_horizontal.png -------------------------------------------------------------------------------- /docs/_static/img/squidpy_vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/docs/_static/img/squidpy_vertical.png -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline }} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | {% block methods %} 7 | {%- if methods %} 8 | .. rubric:: {{ _('Methods') }} 9 | 10 | .. autosummary:: 11 | :toctree: . 12 | {% for item in methods %} 13 | {%- if item not in ['__init__'] %} 14 | ~{{ name }}.{{ item }} 15 | {%- endif %} 16 | {%- endfor %} 17 | {%- for item in all_methods %} 18 | {%- if item in ['__call__'] %} 19 | ~{{ name }}.{{ item }} 20 | {%- endif %} 21 | {%- endfor %} 22 | {%- endif %} 23 | {%- endblock %} 24 | {% block attributes %} 25 | {%- if attributes %} 26 | .. rubric:: {{ _('Attributes') }} 27 | 28 | .. autosummary:: 29 | :toctree: . 30 | {% for item in attributes %} 31 | ~{{ name }}.{{ item }} 32 | {%- endfor %} 33 | {%- endif %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /docs/_templates/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {%- extends "sphinx_rtd_theme/breadcrumbs.html" %} {% block breadcrumbs_aside %} 2 | {% endblock %} 3 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | Import Squidpy as:: 4 | 5 | import squidpy as sq 6 | 7 | Graph 8 | ~~~~~ 9 | 10 | .. module:: squidpy.gr 11 | .. currentmodule:: squidpy 12 | 13 | .. autosummary:: 14 | :toctree: api 15 | 16 | gr.spatial_neighbors 17 | gr.mask_graph 18 | gr.nhood_enrichment 19 | gr.co_occurrence 20 | gr.centrality_scores 21 | gr.interaction_matrix 22 | gr.ripley 23 | gr.ligrec 24 | gr.spatial_autocorr 25 | gr.sepal 26 | 27 | Image 28 | ~~~~~ 29 | 30 | .. module:: squidpy.im 31 | .. currentmodule:: squidpy 32 | 33 | .. autosummary:: 34 | :toctree: api 35 | 36 | im.process 37 | im.segment 38 | im.calculate_image_features 39 | 40 | Plotting 41 | ~~~~~~~~ 42 | 43 | .. module:: squidpy.pl 44 | .. currentmodule:: squidpy 45 | 46 | .. autosummary:: 47 | :toctree: api 48 | 49 | pl.spatial_scatter 50 | pl.spatial_segment 51 | pl.nhood_enrichment 52 | pl.centrality_scores 53 | pl.interaction_matrix 54 | pl.ligrec 55 | pl.ripley 56 | pl.co_occurrence 57 | pl.extract 58 | pl.var_by_distance 59 | 60 | Reading 61 | ~~~~~~~ 62 | 63 | .. module:: squidpy.read 64 | .. currentmodule:: squidpy 65 | 66 | .. autosummary:: 67 | :toctree: api 68 | 69 | read.visium 70 | read.vizgen 71 | read.nanostring 72 | 73 | Tools 74 | ~~~~~~~~ 75 | 76 | .. module:: squidpy.tl 77 | .. currentmodule:: squidpy 78 | 79 | .. autosummary:: 80 | :toctree: api 81 | 82 | tl.var_by_distance 83 | 84 | Datasets 85 | ~~~~~~~~ 86 | 87 | .. module:: squidpy.datasets 88 | .. currentmodule:: squidpy 89 | 90 | .. autosummary:: 91 | :toctree: api 92 | 93 | datasets.four_i 94 | datasets.imc 95 | datasets.seqfish 96 | datasets.merfish 97 | datasets.mibitof 98 | datasets.slideseqv2 99 | datasets.sc_mouse_cortex 100 | datasets.visium 101 | datasets.visium_hne_adata 102 | datasets.visium_hne_adata_crop 103 | datasets.visium_fluo_adata 104 | datasets.visium_fluo_adata_crop 105 | datasets.visium_hne_image 106 | datasets.visium_hne_image_crop 107 | datasets.visium_fluo_image_crop 108 | -------------------------------------------------------------------------------- /docs/classes.rst: -------------------------------------------------------------------------------- 1 | Classes 2 | ======= 3 | 4 | .. currentmodule:: squidpy 5 | 6 | .. autosummary:: 7 | :toctree: classes 8 | 9 | im.ImageContainer 10 | im.SegmentationWatershed 11 | im.SegmentationCustom 12 | .. pl.Interactive 13 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | |PyPI| |Downloads| |CI| |Docs| |Coverage| |Discourse| |Zulip| 2 | 3 | Squidpy - Spatial Single Cell Analysis in Python 4 | ================================================ 5 | .. _website: https://scverse.org/ 6 | .. _governance: https://scverse.org/about/roles/ 7 | .. _NumFOCUS: https://numfocus.org/ 8 | .. _donation: https://numfocus.org/donate-to-scverse/ 9 | 10 | **Squidpy** is a tool for the analysis and visualization of spatial molecular data. 11 | It builds on top of `scanpy`_ and `anndata`_, from which it inherits modularity and scalability. 12 | It provides analysis tools that leverages the spatial coordinates of the data, as well as 13 | tissue images if available. 14 | 15 | .. image:: https://raw.githubusercontent.com/scverse/squidpy/main/docs/_static/img/figure1.png 16 | :alt: Squidpy title figure 17 | :width: 400px 18 | :align: center 19 | :target: https://doi.org/10.1038/s41592-021-01358-2 20 | 21 | .. warning:: 22 | 🚨🚨🚨 **Warning!** 🚨🚨🚨 23 | 24 | The original napari-plugin of Squidpy has been moved to `napari-spatialdata `_. 25 | 26 | All the functionalities previously available are also implemented in the new plugin, which also has many additional new features. 27 | 28 | You can find a rich set of `documentation and examples `_, and we suggest starting with this `tutorial `_. 29 | 30 | If you are new to SpatialData, we invite you to take a look at the documentation `here `_. 31 | 32 | Squidpy is part of the scverse® project (`website`_, `governance`_) and is fiscally sponsored by `NumFOCUS`_. 33 | Please consider making a tax-deductible `donation`_ to help the project pay for developer time, professional services, travel, workshops, and a variety of other needs. 34 | 35 | .. image:: https://raw.githubusercontent.com/numfocus/templates/master/images/numfocus-logo.png 36 | :width: 200 37 | :target: https://numfocus.org/project/scverse 38 | :align: center 39 | 40 | Manuscript 41 | ---------- 42 | .. _Palla, Spitzer et al. (2022): https://doi.org/10.1038/s41592-021-01358-2 43 | 44 | Please see our manuscript `Palla, Spitzer et al. (2022)`_ in **Nature Methods** to learn more. 45 | 46 | Squidpy's key applications 47 | -------------------------- 48 | - Build and analyze the neighborhood graph from spatial coordinates. 49 | - Compute spatial statistics for cell-types and genes. 50 | - Efficiently store, analyze and visualize large tissue images, leveraging `skimage`_. 51 | - Interactively explore `anndata`_ and large tissue images in `napari`_. 52 | 53 | Getting started with Squidpy 54 | ---------------------------- 55 | - Browse :doc:`notebooks/tutorials/index` and :doc:`notebooks/examples/index`. 56 | - Discuss usage on `discourse`_ and development on `github`_. 57 | 58 | Contributing to Squidpy 59 | ----------------------- 60 | We are happy about any contributions! Before you start, check out our `contributing guide`_. 61 | 62 | .. toctree:: 63 | :caption: General 64 | :maxdepth: 2 65 | :hidden: 66 | 67 | installation 68 | api 69 | classes 70 | release_notes 71 | references 72 | 73 | .. toctree:: 74 | :caption: Gallery 75 | :maxdepth: 2 76 | :hidden: 77 | 78 | notebooks/tutorials/index 79 | notebooks/examples/index 80 | 81 | .. |PyPI| image:: https://img.shields.io/pypi/v/squidpy.svg 82 | :target: https://pypi.org/project/squidpy/ 83 | :alt: PyPI 84 | 85 | .. |CI| image:: https://img.shields.io/github/actions/workflow/status/scverse/squidpy/test.yml?branch=main 86 | :target: https://github.com/scverse/squidpy/actions 87 | :alt: CI 88 | 89 | .. |Docs| image:: https://img.shields.io/readthedocs/squidpy 90 | :target: https://squidpy.readthedocs.io/en/stable/ 91 | :alt: Documentation 92 | 93 | .. |Coverage| image:: https://codecov.io/gh/scverse/squidpy/branch/main/graph/badge.svg 94 | :target: https://codecov.io/gh/scverse/squidpy 95 | :alt: Coverage 96 | 97 | .. |Downloads| image:: https://pepy.tech/badge/squidpy 98 | :target: https://pepy.tech/project/squidpy 99 | :alt: Downloads 100 | 101 | .. |Discourse| image:: https://img.shields.io/discourse/posts?color=yellow&logo=discourse&server=https%3A%2F%2Fdiscourse.scverse.org 102 | :target: https://discourse.scverse.org/ 103 | :alt: Discourse 104 | 105 | .. |Zulip| image:: https://img.shields.io/badge/zulip-join_chat-%2367b08f.svg 106 | :target: https://scverse.zulipchat.com 107 | :alt: Zulip 108 | 109 | .. _scanpy: https://scanpy.readthedocs.io/en/stable/ 110 | .. _anndata: https://anndata.readthedocs.io/en/stable/ 111 | .. _napari: https://napari.org/ 112 | .. _skimage: https://scikit-image.org/ 113 | .. _contributing guide: https://github.com/scverse/squidpy/blob/main/CONTRIBUTING.rst 114 | .. _discourse: https://discourse.scverse.org/ 115 | 116 | .. _github: https://github.com/scverse/squidpy 117 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | Squidpy requires Python version >= 3.9 to run. 4 | 5 | PyPI 6 | ---- 7 | Install Squidpy by running:: 8 | 9 | pip install squidpy 10 | 11 | Alternatively, to include all dependencies, such as the interactive image viewer :mod:`napari`, run:: 12 | 13 | pip install 'squidpy[interactive]' 14 | 15 | Conda 16 | ----- 17 | Install Squidpy via Conda as:: 18 | 19 | conda install -c conda-forge squidpy 20 | 21 | Development version 22 | ------------------- 23 | To install Squidpy from GitHub, run:: 24 | 25 | pip install git+https://github.com/scverse/squidpy@main 26 | -------------------------------------------------------------------------------- /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/references.rst: -------------------------------------------------------------------------------- 1 | References 2 | ========== 3 | 4 | .. bibliography:: 5 | :cited: 6 | -------------------------------------------------------------------------------- /docs/release/notes-1.0.0.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.0.0 (2021-02-20) 2 | ========================== 3 | 4 | Initial release. 5 | -------------------------------------------------------------------------------- /docs/release/notes-1.0.1.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.0.1 (2021-06-12) 2 | ========================== 3 | 4 | Bugfixes 5 | -------- 6 | - Fix complex handling in :func:`squidpy.gr.ligrec`. 7 | `#310 `__ 8 | `@michalk8 `__ 9 | 10 | - Various minor documentation fixes. 11 | 12 | Features 13 | -------- 14 | - Re-implementation of :func:`squidpy.gr.interaction_matrix`. 15 | `@ivirshup `__ 16 | `#302 `__ 17 | 18 | - Update installation instructions. 19 | `@michalk8 `__ 20 | `#290 `__ 21 | -------------------------------------------------------------------------------- /docs/release/notes-1.1.0.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.1.0 (2021-07-01) 2 | ========================== 3 | 4 | Bugfixes 5 | -------- 6 | - Fix :class:`squidpy.im.ImageContainer` to work with different scaled. 7 | `#320 `__ 8 | `@hspitzer `__ 9 | 10 | - Fix handling of :attr:`anndata.AnnData.obsm` in :meth:`squidpy.im.ImageContainer.interactive`. 11 | `#335 `__ 12 | `@michalk8 `__ 13 | 14 | - Fix Z-dimension in :meth:`squidpy.im.ImageContainer.interactive`. 15 | `#351 `__ 16 | `@hspitzer `__ 17 | 18 | - Fix plotting bug in :func:`squidpy.pl.ripley`. 19 | `#352 `__ 20 | `@giovp `__ 21 | 22 | - Fix handling of NaNs in :func:`squidpy.gr.ligrec`. 23 | `#362 `__ 24 | `@michalk8 `__ 25 | 26 | Features 27 | -------- 28 | - Add many new tutorials and examples. 29 | 30 | - Add :func:`squidpy.gr.sepal` :cite:`andersson2021` 31 | `#313 `__ 32 | `@giovp `__ 33 | 34 | - Replace ``squidpy.gr.moran`` with :func:`squidpy.gr.spatial_autocorr`, which implements both Moran's I and 35 | Geary's C. 36 | `#317 `__ 37 | `@giovp `__ 38 | 39 | - Add option to compute graph from Delaunay triangulation in :func:`squidpy.gr.spatial_neighbors`. 40 | `#322 `__ 41 | `@MxMstrmn `__ 42 | 43 | - Add lazy computation using :mod:`dask` for :mod:`squidpy.im`. 44 | `#324 `__ 45 | `@michalk8 `__ 46 | 47 | - Allow Z-dimension shared across all layers in :class:`squidpy.im.ImageContainer`. 48 | `#329 `__ 49 | `@hspitzer `__ 50 | 51 | - Replace ``squidpy.gr.ripley_k`` with :func:`squidpy.gr.ripley`. 52 | `#331 `__ 53 | `@giovp `__ 54 | 55 | - Generalize graph building in :func:`squidpy.gr.spatial_neighbors`. 56 | `#340 `__ 57 | `@Koncopd `__ 58 | 59 | - Add 3 new example datasets: 60 | - :func:`squidpy.datasets.merfish` 61 | - :func:`squidpy.datasets.mibitof` 62 | - :func:`squidpy.datasets.slideseqv2` 63 | `#348 `__ 64 | `@giovp `__ 65 | 66 | - Enable additional layer specification in :func:`squidpy.im.calculate_image_features`. 67 | `#354 `__ 68 | `@hspitzer `__ 69 | 70 | - Expose ``canvas_only`` in :meth:`squidpy.pl.Interactive.screenshot`. 71 | `#363 `__ 72 | `@giovp `__ 73 | 74 | - Various minor improvements to the documentation. 75 | `#356 `__ 76 | `@michalk8 `__ 77 | 78 | `#358 `__ 79 | `@michalk8 `__ 80 | 81 | `#359 `__ 82 | `@michalk8 `__ 83 | -------------------------------------------------------------------------------- /docs/release/notes-1.1.1.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.1.1 (2021-08-16) 2 | ========================== 3 | 4 | Features 5 | -------- 6 | 7 | - Allow defining cylindrical shells in :func:`squidpy.gr.spatial_neighbors` by using the ``radius`` argument. 8 | Also rename ``n_neigh``, ``n_neigh_grid`` arguments to ``n_neighs``. 9 | `#393 `__ 10 | 11 | - Allow specifying gene symbols from :attr:`anndata.AnnData.var` in :func:`squidpy.gr.ligrec`. 12 | `#395 `__ 13 | 14 | 15 | Bugfixes 16 | -------- 17 | 18 | - Fix sometimes incorrectly transposing dimensions when reading TIFF files. 19 | `#390 `__ 20 | 21 | 22 | Miscellaneous 23 | ------------- 24 | 25 | - Increase performance of Delaunay graph creation in :func:`squidpy.gr.spatial_neighbors`. 26 | `#381 `__ 27 | 28 | - Update ``mypy`` type-checking and use PEP 604 for type annotations. 29 | `#396 `__ 30 | 31 | 32 | Documentation 33 | ------------- 34 | 35 | - Enable ``towncrier`` for release notes generation. 36 | `#397 `__ 37 | -------------------------------------------------------------------------------- /docs/release/notes-1.1.2.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.1.2 (2021-09-10) 2 | ========================== 3 | 4 | Bugfixes 5 | -------- 6 | 7 | - Fix :mod:`napari` slider and :mod:`numpy` typing. 8 | `#413 `__ 9 | 10 | 11 | Miscellaneous 12 | ------------- 13 | 14 | - Update CI and fix ``_genesymols`` context manager. 15 | `#412 `__ 16 | -------------------------------------------------------------------------------- /docs/release/notes-1.2.0.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.2.0 (2022-04-19) 2 | ========================== 3 | 4 | Features 5 | -------- 6 | 7 | - Add :func:`squidpy.pl.spatial_scatter` and :func:`squidpy.pl.spatial_segment` to statically plot 8 | spatial omics data. 9 | `@giovp `__ 10 | `#437 `__ 11 | 12 | - Add :func:`squidpy.datasets.visium` to download *10x Genomics* datasets. 13 | `@dineshpalli `__ 14 | `#449 `__ 15 | 16 | - Add :func:`squidpy.read.visium`, :func:`squidpy.read.vizgen` and :func:`squidpy.read.nanostring` to 17 | read *Visium*, *Vizgen* and *Nanostring* files, respectively. 18 | `@dineshpalli `__ 19 | `#468 `__ 20 | 21 | - An optional ``ax`` keyword can now be passed to :func:`squidpy.pl.nhood_enrichment` function. The 22 | keyword can be used to create a matplotlib figure outside of squidpy and then plot on any matplotlib 23 | subplot, which could give the user greater flexibility in working with enrichment plots. 24 | `@jo-mueller `__ 25 | `#493 `__ 26 | 27 | - Add option to load remote *Zarr* store in :class:`squidpy.im.ImageContainer`. 28 | `@ilan-gold `__ 29 | `#500 `__ 30 | 31 | - Enable specifying diameter in :meth:`squidpy.im.ImageContainer.generate_spot_crops`. 32 | `@MxMstrmn `__ 33 | `#514 `__ 34 | 35 | - Add ``library_key`` in :func:`squidpy.gr.spatial_neighbors` to support building graphs across 36 | multiple slides. 37 | `@giovp `__ 38 | `#516 `__ 39 | 40 | 41 | Bugfixes 42 | -------- 43 | 44 | - Require ``numba>=0.52.0``. 45 | `@michalk8 `__ 46 | `#420 `__ 47 | 48 | - Fix source/target being ``None`` in :func:`squidpy.gr.ligrec`. 49 | `@michalk8 `__ 50 | `#434 `__ 51 | 52 | - Do not set edge with in :mod:`napari` since it caused all points to be black. 53 | `@michalk8 `__ 54 | `#488 `__ 55 | 56 | - Fix ``use_raw`` in :func:`squidpy.gr.spatial_autocorr`. 57 | `@michalk8 `__ 58 | `#506 `__ 59 | 60 | - Include check to be able to load ImageContainer that were generated from another version of squidpy. 61 | `@MxMstrmn `__ 62 | `#508 `__ 63 | 64 | - Fix a typo when saving a figure caused a strange directory name to be created. 65 | `@michalk8 `__ 66 | `#510 `__ 67 | 68 | 69 | Miscellaneous 70 | ------------- 71 | 72 | - Change imports in the topmost ``__init__.py`` for correct IDE module resolution. 73 | `@chaichontat `__ 74 | `#479 `__ 75 | 76 | - Remove various warnings. 77 | `@michalk8 `__ 78 | `#489 `__ 79 | 80 | - Fix missing authors in release notes. 81 | `@giovp `__ 82 | `#520 `__ 83 | 84 | 85 | Documentation 86 | ------------- 87 | 88 | - Add author to automatically generated news fragment. 89 | `@michalk8 `__ 90 | `#494 `__ 91 | -------------------------------------------------------------------------------- /docs/release/notes-1.2.1.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.2.1 (2022-05-14) 2 | ========================== 3 | 4 | Features 5 | -------- 6 | 7 | - Refactor :meth:`squidpy.im.ImageContainer.subset` to return a view. 8 | `@michalk8 `__ 9 | `#534 `__ 10 | 11 | 12 | Bugfixes 13 | -------- 14 | 15 | - Add discourse, zulip badges and links. 16 | `@giovp `__ 17 | `#525 `__ 18 | 19 | - Fix not correctly subsetting :class:`anndata.AnnData` when interactively visualizing it. 20 | `@michalk8 `__ 21 | `#531 `__ 22 | 23 | - Close #536. Set consistent image resolution key in :func:`squidpy.read.visium`. 24 | `@giovp `__ 25 | `#537 `__ 26 | 27 | - Fix alpha in :func:`squidpy.pl.spatial_scatter` when keys are categorical. 28 | `@michalk8 `__ 29 | `#542 `__ 30 | 31 | - :func:`squidpy.read.nanostring` reads only image file extensions. 32 | `@dineshpalli `__ 33 | `#546 `__ 34 | 35 | - Return ``cell_id`` for segmentation masks in :func:`squidpy.read.nanostring`. 36 | `@giovp `__ 37 | `#547 `__ 38 | 39 | - Add prettier pre-commit check, remove python 3.7 and add mac-os python 3.9 . 40 | `@giovp `__ 41 | `#548 `__ 42 | 43 | - Rename default branch from ``master`` to ``main``. 44 | `@giovp `__ 45 | `#549 `__ 46 | 47 | 48 | Miscellaneous 49 | ------------- 50 | 51 | - Fix news fragment generation checks. 52 | `@michalk8 `__ 53 | `#550 `__ 54 | -------------------------------------------------------------------------------- /docs/release/notes-1.2.2.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.2.2 (2022-05-17) 2 | ========================== 3 | 4 | Miscellaneous 5 | ------------- 6 | 7 | - Rename organization references and update pre-commit checks. 8 | `@giovp `__ 9 | `#551 `__ 10 | 11 | - Fix types. 12 | `@giovp `__ 13 | `#554 `__ 14 | -------------------------------------------------------------------------------- /docs/release/notes-1.2.3.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.2.3 (2022-10-18) 2 | ========================== 3 | 4 | Bugfixes 5 | -------- 6 | 7 | - Fix plotting non-unique categorical colors in :func:`squidpy.pl.spatial_scatter`. 8 | `@michalk8 `__ 9 | `#561 `__ 10 | 11 | - Fix :func:`squidpy.read.vizgen`. 12 | `@giovp `__ 13 | `#568 `__ 14 | 15 | - Convert :attr:`ListedColorMap` to :attr:`Cycler` object. 16 | `@michalk8 `__ 17 | `#580 `__ 18 | 19 | - Accomodate latest changes made in spaceranger 2.0 20 | `@stephenwilliams22 `__ 21 | `#583 `__ 22 | 23 | - Fix ligrec from pandas update. 24 | `@giovp `__ 25 | `#609 `__ 26 | 27 | 28 | Miscellaneous 29 | ------------- 30 | 31 | - Better error message for handling palette in :func:`squidpy.pl.spatial_scatter`. 32 | `@giovp `__ 33 | `#562 `__ 34 | 35 | - Update pre-commits and fix CI. 36 | `@giovp `__ 37 | `#587 `__ 38 | 39 | - Separate linting job on the CI instead as a step. Fix documentation. 40 | `@michalk8 `__ 41 | `#596 `__ 42 | 43 | - Update requirements in docs. 44 | `@michalk8 `__ 45 | `#601 `__ 46 | 47 | - Change docs theme with Furo, update release note. 48 | `@giovp `__ 49 | `#512 `__ 50 | 51 | - Fix release notes and misc. 52 | `@giovp `__ 53 | `#617 `__ 54 | 55 | 56 | Documentation 57 | ------------- 58 | 59 | - Added a squidpy tutorial for Xenium data. 60 | `@LLehner `__ 61 | `#102 `__ 62 | 63 | - New tutorial for 10x Genomics Xenium data. 64 | `@LLehner `__ 65 | `#615 `__ 66 | 67 | - Added tutorial notebook for vizgen mouse liver data. 68 | `@giovp `__ 69 | `#106 `__ 70 | -------------------------------------------------------------------------------- /docs/release/notes-1.3.0.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.3.0 (2023-06-16) 2 | ========================== 3 | 4 | Features 5 | -------- 6 | - Add :func:`squidpy.tl.var_by_distance` to calculate distances to anchor points and store results in a design matrix. 7 | - Add :func:`squidpy.pl.var_by_distance` to visualize variables such as gene expression by distance to an anchor point. 8 | `@LLehner `__ 9 | `#591 `__ 10 | 11 | 12 | Bugfixes 13 | -------- 14 | 15 | - Fix :mod:`pandas` inf :func:`squidpy.pl.ligrec`. 16 | `@michalk8 `__ 17 | `#625 `__ 18 | 19 | - Remove column assignment to improve compatibility with new cell metadata. 20 | `@cornhundred `__ 21 | `#648 `__ 22 | 23 | - Fix :func:`squidpy.pl.extract` on views. 24 | `@michalk8 `__ 25 | `#663 `__ 26 | 27 | - Set coordinates' index type to same as in :attr:`anndata.AnnData.obs` in :func:`squidpy.read.vizgen` 28 | and :func:`squidpy.read.visium`. 29 | `@michalk8 `__ 30 | `#665 `__ 31 | 32 | - Update cell metadata index conversion. 33 | `@djlee1 `__ 34 | `#679 `__ 35 | 36 | - Fix previously updated cell metadata index conversion. 37 | `@dfhannum `__ 38 | `#692 `__ 39 | 40 | 41 | Miscellaneous 42 | ------------- 43 | 44 | - Update pre-commits and unpin numba and numpy. 45 | `@giovp `__ 46 | `#643 `__ 47 | 48 | - Add :attr: option to :func:`squidpy.gr.spatial_autocorr` to select values from :attr:`anndata.AnnData.obs` or :attr:`anndata.AnnData.obsm`. 49 | `@michalk8 `__ 50 | `#664 `__ 51 | 52 | - Add :attr:`attr` option to :func:`squidpy.gr.spatial_autocorr` to select values from :attr:`anndata.AnnData.obs` 53 | or :attr:`anndata.AnnData.obsm.` 54 | `@michalk8 `__ 55 | `#672 `__ 56 | 57 | - Add :attr:`percentile` option to :func:`squidpy.gr.spatial_neighbors` to filter neighbor graph using percentile of distances threshold. 58 | `@LLehner `__ 59 | `#690 `__ 60 | 61 | - Add :class:`spatialdata.SpatialData` as possible input for graph functions. 62 | `@LLehner `__ 63 | `#701 `__ 64 | 65 | 66 | Documentation 67 | ------------- 68 | 69 | - Fix CI badges and tox. 70 | `@michalk8 `__ 71 | `#627 `__ 72 | 73 | - Changed tutorial directory structure. 74 | `@LLehner `__ 75 | `#113 `__ 76 | 77 | - Updated the quality control tutorials for Vizgen, Xenium and Nanostring. 78 | `@pakiessling `__ 79 | `#110 `__ 80 | 81 | - Improved example for :func:`squidpy.tl.var_by_distance` and :func:`squidpy.pl.var_by_distance`. 82 | `@LLehner `__ 83 | `#115 `__ 84 | -------------------------------------------------------------------------------- /docs/release/notes-1.3.1.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.3.1 (2023-10-09) 2 | ========================== 3 | 4 | Miscellaneous 5 | ------------- 6 | 7 | - Deprecated napari. 8 | `@LLehner `__ 9 | `#738 `__ 10 | 11 | - Various fixes. 12 | `@giovp `__ 13 | `#756 `__ 14 | 15 | - Add cruft.json to comply with scverse cookiecutter template. 16 | `@LLehner `__ 17 | `#756 `__ 18 | -------------------------------------------------------------------------------- /docs/release/notes-1.4.0.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.4.0 (2024-02-05) 2 | ========================== 3 | 4 | Bugfixes 5 | -------- 6 | 7 | - Fix building graph in ``knn`` and ``delaunay`` mode. 8 | `@michalk8 `__ 9 | `#792 `__ 10 | 11 | - Correct shuffling of annotations in ``sq.gr.nhood_enrichment``. 12 | `@giovp `__ 13 | `#775 `__ 14 | 15 | 16 | Miscellaneous 17 | ------------- 18 | 19 | - Fix napari installation. 20 | `@giovp `__ 21 | `#767 `__ 22 | 23 | - Made nanostring reader more flexible by adjusting loading of images. 24 | `@FrancescaDr `__ 25 | `#766 `__ 26 | 27 | - Fix ``sq.tl.var_by_distance`` method to support ``pandas 2.2.0``. 28 | `@LLehner `__ 29 | `#794 `__ 30 | -------------------------------------------------------------------------------- /docs/release/notes-1.4.1.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.4.1 (2024-02-06) 2 | ========================== 3 | 4 | Bugfixes 5 | -------- 6 | 7 | - Unpin ``scikit-image``. 8 | `@giovp `__ 9 | `#796 `__ 10 | -------------------------------------------------------------------------------- /docs/release/notes-1.5.0.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.5.0 (2024-05-27) 2 | ========================== 3 | 4 | Bugfixes 5 | -------- 6 | 7 | - Fixed the reading of 10x formatted mtx files. 8 | `@LinearParadox `__ 9 | `#803 `__ 10 | 11 | - Various fixes. 12 | `@michalk8 `__ 13 | `#798 `__ 14 | 15 | - Improved :func:`squidpy.gr.co_occurrence` calculation. 16 | `@DPLemonade `__ 17 | `#816 `__ 18 | -------------------------------------------------------------------------------- /docs/release/notes-1.6.0.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.6.0 (2024-07-23) 2 | ========================== 3 | 4 | Features 5 | -------- 6 | 7 | - Now :func:`squidpy.gr.spatial_graph` can also be used on :class:`spatialdata.SpatialData` objects. 8 | 9 | - Add :func:`squidpy.gr.mask_graph` to mask a spatial graph based on :class:`shapely.Polygon` or :class:`shapely.MultiPolygon` 10 | `@giovp `__ 11 | `#842 `__ 12 | -------------------------------------------------------------------------------- /docs/release/notes-1.6.1.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.6.1 (2024-08-23) 2 | ========================== 3 | 4 | Features 5 | -------- 6 | 7 | - Update :func:`pl.var_by_distance` to plot values from :attr:`AnnData.obs` `@LLehner `__ 8 | `#868 `__ 9 | 10 | 11 | Documentation 12 | ------------- 13 | 14 | - Add Xenium tutorial with :attr:`spatialdata` `@LLehner `__ 15 | `#125 `__ 16 | 17 | 18 | Miscellaneous 19 | ------------- 20 | 21 | - The Squidpy `interactive` module has been removed. Please use the ``napari-spatialdata`` plugin instead. You can find it `here `__ `@giovp `__ 22 | `#877 `__ 23 | - Drop support for Python 3.9 `@giovp `__ 24 | `#881 `__ -------------------------------------------------------------------------------- /docs/release/notes-1.6.2.rst: -------------------------------------------------------------------------------- 1 | Squidpy 1.6.2 (2024-11-12) 2 | ========================== 3 | 4 | Features 5 | -------- 6 | 7 | - New function :func:`squidpy.tl.sliding_window` for creating sliding window assignments 8 | `@FrancescaDr `__ 9 | `@timtreis `__ 10 | `#842 `__ 11 | 12 | Miscellaneous 13 | ------------- 14 | 15 | - Fix sparse matrix interactions in :func:`sq.gr.utils` `@alam-shahul `__ 16 | `#891 `__ 17 | - Update spatialdata dependency `@melonora `__ 18 | `#911 `__ 19 | - Update numpy and python version requirements `@giovp `__ 20 | `#903 `__ -------------------------------------------------------------------------------- /docs/release/notes-dev.rst: -------------------------------------------------------------------------------- 1 | Squidpy dev (the-future) 2 | ======================== 3 | 4 | Features 5 | -------- 6 | 7 | - Fix :attr:`tl.var_by_distance` behaviour when providing :attr:`numpy` arrays of coordinates as anchor point. 8 | - Update :attr:`pl.var_by_distance` to show multiple variables on same plot. 9 | `@LLehner `__ 10 | `#929 `__ 11 | 12 | -------------------------------------------------------------------------------- /docs/release_notes.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | +++++++++++++ 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | 7 | release/notes-dev 8 | 9 | .. toctree:: 10 | :maxdepth: 3 11 | :glob: 12 | :reversed: 13 | 14 | release/notes-*.*.* 15 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | Accomodate 2 | adata 3 | Aldh 4 | AnnData 5 | artifactual 6 | attr 7 | autocorrelation 8 | Axin 9 | backend 10 | barcode 11 | binarize 12 | boolean 13 | Bugfixes 14 | cbf 15 | cca 16 | Cellpose 17 | centralities 18 | centroid 19 | centroids 20 | coeffeicient 21 | colorbar 22 | colorectal 23 | colormap 24 | Colormap 25 | cond 26 | conda 27 | connectivities 28 | convolutional 29 | covariate 30 | covariates 31 | CP 32 | Crofton 33 | csv 34 | cytokeratin 35 | Cytometry 36 | Database 37 | dataframe 38 | dataset 39 | datasets 40 | Dbit 41 | deconvolution 42 | Delaunay 43 | dendrogram 44 | Dentate 45 | dev 46 | dimensionality 47 | Dimensionality 48 | DL 49 | dotplot 50 | erythroid 51 | frac 52 | func 53 | Furo 54 | geary 55 | Geary 56 | GFAP 57 | Glial 58 | grayscale 59 | grey 60 | gyrus 61 | heatmap 62 | hypothalamic 63 | ImageNet 64 | img 65 | immunofluorescence 66 | intercellular 67 | interpretable 68 | KNN 69 | kwargs 70 | leiden 71 | ligand 72 | ligands 73 | ligrec 74 | Linnarson 75 | lobules 76 | localizations 77 | logarithmize 78 | Logarithmize 79 | Macrophage 80 | megakaryocyte 81 | Megakaryocytes 82 | Merfish 83 | mesenchymal 84 | Mibitof 85 | modularity 86 | moran 87 | Moran's I 88 | morphologies 89 | Nanostring 90 | Napari 91 | NegPrb 92 | NEUN 93 | neutrophil 94 | Neutrophils 95 | obsm 96 | occuring 97 | omics 98 | os 99 | Otsu 100 | overlayed 101 | parallelization 102 | peri 103 | Peri 104 | pre 105 | pre-trained 106 | preoptic 107 | prepend 108 | qc 109 | Qt 110 | quantile 111 | quantiles 112 | reproducibility 113 | Rescale 114 | ResNet 115 | RGB 116 | sagittal 117 | sc 118 | scalability 119 | scalebar 120 | scalebars 121 | Scanpy 122 | scRNA 123 | seg 124 | segmentations 125 | semibold 126 | seqFISH 127 | seqV 128 | Sfrp 129 | spaceranger 130 | squidpy 131 | Squidpy 132 | StarDist 133 | stromal 134 | subsetting 135 | Tangram 136 | Tensorflow 137 | th 138 | thresholded 139 | tl 140 | tori 141 | transcriptomics 142 | uncommenting 143 | Visium 144 | vizgen 145 | Vizgen 146 | Vwf 147 | Xenium 148 | zonation 149 | Zonation 150 | zscore 151 | zulip 152 | -------------------------------------------------------------------------------- /docs/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import re 5 | from logging import info, warning 6 | from pathlib import Path 7 | from shutil import copytree, rmtree 8 | from tempfile import TemporaryDirectory 9 | from typing import Any, ForwardRef 10 | 11 | from enchant.tokenize import Filter 12 | from git import Repo 13 | from sphinx_gallery.directives import MiniGallery 14 | 15 | HERE = Path(__file__).parent 16 | 17 | 18 | def _fetch_notebooks(repo_url: str) -> None: 19 | def copy_files(repo_path: str | Path) -> None: 20 | repo_path = Path(repo_path) 21 | 22 | for dirname in ["tutorials", "auto_examples", "gen_modules"]: 23 | rmtree(dirname, ignore_errors=True) # locally re-cloning 24 | copytree(repo_path / "docs" / "source" / dirname, dirname) 25 | 26 | def fetch_remote(repo_url: str) -> None: 27 | info(f"Fetching notebooks from repo `{repo_url}`") 28 | with TemporaryDirectory() as repo_dir: 29 | ref = "main" 30 | repo = Repo.clone_from(repo_url, repo_dir, depth=1, branch=ref) 31 | repo.git.checkout(ref, force=True) 32 | 33 | copy_files(repo_dir) 34 | 35 | def fetch_local(repo_path: str | Path) -> None: 36 | info(f"Fetching notebooks from local path `{repo_path}`") 37 | repo_path = Path(repo_path) 38 | if not repo_path.is_dir(): 39 | raise OSError(f"Path `{repo_path}` is not a directory.") 40 | 41 | copy_files(repo_path) 42 | 43 | notebooks_local_path = Path( 44 | os.environ.get("SQUIDPY_NOTEBOOKS_PATH", HERE.absolute().parent.parent.parent / "squidpy_notebooks") 45 | ) 46 | try: 47 | fetch_local(notebooks_local_path) 48 | except Exception as e: # noqa: BLE001 49 | warning(f"Unable to fetch notebooks locally from `{notebooks_local_path}`, reason: `{e}`. Trying remote") 50 | download = int(os.environ.get("SQUIDPY_DOWNLOAD_NOTEBOOKS", 1)) 51 | if not download: 52 | # use possibly old files, otherwise, bunch of warnings will be shown 53 | info(f"Not fetching notebooks from remove because `SQUIDPY_DOWNLOAD_NOTEBOOKS={download}`") 54 | return 55 | 56 | fetch_remote(repo_url) 57 | 58 | 59 | class MaybeMiniGallery(MiniGallery): 60 | def run(self) -> list[str]: 61 | config = self.state.document.settings.env.config 62 | backreferences_dir = config.sphinx_gallery_conf["backreferences_dir"] 63 | obj_list = self.arguments[0].split() 64 | 65 | new_list = [] 66 | for obj in obj_list: 67 | path = os.path.join("/", backreferences_dir, f"{obj}.examples") # Sphinx treats this as the source dir 68 | 69 | if (HERE / path[1:]).exists(): 70 | new_list.append(obj) 71 | 72 | self.arguments[0] = " ".join(new_list) 73 | try: 74 | return super().run() # type: ignore[no-any-return] 75 | except UnboundLocalError: 76 | # no gallery files 77 | return [] 78 | 79 | 80 | def _get_thumbnails(root: str | Path) -> dict[str, str]: 81 | res = {} 82 | root = Path(root) 83 | thumb_path = Path(__file__).parent.parent.parent / "docs" / "source" 84 | 85 | for fname in root.glob("**/*.py"): 86 | path, name = os.path.split(str(fname)[:-3]) 87 | thumb_fname = f"sphx_glr_{name}_thumb.png" 88 | if (thumb_path / path / "images" / "thumb" / thumb_fname).is_file(): 89 | res[str(fname)[:-3]] = f"_images/{thumb_fname}" 90 | 91 | res["**"] = "_static/img/squidpy_vertical.png" 92 | 93 | return res 94 | 95 | 96 | class ModnameFilter(Filter): 97 | """Ignore module names.""" 98 | 99 | _pat = re.compile(r"squidpy\.(im|gr|pl|read|datasets|ImageContainer).+") 100 | 101 | def _skip(self, word: str) -> bool: 102 | return self._pat.match(word) is not None 103 | 104 | 105 | class SignatureFilter(Filter): 106 | """Ignore function signature artifacts.""" 107 | 108 | def _skip(self, word: str) -> bool: 109 | # TODO(michalk8): find a better way 110 | return word in ("img[", "imgs[", "img", "img_key", "func[", "func", "combine_attrs", "**kwargs", "n_iter") 111 | 112 | 113 | # allow ` | | ... | ` expression for sphinx-autodoc-typehints 114 | def _fwd_ref_init( 115 | self: ForwardRef, 116 | arg: str, 117 | is_argument: bool = True, 118 | module: Any = None, 119 | *, 120 | is_class: bool = False, 121 | ) -> None: 122 | if not isinstance(arg, str): 123 | raise TypeError(f"Forward reference must be a string -- got {arg!r}") 124 | if " | " in arg: 125 | arg = "Union[" + ", ".join(arg.split(" | ")) + "]" 126 | try: 127 | code = compile(arg, "", "eval") 128 | except SyntaxError: 129 | raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}") 130 | self.__forward_arg__ = arg 131 | self.__forward_code__ = code 132 | self.__forward_evaluated__ = False 133 | self.__forward_value__ = None 134 | self.__forward_is_argument__ = is_argument 135 | try: 136 | self.__forward_is_class__ = is_class 137 | except AttributeError: 138 | pass 139 | try: 140 | self.__forward_module__ = module 141 | except AttributeError: 142 | pass 143 | 144 | 145 | ForwardRef.__init__ = _fwd_ref_init # type: ignore[method-assign] 146 | -------------------------------------------------------------------------------- /src/squidpy/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from importlib import metadata 4 | from importlib.metadata import PackageMetadata 5 | 6 | from squidpy import datasets, gr, im, pl, read, tl 7 | 8 | try: 9 | md: PackageMetadata = metadata.metadata(__name__) 10 | __version__ = md["Version"] if "Version" in md else "" 11 | __author__ = md["Author"] if "Author" in md else "" 12 | __maintainer__ = md["Maintainer-email"] if "Maintainer-email" in md else "" 13 | except ImportError: 14 | md = None # type: ignore[assignment] 15 | 16 | del metadata, md 17 | -------------------------------------------------------------------------------- /src/squidpy/_constants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/src/squidpy/_constants/__init__.py -------------------------------------------------------------------------------- /src/squidpy/_constants/_constants.py: -------------------------------------------------------------------------------- 1 | """Constants that user deals with.""" 2 | 3 | from __future__ import annotations 4 | 5 | from enum import unique 6 | 7 | from squidpy._constants._utils import ModeEnum 8 | 9 | 10 | @unique 11 | class ImageFeature(ModeEnum): 12 | TEXTURE = "texture" # doc: This would be a docstring. 13 | SUMMARY = "summary" 14 | COLOR_HIST = "histogram" 15 | SEGMENTATION = "segmentation" 16 | CUSTOM = "custom" 17 | 18 | 19 | # _ligrec.py 20 | @unique 21 | class CorrAxis(ModeEnum): 22 | INTERACTIONS = "interactions" 23 | CLUSTERS = "clusters" 24 | 25 | 26 | @unique 27 | class ComplexPolicy(ModeEnum): 28 | MIN = "min" 29 | ALL = "all" 30 | 31 | 32 | @unique 33 | class Transform(ModeEnum): 34 | SPECTRAL = "spectral" 35 | COSINE = "cosine" 36 | NONE = None 37 | 38 | 39 | @unique 40 | class CoordType(ModeEnum): 41 | GRID = "grid" 42 | GENERIC = "generic" 43 | 44 | 45 | @unique 46 | class Processing(ModeEnum): 47 | SMOOTH = "smooth" 48 | GRAY = "gray" 49 | 50 | 51 | @unique 52 | class SegmentationBackend(ModeEnum): 53 | LOG = "log" 54 | DOG = "dog" 55 | DOH = "doh" 56 | WATERSHED = "watershed" 57 | CUSTOM = "custom" # callable function 58 | 59 | 60 | @unique 61 | class BlobModel(ModeEnum): 62 | LOG = "log" 63 | DOG = "dog" 64 | DOH = "doh" 65 | 66 | 67 | @unique 68 | class Dataset(ModeEnum): 69 | OB = "ob" 70 | SVZ = "svz" 71 | 72 | 73 | @unique 74 | class Centrality(ModeEnum): 75 | DEGREE = "degree_centrality" 76 | CLUSTERING = "average_clustering" 77 | CLOSENESS = "closeness_centrality" 78 | 79 | 80 | @unique 81 | class DendrogramAxis(ModeEnum): 82 | INTERACTING_MOLS = "interacting_molecules" 83 | INTERACTING_CLUSTERS = "interacting_clusters" 84 | BOTH = "both" 85 | 86 | 87 | @unique 88 | class Symbol(ModeEnum): 89 | DISC = "disc" 90 | SQUARE = "square" 91 | 92 | 93 | @unique 94 | class SpatialAutocorr(ModeEnum): 95 | MORAN = "moran" 96 | GEARY = "geary" 97 | 98 | 99 | @unique 100 | class InferDimensions(ModeEnum): 101 | DEFAULT = "default" 102 | CHANNELS_LAST = "channels_last" 103 | Z_LAST = "z_last" 104 | 105 | 106 | @unique 107 | class RipleyStat(ModeEnum): 108 | F = "F" 109 | G = "G" 110 | L = "L" 111 | 112 | 113 | @unique 114 | class ScatterShape(str, ModeEnum): 115 | CIRCLE = "circle" 116 | SQUARE = "square" 117 | HEX = "hex" 118 | 119 | 120 | @unique 121 | class TenxVersions(str, ModeEnum): 122 | # Version numbers as class objects of TenxVersions 123 | V1 = "1.1.0" 124 | V2 = "1.2.0" 125 | V3 = "1.3.0" 126 | 127 | 128 | @unique 129 | class NicheDefinitions(ModeEnum): 130 | NEIGHBORHOOD = "neighborhood" 131 | UTAG = "utag" 132 | CELLCHARTER = "cellcharter" 133 | SPOT = "spot" 134 | BANKSY = "banksy" 135 | -------------------------------------------------------------------------------- /src/squidpy/_constants/_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, ABCMeta 4 | from collections.abc import Callable, Mapping 5 | from enum import Enum, EnumMeta 6 | from functools import wraps 7 | from typing import Any 8 | 9 | 10 | def _pretty_raise_enum(cls: type[ModeEnum], fun: Callable[..., Any]) -> Callable[..., Any]: 11 | @wraps(fun) 12 | def wrapper(*args: Any, **kwargs: Any) -> Any: 13 | try: 14 | return fun(*args, **kwargs) 15 | except ValueError as e: 16 | _cls, value, *_ = args 17 | e.args = (cls._format(value),) 18 | raise e 19 | 20 | if not issubclass(cls, ErrorFormatterABC): 21 | raise TypeError(f"Class `{cls}` must be subtype of `ErrorFormatterABC`.") 22 | elif not len(cls.__members__): 23 | # empty enum, for class hierarchy 24 | return fun 25 | 26 | return wrapper 27 | 28 | 29 | class ErrorFormatterABC(ABC): 30 | """Mixin class that formats invalid value when constructing an enum.""" 31 | 32 | __error_format__ = "Invalid option `{0}` for `{1}`. Valid options are: `{2}`." 33 | 34 | @classmethod 35 | def _format(cls, value: Enum) -> str: 36 | return cls.__error_format__.format( 37 | value, 38 | cls.__name__, 39 | [m.value for m in cls.__members__.values()], # type: ignore[attr-defined] 40 | ) 41 | 42 | 43 | class PrettyEnum(Enum): 44 | """Enum with a pretty __str__ and __repr__.""" 45 | 46 | def __repr__(self) -> str: 47 | return str(self) 48 | 49 | def __str__(self) -> str: 50 | return str(self.value) 51 | 52 | 53 | class ABCEnumMeta(EnumMeta, ABCMeta): 54 | """Metaclass which injects.""" 55 | 56 | def __new__( # noqa: D102 57 | cls, clsname: str, bases: tuple[EnumMeta, ...], namespace: Mapping[str, Any] 58 | ) -> ABCEnumMeta: 59 | res = super().__new__(cls, clsname, bases, namespace) # type: ignore[arg-type] 60 | res.__new__ = _pretty_raise_enum(res, res.__new__) # type: ignore[method-assign,arg-type] 61 | return res 62 | 63 | 64 | # TODO(michalk8): subclass string; remove .s? 65 | class ModeEnum(ErrorFormatterABC, PrettyEnum, metaclass=ABCEnumMeta): 66 | """Enum which prints available values when invalid value has been passed.""" 67 | 68 | @property 69 | def s(self) -> str: 70 | """Return the :attr:`value` as :class:`str`.""" 71 | return str(self.value) 72 | 73 | @property 74 | def v(self) -> Any: 75 | """Alias for :attr:`value`.""" 76 | return self.value 77 | -------------------------------------------------------------------------------- /src/squidpy/datasets/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from squidpy.datasets._10x_datasets import visium, visium_hne_sdata 4 | from squidpy.datasets._dataset import * # noqa: F403 5 | from squidpy.datasets._image import * # noqa: F403 6 | 7 | __all__ = ["visium", "visium_hne_sdata"] 8 | -------------------------------------------------------------------------------- /src/squidpy/datasets/_dataset.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from copy import copy 4 | from typing import Any, Protocol, Union 5 | 6 | from anndata import AnnData 7 | 8 | from squidpy.datasets._10x_datasets import visium_hne_sdata 9 | from squidpy.datasets._utils import AMetadata, PathLike 10 | 11 | 12 | class Dataset(Protocol): 13 | def __call__(self, path: PathLike | None = ..., **kwargs: Any) -> AnnData: ... 14 | 15 | 16 | _4i = AMetadata( 17 | name="four_i", 18 | doc_header="Pre-processed subset 4i dataset from `Gut et al `__.", 19 | shape=(270876, 43), 20 | url="https://ndownloader.figshare.com/files/26254294", 21 | ) 22 | _imc = AMetadata( 23 | name="imc", 24 | doc_header="Pre-processed subset IMC dataset from `Jackson et al " 25 | "`__.", 26 | shape=(4668, 34), 27 | url="https://ndownloader.figshare.com/files/26098406", 28 | ) 29 | _seqfish = AMetadata( 30 | name="seqfish", 31 | doc_header="Pre-processed subset seqFISH dataset from `Lohoff et al " 32 | "`__.", 33 | shape=(19416, 351), 34 | url="https://ndownloader.figshare.com/files/26098403", 35 | ) 36 | _vha = AMetadata( 37 | name="visium_hne_adata", 38 | doc_header="Pre-processed `10x Genomics Visium H&E dataset " 39 | "`__.", 40 | shape=(2688, 18078), 41 | url="https://ndownloader.figshare.com/files/26098397", 42 | ) 43 | _vfa = AMetadata( 44 | name="visium_fluo_adata", 45 | doc_header="Pre-processed `10x Genomics Visium Fluorecent dataset " 46 | "`__.", 48 | shape=(2800, 16562), 49 | url="https://ndownloader.figshare.com/files/26098391", 50 | ) 51 | _vhac = AMetadata( 52 | name="visium_hne_adata_crop", 53 | doc_header="Pre-processed subset `10x Genomics Visium H&E dataset " 54 | "`__.", 55 | shape=(684, 18078), 56 | url="https://ndownloader.figshare.com/files/26098382", 57 | ) 58 | _vfac = AMetadata( 59 | name="visium_fluo_adata_crop", 60 | doc_header="Pre-processed subset `10x Genomics Visium Fluorescent dataset " 61 | "`__.", 63 | shape=(704, 16562), 64 | url="https://ndownloader.figshare.com/files/26098376", 65 | ) 66 | _smc = AMetadata( 67 | name="sc_mouse_cortex", 68 | doc_header="Pre-processed `scRNA-seq mouse cortex `__.", 69 | shape=(21697, 36826), 70 | url="https://ndownloader.figshare.com/files/26404781", 71 | ) 72 | _mibitof = AMetadata( 73 | name="mibitof", 74 | doc_header="Pre-processed MIBI-TOF dataset from `Hartmann et al `__.", 75 | shape=(3309, 36), 76 | url="https://ndownloader.figshare.com/files/28241139", 77 | ) 78 | _merfish = AMetadata( 79 | name="merfish", 80 | doc_header="Pre-processed MERFISH dataset from `Moffitt et al `__.", 81 | shape=(73655, 161), 82 | url="https://ndownloader.figshare.com/files/28169379", 83 | ) 84 | _slideseqv2 = AMetadata( 85 | name="slideseqv2", 86 | doc_header="Pre-processed SlideseqV2 dataset from `Stickles et al `__.", 87 | shape=(41786, 4000), 88 | url="https://ndownloader.figshare.com/files/28242783", 89 | ) 90 | 91 | for name, var in copy(locals()).items(): 92 | if isinstance(var, AMetadata): 93 | var._create_function(name, globals()) 94 | 95 | 96 | __all__ = [ # noqa: F822 97 | "four_i", 98 | "imc", 99 | "seqfish", 100 | "visium_hne_adata", 101 | "visium_hne_adata_crop", 102 | "visium_hne_sdata", 103 | "visium_fluo_adata", 104 | "visium_fluo_adata_crop", 105 | "sc_mouse_cortex", 106 | "mibitof", 107 | "merfish", 108 | "slideseqv2", 109 | ] 110 | -------------------------------------------------------------------------------- /src/squidpy/datasets/_image.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from copy import copy 4 | from typing import Any, Protocol, Union 5 | 6 | from squidpy.datasets._utils import ImgMetadata, PathLike 7 | from squidpy.im._container import ImageContainer 8 | 9 | 10 | class ImageDataset(Protocol): 11 | def __call__(self, path: PathLike | None = ..., **kwargs: Any) -> ImageContainer: ... 12 | 13 | 14 | _vfic = ImgMetadata( 15 | name="visium_fluo_image_crop", 16 | doc_header="Cropped Fluorescent image from `10x Genomics Visium dataset " 17 | "`__.", 19 | shape=(7272, 7272), 20 | library_id="V1_Adult_Mouse_Brain_Coronal_Section_2", 21 | url="https://ndownloader.figshare.com/files/26098364", 22 | ) 23 | _vhic = ImgMetadata( 24 | name="visium_hne_image_crop", 25 | doc_header="Cropped H&E image from `10x Genomics Visium dataset " 26 | "`__.", 27 | shape=(3527, 3527), 28 | library_id="V1_Adult_Mouse_Brain", 29 | url="https://ndownloader.figshare.com/files/26098328", 30 | ) 31 | _vhn = ImgMetadata( 32 | name="visium_hne_image", 33 | doc_header="H&E image from `10x Genomics Visium dataset " 34 | "`__.", 35 | shape=(11757, 11291), 36 | library_id="V1_Adult_Mouse_Brain", 37 | url="https://ndownloader.figshare.com/files/26098124", 38 | ) 39 | 40 | 41 | for name, var in copy(locals()).items(): 42 | if isinstance(var, ImgMetadata): 43 | var._create_function(name, glob_ns=globals()) 44 | 45 | 46 | __all__ = [ # noqa: F822 47 | "visium_fluo_image_crop", 48 | "visium_hne_image_crop", 49 | "visium_hne_image", 50 | ] 51 | -------------------------------------------------------------------------------- /src/squidpy/gr/__init__.py: -------------------------------------------------------------------------------- 1 | """The graph module.""" 2 | 3 | from __future__ import annotations 4 | 5 | from squidpy.gr._build import mask_graph, spatial_neighbors 6 | from squidpy.gr._ligrec import ligrec 7 | from squidpy.gr._nhood import centrality_scores, interaction_matrix, nhood_enrichment 8 | from squidpy.gr._niche import calculate_niche 9 | from squidpy.gr._ppatterns import co_occurrence, spatial_autocorr 10 | from squidpy.gr._ripley import ripley 11 | from squidpy.gr._sepal import sepal 12 | -------------------------------------------------------------------------------- /src/squidpy/im/__init__.py: -------------------------------------------------------------------------------- 1 | """The image module.""" 2 | 3 | from __future__ import annotations 4 | 5 | from squidpy.im._container import ImageContainer 6 | from squidpy.im._feature import calculate_image_features 7 | from squidpy.im._process import process 8 | from squidpy.im._segment import ( 9 | SegmentationCustom, 10 | SegmentationModel, 11 | SegmentationWatershed, 12 | segment, 13 | ) 14 | 15 | __all__ = [ 16 | "ImageContainer", 17 | "calculate_image_features", 18 | "process", 19 | "SegmentationCustom", 20 | "SegmentationModel", 21 | ] 22 | -------------------------------------------------------------------------------- /src/squidpy/im/_coords.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from collections.abc import Hashable 5 | from dataclasses import dataclass 6 | from typing import Any 7 | 8 | import numpy as np 9 | 10 | from squidpy._constants._pkg_constants import Key 11 | from squidpy._utils import NDArrayA 12 | from squidpy.gr._utils import _assert_non_negative 13 | 14 | 15 | def _circular_mask(arr: NDArrayA, y: int, x: int, radius: float) -> NDArrayA: 16 | Y, X = np.ogrid[: arr.shape[0], : arr.shape[1]] 17 | return np.asarray(((Y - y) ** 2 + (X - x) ** 2) <= radius**2) 18 | 19 | 20 | class TupleSerializer(ABC): # noqa: D101 21 | @abstractmethod 22 | def to_tuple(self) -> tuple[float, float, float, float]: 23 | """Return self as a :class:`tuple`.""" 24 | 25 | @classmethod 26 | def from_tuple(cls, value: tuple[float, float, float, float]) -> TupleSerializer: 27 | """Create self from a :class:`tuple`.""" 28 | return cls(*value) # type: ignore[call-arg] 29 | 30 | @property 31 | @abstractmethod 32 | def T(self) -> TupleSerializer: 33 | """Transpose self.""" # currently unused 34 | 35 | def __mul__(self, other: int | float) -> TupleSerializer: 36 | if not isinstance(other, int | float): 37 | return NotImplemented 38 | 39 | a, b, c, d = self.to_tuple() 40 | return type(self)(a * other, b * other, c * other, d * other) # type: ignore[call-arg] 41 | 42 | def __rmul__(self, other: int | float) -> TupleSerializer: 43 | return self * other 44 | 45 | 46 | @dataclass(frozen=True) 47 | class CropCoords(TupleSerializer): 48 | """Top-left and bottom right-corners of a crop.""" 49 | 50 | x0: float 51 | y0: float 52 | x1: float 53 | y1: float 54 | 55 | def __post_init__(self) -> None: 56 | if self.x0 > self.x1: 57 | raise ValueError(f"Expected `x0` <= `x1`, found `{self.x0}` > `{self.x1}`.") 58 | if self.y0 > self.y1: 59 | raise ValueError(f"Expected `y0` <= `y1`, found `{self.y0}` > `{self.y1}`.") 60 | 61 | @property 62 | def T(self) -> CropCoords: 63 | """Transpose self.""" 64 | return CropCoords(x0=self.y0, y0=self.x0, x1=self.y1, y1=self.x1) 65 | 66 | @property 67 | def dx(self) -> float: 68 | """Width.""" 69 | return self.x1 - self.x0 70 | 71 | @property 72 | def dy(self) -> float: 73 | """Height.""" 74 | return self.y1 - self.y0 75 | 76 | @property 77 | def center_x(self) -> float: 78 | """Center of height.""" 79 | return self.x0 + self.dx / 2.0 80 | 81 | @property 82 | def center_y(self) -> float: 83 | """Width of height.""" 84 | return self.x0 + self.dy / 2.0 85 | 86 | def to_image_coordinates(self, padding: CropPadding) -> CropCoords: 87 | """ 88 | Convert global image coordinates to local. 89 | 90 | Parameters 91 | ---------- 92 | padding 93 | Padding for which to adjust. 94 | 95 | Returns 96 | ------- 97 | Padding-adjusted image coordinates. 98 | """ 99 | adj = self + padding 100 | return CropCoords(x0=padding.x_pre, y0=padding.y_pre, x1=adj.dx - padding.x_post, y1=adj.dy - padding.y_post) 101 | 102 | @property 103 | def slice(self) -> tuple[slice, slice]: # noqa: A003 104 | """Return the ``(height, width)`` int slice.""" 105 | # has to convert to int, because of scaling, coords can also be floats 106 | return slice(int(self.y0), int(self.y1)), slice(int(self.x0), int(self.x1)) 107 | 108 | def to_tuple(self) -> tuple[float, float, float, float]: 109 | """Return self as a :class:`tuple`.""" 110 | return self.x0, self.y0, self.x1, self.y1 111 | 112 | def __add__(self, other: CropPadding) -> CropCoords: 113 | if not isinstance(other, CropPadding): 114 | return NotImplemented 115 | 116 | return CropCoords( 117 | x0=self.x0 - other.x_pre, y0=self.y0 - other.y_pre, x1=self.x1 + other.x_post, y1=self.y1 + other.y_post 118 | ) 119 | 120 | def __sub__(self, other: CropCoords) -> CropPadding: 121 | if not isinstance(other, CropCoords): 122 | return NotImplemented 123 | 124 | return CropPadding( 125 | x_pre=abs(self.x0 - other.x0), 126 | y_pre=abs(self.y0 - other.y0), 127 | x_post=abs(self.x1 - other.x1), 128 | y_post=abs(self.y1 - other.y1), 129 | ) 130 | 131 | 132 | @dataclass(frozen=True) 133 | class CropPadding(TupleSerializer): 134 | """Padding of a crop.""" 135 | 136 | x_pre: float 137 | x_post: float 138 | y_pre: float 139 | y_post: float 140 | 141 | def __post_init__(self) -> None: 142 | _assert_non_negative(self.x_pre, name="x_pre") 143 | _assert_non_negative(self.y_pre, name="y_pre") 144 | _assert_non_negative(self.x_post, name="x_post") 145 | _assert_non_negative(self.y_post, name="y_post") 146 | 147 | @property 148 | def T(self) -> CropPadding: 149 | """Transpose self.""" 150 | return CropPadding(x_pre=self.y_pre, y_pre=self.x_pre, x_post=self.y_post, y_post=self.x_post) 151 | 152 | def to_tuple(self) -> tuple[float, float, float, float]: 153 | """Return self as a :class:`tuple`.""" 154 | return self.x_pre, self.x_post, self.y_pre, self.y_post 155 | 156 | 157 | _NULL_COORDS = CropCoords(0, 0, 0, 0) 158 | _NULL_PADDING = CropPadding(0, 0, 0, 0) 159 | 160 | 161 | # functions for updating attributes with new scaling, CropCoords, CropPadding 162 | def _update_attrs_coords(attrs: dict[Hashable, Any], coords: CropCoords) -> dict[Hashable, Any]: 163 | old_coords = attrs.get(Key.img.coords, _NULL_COORDS) 164 | if old_coords != _NULL_COORDS: 165 | new_coords = CropCoords( 166 | x0=old_coords.x0 + coords.x0, 167 | y0=old_coords.y0 + coords.y0, 168 | x1=old_coords.x0 + coords.x1, 169 | y1=old_coords.y0 + coords.y1, 170 | ) 171 | attrs[Key.img.coords] = new_coords 172 | else: 173 | attrs[Key.img.coords] = coords 174 | return attrs 175 | 176 | 177 | def _update_attrs_scale(attrs: dict[Hashable, Any], scale: int | float) -> dict[Hashable, Any]: 178 | old_scale = attrs[Key.img.scale] 179 | attrs[Key.img.scale] = old_scale * scale 180 | attrs[Key.img.padding] = attrs[Key.img.padding] * scale 181 | attrs[Key.img.coords] = attrs[Key.img.coords] * scale 182 | return attrs 183 | -------------------------------------------------------------------------------- /src/squidpy/im/_feature.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping, Sequence 4 | from types import MappingProxyType 5 | from typing import TYPE_CHECKING, Any 6 | 7 | import pandas as pd 8 | from anndata import AnnData 9 | from scanpy import logging as logg 10 | 11 | from squidpy._constants._constants import ImageFeature 12 | from squidpy._docs import d, inject_docs 13 | from squidpy._utils import Signal, SigQueue, _get_n_cores, parallelize 14 | from squidpy.gr._utils import _save_data 15 | from squidpy.im._container import ImageContainer 16 | 17 | __all__ = ["calculate_image_features"] 18 | 19 | 20 | @d.dedent 21 | @inject_docs(f=ImageFeature) 22 | def calculate_image_features( 23 | adata: AnnData, 24 | img: ImageContainer, 25 | layer: str | None = None, 26 | library_id: str | Sequence[str] | None = None, 27 | features: str | Sequence[str] = ImageFeature.SUMMARY.s, 28 | features_kwargs: Mapping[str, Mapping[str, Any]] = MappingProxyType({}), 29 | key_added: str = "img_features", 30 | copy: bool = False, 31 | n_jobs: int | None = None, 32 | backend: str = "loky", 33 | show_progress_bar: bool = True, 34 | **kwargs: Any, 35 | ) -> pd.DataFrame | None: 36 | """ 37 | Calculate image features for all observations in ``adata``. 38 | 39 | Parameters 40 | ---------- 41 | %(adata)s 42 | %(img_container)s 43 | %(img_layer)s 44 | %(img_library_id)s 45 | features 46 | Features to be calculated. Valid options are: 47 | 48 | - `{f.TEXTURE.s!r}` - summary stats based on repeating patterns 49 | :meth:`squidpy.im.ImageContainer.features_texture`. 50 | - `{f.SUMMARY.s!r}` - summary stats of each image channel 51 | :meth:`squidpy.im.ImageContainer.features_summary`. 52 | - `{f.COLOR_HIST.s!r}` - counts in bins of image channel's histogram 53 | :meth:`squidpy.im.ImageContainer.features_histogram`. 54 | - `{f.SEGMENTATION.s!r}` - stats of a cell segmentation mask 55 | :meth:`squidpy.im.ImageContainer.features_segmentation`. 56 | - `{f.CUSTOM.s!r}` - extract features using a custom function 57 | :meth:`squidpy.im.ImageContainer.features_custom`. 58 | 59 | features_kwargs 60 | Keyword arguments for the different features that should be generated, such as 61 | ``{{ {f.TEXTURE.s!r}: {{ ... }}, ... }}``. 62 | key_added 63 | Key in :attr:`anndata.AnnData.obsm` where to store the calculated features. 64 | %(copy)s 65 | %(parallelize)s 66 | kwargs 67 | Keyword arguments for :meth:`squidpy.im.ImageContainer.generate_spot_crops`. 68 | 69 | Returns 70 | ------- 71 | If ``copy = True``, returns a :class:`pandas.DataFrame` where columns correspond to the calculated features. 72 | 73 | Otherwise, modifies the ``adata`` object with the following key: 74 | 75 | - :attr:`anndata.AnnData.uns` ``['{{key_added}}']`` - the above mentioned dataframe. 76 | 77 | Raises 78 | ------ 79 | ValueError 80 | If a feature is not known. 81 | """ 82 | layer = img._get_layer(layer) 83 | if isinstance(features, str | ImageFeature): 84 | features = [features] 85 | features = sorted({ImageFeature(f).s for f in features}) 86 | 87 | n_jobs = _get_n_cores(n_jobs) 88 | start = logg.info(f"Calculating features `{list(features)}` using `{n_jobs}` core(s)") 89 | 90 | res = parallelize( 91 | _calculate_image_features_helper, 92 | collection=adata.obs_names, 93 | extractor=pd.concat, 94 | n_jobs=n_jobs, 95 | backend=backend, 96 | show_progress_bar=show_progress_bar, 97 | )(adata, img, layer=layer, library_id=library_id, features=features, features_kwargs=features_kwargs, **kwargs) 98 | 99 | if copy: 100 | logg.info("Finish", time=start) 101 | return res 102 | 103 | _save_data(adata, attr="obsm", key=key_added, data=res, time=start) 104 | 105 | 106 | def _calculate_image_features_helper( 107 | obs_ids: Sequence[str], 108 | adata: AnnData, 109 | img: ImageContainer, 110 | layer: str, 111 | library_id: str | Sequence[str] | None, 112 | features: list[ImageFeature], 113 | features_kwargs: Mapping[str, Any], 114 | queue: SigQueue | None = None, 115 | **kwargs: Any, 116 | ) -> pd.DataFrame: 117 | features_list = [] 118 | for crop in img.generate_spot_crops( 119 | adata, obs_names=obs_ids, library_id=library_id, return_obs=False, as_array=False, **kwargs 120 | ): 121 | if TYPE_CHECKING: 122 | assert isinstance(crop, ImageContainer) 123 | # load crop in memory to enable faster processing 124 | crop = crop.compute(layer) 125 | 126 | features_dict = {} 127 | for feature in features: 128 | feature = ImageFeature(feature) 129 | feature_kwargs = features_kwargs.get(feature.s, {}) 130 | 131 | if feature == ImageFeature.TEXTURE: 132 | res = crop.features_texture(layer=layer, **feature_kwargs) 133 | elif feature == ImageFeature.COLOR_HIST: 134 | res = crop.features_histogram(layer=layer, **feature_kwargs) 135 | elif feature == ImageFeature.SUMMARY: 136 | res = crop.features_summary(layer=layer, **feature_kwargs) 137 | elif feature == ImageFeature.SEGMENTATION: 138 | res = crop.features_segmentation(intensity_layer=layer, **feature_kwargs) 139 | elif feature == ImageFeature.CUSTOM: 140 | res = crop.features_custom(layer=layer, **feature_kwargs) 141 | else: 142 | # should never get here 143 | raise NotImplementedError(f"Feature `{feature}` is not yet implemented.") 144 | 145 | features_dict.update(res) 146 | features_list.append(features_dict) 147 | 148 | if queue is not None: 149 | queue.put(Signal.UPDATE) 150 | 151 | if queue is not None: 152 | queue.put(Signal.FINISH) 153 | 154 | return pd.DataFrame(features_list, index=list(obs_ids)) 155 | -------------------------------------------------------------------------------- /src/squidpy/im/_process.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Callable, Mapping, Sequence 4 | from types import MappingProxyType 5 | from typing import Any 6 | 7 | import dask.array as da 8 | from dask_image.ndfilters import gaussian_filter as dask_gf 9 | from scanpy import logging as logg 10 | from scipy.ndimage import gaussian_filter as scipy_gf 11 | 12 | from squidpy._constants._constants import Processing 13 | from squidpy._constants._pkg_constants import Key 14 | from squidpy._docs import d, inject_docs 15 | from squidpy._utils import NDArrayA 16 | from squidpy.im._container import ImageContainer 17 | 18 | __all__ = ["process"] 19 | 20 | 21 | @d.dedent 22 | @inject_docs(p=Processing) 23 | def process( 24 | img: ImageContainer, 25 | layer: str | None = None, 26 | library_id: str | Sequence[str] | None = None, 27 | method: str | Callable[..., NDArrayA] = "smooth", 28 | chunks: int | None = None, 29 | lazy: bool = False, 30 | layer_added: str | None = None, 31 | channel_dim: str | None = None, 32 | copy: bool = False, 33 | apply_kwargs: Mapping[str, Any] = MappingProxyType({}), 34 | **kwargs: Any, 35 | ) -> ImageContainer | None: 36 | """ 37 | Process an image by applying a transformation. 38 | 39 | Parameters 40 | ---------- 41 | %(img_container)s 42 | %(img_layer)s 43 | %(library_id)s 44 | If `None`, all Z-dimensions are processed at once, treating the image as a 3D volume. 45 | method 46 | Processing method to use. Valid options are: 47 | 48 | - `{p.SMOOTH.s!r}` - :func:`skimage.filters.gaussian`. 49 | - `{p.GRAY.s!r}` - :func:`skimage.color.rgb2gray`. 50 | 51 | %(custom_fn)s 52 | %(chunks_lazy)s 53 | %(layer_added)s 54 | If `None`, use ``'{{layer}}_{{method}}'``. 55 | channel_dim 56 | Name of the channel dimension of the new image layer. Default is the same as the original, if the 57 | processing function does not change the number of channels, and ``'{{channel}}_{{processing}}'`` otherwise. 58 | %(copy_cont)s 59 | apply_kwargs 60 | Keyword arguments for :meth:`squidpy.im.ImageContainer.apply`. 61 | kwargs 62 | Keyword arguments for ``method``. 63 | 64 | Returns 65 | ------- 66 | If ``copy = True``, returns a new container with the processed image in ``'{{layer_added}}'``. 67 | 68 | Otherwise, modifies the ``img`` with the following key: 69 | 70 | - :class:`squidpy.im.ImageContainer` ``['{{layer_added}}']`` - the processed image. 71 | 72 | Raises 73 | ------ 74 | NotImplementedError 75 | If ``method`` has not been implemented. 76 | """ 77 | from squidpy.pl._utils import _to_grayscale 78 | 79 | layer = img._get_layer(layer) 80 | method = Processing(method) if isinstance(method, str | Processing) else method # type: ignore[assignment] 81 | apply_kwargs = dict(apply_kwargs) 82 | apply_kwargs["lazy"] = lazy 83 | 84 | if channel_dim is None: 85 | channel_dim = str(img[layer].dims[-1]) 86 | layer_new = Key.img.process(method, layer, layer_added=layer_added) 87 | 88 | if callable(method): 89 | callback = method 90 | elif method == Processing.SMOOTH: # type: ignore[comparison-overlap] 91 | if library_id is None: 92 | expected_ndim = 4 93 | kwargs.setdefault("sigma", [1, 1, 0, 0]) # y, x, z, c 94 | else: 95 | expected_ndim = 3 96 | kwargs.setdefault("sigma", [1, 1, 0]) # y, x, c 97 | 98 | sigma = kwargs["sigma"] 99 | if isinstance(sigma, int): 100 | kwargs["sigma"] = sigma = [sigma, sigma] + [0] * (expected_ndim - 2) 101 | if len(sigma) != expected_ndim: 102 | raise ValueError(f"Expected `sigma` to be of length `{expected_ndim}`, found `{len(sigma)}`.") 103 | 104 | if chunks is not None: 105 | # dask_image already handles map_overlap 106 | chunks_, chunks = chunks, None 107 | callback = lambda arr, **kwargs: dask_gf(da.asarray(arr).rechunk(chunks_), **kwargs) # noqa: E731 108 | else: 109 | callback = scipy_gf 110 | elif method == Processing.GRAY: # type: ignore[comparison-overlap] 111 | apply_kwargs["drop_axis"] = 3 112 | callback = _to_grayscale 113 | else: 114 | raise NotImplementedError(f"Method `{method}` is not yet implemented.") 115 | 116 | # to which library_ids should this function be applied? 117 | if library_id is not None: 118 | callback = dict.fromkeys(img._get_library_ids(library_id), callback) # type: ignore[assignment] 119 | 120 | start = logg.info(f"Processing image using `{method}` method") 121 | res: ImageContainer = img.apply( 122 | callback, 123 | layer=layer, 124 | copy=True, 125 | drop=copy, 126 | chunks=chunks, 127 | fn_kwargs=kwargs, 128 | **apply_kwargs, 129 | ) 130 | 131 | # if the method changes the number of channels 132 | if res[layer].shape[-1] != img[layer].shape[-1]: 133 | modifier = "_".join(layer_new.split("_")[1:]) if layer_added is None else layer_added 134 | channel_dim = f"{channel_dim}_{modifier}" 135 | 136 | res._data = res.data.rename({res[layer].dims[-1]: channel_dim}) 137 | logg.info("Finish", time=start) 138 | 139 | if copy: 140 | return res.rename(layer, layer_new) 141 | 142 | img.add_img( 143 | img=res[layer], 144 | layer=layer_new, 145 | copy=False, 146 | lazy=lazy, 147 | dims=res[layer].dims, 148 | library_id=img[layer].coords["z"].values, 149 | ) 150 | -------------------------------------------------------------------------------- /src/squidpy/pl/__init__.py: -------------------------------------------------------------------------------- 1 | """The plotting module.""" 2 | 3 | from __future__ import annotations 4 | 5 | from squidpy.pl._graph import ( 6 | centrality_scores, 7 | co_occurrence, 8 | interaction_matrix, 9 | nhood_enrichment, 10 | ripley, 11 | ) 12 | 13 | # from squidpy.pl._interactive import Interactive # type: ignore[attr-defined] # deprecated 14 | from squidpy.pl._ligrec import ligrec 15 | from squidpy.pl._spatial import spatial_scatter, spatial_segment 16 | from squidpy.pl._utils import extract 17 | from squidpy.pl._var_by_distance import var_by_distance 18 | -------------------------------------------------------------------------------- /src/squidpy/pl/_color_utils.py: -------------------------------------------------------------------------------- 1 | """Utils for plotting functions.""" 2 | 3 | from __future__ import annotations 4 | 5 | from collections.abc import Mapping, Sequence 6 | from typing import Any, TypeAlias, Union 7 | 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | from anndata import AnnData 11 | from cycler import Cycler, cycler 12 | from matplotlib.colors import ListedColormap, to_hex, to_rgba 13 | from scanpy import logging as logg 14 | from scanpy.plotting._utils import add_colors_for_categorical_sample_annotation 15 | 16 | from squidpy._constants._pkg_constants import Key 17 | 18 | Palette_t: TypeAlias = str | ListedColormap | None 19 | 20 | 21 | def _maybe_set_colors( 22 | source: AnnData, target: AnnData, key: str, palette: str | ListedColormap | Cycler | Sequence[Any] | None = None 23 | ) -> None: 24 | color_key = Key.uns.colors(key) 25 | try: 26 | if palette is not None: 27 | raise KeyError("Unable to copy the palette when there was other explicitly specified.") 28 | target.uns[color_key] = source.uns[color_key] 29 | except KeyError: 30 | if isinstance(palette, ListedColormap): # `scanpy` requires it 31 | palette = cycler(color=palette.colors) 32 | add_colors_for_categorical_sample_annotation(target, key=key, force_update_colors=True, palette=palette) 33 | 34 | 35 | def _get_palette( 36 | adata: AnnData, 37 | cluster_key: str | None, 38 | categories: Sequence[Any], 39 | palette: Palette_t = None, 40 | alpha: float = 1.0, 41 | ) -> Mapping[str, str] | None: 42 | if palette is None: 43 | try: 44 | palette = adata.uns[Key.uns.colors(cluster_key)] # type: ignore[arg-type] 45 | if len(palette) != len(categories): 46 | raise ValueError( 47 | f"Expected palette to be of length `{len(categories)}`, found `{len(palette)}`. " 48 | + f"Removing the colors in `adata.uns` with `adata.uns.pop('{cluster_key}_colors')` may help." 49 | ) 50 | return { 51 | cat: to_hex(to_rgba(col)[:3] + (alpha,), keep_alpha=True) 52 | for cat, col in zip(categories, palette, strict=False) 53 | } 54 | except KeyError as e: 55 | logg.error(f"Unable to fetch palette, reason: {e}. Using `None`.") 56 | return None 57 | 58 | len_cat = len(categories) 59 | if isinstance(palette, str): 60 | cmap = plt.colormaps[palette] 61 | palette = [to_hex(x, keep_alpha=True) for x in cmap(np.linspace(0, 1, len_cat), alpha=alpha)] 62 | elif isinstance(palette, ListedColormap): 63 | palette = [to_hex(x, keep_alpha=True) for x in palette(np.linspace(0, 1, len_cat), alpha=alpha)] 64 | else: 65 | raise TypeError(f"Palette is {type(palette)} but should be string or `ListedColormap`.") 66 | 67 | return dict(zip(categories, palette, strict=False)) 68 | -------------------------------------------------------------------------------- /src/squidpy/pl/_interactive/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # from squidpy.pl._interactive.interactive import Interactive 4 | -------------------------------------------------------------------------------- /src/squidpy/pl/_interactive/_model.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Sequence 4 | from dataclasses import dataclass, field 5 | from typing import TYPE_CHECKING 6 | 7 | import numpy as np 8 | from anndata import AnnData 9 | 10 | from squidpy._constants._constants import Symbol 11 | from squidpy._constants._pkg_constants import Key 12 | from squidpy._utils import NDArrayA, _unique_order_preserving 13 | from squidpy.gr._utils import _assert_categorical_obs, _assert_spatial_basis 14 | from squidpy.im import ImageContainer 15 | from squidpy.im._coords import _NULL_COORDS, _NULL_PADDING, CropCoords, CropPadding 16 | from squidpy.pl._utils import ALayer 17 | 18 | __all__ = ["ImageModel"] 19 | 20 | 21 | @dataclass 22 | class ImageModel: 23 | """Model which holds the data for interactive visualization.""" 24 | 25 | adata: AnnData 26 | container: ImageContainer 27 | spatial_key: str = field(default=Key.obsm.spatial, repr=False) 28 | library_key: str | None = None 29 | library_id: str | Sequence[str] | None = None 30 | spot_diameter_key: str = "spot_diameter_fullres" 31 | spot_diameter: NDArrayA | float = field(default=0, init=False) 32 | coordinates: NDArrayA = field(init=False, repr=False) 33 | alayer: ALayer = field(init=False, repr=True) 34 | 35 | palette: str | None = field(default=None, repr=False) 36 | cmap: str = field(default="viridis", repr=False) 37 | blending: str = field(default="opaque", repr=False) 38 | key_added: str = "shapes" 39 | symbol: Symbol = Symbol.DISC 40 | 41 | def __post_init__(self) -> None: 42 | _assert_spatial_basis(self.adata, self.spatial_key) 43 | 44 | self.symbol = Symbol(self.symbol) 45 | self.adata = self.container.subset(self.adata, spatial_key=self.spatial_key) 46 | if not self.adata.n_obs: 47 | raise ValueError("Please ensure that the image contains at least 1 spot.") 48 | self._set_scale_coords() 49 | self._set_library() 50 | 51 | if TYPE_CHECKING: 52 | assert isinstance(self.library_id, Sequence) 53 | 54 | self.alayer = ALayer( 55 | self.adata, 56 | self.library_id, 57 | is_raw=False, 58 | palette=self.palette, 59 | ) 60 | 61 | try: 62 | self.container = ImageContainer._from_dataset(self.container.data.sel(z=self.library_id), deep=None) 63 | except KeyError: 64 | raise KeyError( 65 | f"Unable to subset the image container with library ids `{self.library_id}`. " 66 | f"Valid container library ids are `{self.container.library_ids}`. Please specify a valid `library_id`." 67 | ) from None 68 | 69 | def _set_scale_coords(self) -> None: 70 | self.scale = self.container.data.attrs.get(Key.img.scale, 1) 71 | coordinates = self.adata.obsm[self.spatial_key][:, :2] * self.scale 72 | 73 | c: CropCoords = self.container.data.attrs.get(Key.img.coords, _NULL_COORDS) 74 | p: CropPadding = self.container.data.attrs.get(Key.img.padding, _NULL_PADDING) 75 | if c != _NULL_COORDS: 76 | coordinates -= c.x0 - p.x_pre 77 | coordinates -= c.y0 - p.y_pre 78 | 79 | self.coordinates = coordinates[:, ::-1] 80 | 81 | def _set_library(self) -> None: 82 | if self.library_key is None: 83 | if len(self.container.library_ids) > 1: 84 | raise KeyError( 85 | f"ImageContainer has `{len(self.container.library_ids)}` Z-dimensions. " 86 | f"Please specify `library_key` that maps observations to library ids." 87 | ) 88 | self.coordinates = np.insert(self.coordinates, 0, values=0, axis=1) 89 | self.library_id = self.container.library_ids 90 | if TYPE_CHECKING: 91 | assert isinstance(self.library_id, Sequence) 92 | self.spot_diameter = ( 93 | Key.uns.spot_diameter(self.adata, self.spatial_key, self.library_id[0], self.spot_diameter_key) 94 | * self.scale 95 | ) 96 | return 97 | 98 | _assert_categorical_obs(self.adata, self.library_key) 99 | if self.library_id is None: 100 | self.library_id = self.adata.obs[self.library_key].cat.categories 101 | elif isinstance(self.library_id, str): 102 | self.library_id = [self.library_id] 103 | self.library_id, _ = _unique_order_preserving(self.library_id) # type: ignore[assignment] 104 | 105 | if not len(self.library_id): 106 | raise ValueError("No library ids have been selected.") 107 | # invalid library ids from adata are filtered below 108 | # invalid library ids from container raise KeyError in `__post_init__` after this call 109 | 110 | libraries = self.adata.obs[self.library_key] 111 | mask = libraries.isin(self.library_id) 112 | libraries = libraries[mask].cat.remove_unused_categories() 113 | self.library_id = list(libraries.cat.categories) 114 | 115 | self.coordinates = np.c_[libraries.cat.codes.values, self.coordinates[mask]] 116 | self.spot_diameter = np.array( 117 | [ 118 | np.array([0.0] + [Key.uns.spot_diameter(self.adata, self.spatial_key, lid, self.spot_diameter_key)] * 2) 119 | * self.scale 120 | for lid in libraries 121 | ] 122 | ) 123 | self.adata = self.adata[mask] 124 | -------------------------------------------------------------------------------- /src/squidpy/pl/_interactive/_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import dask.array as da 4 | import numpy as np 5 | import pandas as pd 6 | from anndata import AnnData 7 | from matplotlib.colors import to_hex, to_rgb 8 | from numba import njit 9 | from pandas import CategoricalDtype 10 | from pandas._libs.lib import infer_dtype 11 | from scanpy import logging as logg 12 | from scanpy.plotting._utils import add_colors_for_categorical_sample_annotation 13 | from scipy.spatial import KDTree 14 | 15 | from squidpy._constants._pkg_constants import Key 16 | from squidpy._utils import NDArrayA 17 | 18 | 19 | def _get_categorical( 20 | adata: AnnData, 21 | key: str, 22 | palette: str | None = None, 23 | vec: pd.Series | None = None, 24 | ) -> NDArrayA: 25 | if vec is not None: 26 | if not isinstance(vec.dtype, CategoricalDtype): 27 | raise TypeError(f"Expected a `categorical` type, found `{infer_dtype(vec)}`.") 28 | if key in adata.obs: 29 | logg.debug(f"Overwriting `adata.obs[{key!r}]`") 30 | 31 | adata.obs[key] = vec.values 32 | 33 | add_colors_for_categorical_sample_annotation( 34 | adata, key=key, force_update_colors=palette is not None, palette=palette 35 | ) 36 | col_dict = dict( 37 | zip(adata.obs[key].cat.categories, [to_rgb(i) for i in adata.uns[Key.uns.colors(key)]], strict=False) 38 | ) 39 | 40 | return np.array([col_dict[v] for v in adata.obs[key]]) 41 | 42 | 43 | def _position_cluster_labels(coords: NDArrayA, clusters: pd.Series, colors: NDArrayA) -> dict[str, NDArrayA]: 44 | if not isinstance(clusters.dtype, CategoricalDtype): 45 | raise TypeError(f"Expected `clusters` to be `categorical`, found `{infer_dtype(clusters)}`.") 46 | 47 | coords = coords[:, 1:] # TODO(michalk8): account for current Z-dim? 48 | df = pd.DataFrame(coords) 49 | df["clusters"] = clusters.values 50 | df = df.groupby("clusters")[[0, 1]].apply(lambda g: list(np.median(g.values, axis=0))) 51 | df = pd.DataFrame(list(df), index=df.index) 52 | 53 | kdtree = KDTree(coords) 54 | clusters = np.full(len(coords), fill_value="", dtype=object) 55 | # index consists of the categories that need not be string 56 | clusters[kdtree.query(df.values)[1]] = df.index.astype(str) 57 | # napari v0.4.9 - properties must be 1-D in napari/layers/points/points.py:581 58 | colors = np.array([to_hex(col if cl != "" else (0, 0, 0)) for cl, col in zip(clusters, colors, strict=False)]) 59 | 60 | return {"clusters": clusters, "colors": colors} 61 | 62 | 63 | def _not_in_01(arr: NDArrayA | da.Array) -> bool: 64 | @njit 65 | def _helper_arr(arr: NDArrayA) -> bool: 66 | for val in arr.flat: 67 | if not (0 <= val <= 1): 68 | return True 69 | 70 | return False 71 | 72 | if isinstance(arr, da.Array): 73 | return bool(np.min(arr) < 0 or np.max(arr) > 1) # cast needed 74 | 75 | return bool(_helper_arr(np.asarray(arr))) 76 | 77 | 78 | def _display_channelwise(arr: NDArrayA | da.Array) -> bool: 79 | n_channels: int = arr.shape[-1] 80 | if n_channels not in (3, 4): 81 | return n_channels != 1 82 | if np.issubdtype(arr.dtype, np.uint8): 83 | return False # assume RGB(A) 84 | return _not_in_01(arr) if np.issubdtype(arr.dtype, np.floating) else True 85 | -------------------------------------------------------------------------------- /src/squidpy/pl/_interactive/interactive.py: -------------------------------------------------------------------------------- 1 | """ 2 | interactive.py 3 | 4 | WARNING: This module is deprecated and will be removed in a future version. 5 | Please use `https://github.com/scverse/napari-spatialdata` instead. 6 | """ 7 | 8 | raise ImportError("The squidpy napari plugin is deprecated, please use https://github.com/scverse/napari-spatialdata") 9 | 10 | from __future__ import annotations 11 | 12 | from typing import ( 13 | Any, 14 | Union, # noqa: F401 15 | ) 16 | 17 | import matplotlib.pyplot as plt 18 | from anndata import AnnData 19 | from scanpy import logging as logg 20 | 21 | from squidpy._docs import d 22 | from squidpy._utils import NDArrayA, deprecated 23 | from squidpy.im import ImageContainer 24 | from squidpy.pl._utils import save_fig 25 | 26 | try: 27 | from squidpy.pl._interactive._controller import ImageController 28 | except ImportError as e: 29 | _error: str | None = str(e) 30 | else: 31 | _error = None 32 | 33 | 34 | __all__ = ["Interactive"] 35 | 36 | 37 | @d.dedent 38 | class Interactive: 39 | """ 40 | Interactive viewer for spatial data. 41 | 42 | Parameters 43 | ---------- 44 | %(img_container)s 45 | %(_interactive.parameters)s 46 | """ 47 | 48 | @deprecated( 49 | reason="The squidpy napari plugin is deprecated, please use https://github.com/scverse/napari-spatialdata", 50 | ) 51 | def __init__(self, img: ImageContainer, adata: AnnData, **kwargs: Any): 52 | if _error is not None: 53 | raise ImportError(f"Unable to import the interactive viewer. Reason `{_error}`.") 54 | 55 | self._controller = ImageController(adata, img, **kwargs) 56 | 57 | def show(self, restore: bool = False) -> Interactive: 58 | """ 59 | Launch the :class:`napari.Viewer`. 60 | 61 | Parameters 62 | ---------- 63 | restore 64 | Whether to reinitialize the GUI after it has been destroyed. 65 | 66 | Returns 67 | ------- 68 | Nothing, just launches the viewer. 69 | """ 70 | self._controller.show(restore=restore) 71 | return self 72 | 73 | @d.dedent 74 | def screenshot( 75 | self, 76 | return_result: bool = False, 77 | dpi: float | None = 180, 78 | save: str | None = None, 79 | canvas_only: bool = True, 80 | **kwargs: Any, 81 | ) -> NDArrayA | None: 82 | """ 83 | Plot a screenshot of the viewer's canvas. 84 | 85 | Parameters 86 | ---------- 87 | return_result 88 | If `True`, return the image as an :class:`numpy.uint8`. 89 | dpi 90 | Dots per inch. 91 | save 92 | Whether to save the plot. 93 | canvas_only 94 | Whether to show only the canvas or also the widgets. 95 | kwargs 96 | Keyword arguments for :meth:`matplotlib.axes.Axes.imshow`. 97 | 98 | Returns 99 | ------- 100 | Nothing, if ``return_result = False``, otherwise the image array. 101 | """ 102 | try: 103 | arr = self._controller.screenshot(path=None, canvas_only=canvas_only) 104 | except RuntimeError as e: 105 | logg.error(f"Unable to take a screenshot. Reason: {e}") 106 | return None 107 | 108 | fig, ax = plt.subplots(nrows=1, ncols=1, dpi=dpi) 109 | fig.tight_layout() 110 | 111 | ax.imshow(arr, **kwargs) 112 | plt.axis("off") 113 | 114 | if save is not None: 115 | save_fig(fig, save) 116 | 117 | return arr if return_result else None 118 | 119 | def close(self) -> None: 120 | """Close the viewer.""" 121 | self._controller.close() 122 | 123 | @property 124 | def adata(self) -> AnnData: 125 | """Annotated data object.""" 126 | return self._controller.model.adata 127 | 128 | def __repr__(self) -> str: 129 | return f"Interactive view of {repr(self._controller.model.container)}" 130 | 131 | def __str__(self) -> str: 132 | return repr(self) 133 | -------------------------------------------------------------------------------- /src/squidpy/read/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from squidpy.read._read import nanostring, visium, vizgen 4 | -------------------------------------------------------------------------------- /src/squidpy/read/_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | from typing import Any 5 | 6 | import numpy as np 7 | from anndata import AnnData 8 | from anndata.io import read_text 9 | from h5py import File 10 | from PIL import Image 11 | from scanpy import read_10x_h5, read_10x_mtx 12 | 13 | from squidpy._constants._pkg_constants import Key 14 | from squidpy._utils import NDArrayA 15 | from squidpy.datasets._utils import PathLike 16 | 17 | 18 | def _read_counts( 19 | path: str | Path, 20 | counts_file: str, 21 | library_id: str | None = None, 22 | **kwargs: Any, 23 | ) -> tuple[AnnData, str]: 24 | path = Path(path) 25 | if counts_file.endswith(".h5"): 26 | adata: AnnData = read_10x_h5(path / counts_file, **kwargs) 27 | with File(path / counts_file, mode="r") as f: 28 | attrs = dict(f.attrs) 29 | if library_id is None: 30 | try: 31 | lid = attrs.pop("library_ids")[0] 32 | library_id = lid.decode("utf-8") if isinstance(lid, bytes) else str(lid) 33 | except ValueError: 34 | raise KeyError( 35 | "Unable to extract library id from attributes. Please specify one explicitly." 36 | ) from None 37 | 38 | adata.uns[Key.uns.spatial] = {library_id: {"metadata": {}}} # can overwrite 39 | for key in ["chemistry_description", "software_version"]: 40 | if key not in attrs: 41 | continue 42 | metadata = attrs[key].decode("utf-8") if isinstance(attrs[key], bytes) else attrs[key] 43 | adata.uns[Key.uns.spatial][library_id]["metadata"][key] = metadata 44 | 45 | return adata, library_id 46 | 47 | if library_id is None: 48 | raise ValueError("Please explicitly specify library id.") 49 | 50 | if counts_file.endswith((".csv", ".txt")): 51 | adata = read_text(path / counts_file, **kwargs) 52 | elif counts_file.endswith(".mtx.gz"): 53 | adata = read_10x_mtx(path, **kwargs) 54 | else: 55 | raise NotImplementedError("TODO") 56 | 57 | adata.uns[Key.uns.spatial] = {library_id: {"metadata": {}}} # can overwrite 58 | return adata, library_id 59 | 60 | 61 | def _load_image(path: PathLike) -> NDArrayA: 62 | return np.asarray(Image.open(path)) 63 | -------------------------------------------------------------------------------- /src/squidpy/tl/__init__.py: -------------------------------------------------------------------------------- 1 | """The design matrix module.""" 2 | 3 | from __future__ import annotations 4 | 5 | from squidpy.tl._sliding_window import _calculate_window_corners, sliding_window 6 | from squidpy.tl._var_by_distance import var_by_distance 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/__init__.py -------------------------------------------------------------------------------- /tests/_data/filtered_feature_bc_matrix.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_data/filtered_feature_bc_matrix.h5 -------------------------------------------------------------------------------- /tests/_data/ligrec_no_numba.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_data/ligrec_no_numba.pickle -------------------------------------------------------------------------------- /tests/_data/paul15_means.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_data/paul15_means.pickle -------------------------------------------------------------------------------- /tests/_data/spatial/scalefactors_json.json: -------------------------------------------------------------------------------- 1 | { 2 | "spot_diameter_fullres": 89.42751063343188, 3 | "tissue_hires_scalef": 0.150015, 4 | "fiducial_diameter_fullres": 144.45982486939, 5 | "tissue_lowres_scalef": 0.045004502 6 | } 7 | -------------------------------------------------------------------------------- /tests/_data/spatial/tissue_hires_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_data/spatial/tissue_hires_image.png -------------------------------------------------------------------------------- /tests/_data/spatial/tissue_lowres_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_data/spatial/tissue_lowres_image.png -------------------------------------------------------------------------------- /tests/_data/spatial/tissue_positions_list.csv: -------------------------------------------------------------------------------- 1 | AAACAAGTATCTCCCA-1,1,50,102,8513,9811 2 | AAACACCAATAACTGC-1,1,59,19,9605,4104 3 | AAACAGAGCGACTCCT-1,1,14,94,4205,9250 4 | AAACAGCTTTCAGAAG-1,1,43,9,7691,3412 5 | AAACAGGGTCTATATT-1,1,47,13,8170,3688 6 | AAACATGGTGAGAGGA-1,1,62,0,9967,2798 7 | AAACATTTCCCGGATT-1,1,61,97,9831,9471 8 | AAACCACTACACAGAT-1,1,3,117,2885,10829 9 | AAACCCGAACGAAATC-1,1,45,115,7912,10704 10 | AAACCGGAAATGTTAA-1,1,54,124,8988,11326 11 | AAACCGGGTAGGTACC-1,1,42,28,7568,4718 12 | AAACCGTTCGTCCAGG-1,1,52,42,8763,5684 13 | AAACCTAAGCAGCCGG-1,1,65,83,10312,8509 14 | AAACCTCATGAAGTTG-1,1,37,19,6972,4098 15 | AAACGAAGAACATACC-1,1,6,64,3253,7184 16 | AAACGAAGATGGAGTA-1,1,58,4,9488,3072 17 | AAACGAGACGGTTGAT-1,1,35,79,6722,8224 18 | AAACGCCCGAGATCGG-1,1,4,108,3006,10210 19 | AAACGGGCGTACGGGT-1,1,65,91,10310,9059 20 | AAACGGTTGCGAACTG-1,1,67,59,10555,6858 21 | AAACGTGTTCGCCCTA-1,1,14,118,4201,10901 22 | AAACTAACGTGGCGAC-1,1,8,110,3485,10349 23 | AAACTCGGTTCGCAAT-1,1,66,70,10434,7615 24 | AAACTCGTGATATAAG-1,1,23,113,5279,10560 25 | AAACTGCTGGCTCCAA-1,1,45,67,7921,7402 26 | AAACTTAATTGCACGC-1,1,64,12,10205,3624 27 | AAACTTGCAAACGTAT-1,1,45,19,7929,4100 28 | AAAGAATGTGGACTAA-1,1,71,105,11026,10024 29 | AAAGACCCAAGTCGCG-1,1,10,48,3735,6084 30 | AAAGACTGGGCGCTTT-1,1,29,15,6015,3820 31 | AAAGCTTGCCTACATA-1,1,26,122,5637,11180 32 | AAAGGCCCTATAATAC-1,1,66,22,10442,4313 33 | AAAGGCTACGGACCAT-1,1,62,54,9958,6513 34 | AAAGGCTCTCGCGCCG-1,1,55,55,9120,6580 35 | AAAGGGATGTAGCAAG-1,1,24,62,5408,7052 36 | AAAGGGCAGCTTGAAT-1,1,24,26,5414,4575 37 | AAAGGTAAGCTGTACC-1,1,10,106,3725,10074 38 | AAAGTAGCATTGCTCA-1,1,51,27,8646,4652 39 | AAAGTCACTGATGTAA-1,1,10,52,3734,6360 40 | AAAGTCGACCCTCAGT-1,1,37,15,6972,3822 41 | AAAGTGTGATTTATCT-1,1,44,94,7796,9259 42 | AAAGTTGACTCCCGTA-1,1,42,96,7557,9396 43 | AAATAACCATACGGGA-1,1,14,88,4207,8837 44 | AAATAAGGTAGTGCCC-1,1,31,115,6237,10700 45 | AAATACCTATAAGCAT-1,1,47,69,8160,7540 46 | AAATAGCTTAGACTTT-1,1,36,106,6837,10082 47 | AAATAGGGTGCTATTG-1,1,53,79,8876,8230 48 | AAATCCGATACACGCC-1,1,65,87,10311,8784 49 | AAATCGCGGAAGGAGT-1,1,57,5,9368,3141 50 | AAATCGTGTACCACAA-1,1,44,56,7803,6645 51 | AAATCTAGCCCTGCTA-1,1,53,101,8872,9743 52 | AAATGATTCGATCAGC-1,1,21,111,5040,10421 53 | AAATGCTCGTTACGTT-1,1,16,24,4457,4435 54 | AAATGGCATGTCTTGT-1,1,13,69,4090,7530 55 | AAATGGCCCGTGCCCT-1,1,49,17,8408,3964 56 | AAATGGTCAATGTGCC-1,1,33,51,6487,6298 57 | AAATGTATCTTATCCC-1,1,26,0,5658,2787 58 | AAATTAACGGGTAGCT-1,1,34,58,6606,6780 59 | AAATTAATAAGCGCGA-1,1,32,4,6376,3064 60 | AAATTACACGACTCTG-1,1,14,86,4207,8700 61 | AAATTACCTATCGATG-1,1,26,94,5642,9254 62 | AAATTCCAGGTCCAAA-1,1,35,115,6715,10701 63 | AAATTGATAGTCCTTT-1,1,55,31,9124,4929 64 | AAATTGCGGCGGTTCT-1,1,5,19,3141,4088 65 | AAATTGGTGAGAAGCA-1,1,15,119,4321,10970 66 | AAATTTACCGAAATCC-1,1,6,90,3249,8972 67 | AAATTTGCGGGTGTGG-1,1,47,33,8166,5064 68 | AACAACTGGTAGTTGC-1,1,28,42,5890,5677 69 | AACAATACATTGTCGA-1,1,61,37,9841,5343 70 | AACAATTACTCTACGC-1,1,29,25,6013,4508 71 | AACACACGCTCGCCGC-1,1,51,93,8634,9192 72 | AACACGACTGTACTGA-1,1,9,65,3612,7253 73 | AACACGAGACGCGGCC-1,1,45,21,7929,4238 74 | AACACGCGGCCGCGAA-1,1,6,34,3258,5120 75 | AACAGCTGTGTGGCAA-1,1,59,31,9603,4930 76 | AACAGGAAATCGAATA-1,1,15,67,4330,7393 77 | AACAGGATGGGCCGCG-1,1,29,31,6012,4921 78 | AACAGGTAGTATGGAT-1,1,49,113,8391,10568 79 | AACATATCAACTGGTG-1,1,27,99,5761,9598 80 | AACATCGATACGTCTA-1,1,17,113,4561,10558 81 | AACATTGAAGTTGATC-1,1,67,23,10562,4382 82 | AACATTGTGACTCGAG-1,1,64,52,10198,6376 83 | AACCAAGACTTCTCTG-1,1,36,24,6851,4441 84 | AACCAGTATCACTCTT-1,1,6,92,3248,9110 85 | AACCATAGGGTTGAAC-1,1,65,11,10324,3556 86 | AACCATGGGATCGCTA-1,1,14,30,4217,4847 87 | AACCCAGAGACGGAGA-1,1,15,39,4335,5467 88 | AACCCATCCCATGATC-1,1,56,14,9247,3759 89 | AACCCGACAACCCGTG-1,1,67,43,10558,5758 90 | AACCCGAGCAGAATCG-1,1,57,113,9349,10570 91 | AACCCGATAGGGCTTC-1,1,5,125,3123,11380 92 | AACCCTACTGTCAATA-1,1,40,96,7317,9395 93 | AACCCTGGTGGAACCA-1,1,8,70,3492,7597 94 | AACCGAGCTTGGTCAT-1,1,24,70,5407,7602 95 | AACCGCTAAGGGATGC-1,1,46,92,8036,9122 96 | AACCGTTGTGTTTGCT-1,1,12,64,3971,7186 97 | AACCTCGCTTTAGCCC-1,1,62,82,9953,8439 98 | AACCTGTCACGGAATT-1,1,36,118,6835,10908 99 | AACCTTTAAATACGGT-1,1,54,68,8998,7474 100 | AACCTTTACGACGTCT-1,1,16,92,4445,9113 101 | -------------------------------------------------------------------------------- /tests/_data/test_data.h5ad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_data/test_data.h5ad -------------------------------------------------------------------------------- /tests/_data/test_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_data/test_img.jpg -------------------------------------------------------------------------------- /tests/_images/ContainerShow_axis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_axis.png -------------------------------------------------------------------------------- /tests/_images/ContainerShow_channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_channel.png -------------------------------------------------------------------------------- /tests/_images/ContainerShow_channelwise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_channelwise.png -------------------------------------------------------------------------------- /tests/_images/ContainerShow_channelwise_segmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_channelwise_segmentation.png -------------------------------------------------------------------------------- /tests/_images/ContainerShow_imshow_kwargs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_imshow_kwargs.png -------------------------------------------------------------------------------- /tests/_images/ContainerShow_library_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_library_id.png -------------------------------------------------------------------------------- /tests/_images/ContainerShow_scale_mask_circle_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_scale_mask_circle_crop.png -------------------------------------------------------------------------------- /tests/_images/ContainerShow_segmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_segmentation.png -------------------------------------------------------------------------------- /tests/_images/ContainerShow_transpose_channelwise_False_False.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_transpose_channelwise_False_False.png -------------------------------------------------------------------------------- /tests/_images/ContainerShow_transpose_channelwise_False_True.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_transpose_channelwise_False_True.png -------------------------------------------------------------------------------- /tests/_images/ContainerShow_transpose_channelwise_True_False.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_transpose_channelwise_True_False.png -------------------------------------------------------------------------------- /tests/_images/ContainerShow_transpose_channelwise_True_True.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/ContainerShow_transpose_channelwise_True_True.png -------------------------------------------------------------------------------- /tests/_images/Graph_centrality_scores.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_centrality_scores.png -------------------------------------------------------------------------------- /tests/_images/Graph_centrality_scores_single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_centrality_scores_single.png -------------------------------------------------------------------------------- /tests/_images/Graph_co_occurrence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_co_occurrence.png -------------------------------------------------------------------------------- /tests/_images/Graph_co_occurrence_palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_co_occurrence_palette.png -------------------------------------------------------------------------------- /tests/_images/Graph_interaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_interaction.png -------------------------------------------------------------------------------- /tests/_images/Graph_interaction_dendro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_interaction_dendro.png -------------------------------------------------------------------------------- /tests/_images/Graph_nhood_enrichment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_nhood_enrichment.png -------------------------------------------------------------------------------- /tests/_images/Graph_nhood_enrichment_ax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_nhood_enrichment_ax.png -------------------------------------------------------------------------------- /tests/_images/Graph_nhood_enrichment_dendro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_nhood_enrichment_dendro.png -------------------------------------------------------------------------------- /tests/_images/Graph_ripley_f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_ripley_f.png -------------------------------------------------------------------------------- /tests/_images/Graph_ripley_f_nopalette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_ripley_f_nopalette.png -------------------------------------------------------------------------------- /tests/_images/Graph_ripley_g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_ripley_g.png -------------------------------------------------------------------------------- /tests/_images/Graph_ripley_l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Graph_ripley_l.png -------------------------------------------------------------------------------- /tests/_images/Heatmap_cbar_kwargs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Heatmap_cbar_kwargs.png -------------------------------------------------------------------------------- /tests/_images/Heatmap_cbar_vmin_vmax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Heatmap_cbar_vmin_vmax.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_alpha.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_alpha_none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_alpha_none.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_cmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_cmap.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_dendrogram_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_dendrogram_both.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_dendrogram_clusters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_dendrogram_clusters.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_dendrogram_pairs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_dendrogram_pairs.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_kwargs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_kwargs.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_means_range.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_means_range.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_no_remove_empty_interactions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_no_remove_empty_interactions.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_pvalue_threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_pvalue_threshold.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_remove_empty_interactions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_remove_empty_interactions.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_remove_nonsig_interactions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_remove_nonsig_interactions.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_source_clusters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_source_clusters.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_swap_axes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_swap_axes.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_swap_axes_dedrogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_swap_axes_dedrogram.png -------------------------------------------------------------------------------- /tests/_images/Ligrec_target_clusters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Ligrec_target_clusters.png -------------------------------------------------------------------------------- /tests/_images/Napari_add_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_add_image.png -------------------------------------------------------------------------------- /tests/_images/Napari_blending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_blending.png -------------------------------------------------------------------------------- /tests/_images/Napari_cat_cmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_cat_cmap.png -------------------------------------------------------------------------------- /tests/_images/Napari_cont_cmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_cont_cmap.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_-200_-200_600_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_-200_-200_600_800.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_-200_-200_800_600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_-200_-200_800_600.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_-200_-200_800_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_-200_-200_800_800.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_-200_200_600_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_-200_200_600_800.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_-200_200_800_600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_-200_200_800_600.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_-200_200_800_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_-200_200_800_800.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_200_-200_600_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_200_-200_600_800.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_200_-200_800_600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_200_-200_800_600.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_200_-200_800_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_200_-200_800_800.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_200_200_600_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_200_200_600_800.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_200_200_800_600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_200_200_800_600.png -------------------------------------------------------------------------------- /tests/_images/Napari_corner_case_200_200_800_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_corner_case_200_200_800_800.png -------------------------------------------------------------------------------- /tests/_images/Napari_crop_center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_crop_center.png -------------------------------------------------------------------------------- /tests/_images/Napari_crop_corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_crop_corner.png -------------------------------------------------------------------------------- /tests/_images/Napari_gene_X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_gene_X.png -------------------------------------------------------------------------------- /tests/_images/Napari_obs_categorical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_obs_categorical.png -------------------------------------------------------------------------------- /tests/_images/Napari_obs_continuous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_obs_continuous.png -------------------------------------------------------------------------------- /tests/_images/Napari_scalefactor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_scalefactor.png -------------------------------------------------------------------------------- /tests/_images/Napari_simple_canvas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_simple_canvas.png -------------------------------------------------------------------------------- /tests/_images/Napari_symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_symbol.png -------------------------------------------------------------------------------- /tests/_images/Napari_viewer_canvas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/Napari_viewer_canvas.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_palette_listed_cmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_palette_listed_cmap.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_axfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_axfig.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_categorical_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_categorical_alpha.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_crop.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_crop_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_crop_graph.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_crop_noorigin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_crop_noorigin.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_group.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_group_multi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_group_multi.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_group_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_group_outline.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_image.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_noimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_noimage.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_non_unique_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_non_unique_colors.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_nospatial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_nospatial.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_novisium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_novisium.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_scatter_title_single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_scatter_title_single.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_segment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_segment.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_segment_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_segment_crop.png -------------------------------------------------------------------------------- /tests/_images/SpatialStatic_spatial_segment_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/SpatialStatic_spatial_segment_group.png -------------------------------------------------------------------------------- /tests/_images/var_by_distance_single_anchor_and_gene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/var_by_distance_single_anchor_and_gene.png -------------------------------------------------------------------------------- /tests/_images/var_by_distance_single_anchor_and_gene_two_categories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/var_by_distance_single_anchor_and_gene_two_categories.png -------------------------------------------------------------------------------- /tests/_images/var_by_distance_single_anchor_four_genes_two_categories_two_palettes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/var_by_distance_single_anchor_four_genes_two_categories_two_palettes.png -------------------------------------------------------------------------------- /tests/_images/var_by_distance_single_anchor_one_gene_two_categories_without_scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/_images/var_by_distance_single_anchor_one_gene_two_categories_without_scatter.png -------------------------------------------------------------------------------- /tests/datasets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/datasets/__init__.py -------------------------------------------------------------------------------- /tests/datasets/test_dataset.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | from http.client import RemoteDisconnected 5 | from pathlib import Path 6 | from types import FunctionType 7 | 8 | import pytest 9 | from anndata import AnnData, OldFormatWarning 10 | 11 | import squidpy as sq 12 | 13 | 14 | class TestDatasetsImports: 15 | @pytest.mark.parametrize("func", sq.datasets._dataset.__all__ + sq.datasets._image.__all__) 16 | def test_import(self, func): 17 | assert hasattr(sq.datasets, func), dir(sq.datasets) 18 | fn = getattr(sq.datasets, func) 19 | 20 | assert isinstance(fn, FunctionType) 21 | 22 | 23 | # TODO(michalk8): parse the code and xfail iff server issue 24 | class TestDatasetsDownload: 25 | @pytest.mark.timeout(120) 26 | def test_download_imc(self, tmp_path: Path): 27 | with warnings.catch_warnings(): 28 | warnings.simplefilter("ignore", category=OldFormatWarning) 29 | try: 30 | adata = sq.datasets.imc(tmp_path / "foo") 31 | 32 | assert isinstance(adata, AnnData) 33 | assert adata.shape == (4668, 34) 34 | except RemoteDisconnected as e: 35 | pytest.xfail(str(e)) 36 | 37 | @pytest.mark.timeout(120) 38 | def test_download_visium_hne_image_crop(self, tmp_path: Path): 39 | with warnings.catch_warnings(): 40 | warnings.simplefilter("ignore", category=OldFormatWarning) 41 | try: 42 | img = sq.datasets.visium_hne_image_crop(tmp_path / "foo") 43 | 44 | assert isinstance(img, sq.im.ImageContainer) 45 | assert img.shape == (3527, 3527) 46 | except RemoteDisconnected as e: 47 | pytest.xfail(str(e)) 48 | -------------------------------------------------------------------------------- /tests/datasets/test_download_visium_dataset.py: -------------------------------------------------------------------------------- 1 | """ " 2 | Tests to make sure the Visium example datasets load. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import subprocess 8 | from pathlib import Path 9 | 10 | import pytest 11 | import spatialdata as sd 12 | from anndata.tests.helpers import assert_adata_equal 13 | from scanpy._settings import settings 14 | 15 | from squidpy.datasets import visium, visium_hne_sdata 16 | 17 | 18 | @pytest.mark.timeout(120) 19 | @pytest.mark.internet() 20 | @pytest.mark.parametrize( 21 | "sample", 22 | [ 23 | "V1_Mouse_Kidney", 24 | "Targeted_Visium_Human_SpinalCord_Neuroscience", 25 | "Visium_FFPE_Human_Breast_Cancer", 26 | ], 27 | ) 28 | def test_visium_datasets(tmpdir, sample): 29 | # Tests that reading / downloading datasets works and it does not have any global effects 30 | sample_dataset = visium(sample) 31 | sample_dataset_again = visium(sample) 32 | assert_adata_equal(sample_dataset, sample_dataset_again) 33 | 34 | # Test that changing the dataset directory doesn't break reading 35 | settings.datasetdir = Path(tmpdir) 36 | sample_dataset_again = visium(sample) 37 | assert_adata_equal(sample_dataset, sample_dataset_again) 38 | 39 | # Test that downloading tissue image works 40 | sample_dataset = visium(sample, include_hires_tiff=True) 41 | expected_image_path = settings.datasetdir / sample / "image.tif" 42 | image_path = Path(sample_dataset.uns["spatial"][sample]["metadata"]["source_image_path"]) 43 | assert image_path == expected_image_path 44 | 45 | # Test that tissue image exists and is a valid image file 46 | assert image_path.exists() 47 | 48 | # Test that tissue image is a tif image file (using `file`) 49 | process = subprocess.run(["file", "--mime-type", image_path], stdout=subprocess.PIPE) 50 | output = process.stdout.strip().decode() # make process output string 51 | assert output == str(image_path) + ": image/tiff" 52 | 53 | 54 | @pytest.mark.timeout(120) 55 | @pytest.mark.internet() 56 | def test_visium_sdata_dataset(tmpdir): 57 | sdata = visium_hne_sdata(Path(tmpdir)) 58 | assert isinstance(sdata, sd.SpatialData) 59 | assert list(sdata.shapes.keys()) == ["spots"] 60 | assert list(sdata.images.keys()) == ["hne"] 61 | -------------------------------------------------------------------------------- /tests/graph/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/graph/__init__.py -------------------------------------------------------------------------------- /tests/graph/test_nhood.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pandas as pd 5 | import pytest 6 | from anndata import AnnData 7 | 8 | from squidpy._constants._pkg_constants import Key 9 | from squidpy.gr import ( 10 | centrality_scores, 11 | interaction_matrix, 12 | nhood_enrichment, 13 | spatial_neighbors, 14 | ) 15 | 16 | _CK = "leiden" 17 | 18 | 19 | class TestNhoodEnrichment: 20 | def _assert_common(self, adata: AnnData): 21 | key = Key.uns.nhood_enrichment(_CK) 22 | assert adata.uns[key]["zscore"].dtype == np.dtype("float64") 23 | assert adata.uns[key]["count"].dtype == np.dtype("uint32") 24 | assert adata.uns[key]["zscore"].shape[0] == adata.obs.leiden.cat.categories.shape[0] 25 | assert adata.uns[key]["count"].shape[0] == adata.obs.leiden.cat.categories.shape[0] 26 | 27 | def test_nhood_enrichment(self, adata: AnnData): 28 | spatial_neighbors(adata) 29 | nhood_enrichment(adata, cluster_key=_CK) 30 | 31 | self._assert_common(adata) 32 | 33 | @pytest.mark.parametrize("backend", ["threading", "multiprocessing", "loky"]) 34 | def test_parallel_works(self, adata: AnnData, backend: str): 35 | spatial_neighbors(adata) 36 | 37 | nhood_enrichment(adata, cluster_key=_CK, n_jobs=2, n_perms=20, backend=backend) 38 | 39 | self._assert_common(adata) 40 | 41 | @pytest.mark.parametrize("n_jobs", [1, 2]) 42 | def test_reproducibility(self, adata: AnnData, n_jobs: int): 43 | spatial_neighbors(adata) 44 | 45 | res1 = nhood_enrichment(adata, cluster_key=_CK, seed=42, n_jobs=n_jobs, n_perms=20, copy=True) 46 | res2 = nhood_enrichment(adata, cluster_key=_CK, seed=42, n_jobs=n_jobs, n_perms=20, copy=True) 47 | res3 = nhood_enrichment(adata, cluster_key=_CK, seed=43, n_jobs=n_jobs, n_perms=20, copy=True) 48 | 49 | assert len(res1) == len(res2) 50 | assert len(res2) == len(res3) 51 | 52 | for key in range(len(res1)): 53 | np.testing.assert_array_equal(res2[key], res1[key]) 54 | if key == 0: # z-score 55 | with pytest.raises(AssertionError): 56 | np.testing.assert_array_equal(res3[key], res2[key]) 57 | else: # counts 58 | np.testing.assert_array_equal(res3[key], res2[key]) 59 | 60 | 61 | def test_centrality_scores(nhood_data: AnnData): 62 | adata = nhood_data 63 | centrality_scores( 64 | adata=adata, 65 | cluster_key=_CK, 66 | connectivity_key="spatial", 67 | ) 68 | 69 | key = Key.uns.centrality_scores(_CK) 70 | 71 | assert key in adata.uns_keys() 72 | assert isinstance(adata.uns[key], pd.DataFrame) 73 | assert len(adata.obs[_CK].unique()) == adata.uns[key].shape[0] 74 | assert adata.uns[key]["degree_centrality"].dtype == np.dtype("float64") 75 | assert adata.uns[key]["average_clustering"].dtype == np.dtype("float64") 76 | assert adata.uns[key]["closeness_centrality"].dtype == np.dtype("float64") 77 | 78 | 79 | @pytest.mark.parametrize("copy", [True, False]) 80 | def test_interaction_matrix_copy(nhood_data: AnnData, copy: bool): 81 | adata = nhood_data 82 | res = interaction_matrix( 83 | adata=adata, 84 | cluster_key=_CK, 85 | connectivity_key="spatial", 86 | copy=copy, 87 | ) 88 | 89 | key = Key.uns.interaction_matrix(_CK) 90 | n_cls = adata.obs[_CK].nunique() 91 | 92 | if not copy: 93 | assert res is None 94 | assert key in adata.uns_keys() 95 | res = adata.uns[key] 96 | else: 97 | assert key not in adata.uns_keys() 98 | 99 | assert isinstance(res, np.ndarray) 100 | assert res.shape == (n_cls, n_cls) 101 | 102 | 103 | @pytest.mark.parametrize("normalized", [True, False]) 104 | def test_interaction_matrix_normalize(nhood_data: AnnData, normalized: bool): 105 | adata = nhood_data 106 | res = interaction_matrix( 107 | adata=adata, 108 | cluster_key=_CK, 109 | connectivity_key="spatial", 110 | copy=True, 111 | normalized=normalized, 112 | ) 113 | n_cls = adata.obs["leiden"].nunique() 114 | 115 | assert isinstance(res, np.ndarray) 116 | assert res.shape == (n_cls, n_cls) 117 | 118 | if normalized: 119 | np.testing.assert_allclose(res.sum(1), 1.0), res.sum(1) 120 | else: 121 | assert len(adata.obsp["spatial_connectivities"].data) == res.sum() 122 | 123 | 124 | def test_interaction_matrix_values(adata_intmat: AnnData): 125 | result_weighted = interaction_matrix(adata_intmat, "cat", weights=True, copy=True) 126 | result_unweighted = interaction_matrix(adata_intmat, "cat", weights=False, copy=True) 127 | 128 | expected_weighted = np.array([[5, 1], [2, 3]]) 129 | expected_unweighted = np.array([[4, 1], [2, 2]]) 130 | 131 | np.testing.assert_array_equal(expected_weighted, result_weighted) 132 | np.testing.assert_array_equal(expected_unweighted, result_unweighted) 133 | 134 | 135 | def test_interaction_matrix_nan_values(adata_intmat: AnnData): 136 | adata_intmat.obs.loc["0", "cat"] = np.nan 137 | result_weighted = interaction_matrix(adata_intmat, "cat", weights=True, copy=True) 138 | result_unweighted = interaction_matrix(adata_intmat, "cat", weights=False, copy=True) 139 | 140 | expected_weighted = np.array([[2, 1], [2, 3]]) 141 | expected_unweighted = np.array([[1, 1], [2, 2]]) 142 | 143 | np.testing.assert_array_equal(expected_weighted, result_weighted) 144 | np.testing.assert_array_equal(expected_unweighted, result_unweighted) 145 | -------------------------------------------------------------------------------- /tests/graph/test_niche.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pytest 5 | import scipy 6 | from anndata import AnnData 7 | from pandas.testing import assert_frame_equal 8 | from scipy.sparse import issparse 9 | 10 | import squidpy as sq 11 | from squidpy.gr import calculate_niche, spatial_neighbors 12 | from squidpy.gr._niche import ( 13 | _aggregate, 14 | _calculate_neighborhood_profile, 15 | _hop, 16 | _normalize, 17 | _setdiag, 18 | _utag, 19 | ) 20 | from tests.conftest import PlotTester, PlotTesterMeta 21 | 22 | SPATIAL_CONNECTIVITIES_KEY = "spatial_connectivities" 23 | N_NEIGHBORS = 20 24 | GROUPS = "celltype_mapped_refined" 25 | 26 | 27 | def test_niche_calc_nhood(adata_seqfish: AnnData): 28 | """Check whether niche calculation using neighborhood profile approach works as intended.""" 29 | spatial_neighbors(adata_seqfish, coord_type="generic", delaunay=False, n_neighs=N_NEIGHBORS) 30 | calculate_niche( 31 | adata_seqfish, 32 | groups=GROUPS, 33 | flavor="neighborhood", 34 | n_neighbors=N_NEIGHBORS, 35 | resolutions=[0.1], 36 | min_niche_size=100, 37 | ) 38 | niches = adata_seqfish.obs["nhood_niche_res=0.1"] 39 | 40 | # assert no nans, more niche labels than non-niche labels, and at least 100 obs per niche 41 | assert niches.isna().sum() == 0 42 | assert len(niches[niches != "not_a_niche"]) > len(niches[niches == "not_a_niche"]) 43 | for label in niches.unique(): 44 | if label != "not_a_niche": 45 | assert len(niches[niches == label]) >= 100 46 | 47 | # get obs x neighbor matrix from sparse matrix 48 | matrix = adata_seqfish.obsp[SPATIAL_CONNECTIVITIES_KEY].tocoo() 49 | 50 | # get obs x category matrix where each column is the absolute/relative frequency of a category in the neighborhood 51 | rel_nhood_profile = _calculate_neighborhood_profile(adata_seqfish, groups=GROUPS, matrix=matrix, abs_nhood=False) 52 | abs_nhood_profile = _calculate_neighborhood_profile(adata_seqfish, groups=GROUPS, matrix=matrix, abs_nhood=True) 53 | # assert shape obs x groups 54 | assert rel_nhood_profile.shape == ( 55 | adata_seqfish.n_obs, 56 | len(adata_seqfish.obs[GROUPS].cat.categories), 57 | ) 58 | assert abs_nhood_profile.shape == rel_nhood_profile.shape 59 | # normalization 60 | assert int(rel_nhood_profile.sum(axis=1).sum()) == adata_seqfish.n_obs 61 | assert round(rel_nhood_profile.sum(axis=1).max(), 2) == 1 62 | # maximum amount of categories equals n_neighbors 63 | assert abs_nhood_profile.sum(axis=1).max() == N_NEIGHBORS 64 | 65 | 66 | def test_niche_calc_utag(adata_seqfish: AnnData): 67 | """Check whether niche calculation using UTAG approach works as intended.""" 68 | spatial_neighbors(adata_seqfish, coord_type="generic", delaunay=False, n_neighs=N_NEIGHBORS) 69 | calculate_niche(adata_seqfish, flavor="utag", n_neighbors=N_NEIGHBORS, resolutions=[0.1, 1.0]) 70 | 71 | niches = adata_seqfish.obs["utag_niche_res=1.0"] 72 | niches_low_res = adata_seqfish.obs["utag_niche_res=0.1"] 73 | 74 | assert niches.isna().sum() == 0 75 | assert niches.nunique() > niches_low_res.nunique() 76 | 77 | # assert shape obs x var and sparsity in new feature matrix 78 | new_feature_matrix = _utag( 79 | adata_seqfish, 80 | normalize_adj=True, 81 | spatial_connectivity_key=SPATIAL_CONNECTIVITIES_KEY, 82 | ) 83 | assert new_feature_matrix.shape == adata_seqfish.X.shape 84 | assert issparse(new_feature_matrix) 85 | 86 | spatial_neighbors(adata_seqfish, coord_type="generic", delaunay=False, n_neighs=40) 87 | new_feature_matrix_more_neighs = _utag( 88 | adata_seqfish, 89 | normalize_adj=True, 90 | spatial_connectivity_key=SPATIAL_CONNECTIVITIES_KEY, 91 | ) 92 | 93 | # matrix products should differ when using different amount of neighbors 94 | try: 95 | assert_frame_equal(new_feature_matrix, new_feature_matrix_more_neighs) 96 | except AssertionError: 97 | pass 98 | else: 99 | raise AssertionError 100 | -------------------------------------------------------------------------------- /tests/graph/test_ripley.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pytest 5 | from anndata import AnnData 6 | 7 | from squidpy._constants._constants import RipleyStat 8 | from squidpy.gr import ripley 9 | 10 | CLUSTER_KEY = "leiden" 11 | 12 | 13 | @pytest.mark.parametrize("mode", list(RipleyStat)) 14 | def test_ripley_modes(adata_ripley: AnnData, mode: RipleyStat): 15 | adata = adata_ripley 16 | 17 | ripley(adata, cluster_key=CLUSTER_KEY, mode=mode.s) 18 | 19 | UNS_KEY = f"{CLUSTER_KEY}_ripley_{mode}" 20 | 21 | # assert uns 22 | assert UNS_KEY in adata.uns.keys() 23 | assert f"{mode}_stat" in adata.uns[UNS_KEY].keys() 24 | assert "sims_stat" in adata.uns[UNS_KEY].keys() 25 | assert "bins" in adata.uns[UNS_KEY].keys() 26 | assert "pvalues" in adata.uns[UNS_KEY].keys() 27 | 28 | obs_df = adata.uns[UNS_KEY][f"{mode}_stat"] 29 | sims_df = adata.uns[UNS_KEY]["sims_stat"] 30 | bins = adata.uns[UNS_KEY]["bins"] 31 | pvalues = adata.uns[UNS_KEY]["pvalues"] 32 | 33 | # assert shapes 34 | np.testing.assert_array_equal(adata.obs[CLUSTER_KEY].cat.categories, obs_df[CLUSTER_KEY].cat.categories) 35 | assert obs_df.shape[1] == sims_df.shape[1] 36 | assert pvalues.shape[0] == adata.obs[CLUSTER_KEY].cat.categories.shape[0] 37 | assert bins.shape[0] == 50 38 | 39 | 40 | @pytest.mark.parametrize("mode", list(RipleyStat)) 41 | @pytest.mark.parametrize( 42 | "n_simulations", 43 | [20, 50], 44 | ) 45 | @pytest.mark.parametrize( 46 | "n_observations", 47 | [10, 100], 48 | ) 49 | @pytest.mark.parametrize( 50 | "max_dist", 51 | [None, 1000], 52 | ) 53 | @pytest.mark.parametrize( 54 | "n_steps", 55 | [2, 50, 100], 56 | ) 57 | def test_ripley_results( 58 | adata_ripley: AnnData, mode: RipleyStat, n_simulations: int, n_observations: int, max_dist: np.float_, n_steps: int 59 | ): 60 | adata = adata_ripley 61 | n_clusters = adata.obs[CLUSTER_KEY].cat.categories.shape[0] 62 | 63 | res = ripley( 64 | adata, 65 | cluster_key=CLUSTER_KEY, 66 | mode=mode.s, 67 | n_simulations=n_simulations, 68 | n_observations=n_observations, 69 | max_dist=max_dist, 70 | n_steps=n_steps, 71 | copy=True, 72 | ) 73 | 74 | obs_df = res[f"{mode}_stat"] 75 | sims_df = res["sims_stat"] 76 | bins = res["bins"] 77 | pvalues = res["pvalues"] 78 | 79 | # assert shapes 80 | assert obs_df.shape == (n_steps * n_clusters, 3) 81 | assert bins.shape == (n_steps,) 82 | assert sims_df.shape == (n_steps * n_simulations, 3) 83 | assert pvalues.shape == (n_clusters, n_steps) 84 | 85 | # assert first values is 0 86 | assert sims_df.bins.values[0] == 0.0 87 | assert sims_df.bins.values[0] == obs_df.bins.values[0] 88 | assert sims_df.stats.values[0] == 0.0 89 | assert sims_df.stats.values[0] == obs_df.stats.values[0] 90 | 91 | # assert n_zeros == n_clusters 92 | idx = np.nonzero(obs_df.bins.values)[0] 93 | assert idx.shape[0] == n_steps * n_clusters - n_clusters 94 | -------------------------------------------------------------------------------- /tests/graph/test_sepal.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | from anndata import AnnData 5 | from pandas.testing import assert_frame_equal 6 | 7 | from squidpy.gr import sepal, spatial_neighbors 8 | 9 | UNS_KEY = "sepal_score" 10 | 11 | 12 | def test_sepal_seq_par(adata: AnnData): 13 | """Check whether sepal results are the same for seq. and parallel computation.""" 14 | spatial_neighbors(adata, coord_type="grid") 15 | rng = np.random.default_rng(42) 16 | adata.var["highly_variable"] = rng.choice([True, False], size=adata.var_names.shape, p=[0.005, 0.995]) 17 | 18 | sepal(adata, max_neighs=6) 19 | df = sepal(adata, max_neighs=6, copy=True, n_jobs=1) 20 | df_parallel = sepal(adata, max_neighs=6, copy=True, n_jobs=2) 21 | 22 | idx_df = df.index.values 23 | idx_adata = adata[:, adata.var.highly_variable.values].var_names.values 24 | 25 | assert UNS_KEY in adata.uns.keys() 26 | assert df.columns.shape == (1,) 27 | # test highly variable 28 | assert adata.uns[UNS_KEY].shape == df.shape 29 | # assert idx are sorted and contain same elements 30 | assert not np.array_equal(idx_df, idx_adata) 31 | np.testing.assert_array_equal(sorted(idx_df), sorted(idx_adata)) 32 | # check parallel gives same results 33 | assert_frame_equal(df, df_parallel) 34 | 35 | 36 | def test_sepal_square_seq_par(adata_squaregrid: AnnData): 37 | """Test sepal for square grid.""" 38 | adata = adata_squaregrid 39 | spatial_neighbors(adata, radius=1.0) 40 | rng = np.random.default_rng(42) 41 | adata.var["highly_variable"] = rng.choice([True, False], size=adata.var_names.shape) 42 | 43 | sepal(adata, max_neighs=4) 44 | df_parallel = sepal(adata, copy=True, n_jobs=2, max_neighs=4) 45 | 46 | idx_df = df_parallel.index.values 47 | idx_adata = adata[:, adata.var.highly_variable.values].var_names.values 48 | 49 | assert UNS_KEY in adata.uns.keys() 50 | assert df_parallel.columns.shape == (1,) 51 | # test highly variable 52 | assert adata.uns[UNS_KEY].shape == df_parallel.shape 53 | # assert idx are sorted and contain same elements 54 | assert not np.array_equal(idx_df, idx_adata) 55 | np.testing.assert_array_equal(sorted(idx_df), sorted(idx_adata)) 56 | # check parallel gives same results 57 | assert_frame_equal(adata.uns[UNS_KEY], df_parallel) 58 | -------------------------------------------------------------------------------- /tests/graph/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pandas as pd 5 | import pytest 6 | from anndata import AnnData 7 | 8 | from squidpy._constants._pkg_constants import Key 9 | from squidpy.gr._utils import _shuffle_group 10 | 11 | 12 | class TestUtils: 13 | @pytest.mark.parametrize("cluster_annotations_type", [int, str]) 14 | @pytest.mark.parametrize("library_annotations_type", [int, str]) 15 | @pytest.mark.parametrize("seed", [422, 422222]) 16 | def test_shuffle_group(self, cluster_annotations_type: type, library_annotations_type: type, seed: int): 17 | size = 6 18 | rng = np.random.default_rng(seed) 19 | if isinstance(cluster_annotations_type, int): 20 | libraries = pd.Series(rng.choice([1, 2, 3, 4], size=(size,)), dtype="category") 21 | else: 22 | libraries = pd.Series(rng.choice(["a", "b", "c"], size=(size,)), dtype="category") 23 | 24 | if isinstance(library_annotations_type, int): 25 | cluster_annotations = rng.choice([1, 2, 3, 4], size=(size,)) 26 | else: 27 | cluster_annotations = rng.choice(["X", "Y", "Z"], size=(size,)) 28 | out = _shuffle_group(cluster_annotations, libraries, rng) 29 | for c in libraries.cat.categories: 30 | assert set(out[libraries == c]) == set(cluster_annotations[libraries == c]) 31 | -------------------------------------------------------------------------------- /tests/image/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/image/__init__.py -------------------------------------------------------------------------------- /tests/image/test_io.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import dask.array as da 4 | import numpy as np 5 | import pytest 6 | import tifffile 7 | import xarray as xr 8 | from pytest_mock import MockerFixture 9 | from skimage.io import imread 10 | 11 | from squidpy._constants._constants import InferDimensions 12 | from squidpy.im._io import _get_image_shape_dtype, _infer_dimensions, _lazy_load_image 13 | 14 | 15 | class TestIO: 16 | @staticmethod 17 | def _create_image(path: str, shape: tuple[int, ...]): 18 | dtype = np.uint8 if len(shape) <= 3 else np.float32 19 | img = np.random.randint(0, 255, size=shape).astype(dtype) 20 | 21 | try: 22 | # Old usage (works up to tifffile<2025.2.18) 23 | photometric = tifffile.TIFF.PHOTOMETRIC.MINISBLACK 24 | except AttributeError: 25 | # New usage (works on tifffile >=2025.2.18) 26 | photometric = "MINISBLACK" 27 | 28 | tifffile.imwrite(path, img, photometric=photometric) 29 | 30 | return img 31 | 32 | @pytest.mark.parametrize( 33 | "shape", 34 | [ 35 | (101, 64), 36 | (3, 64, 101), 37 | (64, 101, 4), 38 | (1, 101, 64, 1), 39 | (1, 101, 64, 3), 40 | (3, 101, 64, 1), 41 | (3, 101, 64, 4), 42 | ], 43 | ) 44 | def test_get_shape(self, shape: tuple[int, ...], tmpdir): 45 | path = str(tmpdir / "img.tiff") 46 | img = self._create_image(path, shape) 47 | 48 | actual_shape, actual_dtype = _get_image_shape_dtype(path) 49 | np.testing.assert_array_equal(actual_shape, shape) 50 | assert actual_dtype == img.dtype, (actual_dtype, img.dtype) 51 | 52 | @pytest.mark.parametrize("infer_dim", list(InferDimensions)) 53 | @pytest.mark.parametrize( 54 | "shape", [(101, 64), (101, 64, 3), (3, 64, 101), (1, 101, 64, 3), (1, 101, 64, 1), (3, 101, 64, 1)] 55 | ) 56 | def test_infer_dimensions(self, shape: tuple[int, ...], infer_dim: str, mocker: MockerFixture): 57 | mocker.patch("squidpy.im._io._get_image_shape_dtype", return_value=(shape, np.uint8)) 58 | infer_dim = InferDimensions(infer_dim) 59 | actual_shape, actual_dims, _, _ = _infer_dimensions("non_existent", infer_dim) 60 | 61 | if len(shape) == 2: 62 | np.testing.assert_array_equal(actual_dims, ["y", "x", "z", "channels"]) 63 | np.testing.assert_array_equal(actual_shape, shape + (1, 1)) 64 | elif len(shape) == 3: 65 | if shape[-1] == 3: 66 | if infer_dim == InferDimensions.Z_LAST: 67 | np.testing.assert_array_equal(actual_dims, ["channels", "y", "x", "z"]) 68 | else: 69 | np.testing.assert_array_equal(actual_dims, ["z", "y", "x", "channels"]) 70 | np.testing.assert_array_equal(actual_shape, (1,) + shape) 71 | else: 72 | if infer_dim == InferDimensions.Z_LAST: 73 | np.testing.assert_array_equal(actual_dims, ["z", "y", "x", "channels"]) 74 | else: 75 | np.testing.assert_array_equal(actual_dims, ["channels", "y", "x", "z"]) 76 | np.testing.assert_array_equal(actual_shape, shape + (1,)) 77 | elif len(shape) == 4: 78 | if infer_dim == InferDimensions.DEFAULT: 79 | if shape[0] == 1: 80 | np.testing.assert_array_equal(actual_dims, ["z", "y", "x", "channels"]) 81 | elif shape[-1] == 1: 82 | np.testing.assert_array_equal(actual_dims, ["channels", "y", "x", "z"]) 83 | else: 84 | np.testing.assert_array_equal(actual_dims, ["z", "y", "x", "channels"]) 85 | elif infer_dim == InferDimensions.Z_LAST: 86 | np.testing.assert_array_equal(actual_dims, ["channels", "y", "x", "z"]) 87 | else: 88 | np.testing.assert_array_equal(actual_dims, ["z", "y", "x", "channels"]) 89 | np.testing.assert_array_equal(actual_shape, shape) 90 | 91 | @pytest.mark.parametrize("chunks", [100, (1, 100, 100, 3), "auto", None, {"y": 100, "x": 100}]) 92 | def test_lazy_load_image(self, chunks: int | tuple[int, ...] | str | dict[str, int] | None, tmpdir): 93 | path = str(tmpdir / "img.tiff") 94 | img = self._create_image(path, (256, 256, 3)) 95 | 96 | res = _lazy_load_image(path, chunks=chunks) 97 | 98 | assert isinstance(res, xr.DataArray) 99 | assert isinstance(res.data, da.Array) 100 | if chunks not in (None, "auto"): 101 | np.testing.assert_array_equal(res.data.chunksize, [100, 100, 1, 3]) 102 | 103 | np.testing.assert_array_equal(img, np.squeeze(res.values)) 104 | 105 | @pytest.mark.parametrize("n", [0, 1, 2, 3, 5]) 106 | def test_explicit_dimension_mismatch(self, tmpdir, n: int): 107 | path = str(tmpdir / "img.tiff") 108 | _ = self._create_image(path, (5, 100, 100, 2)) 109 | 110 | with pytest.raises(ValueError, match="Image is `4` dimensional"): 111 | _ = _lazy_load_image(path, dims=tuple(str(i) for i in range(n))) 112 | 113 | @pytest.mark.parametrize("n", [3, 4, 5]) 114 | def test_read_tiff_skimage(self, tmpdir, n: int): 115 | path = str(tmpdir / "img.tiff") 116 | img = self._create_image(path, (n, 100, 100)) 117 | 118 | res = _lazy_load_image(path, dims=("c", "y", "x")) 119 | res_skimage = imread(path, plugin="tifffile") 120 | 121 | assert isinstance(res, xr.DataArray) 122 | assert isinstance(res_skimage, np.ndarray) 123 | 124 | np.testing.assert_array_equal(img, res.transpose("c", "y", "x", ...).values.squeeze(-1)) 125 | if n in (3, 4): 126 | np.testing.assert_array_equal(res_skimage.shape, [100, 100, n]) 127 | with pytest.raises(AssertionError, match="Arrays are not equal"): 128 | np.testing.assert_array_equal(res_skimage.shape, np.squeeze(res.shape)) 129 | else: 130 | np.testing.assert_array_equal(res_skimage, res.transpose("c", "y", "x", ...).values.squeeze(-1)) 131 | -------------------------------------------------------------------------------- /tests/plotting/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/plotting/__init__.py -------------------------------------------------------------------------------- /tests/plotting/test_image.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | import pandas as pd 6 | import pytest 7 | import scanpy as sc 8 | from anndata import AnnData 9 | from matplotlib.testing.compare import compare_images 10 | 11 | import squidpy as sq 12 | from squidpy.im import ImageContainer 13 | from tests.conftest import ACTUAL, DPI, EXPECTED, TOL, PlotTester, PlotTesterMeta 14 | 15 | 16 | class TestContainerShow(PlotTester, metaclass=PlotTesterMeta): 17 | def test_channelwise_wrong_number_of_axes(self, cont: ImageContainer): 18 | fig, ax = plt.subplots(dpi=DPI, tight_layout=True) 19 | with pytest.raises(ValueError, match=r"Expected `ax` to be of shape `\(1, 3\)`"): 20 | cont.show(ax=ax, channelwise=True) 21 | 22 | def test_plot_axis(self, cont: ImageContainer): 23 | cont.add_img(np.random.RandomState(42).normal(size=(*cont.shape, 3)), layer="foo") 24 | fig, (ax1, ax2) = plt.subplots(ncols=2, dpi=DPI, tight_layout=True) 25 | 26 | cont.show("image", ax=ax1) 27 | cont.show("foo", ax=ax2) 28 | 29 | def test_plot_channel(self, cont: ImageContainer): 30 | cont.show(channel=1, dpi=DPI) 31 | 32 | def test_plot_library_id(self, small_cont_4d: ImageContainer): 33 | small_cont_4d.show(library_id=["1"], dpi=DPI) 34 | 35 | def test_plot_segmentation(self, cont: ImageContainer): 36 | seg = np.random.RandomState(43).randint(0, 255, size=(*cont.shape, 1)) 37 | seg[seg <= 200] = 0 38 | cont.add_img(seg, layer="foo") 39 | cont["foo"].attrs["segmentation"] = True 40 | 41 | cont.show("image", segmentation_layer="foo", dpi=DPI) 42 | 43 | def test_plot_imshow_kwargs(self, cont: ImageContainer): 44 | cont.show(channel=2, cmap="inferno", dpi=DPI) 45 | 46 | def test_plot_channelwise(self, cont: ImageContainer): 47 | cont.show(channelwise=True, dpi=DPI) 48 | 49 | def test_plot_channelwise_segmentation(self, cont: ImageContainer): 50 | seg = np.random.RandomState(43).randint(0, 255, size=(*cont.shape, 1)) 51 | seg[seg <= 200] = 0 52 | cont.add_img(seg, layer="foo") 53 | cont["foo"].attrs["segmentation"] = True 54 | 55 | cont.show("image", channelwise=True, segmentation_layer="foo", dpi=DPI, segmentation_alpha=1) 56 | 57 | def test_plot_scale_mask_circle_crop(self, cont: ImageContainer): 58 | cont.crop_corner(0, 0, (200, 200), mask_circle=True, scale=2).show(dpi=DPI) 59 | 60 | @pytest.mark.parametrize("channelwise", [False, True]) 61 | @pytest.mark.parametrize("transpose", [False, True]) 62 | def test_transpose_channelwise(self, small_cont_4d: ImageContainer, transpose: bool, channelwise: bool): 63 | basename = f"{self.__class__.__name__[4:]}_transpose_channelwise_{transpose}_{channelwise}.png" 64 | small_cont_4d.show(transpose=transpose, channelwise=channelwise, dpi=DPI) 65 | 66 | plt.savefig(ACTUAL / basename, dpi=DPI) 67 | plt.close() 68 | res = compare_images(str(EXPECTED / basename), ACTUAL / basename, TOL) 69 | 70 | assert res is None, res 71 | 72 | 73 | @pytest.mark.parametrize("is_view", [False, True]) 74 | def test_extract(adata: AnnData, cont: ImageContainer, caplog, is_view: bool): 75 | sq.im.calculate_image_features(adata, cont, features=["summary"]) 76 | 77 | # extract columns (default values) 78 | extr_adata = sq.pl.extract(adata[:10] if is_view else adata) 79 | # Test that expected columns exist 80 | for col in [ 81 | "summary_ch-0_quantile-0.9", 82 | "summary_ch-0_quantile-0.5", 83 | "summary_ch-0_quantile-0.1", 84 | "summary_ch-1_quantile-0.9", 85 | "summary_ch-1_quantile-0.5", 86 | "summary_ch-1_quantile-0.1", 87 | "summary_ch-2_quantile-0.9", 88 | "summary_ch-2_quantile-0.5", 89 | "summary_ch-2_quantile-0.1", 90 | ]: 91 | np.testing.assert_array_equal(np.isfinite(extr_adata.obs[col]), True) 92 | 93 | # get obsm that is a numpy array 94 | adata.obsm["pca_features"] = sc.pp.pca(np.asarray(adata.obsm["img_features"]), n_comps=3) 95 | # extract columns 96 | extr_adata = sq.pl.extract(adata[3:10] if is_view else adata, obsm_key="pca_features", prefix="pca_features") 97 | # Test that expected columns exist 98 | for col in ["pca_features_0", "pca_features_1", "pca_features_2"]: 99 | np.testing.assert_array_equal(np.isfinite(extr_adata.obs[col]), True) 100 | 101 | # extract multiple obsm at once (no prefix) 102 | extr_adata = sq.pl.extract(adata, obsm_key=["img_features", "pca_features"]) 103 | # Test that expected columns exist 104 | for col in [ 105 | "summary_ch-0_quantile-0.9", 106 | "summary_ch-0_quantile-0.5", 107 | "summary_ch-0_quantile-0.1", 108 | "summary_ch-1_quantile-0.9", 109 | "summary_ch-1_quantile-0.5", 110 | "summary_ch-1_quantile-0.1", 111 | "summary_ch-2_quantile-0.9", 112 | "summary_ch-2_quantile-0.5", 113 | "summary_ch-2_quantile-0.1", 114 | "0", 115 | "1", 116 | "2", 117 | ]: 118 | np.testing.assert_array_equal(np.isfinite(extr_adata.obs[col]), True) 119 | -------------------------------------------------------------------------------- /tests/plotting/test_var_by_distance_plot.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import scanpy as sc 4 | from anndata import AnnData 5 | 6 | from squidpy import pl, tl 7 | from tests.conftest import PlotTester, PlotTesterMeta 8 | 9 | sc.pl.set_rcParams_defaults() 10 | sc.set_figure_params(dpi=40) 11 | 12 | # WARNING: 13 | # 1. all classes must both subclass PlotTester and use metaclass=PlotTesterMeta 14 | # 2. tests which produce a plot must be prefixed with `test_plot_` 15 | # 3. if the tolerance needs to be change, don't prefix the function with `test_plot_`, but with something else 16 | # the comp. function can be accessed as `self.compare(, tolerance=)` 17 | # ".png" is appended to , no need to set it 18 | 19 | 20 | class TestVarDist(PlotTester, metaclass=PlotTesterMeta): 21 | def test_tol_plot_var_by_distance(self, adata_mibitof: AnnData): 22 | tl.var_by_distance( 23 | adata_mibitof, 24 | cluster_key="Cluster", 25 | groups="Epithelial", 26 | library_key="point", 27 | ) 28 | pl.var_by_distance( 29 | adata=adata_mibitof, 30 | design_matrix_key="design_matrix", 31 | var="HK1", 32 | anchor_key="Epithelial", 33 | color="HK1", 34 | figsize=(5, 4), 35 | ) 36 | self.compare("var_by_distance_single_anchor_and_gene") # tolerance added due to numerical errors of spline 37 | 38 | def test_tol_plot_var_by_distance_with_covariate(self, adata_mibitof: AnnData): 39 | tl.var_by_distance( 40 | adata_mibitof, cluster_key="Cluster", groups="Epithelial", library_key="point", covariates="donor" 41 | ) 42 | pl.var_by_distance( 43 | adata=adata_mibitof, 44 | design_matrix_key="design_matrix", 45 | var="IDH2", 46 | anchor_key="Epithelial", 47 | covariate="donor", 48 | figsize=(5, 4), 49 | ) 50 | self.compare( 51 | "var_by_distance_single_anchor_and_gene_two_categories" 52 | ) # tolerance added due to numerical errors of spline 53 | 54 | def test_tol_plot_var_by_distance_various_palettes(self, adata_mibitof: AnnData): 55 | tl.var_by_distance( 56 | adata_mibitof, cluster_key="Cluster", groups="Epithelial", library_key="point", covariates="donor" 57 | ) 58 | pl.var_by_distance( 59 | adata=adata_mibitof, 60 | design_matrix_key="design_matrix", 61 | var=["IDH2", "H3", "vimentin", "CD98"], 62 | anchor_key="Epithelial", 63 | color="Cluster", 64 | covariate="donor", 65 | scatter_palette="plasma", 66 | line_palette=["red", "blue"], 67 | figsize=(10, 4), 68 | ) 69 | self.compare( 70 | "var_by_distance_single_anchor_four_genes_two_categories_two_palettes" 71 | ) # tolerance added due to numerical errors of spline 72 | 73 | def test_tol_plot_var_by_distance_without_scatter(self, adata_mibitof: AnnData): 74 | tl.var_by_distance( 75 | adata_mibitof, cluster_key="Cluster", groups="Epithelial", library_key="point", covariates="donor" 76 | ) 77 | pl.var_by_distance( 78 | adata=adata_mibitof, 79 | design_matrix_key="design_matrix", 80 | var="CD98", 81 | anchor_key="Epithelial", 82 | covariate="donor", 83 | line_palette=["blue", "orange"], 84 | show_scatter=False, 85 | figsize=(5, 4), 86 | ) 87 | self.compare( 88 | "var_by_distance_single_anchor_one_gene_two_categories_without_scatter" 89 | ) # tolerance added due to numerical errors of spline 90 | -------------------------------------------------------------------------------- /tests/read/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/read/__init__.py -------------------------------------------------------------------------------- /tests/read/test_visium.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from anndata.tests.helpers import assert_adata_equal 4 | 5 | from squidpy._constants._pkg_constants import Key 6 | from squidpy.read import visium 7 | 8 | 9 | def test_read_visium(): 10 | # Test that reading .h5 file works and does not have any global effects. 11 | h5_file_path = "tests/_data" 12 | spec_genome_v3 = visium(h5_file_path, genome="GRCh38", load_images=True) 13 | nospec_genome_v3 = visium(h5_file_path) 14 | assert_adata_equal(spec_genome_v3, nospec_genome_v3) 15 | adata = spec_genome_v3 16 | assert "spatial" in adata.uns.keys() 17 | lib_id = list(adata.uns[Key.uns.spatial].keys())[0] 18 | assert Key.uns.image_key in adata.uns[Key.uns.spatial][lib_id].keys() 19 | assert Key.uns.scalefactor_key in adata.uns[Key.uns.spatial][lib_id].keys() 20 | assert Key.uns.image_res_key in adata.uns[Key.uns.spatial][lib_id][Key.uns.image_key] 21 | assert Key.uns.size_key in adata.uns[Key.uns.spatial][lib_id][Key.uns.scalefactor_key] 22 | 23 | 24 | def test_read_text(): 25 | text_file_path = "tests/_data/" 26 | text_file = "spatial/tissue_positions_list.csv" 27 | adata1 = visium(path=text_file_path, counts_file=text_file, library_id="foo") 28 | adata2 = visium(path=text_file_path, counts_file=text_file, library_id="foo") 29 | assert_adata_equal(adata1, adata2) 30 | -------------------------------------------------------------------------------- /tests/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scverse/squidpy/c3277fb1509a36cd6d4e9de47a5c504e58853605/tests/tools/__init__.py -------------------------------------------------------------------------------- /tests/tools/test_var_by_distance.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from anndata import AnnData 5 | 6 | from squidpy.tl import var_by_distance 7 | from squidpy.tl._var_by_distance import _normalize_distances 8 | 9 | 10 | class TestVarDist: 11 | @pytest.mark.parametrize("groups", ["Endothelial", ["Endothelial", "Epithelial", "Fibroblast"]]) 12 | @pytest.mark.parametrize("cluster_key", ["Cluster"]) 13 | @pytest.mark.parametrize("covariates", ["category", ["category", "cell_size"], None]) 14 | @pytest.mark.parametrize("library_key", ["point"]) 15 | def test_design_matrix_several_slides( 16 | self, 17 | adata_mibitof: AnnData, 18 | groups: str | list[str], 19 | cluster_key: str, 20 | library_key: str | None, 21 | covariates: str | list[str] | None, 22 | ): 23 | df = var_by_distance( 24 | adata_mibitof, 25 | cluster_key=cluster_key, 26 | groups=groups, 27 | library_key=library_key, 28 | covariates=covariates, 29 | copy=True, 30 | ) 31 | 32 | if not isinstance(groups, list): 33 | groups = [groups] 34 | if not isinstance(covariates, list) and covariates is not None: 35 | covariates = [covariates] 36 | n_covariates = len(covariates) if covariates is not None else 0 37 | slides = 1 if isinstance(library_key, str) else 0 38 | 39 | assert len(df) == adata_mibitof.n_obs # correct amount of rows 40 | assert len(df.columns) == 1 + 2 * len(groups) + slides + n_covariates # correct amount of columns 41 | 42 | for anchor in groups: 43 | assert df[anchor].min() == 0 and df[anchor].max() <= 1 # correct normalized range 44 | assert df.Cluster.dtype.name == "category" # correct dtype 45 | 46 | if library_key is not None: 47 | assert df[library_key].nunique() > 1 # more than one slide 48 | 49 | if covariates is not None: 50 | assert df[covariates].equals( 51 | adata_mibitof.obs[covariates] 52 | ) # covariate column in design matrix match covariate column in .obs 53 | 54 | for anchor in groups: 55 | anchor_ids_adata = adata_mibitof.obs.index[adata_mibitof.obs[cluster_key] == anchor].tolist() 56 | anchor_ids_design_matrix = df.index[df["Cluster"] == anchor].tolist() 57 | zero_dist_ids = df.index[df[f"{anchor}_raw"] == 0.0].tolist() 58 | nan_ids = df.index[df[anchor].isna()].tolist() 59 | 60 | assert anchor_ids_adata == anchor_ids_design_matrix # anchor point indices match before and after 61 | assert anchor_ids_adata == zero_dist_ids # anchor point indices have zero values in anchor_raw column 62 | assert ( 63 | nan_ids <= zero_dist_ids 64 | ) # zero value indices must be subset of indices with NaN values in anchor column 65 | 66 | @pytest.mark.parametrize("groups", ["Spinal cord"]) 67 | @pytest.mark.parametrize("cluster_key", ["celltype_mapped_refined"]) 68 | @pytest.mark.parametrize("library_key", [None]) 69 | def test_design_matrix_single_slide( 70 | self, 71 | adata_seqfish: AnnData, 72 | groups: str, 73 | cluster_key: str, 74 | library_key: str | None, 75 | ): 76 | df = var_by_distance(adata_seqfish, groups=groups, cluster_key=cluster_key, library_key=library_key, copy=True) 77 | 78 | assert len(df) == adata_seqfish.n_obs # correct amount of rows 79 | assert len(df.columns) == len([groups]) * 2 + len([cluster_key]) # correct amount of columns 80 | 81 | anchor_ids = adata_seqfish.obs.index[adata_seqfish.obs[cluster_key] == groups] 82 | nan_ids = df.index[df[groups].isna()] 83 | 84 | assert anchor_ids.equals(nan_ids) # nan ids match anchor point ids 85 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = test_*.py 3 | testpaths = tests/ 4 | xfail_strict = true 5 | ; qt_api=pyqt5 6 | addopts = 7 | --ignore=tests/plotting/test_interactive.py 8 | 9 | filterwarnings = 10 | ignore::UserWarning 11 | ignore::anndata.OldFormatWarning 12 | ignore:.*pkg_resources:DeprecationWarning 13 | 14 | [coverage:run] 15 | branch = true 16 | parallel = true 17 | source = squidpy 18 | omit = 19 | */__init__.py 20 | */_version.py 21 | squidpy/pl/_interactive/* 22 | 23 | [coverage:paths] 24 | source = 25 | squidpy 26 | */site-packages/squidpy 27 | 28 | [coverage:report] 29 | exclude_lines = 30 | \#.*pragma:\s*no.?cover 31 | 32 | ^if __name__ == .__main__.:$ 33 | 34 | ^\s*raise AssertionError\b 35 | ^\s*raise NotImplementedError\b 36 | ^\s*return NotImplemented\b 37 | show_missing = true 38 | precision = 2 39 | skip_empty = True 40 | sort = Miss 41 | 42 | [gh-actions] 43 | python = 44 | 3.10: py3.10 45 | 3.11: py3.11 46 | 3.12: py3.12 47 | 48 | [gh-actions:env] 49 | PLATFORM = 50 | ubuntu-latest: linux 51 | macos-latest: macos 52 | 53 | [tox] 54 | isolated_build = True 55 | envlist = 56 | covclean 57 | py3.10-linux 58 | py3.11-linux 59 | py3.12-linux 60 | py3.12-macos 61 | coverage 62 | readme 63 | check-docs 64 | docs 65 | skip_missing_interpreters = true 66 | 67 | [testenv] 68 | platform = 69 | linux: linux 70 | macos: (osx|darwin) 71 | deps = 72 | pytest 73 | pytest-xdist 74 | pytest-cov 75 | ; pytest-qt 76 | pytest-mock 77 | pytest-timeout 78 | # see: https://github.com/numba/llvmlite/issues/669 79 | extras = 80 | interactive 81 | test 82 | setenv = linux: PYTEST_FLAGS=--test-napari 83 | passenv = TOXENV,CI,CODECOV_*,GITHUB_ACTIONS,PYTEST_FLAGS,DISPLAY,XAUTHORITY,MPLBACKEND 84 | usedevelop = true 85 | commands = 86 | python -m pytest --color=yes --cov --cov-append --cov-report=xml --cov-config={toxinidir}/tox.ini --ignore docs/ {posargs:-vv} {env:PYTEST_FLAGS:} 87 | 88 | [testenv:covclean] 89 | description = Clean coverage files. 90 | deps = coverage 91 | skip_install = True 92 | commands = coverage erase 93 | 94 | [testenv:coverage] 95 | description = Report the coverage difference. 96 | deps = 97 | coverage 98 | diff_cover 99 | skip_install = true 100 | depends = py3.10-linux, py3.11-linux, py3.12-linux, py3.12-macos 101 | parallel_show_output = True 102 | commands = 103 | coverage report --omit="tox/*" 104 | coverage xml --omit="tox/*" -o {toxinidir}/coverage.xml 105 | diff-cover --compare-branch origin/main {toxinidir}/coverage.xml 106 | 107 | [testenv:clean-docs] 108 | description = Clean the documentation artifacts. 109 | deps = 110 | skip_install = true 111 | changedir = {toxinidir}/docs 112 | allowlist_externals = make 113 | commands = make clean 114 | 115 | [testenv:check-docs] 116 | description = Lint the documentation. 117 | deps = 118 | extras = docs 119 | ignore_errors = true 120 | allowlist_externals = make 121 | pass_env = PYENCHANT_LIBRARY_PATH 122 | set_env = SPHINXOPTS = -W -q --keep-going 123 | changedir = {tox_root}{/}docs 124 | commands = 125 | make linkcheck {posargs} 126 | 127 | [testenv:docs] 128 | description = Build the documentation. 129 | deps = 130 | extras = docs 131 | allowlist_externals = make 132 | changedir = {tox_root}{/}docs 133 | commands = 134 | make html {posargs} 135 | commands_post = 136 | python -c 'import pathlib; print("Documentation is under:", pathlib.Path("{tox_root}") / "docs" / "_build" / "html" / "index.html")' 137 | 138 | [testenv:download-data] 139 | description = Download and cache data. 140 | skip_install = false 141 | deps = 142 | commands = python ./.scripts/ci/download_data.py {posargs} 143 | --------------------------------------------------------------------------------