plexora 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plexora/__init__.py +129 -0
- plexora/__main__.py +15 -0
- plexora/_url.py +105 -0
- plexora/api/__init__.py +55 -0
- plexora/api/dataset.py +398 -0
- plexora/api/http.py +19 -0
- plexora/api/plugin.py +541 -0
- plexora/api/store.py +221 -0
- plexora/cli.py +777 -0
- plexora/client/dist/354_bundle.js +2 -0
- plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
- plexora/client/dist/418_bundle.js +2 -0
- plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
- plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
- plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
- plexora/client/dist/770_bundle.js +1 -0
- plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
- plexora/client/dist/vendor_bundle.js +2 -0
- plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
- plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
- plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
- plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
- plexora/client/src/css/import.css +474 -0
- plexora/client/src/css/main.css +903 -0
- plexora/client/src/css/openProject.css +363 -0
- plexora/client/src/css/quickView.css +166 -0
- plexora/client/src/css/tokens.css +51 -0
- plexora/client/src/css/viewer.css +1655 -0
- plexora/client/src/img/apple-touch-icon.png +0 -0
- plexora/client/src/img/favicon.ico +0 -0
- plexora/client/src/img/logo.ai +8892 -33
- plexora/client/src/img/logo.svg +1 -0
- plexora/client/src/img/logo_with_text.ai +9011 -34
- plexora/client/src/img/logo_with_text.svg +1 -0
- plexora/client/src/js/main.js +704 -0
- plexora/client/src/js/pluginRegistry.js +108 -0
- plexora/client/src/js/services/appStatus.js +286 -0
- plexora/client/src/js/services/browsePicker.js +73 -0
- plexora/client/src/js/services/dataLayer.js +400 -0
- plexora/client/src/js/services/datasetContext.js +143 -0
- plexora/client/src/js/services/glRenderer.js +143 -0
- plexora/client/src/js/services/importFormValidation.js +257 -0
- plexora/client/src/js/services/numericData.js +100 -0
- plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
- plexora/client/src/js/services/simpleEventHandler.js +25 -0
- plexora/client/src/js/vendor.js +29 -0
- plexora/client/src/js/views/channelList.js +663 -0
- plexora/client/src/js/views/colorSwatchPicker.js +202 -0
- plexora/client/src/js/views/columnClassifier.js +146 -0
- plexora/client/src/js/views/coordinateField.js +133 -0
- plexora/client/src/js/views/dataSourceField.js +276 -0
- plexora/client/src/js/views/imageViewer.js +3306 -0
- plexora/client/src/js/views/miniMap.js +787 -0
- plexora/client/src/js/views/navbarControls.js +178 -0
- plexora/client/src/js/views/openProjectPage.js +239 -0
- plexora/client/src/js/views/projectEdit.js +335 -0
- plexora/client/src/js/views/quickViewLanding.js +136 -0
- plexora/client/src/js/views/rainbow.js +326 -0
- plexora/client/src/js/views/requirementsModal.js +572 -0
- plexora/client/src/js/views/rgbImageViewer.js +115 -0
- plexora/client/src/js/views/searchableSelect.js +411 -0
- plexora/client/src/js/views/segmentationProgress.js +122 -0
- plexora/client/src/js/views/toolLoader.js +747 -0
- plexora/client/src/js/views/viewerControls.js +689 -0
- plexora/client/src/js/views/viewerManager.js +434 -0
- plexora/client/src/js/views/viewerSidebar.js +1117 -0
- plexora/client/src/js/workers/tileDecoder.js +130 -0
- plexora/client/src/shaders/frag.glsl +528 -0
- plexora/client/src/shaders/vert.glsl +12 -0
- plexora/client/templates/base.html +212 -0
- plexora/client/templates/index.html +238 -0
- plexora/client/templates/open_project.html +96 -0
- plexora/client/templates/project_columns.html +86 -0
- plexora/client/templates/project_edit.html +196 -0
- plexora/client/templates/upload.html +118 -0
- plexora/connect.py +528 -0
- plexora/datasource.py +782 -0
- plexora/jupyter.py +406 -0
- plexora/notebook_env.py +232 -0
- plexora/paths.py +486 -0
- plexora/plugins/__init__.py +12 -0
- plexora/plugins/cell_explorer/__init__.py +100 -0
- plexora/plugins/cell_explorer/server/__init__.py +11 -0
- plexora/plugins/cell_explorer/server/routes.py +167 -0
- plexora/plugins/cell_explorer/server/state.py +281 -0
- plexora/plugins/cell_explorer/server/values.py +145 -0
- plexora/plugins/cell_explorer/server/variables.py +367 -0
- plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
- plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
- plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
- plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
- plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
- plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
- plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
- plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
- plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
- plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
- plexora/plugins/figure_builder/__init__.py +125 -0
- plexora/plugins/figure_builder/server/__init__.py +7 -0
- plexora/plugins/figure_builder/server/compose.py +365 -0
- plexora/plugins/figure_builder/server/export.py +784 -0
- plexora/plugins/figure_builder/server/export_jobs.py +148 -0
- plexora/plugins/figure_builder/server/operations.py +556 -0
- plexora/plugins/figure_builder/server/pixels.py +142 -0
- plexora/plugins/figure_builder/server/provenance.py +174 -0
- plexora/plugins/figure_builder/server/render.py +278 -0
- plexora/plugins/figure_builder/server/repository.py +701 -0
- plexora/plugins/figure_builder/server/routes.py +594 -0
- plexora/plugins/figure_builder/server/schema.py +1023 -0
- plexora/plugins/figure_builder/server/sources.py +120 -0
- plexora/plugins/figure_builder/server/textmetrics.py +155 -0
- plexora/plugins/figure_builder/static/figureActions.js +363 -0
- plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
- plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
- plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
- plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
- plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
- plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
- plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
- plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
- plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
- plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
- plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
- plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
- plexora/plugins/figure_builder/static/figureRichText.js +634 -0
- plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
- plexora/plugins/figure_builder/static/figureSchema.js +218 -0
- plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
- plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
- plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
- plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
- plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
- plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
- plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
- plexora/plugins/gating/__init__.py +76 -0
- plexora/plugins/gating/server/__init__.py +0 -0
- plexora/plugins/gating/server/anndata_gates.py +367 -0
- plexora/plugins/gating/server/database.py +16 -0
- plexora/plugins/gating/server/model.py +320 -0
- plexora/plugins/gating/server/routes.py +235 -0
- plexora/plugins/gating/static/csvGatingList.js +947 -0
- plexora/plugins/gating/static/gating.css +289 -0
- plexora/plugins/gating/static/gatingApi.js +221 -0
- plexora/plugins/gating/static/gatingSidebarController.js +513 -0
- plexora/plugins/gating/templates/gating/legacy.html +26 -0
- plexora/plugins/gating/templates/gating/panel.html +69 -0
- plexora/plugins/roi/__init__.py +95 -0
- plexora/plugins/roi/server/__init__.py +8 -0
- plexora/plugins/roi/server/adapters.py +655 -0
- plexora/plugins/roi/server/geojson.py +267 -0
- plexora/plugins/roi/server/geometry.py +181 -0
- plexora/plugins/roi/server/mapping.py +169 -0
- plexora/plugins/roi/server/operations.py +293 -0
- plexora/plugins/roi/server/repository.py +215 -0
- plexora/plugins/roi/server/routes.py +409 -0
- plexora/plugins/roi/server/schema.py +323 -0
- plexora/plugins/roi/static/roi.css +583 -0
- plexora/plugins/roi/static/roiApi.js +177 -0
- plexora/plugins/roi/static/roiFigureBridge.js +107 -0
- plexora/plugins/roi/static/roiGeometry.js +350 -0
- plexora/plugins/roi/static/roiRenderer.js +278 -0
- plexora/plugins/roi/static/roiSidebarController.js +1038 -0
- plexora/plugins/roi/static/roiState.js +518 -0
- plexora/plugins/roi/static/roiTools.js +993 -0
- plexora/plugins/roi/templates/roi/panel.html +240 -0
- plexora/proxy.py +57 -0
- plexora/server/models/adapters/__init__.py +94 -0
- plexora/server/models/adapters/anndata_adapter.py +388 -0
- plexora/server/models/adapters/base.py +93 -0
- plexora/server/models/adapters/classify.py +192 -0
- plexora/server/models/adapters/csv_adapter.py +105 -0
- plexora/server/models/adapters/inspection.py +265 -0
- plexora/server/models/adapters/spatialdata_adapter.py +248 -0
- plexora/server/models/centroid_tiles.py +305 -0
- plexora/server/models/data_model.py +1659 -0
- plexora/server/models/database_model.py +169 -0
- plexora/server/models/project.py +1270 -0
- plexora/server/plugins.py +199 -0
- plexora/server/routes/browse_routes.py +41 -0
- plexora/server/routes/data_routes.py +294 -0
- plexora/server/routes/import_routes.py +603 -0
- plexora/server/routes/page_routes.py +139 -0
- plexora/server/routes/project_routes.py +438 -0
- plexora/server/routes/quick_view_routes.py +62 -0
- plexora/server/routes/system_routes.py +39 -0
- plexora/server/routes/tool_routes.py +418 -0
- plexora/server/utils/addHEColumns.py +11 -0
- plexora/server/utils/fast_png.py +97 -0
- plexora/server/utils/fullConversion.py +50 -0
- plexora/server/utils/native_dialog.py +168 -0
- plexora/server/utils/pre_normalization.py +47 -0
- plexora/server/utils/segmentation_pyramid.py +598 -0
- plexora/server/utils/smallestenclosingcircle.py +126 -0
- plexora/server/utils/tiffsurgeon.py +372 -0
- plexora/server_cli.py +48 -0
- plexora/test.py +0 -0
- plexora-0.0.1.dist-info/METADATA +341 -0
- plexora-0.0.1.dist-info/RECORD +248 -0
- plexora-0.0.1.dist-info/WHEEL +5 -0
- plexora-0.0.1.dist-info/entry_points.txt +12 -0
- plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
- plexora-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import h5py
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import polars as pl
|
|
11
|
+
|
|
12
|
+
from plexora import api
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from anndata.io import read_elem, write_elem # anndata >= 0.10, public API
|
|
16
|
+
except ImportError: # pragma: no cover - older anndata fallback
|
|
17
|
+
from anndata._io.specs import read_elem, write_elem
|
|
18
|
+
|
|
19
|
+
# Column name used for the var_names index when the gates table is
|
|
20
|
+
# represented in-memory as a polars DataFrame (AnnData's own on-disk
|
|
21
|
+
# dataframe encoding stores the index separately, under this same name, so
|
|
22
|
+
# a real ad.read_h5ad() sees adata.uns[table_name].index.name == this value).
|
|
23
|
+
INDEX_COLUMN = "var_names"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _pandas_to_polars(df: pd.DataFrame, index_name: str) -> pl.DataFrame:
|
|
27
|
+
"""Manual conversion (no pl.from_pandas()/pyarrow -- not an installed
|
|
28
|
+
dependency here, see anndata_gates design notes in the project plan)."""
|
|
29
|
+
data = {index_name: df.index.astype(str).tolist()}
|
|
30
|
+
for col in df.columns:
|
|
31
|
+
data[str(col)] = df[col].to_numpy(dtype="float64")
|
|
32
|
+
return pl.DataFrame(data)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _polars_to_pandas(df: pl.DataFrame, index_name: str) -> pd.DataFrame:
|
|
36
|
+
"""Manual conversion (no .to_pandas()/pyarrow) -- the write_elem/read_elem
|
|
37
|
+
calls into AnnData's own on-disk dataframe codec are the only reason
|
|
38
|
+
pandas appears in this module at all."""
|
|
39
|
+
image_columns = [c for c in df.columns if c != index_name]
|
|
40
|
+
return pd.DataFrame(
|
|
41
|
+
df.select(image_columns).to_numpy(),
|
|
42
|
+
index=pd.Index(df[index_name].to_list(), name=index_name),
|
|
43
|
+
columns=image_columns,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _resolve_path(source) -> str:
|
|
48
|
+
"""Location of the AnnData *group* holding this datasource's data: the
|
|
49
|
+
.h5ad file itself, or the selected table inside a SpatialData .zarr
|
|
50
|
+
store. Everything below reads and writes through this one location, so
|
|
51
|
+
gates land in the table the user actually imported -- never anywhere
|
|
52
|
+
else in the store.
|
|
53
|
+
|
|
54
|
+
`source` is the api.TableSource core hands the plugin. It is deliberately
|
|
55
|
+
not the raw config entry: where a table lives on disk is core's business,
|
|
56
|
+
and this plugin only needs to be told, not to know the file format.
|
|
57
|
+
"""
|
|
58
|
+
if source is None or not source.path:
|
|
59
|
+
raise ValueError("No AnnData file path configured for this datasource")
|
|
60
|
+
if source.kind == 'spatialdata':
|
|
61
|
+
from plexora.server.models.adapters.spatialdata_adapter import table_path
|
|
62
|
+
|
|
63
|
+
return str(table_path(source.path, source.table))
|
|
64
|
+
return source.path
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _consolidated_format(path: Path) -> int | None:
|
|
68
|
+
"""Which zarr format's consolidated index this group carries, if any.
|
|
69
|
+
|
|
70
|
+
A consolidated index is a cached copy of every child's metadata, kept
|
|
71
|
+
beside the group: `.zmetadata` in zarr v2, a `consolidated_metadata` key
|
|
72
|
+
inside `zarr.json` in v3. Readers that find one trust it completely and
|
|
73
|
+
never list the directory -- which is what makes it both the reason a
|
|
74
|
+
write is refused and the reason a write must be followed by a refresh.
|
|
75
|
+
|
|
76
|
+
Read off disk rather than from an opened group so the answer is known
|
|
77
|
+
before choosing how to open it.
|
|
78
|
+
"""
|
|
79
|
+
if (path / '.zmetadata').is_file():
|
|
80
|
+
return 2
|
|
81
|
+
metadata = path / 'zarr.json'
|
|
82
|
+
if metadata.is_file():
|
|
83
|
+
try:
|
|
84
|
+
document = json.loads(metadata.read_text(encoding='utf-8'))
|
|
85
|
+
except (OSError, ValueError): # pragma: no cover - unreadable metadata
|
|
86
|
+
return None
|
|
87
|
+
if document.get('consolidated_metadata') is not None:
|
|
88
|
+
return 3
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@contextlib.contextmanager
|
|
93
|
+
def _open_group(path: str, writable: bool = False):
|
|
94
|
+
"""Yield the root group of an on-disk AnnData, for either backend.
|
|
95
|
+
|
|
96
|
+
anndata's element codec (read_elem/write_elem) is storage-agnostic, so
|
|
97
|
+
every caller below works unchanged against an h5py group from an .h5ad
|
|
98
|
+
or a zarr group from a SpatialData table. A .zarr store is a directory;
|
|
99
|
+
that's what distinguishes the two here.
|
|
100
|
+
|
|
101
|
+
Writing to a *consolidated* zarr group needs two extra steps, both
|
|
102
|
+
required and neither optional:
|
|
103
|
+
|
|
104
|
+
1. Open with `use_consolidated=False`. anndata refuses to write to a
|
|
105
|
+
group whose metadata is consolidated (`is_group_consolidated()` in
|
|
106
|
+
anndata/_io/specs/registry.py) and raises "Cannot overwrite/edit a
|
|
107
|
+
store with consolidated metadata" -- a real store written by
|
|
108
|
+
spatialdata hits this, because it consolidates each table. Opening
|
|
109
|
+
without the index sidesteps a guard that exists to stop exactly the
|
|
110
|
+
staleness step 2 repairs.
|
|
111
|
+
2. Rebuild the index afterwards. Skipping it loses the write silently:
|
|
112
|
+
the new group is on disk, but every reader consults the stale index
|
|
113
|
+
instead of listing the directory, so `anndata.read_zarr` and
|
|
114
|
+
`spatialdata.read_zarr` both report no gates and the save appears to
|
|
115
|
+
have done nothing. Verified against a copy of a real store.
|
|
116
|
+
|
|
117
|
+
The refresh is confined to this group -- the table, never the store
|
|
118
|
+
root. That is not just conservatism: re-consolidating a SpatialData
|
|
119
|
+
root, which is zarr v3, silently DROPS every v2 table from the index
|
|
120
|
+
(real stores mix the two), leaving a store whose tables have vanished.
|
|
121
|
+
Nothing needs it anyway; a root index enumerates which elements exist,
|
|
122
|
+
and adding a key inside a table does not change that.
|
|
123
|
+
"""
|
|
124
|
+
if Path(path).is_dir():
|
|
125
|
+
import zarr
|
|
126
|
+
|
|
127
|
+
# Path (not str) deliberately: zarr v3 parses a string store as a
|
|
128
|
+
# URL, mangling table names containing characters like '#'.
|
|
129
|
+
location = Path(path)
|
|
130
|
+
if not writable:
|
|
131
|
+
yield zarr.open_group(location, mode='r')
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
consolidated = _consolidated_format(location)
|
|
135
|
+
yield zarr.open_group(location, mode='a', use_consolidated=False)
|
|
136
|
+
# Only on success, and only if there was an index to begin with:
|
|
137
|
+
# a store that never had one needs no refresh, and writing one
|
|
138
|
+
# would change how every other tool reads it.
|
|
139
|
+
if consolidated is not None:
|
|
140
|
+
zarr.consolidate_metadata(
|
|
141
|
+
zarr.storage.LocalStore(location), zarr_format=consolidated
|
|
142
|
+
)
|
|
143
|
+
return
|
|
144
|
+
with h5py.File(path, 'r+' if writable else 'r') as handle:
|
|
145
|
+
yield handle
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _read_frame(group, key) -> pd.DataFrame:
|
|
149
|
+
"""Read just obs or var off an already-open group. This is the same
|
|
150
|
+
codec anndata uses internally, so it costs one dataframe read and never
|
|
151
|
+
touches X -- the property the old backed-mode .h5ad read had, now true
|
|
152
|
+
for the zarr backend too (zarr-backed AnnData has no backed mode)."""
|
|
153
|
+
return read_elem(group[key])
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _read_obs_column(path: str, subset: dict, column: str) -> pl.Series | None:
|
|
157
|
+
"""Lightweight read of a single obs column, optionally filtered by the
|
|
158
|
+
same subset.column/subset.value rule AnnDataAdapter.load_table()
|
|
159
|
+
applies. Returns None if the column doesn't exist. Never loads X --
|
|
160
|
+
safe/cheap to call repeatedly."""
|
|
161
|
+
with _open_group(path) as group:
|
|
162
|
+
obs = _read_frame(group, 'obs')
|
|
163
|
+
if column not in obs.columns:
|
|
164
|
+
return None
|
|
165
|
+
subset_column = (subset or {}).get('column')
|
|
166
|
+
if subset_column:
|
|
167
|
+
if subset_column not in obs.columns:
|
|
168
|
+
raise ValueError(f"Subset column {subset_column!r} not found in adata.obs")
|
|
169
|
+
mask = obs[subset_column].astype(str).to_numpy() == str(subset.get('value'))
|
|
170
|
+
obs = obs.loc[mask]
|
|
171
|
+
if obs.empty:
|
|
172
|
+
raise ValueError("Subset filter matched zero observations")
|
|
173
|
+
return pl.Series(column, obs[column].astype(str).to_numpy())
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def resolve_current_image_id(
|
|
177
|
+
path: str, source, datasource_name: str, imageid_column: str
|
|
178
|
+
) -> str:
|
|
179
|
+
"""Which column of the gates table this Plexora datasource's gates
|
|
180
|
+
should be written to. Re-applies this datasource's own registration
|
|
181
|
+
subset (if any) and requires the configured imageid column to resolve
|
|
182
|
+
to exactly one value within it; falls back to the datasource's own
|
|
183
|
+
registered name if the column doesn't exist at all."""
|
|
184
|
+
subset = dict(getattr(source, 'subset', None) or {})
|
|
185
|
+
values = _read_obs_column(path, subset, imageid_column)
|
|
186
|
+
if values is None:
|
|
187
|
+
return datasource_name
|
|
188
|
+
|
|
189
|
+
unique_values = values.unique().to_list()
|
|
190
|
+
if len(unique_values) != 1:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"Column {imageid_column!r} does not resolve to a single image "
|
|
193
|
+
f"for datasource {datasource_name!r} (found {len(unique_values)} "
|
|
194
|
+
"distinct values within this datasource's own data) -- refusing "
|
|
195
|
+
"to guess which gates-table column to update."
|
|
196
|
+
)
|
|
197
|
+
return unique_values[0]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def all_image_ids(path: str, imageid_column: str) -> list[str] | None:
|
|
201
|
+
"""Every distinct value of imageid_column across the *whole* source
|
|
202
|
+
file (no subset applied) -- used to eagerly create one gates-table
|
|
203
|
+
column per known image. None if the column doesn't exist anywhere."""
|
|
204
|
+
values = _read_obs_column(path, {}, imageid_column)
|
|
205
|
+
if values is None:
|
|
206
|
+
return None
|
|
207
|
+
return sorted(values.unique().to_list())
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _read_var_names(path: str) -> list[str]:
|
|
211
|
+
with _open_group(path) as group:
|
|
212
|
+
return [str(v) for v in _read_frame(group, 'var').index]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def save_gates_to_anndata(
|
|
216
|
+
source,
|
|
217
|
+
datasource_name: str,
|
|
218
|
+
active_gates: dict,
|
|
219
|
+
table_name: str = "gates",
|
|
220
|
+
imageid_column: str = "imageid",
|
|
221
|
+
) -> dict:
|
|
222
|
+
"""Writes only adata.uns[table_name] (rows=var_names, one column per
|
|
223
|
+
known image) via direct h5py + anndata's element codec -- never a full
|
|
224
|
+
ad.read_h5ad()/write_h5ad() round trip. Overwrites only the column for
|
|
225
|
+
the image this datasource currently represents; every other image's
|
|
226
|
+
column (freshly added blank, or previously saved) is left untouched.
|
|
227
|
+
|
|
228
|
+
active_gates: {channel_name: lower_bound}, already filtered to
|
|
229
|
+
currently-active gates by the caller.
|
|
230
|
+
"""
|
|
231
|
+
path = _resolve_path(source)
|
|
232
|
+
var_names = _read_var_names(path)
|
|
233
|
+
current_image_id = resolve_current_image_id(
|
|
234
|
+
path, source, datasource_name, imageid_column
|
|
235
|
+
)
|
|
236
|
+
known_image_ids = all_image_ids(path, imageid_column) or [current_image_id]
|
|
237
|
+
if current_image_id not in known_image_ids:
|
|
238
|
+
known_image_ids = sorted(set(known_image_ids) | {current_image_id})
|
|
239
|
+
|
|
240
|
+
with _open_group(path, writable=True) as f:
|
|
241
|
+
uns = f.require_group('uns')
|
|
242
|
+
|
|
243
|
+
if table_name in uns:
|
|
244
|
+
existing = read_elem(uns[table_name])
|
|
245
|
+
if not isinstance(existing, pd.DataFrame):
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"adata.uns[{table_name!r}] already exists and is not a table"
|
|
248
|
+
)
|
|
249
|
+
existing_table = _pandas_to_polars(existing, INDEX_COLUMN)
|
|
250
|
+
existing_var_names = existing_table[INDEX_COLUMN].to_list()
|
|
251
|
+
if existing_var_names == var_names:
|
|
252
|
+
# Fast, exact path -- the overwhelmingly common case (panel
|
|
253
|
+
# hasn't changed between saves). No realignment needed.
|
|
254
|
+
table = existing_table
|
|
255
|
+
else:
|
|
256
|
+
# Realign rows to the current var_names -- stale vars are
|
|
257
|
+
# dropped, new vars appear as all-NaN rows. Expected if the
|
|
258
|
+
# source panel changed since the last save, not a bug.
|
|
259
|
+
# Done via an explicit first-occurrence-name lookup rather
|
|
260
|
+
# than a polars join: real panels can have duplicate
|
|
261
|
+
# var_names (e.g. a marker re-stained across cycles -- see
|
|
262
|
+
# the var_index note below), and joining on a non-unique key
|
|
263
|
+
# produces a row explosion (each duplicate on one side
|
|
264
|
+
# matches every duplicate on the other) instead of a clean
|
|
265
|
+
# 1:1 realignment.
|
|
266
|
+
old_index = {}
|
|
267
|
+
for i, name in enumerate(existing_var_names):
|
|
268
|
+
if name not in old_index:
|
|
269
|
+
old_index[name] = i
|
|
270
|
+
table = pl.DataFrame({INDEX_COLUMN: var_names})
|
|
271
|
+
for col in existing_table.columns:
|
|
272
|
+
if col == INDEX_COLUMN:
|
|
273
|
+
continue
|
|
274
|
+
old_values = existing_table[col].to_list()
|
|
275
|
+
new_values = [
|
|
276
|
+
old_values[old_index[name]] if name in old_index else None
|
|
277
|
+
for name in var_names
|
|
278
|
+
]
|
|
279
|
+
table = table.with_columns(pl.Series(col, new_values, dtype=pl.Float64))
|
|
280
|
+
else:
|
|
281
|
+
table = pl.DataFrame({INDEX_COLUMN: var_names})
|
|
282
|
+
|
|
283
|
+
for image_id in known_image_ids:
|
|
284
|
+
if image_id not in table.columns:
|
|
285
|
+
table = table.with_columns(
|
|
286
|
+
pl.lit(None, dtype=pl.Float64).alias(image_id)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
values = np.full(len(var_names), np.nan, dtype="float64")
|
|
290
|
+
# active_gates is keyed by the *gating channel* name, which for a
|
|
291
|
+
# duplicate var_name is the deduplicated display name the frontend
|
|
292
|
+
# actually shows/sends (AnnDataAdapter.api.deduplicate_names(): first
|
|
293
|
+
# occurrence keeps the plain name, e.g. "PTPRC"; the next becomes
|
|
294
|
+
# "PTPRC_1", etc.) -- not the raw, still-duplicated var_names list.
|
|
295
|
+
# Matching against raw var_names here used to silently drop every
|
|
296
|
+
# gate on a "_N"-suffixed channel (no raw var_name is ever literally
|
|
297
|
+
# "PTPRC_1"), undercounting how many gated markers actually got
|
|
298
|
+
# written with zero error -- confirmed against real exemplar data
|
|
299
|
+
# (orion.h5ad has a duplicated "PTPRC").
|
|
300
|
+
var_index = {name: i for i, name in enumerate(api.deduplicate_names(var_names))}
|
|
301
|
+
n_written = 0
|
|
302
|
+
for channel, lower_bound in active_gates.items():
|
|
303
|
+
idx = var_index.get(channel)
|
|
304
|
+
if idx is None or lower_bound is None:
|
|
305
|
+
continue
|
|
306
|
+
values[idx] = float(lower_bound)
|
|
307
|
+
n_written += 1
|
|
308
|
+
|
|
309
|
+
table = table.with_columns(pl.Series(current_image_id, values, dtype=pl.Float64))
|
|
310
|
+
|
|
311
|
+
if table_name in uns:
|
|
312
|
+
del uns[table_name]
|
|
313
|
+
write_elem(uns, table_name, _polars_to_pandas(table, INDEX_COLUMN))
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"table_name": table_name,
|
|
317
|
+
"imageid_column": imageid_column,
|
|
318
|
+
"image_id": current_image_id,
|
|
319
|
+
"path": path,
|
|
320
|
+
"n_active_gates": n_written,
|
|
321
|
+
"n_image_columns": len(table.columns) - 1,
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def load_gates_from_anndata(
|
|
326
|
+
source,
|
|
327
|
+
datasource_name: str,
|
|
328
|
+
table_name: str = "gates",
|
|
329
|
+
imageid_column: str = "imageid",
|
|
330
|
+
) -> dict:
|
|
331
|
+
"""Reverse of save_gates_to_anndata: reads adata.uns[table_name]'s column
|
|
332
|
+
for this datasource's current image back out as {channel: lower_bound},
|
|
333
|
+
keyed by the same deduplicated display names save_gates_to_anndata
|
|
334
|
+
writes against -- so it plugs directly into gatingList.gating_channels
|
|
335
|
+
client-side without a name-mapping step. Read-only, no h5py write mode.
|
|
336
|
+
"""
|
|
337
|
+
path = _resolve_path(source)
|
|
338
|
+
current_image_id = resolve_current_image_id(
|
|
339
|
+
path, source, datasource_name, imageid_column
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
with _open_group(path) as f:
|
|
343
|
+
uns = f.get('uns')
|
|
344
|
+
if uns is None or table_name not in uns:
|
|
345
|
+
return {"image_id": current_image_id, "gates": {}}
|
|
346
|
+
existing = read_elem(uns[table_name])
|
|
347
|
+
if not isinstance(existing, pd.DataFrame) or current_image_id not in existing.columns:
|
|
348
|
+
return {"image_id": current_image_id, "gates": {}}
|
|
349
|
+
column = existing[current_image_id]
|
|
350
|
+
|
|
351
|
+
# Table rows are ordered like var_names at the time of the last save
|
|
352
|
+
# (see save_gates_to_anndata's realignment note) -- matched back to
|
|
353
|
+
# today's deduplicated display names by position, the same "common,
|
|
354
|
+
# overwhelmingly likely" fast-path assumption that function documents.
|
|
355
|
+
var_names = _read_var_names(path)
|
|
356
|
+
display_names = api.deduplicate_names(var_names)
|
|
357
|
+
|
|
358
|
+
gates = {}
|
|
359
|
+
for position, name in enumerate(display_names):
|
|
360
|
+
if position >= len(column):
|
|
361
|
+
break
|
|
362
|
+
value = column.iloc[position]
|
|
363
|
+
if value is None or (isinstance(value, float) and np.isnan(value)):
|
|
364
|
+
continue
|
|
365
|
+
gates[name] = float(value)
|
|
366
|
+
|
|
367
|
+
return {"image_id": current_image_id, "gates": gates}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Where gating's saved gates used to live.
|
|
2
|
+
|
|
3
|
+
Persistence goes through `plexora.api.store`, which namespaces every table as
|
|
4
|
+
`plugin_gating_<name>`. This name is all that remains of the older scheme, in
|
|
5
|
+
which gating wrote a bare `gatinglist` table straight into the shared
|
|
6
|
+
per-datasource sqlite file.
|
|
7
|
+
|
|
8
|
+
It is declared so `PluginStore` can adopt gates saved by those builds: the row
|
|
9
|
+
is copied into the namespaced table on first read and this table is then
|
|
10
|
+
dropped. Once every project a user opens has been read once, nothing refers to
|
|
11
|
+
it and the constant can go.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
#: Pre-namespacing table name, retained only so existing projects can be
|
|
15
|
+
#: converted. Nothing writes here; the store deletes it after adopting it.
|
|
16
|
+
LEGACY_STATE_TABLE = "gatinglist"
|