ocdkit 0.0.7__tar.gz → 0.0.8__tar.gz
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.
- {ocdkit-0.0.7 → ocdkit-0.0.8}/.github/workflows/test_and_deploy.yml +27 -10
- {ocdkit-0.0.7/src/ocdkit.egg-info → ocdkit-0.0.8}/PKG-INFO +3 -1
- ocdkit-0.0.8/docs/3d_integration_plan.md +80 -0
- ocdkit-0.0.8/docs/3d_viewer_STATUS.md +62 -0
- ocdkit-0.0.8/docs/3d_viewer_plan.md +192 -0
- ocdkit-0.0.8/docs/tileserve_out_of_process.md +136 -0
- ocdkit-0.0.8/outputs/repro/3d_backend/explore_3d_core.py +100 -0
- ocdkit-0.0.8/outputs/repro/label_gpu/parity.py +236 -0
- ocdkit-0.0.8/outputs/repro/volume_2d3d_suite/run.py +189 -0
- ocdkit-0.0.8/outputs/repro/volume_3dpick/run.py +111 -0
- ocdkit-0.0.8/outputs/repro/volume_color_match/run.py +113 -0
- ocdkit-0.0.8/outputs/repro/volume_fill/run.py +115 -0
- ocdkit-0.0.8/outputs/repro/volume_mode_integration/run.py +168 -0
- ocdkit-0.0.8/outputs/repro/volume_nav/run.py +84 -0
- ocdkit-0.0.8/outputs/repro/volume_persist/run.py +92 -0
- ocdkit-0.0.8/outputs/repro/volume_undo/run.py +147 -0
- ocdkit-0.0.8/outputs/repro/wgpu_raymarch_headless/proof.py +225 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/pyproject.toml +5 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/figure.py +1512 -253
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/figure_server.py +20 -5
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/load/module.py +90 -10
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/__init__.py +40 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/defaults.py +76 -42
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/display.py +8 -6
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/image_grid.py +356 -107
- ocdkit-0.0.8/src/ocdkit/plot/linked_cell.py +349 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/ncolor.py +25 -1
- ocdkit-0.0.8/src/ocdkit/plot/pyramid.py +10 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/svg.py +17 -6
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/web/colormap_image.js +12 -1
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/web/hdr_colormap.js +10 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/web/label_gl.js +209 -81
- ocdkit-0.0.8/src/ocdkit/plot/web/label_gpu.js +358 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/web/spectra_density_gl.js +377 -56
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tileserve/__init__.py +6 -4
- ocdkit-0.0.8/src/ocdkit/tileserve/_proc.py +242 -0
- ocdkit-0.0.8/src/ocdkit/tileserve/embed.py +110 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tileserve/jupyter_ext.py +26 -4
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tileserve/layout.py +40 -7
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tileserve/server.py +206 -19
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tileserve/viewer.py +225 -320
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/app.py +9 -5
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/assets.py +64 -16
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/plugins/base.py +8 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/routers/__init__.py +2 -2
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/routers/index.py +9 -7
- ocdkit-0.0.8/src/ocdkit/viewer/routers/session_routes.py +474 -0
- ocdkit-0.0.8/src/ocdkit/viewer/routers/volume.py +52 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/routes.py +8 -3
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/schemas.py +4 -0
- ocdkit-0.0.8/src/ocdkit/viewer/session.py +1084 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/app.js +153 -14
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/css/controls.css +14 -4
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/css/layout.css +12 -4
- ocdkit-0.0.8/src/ocdkit/viewer/web/css/viewer.css +145 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/html/left-panel.html +25 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/html/viewer.html +13 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/index.html +4 -4
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/blit.wgsl +21 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/cubes.wgsl +57 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/file-navigation.js +13 -1
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/hdr_ui.js +33 -6
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/mat4.js +150 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/overlay.wgsl +31 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/raymarch.wgsl +222 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/raymarch_compute.wgsl +176 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/volume-mode.js +663 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/volume3d-gpu.js +833 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/volume3d-overlays-gpu.js +92 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/volume3d-overlays.js +165 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/volume3d-view.js +172 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/js/volume3d.js +212 -0
- ocdkit-0.0.8/src/ocdkit/viewer/web/volume.html +225 -0
- ocdkit-0.0.8/src/ocdkit/wgpu/__init__.py +114 -0
- ocdkit-0.0.8/src/ocdkit/wgpu/aggregators.py +840 -0
- ocdkit-0.0.8/src/ocdkit/wgpu/core.py +488 -0
- ocdkit-0.0.8/src/ocdkit/wgpu/lines.py +3072 -0
- ocdkit-0.0.8/src/ocdkit/wgpu/scatter.py +307 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8/src/ocdkit.egg-info}/PKG-INFO +3 -1
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit.egg-info/SOURCES.txt +57 -2
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit.egg-info/requires.txt +3 -0
- ocdkit-0.0.8/src/ocdkit.egg-info/scm_file_list.json +332 -0
- ocdkit-0.0.8/src/ocdkit.egg-info/scm_version.json +8 -0
- ocdkit-0.0.8/tests/js/emit_camera.mjs +20 -0
- ocdkit-0.0.8/tests/js/mat4.test.mjs +81 -0
- ocdkit-0.0.8/tests/js/overlays3d.test.mjs +85 -0
- ocdkit-0.0.8/tests/js/volume3d.test.mjs +132 -0
- ocdkit-0.0.8/tests/test_overlay_wgsl.py +94 -0
- ocdkit-0.0.8/tests/test_raymarch_wgsl.py +349 -0
- ocdkit-0.0.8/tests/test_volume_autosave.py +113 -0
- ocdkit-0.0.8/tests/test_volume_fill.py +132 -0
- ocdkit-0.0.8/tests/test_volume_nav.py +83 -0
- ocdkit-0.0.8/tests/test_volume_open.py +319 -0
- ocdkit-0.0.8/tests/test_volume_page.py +128 -0
- ocdkit-0.0.8/tests/test_volume_page_assets.py +47 -0
- ocdkit-0.0.8/tests/test_volume_pick3d.py +86 -0
- ocdkit-0.0.8/tests/test_volume_route.py +91 -0
- ocdkit-0.0.8/tests/test_volume_undo.py +196 -0
- ocdkit-0.0.8/tests/test_wgpu.py +155 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/viewer/test_async_dispatch.py +1 -1
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/viewer/test_ui_mode.py +24 -18
- ocdkit-0.0.7/badges/coverage.svg +0 -1
- ocdkit-0.0.7/badges/tests.svg +0 -1
- ocdkit-0.0.7/src/ocdkit/viewer/routers/session_routes.py +0 -192
- ocdkit-0.0.7/src/ocdkit/viewer/session.py +0 -311
- ocdkit-0.0.7/src/ocdkit/viewer/web/css/viewer.css +0 -85
- ocdkit-0.0.7/src/ocdkit/viewer/web/html/viewer.html +0 -7
- {ocdkit-0.0.7 → ocdkit-0.0.8}/.gitignore +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/LICENSE +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/MANIFEST.in +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/README.md +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/docs/plot-backend-roadmap.md +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/docs/plugin-authoring.md +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/docs/pywebview-desktop-integration.md +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/jupyter-config/jupyter_server_config.d/ocdkit_tileserve.json +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/bench_colorize.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/bench_contour_alignment.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/bench_hdr_cmap.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/bench_vector_contours.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/bench_vector_contours_tier2.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/check_compare_pixels.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/check_hdr_cmap.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/check_image_grid_auto_color.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/check_jxl_p3_bytes.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/check_playwright_render.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/check_uhdr_sdr_base.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/coverage_cross_device.env.example +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/coverage_cross_device.sh +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/hdr_harness.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/scripts/make_chromaticity_p3.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/setup.cfg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/__main__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/convert.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/filters.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/imports.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/index.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/morphology.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/normalize.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/ops.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/parallel.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/spatial.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/transform.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/union_find.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/array/warp.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/cli/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/cli/__main__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/cli/main.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/cli/migrate.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/cli/paths.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/desktop/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/desktop/pinning.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/imports.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/fig_export.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/files.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/image.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/imports.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/live_figure.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/mpl_axes_cropper.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/path.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/pptx.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/io/result.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/load/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/load/object.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/logging/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/logging/handler.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/measure/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/measure/bbox.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/measure/diameter.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/measure/imports.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/measure/medoid.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/measure/metrics.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/bench.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/color.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/composite.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/composite_grid.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/contour.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/export.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/figure.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/grid.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/hdr_cmap.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/imports.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/label.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/layout.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/luts.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/style.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/text_metrics.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/web/hdr_headroom.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/web/scatter_gl.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/web/spectra_density_element.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/plot/web/tile_grid_element.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/testing/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/testing/collisions.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/testing/imports.py +0 -0
- /ocdkit-0.0.7/src/ocdkit/plot/pyramid.py → /ocdkit-0.0.8/src/ocdkit/tileserve/_pyramid.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tileserve/headless.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tileserve/shaders.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tls/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tls/external_ca.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tls/hostnames.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tls/imports.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tls/local_ca.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tls/paths.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/tls/trust.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/utils/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/utils/collections.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/utils/gpu.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/utils/kwargs.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/utils/paths.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/__main__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/cli.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/demo.html +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/dependencies.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/edr_bridge.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/exceptions.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/masks.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/middleware.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/model_registry.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/plugins/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/plugins/registry.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/plugins/schema.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/plugins/threshold.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/routers/log.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/routers/mask.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/routers/plugin.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/routers/segment.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/routers/system.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/routers/trust.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/sample_image.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/segmentation.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/system.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/css/tools.css +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/hdr_cmap_prototype.html +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/hdr_cmap_uhdr.html +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/hdr_css_test.html +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/html/_attic_omnipose-panel.html +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/html/sidebar.html +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/affinity.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/arrow-back-up.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/arrow-forward-up.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/dbscan-nested-arcs.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/download.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/droplet-half-2.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/eraser.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/home-2.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/minus.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/palette.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/pencil.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/plus.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/rotate-rectangle.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/icons/topology-star.svg +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/brush.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/colormap.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/debug-apple-material.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/hdr_image_layer.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/history.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/interactions.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/logging.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/mask-pipeline.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/painting.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/plugin-panel.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/pointer-state.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/state-persistence.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/tooltip-editor.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/ui-utils.js +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit/viewer/web/js/wasm_fill.c +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit.egg-info/dependency_links.txt +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit.egg-info/entry_points.txt +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/src/ocdkit.egg-info/top_level.txt +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/e2e/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/e2e/conftest.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/e2e/test_browser_smoke.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/e2e/test_pywebview_snapshot.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/fixtures/multichan_3c_4x4.czi +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/fixtures/tiny_8x8.czi +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_array.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_gpu.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_import_cycles.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_io.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_io_live_figure.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_io_pptx.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_measure.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_module_collisions.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_module_discovery.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_morphology.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_paths_migration.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_plot_color.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_plot_contour.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_plot_display.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_plot_export.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_plot_figure.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_plot_grid.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_plot_label.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_plot_notebook.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_registration.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_slice.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_spatial.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/test_tileserve_pick_level.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/viewer/__init__.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/viewer/test_active_plugin_cache.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/viewer/test_app.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/viewer/test_envelope_and_middleware.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/viewer/test_plugin_contract.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/viewer/test_session_eviction.py +0 -0
- {ocdkit-0.0.7 → ocdkit-0.0.8}/tests/viewer/test_title_config.py +0 -0
|
@@ -2,8 +2,12 @@ name: CI/CD
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
+
branches:
|
|
6
|
+
- '**' # run tests on every branch push ...
|
|
7
|
+
- '!badges' # ... except the CI-managed badges branch ([skip ci] also guards it)
|
|
5
8
|
tags:
|
|
6
|
-
- "v*"
|
|
9
|
+
- "v*" # tags still gate deploy + badge publishing (see job `if:`s)
|
|
10
|
+
pull_request:
|
|
7
11
|
|
|
8
12
|
jobs:
|
|
9
13
|
test:
|
|
@@ -30,7 +34,7 @@ jobs:
|
|
|
30
34
|
run: |
|
|
31
35
|
python -m pip install --upgrade pip
|
|
32
36
|
pip install --extra-index-url https://download.pytorch.org/whl/cpu \
|
|
33
|
-
-e . pytest pytest-cov nbformat nbclient ipykernel ipywidgets
|
|
37
|
+
-e '.[viewer]' httpx nest-asyncio pytest pytest-cov pytest-timeout nbformat nbclient ipykernel ipywidgets wgpu
|
|
34
38
|
|
|
35
39
|
- name: Run tests
|
|
36
40
|
shell: bash
|
|
@@ -40,6 +44,7 @@ jobs:
|
|
|
40
44
|
python -m pytest tests/ \
|
|
41
45
|
--cov=ocdkit --cov-report=xml --cov-report=term \
|
|
42
46
|
--junitxml=junit.xml \
|
|
47
|
+
--timeout=600 \
|
|
43
48
|
-v
|
|
44
49
|
|
|
45
50
|
- name: Upload coverage & junit artifacts
|
|
@@ -81,7 +86,7 @@ jobs:
|
|
|
81
86
|
run: python -m twine upload dist/*
|
|
82
87
|
|
|
83
88
|
badges:
|
|
84
|
-
name: Build and
|
|
89
|
+
name: Build and publish badges
|
|
85
90
|
runs-on: ubuntu-latest
|
|
86
91
|
needs: test
|
|
87
92
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
@@ -89,8 +94,6 @@ jobs:
|
|
|
89
94
|
contents: write
|
|
90
95
|
steps:
|
|
91
96
|
- uses: actions/checkout@v4
|
|
92
|
-
with:
|
|
93
|
-
ref: main
|
|
94
97
|
|
|
95
98
|
- name: Download test artifacts
|
|
96
99
|
uses: actions/download-artifact@v4
|
|
@@ -108,8 +111,22 @@ jobs:
|
|
|
108
111
|
genbadge coverage -i coverage.xml -o badges/coverage.svg
|
|
109
112
|
genbadge tests -i junit.xml -o badges/tests.svg
|
|
110
113
|
|
|
111
|
-
- name:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
- name: Publish badges to the orphan 'badges' branch
|
|
115
|
+
# Badges live on a dedicated 'badges' branch, NEVER on main. Committing
|
|
116
|
+
# them to main advanced origin/main on every release and rejected the
|
|
117
|
+
# next developer push ("Updates were rejected ... fetch first"). The
|
|
118
|
+
# branch is rebuilt from scratch and force-pushed each release, holding
|
|
119
|
+
# only the SVGs; reference them via the raw badges-branch URL, e.g.
|
|
120
|
+
# https://raw.githubusercontent.com/${{ github.repository }}/badges/badges/coverage.svg
|
|
121
|
+
run: |
|
|
122
|
+
git config user.name "github-actions[bot]"
|
|
123
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
124
|
+
svgs="$(mktemp -d)"
|
|
125
|
+
cp badges/*.svg "$svgs/"
|
|
126
|
+
git checkout --orphan badges
|
|
127
|
+
git rm -rf . >/dev/null 2>&1 || true
|
|
128
|
+
mkdir -p badges
|
|
129
|
+
cp "$svgs"/*.svg badges/
|
|
130
|
+
git add badges/coverage.svg badges/tests.svg
|
|
131
|
+
git commit -m "CI: update coverage and test badges [skip ci]"
|
|
132
|
+
git push -f origin badges
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ocdkit
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8
|
|
4
4
|
Summary: Obsessive Coder's Dependency Toolkit — Python utilities for array manipulation, GPU dispatch, image I/O, morphology, and plotting.
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -34,6 +34,8 @@ Requires-Dist: fastapi; extra == "viewer"
|
|
|
34
34
|
Requires-Dist: uvicorn[standard]; extra == "viewer"
|
|
35
35
|
Requires-Dist: imageio; extra == "viewer"
|
|
36
36
|
Requires-Dist: python-multipart; extra == "viewer"
|
|
37
|
+
Provides-Extra: gpu
|
|
38
|
+
Requires-Dist: wgpu; extra == "gpu"
|
|
37
39
|
Provides-Extra: desktop
|
|
38
40
|
Requires-Dist: pywebview; extra == "desktop"
|
|
39
41
|
Requires-Dist: pywin32; sys_platform == "win32" and extra == "desktop"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Integrating the 3D volume viewer into the MAIN ocdkit viewer
|
|
2
|
+
|
|
3
|
+
The 3D viewer currently lives as a self-contained page (`viewer/web/volume.html` +
|
|
4
|
+
`js/volume3d*.js` + `raymarch.wgsl`/`overlay.wgsl`). This plans folding it into the
|
|
5
|
+
main viewer app (the `/` page driven by `app.js`).
|
|
6
|
+
|
|
7
|
+
## Prerequisites — status
|
|
8
|
+
- **three.js-free: CONFIRMED.** No imports / importmap / vendored three.js; the 3D
|
|
9
|
+
view is pure raw-WebGPU with a quaternion arcball camera (free rotation, pan,
|
|
10
|
+
dolly). Design spec met; plan keeps it that way.
|
|
11
|
+
- 3D component (`volume3d-gpu.js` + `raymarch.wgsl` + `volume3d-overlays*`),
|
|
12
|
+
2.5D (`volume3d-view.js`), and decode (`volume3d.js`) are self-contained, consume
|
|
13
|
+
the `POST /api/volume` bundle, and are headless-tested (wgpu-native + Node +
|
|
14
|
+
Playwright). Camera incl. pan is done.
|
|
15
|
+
- Backend bundle via the `build_volume_bundle` plugin capability + `POST /api/volume`
|
|
16
|
+
(intensity uint8, label volume, flow, distance, affinity, trajectories, recon
|
|
17
|
+
points; lazy/opt-in heavy parts).
|
|
18
|
+
|
|
19
|
+
## Main viewer structure (mapped)
|
|
20
|
+
- `#viewer` (html/viewer.html:3) holds `<canvas id="canvas">` (2D WebGL2) + brush
|
|
21
|
+
preview; `assets.py:_get_layout_markup()` assembles fragments into `#app`.
|
|
22
|
+
- File load: `file-navigation.js:requestImageChange()` → `POST /api/open_image` →
|
|
23
|
+
`app.js:reinitializeForNewImage(config)`. CONFIG carries width/height/
|
|
24
|
+
imageDataUrl/imagePath/directory. **No volume/3D notion exists yet.**
|
|
25
|
+
- **No view-mode system** (only tool modes). Need a lightweight 2D⇄2.5D⇄3D switch.
|
|
26
|
+
- Per-slice reuse points: `app.js:uploadBaseTextureFromCanvas()` (1396),
|
|
27
|
+
`reinitializeForNewImage()` (10051), `maskValues` buffer (458), `draw()` (6848),
|
|
28
|
+
`resizeCanvas()` (7220).
|
|
29
|
+
- Session: `session.py` SessionState + JS CONFIG/state; viewer state persisted to
|
|
30
|
+
localStorage (`saveViewerState`).
|
|
31
|
+
|
|
32
|
+
## Recommended architecture
|
|
33
|
+
- **Embed as a view mode, keep raw-WebGPU.** Add a sibling `<canvas id="volumeViewer">`
|
|
34
|
+
inside `#viewer` (separate element → separate GPU context, avoids the getContext
|
|
35
|
+
lock), plus a 2D / 2.5D / 3D toggle. Mount the existing `VolumeGPU` on it. The
|
|
36
|
+
2D mode keeps app.js untouched (no regression).
|
|
37
|
+
- Feature-detect WebGPU; if absent, hide 3D and keep 2D/2.5D (already graceful).
|
|
38
|
+
|
|
39
|
+
## Decisions (forks) — see questions
|
|
40
|
+
1. **2.5D slice view**: (a) reuse app.js per-slice — feed each z-slice + its mask to
|
|
41
|
+
the existing 2D WebGL2 renderer so you get the full painting/ncolor/affinity
|
|
42
|
+
toolset per slice (high value, more integration into the 11k-line app.js); vs
|
|
43
|
+
(b) embed the standalone canvas2d `volume3d-view.js` (simple, isolated, fewer
|
|
44
|
+
tools).
|
|
45
|
+
2. **MVP scope**: view-only first (load volume + masks → 2.5D + 3D), with in-app 3D
|
|
46
|
+
*segmentation* as a later phase; vs include 3D segmentation now.
|
|
47
|
+
3. **Mask source**: existing masks (sidecar/precomputed) + `do_recon` points; vs run
|
|
48
|
+
3D segmentation in-app (needs the affinity bug fix below).
|
|
49
|
+
|
|
50
|
+
## Phases
|
|
51
|
+
- **A — Server volume open** (~2–3 d): detect a 3D/multi-page tiff (or `.npz`) in
|
|
52
|
+
`open_image`; set `config.isVolume` + volume metadata; serve the bundle (reuse
|
|
53
|
+
`build_volume_bundle`). File navigator already lists files.
|
|
54
|
+
- **B — Embed 3D in the app** (~3–5 d): sibling `#volumeViewer` canvas + view-mode
|
|
55
|
+
toggle; mount `VolumeGPU`; port the volume.html control panel (mode / density /
|
|
56
|
+
zScale / image+label layers / shading / overlays / reset) into a main-viewer
|
|
57
|
+
panel section. Wire resize.
|
|
58
|
+
- **C — 2.5D slice nav** (per decision #1): reuse app.js per-slice (slice slider +
|
|
59
|
+
per-slice overlays + painting) ~3–5 d, OR embed `volume3d-view.js` ~1–2 d.
|
|
60
|
+
- **D — Polish** (~2–3 d): persist camera/slice/render settings in viewer state;
|
|
61
|
+
volume-aware file navigator (next/prev volume); status/HUD.
|
|
62
|
+
- **E — Later**: cross-slice painting, saving 3D masks.
|
|
63
|
+
|
|
64
|
+
## Native 3D segmentation — FIXED (not divergence)
|
|
65
|
+
3D `affinity_seg` crashed for two reasons, both in `omnipose/core/masks.py` (not
|
|
66
|
+
`divergence`, whose batched-torch contract is correct and shared with `loss.py`):
|
|
67
|
+
1. `_get_affinity_torch` is batched-design (`(B,D,*spatial)` → `(S,B,*DIMS)`, with a
|
|
68
|
+
downstream `.squeeze()` that drops B), but `masks.py` called it with **unbatched**
|
|
69
|
+
inputs. Fix: add `[None]` (B=1) at the call site.
|
|
70
|
+
2. `flow_error` (flow-QC) called `masks_to_flows_batch` without `dim`, defaulting to
|
|
71
|
+
`dim=2` on 3D data. Fix: pass `dim=maski.ndim`.
|
|
72
|
+
Verified end-to-end on synthetic + the real spacetime crop (2D + 3D). These edits
|
|
73
|
+
live in the WIP `masks.py` (uncommitted), to be committed with the core refactor.
|
|
74
|
+
|
|
75
|
+
## Risks
|
|
76
|
+
- app.js is 11k lines — embedding must keep the 2D path byte-identical; 3D is an
|
|
77
|
+
additive sibling canvas + a view flag.
|
|
78
|
+
- WebGPU only for the 3D mode (feature-detected); 2D/2.5D cover the rest.
|
|
79
|
+
- 3D `affinity_seg` divergence bug blocks in-app 3D segmentation until fixed.
|
|
80
|
+
- One GPU context per canvas (3D canvas is its own element — already the pattern).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 3D viewer build — STATUS
|
|
2
|
+
|
|
3
|
+
Branch `feat/3d-viewer` in both repos (omnipose backend, ocdkit frontend).
|
|
4
|
+
Autonomous build against the plan in `3d_viewer_plan.md`. No push.
|
|
5
|
+
|
|
6
|
+
## ✅ COMPLETE — P0–P3 implemented, all headless suites green
|
|
7
|
+
Final tally: omnipose pytest 15 · ocdkit pytest 11 (route 4, page 2, raymarch 4,
|
|
8
|
+
overlay 1) · Node 23 (mat4 5, volume3d 13, overlays3d 5) · wgpu-native proof PASS.
|
|
9
|
+
Commits — omnipose: 881d7b2 (single clean backend commit; see note). ocdkit:
|
|
10
|
+
3bd3ef9, a85201c, 5fafa99, abbf156, fba299a, ce5db13, 34d4ffb, 3274413, d77a547.
|
|
11
|
+
Not pushed.
|
|
12
|
+
|
|
13
|
+
NOTE (omnipose history): the first backend commit accidentally swept in a large
|
|
14
|
+
pre-staged refactor that was already in the index (test renames, networks/*, etc.
|
|
15
|
+
— NOT 3D-viewer work). Fixed by resetting feat/3d-viewer to main and re-committing
|
|
16
|
+
ONLY the 4 viewer files as 881d7b2. Your refactor is fully preserved, just back to
|
|
17
|
+
unstaged working-tree state (`git add` to re-stage). ocdkit commits were unaffected.
|
|
18
|
+
|
|
19
|
+
ONE thing needs a real-WebGPU browser (you) for final visual confirmation: the
|
|
20
|
+
live WebGPU device render of the 3D volume + overlays. Its shader, camera math,
|
|
21
|
+
and uniform/texture byte-layouts are each individually headless-verified
|
|
22
|
+
(wgpu-native + Node); only the in-browser device path can't run in headless
|
|
23
|
+
Chromium (no adapter). Open the viewer in Chrome/Safari, or use Deno, to confirm.
|
|
24
|
+
To view: serve the viewer and open `/static/volume.html?masks=<path>&raw=<path>`
|
|
25
|
+
(e.g. the spacetime stack), toggle 2.5D⇄3D.
|
|
26
|
+
|
|
27
|
+
## Legend
|
|
28
|
+
[x] done + headless-tested · [~] in progress · [ ] todo
|
|
29
|
+
|
|
30
|
+
## P0 — backend dim-generic engine
|
|
31
|
+
- [x] Empirically mapped omnipose 3D core API (masks_to_flows→Result(.mu (3,Z,H,W), .dists), masks_to_affinity/spatial_affinity→(27,Z,H,W), kernel_setup(3)→26 non-centre steps). Script: `ocdkit/outputs/repro/3d_backend/explore_3d_core.py`
|
|
32
|
+
- [x] `omnipose/gui/_volume3d.py` payload engine: kernel_steps, flow_and_dist, affinity_volume, flow_rgb_slices (in-plane 2.5D), rgb_flow_3d (directional), dist_rgb_slices, points_from_p, parse_links, trajectories (centroid tracks + lineage), encode/decode (gzip+b64, label-dtype narrowing), build_bundle, bundle_from_files
|
|
33
|
+
- [x] `Segmenter.build_volume_bundle(...)` delegating method (routes flow solve through the GPU device)
|
|
34
|
+
- [x] pytest `omnipose/tests/test_volume3d.py` — 15 pass incl. real spacetime crop (mask roundtrip exact, flow (3,Z,H,W), affinity (26,Z,H,W), 36 lineage edges, every parent→2 daughters)
|
|
35
|
+
- [ ] (deferred to P1 wiring) `do_3D` widget + segment() volume model-eval path — the GT-mask bundle path covers the test case; model-3D is a later refinement
|
|
36
|
+
|
|
37
|
+
## P1 — 2.5D slice frontend (existing WebGL2)
|
|
38
|
+
- [x] client logic `viewer/web/js/volume3d.js`: decodeArray (gzip/b64/typed incl float16), volume/rgb slice views, in/through-plane affinity split, affinity slice segments (deduped), points-near-slice, trajectory projection + lineage segments
|
|
39
|
+
- [x] Node CPU-harness `tests/js/volume3d.test.mjs` — 13 pass incl. exact Python->JS cross-language decode (uint8/float16/uint32). Runtimes: node v26 + deno at /opt/homebrew/bin
|
|
40
|
+
- [x] plugin capability `build_volume_bundle` (base.py contract + manifest flag; omnipose ocdkit_plugin.py + Segmenter.build_volume_bundle_from_files) + ocdkit route `POST /api/volume` (routers/volume.py, registered). Tests `tests/test_volume_route.py` — 4 pass incl. end-to-end through the real omnipose plugin on the spacetime stack (133x302x302, 40 labels, 36 lineage edges). Installed httpx + python-multipart for TestClient.
|
|
41
|
+
- [x] DECISION: built a self-contained volume-viewer PAGE (viewer/web/volume.html + js/volume3d-view.js) using volume3d.js, NOT editing the 11k-line app.js. Zero regression risk; one mount point for 2.5D + P2 WebGPU-3D. Existing app.js untouched, so "depth==1 identical" holds trivially.
|
|
42
|
+
- [x] 2.5D slice page: fetch POST /api/volume (or injected __TEST_BUNDLE__), canvas2d slice render (image/flow/distance/mask) + slice slider/wheel/arrow-keys; async gzip decode via DecompressionStream; refactored volume3d.js to expose bytesToTyped/b64ToBytes
|
|
43
|
+
- [x] per-slice overlays (affinity in-plane segments, points-near-z, trajectory projection + lineage dashed)
|
|
44
|
+
- [x] Playwright smoke test `tests/test_volume_page.py` — PASS in headless Chromium: renders non-blank, slices navigate, all 4 layers render, affinity+trajectory overlays draw, no JS errors
|
|
45
|
+
|
|
46
|
+
## P2 — true-3D volume (raw WebGPU, no three.js)
|
|
47
|
+
- [x] (prereq) headless WGSL ray-march proven via wgpu-native: `ocdkit/outputs/repro/wgpu_raymarch_headless/proof.py`
|
|
48
|
+
- [x] P2a: canonical `viewer/web/js/raymarch.wgsl` (perspective/ortho via invViewProj; MIP/additive/mean; intensity texture_3d<f32> + label texture_3d<u32>; in-shader label colour matching the 2.5D view; density/labelOpacity/showLabels uniforms). Validated by `tests/test_raymarch_wgsl.py` (wgpu-native, loads the EXACT shipped file) — 3 pass: MIP==np.max, mean==np.mean, label colour+blend exact.
|
|
49
|
+
- [x] P2b: pure camera math `viewer/web/js/mat4.js` (column-major, WebGPU [0,1]-depth perspective, lookAt, invert, orbit) — Node-tested `tests/js/mat4.test.mjs` (5 pass: invert∘mat=I, orbit geometry, invViewProj→centre-ray=forward, project/unproject round-trip). Browser host `viewer/web/js/volume3d-gpu.js` (raw WebGPU, no three.js: feature-detect→null, rgba16float display-p3 canvas, orbit camera, r32float intensity + uint label 3D textures with byte layout matching the verified wgpu-native harness, render loop, mode/density/labelOpacity/zScale + drag-orbit/wheel). Perspective integration verified: `tests/test_raymarch_wgsl.py` bridges the shipped mat4.js (via Node emit_camera.mjs) into the shader → centred cube projects to centred pixels (4 wgpu-native tests pass). Live WebGPU device render needs a real browser (user check).
|
|
50
|
+
- [x] P2c: wired into volume.html — second canvas #stage3d (separate context to avoid getContext locking), 2.5D<->3D toggle, MIP/additive/mean buttons + density/zScale/labelOpacity sliders + showLabels (call VolumeGPU methods), shares the decoded bundle (vv.d). Graceful fallback: VolumeGPU.create returns null without a usable adapter -> "WebGPU unavailable", reverts to 2.5D. Playwright test `tests/test_volume_page.py::test_3d_toggle_degrades_or_renders` PASS (headless Chromium has navigator.gpu but no adapter -> verified clean fallback, 2.5D still renders, no JS errors).
|
|
51
|
+
- NOTE: live WebGPU device render is the one piece not headless-verifiable here (bundled Chromium has no adapter). Its shader (P2a) + camera (P2b) + uniform/texture byte-layout (matching the verified wgpu-native harness) are all tested; needs a real-WebGPU browser (Chrome/Safari) or Deno for final visual confirmation.
|
|
52
|
+
|
|
53
|
+
## P3 — 3D overlays (raw-WebGPU line primitives)
|
|
54
|
+
- [x] P3a: pure builders `viewer/web/js/volume3d-overlays.js` — trajPolylines3D, lineageSegs3D, pointCrosses3D (points as 3D crosses), flowQuiver3D (subsampled, dir-coloured), affinitySegs3D (deduped + decimated with logged cap). All emit line segments in voxel coords + per-vertex colour. Node-tested `tests/js/overlays3d.test.mjs` (5 pass: counts, coords, colour determinism, dedup, cap).
|
|
55
|
+
- [x] P3b: `viewer/web/js/overlay.wgsl` line-list shader (voxel->world via box uniforms -> viewProj, per-vertex colour). wgpu-native test `tests/test_overlay_wgsl.py` (1 pass: known segment -> coloured pixels at expected row/cols).
|
|
56
|
+
- [x] P3c: `viewer/web/js/volume3d-overlays-gpu.js` OverlayLayer (builds GPU buffers from builders, draws into VolumeGPU's render pass sharing the camera). Integrated into volume3d-gpu.js (render computes camera once, draws overlays on top) + decodeBundle now decodes flow.raw. volume.html: 3D overlay checkboxes (trajectories+lineage / points / flow / affinity) -> vgpu.setOverlay. Playwright page tests still pass (no JS errors, graceful degradation).
|
|
57
|
+
|
|
58
|
+
## Verification channels
|
|
59
|
+
- backend: `python -m pytest omnipose/tests/test_volume3d.py`
|
|
60
|
+
- WGSL: `python ocdkit/outputs/repro/wgpu_raymarch_headless/proof.py`
|
|
61
|
+
- JS logic: Node CPU harness (P1+)
|
|
62
|
+
- browser integration (eventual, needs you): real Chrome / Deno / pywebview
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# 3D segmentation + visualization for the omnipose / ocdkit viewer
|
|
2
|
+
|
|
3
|
+
Scope + phased implementation plan. Test case: spacetime cells
|
|
4
|
+
`/Volumes/DataDrive/3D_spacetime/linked/a_baylii/dnaA_xy1_crop.tif`
|
|
5
|
+
(133 time-frames × 302 × 302; masks have 40 labels = spacetime tubes;
|
|
6
|
+
`_links.txt` = division lineage, e.g. `1,7 / 1,8` = cell 1 → daughters 7,8).
|
|
7
|
+
|
|
8
|
+
## Key findings (from code survey)
|
|
9
|
+
|
|
10
|
+
- **omnipose core is already dimension-generic.** Flows, affinity graph, distance,
|
|
11
|
+
and Euler-integration points compute as `(dim, *spatial)` for `dim=2` or `3`;
|
|
12
|
+
`eval.py` already has a `do_3D` path. `kernel_setup(dim)` yields 8 steps (2D) or
|
|
13
|
+
26 steps (3D).
|
|
14
|
+
- **The two presentation layers are 2D-locked:**
|
|
15
|
+
- Segmenter `src/omnipose/gui/_segmenter.py` squeezes 3D input to 2D
|
|
16
|
+
(`arr.mean(axis=-1)`, ~L156), guards affinity on `mask.ndim==2` (~L432),
|
|
17
|
+
emits `[y,x]` points (~L862).
|
|
18
|
+
- Frontend `ocdkit/src/ocdkit/viewer/web/app.js` (~11.5k LOC) assumes a single
|
|
19
|
+
`H×W` plane: 3×3 affine viewport (~L5754), 2D textures, 8-step affinity
|
|
20
|
+
(~L6065), `[y,x]` points (~L8727).
|
|
21
|
+
- **One genuine gap:** `ocdkit/plot/color.py:rgb_flow` maps `dy + dx·i` to a complex
|
|
22
|
+
plane → no 3D analog (can't HSV a 3-vector that way).
|
|
23
|
+
- **Two distinct graph objects** for spacetime: the spatial **affinity graph**
|
|
24
|
+
(26-neighbor voxel connectivity, already in core) and the temporal **lineage /
|
|
25
|
+
trajectory** (label→label division edges, currently only in the `_links.txt`
|
|
26
|
+
sidecar — not emitted by the viewer at all).
|
|
27
|
+
|
|
28
|
+
## Architecture decision (settled)
|
|
29
|
+
|
|
30
|
+
- **2D / 2.5D slice view:** keep the existing hand-rolled **WebGL2** pipeline in
|
|
31
|
+
`app.js`, driven by a slice index. When depth==1 it must be byte-identical to
|
|
32
|
+
today (no regression).
|
|
33
|
+
- **True 3D view:** a NEW, isolated **raw-WebGPU** layer (own canvas), **no
|
|
34
|
+
three.js.** Rationale: the colormaps repo started on three.js (`js/3d-view.js`)
|
|
35
|
+
then deliberately replaced it with a hand-rolled raw-WebGPU renderer
|
|
36
|
+
(`js/webgpu-view.js`, header: *"Replaces Three.js — native P3 HDR support
|
|
37
|
+
(rgba16float + display-p3)"*) for HDR control; ocdkit's own
|
|
38
|
+
`plot/web/colormap_image.js` already follows the same "WebGPU(HDR) → WebGL2(SDR)"
|
|
39
|
+
hand-rolled idiom. Reuse:
|
|
40
|
+
- **colormaps `js/webgpu-view.js`** — LINE / POINT / THICK_LINE / SURFACE
|
|
41
|
+
pipelines + `mat4x4 mvp` camera + display-p3 HDR canvas → affinity edges,
|
|
42
|
+
points, trajectory polylines, bbox/axes, label isosurfaces.
|
|
43
|
+
- **hostpkg `prototyping/sim3d` ray-march** (AABB slab + MIP/additive/mean +
|
|
44
|
+
`Data3DTexture`) → the volume. Its TSL needs rewriting to plain WGSL, but the
|
|
45
|
+
algorithm ports verbatim (already proven, see below).
|
|
46
|
+
- **WebGPU-only for the 3D view is acceptable** (current Chrome/Safari/Edge have
|
|
47
|
+
WebGPU); a WebGL2 ray-march fallback can be added later if needed. The 2.5D tier
|
|
48
|
+
covers non-WebGPU browsers.
|
|
49
|
+
|
|
50
|
+
## Testing strategy (verified)
|
|
51
|
+
|
|
52
|
+
- **WGSL / pipelines → wgpu-native in Python, headless, no browser.** Proven on
|
|
53
|
+
this MBP (Apple M5 Max, Metal, `wgpu` 0.31.0): the hostpkg ray-march algorithm
|
|
54
|
+
ported to raw WGSL renders MIP/mean/emission-absorption from two orthographic
|
|
55
|
+
directions and matches `np.max` / `np.mean` / `1-prod(1-v)` exactly to fp16
|
|
56
|
+
(~5e-4). Harness: `outputs/repro/wgpu_raymarch_headless/proof.py`. Extend it for
|
|
57
|
+
every new WGSL stage (label colormap, blend, streamlines) BEFORE it ships.
|
|
58
|
+
- **JS host logic** (camera matrices, buffer packing, slice indexing, payload
|
|
59
|
+
decode) → Node CPU-harness pattern (load the real module, call internals on
|
|
60
|
+
synthetic inputs, diff vs NumPy).
|
|
61
|
+
- **True browser integration** (canvas context-type locking, live EDR headroom):
|
|
62
|
+
real Google Chrome via Playwright `channel="chrome" --headless=new
|
|
63
|
+
--enable-unsafe-webgpu`, or Deno (native `navigator.gpu`), or the pywebview
|
|
64
|
+
launcher for visual HDR checks. Playwright's *bundled* Chromium has no WebGPU.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Phase 0 — Backend goes dimension-generic (~2–4 days)
|
|
69
|
+
|
|
70
|
+
Files: `omnipose/src/omnipose/gui/_segmenter.py`, `omnipose/src/omnipose/gui/ocdkit_plugin.py`,
|
|
71
|
+
`ocdkit/src/ocdkit/plot/color.py`.
|
|
72
|
+
|
|
73
|
+
1. **`ocdkit_plugin.py`:** add a `WidgetSpec` toggle `do_3D` (a.k.a. "Volume / 3D");
|
|
74
|
+
`_coerce_settings` forwards it. The host viewer surfaces it.
|
|
75
|
+
2. **`segment()`:** when `do_3D`, treat input as `(Z,H,W)` volume — do NOT
|
|
76
|
+
`mean(axis=-1)`. Thread `dim=3` into `model.eval` (uses the existing `do_3D`
|
|
77
|
+
path). Distinguish `(H,W,C)` color vs `(Z,H,W)` volume via the explicit flag,
|
|
78
|
+
not a shape heuristic.
|
|
79
|
+
3. **Cache 3D-shaped products:** `dP (3,Z,H,W)`, `dist (Z,H,W)`,
|
|
80
|
+
`affinity (26,Z,H,W)`, `p (3,Z,H,W)`, `mask (Z,H,W)`.
|
|
81
|
+
4. **`get_affinity_graph_payload()`:** drop the `mask.ndim != 2` guard; emit
|
|
82
|
+
`steps (26,3)` + a `dim`/`depth` field. **Do NOT ship the whole 26×Z×H×W array**
|
|
83
|
+
(~300 MB for the test stack) — provide a **per-slice** endpoint
|
|
84
|
+
(`affinity(z)`) the frontend requests on demand, plus gzip. (Slice view needs
|
|
85
|
+
only one z at a time; true-3D affinity is region-on-demand — see P3.)
|
|
86
|
+
5. **`get_points_payload()`:** emit `[z,y,x]` interleaved (3/point) + `dim` field.
|
|
87
|
+
6. **Flow visualization:**
|
|
88
|
+
- *Cheap, ships in P0:* per-slice in-plane RGB — run existing `rgb_flow` on
|
|
89
|
+
`dP[1:, z]` (the `(dy,dx)` components) for each z → a depth-stack of PNGs
|
|
90
|
+
(or a tiled atlas). Emit as a flow volume.
|
|
91
|
+
- *For true-3D later:* emit the raw `(3,Z,H,W)` flow as a typed-array payload
|
|
92
|
+
(downsample for size). Add `rgb_flow_3d(dP)` to `color.py` — a directional
|
|
93
|
+
colormap mapping a unit 3-vector → RGB (e.g. abs-components or a spherical
|
|
94
|
+
map) for the volumetric flow-color option.
|
|
95
|
+
7. **NEW `get_trajectory_payload()`:** parse the `_links.txt` sidecar when present,
|
|
96
|
+
else compute. Emit `{ centroids: per-label per-frame [t,y,x],
|
|
97
|
+
edges: [[parent,daughter],...] }`. This is the "trajectories" overlay.
|
|
98
|
+
8. **Payload sizing:** narrow mask dtype when `max_label` fits (uint8/uint16),
|
|
99
|
+
gzip volume payloads. Mask `(Z,H,W)` uint32 = ~48 MB raw for the test stack.
|
|
100
|
+
|
|
101
|
+
Tests: plain pytest on the spacetime tif — assert payload shapes/dtypes. No GPU.
|
|
102
|
+
|
|
103
|
+
## Phase 1 — 2.5D slice scrolling (~3–5 days) ← biggest value/effort ratio
|
|
104
|
+
|
|
105
|
+
Files: `app.js` (volume data model + slice upload), new `js/volume-nav.js`,
|
|
106
|
+
`html/sidebar.html` / controls (slice slider), css. Existing WebGL2 path only.
|
|
107
|
+
|
|
108
|
+
1. **Volume data model:** hold image/mask/overlay **volumes** `(Z,H,W)` + `currentZ`
|
|
109
|
+
+ axis label ("t" for spacetime). Decode the new volume payloads. depth==1 →
|
|
110
|
+
exactly current behavior.
|
|
111
|
+
2. **Slice navigation:** scrollwheel (over canvas), slider, arrow keys, a
|
|
112
|
+
`z: 12 / 133` readout. On change → re-upload the active slice into the existing
|
|
113
|
+
2D textures (base, mask RG, outline, flow, distance) + redraw. One H×W upload =
|
|
114
|
+
cheap.
|
|
115
|
+
3. **Per-slice overlays through the existing renderers:**
|
|
116
|
+
- Mask: index volume at z → existing Uint32 H×W path; recompute outline per slice
|
|
117
|
+
(lazy cache).
|
|
118
|
+
- Affinity: request `affinity(z)`; draw in-plane steps (`dz==0`) as GL_LINES;
|
|
119
|
+
optionally mark through-plane steps (`dz≠0`) as dots.
|
|
120
|
+
- Points: filter `[z,y,x]` to `|pz - z| < 0.5` → existing GL_POINTS.
|
|
121
|
+
- Flow / distance: index the per-slice PNG stack.
|
|
122
|
+
- Trajectories: project centroid tracks to 2D; draw the polyline up to current t
|
|
123
|
+
with a marker at frame t (reuse the lines/points overlay).
|
|
124
|
+
4. **No-regression guard:** explicit test that depth==1 output matches current.
|
|
125
|
+
|
|
126
|
+
Delivers "scroll through the stack and see flow / affinity / points / trajectories"
|
|
127
|
+
on the proven-stable WebGL2 path, with zero WebGPU dependency.
|
|
128
|
+
|
|
129
|
+
## Phase 2 — True 3D volume view (~1–2 weeks)
|
|
130
|
+
|
|
131
|
+
New raw-WebGPU layer (no three.js), under `viewer/web/js/volume3d/`:
|
|
132
|
+
|
|
133
|
+
- `renderer.js` — adapter/device init (feature-detect WebGPU; hide the 3D toggle if
|
|
134
|
+
absent), HDR `rgba16float` display-p3 canvas (copy `colormap_image.js` /
|
|
135
|
+
colormaps `webgpu-view.js`), render loop.
|
|
136
|
+
- `camera.js` — arcball/orbit + perspective `mat4` (port from colormaps
|
|
137
|
+
`webgpu-view.js`; confirm it has mouse-drag orbit, add if not).
|
|
138
|
+
- `raymarch.wgsl.js` — the proven ported ray-march; MIP / additive / mean selected
|
|
139
|
+
by uniform; **Z-scale uniform** for time anisotropy.
|
|
140
|
+
|
|
141
|
+
1. **View-mode toggle** (2.5D ⇄ 3D) mounting/unmounting the 3D canvas over the same
|
|
142
|
+
viewport region.
|
|
143
|
+
2. **Volumes:** upload raw intensity (`r16float`/`r8`) + a **label volume**
|
|
144
|
+
(`r8` for ≤255 ids, else `r32uint`) as a second 3D texture; sample both, colormap
|
|
145
|
+
labels via the existing palette LUT, blend (opacity slider). MIP/additive/mean
|
|
146
|
+
buttons + density/threshold sliders (mirror hostpkg uniforms).
|
|
147
|
+
3. **Picking:** raycast → first-hit label for hover/highlight (mirror the 2D
|
|
148
|
+
`labelAt` pattern).
|
|
149
|
+
|
|
150
|
+
Every WGSL change validated by extending `proof.py` (wgpu-native) before shipping.
|
|
151
|
+
|
|
152
|
+
## Phase 3 — 3D overlays, incremental (~1–2 weeks)
|
|
153
|
+
|
|
154
|
+
Reuse colormaps `webgpu-view.js` LINE / THICK_LINE / POINT / SURFACE pipelines.
|
|
155
|
+
|
|
156
|
+
- **Trajectories / lineage (do first — highest impact for spacetime):** 3D polylines
|
|
157
|
+
of per-label centroid tracks along the time axis + division branch points +
|
|
158
|
+
endpoint markers → the lineage shows as branching tubes. THICK_LINE + POINT.
|
|
159
|
+
- **Points:** 3D scatter of cell sinks `[z,y,x]` → POINT pipeline.
|
|
160
|
+
- **Flow:** start with subsampled 3D quiver (LINE); upgrade to streamlines
|
|
161
|
+
(integrate the raw 3-vector field → polylines) or a directional-color volume
|
|
162
|
+
(`rgb_flow_3d`).
|
|
163
|
+
- **Affinity (defer / decimate — the one real perf risk):** 26 steps × ~12M voxels
|
|
164
|
+
is huge. Options: render on-demand around the hovered cell, a coarse decimated
|
|
165
|
+
field, or keep affinity slice-only in true-3D. **Log any decimation** (no silent
|
|
166
|
+
caps).
|
|
167
|
+
- Axes / bbox + time-scale UI.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Cross-cutting risks
|
|
172
|
+
|
|
173
|
+
- **Memory** (133×302×302 ≈ 12M voxels): raw r16 ≈ 24 MB, label r8 ≈ 12 MB, flow
|
|
174
|
+
3×f32 ≈ 145 MB (downsample for 3D), **affinity 26× ≈ 300 MB** (keep slice-only /
|
|
175
|
+
region-on-demand — never ship whole).
|
|
176
|
+
- **Anisotropy:** time axis ≠ space; Z-scale uniform + UI control.
|
|
177
|
+
- **WebGPU availability:** 2.5D works everywhere (WebGL2); 3D needs WebGPU
|
|
178
|
+
(feature-detected). Optional WebGL2 ray-march fallback later.
|
|
179
|
+
- **No-regression** on the 2D path is a hard requirement.
|
|
180
|
+
|
|
181
|
+
## Effort + suggested first PR
|
|
182
|
+
|
|
183
|
+
| Phase | Scope | Estimate |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| P0 | backend dim-generic | 2–4 days |
|
|
186
|
+
| P1 | 2.5D slice view (all overlays) | 3–5 days |
|
|
187
|
+
| P2 | true-3D volume (raw WebGPU) | 1–2 weeks |
|
|
188
|
+
| P3 | 3D overlays (lineage, points, flow, affinity) | 1–2 weeks |
|
|
189
|
+
|
|
190
|
+
**First PR = P0 + P1 (~1 week):** full 2.5D viewing of the spacetime stack with all
|
|
191
|
+
overlays, end-to-end testable on `dnaA_xy1_crop.tif`, no WebGPU dependency, no
|
|
192
|
+
regression risk. P2/P3 build the rotatable volume on top.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Tile server → out-of-process (design + phasing)
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
The tileserve HTTP server runs as a **daemon thread inside the Jupyter kernel**
|
|
5
|
+
(`tileserve/server.py`, FastAPI/uvicorn on stable ports 8137–8140). When the
|
|
6
|
+
kernel computes (Run-All), the server thread is GIL-starved, so tile/attach
|
|
7
|
+
fetches stall — surfacing as `SvgFigure hi-res upgrade gave up`, soft tiles, and
|
|
8
|
+
a ~22× TTFB spike under load. Moving serving to its own process removes the GIL
|
|
9
|
+
coupling entirely.
|
|
10
|
+
|
|
11
|
+
## Current architecture (factual map)
|
|
12
|
+
- **Server**: FastAPI + uvicorn, daemon thread; `ensure_server()` picks a stable
|
|
13
|
+
port (8137–8140) and blocks until the socket accepts; `_SOURCES: dict[sid,
|
|
14
|
+
TileSource]` is the registry.
|
|
15
|
+
- **Source data** lives in kernel memory: `TileSource._pyr` (numpy pyramid
|
|
16
|
+
arrays), `.attachments` (pre-encoded byte blobs), `.meta` (small dicts).
|
|
17
|
+
- **`/tile`** (`server.py` ~321): crop the pyramid array + dtype-convert +
|
|
18
|
+
`_encode_level` (raw `tobytes()` / jxl / png) — **per-request compute, holds
|
|
19
|
+
the GIL**. **`/attach`**: returns pre-computed bytes (zero compute). `/info`,
|
|
20
|
+
`/layout`, `/grid`: dict/HTML assembly.
|
|
21
|
+
- **Production stays in the kernel**: `register_lazy` producers, `ArraySource`/
|
|
22
|
+
`_RawF16Source.get_bytes` (can touch a Scene via `resolve_linear_p3`) run once
|
|
23
|
+
in a kernel daemon thread. Only the *repeated serving* is what contends.
|
|
24
|
+
- **Proxy**: `jupyter_ext.py` maps `/ocdkit-tiles/<port>/…` → `127.0.0.1:<port>`;
|
|
25
|
+
`_ALLOWED_PORTS = {8137..8140}` mirrors `server._STABLE_PORTS`.
|
|
26
|
+
|
|
27
|
+
## Phase 0 findings (done — this de-risks the rest)
|
|
28
|
+
1. **Domain-decoupled**: serving a tile never calls a live Scene/torch object;
|
|
29
|
+
all needed data is snapshotted at register/fill/attach time. ✓ feasible.
|
|
30
|
+
2. **Import graph is heavy**: `import ocdkit.tileserve.server` pulls **torch,
|
|
31
|
+
dask, pandas, scipy, PIL** — NOT because serving needs them, but because
|
|
32
|
+
`plot/__init__.py:11,14` eagerly does `from .figure import figure` /
|
|
33
|
+
`from .image_grid import image_grid`. `plot/pyramid.py` itself is torch-free.
|
|
34
|
+
⇒ a subprocess that imports the server as-is duplicates torch (~300MB, ~2s).
|
|
35
|
+
3. Importing the server spawns **no threads** and does **not** load FastAPI/
|
|
36
|
+
uvicorn at import (lazy) — good; the process boundary is clean.
|
|
37
|
+
|
|
38
|
+
**Consequence**: add an import-decoupling step. Two options:
|
|
39
|
+
- **A (lazy `plot/__init__`)**: convert the eager figure/image_grid imports to
|
|
40
|
+
`__getattr__` lazy access (the package already uses `enable_submodules`).
|
|
41
|
+
Speeds up *all* ocdkit imports; risk = call sites that expect eager
|
|
42
|
+
`ocdkit.plot.figure` at import.
|
|
43
|
+
- **B (move the torch-free bits)**: relocate `pyramid.py` + the jxl/png byte
|
|
44
|
+
encoders into a torch-free module (e.g. `tileserve/_raster.py`) the server
|
|
45
|
+
imports directly, bypassing `plot/__init__`. Contained, lower-risk; updates
|
|
46
|
+
the few `from ..plot.pyramid import` sites.
|
|
47
|
+
- **C (accept torch first)**: ship Phase 1 importing the server as-is (heavy
|
|
48
|
+
subprocess); the baselined `setup()` pre-warm hides startup. Correctness-
|
|
49
|
+
complete (serving is off-GIL regardless); optimize imports in 1.5. ← recommended
|
|
50
|
+
for the first cut, since it fixes the symptom without import surgery.
|
|
51
|
+
|
|
52
|
+
## Phase 0 findings — host extensions + the RPC surface (also done)
|
|
53
|
+
4. **The child needs host route extensions.** `make_app()` carries the generic
|
|
54
|
+
routes (`/info /tile /attach /grid`), but hosts mount more via
|
|
55
|
+
`register_extension(fn)` — e.g. hostpkg's `serve/tiles.py:140`
|
|
56
|
+
`register_extension(_hostpkg_routes)` adds `/spectra/{sid}/{row}`. The child must
|
|
57
|
+
register the SAME extensions, so the spawn handshake passes the kernel's list of
|
|
58
|
+
extension-registering module names (e.g. `['hostpkg.serve.tiles']`); the child
|
|
59
|
+
imports them before `make_app()`. (Per Option C this also pulls the host's
|
|
60
|
+
weight — acceptable for the first cut.)
|
|
61
|
+
5. **The RPC/command surface spans BOTH packages — and all of it is snapshot-safe.**
|
|
62
|
+
The route handlers read from module-global stores populated at register time:
|
|
63
|
+
`_hostpkg_routes`'s `/spectra` serves from `_SPECTRA` (filled by
|
|
64
|
+
`register_spectra`), with **no live Scene access at request time** (verified;
|
|
65
|
+
its docstring notes all domain overlays ride the generic `/attach`). So the
|
|
66
|
+
data-population functions that must become RPC-to-child are:
|
|
67
|
+
- ocdkit: `register / register_pending / fill / register_lazy / register_array /
|
|
68
|
+
attach / drop`
|
|
69
|
+
- hostpkg (`serve/tiles.py`): `register_spectra / set_spectra_axes /
|
|
70
|
+
set_panel_axes / set_spectra_data / set_outline / set_cellinfo /
|
|
71
|
+
set_cell_contours`
|
|
72
|
+
Generalize as a **command registry**: each package registers its population
|
|
73
|
+
functions as "child commands"; the kernel-side proxies route them over the
|
|
74
|
+
control socket; the child applies them to its own stores. `register_lazy`
|
|
75
|
+
producers still run in the kernel (they need live objects); only their RESULT
|
|
76
|
+
is pushed.
|
|
77
|
+
|
|
78
|
+
Implication: **Phase 1 touches both ocdkit and hostpkg.** It's a well-scoped but
|
|
79
|
+
substantial cross-package refactor of the data-population path, not a drop-in.
|
|
80
|
+
|
|
81
|
+
## Target architecture
|
|
82
|
+
A lightweight **child process** runs the same FastAPI app and binds the same
|
|
83
|
+
ports. **Nothing client-side changes** — `embed.py`, `jupyter_ext.py`, the proxy,
|
|
84
|
+
and all baked URLs keep working because the port contract is preserved. The
|
|
85
|
+
kernel becomes a *producer + pusher*; the child is the *server*.
|
|
86
|
+
|
|
87
|
+
| Concern | Process |
|
|
88
|
+
|---|---|
|
|
89
|
+
| HTTP serving, `/tile` crop+encode, `/attach`, `/info`, `/grid` | Child (off-GIL) |
|
|
90
|
+
| Source construction, lazy producers (Scene/torch), attachment encode | Kernel (once) |
|
|
91
|
+
| `_SOURCES` | Kernel = source of truth; child holds a serving mirror |
|
|
92
|
+
|
|
93
|
+
### Data transfer
|
|
94
|
+
- **Pyramid arrays** (`_pyr`, tens of MB, served by crop-on-demand): a
|
|
95
|
+
`multiprocessing.shared_memory` / mmap'd `/dev/shm` buffer — zero-copy. Kernel
|
|
96
|
+
writes once, sends `{shm_name, shape, dtype, meta}`; child maps + crops +
|
|
97
|
+
encodes there. (Phase 4: allocate the pyramid *directly* in shm — no copy.)
|
|
98
|
+
- **Attachment bytes** (already small/compressed): over the control channel.
|
|
99
|
+
- **Metadata** (`/info`, `/layout`): JSON over the control channel.
|
|
100
|
+
- **Control channel**: a Unix-domain socket; kernel→child commands
|
|
101
|
+
`register / fill / attach / lazy_result / evict`, mirroring `server.py`'s API.
|
|
102
|
+
|
|
103
|
+
### Lifecycle
|
|
104
|
+
- `ensure_server()` spawns the child (daemon, dies with the kernel) + handshakes
|
|
105
|
+
the control socket; `reset_server()` kills + respawns.
|
|
106
|
+
- Open decisions: (a) adopt an existing child across kernel restarts vs. spawn
|
|
107
|
+
fresh; (b) shm cleanup on source evict + on exit (resource_tracker quirks);
|
|
108
|
+
(c) child-crash → respawn + kernel re-pushes live sources (kernel is the truth).
|
|
109
|
+
|
|
110
|
+
## Phasing (each independently shippable)
|
|
111
|
+
- **Phase 0 — DONE**: feasibility audit (above). Lock the kernel↔server API
|
|
112
|
+
surface (`register_pending/fill/register_lazy/attach/get_source/evict`).
|
|
113
|
+
- **Phase 1**: extract the FastAPI app into a standalone entry
|
|
114
|
+
(`python -m ocdkit.tileserve._server_proc`) driven by a control socket;
|
|
115
|
+
push data **by value** (socket copy) to prove the boundary. Use option **C**
|
|
116
|
+
(accept heavy imports) — this alone kills the GIL contention. `TileSource`
|
|
117
|
+
becomes a thin client; `ensure_server`/`reset_server` manage the child.
|
|
118
|
+
- **Phase 1.5**: import decoupling (option **A** or **B**) → light child.
|
|
119
|
+
- **Phase 2**: swap array transfer to shared memory (zero-copy).
|
|
120
|
+
- **Phase 3**: lifecycle hardening (crash respawn, shm cleanup, restart adoption,
|
|
121
|
+
Windows `spawn` + shm naming).
|
|
122
|
+
- **Phase 4**: allocate pyramids directly in shm (drop the copy).
|
|
123
|
+
|
|
124
|
+
Phase 1 resolves the user-visible symptom; 1.5–4 are perf/robustness.
|
|
125
|
+
|
|
126
|
+
## Verification
|
|
127
|
+
Re-run the original GIL-starvation harnesses (`outputs/repro/server_ttfb_
|
|
128
|
+
contention.py`, `contention_harness.py`) with a GIL-holding kernel loop and
|
|
129
|
+
confirm TTFB stays flat (was 0.6→13ms under load). Add a child-crash/respawn
|
|
130
|
+
test in Phase 3.
|
|
131
|
+
|
|
132
|
+
## Alternatives rejected
|
|
133
|
+
- **Sub-interpreters (PEP 684)**: uvicorn/FastAPI aren't sub-interpreter-safe;
|
|
134
|
+
cross-interpreter array sharing is restricted. Too immature.
|
|
135
|
+
- **Release the GIL in the encoder only**: the uvicorn loop + routing is still
|
|
136
|
+
Python and contends — partial fix.
|