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,320 @@
|
|
|
1
|
+
"""Gating's query and persistence logic.
|
|
2
|
+
|
|
3
|
+
Reaches the host only through `plexora.api`. Everything it needs -- the
|
|
4
|
+
feature table, the role->column map, per-plugin storage -- arrives as handles,
|
|
5
|
+
so this module names no core internals and holds no core state.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pickle
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import polars as pl
|
|
12
|
+
from scipy.stats import norm
|
|
13
|
+
from sklearn.mixture import GaussianMixture
|
|
14
|
+
|
|
15
|
+
from plexora import api
|
|
16
|
+
from plexora.plugins.gating.server.database import LEGACY_STATE_TABLE
|
|
17
|
+
|
|
18
|
+
#: Identifies this plugin's storage namespace. Must stay 'gating' -- it is what
|
|
19
|
+
#: `plugin_gating_state` is keyed on, and changing it would strand saved gates.
|
|
20
|
+
PLUGIN_NAME = "gating"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _store(datasource_name):
|
|
24
|
+
"""Gating's slice of this datasource's database.
|
|
25
|
+
|
|
26
|
+
`legacy_state_table` points at the un-namespaced `gatinglist` table earlier
|
|
27
|
+
builds wrote directly. Reads fall back to it when the namespaced table is
|
|
28
|
+
empty, so upgrading the host does not lose a user's saved gates; writes
|
|
29
|
+
always go to the namespaced table, so the old one is left frozen rather
|
|
30
|
+
than kept in sync.
|
|
31
|
+
"""
|
|
32
|
+
return api.store(datasource_name, PLUGIN_NAME, legacy_state_table=LEGACY_STATE_TABLE)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_gated_cells(datasource_name, gates, start_keys):
|
|
36
|
+
table = api.dataset(datasource_name).table
|
|
37
|
+
if not gates:
|
|
38
|
+
return []
|
|
39
|
+
id_key = start_keys[0]
|
|
40
|
+
values = table.frame()[id_key].to_numpy()[table.range_mask(gates)].tolist()
|
|
41
|
+
return [{id_key: v} for v in values]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def download_gating_csv(datasource_name, gates, channels, selection_ids, encoding):
|
|
45
|
+
dataset = api.dataset(datasource_name)
|
|
46
|
+
df = dataset.table.frame()
|
|
47
|
+
|
|
48
|
+
csv = df
|
|
49
|
+
# The role, not a literal column name -- schema.cell_id is whatever the
|
|
50
|
+
# project recorded. No fallback: this plugin declares cell_id in its
|
|
51
|
+
# Requires, so core collects it before the tool can open at all, and a
|
|
52
|
+
# literal default here would only mask a project that slipped through.
|
|
53
|
+
idField = dataset.schema.cell_id
|
|
54
|
+
|
|
55
|
+
if selection_ids:
|
|
56
|
+
datasource_filter = df.filter(pl.col(idField).is_in(selection_ids))
|
|
57
|
+
else:
|
|
58
|
+
datasource_filter = df
|
|
59
|
+
|
|
60
|
+
expr = None
|
|
61
|
+
for key, value in gates.items():
|
|
62
|
+
cond = (pl.col(key) > value[0]) & (pl.col(key) < value[1])
|
|
63
|
+
expr = cond if expr is None else (expr & cond)
|
|
64
|
+
if expr is not None:
|
|
65
|
+
ids = datasource_filter.filter(expr)['id'].to_numpy()
|
|
66
|
+
else:
|
|
67
|
+
# No gates set: no filter, nothing gated in. (pandas' .query('')
|
|
68
|
+
# used to raise ValueError here -- fixed rather than preserved.)
|
|
69
|
+
ids = np.array([], dtype=np.int64)
|
|
70
|
+
|
|
71
|
+
if 'Area' in channels:
|
|
72
|
+
del channels['Area']
|
|
73
|
+
is_in_ids = pl.col('id').is_in(ids)
|
|
74
|
+
for channel in channels:
|
|
75
|
+
if channel in gates:
|
|
76
|
+
# Cast to the original column's dtype for CSV-text parity with
|
|
77
|
+
# the pandas version: csv.loc[mask, channel] = 1 silently
|
|
78
|
+
# upcast an int literal into what's typically a float64 marker
|
|
79
|
+
# column (rendering "1.0"), whereas a bare Polars int literal
|
|
80
|
+
# would render "1" -- a real text diff in the exported CSV.
|
|
81
|
+
dtype = csv.schema[channel]
|
|
82
|
+
if encoding == 'binary':
|
|
83
|
+
value_expr = pl.when(is_in_ids).then(pl.lit(1)).otherwise(pl.lit(0)).cast(dtype)
|
|
84
|
+
else:
|
|
85
|
+
value_expr = pl.when(is_in_ids).then(pl.col(channel)).otherwise(pl.lit(0).cast(dtype))
|
|
86
|
+
csv = csv.with_columns(value_expr.alias(channel))
|
|
87
|
+
else:
|
|
88
|
+
csv = csv.with_columns(pl.lit(0).alias(channel))
|
|
89
|
+
|
|
90
|
+
return csv
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def download_gates(datasource_name, gates, channels):
|
|
94
|
+
rows = []
|
|
95
|
+
for key, value in channels.items():
|
|
96
|
+
rows.append([key, value[0], value[1]])
|
|
97
|
+
csv = pl.DataFrame(rows, schema=['channel', 'gate_start', 'gate_end'], orient='row')
|
|
98
|
+
csv = csv.with_columns(pl.lit(False).alias('gate_active'))
|
|
99
|
+
|
|
100
|
+
schema = csv.schema
|
|
101
|
+
for channel in gates:
|
|
102
|
+
is_channel = pl.col('channel') == channel
|
|
103
|
+
csv = csv.with_columns([
|
|
104
|
+
pl.when(is_channel).then(pl.lit(True)).otherwise(pl.col('gate_active')).alias('gate_active'),
|
|
105
|
+
pl.when(is_channel).then(pl.lit(gates[channel][0]).cast(schema['gate_start']))
|
|
106
|
+
.otherwise(pl.col('gate_start')).alias('gate_start'),
|
|
107
|
+
pl.when(is_channel).then(pl.lit(gates[channel][1]).cast(schema['gate_end']))
|
|
108
|
+
.otherwise(pl.col('gate_end')).alias('gate_end'),
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
return csv
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def save_gating_list(datasource_name, gates, channels):
|
|
115
|
+
rows = []
|
|
116
|
+
for key, value in channels.items():
|
|
117
|
+
rows.append([key, value[0], value[1]])
|
|
118
|
+
csv = pl.DataFrame(rows, schema=['channel', 'gate_start', 'gate_end'], orient='row')
|
|
119
|
+
csv = csv.with_columns(pl.lit(False).alias('gate_active'))
|
|
120
|
+
|
|
121
|
+
schema = csv.schema
|
|
122
|
+
for channel in gates:
|
|
123
|
+
is_channel = pl.col('channel') == channel
|
|
124
|
+
csv = csv.with_columns([
|
|
125
|
+
pl.when(is_channel).then(pl.lit(True)).otherwise(pl.col('gate_active')).alias('gate_active'),
|
|
126
|
+
pl.when(is_channel).then(pl.lit(gates[channel][0]).cast(schema['gate_start']))
|
|
127
|
+
.otherwise(pl.col('gate_start')).alias('gate_start'),
|
|
128
|
+
pl.when(is_channel).then(pl.lit(gates[channel][1]).cast(schema['gate_end']))
|
|
129
|
+
.otherwise(pl.col('gate_end')).alias('gate_end'),
|
|
130
|
+
])
|
|
131
|
+
|
|
132
|
+
temp = csv.to_dicts()
|
|
133
|
+
f = pickle.dumps(temp, protocol=4)
|
|
134
|
+
_store(datasource_name).put_state(f)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_saved_gating_list(datasource_name):
|
|
138
|
+
cells = _store(datasource_name).get_state()
|
|
139
|
+
if cells is None:
|
|
140
|
+
return None
|
|
141
|
+
return pickle.loads(cells)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
#: Components in the auto-gate fit.
|
|
145
|
+
#:
|
|
146
|
+
#: Three, not two. A marker's negative population is not a point -- it is a
|
|
147
|
+
#: broad distribution of background and autofluorescence, and on a log scale it
|
|
148
|
+
#: is close to symmetric. A two-component fit given that has an easier job
|
|
149
|
+
#: splitting the *background* down the middle than separating background from
|
|
150
|
+
#: positives, and the resulting gate sits inside the negative population: every
|
|
151
|
+
#: cell above the middle of the background reads as positive.
|
|
152
|
+
#:
|
|
153
|
+
#: With a third component the background can occupy two and the bright tail
|
|
154
|
+
#: gets its own, so the boundary between the top two is a real one. Same count,
|
|
155
|
+
#: and the same reasoning, as core's `get_channel_gmm` for image channels.
|
|
156
|
+
_GATE_COMPONENTS = 3
|
|
157
|
+
|
|
158
|
+
#: EM cost scales with N per iteration, and a 1-D mixture's fitted parameters
|
|
159
|
+
#: barely move between 100k rows and millions. Fixed seed, so a given column
|
|
160
|
+
#: gates the same way twice.
|
|
161
|
+
_GMM_FIT_SAMPLE_CAP = 100_000
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _fit_mixture(values, components):
|
|
165
|
+
"""(means, sds, weights), ascending by mean, or None if it cannot fit.
|
|
166
|
+
|
|
167
|
+
A column with fewer distinct values than components -- a flag, a constant,
|
|
168
|
+
a nearly empty channel -- has no mixture to find, and sklearn either raises
|
|
169
|
+
or returns degenerate components. Saying so is better than a gate derived
|
|
170
|
+
from noise.
|
|
171
|
+
"""
|
|
172
|
+
values = values[np.isfinite(values)]
|
|
173
|
+
if values.size < components or np.unique(values).size < components:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
sample = values
|
|
177
|
+
if sample.shape[0] > _GMM_FIT_SAMPLE_CAP:
|
|
178
|
+
rng = np.random.default_rng(0)
|
|
179
|
+
sample = sample[rng.choice(sample.shape[0], size=_GMM_FIT_SAMPLE_CAP,
|
|
180
|
+
replace=False)]
|
|
181
|
+
|
|
182
|
+
gmm = GaussianMixture(components, max_iter=1000, tol=1e-6, random_state=0)
|
|
183
|
+
gmm.fit(sample.reshape(-1, 1))
|
|
184
|
+
order = np.argsort(gmm.means_[:, 0])
|
|
185
|
+
return (gmm.means_[order, 0],
|
|
186
|
+
np.sqrt(gmm.covariances_[order, 0, 0]),
|
|
187
|
+
gmm.weights_[order])
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _populations(fitted, x):
|
|
191
|
+
"""(background, positive) weighted densities at `x`.
|
|
192
|
+
|
|
193
|
+
Two populations out of three components: the brightest one is the positive
|
|
194
|
+
population, and everything below it pooled is the background. Pooling
|
|
195
|
+
rather than taking the second component alone is what makes the pair add up
|
|
196
|
+
to the whole distribution, so the two curves drawn over the histogram cover
|
|
197
|
+
it instead of leaving the largest peak unexplained.
|
|
198
|
+
"""
|
|
199
|
+
means, sds, weights = fitted
|
|
200
|
+
positive = norm(means[-1], sds[-1]).pdf(x) * weights[-1]
|
|
201
|
+
background = sum(norm(means[i], sds[i]).pdf(x) * weights[i]
|
|
202
|
+
for i in range(len(means) - 1))
|
|
203
|
+
return background, positive
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _crossover(fitted):
|
|
207
|
+
"""Where the positive population overtakes the background.
|
|
208
|
+
|
|
209
|
+
The threshold, rather than `mean(means)` -- which is what this used to take
|
|
210
|
+
and which ignores both how wide each component is and how much of the data
|
|
211
|
+
it holds. A narrow background beside a broad positive tail has its midpoint
|
|
212
|
+
far below the point where the two actually change places, and that
|
|
213
|
+
difference is the gate being wrong by tens of percent of the cells.
|
|
214
|
+
"""
|
|
215
|
+
means = fitted[0]
|
|
216
|
+
x = np.linspace(means[0], means[-1], 2000)
|
|
217
|
+
background, positive = _populations(fitted, x)
|
|
218
|
+
above = np.flatnonzero(positive > background)
|
|
219
|
+
# Never overtakes: nothing here is separable, so the top component's own
|
|
220
|
+
# centre is as honest an answer as there is.
|
|
221
|
+
return float(x[above[0]]) if above.size else float(means[-1])
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def auto_gate(values, log_transformed, at=None):
|
|
225
|
+
"""Where the positive population starts, in the values' own units.
|
|
226
|
+
|
|
227
|
+
Fitted on a log scale whichever scale the table is stored on: marker
|
|
228
|
+
intensities are log-normal, and a mixture of *normals* fitted to raw counts
|
|
229
|
+
is fitting the wrong shape -- the components chase the skew instead of the
|
|
230
|
+
populations. So raw values are log1p'd for the fit and the answer mapped
|
|
231
|
+
back with expm1, while values the project already log1p'd are fitted as
|
|
232
|
+
they stand. The same underlying data then gives the same gate either way,
|
|
233
|
+
which it did not before: turning the log switch on used to move the gate
|
|
234
|
+
from roughly the right place to the middle of the background.
|
|
235
|
+
|
|
236
|
+
That is also why the project's own flag decides this rather than a guess at
|
|
237
|
+
the numbers. Logging twice is its own failure -- it compresses the
|
|
238
|
+
separation until a marker with 3% positives gates at 28% -- so this cannot
|
|
239
|
+
be "always log1p", and nothing in the values themselves tells the two
|
|
240
|
+
apart.
|
|
241
|
+
|
|
242
|
+
`at` asks for the fitted curves as well, evaluated at those points and in
|
|
243
|
+
the values' own units. Returns (gate, background, positive), any of which
|
|
244
|
+
is None when the column has no mixture to find -- a constant, a flag, a
|
|
245
|
+
nearly empty channel. The caller ships nothing rather than a number derived
|
|
246
|
+
from noise.
|
|
247
|
+
"""
|
|
248
|
+
values = values[np.isfinite(values)]
|
|
249
|
+
# log1p rather than log: it is the transform Plexora itself applies, expm1
|
|
250
|
+
# inverts it exactly, and it is defined at zero -- which is where a large
|
|
251
|
+
# part of a quantification column sits. Negative values (arcsinh, z-scored)
|
|
252
|
+
# have no log to take, so those are fitted as they stand.
|
|
253
|
+
to_log = not log_transformed and values.size > 0 and values.min() >= 0
|
|
254
|
+
fitted = _fit_mixture(np.log1p(values) if to_log else values, _GATE_COMPONENTS)
|
|
255
|
+
if fitted is None:
|
|
256
|
+
return None, None, None
|
|
257
|
+
|
|
258
|
+
gate = _crossover(fitted)
|
|
259
|
+
gate = float(np.expm1(gate)) if to_log else gate
|
|
260
|
+
if at is None:
|
|
261
|
+
return gate, None, None
|
|
262
|
+
|
|
263
|
+
# Back into the values' own units, which is what the histogram underneath
|
|
264
|
+
# these curves is binned in. Densities do not survive a change of variable
|
|
265
|
+
# unchanged -- dividing by (1 + x) is the log1p Jacobian, and without it
|
|
266
|
+
# the curves would be the right shape at the wrong height.
|
|
267
|
+
background, positive = _populations(fitted, np.log1p(at) if to_log else at)
|
|
268
|
+
if to_log:
|
|
269
|
+
background, positive = background / (1 + at), positive / (1 + at)
|
|
270
|
+
return gate, background, positive
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _curve(x, y):
|
|
274
|
+
"""A fitted density as the client plots it. Empty when there was no fit."""
|
|
275
|
+
if y is None:
|
|
276
|
+
return []
|
|
277
|
+
return [{'x': float(x[i]), 'y': float(y[i])} for i in range(len(x))]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def get_gating_gmm(channel_name, datasource_name, selection_ids):
|
|
281
|
+
dataset = api.dataset(datasource_name)
|
|
282
|
+
df = dataset.table.frame()
|
|
283
|
+
|
|
284
|
+
selection_key = tuple(sorted(selection_ids)) if selection_ids else None
|
|
285
|
+
cache_key = (channel_name, selection_key)
|
|
286
|
+
|
|
287
|
+
def _compute():
|
|
288
|
+
packet_gmm = {}
|
|
289
|
+
|
|
290
|
+
idField = dataset.schema.cell_id
|
|
291
|
+
if selection_ids:
|
|
292
|
+
datasource_filter = df.filter(pl.col(idField).is_in(selection_ids))
|
|
293
|
+
else:
|
|
294
|
+
# No selection to filter by (the only case current callers use,
|
|
295
|
+
# since lasso/spatial-selection was removed) -- avoid a full
|
|
296
|
+
# 2M-row copy that's immediately discarded.
|
|
297
|
+
datasource_filter = df
|
|
298
|
+
|
|
299
|
+
column_data = df[channel_name].to_numpy()
|
|
300
|
+
# The histogram the curves below are laid over -- binned on the whole
|
|
301
|
+
# column, in its own units, and deliberately not subsampled.
|
|
302
|
+
bin_edges = np.histogram_bin_edges(column_data[~np.isnan(column_data)], bins=50)
|
|
303
|
+
midpoints = (bin_edges[1:] + bin_edges[:-1]) / 2
|
|
304
|
+
|
|
305
|
+
column_data_filtered = datasource_filter[channel_name].to_numpy()
|
|
306
|
+
|
|
307
|
+
# One fit answers both: where to put the gate, and the two curves that
|
|
308
|
+
# show why it went there. They used to be able to disagree -- the
|
|
309
|
+
# curves were the fit, the gate was a summary of it that ignored their
|
|
310
|
+
# widths -- so the auto button landed somewhere the picture did not
|
|
311
|
+
# explain.
|
|
312
|
+
gate, background, positive = auto_gate(
|
|
313
|
+
column_data_filtered, dataset.table.log_transformed, at=midpoints)
|
|
314
|
+
if gate is not None:
|
|
315
|
+
packet_gmm['gate'] = gate
|
|
316
|
+
packet_gmm['gmm_1'] = _curve(midpoints, background)
|
|
317
|
+
packet_gmm['gmm_2'] = _curve(midpoints, positive)
|
|
318
|
+
return packet_gmm
|
|
319
|
+
|
|
320
|
+
return dataset.cached(cache_key, _compute)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from flask import Blueprint, Response, abort, jsonify, request, stream_with_context
|
|
4
|
+
import polars as pl
|
|
5
|
+
|
|
6
|
+
from plexora import api
|
|
7
|
+
from plexora.plugins.gating.server import anndata_gates
|
|
8
|
+
from plexora.plugins.gating.server import model as gating_model
|
|
9
|
+
from plexora.plugins.gating.server.model import PLUGIN_NAME
|
|
10
|
+
|
|
11
|
+
# template_folder/static_folder make this plugin self-contained: Flask's
|
|
12
|
+
# DispatchingJinjaLoader already searches every blueprint's template folder, and
|
|
13
|
+
# the blueprint serves its own assets. Both were available all along and unused
|
|
14
|
+
# -- core previously had to know where a module's files lived.
|
|
15
|
+
gating_bp = Blueprint(
|
|
16
|
+
'gating', __name__,
|
|
17
|
+
template_folder='../templates',
|
|
18
|
+
static_folder='../static',
|
|
19
|
+
static_url_path='/static',
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _files(datasource):
|
|
24
|
+
"""This plugin's own file storage for a datasource -- where the uploaded
|
|
25
|
+
gates CSV lands. Scoped per plugin rather than written straight into the
|
|
26
|
+
datasource directory, so two plugins cannot overwrite each other's uploads
|
|
27
|
+
and an uninstall knows what belongs to whom."""
|
|
28
|
+
return api.store(datasource, PLUGIN_NAME).directory()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@gating_bp.route('/get_gated_cell_ids', methods=['GET'])
|
|
32
|
+
def get_gated_cell_ids():
|
|
33
|
+
datasource = request.args.get('datasource')
|
|
34
|
+
filter = json.loads(request.args.get('filter'))
|
|
35
|
+
start_keys = list(request.args.get('start_keys').split(','))
|
|
36
|
+
resp = gating_model.get_gated_cells(datasource, filter, start_keys)
|
|
37
|
+
return api.json_response(resp)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@gating_bp.route('/get_gating_gmm', methods=['POST'])
|
|
41
|
+
def get_gating_gmm():
|
|
42
|
+
post_data = json.loads(request.data)
|
|
43
|
+
channel = post_data['channel']
|
|
44
|
+
datasource = post_data['datasource']
|
|
45
|
+
selection_ids = post_data['selection_ids']
|
|
46
|
+
resp = gating_model.get_gating_gmm(channel, datasource, selection_ids)
|
|
47
|
+
return api.json_response(resp)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@gating_bp.route('/upload_gates', methods=['POST'])
|
|
51
|
+
def upload_gates():
|
|
52
|
+
file = request.files['file']
|
|
53
|
+
if file.filename.endswith('.csv') == False:
|
|
54
|
+
abort(422)
|
|
55
|
+
datasource = request.form['datasource']
|
|
56
|
+
file.save(_files(datasource) / 'uploaded_gates.csv')
|
|
57
|
+
resp = jsonify(success=True)
|
|
58
|
+
return resp
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _stream_csv(df, chunksize=100_000):
|
|
62
|
+
"""Yield a large DataFrame as CSV in row chunks instead of materializing
|
|
63
|
+
the full serialized string (and holding it alongside the DataFrame) in
|
|
64
|
+
memory at once, as df.write_csv() would for a multi-million-row gating
|
|
65
|
+
export. Polars has no built-in chunked-string-generator, so this slices
|
|
66
|
+
and writes each chunk by hand."""
|
|
67
|
+
header = True
|
|
68
|
+
for start in range(0, df.height, chunksize):
|
|
69
|
+
yield df.slice(start, chunksize).write_csv(include_header=header)
|
|
70
|
+
header = False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@gating_bp.route('/download_gating_csv', methods=['POST'])
|
|
74
|
+
def download_gating_csv():
|
|
75
|
+
datasource = request.form['datasource']
|
|
76
|
+
filename = request.form['filename']
|
|
77
|
+
|
|
78
|
+
filter = json.loads(request.form['filter'])
|
|
79
|
+
channels = json.loads(request.form['channels'])
|
|
80
|
+
selection_ids = json.loads(request.form['selection_ids'])
|
|
81
|
+
fullCsv = json.loads(request.form['fullCsv'])
|
|
82
|
+
encoding = request.form['encoding']
|
|
83
|
+
if fullCsv:
|
|
84
|
+
csv = gating_model.download_gating_csv(datasource, filter, channels, selection_ids, encoding)
|
|
85
|
+
return Response(
|
|
86
|
+
stream_with_context(_stream_csv(csv)),
|
|
87
|
+
mimetype="text/csv",
|
|
88
|
+
headers={"Content-disposition":
|
|
89
|
+
"attachment; filename=" + filename + ".csv"})
|
|
90
|
+
else:
|
|
91
|
+
csv = gating_model.download_gates(datasource, filter, channels)
|
|
92
|
+
return Response(
|
|
93
|
+
csv.write_csv(),
|
|
94
|
+
mimetype="text/csv",
|
|
95
|
+
headers={"Content-disposition":
|
|
96
|
+
"attachment; filename=" + filename + ".csv"})
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@gating_bp.route('/save_gating_list', methods=['POST'])
|
|
100
|
+
def save_gating_list():
|
|
101
|
+
post_data = json.loads(request.data)
|
|
102
|
+
|
|
103
|
+
datasource = post_data['datasource']
|
|
104
|
+
filter = post_data['filter']
|
|
105
|
+
channels = post_data['channels']
|
|
106
|
+
|
|
107
|
+
# DB-only on every save -- the .h5ad file is only ever written to
|
|
108
|
+
# explicitly, via the "Save Gates to AnnData" button (save_gates_to_anndata()
|
|
109
|
+
# below). Writing to the source file on every debounced slider edit was
|
|
110
|
+
# tried and reverted: the user wants edits to stay local/undo-able in the
|
|
111
|
+
# DB until they deliberately commit them to the file.
|
|
112
|
+
gating_model.save_gating_list(datasource, filter, channels)
|
|
113
|
+
|
|
114
|
+
resp = jsonify(success=True)
|
|
115
|
+
return resp
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@gating_bp.route('/get_saved_gating_list', methods=['GET'])
|
|
119
|
+
def get_saved_gating_list():
|
|
120
|
+
datasource = request.args.get('datasource')
|
|
121
|
+
resp = gating_model.get_saved_gating_list(datasource)
|
|
122
|
+
return api.json_response(resp)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@gating_bp.route('/save_gates_to_anndata', methods=['POST'])
|
|
126
|
+
def save_gates_to_anndata():
|
|
127
|
+
"""Writes only adata.uns[table_name] (lower gate bound per marker, one
|
|
128
|
+
column per image) back into the source .h5ad -- never a full anndata
|
|
129
|
+
rewrite. Gates are derived from the persisted GatingList row (the DB),
|
|
130
|
+
not from anything the client sends -- the DB is the single source of
|
|
131
|
+
truth here, same as CSV download/restore-on-reload.
|
|
132
|
+
|
|
133
|
+
Important: gate_active is NOT the right signal for "was this channel
|
|
134
|
+
customized" -- it only ever reflects whichever single marker is
|
|
135
|
+
currently displayed (gatingList.selections is reset on every marker
|
|
136
|
+
switch, by design, for the live single-marker slider/segmentation-
|
|
137
|
+
outline view). The gate_start/gate_end *values* for every OTHER
|
|
138
|
+
previously-gated channel are still correctly persisted though --
|
|
139
|
+
save_gating_list writes them from gating_channels, which is never
|
|
140
|
+
wiped -- so a channel counts as "has a gate" here by comparing its
|
|
141
|
+
stored gate_start/gate_end against its own true full data range
|
|
142
|
+
(get_datasource_description), exactly like the marker dropdown's
|
|
143
|
+
green-dot indicator does client-side (viewerSidebar.js's
|
|
144
|
+
hasCustomGate), not by trusting gate_active."""
|
|
145
|
+
post_data = json.loads(request.data)
|
|
146
|
+
datasource = post_data['datasource']
|
|
147
|
+
table_name = post_data.get('table_name') or 'gates'
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
dataset = api.dataset(datasource)
|
|
151
|
+
except KeyError:
|
|
152
|
+
return jsonify(success=False, error="Unknown datasource"), 400
|
|
153
|
+
# A SpatialData datasource's gates go into the uns of the one table it
|
|
154
|
+
# was imported from, using the same codec -- see anndata_gates._open_group.
|
|
155
|
+
if dataset.source_kind not in ('anndata', 'spatialdata'):
|
|
156
|
+
return jsonify(success=False, error="Not an AnnData or SpatialData datasource"), 400
|
|
157
|
+
# The role, from the project record. This used to be a free-text box on
|
|
158
|
+
# the panel defaulting to the literal 'imageid' -- the plugin asking the
|
|
159
|
+
# user for something core already had a place to store. It is declared in
|
|
160
|
+
# Requires as an optional role now, so the host collects it once and every
|
|
161
|
+
# plugin sees the same answer.
|
|
162
|
+
imageid_column = dataset.schema.image_id
|
|
163
|
+
if not imageid_column:
|
|
164
|
+
return jsonify(success=False, error="No image ID column recorded for this project",
|
|
165
|
+
needs="role:image_id"), 400
|
|
166
|
+
|
|
167
|
+
saved_rows = gating_model.get_saved_gating_list(datasource) or []
|
|
168
|
+
description = dataset.table.describe()
|
|
169
|
+
active_gates = {}
|
|
170
|
+
for row in saved_rows:
|
|
171
|
+
channel = row.get('channel')
|
|
172
|
+
if not channel:
|
|
173
|
+
continue
|
|
174
|
+
gate_start = row.get('gate_start')
|
|
175
|
+
gate_end = row.get('gate_end')
|
|
176
|
+
if gate_start is None or gate_end is None:
|
|
177
|
+
continue
|
|
178
|
+
desc = description.get(channel) or {}
|
|
179
|
+
if gate_start == desc.get('min') and gate_end == desc.get('max'):
|
|
180
|
+
continue # still at the full default range -- never customized
|
|
181
|
+
active_gates[channel] = gate_start
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
result = anndata_gates.save_gates_to_anndata(
|
|
185
|
+
dataset.table.source, datasource, active_gates,
|
|
186
|
+
table_name=table_name, imageid_column=imageid_column)
|
|
187
|
+
except ValueError as exc:
|
|
188
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
189
|
+
|
|
190
|
+
return jsonify(success=True, **result)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@gating_bp.route('/get_gates_from_anndata', methods=['GET'])
|
|
194
|
+
def get_gates_from_anndata():
|
|
195
|
+
"""Read-only counterpart of /save_gates_to_anndata -- lets the sidebar
|
|
196
|
+
pick up gates already present in adata.uns[table_name] (e.g. set up
|
|
197
|
+
outside Plexora before import) the first time a datasource with no
|
|
198
|
+
Plexora-side saved gating list is opened."""
|
|
199
|
+
datasource = request.args.get('datasource')
|
|
200
|
+
table_name = request.args.get('table_name') or 'gates'
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
dataset = api.dataset(datasource)
|
|
204
|
+
except KeyError:
|
|
205
|
+
return jsonify(success=False, error="Unknown datasource"), 400
|
|
206
|
+
# A SpatialData datasource's gates go into the uns of the one table it
|
|
207
|
+
# was imported from, using the same codec -- see anndata_gates._open_group.
|
|
208
|
+
if dataset.source_kind not in ('anndata', 'spatialdata'):
|
|
209
|
+
return jsonify(success=False, error="Not an AnnData or SpatialData datasource"), 400
|
|
210
|
+
imageid_column = dataset.schema.image_id
|
|
211
|
+
if not imageid_column:
|
|
212
|
+
# Read-only path: no image id recorded means no gates to find, which
|
|
213
|
+
# is an ordinary empty result rather than something to demand of the
|
|
214
|
+
# user. The save path asks; this one does not.
|
|
215
|
+
return jsonify(success=True, image_id=datasource, gates={})
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
result = anndata_gates.load_gates_from_anndata(
|
|
219
|
+
dataset.table.source, datasource,
|
|
220
|
+
table_name=table_name, imageid_column=imageid_column)
|
|
221
|
+
except ValueError as exc:
|
|
222
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
223
|
+
|
|
224
|
+
return jsonify(success=True, **result)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@gating_bp.route('/get_uploaded_gating_csv_values', methods=['GET'])
|
|
228
|
+
def get_gating_csv_values():
|
|
229
|
+
datasource = request.args.get('datasource')
|
|
230
|
+
file_path = _files(datasource) / 'uploaded_gates.csv'
|
|
231
|
+
if file_path.is_file() == False:
|
|
232
|
+
abort(422)
|
|
233
|
+
csv = pl.read_csv(file_path)
|
|
234
|
+
obj = csv.to_dicts()
|
|
235
|
+
return api.json_response(obj)
|