Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
5ab84f3
Add lightsheet OME-Zarr NDI document type + LightsheetZarrManager (sc…
stevevanhooser Sep 9, 2026
282f92d
readViewport scaffold: initialize outputs and mark unused args
stevevanhooser Sep 9, 2026
9bbf265
writeChunkFile scaffold: initialize outputs and mark unused args
stevevanhooser Sep 9, 2026
27a8f78
Drop unused nChunks assignment in lightsheet makePyramid.makeOneLevel
stevevanhooser Sep 9, 2026
2207091
Drop the readViewport scaffold from the PR
stevevanhooser Sep 9, 2026
037a4b3
Drop the writeChunkFile scaffold from the PR
stevevanhooser Sep 9, 2026
8f8af4e
Drop the materializeChunks branch from makePyramid
stevevanhooser Sep 9, 2026
6a0ef06
README: drop removed scaffolds; point at WDS/NDI-python
stevevanhooser Sep 9, 2026
6a03187
Add the two missing _schema.json validators for the lightsheet doc pair
stevevanhooser Sep 9, 2026
59d3b62
Use NaN (not null) for unbounded matrix parameters in the lightsheet …
stevevanhooser Sep 9, 2026
49b1187
Merge branch 'main' into claude/lightsheet-zarr-ndi-viewer-djp5vk
stevevanhooser Sep 9, 2026
30dfd8a
Restructure lightsheet pyramid: one parent per source volume, per-lev…
stevevanhooser Sep 9, 2026
1af66bb
Address Code Analyzer: paren-index perEntryReduction instead of brace…
stevevanhooser Sep 9, 2026
c8f3479
Re-tile OME-Zarr on ingest to an 8 MB per-chunk byte budget
claude Sep 9, 2026
c92b92a
Cover the lightsheet builders and manager statics
claude Sep 9, 2026
1d11302
Fix element-wise axis test in chooseTileShape
claude Sep 9, 2026
04293e3
Store axes_units and channel_names as strings, not matrices
claude Sep 9, 2026
e9d9fd0
Make TestFromOMEZarr self-contained so it does not need NDR's fixture
claude Sep 10, 2026
3576d71
Add a blob OME-Zarr fixture generator for end-to-end smoke tests
claude Sep 10, 2026
41ddaa5
Drop the redundant %#ok<INUSA> suppression on options.Seed
claude Sep 10, 2026
8e1c865
Scale blob positions with Shape so small fixtures don't overflow
claude Sep 10, 2026
bf420e1
Fix NDR resolution check and loosen spike-value assertion
claude Sep 10, 2026
04faa35
Cover ingestBlobFixture with the one-line-demo test
claude Sep 10, 2026
3610ce2
Write .zgroup markers on the blob fixture so Zarr readers accept it
claude Sep 10, 2026
89f4763
Emit compressor and filters as JSON null in Zarr .zarray
claude Sep 10, 2026
c994364
Give the wide blob concentric-ring structure and pull it in from the …
claude Sep 10, 2026
3820dd3
Widen wide-feature wavelength and split its amplitude for the pyramid
claude Sep 10, 2026
bd6a245
Materialize chunks, add ndi.fun.doc.lightsheet.view, wire it end to end
claude Sep 10, 2026
5f631f2
Drop redundant Code Analyzer suppressions
claude Sep 10, 2026
a7ba07a
Add Blosc+Zstd codec to the chunk materializer
claude Sep 10, 2026
f3a430b
Address Code Analyzer notes on the codec code
claude Sep 10, 2026
f37fbf1
makePyramid: use ndr.format.blosc.encode instead of shelling to zstd
claude Sep 10, 2026
eb04a79
makePyramid: keep chunk tempfiles alive until database_add finishes
claude Sep 10, 2026
1c19307
makePyramid: drop stale %#ok<NASGU> on the tmpCleaner onCleanup
claude Sep 10, 2026
b30fd91
blob fixture: two channels by default, OMERO channel metadata
claude Sep 10, 2026
90b4c55
makeBlobFixture: drop stale %#ok<ISCLSTR> on iscellstr check
claude Sep 10, 2026
aa7f51a
TestBlobFixture: pin existing tests to NumChannels=1, add 2-channel test
claude Sep 10, 2026
1f3730b
TestBlobFixture: defensive cell/struct handling in the new multi-chan…
claude Sep 10, 2026
74a8588
makePyramid: stream per-chunk reads instead of loading each level whole
claude Sep 11, 2026
8578add
clevel: cap validation at 9, not 22 (Blosc wrapper limit)
claude Sep 11, 2026
17f3432
LightsheetZarrManager: mirror GEFManager UI on the cloud palette
claude Sep 13, 2026
7a45f12
lightsheet: progressFcn callback on makePyramid / fromOMEZarr
claude Sep 13, 2026
c32fae8
fromOMEZarr: default progress bar is the NDI ProgressBarWindow
claude Sep 13, 2026
98ddeb1
lightsheet: parallel compression via serial-read / batched parfor
claude Sep 13, 2026
f19bf39
lightsheet: numWorkers=-1 (default) reads an already-open parpool
claude Sep 13, 2026
85cbaae
lightsheet: print worker count and per-level plan at ingest start
claude Sep 13, 2026
9b0ca22
lightsheet: one union-bbox read per batch instead of one per chunk
claude Sep 13, 2026
17f48a5
lightsheet: prefetch super-blocks decoupled from parfor pool size
claude Sep 13, 2026
209a1de
lightsheet tests: chunk-equality helper + prefetch-invariance test
claude Sep 13, 2026
7027cd8
lightsheet: async prefetch overlaps disk reads with parfor compression
claude Sep 13, 2026
f7eaf67
lightsheet: one parfor per super-block, one progress tick per super-b…
claude Sep 13, 2026
79fd7d9
lightsheet: push pad/permute/typecast into the parfor workers
claude Sep 13, 2026
e6b54a7
lightsheet: stop broadcasting per-chunk buffers to every parfor worker
claude Sep 13, 2026
7d94e1b
lightsheet: use blosc.encodeChunk fast path in padEncodeAndWrite
claude Sep 13, 2026
71d7387
lightsheet: add debugLog option to trace per-super-block memory growth
claude Sep 13, 2026
58891a1
lightsheet debugLog: use datetime + matlabProcessID (Code Analyzer)
claude Sep 13, 2026
0e73162
lightsheet: install chunks with one did.document.addFileSeries call
claude Sep 13, 2026
8b751b4
debugLog: fall back to feature('getpid') when matlabProcessID is missing
claude Sep 13, 2026
750ec1b
debugLog: get PID via Java ManagementFactory (avoid lint findings)
claude Sep 13, 2026
7d4842f
lightsheet: cap super-block bbox byte volume to prevent readArray OOM
claude Sep 13, 2026
d2afcaa
lightsheet: preallocate superBlocks cell to nChunks (Code Analyzer AG…
claude Sep 13, 2026
8134a20
lightsheetZarrLevel: declare chunk.bin as a DID file series (bare man…
claude Sep 14, 2026
0e305da
lightsheetZarrLevel schema: mirror file_list rename in validation schema
claude Sep 14, 2026
71df6b3
lightsheet: add test-blob ingest + view demo script
claude Sep 14, 2026
a9d1ada
lightsheet demo: pass --reduction so napari has one ladder to open
claude Sep 14, 2026
d36fcc1
lightsheet demo: drop reduction flag now that CLI defaults to mean
claude Sep 14, 2026
871b6b9
cloud sync: reconstruct series ingest_locations on SyncFiles=false too
claude Sep 14, 2026
b127172
downloadNdiDocuments: drop the dead onCleanup.empty pre-declaration
claude Sep 14, 2026
f89c58e
cloud sync: install series manifests locally on SyncFiles=false too
claude Sep 14, 2026
b379744
Merge origin/main into claude/lightsheet-zarr-ndi-viewer-djp5vk
claude Sep 15, 2026
429d40a
Merge origin/main into claude/lightsheet-zarr-ndi-viewer-djp5vk
claude Sep 16, 2026
51740f5
Merge origin/main into claude/lightsheet-zarr-ndi-viewer-djp5vk
claude Sep 16, 2026
ae0c972
lightsheet: end-to-end blob cloud round-trip demo function
claude Sep 16, 2026
cadca4a
lightsheet: move the cloud round-trip demo into ndi.test.cloud
claude Sep 16, 2026
6af807c
lightsheet demo: bracket the split format string on the download say()
claude Sep 16, 2026
4181ea3
lightsheet demo: rename say -> narrate to avoid path-shadow, use nest…
claude Sep 16, 2026
db0311f
lightsheet demo: clear the readblankdefinition cache on entry
claude Sep 16, 2026
e130302
lightsheet demo: dump viewer stderr + NDI_LIGHTSHEET_DEBUG to a file
claude Sep 16, 2026
3d68539
lightsheet: zsh helper to locate + cat the napari debug log
claude Sep 16, 2026
f268613
lightsheet demo: after the cloud viewer, tail the debug log inline
claude Sep 16, 2026
ad89791
lightsheet demo: wait for server-side extraction before download
claude Sep 16, 2026
a8e8148
Merge remote-tracking branch 'origin/main' into claude/lightsheet-zar…
claude Sep 20, 2026
b88d056
cloud upload: progress feedback during pre-upload file inventory
claude Sep 20, 2026
470fb59
cloud upload: standard NDI progress bar for inventory and batch upload
claude Sep 20, 2026
044c6a0
zipForUpload: single-pass batching, O(N_files) instead of O(batches*N)
claude Sep 20, 2026
f847d7c
uploadDataset: route to a named organization
claude Sep 20, 2026
b367283
ProgressBarWindow: per-bar Timeout; list_binary_files and zipForUploa…
claude Sep 21, 2026
4e45d96
cloud upload: verbose diagnostics on the pre-upload cloud check + fin…
claude Sep 21, 2026
2280b03
Merge remote-tracking branch 'origin/main' into claude/lightsheet-zar…
claude Sep 23, 2026
58cbdee
cloud download: fast + visible series-ingest reconstruction
claude Sep 24, 2026
9467a0a
cloud download: employ listFilesAll for uid validation on the remote …
claude Sep 24, 2026
249bf5d
Merge remote-tracking branch 'origin/main' into claude/lightsheet-zar…
claude Sep 25, 2026
60a79cf
LightsheetZarrManager: pull Levels + Reduction from the child level docs
claude Sep 25, 2026
9646b62
batchSignedUrlLookup: retry scope fetch with backoff on transient raise
claude Sep 27, 2026
2b23dd3
TestProgressBarWindow: guard the timeout-restore teardown
claude Sep 27, 2026
a84130a
fetchManifest: bypass batch for single-uid manifest fetch
claude Sep 27, 2026
8f468dc
Add TestSignedUrlSetJobScaling: reproducer for server-side job bug
claude Sep 27, 2026
286d261
Add TestSignedUrlSetJobFixture: pin async job vs. getFileDetails disa…
claude Sep 27, 2026
aab35a1
TestSignedUrlSetJobFixture: build a fresh fixture instead of hardcodi…
claude Sep 27, 2026
427fbd5
TestSignedUrl*: probe toolchain with which() not exist(...,'file')
claude Sep 27, 2026
1223661
TestSignedUrlSetJobFixture: narrate + fetch real fileCount from resul…
claude Sep 27, 2026
27661a3
signedUrlDiskCache: persistent signed-URL disk cache for reopen
claude Sep 27, 2026
988ec74
signedUrlDiskCache: swap fragile Java I/O for MATLAB builtins
claude Sep 27, 2026
5541a00
AuthTest: index organizations(1).id so a multi-org account still passes
claude Sep 27, 2026
0288006
batchSignedUrlLookup: exponential backoff for partial-map retry
claude Sep 28, 2026
2039130
didsqlite: single-file DID fetches bypass batch signed-URL scope
claude Sep 28, 2026
fe90588
TestSignedUrlSetJobScaling: add N=50k / 100k rows for the >100k regime
claude Sep 28, 2026
b0dcce1
TestSignedUrlSetJobScaling: pass 'chunks' to fromOMEZarr so N is real
claude Sep 28, 2026
e3d5f51
TestSignedUrlSetJobScaling: scale waitForAllBulkUploads timeout with N
claude Sep 28, 2026
1d3c1c2
TestSignedUrlSetJobScaling: treat bulk-extract timeout as an observation
claude Sep 28, 2026
d5e43d3
TestPublishWithDocsAndFiles: poll getFileDetails until uploaded=true
claude Sep 30, 2026
5c9e8a8
lightsheet pyramid: default tile budget 8 MB -> 32 MB
claude Oct 4, 2026
37afa16
Merge origin/main into claude/lightsheet-zarr-ndi-viewer-djp5vk
claude Oct 11, 2026
2f9b0ea
lightsheet: launch the napari viewer with debugging on and its log kept
claude Oct 12, 2026
04ce18f
lightsheet: fromSPIM -- pyramid straight from SmartSPIM All_Channels
claude Oct 12, 2026
668beb1
TestFromSPIM: the fixture's slices are 0-based; preallocate fromSPIM'…
claude Oct 12, 2026
efff004
Merge remote-tracking branch 'origin/main' into claude/lightsheet-zar…
claude Oct 13, 2026
fb10169
LightsheetZarrManager: View dialog previews the exact logged launch c…
claude Oct 13, 2026
b8e4b54
TestLaunchViewer: match the manager's logged launch and preview
claude Oct 13, 2026
d94973c
Merge origin/main into claude/lightsheet-zarr-ndi-viewer-djp5vk
claude Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/ndi/+ndi/+fun/+doc/+lightsheet/README-lightsheet-zarr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# lightsheet OME-Zarr documents (design + status)

This package (`+ndi/+fun/+doc/+lightsheet/`) lets a session hold a
lightsheet OME-Zarr (NGFF v0.4) multiscale pyramid as NDI documents,
and hands one to napari for viewing through NDI-python.

The pattern mirrors `+ndi/+fun/+doc/+gene/` (spatial transcriptomics
Stereo-seq / GEF) so a reader of that package will recognise every
piece here.

## Document shape

Two new document classes:

- **`lightsheetZarrPyramid`** - one per REDUCTION variant (`mean`,
`max`, ...). Holds the frame (axes, level-0 shape, voxel size,
translation, channels, dtype). Depends on subject + optional element
+ optional `fileReference` for the source store.
- **`lightsheetZarrLevel`** - one per (pyramid, level). Owns the chunk
bytes as a `chunk.bin_#` file series and the per-level metadata
(shape, chunks, voxel size, translation, codec, ...). Depends on its
parent pyramid.

NDI documents are immutable, so:
- A new reduction is a new pyramid document + its own level ladder.
- A deeper coarsening is a new level document; the parent's `n_levels`
is out of date until a new parent version is written for that count.

## MATLAB API surface

- `ndi.fun.doc.lightsheet.fromOMEZarr(session, zarrPath, ...)` -
probe an OME-Zarr store, write ONE pyramid document per multiscales
entry and ONE level document per level. Metadata-only; does not copy
chunk bytes. **Working.**
- `ndi.fun.doc.lightsheet.makePyramid(session, pyramidEntry, ...)` -
the per-multiscales-entry worker `fromOMEZarr` calls. Reusable when
the caller already has an `ndr.format.omezarr.listPyramids` struct
in hand. **Working.**
- `ndi.fun.doc.lightsheet.makeSourceFile(session, zarrPath)` -
fileReference for the store. **Working.**
- `ndi.fun.doc.lightsheet.levelTable(session, pyramidDoc)` -
finest-first table of a pyramid's levels; `Properties.UserData`
carries the parent frame. **Working.**
- `ndi.fun.doc.lightsheet.chooseLevel(levels, targetVoxelSize)` -
pick the coarsest level whose voxel size still meets the target.
**Working.**
- `ndi.fun.doc.lightsheet.viewCommand(launcher, sessionPath, pyramidID, ...)`
- pure command builder. Same pattern as
`ndi.gui.app.GEFManager.viewCommand`; the launcher is
`/usr/local/bin/napariViewLightsheet`, a shell wrapper around the
`napariViewLightsheet` console script that ships in
`Waltham-Data-Science/NDI-python`. **Working.**

## GUI

`ndi.gui.app.LightsheetZarrManager(session)` mirrors `GEFManager`:
lists pyramids in a session, and drives Add / View / Delete. `View`
builds the napari command from `viewCommand` and shells out.

## What still needs building

1. **Chunk materialization** into `chunk.bin_#` on
`lightsheetZarrLevel`. Uses the same file-series upload path as
`ndi.cloud.sync.internal.uploadFilesForDatasetDocuments`, plus a
small `chunk_index` side-field so sparse pyramids can elide
fill-value chunks without breaking the 1-based `chunk.bin_#`
addressing. Land that side-field on the schema first, then a
`makePyramid` option (or a stand-alone `materializeChunks`
function) can write bytes.
2. **NDI-python napari reader.** The MATLAB `viewCommand` targets
`/usr/local/bin/napariViewLightsheet`, a shell wrapper around
`ndi.gui.app.lightsheetZarr.cli:main` in
`Waltham-Data-Science/NDI-python` (installed as
`[project.scripts].napariViewLightsheet`). The CLI, viewer, and
metadata reader are landed; the per-chunk dask fetcher that
`levelArrays` calls is a scaffold today. See that repo's
`src/ndi/gui/app/lightsheetZarr/README-lightsheet-zarr.md`.
3. **`readViewport` (MATLAB).** A MATLAB path for reading a rectangle
out of a pyramid at the appropriate level. Two backends are
plausible: read the source store via `ndr.format.omezarr.readArray`
(works today, but only when the store is on disk), or read the
materialized `chunk.bin_#` files via the cloud API (blocked on #1).
The scaffold from an earlier version of this PR was removed to
keep the package free of `notImplemented` bodies; add it back with
real code when #1 lands.
4. **NDR helpers.** `ndr.format.omezarr.probe` is small; when it lands
in NDR-matlab / NDR-python, `fromOMEZarr` here can call it instead
of `listPyramids` for cheaper metadata reads. (Landing on the same
branch name in the NDR repos.)
5. **Tests.** Once #1 lands, extend
`tests/+ndi/+unittest/+fun/+doc/+lightsheet/` with round-trip and
viewport tests analogous to `+gene/`'s.

## Related

- `ndi.gui.app.GEFManager` - the sibling manager for Stereo-seq GEF
spatial transcriptomics. Same launcher pattern (a shell wrapper
around an NDI-python console script). Read it first if this is your
first NDI file-format ingest.
- `ndr.format.omezarr.*` - the metadata / read primitives this
package builds on.
64 changes: 64 additions & 0 deletions src/ndi/+ndi/+fun/+doc/+lightsheet/chooseLevel.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
function [levelIndex, levelRow] = chooseLevel(levels, targetVoxelSize)
% NDI.FUN.DOC.LIGHTSHEET.CHOOSELEVEL - pick the coarsest level meeting a target
%
% [LEVELINDEX, LEVELROW] = NDI.FUN.DOC.LIGHTSHEET.CHOOSELEVEL(LEVELS,
% TARGETVOXELSIZE)
%
% Given LEVELS (a struct array or table with per-level `voxel_size`
% and `level` fields, finest-first, e.g. from LEVELTABLE), returns
% the coarsest level whose per-axis voxel size is still <=
% TARGETVOXELSIZE. If every level is coarser than the target, returns
% level 0. If every level is finer, returns the coarsest.
%
% LEVELINDEX is the 0-based level index (matches
% lightsheetZarrLevel.level).
%
% TARGETVOXELSIZE may be a scalar (applied to every spatial axis) or a
% vector aligned with the level's `voxel_size`.
%
% See also: ndi.fun.doc.lightsheet.levelTable

arguments
levels
targetVoxelSize (1,:) double
end

if istable(levels)
vs = levels.voxel_size;
li = levels.level;
n = height(levels);
else
n = numel(levels);
vs = cell(n, 1);
li = zeros(n, 1);
for k = 1:n
vs{k} = reshape(double(levels(k).voxel_size), 1, []);
li(k) = levels(k).level;
end
end
if n == 0
error('NDI:lightsheet:chooseLevel:emptyLevels', ...
'levels is empty.');
end

bestK = 1;
for k = 1:n
v = vs{k};
if isscalar(targetVoxelSize)
ok = all(v <= targetVoxelSize);
else
m = min(numel(v), numel(targetVoxelSize));
ok = all(v(1:m) <= targetVoxelSize(1:m));
end
if ok
bestK = k;
end
end

levelIndex = li(bestK);
if istable(levels)
levelRow = levels(bestK, :);
else
levelRow = levels(bestK);
end
end
185 changes: 185 additions & 0 deletions src/ndi/+ndi/+fun/+doc/+lightsheet/fromOMEZarr.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
function [pyramidDoc, levelDocs, info] = fromOMEZarr(session, zarrPath, options)
% NDI.FUN.DOC.LIGHTSHEET.FROMOMEZARR - build lightsheetZarrPyramid docs from an OME-Zarr store
%
% [PYRAMIDDOC, LEVELDOCS, INFO] = NDI.FUN.DOC.LIGHTSHEET.FROMOMEZARR(...
% SESSION, ZARRPATH)
%
% Walks an OME-Zarr (NGFF v0.4) store at ZARRPATH, enumerates its
% multiscale pyramids with NDR.FORMAT.OMEZARR.LISTPYRAMIDS, and writes
% ONE lightsheetZarrPyramid document (the source volume) plus one
% lightsheetZarrLevel document per unique level to SESSION.
%
% ONE DOCUMENT PER SOURCE VOLUME. The parent describes the source
% volume; reductions (mean, max, ...) live on the level children.
% A store with mean and max pyramids produces one parent, not two.
%
% LEVEL 0 IS DEDUPED. When two NGFF multiscales entries reference
% the same underlying array (typically level 0: their
% `datasets[0].path` is the same string), one level document is
% written -- with reduction_function='none' -- and both readers use
% it. Reduced levels are per-reduction and get
% reduction_function='mean'/'max'/... .
%
% READING IS METADATA-ONLY. No chunk bytes are read here; the
% source store's .zattrs and per-level .zarray are enough to fill
% the documents. Materializing chunk bytes into a level document's
% `chunk.bin_#` file series is follow-up work (see the package
% README).
%
% Optional Name-Value Arguments:
% subjectID - char/string, id of the subject depended on. If empty,
% the caller is refused: a lightsheetZarrPyramid without
% a subject is not a valid document.
% elementID - char/string, id of the imaging element the volume was
% captured on. May be empty.
% pyramidNames - cellstr/string, restrict to these multiscales.name
% values. Empty means all.
% reductionMap - containers.Map / struct mapping pyramid name (as
% reported by listPyramids) or type to a reduction
% label ('mean', 'max', ...). When a name is not in
% the map, `pyramid_type` is inspected first, then a
% fallback of 'mean' is used with a warning.
% pipelineVersion - char/string tag written into
% lightsheetZarrPyramid.pipeline_version.
% sourceFileID - id of an existing fileReference document for
% ZARRPATH. When empty, a new fileReference is created
% via NDI.FUN.DOC.LIGHTSHEET.MAKESOURCEFILE.
% label - human-readable label written into the parent pyramid.
%
% INFO returns a struct with fields:
% zarrPath - absolute path resolved
% pyramids - the raw output of ndr.format.omezarr.listPyramids
% reductions - cellstr, unique reductions present in the ladder
% sourceFileID - id of the fileReference document used
% sharedLevel0 - logical, true if level 0 was shared across
% reductions and deduped
%
% Example:
% S = ndi.session.dir('mysession','/path/to/session');
% [P, L, info] = ndi.fun.doc.lightsheet.fromOMEZarr(S, ...
% '/cloudcache/subject42/volume.ome.zarr', ...
% 'subjectID', subj.id());
%
% See also: ndi.fun.doc.lightsheet.makePyramid,
% ndi.fun.doc.lightsheet.chooseLevel,
% ndi.fun.doc.lightsheet.viewCommand,
% ndr.format.omezarr.listPyramids,
% ndr.format.omezarr.probe

arguments
session (1,1)
zarrPath (1,:) char
options.subjectID char = ''
options.elementID char = ''
options.pyramidNames = {}
options.reductionMap = struct()
options.pipelineVersion char = ''
options.sourceFileID char = ''
options.label char = ''
end

if isempty(strtrim(options.subjectID))
error('NDI:lightsheet:fromOMEZarr:noSubject', ...
['A lightsheetZarrPyramid depends_on a subject; pass ' ...
'''subjectID''.']);
end

if ~isfolder(zarrPath)
error('NDI:lightsheet:fromOMEZarr:noSuchStore', ...
'OME-Zarr store not found at %s.', zarrPath);
end

if ~ndr.format.omezarr.isOMEZarr(zarrPath)
error('NDI:lightsheet:fromOMEZarr:notOMEZarr', ...
'%s does not look like an OME-Zarr store (.zattrs missing multiscales).', ...
zarrPath);
end

pyramids = ndr.format.omezarr.listPyramids(zarrPath);
if isempty(pyramids)
error('NDI:lightsheet:fromOMEZarr:noPyramids', ...
'%s reports zero multiscales entries.', zarrPath);
end

keep = true(numel(pyramids), 1);
if ~isempty(options.pyramidNames)
wanted = cellstr(options.pyramidNames);
for i = 1:numel(pyramids)
keep(i) = any(strcmp(pyramids(i).name, wanted));
end
if ~any(keep)
error('NDI:lightsheet:fromOMEZarr:noNamedPyramid', ...
['None of the requested pyramid names (%s) matched. ' ...
'Available: %s.'], strjoin(wanted, ', '), ...
strjoin({pyramids.name}, ', '));
end
end
pyramids = pyramids(keep);

n = numel(pyramids);
perEntryReduction = cell(n, 1);
for i = 1:n
perEntryReduction{i} = pickReduction(pyramids(i), options.reductionMap);
end

sourceFileID = options.sourceFileID;
if isempty(sourceFileID)
srcDoc = ndi.fun.doc.lightsheet.makeSourceFile(session, zarrPath);
session.database_add(srcDoc);
sourceFileID = srcDoc.id();
end

[pyramidDoc, levelDocs, sharedLevel0] = ndi.fun.doc.lightsheet.makePyramid( ...
session, pyramids, perEntryReduction, ...
'subjectID', options.subjectID, ...
'elementID', options.elementID, ...
'sourceFileID', sourceFileID, ...
'pipelineVersion', options.pipelineVersion, ...
'label', options.label);

info = struct( ...
'zarrPath', char(zarrPath), ...
'pyramids', pyramids, ...
'reductions', {unique(perEntryReduction)}, ...
'sourceFileID', sourceFileID, ...
'sharedLevel0', sharedLevel0);
end

function r = pickReduction(pyramid, reductionMap)
% pickReduction - choose 'mean' or 'max' for this pyramid entry
%
% Preference: explicit map by name, then by type, then a heuristic:
% pyramid.type == 'max' -> 'max', anything else -> 'mean'. Warns when
% falling back because pyramid.type is unreliable.
r = '';
if ~isempty(reductionMap)
if isstruct(reductionMap)
if ~isempty(pyramid.name) && isfield(reductionMap, pyramid.name)
r = char(reductionMap.(pyramid.name));
elseif ~isempty(pyramid.type) && isfield(reductionMap, pyramid.type)
r = char(reductionMap.(pyramid.type));
end
elseif isa(reductionMap, 'containers.Map')
if ~isempty(pyramid.name) && isKey(reductionMap, pyramid.name)
r = char(reductionMap(pyramid.name));
elseif ~isempty(pyramid.type) && isKey(reductionMap, pyramid.type)
r = char(reductionMap(pyramid.type));
end
end
end
if isempty(r)
t = lower(strtrim(char(pyramid.type)));
n = lower(strtrim(char(pyramid.name)));
if strcmp(t, 'max') || contains(n, 'max')
r = 'max';
else
r = 'mean';
if ~isempty(t) && ~any(strcmp(t, {'mean','box','average'}))
warning('NDI:lightsheet:fromOMEZarr:reductionFallback', ...
['Pyramid %s has type ''%s''; falling back to ' ...
'''mean''. Pass reductionMap to override.'], ...
pyramid.name, pyramid.type);
end
end
end
end
Loading
Loading