From 3158ce8c768033bafebef0334ae8bed96944c705 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 2 Apr 2024 14:51:56 +0200 Subject: [PATCH 01/58] (refactor): separate out schema creation --- vitessce/wrappers.py | 165 ++++++++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 72 deletions(-) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 19b0d460..fe1e0321 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1,3 +1,4 @@ +from functools import partialmethod import os from os.path import join import tempfile @@ -986,82 +987,102 @@ def get_zarr_url(self, base_url="", dataset_uid="", obj_i=""): return self._adata_url else: return self.get_local_dir_url(base_url, dataset_uid, obj_i, self._adata_path, self.local_dir_uid) + + @staticmethod + def _gen_obs_embedding_schema(options, paths=None, names=None, dims=None): + if paths is not None: + if names is not None: + for key, mapping in zip(paths, names): + options["obsEmbedding"].append({ + "path": mapping, + "dims": [0, 1], + "embeddingType": key + }) + else: + for mapping in paths: + mapping_key = mapping.split('/')[-1] + options["obsEmbedding"].append({ + "path": mapping, + "dims": [0, 1], + "embeddingType": mapping_key + }) + if dims is not None: + for dim_i, dim in enumerate(dims): + options["obsEmbedding"][dim_i]['dims'] = dim + return options + + @staticmethod + def _gen_obs_sets_schema(options, paths, names): + if paths is not None: + options["obsSets"] = [] + if names is not None: + names = names + else: + names = [obs.split('/')[-1] for obs in paths] + for obs, name in zip(paths, names): + options["obsSets"].append({ + "name": name, + "path": obs + }) + return options + + @staticmethod + def _gen_obs_feature_matrix_schema(options, matrix_path, var_filter_path, init_var_filter_path): + if matrix_path is not None: + options["obsFeatureMatrix"] = { + "path": matrix_path + } + if var_filter_path is not None: + options["obsFeatureMatrix"]["featureFilterPath"] = var_filter_path + if init_var_filter_path is not None: + options["obsFeatureMatrix"]["initialFeatureFilterPath"] = init_var_filter_path + return options + + @staticmethod + def _gen_obs_labels_schema(options, paths, names): + if paths is not None: + if names is not None and len(paths) == len(names): + # A name was provided for each path element, so use those values. + names = names + else: + # Names were not provided for each path element, + # so fall back to using the final part of each path for the names. + names = [labels_path.split('/')[-1] for labels_path in paths] + obs_labels = [] + for path, name in zip(paths, names): + obs_labels.append({"path": path, "obsLabelsType": name}) + options["obsLabels"] = obs_labels + return options + + + @staticmethod + def _gen_path_schema(key, path, options): + if path is not None: + options[key] = { + "path": path + } + return options + + _gen_obs_locations_schema = partialmethod(_gen_path_schema, "obsLocations") + _gen_obs_segmentations_schema = partialmethod(_gen_path_schema, "obsSegmentations") + _gen_obs_spots_schema = partialmethod(_gen_path_schema, "obsSpots") + _gen_obs_points_schema = partialmethod(_gen_path_schema, "obsPoints") + _gen_feature_labels_schema = partialmethod(_gen_path_schema, "featureLabels") + def make_file_def_creator(self, dataset_uid, obj_i): def get_anndata_zarr(base_url): options = {} - if self._spatial_centroid_obsm is not None: - options["obsLocations"] = { - "path": self._spatial_centroid_obsm - } - if self._spatial_polygon_obsm is not None: - options["obsSegmentations"] = { - "path": self._spatial_polygon_obsm - } - if self._spatial_spots_obsm is not None: - options["obsSpots"] = { - "path": self._spatial_spots_obsm - } - if self._spatial_points_obsm is not None: - options["obsPoints"] = { - "path": self._spatial_points_obsm - } - if self._mappings_obsm is not None: - options["obsEmbedding"] = [] - if self._mappings_obsm_names is not None: - for key, mapping in zip(self._mappings_obsm_names, self._mappings_obsm): - options["obsEmbedding"].append({ - "path": mapping, - "dims": [0, 1], - "embeddingType": key - }) - else: - for mapping in self._mappings_obsm: - mapping_key = mapping.split('/')[-1] - self._mappings_obsm_names = mapping_key - options["obsEmbedding"].append({ - "path": mapping, - "dims": [0, 1], - "embeddingType": mapping_key - }) - if self._mappings_obsm_dims is not None: - for dim_i, dim in enumerate(self._mappings_obsm_dims): - options["obsEmbedding"][dim_i]['dims'] = dim - if self._cell_set_obs is not None: - options["obsSets"] = [] - if self._cell_set_obs_names is not None: - names = self._cell_set_obs_names - else: - names = [obs.split('/')[-1] for obs in self._cell_set_obs] - for obs, name in zip(self._cell_set_obs, names): - options["obsSets"].append({ - "name": name, - "path": obs - }) - if self._expression_matrix is not None: - options["obsFeatureMatrix"] = { - "path": self._expression_matrix - } - if self._gene_var_filter is not None: - options["obsFeatureMatrix"]["featureFilterPath"] = self._gene_var_filter - if self._matrix_gene_var_filter is not None: - options["obsFeatureMatrix"]["initialFeatureFilterPath"] = self._matrix_gene_var_filter - if self._gene_alias is not None: - options["featureLabels"] = { - "path": self._gene_alias - } - if self._obs_labels_paths is not None: - if self._obs_labels_names is not None and len(self._obs_labels_paths) == len(self._obs_labels_names): - # A name was provided for each path element, so use those values. - names = self._obs_labels_names - else: - # Names were not provided for each path element, - # so fall back to using the final part of each path for the names. - names = [labels_path.split('/')[-1] for labels_path in self._obs_labels_paths] - obs_labels = [] - for path, name in zip(self._obs_labels_paths, names): - obs_labels.append({"path": path, "obsLabelsType": name}) - options["obsLabels"] = obs_labels + options = self._gen_obs_locations_schema(self._spatial_centroid_obsm, options) + options = self._gen_obs_segmentations_schema(self._spatial_polygon_obsm, options) + options = self._gen_obs_spots_schema(self._spatial_spots_obsm, options) + options = self._gen_obs_points_schema(self._spatial_points_obsm, options) + options = self._gen_obs_embedding_schema(options, self._mappings_obsm, self._mappings_obsm_names, self._mappings_obsm_dims) + options = self._gen_obs_sets_schema(options, self._cell_set_obs, self._cell_set_obs_names) + options = self._gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) + options = self._gen_feature_labels_schema(self._gene_alias, options) + options = self._gen_obs_labels_schema(options, self._obs_labels_paths, self._obs_labels_names) + if len(options.keys()) > 0: obj_file_def = { "fileType": ft.ANNDATA_ZARR.value, From 9bef112d1e8ef70d8c4b83d45ae26afeadc24e34 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 2 Apr 2024 16:22:40 +0200 Subject: [PATCH 02/58] (feat): initial pass at spatial data --- vitessce/constants.py | 1 + vitessce/wrappers.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/vitessce/constants.py b/vitessce/constants.py index 5ca75390..df3503bf 100644 --- a/vitessce/constants.py +++ b/vitessce/constants.py @@ -162,6 +162,7 @@ class FileType(DocEnum): An enum type representing the file format or schema to which a file conforms. """ ANNDATA_ZARR = "anndata.zarr", "Joint file type for AnnData objects" + SPATIALDATA_ZARR = "spatialdata.zarr", "Joint file type for SpatialData objects" OBS_EMBEDDING_CSV = 'obsEmbedding.csv', "File type for obsEmbedding values stored in a CSV file" OBS_LOCATIONS_CSV = 'obsLocations.csv', "File type for obsLocations values stored in a CSV file" OBS_LABELS_CSV = 'obsLabels.csv', "File type for obsLabels values stored in a CSV file" diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index fe1e0321..8887d434 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1113,6 +1113,49 @@ def auto_view_config(self, vc): else: vc.layout((scatterplot | (cell_sets / genes)) / heatmap) + +class SpatialDataWrapper(AnnDataWrapper): + + def __init__(self, path, *args, **kwargs): + self._path = path + self._args = args + self._kwargs = kwargs + + @staticmethod + def _gen_image_schema(options, path, transformation): + if path is not None: + options["image"] = { + "path": path + } + if transformation is not None: + options['coordinateTransformations'] = transformation + return options + + def make_file_def_creator(self, dataset_uid, obj_i): + def generator(base_url): + options = {} + options = self._gen_obs_labels_schema(options, self._obs_labels_paths, self._obs_labels_names) + options = self._gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) + options = self._gen_obs_sets_schema(options, self._cell_set_obs, self._cell_set_obs_names) + options = self._gen_obs_spots_schema(self._spatial_spots_obsm, options) + options = self._gen_image_schema(options, self._image_path, self._transformation) + + if len(options.keys()) > 0: + obj_file_def = { + "fileType": ft.SPATIALDATA_ZARR.value, + "url": self.get_zarr_url(base_url, dataset_uid, obj_i), + "options": options + } + if self._request_init is not None: + obj_file_def['requestInit'] = self._request_init + if self._coordination_values is not None: + obj_file_def['coordinationValues'] = self._coordination_values + return obj_file_def + return None + + return generator + + class MultivecZarrWrapper(AbstractWrapper): From 7cb5e8d50fb1dfbf6a14b8c24e628f9ee453a986 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 2 Apr 2024 21:28:30 +0200 Subject: [PATCH 03/58] (feat): working notebook minus spots (maybe a vitessce bug?) --- docs/notebooks/spatial_data.ipynb | 341 ++++++++++++++++++++++++++++++ vitessce/__init__.py | 1 + vitessce/config.py | 2 +- vitessce/constants.py | 2 +- vitessce/wrappers.py | 67 ++++-- 5 files changed, 394 insertions(+), 19 deletions(-) create mode 100644 docs/notebooks/spatial_data.ipynb diff --git a/docs/notebooks/spatial_data.ipynb b/docs/notebooks/spatial_data.ipynb new file mode 100644 index 00000000..1b860722 --- /dev/null +++ b/docs/notebooks/spatial_data.ipynb @@ -0,0 +1,341 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "# Vitessce Widget Tutorial" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualization of SpatialData Object" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Import dependencies\n", + "\n", + "We need to import the classes and functions that we will be using from the corresponding packages." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "from pathlib import Path\n", + "from urllib.request import urlretrieve\n", + "import dask\n", + "\n", + "dask.config.set({'dataframe.query-planning-warning': False})\n", + "\n", + "from spatialdata import read_zarr\n", + "import scanpy as sc\n", + "\n", + "from vitessce import (\n", + " VitessceConfig,\n", + " Component as cm,\n", + " CoordinationType as ct,\n", + " AbstractWrapper,\n", + " SpatialDataWrapper\n", + ")\n", + "from vitessce.data_utils import (\n", + " optimize_adata,\n", + " VAR_CHUNK_SIZE,\n", + ")\n", + "import zipfile\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "zip_filepath = Path(\"data/visium.spatialdata.zarr.zip\")\n", + "spatialdata_filepath = zip_filepath.with_suffix('')\n", + "if not zip_filepath.exists():\n", + " spatialdata_filepath.parent.mkdir(exist_ok=True)\n", + " urlretrieve('https://s3.embl.de/spatialdata/spatialdata-sandbox/visium_associated_xenium_io.zip', zip_filepath)\n", + " with zipfile.ZipFile(zip_filepath,\"r\") as zip_ref:\n", + " zip_ref.extractall(spatialdata_filepath.parent)\n", + " (spatialdata_filepath.parent / \"data.zarr\").rename(spatialdata_filepath)\n", + " (spatialdata_filepath / \"tables\").rename(spatialdata_filepath / \"table\") # TODO: remove once fixed in Vitessce" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Load the data\n", + "\n", + "Note: this function may print a `FutureWarning`" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_98958/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", + " spatialdata = read_zarr(spatialdata_filepath)\n" + ] + } + ], + "source": [ + "spatialdata = read_zarr(spatialdata_filepath)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "SpatialData object with:\n", + "├── Images\n", + "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_full_image': MultiscaleSpatialImage[cyx] (3, 21571, 19505), (3, 10785, 9752), (3, 5392, 4876), (3, 2696, 2438), (3, 1348, 1219)\n", + "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_hires_image': SpatialImage[cyx] (3, 2000, 1809)\n", + "│ └── 'CytAssist_FFPE_Human_Breast_Cancer_lowres_image': SpatialImage[cyx] (3, 600, 543)\n", + "├── Shapes\n", + "│ └── 'CytAssist_FFPE_Human_Breast_Cancer': GeoDataFrame shape: (4992, 2) (2D shapes)\n", + "└── Tables\n", + " └── 'table': AnnData (4992, 18085)\n", + "with coordinate systems:\n", + "▸ 'downscaled_hires', with elements:\n", + " CytAssist_FFPE_Human_Breast_Cancer_hires_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", + "▸ 'downscaled_lowres', with elements:\n", + " CytAssist_FFPE_Human_Breast_Cancer_lowres_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", + "▸ 'global', with elements:\n", + " CytAssist_FFPE_Human_Breast_Cancer_full_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "spatialdata" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Create the Vitessce widget configuration\n", + "\n", + "Vitessce needs to know which pieces of data we are interested in visualizing, the visualization types we would like to use, and how we want to coordinate (or link) the views." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.1. Instantiate a `VitessceConfig` object\n", + "\n", + "Use the `VitessceConfig` constructor to create an instance." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "vc = VitessceConfig(schema_version=\"1.0.16\", name='Visium SpatialData Demo (visium_associated_xenium_io)', description='From https://spatialdata.scverse.org/en/latest/tutorials/notebooks/datasets/README.html')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.2. Add a dataset to the `VitessceConfig` instance\n", + "\n", + "In Vitessce, a dataset is a container for one file per data type. The `.add_dataset(name)` method on the `vc` instance sets up and returns a new dataset instance.\n", + "\n", + "Then, we can call the dataset's `.add_object(wrapper_object)` method to attach a \"data wrapper\" instance to our new dataset. For example, the `AnnDataWrapper` helps to configure AnnData Zarr stores for use in the Vitessce configuration.\n", + "\n", + "Dataset wrapper classes may require additional parameters to resolve ambiguities. For instance, `AnnData` objects may store multiple clusterings or cell type annotation columns in the `adata.obs` dataframe. We can use the parameter `obs_set_paths` to tell Vitessce that certain columns of the `obs` dataframe correspond to cell type annotations or cell clusterings." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "wrapper = SpatialDataWrapper(\n", + " spatialdata_path=spatialdata_filepath.absolute(),\n", + " image_path=\"images/CytAssist_FFPE_Human_Breast_Cancer_full_image\",\n", + " obs_feature_matrix_path = \"table/table/X\",\n", + " feature_labels_path = \"table/table/var/gene_ids\",\n", + " shapes_path = \"shapes/CytAssist_FFPE_Human_Breast_Cancer\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = vc.add_dataset(name='Breast Cancer Visium').add_object(wrapper)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "spatial = vc.add_view(\"spatialBeta\", dataset=dataset)\n", + "genes = vc.add_view(cm.FEATURE_LIST, dataset=dataset)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.4. Define the visualization layout\n", + "\n", + "The `vc.layout(view_concat)` method allows us to specify how our views will be arranged in the layout grid in the widget. The `|` and `/` characters are magic syntax for `hconcat(v1, v2)` and `vconcat(v1, v2)`, respectively." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "vc.layout(spatial | genes);" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "449b622426ed4a88aead41ca9ec09a2e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)', '…" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vw = vc.widget()\n", + "vw" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "vw.close()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'version': '1.0.16',\n", + " 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)',\n", + " 'description': 'From https://spatialdata.scverse.org/en/latest/tutorials/notebooks/datasets/README.html',\n", + " 'datasets': [{'uid': 'A',\n", + " 'name': 'Breast Cancer Visium',\n", + " 'files': [{'fileType': 'spatialdata.zarr',\n", + " 'url': '/A/0/94f0b436-6c22-4adf-ba35-ce35125b467c.spatialdata.zarr',\n", + " 'options': {'obsFeatureMatrix': {'path': 'table/table/X'},\n", + " 'obsSpots': {'path': 'shapes/CytAssist_FFPE_Human_Breast_Cancer',\n", + " 'tablePath': 'table/table',\n", + " 'region': 'region'},\n", + " 'image': {'path': 'images/CytAssist_FFPE_Human_Breast_Cancer_full_image'},\n", + " 'featureLabels': {'path': 'table/table/var/gene_ids'}}}]}],\n", + " 'coordinationSpace': {'dataset': {'A': 'A'}},\n", + " 'layout': [{'component': 'spatialBeta',\n", + " 'coordinationScopes': {'dataset': 'A'},\n", + " 'x': 0.0,\n", + " 'y': 0,\n", + " 'w': 6.0,\n", + " 'h': 12},\n", + " {'component': 'featureList',\n", + " 'coordinationScopes': {'dataset': 'A'},\n", + " 'x': 6.0,\n", + " 'y': 0,\n", + " 'w': 6.0,\n", + " 'h': 12}],\n", + " 'initStrategy': 'auto'}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vc.to_dict('')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/vitessce/__init__.py b/vitessce/__init__.py index bc8ae8db..2d740eeb 100644 --- a/vitessce/__init__.py +++ b/vitessce/__init__.py @@ -53,6 +53,7 @@ ObsSegmentationsOmeTiffWrapper, ImageOmeZarrWrapper, ObsSegmentationsOmeZarrWrapper, + SpatialDataWrapper, ) except ModuleNotFoundError as e: # pragma: no cover warn(f'Extra installs are necessary to use wrappers: {e}') diff --git a/vitessce/config.py b/vitessce/config.py index d880d239..711bf4b0 100644 --- a/vitessce/config.py +++ b/vitessce/config.py @@ -1023,7 +1023,7 @@ def add_view(self, view_type, dataset=None, dataset_uid=None, x=0, y=0, w=1, h=1 dataset_uid, str) assert dataset is None or dataset_uid is None component = view_type - assert type(component) in [str, cm] + # assert type(component) in [str, cm] if dataset is None: dataset = self.get_dataset_by_uid(dataset_uid) diff --git a/vitessce/constants.py b/vitessce/constants.py index df3503bf..252fcfe3 100644 --- a/vitessce/constants.py +++ b/vitessce/constants.py @@ -14,7 +14,7 @@ def __new__(cls, value, doc): def norm_enum(enum_val, expected_enum_class=None): - assert isinstance(enum_val, str) or isinstance(enum_val, expected_enum_class) + # assert isinstance(enum_val, str) or isinstance(enum_val, expected_enum_class), f"enum_val was {type(enum_val)} and not a string or expected value {type(expected_enum_class)}" # We don't actually use the expected_enum_class, # since it would not account for things like plugin coordination types, etc. # But we can pass it around anyway and in the future could use diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 8887d434..a852462f 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -2,9 +2,12 @@ import os from os.path import join import tempfile +from typing import Optional from uuid import uuid4 from pathlib import PurePath, PurePosixPath +import numpy as np + from .constants import ( norm_enum, ViewType as cm, @@ -925,8 +928,8 @@ def __init__(self, adata_path=None, adata_url=None, obs_feature_matrix_path=None """ super().__init__(**kwargs) self._repr = make_repr(locals()) - self._adata_path = adata_path - self._adata_url = adata_url + self._path = adata_path + self._url = adata_url if adata_url is not None and (adata_path is not None): raise ValueError( "Did not expect adata_url to be provided with adata_path") @@ -971,22 +974,22 @@ def convert_and_save(self, dataset_uid, obj_i, base_dir=None): file_def_creator = self.make_file_def_creator( dataset_uid, obj_i) - routes = self.make_anndata_routes(dataset_uid, obj_i) + routes = self.make_routes(dataset_uid, obj_i) self.file_def_creators.append(file_def_creator) self.routes += routes - def make_anndata_routes(self, dataset_uid, obj_i): + def make_routes(self, dataset_uid, obj_i): if self.is_remote: return [] else: - return self.get_local_dir_route(dataset_uid, obj_i, self._adata_path, self.local_dir_uid) + return self.get_local_dir_route(dataset_uid, obj_i, self._path, self.local_dir_uid) def get_zarr_url(self, base_url="", dataset_uid="", obj_i=""): if self.is_remote: - return self._adata_url + return self._url else: - return self.get_local_dir_url(base_url, dataset_uid, obj_i, self._adata_path, self.local_dir_uid) + return self.get_local_dir_url(base_url, dataset_uid, obj_i, self._path, self.local_dir_uid) @staticmethod def _gen_obs_embedding_schema(options, paths=None, names=None, dims=None): @@ -1116,30 +1119,60 @@ def auto_view_config(self, vc): class SpatialDataWrapper(AnnDataWrapper): - def __init__(self, path, *args, **kwargs): - self._path = path - self._args = args + def __init__(self, *, spatialdata_path: str, image_path: str="", affine_transformation: Optional[np.ndarray] = None, shapes_path: Optional[str] = None, **kwargs): + super().__init__(adata_path=spatialdata_path, **kwargs) # HACK to use adata_path + self.local_dir_uid = make_unique_filename(".spatialdata.zarr") # correct? + self._image_path = image_path + self._affine_transformation = affine_transformation self._kwargs = kwargs + self._shapes_path = shapes_path + # TODO(unify this better with common arguments) + self._path = spatialdata_path + self._url = None + if self._path is not None and (self._url is not None): + raise ValueError( + "Did not expect path to be provided with url") + if self._url is None and (self._path is None): + raise ValueError( + "Expected either url or path to be provided") + if self._url is None: + self.is_remote = False + self.zarr_folder = 'spatialdata.zarr' + else: + self.is_remote = True + self.zarr_folder = None @staticmethod - def _gen_image_schema(options, path, transformation): + def _gen_image_schema(options, path: str, affine_transformation: Optional[np.ndarray] = None): if path is not None: options["image"] = { "path": path } - if transformation is not None: - options['coordinateTransformations'] = transformation + if affine_transformation is not None: + options['coordinateTransformations'] = affine_transformation + return options + + @staticmethod + def _gen_obs_spots_schema(options, shapes_path): + if shapes_path is not None: + options['obsSpots'] = { + "path": shapes_path, + "tablePath": "table/table", + "region": "region" + } return options - def make_file_def_creator(self, dataset_uid, obj_i): + + + def make_file_def_creator(self, dataset_uid: str, obj_i: str): def generator(base_url): options = {} options = self._gen_obs_labels_schema(options, self._obs_labels_paths, self._obs_labels_names) options = self._gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) options = self._gen_obs_sets_schema(options, self._cell_set_obs, self._cell_set_obs_names) - options = self._gen_obs_spots_schema(self._spatial_spots_obsm, options) - options = self._gen_image_schema(options, self._image_path, self._transformation) - + options = self._gen_obs_spots_schema(options, self._shapes_path) + options = self._gen_image_schema(options, self._image_path, self._affine_transformation) + options = self._gen_feature_labels_schema(self._gene_alias, options) if len(options.keys()) > 0: obj_file_def = { "fileType": ft.SPATIALDATA_ZARR.value, From 6da29524489e73a2a82374b35b1210e62833311e Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Wed, 3 Apr 2024 12:17:19 +0200 Subject: [PATCH 04/58] (fix): try to get spots to appear --- docs/notebooks/spatial_data.ipynb | 185 +++++++++++++++++++++--------- 1 file changed, 132 insertions(+), 53 deletions(-) diff --git a/docs/notebooks/spatial_data.ipynb b/docs/notebooks/spatial_data.ipynb index 1b860722..3634ce69 100644 --- a/docs/notebooks/spatial_data.ipynb +++ b/docs/notebooks/spatial_data.ipynb @@ -92,7 +92,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_98958/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", + "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_55042/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", " spatialdata = read_zarr(spatialdata_filepath)\n" ] } @@ -136,6 +136,39 @@ "spatialdata" ] }, + { + "cell_type": "code", + "execution_count": 105, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'CytAssist_FFPE_Human_Breast_Cancer': geometry radius\n", + "0 POINT (4131.000 4636.000) 63.917904\n", + "1 POINT (13355.000 16640.000) 63.917904\n", + "2 POINT (8845.000 12067.000) 63.917904\n", + "3 POINT (14716.000 15518.000) 63.917904\n", + "4 POINT (11404.000 12365.000) 63.917904\n", + "... ... ...\n", + "4987 POINT (8731.000 9049.000) 63.917904\n", + "4988 POINT (6598.000 4776.000) 63.917904\n", + "4989 POINT (10447.000 6653.000) 63.917904\n", + "4990 POINT (11039.000 12368.000) 63.917904\n", + "4991 POINT (3720.000 10518.000) 63.917904\n", + "\n", + "[4992 rows x 2 columns]}" + ] + }, + "execution_count": 105, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "spatialdata.shapes" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -156,7 +189,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 145, "metadata": {}, "outputs": [], "source": [ @@ -178,7 +211,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 146, "metadata": {}, "outputs": [], "source": [ @@ -187,13 +220,14 @@ " image_path=\"images/CytAssist_FFPE_Human_Breast_Cancer_full_image\",\n", " obs_feature_matrix_path = \"table/table/X\",\n", " feature_labels_path = \"table/table/var/gene_ids\",\n", - " shapes_path = \"shapes/CytAssist_FFPE_Human_Breast_Cancer\"\n", + " shapes_path = \"shapes/CytAssist_FFPE_Human_Breast_Cancer\",\n", + " # obs_set_paths = [\"table/table/obs/region\"]\n", ")" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 147, "metadata": {}, "outputs": [], "source": [ @@ -202,12 +236,33 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 148, "metadata": {}, "outputs": [], "source": [ "spatial = vc.add_view(\"spatialBeta\", dataset=dataset)\n", - "genes = vc.add_view(cm.FEATURE_LIST, dataset=dataset)" + "feature_list = vc.add_view(cm.FEATURE_LIST, dataset=dataset)\n", + "layer_controller = vc.add_view(\"layerControllerBeta\", dataset=dataset)\n", + "# The below is trying to get the spots to appear.....delete maybe?\n", + "[feature_selection, obs_color_encoding] = vc.add_coordination(ct.FEATURE_SELECTION, ct.OBS_COLOR_ENCODING);\n", + "obs_color_encoding.set_value('geneSelection')\n", + "vc.link_views_by_dict(\n", + " [spatial, layer_controller],\n", + " {\n", + " ct.FEATURE_SELECTION: feature_selection, ct.OBS_COLOR_ENCODING: obs_color_encoding\n", + " },\n", + " scope_prefix=\"init_A_obsSpots\"\n", + ")\n", + "vc.link_views_by_dict(\n", + " [spatial, layer_controller],\n", + " {\n", + " 'photometricInterpretation': 'RGB',\n", + " },\n", + " scope_prefix=\"init_A_image\"\n", + ")\n", + "vc.link_views([spatial, layer_controller, feature_list], ['obsType'], ['spot'])\n", + "feature_list.use_coordination(feature_selection)\n", + "feature_list.use_coordination(obs_color_encoding)" ] }, { @@ -221,51 +276,16 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 149, "metadata": {}, "outputs": [], "source": [ - "vc.layout(spatial | genes);" + "vc.layout(spatial | (feature_list / layer_controller));" ] }, { "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "449b622426ed4a88aead41ca9ec09a2e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)', '…" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "vw = vc.widget()\n", - "vw" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "vw.close()" - ] - }, - { - "cell_type": "code", - "execution_count": 11, + "execution_count": 150, "metadata": {}, "outputs": [ { @@ -277,36 +297,95 @@ " 'datasets': [{'uid': 'A',\n", " 'name': 'Breast Cancer Visium',\n", " 'files': [{'fileType': 'spatialdata.zarr',\n", - " 'url': '/A/0/94f0b436-6c22-4adf-ba35-ce35125b467c.spatialdata.zarr',\n", + " 'url': 'localhost:8080/A/0/5ba1d994-1ad8-4811-9337-ae0c8293069c.spatialdata.zarr',\n", " 'options': {'obsFeatureMatrix': {'path': 'table/table/X'},\n", " 'obsSpots': {'path': 'shapes/CytAssist_FFPE_Human_Breast_Cancer',\n", " 'tablePath': 'table/table',\n", " 'region': 'region'},\n", " 'image': {'path': 'images/CytAssist_FFPE_Human_Breast_Cancer_full_image'},\n", " 'featureLabels': {'path': 'table/table/var/gene_ids'}}}]}],\n", - " 'coordinationSpace': {'dataset': {'A': 'A'}},\n", + " 'coordinationSpace': {'dataset': {'A': 'A'},\n", + " 'featureSelection': {'A': None},\n", + " 'obsColorEncoding': {'A': 'geneSelection'},\n", + " 'metaCoordinationScopes': {'init_A_obsSpots_00': {'featureSelection': 'A',\n", + " 'obsColorEncoding': 'A'},\n", + " 'init_A_image_00': {'photometricInterpretation': 'init_A_image_00'}},\n", + " 'metaCoordinationScopesBy': {'init_A_obsSpots_00': {},\n", + " 'init_A_image_00': {}},\n", + " 'photometricInterpretation': {'init_A_image_00': 'RGB'},\n", + " 'obsType': {'A': 'spot'}},\n", " 'layout': [{'component': 'spatialBeta',\n", - " 'coordinationScopes': {'dataset': 'A'},\n", + " 'coordinationScopes': {'dataset': 'A',\n", + " 'metaCoordinationScopes': ['init_A_obsSpots_00', 'init_A_image_00'],\n", + " 'metaCoordinationScopesBy': ['init_A_obsSpots_00', 'init_A_image_00'],\n", + " 'obsType': 'A'},\n", " 'x': 0.0,\n", " 'y': 0,\n", " 'w': 6.0,\n", " 'h': 12},\n", " {'component': 'featureList',\n", - " 'coordinationScopes': {'dataset': 'A'},\n", + " 'coordinationScopes': {'dataset': 'A',\n", + " 'obsType': 'A',\n", + " 'featureSelection': 'A',\n", + " 'obsColorEncoding': 'A'},\n", " 'x': 6.0,\n", - " 'y': 0,\n", + " 'y': 0.0,\n", + " 'w': 6.0,\n", + " 'h': 6.0},\n", + " {'component': 'layerControllerBeta',\n", + " 'coordinationScopes': {'dataset': 'A',\n", + " 'metaCoordinationScopes': ['init_A_obsSpots_00', 'init_A_image_00'],\n", + " 'metaCoordinationScopesBy': ['init_A_obsSpots_00', 'init_A_image_00'],\n", + " 'obsType': 'A'},\n", + " 'x': 6.0,\n", + " 'y': 6.0,\n", " 'w': 6.0,\n", - " 'h': 12}],\n", + " 'h': 6.0}],\n", " 'initStrategy': 'auto'}" ] }, - "execution_count": 11, + "execution_count": 150, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vc.to_dict('localhost:8080')" + ] + }, + { + "cell_type": "code", + "execution_count": 151, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "65b786f9dba84e5b9fab459e88b1fdfb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)', '…" + ] + }, + "execution_count": 151, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "vc.to_dict('')" + "vw = vc.widget()\n", + "vw" + ] + }, + { + "cell_type": "code", + "execution_count": 130, + "metadata": {}, + "outputs": [], + "source": [ + "vw.close()\n" ] }, { From 44f3834df04e282f8e4c3cddb8868cd4990b2e16 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Wed, 3 Apr 2024 12:18:30 +0200 Subject: [PATCH 05/58] (chore): comment out other stuff to make widget show more --- docs/notebooks/spatial_data.ipynb | 94 +++++++++++++------------------ 1 file changed, 38 insertions(+), 56 deletions(-) diff --git a/docs/notebooks/spatial_data.ipynb b/docs/notebooks/spatial_data.ipynb index 3634ce69..7b05c5f9 100644 --- a/docs/notebooks/spatial_data.ipynb +++ b/docs/notebooks/spatial_data.ipynb @@ -92,7 +92,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_55042/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", + "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_21578/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", " spatialdata = read_zarr(spatialdata_filepath)\n" ] } @@ -138,7 +138,7 @@ }, { "cell_type": "code", - "execution_count": 105, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -160,7 +160,7 @@ "[4992 rows x 2 columns]}" ] }, - "execution_count": 105, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -189,7 +189,7 @@ }, { "cell_type": "code", - "execution_count": 145, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -211,7 +211,7 @@ }, { "cell_type": "code", - "execution_count": 146, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -227,7 +227,7 @@ }, { "cell_type": "code", - "execution_count": 147, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -236,7 +236,7 @@ }, { "cell_type": "code", - "execution_count": 148, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -244,25 +244,25 @@ "feature_list = vc.add_view(cm.FEATURE_LIST, dataset=dataset)\n", "layer_controller = vc.add_view(\"layerControllerBeta\", dataset=dataset)\n", "# The below is trying to get the spots to appear.....delete maybe?\n", - "[feature_selection, obs_color_encoding] = vc.add_coordination(ct.FEATURE_SELECTION, ct.OBS_COLOR_ENCODING);\n", - "obs_color_encoding.set_value('geneSelection')\n", - "vc.link_views_by_dict(\n", - " [spatial, layer_controller],\n", - " {\n", - " ct.FEATURE_SELECTION: feature_selection, ct.OBS_COLOR_ENCODING: obs_color_encoding\n", - " },\n", - " scope_prefix=\"init_A_obsSpots\"\n", - ")\n", - "vc.link_views_by_dict(\n", - " [spatial, layer_controller],\n", - " {\n", - " 'photometricInterpretation': 'RGB',\n", - " },\n", - " scope_prefix=\"init_A_image\"\n", - ")\n", - "vc.link_views([spatial, layer_controller, feature_list], ['obsType'], ['spot'])\n", - "feature_list.use_coordination(feature_selection)\n", - "feature_list.use_coordination(obs_color_encoding)" + "# [feature_selection, obs_color_encoding] = vc.add_coordination(ct.FEATURE_SELECTION, ct.OBS_COLOR_ENCODING);\n", + "# obs_color_encoding.set_value('geneSelection')\n", + "# vc.link_views_by_dict(\n", + "# [spatial, layer_controller],\n", + "# {\n", + "# ct.FEATURE_SELECTION: feature_selection, ct.OBS_COLOR_ENCODING: obs_color_encoding\n", + "# },\n", + "# scope_prefix=\"init_A_obsSpots\"\n", + "# )\n", + "# vc.link_views_by_dict(\n", + "# [spatial, layer_controller],\n", + "# {\n", + "# 'photometricInterpretation': 'RGB',\n", + "# },\n", + "# scope_prefix=\"init_A_image\"\n", + "# )\n", + "# vc.link_views([spatial, layer_controller, feature_list], ['obsType'], ['spot'])\n", + "# feature_list.use_coordination(feature_selection)\n", + "# feature_list.use_coordination(obs_color_encoding)" ] }, { @@ -276,7 +276,7 @@ }, { "cell_type": "code", - "execution_count": 149, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -285,7 +285,7 @@ }, { "cell_type": "code", - "execution_count": 150, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -297,46 +297,28 @@ " 'datasets': [{'uid': 'A',\n", " 'name': 'Breast Cancer Visium',\n", " 'files': [{'fileType': 'spatialdata.zarr',\n", - " 'url': 'localhost:8080/A/0/5ba1d994-1ad8-4811-9337-ae0c8293069c.spatialdata.zarr',\n", + " 'url': 'localhost:8080/A/0/ed5b4b78-cdd7-41ba-b348-70392f545dd9.spatialdata.zarr',\n", " 'options': {'obsFeatureMatrix': {'path': 'table/table/X'},\n", " 'obsSpots': {'path': 'shapes/CytAssist_FFPE_Human_Breast_Cancer',\n", " 'tablePath': 'table/table',\n", " 'region': 'region'},\n", " 'image': {'path': 'images/CytAssist_FFPE_Human_Breast_Cancer_full_image'},\n", " 'featureLabels': {'path': 'table/table/var/gene_ids'}}}]}],\n", - " 'coordinationSpace': {'dataset': {'A': 'A'},\n", - " 'featureSelection': {'A': None},\n", - " 'obsColorEncoding': {'A': 'geneSelection'},\n", - " 'metaCoordinationScopes': {'init_A_obsSpots_00': {'featureSelection': 'A',\n", - " 'obsColorEncoding': 'A'},\n", - " 'init_A_image_00': {'photometricInterpretation': 'init_A_image_00'}},\n", - " 'metaCoordinationScopesBy': {'init_A_obsSpots_00': {},\n", - " 'init_A_image_00': {}},\n", - " 'photometricInterpretation': {'init_A_image_00': 'RGB'},\n", - " 'obsType': {'A': 'spot'}},\n", + " 'coordinationSpace': {'dataset': {'A': 'A'}},\n", " 'layout': [{'component': 'spatialBeta',\n", - " 'coordinationScopes': {'dataset': 'A',\n", - " 'metaCoordinationScopes': ['init_A_obsSpots_00', 'init_A_image_00'],\n", - " 'metaCoordinationScopesBy': ['init_A_obsSpots_00', 'init_A_image_00'],\n", - " 'obsType': 'A'},\n", + " 'coordinationScopes': {'dataset': 'A'},\n", " 'x': 0.0,\n", " 'y': 0,\n", " 'w': 6.0,\n", " 'h': 12},\n", " {'component': 'featureList',\n", - " 'coordinationScopes': {'dataset': 'A',\n", - " 'obsType': 'A',\n", - " 'featureSelection': 'A',\n", - " 'obsColorEncoding': 'A'},\n", + " 'coordinationScopes': {'dataset': 'A'},\n", " 'x': 6.0,\n", " 'y': 0.0,\n", " 'w': 6.0,\n", " 'h': 6.0},\n", " {'component': 'layerControllerBeta',\n", - " 'coordinationScopes': {'dataset': 'A',\n", - " 'metaCoordinationScopes': ['init_A_obsSpots_00', 'init_A_image_00'],\n", - " 'metaCoordinationScopesBy': ['init_A_obsSpots_00', 'init_A_image_00'],\n", - " 'obsType': 'A'},\n", + " 'coordinationScopes': {'dataset': 'A'},\n", " 'x': 6.0,\n", " 'y': 6.0,\n", " 'w': 6.0,\n", @@ -344,7 +326,7 @@ " 'initStrategy': 'auto'}" ] }, - "execution_count": 150, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -355,13 +337,13 @@ }, { "cell_type": "code", - "execution_count": 151, + "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "65b786f9dba84e5b9fab459e88b1fdfb", + "model_id": "", "version_major": 2, "version_minor": 0 }, @@ -369,7 +351,7 @@ "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)', '…" ] }, - "execution_count": 151, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -381,7 +363,7 @@ }, { "cell_type": "code", - "execution_count": 130, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ From 4df109239eff4bf0d7065a642bd6014e4ab263dc Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:30:21 -0400 Subject: [PATCH 06/58] Fix visium config --- docs/notebooks/spatial_data.ipynb | 184 ++++++------------------------ 1 file changed, 38 insertions(+), 146 deletions(-) diff --git a/docs/notebooks/spatial_data.ipynb b/docs/notebooks/spatial_data.ipynb index 7b05c5f9..e6841acc 100644 --- a/docs/notebooks/spatial_data.ipynb +++ b/docs/notebooks/spatial_data.ipynb @@ -27,7 +27,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -47,8 +47,10 @@ " VitessceConfig,\n", " Component as cm,\n", " CoordinationType as ct,\n", + " CoordinationLevel as CL,\n", " AbstractWrapper,\n", - " SpatialDataWrapper\n", + " SpatialDataWrapper,\n", + " get_initial_coordination_scope_prefix\n", ")\n", "from vitessce.data_utils import (\n", " optimize_adata,\n", @@ -59,7 +61,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -85,86 +87,27 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_21578/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", - " spatialdata = read_zarr(spatialdata_filepath)\n" - ] - } - ], + "outputs": [], "source": [ "spatialdata = read_zarr(spatialdata_filepath)" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "SpatialData object with:\n", - "├── Images\n", - "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_full_image': MultiscaleSpatialImage[cyx] (3, 21571, 19505), (3, 10785, 9752), (3, 5392, 4876), (3, 2696, 2438), (3, 1348, 1219)\n", - "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_hires_image': SpatialImage[cyx] (3, 2000, 1809)\n", - "│ └── 'CytAssist_FFPE_Human_Breast_Cancer_lowres_image': SpatialImage[cyx] (3, 600, 543)\n", - "├── Shapes\n", - "│ └── 'CytAssist_FFPE_Human_Breast_Cancer': GeoDataFrame shape: (4992, 2) (2D shapes)\n", - "└── Tables\n", - " └── 'table': AnnData (4992, 18085)\n", - "with coordinate systems:\n", - "▸ 'downscaled_hires', with elements:\n", - " CytAssist_FFPE_Human_Breast_Cancer_hires_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", - "▸ 'downscaled_lowres', with elements:\n", - " CytAssist_FFPE_Human_Breast_Cancer_lowres_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", - "▸ 'global', with elements:\n", - " CytAssist_FFPE_Human_Breast_Cancer_full_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "spatialdata" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'CytAssist_FFPE_Human_Breast_Cancer': geometry radius\n", - "0 POINT (4131.000 4636.000) 63.917904\n", - "1 POINT (13355.000 16640.000) 63.917904\n", - "2 POINT (8845.000 12067.000) 63.917904\n", - "3 POINT (14716.000 15518.000) 63.917904\n", - "4 POINT (11404.000 12365.000) 63.917904\n", - "... ... ...\n", - "4987 POINT (8731.000 9049.000) 63.917904\n", - "4988 POINT (6598.000 4776.000) 63.917904\n", - "4989 POINT (10447.000 6653.000) 63.917904\n", - "4990 POINT (11039.000 12368.000) 63.917904\n", - "4991 POINT (3720.000 10518.000) 63.917904\n", - "\n", - "[4992 rows x 2 columns]}" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "spatialdata.shapes" ] @@ -189,7 +132,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -211,7 +154,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -221,13 +164,14 @@ " obs_feature_matrix_path = \"table/table/X\",\n", " feature_labels_path = \"table/table/var/gene_ids\",\n", " shapes_path = \"shapes/CytAssist_FFPE_Human_Breast_Cancer\",\n", - " # obs_set_paths = [\"table/table/obs/region\"]\n", + " # obs_set_paths = [\"table/table/obs/region\"],\n", + " coordination_values={\"obsType\":\"spot\"}\n", ")" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -236,7 +180,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -253,14 +197,12 @@ "# },\n", "# scope_prefix=\"init_A_obsSpots\"\n", "# )\n", - "# vc.link_views_by_dict(\n", - "# [spatial, layer_controller],\n", - "# {\n", - "# 'photometricInterpretation': 'RGB',\n", - "# },\n", - "# scope_prefix=\"init_A_image\"\n", - "# )\n", - "# vc.link_views([spatial, layer_controller, feature_list], ['obsType'], ['spot'])\n", + "vc.link_views_by_dict([spatial, layer_controller], {\n", + " 'imageLayer': CL([{\n", + " 'photometricInterpretation': 'RGB',\n", + " }]),\n", + "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"image\"))\n", + "vc.link_views([spatial, layer_controller, feature_list], ['obsType'], ['spot'])\n", "# feature_list.use_coordination(feature_selection)\n", "# feature_list.use_coordination(obs_color_encoding)" ] @@ -276,7 +218,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -285,77 +227,18 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'version': '1.0.16',\n", - " 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)',\n", - " 'description': 'From https://spatialdata.scverse.org/en/latest/tutorials/notebooks/datasets/README.html',\n", - " 'datasets': [{'uid': 'A',\n", - " 'name': 'Breast Cancer Visium',\n", - " 'files': [{'fileType': 'spatialdata.zarr',\n", - " 'url': 'localhost:8080/A/0/ed5b4b78-cdd7-41ba-b348-70392f545dd9.spatialdata.zarr',\n", - " 'options': {'obsFeatureMatrix': {'path': 'table/table/X'},\n", - " 'obsSpots': {'path': 'shapes/CytAssist_FFPE_Human_Breast_Cancer',\n", - " 'tablePath': 'table/table',\n", - " 'region': 'region'},\n", - " 'image': {'path': 'images/CytAssist_FFPE_Human_Breast_Cancer_full_image'},\n", - " 'featureLabels': {'path': 'table/table/var/gene_ids'}}}]}],\n", - " 'coordinationSpace': {'dataset': {'A': 'A'}},\n", - " 'layout': [{'component': 'spatialBeta',\n", - " 'coordinationScopes': {'dataset': 'A'},\n", - " 'x': 0.0,\n", - " 'y': 0,\n", - " 'w': 6.0,\n", - " 'h': 12},\n", - " {'component': 'featureList',\n", - " 'coordinationScopes': {'dataset': 'A'},\n", - " 'x': 6.0,\n", - " 'y': 0.0,\n", - " 'w': 6.0,\n", - " 'h': 6.0},\n", - " {'component': 'layerControllerBeta',\n", - " 'coordinationScopes': {'dataset': 'A'},\n", - " 'x': 6.0,\n", - " 'y': 6.0,\n", - " 'w': 6.0,\n", - " 'h': 6.0}],\n", - " 'initStrategy': 'auto'}" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "vc.to_dict('localhost:8080')" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)', '…" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "vw = vc.widget()\n", "vw" @@ -363,13 +246,22 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "vw.close()\n" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vw.config" + ] + }, { "cell_type": "code", "execution_count": null, From 74f8c382a94329cd71ae5ace170b42bab1259438 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Thu, 4 Apr 2024 14:11:50 +0200 Subject: [PATCH 07/58] (feat): firstt pass at `from_spatialdata_object` --- vitessce/wrappers.py | 50 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index a852462f..a2a85145 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -2,11 +2,13 @@ import os from os.path import join import tempfile -from typing import Optional +from typing import Callable, Optional, Type, TypeVar from uuid import uuid4 from pathlib import PurePath, PurePosixPath import numpy as np +from spatialdata import SpatialData +from datatree import DataTree from .constants import ( norm_enum, @@ -1116,10 +1118,11 @@ def auto_view_config(self, vc): else: vc.layout((scatterplot | (cell_sets / genes)) / heatmap) - + +SpatialDataWrapperType = TypeVar('SpatialDataWrapperType', bound='SpatialDataWrapper') class SpatialDataWrapper(AnnDataWrapper): - def __init__(self, *, spatialdata_path: str, image_path: str="", affine_transformation: Optional[np.ndarray] = None, shapes_path: Optional[str] = None, **kwargs): + def __init__(self, *, spatialdata_path: str, image_path: Optional[str]=None, affine_transformation: Optional[np.ndarray] = None, shapes_path: Optional[str] = None, labels_path: Optional[str] = None, **kwargs): super().__init__(adata_path=spatialdata_path, **kwargs) # HACK to use adata_path self.local_dir_uid = make_unique_filename(".spatialdata.zarr") # correct? self._image_path = image_path @@ -1143,7 +1146,7 @@ def __init__(self, *, spatialdata_path: str, image_path: str="", affine_transfor self.zarr_folder = None @staticmethod - def _gen_image_schema(options, path: str, affine_transformation: Optional[np.ndarray] = None): + def _gen_image_schema(options, path: str, affine_transformation: Optional[np.ndarray] = None) -> dict: if path is not None: options["image"] = { "path": path @@ -1153,7 +1156,7 @@ def _gen_image_schema(options, path: str, affine_transformation: Optional[np.nda return options @staticmethod - def _gen_obs_spots_schema(options, shapes_path): + def _gen_obs_spots_schema(options: dict, shapes_path: str) -> dict: if shapes_path is not None: options['obsSpots'] = { "path": shapes_path, @@ -1161,10 +1164,41 @@ def _gen_obs_spots_schema(options, shapes_path): "region": "region" } return options - - - def make_file_def_creator(self, dataset_uid: str, obj_i: str): + @classmethod + def from_spatialdata_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData) -> list[SpatialDataWrapperType]: + wrappers = [] + shapes_path = None + image_path = None + labels_path = None + obs_feature_matrix_path = None + for table in sdata.tables: + spatialdata_attr = table.uns['spatialdata_attrs'] + region = spatialdata_attr['region'] + if isinstance(region, list): + if len(region) > 1: + raise ValueError("Vitessce cannot subset AnnData objects on the flow") + region = region[0] + if hasattr(sdata, "shapes"): + shapes_path = sdata.path / "shapes" / region + if hasattr(sdata, "images"): + image_path = sdata.path / "images" / region # this is definitely wrong, but what is right? + if hasattr(sdata, "labels"): + labels_path = sdata.path / "labels" / region + obs_feature_matrix_path = sdata.path / "tables" / table / "X" + wrappers += [ + cls( + spatialdata_path = str(sdata.path), + image_path = str(image_path), + labels_path = str(labels_path), + obs_feature_matrix_path = str(obs_feature_matrix_path), + shapes_path = str(shapes_path), + coordination_values={"obsType":"spot"} # TODO: should we remove? + ) + ] + return wrappers + + def make_file_def_creator(self, dataset_uid: str, obj_i: str) -> Optional[Callable]: def generator(base_url): options = {} options = self._gen_obs_labels_schema(options, self._obs_labels_paths, self._obs_labels_names) From bd298cc2d8bdcd6c527ec0d12f88071b47eff85b Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 16 Apr 2024 14:15:26 +0200 Subject: [PATCH 08/58] (refactor): refactor `from_spatialdata_object` and util funcs from static methods --- vitessce/utils.py | 102 ++++++++++++++++++++++ vitessce/wrappers.py | 200 ++++++++++++++----------------------------- 2 files changed, 165 insertions(+), 137 deletions(-) diff --git a/vitessce/utils.py b/vitessce/utils.py index fdb31c27..d8a39f7e 100644 --- a/vitessce/utils.py +++ b/vitessce/utils.py @@ -1,3 +1,9 @@ +from functools import partial +from typing import Optional + +import numpy as np + + def get_next_scope_numeric(prev_scopes): next_scope_int = 0 next_scope_str = None @@ -33,3 +39,99 @@ def get_initial_coordination_scope_prefix(dataset_uid, data_type): def get_initial_coordination_scope_name(dataset_uid, data_type, i=None): prefix = get_initial_coordination_scope_prefix(dataset_uid, data_type) return f"{prefix}{0 if i is None else i}" + + + +def gen_obs_embedding_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None, dims: Optional[list[list[int]]] = None): + if paths is not None: + if names is not None: + for key, mapping in zip(paths, names): + options["obsEmbedding"].append({ + "path": mapping, + "dims": [0, 1], + "embeddingType": key + }) + else: + for mapping in paths: + mapping_key = mapping.split('/')[-1] + options["obsEmbedding"].append({ + "path": mapping, + "dims": [0, 1], + "embeddingType": mapping_key + }) + if dims is not None: + for dim_i, dim in enumerate(dims): + options["obsEmbedding"][dim_i]['dims'] = dim + return options + +def gen_obs_sets_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None): + if paths is not None: + options["obsSets"] = [] + if names is not None: + names = names + else: + names = [obs.split('/')[-1] for obs in paths] + for obs, name in zip(paths, names): + options["obsSets"].append({ + "name": name, + "path": obs + }) + return options + +def gen_obs_feature_matrix_schema(options: dict, matrix_path: Optional[str] = None, var_filter_path: Optional[str] = None, init_var_filter_path: Optional[str] = None): + if matrix_path is not None: + options["obsFeatureMatrix"] = { + "path": matrix_path + } + if var_filter_path is not None: + options["obsFeatureMatrix"]["featureFilterPath"] = var_filter_path + if init_var_filter_path is not None: + options["obsFeatureMatrix"]["initialFeatureFilterPath"] = init_var_filter_path + return options + +def gen_obs_labels_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None): + if paths is not None: + if names is not None and len(paths) == len(names): + # A name was provided for each path element, so use those values. + names = names + else: + # Names were not provided for each path element, + # so fall back to using the final part of each path for the names. + names = [labels_path.split('/')[-1] for labels_path in paths] + obs_labels = [] + for path, name in zip(paths, names): + obs_labels.append({"path": path, "obsLabelsType": name}) + options["obsLabels"] = obs_labels + return options + + +def gen_path_schema(key: str, path: Optional[str], options: dict): + if path is not None: + options[key] = { + "path": path + } + return options + +gen_obs_locations_schema = partial(gen_path_schema, "obsLocations") +gen_obs_segmentations_schema = partial(gen_path_schema, "obsSegmentations") +gen_obs_spots_schema = partial(gen_path_schema, "obsSpots") +gen_obs_points_schema = partial(gen_path_schema, "obsPoints") +gen_feature_labels_schema = partial(gen_path_schema, "featureLabels") + +def gen_image_schema(options, path: str, affine_transformation: Optional[np.ndarray] = None) -> dict: + if path is not None: + options["image"] = { + "path": path + } + if affine_transformation is not None: + options['coordinateTransformations'] = affine_transformation + return options + +def gen_obs_spots_schema(options: dict, shapes_path: Optional[str] = None) -> dict: + if shapes_path is not None: + options['obsSpots'] = { + "path": shapes_path, + "tablePath": "table/table", + "region": "region" + } + return options \ No newline at end of file diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index a2a85145..a1cbcff8 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1,14 +1,15 @@ -from functools import partialmethod +from collections import defaultdict import os from os.path import join import tempfile -from typing import Callable, Optional, Type, TypeVar +from typing import Callable, Optional, Type, TypeVar, Union from uuid import uuid4 from pathlib import PurePath, PurePosixPath import numpy as np from spatialdata import SpatialData -from datatree import DataTree + +from vitessce.utils import gen_obs_locations_schema, gen_obs_segmentations_schema, gen_obs_spots_schema, gen_obs_points_schema, gen_obs_embedding_schema, gen_feature_labels_schema, gen_obs_spots_schema, gen_feature_labels_schema, gen_image_schema, gen_obs_feature_matrix_schema, gen_obs_labels_schema, gen_obs_sets_schema from .constants import ( norm_enum, @@ -992,101 +993,20 @@ def get_zarr_url(self, base_url="", dataset_uid="", obj_i=""): return self._url else: return self.get_local_dir_url(base_url, dataset_uid, obj_i, self._path, self.local_dir_uid) - - @staticmethod - def _gen_obs_embedding_schema(options, paths=None, names=None, dims=None): - if paths is not None: - if names is not None: - for key, mapping in zip(paths, names): - options["obsEmbedding"].append({ - "path": mapping, - "dims": [0, 1], - "embeddingType": key - }) - else: - for mapping in paths: - mapping_key = mapping.split('/')[-1] - options["obsEmbedding"].append({ - "path": mapping, - "dims": [0, 1], - "embeddingType": mapping_key - }) - if dims is not None: - for dim_i, dim in enumerate(dims): - options["obsEmbedding"][dim_i]['dims'] = dim - return options - - @staticmethod - def _gen_obs_sets_schema(options, paths, names): - if paths is not None: - options["obsSets"] = [] - if names is not None: - names = names - else: - names = [obs.split('/')[-1] for obs in paths] - for obs, name in zip(paths, names): - options["obsSets"].append({ - "name": name, - "path": obs - }) - return options - - @staticmethod - def _gen_obs_feature_matrix_schema(options, matrix_path, var_filter_path, init_var_filter_path): - if matrix_path is not None: - options["obsFeatureMatrix"] = { - "path": matrix_path - } - if var_filter_path is not None: - options["obsFeatureMatrix"]["featureFilterPath"] = var_filter_path - if init_var_filter_path is not None: - options["obsFeatureMatrix"]["initialFeatureFilterPath"] = init_var_filter_path - return options - - @staticmethod - def _gen_obs_labels_schema(options, paths, names): - if paths is not None: - if names is not None and len(paths) == len(names): - # A name was provided for each path element, so use those values. - names = names - else: - # Names were not provided for each path element, - # so fall back to using the final part of each path for the names. - names = [labels_path.split('/')[-1] for labels_path in paths] - obs_labels = [] - for path, name in zip(paths, names): - obs_labels.append({"path": path, "obsLabelsType": name}) - options["obsLabels"] = obs_labels - return options - - - @staticmethod - def _gen_path_schema(key, path, options): - if path is not None: - options[key] = { - "path": path - } - return options - - _gen_obs_locations_schema = partialmethod(_gen_path_schema, "obsLocations") - _gen_obs_segmentations_schema = partialmethod(_gen_path_schema, "obsSegmentations") - _gen_obs_spots_schema = partialmethod(_gen_path_schema, "obsSpots") - _gen_obs_points_schema = partialmethod(_gen_path_schema, "obsPoints") - _gen_feature_labels_schema = partialmethod(_gen_path_schema, "featureLabels") def make_file_def_creator(self, dataset_uid, obj_i): def get_anndata_zarr(base_url): options = {} - options = self._gen_obs_locations_schema(self._spatial_centroid_obsm, options) - options = self._gen_obs_segmentations_schema(self._spatial_polygon_obsm, options) - options = self._gen_obs_spots_schema(self._spatial_spots_obsm, options) - options = self._gen_obs_points_schema(self._spatial_points_obsm, options) - options = self._gen_obs_embedding_schema(options, self._mappings_obsm, self._mappings_obsm_names, self._mappings_obsm_dims) - options = self._gen_obs_sets_schema(options, self._cell_set_obs, self._cell_set_obs_names) - options = self._gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) - options = self._gen_feature_labels_schema(self._gene_alias, options) - options = self._gen_obs_labels_schema(options, self._obs_labels_paths, self._obs_labels_names) + options = gen_obs_locations_schema(self._spatial_centroid_obsm, options) + options = gen_obs_segmentations_schema(self._spatial_polygon_obsm, options) + options = gen_obs_spots_schema(self._spatial_spots_obsm, options) + options = gen_obs_points_schema(self._spatial_points_obsm, options) + options = gen_obs_embedding_schema(options, self._mappings_obsm, self._mappings_obsm_names, self._mappings_obsm_dims) + options = gen_obs_sets_schema(options, self._cell_set_obs, self._cell_set_obs_names) + options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) + options = gen_feature_labels_schema(self._gene_alias, options) + options = gen_obs_labels_schema(options, self._obs_labels_paths, self._obs_labels_names) if len(options.keys()) > 0: obj_file_def = { @@ -1144,55 +1064,61 @@ def __init__(self, *, spatialdata_path: str, image_path: Optional[str]=None, aff else: self.is_remote = True self.zarr_folder = None - - @staticmethod - def _gen_image_schema(options, path: str, affine_transformation: Optional[np.ndarray] = None) -> dict: - if path is not None: - options["image"] = { - "path": path - } - if affine_transformation is not None: - options['coordinateTransformations'] = affine_transformation - return options - - @staticmethod - def _gen_obs_spots_schema(options: dict, shapes_path: str) -> dict: - if shapes_path is not None: - options['obsSpots'] = { - "path": shapes_path, - "tablePath": "table/table", - "region": "region" - } - return options @classmethod - def from_spatialdata_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData) -> list[SpatialDataWrapperType]: + def from_spatialdata_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_keys_to_image_paths: dict[str, Union[str,None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str,None]] = defaultdict(type(None))) -> list[SpatialDataWrapperType]: + """Instantiate a wrapper for SpatialData stores, one per table, directly from the SpatialData object. + By default, we "show everything" that can reasonable be inferred given the information. If you wish to have more control, + consider instantiating the object directly. This function will error if something cannot be inferred i.e., the user does not present + regions explicitly but there is more than one for a given table. + + + Parameters + ---------- + cls : Type[SpatialDataWrapperType] + _description_ + sdata : SpatialData + _description_ + table_keys_to_image_paths : dict[str, str], optional + which image paths to use for a given table for the visualization, by default None for each table key. + table_keys_to_regions : dict[str, str], optional + which regions to use for a given table for the visualization, by default None for each table key. + + Returns + ------- + list[SpatialDataWrapperType] + + Raises + ------ + ValueError + """ wrappers = [] - shapes_path = None - image_path = None - labels_path = None - obs_feature_matrix_path = None - for table in sdata.tables: + for table_key, table in sdata.tables.items(): + shapes_path = None + image_path = table_keys_to_image_paths[table_key] + labels_path = None spatialdata_attr = table.uns['spatialdata_attrs'] - region = spatialdata_attr['region'] + region = table_keys_to_regions[table_key] + if region is not None: + assert region in spatialdata_attr['region'] + else: + region = spatialdata_attr['region'] if isinstance(region, list): if len(region) > 1: - raise ValueError("Vitessce cannot subset AnnData objects on the flow") + raise ValueError("Vitessce cannot subset AnnData objects on the fly. Please provide an explicit region") region = region[0] - if hasattr(sdata, "shapes"): - shapes_path = sdata.path / "shapes" / region - if hasattr(sdata, "images"): - image_path = sdata.path / "images" / region # this is definitely wrong, but what is right? - if hasattr(sdata, "labels"): - labels_path = sdata.path / "labels" / region - obs_feature_matrix_path = sdata.path / "tables" / table / "X" + if region in sdata.shapes: + shapes_path = f"shapes/{region}" + if region in sdata.labels: + labels_path = f"labels/{region}" + obs_feature_matrix_path = f"table/{table_key}/X" # TODO: fix first key needing to be "table" in vitessce-js wrappers += [ cls( spatialdata_path = str(sdata.path), - image_path = str(image_path), - labels_path = str(labels_path), + image_path = str(image_path) if image_path is not None else None, + labels_path = str(labels_path) if labels_path is not None else None, obs_feature_matrix_path = str(obs_feature_matrix_path), - shapes_path = str(shapes_path), + shapes_path = str(shapes_path) if shapes_path is not None else None, coordination_values={"obsType":"spot"} # TODO: should we remove? ) ] @@ -1201,12 +1127,12 @@ def from_spatialdata_object(cls: Type[SpatialDataWrapperType], sdata: SpatialDat def make_file_def_creator(self, dataset_uid: str, obj_i: str) -> Optional[Callable]: def generator(base_url): options = {} - options = self._gen_obs_labels_schema(options, self._obs_labels_paths, self._obs_labels_names) - options = self._gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) - options = self._gen_obs_sets_schema(options, self._cell_set_obs, self._cell_set_obs_names) - options = self._gen_obs_spots_schema(options, self._shapes_path) - options = self._gen_image_schema(options, self._image_path, self._affine_transformation) - options = self._gen_feature_labels_schema(self._gene_alias, options) + options = gen_obs_labels_schema(options, self._obs_labels_paths, self._obs_labels_names) + options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) + options = gen_obs_sets_schema(options, self._cell_set_obs, self._cell_set_obs_names) + options = gen_obs_spots_schema(options, self._shapes_path) + options = gen_image_schema(options, self._image_path, self._affine_transformation) + options = gen_feature_labels_schema(self._gene_alias, options) if len(options.keys()) > 0: obj_file_def = { "fileType": ft.SPATIALDATA_ZARR.value, From fdfcc205838b306b4279f1bf541911a09f6f498f Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 19 Apr 2024 17:09:39 +0200 Subject: [PATCH 09/58] (feat): add url capability --- vitessce/wrappers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index a1cbcff8..0ceb112d 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1042,16 +1042,17 @@ def auto_view_config(self, vc): SpatialDataWrapperType = TypeVar('SpatialDataWrapperType', bound='SpatialDataWrapper') class SpatialDataWrapper(AnnDataWrapper): - def __init__(self, *, spatialdata_path: str, image_path: Optional[str]=None, affine_transformation: Optional[np.ndarray] = None, shapes_path: Optional[str] = None, labels_path: Optional[str] = None, **kwargs): - super().__init__(adata_path=spatialdata_path, **kwargs) # HACK to use adata_path + def __init__(self, *, spatialdata_url: Optional[str]=None, spatialdata_path: Optional[str]=None, image_path: Optional[str]=None, affine_transformation: Optional[np.ndarray] = None, shapes_path: Optional[str] = None, labels_path: Optional[str] = None, **kwargs): + super().__init__(adata_path=spatialdata_path, adata_url=spatialdata_url, **kwargs) # HACK to use adata_path self.local_dir_uid = make_unique_filename(".spatialdata.zarr") # correct? self._image_path = image_path self._affine_transformation = affine_transformation self._kwargs = kwargs self._shapes_path = shapes_path + self._labels_path = labels_path # TODO(unify this better with common arguments) self._path = spatialdata_path - self._url = None + self._url = spatialdata_url if self._path is not None and (self._url is not None): raise ValueError( "Did not expect path to be provided with url") From aeb19e92531b27ed3fa6cf87e8ea5b06b622daba Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Mon, 6 May 2024 11:16:42 +0200 Subject: [PATCH 10/58] (temp): spatial data fixes --- docs/notebooks/spatial_data-Copy1.ipynb | 300 ++++++++++++++++++++++++ docs/notebooks/spatial_data.ipynb | 187 ++++++++++----- 2 files changed, 431 insertions(+), 56 deletions(-) create mode 100644 docs/notebooks/spatial_data-Copy1.ipynb diff --git a/docs/notebooks/spatial_data-Copy1.ipynb b/docs/notebooks/spatial_data-Copy1.ipynb new file mode 100644 index 00000000..904fd247 --- /dev/null +++ b/docs/notebooks/spatial_data-Copy1.ipynb @@ -0,0 +1,300 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "# Vitessce Widget Tutorial" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualization of SpatialData Object" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Import dependencies\n", + "\n", + "We need to import the classes and functions that we will be using from the corresponding packages." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "from pathlib import Path\n", + "from urllib.request import urlretrieve\n", + "import dask\n", + "\n", + "dask.config.set({'dataframe.query-planning-warning': False})\n", + "\n", + "from spatialdata import read_zarr\n", + "import scanpy as sc\n", + "\n", + "from vitessce import (\n", + " VitessceConfig,\n", + " Component as cm,\n", + " CoordinationType as ct,\n", + " OmeZarrWrapper,\n", + " CoordinationLevel as CL,\n", + ")\n", + "from vitessce.data_utils import (\n", + " optimize_adata,\n", + " VAR_CHUNK_SIZE,\n", + ")\n", + "import zipfile\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "zip_filepath = Path(\"data/visium.spatialdata.zarr.zip\")\n", + "spatialdata_filepath = zip_filepath.with_suffix('')\n", + "if not zip_filepath.exists():\n", + " spatialdata_filepath.parent.mkdir(exist_ok=True)\n", + " urlretrieve('https://s3.embl.de/spatialdata/spatialdata-sandbox/visium_associated_xenium_io.zip', zip_filepath)\n", + " with zipfile.ZipFile(zip_filepath,\"r\") as zip_ref:\n", + " zip_ref.extractall(spatialdata_filepath.parent)\n", + " (spatialdata_filepath.parent / \"data.zarr\").rename(spatialdata_filepath)\n", + " (spatialdata_filepath / \"tables\").rename(spatialdata_filepath / \"table\") # TODO: remove once fixed in Vitessce" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Load the data\n", + "\n", + "Note: this function may print a `FutureWarning`" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_19455/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", + " spatialdata = read_zarr(spatialdata_filepath)\n" + ] + } + ], + "source": [ + "spatialdata = read_zarr(spatialdata_filepath)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "SpatialData object with:\n", + "├── Images\n", + "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_full_image': MultiscaleSpatialImage[cyx] (3, 21571, 19505), (3, 10785, 9752), (3, 5392, 4876), (3, 2696, 2438), (3, 1348, 1219)\n", + "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_hires_image': SpatialImage[cyx] (3, 2000, 1809)\n", + "│ └── 'CytAssist_FFPE_Human_Breast_Cancer_lowres_image': SpatialImage[cyx] (3, 600, 543)\n", + "├── Shapes\n", + "│ └── 'CytAssist_FFPE_Human_Breast_Cancer': GeoDataFrame shape: (4992, 2) (2D shapes)\n", + "└── Tables\n", + " └── 'table': AnnData (4992, 18085)\n", + "with coordinate systems:\n", + "▸ 'downscaled_hires', with elements:\n", + " CytAssist_FFPE_Human_Breast_Cancer_hires_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", + "▸ 'downscaled_lowres', with elements:\n", + " CytAssist_FFPE_Human_Breast_Cancer_lowres_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", + "▸ 'global', with elements:\n", + " CytAssist_FFPE_Human_Breast_Cancer_full_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "spatialdata" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Create the Vitessce widget configuration\n", + "\n", + "Vitessce needs to know which pieces of data we are interested in visualizing, the visualization types we would like to use, and how we want to coordinate (or link) the views." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.1. Instantiate a `VitessceConfig` object\n", + "\n", + "Use the `VitessceConfig` constructor to create an instance." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "vc = VitessceConfig(schema_version=\"1.0.16\", name='Visium OmeZarr Demo (visium_associated_xenium_io)', description='From https://spatialdata.scverse.org/en/latest/tutorials/notebooks/datasets/README.html')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.2. Add a dataset to the `VitessceConfig` instance\n", + "\n", + "In Vitessce, a dataset is a container for one file per data type. The `.add_dataset(name)` method on the `vc` instance sets up and returns a new dataset instance.\n", + "\n", + "Then, we can call the dataset's `.add_object(wrapper_object)` method to attach a \"data wrapper\" instance to our new dataset. For example, the `AnnDataWrapper` helps to configure AnnData Zarr stores for use in the Vitessce configuration.\n", + "\n", + "Dataset wrapper classes may require additional parameters to resolve ambiguities. For instance, `AnnData` objects may store multiple clusterings or cell type annotation columns in the `adata.obs` dataframe. We can use the parameter `obs_set_paths` to tell Vitessce that certain columns of the `obs` dataframe correspond to cell type annotations or cell clusterings." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "wrapper = OmeZarrWrapper(\n", + " img_path=spatialdata_filepath / \"images/CytAssist_FFPE_Human_Breast_Cancer_full_image\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = vc.add_dataset(name='Breast Cancer Visium', uid='Visium').add_object(wrapper)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "spatial = vc.add_view(\"spatialBeta\", dataset=dataset)\n", + "layer_controller = vc.add_view(\"layerControllerBeta\", dataset=dataset)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.4. Define the visualization layout\n", + "\n", + "The `vc.layout(view_concat)` method allows us to specify how our views will be arranged in the layout grid in the widget. The `|` and `/` characters are magic syntax for `hconcat(v1, v2)` and `vconcat(v1, v2)`, respectively." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "vc.layout(spatial | layer_controller);" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bf34d533620949c7a5021930af4c0f1f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium OmeZarr Demo (visium_associated_xenium_io)', 'desc…" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vw = vc.widget()\n", + "vw" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "vw.close()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/notebooks/spatial_data.ipynb b/docs/notebooks/spatial_data.ipynb index e6841acc..30cb21c3 100644 --- a/docs/notebooks/spatial_data.ipynb +++ b/docs/notebooks/spatial_data.ipynb @@ -27,7 +27,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -61,7 +61,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -87,31 +87,57 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_79130/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", + " spatialdata = read_zarr(spatialdata_filepath)\n" + ] + } + ], "source": [ "spatialdata = read_zarr(spatialdata_filepath)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "SpatialData object with:\n", + "├── Images\n", + "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_full_image': MultiscaleSpatialImage[cyx] (3, 21571, 19505), (3, 10785, 9752), (3, 5392, 4876), (3, 2696, 2438), (3, 1348, 1219)\n", + "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_hires_image': SpatialImage[cyx] (3, 2000, 1809)\n", + "│ └── 'CytAssist_FFPE_Human_Breast_Cancer_lowres_image': SpatialImage[cyx] (3, 600, 543)\n", + "├── Shapes\n", + "│ └── 'CytAssist_FFPE_Human_Breast_Cancer': GeoDataFrame shape: (4992, 2) (2D shapes)\n", + "└── Tables\n", + " └── 'table': AnnData (4992, 18085)\n", + "with coordinate systems:\n", + "▸ 'downscaled_hires', with elements:\n", + " CytAssist_FFPE_Human_Breast_Cancer_hires_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", + "▸ 'downscaled_lowres', with elements:\n", + " CytAssist_FFPE_Human_Breast_Cancer_lowres_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", + "▸ 'global', with elements:\n", + " CytAssist_FFPE_Human_Breast_Cancer_full_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "spatialdata" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "spatialdata.shapes" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -132,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -154,24 +180,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ + "# [wrapper] = SpatialDataWrapper.from_spatialdata_object(\n", + "# spatialdata,\n", + "# table_keys_to_image_paths={\"table\": \"images/CytAssist_FFPE_Human_Breast_Cancer_full_image\"},\n", + "# )\n", "wrapper = SpatialDataWrapper(\n", - " spatialdata_path=spatialdata_filepath.absolute(),\n", - " image_path=\"images/CytAssist_FFPE_Human_Breast_Cancer_full_image\",\n", + " spatialdata_url = \"https://storage.googleapis.com/vitessce-demo-data/spatialdata-september-2023/visium_io.zarr\",\n", + " image_path=\"images/Visium_Mouse_Olfactory_Bulb_full_image\",\n", " obs_feature_matrix_path = \"table/table/X\",\n", - " feature_labels_path = \"table/table/var/gene_ids\",\n", - " shapes_path = \"shapes/CytAssist_FFPE_Human_Breast_Cancer\",\n", - " # obs_set_paths = [\"table/table/obs/region\"],\n", - " coordination_values={\"obsType\":\"spot\"}\n", + " shapes_path = \"shapes/Visium_Mouse_Olfactory_Bulb\",\n", + " coordination_values={\"obsType\":\"spot\"} # TODO: should we remove?\n", ")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": {}, "outputs": [], "source": [ @@ -180,31 +208,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "spatial = vc.add_view(\"spatialBeta\", dataset=dataset)\n", "feature_list = vc.add_view(cm.FEATURE_LIST, dataset=dataset)\n", "layer_controller = vc.add_view(\"layerControllerBeta\", dataset=dataset)\n", - "# The below is trying to get the spots to appear.....delete maybe?\n", - "# [feature_selection, obs_color_encoding] = vc.add_coordination(ct.FEATURE_SELECTION, ct.OBS_COLOR_ENCODING);\n", - "# obs_color_encoding.set_value('geneSelection')\n", - "# vc.link_views_by_dict(\n", - "# [spatial, layer_controller],\n", - "# {\n", - "# ct.FEATURE_SELECTION: feature_selection, ct.OBS_COLOR_ENCODING: obs_color_encoding\n", - "# },\n", - "# scope_prefix=\"init_A_obsSpots\"\n", - "# )\n", "vc.link_views_by_dict([spatial, layer_controller], {\n", " 'imageLayer': CL([{\n", " 'photometricInterpretation': 'RGB',\n", " }]),\n", "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"image\"))\n", - "vc.link_views([spatial, layer_controller, feature_list], ['obsType'], ['spot'])\n", - "# feature_list.use_coordination(feature_selection)\n", - "# feature_list.use_coordination(obs_color_encoding)" + "vc.link_views([spatial, layer_controller, feature_list], ['obsType'], ['spot'])" ] }, { @@ -218,27 +245,35 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": {}, "outputs": [], "source": [ - "vc.layout(spatial | (feature_list / layer_controller));" + "vc.layout(spatial | (feature_list / layer_controller));\n", + "# vc.layout(spatial | layer_controller);" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": {}, - "outputs": [], - "source": [ - "vc.to_dict('localhost:8080')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c49818353b50442581859b138914956d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)', '…" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "vw = vc.widget()\n", "vw" @@ -246,7 +281,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": {}, "outputs": [], "source": [ @@ -255,11 +290,51 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'version': '1.0.16',\n", + " 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)',\n", + " 'description': 'From https://spatialdata.scverse.org/en/latest/tutorials/notebooks/datasets/README.html',\n", + " 'datasets': [{'uid': 'A',\n", + " 'name': 'Breast Cancer Visium',\n", + " 'files': [{'fileType': 'spatialdata.zarr',\n", + " 'url': 'https://storage.googleapis.com/vitessce-demo-data/spatialdata-september-2023/visium_io.zarr',\n", + " 'options': {'image': {'path': 'images/Visium_Mouse_Olfactory_Bulb_full_image'}}}]}],\n", + " 'coordinationSpace': {'dataset': {'A': 'A'},\n", + " 'imageLayer': {'init_A_image_0': '__dummy__'},\n", + " 'photometricInterpretation': {'init_A_image_0': 'RGB'},\n", + " 'metaCoordinationScopes': {'init_A_image_0': {'imageLayer': ['init_A_image_0']}},\n", + " 'metaCoordinationScopesBy': {'init_A_image_0': {'imageLayer': {'photometricInterpretation': {'init_A_image_0': 'init_A_image_0'}}}}},\n", + " 'layout': [{'component': 'spatialBeta',\n", + " 'coordinationScopes': {'dataset': 'A',\n", + " 'metaCoordinationScopes': ['init_A_image_0'],\n", + " 'metaCoordinationScopesBy': ['init_A_image_0']},\n", + " 'x': 0.0,\n", + " 'y': 0,\n", + " 'w': 6.0,\n", + " 'h': 12},\n", + " {'component': 'layerControllerBeta',\n", + " 'coordinationScopes': {'dataset': 'A',\n", + " 'metaCoordinationScopes': ['init_A_image_0'],\n", + " 'metaCoordinationScopesBy': ['init_A_image_0']},\n", + " 'x': 6.0,\n", + " 'y': 0,\n", + " 'w': 6.0,\n", + " 'h': 12}],\n", + " 'initStrategy': 'auto'}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "vw.config" + "vc.to_dict(\"\")" ] }, { From daec71118098884c8e90b43c5ca42e748ad3e48a Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 7 May 2024 14:44:21 +0200 Subject: [PATCH 11/58] (chore): more clean ups and fixes --- docs/notebooks/spatial_data.ipynb | 80 +++++++++++++++++-------------- vitessce/wrappers.py | 70 ++++++++++++++------------- 2 files changed, 82 insertions(+), 68 deletions(-) diff --git a/docs/notebooks/spatial_data.ipynb b/docs/notebooks/spatial_data.ipynb index 30cb21c3..e5df3109 100644 --- a/docs/notebooks/spatial_data.ipynb +++ b/docs/notebooks/spatial_data.ipynb @@ -94,7 +94,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_79130/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", + "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_86408/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", " spatialdata = read_zarr(spatialdata_filepath)\n" ] } @@ -105,7 +105,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -129,7 +129,7 @@ " CytAssist_FFPE_Human_Breast_Cancer_full_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)" ] }, - "execution_count": 11, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -158,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 71, "metadata": {}, "outputs": [], "source": [ @@ -180,26 +180,19 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 72, "metadata": {}, "outputs": [], "source": [ - "# [wrapper] = SpatialDataWrapper.from_spatialdata_object(\n", - "# spatialdata,\n", - "# table_keys_to_image_paths={\"table\": \"images/CytAssist_FFPE_Human_Breast_Cancer_full_image\"},\n", - "# )\n", - "wrapper = SpatialDataWrapper(\n", - " spatialdata_url = \"https://storage.googleapis.com/vitessce-demo-data/spatialdata-september-2023/visium_io.zarr\",\n", - " image_path=\"images/Visium_Mouse_Olfactory_Bulb_full_image\",\n", - " obs_feature_matrix_path = \"table/table/X\",\n", - " shapes_path = \"shapes/Visium_Mouse_Olfactory_Bulb\",\n", - " coordination_values={\"obsType\":\"spot\"} # TODO: should we remove?\n", + "[wrapper] = SpatialDataWrapper.from_object(\n", + " spatialdata,\n", + " table_keys_to_image_elems={\"table\": \"images/CytAssist_FFPE_Human_Breast_Cancer_full_image\"},\n", ")" ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 73, "metadata": {}, "outputs": [], "source": [ @@ -208,16 +201,16 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 74, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 22, + "execution_count": 74, "metadata": {}, "output_type": "execute_result" } @@ -231,7 +224,8 @@ " 'photometricInterpretation': 'RGB',\n", " }]),\n", "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"image\"))\n", - "vc.link_views([spatial, layer_controller, feature_list], ['obsType'], ['spot'])" + "obs_sets = vc.add_view(cm.OBS_SETS, dataset=dataset)\n", + "vc.link_views([spatial, layer_controller, feature_list, obs_sets], ['obsType'], ['spot'])" ] }, { @@ -245,23 +239,22 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 75, "metadata": {}, "outputs": [], "source": [ - "vc.layout(spatial | (feature_list / layer_controller));\n", - "# vc.layout(spatial | layer_controller);" + "vc.layout(spatial | (feature_list / layer_controller / obs_sets));" ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 76, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c49818353b50442581859b138914956d", + "model_id": "8b0f04224d574808b63f2c768461e1b9", "version_major": 2, "version_minor": 0 }, @@ -269,7 +262,7 @@ "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)', '…" ] }, - "execution_count": 24, + "execution_count": 76, "metadata": {}, "output_type": "execute_result" } @@ -281,7 +274,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 70, "metadata": {}, "outputs": [], "source": [ @@ -290,7 +283,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 62, "metadata": {}, "outputs": [ { @@ -302,33 +295,48 @@ " 'datasets': [{'uid': 'A',\n", " 'name': 'Breast Cancer Visium',\n", " 'files': [{'fileType': 'spatialdata.zarr',\n", - " 'url': 'https://storage.googleapis.com/vitessce-demo-data/spatialdata-september-2023/visium_io.zarr',\n", - " 'options': {'image': {'path': 'images/Visium_Mouse_Olfactory_Bulb_full_image'}}}]}],\n", + " 'url': '/A/0/279375f1-8232-4905-b2c4-8566c89e183a.spatialdata.zarr',\n", + " 'options': {'obsFeatureMatrix': {'path': 'table/table/X'},\n", + " 'obsSets': [{'name': 'region', 'path': 'table/table/obs/region'}],\n", + " 'obsSpots': {'path': 'shapes/CytAssist_FFPE_Human_Breast_Cancer',\n", + " 'tablePath': 'table/table',\n", + " 'region': 'region'},\n", + " 'image': {'path': 'images/CytAssist_FFPE_Human_Breast_Cancer_full_image'}},\n", + " 'coordinationValues': {'obsType': 'spot'}}]}],\n", " 'coordinationSpace': {'dataset': {'A': 'A'},\n", " 'imageLayer': {'init_A_image_0': '__dummy__'},\n", " 'photometricInterpretation': {'init_A_image_0': 'RGB'},\n", " 'metaCoordinationScopes': {'init_A_image_0': {'imageLayer': ['init_A_image_0']}},\n", - " 'metaCoordinationScopesBy': {'init_A_image_0': {'imageLayer': {'photometricInterpretation': {'init_A_image_0': 'init_A_image_0'}}}}},\n", + " 'metaCoordinationScopesBy': {'init_A_image_0': {'imageLayer': {'photometricInterpretation': {'init_A_image_0': 'init_A_image_0'}}}},\n", + " 'obsType': {'A': 'spot'}},\n", " 'layout': [{'component': 'spatialBeta',\n", " 'coordinationScopes': {'dataset': 'A',\n", " 'metaCoordinationScopes': ['init_A_image_0'],\n", - " 'metaCoordinationScopesBy': ['init_A_image_0']},\n", + " 'metaCoordinationScopesBy': ['init_A_image_0'],\n", + " 'obsType': 'A'},\n", " 'x': 0.0,\n", " 'y': 0,\n", " 'w': 6.0,\n", " 'h': 12},\n", + " {'component': 'featureList',\n", + " 'coordinationScopes': {'dataset': 'A', 'obsType': 'A'},\n", + " 'x': 6.0,\n", + " 'y': 0.0,\n", + " 'w': 6.0,\n", + " 'h': 6.0},\n", " {'component': 'layerControllerBeta',\n", " 'coordinationScopes': {'dataset': 'A',\n", " 'metaCoordinationScopes': ['init_A_image_0'],\n", - " 'metaCoordinationScopesBy': ['init_A_image_0']},\n", + " 'metaCoordinationScopesBy': ['init_A_image_0'],\n", + " 'obsType': 'A'},\n", " 'x': 6.0,\n", - " 'y': 0,\n", + " 'y': 6.0,\n", " 'w': 6.0,\n", - " 'h': 12}],\n", + " 'h': 6.0}],\n", " 'initStrategy': 'auto'}" ] }, - "execution_count": 10, + "execution_count": 62, "metadata": {}, "output_type": "execute_result" } diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 54ec15d0..35f9fe54 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -993,7 +993,7 @@ def __init__(self, base_path=None, base_url=None, base_store=None, obs_feature_m self.is_remote = False self.is_store = False self.zarr_folder = 'anndata.zarr' - elif adata_url is not None: + elif base_url is not None: self.is_remote = True self.is_store = False self.zarr_folder = None @@ -1005,11 +1005,11 @@ def __init__(self, base_path=None, base_url=None, base_store=None, obs_feature_m self.local_dir_uid = make_unique_filename(".adata.zarr") self._expression_matrix = obs_feature_matrix_elem - self._cell_set_obs_names = obs_set_names + self._set_obs_set_names = obs_set_names self._mappings_obsm_names = obs_embedding_names self._gene_var_filter = feature_filter_elem self._matrix_gene_var_filter = initial_feature_filter_elem - self._cell_set_obs = obs_set_elems + self._obs_set_elems = obs_set_elems self._spatial_centroid_obsm = obs_locations_elem self._spatial_polygon_obsm = obs_segmentations_elem self._mappings_obsm = obs_embedding_elems @@ -1064,10 +1064,10 @@ def get_anndata_zarr(base_url): options = gen_obs_spots_schema(self._spatial_spots_obsm, options) options = gen_obs_points_schema(self._spatial_points_obsm, options) options = gen_obs_embedding_schema(options, self._mappings_obsm, self._mappings_obsm_names, self._mappings_obsm_dims) - options = gen_obs_sets_schema(options, self._cell_set_obs, self._cell_set_obs_names) + options = gen_obs_sets_schema(options, self._obs_set_elems, self._set_obs_set_names) options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) - options = gen_feature_labels_schema(self._gene_alias, options) - options = gen_obs_labels_schema(options, self._obs_labels_paths, self._obs_labels_names) + options = gen_feature_labels_schema(self._feature_labels, options) + options = gen_obs_labels_schema(options, self._obs_labels_elems, self._obs_labels_names) if len(options.keys()) > 0: obj_file_def = { @@ -1103,17 +1103,14 @@ def auto_view_config(self, vc): SpatialDataWrapperType = TypeVar('SpatialDataWrapperType', bound='SpatialDataWrapper') class SpatialDataWrapper(AnnDataWrapper): - def __init__(self, *, spatialdata_url: Optional[str]=None, spatialdata_path: Optional[str]=None, image_path: Optional[str]=None, affine_transformation: Optional[np.ndarray] = None, shapes_path: Optional[str] = None, labels_path: Optional[str] = None, **kwargs): - super().__init__(adata_path=spatialdata_path, adata_url=spatialdata_url, **kwargs) # HACK to use adata_path + def __init__(self, *, image_elem: Optional[str]=None, affine_transformation: Optional[np.ndarray] = None, shapes_elem: Optional[str] = None, labels_elem: Optional[str] = None, **kwargs): + super().__init__(**kwargs) # HACK to use adata_path self.local_dir_uid = make_unique_filename(".spatialdata.zarr") # correct? - self._image_path = image_path + self._image_elem = image_elem self._affine_transformation = affine_transformation self._kwargs = kwargs - self._shapes_path = shapes_path - self._labels_path = labels_path - # TODO(unify this better with common arguments) - self._path = spatialdata_path - self._url = spatialdata_url + self._shapes_elem = shapes_elem + self._labels_elem = labels_elem if self._path is not None and (self._url is not None): raise ValueError( "Did not expect path to be provided with url") @@ -1128,7 +1125,7 @@ def __init__(self, *, spatialdata_url: Optional[str]=None, spatialdata_path: Opt self.zarr_folder = None @classmethod - def from_spatialdata_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_keys_to_image_paths: dict[str, Union[str,None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str,None]] = defaultdict(type(None))) -> list[SpatialDataWrapperType]: + def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_keys_to_image_elems: dict[str, Union[str,None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str,None]] = defaultdict(type(None))) -> list[SpatialDataWrapperType]: """Instantiate a wrapper for SpatialData stores, one per table, directly from the SpatialData object. By default, we "show everything" that can reasonable be inferred given the information. If you wish to have more control, consider instantiating the object directly. This function will error if something cannot be inferred i.e., the user does not present @@ -1141,7 +1138,7 @@ def from_spatialdata_object(cls: Type[SpatialDataWrapperType], sdata: SpatialDat _description_ sdata : SpatialData _description_ - table_keys_to_image_paths : dict[str, str], optional + table_keys_to_image_elems : dict[str, str], optional which image paths to use for a given table for the visualization, by default None for each table key. table_keys_to_regions : dict[str, str], optional which regions to use for a given table for the visualization, by default None for each table key. @@ -1156,9 +1153,9 @@ def from_spatialdata_object(cls: Type[SpatialDataWrapperType], sdata: SpatialDat """ wrappers = [] for table_key, table in sdata.tables.items(): - shapes_path = None - image_path = table_keys_to_image_paths[table_key] - labels_path = None + shapes_elem = None + image_elem = table_keys_to_image_elems[table_key] + labels_elem = None spatialdata_attr = table.uns['spatialdata_attrs'] region = table_keys_to_regions[table_key] if region is not None: @@ -1170,17 +1167,25 @@ def from_spatialdata_object(cls: Type[SpatialDataWrapperType], sdata: SpatialDat raise ValueError("Vitessce cannot subset AnnData objects on the fly. Please provide an explicit region") region = region[0] if region in sdata.shapes: - shapes_path = f"shapes/{region}" + shapes_elem = f"shapes/{region}" if region in sdata.labels: - labels_path = f"labels/{region}" - obs_feature_matrix_path = f"table/{table_key}/X" # TODO: fix first key needing to be "table" in vitessce-js + labels_elem = f"labels/{region}" + obs_feature_matrix_elem = f"table/{table_key}/X" + if 'highly_variable' in table.var: + # TODO: fix first key needing to be "table" in vitessce-js + initial_feature_filter_elem = 'highly_variable' + else: + initial_feature_filter_elem = None + obs_set_elems = [f"table/{table_key}/obs/{elem}" for elem in table.obs if table.obs[elem].dtype == 'category'] wrappers += [ cls( - spatialdata_path = str(sdata.path), - image_path = str(image_path) if image_path is not None else None, - labels_path = str(labels_path) if labels_path is not None else None, - obs_feature_matrix_path = str(obs_feature_matrix_path), - shapes_path = str(shapes_path) if shapes_path is not None else None, + base_path = str(sdata.path), + image_elem = str(image_elem) if image_elem is not None else None, + labels_elem = str(labels_elem) if labels_elem is not None else None, + obs_feature_matrix_elem = str(obs_feature_matrix_elem), + shapes_elem = str(shapes_elem) if shapes_elem is not None else None, + initial_feature_filter_elem = initial_feature_filter_elem, + obs_set_elems = obs_set_elems, coordination_values={"obsType":"spot"} # TODO: should we remove? ) ] @@ -1189,12 +1194,13 @@ def from_spatialdata_object(cls: Type[SpatialDataWrapperType], sdata: SpatialDat def make_file_def_creator(self, dataset_uid: str, obj_i: str) -> Optional[Callable]: def generator(base_url): options = {} - options = gen_obs_labels_schema(options, self._obs_labels_paths, self._obs_labels_names) + options = gen_obs_labels_schema(options, self._obs_labels_elems, self._obs_labels_names) options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) - options = gen_obs_sets_schema(options, self._cell_set_obs, self._cell_set_obs_names) - options = gen_obs_spots_schema(options, self._shapes_path) - options = gen_image_schema(options, self._image_path, self._affine_transformation) - options = gen_feature_labels_schema(self._gene_alias, options) + options = gen_obs_sets_schema(options, self._obs_set_elems, self._set_obs_set_names) + options['obsSets'] = { 'obsSets': options['obsSets'] } # see https://github.com/vitessce/vitessce/blob/cd7e81956786a8130658d6745ff03986e2e6f806/packages/schemas/src/file-def-options.ts#L138-L146 for nested structure + options = gen_obs_spots_schema(options, self._shapes_elem) + options = gen_image_schema(options, self._image_elem, self._affine_transformation) + options = gen_feature_labels_schema(self._feature_labels, options) if len(options.keys()) > 0: obj_file_def = { "fileType": ft.SPATIALDATA_ZARR.value, From b07a72a1035023948807e23130edca77dbda9dfe Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 15 May 2024 09:51:38 -0600 Subject: [PATCH 12/58] Create module for plugin JS (#337) * Create module for plugin JS * Add docs * Lint * Ignore unused imports --- docs/data_options.rst | 39 +++++++ docs/index.rst | 1 + docs/widget_plugins.rst | 152 +++++++++++++++++++++++++ pyproject.toml | 1 + setup.cfg | 1 + vitessce/widget_plugins/__init__.py | 1 + vitessce/widget_plugins/demo_plugin.py | 50 ++++++++ vitessce/wrappers.py | 2 +- 8 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 docs/widget_plugins.rst create mode 100644 vitessce/widget_plugins/__init__.py create mode 100644 vitessce/widget_plugins/demo_plugin.py diff --git a/docs/data_options.rst b/docs/data_options.rst index d600efde..a6c15663 100644 --- a/docs/data_options.rst +++ b/docs/data_options.rst @@ -106,5 +106,44 @@ Jupyter process: remote service like Colab/Binder; Files: remote & accessed via Unfortunately, this will not work because the remote server cannot access the files that are on another machine behind SSH. +======================================================================== +Jupyter process: anywhere; Files: anywhere that can be accessed via Zarr +======================================================================== +If the data is readable via Zarr (i.e., `zarr.storage.*Store`) and the Jupyter process can access the store contents, then the Vitessce widget can access the data by specifying the Zarr store as the data source for Vitessce data wrapper class instances. +This is currently supported for the ``AnnDataWrapper`` class using the ``adata_store`` parameter (as opposed to ``adata_path`` or ``adata_url``). + +.. code-block:: python + + from vitessce import VitessceConfig, AnnDataWrapper + + # ... + adata.write_zarr("my_store.adata.zarr") + + vc = VitessceConfig(name="My Vitessce Configuration") + vc.add_dataset(name="My Dataset").add_object(AnnDataWrapper( + adata_store="my_store.adata.zarr", + # ... + )) + # ... + vc.widget() + + +Or, with a Zarr store instance (instead of a local string path to a DirectoryStore): + +.. code-block:: python + + import zarr + from vitessce import VitessceConfig, AnnDataWrapper + + # ... + store = zarr.storage.FSStore("s3://my_bucket/path/to/my_store.adata.zarr") + + vc = VitessceConfig(name="My Vitessce Configuration") + vc.add_dataset(name="My Dataset").add_object(AnnDataWrapper( + adata_store=store, + # ... + )) + # ... + vc.widget() diff --git a/docs/index.rst b/docs/index.rst index c47bf0b3..a92caae6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,6 +48,7 @@ The Vitessce widget is compatible with the following interactive Python platform api_config api_data data_options + widget_plugins screenshots diff --git a/docs/widget_plugins.rst b/docs/widget_plugins.rst new file mode 100644 index 00000000..cfe1d325 --- /dev/null +++ b/docs/widget_plugins.rst @@ -0,0 +1,152 @@ +Widget plugins +############## + +Vitessce supports multiple types of `plugins `_ defined using JavaScript code. + +Leveraging concepts from `anywidget `_, we can define such plugins directly from Python: plugin developers can supply custom JavaScript code via a Python string. + +The most minimal example of such plugin JavaScript code is the following: + +.. code-block:: python + + PLUGIN_ESM = """ + function createPlugins(utilsForPlugins) { + const { + React, + PluginFileType, + PluginViewType, + PluginCoordinationType, + PluginJointFileType, + z, + useCoordination, + } = utilsForPlugins; + return { + pluginViewTypes: undefined, + pluginFileTypes: undefined, + pluginCoordinationTypes: undefined, + pluginJointFileTypes: undefined, + }; + } + export default { createPlugins }; + """ + + +The plugin string must be defined as an EcmaScript Module (ESM) that exports a function named ``createPlugins``. +The ``createPlugins`` function is called (on the initial render of the Jupyter widget) with the ``utilsForPlugins`` argument (to facilitate dependency injection) and returns an object with the following properties: + +- ``pluginViewTypes``: an array of objects that define the view types of the plugin. +- ``pluginFileTypes```: an array of objects that define the file types of the plugin. +- ``pluginCoordinationTypes``: an array of objects that define the coordination types of the plugin. +- ``pluginJointFileTypes``: an array of objects that define the joint file types of the plugin. + +If defined, these plugin arrays are passed to the Vitessce component as `props `_ with the same names. + +**Note**: For maximum stability of plugins, we recommend that plugin developers document which version(s) of the vitessce Python package that plugins have been developed under. + +-------------------------------- +Passing plugin ESM to the widget +-------------------------------- + +The plugin string can be passed to the widget using the ``plugin_esm`` parameter: + + +.. code-block:: python + + from vitessce import VitessceConfig + + vc = VitessceConfig( + description="A Vitessce widget with a custom plugin", + widget=[ + { + "plugin": PLUGIN_ESM, + }, + ], + ) + # Some more configuration here... + + vc.widget(plugin_esm=PLUGIN_ESM) + + +------------------------------- +Defining plugin views using JSX +------------------------------- + +Vitessce plugin view types are defined as React components. +During typical React component development, JSX syntax is used. +However, JSX is not valid JavaScript and therefore must be transformed to valid JavaScript before it can be passed to the widget where it will be interpreted as ESM. +Vitessce plugin developers then have two options for defining React components for plugin view types: + +* Use ``React.createElement`` directly (without JSX). +* Use the ``transform`` function from `esbuild_py `_ to perform JSX to JS transformation. + +.. code-block:: python + + from esbuild_py import transform + + PLUGIN_ESM = transform(""" + function createPlugins(utilsForPlugins) { + const { + React, + PluginFileType, + PluginViewType, + PluginCoordinationType, + PluginJointFileType, + z, + useCoordination, + } = utilsForPlugins; + + function MyPluginView(props) { + return ( +

Hello world from JSX!

+ ); + } + + const pluginViewTypes = [ + new PluginViewType('myPlugin', MyPluginView, []), + ]; + return { pluginViewTypes }; + } + export default { createPlugins }; + """) + + +To import additional dependencies, JavaScript (more specifically, ESM) can be dynamically imported from CDN (such as ``unpkg`` or ``esm.sh``) within the ``createPlugins`` function: + +.. code-block:: python + + PLUGIN_ESM = """ + async function createPlugins(utilsForPlugins) { + const { + React, + PluginFileType, + PluginViewType, + PluginCoordinationType, + PluginJointFileType, + z, + useCoordination, + } = utilsForPlugins; + + const d3 = await import('https://cdn.jsdelivr.net/npm/d3@7/+esm'); + + // Do something with d3 here... + + return { + pluginViewTypes: undefined, + pluginFileTypes: undefined, + pluginCoordinationTypes: undefined, + pluginJointFileTypes: undefined, + }; + } + export default { createPlugins }; + """ + + +To support more complex import scenarios, see `dynamic-importmap `_. + + + +vitessce.widget_plugins +*********************** + +.. automodule:: vitessce.widget_plugins.demo_plugin + :members: \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5bac98af..58ac240d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ docs = [ ] all = [ 'jupyter-server-proxy>=1.5.2', + 'esbuild_py>=0.1.3', 'anywidget>=0.9.10', 'uvicorn>=0.17.0', 'ujson>=4.0.1', diff --git a/setup.cfg b/setup.cfg index b20c7454..e709e527 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,7 @@ per-file-ignores = # Special case: names are reimported from __init__.py, so unused imports are expected. vitessce/__init__.py: F401 vitessce/data_utils/__init__.py: F401 + vitessce/widget_plugins/__init__.py: F401 ignore = E501, # Ignore line too long W605, # Ignore invalid escape sequence '\*' diff --git a/vitessce/widget_plugins/__init__.py b/vitessce/widget_plugins/__init__.py new file mode 100644 index 00000000..72b2a498 --- /dev/null +++ b/vitessce/widget_plugins/__init__.py @@ -0,0 +1 @@ +from .demo_plugin import PLUGIN_ESM as demo_plugin_esm diff --git a/vitessce/widget_plugins/demo_plugin.py b/vitessce/widget_plugins/demo_plugin.py new file mode 100644 index 00000000..47790813 --- /dev/null +++ b/vitessce/widget_plugins/demo_plugin.py @@ -0,0 +1,50 @@ +from esbuild_py import transform + +PLUGIN_ESM = transform(""" +function createPlugins(utilsForPlugins) { + const { + React, + PluginFileType, + PluginViewType, + PluginCoordinationType, + PluginJointFileType, + z, + useCoordination, + } = utilsForPlugins; + function DemoView(props) { + const { coordinationScopes } = props; + const [{ + obsType, + }, { + setObsType, + }] = useCoordination(['obsType'], coordinationScopes); + + return ( +
+

Demo plugin view

+

obsType: {obsType}

+ +
+ ); + } + + const pluginViewTypes = [ + new PluginViewType('demo', DemoView, ['obsType']), + ]; + return { pluginViewTypes }; +} +export default { createPlugins }; +""") +""" +Example of a minimal plugin view that gets the obsType coordination value from the coordination space and renders a button. +This plugin view is not meant to be useful for end-users, but rather to demonstrate how to develop a plugin view that uses coordination (and uses eslint_py for JSX transformation). + +:meta hide-value: + +.. code-block:: python + + from vitessce.widget_plugins import demo_plugin_esm + + # ... + vc.widget(plugin_esm=demo_plugin_esm) +""" diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index f8f70269..921ffa8d 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -943,7 +943,7 @@ def __init__(self, adata_path=None, adata_url=None, adata_store=None, obs_featur :param str adata_path: A path to an AnnData object written to a Zarr store containing single-cell experiment data. :param str adata_url: A remote url pointing to a zarr-backed AnnData store. - :param adata_store: A path to pass to zarr.FSStore, or an existing store instance. + :param adata_store: A path to pass to zarr.DirectoryStore, or an existing store instance. :type adata_store: str or zarr.Storage :param str obs_feature_matrix_path: Location of the expression (cell x gene) matrix, like `X` or `obsm/highly_variable_genes_subset` :param str feature_filter_path: A string like `var/highly_variable` used in conjunction with `obs_feature_matrix_path` if obs_feature_matrix_path points to a subset of `X` of the full `var` list. From 07c1db2a094d74c2a2940eed50f6c42e0e6f5d7d Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Thu, 16 May 2024 16:18:40 +0200 Subject: [PATCH 13/58] (fix): adapt to api --- vitessce/wrappers.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index ccfa59fb..78c6f701 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1177,13 +1177,13 @@ def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_key obs_set_elems = [f"table/{table_key}/obs/{elem}" for elem in table.obs if table.obs[elem].dtype == 'category'] wrappers += [ cls( - base_path = str(sdata.path), - image_elem = str(image_elem) if image_elem is not None else None, - labels_elem = str(labels_elem) if labels_elem is not None else None, - obs_feature_matrix_elem = str(obs_feature_matrix_elem), - shapes_elem = str(shapes_elem) if shapes_elem is not None else None, - initial_feature_filter_elem = initial_feature_filter_elem, - obs_set_elems = obs_set_elems, + adata_path = str(sdata.path), + image_path = str(image_elem) if image_elem is not None else None, + labels_path = str(labels_elem) if labels_elem is not None else None, + obs_feature_matrix_path = str(obs_feature_matrix_elem), + shapes_path = str(shapes_elem) if shapes_elem is not None else None, + initial_feature_filter_path = initial_feature_filter_elem, + obs_set_paths = obs_set_elems, coordination_values={"obsType":"spot"} # TODO: should we remove? ) ] From 933d6de3163455151597d4a282609056de0fc9cd Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Thu, 16 May 2024 17:29:57 +0200 Subject: [PATCH 14/58] (chore): remove erroneous notebook --- docs/notebooks/spatial_data-Copy1.ipynb | 300 ------------------------ 1 file changed, 300 deletions(-) delete mode 100644 docs/notebooks/spatial_data-Copy1.ipynb diff --git a/docs/notebooks/spatial_data-Copy1.ipynb b/docs/notebooks/spatial_data-Copy1.ipynb deleted file mode 100644 index 904fd247..00000000 --- a/docs/notebooks/spatial_data-Copy1.ipynb +++ /dev/null @@ -1,300 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "# Vitessce Widget Tutorial" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Visualization of SpatialData Object" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1. Import dependencies\n", - "\n", - "We need to import the classes and functions that we will be using from the corresponding packages." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The autoreload extension is already loaded. To reload it, use:\n", - " %reload_ext autoreload\n" - ] - } - ], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2\n", - "\n", - "from pathlib import Path\n", - "from urllib.request import urlretrieve\n", - "import dask\n", - "\n", - "dask.config.set({'dataframe.query-planning-warning': False})\n", - "\n", - "from spatialdata import read_zarr\n", - "import scanpy as sc\n", - "\n", - "from vitessce import (\n", - " VitessceConfig,\n", - " Component as cm,\n", - " CoordinationType as ct,\n", - " OmeZarrWrapper,\n", - " CoordinationLevel as CL,\n", - ")\n", - "from vitessce.data_utils import (\n", - " optimize_adata,\n", - " VAR_CHUNK_SIZE,\n", - ")\n", - "import zipfile\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "zip_filepath = Path(\"data/visium.spatialdata.zarr.zip\")\n", - "spatialdata_filepath = zip_filepath.with_suffix('')\n", - "if not zip_filepath.exists():\n", - " spatialdata_filepath.parent.mkdir(exist_ok=True)\n", - " urlretrieve('https://s3.embl.de/spatialdata/spatialdata-sandbox/visium_associated_xenium_io.zip', zip_filepath)\n", - " with zipfile.ZipFile(zip_filepath,\"r\") as zip_ref:\n", - " zip_ref.extractall(spatialdata_filepath.parent)\n", - " (spatialdata_filepath.parent / \"data.zarr\").rename(spatialdata_filepath)\n", - " (spatialdata_filepath / \"tables\").rename(spatialdata_filepath / \"table\") # TODO: remove once fixed in Vitessce" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3. Load the data\n", - "\n", - "Note: this function may print a `FutureWarning`" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_19455/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", - " spatialdata = read_zarr(spatialdata_filepath)\n" - ] - } - ], - "source": [ - "spatialdata = read_zarr(spatialdata_filepath)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "SpatialData object with:\n", - "├── Images\n", - "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_full_image': MultiscaleSpatialImage[cyx] (3, 21571, 19505), (3, 10785, 9752), (3, 5392, 4876), (3, 2696, 2438), (3, 1348, 1219)\n", - "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_hires_image': SpatialImage[cyx] (3, 2000, 1809)\n", - "│ └── 'CytAssist_FFPE_Human_Breast_Cancer_lowres_image': SpatialImage[cyx] (3, 600, 543)\n", - "├── Shapes\n", - "│ └── 'CytAssist_FFPE_Human_Breast_Cancer': GeoDataFrame shape: (4992, 2) (2D shapes)\n", - "└── Tables\n", - " └── 'table': AnnData (4992, 18085)\n", - "with coordinate systems:\n", - "▸ 'downscaled_hires', with elements:\n", - " CytAssist_FFPE_Human_Breast_Cancer_hires_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", - "▸ 'downscaled_lowres', with elements:\n", - " CytAssist_FFPE_Human_Breast_Cancer_lowres_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", - "▸ 'global', with elements:\n", - " CytAssist_FFPE_Human_Breast_Cancer_full_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "spatialdata" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 4. Create the Vitessce widget configuration\n", - "\n", - "Vitessce needs to know which pieces of data we are interested in visualizing, the visualization types we would like to use, and how we want to coordinate (or link) the views." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4.1. Instantiate a `VitessceConfig` object\n", - "\n", - "Use the `VitessceConfig` constructor to create an instance." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "vc = VitessceConfig(schema_version=\"1.0.16\", name='Visium OmeZarr Demo (visium_associated_xenium_io)', description='From https://spatialdata.scverse.org/en/latest/tutorials/notebooks/datasets/README.html')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4.2. Add a dataset to the `VitessceConfig` instance\n", - "\n", - "In Vitessce, a dataset is a container for one file per data type. The `.add_dataset(name)` method on the `vc` instance sets up and returns a new dataset instance.\n", - "\n", - "Then, we can call the dataset's `.add_object(wrapper_object)` method to attach a \"data wrapper\" instance to our new dataset. For example, the `AnnDataWrapper` helps to configure AnnData Zarr stores for use in the Vitessce configuration.\n", - "\n", - "Dataset wrapper classes may require additional parameters to resolve ambiguities. For instance, `AnnData` objects may store multiple clusterings or cell type annotation columns in the `adata.obs` dataframe. We can use the parameter `obs_set_paths` to tell Vitessce that certain columns of the `obs` dataframe correspond to cell type annotations or cell clusterings." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "wrapper = OmeZarrWrapper(\n", - " img_path=spatialdata_filepath / \"images/CytAssist_FFPE_Human_Breast_Cancer_full_image\",\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "dataset = vc.add_dataset(name='Breast Cancer Visium', uid='Visium').add_object(wrapper)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "spatial = vc.add_view(\"spatialBeta\", dataset=dataset)\n", - "layer_controller = vc.add_view(\"layerControllerBeta\", dataset=dataset)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4.4. Define the visualization layout\n", - "\n", - "The `vc.layout(view_concat)` method allows us to specify how our views will be arranged in the layout grid in the widget. The `|` and `/` characters are magic syntax for `hconcat(v1, v2)` and `vconcat(v1, v2)`, respectively." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "vc.layout(spatial | layer_controller);" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "bf34d533620949c7a5021930af4c0f1f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium OmeZarr Demo (visium_associated_xenium_io)', 'desc…" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "vw = vc.widget()\n", - "vw" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "vw.close()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.0" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} From 596c420acb085c39ea7358ecb796918d63e6cdd8 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 12:00:56 +0200 Subject: [PATCH 15/58] (fix): add type support --- vitessce/wrappers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 78c6f701..0fcf5d32 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections import defaultdict import os from os.path import join From 50ce52ba411a89ebfe0a6df5ade3d4248ec2907e Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 12:08:18 +0200 Subject: [PATCH 16/58] (chore): add test --- tests/test_wrappers.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index d351cb1c..2ad1658e 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -22,7 +22,7 @@ ObsSegmentationsOmeZarrWrapper, ) -from vitessce.wrappers import file_path_to_url_path +from vitessce.wrappers import SpatialDataWrapper, file_path_to_url_path data_path = Path('tests/data') @@ -334,3 +334,17 @@ def test_multivec_zarr_with_base_dir(self): 'fileType': 'genomic-profiles.zarr', 'url': 'http://localhost:8000/test_out.snap.multivec.zarr', }) + + def test_spatial_data_with_base_dir(self): + + spatial_data_path = 'test.spatialdata.zarr' + w = SpatialDataWrapper(adata_path=spatial_data_path, image_elem="picture", obs_set_paths=['obs/CellType'], obs_set_names=['Cell Type'], obs_embedding_paths=[ + 'obsm/X_umap'], obs_embedding_names=['UMAP']) + w.base_dir = data_path + w.local_dir_uid = 'spatialdata.zarr' + + file_def_creator = w.make_file_def_creator('A', 0) + file_def = file_def_creator('http://localhost:8000') + print(file_def) + self.assertEqual(file_def, + {'fileType': 'spatialdata.zarr', 'url': 'http://localhost:8000/test.spatialdata.zarr', 'options': {'obsSets': {'obsSets': [{'name': 'Cell Type', 'path': 'obs/CellType'}]}, 'image': {'path': 'picture'}}}) \ No newline at end of file From c8c93aad766453555ad9085535733aaef665be6b Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 12:11:38 +0200 Subject: [PATCH 17/58] (fix): automatic lint --- tests/test_wrappers.py | 6 +++--- vitessce/utils.py | 17 +++++++++++------ vitessce/wrappers.py | 41 +++++++++++++++++++++-------------------- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index 2ad1658e..50754b54 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -339,12 +339,12 @@ def test_spatial_data_with_base_dir(self): spatial_data_path = 'test.spatialdata.zarr' w = SpatialDataWrapper(adata_path=spatial_data_path, image_elem="picture", obs_set_paths=['obs/CellType'], obs_set_names=['Cell Type'], obs_embedding_paths=[ - 'obsm/X_umap'], obs_embedding_names=['UMAP']) + 'obsm/X_umap'], obs_embedding_names=['UMAP']) w.base_dir = data_path w.local_dir_uid = 'spatialdata.zarr' file_def_creator = w.make_file_def_creator('A', 0) file_def = file_def_creator('http://localhost:8000') print(file_def) - self.assertEqual(file_def, - {'fileType': 'spatialdata.zarr', 'url': 'http://localhost:8000/test.spatialdata.zarr', 'options': {'obsSets': {'obsSets': [{'name': 'Cell Type', 'path': 'obs/CellType'}]}, 'image': {'path': 'picture'}}}) \ No newline at end of file + self.assertEqual(file_def, + {'fileType': 'spatialdata.zarr', 'url': 'http://localhost:8000/test.spatialdata.zarr', 'options': {'obsSets': {'obsSets': [{'name': 'Cell Type', 'path': 'obs/CellType'}]}, 'image': {'path': 'picture'}}}) diff --git a/vitessce/utils.py b/vitessce/utils.py index d8a39f7e..fc29bbea 100644 --- a/vitessce/utils.py +++ b/vitessce/utils.py @@ -41,7 +41,6 @@ def get_initial_coordination_scope_name(dataset_uid, data_type, i=None): return f"{prefix}{0 if i is None else i}" - def gen_obs_embedding_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None, dims: Optional[list[list[int]]] = None): if paths is not None: if names is not None: @@ -64,6 +63,7 @@ def gen_obs_embedding_schema(options: dict, paths: Optional[list[str]] = None, n options["obsEmbedding"][dim_i]['dims'] = dim return options + def gen_obs_sets_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None): if paths is not None: options["obsSets"] = [] @@ -78,6 +78,7 @@ def gen_obs_sets_schema(options: dict, paths: Optional[list[str]] = None, names: }) return options + def gen_obs_feature_matrix_schema(options: dict, matrix_path: Optional[str] = None, var_filter_path: Optional[str] = None, init_var_filter_path: Optional[str] = None): if matrix_path is not None: options["obsFeatureMatrix"] = { @@ -89,6 +90,7 @@ def gen_obs_feature_matrix_schema(options: dict, matrix_path: Optional[str] = No options["obsFeatureMatrix"]["initialFeatureFilterPath"] = init_var_filter_path return options + def gen_obs_labels_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None): if paths is not None: if names is not None and len(paths) == len(names): @@ -112,12 +114,14 @@ def gen_path_schema(key: str, path: Optional[str], options: dict): } return options -gen_obs_locations_schema = partial(gen_path_schema, "obsLocations") -gen_obs_segmentations_schema = partial(gen_path_schema, "obsSegmentations") -gen_obs_spots_schema = partial(gen_path_schema, "obsSpots") -gen_obs_points_schema = partial(gen_path_schema, "obsPoints") + +gen_obs_locations_schema = partial(gen_path_schema, "obsLocations") +gen_obs_segmentations_schema = partial(gen_path_schema, "obsSegmentations") +gen_obs_spots_schema = partial(gen_path_schema, "obsSpots") +gen_obs_points_schema = partial(gen_path_schema, "obsPoints") gen_feature_labels_schema = partial(gen_path_schema, "featureLabels") + def gen_image_schema(options, path: str, affine_transformation: Optional[np.ndarray] = None) -> dict: if path is not None: options["image"] = { @@ -127,6 +131,7 @@ def gen_image_schema(options, path: str, affine_transformation: Optional[np.ndar options['coordinateTransformations'] = affine_transformation return options + def gen_obs_spots_schema(options: dict, shapes_path: Optional[str] = None) -> dict: if shapes_path is not None: options['obsSpots'] = { @@ -134,4 +139,4 @@ def gen_obs_spots_schema(options: dict, shapes_path: Optional[str] = None) -> di "tablePath": "table/table", "region": "region" } - return options \ No newline at end of file + return options diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 0fcf5d32..6aa422db 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1100,12 +1100,15 @@ def auto_view_config(self, vc): vc.layout((scatterplot | (cell_sets / genes)) / heatmap) -SpatialDataWrapperType = TypeVar('SpatialDataWrapperType', bound='SpatialDataWrapper') + +SpatialDataWrapperType = TypeVar('SpatialDataWrapperType', bound='SpatialDataWrapper') + + class SpatialDataWrapper(AnnDataWrapper): - def __init__(self, *, image_elem: Optional[str]=None, affine_transformation: Optional[np.ndarray] = None, shapes_elem: Optional[str] = None, labels_elem: Optional[str] = None, **kwargs): - super().__init__(**kwargs) # HACK to use adata_path - self.local_dir_uid = make_unique_filename(".spatialdata.zarr") # correct? + def __init__(self, *, image_elem: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None, shapes_elem: Optional[str] = None, labels_elem: Optional[str] = None, **kwargs): + super().__init__(**kwargs) # HACK to use adata_path + self.local_dir_uid = make_unique_filename(".spatialdata.zarr") # correct? self._image_elem = image_elem self._affine_transformation = affine_transformation self._kwargs = kwargs @@ -1123,14 +1126,14 @@ def __init__(self, *, image_elem: Optional[str]=None, affine_transformation: Opt else: self.is_remote = True self.zarr_folder = None - + @classmethod - def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_keys_to_image_elems: dict[str, Union[str,None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str,None]] = defaultdict(type(None))) -> list[SpatialDataWrapperType]: + def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_keys_to_image_elems: dict[str, Union[str, None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str, None]] = defaultdict(type(None))) -> list[SpatialDataWrapperType]: """Instantiate a wrapper for SpatialData stores, one per table, directly from the SpatialData object. By default, we "show everything" that can reasonable be inferred given the information. If you wish to have more control, consider instantiating the object directly. This function will error if something cannot be inferred i.e., the user does not present regions explicitly but there is more than one for a given table. - + Parameters ---------- @@ -1172,32 +1175,32 @@ def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_key labels_elem = f"labels/{region}" obs_feature_matrix_elem = f"table/{table_key}/X" if 'highly_variable' in table.var: - # TODO: fix first key needing to be "table" in vitessce-js + # TODO: fix first key needing to be "table" in vitessce-js initial_feature_filter_elem = 'highly_variable' else: initial_feature_filter_elem = None obs_set_elems = [f"table/{table_key}/obs/{elem}" for elem in table.obs if table.obs[elem].dtype == 'category'] wrappers += [ cls( - adata_path = str(sdata.path), - image_path = str(image_elem) if image_elem is not None else None, - labels_path = str(labels_elem) if labels_elem is not None else None, - obs_feature_matrix_path = str(obs_feature_matrix_elem), - shapes_path = str(shapes_elem) if shapes_elem is not None else None, - initial_feature_filter_path = initial_feature_filter_elem, - obs_set_paths = obs_set_elems, - coordination_values={"obsType":"spot"} # TODO: should we remove? + adata_path=str(sdata.path), + image_path=str(image_elem) if image_elem is not None else None, + labels_path=str(labels_elem) if labels_elem is not None else None, + obs_feature_matrix_path=str(obs_feature_matrix_elem), + shapes_path=str(shapes_elem) if shapes_elem is not None else None, + initial_feature_filter_path=initial_feature_filter_elem, + obs_set_paths=obs_set_elems, + coordination_values={"obsType": "spot"} # TODO: should we remove? ) ] return wrappers - + def make_file_def_creator(self, dataset_uid: str, obj_i: str) -> Optional[Callable]: def generator(base_url): options = {} options = gen_obs_labels_schema(options, self._obs_labels_elems, self._obs_labels_names) options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) options = gen_obs_sets_schema(options, self._obs_set_elems, self._obs_set_names) - options['obsSets'] = { 'obsSets': options['obsSets'] } # see https://github.com/vitessce/vitessce/blob/cd7e81956786a8130658d6745ff03986e2e6f806/packages/schemas/src/file-def-options.ts#L138-L146 for nested structure + options['obsSets'] = {'obsSets': options['obsSets']} # see https://github.com/vitessce/vitessce/blob/cd7e81956786a8130658d6745ff03986e2e6f806/packages/schemas/src/file-def-options.ts#L138-L146 for nested structure options = gen_obs_spots_schema(options, self._shapes_elem) options = gen_image_schema(options, self._image_elem, self._affine_transformation) options = gen_feature_labels_schema(self._feature_labels, options) @@ -1216,8 +1219,6 @@ def generator(base_url): return generator - - class MultivecZarrWrapper(AbstractWrapper): From f906eb320ad22f1354bc32afdc3f570243dbaf01 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 12:18:42 +0200 Subject: [PATCH 18/58] (fix): add default for `obsEmbedding` --- vitessce/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vitessce/utils.py b/vitessce/utils.py index fc29bbea..f85d27cd 100644 --- a/vitessce/utils.py +++ b/vitessce/utils.py @@ -43,6 +43,8 @@ def get_initial_coordination_scope_name(dataset_uid, data_type, i=None): def gen_obs_embedding_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None, dims: Optional[list[list[int]]] = None): if paths is not None: + if "obsEmbedding" not in options: + options["obsEmbedding"] = [] if names is not None: for key, mapping in zip(paths, names): options["obsEmbedding"].append({ @@ -59,6 +61,8 @@ def gen_obs_embedding_schema(options: dict, paths: Optional[list[str]] = None, n "embeddingType": mapping_key }) if dims is not None: + if "obsEmbedding" not in options: + options["obsEmbedding"] = [] for dim_i, dim in enumerate(dims): options["obsEmbedding"][dim_i]['dims'] = dim return options @@ -132,7 +136,7 @@ def gen_image_schema(options, path: str, affine_transformation: Optional[np.ndar return options -def gen_obs_spots_schema(options: dict, shapes_path: Optional[str] = None) -> dict: +def gen_obs_spots_from_shapes_schema(options: dict, shapes_path: Optional[str] = None) -> dict: if shapes_path is not None: options['obsSpots'] = { "path": shapes_path, From eaf63a7d27c4be8d3d39d27f6dcafae129c773cb Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 12:18:51 +0200 Subject: [PATCH 19/58] (fix): redefinition --- vitessce/wrappers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 6aa422db..7e33f3ba 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -13,7 +13,7 @@ import numpy as np from spatialdata import SpatialData -from vitessce.utils import gen_obs_locations_schema, gen_obs_segmentations_schema, gen_obs_spots_schema, gen_obs_points_schema, gen_obs_embedding_schema, gen_feature_labels_schema, gen_obs_spots_schema, gen_feature_labels_schema, gen_image_schema, gen_obs_feature_matrix_schema, gen_obs_labels_schema, gen_obs_sets_schema +from vitessce.utils import gen_obs_locations_schema, gen_obs_segmentations_schema, gen_obs_spots_from_shapes_schema, gen_obs_spots_schema, gen_obs_points_schema, gen_obs_embedding_schema, gen_feature_labels_schema, gen_image_schema, gen_obs_feature_matrix_schema, gen_obs_labels_schema, gen_obs_sets_schema from .constants import ( norm_enum, @@ -1201,7 +1201,7 @@ def generator(base_url): options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) options = gen_obs_sets_schema(options, self._obs_set_elems, self._obs_set_names) options['obsSets'] = {'obsSets': options['obsSets']} # see https://github.com/vitessce/vitessce/blob/cd7e81956786a8130658d6745ff03986e2e6f806/packages/schemas/src/file-def-options.ts#L138-L146 for nested structure - options = gen_obs_spots_schema(options, self._shapes_elem) + options = gen_obs_spots_from_shapes_schema(options, self._shapes_elem) options = gen_image_schema(options, self._image_elem, self._affine_transformation) options = gen_feature_labels_schema(self._feature_labels, options) if len(options.keys()) > 0: From ba10f0e8ccccbe0be38a31ff7091311e5986e95e Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 12:22:51 +0200 Subject: [PATCH 20/58] (fix): `path` and `embeddingType` swapped --- tests/test_wrappers.py | 21 +++++++++++++++++++-- vitessce/utils.py | 4 ++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index 50754b54..f09feef8 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -233,10 +233,27 @@ def test_anndata_with_base_dir(self): file_def_creator = w.make_file_def_creator('A', 0) file_def = file_def_creator('http://localhost:8000') + print(file_def) self.assertEqual(file_def, {'fileType': 'anndata.zarr', 'url': 'http://localhost:8000/test.h5ad.zarr', 'options': { - 'obsEmbedding': [{'path': 'obsm/X_umap', 'embeddingType': 'UMAP', 'dims': [0, 1]}], - 'obsSets': [{'path': 'obs/CellType', 'name': 'Cell Type'}] + 'obsEmbedding': [{'path': 'obsm/X_umap', 'dims': [0, 1], 'embeddingType': 'UMAP'}], + 'obsSets': [{'name': 'Cell Type', 'path': 'obs/CellType'}] + }}) + + def test_anndata_with_base_dir_no_names(self): + adata_path = 'test.h5ad.zarr' + w = AnnDataWrapper(adata_path, obs_set_paths=['obs/CellType'], obs_embedding_paths=[ + 'obsm/X_umap']) + w.base_dir = data_path + w.local_dir_uid = 'anndata.zarr' + + file_def_creator = w.make_file_def_creator('A', 0) + file_def = file_def_creator('http://localhost:8000') + print(file_def) + self.assertEqual(file_def, {'fileType': 'anndata.zarr', 'url': 'http://localhost:8000/test.h5ad.zarr', + 'options': { + 'obsEmbedding': [{'path': 'obsm/X_umap', 'dims': [0, 1], 'embeddingType': 'X_umap'}], + 'obsSets': [{'name': 'CellType', 'path': 'obs/CellType'}] }}) def test_csv(self): diff --git a/vitessce/utils.py b/vitessce/utils.py index f85d27cd..92833923 100644 --- a/vitessce/utils.py +++ b/vitessce/utils.py @@ -48,9 +48,9 @@ def gen_obs_embedding_schema(options: dict, paths: Optional[list[str]] = None, n if names is not None: for key, mapping in zip(paths, names): options["obsEmbedding"].append({ - "path": mapping, + "path": key, "dims": [0, 1], - "embeddingType": key + "embeddingType": mapping }) else: for mapping in paths: From 6ea8c33be9b2a0fd2160019c378fea0cd406062b Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 12:33:17 +0200 Subject: [PATCH 21/58] (fix): add annotattions to `utils.py` --- vitessce/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vitessce/utils.py b/vitessce/utils.py index 92833923..e475a92c 100644 --- a/vitessce/utils.py +++ b/vitessce/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from functools import partial from typing import Optional From c43845f401a9d542c9893f94aa14235ccb37e308 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 12:36:36 +0200 Subject: [PATCH 22/58] (chore): add `spatialdata` dep --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 5bac98af..0b837271 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ 'black>=21.11b1', 'numpy>=1.21.2', 'anndata>=0.7.8,<0.11', + 'spatialdata>=0.1.2' 'scanpy>=1.9.3', 'ome-zarr==0.8.3', 'tifffile>=2020.10.1', From 91f27b652a992b2207eab1055ed42e25584965f4 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 13:32:57 +0200 Subject: [PATCH 23/58] (fix): oops, comma for `toml` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0b837271..b50d9469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ 'black>=21.11b1', 'numpy>=1.21.2', 'anndata>=0.7.8,<0.11', - 'spatialdata>=0.1.2' + 'spatialdata>=0.1.2', 'scanpy>=1.9.3', 'ome-zarr==0.8.3', 'tifffile>=2020.10.1', From 7be038ea5a41f5172e3ba283e2a28e172fda1b6d Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 13:36:55 +0200 Subject: [PATCH 24/58] (fix): no more python3.8 bc of spatialdata --- .github/workflows/test.yml | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d29bbec8..2dddba77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - version: ['3.8', '3.12'] + version: ['3.9', '3.12'] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 diff --git a/pyproject.toml b/pyproject.toml index b50d9469..78c51b40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ description = "Jupyter widget facilitating interactive visualization of spatial single-cell data with Vitessce" readme = "README.md" license = {file = "LICENSE"} -requires-python = ">=3.7" +requires-python = ">=3.9" keywords = ["ipython", "jupyter", "widgets"] classifiers = [ 'Development Status :: 4 - Beta', @@ -19,10 +19,10 @@ classifiers = [ 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'Topic :: Multimedia :: Graphics', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ] dependencies = [ 'zarr>=2.5.0', From eb12a0030340c916c2de437805544f05a178825b Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 13:37:46 +0200 Subject: [PATCH 25/58] (fix): remove whitespace --- tests/test_wrappers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index f09feef8..a72e9f23 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -239,7 +239,7 @@ def test_anndata_with_base_dir(self): 'obsEmbedding': [{'path': 'obsm/X_umap', 'dims': [0, 1], 'embeddingType': 'UMAP'}], 'obsSets': [{'name': 'Cell Type', 'path': 'obs/CellType'}] }}) - + def test_anndata_with_base_dir_no_names(self): adata_path = 'test.h5ad.zarr' w = AnnDataWrapper(adata_path, obs_set_paths=['obs/CellType'], obs_embedding_paths=[ From 8d6472a9be1d21ff501bcc87aa4b9aa5e07d2da2 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 24 May 2024 13:41:26 +0200 Subject: [PATCH 26/58] (api): small api cleanups --- vitessce/wrappers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 7e33f3ba..c3ced9a8 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1106,8 +1106,8 @@ def auto_view_config(self, vc): class SpatialDataWrapper(AnnDataWrapper): - def __init__(self, *, image_elem: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None, shapes_elem: Optional[str] = None, labels_elem: Optional[str] = None, **kwargs): - super().__init__(**kwargs) # HACK to use adata_path + def __init__(self, spatialdata_path: Optional[str] = None, spatialdata_url: Optional[str] = None, spatialdata_store: Optional[str] = None, image_elem: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None, shapes_elem: Optional[str] = None, labels_elem: Optional[str] = None, **kwargs): + super().__init__(adata_path=spatialdata_path, adata_url=spatialdata_url, adata_store=spatialdata_store, **kwargs) self.local_dir_uid = make_unique_filename(".spatialdata.zarr") # correct? self._image_elem = image_elem self._affine_transformation = affine_transformation @@ -1128,7 +1128,7 @@ def __init__(self, *, image_elem: Optional[str] = None, affine_transformation: O self.zarr_folder = None @classmethod - def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_keys_to_image_elems: dict[str, Union[str, None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str, None]] = defaultdict(type(None))) -> list[SpatialDataWrapperType]: + def from_object(cls: Type[SpatialDataWrapperType], spatialdata: SpatialData, table_keys_to_image_elems: dict[str, Union[str, None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str, None]] = defaultdict(type(None))) -> list[SpatialDataWrapperType]: """Instantiate a wrapper for SpatialData stores, one per table, directly from the SpatialData object. By default, we "show everything" that can reasonable be inferred given the information. If you wish to have more control, consider instantiating the object directly. This function will error if something cannot be inferred i.e., the user does not present @@ -1139,7 +1139,7 @@ def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_key ---------- cls : Type[SpatialDataWrapperType] _description_ - sdata : SpatialData + spatialdata : SpatialData _description_ table_keys_to_image_elems : dict[str, str], optional which image paths to use for a given table for the visualization, by default None for each table key. @@ -1155,7 +1155,7 @@ def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_key ValueError """ wrappers = [] - for table_key, table in sdata.tables.items(): + for table_key, table in spatialdata.tables.items(): shapes_elem = None image_elem = table_keys_to_image_elems[table_key] labels_elem = None @@ -1169,9 +1169,9 @@ def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_key if len(region) > 1: raise ValueError("Vitessce cannot subset AnnData objects on the fly. Please provide an explicit region") region = region[0] - if region in sdata.shapes: + if region in spatialdata.shapes: shapes_elem = f"shapes/{region}" - if region in sdata.labels: + if region in spatialdata.labels: labels_elem = f"labels/{region}" obs_feature_matrix_elem = f"table/{table_key}/X" if 'highly_variable' in table.var: @@ -1182,7 +1182,7 @@ def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_key obs_set_elems = [f"table/{table_key}/obs/{elem}" for elem in table.obs if table.obs[elem].dtype == 'category'] wrappers += [ cls( - adata_path=str(sdata.path), + spatialdata_path=str(spatialdata.path), image_path=str(image_elem) if image_elem is not None else None, labels_path=str(labels_elem) if labels_elem is not None else None, obs_feature_matrix_path=str(obs_feature_matrix_elem), From 779d4a11e26d21d4447f9a2df142406543049e37 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Mon, 27 May 2024 08:21:35 +0200 Subject: [PATCH 27/58] (fix): args bug --- tests/test_wrappers.py | 2 +- vitessce/wrappers.py | 51 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index a72e9f23..b487a3be 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -355,7 +355,7 @@ def test_multivec_zarr_with_base_dir(self): def test_spatial_data_with_base_dir(self): spatial_data_path = 'test.spatialdata.zarr' - w = SpatialDataWrapper(adata_path=spatial_data_path, image_elem="picture", obs_set_paths=['obs/CellType'], obs_set_names=['Cell Type'], obs_embedding_paths=[ + w = SpatialDataWrapper(spatialdata_path=spatial_data_path, image_elem="picture", obs_set_paths=['obs/CellType'], obs_set_names=['Cell Type'], obs_embedding_paths=[ 'obsm/X_umap'], obs_embedding_names=['UMAP']) w.base_dir = data_path w.local_dir_uid = 'spatialdata.zarr' diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index c3ced9a8..9230e1c5 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -945,6 +945,15 @@ def image_file_def_creator(base_url): return image_file_def_creator +def raise_error_if_more_than_one_none(inputs): + num_inputs = sum([1 for x in inputs if x is not None]) + if num_inputs > 1: + raise ValueError( + "Expected only one of adata_path, adata_url, or adata_store to be provided") + if num_inputs == 0: + raise ValueError( + "Expected one of adata_path, adata_url, or adata_store to be provided") + return True class AnnDataWrapper(AbstractWrapper): def __init__(self, adata_path=None, adata_url=None, adata_store=None, obs_feature_matrix_path=None, feature_filter_path=None, initial_feature_filter_path=None, obs_set_paths=None, obs_set_names=None, obs_locations_path=None, obs_segmentations_path=None, obs_embedding_paths=None, obs_embedding_names=None, obs_embedding_dims=None, obs_spots_path=None, obs_points_path=None, feature_labels_path=None, obs_labels_path=None, convert_to_dense=True, coordination_values=None, obs_labels_paths=None, obs_labels_names=None, **kwargs): @@ -982,13 +991,7 @@ def __init__(self, adata_path=None, adata_url=None, adata_store=None, obs_featur self._url = adata_url self._store = adata_store - num_inputs = sum([1 for x in [adata_path, adata_url, adata_store] if x is not None]) - if num_inputs > 1: - raise ValueError( - "Expected only one of adata_path, adata_url, or adata_store to be provided") - if num_inputs == 0: - raise ValueError( - "Expected one of adata_path, adata_url, or adata_store to be provided") + raise_error_if_more_than_one_none([adata_path, adata_url, adata_store]) if adata_path is not None: self.is_remote = False @@ -1107,6 +1110,40 @@ def auto_view_config(self, vc): class SpatialDataWrapper(AnnDataWrapper): def __init__(self, spatialdata_path: Optional[str] = None, spatialdata_url: Optional[str] = None, spatialdata_store: Optional[str] = None, image_elem: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None, shapes_elem: Optional[str] = None, labels_elem: Optional[str] = None, **kwargs): + """_summary_ + + Parameters + ---------- + spatialdata_path : Optional[str], optional + SpatialData path, exclusive with other `{spatialdata,adata}_xxxx` arguments, by default None + spatialdata_url : Optional[str], optional + SpatialData url, exclusive with other `{spatialdata,adata}_xxxx` arguments, by default None + spatialdata_store : Optional[str], optional + SpatialData store, exclusive with other `{spatialdata,adata}_xxxx` arguments, by default None + image_elem : Optional[str], optional + location of the image, by default None + affine_transformation : Optional[np.ndarray], optional + transformation to be applied to the image, by default None + shapes_elem : Optional[str], optional + location of the shapes, by default None + labels_elem : Optional[str], optional + location of the labels, by default None + + Raises + ------ + ValueError + If more than one of `{spatialdata,adata}_xxxx` is not `None` or all are. + """ + raise_error_if_more_than_one_none( + [ + spatialdata_path, + kwargs.get('adata_path', None), + spatialdata_url, + kwargs.get('adata_url', None), + spatialdata_store, + kwargs.get('adata_store', None) + ] + ) super().__init__(adata_path=spatialdata_path, adata_url=spatialdata_url, adata_store=spatialdata_store, **kwargs) self.local_dir_uid = make_unique_filename(".spatialdata.zarr") # correct? self._image_elem = image_elem From 05c8dbd26c9d108634de6214e9dd2958d3b8e165 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Mon, 27 May 2024 08:21:42 +0200 Subject: [PATCH 28/58] (fix): try spatialdata from master --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 78c51b40..19132f99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ 'black>=21.11b1', 'numpy>=1.21.2', 'anndata>=0.7.8,<0.11', - 'spatialdata>=0.1.2', + 'spatialdata@git+https://github.com/scverse/spatialdata.git', 'scanpy>=1.9.3', 'ome-zarr==0.8.3', 'tifffile>=2020.10.1', From 3862c7052391b3e33df60de1dfc0c77cd29b1935 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Mon, 27 May 2024 08:24:35 +0200 Subject: [PATCH 29/58] (chore): lint --- vitessce/wrappers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 9230e1c5..fb76aff4 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -945,16 +945,20 @@ def image_file_def_creator(base_url): return image_file_def_creator + def raise_error_if_more_than_one_none(inputs): num_inputs = sum([1 for x in inputs if x is not None]) if num_inputs > 1: raise ValueError( - "Expected only one of adata_path, adata_url, or adata_store to be provided") + "Expected only one of adata_path, adata_url, or adata_store to be provided" + ) if num_inputs == 0: raise ValueError( - "Expected one of adata_path, adata_url, or adata_store to be provided") + "Expected one of adata_path, adata_url, or adata_store to be provided" + ) return True + class AnnDataWrapper(AbstractWrapper): def __init__(self, adata_path=None, adata_url=None, adata_store=None, obs_feature_matrix_path=None, feature_filter_path=None, initial_feature_filter_path=None, obs_set_paths=None, obs_set_names=None, obs_locations_path=None, obs_segmentations_path=None, obs_embedding_paths=None, obs_embedding_names=None, obs_embedding_dims=None, obs_spots_path=None, obs_points_path=None, feature_labels_path=None, obs_labels_path=None, convert_to_dense=True, coordination_values=None, obs_labels_paths=None, obs_labels_names=None, **kwargs): """ From 5ae1daea28ce8d0d37158ecc7f3c89f68116479e Mon Sep 17 00:00:00 2001 From: Nikolay Akhmetov Date: Mon, 17 Jun 2024 13:34:43 -0400 Subject: [PATCH 30/58] NickAkhmetov/Cap `numpy` under v2 (#349) * Cap `numpy` under v2 * bump patch version --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 58ac240d..8c7c465d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vitessce" -version = "3.2.6" +version = "3.2.7" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ] @@ -31,7 +31,7 @@ dependencies = [ 'negspy>=0.2.24', 'pandas>=1.1.2', 'black>=21.11b1', - 'numpy>=1.21.2', + 'numpy>=1.21.2,<2.0', 'anndata>=0.7.8,<0.11', 'scanpy>=1.9.3', 'ome-zarr==0.8.3', From 559e6c54f6222c86d07a9ca50fadbf478b401dcc Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 25 Jun 2024 15:19:31 +0200 Subject: [PATCH 31/58] (chore): try `spatialdata` update --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 19132f99..1e66f253 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,9 +33,9 @@ dependencies = [ 'black>=21.11b1', 'numpy>=1.21.2', 'anndata>=0.7.8,<0.11', - 'spatialdata@git+https://github.com/scverse/spatialdata.git', + 'spatialdata@git+https://github.com/scverse/spatialdata.git#egg=fix/dask', 'scanpy>=1.9.3', - 'ome-zarr==0.8.3', + 'ome-zarr>=0.8.3', 'tifffile>=2020.10.1', 'jsonschema>=3.2', 'tqdm>=4.1.0' From 31bd2ccad537fcc546f285417b10dd31cb0e4e09 Mon Sep 17 00:00:00 2001 From: Nikolay Akhmetov Date: Fri, 19 Jul 2024 13:09:51 -0400 Subject: [PATCH 32/58] NickAkhmetov/Copy `requestInit` when using `from_dict` (#352) --- pyproject.toml | 2 +- tests/test_config.py | 14 ++++++++++++-- vitessce/config.py | 17 +++++++++++++---- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c7c465d..b7731d97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vitessce" -version = "3.2.7" +version = "3.2.8" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ] diff --git a/tests/test_config.py b/tests/test_config.py index 86c0ed2b..1fb545a5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -522,7 +522,12 @@ def test_config_from_dict(): 'files': [ { 'url': 'http://cells.json', - 'fileType': 'cells.json' + 'fileType': 'cells.json', + 'requestInit': { + 'headers': { + 'Authorization': 'Bearer token' + } + } } ] } @@ -568,7 +573,12 @@ def test_config_from_dict(): 'files': [ { 'url': 'http://cells.json', - 'fileType': 'cells.json' + 'fileType': 'cells.json', + 'requestInit': { + 'headers': { + 'Authorization': 'Bearer token' + } + } } ] }, diff --git a/vitessce/config.py b/vitessce/config.py index 4663ef38..040674ea 100644 --- a/vitessce/config.py +++ b/vitessce/config.py @@ -50,7 +50,7 @@ class VitessceConfigDatasetFile: A class to represent a file (described by a URL, data type, and file type) in a Vitessce view config dataset. """ - def __init__(self, file_type, url=None, coordination_values=None, options=None, data_type=None): + def __init__(self, file_type, url=None, coordination_values=None, options=None, data_type=None, request_init=None): """ Not meant to be instantiated directly, but instead created and returned by the ``VitessceConfigDataset.add_file()`` method. @@ -62,6 +62,8 @@ def __init__(self, file_type, url=None, coordination_values=None, options=None, :param options: Extra options to pass to the file loader class. :type options: dict or list or None :param data_type: Deprecated / not used. Only included for backwards compatibility with the old API. + :param request_init: Optional request init object to pass to the fetch API. + :type request_init: dict or None """ self.file = { "fileType": file_type @@ -72,6 +74,8 @@ def __init__(self, file_type, url=None, coordination_values=None, options=None, self.file["options"] = options if coordination_values: self.file["coordinationValues"] = coordination_values + if request_init: + self.file["requestInit"] = request_init def __repr__(self): repr_dict = { @@ -83,6 +87,8 @@ def __repr__(self): repr_dict["coordination_values"] = self.file["coordinationValues"] if "options" in self.file: repr_dict["options"] = self.file["options"] + if "requestInit" in self.file: + repr_dict["request_init"] = self.file["requestInit"] return make_repr(repr_dict, class_def=self.__class__) @@ -135,7 +141,7 @@ def get_uid(self): """ return self.dataset["uid"] - def add_file(self, file_type, url=None, coordination_values=None, options=None, data_type=None): + def add_file(self, file_type, url=None, coordination_values=None, options=None, data_type=None, request_init=None): """ Add a new file definition to this dataset instance. @@ -148,6 +154,8 @@ def add_file(self, file_type, url=None, coordination_values=None, options=None, :param options: Extra options to pass to the file loader class. Optional. :type options: dict or list or None :param data_type: Deprecated / not used. Only included for backwards compatibility with the old API. + :param request_init: Optional request init object to pass to the fetch API. + :type request_init: dict or None :returns: Self, to allow function chaining. :rtype: VitessceConfigDataset @@ -171,7 +179,7 @@ def add_file(self, file_type, url=None, coordination_values=None, options=None, file_type_str = norm_enum(file_type, ft) self._add_file(VitessceConfigDatasetFile( - url=url, file_type=file_type_str, coordination_values=coordination_values, options=options)) + url=url, file_type=file_type_str, coordination_values=coordination_values, options=options, request_init=request_init)) return self def _add_file(self, obj): @@ -1606,7 +1614,8 @@ def from_dict(config): file_type=f["fileType"], url=f.get("url"), coordination_values=f.get("coordinationValues"), - options=f.get("options") + options=f.get("options"), + request_init=f.get("requestInit") ) if 'coordinationSpace' in config: for c_type in config['coordinationSpace'].keys(): From 2e36cb27784c501502cec39823643e5aec371b5e Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:02:14 -0600 Subject: [PATCH 33/58] Plugin class (#350) * WIP: class to encapsulate widget logic * Potentially working widget things * Example of chat plugin * Lint * Update * Update --- .coveragerc_omit | 3 +- docs/notebooks/widget_plugin_custom.ipynb | 207 ++++++++++++++++++++++ docs/notebooks/widget_plugin_demo.ipynb | 139 +++++++++++++++ pyproject.toml | 5 +- vitessce/__init__.py | 2 +- vitessce/widget.py | 108 ++++++++--- vitessce/widget_plugins/__init__.py | 2 +- vitessce/widget_plugins/demo_plugin.py | 22 ++- 8 files changed, 454 insertions(+), 34 deletions(-) create mode 100644 docs/notebooks/widget_plugin_custom.ipynb create mode 100644 docs/notebooks/widget_plugin_demo.ipynb diff --git a/.coveragerc_omit b/.coveragerc_omit index 79315301..0a180d29 100644 --- a/.coveragerc_omit +++ b/.coveragerc_omit @@ -10,4 +10,5 @@ omit = vitessce/data_utils/anndata.py vitessce/data_utils/ome.py vitessce/data_utils/entities.py - vitessce/data_utils/multivec.py \ No newline at end of file + vitessce/data_utils/multivec.py + vitessce/widget_plugins/demo_plugin.py \ No newline at end of file diff --git a/docs/notebooks/widget_plugin_custom.ipynb b/docs/notebooks/widget_plugin_custom.ipynb new file mode 100644 index 00000000..440b77d5 --- /dev/null +++ b/docs/notebooks/widget_plugin_custom.ipynb @@ -0,0 +1,207 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "# Vitessce Widget Tutorial" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "from vitessce import (\n", + " VitessceConfig,\n", + " Component as cm,\n", + " CoordinationType as ct,\n", + " OmeTiffWrapper,\n", + " MultiImageWrapper,\n", + " VitesscePlugin\n", + ")\n", + "from esbuild_py import transform" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "PLUGIN_ESM = transform(\"\"\"\n", + "function createPlugins(utilsForPlugins) {\n", + " const {\n", + " React,\n", + " PluginFileType,\n", + " PluginViewType,\n", + " PluginCoordinationType,\n", + " PluginJointFileType,\n", + " z,\n", + " useCoordination,\n", + " invokeCommand,\n", + " } = utilsForPlugins;\n", + " \n", + " const CSS = `\n", + " .chat {\n", + " overflow-y: scroll;\n", + " }\n", + " `;\n", + " \n", + " function ChatView(props) {\n", + " \n", + " const [nextMessage, setNextMessage] = React.useState('');\n", + " const [isLoading, setIsLoading] = React.useState(false);\n", + " const [chatHistory, setChatHistory] = React.useState([]); // chatHistory is an array of message objects like [{ user, text }, ...]\n", + " \n", + " async function handleClick() { \n", + " setChatHistory(prev => ([\n", + " ...prev,\n", + " { user: 'You', text: nextMessage },\n", + " ]));\n", + " setIsLoading(true);\n", + " const [chatReceiveValue, chatReceiveBuffers] = await invokeCommand(\"chat_send\", nextMessage, []);\n", + " setChatHistory(prev => ([\n", + " ...prev,\n", + " { user: 'AI', text: chatReceiveValue.text },\n", + " ]));\n", + " setIsLoading(false);\n", + " }\n", + " \n", + " return (\n", + " <>\n", + " \n", + "
\n", + "

Chat view

\n", + "
\n", + " {chatHistory.map(message => (\n", + "

\n", + " {message.user}:\n", + " {message.text}\n", + "

\n", + " ))}\n", + "
\n", + " setNextMessage(e.target.value)} disabled={isLoading} />\n", + " \n", + "
\n", + " \n", + " );\n", + " }\n", + "\n", + " const pluginViewTypes = [\n", + " new PluginViewType('chat', ChatView, []),\n", + " ];\n", + " return { pluginViewTypes };\n", + "}\n", + "export default { createPlugins };\n", + "\"\"\")\n", + "\n", + "\n", + "def handle_chat_message(message, buffers):\n", + " return { \"text\": message.upper() }, []\n", + "\n", + "\n", + "class ChatPlugin(VitesscePlugin):\n", + " plugin_esm = PLUGIN_ESM\n", + " commands = {\n", + " \"chat_send\": handle_chat_message,\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "vc = VitessceConfig(schema_version=\"1.0.15\", name='Spraggins Multi-Modal', description='PAS + IMS + AF From https://portal.hubmapconsortium.org/browse/collection/6a6efd0c1a2681dc7d2faab8e4ab0bca')\n", + "dataset = vc.add_dataset(name='Spraggins').add_object(\n", + " MultiImageWrapper(\n", + " image_wrappers=[\n", + " OmeTiffWrapper(img_url='https://assets.hubmapconsortium.org/f4188a148e4c759092d19369d310883b/ometiff-pyramids/processedMicroscopy/VAN0006-LK-2-85-PAS_images/VAN0006-LK-2-85-PAS_registered.ome.tif?token=', name='PAS'),\n", + " OmeTiffWrapper(img_url='https://assets.hubmapconsortium.org/2130d5f91ce61d7157a42c0497b06de8/ometiff-pyramids/processedMicroscopy/VAN0006-LK-2-85-AF_preIMS_images/VAN0006-LK-2-85-AF_preIMS_registered.ome.tif?token=', name='AF'),\n", + " OmeTiffWrapper(img_url='https://assets.hubmapconsortium.org/be503a021ed910c0918842e318e6efa2/ometiff-pyramids/ometiffs/VAN0006-LK-2-85-IMS_PosMode_multilayer.ome.tif?token=', name='IMS Pos Mode'),\n", + " OmeTiffWrapper(img_url='https://assets.hubmapconsortium.org/ca886a630b2038997a4cfbbf4abfd283/ometiff-pyramids/ometiffs/VAN0006-LK-2-85-IMS_NegMode_multilayer.ome.tif?token=', name='IMS Neg Mode')\n", + " ],\n", + " use_physical_size_scaling=True,\n", + " )\n", + ")\n", + "spatial = vc.add_view(cm.SPATIAL, dataset=dataset)\n", + "status = vc.add_view(\"chat\", dataset=dataset)\n", + "lc = vc.add_view(cm.LAYER_CONTROLLER, dataset=dataset).set_props(disableChannelsIfRgbDetected=True)\n", + "vc.layout(spatial | (lc / status));" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Create the Vitessce widget" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "52bbdb1e3f91423b8dd934e3a4ff796e", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "VitessceWidget(config={'version': '1.0.15', 'name': 'Spraggins Multi-Modal', 'description': 'PAS + IMS + AF Fr…" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vw = vc.widget(plugins=[ChatPlugin()])\n", + "vw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/notebooks/widget_plugin_demo.ipynb b/docs/notebooks/widget_plugin_demo.ipynb new file mode 100644 index 00000000..9fd0a419 --- /dev/null +++ b/docs/notebooks/widget_plugin_demo.ipynb @@ -0,0 +1,139 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "# Vitessce Widget Tutorial" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualization of Multi-Modal Imaging Data\n", + "We visualize IMS, PAS, and AF imaging data overlaid from the Spraggins Lab of the Biomolecular Multimodal Imaging Center (BIOMC) at Vanderbilt University, uploaded to the HuBMAP data portal." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from vitessce import (\n", + " VitessceConfig,\n", + " Component as cm,\n", + " CoordinationType as ct,\n", + " OmeTiffWrapper,\n", + " MultiImageWrapper,\n", + ")\n", + "from os.path import join" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from vitessce.widget_plugins import DemoPlugin" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Configure Vitessce\n", + "Set up the images from the three different assays, with the `use_physical_size_scaling` set to `True` so that the IMS image scales to the other images based on their physical sizes." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "vc = VitessceConfig(schema_version=\"1.0.15\", name='Spraggins Multi-Modal', description='PAS + IMS + AF From https://portal.hubmapconsortium.org/browse/collection/6a6efd0c1a2681dc7d2faab8e4ab0bca')\n", + "dataset = vc.add_dataset(name='Spraggins').add_object(\n", + " MultiImageWrapper(\n", + " image_wrappers=[\n", + " OmeTiffWrapper(img_url='https://assets.hubmapconsortium.org/f4188a148e4c759092d19369d310883b/ometiff-pyramids/processedMicroscopy/VAN0006-LK-2-85-PAS_images/VAN0006-LK-2-85-PAS_registered.ome.tif?token=', name='PAS'),\n", + " OmeTiffWrapper(img_url='https://assets.hubmapconsortium.org/2130d5f91ce61d7157a42c0497b06de8/ometiff-pyramids/processedMicroscopy/VAN0006-LK-2-85-AF_preIMS_images/VAN0006-LK-2-85-AF_preIMS_registered.ome.tif?token=', name='AF'),\n", + " OmeTiffWrapper(img_url='https://assets.hubmapconsortium.org/be503a021ed910c0918842e318e6efa2/ometiff-pyramids/ometiffs/VAN0006-LK-2-85-IMS_PosMode_multilayer.ome.tif?token=', name='IMS Pos Mode'),\n", + " OmeTiffWrapper(img_url='https://assets.hubmapconsortium.org/ca886a630b2038997a4cfbbf4abfd283/ometiff-pyramids/ometiffs/VAN0006-LK-2-85-IMS_NegMode_multilayer.ome.tif?token=', name='IMS Neg Mode')\n", + " ],\n", + " use_physical_size_scaling=True,\n", + " )\n", + ")\n", + "spatial = vc.add_view(cm.SPATIAL, dataset=dataset)\n", + "status = vc.add_view(\"demo\", dataset=dataset)\n", + "lc = vc.add_view(cm.LAYER_CONTROLLER, dataset=dataset).set_props(disableChannelsIfRgbDetected=True)\n", + "vc.layout(spatial | (lc / status));" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Create the Vitessce widget" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "837fec6d047c4f83be8530996e324fb9", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "VitessceWidget(config={'version': '1.0.15', 'name': 'Spraggins Multi-Modal', 'description': 'PAS + IMS + AF Fr…" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vw = vc.widget(plugins=[DemoPlugin()])\n", + "vw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/pyproject.toml b/pyproject.toml index b7731d97..ffb2faa2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vitessce" -version = "3.2.8" +version = "3.3.0" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ] @@ -19,10 +19,11 @@ classifiers = [ 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'Topic :: Multimedia :: Graphics', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ] dependencies = [ 'zarr>=2.5.0', diff --git a/vitessce/__init__.py b/vitessce/__init__.py index bc8ae8db..2d3e3b44 100644 --- a/vitessce/__init__.py +++ b/vitessce/__init__.py @@ -37,7 +37,7 @@ # We allow installation without all of the dependencies that the widget requires. # The imports below will fail in that case, and corresponding globals will be undefined. try: - from .widget import VitessceWidget, data_server + from .widget import VitessceWidget, VitesscePlugin, data_server except ModuleNotFoundError as e: # pragma: no cover warn(f'Extra installs are necessary to use widgets: {e}') diff --git a/vitessce/widget.py b/vitessce/widget.py index 473dc3b9..a10ac0db 100644 --- a/vitessce/widget.py +++ b/vitessce/widget.py @@ -199,7 +199,7 @@ def get_uid_str(uid): const jsDevMode = view.model.get('js_dev_mode'); const jsPackageVersion = view.model.get('js_package_version'); const customJsUrl = view.model.get('custom_js_url'); - const pluginEsm = view.model.get('plugin_esm'); + const pluginEsmArr = view.model.get('plugin_esm'); const remountOnUidChange = view.model.get('remount_on_uid_change'); const storeUrls = view.model.get('store_urls'); @@ -220,10 +220,10 @@ def get_uid_str(uid): useCoordination, } = await importWithMap("vitessce", importMap); - let pluginViewTypes; - let pluginCoordinationTypes; - let pluginFileTypes; - let pluginJointFileTypes; + let pluginViewTypes = []; + let pluginCoordinationTypes = []; + let pluginFileTypes = []; + let pluginJointFileTypes = []; const stores = Object.fromEntries( storeUrls.map(storeUrl => ([ @@ -238,26 +238,41 @@ def get_uid_str(uid): ])), ); - try { - const pluginEsmUrl = URL.createObjectURL(new Blob([pluginEsm], { type: "text/javascript" })); - const pluginModule = (await import(pluginEsmUrl)).default; - URL.revokeObjectURL(pluginEsmUrl); - - const pluginsObj = await pluginModule.createPlugins({ - React, - PluginFileType, - PluginViewType, - PluginCoordinationType, - PluginJointFileType, - z, - useCoordination, - }); - pluginViewTypes = pluginsObj.pluginViewTypes; - pluginCoordinationTypes = pluginsObj.pluginCoordinationTypes; - pluginFileTypes = pluginsObj.pluginFileTypes; - pluginJointFileTypes = pluginsObj.pluginJointFileTypes; - } catch(e) { - console.error(e); + function invokePluginCommand(commandName, commandParams, commandBuffers) { + return view.experimental.invoke("_plugin_command", [commandName, commandParams], commandBuffers); + } + + for (const pluginEsm of pluginEsmArr) { + try { + const pluginEsmUrl = URL.createObjectURL(new Blob([pluginEsm], { type: "text/javascript" })); + const pluginModule = (await import(pluginEsmUrl)).default; + URL.revokeObjectURL(pluginEsmUrl); + + const pluginsObj = await pluginModule.createPlugins({ + React, + PluginFileType, + PluginViewType, + PluginCoordinationType, + PluginJointFileType, + z, + useCoordination, + invokeCommand: invokePluginCommand, + }); + if(Array.isArray(pluginsObj.pluginViewTypes)) { + pluginViewTypes = [...pluginViewTypes, ...pluginsObj.pluginViewTypes]; + } + if(Array.isArray(pluginsObj.pluginCoordinationTypes)) { + pluginCoordinationTypes = [...pluginCoordinationTypes, ...pluginsObj.pluginCoordinationTypes]; + } + if(Array.isArray(pluginsObj.pluginFileTypes)) { + pluginFileTypes = [...pluginFileTypes, ...pluginsObj.pluginFileTypes]; + } + if(Array.isArray(pluginsObj.pluginJointFileTypes)) { + pluginJointFileTypes = [...pluginJointFileTypes, ...pluginsObj.pluginJointFileTypes]; + } + } catch(e) { + console.error(e); + } } function VitessceWidget(props) { @@ -366,6 +381,7 @@ def get_uid_str(uid): PluginJointFileType, z, useCoordination, + invokeCommand, } = utilsForPlugins; return { pluginViewTypes: undefined, @@ -377,6 +393,16 @@ def get_uid_str(uid): export default { createPlugins }; """ +# Abstract class for widget plugins to subclass + + +class VitesscePlugin: + plugin_esm = DEFAULT_PLUGIN_ESM + commands = {} + + def on_config_change(self, new_config): + raise NotImplementedError("on_config_change may optionally be implemented by subclasses.") + class VitessceWidget(anywidget.AnyWidget): """ @@ -400,12 +426,12 @@ class VitessceWidget(anywidget.AnyWidget): js_package_version = Unicode('3.3.12').tag(sync=True) js_dev_mode = Bool(False).tag(sync=True) custom_js_url = Unicode('').tag(sync=True) - plugin_esm = Unicode(DEFAULT_PLUGIN_ESM).tag(sync=True) + plugin_esm = List(trait=Unicode(''), default_value=[]).tag(sync=True) remount_on_uid_change = Bool(True).tag(sync=True) store_urls = List(trait=Unicode(''), default_value=[]).tag(sync=True) - def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.3.12', js_dev_mode=False, custom_js_url='', plugin_esm=DEFAULT_PLUGIN_ESM, remount_on_uid_change=True): + def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.3.12', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True): """ Construct a new Vitessce widget. @@ -418,7 +444,7 @@ def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy= :param str js_package_version: The version of the NPM package ('vitessce' if not js_dev_mode else '@vitessce/dev'). :param bool js_dev_mode: Should @vitessce/dev be used (typically for debugging purposes)? By default, False. :param str custom_js_url: A URL to a JavaScript file to use (instead of 'vitessce' or '@vitessce/dev' NPM package). - :param str plugin_esm: JavaScript module that defines a createPlugins function. Optional. + :param list[WidgetPlugin] plugins: A list of subclasses of WidgetPlugin, defining plugin_esm (string) and/or on_config_change (function). Optional. :param bool remount_on_uid_change: Passed to the remountOnUidChange prop of the React component. By default, True. .. code-block:: python @@ -439,6 +465,12 @@ def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy= routes = config.get_routes() self._stores = config.get_stores(base_url=base_url) + self._plugins = plugins or [] + + plugin_esm = [p.plugin_esm for p in self._plugins] + self._plugin_commands = {} + for plugin in self._plugins: + self._plugin_commands.update(plugin.commands) uid_str = get_uid_str(uid) @@ -449,6 +481,20 @@ def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy= uid=uid_str, store_urls=list(self._stores.keys()) ) + # Register chained plugin on_config_change functions with a change observer. + def handle_config_change(change): + new_config = change.new + for plugin in self._plugins: + try: + new_config = plugin.on_config_change(new_config) + except NotImplementedError: + # It is optional for plugins to implement on_config_change. + pass + if new_config is not None: + self.config = new_config + + self.observe(handle_config_change, names=['config']) + serve_routes(config, routes, use_port) def _get_coordination_value(self, coordination_type, coordination_scope): @@ -488,6 +534,12 @@ def _zarr_get(self, params, buffers): buffers = [] return {"success": len(buffers) == 1}, buffers + @anywidget.experimental.command + def _plugin_command(self, params, buffers): + [command_name, command_params] = params + command_func = self._plugin_commands[command_name] + return command_func(command_params, buffers) + # Launch Vitessce using plain HTML representation (no ipywidgets) diff --git a/vitessce/widget_plugins/__init__.py b/vitessce/widget_plugins/__init__.py index 72b2a498..ec2857d7 100644 --- a/vitessce/widget_plugins/__init__.py +++ b/vitessce/widget_plugins/__init__.py @@ -1 +1 @@ -from .demo_plugin import PLUGIN_ESM as demo_plugin_esm +from .demo_plugin import DemoPlugin diff --git a/vitessce/widget_plugins/demo_plugin.py b/vitessce/widget_plugins/demo_plugin.py index 47790813..5717aa13 100644 --- a/vitessce/widget_plugins/demo_plugin.py +++ b/vitessce/widget_plugins/demo_plugin.py @@ -1,4 +1,6 @@ from esbuild_py import transform +from ..widget import VitesscePlugin + PLUGIN_ESM = transform(""" function createPlugins(utilsForPlugins) { @@ -10,6 +12,7 @@ PluginJointFileType, z, useCoordination, + invokeCommand, } = utilsForPlugins; function DemoView(props) { const { coordinationScopes } = props; @@ -19,11 +22,15 @@ setObsType, }] = useCoordination(['obsType'], coordinationScopes); + function handleClick() { + console.log(invokeCommand('demo_command', "Hello from command", [])); + } + return (

Demo plugin view

obsType: {obsType}

- +
); } @@ -35,6 +42,12 @@ } export default { createPlugins }; """) + + +def handle_demo_command(message, buffers): + return message.upper(), [] + + """ Example of a minimal plugin view that gets the obsType coordination value from the coordination space and renders a button. This plugin view is not meant to be useful for end-users, but rather to demonstrate how to develop a plugin view that uses coordination (and uses eslint_py for JSX transformation). @@ -48,3 +61,10 @@ # ... vc.widget(plugin_esm=demo_plugin_esm) """ + + +class DemoPlugin(VitesscePlugin): + plugin_esm = PLUGIN_ESM + commands = { + "demo_command": handle_demo_command, + } From c88eb372fbbc8939ffadf1c5048e1a0c35b55b39 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Mon, 29 Jul 2024 07:55:19 -0600 Subject: [PATCH 34/58] Update fill_template.py (#353) --- demos/fill_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/fill_template.py b/demos/fill_template.py index 2c00a487..c2296e76 100644 --- a/demos/fill_template.py +++ b/demos/fill_template.py @@ -12,7 +12,7 @@ def render_json(dir_name, version_str, url_type, port): BASE_URL = { 'local': f'http://localhost:{port}/{dir_name}/data/processed', - 'remote': f'https://s3.amazonaws.com/vitessce-data/{version_str}/main/{dir_name}' + 'remote': f'https://data-1.vitessce.io/{version_str}/main/{dir_name}' } BASE_URL_GCP = { 'local': f'http://localhost:{port}/{dir_name}/data/processed', From bc7b3537b638fef893014365894257d0f5e5caee Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Fri, 2 Aug 2024 07:27:06 -0600 Subject: [PATCH 35/58] Plugin for SpatialQuery (#354) * Spatial-Query plugin and example notebook * Version * Widget plugins module * Update * Linting * Lint * Update comments * Update docs * Update --- .coveragerc_omit | 3 +- .../widget_plugin_spatial-query.ipynb | 169 ++++++++++ docs/widget_plugins.rst | 22 +- pyproject.toml | 4 +- vitessce/widget.py | 15 +- vitessce/widget_plugins/__init__.py | 1 + vitessce/widget_plugins/demo_plugin.py | 22 +- vitessce/widget_plugins/spatial_query.py | 306 ++++++++++++++++++ 8 files changed, 512 insertions(+), 30 deletions(-) create mode 100644 docs/notebooks/widget_plugin_spatial-query.ipynb create mode 100644 vitessce/widget_plugins/spatial_query.py diff --git a/.coveragerc_omit b/.coveragerc_omit index 0a180d29..96a24637 100644 --- a/.coveragerc_omit +++ b/.coveragerc_omit @@ -11,4 +11,5 @@ omit = vitessce/data_utils/ome.py vitessce/data_utils/entities.py vitessce/data_utils/multivec.py - vitessce/widget_plugins/demo_plugin.py \ No newline at end of file + vitessce/widget_plugins/demo_plugin.py + vitessce/widget_plugins/spatial_query.py \ No newline at end of file diff --git a/docs/notebooks/widget_plugin_spatial-query.ipynb b/docs/notebooks/widget_plugin_spatial-query.ipynb new file mode 100644 index 00000000..88d28bf0 --- /dev/null +++ b/docs/notebooks/widget_plugin_spatial-query.ipynb @@ -0,0 +1,169 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "#!pip install \"vitessce[all]==3.3.0\" esbuild_py anndata\n", + "!pip install \"mlxtend~=0.23.0\"\n", + "#!pip install -i \"https://test.pypi.org/simple/\" SpatialQuery\n", + "!pip install \"SpatialQuery @ git+https://github.com/ShaokunAn/Spatial-Query@main\"" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from os.path import join\n", + "from anndata import read_h5ad\n", + "from vitessce import (\n", + " VitessceConfig,\n", + " AnnDataWrapper,\n", + " ViewType as vt,\n", + " CoordinationType as ct,\n", + " CoordinationLevel as CL,\n", + ")\n", + "from vitessce.widget_plugins import SpatialQueryPlugin" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "adata = read_h5ad(join(\"data\", \"HBM987_KWLK_254\", \"secondary_analysis.h5ad\"))\n", + "zarr_path = join(\"data\", \"HBM987_KWLK_254\", \"secondary_analysis.h5ad.zarr\")\n", + "adata.write_zarr(zarr_path)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "plugin = SpatialQueryPlugin(adata)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "vc = VitessceConfig(schema_version=\"1.0.16\", name=\"Spatial-Query\")\n", + "dataset = vc.add_dataset(\"Query results\").add_object(AnnDataWrapper(\n", + " adata_path=zarr_path,\n", + " obs_feature_matrix_path=\"X\",\n", + " obs_set_paths=[\"obs/predicted.ASCT.celltype\"],\n", + " obs_set_names=[\"Cell Type\"],\n", + " obs_spots_path=\"obsm/X_spatial\",\n", + " feature_labels_path=\"var/hugo_symbol\",\n", + " coordination_values={\n", + " \"featureLabelsType\": \"Gene symbol\",\n", + " }\n", + "))\n", + "\n", + "spatial_view = vc.add_view(\"spatialBeta\", dataset=dataset)\n", + "lc_view = vc.add_view(\"layerControllerBeta\", dataset=dataset)\n", + "sets_view = vc.add_view(\"obsSets\", dataset=dataset)\n", + "features_view = vc.add_view(\"featureList\", dataset=dataset)\n", + "sq_view = vc.add_view(\"spatialQuery\", dataset=dataset)\n", + "\n", + "obs_set_selection_scope, = vc.add_coordination(\"obsSetSelection\",)\n", + "obs_set_selection_scope.set_value(None)\n", + "\n", + "sets_view.use_coordination(obs_set_selection_scope)\n", + "sq_view.use_coordination(obs_set_selection_scope)\n", + "spatial_view.use_coordination(obs_set_selection_scope)\n", + "features_view.use_coordination(obs_set_selection_scope)\n", + "\n", + "vc.link_views([spatial_view, lc_view, sets_view, features_view],\n", + " [\"additionalObsSets\", \"obsSetColor\"],\n", + " [plugin.additional_obs_sets, plugin.obs_set_color]\n", + ")\n", + "vc.link_views_by_dict([spatial_view, lc_view], {\n", + " \"spotLayer\": CL([\n", + " {\n", + " \"obsType\": \"cell\",\n", + " \"spatialSpotRadius\": 15,\n", + " },\n", + " ])\n", + "})\n", + "\n", + "vc.layout((spatial_view | (lc_view / features_view)) / (sets_view | sq_view));" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/mkeller/software/miniconda3/envs/vitessce-python-notebooks/lib/python3.9/site-packages/traitlets/traitlets.py:869: DeprecationWarning: Deprecated in traitlets 4.1, use the instance .metadata dictionary directly, like x.metadata[key] or x.metadata.get(key, default)\n", + " warn(\"Deprecated in traitlets 4.1, \" + msg, DeprecationWarning, stacklevel=2)\n", + "/Users/mkeller/software/miniconda3/envs/vitessce-python-notebooks/lib/python3.9/site-packages/traitlets/traitlets.py:869: DeprecationWarning: Deprecated in traitlets 4.1, use the instance .metadata dictionary directly, like x.metadata[key] or x.metadata.get(key, default)\n", + " warn(\"Deprecated in traitlets 4.1, \" + msg, DeprecationWarning, stacklevel=2)\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6f30fb3eac1644478df256bf79a986e9", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "VitessceWidget(config={'version': '1.0.16', 'name': 'Spatial-Query', 'description': '', 'datasets': [{'uid': '…" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vw = vc.widget(height=900, plugins=[plugin], remount_on_uid_change=False)\n", + "vw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/widget_plugins.rst b/docs/widget_plugins.rst index cfe1d325..a1c7f927 100644 --- a/docs/widget_plugins.rst +++ b/docs/widget_plugins.rst @@ -47,24 +47,21 @@ If defined, these plugin arrays are passed to the Vitessce component as `props < Passing plugin ESM to the widget -------------------------------- -The plugin string can be passed to the widget using the ``plugin_esm`` parameter: +The plugin string can be passed to the widget using the ``plugins`` parameter and passing a subclass of ``VitesscePlugin``: .. code-block:: python - from vitessce import VitessceConfig + from vitessce import VitessceConfig, VitesscePlugin - vc = VitessceConfig( - description="A Vitessce widget with a custom plugin", - widget=[ - { - "plugin": PLUGIN_ESM, - }, - ], - ) + class MyPlugin(VitesscePlugin): + plugin_esm = PLUGIN_ESM + + vc = VitessceConfig(description="A Vitessce widget with a custom plugin") # Some more configuration here... - vc.widget(plugin_esm=PLUGIN_ESM) + plugin = MyPlugin() + vc.widget(plugins=[plugin]) ------------------------------- @@ -149,4 +146,7 @@ vitessce.widget_plugins *********************** .. automodule:: vitessce.widget_plugins.demo_plugin + :members: + +.. automodule:: vitessce.widget_plugins.spatial_query :members: \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ffb2faa2..e3bc7f44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vitessce" -version = "3.3.0" +version = "3.3.1" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ] @@ -96,4 +96,4 @@ notebook = [] repository = "https://github.com/vitessce/vitessce-python" [tool.setuptools] -packages = ["vitessce", "vitessce.data_utils"] +packages = ["vitessce", "vitessce.data_utils", "vitessce.widget_plugins"] diff --git a/vitessce/widget.py b/vitessce/widget.py index a10ac0db..0e2ee988 100644 --- a/vitessce/widget.py +++ b/vitessce/widget.py @@ -393,14 +393,23 @@ def get_uid_str(uid): export default { createPlugins }; """ -# Abstract class for widget plugins to subclass - class VitesscePlugin: + """ + A class that represents a Vitessce widget plugin. Custom plugins can be created by subclassing this class. + """ plugin_esm = DEFAULT_PLUGIN_ESM commands = {} def on_config_change(self, new_config): + """ + Config change handler. + + :param dict new_config: The new config object. + + :returns: config (likely with new "uid" property) or None + :rtype: dict or None + """ raise NotImplementedError("on_config_change may optionally be implemented by subclasses.") @@ -444,7 +453,7 @@ def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy= :param str js_package_version: The version of the NPM package ('vitessce' if not js_dev_mode else '@vitessce/dev'). :param bool js_dev_mode: Should @vitessce/dev be used (typically for debugging purposes)? By default, False. :param str custom_js_url: A URL to a JavaScript file to use (instead of 'vitessce' or '@vitessce/dev' NPM package). - :param list[WidgetPlugin] plugins: A list of subclasses of WidgetPlugin, defining plugin_esm (string) and/or on_config_change (function). Optional. + :param list[WidgetPlugin] plugins: A list of subclasses of VitesscePlugin. Optional. :param bool remount_on_uid_change: Passed to the remountOnUidChange prop of the React component. By default, True. .. code-block:: python diff --git a/vitessce/widget_plugins/__init__.py b/vitessce/widget_plugins/__init__.py index ec2857d7..80c8d659 100644 --- a/vitessce/widget_plugins/__init__.py +++ b/vitessce/widget_plugins/__init__.py @@ -1 +1,2 @@ from .demo_plugin import DemoPlugin +from .spatial_query import SpatialQueryPlugin diff --git a/vitessce/widget_plugins/demo_plugin.py b/vitessce/widget_plugins/demo_plugin.py index 5717aa13..06737d93 100644 --- a/vitessce/widget_plugins/demo_plugin.py +++ b/vitessce/widget_plugins/demo_plugin.py @@ -48,22 +48,18 @@ def handle_demo_command(message, buffers): return message.upper(), [] -""" -Example of a minimal plugin view that gets the obsType coordination value from the coordination space and renders a button. -This plugin view is not meant to be useful for end-users, but rather to demonstrate how to develop a plugin view that uses coordination (and uses eslint_py for JSX transformation). - -:meta hide-value: - -.. code-block:: python - - from vitessce.widget_plugins import demo_plugin_esm +class DemoPlugin(VitesscePlugin): + """ + Example of a minimal plugin view that gets the obsType coordination value from the coordination space and renders a button. + This plugin view is not meant to be useful for end-users, but rather to demonstrate how to develop a plugin view that uses coordination (and uses eslint_py for JSX transformation). - # ... - vc.widget(plugin_esm=demo_plugin_esm) -""" + .. code-block:: python + from vitessce.widget_plugins import DemoPlugin -class DemoPlugin(VitesscePlugin): + # ... + vc.widget(plugins=[DemoPlugin()]) + """ plugin_esm = PLUGIN_ESM commands = { "demo_command": handle_demo_command, diff --git a/vitessce/widget_plugins/spatial_query.py b/vitessce/widget_plugins/spatial_query.py new file mode 100644 index 00000000..b5b4c75d --- /dev/null +++ b/vitessce/widget_plugins/spatial_query.py @@ -0,0 +1,306 @@ +from esbuild_py import transform +from ..widget import VitesscePlugin + + +PLUGIN_ESM = transform(""" +function createPlugins(utilsForPlugins) { + const { + React, + PluginFileType, + PluginViewType, + PluginCoordinationType, + PluginJointFileType, + z, + useCoordination, + } = utilsForPlugins; + function SpatialQueryView(props) { + const { coordinationScopes } = props; + const [{ + queryParams, + obsSetSelection, + }, { + setQueryParams, + }] = useCoordination(['queryParams', 'obsSetSelection', 'obsType'], coordinationScopes); + + const [uuid, setUuid] = React.useState(1); + const [queryType, setQueryType] = React.useState('grid'); + const [maxDist, setMaxDist] = React.useState(100); + const [minSize, setMinSize] = React.useState(4); + const [minCount, setMinCount] = React.useState(10); + const [minSupport, setMinSupport] = React.useState(0.5); + + const cellTypeOfInterest = obsSetSelection?.length === 1 && obsSetSelection[0][0] === "Cell Type" + ? obsSetSelection[0][1] + : null; + + const onQueryTypeChange = React.useCallback((e) => { + setQueryType(e.target.value); + }, []); + + return ( +
+

Spatial Query Manager

+ +
+ +
+ +
+ +
+ +
+ {/* TODO: disDuplicates: Distinguish duplicates in patterns. */} + +
+ ); + } + + const pluginCoordinationTypes = [ + new PluginCoordinationType('queryParams', null, z.object({ + cellTypeOfInterest: z.string().nullable(), + queryType: z.enum(['grid', 'rand', 'ct-center']), + maxDist: z.number(), + minSize: z.number(), + minCount: z.number(), + minSupport: z.number(), + disDuplicates: z.boolean(), + uuid: z.number(), + }).partial().nullable()), + ]; + + const pluginViewTypes = [ + new PluginViewType('spatialQuery', SpatialQueryView, ['queryParams', 'obsSetSelection', 'obsType']), + ]; + return { pluginViewTypes, pluginCoordinationTypes }; +} +export default { createPlugins }; +""") + + +class SpatialQueryPlugin(VitesscePlugin): + """ + Spatial-Query plugin view renders controls to change parameters passed to the Spatial-Query methods. + """ + plugin_esm = PLUGIN_ESM + commands = {} + + def __init__(self, adata, spatial_key="X_spatial", label_key="cell_type"): + """ + Construct a new Vitessce widget. + + :param adata: AnnData. + :type adata: anndata.AnnData + :param str spatial_key: The key in adata.obsm that contains the (x, y) coordinates of each cell. By default, "X_spatial". + :param str label_key: The column in adata.obs that contains the cell type labels. By default, "cell_type". + + .. code-block:: python + + from vitessce.widget_plugins import SpatialQueryPlugin + + plugin = SpatialQueryPlugin(adata, spatial_key="X_spatial", label_key="cell_type") + # ... + vc.widget(plugins=[plugin], remount_on_uid_change=False) + """ + from SpatialQuery.spatial_query import spatial_query + import matplotlib.pyplot as plt # Add as dependency / optional dependency? + + self.adata = adata + self.spatial_key = spatial_key + self.label_key = label_key + + self.tt = spatial_query(adata=adata, dataset='test', spatial_key=spatial_key, label_key=label_key, leaf_size=10) + + self.tab20_rgb = [[int(r * 255), int(g * 255), int(b * 255)] for (r, g, b, a) in [plt.cm.tab20(i) for i in range(20)]] + + self.additional_obs_sets = { + "version": "0.1.3", + "tree": [ + { + "name": "Spatial-Query Results", + "children": [ + + ] + } + ] + } + + self.obs_set_color = [ + { + "color": [255, 255, 255], + "path": ["Cell Type"], + }, + { + "color": [255, 255, 255], + "path": ["Spatial-Query Results"], + } + ] + + self.ct_to_color = dict() + + for ct_i, cell_type in enumerate(adata.obs[label_key].unique().tolist()): + color = self.tab20_rgb[ct_i % 20] + self.ct_to_color[cell_type] = color + path = ["Cell Type", cell_type] + self.obs_set_color.append({ + "color": color, + "path": path + }) + + self.cell_i_to_cell_id = dict(zip(range(adata.obs.shape[0]), adata.obs.index.tolist())) + self.cell_id_to_cell_type = dict(zip(adata.obs.index.tolist(), adata.obs[label_key].tolist())) + + def get_matching_cell_ids(self, cell_type, cell_i): + cell_ids = [self.cell_i_to_cell_id[i] for i in cell_i] + matches = [] + for cell_id in cell_ids: + cell_ct = self.cell_id_to_cell_type[cell_id] + if cell_ct == cell_type: + matches.append([cell_id, None]) + return matches + + def fp_tree_to_obs_sets_tree(self, fp_tree, sq_id): + additional_obs_sets = { + "version": "0.1.3", + "tree": [ + { + "name": f"Spatial-Query Results {sq_id}", + "children": [ + + ] + } + ] + } + + obs_set_color = [] + + for row_i, row in fp_tree.iterrows(): + motif = row["itemsets"] + cell_i = row["cell_id"] + + motif_name = str(list(motif)) + + additional_obs_sets["tree"][0]["children"].append({ + "name": motif_name, + "children": [ + { + "name": cell_type, + "set": self.get_matching_cell_ids(cell_type, cell_i) + } + for cell_type in motif + ] + }) + + obs_set_color.append({ + "color": [255, 255, 255], + "path": [additional_obs_sets["tree"][0]["name"], motif_name] + }) + + for cell_type in motif: + color = self.ct_to_color[cell_type] + path = [additional_obs_sets["tree"][0]["name"], motif_name, cell_type] + obs_set_color.append({ + "color": color, + "path": path + }) + return (additional_obs_sets, obs_set_color) + + def run_sq(self, prev_config): + query_params = prev_config["coordinationSpace"]["queryParams"]["A"] + + max_dist = query_params.get("maxDist", 150) + min_size = query_params.get("minSize", 4) + min_count = query_params.get("minCount", 10) + min_support = query_params.get("minSupport", 0.5) + dis_duplicates = query_params.get("disDuplicates", False) # if distinguish duplicates of cell types in neighborhood + query_type = query_params.get("queryType", "grid") + + query_uuid = query_params["uuid"] + + params_dict = dict( + max_dist=max_dist, + min_size=min_size, + min_count=min_count, + min_support=min_support, + dis_duplicates=dis_duplicates, + if_display=True, + fig_size=(9, 6), + return_cellID=True, + ) + print(params_dict) + + if query_type == "rand": + # TODO: implement param similar to return_grid for find_patterns_rand (to return the random points used) + fp_tree = self.tt.find_patterns_rand(**params_dict) + elif query_type == "grid": + params_dict["return_grid"] = True + fp_tree, grid_pos = self.tt.find_patterns_grid(**params_dict) + # TODO: support query_type == "ct-center" + + # Previous values + additional_obs_sets = prev_config["coordinationSpace"]["additionalObsSets"]["A"] + obs_set_color = prev_config["coordinationSpace"]["obsSetColor"]["A"] + + # Perform query + (new_additional_obs_sets, new_obs_set_color) = self.fp_tree_to_obs_sets_tree(fp_tree, query_uuid) + + additional_obs_sets["tree"][0] = new_additional_obs_sets["tree"][0] + prev_config["coordinationSpace"]["additionalObsSets"]["A"] = additional_obs_sets + + obs_set_color += new_obs_set_color + prev_config["coordinationSpace"]["obsSetColor"]["A"] = obs_set_color + + motif_to_select = new_additional_obs_sets["tree"][0]["children"][0]["name"] + new_obs_set_selection = [[new_additional_obs_sets["tree"][0]["name"], motif_to_select, node["name"]] for node in new_additional_obs_sets["tree"][0]["children"][0]["children"]] + prev_config["coordinationSpace"]["obsSetSelection"]["A"] = new_obs_set_selection + + # TODO: set obsSetExpansion + + return {**prev_config, "uid": f"with_query_{query_uuid}"} + + def on_config_change(self, new_config): + query_params = new_config["coordinationSpace"]["queryParams"]["A"] + if query_params and "uuid" in query_params: + print(query_params) + query_uuid = query_params.get("uuid", None) + if new_config["uid"] != f"with_query_{query_uuid}": + return self.run_sq(new_config) + return None From 55b3f57a681e82cedae7a67e2c75abd70d49f4b6 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Sat, 31 Aug 2024 15:37:28 -0400 Subject: [PATCH 36/58] Unit test for to_dict output following .add_object(AnnDataWrapper(...)) (#357) * Unit test for anndata wrapper + to_dict * linting --- tests/test_config.py | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index 1fb545a5..3b836876 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -12,6 +12,7 @@ AbstractWrapper, make_repr, CoordinationLevel as CL, + AnnDataWrapper, # Neither of these is in the source code, but they do appear in code which is eval'd. VitessceChainableConfig, @@ -61,6 +62,51 @@ def test_config_add_dataset(): } +def test_config_add_anndata_url(): + vc = VitessceConfig(schema_version="1.0.15") + vc.add_dataset(name='My Dataset').add_object( + AnnDataWrapper( + adata_url="http://example.com/adata.h5ad.zarr", + obs_set_paths=["obs/louvain"], + ) + ) + + vc_dict = vc.to_dict() + + assert vc_dict == { + "version": "1.0.15", + "name": "", + "description": "", + "datasets": [ + { + 'uid': 'A', + 'name': 'My Dataset', + 'files': [ + { + "fileType": "anndata.zarr", + "url": "http://example.com/adata.h5ad.zarr", + "options": { + "obsSets": [ + { + "name": "louvain", + "path": "obs/louvain", + } + ] + } + } + ] + } + ], + 'coordinationSpace': { + 'dataset': { + 'A': 'A' + }, + }, + "layout": [], + "initStrategy": "auto" + } + + def test_config_add_dataset_add_files(): vc = VitessceConfig(schema_version="1.0.15") vc.add_dataset(name='My Chained Dataset').add_file( From 06a04e7dfdb83965722790f5599389074de12143 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:35:17 -0400 Subject: [PATCH 37/58] Misc: Spatial-query, kerchunk, and OME helper functions (#360) * Misc changes * Bigtiff helper for rgb * Remove unused import * Lint * Unused variables --- pyproject.toml | 2 ++ vitessce/data_utils/anndata.py | 11 ++++++++ vitessce/data_utils/ome.py | 22 ++++++++++++++-- vitessce/widget_plugins/spatial_query.py | 32 ++++++++++++++++++------ 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3bc7f44..3e2a1ee2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,8 @@ all = [ 'ujson>=4.0.1', 'starlette==0.14.0', 'generate-tiff-offsets>=0.1.7', + 'kerchunk>=0.2.6', + 'fsspec>=2023.12.2', # aiofiles is not explicitly referenced in our code, # but it is an implicit dependency of starlette==0.14.0. diff --git a/vitessce/data_utils/anndata.py b/vitessce/data_utils/anndata.py index a9743670..648fc85d 100644 --- a/vitessce/data_utils/anndata.py +++ b/vitessce/data_utils/anndata.py @@ -6,6 +6,17 @@ VAR_CHUNK_SIZE = 10 +def generate_h5ad_ref_spec(h5_url, omit_url=True): + from kerchunk.hdf import SingleHdf5ToZarr + h5chunks = SingleHdf5ToZarr(h5_url, inline_threshold=300) + h5dict = h5chunks.translate() + if omit_url: + for key, val in h5dict['refs'].items(): + if isinstance(val, list): + h5dict['refs'][key] = [None, *val[1:]] + return h5dict + + def cast_arr(arr): """ Try to cast an array to a dtype that takes up less space. diff --git a/vitessce/data_utils/ome.py b/vitessce/data_utils/ome.py index 633d48b3..75a0272b 100644 --- a/vitessce/data_utils/ome.py +++ b/vitessce/data_utils/ome.py @@ -5,6 +5,21 @@ from .anndata import cast_arr +def needs_bigtiff(img_arr_shape): + """ + Helper function to determine if an image array is too large for standard TIFF format. + + :param img_arr_shape: The shape of the image array. + :type img_arr_shape: tuple[int] + :return: True if the image array is too large for standard TIFF format, False otherwise. + :rtype: bool + """ + num_pixels = 1 + for n in img_arr_shape.shape: + num_pixels *= n + return (num_pixels > 2**32) + + def rgb_img_to_ome_tiff(img_arr, output_path, img_name="Image", axes="CYX"): """ Convert an RGB image to OME-TIFF. @@ -16,8 +31,9 @@ def rgb_img_to_ome_tiff(img_arr, output_path, img_name="Image", axes="CYX"): :param str axes: The array axis ordering. By default, "CYX" """ img_arr = img_arr.astype(np.dtype('uint8')) + bigtiff = needs_bigtiff(img_arr.shape) - tiff_writer = TiffWriter(output_path, ome=True) + tiff_writer = TiffWriter(output_path, ome=True, bigtiff=bigtiff) tiff_writer.write( img_arr, metadata={ @@ -38,7 +54,9 @@ def multiplex_img_to_ome_tiff(img_arr, channel_names, output_path, axes="CYX"): :param str output_path: The path to save the Zarr store. :param str axes: The array axis ordering. By default, "CYX" """ - tiff_writer = TiffWriter(output_path, ome=True) + bigtiff = needs_bigtiff(img_arr.shape) + + tiff_writer = TiffWriter(output_path, ome=True, bigtiff=bigtiff) tiff_writer.write( img_arr, metadata={ diff --git a/vitessce/widget_plugins/spatial_query.py b/vitessce/widget_plugins/spatial_query.py index b5b4c75d..5ac589f4 100644 --- a/vitessce/widget_plugins/spatial_query.py +++ b/vitessce/widget_plugins/spatial_query.py @@ -213,7 +213,10 @@ def fp_tree_to_obs_sets_tree(self, fp_tree, sq_id): obs_set_color = [] for row_i, row in fp_tree.iterrows(): - motif = row["itemsets"] + try: + motif = row["itemsets"] + except KeyError: + motif = row["motifs"] cell_i = row["cell_id"] motif_name = str(list(motif)) @@ -248,32 +251,45 @@ def run_sq(self, prev_config): max_dist = query_params.get("maxDist", 150) min_size = query_params.get("minSize", 4) - min_count = query_params.get("minCount", 10) + # min_count = query_params.get("minCount", 10) min_support = query_params.get("minSupport", 0.5) - dis_duplicates = query_params.get("disDuplicates", False) # if distinguish duplicates of cell types in neighborhood + # dis_duplicates = query_params.get("disDuplicates", False) # if distinguish duplicates of cell types in neighborhood query_type = query_params.get("queryType", "grid") + cell_type_of_interest = query_params.get("cellTypeOfInterest", None) query_uuid = query_params["uuid"] params_dict = dict( max_dist=max_dist, min_size=min_size, - min_count=min_count, + # min_count=min_count, min_support=min_support, - dis_duplicates=dis_duplicates, + # dis_duplicates=dis_duplicates, if_display=True, fig_size=(9, 6), return_cellID=True, ) print(params_dict) + # TODO: add unit tests for this functionality + if query_type == "rand": # TODO: implement param similar to return_grid for find_patterns_rand (to return the random points used) fp_tree = self.tt.find_patterns_rand(**params_dict) elif query_type == "grid": params_dict["return_grid"] = True fp_tree, grid_pos = self.tt.find_patterns_grid(**params_dict) - # TODO: support query_type == "ct-center" + elif query_type == "ct-center": + fp_tree = self.tt.motif_enrichment_knn( + ct=cell_type_of_interest, + k=20, # TODO: make this a parameter in the UI. + min_support=min_support, + # dis_duplicates=dis_duplicates, + return_cellID=True, + ) + print(fp_tree) + + # TODO: implement query types that are dependent on motif selection. # Previous values additional_obs_sets = prev_config["coordinationSpace"]["additionalObsSets"]["A"] @@ -292,7 +308,9 @@ def run_sq(self, prev_config): new_obs_set_selection = [[new_additional_obs_sets["tree"][0]["name"], motif_to_select, node["name"]] for node in new_additional_obs_sets["tree"][0]["children"][0]["children"]] prev_config["coordinationSpace"]["obsSetSelection"]["A"] = new_obs_set_selection - # TODO: set obsSetExpansion + # TODO: need to fix bug that prevents this from working + # Reference: https://github.com/vitessce/vitessce/blob/774328ab5c4436576dd2e8e4fff0758d6c6cce89/packages/view-types/obs-sets-manager/src/ObsSetsManagerSubscriber.js#L104 + prev_config["coordinationSpace"]["obsSetExpansion"]["A"] = [path[:-1] for path in new_obs_set_selection] return {**prev_config, "uid": f"with_query_{query_uuid}"} From 94501cc83206be97066ef9fc092d73982e211842 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:53:02 -0400 Subject: [PATCH 38/58] Support for H5AD-based AnnData in AnnDataWrapper (#361) * Update AnnDataWrapper to support h5ad files with ref spec json * Lint * Fix tests * Update --- docs/data_examples.rst | 3 +- docs/notebooks/widget_brain_h5ad.ipynb | 177 +++++++++++++++++++++++++ pyproject.toml | 2 +- tests/test_wrappers.py | 41 ++++++ vitessce/constants.py | 1 + vitessce/data_utils/__init__.py | 1 + vitessce/widget.py | 6 +- vitessce/wrappers.py | 71 +++++++++- 8 files changed, 290 insertions(+), 12 deletions(-) create mode 100644 docs/notebooks/widget_brain_h5ad.ipynb diff --git a/docs/data_examples.rst b/docs/data_examples.rst index 673cb77f..4553453c 100644 --- a/docs/data_examples.rst +++ b/docs/data_examples.rst @@ -7,4 +7,5 @@ Data preparation examples notebooks/data_export_s3 notebooks/data_export_files - notebooks/widget_brain_with_base_dir \ No newline at end of file + notebooks/widget_brain_with_base_dir + notebooks/widget_brain_h5ad \ No newline at end of file diff --git a/docs/notebooks/widget_brain_h5ad.ipynb b/docs/notebooks/widget_brain_h5ad.ipynb new file mode 100644 index 00000000..d3a37359 --- /dev/null +++ b/docs/notebooks/widget_brain_h5ad.ipynb @@ -0,0 +1,177 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "# Vitessce Widget Tutorial" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualization of single-cell RNA seq data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from os.path import join, isfile, isdir\n", + "from urllib.request import urlretrieve\n", + "from anndata import read_h5ad\n", + "import scanpy as sc\n", + "import json\n", + "\n", + "from vitessce import (\n", + " VitessceConfig,\n", + " Component as cm,\n", + " CoordinationType as ct,\n", + " AnnDataWrapper,\n", + ")\n", + "from vitessce.data_utils import (\n", + " generate_h5ad_ref_spec\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 0. Download data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "h5_url = \"https://datasets.cellxgene.cziscience.com/84df8fa1-ab53-43c9-a439-95dcb9148265.h5ad\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "adata_filepath = join(\"data\", \"84df8fa1-ab53-43c9-a439-95dcb9148265.h5ad\")\n", + "if not isfile(adata_filepath):\n", + " os.makedirs(\"data\", exist_ok=True)\n", + " urlretrieve(h5_url, adata_filepath)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Create a Reference Spec JSON file for the H5AD file\n", + "\n", + "In order for Vitessce to load H5AD files, we also need to provide a corresponding [Reference Spec](https://fsspec.github.io/kerchunk/spec.html) JSON file which contains mappings between AnnData object keys and the byte offsets at which those AnnData object values begin within the H5AD file binary contents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "json_filepath = join(\"data\", \"84df8fa1-ab53-43c9-a439-95dcb9148265.h5ad.reference.json\")\n", + "if not isfile(json_filepath):\n", + " ref_dict = generate_h5ad_ref_spec(h5_url)\n", + " with open(json_filepath, \"w\") as f:\n", + " json.dump(ref_dict, f)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "## 2. Create the Vitessce widget configuration\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vc = VitessceConfig(schema_version=\"1.0.17\", name='Nakshatri et al', description='snRNA-seq analyses of breast tissues of healthy women of diverse genetic ancestry')\n", + "\n", + "dataset = vc.add_dataset(name='84df8fa1').add_object(AnnDataWrapper(\n", + " adata_path=adata_filepath,\n", + " ref_path=json_filepath, # We specify paths to both the H5AD and JSON files\n", + " obs_embedding_paths=[\"obsm/X_wnn.umap\"],\n", + " obs_embedding_names=[\"UMAP\"],\n", + " obs_set_paths=[\"obs/cell_type\"],\n", + " obs_set_names=[\"Cell Type\"],\n", + " obs_feature_matrix_path=\"X\",\n", + " )\n", + ")\n", + "\n", + "scatterplot = vc.add_view(cm.SCATTERPLOT, dataset=dataset, mapping=\"UMAP\")\n", + "cell_sets = vc.add_view(cm.OBS_SETS, dataset=dataset)\n", + "cell_set_sizes = vc.add_view(cm.OBS_SET_SIZES, dataset=dataset)\n", + "genes = vc.add_view(cm.FEATURE_LIST, dataset=dataset)\n", + "\n", + "vc.layout((scatterplot | cell_sets) / (cell_set_sizes | genes));" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "## 3. Create the widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vw = vc.widget()\n", + "vw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/pyproject.toml b/pyproject.toml index 3e2a1ee2..17361a01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vitessce" -version = "3.3.1" +version = "3.3.2" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ] diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index d351cb1c..3c123fe2 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -239,6 +239,47 @@ def test_anndata_with_base_dir(self): 'obsSets': [{'path': 'obs/CellType', 'name': 'Cell Type'}] }}) + def test_anndata_with_h5ad_and_ref_json(self): + adata_path = data_path / 'test.h5ad' + ref_json_path = data_path / 'test.h5ad.ref.json' + w = AnnDataWrapper(adata_path, ref_path=ref_json_path, + obs_set_paths=['obs/CellType'], obs_set_names=['Cell Type'], + obs_labels_names=['Cell Label'], obs_labels_paths=['obs/CellLabel'], + obs_embedding_paths=['obsm/X_umap'], obs_embedding_names=['UMAP']) + w.local_file_uid = 'anndata.h5ad' + w.local_ref_uid = 'anndata.reference.json' + + file_def_creator = w.make_file_def_creator('A', 0) + file_def = file_def_creator('http://localhost:8000') + self.assertEqual(file_def, {'fileType': 'anndata.h5ad', 'url': 'http://localhost:8000/A/0/anndata.h5ad', + 'options': { + 'refSpecUrl': 'http://localhost:8000/A/0/anndata.reference.json', + 'obsEmbedding': [{'path': 'obsm/X_umap', 'embeddingType': 'UMAP', 'dims': [0, 1]}], + 'obsSets': [{'path': 'obs/CellType', 'name': 'Cell Type'}], + 'obsLabels': [{'path': 'obs/CellLabel', 'obsLabelsType': 'Cell Label'}] + }}) + + def test_anndata_with_h5ad_and_ref_json_with_base_dir(self): + adata_path = 'test.h5ad' + ref_json_path = 'test.h5ad.ref.json' + w = AnnDataWrapper(adata_path, ref_path=ref_json_path, + obs_set_paths=['obs/CellType'], obs_set_names=['Cell Type'], + obs_labels_names=['Cell Label'], obs_labels_paths=['obs/CellLabel'], + obs_embedding_paths=['obsm/X_umap'], obs_embedding_names=['UMAP']) + w.base_dir = data_path + w.local_file_uid = 'anndata.h5ad' + w.local_ref_uid = 'anndata.reference.json' + + file_def_creator = w.make_file_def_creator('A', 0) + file_def = file_def_creator('http://localhost:8000') + self.assertEqual(file_def, {'fileType': 'anndata.h5ad', 'url': 'http://localhost:8000/test.h5ad', + 'options': { + 'refSpecUrl': 'http://localhost:8000/test.h5ad.ref.json', + 'obsEmbedding': [{'path': 'obsm/X_umap', 'embeddingType': 'UMAP', 'dims': [0, 1]}], + 'obsSets': [{'path': 'obs/CellType', 'name': 'Cell Type'}], + 'obsLabels': [{'path': 'obs/CellLabel', 'obsLabelsType': 'Cell Label'}] + }}) + def test_csv(self): w = CsvWrapper( csv_path=data_path / 'test.umap.csv', diff --git a/vitessce/constants.py b/vitessce/constants.py index 5ca75390..4ba48608 100644 --- a/vitessce/constants.py +++ b/vitessce/constants.py @@ -162,6 +162,7 @@ class FileType(DocEnum): An enum type representing the file format or schema to which a file conforms. """ ANNDATA_ZARR = "anndata.zarr", "Joint file type for AnnData objects" + ANNDATA_H5AD = "anndata.h5ad", "Joint file type for AnnData objects" OBS_EMBEDDING_CSV = 'obsEmbedding.csv', "File type for obsEmbedding values stored in a CSV file" OBS_LOCATIONS_CSV = 'obsLocations.csv', "File type for obsLocations values stored in a CSV file" OBS_LABELS_CSV = 'obsLabels.csv', "File type for obsLabels values stored in a CSV file" diff --git a/vitessce/data_utils/__init__.py b/vitessce/data_utils/__init__.py index 88077ca1..0b1d4cce 100644 --- a/vitessce/data_utils/__init__.py +++ b/vitessce/data_utils/__init__.py @@ -6,6 +6,7 @@ sort_var_axis, to_diamond, VAR_CHUNK_SIZE, + generate_h5ad_ref_spec, ) from .ome import ( rgb_img_to_ome_zarr, diff --git a/vitessce/widget.py b/vitessce/widget.py index 0e2ee988..e4ca766f 100644 --- a/vitessce/widget.py +++ b/vitessce/widget.py @@ -432,7 +432,7 @@ class VitessceWidget(anywidget.AnyWidget): next_port = DEFAULT_PORT - js_package_version = Unicode('3.3.12').tag(sync=True) + js_package_version = Unicode('3.4.10').tag(sync=True) js_dev_mode = Bool(False).tag(sync=True) custom_js_url = Unicode('').tag(sync=True) plugin_esm = List(trait=Unicode(''), default_value=[]).tag(sync=True) @@ -440,7 +440,7 @@ class VitessceWidget(anywidget.AnyWidget): store_urls = List(trait=Unicode(''), default_value=[]).tag(sync=True) - def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.3.12', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True): + def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.4.10', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True): """ Construct a new Vitessce widget. @@ -552,7 +552,7 @@ def _plugin_command(self, params, buffers): # Launch Vitessce using plain HTML representation (no ipywidgets) -def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.3.12', js_dev_mode=False, custom_js_url='', plugin_esm=DEFAULT_PLUGIN_ESM, remount_on_uid_change=True): +def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.4.10', js_dev_mode=False, custom_js_url='', plugin_esm=DEFAULT_PLUGIN_ESM, remount_on_uid_change=True): from IPython.display import display, HTML uid_str = "vitessce" + get_uid_str(uid) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 921ffa8d..7e4569eb 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -123,10 +123,14 @@ def get_out_dir_route(self, dataset_uid, obj_i): app=StaticFiles(directory=out_dir, html=False))] return [] - def get_local_dir_url(self, base_url, dataset_uid, obj_i, local_dir_path, local_dir_uid): + def get_local_file_url(self, base_url, dataset_uid, obj_i, local_file_path, local_file_uid): if not self.is_remote and self.base_dir is not None: - return self._get_url_simple(base_url, file_path_to_url_path(local_dir_path, prepend_slash=False)) - return self._get_url(base_url, dataset_uid, obj_i, local_dir_uid) + return self._get_url_simple(base_url, file_path_to_url_path(local_file_path, prepend_slash=False)) + return self._get_url(base_url, dataset_uid, obj_i, local_file_uid) + + def get_local_dir_url(self, base_url, dataset_uid, obj_i, local_dir_path, local_dir_uid): + # Logic for files and directories is the same for this function. + return self.get_local_file_url(base_url, dataset_uid, obj_i, local_dir_path, local_dir_uid) def register_zarr_store(self, dataset_uid, obj_i, store_or_local_dir_path, local_dir_uid): if not self.is_remote and self.is_store: @@ -177,6 +181,21 @@ def get_local_dir_route(self, dataset_uid, obj_i, local_dir_path, local_dir_uid) app=StaticFiles(directory=local_dir_path, html=False))] return [] + def get_local_file_route(self, dataset_uid, obj_i, local_file_path, local_file_uid): + if not self.is_remote: + from .routes import range_repsonse, FileRoute + + if self.base_dir is None: + route_path = self._get_route_str(dataset_uid, obj_i, local_file_uid) + else: + route_path = file_path_to_url_path(local_file_path) + local_file_path = join(self.base_dir, local_file_path) + + return [ + FileRoute(route_path, lambda req: range_repsonse(req, local_file_path), local_file_path), + ] + return [] + def _get_url(self, base_url, dataset_uid, obj_i, *args): return base_url + self._get_route_str(dataset_uid, obj_i, *args) @@ -937,7 +956,7 @@ def image_file_def_creator(base_url): class AnnDataWrapper(AbstractWrapper): - def __init__(self, adata_path=None, adata_url=None, adata_store=None, obs_feature_matrix_path=None, feature_filter_path=None, initial_feature_filter_path=None, obs_set_paths=None, obs_set_names=None, obs_locations_path=None, obs_segmentations_path=None, obs_embedding_paths=None, obs_embedding_names=None, obs_embedding_dims=None, obs_spots_path=None, obs_points_path=None, feature_labels_path=None, obs_labels_path=None, convert_to_dense=True, coordination_values=None, obs_labels_paths=None, obs_labels_names=None, **kwargs): + def __init__(self, adata_path=None, adata_url=None, adata_store=None, ref_path=None, ref_url=None, obs_feature_matrix_path=None, feature_filter_path=None, initial_feature_filter_path=None, obs_set_paths=None, obs_set_names=None, obs_locations_path=None, obs_segmentations_path=None, obs_embedding_paths=None, obs_embedding_names=None, obs_embedding_dims=None, obs_spots_path=None, obs_points_path=None, feature_labels_path=None, obs_labels_path=None, convert_to_dense=True, coordination_values=None, obs_labels_paths=None, obs_labels_names=None, **kwargs): """ Wrap an AnnData object by creating an instance of the ``AnnDataWrapper`` class. @@ -972,6 +991,19 @@ def __init__(self, adata_path=None, adata_url=None, adata_store=None, obs_featur self._adata_url = adata_url self._adata_store = adata_store + # For reference spec JSON with .h5ad files + self._ref_path = ref_path + self._ref_url = ref_url + + if ref_path is not None or ref_url is not None: + self.is_h5ad = True + else: + self.is_h5ad = False + + if adata_store is not None and (ref_path is not None or ref_url is not None): + raise ValueError( + "Did not expect ref_path or ref_url to be provided with adata_store") + num_inputs = sum([1 for x in [adata_path, adata_url, adata_store] if x is not None]) if num_inputs > 1: raise ValueError( @@ -995,6 +1027,9 @@ def __init__(self, adata_path=None, adata_url=None, adata_store=None, obs_featur self.zarr_folder = None self.local_dir_uid = make_unique_filename(".adata.zarr") + self.local_file_uid = make_unique_filename(".h5ad") + self.local_ref_uid = make_unique_filename(".ref.json") + self._expression_matrix = obs_feature_matrix_path self._cell_set_obs_names = obs_set_names self._mappings_obsm_names = obs_embedding_names @@ -1037,7 +1072,13 @@ def make_anndata_routes(self, dataset_uid, obj_i): self.register_zarr_store(dataset_uid, obj_i, self._adata_store, self.local_dir_uid) return [] else: - return self.get_local_dir_route(dataset_uid, obj_i, self._adata_path, self.local_dir_uid) + if self.is_h5ad: + return [ + *self.get_local_file_route(dataset_uid, obj_i, self._adata_path, self.local_file_uid), + *self.get_local_file_route(dataset_uid, obj_i, self._ref_path, self.local_ref_uid) + ] + else: + return self.get_local_dir_route(dataset_uid, obj_i, self._adata_path, self.local_dir_uid) def get_zarr_url(self, base_url="", dataset_uid="", obj_i=""): if self.is_remote: @@ -1045,6 +1086,18 @@ def get_zarr_url(self, base_url="", dataset_uid="", obj_i=""): else: return self.get_local_dir_url(base_url, dataset_uid, obj_i, self._adata_path, self.local_dir_uid) + def get_h5ad_url(self, base_url="", dataset_uid="", obj_i=""): + if self.is_remote: + return self._adata_url + else: + return self.get_local_file_url(base_url, dataset_uid, obj_i, self._adata_path, self.local_file_uid) + + def get_ref_url(self, base_url="", dataset_uid="", obj_i=""): + if self.is_remote: + return self._ref_url + else: + return self.get_local_file_url(base_url, dataset_uid, obj_i, self._ref_path, self.local_ref_uid) + def make_file_def_creator(self, dataset_uid, obj_i): def get_anndata_zarr(base_url): options = {} @@ -1120,10 +1173,14 @@ def get_anndata_zarr(base_url): for path, name in zip(self._obs_labels_paths, names): obs_labels.append({"path": path, "obsLabelsType": name}) options["obsLabels"] = obs_labels + if len(options.keys()) > 0: + if self.is_h5ad: + options["refSpecUrl"] = self.get_ref_url(base_url, dataset_uid, obj_i) + obj_file_def = { - "fileType": ft.ANNDATA_ZARR.value, - "url": self.get_zarr_url(base_url, dataset_uid, obj_i), + "fileType": ft.ANNDATA_ZARR.value if not self.is_h5ad else ft.ANNDATA_H5AD.value, + "url": self.get_zarr_url(base_url, dataset_uid, obj_i) if not self.is_h5ad else self.get_h5ad_url(base_url, dataset_uid, obj_i), "options": options } if self._request_init is not None: From c48fdd8fc9cd0ab1ade5158b90c9c13544a8acbd Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:10:17 -0400 Subject: [PATCH 39/58] Fix binder example (#363) * Fix binder issue * bump version --- docs/notebooks/widget_pbmc.ipynb | 15 +++++---------- pyproject.toml | 2 +- vitessce/widget.py | 16 ++++++++++++---- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/notebooks/widget_pbmc.ipynb b/docs/notebooks/widget_pbmc.ipynb index f972949c..284546aa 100644 --- a/docs/notebooks/widget_pbmc.ipynb +++ b/docs/notebooks/widget_pbmc.ipynb @@ -118,7 +118,9 @@ "source": [ "## 4. Create a Vitessce view config\n", "\n", - "Define the data and views you would like to include in the widget." + "Define the data and views you would like to include in the widget.\n", + "\n", + "For more details about how to configure data depending on where the files are located relative to the notebook execution, see https://python-docs.vitessce.io/data_options.html." ] }, { @@ -129,7 +131,7 @@ "source": [ "vc = VitessceConfig(schema_version=\"1.0.15\", name='PBMC Reference')\n", "dataset = vc.add_dataset(name='PBMC 3k').add_object(AnnDataWrapper(\n", - " adata_path=zarr_filepath,\n", + " adata_store=zarr_filepath,\n", " obs_set_paths=[\"obs/leiden\"],\n", " obs_set_names=[\"Leiden\"],\n", " obs_embedding_paths=[\"obsm/X_umap\", \"obsm/X_pca\"],\n", @@ -153,13 +155,6 @@ "## 5. Create the Vitessce widget" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "A widget can be created with the `.widget()` method on the config instance. Here, the `proxy=True` parameter allows this widget to be used in a cloud notebook environment, such as Binder." - ] - }, { "cell_type": "code", "execution_count": null, @@ -201,7 +196,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.0" + "version": "3.9.0" } }, "nbformat": 4, diff --git a/pyproject.toml b/pyproject.toml index 17361a01..def6f149 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vitessce" -version = "3.3.2" +version = "3.3.3" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ] diff --git a/vitessce/widget.py b/vitessce/widget.py index e4ca766f..f00d0d1f 100644 --- a/vitessce/widget.py +++ b/vitessce/widget.py @@ -202,6 +202,7 @@ def get_uid_str(uid): const pluginEsmArr = view.model.get('plugin_esm'); const remountOnUidChange = view.model.get('remount_on_uid_change'); const storeUrls = view.model.get('store_urls'); + const invokeTimeout = view.model.get('invoke_timeout'); const pkgName = (jsDevMode ? "@vitessce/dev" : "vitessce"); @@ -230,7 +231,9 @@ def get_uid_str(uid): storeUrl, { async get(key) { - const [data, buffers] = await view.experimental.invoke("_zarr_get", [storeUrl, key]); + const [data, buffers] = await view.experimental.invoke("_zarr_get", [storeUrl, key], { + signal: AbortSignal.timeout(invokeTimeout), + }); if (!data.success) return undefined; return buffers[0].buffer; }, @@ -239,7 +242,9 @@ def get_uid_str(uid): ); function invokePluginCommand(commandName, commandParams, commandBuffers) { - return view.experimental.invoke("_plugin_command", [commandName, commandParams], commandBuffers); + return view.experimental.invoke("_plugin_command", [commandName, commandParams], commandBuffers, { + signal: AbortSignal.timeout(invokeTimeout), + }); } for (const pluginEsm of pluginEsmArr) { @@ -437,10 +442,11 @@ class VitessceWidget(anywidget.AnyWidget): custom_js_url = Unicode('').tag(sync=True) plugin_esm = List(trait=Unicode(''), default_value=[]).tag(sync=True) remount_on_uid_change = Bool(True).tag(sync=True) + invoke_timeout = Int(30000).tag(sync=True) store_urls = List(trait=Unicode(''), default_value=[]).tag(sync=True) - def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.4.10', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True): + def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.4.10', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, invoke_timeout=30000): """ Construct a new Vitessce widget. @@ -455,6 +461,7 @@ def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy= :param str custom_js_url: A URL to a JavaScript file to use (instead of 'vitessce' or '@vitessce/dev' NPM package). :param list[WidgetPlugin] plugins: A list of subclasses of VitesscePlugin. Optional. :param bool remount_on_uid_change: Passed to the remountOnUidChange prop of the React component. By default, True. + :param int invoke_timeout: The timeout in milliseconds for invoking Python functions from JavaScript. By default, 30000. .. code-block:: python :emphasize-lines: 4 @@ -486,7 +493,7 @@ def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy= super(VitessceWidget, self).__init__( config=config_dict, height=height, theme=theme, proxy=proxy, js_package_version=js_package_version, js_dev_mode=js_dev_mode, custom_js_url=custom_js_url, - plugin_esm=plugin_esm, remount_on_uid_change=remount_on_uid_change, + plugin_esm=plugin_esm, remount_on_uid_change=remount_on_uid_change, invoke_timeout=invoke_timeout, uid=uid_str, store_urls=list(self._stores.keys()) ) @@ -569,6 +576,7 @@ def ipython_display(config, height=600, theme='auto', base_url=None, host_name=N "custom_js_url": custom_js_url, "plugin_esm": plugin_esm, "remount_on_uid_change": remount_on_uid_change, + "invoke_timeout": 30000, "proxy": proxy, "has_host_name": host_name is not None, "height": height, From b02d7670bb6c07c8d7503a8b34bc26287af8f6fd Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:00:34 -0400 Subject: [PATCH 40/58] Fix bug (#367) --- pyproject.toml | 2 +- vitessce/widget.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index def6f149..7178cabb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vitessce" -version = "3.3.3" +version = "3.3.4" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ] diff --git a/vitessce/widget.py b/vitessce/widget.py index f00d0d1f..d2e61bdc 100644 --- a/vitessce/widget.py +++ b/vitessce/widget.py @@ -242,8 +242,9 @@ def get_uid_str(uid): ); function invokePluginCommand(commandName, commandParams, commandBuffers) { - return view.experimental.invoke("_plugin_command", [commandName, commandParams], commandBuffers, { + return view.experimental.invoke("_plugin_command", [commandName, commandParams], { signal: AbortSignal.timeout(invokeTimeout), + ...(commandBuffers ? { buffers: commandBuffers } : {}), }); } From bf5853b5b5f2dbc8f9c3c2094ba1c34ecff571e1 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 24 Sep 2024 12:39:45 +0200 Subject: [PATCH 41/58] (fix): `image_elem` usage + table key --- docs/notebooks/spatial_data.ipynb | 81 +++++++++++++++---------------- vitessce/wrappers.py | 12 +++-- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/docs/notebooks/spatial_data.ipynb b/docs/notebooks/spatial_data.ipynb index e5df3109..cb482c96 100644 --- a/docs/notebooks/spatial_data.ipynb +++ b/docs/notebooks/spatial_data.ipynb @@ -70,10 +70,10 @@ "if not zip_filepath.exists():\n", " spatialdata_filepath.parent.mkdir(exist_ok=True)\n", " urlretrieve('https://s3.embl.de/spatialdata/spatialdata-sandbox/visium_associated_xenium_io.zip', zip_filepath)\n", + "if not spatialdata_filepath.exists():\n", " with zipfile.ZipFile(zip_filepath,\"r\") as zip_ref:\n", " zip_ref.extractall(spatialdata_filepath.parent)\n", - " (spatialdata_filepath.parent / \"data.zarr\").rename(spatialdata_filepath)\n", - " (spatialdata_filepath / \"tables\").rename(spatialdata_filepath / \"table\") # TODO: remove once fixed in Vitessce" + " (spatialdata_filepath.parent / \"data.zarr\").rename(spatialdata_filepath)" ] }, { @@ -89,16 +89,7 @@ "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/var/folders/k9/9wc7lvwj2g34_r74kn6cr0nr0000gn/T/ipykernel_86408/1452425863.py:1: DeprecationWarning: Table group found in zarr store at location /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr. Please update the zarr storeto use tables instead.\n", - " spatialdata = read_zarr(spatialdata_filepath)\n" - ] - } - ], + "outputs": [], "source": [ "spatialdata = read_zarr(spatialdata_filepath)" ] @@ -111,21 +102,21 @@ { "data": { "text/plain": [ - "SpatialData object with:\n", + "SpatialData object, with associated Zarr store: /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr\n", "├── Images\n", - "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_full_image': MultiscaleSpatialImage[cyx] (3, 21571, 19505), (3, 10785, 9752), (3, 5392, 4876), (3, 2696, 2438), (3, 1348, 1219)\n", - "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_hires_image': SpatialImage[cyx] (3, 2000, 1809)\n", - "│ └── 'CytAssist_FFPE_Human_Breast_Cancer_lowres_image': SpatialImage[cyx] (3, 600, 543)\n", + "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_full_image': DataTree[cyx] (3, 21571, 19505), (3, 10785, 9752), (3, 5392, 4876), (3, 2696, 2438), (3, 1348, 1219)\n", + "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_hires_image': DataArray[cyx] (3, 2000, 1809)\n", + "│ └── 'CytAssist_FFPE_Human_Breast_Cancer_lowres_image': DataArray[cyx] (3, 600, 543)\n", "├── Shapes\n", "│ └── 'CytAssist_FFPE_Human_Breast_Cancer': GeoDataFrame shape: (4992, 2) (2D shapes)\n", "└── Tables\n", " └── 'table': AnnData (4992, 18085)\n", "with coordinate systems:\n", - "▸ 'downscaled_hires', with elements:\n", + " ▸ 'downscaled_hires', with elements:\n", " CytAssist_FFPE_Human_Breast_Cancer_hires_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", - "▸ 'downscaled_lowres', with elements:\n", + " ▸ 'downscaled_lowres', with elements:\n", " CytAssist_FFPE_Human_Breast_Cancer_lowres_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", - "▸ 'global', with elements:\n", + " ▸ 'global', with elements:\n", " CytAssist_FFPE_Human_Breast_Cancer_full_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)" ] }, @@ -158,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -180,7 +171,7 @@ }, { "cell_type": "code", - "execution_count": 72, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -192,7 +183,7 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -201,16 +192,16 @@ }, { "cell_type": "code", - "execution_count": 74, + "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 74, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -225,7 +216,7 @@ " }]),\n", "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"image\"))\n", "obs_sets = vc.add_view(cm.OBS_SETS, dataset=dataset)\n", - "vc.link_views([spatial, layer_controller, feature_list, obs_sets], ['obsType'], ['spot'])" + "vc.link_views([spatial, layer_controller, feature_list, obs_sets], ['obsType'], [wrapper.obs_type_label])" ] }, { @@ -239,7 +230,7 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -248,21 +239,23 @@ }, { "cell_type": "code", - "execution_count": 76, - "metadata": {}, + "execution_count": 20, + "metadata": { + "scrolled": true + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8b0f04224d574808b63f2c768461e1b9", + "model_id": "4b1c23ba8ed04d28a2ee0551ae88902b", "version_major": 2, - "version_minor": 0 + "version_minor": 1 }, "text/plain": [ "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)', '…" ] }, - "execution_count": 76, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -274,7 +267,7 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -283,7 +276,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -295,12 +288,10 @@ " 'datasets': [{'uid': 'A',\n", " 'name': 'Breast Cancer Visium',\n", " 'files': [{'fileType': 'spatialdata.zarr',\n", - " 'url': '/A/0/279375f1-8232-4905-b2c4-8566c89e183a.spatialdata.zarr',\n", - " 'options': {'obsFeatureMatrix': {'path': 'table/table/X'},\n", - " 'obsSets': [{'name': 'region', 'path': 'table/table/obs/region'}],\n", - " 'obsSpots': {'path': 'shapes/CytAssist_FFPE_Human_Breast_Cancer',\n", - " 'tablePath': 'table/table',\n", - " 'region': 'region'},\n", + " 'url': '/A/0/cdbd3dcc-6415-4572-a9df-1ec6472cfcf2.spatialdata.zarr',\n", + " 'options': {'obsFeatureMatrix': {'path': 'tables/table/X'},\n", + " 'obsSets': {'obsSets': [{'name': 'region',\n", + " 'path': 'tables/table/obs/region'}]},\n", " 'image': {'path': 'images/CytAssist_FFPE_Human_Breast_Cancer_full_image'}},\n", " 'coordinationValues': {'obsType': 'spot'}}]}],\n", " 'coordinationSpace': {'dataset': {'A': 'A'},\n", @@ -323,20 +314,26 @@ " 'x': 6.0,\n", " 'y': 0.0,\n", " 'w': 6.0,\n", - " 'h': 6.0},\n", + " 'h': 3.0},\n", " {'component': 'layerControllerBeta',\n", " 'coordinationScopes': {'dataset': 'A',\n", " 'metaCoordinationScopes': ['init_A_image_0'],\n", " 'metaCoordinationScopesBy': ['init_A_image_0'],\n", " 'obsType': 'A'},\n", " 'x': 6.0,\n", + " 'y': 3.0,\n", + " 'w': 6.0,\n", + " 'h': 3.0},\n", + " {'component': 'obsSets',\n", + " 'coordinationScopes': {'dataset': 'A', 'obsType': 'A'},\n", + " 'x': 6.0,\n", " 'y': 6.0,\n", " 'w': 6.0,\n", " 'h': 6.0}],\n", " 'initStrategy': 'auto'}" ] }, - "execution_count": 62, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index fb76aff4..216dff69 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1167,9 +1167,12 @@ def __init__(self, spatialdata_path: Optional[str] = None, spatialdata_url: Opti else: self.is_remote = True self.zarr_folder = None + self.obs_type_label = None + if "obsType" in self._coordination_values: + self.obs_type_label = self._coordination_values["obsType"] @classmethod - def from_object(cls: Type[SpatialDataWrapperType], spatialdata: SpatialData, table_keys_to_image_elems: dict[str, Union[str, None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str, None]] = defaultdict(type(None))) -> list[SpatialDataWrapperType]: + def from_object(cls: Type[SpatialDataWrapperType], spatialdata: SpatialData, table_keys_to_image_elems: dict[str, Union[str, None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str, None]] = defaultdict(type(None)), obs_type_label: str = "spot") -> list[SpatialDataWrapperType]: """Instantiate a wrapper for SpatialData stores, one per table, directly from the SpatialData object. By default, we "show everything" that can reasonable be inferred given the information. If you wish to have more control, consider instantiating the object directly. This function will error if something cannot be inferred i.e., the user does not present @@ -1196,6 +1199,7 @@ def from_object(cls: Type[SpatialDataWrapperType], spatialdata: SpatialData, tab ValueError """ wrappers = [] + parent_table_key = "table" if (spatialdata.path / "table").exists() else "tables" for table_key, table in spatialdata.tables.items(): shapes_elem = None image_elem = table_keys_to_image_elems[table_key] @@ -1214,17 +1218,17 @@ def from_object(cls: Type[SpatialDataWrapperType], spatialdata: SpatialData, tab shapes_elem = f"shapes/{region}" if region in spatialdata.labels: labels_elem = f"labels/{region}" - obs_feature_matrix_elem = f"table/{table_key}/X" + obs_feature_matrix_elem = f"{parent_table_key}/{table_key}/X" if 'highly_variable' in table.var: # TODO: fix first key needing to be "table" in vitessce-js initial_feature_filter_elem = 'highly_variable' else: initial_feature_filter_elem = None - obs_set_elems = [f"table/{table_key}/obs/{elem}" for elem in table.obs if table.obs[elem].dtype == 'category'] + obs_set_elems = [f"{parent_table_key}/{table_key}/obs/{elem}" for elem in table.obs if table.obs[elem].dtype == 'category'] wrappers += [ cls( spatialdata_path=str(spatialdata.path), - image_path=str(image_elem) if image_elem is not None else None, + image_elem=str(image_elem) if image_elem is not None else None, labels_path=str(labels_elem) if labels_elem is not None else None, obs_feature_matrix_path=str(obs_feature_matrix_elem), shapes_path=str(shapes_elem) if shapes_elem is not None else None, From 73b8f52e9e24830ce5531c6b1d1201d400dba43f Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 24 Sep 2024 12:40:11 +0200 Subject: [PATCH 42/58] (chore): update SpatialData --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1e66f253..426e9c9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ 'black>=21.11b1', 'numpy>=1.21.2', 'anndata>=0.7.8,<0.11', - 'spatialdata@git+https://github.com/scverse/spatialdata.git#egg=fix/dask', + 'spatialdata>=0.2.2', 'scanpy>=1.9.3', 'ome-zarr>=0.8.3', 'tifffile>=2020.10.1', From 6dc8779609cfcacd63082d93b592725a3f97726b Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 24 Sep 2024 12:43:41 +0200 Subject: [PATCH 43/58] (fix): `shapes_elem` --- vitessce/wrappers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 216dff69..c3cf14bc 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1231,7 +1231,7 @@ def from_object(cls: Type[SpatialDataWrapperType], spatialdata: SpatialData, tab image_elem=str(image_elem) if image_elem is not None else None, labels_path=str(labels_elem) if labels_elem is not None else None, obs_feature_matrix_path=str(obs_feature_matrix_elem), - shapes_path=str(shapes_elem) if shapes_elem is not None else None, + shapes_elem=str(shapes_elem) if shapes_elem is not None else None, initial_feature_filter_path=initial_feature_filter_elem, obs_set_paths=obs_set_elems, coordination_values={"obsType": "spot"} # TODO: should we remove? From d8db5e018e40e982da1fe3b4f50d4163352262f2 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 24 Sep 2024 12:55:54 +0200 Subject: [PATCH 44/58] (fix): capitalize name of obs set --- vitessce/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vitessce/utils.py b/vitessce/utils.py index e475a92c..393d17ba 100644 --- a/vitessce/utils.py +++ b/vitessce/utils.py @@ -76,7 +76,7 @@ def gen_obs_sets_schema(options: dict, paths: Optional[list[str]] = None, names: if names is not None: names = names else: - names = [obs.split('/')[-1] for obs in paths] + names = [obs.split('/')[-1].capitalize() for obs in paths] for obs, name in zip(paths, names): options["obsSets"].append({ "name": name, @@ -138,11 +138,11 @@ def gen_image_schema(options, path: str, affine_transformation: Optional[np.ndar return options -def gen_obs_spots_from_shapes_schema(options: dict, shapes_path: Optional[str] = None) -> dict: +def gen_obs_spots_from_shapes_schema(options: dict, shapes_path: Optional[str] = None, table_path: str = "tables/table") -> dict: if shapes_path is not None: options['obsSpots'] = { "path": shapes_path, - "tablePath": "table/table", + "tablePath": table_path, "region": "region" } return options From 76a665236d00bcb292141b30b7a1e2ca09eee48a Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 24 Sep 2024 12:56:40 +0200 Subject: [PATCH 45/58] (fix): use table for spots --- docs/notebooks/spatial_data.ipynb | 57 +++++++++++++++++++------------ vitessce/wrappers.py | 5 +-- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/docs/notebooks/spatial_data.ipynb b/docs/notebooks/spatial_data.ipynb index cb482c96..af2da7a5 100644 --- a/docs/notebooks/spatial_data.ipynb +++ b/docs/notebooks/spatial_data.ipynb @@ -27,9 +27,18 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 14, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + } + ], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -61,7 +70,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -87,7 +96,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -96,7 +105,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -120,7 +129,7 @@ " CytAssist_FFPE_Human_Breast_Cancer_full_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)" ] }, - "execution_count": 4, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -149,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 80, "metadata": {}, "outputs": [], "source": [ @@ -171,7 +180,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 81, "metadata": {}, "outputs": [], "source": [ @@ -183,7 +192,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 82, "metadata": {}, "outputs": [], "source": [ @@ -192,16 +201,16 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 83, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 18, + "execution_count": 83, "metadata": {}, "output_type": "execute_result" } @@ -230,7 +239,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 84, "metadata": {}, "outputs": [], "source": [ @@ -239,7 +248,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 85, "metadata": { "scrolled": true }, @@ -247,7 +256,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4b1c23ba8ed04d28a2ee0551ae88902b", + "model_id": "", "version_major": 2, "version_minor": 1 }, @@ -255,7 +264,7 @@ "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)', '…" ] }, - "execution_count": 20, + "execution_count": 85, "metadata": {}, "output_type": "execute_result" } @@ -267,7 +276,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 87, "metadata": {}, "outputs": [], "source": [ @@ -276,7 +285,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 86, "metadata": {}, "outputs": [ { @@ -288,10 +297,14 @@ " 'datasets': [{'uid': 'A',\n", " 'name': 'Breast Cancer Visium',\n", " 'files': [{'fileType': 'spatialdata.zarr',\n", - " 'url': '/A/0/cdbd3dcc-6415-4572-a9df-1ec6472cfcf2.spatialdata.zarr',\n", + " 'url': '/A/0/bdccdfab-49c3-4dad-8c85-aaca4cf984f6.spatialdata.zarr',\n", " 'options': {'obsFeatureMatrix': {'path': 'tables/table/X'},\n", - " 'obsSets': {'obsSets': [{'name': 'region',\n", - " 'path': 'tables/table/obs/region'}]},\n", + " 'obsSets': {'obsSets': [{'name': 'Region',\n", + " 'path': 'tables/table/obs/region'}],\n", + " 'tablePath': 'tables/table'},\n", + " 'obsSpots': {'path': 'shapes/CytAssist_FFPE_Human_Breast_Cancer',\n", + " 'tablePath': 'tables/table',\n", + " 'region': 'region'},\n", " 'image': {'path': 'images/CytAssist_FFPE_Human_Breast_Cancer_full_image'}},\n", " 'coordinationValues': {'obsType': 'spot'}}]}],\n", " 'coordinationSpace': {'dataset': {'A': 'A'},\n", @@ -333,7 +346,7 @@ " 'initStrategy': 'auto'}" ] }, - "execution_count": 14, + "execution_count": 86, "metadata": {}, "output_type": "execute_result" } diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index c3cf14bc..d45d6139 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1113,7 +1113,7 @@ def auto_view_config(self, vc): class SpatialDataWrapper(AnnDataWrapper): - def __init__(self, spatialdata_path: Optional[str] = None, spatialdata_url: Optional[str] = None, spatialdata_store: Optional[str] = None, image_elem: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None, shapes_elem: Optional[str] = None, labels_elem: Optional[str] = None, **kwargs): + def __init__(self, spatialdata_path: Optional[str] = None, spatialdata_url: Optional[str] = None, spatialdata_store: Optional[str] = None, image_elem: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None, shapes_elem: Optional[str] = None, labels_elem: Optional[str] = None, table_path: str = "tables/table", **kwargs): """_summary_ Parameters @@ -1170,6 +1170,7 @@ def __init__(self, spatialdata_path: Optional[str] = None, spatialdata_url: Opti self.obs_type_label = None if "obsType" in self._coordination_values: self.obs_type_label = self._coordination_values["obsType"] + self._table_path = table_path @classmethod def from_object(cls: Type[SpatialDataWrapperType], spatialdata: SpatialData, table_keys_to_image_elems: dict[str, Union[str, None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str, None]] = defaultdict(type(None)), obs_type_label: str = "spot") -> list[SpatialDataWrapperType]: @@ -1246,7 +1247,7 @@ def generator(base_url): options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) options = gen_obs_sets_schema(options, self._obs_set_elems, self._obs_set_names) options['obsSets'] = {'obsSets': options['obsSets']} # see https://github.com/vitessce/vitessce/blob/cd7e81956786a8130658d6745ff03986e2e6f806/packages/schemas/src/file-def-options.ts#L138-L146 for nested structure - options = gen_obs_spots_from_shapes_schema(options, self._shapes_elem) + options = gen_obs_spots_from_shapes_schema(options, self._shapes_elem, self._table_path) options = gen_image_schema(options, self._image_elem, self._affine_transformation) options = gen_feature_labels_schema(self._feature_labels, options) if len(options.keys()) > 0: From 5f8cfca311cce01e2425ff3bf2b590f5ff4607a2 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 24 Sep 2024 13:02:02 +0200 Subject: [PATCH 46/58] (fix): capitalization --- vitessce/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vitessce/utils.py b/vitessce/utils.py index 393d17ba..9240ea07 100644 --- a/vitessce/utils.py +++ b/vitessce/utils.py @@ -76,7 +76,11 @@ def gen_obs_sets_schema(options: dict, paths: Optional[list[str]] = None, names: if names is not None: names = names else: - names = [obs.split('/')[-1].capitalize() for obs in paths] + names = [] + for obs in paths: + obs_end_path = obs.split('/')[-1] + first_letter_capitalized = obs_end_path.capitalize() + names = [first_letter_capitalized + obs_end_path[1:]] for obs, name in zip(paths, names): options["obsSets"].append({ "name": name, From f7cba04af33f5667b3d9535b92458d7b9968ffbb Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 24 Sep 2024 13:02:28 +0200 Subject: [PATCH 47/58] (fix): first letter only --- vitessce/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitessce/utils.py b/vitessce/utils.py index 9240ea07..b83d03fa 100644 --- a/vitessce/utils.py +++ b/vitessce/utils.py @@ -79,7 +79,7 @@ def gen_obs_sets_schema(options: dict, paths: Optional[list[str]] = None, names: names = [] for obs in paths: obs_end_path = obs.split('/')[-1] - first_letter_capitalized = obs_end_path.capitalize() + first_letter_capitalized = obs_end_path.capitalize()[0] names = [first_letter_capitalized + obs_end_path[1:]] for obs, name in zip(paths, names): options["obsSets"].append({ From b5e1a1986e8d6a5b35c38787c2e54899c9887883 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Tue, 24 Sep 2024 13:09:03 +0200 Subject: [PATCH 48/58] (fix): check `self._coordination_values` --- vitessce/wrappers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index d45d6139..918c90b4 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1168,7 +1168,7 @@ def __init__(self, spatialdata_path: Optional[str] = None, spatialdata_url: Opti self.is_remote = True self.zarr_folder = None self.obs_type_label = None - if "obsType" in self._coordination_values: + if self._coordination_values is not None and "obsType" in self._coordination_values: self.obs_type_label = self._coordination_values["obsType"] self._table_path = table_path From dc021ebe57fa86be20830ebc8a19c2ed8446254c Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 2 Oct 2024 08:43:58 -0400 Subject: [PATCH 49/58] Add notebook (#369) --- .../notebooks/widget_segmentations_beta.ipynb | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 docs/notebooks/widget_segmentations_beta.ipynb diff --git a/docs/notebooks/widget_segmentations_beta.ipynb b/docs/notebooks/widget_segmentations_beta.ipynb new file mode 100644 index 00000000..6c82c420 --- /dev/null +++ b/docs/notebooks/widget_segmentations_beta.ipynb @@ -0,0 +1,104 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "# Vitessce Widget Tutorial" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from vitessce import (\n", + " VitessceConfig,\n", + " Component as cm,\n", + " CoordinationType as ct,\n", + " OmeTiffWrapper,\n", + " MultiImageWrapper,\n", + " CoordinationLevel as CL,\n", + " ObsSegmentationsOmeTiffWrapper,\n", + " ImageOmeTiffWrapper,\n", + " get_initial_coordination_scope_prefix,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vc = VitessceConfig(schema_version=\"1.0.16\")\n", + "dataset = vc.add_dataset(name='Spraggins').add_object(\n", + " ImageOmeTiffWrapper(\n", + " img_url=\"https://storage.googleapis.com/vitessce-demo-data/kpmp-f2f-march-2023/S-1905-017737/S-1905-017737_PAS_2of2_bf.ome.tif\",\n", + " offsets_url=\"https://storage.googleapis.com/vitessce-demo-data/kpmp-f2f-march-2023/S-1905-017737/S-1905-017737_PAS_2of2_bf.offsets.json\"\n", + " )\n", + ").add_object(\n", + " ObsSegmentationsOmeTiffWrapper(\n", + " img_url=\"https://storage.googleapis.com/vitessce-demo-data/kpmp-f2f-march-2023/S-1905-017737/S-1905-017737_PAS_2of2.ome.tif\",\n", + " offsets_url=\"https://storage.googleapis.com/vitessce-demo-data/kpmp-f2f-march-2023/S-1905-017737/S-1905-017737_PAS_2of2.offsets.json\",\n", + " obs_types_from_channel_names=True\n", + " )\n", + ")\n", + "\n", + "spatial = vc.add_view(\"spatialBeta\", dataset=dataset)\n", + "lc = vc.add_view(\"layerControllerBeta\", dataset=dataset)\n", + "\n", + "vc.link_views_by_dict([spatial, lc], {\n", + " \"imageLayer\": CL([\n", + " {\n", + " \"photometricInterpretation\": \"RGB\" \n", + " }\n", + " ]),\n", + "}, meta=True, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"image\"))\n", + "\n", + "vc.layout(spatial | lc);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vw = vc.widget()\n", + "vw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From f267084ee22de69addd2cd9a70725d15d303b25b Mon Sep 17 00:00:00 2001 From: tkakar Date: Wed, 2 Oct 2024 09:16:14 -0400 Subject: [PATCH 50/58] Added utility functions to update and remove a view (#368) * Added utility functions to update and remove a view * Added tests for the utility functions * Refactored to multiple functions --- tests/test_config_updates.py | 87 ++++++++++++++++++++++++++++++++++++ vitessce/config.py | 83 ++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 tests/test_config_updates.py diff --git a/tests/test_config_updates.py b/tests/test_config_updates.py new file mode 100644 index 00000000..b413ae12 --- /dev/null +++ b/tests/test_config_updates.py @@ -0,0 +1,87 @@ +import pytest + +from vitessce import ( + VitessceConfig, + ViewType as cm, +) + + +@pytest.fixture +def vitessce_config(): + vc = VitessceConfig(schema_version="1.0.15") + my_dataset = vc.add_dataset(name='My Dataset') + vc.add_view(cm.SPATIAL, dataset=my_dataset) + vc.add_view(cm.SCATTERPLOT, dataset=my_dataset) + vc.add_view(cm.SCATTERPLOT, dataset=my_dataset) + return vc + + +def test_get_views(vitessce_config): + views = vitessce_config.get_views() + assert len(views) == 3 + assert views[0].view["component"].lower() == "spatial" + assert views[1].view["component"].lower() == "scatterplot" + + +def test_get_view_by_index(vitessce_config): + view = vitessce_config.get_view_by_index(0) + vc_dict = view.to_dict() + assert vc_dict["component"] == "spatial" + + view = vitessce_config.get_view_by_index(1) + vc_dict = view.to_dict() + assert vc_dict["component"] == "scatterplot" + + with pytest.raises(IndexError): + vitessce_config.get_view_by_index(5) + + +def test_get_view_by_component(vitessce_config): + view = vitessce_config.get_first_view_by_type("spatial") + vc_dict = view.to_dict() + assert vc_dict["component"] == "spatial" + + view = vitessce_config.get_first_view_by_type("SCATTERPLOT") + vc_dict = view.to_dict() + assert vc_dict["component"] == "scatterplot" + + with pytest.raises(ValueError): + vitessce_config.get_first_view_by_type("TEST") + + +def test_get_view_by_invalid_type(vitessce_config): + with pytest.raises(TypeError): + vitessce_config.get_first_view_by_type(3.5) + + +def test_remove_view_by_index(vitessce_config): + removed_view = vitessce_config.remove_view_by_index(0) + rv_dict = removed_view.to_dict() + assert rv_dict["component"] == "spatial" + assert len(vitessce_config.get_views()) == 2 + + removed_view = vitessce_config.remove_view_by_index(1) + rv_dict = removed_view.to_dict() + assert rv_dict["component"] == "scatterplot" + assert len(vitessce_config.get_views()) == 1 + + with pytest.raises(IndexError): + vitessce_config.remove_view_by_index(5) + + +def test_remove_view_by_component(vitessce_config): + removed_view = vitessce_config.remove_first_view_by_type("spatial") + rv_dict = removed_view.to_dict() + assert rv_dict["component"] == "spatial" + assert len(vitessce_config.get_views()) == 2 + + with pytest.raises(ValueError): + vitessce_config.remove_first_view_by_type("spatial") + + with pytest.raises(ValueError): + vitessce_config.remove_first_view_by_type("TEST") + + +def test_remove_view_by_invalid_index(vitessce_config): + with pytest.raises(TypeError): + vitessce_config.remove_view_by_index(3.5) diff --git a/vitessce/config.py b/vitessce/config.py index 040674ea..63103eb9 100644 --- a/vitessce/config.py +++ b/vitessce/config.py @@ -1495,6 +1495,89 @@ def to_dict(self, base_url=None): "layout": [c.to_dict() for c in self.config["layout"]] } + def get_views(self): + """ + Provides all the views in the config.layout object list + + :returns: A list of VitessceConfigView objects. + + """ + return self.config["layout"] + + def get_view_by_index(self, index): + """ + Get a view from the layout by the index specified by the 'index' parameter. + + :param index: Index (int) of the view in the Layout array. + :type index: int + + :returns: The view corresponding to the provided index + :rtype: VitessceConfigView or None if not found + """ + if isinstance(index, int): + if 0 <= index < len(self.config["layout"]): + return self.config["layout"][index] + else: + raise IndexError("index out of range") + else: + raise TypeError("index must be an integer") + + def get_first_view_by_type(self, view_type): + """ + Get a view from the layout by view type (component) specified by the 'view_type' parameter. + + :param view_type: The view type (str) of the view in the Layout array. + :type view_type: str + + :returns: The view corresponding to the provided view_type. + :rtype: VitessceConfigView or None if not found + """ + if isinstance(view_type, str): + for view in self.config["layout"]: + if view.view["component"].lower() == view_type.lower(): + return view + raise ValueError(f"No view found with component view_type: {view_type}") + else: + raise TypeError("view_type must be a string representing the view type") + + def remove_view_by_index(self, index): + """ + Removes a view from the layout by the index specified by the 'index' parameter. + + :param index: the index (int) of the view + :type index: int + + :returns: The layout component of the config corresponding to the specified index + :rtype: VitessceConfigView or None if not found + + """ + if isinstance(index, int): + if 0 <= index < len(self.config["layout"]): + return self.config["layout"].pop(index) + else: + raise IndexError("Index out of range") + else: + raise TypeError("index must be an integer") + + def remove_first_view_by_type(self, view_type): + """ + Removes a view from the layout by the view type (component) specified by the 'view_type' parameter. + + :param view_by: A component view_type (str). + :type view_by: str + + :returns: The layout component of the config corresponding to the specified view_type + :rtype: VitessceConfigView or None if not found + + """ + if isinstance(view_type, str): + for i, view in enumerate(self.config["layout"]): + if view.view["component"].lower() == view_type.lower(): + return self.config["layout"].pop(i) + raise ValueError(f"No view found with component type: {view_type}") + else: + raise TypeError("view_by must a string representing component type") + def get_routes(self): """ Convert the routes for this view config from the datasets. From 6bac2c256c7acb03192c6d1255513323c28c1799 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:39:56 -0400 Subject: [PATCH 51/58] Keller mark/more exports (#370) * Update exports for createPlugins * Version --- pyproject.toml | 2 +- vitessce/widget.py | 24 ++++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7178cabb..20c11bb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vitessce" -version = "3.3.4" +version = "3.3.5" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ] diff --git a/vitessce/widget.py b/vitessce/widget.py index d2e61bdc..b38da7ea 100644 --- a/vitessce/widget.py +++ b/vitessce/widget.py @@ -219,6 +219,15 @@ def get_uid_str(uid): PluginJointFileType, z, useCoordination, + useGridItemSize, + // TODO: names and function signatures are subject to change for the following functions + // Reference: https://github.com/keller-mark/use-coordination/issues/37#issuecomment-1946226827 + useComplexCoordination, + useMultiCoordinationScopesNonNull, + useMultiCoordinationScopesSecondaryNonNull, + useComplexCoordinationSecondary, + useCoordinationScopes, + useCoordinationScopesBy, } = await importWithMap("vitessce", importMap); let pluginViewTypes = []; @@ -261,8 +270,15 @@ def get_uid_str(uid): PluginCoordinationType, PluginJointFileType, z, - useCoordination, invokeCommand: invokePluginCommand, + useCoordination, + useGridItemSize, + useComplexCoordination, + useMultiCoordinationScopesNonNull, + useMultiCoordinationScopesSecondaryNonNull, + useComplexCoordinationSecondary, + useCoordinationScopes, + useCoordinationScopesBy, }); if(Array.isArray(pluginsObj.pluginViewTypes)) { pluginViewTypes = [...pluginViewTypes, ...pluginsObj.pluginViewTypes]; @@ -438,7 +454,7 @@ class VitessceWidget(anywidget.AnyWidget): next_port = DEFAULT_PORT - js_package_version = Unicode('3.4.10').tag(sync=True) + js_package_version = Unicode('3.4.12').tag(sync=True) js_dev_mode = Bool(False).tag(sync=True) custom_js_url = Unicode('').tag(sync=True) plugin_esm = List(trait=Unicode(''), default_value=[]).tag(sync=True) @@ -447,7 +463,7 @@ class VitessceWidget(anywidget.AnyWidget): store_urls = List(trait=Unicode(''), default_value=[]).tag(sync=True) - def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.4.10', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, invoke_timeout=30000): + def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.4.12', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, invoke_timeout=30000): """ Construct a new Vitessce widget. @@ -560,7 +576,7 @@ def _plugin_command(self, params, buffers): # Launch Vitessce using plain HTML representation (no ipywidgets) -def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.4.10', js_dev_mode=False, custom_js_url='', plugin_esm=DEFAULT_PLUGIN_ESM, remount_on_uid_change=True): +def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.4.12', js_dev_mode=False, custom_js_url='', plugin_esm=DEFAULT_PLUGIN_ESM, remount_on_uid_change=True): from IPython.display import display, HTML uid_str = "vitessce" + get_uid_str(uid) From d4ebe79487c4f99a63b5832add076ed1ab99ea26 Mon Sep 17 00:00:00 2001 From: ilan-gold Date: Fri, 4 Oct 2024 17:18:23 +0200 Subject: [PATCH 52/58] (chore): add `obsSets` check --- vitessce/wrappers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 918c90b4..f7c14f2d 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1246,7 +1246,8 @@ def generator(base_url): options = gen_obs_labels_schema(options, self._obs_labels_elems, self._obs_labels_names) options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) options = gen_obs_sets_schema(options, self._obs_set_elems, self._obs_set_names) - options['obsSets'] = {'obsSets': options['obsSets']} # see https://github.com/vitessce/vitessce/blob/cd7e81956786a8130658d6745ff03986e2e6f806/packages/schemas/src/file-def-options.ts#L138-L146 for nested structure + if 'obsSets' in options: + options['obsSets'] = {'obsSets': options['obsSets']} # see https://github.com/vitessce/vitessce/blob/cd7e81956786a8130658d6745ff03986e2e6f806/packages/schemas/src/file-def-options.ts#L138-L146 for nested structure options = gen_obs_spots_from_shapes_schema(options, self._shapes_elem, self._table_path) options = gen_image_schema(options, self._image_elem, self._affine_transformation) options = gen_feature_labels_schema(self._feature_labels, options) From bc5e21c4f5ec8015207a5b90ac3f2a83caec85d5 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:29:06 -0400 Subject: [PATCH 53/58] Allow passing artifacts as data in addition to paths, urls, and zarr stores (#371) * Register artifacts in wrapper constructors * Docs * Add test * Update * Version bump * Lint * Update is_remote logic * Update --- pyproject.toml | 2 +- tests/test_config.py | 63 ++++++++++++++++++ vitessce/config.py | 19 ++++++ vitessce/wrappers.py | 148 +++++++++++++++++++++++++++++++++---------- 4 files changed, 197 insertions(+), 35 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 20c11bb4..3ed83ad5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vitessce" -version = "3.3.5" +version = "3.4.0" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ] diff --git a/tests/test_config.py b/tests/test_config.py index 3b836876..fe572629 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -20,6 +20,20 @@ ) +class MockArtifactPath: + def __init__(self, url): + self.url = url + + def to_url(self): + return self.url + + +class MockArtifact: + def __init__(self, name, url): + self.name = name + self.path = MockArtifactPath(url) + + def test_config_creation(): vc = VitessceConfig(schema_version="1.0.15") vc_dict = vc.to_dict() @@ -107,6 +121,55 @@ def test_config_add_anndata_url(): } +def test_config_add_anndata_artifact(): + vc = VitessceConfig(schema_version="1.0.15") + vc.add_dataset(name='My Dataset').add_object( + AnnDataWrapper( + adata_artifact=MockArtifact("My anndata artifact", "http://example.com/adata.h5ad.zarr"), + obs_set_paths=["obs/louvain"], + ) + ) + + vc_dict = vc.to_dict() + + assert vc_dict == { + "version": "1.0.15", + "name": "", + "description": "", + "datasets": [ + { + 'uid': 'A', + 'name': 'My Dataset', + 'files': [ + { + "fileType": "anndata.zarr", + "url": "http://example.com/adata.h5ad.zarr", + "options": { + "obsSets": [ + { + "name": "louvain", + "path": "obs/louvain", + } + ] + } + } + ] + } + ], + 'coordinationSpace': { + 'dataset': { + 'A': 'A' + }, + }, + "layout": [], + "initStrategy": "auto" + } + + vc_artifacts = vc.get_artifacts() + assert list(vc_artifacts.keys()) == ["http://example.com/adata.h5ad.zarr"] + assert vc_artifacts["http://example.com/adata.h5ad.zarr"].name == "My anndata artifact" + + def test_config_add_dataset_add_files(): vc = VitessceConfig(schema_version="1.0.15") vc.add_dataset(name='My Chained Dataset').add_file( diff --git a/vitessce/config.py b/vitessce/config.py index 63103eb9..2574d706 100644 --- a/vitessce/config.py +++ b/vitessce/config.py @@ -235,6 +235,13 @@ def get_routes(self): return routes + def get_artifacts(self): + artifacts = {} + for obj in self.objs: + artifacts.update(obj.get_artifacts()) + + return artifacts + def get_stores(self, base_url=None): stores = {} for obj in self.objs: @@ -1590,6 +1597,18 @@ def get_routes(self): routes += d.get_routes() return routes + def get_artifacts(self): + """ + Get all artifacts for this view config from the datasets. + + :returns: A dict mapping artifact URLs to corresponding artifact objects. + :rtype: dict[str, lamindb.Artifact] + """ + artifacts = {} + for d in self.config["datasets"]: + artifacts.update(d.get_artifacts()) + return artifacts + def get_stores(self, base_url=None): """ Convert the routes for this view config from the datasets. diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 7e4569eb..87017f67 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -47,6 +47,7 @@ def __init__(self, **kwargs): self.file_def_creators = [] self.base_dir = None self.stores = {} + self.artifacts = {} self._request_init = kwargs['request_init'] if 'request_init' in kwargs else None def __repr__(self): @@ -74,6 +75,28 @@ def get_routes(self): """ return self.routes + def register_artifact(self, artifact): + """ + Register an artifact. + + :param artifact: The artifact object to register. + :type artifact: lamindb.Artifact + :returns: The artifact URL. + :rtype: str + """ + artifact_url = artifact.path.to_url() + self.artifacts[artifact_url] = artifact + return artifact_url + + def get_artifacts(self): + """ + Obtain the dictionary that maps artifact URLs to artifact objects. + + :returns: A dictionary that maps artifact URLs to Artifact objects. + :rtype: dict[str, lamindb.Artifact] + """ + return self.artifacts + def get_stores(self, base_url): """ Obtain the stores that have been created for this wrapper class. @@ -417,21 +440,36 @@ class ImageOmeTiffWrapper(AbstractWrapper): :param \\*\\*kwargs: Keyword arguments inherited from :class:`~vitessce.wrappers.AbstractWrapper` """ - def __init__(self, img_path=None, offsets_path=None, img_url=None, offsets_url=None, coordinate_transformations=None, coordination_values=None, **kwargs): + def __init__(self, img_path=None, img_url=None, img_artifact=None, offsets_path=None, offsets_url=None, offsets_artifact=None, coordinate_transformations=None, coordination_values=None, **kwargs): super().__init__(**kwargs) self._repr = make_repr(locals()) + num_inputs = sum([1 for x in [img_path, img_url, img_artifact] if x is not None]) + if num_inputs != 1: + raise ValueError( + "Expected one of img_path, img_url, or img_artifact to be provided") + + num_inputs = sum([1 for x in [offsets_path, offsets_url, offsets_artifact] if x is not None]) + if num_inputs > 1: + raise ValueError( + "Expected zero or one of offsets_path, offsets_url, or offsets_artifact to be provided") + self._img_path = img_path self._img_url = img_url + self._img_artifact = img_artifact self._offsets_path = offsets_path self._offsets_url = offsets_url + self._offsets_artifact = offsets_artifact self._coordinate_transformations = coordinate_transformations self._coordination_values = coordination_values - self.is_remote = img_url is not None + self.is_remote = img_url is not None or img_artifact is not None self.local_img_uid = make_unique_filename(".ome.tif") self.local_offsets_uid = make_unique_filename(".offsets.json") - if img_url is not None and (img_path is not None or offsets_path is not None): - raise ValueError( - "Did not expect img_path or offsets_path to be provided with img_url") + + if img_artifact is not None: + self._img_url = self.register_artifact(img_artifact) + + if offsets_artifact is not None: + self._offsets_url = self.register_artifact(offsets_artifact) def convert_and_save(self, dataset_uid, obj_i, base_dir=None): # Only create out-directory if needed @@ -521,31 +559,50 @@ class ObsSegmentationsOmeTiffWrapper(AbstractWrapper): Wrap an OME-TIFF File by creating an instance of the ``ObsSegmentationsOmeTiffWrapper`` class. Intended to be used with the spatialBeta and layerControllerBeta views. :param str img_path: A local filepath to an OME-TIFF file. - :param str offsets_path: A local filepath to an offsets.json file. :param str img_url: A remote URL of an OME-TIFF file. + :param img_artifact: A lamindb Artifact corresponding to the image. + :type img_artifact: lamindb.Artifact + :param str offsets_path: A local filepath to an offsets.json file. :param str offsets_url: A remote URL of an offsets.json file. + :param offsets_artifact: A lamindb Artifact corresponding to the offsets JSON. + :type offsets_artifact: lamindb.Artifact :param list coordinate_transformations: A column-major ordered matrix for transforming this image (see http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/#homogeneous-coordinates for more information). :param bool obs_types_from_channel_names: Whether to use the channel names to determine the obs types. Optional. :param dict coordination_values: Optional coordinationValues to be passed in the file definition. :param \\*\\*kwargs: Keyword arguments inherited from :class:`~vitessce.wrappers.AbstractWrapper` """ - def __init__(self, img_path=None, offsets_path=None, img_url=None, offsets_url=None, coordinate_transformations=None, obs_types_from_channel_names=None, coordination_values=None, **kwargs): + def __init__(self, img_path=None, img_url=None, img_artifact=None, offsets_path=None, offsets_url=None, offsets_artifact=None, coordinate_transformations=None, obs_types_from_channel_names=None, coordination_values=None, **kwargs): super().__init__(**kwargs) self._repr = make_repr(locals()) + num_inputs = sum([1 for x in [img_path, img_url, img_artifact] if x is not None]) + if num_inputs != 1: + raise ValueError( + "Expected one of img_path, img_url, or img_artifact to be provided") + + num_inputs = sum([1 for x in [offsets_path, offsets_url, offsets_artifact] if x is not None]) + if num_inputs > 1: + raise ValueError( + "Expected zero or one of offsets_path, offsets_url, or offsets_artifact to be provided") + self._img_path = img_path self._img_url = img_url + self._img_artifact = img_artifact self._offsets_path = offsets_path self._offsets_url = offsets_url + self._offsets_artifact = offsets_artifact self._coordinate_transformations = coordinate_transformations self._obs_types_from_channel_names = obs_types_from_channel_names self._coordination_values = coordination_values - self.is_remote = img_url is not None + self.is_remote = img_url is not None or img_artifact is not None self.local_img_uid = make_unique_filename(".ome.tif") self.local_offsets_uid = make_unique_filename(".offsets.json") - if img_url is not None and (img_path is not None or offsets_path is not None): - raise ValueError( - "Did not expect img_path or offsets_path to be provided with img_url") + + if img_artifact is not None: + self._img_url = self.register_artifact(img_artifact) + + if offsets_artifact is not None: + self._offsets_url = self.register_artifact(offsets_artifact) def convert_and_save(self, dataset_uid, obj_i, base_dir=None): # Only create out-directory if needed @@ -809,28 +866,36 @@ class ImageOmeZarrWrapper(AbstractWrapper): :param str img_path: A local filepath to an OME-NGFF Zarr store. :param str img_url: A remote URL of an OME-NGFF Zarr store. + :param img_artifact: A lamindb Artifact corresponding to the image. + :type img_artifact: lamindb.Artifact :param list coordinate_transformations: A column-major ordered matrix for transforming this image (see http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/#homogeneous-coordinates for more information). :param dict coordination_values: Optional coordinationValues to be passed in the file definition. :param \\*\\*kwargs: Keyword arguments inherited from :class:`~vitessce.wrappers.AbstractWrapper` """ - def __init__(self, img_path=None, img_url=None, coordinate_transformations=None, coordination_values=None, **kwargs): + def __init__(self, img_path=None, img_url=None, img_artifact=None, coordinate_transformations=None, coordination_values=None, **kwargs): super().__init__(**kwargs) self._repr = make_repr(locals()) - if img_url is not None and img_path is not None: - raise ValueError( - "Did not expect img_path to be provided with img_url") - if img_url is None and img_path is None: + + num_inputs = sum([1 for x in [img_path, img_url, img_artifact] if x is not None]) + if num_inputs != 1: raise ValueError( - "Expected either img_url or img_path to be provided") + "Expected one of img_path, img_url, or img_artifact to be provided") + self._img_path = img_path self._img_url = img_url + self._img_artifact = img_artifact self._coordinate_transformations = coordinate_transformations self._coordination_values = coordination_values if self._img_path is not None: self.is_remote = False else: self.is_remote = True + + if self._img_artifact is not None: + # To serve as a placeholder in the config JSON URL field + self._img_url = self.register_artifact(img_artifact) + self.local_dir_uid = make_unique_filename(".ome.zarr") def convert_and_save(self, dataset_uid, obj_i, base_dir=None): @@ -883,23 +948,25 @@ class ObsSegmentationsOmeZarrWrapper(AbstractWrapper): :param str img_path: A local filepath to an OME-NGFF Zarr store. :param str img_url: A remote URL of an OME-NGFF Zarr store. + :param img_artifact: A lamindb Artifact corresponding to the image. + :type img_artifact: lamindb.Artifact :param list coordinate_transformations: A column-major ordered matrix for transforming this image (see http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/#homogeneous-coordinates for more information). :param dict coordination_values: Optional coordinationValues to be passed in the file definition. :param bool obs_types_from_channel_names: Whether to use the channel names to determine the obs types. Optional. :param \\*\\*kwargs: Keyword arguments inherited from :class:`~vitessce.wrappers.AbstractWrapper` """ - def __init__(self, img_path=None, img_url=None, coordinate_transformations=None, coordination_values=None, obs_types_from_channel_names=None, **kwargs): + def __init__(self, img_path=None, img_url=None, img_artifact=None, coordinate_transformations=None, coordination_values=None, obs_types_from_channel_names=None, **kwargs): super().__init__(**kwargs) self._repr = make_repr(locals()) - if img_url is not None and img_path is not None: - raise ValueError( - "Did not expect img_path to be provided with img_url") - if img_url is None and img_path is None: + + num_inputs = sum([1 for x in [img_path, img_url, img_artifact] if x is not None]) + if num_inputs != 1: raise ValueError( - "Expected either img_url or img_path to be provided") + "Expected one of img_path, img_url, or img_artifact to be provided") self._img_path = img_path self._img_url = img_url + self._img_artifact = img_artifact self._coordinate_transformations = coordinate_transformations self._obs_types_from_channel_names = obs_types_from_channel_names self._coordination_values = coordination_values @@ -907,6 +974,11 @@ def __init__(self, img_path=None, img_url=None, coordinate_transformations=None, self.is_remote = False else: self.is_remote = True + + if self._img_artifact is not None: + # To serve as a placeholder in the config JSON URL field + self._img_url = self.register_artifact(img_artifact) + self.local_dir_uid = make_unique_filename(".ome.zarr") def convert_and_save(self, dataset_uid, obj_i, base_dir=None): @@ -956,7 +1028,7 @@ def image_file_def_creator(base_url): class AnnDataWrapper(AbstractWrapper): - def __init__(self, adata_path=None, adata_url=None, adata_store=None, ref_path=None, ref_url=None, obs_feature_matrix_path=None, feature_filter_path=None, initial_feature_filter_path=None, obs_set_paths=None, obs_set_names=None, obs_locations_path=None, obs_segmentations_path=None, obs_embedding_paths=None, obs_embedding_names=None, obs_embedding_dims=None, obs_spots_path=None, obs_points_path=None, feature_labels_path=None, obs_labels_path=None, convert_to_dense=True, coordination_values=None, obs_labels_paths=None, obs_labels_names=None, **kwargs): + def __init__(self, adata_path=None, adata_url=None, adata_store=None, adata_artifact=None, ref_path=None, ref_url=None, ref_artifact=None, obs_feature_matrix_path=None, feature_filter_path=None, initial_feature_filter_path=None, obs_set_paths=None, obs_set_names=None, obs_locations_path=None, obs_segmentations_path=None, obs_embedding_paths=None, obs_embedding_names=None, obs_embedding_dims=None, obs_spots_path=None, obs_points_path=None, feature_labels_path=None, obs_labels_path=None, convert_to_dense=True, coordination_values=None, obs_labels_paths=None, obs_labels_names=None, **kwargs): """ Wrap an AnnData object by creating an instance of the ``AnnDataWrapper`` class. @@ -964,6 +1036,8 @@ def __init__(self, adata_path=None, adata_url=None, adata_store=None, ref_path=N :param str adata_url: A remote url pointing to a zarr-backed AnnData store. :param adata_store: A path to pass to zarr.DirectoryStore, or an existing store instance. :type adata_store: str or zarr.Storage + :param adata_artifact: A lamindb Artifact corresponding to the AnnData object. + :type adata_artifact: lamindb.Artifact :param str obs_feature_matrix_path: Location of the expression (cell x gene) matrix, like `X` or `obsm/highly_variable_genes_subset` :param str feature_filter_path: A string like `var/highly_variable` used in conjunction with `obs_feature_matrix_path` if obs_feature_matrix_path points to a subset of `X` of the full `var` list. :param str initial_feature_filter_path: A string like `var/highly_variable` used in conjunction with `obs_feature_matrix_path` if obs_feature_matrix_path points to a subset of `X` of the full `var` list. @@ -990,36 +1064,42 @@ def __init__(self, adata_path=None, adata_url=None, adata_store=None, ref_path=N self._adata_path = adata_path self._adata_url = adata_url self._adata_store = adata_store + self._adata_artifact = adata_artifact # For reference spec JSON with .h5ad files self._ref_path = ref_path self._ref_url = ref_url + self._ref_artifact = ref_artifact - if ref_path is not None or ref_url is not None: + if ref_path is not None or ref_url is not None or ref_artifact is not None: self.is_h5ad = True else: self.is_h5ad = False - if adata_store is not None and (ref_path is not None or ref_url is not None): + if adata_store is not None and (ref_path is not None or ref_url is not None or ref_artifact is not None): raise ValueError( - "Did not expect ref_path or ref_url to be provided with adata_store") + "Did not expect reference JSON to be provided with adata_store") - num_inputs = sum([1 for x in [adata_path, adata_url, adata_store] if x is not None]) - if num_inputs > 1: - raise ValueError( - "Expected only one of adata_path, adata_url, or adata_store to be provided") - if num_inputs == 0: + num_inputs = sum([1 for x in [adata_path, adata_url, adata_store, adata_artifact] if x is not None]) + if num_inputs != 1: raise ValueError( - "Expected one of adata_path, adata_url, or adata_store to be provided") + "Expected one of adata_path, adata_url, adata_artifact, or adata_store to be provided") if adata_path is not None: self.is_remote = False self.is_store = False self.zarr_folder = 'anndata.zarr' - elif adata_url is not None: + elif adata_url is not None or adata_artifact is not None: self.is_remote = True self.is_store = False self.zarr_folder = None + + # Store artifacts on AbstractWrapper.artifacts for downstream access, + # e.g. in lamindb.save_vitessce_config + if adata_artifact is not None: + self._adata_url = self.register_artifact(adata_artifact) + if ref_artifact is not None: + self._ref_url = self.register_artifact(ref_artifact) else: # Store case self.is_remote = False From eb223446cecdc8db63938175c6cca2d0924ba6a8 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:17:27 -0400 Subject: [PATCH 54/58] SpatialData changes into original SpatialData branch (#374) * Try again * Fix bugs * Lint * Update * Omit --- .coveragerc_omit | 1 + docs/notebooks/spatial_data.ipynb | 164 ++++----------------------- pyproject.toml | 1 - tests/test_wrappers.py | 6 +- vitessce/file_def_utils.py | 154 +++++++++++++++++++++++++ vitessce/utils.py | 117 ------------------- vitessce/wrappers.py | 181 +++++++++++++++++------------- 7 files changed, 283 insertions(+), 341 deletions(-) create mode 100644 vitessce/file_def_utils.py diff --git a/.coveragerc_omit b/.coveragerc_omit index 96a24637..b2036261 100644 --- a/.coveragerc_omit +++ b/.coveragerc_omit @@ -2,6 +2,7 @@ omit = vitessce/config.py vitessce/export.py + vitessce/file_def_utils.py vitessce/routes.py vitessce/widget.py vitessce/wrappers.py diff --git a/docs/notebooks/spatial_data.ipynb b/docs/notebooks/spatial_data.ipynb index af2da7a5..6e14a242 100644 --- a/docs/notebooks/spatial_data.ipynb +++ b/docs/notebooks/spatial_data.ipynb @@ -27,18 +27,9 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The autoreload extension is already loaded. To reload it, use:\n", - " %reload_ext autoreload\n" - ] - } - ], + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -70,7 +61,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -96,7 +87,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -105,35 +96,9 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "SpatialData object, with associated Zarr store: /Users/ilangold/Projects/Gehlenborg/vitessce-python/docs/notebooks/data/visium.spatialdata.zarr\n", - "├── Images\n", - "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_full_image': DataTree[cyx] (3, 21571, 19505), (3, 10785, 9752), (3, 5392, 4876), (3, 2696, 2438), (3, 1348, 1219)\n", - "│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_hires_image': DataArray[cyx] (3, 2000, 1809)\n", - "│ └── 'CytAssist_FFPE_Human_Breast_Cancer_lowres_image': DataArray[cyx] (3, 600, 543)\n", - "├── Shapes\n", - "│ └── 'CytAssist_FFPE_Human_Breast_Cancer': GeoDataFrame shape: (4992, 2) (2D shapes)\n", - "└── Tables\n", - " └── 'table': AnnData (4992, 18085)\n", - "with coordinate systems:\n", - " ▸ 'downscaled_hires', with elements:\n", - " CytAssist_FFPE_Human_Breast_Cancer_hires_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", - " ▸ 'downscaled_lowres', with elements:\n", - " CytAssist_FFPE_Human_Breast_Cancer_lowres_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)\n", - " ▸ 'global', with elements:\n", - " CytAssist_FFPE_Human_Breast_Cancer_full_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "spatialdata" ] @@ -158,7 +123,7 @@ }, { "cell_type": "code", - "execution_count": 80, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -180,7 +145,7 @@ }, { "cell_type": "code", - "execution_count": 81, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -192,7 +157,7 @@ }, { "cell_type": "code", - "execution_count": 82, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -201,20 +166,9 @@ }, { "cell_type": "code", - "execution_count": 83, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 83, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "spatial = vc.add_view(\"spatialBeta\", dataset=dataset)\n", "feature_list = vc.add_view(cm.FEATURE_LIST, dataset=dataset)\n", @@ -224,6 +178,11 @@ " 'photometricInterpretation': 'RGB',\n", " }]),\n", "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"image\"))\n", + "vc.link_views_by_dict([spatial, layer_controller], {\n", + " 'spotLayer': CL([{\n", + " 'obsType': 'spot',\n", + " }]),\n", + "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"obsSpots\"))\n", "obs_sets = vc.add_view(cm.OBS_SETS, dataset=dataset)\n", "vc.link_views([spatial, layer_controller, feature_list, obs_sets], ['obsType'], [wrapper.obs_type_label])" ] @@ -239,7 +198,7 @@ }, { "cell_type": "code", - "execution_count": 84, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -248,27 +207,11 @@ }, { "cell_type": "code", - "execution_count": 85, + "execution_count": null, "metadata": { "scrolled": true }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "VitessceWidget(config={'version': '1.0.16', 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)', '…" - ] - }, - "execution_count": 85, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "vw = vc.widget()\n", "vw" @@ -276,7 +219,7 @@ }, { "cell_type": "code", - "execution_count": 87, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -285,72 +228,9 @@ }, { "cell_type": "code", - "execution_count": 86, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'version': '1.0.16',\n", - " 'name': 'Visium SpatialData Demo (visium_associated_xenium_io)',\n", - " 'description': 'From https://spatialdata.scverse.org/en/latest/tutorials/notebooks/datasets/README.html',\n", - " 'datasets': [{'uid': 'A',\n", - " 'name': 'Breast Cancer Visium',\n", - " 'files': [{'fileType': 'spatialdata.zarr',\n", - " 'url': '/A/0/bdccdfab-49c3-4dad-8c85-aaca4cf984f6.spatialdata.zarr',\n", - " 'options': {'obsFeatureMatrix': {'path': 'tables/table/X'},\n", - " 'obsSets': {'obsSets': [{'name': 'Region',\n", - " 'path': 'tables/table/obs/region'}],\n", - " 'tablePath': 'tables/table'},\n", - " 'obsSpots': {'path': 'shapes/CytAssist_FFPE_Human_Breast_Cancer',\n", - " 'tablePath': 'tables/table',\n", - " 'region': 'region'},\n", - " 'image': {'path': 'images/CytAssist_FFPE_Human_Breast_Cancer_full_image'}},\n", - " 'coordinationValues': {'obsType': 'spot'}}]}],\n", - " 'coordinationSpace': {'dataset': {'A': 'A'},\n", - " 'imageLayer': {'init_A_image_0': '__dummy__'},\n", - " 'photometricInterpretation': {'init_A_image_0': 'RGB'},\n", - " 'metaCoordinationScopes': {'init_A_image_0': {'imageLayer': ['init_A_image_0']}},\n", - " 'metaCoordinationScopesBy': {'init_A_image_0': {'imageLayer': {'photometricInterpretation': {'init_A_image_0': 'init_A_image_0'}}}},\n", - " 'obsType': {'A': 'spot'}},\n", - " 'layout': [{'component': 'spatialBeta',\n", - " 'coordinationScopes': {'dataset': 'A',\n", - " 'metaCoordinationScopes': ['init_A_image_0'],\n", - " 'metaCoordinationScopesBy': ['init_A_image_0'],\n", - " 'obsType': 'A'},\n", - " 'x': 0.0,\n", - " 'y': 0,\n", - " 'w': 6.0,\n", - " 'h': 12},\n", - " {'component': 'featureList',\n", - " 'coordinationScopes': {'dataset': 'A', 'obsType': 'A'},\n", - " 'x': 6.0,\n", - " 'y': 0.0,\n", - " 'w': 6.0,\n", - " 'h': 3.0},\n", - " {'component': 'layerControllerBeta',\n", - " 'coordinationScopes': {'dataset': 'A',\n", - " 'metaCoordinationScopes': ['init_A_image_0'],\n", - " 'metaCoordinationScopesBy': ['init_A_image_0'],\n", - " 'obsType': 'A'},\n", - " 'x': 6.0,\n", - " 'y': 3.0,\n", - " 'w': 6.0,\n", - " 'h': 3.0},\n", - " {'component': 'obsSets',\n", - " 'coordinationScopes': {'dataset': 'A', 'obsType': 'A'},\n", - " 'x': 6.0,\n", - " 'y': 6.0,\n", - " 'w': 6.0,\n", - " 'h': 6.0}],\n", - " 'initStrategy': 'auto'}" - ] - }, - "execution_count": 86, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "vc.to_dict(\"\")" ] diff --git a/pyproject.toml b/pyproject.toml index 53ee7f14..9f918d32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,6 @@ classifiers = [ 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'Topic :: Multimedia :: Graphics', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index 8571b53d..1e86a23c 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -233,7 +233,6 @@ def test_anndata_with_base_dir(self): file_def_creator = w.make_file_def_creator('A', 0) file_def = file_def_creator('http://localhost:8000') - print(file_def) self.assertEqual(file_def, {'fileType': 'anndata.zarr', 'url': 'http://localhost:8000/test.h5ad.zarr', 'options': { 'obsEmbedding': [{'path': 'obsm/X_umap', 'dims': [0, 1], 'embeddingType': 'UMAP'}], @@ -249,7 +248,6 @@ def test_anndata_with_base_dir_no_names(self): file_def_creator = w.make_file_def_creator('A', 0) file_def = file_def_creator('http://localhost:8000') - print(file_def) self.assertEqual(file_def, {'fileType': 'anndata.zarr', 'url': 'http://localhost:8000/test.h5ad.zarr', 'options': { 'obsEmbedding': [{'path': 'obsm/X_umap', 'dims': [0, 1], 'embeddingType': 'X_umap'}], @@ -396,7 +394,7 @@ def test_multivec_zarr_with_base_dir(self): def test_spatial_data_with_base_dir(self): spatial_data_path = 'test.spatialdata.zarr' - w = SpatialDataWrapper(spatialdata_path=spatial_data_path, image_elem="picture", obs_set_paths=['obs/CellType'], obs_set_names=['Cell Type'], obs_embedding_paths=[ + w = SpatialDataWrapper(sdata_path=spatial_data_path, image_path="images/picture", obs_set_paths=['obs/CellType'], obs_set_names=['Cell Type'], obs_embedding_paths=[ 'obsm/X_umap'], obs_embedding_names=['UMAP']) w.base_dir = data_path w.local_dir_uid = 'spatialdata.zarr' @@ -405,4 +403,4 @@ def test_spatial_data_with_base_dir(self): file_def = file_def_creator('http://localhost:8000') print(file_def) self.assertEqual(file_def, - {'fileType': 'spatialdata.zarr', 'url': 'http://localhost:8000/test.spatialdata.zarr', 'options': {'obsSets': {'obsSets': [{'name': 'Cell Type', 'path': 'obs/CellType'}]}, 'image': {'path': 'picture'}}}) + {'fileType': 'spatialdata.zarr', 'url': 'http://localhost:8000/test.spatialdata.zarr', 'options': {'obsSets': {'obsSets': [{'name': 'Cell Type', 'path': 'obs/CellType'}], 'tablePath': 'tables/table'}, 'image': {'path': 'images/picture'}}}) diff --git a/vitessce/file_def_utils.py b/vitessce/file_def_utils.py new file mode 100644 index 00000000..ece8508d --- /dev/null +++ b/vitessce/file_def_utils.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from functools import partial +from typing import Optional + +import numpy as np + + +def gen_obs_embedding_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None, dims: Optional[list[list[int]]] = None): + if paths is not None: + if "obsEmbedding" not in options: + options["obsEmbedding"] = [] + if names is not None: + for key, mapping in zip(paths, names): + options["obsEmbedding"].append({ + "path": key, + "dims": [0, 1], + "embeddingType": mapping + }) + else: + for mapping in paths: + mapping_key = mapping.split('/')[-1] + options["obsEmbedding"].append({ + "path": mapping, + "dims": [0, 1], + "embeddingType": mapping_key + }) + if dims is not None: + if "obsEmbedding" not in options: + options["obsEmbedding"] = [] + for dim_i, dim in enumerate(dims): + options["obsEmbedding"][dim_i]['dims'] = dim + return options + + +def gen_obs_sets_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None): + if paths is not None: + options["obsSets"] = [] + if names is not None: + names = names + else: + names = [] + for obs in paths: + obs_end_path = obs.split('/')[-1] + names += [obs_end_path] + for obs, name in zip(paths, names): + options["obsSets"].append({ + "name": name, + "path": obs + }) + return options + + +def gen_sdata_obs_sets_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None, table_path: Optional[str] = None, region: Optional[str] = None): + if paths is not None: + options["obsSets"] = {"obsSets": []} + if names is not None: + names = names + else: + names = [] + for obs in paths: + obs_end_path = obs.split('/')[-1] + names += [obs_end_path] + for obs, name in zip(paths, names): + options["obsSets"]["obsSets"].append({ + "name": name, + "path": obs + }) + if table_path is not None: + options["obsSets"]["tablePath"] = table_path + if region is not None: + options["obsSets"]["region"] = region + return options + + +def gen_obs_feature_matrix_schema(options: dict, matrix_path: Optional[str] = None, var_filter_path: Optional[str] = None, init_var_filter_path: Optional[str] = None): + if matrix_path is not None: + options["obsFeatureMatrix"] = { + "path": matrix_path + } + if var_filter_path is not None: + options["obsFeatureMatrix"]["featureFilterPath"] = var_filter_path + if init_var_filter_path is not None: + options["obsFeatureMatrix"]["initialFeatureFilterPath"] = init_var_filter_path + return options + + +def gen_obs_labels_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None): + if paths is not None: + if names is not None and len(paths) == len(names): + # A name was provided for each path element, so use those values. + names = names + else: + # Names were not provided for each path element, + # so fall back to using the final part of each path for the names. + names = [labels_path.split('/')[-1] for labels_path in paths] + obs_labels = [] + for path, name in zip(paths, names): + obs_labels.append({"path": path, "obsLabelsType": name}) + options["obsLabels"] = obs_labels + return options + + +def gen_path_schema(key: str, path: Optional[str], options: dict): + if path is not None: + options[key] = { + "path": path + } + return options + + +gen_obs_locations_schema = partial(gen_path_schema, "obsLocations") +gen_obs_segmentations_schema = partial(gen_path_schema, "obsSegmentations") +gen_obs_spots_schema = partial(gen_path_schema, "obsSpots") +gen_obs_points_schema = partial(gen_path_schema, "obsPoints") +gen_feature_labels_schema = partial(gen_path_schema, "featureLabels") + + +def gen_sdata_image_schema(options, path: str, coordinate_system: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None) -> dict: + if path is not None: + options["image"] = { + "path": path + } + if affine_transformation is not None: + options["image"]['coordinateTransformations'] = affine_transformation + if coordinate_system is not None: + options["image"]['coordinateSystem'] = coordinate_system + return options + + +def gen_sdata_labels_schema(options, path: str, table_path: str = "tables/table", coordinate_system: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None) -> dict: + if path is not None: + options["labels"] = { + "path": path + } + if table_path is not None: + options["labels"]['tablePath'] = table_path + if affine_transformation is not None: + options["labels"]['coordinateTransformations'] = affine_transformation + if coordinate_system is not None: + options["labels"]['coordinateSystem'] = coordinate_system + return options + + +def gen_sdata_obs_spots_schema(options: dict, shapes_path: Optional[str] = None, table_path: str = "tables/table", region: Optional[str] = None, coordinate_system: Optional[str] = None) -> dict: + if shapes_path is not None: + options['obsSpots'] = { + "path": shapes_path, + "tablePath": table_path, + "region": region + } + if coordinate_system is not None: + options['obsSpots']['coordinateSystem'] = coordinate_system + return options diff --git a/vitessce/utils.py b/vitessce/utils.py index b83d03fa..fdb31c27 100644 --- a/vitessce/utils.py +++ b/vitessce/utils.py @@ -1,11 +1,3 @@ -from __future__ import annotations - -from functools import partial -from typing import Optional - -import numpy as np - - def get_next_scope_numeric(prev_scopes): next_scope_int = 0 next_scope_str = None @@ -41,112 +33,3 @@ def get_initial_coordination_scope_prefix(dataset_uid, data_type): def get_initial_coordination_scope_name(dataset_uid, data_type, i=None): prefix = get_initial_coordination_scope_prefix(dataset_uid, data_type) return f"{prefix}{0 if i is None else i}" - - -def gen_obs_embedding_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None, dims: Optional[list[list[int]]] = None): - if paths is not None: - if "obsEmbedding" not in options: - options["obsEmbedding"] = [] - if names is not None: - for key, mapping in zip(paths, names): - options["obsEmbedding"].append({ - "path": key, - "dims": [0, 1], - "embeddingType": mapping - }) - else: - for mapping in paths: - mapping_key = mapping.split('/')[-1] - options["obsEmbedding"].append({ - "path": mapping, - "dims": [0, 1], - "embeddingType": mapping_key - }) - if dims is not None: - if "obsEmbedding" not in options: - options["obsEmbedding"] = [] - for dim_i, dim in enumerate(dims): - options["obsEmbedding"][dim_i]['dims'] = dim - return options - - -def gen_obs_sets_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None): - if paths is not None: - options["obsSets"] = [] - if names is not None: - names = names - else: - names = [] - for obs in paths: - obs_end_path = obs.split('/')[-1] - first_letter_capitalized = obs_end_path.capitalize()[0] - names = [first_letter_capitalized + obs_end_path[1:]] - for obs, name in zip(paths, names): - options["obsSets"].append({ - "name": name, - "path": obs - }) - return options - - -def gen_obs_feature_matrix_schema(options: dict, matrix_path: Optional[str] = None, var_filter_path: Optional[str] = None, init_var_filter_path: Optional[str] = None): - if matrix_path is not None: - options["obsFeatureMatrix"] = { - "path": matrix_path - } - if var_filter_path is not None: - options["obsFeatureMatrix"]["featureFilterPath"] = var_filter_path - if init_var_filter_path is not None: - options["obsFeatureMatrix"]["initialFeatureFilterPath"] = init_var_filter_path - return options - - -def gen_obs_labels_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None): - if paths is not None: - if names is not None and len(paths) == len(names): - # A name was provided for each path element, so use those values. - names = names - else: - # Names were not provided for each path element, - # so fall back to using the final part of each path for the names. - names = [labels_path.split('/')[-1] for labels_path in paths] - obs_labels = [] - for path, name in zip(paths, names): - obs_labels.append({"path": path, "obsLabelsType": name}) - options["obsLabels"] = obs_labels - return options - - -def gen_path_schema(key: str, path: Optional[str], options: dict): - if path is not None: - options[key] = { - "path": path - } - return options - - -gen_obs_locations_schema = partial(gen_path_schema, "obsLocations") -gen_obs_segmentations_schema = partial(gen_path_schema, "obsSegmentations") -gen_obs_spots_schema = partial(gen_path_schema, "obsSpots") -gen_obs_points_schema = partial(gen_path_schema, "obsPoints") -gen_feature_labels_schema = partial(gen_path_schema, "featureLabels") - - -def gen_image_schema(options, path: str, affine_transformation: Optional[np.ndarray] = None) -> dict: - if path is not None: - options["image"] = { - "path": path - } - if affine_transformation is not None: - options['coordinateTransformations'] = affine_transformation - return options - - -def gen_obs_spots_from_shapes_schema(options: dict, shapes_path: Optional[str] = None, table_path: str = "tables/table") -> dict: - if shapes_path is not None: - options['obsSpots'] = { - "path": shapes_path, - "tablePath": table_path, - "region": "region" - } - return options diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 6d408955..85c19b25 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from collections import defaultdict import os from os.path import join @@ -13,7 +15,24 @@ import numpy as np from spatialdata import SpatialData -from vitessce.utils import gen_obs_locations_schema, gen_obs_segmentations_schema, gen_obs_spots_from_shapes_schema, gen_obs_spots_schema, gen_obs_points_schema, gen_obs_embedding_schema, gen_feature_labels_schema, gen_image_schema, gen_obs_feature_matrix_schema, gen_obs_labels_schema, gen_obs_sets_schema +if TYPE_CHECKING: + import lamindb as ln + +from vitessce.file_def_utils import ( + gen_obs_locations_schema, + gen_obs_segmentations_schema, + gen_obs_spots_schema, + gen_obs_points_schema, + gen_obs_embedding_schema, + gen_feature_labels_schema, + gen_obs_feature_matrix_schema, + gen_obs_labels_schema, + gen_obs_sets_schema, + gen_sdata_image_schema, + gen_sdata_labels_schema, + gen_sdata_obs_spots_schema, + gen_sdata_obs_sets_schema +) from .constants import ( norm_enum, @@ -1037,15 +1056,33 @@ def image_file_def_creator(base_url): return image_file_def_creator -def raise_error_if_more_than_one_none(inputs): +def raise_error_if_zero_or_more_than_one(inputs): num_inputs = sum([1 for x in inputs if x is not None]) if num_inputs > 1: raise ValueError( - "Expected only one of adata_path, adata_url, or adata_store to be provided" + "Expected only one type of data input parameter to be provided (_url, _path, _store, etc.), but received more than one." ) if num_inputs == 0: raise ValueError( - "Expected one of adata_path, adata_url, or adata_store to be provided" + "Expected one type of data input parameter to be provided (_url, _path, _store, etc.), but received none." + ) + return True + + +def raise_error_if_any(inputs): + num_inputs = sum([1 for x in inputs if x is not None]) + if num_inputs > 0: + raise ValueError( + "Did not expect any of these parameters to be provided, but received one or more: " + str(inputs) + ) + return True + + +def raise_error_if_more_than_one(inputs): + num_inputs = sum([1 for x in inputs if x is not None]) + if num_inputs > 1: + raise ValueError( + "Expected only one of these parameters to be provided, but received more than one: " + str(inputs) ) return True @@ -1103,10 +1140,7 @@ def __init__(self, adata_path=None, adata_url=None, adata_store=None, adata_arti raise ValueError( "Did not expect reference JSON to be provided with adata_store") - num_inputs = sum([1 for x in [adata_path, adata_url, adata_store, adata_artifact] if x is not None]) - if num_inputs != 1: - raise ValueError( - "Expected one of adata_path, adata_url, adata_artifact, or adata_store to be provided") + raise_error_if_zero_or_more_than_one([adata_path, adata_url, adata_store, adata_artifact]) if adata_path is not None: self.is_remote = False @@ -1173,7 +1207,7 @@ def make_routes(self, dataset_uid, obj_i): if self.is_remote: return [] elif self.is_store: - self.register_zarr_store(dataset_uid, obj_i, self._store, self.local_dir_uid) + self.register_zarr_store(dataset_uid, obj_i, self._adata_store, self.local_dir_uid) return [] else: if self.is_h5ad: @@ -1186,9 +1220,9 @@ def make_routes(self, dataset_uid, obj_i): def get_zarr_url(self, base_url="", dataset_uid="", obj_i=""): if self.is_remote: - return self._url + return self._adata_url else: - return self.get_local_dir_url(base_url, dataset_uid, obj_i, self._path, self.local_dir_uid) + return self.get_local_dir_url(base_url, dataset_uid, obj_i, self._adata_path, self.local_dir_uid) def get_h5ad_url(self, base_url="", dataset_uid="", obj_i=""): if self.is_remote: @@ -1210,7 +1244,7 @@ def get_anndata_zarr(base_url): options = gen_obs_spots_schema(self._spatial_spots_obsm, options) options = gen_obs_points_schema(self._spatial_points_obsm, options) options = gen_obs_embedding_schema(options, self._mappings_obsm, self._mappings_obsm_names, self._mappings_obsm_dims) - options = gen_obs_sets_schema(options, self._obs_set_elems, self._obs_set_names) + options = gen_obs_sets_schema(options, self._obs_set_elems, self._obs_set_names,) options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) options = gen_feature_labels_schema(self._feature_labels, options) options = gen_obs_labels_schema(options, self._obs_labels_elems, self._obs_labels_names) @@ -1255,67 +1289,59 @@ def auto_view_config(self, vc): class SpatialDataWrapper(AnnDataWrapper): - def __init__(self, spatialdata_path: Optional[str] = None, spatialdata_url: Optional[str] = None, spatialdata_store: Optional[str] = None, image_elem: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None, shapes_elem: Optional[str] = None, labels_elem: Optional[str] = None, table_path: str = "tables/table", **kwargs): - """_summary_ - - Parameters - ---------- - spatialdata_path : Optional[str], optional - SpatialData path, exclusive with other `{spatialdata,adata}_xxxx` arguments, by default None - spatialdata_url : Optional[str], optional - SpatialData url, exclusive with other `{spatialdata,adata}_xxxx` arguments, by default None - spatialdata_store : Optional[str], optional - SpatialData store, exclusive with other `{spatialdata,adata}_xxxx` arguments, by default None - image_elem : Optional[str], optional - location of the image, by default None - affine_transformation : Optional[np.ndarray], optional - transformation to be applied to the image, by default None - shapes_elem : Optional[str], optional - location of the shapes, by default None - labels_elem : Optional[str], optional - location of the labels, by default None - - Raises - ------ - ValueError - If more than one of `{spatialdata,adata}_xxxx` is not `None` or all are. + def __init__(self, sdata_path: Optional[str] = None, sdata_url: Optional[str] = None, sdata_store: Optional[Union[str, zarr.storage.StoreLike]] = None, sdata_artifact: Optional[ln.Artifact] = None, image_path: Optional[str] = None, region: Optional[str] = None, coordinate_system: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None, spot_shapes_path: Optional[str] = None, labels_path: Optional[str] = None, table_path: str = "tables/table", **kwargs): """ - raise_error_if_more_than_one_none( - [ - spatialdata_path, - kwargs.get('adata_path', None), - spatialdata_url, - kwargs.get('adata_url', None), - spatialdata_store, - kwargs.get('adata_store', None) - ] - ) - super().__init__(adata_path=spatialdata_path, adata_url=spatialdata_url, adata_store=spatialdata_store, **kwargs) - self.local_dir_uid = make_unique_filename(".spatialdata.zarr") # correct? - self._image_elem = image_elem + Wrap a SpatialData object. + + :param sdata_path: SpatialData path, exclusive with other `{sdata,adata}_xxxx` arguments, by default None + :type sdata_path: Optional[str] + :param sdata_url: SpatialData url, exclusive with other `{sdata,adata}_xxxx` arguments, by default None + :type sdata_url: Optional[str] + :param sdata_store: SpatialData store, exclusive with other `{spatialdata,adata}_xxxx` arguments, by default None + :type sdata_store: Optional[Union[str, zarr.storage.StoreLike]] + :param sdata_artifact: Artifact that corresponds to a SpatialData object. + :type sdata_artifact: Optional[ln.Artifact] + :param image_elem: Name of the image element of interest. By default, None. + :type image_elem: Optional[str] + :param coordinate_system: Name of a target coordinate system. + :type coordinate_system: Optional[str] + :param affine_transformation: Transformation to be applied to the image. By default, None. Prefer coordinate_system. + :type affine_transformation: Optional[np.ndarray] + :param shapes_elem: location of the shapes, by default None + :type shapes_elem: Optional[str] + :param labels_elem: location of the labels, by default None + :type labels_elem: Optional[str] + """ + raise_error_if_zero_or_more_than_one([ + sdata_path, + sdata_url, + sdata_store, + sdata_artifact, + ]) + raise_error_if_any([ + kwargs.get('adata_path', None), + kwargs.get('adata_url', None), + kwargs.get('adata_store', None), + kwargs.get('adata_artifact', None) + ]) + super().__init__(adata_path=sdata_path, adata_url=sdata_url, adata_store=sdata_store, adata_artifact=sdata_artifact, **kwargs) + self.local_dir_uid = make_unique_filename(".sdata.zarr") + self._image_path = image_path + self._region = region + self._coordinate_system = coordinate_system self._affine_transformation = affine_transformation self._kwargs = kwargs - self._shapes_elem = shapes_elem - self._labels_elem = labels_elem - if self._path is not None and (self._url is not None): - raise ValueError( - "Did not expect path to be provided with url") - if self._url is None and (self._path is None): - raise ValueError( - "Expected either url or path to be provided") - if self._url is None: - self.is_remote = False + self._spot_shapes_path = spot_shapes_path + self._labels_path = labels_path + if self._adata_path is not None: self.zarr_folder = 'spatialdata.zarr' - else: - self.is_remote = True - self.zarr_folder = None self.obs_type_label = None if self._coordination_values is not None and "obsType" in self._coordination_values: self.obs_type_label = self._coordination_values["obsType"] self._table_path = table_path @classmethod - def from_object(cls: Type[SpatialDataWrapperType], spatialdata: SpatialData, table_keys_to_image_elems: dict[str, Union[str, None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str, None]] = defaultdict(type(None)), obs_type_label: str = "spot") -> list[SpatialDataWrapperType]: + def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_keys_to_image_elems: dict[str, Union[str, None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str, None]] = defaultdict(type(None)), obs_type_label: str = "spot") -> list[SpatialDataWrapperType]: """Instantiate a wrapper for SpatialData stores, one per table, directly from the SpatialData object. By default, we "show everything" that can reasonable be inferred given the information. If you wish to have more control, consider instantiating the object directly. This function will error if something cannot be inferred i.e., the user does not present @@ -1342,9 +1368,9 @@ def from_object(cls: Type[SpatialDataWrapperType], spatialdata: SpatialData, tab ValueError """ wrappers = [] - parent_table_key = "table" if (spatialdata.path / "table").exists() else "tables" - for table_key, table in spatialdata.tables.items(): - shapes_elem = None + parent_table_key = "table" if (sdata.path / "table").exists() else "tables" + for table_key, table in sdata.tables.items(): + spot_shapes_elem = None image_elem = table_keys_to_image_elems[table_key] labels_elem = None spatialdata_attr = table.uns['spatialdata_attrs'] @@ -1357,9 +1383,11 @@ def from_object(cls: Type[SpatialDataWrapperType], spatialdata: SpatialData, tab if len(region) > 1: raise ValueError("Vitessce cannot subset AnnData objects on the fly. Please provide an explicit region") region = region[0] - if region in spatialdata.shapes: - shapes_elem = f"shapes/{region}" - if region in spatialdata.labels: + if region in sdata.shapes: + spot_shapes_elem = f"shapes/{region}" + # Currently, only circle shapes are supported. + # TODO: add if statement to check that this region contains spot shapes rather than other types of shapes + if region in sdata.labels: labels_elem = f"labels/{region}" obs_feature_matrix_elem = f"{parent_table_key}/{table_key}/X" if 'highly_variable' in table.var: @@ -1370,11 +1398,11 @@ def from_object(cls: Type[SpatialDataWrapperType], spatialdata: SpatialData, tab obs_set_elems = [f"{parent_table_key}/{table_key}/obs/{elem}" for elem in table.obs if table.obs[elem].dtype == 'category'] wrappers += [ cls( - spatialdata_path=str(spatialdata.path), - image_elem=str(image_elem) if image_elem is not None else None, + sdata_path=str(sdata.path), + image_path=str(image_elem) if image_elem is not None else None, labels_path=str(labels_elem) if labels_elem is not None else None, obs_feature_matrix_path=str(obs_feature_matrix_elem), - shapes_elem=str(shapes_elem) if shapes_elem is not None else None, + spot_shapes_path=str(spot_shapes_elem) if spot_shapes_elem is not None else None, initial_feature_filter_path=initial_feature_filter_elem, obs_set_paths=obs_set_elems, coordination_values={"obsType": "spot"} # TODO: should we remove? @@ -1387,11 +1415,10 @@ def generator(base_url): options = {} options = gen_obs_labels_schema(options, self._obs_labels_elems, self._obs_labels_names) options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) - options = gen_obs_sets_schema(options, self._obs_set_elems, self._obs_set_names) - if 'obsSets' in options: - options['obsSets'] = {'obsSets': options['obsSets']} # see https://github.com/vitessce/vitessce/blob/cd7e81956786a8130658d6745ff03986e2e6f806/packages/schemas/src/file-def-options.ts#L138-L146 for nested structure - options = gen_obs_spots_from_shapes_schema(options, self._shapes_elem, self._table_path) - options = gen_image_schema(options, self._image_elem, self._affine_transformation) + options = gen_sdata_obs_sets_schema(options, self._obs_set_elems, self._obs_set_names, self._table_path, self._region) + options = gen_sdata_obs_spots_schema(options, self._spot_shapes_path, self._table_path, self._region, self._coordinate_system) + options = gen_sdata_image_schema(options, self._image_path, self._coordinate_system, self._affine_transformation) + options = gen_sdata_labels_schema(options, self._labels_path, self._table_path, self._coordinate_system, self._affine_transformation) options = gen_feature_labels_schema(self._feature_labels, options) if len(options.keys()) > 0: obj_file_def = { From 0b04e15114d231e1bf870910136829e839aeb0b6 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:19:08 -0400 Subject: [PATCH 55/58] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9f918d32..f5b5f17e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vitessce" -version = "3.4.0" +version = "3.4.1" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ] From 0a4a4b0efba3524af3793ce39e8455f82501b7f5 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:12:24 -0400 Subject: [PATCH 56/58] Keller mark/spatial data2 (#375) * Try again * Fix bugs * Lint * Update * Omit * Add test * JS version --- tests/test_wrappers.py | 66 +++++++++++++++++++++++++++++++++++--- vitessce/file_def_utils.py | 21 ++++++++++-- vitessce/widget.py | 6 ++-- vitessce/wrappers.py | 25 ++++++++------- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index 1e86a23c..5ced8d6d 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -394,13 +394,69 @@ def test_multivec_zarr_with_base_dir(self): def test_spatial_data_with_base_dir(self): spatial_data_path = 'test.spatialdata.zarr' - w = SpatialDataWrapper(sdata_path=spatial_data_path, image_path="images/picture", obs_set_paths=['obs/CellType'], obs_set_names=['Cell Type'], obs_embedding_paths=[ - 'obsm/X_umap'], obs_embedding_names=['UMAP']) + w = SpatialDataWrapper( + sdata_path=spatial_data_path, + image_path="images/picture", + obs_set_paths=['obs/CellType'], + obs_set_names=['Cell Type'], + obs_embedding_paths=['obsm/X_umap'], + obs_embedding_names=['UMAP'] + ) w.base_dir = data_path w.local_dir_uid = 'spatialdata.zarr' file_def_creator = w.make_file_def_creator('A', 0) file_def = file_def_creator('http://localhost:8000') - print(file_def) - self.assertEqual(file_def, - {'fileType': 'spatialdata.zarr', 'url': 'http://localhost:8000/test.spatialdata.zarr', 'options': {'obsSets': {'obsSets': [{'name': 'Cell Type', 'path': 'obs/CellType'}], 'tablePath': 'tables/table'}, 'image': {'path': 'images/picture'}}}) + self.assertEqual(file_def, { + 'fileType': 'spatialdata.zarr', + 'url': 'http://localhost:8000/test.spatialdata.zarr', + 'options': { + 'obsSets': { + 'obsSets': [{'name': 'Cell Type', 'path': 'obs/CellType'}], + 'tablePath': 'tables/table' + }, + 'image': {'path': 'images/picture'} + }}) + + def test_spatial_data_with_base_dir_2(self): + spatial_data_path = 'test.spatialdata.zarr' + w = SpatialDataWrapper( + sdata_path=spatial_data_path, + image_path='images/CytAssist_FFPE_Human_Breast_Cancer_full_image', + coordinate_system='aligned', + region='CytAssist_FFPE_Human_Breast_Cancer', + obs_feature_matrix_path='tables/table/X', + obs_spots_path='shapes/CytAssist_FFPE_Human_Breast_Cancer', + table_path='tables/table', + coordination_values={ + "obsType": "spot" + } + ) + w.base_dir = data_path + w.local_dir_uid = 'spatialdata.zarr' + + file_def_creator = w.make_file_def_creator('A', 0) + file_def = file_def_creator('http://localhost:8000') + self.assertDictEqual(file_def, { + 'fileType': 'spatialdata.zarr', + 'url': 'http://localhost:8000/test.spatialdata.zarr', + 'options': { + 'image': { + 'path': 'images/CytAssist_FFPE_Human_Breast_Cancer_full_image', + 'coordinateSystem': 'aligned', + }, + 'obsFeatureMatrix': { + 'path': 'tables/table/X', + 'region': 'CytAssist_FFPE_Human_Breast_Cancer' + }, + 'obsSpots': { + 'path': 'shapes/CytAssist_FFPE_Human_Breast_Cancer', + 'tablePath': 'tables/table', + 'region': 'CytAssist_FFPE_Human_Breast_Cancer', + 'coordinateSystem': 'aligned', + } + }, + 'coordinationValues': { + "obsType": "spot" + } + }) diff --git a/vitessce/file_def_utils.py b/vitessce/file_def_utils.py index ece8508d..4723bc63 100644 --- a/vitessce/file_def_utils.py +++ b/vitessce/file_def_utils.py @@ -142,13 +142,28 @@ def gen_sdata_labels_schema(options, path: str, table_path: str = "tables/table" return options -def gen_sdata_obs_spots_schema(options: dict, shapes_path: Optional[str] = None, table_path: str = "tables/table", region: Optional[str] = None, coordinate_system: Optional[str] = None) -> dict: +def gen_sdata_obs_spots_schema(options: dict, shapes_path: str, table_path: str = "tables/table", region: Optional[str] = None, coordinate_system: Optional[str] = None) -> dict: if shapes_path is not None: options['obsSpots'] = { "path": shapes_path, - "tablePath": table_path, - "region": region + "tablePath": table_path } + if region is not None: + options['obsSpots']['region'] = region if coordinate_system is not None: options['obsSpots']['coordinateSystem'] = coordinate_system return options + + +def gen_sdata_obs_feature_matrix_schema(options: dict, matrix_path: Optional[str] = None, var_filter_path: Optional[str] = None, init_var_filter_path: Optional[str] = None, region: Optional[str] = None): + if matrix_path is not None: + options["obsFeatureMatrix"] = { + "path": matrix_path + } + if region is not None: + options['obsFeatureMatrix']['region'] = region + if var_filter_path is not None: + options["obsFeatureMatrix"]["featureFilterPath"] = var_filter_path + if init_var_filter_path is not None: + options["obsFeatureMatrix"]["initialFeatureFilterPath"] = init_var_filter_path + return options diff --git a/vitessce/widget.py b/vitessce/widget.py index b38da7ea..4b344739 100644 --- a/vitessce/widget.py +++ b/vitessce/widget.py @@ -454,7 +454,7 @@ class VitessceWidget(anywidget.AnyWidget): next_port = DEFAULT_PORT - js_package_version = Unicode('3.4.12').tag(sync=True) + js_package_version = Unicode('3.4.14').tag(sync=True) js_dev_mode = Bool(False).tag(sync=True) custom_js_url = Unicode('').tag(sync=True) plugin_esm = List(trait=Unicode(''), default_value=[]).tag(sync=True) @@ -463,7 +463,7 @@ class VitessceWidget(anywidget.AnyWidget): store_urls = List(trait=Unicode(''), default_value=[]).tag(sync=True) - def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.4.12', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, invoke_timeout=30000): + def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.4.14', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, invoke_timeout=30000): """ Construct a new Vitessce widget. @@ -576,7 +576,7 @@ def _plugin_command(self, params, buffers): # Launch Vitessce using plain HTML representation (no ipywidgets) -def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.4.12', js_dev_mode=False, custom_js_url='', plugin_esm=DEFAULT_PLUGIN_ESM, remount_on_uid_change=True): +def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.4.14', js_dev_mode=False, custom_js_url='', plugin_esm=DEFAULT_PLUGIN_ESM, remount_on_uid_change=True): from IPython.display import display, HTML uid_str = "vitessce" + get_uid_str(uid) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 85c19b25..fda7345f 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -31,7 +31,8 @@ gen_sdata_image_schema, gen_sdata_labels_schema, gen_sdata_obs_spots_schema, - gen_sdata_obs_sets_schema + gen_sdata_obs_sets_schema, + gen_sdata_obs_feature_matrix_schema, ) from .constants import ( @@ -1289,7 +1290,7 @@ def auto_view_config(self, vc): class SpatialDataWrapper(AnnDataWrapper): - def __init__(self, sdata_path: Optional[str] = None, sdata_url: Optional[str] = None, sdata_store: Optional[Union[str, zarr.storage.StoreLike]] = None, sdata_artifact: Optional[ln.Artifact] = None, image_path: Optional[str] = None, region: Optional[str] = None, coordinate_system: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None, spot_shapes_path: Optional[str] = None, labels_path: Optional[str] = None, table_path: str = "tables/table", **kwargs): + def __init__(self, sdata_path: Optional[str] = None, sdata_url: Optional[str] = None, sdata_store: Optional[Union[str, zarr.storage.StoreLike]] = None, sdata_artifact: Optional[ln.Artifact] = None, image_path: Optional[str] = None, region: Optional[str] = None, coordinate_system: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None, obs_spots_path: Optional[str] = None, labels_path: Optional[str] = None, table_path: str = "tables/table", **kwargs): """ Wrap a SpatialData object. @@ -1301,16 +1302,16 @@ def __init__(self, sdata_path: Optional[str] = None, sdata_url: Optional[str] = :type sdata_store: Optional[Union[str, zarr.storage.StoreLike]] :param sdata_artifact: Artifact that corresponds to a SpatialData object. :type sdata_artifact: Optional[ln.Artifact] - :param image_elem: Name of the image element of interest. By default, None. - :type image_elem: Optional[str] + :param image_path: Path to the image element of interest. By default, None. + :type image_path: Optional[str] :param coordinate_system: Name of a target coordinate system. :type coordinate_system: Optional[str] :param affine_transformation: Transformation to be applied to the image. By default, None. Prefer coordinate_system. :type affine_transformation: Optional[np.ndarray] - :param shapes_elem: location of the shapes, by default None - :type shapes_elem: Optional[str] - :param labels_elem: location of the labels, by default None - :type labels_elem: Optional[str] + :param obs_spots_path: Location of shapes that should be interpreted as spot observations, by default None + :type obs_spots_path: Optional[str] + :param labels_path: Location of the labels (segmentation bitmask image), by default None + :type labels_path: Optional[str] """ raise_error_if_zero_or_more_than_one([ sdata_path, @@ -1331,7 +1332,7 @@ def __init__(self, sdata_path: Optional[str] = None, sdata_url: Optional[str] = self._coordinate_system = coordinate_system self._affine_transformation = affine_transformation self._kwargs = kwargs - self._spot_shapes_path = spot_shapes_path + self._obs_spots_path = obs_spots_path self._labels_path = labels_path if self._adata_path is not None: self.zarr_folder = 'spatialdata.zarr' @@ -1402,7 +1403,7 @@ def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_key image_path=str(image_elem) if image_elem is not None else None, labels_path=str(labels_elem) if labels_elem is not None else None, obs_feature_matrix_path=str(obs_feature_matrix_elem), - spot_shapes_path=str(spot_shapes_elem) if spot_shapes_elem is not None else None, + obs_spots_path=str(spot_shapes_elem) if spot_shapes_elem is not None else None, initial_feature_filter_path=initial_feature_filter_elem, obs_set_paths=obs_set_elems, coordination_values={"obsType": "spot"} # TODO: should we remove? @@ -1414,9 +1415,9 @@ def make_file_def_creator(self, dataset_uid: str, obj_i: str) -> Optional[Callab def generator(base_url): options = {} options = gen_obs_labels_schema(options, self._obs_labels_elems, self._obs_labels_names) - options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) + options = gen_sdata_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter, self._region) options = gen_sdata_obs_sets_schema(options, self._obs_set_elems, self._obs_set_names, self._table_path, self._region) - options = gen_sdata_obs_spots_schema(options, self._spot_shapes_path, self._table_path, self._region, self._coordinate_system) + options = gen_sdata_obs_spots_schema(options, self._obs_spots_path, self._table_path, self._region, self._coordinate_system) options = gen_sdata_image_schema(options, self._image_path, self._coordinate_system, self._affine_transformation) options = gen_sdata_labels_schema(options, self._labels_path, self._table_path, self._coordinate_system, self._affine_transformation) options = gen_feature_labels_schema(self._feature_labels, options) From 5da19a8d428663dc95756e899ee40fbab2f4c3ad Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Tue, 15 Oct 2024 09:58:58 -0400 Subject: [PATCH 57/58] SpatialDataWrapper v2 (#373) * (refactor): separate out schema creation * (feat): initial pass at spatial data * (feat): working notebook minus spots (maybe a vitessce bug?) * (fix): try to get spots to appear * (chore): comment out other stuff to make widget show more * Fix visium config * (feat): firstt pass at `from_spatialdata_object` * (refactor): refactor `from_spatialdata_object` and util funcs from static methods * (feat): add url capability * (temp): spatial data fixes * (chore): change variable names * (chore): fix notebooks * (refactor): `path`->`elem` * (chore): more clean ups and fixes * (refactor): small name changes * (refactor): revert public changes * (fix): revert `.ipynb` files * (chore): fix last final args * (fix): name * (fix): remove `request_init` completely and fix `setup.cfg` * (fix): adapt to api * (chore): remove erroneous notebook * (fix): add type support * (chore): add test * (fix): automatic lint * (fix): add default for `obsEmbedding` * (fix): redefinition * (fix): `path` and `embeddingType` swapped * (fix): add annotattions to `utils.py` * (chore): add `spatialdata` dep * (fix): oops, comma for `toml` * (fix): no more python3.8 bc of spatialdata * (fix): remove whitespace * (api): small api cleanups * (fix): args bug * (fix): try spatialdata from master * (chore): lint * (chore): try `spatialdata` update * (fix): `image_elem` usage + table key * (chore): update SpatialData * (fix): `shapes_elem` * (fix): capitalize name of obs set * (fix): use table for spots * (fix): capitalization * (fix): first letter only * (fix): check `self._coordination_values` * (chore): add `obsSets` check * SpatialData changes into original SpatialData branch (#374) * Try again * Fix bugs * Lint * Update * Omit * Update pyproject.toml * Keller mark/spatial data2 (#375) * Try again * Fix bugs * Lint * Update * Omit * Add test * JS version --------- Co-authored-by: ilan-gold --- .coveragerc_omit | 1 + .github/workflows/test.yml | 2 +- docs/notebooks/spatial_data.ipynb | 267 +++++++++++++++++++++++++ pyproject.toml | 10 +- setup.cfg | 15 +- tests/test_wrappers.py | 91 ++++++++- vitessce/__init__.py | 1 + vitessce/config.py | 2 +- vitessce/constants.py | 3 +- vitessce/file_def_utils.py | 169 ++++++++++++++++ vitessce/widget.py | 6 +- vitessce/wrappers.py | 314 ++++++++++++++++++++++-------- 12 files changed, 779 insertions(+), 102 deletions(-) create mode 100644 docs/notebooks/spatial_data.ipynb create mode 100644 vitessce/file_def_utils.py diff --git a/.coveragerc_omit b/.coveragerc_omit index 96a24637..b2036261 100644 --- a/.coveragerc_omit +++ b/.coveragerc_omit @@ -2,6 +2,7 @@ omit = vitessce/config.py vitessce/export.py + vitessce/file_def_utils.py vitessce/routes.py vitessce/widget.py vitessce/wrappers.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d29bbec8..2dddba77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - version: ['3.8', '3.12'] + version: ['3.9', '3.12'] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 diff --git a/docs/notebooks/spatial_data.ipynb b/docs/notebooks/spatial_data.ipynb new file mode 100644 index 00000000..6e14a242 --- /dev/null +++ b/docs/notebooks/spatial_data.ipynb @@ -0,0 +1,267 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "# Vitessce Widget Tutorial" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualization of SpatialData Object" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Import dependencies\n", + "\n", + "We need to import the classes and functions that we will be using from the corresponding packages." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "from pathlib import Path\n", + "from urllib.request import urlretrieve\n", + "import dask\n", + "\n", + "dask.config.set({'dataframe.query-planning-warning': False})\n", + "\n", + "from spatialdata import read_zarr\n", + "import scanpy as sc\n", + "\n", + "from vitessce import (\n", + " VitessceConfig,\n", + " Component as cm,\n", + " CoordinationType as ct,\n", + " CoordinationLevel as CL,\n", + " AbstractWrapper,\n", + " SpatialDataWrapper,\n", + " get_initial_coordination_scope_prefix\n", + ")\n", + "from vitessce.data_utils import (\n", + " optimize_adata,\n", + " VAR_CHUNK_SIZE,\n", + ")\n", + "import zipfile\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "zip_filepath = Path(\"data/visium.spatialdata.zarr.zip\")\n", + "spatialdata_filepath = zip_filepath.with_suffix('')\n", + "if not zip_filepath.exists():\n", + " spatialdata_filepath.parent.mkdir(exist_ok=True)\n", + " urlretrieve('https://s3.embl.de/spatialdata/spatialdata-sandbox/visium_associated_xenium_io.zip', zip_filepath)\n", + "if not spatialdata_filepath.exists():\n", + " with zipfile.ZipFile(zip_filepath,\"r\") as zip_ref:\n", + " zip_ref.extractall(spatialdata_filepath.parent)\n", + " (spatialdata_filepath.parent / \"data.zarr\").rename(spatialdata_filepath)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Load the data\n", + "\n", + "Note: this function may print a `FutureWarning`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "spatialdata = read_zarr(spatialdata_filepath)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "spatialdata" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Create the Vitessce widget configuration\n", + "\n", + "Vitessce needs to know which pieces of data we are interested in visualizing, the visualization types we would like to use, and how we want to coordinate (or link) the views." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.1. Instantiate a `VitessceConfig` object\n", + "\n", + "Use the `VitessceConfig` constructor to create an instance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vc = VitessceConfig(schema_version=\"1.0.16\", name='Visium SpatialData Demo (visium_associated_xenium_io)', description='From https://spatialdata.scverse.org/en/latest/tutorials/notebooks/datasets/README.html')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.2. Add a dataset to the `VitessceConfig` instance\n", + "\n", + "In Vitessce, a dataset is a container for one file per data type. The `.add_dataset(name)` method on the `vc` instance sets up and returns a new dataset instance.\n", + "\n", + "Then, we can call the dataset's `.add_object(wrapper_object)` method to attach a \"data wrapper\" instance to our new dataset. For example, the `AnnDataWrapper` helps to configure AnnData Zarr stores for use in the Vitessce configuration.\n", + "\n", + "Dataset wrapper classes may require additional parameters to resolve ambiguities. For instance, `AnnData` objects may store multiple clusterings or cell type annotation columns in the `adata.obs` dataframe. We can use the parameter `obs_set_paths` to tell Vitessce that certain columns of the `obs` dataframe correspond to cell type annotations or cell clusterings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "[wrapper] = SpatialDataWrapper.from_object(\n", + " spatialdata,\n", + " table_keys_to_image_elems={\"table\": \"images/CytAssist_FFPE_Human_Breast_Cancer_full_image\"},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = vc.add_dataset(name='Breast Cancer Visium').add_object(wrapper)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "spatial = vc.add_view(\"spatialBeta\", dataset=dataset)\n", + "feature_list = vc.add_view(cm.FEATURE_LIST, dataset=dataset)\n", + "layer_controller = vc.add_view(\"layerControllerBeta\", dataset=dataset)\n", + "vc.link_views_by_dict([spatial, layer_controller], {\n", + " 'imageLayer': CL([{\n", + " 'photometricInterpretation': 'RGB',\n", + " }]),\n", + "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"image\"))\n", + "vc.link_views_by_dict([spatial, layer_controller], {\n", + " 'spotLayer': CL([{\n", + " 'obsType': 'spot',\n", + " }]),\n", + "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"obsSpots\"))\n", + "obs_sets = vc.add_view(cm.OBS_SETS, dataset=dataset)\n", + "vc.link_views([spatial, layer_controller, feature_list, obs_sets], ['obsType'], [wrapper.obs_type_label])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.4. Define the visualization layout\n", + "\n", + "The `vc.layout(view_concat)` method allows us to specify how our views will be arranged in the layout grid in the widget. The `|` and `/` characters are magic syntax for `hconcat(v1, v2)` and `vconcat(v1, v2)`, respectively." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vc.layout(spatial | (feature_list / layer_controller / obs_sets));" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "vw = vc.widget()\n", + "vw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vw.close()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vc.to_dict(\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/pyproject.toml b/pyproject.toml index 3ed83ad5..f5b5f17e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta" [project] name = "vitessce" -version = "3.4.0" +version = "3.4.1" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ] description = "Jupyter widget facilitating interactive visualization of spatial single-cell data with Vitessce" readme = "README.md" license = {file = "LICENSE"} -requires-python = ">=3.7" +requires-python = ">=3.9" keywords = ["ipython", "jupyter", "widgets"] classifiers = [ 'Development Status :: 4 - Beta', @@ -19,7 +19,6 @@ classifiers = [ 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'Topic :: Multimedia :: Graphics', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', @@ -34,8 +33,9 @@ dependencies = [ 'black>=21.11b1', 'numpy>=1.21.2,<2.0', 'anndata>=0.7.8,<0.11', + 'spatialdata>=0.2.2', 'scanpy>=1.9.3', - 'ome-zarr==0.8.3', + 'ome-zarr>=0.8.3', 'tifffile>=2020.10.1', 'jsonschema>=3.2', 'tqdm>=4.1.0' @@ -81,7 +81,7 @@ all = [ 'starlette==0.14.0', 'generate-tiff-offsets>=0.1.7', 'kerchunk>=0.2.6', - 'fsspec>=2023.12.2', + 'fsspec', # aiofiles is not explicitly referenced in our code, # but it is an implicit dependency of starlette==0.14.0. diff --git a/setup.cfg b/setup.cfg index e709e527..09602d86 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,11 +8,16 @@ per-file-ignores = vitessce/data_utils/__init__.py: F401 vitessce/widget_plugins/__init__.py: F401 ignore = - E501, # Ignore line too long - W605, # Ignore invalid escape sequence '\*' - W503, # Ignore line break before binary operator: Skim down the left edge to understand intent. - E127 # Ignore continuation line over-indented for visual indent - E128 # Ignore continuation line under-indented for visual indent + # Ignore line too long + E501, + # Ignore invalid escape sequence '\*' + W605, + # Ignore line break before binary operator: Skim down the left edge to understand intent. + W503, + # Ignore continuation line over-indented for visual indent + E127 + # Ignore continuation line under-indented for visual indent + E128 exclude = ./js/node_modules/, ./docs/notebooks/.ipynb_checkpoints/, diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index 3c123fe2..5ced8d6d 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -22,7 +22,7 @@ ObsSegmentationsOmeZarrWrapper, ) -from vitessce.wrappers import file_path_to_url_path +from vitessce.wrappers import SpatialDataWrapper, file_path_to_url_path data_path = Path('tests/data') @@ -235,8 +235,23 @@ def test_anndata_with_base_dir(self): file_def = file_def_creator('http://localhost:8000') self.assertEqual(file_def, {'fileType': 'anndata.zarr', 'url': 'http://localhost:8000/test.h5ad.zarr', 'options': { - 'obsEmbedding': [{'path': 'obsm/X_umap', 'embeddingType': 'UMAP', 'dims': [0, 1]}], - 'obsSets': [{'path': 'obs/CellType', 'name': 'Cell Type'}] + 'obsEmbedding': [{'path': 'obsm/X_umap', 'dims': [0, 1], 'embeddingType': 'UMAP'}], + 'obsSets': [{'name': 'Cell Type', 'path': 'obs/CellType'}] + }}) + + def test_anndata_with_base_dir_no_names(self): + adata_path = 'test.h5ad.zarr' + w = AnnDataWrapper(adata_path, obs_set_paths=['obs/CellType'], obs_embedding_paths=[ + 'obsm/X_umap']) + w.base_dir = data_path + w.local_dir_uid = 'anndata.zarr' + + file_def_creator = w.make_file_def_creator('A', 0) + file_def = file_def_creator('http://localhost:8000') + self.assertEqual(file_def, {'fileType': 'anndata.zarr', 'url': 'http://localhost:8000/test.h5ad.zarr', + 'options': { + 'obsEmbedding': [{'path': 'obsm/X_umap', 'dims': [0, 1], 'embeddingType': 'X_umap'}], + 'obsSets': [{'name': 'CellType', 'path': 'obs/CellType'}] }}) def test_anndata_with_h5ad_and_ref_json(self): @@ -375,3 +390,73 @@ def test_multivec_zarr_with_base_dir(self): 'fileType': 'genomic-profiles.zarr', 'url': 'http://localhost:8000/test_out.snap.multivec.zarr', }) + + def test_spatial_data_with_base_dir(self): + + spatial_data_path = 'test.spatialdata.zarr' + w = SpatialDataWrapper( + sdata_path=spatial_data_path, + image_path="images/picture", + obs_set_paths=['obs/CellType'], + obs_set_names=['Cell Type'], + obs_embedding_paths=['obsm/X_umap'], + obs_embedding_names=['UMAP'] + ) + w.base_dir = data_path + w.local_dir_uid = 'spatialdata.zarr' + + file_def_creator = w.make_file_def_creator('A', 0) + file_def = file_def_creator('http://localhost:8000') + self.assertEqual(file_def, { + 'fileType': 'spatialdata.zarr', + 'url': 'http://localhost:8000/test.spatialdata.zarr', + 'options': { + 'obsSets': { + 'obsSets': [{'name': 'Cell Type', 'path': 'obs/CellType'}], + 'tablePath': 'tables/table' + }, + 'image': {'path': 'images/picture'} + }}) + + def test_spatial_data_with_base_dir_2(self): + spatial_data_path = 'test.spatialdata.zarr' + w = SpatialDataWrapper( + sdata_path=spatial_data_path, + image_path='images/CytAssist_FFPE_Human_Breast_Cancer_full_image', + coordinate_system='aligned', + region='CytAssist_FFPE_Human_Breast_Cancer', + obs_feature_matrix_path='tables/table/X', + obs_spots_path='shapes/CytAssist_FFPE_Human_Breast_Cancer', + table_path='tables/table', + coordination_values={ + "obsType": "spot" + } + ) + w.base_dir = data_path + w.local_dir_uid = 'spatialdata.zarr' + + file_def_creator = w.make_file_def_creator('A', 0) + file_def = file_def_creator('http://localhost:8000') + self.assertDictEqual(file_def, { + 'fileType': 'spatialdata.zarr', + 'url': 'http://localhost:8000/test.spatialdata.zarr', + 'options': { + 'image': { + 'path': 'images/CytAssist_FFPE_Human_Breast_Cancer_full_image', + 'coordinateSystem': 'aligned', + }, + 'obsFeatureMatrix': { + 'path': 'tables/table/X', + 'region': 'CytAssist_FFPE_Human_Breast_Cancer' + }, + 'obsSpots': { + 'path': 'shapes/CytAssist_FFPE_Human_Breast_Cancer', + 'tablePath': 'tables/table', + 'region': 'CytAssist_FFPE_Human_Breast_Cancer', + 'coordinateSystem': 'aligned', + } + }, + 'coordinationValues': { + "obsType": "spot" + } + }) diff --git a/vitessce/__init__.py b/vitessce/__init__.py index 2d3e3b44..00af54d6 100644 --- a/vitessce/__init__.py +++ b/vitessce/__init__.py @@ -53,6 +53,7 @@ ObsSegmentationsOmeTiffWrapper, ImageOmeZarrWrapper, ObsSegmentationsOmeZarrWrapper, + SpatialDataWrapper, ) except ModuleNotFoundError as e: # pragma: no cover warn(f'Extra installs are necessary to use wrappers: {e}') diff --git a/vitessce/config.py b/vitessce/config.py index 2574d706..efb53174 100644 --- a/vitessce/config.py +++ b/vitessce/config.py @@ -1048,7 +1048,7 @@ def add_view(self, view_type, dataset=None, dataset_uid=None, x=0, y=0, w=1, h=1 dataset_uid, str) assert dataset is None or dataset_uid is None component = view_type - assert type(component) in [str, cm] + # assert type(component) in [str, cm] if dataset is None: dataset = self.get_dataset_by_uid(dataset_uid) diff --git a/vitessce/constants.py b/vitessce/constants.py index 4ba48608..2accca3e 100644 --- a/vitessce/constants.py +++ b/vitessce/constants.py @@ -14,7 +14,7 @@ def __new__(cls, value, doc): def norm_enum(enum_val, expected_enum_class=None): - assert isinstance(enum_val, str) or isinstance(enum_val, expected_enum_class) + # assert isinstance(enum_val, str) or isinstance(enum_val, expected_enum_class), f"enum_val was {type(enum_val)} and not a string or expected value {type(expected_enum_class)}" # We don't actually use the expected_enum_class, # since it would not account for things like plugin coordination types, etc. # But we can pass it around anyway and in the future could use @@ -162,6 +162,7 @@ class FileType(DocEnum): An enum type representing the file format or schema to which a file conforms. """ ANNDATA_ZARR = "anndata.zarr", "Joint file type for AnnData objects" + SPATIALDATA_ZARR = "spatialdata.zarr", "Joint file type for SpatialData objects" ANNDATA_H5AD = "anndata.h5ad", "Joint file type for AnnData objects" OBS_EMBEDDING_CSV = 'obsEmbedding.csv', "File type for obsEmbedding values stored in a CSV file" OBS_LOCATIONS_CSV = 'obsLocations.csv', "File type for obsLocations values stored in a CSV file" diff --git a/vitessce/file_def_utils.py b/vitessce/file_def_utils.py new file mode 100644 index 00000000..4723bc63 --- /dev/null +++ b/vitessce/file_def_utils.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +from functools import partial +from typing import Optional + +import numpy as np + + +def gen_obs_embedding_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None, dims: Optional[list[list[int]]] = None): + if paths is not None: + if "obsEmbedding" not in options: + options["obsEmbedding"] = [] + if names is not None: + for key, mapping in zip(paths, names): + options["obsEmbedding"].append({ + "path": key, + "dims": [0, 1], + "embeddingType": mapping + }) + else: + for mapping in paths: + mapping_key = mapping.split('/')[-1] + options["obsEmbedding"].append({ + "path": mapping, + "dims": [0, 1], + "embeddingType": mapping_key + }) + if dims is not None: + if "obsEmbedding" not in options: + options["obsEmbedding"] = [] + for dim_i, dim in enumerate(dims): + options["obsEmbedding"][dim_i]['dims'] = dim + return options + + +def gen_obs_sets_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None): + if paths is not None: + options["obsSets"] = [] + if names is not None: + names = names + else: + names = [] + for obs in paths: + obs_end_path = obs.split('/')[-1] + names += [obs_end_path] + for obs, name in zip(paths, names): + options["obsSets"].append({ + "name": name, + "path": obs + }) + return options + + +def gen_sdata_obs_sets_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None, table_path: Optional[str] = None, region: Optional[str] = None): + if paths is not None: + options["obsSets"] = {"obsSets": []} + if names is not None: + names = names + else: + names = [] + for obs in paths: + obs_end_path = obs.split('/')[-1] + names += [obs_end_path] + for obs, name in zip(paths, names): + options["obsSets"]["obsSets"].append({ + "name": name, + "path": obs + }) + if table_path is not None: + options["obsSets"]["tablePath"] = table_path + if region is not None: + options["obsSets"]["region"] = region + return options + + +def gen_obs_feature_matrix_schema(options: dict, matrix_path: Optional[str] = None, var_filter_path: Optional[str] = None, init_var_filter_path: Optional[str] = None): + if matrix_path is not None: + options["obsFeatureMatrix"] = { + "path": matrix_path + } + if var_filter_path is not None: + options["obsFeatureMatrix"]["featureFilterPath"] = var_filter_path + if init_var_filter_path is not None: + options["obsFeatureMatrix"]["initialFeatureFilterPath"] = init_var_filter_path + return options + + +def gen_obs_labels_schema(options: dict, paths: Optional[list[str]] = None, names: Optional[list[str]] = None): + if paths is not None: + if names is not None and len(paths) == len(names): + # A name was provided for each path element, so use those values. + names = names + else: + # Names were not provided for each path element, + # so fall back to using the final part of each path for the names. + names = [labels_path.split('/')[-1] for labels_path in paths] + obs_labels = [] + for path, name in zip(paths, names): + obs_labels.append({"path": path, "obsLabelsType": name}) + options["obsLabels"] = obs_labels + return options + + +def gen_path_schema(key: str, path: Optional[str], options: dict): + if path is not None: + options[key] = { + "path": path + } + return options + + +gen_obs_locations_schema = partial(gen_path_schema, "obsLocations") +gen_obs_segmentations_schema = partial(gen_path_schema, "obsSegmentations") +gen_obs_spots_schema = partial(gen_path_schema, "obsSpots") +gen_obs_points_schema = partial(gen_path_schema, "obsPoints") +gen_feature_labels_schema = partial(gen_path_schema, "featureLabels") + + +def gen_sdata_image_schema(options, path: str, coordinate_system: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None) -> dict: + if path is not None: + options["image"] = { + "path": path + } + if affine_transformation is not None: + options["image"]['coordinateTransformations'] = affine_transformation + if coordinate_system is not None: + options["image"]['coordinateSystem'] = coordinate_system + return options + + +def gen_sdata_labels_schema(options, path: str, table_path: str = "tables/table", coordinate_system: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None) -> dict: + if path is not None: + options["labels"] = { + "path": path + } + if table_path is not None: + options["labels"]['tablePath'] = table_path + if affine_transformation is not None: + options["labels"]['coordinateTransformations'] = affine_transformation + if coordinate_system is not None: + options["labels"]['coordinateSystem'] = coordinate_system + return options + + +def gen_sdata_obs_spots_schema(options: dict, shapes_path: str, table_path: str = "tables/table", region: Optional[str] = None, coordinate_system: Optional[str] = None) -> dict: + if shapes_path is not None: + options['obsSpots'] = { + "path": shapes_path, + "tablePath": table_path + } + if region is not None: + options['obsSpots']['region'] = region + if coordinate_system is not None: + options['obsSpots']['coordinateSystem'] = coordinate_system + return options + + +def gen_sdata_obs_feature_matrix_schema(options: dict, matrix_path: Optional[str] = None, var_filter_path: Optional[str] = None, init_var_filter_path: Optional[str] = None, region: Optional[str] = None): + if matrix_path is not None: + options["obsFeatureMatrix"] = { + "path": matrix_path + } + if region is not None: + options['obsFeatureMatrix']['region'] = region + if var_filter_path is not None: + options["obsFeatureMatrix"]["featureFilterPath"] = var_filter_path + if init_var_filter_path is not None: + options["obsFeatureMatrix"]["initialFeatureFilterPath"] = init_var_filter_path + return options diff --git a/vitessce/widget.py b/vitessce/widget.py index b38da7ea..4b344739 100644 --- a/vitessce/widget.py +++ b/vitessce/widget.py @@ -454,7 +454,7 @@ class VitessceWidget(anywidget.AnyWidget): next_port = DEFAULT_PORT - js_package_version = Unicode('3.4.12').tag(sync=True) + js_package_version = Unicode('3.4.14').tag(sync=True) js_dev_mode = Bool(False).tag(sync=True) custom_js_url = Unicode('').tag(sync=True) plugin_esm = List(trait=Unicode(''), default_value=[]).tag(sync=True) @@ -463,7 +463,7 @@ class VitessceWidget(anywidget.AnyWidget): store_urls = List(trait=Unicode(''), default_value=[]).tag(sync=True) - def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.4.12', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, invoke_timeout=30000): + def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.4.14', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, invoke_timeout=30000): """ Construct a new Vitessce widget. @@ -576,7 +576,7 @@ def _plugin_command(self, params, buffers): # Launch Vitessce using plain HTML representation (no ipywidgets) -def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.4.12', js_dev_mode=False, custom_js_url='', plugin_esm=DEFAULT_PLUGIN_ESM, remount_on_uid_change=True): +def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.4.14', js_dev_mode=False, custom_js_url='', plugin_esm=DEFAULT_PLUGIN_ESM, remount_on_uid_change=True): from IPython.display import display, HTML uid_str = "vitessce" + get_uid_str(uid) diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 87017f67..fda7345f 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -1,10 +1,40 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from collections import defaultdict import os from os.path import join import tempfile +from typing import Callable, Optional, Type, TypeVar, Union from uuid import uuid4 from pathlib import PurePath, PurePosixPath +import warnings import zarr +import numpy as np +from spatialdata import SpatialData + +if TYPE_CHECKING: + import lamindb as ln + +from vitessce.file_def_utils import ( + gen_obs_locations_schema, + gen_obs_segmentations_schema, + gen_obs_spots_schema, + gen_obs_points_schema, + gen_obs_embedding_schema, + gen_feature_labels_schema, + gen_obs_feature_matrix_schema, + gen_obs_labels_schema, + gen_obs_sets_schema, + gen_sdata_image_schema, + gen_sdata_labels_schema, + gen_sdata_obs_spots_schema, + gen_sdata_obs_sets_schema, + gen_sdata_obs_feature_matrix_schema, +) + from .constants import ( norm_enum, ViewType as cm, @@ -1027,6 +1057,37 @@ def image_file_def_creator(base_url): return image_file_def_creator +def raise_error_if_zero_or_more_than_one(inputs): + num_inputs = sum([1 for x in inputs if x is not None]) + if num_inputs > 1: + raise ValueError( + "Expected only one type of data input parameter to be provided (_url, _path, _store, etc.), but received more than one." + ) + if num_inputs == 0: + raise ValueError( + "Expected one type of data input parameter to be provided (_url, _path, _store, etc.), but received none." + ) + return True + + +def raise_error_if_any(inputs): + num_inputs = sum([1 for x in inputs if x is not None]) + if num_inputs > 0: + raise ValueError( + "Did not expect any of these parameters to be provided, but received one or more: " + str(inputs) + ) + return True + + +def raise_error_if_more_than_one(inputs): + num_inputs = sum([1 for x in inputs if x is not None]) + if num_inputs > 1: + raise ValueError( + "Expected only one of these parameters to be provided, but received more than one: " + str(inputs) + ) + return True + + class AnnDataWrapper(AbstractWrapper): def __init__(self, adata_path=None, adata_url=None, adata_store=None, adata_artifact=None, ref_path=None, ref_url=None, ref_artifact=None, obs_feature_matrix_path=None, feature_filter_path=None, initial_feature_filter_path=None, obs_set_paths=None, obs_set_names=None, obs_locations_path=None, obs_segmentations_path=None, obs_embedding_paths=None, obs_embedding_names=None, obs_embedding_dims=None, obs_spots_path=None, obs_points_path=None, feature_labels_path=None, obs_labels_path=None, convert_to_dense=True, coordination_values=None, obs_labels_paths=None, obs_labels_names=None, **kwargs): """ @@ -1080,10 +1141,7 @@ def __init__(self, adata_path=None, adata_url=None, adata_store=None, adata_arti raise ValueError( "Did not expect reference JSON to be provided with adata_store") - num_inputs = sum([1 for x in [adata_path, adata_url, adata_store, adata_artifact] if x is not None]) - if num_inputs != 1: - raise ValueError( - "Expected one of adata_path, adata_url, adata_artifact, or adata_store to be provided") + raise_error_if_zero_or_more_than_one([adata_path, adata_url, adata_store, adata_artifact]) if adata_path is not None: self.is_remote = False @@ -1111,24 +1169,25 @@ def __init__(self, adata_path=None, adata_url=None, adata_store=None, adata_arti self.local_ref_uid = make_unique_filename(".ref.json") self._expression_matrix = obs_feature_matrix_path - self._cell_set_obs_names = obs_set_names + self._obs_set_names = obs_set_names self._mappings_obsm_names = obs_embedding_names self._gene_var_filter = feature_filter_path self._matrix_gene_var_filter = initial_feature_filter_path - self._cell_set_obs = obs_set_paths + self._obs_set_elems = obs_set_paths self._spatial_centroid_obsm = obs_locations_path self._spatial_polygon_obsm = obs_segmentations_path self._mappings_obsm = obs_embedding_paths self._mappings_obsm_dims = obs_embedding_dims self._spatial_spots_obsm = obs_spots_path self._spatial_points_obsm = obs_points_path - self._gene_alias = feature_labels_path + self._feature_labels = feature_labels_path # Support legacy provision of single obs labels path if (obs_labels_path is not None): - self._obs_labels_paths = [obs_labels_path] + warnings.warn("`obs_labels_path` will be deprecated in a future release.", DeprecationWarning) + self._obs_labels_elems = [obs_labels_path] self._obs_labels_names = [obs_labels_path.split('/')[-1]] else: - self._obs_labels_paths = obs_labels_paths + self._obs_labels_elems = obs_labels_paths self._obs_labels_names = obs_labels_names self._convert_to_dense = convert_to_dense self._coordination_values = coordination_values @@ -1140,12 +1199,12 @@ def convert_and_save(self, dataset_uid, obj_i, base_dir=None): file_def_creator = self.make_file_def_creator( dataset_uid, obj_i) - routes = self.make_anndata_routes(dataset_uid, obj_i) + routes = self.make_routes(dataset_uid, obj_i) self.file_def_creators.append(file_def_creator) self.routes += routes - def make_anndata_routes(self, dataset_uid, obj_i): + def make_routes(self, dataset_uid, obj_i): if self.is_remote: return [] elif self.is_store: @@ -1181,78 +1240,15 @@ def get_ref_url(self, base_url="", dataset_uid="", obj_i=""): def make_file_def_creator(self, dataset_uid, obj_i): def get_anndata_zarr(base_url): options = {} - if self._spatial_centroid_obsm is not None: - options["obsLocations"] = { - "path": self._spatial_centroid_obsm - } - if self._spatial_polygon_obsm is not None: - options["obsSegmentations"] = { - "path": self._spatial_polygon_obsm - } - if self._spatial_spots_obsm is not None: - options["obsSpots"] = { - "path": self._spatial_spots_obsm - } - if self._spatial_points_obsm is not None: - options["obsPoints"] = { - "path": self._spatial_points_obsm - } - if self._mappings_obsm is not None: - options["obsEmbedding"] = [] - if self._mappings_obsm_names is not None: - for key, mapping in zip(self._mappings_obsm_names, self._mappings_obsm): - options["obsEmbedding"].append({ - "path": mapping, - "dims": [0, 1], - "embeddingType": key - }) - else: - for mapping in self._mappings_obsm: - mapping_key = mapping.split('/')[-1] - self._mappings_obsm_names = mapping_key - options["obsEmbedding"].append({ - "path": mapping, - "dims": [0, 1], - "embeddingType": mapping_key - }) - if self._mappings_obsm_dims is not None: - for dim_i, dim in enumerate(self._mappings_obsm_dims): - options["obsEmbedding"][dim_i]['dims'] = dim - if self._cell_set_obs is not None: - options["obsSets"] = [] - if self._cell_set_obs_names is not None: - names = self._cell_set_obs_names - else: - names = [obs.split('/')[-1] for obs in self._cell_set_obs] - for obs, name in zip(self._cell_set_obs, names): - options["obsSets"].append({ - "name": name, - "path": obs - }) - if self._expression_matrix is not None: - options["obsFeatureMatrix"] = { - "path": self._expression_matrix - } - if self._gene_var_filter is not None: - options["obsFeatureMatrix"]["featureFilterPath"] = self._gene_var_filter - if self._matrix_gene_var_filter is not None: - options["obsFeatureMatrix"]["initialFeatureFilterPath"] = self._matrix_gene_var_filter - if self._gene_alias is not None: - options["featureLabels"] = { - "path": self._gene_alias - } - if self._obs_labels_paths is not None: - if self._obs_labels_names is not None and len(self._obs_labels_paths) == len(self._obs_labels_names): - # A name was provided for each path element, so use those values. - names = self._obs_labels_names - else: - # Names were not provided for each path element, - # so fall back to using the final part of each path for the names. - names = [labels_path.split('/')[-1] for labels_path in self._obs_labels_paths] - obs_labels = [] - for path, name in zip(self._obs_labels_paths, names): - obs_labels.append({"path": path, "obsLabelsType": name}) - options["obsLabels"] = obs_labels + options = gen_obs_locations_schema(self._spatial_centroid_obsm, options) + options = gen_obs_segmentations_schema(self._spatial_polygon_obsm, options) + options = gen_obs_spots_schema(self._spatial_spots_obsm, options) + options = gen_obs_points_schema(self._spatial_points_obsm, options) + options = gen_obs_embedding_schema(options, self._mappings_obsm, self._mappings_obsm_names, self._mappings_obsm_dims) + options = gen_obs_sets_schema(options, self._obs_set_elems, self._obs_set_names,) + options = gen_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter) + options = gen_feature_labels_schema(self._feature_labels, options) + options = gen_obs_labels_schema(options, self._obs_labels_elems, self._obs_labels_names) if len(options.keys()) > 0: if self.is_h5ad: @@ -1289,6 +1285,158 @@ def auto_view_config(self, vc): / heatmap) +SpatialDataWrapperType = TypeVar('SpatialDataWrapperType', bound='SpatialDataWrapper') + + +class SpatialDataWrapper(AnnDataWrapper): + + def __init__(self, sdata_path: Optional[str] = None, sdata_url: Optional[str] = None, sdata_store: Optional[Union[str, zarr.storage.StoreLike]] = None, sdata_artifact: Optional[ln.Artifact] = None, image_path: Optional[str] = None, region: Optional[str] = None, coordinate_system: Optional[str] = None, affine_transformation: Optional[np.ndarray] = None, obs_spots_path: Optional[str] = None, labels_path: Optional[str] = None, table_path: str = "tables/table", **kwargs): + """ + Wrap a SpatialData object. + + :param sdata_path: SpatialData path, exclusive with other `{sdata,adata}_xxxx` arguments, by default None + :type sdata_path: Optional[str] + :param sdata_url: SpatialData url, exclusive with other `{sdata,adata}_xxxx` arguments, by default None + :type sdata_url: Optional[str] + :param sdata_store: SpatialData store, exclusive with other `{spatialdata,adata}_xxxx` arguments, by default None + :type sdata_store: Optional[Union[str, zarr.storage.StoreLike]] + :param sdata_artifact: Artifact that corresponds to a SpatialData object. + :type sdata_artifact: Optional[ln.Artifact] + :param image_path: Path to the image element of interest. By default, None. + :type image_path: Optional[str] + :param coordinate_system: Name of a target coordinate system. + :type coordinate_system: Optional[str] + :param affine_transformation: Transformation to be applied to the image. By default, None. Prefer coordinate_system. + :type affine_transformation: Optional[np.ndarray] + :param obs_spots_path: Location of shapes that should be interpreted as spot observations, by default None + :type obs_spots_path: Optional[str] + :param labels_path: Location of the labels (segmentation bitmask image), by default None + :type labels_path: Optional[str] + """ + raise_error_if_zero_or_more_than_one([ + sdata_path, + sdata_url, + sdata_store, + sdata_artifact, + ]) + raise_error_if_any([ + kwargs.get('adata_path', None), + kwargs.get('adata_url', None), + kwargs.get('adata_store', None), + kwargs.get('adata_artifact', None) + ]) + super().__init__(adata_path=sdata_path, adata_url=sdata_url, adata_store=sdata_store, adata_artifact=sdata_artifact, **kwargs) + self.local_dir_uid = make_unique_filename(".sdata.zarr") + self._image_path = image_path + self._region = region + self._coordinate_system = coordinate_system + self._affine_transformation = affine_transformation + self._kwargs = kwargs + self._obs_spots_path = obs_spots_path + self._labels_path = labels_path + if self._adata_path is not None: + self.zarr_folder = 'spatialdata.zarr' + self.obs_type_label = None + if self._coordination_values is not None and "obsType" in self._coordination_values: + self.obs_type_label = self._coordination_values["obsType"] + self._table_path = table_path + + @classmethod + def from_object(cls: Type[SpatialDataWrapperType], sdata: SpatialData, table_keys_to_image_elems: dict[str, Union[str, None]] = defaultdict(type(None)), table_keys_to_regions: dict[str, Union[str, None]] = defaultdict(type(None)), obs_type_label: str = "spot") -> list[SpatialDataWrapperType]: + """Instantiate a wrapper for SpatialData stores, one per table, directly from the SpatialData object. + By default, we "show everything" that can reasonable be inferred given the information. If you wish to have more control, + consider instantiating the object directly. This function will error if something cannot be inferred i.e., the user does not present + regions explicitly but there is more than one for a given table. + + + Parameters + ---------- + cls : Type[SpatialDataWrapperType] + _description_ + spatialdata : SpatialData + _description_ + table_keys_to_image_elems : dict[str, str], optional + which image paths to use for a given table for the visualization, by default None for each table key. + table_keys_to_regions : dict[str, str], optional + which regions to use for a given table for the visualization, by default None for each table key. + + Returns + ------- + list[SpatialDataWrapperType] + + Raises + ------ + ValueError + """ + wrappers = [] + parent_table_key = "table" if (sdata.path / "table").exists() else "tables" + for table_key, table in sdata.tables.items(): + spot_shapes_elem = None + image_elem = table_keys_to_image_elems[table_key] + labels_elem = None + spatialdata_attr = table.uns['spatialdata_attrs'] + region = table_keys_to_regions[table_key] + if region is not None: + assert region in spatialdata_attr['region'] + else: + region = spatialdata_attr['region'] + if isinstance(region, list): + if len(region) > 1: + raise ValueError("Vitessce cannot subset AnnData objects on the fly. Please provide an explicit region") + region = region[0] + if region in sdata.shapes: + spot_shapes_elem = f"shapes/{region}" + # Currently, only circle shapes are supported. + # TODO: add if statement to check that this region contains spot shapes rather than other types of shapes + if region in sdata.labels: + labels_elem = f"labels/{region}" + obs_feature_matrix_elem = f"{parent_table_key}/{table_key}/X" + if 'highly_variable' in table.var: + # TODO: fix first key needing to be "table" in vitessce-js + initial_feature_filter_elem = 'highly_variable' + else: + initial_feature_filter_elem = None + obs_set_elems = [f"{parent_table_key}/{table_key}/obs/{elem}" for elem in table.obs if table.obs[elem].dtype == 'category'] + wrappers += [ + cls( + sdata_path=str(sdata.path), + image_path=str(image_elem) if image_elem is not None else None, + labels_path=str(labels_elem) if labels_elem is not None else None, + obs_feature_matrix_path=str(obs_feature_matrix_elem), + obs_spots_path=str(spot_shapes_elem) if spot_shapes_elem is not None else None, + initial_feature_filter_path=initial_feature_filter_elem, + obs_set_paths=obs_set_elems, + coordination_values={"obsType": "spot"} # TODO: should we remove? + ) + ] + return wrappers + + def make_file_def_creator(self, dataset_uid: str, obj_i: str) -> Optional[Callable]: + def generator(base_url): + options = {} + options = gen_obs_labels_schema(options, self._obs_labels_elems, self._obs_labels_names) + options = gen_sdata_obs_feature_matrix_schema(options, self._expression_matrix, self._gene_var_filter, self._matrix_gene_var_filter, self._region) + options = gen_sdata_obs_sets_schema(options, self._obs_set_elems, self._obs_set_names, self._table_path, self._region) + options = gen_sdata_obs_spots_schema(options, self._obs_spots_path, self._table_path, self._region, self._coordinate_system) + options = gen_sdata_image_schema(options, self._image_path, self._coordinate_system, self._affine_transformation) + options = gen_sdata_labels_schema(options, self._labels_path, self._table_path, self._coordinate_system, self._affine_transformation) + options = gen_feature_labels_schema(self._feature_labels, options) + if len(options.keys()) > 0: + obj_file_def = { + "fileType": ft.SPATIALDATA_ZARR.value, + "url": self.get_zarr_url(base_url, dataset_uid, obj_i), + "options": options + } + if self._request_init is not None: + obj_file_def['requestInit'] = self._request_init + if self._coordination_values is not None: + obj_file_def['coordinationValues'] = self._coordination_values + return obj_file_def + return None + + return generator + + class MultivecZarrWrapper(AbstractWrapper): def __init__(self, zarr_path=None, zarr_url=None, **kwargs): From ffce2a49799f32ede877fb10907e02dd9534ac21 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:05:51 -0400 Subject: [PATCH 58/58] Fix spatialdata nb --- docs/notebooks/spatial_data.ipynb | 122 +++++++----------------------- 1 file changed, 29 insertions(+), 93 deletions(-) diff --git a/docs/notebooks/spatial_data.ipynb b/docs/notebooks/spatial_data.ipynb index 6e14a242..ff0725ef 100644 --- a/docs/notebooks/spatial_data.ipynb +++ b/docs/notebooks/spatial_data.ipynb @@ -80,9 +80,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3. Load the data\n", - "\n", - "Note: this function may print a `FutureWarning`" + "## 2. Load the data" ] }, { @@ -107,69 +105,40 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 4. Create the Vitessce widget configuration\n", + "## 3. Create the Vitessce widget configuration\n", "\n", "Vitessce needs to know which pieces of data we are interested in visualizing, the visualization types we would like to use, and how we want to coordinate (or link) the views." ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4.1. Instantiate a `VitessceConfig` object\n", - "\n", - "Use the `VitessceConfig` constructor to create an instance." - ] - }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "vc = VitessceConfig(schema_version=\"1.0.16\", name='Visium SpatialData Demo (visium_associated_xenium_io)', description='From https://spatialdata.scverse.org/en/latest/tutorials/notebooks/datasets/README.html')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4.2. Add a dataset to the `VitessceConfig` instance\n", - "\n", - "In Vitessce, a dataset is a container for one file per data type. The `.add_dataset(name)` method on the `vc` instance sets up and returns a new dataset instance.\n", - "\n", - "Then, we can call the dataset's `.add_object(wrapper_object)` method to attach a \"data wrapper\" instance to our new dataset. For example, the `AnnDataWrapper` helps to configure AnnData Zarr stores for use in the Vitessce configuration.\n", + "vc = VitessceConfig(\n", + " schema_version=\"1.0.16\",\n", + " name='Visium SpatialData Demo (visium_associated_xenium_io)',\n", + " description='From https://spatialdata.scverse.org/en/latest/tutorials/notebooks/datasets/README.html'\n", + ")\n", + "# Add data to the configuration:\n", + "wrapper = SpatialDataWrapper(\n", + " sdata_path=spatialdata_filepath,\n", + " # The following paths are relative to the root of the SpatialData zarr store on-disk.\n", + " image_path=\"images/CytAssist_FFPE_Human_Breast_Cancer_full_image\",\n", + " table_path=\"table/table\",\n", + " obs_feature_matrix_path=\"table/table/X\",\n", + " obs_spots_path=\"shapes/CytAssist_FFPE_Human_Breast_Cancer\",\n", + " region=\"CytAssist_FFPE_Human_Breast_Cancer\",\n", + " coordinate_system=\"global\",\n", + " coordination_values={\n", + " # The following tells Vitessce to consider each observation as a \"spot\"\n", + " \"obsType\": \"spot\",\n", + " }\n", + ")\n", + "dataset = vc.add_dataset(name='Breast Cancer Visium').add_object(wrapper)\n", "\n", - "Dataset wrapper classes may require additional parameters to resolve ambiguities. For instance, `AnnData` objects may store multiple clusterings or cell type annotation columns in the `adata.obs` dataframe. We can use the parameter `obs_set_paths` to tell Vitessce that certain columns of the `obs` dataframe correspond to cell type annotations or cell clusterings." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "[wrapper] = SpatialDataWrapper.from_object(\n", - " spatialdata,\n", - " table_keys_to_image_elems={\"table\": \"images/CytAssist_FFPE_Human_Breast_Cancer_full_image\"},\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dataset = vc.add_dataset(name='Breast Cancer Visium').add_object(wrapper)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ + "# Add views (visualizations) to the configuration:\n", "spatial = vc.add_view(\"spatialBeta\", dataset=dataset)\n", "feature_list = vc.add_view(cm.FEATURE_LIST, dataset=dataset)\n", "layer_controller = vc.add_view(\"layerControllerBeta\", dataset=dataset)\n", @@ -178,22 +147,18 @@ " 'photometricInterpretation': 'RGB',\n", " }]),\n", "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"image\"))\n", - "vc.link_views_by_dict([spatial, layer_controller], {\n", - " 'spotLayer': CL([{\n", - " 'obsType': 'spot',\n", - " }]),\n", - "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"obsSpots\"))\n", "obs_sets = vc.add_view(cm.OBS_SETS, dataset=dataset)\n", - "vc.link_views([spatial, layer_controller, feature_list, obs_sets], ['obsType'], [wrapper.obs_type_label])" + "vc.link_views([spatial, layer_controller, feature_list, obs_sets], ['obsType'], [wrapper.obs_type_label])\n", + "\n", + "# Layout the views\n", + "vc.layout(spatial | (feature_list / layer_controller / obs_sets));" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 4.4. Define the visualization layout\n", - "\n", - "The `vc.layout(view_concat)` method allows us to specify how our views will be arranged in the layout grid in the widget. The `|` and `/` characters are magic syntax for `hconcat(v1, v2)` and `vconcat(v1, v2)`, respectively." + "### 4. Render the widget" ] }, { @@ -201,40 +166,11 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "vc.layout(spatial | (feature_list / layer_controller / obs_sets));" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": true - }, - "outputs": [], "source": [ "vw = vc.widget()\n", "vw" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vw.close()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vc.to_dict(\"\")" - ] - }, { "cell_type": "code", "execution_count": null,