Skip to content

feat: add dynamic_shape mode and coords to StreamView#112

Merged
tlambert03 merged 18 commits intopymmcore-plus:mainfrom
tlambert03:live-view-shape
Feb 26, 2026
Merged

feat: add dynamic_shape mode and coords to StreamView#112
tlambert03 merged 18 commits intopymmcore-plus:mainfrom
tlambert03:live-view-shape

Conversation

@tlambert03
Copy link
Member

@tlambert03 tlambert03 commented Feb 26, 2026

The current default contract of StreamView is that its shape always reflects the full (expected) dimensions, as defined by the AcquisitionSettings. You can always index into it according to your expectations...

But in many cases (such as the ndv example already checked into the repo), a GUI will want some object it can query to know "what are the valid bounds of the data that I can request." The current ndv example (in main) shows how one can manually manage this by connecting to on('coords_expanded') and then managing how one indexes into the StreamView. But that's a bit of boilerplate that is likely to be very consistent across usage patterns. So:

This PR introduces a dynamic_shape mode to StreamView (via stream.view(dynamic_shape=True)), which changes the semantics of shape, coords (new), and __len__. It also adds a coords property to all views (dynamic or not). (having both dims and coords finishes the x-array like API that's useful in a number of contexts, closes #105).

Property dynamic_shape=False (old) dynamic_shape=True (new default)
shape Static: full expected shape, e.g. (3, 2, 512, 512) Dynamic: high water marks, e.g.
(0, 0, 512, 512)(1, 1, 512, 512) → …
Frame dims (Y, X) always full.
dims Static Dimension labels from settings, e.g. ("t", "c", "y", "x") Same — unchanged
(this is important ... changing ndim is bad )
ndim Number of dimensions, e.g. 4 — constant Same — unchanged
__len__ Size of first dimension (full), e.g. 3 Size of first dimension (dynamic), e.g.
01 → …
coords Static Full coordinates from settings, e.g.
{"t": range(3), "c": ["DAPI", "GFP"], ...}
Dynamic: only coordinates acquired so far, e.g.
{"t": range(0), "c": [], ...}{"t": range(1), "c": ["DAPI"], ...}
__getitem__ Returns zeros for unwritten frames Same — returns zeros for unwritten frames (or IndexError if strict=True)

An additional strict parameter controls indexing behavior: stream.view(dynamic_shape=True, strict=True) raises IndexError on integer indices outside the current "seen" bounds, while slices are always clipped (standard numpy behavior).

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 26, 2026

Merging this PR will degrade performance by 51.58%

❌ 1 (👁 1) regressed benchmark
✅ 18 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
👁 test_bench_append[t5_c2_z3_xy256-tensorstore] 51.3 ms 105.8 ms -51.58%

Comparing tlambert03:live-view-shape (234d013) with main (1ac01ed)

Open in CodSpeed

@tlambert03
Copy link
Member Author

tlambert03 commented Feb 26, 2026

with this change, the ndv example reduces to:

...

with create_stream(settings) as stream:
    # Create a StreamView.  This is an array-like object with dims/coords
    # that reflect the stream's current state (by default, live_shape=True).
    view = stream.view()

    # ndv has built-in support for array-like objects that support the xarray
    # dims/coords convention.  So we can directly pass our StreamView to an ndv viewer
    viewer = ndv.ArrayViewer(view)
    viewer.show()

    # the only thing we need to connect is the StreamView's coords_changed signal,
    # so that the viewer updates when new frames are added and the shape/coords change.
    view.coords_changed.connect(viewer.data_wrapper.dims_changed)

❤️

the dims/coords convention (started by xarray) is growing in adoption too, so it's conceivable this sort of thing would work for napari in the future too

@codecov
Copy link

codecov bot commented Feb 26, 2026

Codecov Report

❌ Patch coverage is 92.06349% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.43%. Comparing base (5cc4747) to head (234d013).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/ome_writers/_stream_view.py 91.07% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #112      +/-   ##
==========================================
- Coverage   97.46%   97.43%   -0.03%     
==========================================
  Files          21       21              
  Lines        2285     2337      +52     
==========================================
+ Hits         2227     2277      +50     
- Misses         58       60       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tlambert03 tlambert03 changed the title feat: add live_shape mode to StreamView feat: add live_shape mode and coords to StreamView Feb 26, 2026
@tlambert03 tlambert03 changed the title feat: add live_shape mode and coords to StreamView feat: add dynamic_shape mode and coords to StreamView Feb 26, 2026
@tlambert03 tlambert03 merged commit 0a92ad1 into pymmcore-plus:main Feb 26, 2026
116 checks passed
@tlambert03 tlambert03 deleted the live-view-shape branch February 26, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TODO: possible add coords property to AcquisitionView

1 participant