diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0801eaac..0f580a64 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,6 @@ jobs: with: activate-environment: vitessce-jupyter-dev environment-file: environment.yml - python-version: 3.8 auto-activate-base: false - run: | conda info diff --git a/docs/notebooks/config_to_python.ipynb b/docs/notebooks/config_to_python.ipynb new file mode 100644 index 00000000..b604b97f --- /dev/null +++ b/docs/notebooks/config_to_python.ipynb @@ -0,0 +1,399 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "10f1c457-d0c8-4630-8e49-45df13b9c6d0", + "metadata": {}, + "source": [ + "# Generate Python code to reconstruct a VitessceConfig instance" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "67d5193a", + "metadata": {}, + "outputs": [], + "source": [ + "from vitessce import VitessceConfig, VitessceChainableConfig, VitessceConfigDatasetFile" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "721dacde", + "metadata": {}, + "outputs": [], + "source": [ + "from example_configs import dries as dries_config" + ] + }, + { + "cell_type": "markdown", + "id": "0954be13-6157-4e72-871d-4b1cd6b36ff0", + "metadata": {}, + "source": [ + "## Load a view config from a dict representation" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "61000b61", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "vc = VitessceConfig.from_dict(dries_config)" + ] + }, + { + "cell_type": "markdown", + "id": "0999e895-194e-4890-9cf7-b7c7cc60c9ee", + "metadata": {}, + "source": [ + "## Print to JSON" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f777b428-4d75-487e-b08d-9d66f30fbe9a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"version\": \"1.0.0\",\n", + " \"name\": \"Dries\",\n", + " \"description\": \"Giotto, a pipeline for integrative analysis and visualization of single-cell spatial transcriptomic data\",\n", + " \"datasets\": [\n", + " {\n", + " \"uid\": \"dries-2019\",\n", + " \"name\": \"Dries 2019\",\n", + " \"files\": [\n", + " {\n", + " \"type\": \"cells\",\n", + " \"fileType\": \"cells.json\",\n", + " \"url\": \"https://s3.amazonaws.com/vitessce-data/0.0.31/master_release/dries/dries.cells.json\"\n", + " },\n", + " {\n", + " \"type\": \"cell-sets\",\n", + " \"fileType\": \"cell-sets.json\",\n", + " \"url\": \"https://s3.amazonaws.com/vitessce-data/0.0.31/master_release/dries/dries.cell-sets.json\"\n", + " }\n", + " ]\n", + " }\n", + " ],\n", + " \"coordinationSpace\": {\n", + " \"dataset\": {\n", + " \"A\": \"dries-2019\"\n", + " },\n", + " \"embeddingType\": {\n", + " \"TSNE\": \"t-SNE\",\n", + " \"UMAP\": \"UMAP\"\n", + " },\n", + " \"embeddingZoom\": {\n", + " \"TSNE\": 3,\n", + " \"UMAP\": 3\n", + " },\n", + " \"spatialZoom\": {\n", + " \"A\": -4.4\n", + " },\n", + " \"spatialTargetX\": {\n", + " \"A\": 3800\n", + " },\n", + " \"spatialTargetY\": {\n", + " \"A\": -900\n", + " }\n", + " },\n", + " \"layout\": [\n", + " {\n", + " \"component\": \"description\",\n", + " \"coordinationScopes\": {},\n", + " \"x\": 9,\n", + " \"y\": 0,\n", + " \"w\": 3,\n", + " \"h\": 4,\n", + " \"props\": {\n", + " \"description\": \"Giotto, a pipeline for integrative analysis and visualization of single-cell spatial transcriptomic data\"\n", + " }\n", + " },\n", + " {\n", + " \"component\": \"cellSets\",\n", + " \"coordinationScopes\": {},\n", + " \"x\": 9,\n", + " \"y\": 4,\n", + " \"w\": 3,\n", + " \"h\": 4\n", + " },\n", + " {\n", + " \"component\": \"cellSetSizes\",\n", + " \"coordinationScopes\": {},\n", + " \"x\": 5,\n", + " \"y\": 4,\n", + " \"w\": 4,\n", + " \"h\": 4\n", + " },\n", + " {\n", + " \"component\": \"scatterplot\",\n", + " \"coordinationScopes\": {\n", + " \"embeddingType\": \"TSNE\",\n", + " \"embeddingZoom\": \"TSNE\"\n", + " },\n", + " \"x\": 0,\n", + " \"y\": 2,\n", + " \"w\": 5,\n", + " \"h\": 4\n", + " },\n", + " {\n", + " \"component\": \"spatial\",\n", + " \"coordinationScopes\": {\n", + " \"spatialZoom\": \"A\",\n", + " \"spatialTargetX\": \"A\",\n", + " \"spatialTargetY\": \"A\"\n", + " },\n", + " \"x\": 5,\n", + " \"y\": 0,\n", + " \"w\": 4,\n", + " \"h\": 4,\n", + " \"props\": {\n", + " \"cellRadius\": 50\n", + " }\n", + " },\n", + " {\n", + " \"component\": \"scatterplot\",\n", + " \"coordinationScopes\": {\n", + " \"embeddingType\": \"UMAP\",\n", + " \"embeddingZoom\": \"UMAP\"\n", + " },\n", + " \"x\": 0,\n", + " \"y\": 0,\n", + " \"w\": 5,\n", + " \"h\": 4\n", + " }\n", + " ],\n", + " \"initStrategy\": \"auto\"\n", + "}\n" + ] + } + ], + "source": [ + "import json\n", + "print(json.dumps(vc.to_dict(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "6b51dc66-096c-4c69-a041-62c5bdd523e7", + "metadata": {}, + "source": [ + "## Print to Python\n", + "\n", + "The `vc.to_python` function generates formatted Python code which can be used to re-generate the `vc` instance." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "15ae4ca3-490e-4b99-ae4d-8fafedf60885", + "metadata": {}, + "outputs": [], + "source": [ + "imports, code = vc.to_python()" + ] + }, + { + "cell_type": "markdown", + "id": "619a87f0-9eff-49c0-a092-7859c2a54d16", + "metadata": {}, + "source": [ + "The first value returned is a list of classes used by the code snippet." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "6ec458e3-1970-42ab-876c-cc7407d9cc19", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['VitessceChainableConfig', 'VitessceConfigDatasetFile']" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "imports" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0a458da5-9fe1-45fa-b6d5-a09acb3493d3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "VitessceChainableConfig(\n", + " name=\"Dries\",\n", + " description=\"Giotto, a pipeline for integrative analysis and visualization of single-cell spatial transcriptomic data\",\n", + " schema_version=\"1.0.0\",\n", + ").add_dataset(\n", + " uid=\"dries-2019\",\n", + " name=\"Dries 2019\",\n", + " files=[\n", + " VitessceConfigDatasetFile(\n", + " data_type=\"cells\",\n", + " file_type=\"cells.json\",\n", + " url=\"https://s3.amazonaws.com/vitessce-data/0.0.31/master_release/dries/dries.cells.json\",\n", + " ),\n", + " VitessceConfigDatasetFile(\n", + " data_type=\"cell-sets\",\n", + " file_type=\"cell-sets.json\",\n", + " url=\"https://s3.amazonaws.com/vitessce-data/0.0.31/master_release/dries/dries.cell-sets.json\",\n", + " ),\n", + " ],\n", + ").set_coordination_value(\n", + " c_type=\"embeddingType\", c_scope=\"TSNE\", c_value=\"t-SNE\"\n", + ").set_coordination_value(\n", + " c_type=\"embeddingType\", c_scope=\"UMAP\", c_value=\"UMAP\"\n", + ").set_coordination_value(\n", + " c_type=\"embeddingZoom\", c_scope=\"TSNE\", c_value=3\n", + ").set_coordination_value(\n", + " c_type=\"embeddingZoom\", c_scope=\"UMAP\", c_value=3\n", + ").set_coordination_value(\n", + " c_type=\"spatialZoom\", c_scope=\"A\", c_value=-4.4\n", + ").set_coordination_value(\n", + " c_type=\"spatialTargetX\", c_scope=\"A\", c_value=3800\n", + ").set_coordination_value(\n", + " c_type=\"spatialTargetY\", c_scope=\"A\", c_value=-900\n", + ").add_view(\n", + " dataset_uid=\"dries-2019\",\n", + " component=\"description\",\n", + " x=9,\n", + " y=0,\n", + " w=3,\n", + " h=4,\n", + " props={\n", + " \"description\": \"Giotto, a pipeline for integrative analysis and visualization of single-cell spatial transcriptomic data\"\n", + " },\n", + ").add_view(\n", + " dataset_uid=\"dries-2019\", component=\"cellSets\", x=9, y=4, w=3, h=4\n", + ").add_view(\n", + " dataset_uid=\"dries-2019\", component=\"cellSetSizes\", x=5, y=4, w=4, h=4\n", + ").add_view(\n", + " dataset_uid=\"dries-2019\",\n", + " component=\"scatterplot\",\n", + " x=0,\n", + " y=2,\n", + " w=5,\n", + " h=4,\n", + " coordination_scopes={\"embeddingType\": \"TSNE\", \"embeddingZoom\": \"TSNE\"},\n", + ").add_view(\n", + " dataset_uid=\"dries-2019\",\n", + " component=\"spatial\",\n", + " x=5,\n", + " y=0,\n", + " w=4,\n", + " h=4,\n", + " coordination_scopes={\n", + " \"spatialZoom\": \"A\",\n", + " \"spatialTargetX\": \"A\",\n", + " \"spatialTargetY\": \"A\",\n", + " },\n", + " props={\"cellRadius\": 50},\n", + ").add_view(\n", + " dataset_uid=\"dries-2019\",\n", + " component=\"scatterplot\",\n", + " x=0,\n", + " y=0,\n", + " w=5,\n", + " h=4,\n", + " coordination_scopes={\"embeddingType\": \"UMAP\", \"embeddingZoom\": \"UMAP\"},\n", + ")\n", + "\n" + ] + } + ], + "source": [ + "print(code)" + ] + }, + { + "cell_type": "markdown", + "id": "e47433f0-0b65-4204-becb-ec9b0c59b731", + "metadata": {}, + "source": [ + "The second value is the code snippet. When evaluated, the result will be a new `VitessceConfig` instance." + ] + }, + { + "cell_type": "markdown", + "id": "a82a7938-923c-4947-bdf9-49d1f30134f6", + "metadata": {}, + "source": [ + "## Evaluate the code and render a Vitessce widget" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "94434a43", + "metadata": {}, + "outputs": [], + "source": [ + "reconstructed_vc = eval(code)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "bf0110ff", + "metadata": {}, + "outputs": [], + "source": [ + "#reconstructed_vc.widget()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce75dcdd-4f26-4009-b46b-090c4404f29f", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/notebooks/data_export_s3.ipynb b/docs/notebooks/data_export_s3.ipynb index 8ee653f0..acffc4d8 100644 --- a/docs/notebooks/data_export_s3.ipynb +++ b/docs/notebooks/data_export_s3.ipynb @@ -197,4 +197,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/docs/notebooks/web_app_brain.ipynb b/docs/notebooks/web_app_brain.ipynb index 6b728978..6b2d3167 100644 --- a/docs/notebooks/web_app_brain.ipynb +++ b/docs/notebooks/web_app_brain.ipynb @@ -186,10 +186,10 @@ "metadata": {}, "outputs": [], "source": [ - "scatterplot = vc.add_view(dataset, cm.SCATTERPLOT, mapping=\"UMAP\")\n", - "cell_sets = vc.add_view(dataset, cm.CELL_SETS)\n", - "genes = vc.add_view(dataset, cm.GENES)\n", - "heatmap = vc.add_view(dataset, cm.HEATMAP)" + "scatterplot = vc.add_view(cm.SCATTERPLOT, dataset=dataset, mapping=\"UMAP\")\n", + "cell_sets = vc.add_view(cm.CELL_SETS, dataset=dataset)\n", + "genes = vc.add_view(cm.GENES, dataset=dataset)\n", + "heatmap = vc.add_view(cm.HEATMAP, dataset=dataset)" ] }, { @@ -227,13 +227,6 @@ "source": [ "vc.web_app()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/notebooks/widget_brain.ipynb b/docs/notebooks/widget_brain.ipynb index a174ac97..b94e5746 100644 --- a/docs/notebooks/widget_brain.ipynb +++ b/docs/notebooks/widget_brain.ipynb @@ -186,10 +186,10 @@ "metadata": {}, "outputs": [], "source": [ - "scatterplot = vc.add_view(dataset, cm.SCATTERPLOT, mapping=\"UMAP\")\n", - "cell_sets = vc.add_view(dataset, cm.CELL_SETS)\n", - "genes = vc.add_view(dataset, cm.GENES)\n", - "heatmap = vc.add_view(dataset, cm.HEATMAP)" + "scatterplot = vc.add_view(cm.SCATTERPLOT, dataset=dataset, mapping=\"UMAP\")\n", + "cell_sets = vc.add_view(cm.CELL_SETS, dataset=dataset)\n", + "genes = vc.add_view(cm.GENES, dataset=dataset)\n", + "heatmap = vc.add_view(cm.HEATMAP, dataset=dataset)" ] }, { diff --git a/docs/notebooks/widget_genomic_profiles.ipynb b/docs/notebooks/widget_genomic_profiles.ipynb index f169dbe0..582a29fc 100644 --- a/docs/notebooks/widget_genomic_profiles.ipynb +++ b/docs/notebooks/widget_genomic_profiles.ipynb @@ -102,9 +102,10 @@ "source": [ "vc = VitessceConfig(name='HuBMAP snATAC-seq')\n", "dataset = vc.add_dataset(name='HBM485.TBWH.322').add_object(w)\n", - "genomic_profiles = vc.add_view(dataset, cm.GENOMIC_PROFILES)\n", - "scatter = vc.add_view(dataset, cm.SCATTERPLOT, mapping = \"UMAP\")\n", - "cell_sets = vc.add_view(dataset, cm.CELL_SETS)\n", + "\n", + "genomic_profiles = vc.add_view(cm.GENOMIC_PROFILES, dataset=dataset)\n", + "scatter = vc.add_view(cm.SCATTERPLOT, dataset=dataset, mapping = \"UMAP\")\n", + "cell_sets = vc.add_view(cm.CELL_SETS, dataset=dataset)\n", "\n", "vc.layout(genomic_profiles / (scatter | cell_sets));" ] diff --git a/docs/notebooks/widget_imaging.ipynb b/docs/notebooks/widget_imaging.ipynb index bf47d64b..2cfdf50a 100644 --- a/docs/notebooks/widget_imaging.ipynb +++ b/docs/notebooks/widget_imaging.ipynb @@ -59,9 +59,9 @@ " use_physical_size_scaling=True,\n", " )\n", ")\n", - "spatial = vc.add_view(dataset, cm.SPATIAL)\n", - "status = vc.add_view(dataset, cm.STATUS)\n", - "lc = vc.add_view(dataset, cm.LAYER_CONTROLLER).set_props(disableChannelsIfRgbDetected=True)\n", + "spatial = vc.add_view(cm.SPATIAL, dataset=dataset)\n", + "status = vc.add_view(cm.STATUS, dataset=dataset)\n", + "lc = vc.add_view(cm.LAYER_CONTROLLER, dataset=dataset).set_props(disableChannelsIfRgbDetected=True)\n", "vc.layout(spatial | (lc / status));" ] }, @@ -81,13 +81,6 @@ "vw = vc.widget()\n", "vw" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/notebooks/widget_imaging_segmentation.ipynb b/docs/notebooks/widget_imaging_segmentation.ipynb index 0cf3d2bf..73bc951a 100644 --- a/docs/notebooks/widget_imaging_segmentation.ipynb +++ b/docs/notebooks/widget_imaging_segmentation.ipynb @@ -56,9 +56,9 @@ " ]\n", " )\n", ")\n", - "spatial = vc.add_view(dataset, cm.SPATIAL)\n", - "status = vc.add_view(dataset, cm.STATUS)\n", - "lc = vc.add_view(dataset, cm.LAYER_CONTROLLER)\n", + "spatial = vc.add_view(cm.SPATIAL, dataset=dataset)\n", + "status = vc.add_view(cm.STATUS, dataset=dataset)\n", + "lc = vc.add_view(cm.LAYER_CONTROLLER, dataset=dataset)\n", "vc.layout(spatial | (lc / status));" ] }, diff --git a/docs/notebooks/widget_loom.ipynb b/docs/notebooks/widget_loom.ipynb index f1855369..6b9f74e5 100644 --- a/docs/notebooks/widget_loom.ipynb +++ b/docs/notebooks/widget_loom.ipynb @@ -96,9 +96,11 @@ "vc = VitessceConfig(name='Loom Example', description='osmFISH dataset of the mouse cortex including all cells')\n", "w = AnnDataWrapper(adata, cell_set_obs=[\"ClusterName\"], cell_set_obs_names=[\"Clusters\"], spatial_centroid_obsm=\"spatial\", mappings_obsm=[\"tSNE\"])\n", "dataset = vc.add_dataset(name='SScortex').add_object(w)\n", - "tsne = vc.add_view(dataset, cm.SCATTERPLOT, mapping=\"tSNE\")\n", - "cell_sets = vc.add_view(dataset, cm.CELL_SETS)\n", - "spatial = vc.add_view(dataset, cm.SPATIAL)\n", + "\n", + "tsne = vc.add_view(cm.SCATTERPLOT, dataset=dataset, mapping=\"tSNE\")\n", + "cell_sets = vc.add_view(cm.CELL_SETS, dataset=dataset)\n", + "spatial = vc.add_view(cm.SPATIAL, dataset=dataset)\n", + "\n", "vc.link_views([spatial], [ct.SPATIAL_ZOOM, ct.SPATIAL_TARGET_X, ct.SPATIAL_TARGET_Y], [-6.43, 10417.69, 24885.55])\n", "vc.layout(spatial | (tsne / cell_sets));" ] diff --git a/docs/notebooks/widget_on_colab.ipynb b/docs/notebooks/widget_on_colab.ipynb index 788ce841..230d8259 100644 --- a/docs/notebooks/widget_on_colab.ipynb +++ b/docs/notebooks/widget_on_colab.ipynb @@ -82,9 +82,9 @@ " use_physical_size_scaling=True,\n", " )\n", ")\n", - "spatial = vc.add_view(dataset, cm.SPATIAL)\n", - "status = vc.add_view(dataset, cm.STATUS)\n", - "lc = vc.add_view(dataset, cm.LAYER_CONTROLLER).set_props(disableChannelsIfRgbDetected=True)\n", + "spatial = vc.add_view(cm.SPATIAL, dataset=dataset)\n", + "status = vc.add_view(cm.STATUS, dataset=dataset)\n", + "lc = vc.add_view(cm.LAYER_CONTROLLER, dataset=dataset).set_props(disableChannelsIfRgbDetected=True)\n", "vc.layout(spatial | (lc / status));" ] }, @@ -104,13 +104,6 @@ "vw = vc.widget()\n", "vw" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/notebooks/widget_pbmc.ipynb b/docs/notebooks/widget_pbmc.ipynb index b8fb9194..c7661190 100644 --- a/docs/notebooks/widget_pbmc.ipynb +++ b/docs/notebooks/widget_pbmc.ipynb @@ -100,11 +100,13 @@ "source": [ "vc = VitessceConfig(name='PBMC Reference')\n", "dataset = vc.add_dataset(name='PBMC 3k').add_object(AnnDataWrapper(adata, cell_set_obs=[\"leiden\"], cell_set_obs_names=[\"Leiden\"], mappings_obsm=[\"X_umap\", \"X_pca\"], mappings_obsm_names=[\"UMAP\", \"PCA\"], expression_matrix=\"X\"))\n", - "umap = vc.add_view(dataset, cm.SCATTERPLOT, mapping=\"UMAP\")\n", - "pca = vc.add_view(dataset, cm.SCATTERPLOT, mapping=\"PCA\")\n", - "cell_sets = vc.add_view(dataset, cm.CELL_SETS)\n", - "genes = vc.add_view(dataset, cm.GENES)\n", - "heatmap = vc.add_view(dataset, cm.HEATMAP)\n", + "\n", + "umap = vc.add_view(cm.SCATTERPLOT, dataset=dataset, mapping=\"UMAP\")\n", + "pca = vc.add_view(cm.SCATTERPLOT, dataset=dataset, mapping=\"PCA\")\n", + "cell_sets = vc.add_view(cm.CELL_SETS, dataset=dataset)\n", + "genes = vc.add_view(cm.GENES, dataset=dataset)\n", + "heatmap = vc.add_view(cm.HEATMAP, dataset=dataset)\n", + "\n", "vc.layout((umap / pca) | ((cell_sets | genes) / heatmap));" ] }, diff --git a/docs/notebooks/widget_pbmc_remote.ipynb b/docs/notebooks/widget_pbmc_remote.ipynb index d7800913..2f63e9c5 100644 --- a/docs/notebooks/widget_pbmc_remote.ipynb +++ b/docs/notebooks/widget_pbmc_remote.ipynb @@ -80,11 +80,13 @@ "source": [ "vc = VitessceConfig(name='PBMC Reference')\n", "dataset = vc.add_dataset(name='PBMC 3k').add_object(AnnDataWrapper(adata_url=url, cell_set_obs=[\"louvain\"], cell_set_obs_names=[\"Louvain\"], mappings_obsm=[\"X_umap\", \"X_pca\"], mappings_obsm_names=[\"UMAP\", \"PCA\"], expression_matrix=\"X\"))\n", - "umap = vc.add_view(dataset, cm.SCATTERPLOT, mapping=\"UMAP\")\n", - "pca = vc.add_view(dataset, cm.SCATTERPLOT, mapping=\"PCA\")\n", - "cell_sets = vc.add_view(dataset, cm.CELL_SETS)\n", - "genes = vc.add_view(dataset, cm.GENES)\n", - "heatmap = vc.add_view(dataset, cm.HEATMAP)\n", + "\n", + "umap = vc.add_view(cm.SCATTERPLOT, dataset=dataset, mapping=\"UMAP\")\n", + "pca = vc.add_view(cm.SCATTERPLOT, dataset=dataset, mapping=\"PCA\")\n", + "cell_sets = vc.add_view(cm.CELL_SETS, dataset=dataset)\n", + "genes = vc.add_view(cm.GENES, dataset=dataset)\n", + "heatmap = vc.add_view(cm.HEATMAP, dataset=dataset)\n", + "\n", "vc.layout((umap / pca) | ((cell_sets | genes) / heatmap));" ] }, diff --git a/docs/notebooks/widget_shortcut.ipynb b/docs/notebooks/widget_shortcut.ipynb index fb9502f4..abaf74b5 100644 --- a/docs/notebooks/widget_shortcut.ipynb +++ b/docs/notebooks/widget_shortcut.ipynb @@ -129,13 +129,6 @@ ")).widget(height=800)\n", "vw" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/environment.yml b/environment.yml index 6aa5e740..7c96641f 100644 --- a/environment.yml +++ b/environment.yml @@ -3,7 +3,7 @@ channels: - bioconda - conda-forge dependencies: - - python==3.8 + - python==3.9 - numpy>=1.14.0 - pandas>=1.1.2 - anndata>=0.7.4 diff --git a/setup.py b/setup.py index 3fc2f770..ea3e3a87 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,8 @@ 'scipy>=1.2.1', 'negspy>=0.2.24', 'generate-tiff-offsets>=0.1.7', - 'pandas>=1.1.2' + 'pandas>=1.1.2', + 'black>=21.11b1' ], packages=find_packages(), zip_safe=False, diff --git a/tests/test_config.py b/tests/test_config.py index 3f5590ca..a31fc045 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,15 +1,19 @@ import json import unittest +import ast from vitessce import ( VitessceConfig, + VitessceChainableConfig, + VitessceConfigDatasetFile, CoordinationType as ct, Component as cm, DataType as dt, FileType as ft, hconcat, vconcat, - AbstractWrapper + AbstractWrapper, + make_repr ) class TestConfig(unittest.TestCase): @@ -108,7 +112,7 @@ def test_config_add_spatial_view(self): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - my_view = vc.add_view(my_dataset, cm.SPATIAL) + my_view = vc.add_view(cm.SPATIAL, dataset=my_dataset) vc_dict = vc.to_dict() vc_json = json.dumps(vc_dict) @@ -148,7 +152,7 @@ def test_config_add_scatterplot_view_with_mapping(self): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - my_view = vc.add_view(my_dataset, cm.SCATTERPLOT, mapping="X_umap") + my_view = vc.add_view(cm.SCATTERPLOT, dataset=my_dataset, mapping="X_umap") vc_dict = vc.to_dict() vc_json = json.dumps(vc_dict) @@ -192,7 +196,7 @@ def test_config_add_scatterplot_view_with_embedding_coordinations(self): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - my_view = vc.add_view(my_dataset, cm.SCATTERPLOT) + my_view = vc.add_view(cm.SCATTERPLOT, dataset=my_dataset) et_scope, ez_scope, ex_scope, ey_scope = vc.add_coordination(ct.EMBEDDING_TYPE, ct.EMBEDDING_ZOOM, ct.EMBEDDING_TARGET_X, ct.EMBEDDING_TARGET_Y) my_view.use_coordination(et_scope, ez_scope, ex_scope, ey_scope) @@ -335,7 +339,7 @@ def get_cell_sets(base_url): def test_config_set_layout_single_view(self): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - my_view = vc.add_view(my_dataset, cm.SPATIAL) + my_view = vc.add_view(cm.SPATIAL, dataset=my_dataset) vc.layout(my_view) vc_dict = vc.to_dict() @@ -375,9 +379,9 @@ def test_config_set_layout_single_view(self): def test_config_set_layout_multi_view(self): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) - v2 = vc.add_view(my_dataset, cm.SPATIAL) - v3 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v2 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v3 = vc.add_view(cm.SPATIAL, dataset=my_dataset) vc.layout(hconcat(v1, vconcat(v2, v3))) @@ -438,9 +442,9 @@ def test_config_set_layout_multi_view(self): def test_config_set_layout_multi_view_magic(self): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) - v2 = vc.add_view(my_dataset, cm.SPATIAL) - v3 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v2 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v3 = vc.add_view(cm.SPATIAL, dataset=my_dataset) vc.layout(v1 | (v2 / v3)) @@ -498,7 +502,7 @@ def test_config_set_layout_multi_view_magic(self): "initStrategy": "auto" }) - def test_load_config(self): + def test_config_from_dict(self): vc = VitessceConfig.from_dict({ "version": "1.0.4", "name": "Test name", @@ -596,3 +600,125 @@ def test_load_config(self): "initStrategy": "auto" }) + def test_config_from_dict_raises_error_if_dataset_ambiguous(self): + with self.assertRaises(ValueError): + VitessceConfig.from_dict({ + "version": "1.0.4", + "name": "Test name", + "description": "Test description", + "datasets": [ + { + 'uid': 'A', + 'name': 'My First Dataset', + 'files': [ + { + 'url': 'http://cells-1.json', + 'type': 'cells', + 'fileType': 'cells.json' + } + ] + }, + { + 'uid': 'B', + 'name': 'My Second Dataset', + 'files': [ + { + 'url': 'http://cells-2.json', + 'type': 'cells', + 'fileType': 'cells.json' + } + ] + } + ], + 'coordinationSpace': { + 'dataset': { + 'A': 'A' + }, + 'spatialZoom': { + 'ABC': 11 + }, + }, + "layout": [ + { + "component": "spatial", + "props": { + "cellRadius": 50 + }, + "coordinationScopes": { + "spatialZoom": 'ABC' + }, + "x": 5, + "y": 0, + "w": 4, + "h": 4 + }, + ], + "initStrategy": "auto" + }) + + + def test_config_to_python_with_data_objects(self): + vc = VitessceConfig() + + class MockWrapperA(AbstractWrapper): + def __init__(self, name, **kwargs): + super().__init__(**kwargs) + self._repr = make_repr(locals()) + self.name = name + def convert_and_save(self, dataset_uid, obj_i): + def get_molecules(base_url): + return { + "url": f"{base_url}/molecules", + "type": "molecules", + "fileType": "molecules.json" + } + def get_cells(base_url): + return { + "url": f"{base_url}/cells", + "type": "cells", + "fileType": "cells.json" + } + self.file_def_creators += [get_molecules, get_cells] + + class MockWrapperB(AbstractWrapper): + def __init__(self, name, **kwargs): + super().__init__(**kwargs) + self._repr = make_repr(locals()) + self.name = name + def convert_and_save(self, dataset_uid, obj_i): + def get_cell_sets(base_url): + return { + "url": f"{base_url}/cell-sets", + "type": "cell-sets", + "fileType": "cell-sets.json" + } + self.file_def_creators += [get_cell_sets] + + dataset_a = vc.add_dataset(name='My First Dataset').add_object( + obj=MockWrapperA("Experiment A") + ).add_file( + url="http://example.com/my_cells.json", + file_type=ft.CELLS_JSON, + data_type=dt.CELLS + ) + dataset_b = vc.add_dataset(name='My Second Dataset').add_object( + obj=MockWrapperB("Experiment B") + ) + vc.add_view(cm.SPATIAL, dataset=dataset_a, x=0, y=0, w=3, h=3).set_props(title="My spatial plot") + vc.add_view(cm.SCATTERPLOT, dataset=dataset_b, x=3, y=0, w=3, h=3, mapping="PCA").set_props(title="My scatterplot") + base_url = "http://localhost:8000" + + classes_to_import, code_block = vc.to_python() + self.assertEqual(classes_to_import, ['VitessceChainableConfig', 'VitessceConfigDatasetFile']) + + # Evaluate the code string directly + reconstructed_vc = eval(code_block) + self.assertEqual(vc.to_dict(base_url=base_url), reconstructed_vc.to_dict(base_url=base_url)) + + # Convert code string to an AST and back before evaluation + if hasattr(ast, 'unparse'): + # Unparse added in Python 3.9 + ast_reconstructed_vc = eval(ast.unparse(ast.parse(code_block))) + self.assertEqual(vc.to_dict(base_url=base_url), ast_reconstructed_vc.to_dict(base_url=base_url)) + else: + ast.parse(code_block) \ No newline at end of file diff --git a/vitessce/__init__.py b/vitessce/__init__.py index 617a2409..b8cde02b 100644 --- a/vitessce/__init__.py +++ b/vitessce/__init__.py @@ -3,7 +3,13 @@ from ._version import __version__ from .widget import VitessceWidget -from .config import VitessceConfig, hconcat, vconcat +from .config import ( + VitessceConfig, + VitessceChainableConfig, + VitessceConfigDatasetFile, + hconcat, + vconcat, +) from .constants import CoordinationType, Component, DataType, FileType from .wrappers import ( AbstractWrapper, @@ -21,6 +27,7 @@ export_to_s3, export_to_files, ) +from .repr import make_repr try: if "google.colab" in sys.modules: diff --git a/vitessce/config.py b/vitessce/config.py index e871405b..7b2e24b6 100644 --- a/vitessce/config.py +++ b/vitessce/config.py @@ -1,5 +1,10 @@ +import sys +import inspect import json +import copy as copy_module +import black from uuid import uuid4 +from collections import OrderedDict from .constants import ( CoordinationType as ct, @@ -20,6 +25,7 @@ export_to_s3, export_to_files, ) +from .repr import make_repr, make_params_repr def _get_next_scope(prev_scopes): chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' @@ -54,22 +60,37 @@ 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, url, data_type, file_type, options): + def __init__(self, data_type, file_type, url=None, options=None): """ Not meant to be instantiated directly, but instead created and returned by the ``VitessceConfigDataset.add_file()`` method. - :param str url: A URL to this file. Can be a localhost URL or a remote URL. :param str data_type: A data type. :param str file_type: A file type. - :type options: Extra options to pass to the file loader class. + :param url: A URL to this file. Can be a localhost URL or a remote URL. + :type url: str or None + :param options: Extra options to pass to the file loader class. :type options: dict or list or None """ self.file = { - "url": url, "type": data_type, - "fileType": file_type, - **({ "options": options } if options is not None else {}) + "fileType": file_type } + if url: + self.file["url"] = url + if options: + self.file["options"] = options + + def __repr__(self): + repr_dict = { + "data_type": self.file["type"], + "file_type": self.file["fileType"], + } + if "url" in self.file: + repr_dict["url"] = self.file["url"] + if "options" in self.file: + repr_dict["options"] = self.file["options"] + + return make_repr(repr_dict, class_def=self.__class__) def to_dict(self): return self.file @@ -92,6 +113,21 @@ def __init__(self, uid, name): } self.objs = [] + def _to_py_params(self): + return { + "uid": self.dataset["uid"], + "name": self.dataset["name"] + } + + def get_name(self): + """ + Get the name for this dataset. + + :returns: The name. + :rtype: str + """ + return self.dataset["name"] + def get_uid(self): """ Get the uid value for this dataset. @@ -101,15 +137,16 @@ def get_uid(self): """ return self.dataset["uid"] - def add_file(self, url, data_type, file_type, options=None): + def add_file(self, data_type, file_type, url=None, options=None): """ Add a new file definition to this dataset instance. - :param str url: The URL for the file, pointing to either a local or remote location. :param data_type: The type of data stored in the file. Must be compatible with the specified file type. :type data_type: str or vitessce.constants.DataType :param file_type: The file type. Must be compatible with the specified data type. :type file_type: str or vitessce.constants.FileType + :param url: The URL for the file, pointing to either a local or remote location. + :type url: str or None :type options: Extra options to pass to the file loader class. Optional. :type options: dict or list or None @@ -132,22 +169,26 @@ def add_file(self, url, data_type, file_type, options=None): ) """ - assert type(data_type) == str or type(data_type) == dt - assert type(file_type) == str or type(file_type) == ft + assert isinstance(data_type, str) or isinstance(data_type, dt) + assert isinstance(file_type, str) or isinstance(file_type, ft) # TODO: assert that the file type is compatible with the data type (and vice versa) - if type(data_type) == str: + if isinstance(data_type, str): data_type_str = data_type else: data_type_str = data_type.value - if type(file_type) == str: + if isinstance(file_type, str): file_type_str = file_type else: file_type_str = file_type.value - self.dataset["files"].append(VitessceConfigDatasetFile(url=url, data_type=data_type_str, file_type=file_type_str, options=options)) + self._add_file(VitessceConfigDatasetFile(url=url, data_type=data_type_str, file_type=file_type_str, options=options)) + return self + + def _add_file(self, obj): + self.dataset["files"].append(obj) return self def add_object(self, obj): @@ -163,6 +204,24 @@ def add_object(self, obj): obj.convert_and_save(self.dataset["uid"], len(self.objs)) self.objs.append(obj) return self + + def _get_files(self): + """ + Get a list of files and data objects associated with this dataset. + + :returns: The list of files and datasets. + :rtype: list of VitessceConfigDatasetFile + """ + return self.dataset["files"] + + def _get_objects(self): + """ + Get a list of data objects associated with this dataset. + + :returns: The list of data objects. + :rtype: list of AbstractWrapper instances + """ + return self.objs def to_dict(self, base_url=None): obj_file_defs = [] @@ -214,9 +273,9 @@ def hconcat(*views): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) - v2 = vc.add_view(my_dataset, cm.SPATIAL) - v3 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v2 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v3 = vc.add_view(cm.SPATIAL, dataset=my_dataset) vc.layout(hconcat(v1, vconcat(v2, v3))) """ return VitessceConfigViewHConcat(views) @@ -254,9 +313,9 @@ def vconcat(*views): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) - v2 = vc.add_view(my_dataset, cm.SPATIAL) - v3 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v2 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v3 = vc.add_view(cm.SPATIAL, dataset=my_dataset) vc.layout(hconcat(v1, vconcat(v2, v3))) """ return VitessceConfigViewVConcat(views) @@ -285,6 +344,36 @@ def __init__(self, component, coordination_scopes, x, y, w, h): "h": h } + def _to_py_params(self): + params_dict = { + "component": self.view["component"], + "x": self.view["x"], + "y": self.view["y"], + "w": self.view["w"], + "h": self.view["h"] + } + # Only include coordination_scopes if there are coordination scopes other than + # the coorindation scope for the 'dataset' coordination type. + non_dataset_coordination_scopes = { + c_type: c_scope + for c_type, c_scope in self.view["coordinationScopes"].items() + if c_type != ct.DATASET.value + } + if len(non_dataset_coordination_scopes) > 0: + params_dict["coordination_scopes"] = non_dataset_coordination_scopes + return params_dict + + def get_coordination_scope(self, c_type): + """ + Get the coordination scope name for a particular coordination type. + + :param str c_type: The coordination type of interest. + + :returns: The coordination scope name. + :rtype: str or None + """ + return self.view["coordinationScopes"].get(c_type) + def use_coordination(self, *c_scopes): """ Attach a coordination scope to this view instance. All views using the same coordination scope for a particular coordination type will effectively be linked together. @@ -302,8 +391,8 @@ def use_coordination(self, *c_scopes): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) - v2 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v2 = vc.add_view(cm.SPATIAL, dataset=my_dataset) zoom_scope, x_scope, y_scope = vc.add_coordination( ct.SPATIAL_ZOOM, ct.SPATIAL_TARGET_X, @@ -321,6 +410,17 @@ def use_coordination(self, *c_scopes): return self def set_xywh(self, x, y, w, h): + """ + Set the dimensions for this view. + + :param int x: The horizontal position. + :param int y: The vertical position. + :param int w: The width. + :param int h: The height. + + :returns: Self, to allow chaining. + :rtype: VitessceConfigView + """ self.view["x"] = x self.view["y"] = y self.view["w"] = w @@ -328,15 +428,29 @@ def set_xywh(self, x, y, w, h): return self def set_props(self, **kwargs): - if "props" in self.view.keys(): - self.view["props"] = { - **self.view["props"], - **kwargs - } + """ + Set the props for this view. + + :param \*\*kwargs: A variable number of named props. + + :returns: Self, to allow chaining. + :rtype: VitessceConfigView + """ + if "props" in self.view: + self.view["props"].update(kwargs) else: self.view["props"] = kwargs return self + def get_props(self): + """ + Get the props for this view. + + :returns: The props. + :rtype: dict or None + """ + return self.view.get("props") + def to_dict(self): return self.view @@ -351,16 +465,24 @@ class VitessceConfigCoordinationScope: """ A class to represent a coordination scope in the Vitessce view config coordination space. """ - def __init__(self, c_type, c_scope): + def __init__(self, c_type, c_scope, c_value=None): """ Not meant to be instantiated directly, but instead created and returned by the ``VitessceConfig.add_coordination()`` method. :param str c_type: The coordination type for this coordination scope. :param str c_scope: The coordination scope name. + :param c_value: The value for the coordination scope. Optional. """ self.c_type = c_type self.c_scope = c_scope - self.c_value = None + self.c_value = c_value + + def _to_py_params(self): + return { + "c_type": self.c_type, + "c_scope": self.c_scope, + "c_value": self.c_value, + } def set_value(self, c_value): """ @@ -378,8 +500,8 @@ def set_value(self, c_value): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) - v2 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v2 = vc.add_view(cm.SPATIAL, dataset=my_dataset) zoom_scope, x_scope, y_scope = vc.add_coordination( ct.SPATIAL_ZOOM, ct.SPATIAL_TARGET_X, @@ -399,12 +521,13 @@ class VitessceConfig: A class to represent a Vitessce view config. """ - def __init__(self, name=None, description=None): + def __init__(self, name=None, description=None, schema_version="1.0.4"): """ Construct a Vitessce view config object. :param str name: A name for the view config. Optional. :param str description: A description for the view config. Optional. + :param str schema_version: The view config schema version. .. code-block:: python :emphasize-lines: 3 @@ -414,7 +537,7 @@ def __init__(self, name=None, description=None): vc = VitessceConfig(name='My Config') """ self.config = { - "version": "1.0.4", + "version": schema_version, "name": name, "description": description, "datasets": [], @@ -432,13 +555,23 @@ def __init__(self, name=None, description=None): else: self.config["description"] = description + def _to_py_params(self): + return { + "name": self.config["name"], + "description": self.config["description"], + "schema_version": self.config["version"], + } - def add_dataset(self, name="", uid=None): + def add_dataset(self, name="", uid=None, files=None, objs=None): """ Add a dataset to the config. :param str name: A name for this dataset. - :param str uid: A unique identifier for this dataset. Optional. If None, will be automatically generated. + :param str uid: A unique identifier for this dataset. Optional. If None, one will be automatically generated. + :param files: A list of VitessceConfigDatasetFile instances. optional. + :type files: list or None + :param objs: A list of AbstractWrapper instances. Optional. + :type objs: list or None :returns: The instance for the new dataset. :rtype: VitessceConfigDataset @@ -464,22 +597,19 @@ def add_dataset(self, name="", uid=None): self.config["datasets"].append(vcd) [d_scope] = self.add_coordination(ct.DATASET) d_scope.set_value(uid) - return vcd - - def get_datasets(self): - """ - Get the datasets associated with this configuration. - - :returns: The list of dataset objects. - :rtype: list of VitessceConfigDataset - """ - return self.config["datasets"] - - - def get_dataset(self, uid): + if isinstance(files, list): + for obj in files: + vcd._add_file(obj) + if isinstance(objs, list): + for obj in objs: + vcd.add_object(obj) + + return vcd + + def get_dataset_by_uid(self, uid): """ - Get a dataset associated with this configuration. + Get a dataset associated with this configuration based on its uid. :param str uid: The unique identifier for the dataset of interest. @@ -487,25 +617,54 @@ def get_dataset(self, uid): :rtype: VitessceConfigDataset or None """ for dataset in self.config["datasets"]: - if uid == dataset.get_uid(): + if dataset.get_uid() == uid: return dataset return None + + def get_dataset_by_coordination_scope_name(self, query_scope_name): + """ + Get a dataset associated with this configuration based on a coordination scope. + :param str query_scope_name: The unique identifier for the dataset coordination scope of interest. - def add_view(self, dataset, component, x=0, y=0, w=1, h=1, mapping=None): + :returns: The dataset object. + :rtype: VitessceConfigDataset or None + """ + if ct.DATASET.value in self.config["coordinationSpace"]: + for scope_name, dataset_scope in self.config["coordinationSpace"][ct.DATASET.value].items(): + if scope_name == query_scope_name: + return self.get_dataset_by_uid(dataset_scope.c_value) + return None + + def get_datasets(self): + """ + Get the datasets associated with this configuration. + + :returns: The list of dataset objects. + :rtype: list of VitessceConfigDataset + """ + return self.config["datasets"] + + def add_view(self, component, dataset=None, dataset_uid=None, x=0, y=0, w=1, h=1, mapping=None, coordination_scopes=None, props=None): """ Add a view to the config. - :param dataset: A dataset instance to be used for the data visualized in this view. - :type dataset: VitessceConfigDataset :param component: A component name, either as a string or using the Component enum values. :type component: str or vitessce.constants.Component + :param dataset: A dataset instance to be used for the data visualized in this view. Must provide dataset or dataset_uid, but not both. + :type dataset: VitessceConfigDataset or None + :param dataset_uid: A unique ID for a dataset to be used for the data visualized in this view. Must provide dataset or dataset_uid, but not both. + :type dataset_uid: str or None :param str mapping: An optional convenience parameter for setting the EMBEDDING_TYPE coordination scope value. This parameter is only applicable to the SCATTERPLOT component. :param int x: The horizontal position of the view. Must be an integer between 0 and 11. Optional. This will be ignored if you call the `layout` method of this class using `VitessceConfigViewHConcat` and `VitessceConfigViewVConcat` objects. :param int y: The vertical position of the view. Must be an integer between 0 and 11. Optional. This will be ignored if you call the `layout` method of this class using `VitessceConfigViewHConcat` and `VitessceConfigViewVConcat` objects. :param int w: The width of the view. Must be an integer between 1 and 12. Optional. This will be ignored if you call the `layout` method of this class using `VitessceConfigViewHConcat` and `VitessceConfigViewVConcat` objects. :param int h: The height of the view. Must be an integer between 1 and 12. Optional. This will be ignored if you call the `layout` method of this class using `VitessceConfigViewHConcat` and `VitessceConfigViewVConcat` objects. + :param coordination_scopes: A mapping from coordination types to coordination scope names for this view. + :type coordination_scopes: dict or None + :param props: Props to set for the view using the VitessceConfigView.set_props method. + :type props: dict or None :returns: The instance for the new view. :rtype: VitessceConfigView @@ -517,11 +676,18 @@ def add_view(self, dataset, component, x=0, y=0, w=1, h=1, mapping=None): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) - v2 = vc.add_view(my_dataset, cm.SCATTERPLOT, mapping="X_umap") + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v2 = vc.add_view(cm.SCATTERPLOT, dataset=my_dataset, mapping="X_umap") """ - assert type(dataset) == VitessceConfigDataset - assert type(component) == str or type(component) == cm + # User should only provide dataset or dataset_uid, but not both. + assert isinstance(dataset, VitessceConfigDataset) or isinstance(dataset_uid, str) + assert dataset is None or dataset_uid is None + assert type(component) in [str, cm] + + if dataset is None: + dataset = self.get_dataset_by_uid(dataset_uid) + if dataset is None: + raise ValueError("A dataset with the provided dataset_uid could not be found.") if type(component) == str: component_str = component @@ -540,20 +706,25 @@ def add_view(self, dataset, component, x=0, y=0, w=1, h=1, mapping=None): raise ValueError("No coordination scope matching the dataset parameter could be found in the coordination space.") # Set up the view's dataset coordination scope based on the dataset parameter. - coordination_scopes = { - ct.DATASET.value: dataset_scope, + internal_coordination_scopes = { + ct.DATASET.value: dataset_scope } - vcv = VitessceConfigView(component_str, coordination_scopes, x, y, w, h) + if coordination_scopes is not None: + internal_coordination_scopes.update(coordination_scopes) + vcv = VitessceConfigView(component_str, internal_coordination_scopes, x, y, w, h) # Use the mapping parameter if component is scatterplot and the mapping is not None if mapping is not None: [et_scope] = self.add_coordination(ct.EMBEDDING_TYPE) et_scope.set_value(mapping) vcv.use_coordination(et_scope) + + if isinstance(props, dict): + vcv.set_props(**props) + self.config["layout"].append(vcv) return vcv - def add_coordination(self, *c_types): """ Add scope(s) for new coordination type(s) to the config. @@ -571,8 +742,8 @@ def add_coordination(self, *c_types): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) - v2 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v2 = vc.add_view(cm.SPATIAL, dataset=my_dataset) zoom_scope, x_scope, y_scope = vc.add_coordination( ct.SPATIAL_ZOOM, ct.SPATIAL_TARGET_X, @@ -586,11 +757,11 @@ def add_coordination(self, *c_types): """ result = [] for c_type in c_types: - assert type(c_type) == ct or type(c_type) == str - if type(c_type) == ct: - c_type_str = c_type.value - else: + assert isinstance(c_type, ct) or isinstance(c_type, str) + if isinstance(c_type, str): c_type_str = c_type + else: + c_type_str = c_type.value prev_scopes = list(self.config["coordinationSpace"][c_type_str].keys()) if c_type_str in self.config["coordinationSpace"].keys() else [] scope = VitessceConfigCoordinationScope(c_type_str, _get_next_scope(prev_scopes)) if scope.c_type not in self.config["coordinationSpace"]: @@ -599,6 +770,23 @@ def add_coordination(self, *c_types): result.append(scope) return result + def set_coordination_value(self, c_type, c_scope, c_value): + """ + Set the value for a coordination scope. If a coordination object for the coordination type does not yet exist in the coordination space, it will be created. + + :param str c_type: The coordination type for this coordination scope. + :param str c_scope: The coordination scope name. + :param any c_value: The value for the coordination scope. Optional. + + :returns: The coordination scope instance. + :rtype: VitessceConfigCoordinationScope + """ + scope = VitessceConfigCoordinationScope(c_type, c_scope, c_value) + if scope.c_type not in self.config["coordinationSpace"]: + self.config["coordinationSpace"][scope.c_type] = {} + self.config["coordinationSpace"][scope.c_type][scope.c_scope] = scope + return scope + def link_views(self, views, c_types, c_values = None): """ A convenience function for setting up new coordination scopes across a set of views. @@ -640,9 +828,9 @@ def layout(self, view_concat): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) - v2 = vc.add_view(my_dataset, cm.SPATIAL) - v3 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v2 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v3 = vc.add_view(cm.SPATIAL, dataset=my_dataset) vc.layout(hconcat(v1, vconcat(v2, v3))) .. code-block:: python @@ -652,9 +840,9 @@ def layout(self, view_concat): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) - v2 = vc.add_view(my_dataset, cm.SPATIAL) - v3 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v2 = vc.add_view(cm.SPATIAL, dataset=my_dataset) + v3 = vc.add_view(cm.SPATIAL, dataset=my_dataset) vc.layout(v1 | (v2 / v3)) # * magic * (alternative syntax) """ @@ -726,6 +914,62 @@ def get_routes(self): for d in self.config["datasets"]: routes += d.get_routes() return routes + + def to_python(self): + """ + Convert the VitessceConfig instance to a one-line Python code snippet that can be used to generate it. + + :returns: (A list of classes from the vitessce package used in the code block, The formatted code block) + :rtype: (list[str], str) + """ + classes_to_import = OrderedDict() + classes_to_import[VitessceChainableConfig.__name__] = True + code_block = f'{VitessceChainableConfig.__name__}({make_params_repr(self._to_py_params())})' + + for vcd in self.config["datasets"]: + vcd_file_list_contents = ', '.join([ repr(f) for f in vcd._get_files() ]) + vcd_obj_list_contents = ', '.join([ repr(f) for f in vcd._get_objects() ]) + add_dataset_func = self.add_dataset.__name__ + add_dataset_params_list = [ + make_params_repr(vcd._to_py_params()), + ] + if len(vcd._get_files()) > 0: + add_dataset_params_list.append(f'files=[{vcd_file_list_contents}]') + classes_to_import[VitessceConfigDatasetFile.__name__] = True + if len(vcd._get_objects()) > 0: + add_dataset_params_list.append(f'objs=[{vcd_obj_list_contents}]') + add_dataset_params = ', '.join(add_dataset_params_list) + code_block += f'.{add_dataset_func}({add_dataset_params})' + for obj in vcd._get_objects(): + if "vitessce" in sys.modules and obj.__class__.__name__ in dict(inspect.getmembers(sys.modules["vitessce"])): + classes_to_import[obj.__class__.__name__] = True + for c_type, c_obj in self.config["coordinationSpace"].items(): + if c_type != ct.DATASET.value: + for c_scope_name, c_scope in c_obj.items(): + set_coordination_func = self.set_coordination_value.__name__ + set_coordination_params = make_params_repr(c_scope._to_py_params()) + code_block += f'.{set_coordination_func}({set_coordination_params})' + + for vcv in self.config["layout"]: + dataset_for_view = self.get_dataset_by_coordination_scope_name(vcv.get_coordination_scope(ct.DATASET.value)) + if dataset_for_view is not None: + dataset_uid = dataset_for_view.get_uid() + elif len(self.config["datasets"]) == 1: + # If there is only one dataset available, assume it is the dataset for this view. + dataset_uid = self.config["datasets"][0].get_uid() + else: + raise ValueError("At least one dataset must be present in the config before adding a view.") + add_view_params_dict = { + "dataset_uid": dataset_uid, + } + add_view_params_dict.update(vcv._to_py_params()) + if vcv.get_props() is not None: + add_view_params_dict["props"] = vcv.get_props() + add_view_func = self.add_view.__name__ + add_view_params = make_params_repr(add_view_params_dict) + code_block += f'.{add_view_func}({add_view_params})' + formatted_code_block = black.format_str(code_block, mode=black.FileMode()) + return list(classes_to_import), formatted_code_block @staticmethod def from_dict(config): @@ -744,31 +988,32 @@ def from_dict(config): vc = VitessceConfig.from_dict(my_existing_config) """ - # TODO: Validate the incoming config. - - vc = VitessceConfig(name=config["name"], description=config["description"]) + vc = VitessceConfig(name=config["name"], description=config["description"], schema_version=config["version"]) # Add each dataset from the incoming config. for d in config["datasets"]: new_dataset = vc.add_dataset(uid=d["uid"], name=d["name"]) for f in d["files"]: new_file = new_dataset.add_file( - url=f["url"], + url=f.get("url"), data_type=f["type"], - file_type=f["fileType"] + file_type=f["fileType"], + options=f.get("options") ) - - for c_type in config['coordinationSpace'].keys(): - if c_type != ct.DATASET.value: - c_obj = config['coordinationSpace'][c_type] - vc.config['coordinationSpace'][c_type] = {} - for c_scope_name, c_scope_value in c_obj.items(): - scope = VitessceConfigCoordinationScope(c_type, c_scope_name) - scope.set_value(c_scope_value) - vc.config['coordinationSpace'][c_type][c_scope_name] = scope - + if 'coordinationSpace' in config: + for c_type in config['coordinationSpace'].keys(): + if c_type != ct.DATASET.value: + c_obj = config['coordinationSpace'][c_type] + vc.config['coordinationSpace'][c_type] = {} + for c_scope_name, c_scope_value in c_obj.items(): + scope = VitessceConfigCoordinationScope(c_type, c_scope_name) + scope.set_value(c_scope_value) + vc.config['coordinationSpace'][c_type][c_scope_name] = scope + for c in config['layout']: c_coord_scopes = c['coordinationScopes'] if 'coordinationScopes' in c.keys() else {} + if len(config["datasets"]) > 1 and ct.DATASET.value not in c_coord_scopes: + raise ValueError("Multiple datasets are present, so every view must have an explicit dataset coordination scope.") new_view = VitessceConfigView(c['component'], c_coord_scopes, c['x'], c['y'], c['w'], c['h']) if 'props' in c.keys(): new_view.set_props(**c['props']) @@ -822,7 +1067,7 @@ def widget(self, **kwargs): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) vc.layout(v1) vw = vc.widget() vw @@ -849,7 +1094,7 @@ def web_app(self, **kwargs): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) vc.layout(v1) vc.web_app() """ @@ -871,7 +1116,7 @@ def export(self, to, *args, **kwargs): vc = VitessceConfig() my_dataset = vc.add_dataset(name='My Dataset') - v1 = vc.add_view(my_dataset, cm.SPATIAL) + v1 = vc.add_view(cm.SPATIAL, dataset=my_dataset) vc.layout(v1) config_dict = vc.export(to="S3") @@ -884,3 +1129,74 @@ def export(self, to, *args, **kwargs): raise ValueError("Unknown export destination.") +class VitessceChainableConfig(VitessceConfig): + """ + A class to represent a Vitessce view config, where the methods ``add_dataset``, ``add_view``, and ``set_coordination_value`` return self (the config instance). This class inherits from ``VitessceConfig``. + """ + def __init__(self, **kwargs): + """ + Construct a Vitessce view config object. + + :param \*\*kwargs: Takes the same arguments as the constructor on the ``VitessceConfig`` class. + + .. code-block:: python + :emphasize-lines: 3 + + from vitessce import VitessceChainableConfig + + vc = VitessceChainableConfig(name='My Config') + """ + super().__init__(**kwargs) + + def __copy__(self): + new_vc = VitessceChainableConfig(name=self.config["name"], description=self.config["description"], schema_version=self.config["version"]) + new_vc.config = self.config.copy() + return new_vc + + def add_dataset(self, copy=True, **kwargs): + """ + Add a dataset to this config. + + :param \*\*kwargs: Takes the same arguments as the ``add_dataset`` method on the ``VitessceConfig`` class. + + :returns: The config instance. + :rtype: VitessceChainableConfig + """ + if copy: + new_vc = copy_module.copy(self) + return new_vc.add_dataset(copy=False, **kwargs) + super().add_dataset(**kwargs) + return self + + def add_view(self, component, copy=True, **kwargs): + """ + Add a view to this config. + + :param component: Takes the same arguments as the ``add_view`` method on the ``VitessceConfig`` class. + :param \*\*kwargs: Takes the same arguments as the ``add_view`` method on the ``VitessceConfig`` class. + + :returns: The config instance. + :rtype: VitessceChainableConfig + """ + if copy: + new_vc = copy_module.copy(self) + return new_vc.add_view(component, copy=False, **kwargs) + super().add_view(component, **kwargs) + return self + + def set_coordination_value(self, c_type, c_scope, c_value, copy=True): + """ + Add a coordination value to this config. + + :param c_type: Takes the same arguments as the ``set_coordination_value`` method on the ``VitessceConfig`` class. + :param c_scope: Takes the same arguments as the ``set_coordination_value`` method on the ``VitessceConfig`` class. + :param c_value: Takes the same arguments as the ``set_coordination_value`` method on the ``VitessceConfig`` class. + + :returns: The config instance. + :rtype: VitessceChainableConfig + """ + if copy: + new_vc = copy_module.copy(self) + return new_vc.set_coordination_value(c_type, c_scope, c_value, copy=False) + super().set_coordination_value(c_type, c_scope, c_value) + return self \ No newline at end of file diff --git a/vitessce/repr.py b/vitessce/repr.py new file mode 100644 index 00000000..d1b8f27d --- /dev/null +++ b/vitessce/repr.py @@ -0,0 +1,59 @@ +import inspect + +def make_repr(init_locals, class_def=None): + ''' + >>> from .wrappers import MultiImageWrapper + >>> orig = MultiImageWrapper('IMAGE_WRAPPERS', foo='bar') + >>> orig_repr = repr(orig) + >>> print(orig_repr) + MultiImageWrapper(image_wrappers='IMAGE_WRAPPERS', foo='bar') + >>> evalled = eval(orig_repr) + >>> assert orig_repr == repr(evalled) + ''' + # Get the class definition from locals. + clazz = None + if '__class__' in init_locals: + clazz = init_locals.pop('__class__') # Requires superclass to be initialized. + elif 'self' in init_locals and hasattr(init_locals['self'], '__class__'): + clazz = init_locals["self"].__class__ + elif class_def is not None: + clazz = class_def + else: + raise ValueError("make_repr could not locate the class definition") + + # Remove self from locals. + if 'self' in init_locals: + del init_locals['self'] + + # Get the class name. + class_name = clazz.__name__ + + # Remove redundant constructor parameters (when the value equals the default value). + for k, v in inspect.signature(clazz).parameters.items(): + try: + if k in init_locals and init_locals[k] == v.default: + del init_locals[k] + except: + # Equality comparison may not be implemented for the value object. + pass + + # Convert the kwargs dict to named args. + if 'kwargs' in init_locals: + kwargs = init_locals.pop('kwargs') + else: + kwargs = {} + + args = { + **init_locals, + **kwargs + } + params = ', '.join([f'{k}={repr(v)}' for k, v in args.items()]) + return f'{class_name}({params})' + +def make_params_repr(args): + ''' + >>> print(make_params_repr({ "uid": 1, "name": "My Dataset"})) + uid=1, name='My Dataset' + ''' + params = ', '.join([f'{k}={repr(v)}' for k, v in args.items()]) + return params \ No newline at end of file diff --git a/vitessce/wrappers.py b/vitessce/wrappers.py index 547b0963..de7d1df4 100644 --- a/vitessce/wrappers.py +++ b/vitessce/wrappers.py @@ -26,6 +26,7 @@ ) from .entities import Cells, CellSets, GenomicProfiles from .routes import range_repsonse +from .repr import make_repr VAR_CHUNK_SIZE = 10 @@ -39,11 +40,6 @@ class AbstractWrapper: """ An abstract class that can be extended when implementing custom dataset object wrapper classes. - - TODO: Add some useful tests. - - >>> assert True - """ def __init__(self, **kwargs): @@ -57,6 +53,9 @@ def __init__(self, **kwargs): self.is_remote = False self.file_def_creators = [] + def __repr__(self): + return self._repr + def convert_and_save(self, dataset_uid, obj_i): """ Fill in the file_def_creators array. @@ -132,6 +131,7 @@ def auto_view_config(self, vc): """ raise NotImplementedError("Auto view configuration has not yet been implemented for this data object wrapper class.") + class MultiImageWrapper(AbstractWrapper): """ Wrap multiple imaging datasets by creating an instance of the ``MultiImageWrapper`` class. @@ -141,6 +141,7 @@ class MultiImageWrapper(AbstractWrapper): """ def __init__(self, image_wrappers, use_physical_size_scaling=False, **kwargs): super().__init__(**kwargs) + self._repr = make_repr(locals()) self.image_wrappers = image_wrappers self.use_physical_size_scaling = use_physical_size_scaling @@ -198,6 +199,7 @@ class OmeTiffWrapper(AbstractWrapper): def __init__(self, img_path=None, offsets_path=None, img_url=None, offsets_url=None, name="", transformation_matrix=None, is_bitmask=False, **kwargs): super().__init__(**kwargs) + self._repr = make_repr(locals()) self.name = name self._img_path = img_path self._img_url = img_url @@ -395,6 +397,7 @@ def __init__(self, adata=None, adata_url=None, expression_matrix=None, matrix_ge :param \\*\\*kwargs: Keyword arguments inherited from :class:`~vitessce.wrappers.AbstractWrapper` """ super().__init__(**kwargs) + self._repr = make_repr(locals()) self._adata = adata self._adata_url = adata_url if adata is not None: @@ -541,12 +544,12 @@ def get_expression_matrix(base_url): def auto_view_config(self, vc): dataset = vc.add_dataset().add_object(self) mapping_name = self._mappings_obsm_names[0] if (self._mappings_obsm_names is not None) else self._mappings_obsm[0].split('/')[-1] - scatterplot = vc.add_view(dataset, cm.SCATTERPLOT, mapping=mapping_name) - cell_sets = vc.add_view(dataset, cm.CELL_SETS) - genes = vc.add_view(dataset, cm.GENES) - heatmap = vc.add_view(dataset, cm.HEATMAP) + scatterplot = vc.add_view(cm.SCATTERPLOT, dataset=dataset, mapping=mapping_name) + cell_sets = vc.add_view(cm.CELL_SETS, dataset=dataset) + genes = vc.add_view(cm.GENES, dataset=dataset) + heatmap = vc.add_view(cm.HEATMAP, dataset=dataset) if self._spatial_polygon_obsm is not None or self._spatial_centroid_obsm is not None: - spatial = vc.add_view(dataset, cm.SPATIAL) + spatial = vc.add_view(cm.SPATIAL, dataset=dataset) vc.layout((scatterplot | spatial) / (heatmap | (cell_sets / genes))) else: vc.layout((scatterplot | (cell_sets / genes)) / heatmap) @@ -561,6 +564,7 @@ class SnapWrapper(AbstractWrapper): def __init__(self, in_mtx, in_barcodes_df, in_bins_df, in_clusters_df, starting_resolution=5000, **kwargs): super().__init__(**kwargs) + self._repr = make_repr(locals()) self.in_mtx = in_mtx # scipy.sparse.coo.coo_matrix (filtered_cell_by_bin.mtx) self.in_barcodes_df = in_barcodes_df # pandas dataframe (barcodes.txt) self.in_bins_df = in_bins_df # pandas dataframe (bins.txt) @@ -797,8 +801,8 @@ def get_cells(base_url): def auto_view_config(self, vc): dataset = vc.add_dataset().add_object(self) - genomic_profiles = vc.add_view(dataset, cm.GENOMIC_PROFILES) - scatter = vc.add_view(dataset, cm.SCATTERPLOT, mapping = "UMAP") - cell_sets = vc.add_view(dataset, cm.CELL_SETS) + genomic_profiles = vc.add_view(cm.GENOMIC_PROFILES, dataset=dataset) + scatter = vc.add_view(cm.SCATTERPLOT, dataset=dataset, mapping="UMAP") + cell_sets = vc.add_view(cm.CELL_SETS, dataset=dataset) vc.layout(genomic_profiles / (scatter | cell_sets))