Changelog¶
[v1.1.0]¶
Highlights:
- The IO layer got much faster: metadata is no longer re-read and re-decoded on every access (~250x on the hottest property, zero store reads when cached), dask writes align to the array's write unit (no more whole-shard read-modify-writes, and
zarrsfinally engages on local stores), and plate walking is flat instead of quadratic. - Iterators run at scale, under one verb table: a topic verb per iterator (
process,segment,measure,detect) over the sharedmap/reduce/iterlayer, one reconciliation declaration each (with_stitch,with_nms,with_join,on_overlap) backed by swappable protocols, thread/process fan-out in conflict-free waves, halos at unchanged parallelism, batched inference, and the oneprepare_jobs → for_job → finalizedistributed recipe. - New capabilities:
with_stitch(...)merges objects split across region boundaries on any ROI list — grids, overlapping FOVs, ragged tables, tiled masked objects; the newObjectDetectionIteratorturns a tile-by-tile detector into one deduplicated ROI table (#111);FeatureExtractorIterator.measurejoins per-region measurements into a singleFeatureTable.
Features¶
ThreadedMapperandProcessMapperparallelizemap/reduce. Writes run in conflict-free waves, and wave order is the canonical write order for every mapper — serial included — so overlapping writes land identically everywhere. The default stays serial.- One verb table across the five iterators: a partition-aware topic verb per class (
process,segment— masked included —measure,detect) over the shared generic layer —map,reduce(collect without writing) — both numpy — anditer(the*_as_numpyaliases stay; bareiter()still defaults to the dask backend in 1.1 and warns, see Deprecated) — plus tiling calls that say what they tile by:by_grid(size_*, stride_*, tail=...),by_blocks(num_*),by_chunks(),by_write_units().finalize()is the one gather everywhere. write_order=onon_overlap(...)andStitchConfig— performance first, reproducibility opt-in, exactness always."any"(the default) schedules pixel-contested writes for parallelism alone (1.5–2.5x on parallel mappers over overlapping tilings; deterministic per version, seam ownership schedule-defined);"roi"opts into the later-ROI-wins order,mapbit-identical to the manualiterloop. A pair relaxes only when both sides declare"any".- One reconciliation declaration per iterator, in the builder chain:
on_overlap(policy)on the writers (contested write pixels:"last"for last-writer-wins, or anymerge=rule),with_stitch(config)on segmentation,with_join(join)on features,with_nms(nms)on detection — each backed by a swappable protocol (SeamMatcherProtocol/IouSeamMatcher,JoinProtocol/ConcatJoin,NmsProtocol/GreedyNms). BatchedMapper(batch_size=...)stacks patches into one(B, ...)array perfunccall, for neural-network inference;iter(batch_size=...)is the manual batched loop (numpy-only and eager).with_halo(x=, y=, ...)reads a grown region and writes only the core; write footprints — and so parallelism — are unchanged, and misuse refuses loudly. On the read-only iterators it is a pure read margin, reconciled by NMS (detection) or your join (features).with_stitch(...)merges objects split across region boundaries into one id, on any ROI list — grids with a halo, overlapping FOVs, ragged tables — compacting ids to a dense1..N; a run interrupted before the final resolve leaves a valid, merely over-split label (an interruption inside the in-place compaction itself is detected and the retry refused). OnMaskedSegmentationIteratorit merges sub-objects within one mask (noUniqueLabelsTransformneeded; combining raises).StitchConfig(seam_matcher=...)swaps the pair criterion.- Distributed runs:
prepare_jobs(n_jobs)→for_job(**args).<topic verb>(func)per SLURM/Fractal task → onefinalize()gather (read-only iterators return the merged table; slices bank and returnNone). Partitions never share a write unit, every step validates a plan fingerprint, and the result matches a serial run. ObjectDetectionIterator.detect(func)(#111) runs a(patch) -> list[Roi]detector per tile, anchors the boxes to world coordinates, deduplicates by the declared NMS, and returns one renumberedRoiTable;Roi.anchor,Detection, andbbox_iouare public.FeatureExtractorIterator.measurejoins per-ROIfunc(image, label, roi)results into oneFeatureTable; every row is stamped withroi_index/roi_nameprovenance, so a border object measured by several halo-grown regions is deduplicated in your declared join.set_roi_maskedtakesmerge=: the rule applies inside the mask (outside it the disk always wins), so masked writes can combine with what is on disk instead of only overwriting.Label.relabel_sequential()renumbers any label to a dense1..Nin one pass.merge=on writes combines with what is on disk:"max"/"min"/"sum"(order-independent),"keep_nonzero", a callable, or a policy.MaskTransformfills on read andMaskMergeprotects on write, replacing the four masked pipe classes (get_roi_masked/set_roi_maskedunchanged).UniqueLabelsTransformgives each region a disjoint id block — derived, not counted, so parallel-safe and idempotent.- The transform contract is two methods,
on_get(array, ctx)/on_set(array, ctx), importable fromngio.transforms; a transform that silently materialises a dask array fails loudly. consolidate(mode="auto")takes the 3–5x-faster in-memory path exactly where it is provably identical to the chunked one.consolidate(regions=...)rebuilds only the pyramid regions that changed, byte-identical to a full rebuild; writing iterators use it automatically, andImage.track_writes()records the regions for you.NgioConfiggained three sections, each with a public model:zarr(ZarrConfig),dask(DaskConfig), andconsolidation(ConsolidationConfig).- Every
max_workersaccepts"auto";refresh()on containers and plates re-reads all held metadata; the scheduling primitives (plan_waves,canonical_unit_order,TailPolicy, …) are public inngio.iterators. - New public names:
NgioFutureWarning,ChannelSlicingInputType(ngio.images),ConsolidationModeandRegionsLike(ngio.common), andAbstractImage.write_granularity— the shape zarr writes atomically. - The facade carries its own vocabulary: the table classes (
RoiTable,MaskingRoiTable,FeatureTable,ConditionTable,GenericRoiTable,GenericTable,Table) andTransformProtocol/MergePolicy/MergeInputare importable fromngiodirectly. Image.resolve_channel_selection(selection)resolves anything theget_*methods accept aschannel_selection— index, label,ChannelSelectionModel, or a sequence of those — to its slicing entry, touching only metadata. This is the supported way to validate a selection before loading data (Fractal tasks previously imported the private parser for theirskip_if_missingchecks).
Fixes¶
- Container and plate table access align: one
NgioValidationErrorfor a missing/tableson both, the plate memoises a read-only "no tables" probe like the container, andadd_imageraisesNgioValueErrorinstead of a bareValueError. list_roi_tablesincludesgeneric_roi_tabletables; the type joinedTypedTable/TypedRoiTable, with the runtime tuple public asngio.tables.ROI_TABLE_TYPES.Label's array methods are real methods with their own signatures — they were rebinds of private base methods, so docs and IDEs showed_get_as_numpyet al.PixelSizecomparisons respect units:__eq__/__lt__/distanceconvert magnitudes when both space units are known (with anNgioUserWarning);distanceis spatial-only —tseconds no longer entered the norm that ranks pyramid levels. Differing unknown orNoneunits compare unequal under__eq__(identical unknown units compare by magnitude); ordering anddistancefall back to raw magnitudes, so unit-less stores still rank pyramid levels.- Metadata autodetect only treats a pydantic
ValidationErroras "not this version": anNgioErrorfrom inside a decoder (a badaxes_setup, corrupt values) surfaces directly instead of being blamed on the file. axes_orderrefuses to drop a non-singleton axis with a named error (it silently squeezed, surfacing as a raw numpy error only when the axis happened to be bigger than 1).AxesSetup.from_ordered_listplaces non-canonical axis names only into free spatial (z/y/x) slots — an unknown name can no longer silently become the channel or time axis — and raises when no spatial slot is left.- The
io_retry.retry_all_errorswarning fires once at config load — the model validator re-fired it on everymodel_copy, raising under-W error. - Overriding a roi-pinned axis with a slicing kwarg drops the pipe's
roi(it no longer describes the effective region), so a consumer trusting it — a mask transform, world anchoring — refuses instead of misplacing pixels;*_maskedsame-axis overrides refuse outright. The roi/kwarg hierarchy is documented on every method accepting both. - Pickling a
StitchPlanno longer creates the scratch group as a side effect: the writing verbs resolve it up front, and pickling an unresolved plan raises. - Metadata updates no longer drop what they were not about (#231):
set_axes_units/set_axes_namessilently reset OMERO channels (NGFF 0.5) and a label's source metadata; subclass state is now preserved. - ngio's own writes could be invisible on a store carrying consolidated metadata — ngio now ignores
.zmetadataeverywhere (it never wrote any). - A dask write could lose updates when the write unit exceeded dask's block budget; the budget now covers one write unit, and the process-global
DASK_STORE_LOCK(which never worked across processes) is gone —da.to_zarrgives each write unit exactly one writer. - Writing through a chain of two or more transforms now inverts the chain — it applied the inverses in forward order, silently wrong for any user chain on a write path.
- A merge can no longer broadcast a wrong-shaped patch or hand zarr a promoted dtype (a float patch under
merge="max"used to wrap-cast,-1.0becoming65535); integer"sum"still wraps within the dtype — numpy addition, documented. - The zoom transform's edge cases are pinned: negative starts clamp like the read, empty selections zoom to empty patches, and a scaled list selection raises instead of silently stretching non-adjacent pixels.
cache=Falsere-probes: a container's remembered "no tables" answer and a table's memoised type could outlive writes made through another handle; both now honour the cache flag.normalize_anndatadropped.rawwhile normalisingobs; it is carried through again (the result still shares its other components with the input rather than copying them).- A failed
mapon a stitched iterator cleaned up the scratch even when it had only opened a prepared root (a resumed run, or the gather step) — one failure could delete every job's banked predictions. Cleanup now runs only when the failing run created the scratch itself. - The write-conflict graph keyed output arrays by handle identity, so two
zarr.Arrayhandles onto one stored array were never compared — a latent lost update under parallel mappers. It now keys on the stored array (store + path). - A whole-array dask write now verifies its block grid gives each write unit exactly one writer, raising loudly instead of leaning on a silenced dask warning.
relabel_sequentialand the stitch compaction walk sharded labels per shard, not per inner chunk (each inner-chunk write was a full-shard read-modify-write).- A
BatchedMapperreduction on a ragged batch raises: the result is computed on padded patches, so the padding had already leaked into the values. - Invalid sharding/compression setups fail before anything is written, with named errors instead of a raw zarr error after the group metadata already landed (a half-built container): shards on OME-Zarr 0.4, an explicit shard shape with
chunks="auto", and a cross-format derive inheriting codecs or shards (compressors="auto"picks the target format's default;shards="auto"on a 0.4 target means no sharding). find_dimension_separatoraccepts the v2 chunk-key encoding on zarr v3 arrays, so deriving from such stores no longer fails.- Serialising a ROI table warns once per unknown extra column instead of once per ROI, the message explains the round-trip consequence, and
confidence— written bydetect— is recognised and no longer warns. - Stale table names are skipped by typed listings instead of creating a group for the missing table; shards clip to whole chunk multiples instead of producing a geometry zarr rejects;
mode="coarsen"asked to upsample raises a named error instead of a bare divide-by-zero; a dropped channel selection is no longer validated before the removal that exempted it; a zero-sized axis reports itself instead of blaming the tail policy; stitch plan warnings fire at the start ofmap, pointing at the caller, not mid-run from a worker. create_ome_zarr_from_array(store=<pre-opened zarr.Group>)failed validating{}: the populate step's cached reopen trusted the caller group's in-memory attributes, which predate the metadata write. Acache=Truehandler built on a pre-opened group now takes its first attribute read from the store — the canonicalplate.get_image_store(...) → create_ome_zarr_from_array(...)converter pattern works again.GenericRoiTableV1.from_table_datareturnsSelfagain, not the base class — the narrowed annotation madeRoiTableV1fail theTableprotocol under type checkers, flagging every downstreamadd_table(name, roi_table).GenericRoiTableis constructible and loadable again:from_handlerwas left abstract with apassbody and never overridden, so the public export could not be instantiated and — sinceabstractmethodonly guards instantiation, not classmethod calls —get_as(name, GenericRoiTable)silently returnedNone. It now has a concretefrom_handler, a default meta (sameFieldIndexindex asRoiTable), and the abstract signature gained theattrspass-through theTableprotocol already declared; a future missing override raisesNotImplementedErrorinstead of returningNone.-
plate.get_images(max_workers=...)forwardsmax_workersto its internalimages_paths()listing. Dropping it on that hop kept the per-well walk serial and fired the plate fan-outNgioFutureWarningat callers who had already opted in — its own documented remedy could not silence it, transitively from every table helper built onget_images. -
A
cache=Trueplate now sees its ownadd_image/atomic_add_image/add_well: the add path evicted the cached well's group handler mid-operation, so every listing (images_paths,get_well,get_images, and the table helpers on them) kept serving the pre-write image list untilrefresh(). - The remove paths align with the add fix:
remove_image/atomic_remove_image(and the well removal they can cascade into) no longer orphan the other wells' cached handlers — a remove-then-add sequence on acache=Trueplate served pre-add listings — and the removed well's and image's cached objects are dropped instead of lingering. open_table_as(store, GenericRoiTable)opens a foreign ROI-typed table that carries noindex_keyattrs again: the new typed meta imposedFieldIndexon read, where the reader must use the stored index (fresh tables still write theFieldIndexdefault).add()+consolidate()on such a table serializes under the table's own stored index name — the rebuild used to produce aNone-named column whose failed anndata write truncated the group first, destroying the original table on disk.cache=Trueno longer breaks the firstderive_labelon a container (the/labelsbootstrap read stale pre-write attrs) or the read-back after theget_image_store→create_ome_zarr_from_arrayconverter pattern (a child handler pinned a cached pre-write group snapshot as fresh) — both regressions vs 1.0.x, loud errors, introduced with the 1.1.0 caching rework.- The zarr-python copy fallback (memory/zip stores —
derive_*(copy_labels=True), the anndata backend) writes through the write-unit-aligned dask path; it was the oneda.to_zarrcall the 1.1.0 alignment fix missed, and a shard larger than dask's block budget silently lost most of its pixels. GenericRoiTableis registered and typed:get(strict=True)/get_generic_roi_tableload tables typedgeneric_roi_table(they were listed bylist_roi_tablesbut unloadable, and could makeget_masked_imageraise), and a freshGenericRoiTablewritestypeto disk so its identity survives the round trip. Tables written by earlier ngio versions carry no type and keep loading as generic tables.RoiTable()/MaskingRoiTable()/GenericRoiTable()followed byadd(roi)thenadd_tableno longer writes an empty table: serialization reset lazily-unbuilt ROIs to an empty wrapper, silently discarding them from the store and the in-memory table alike.- The inner merge of
set_roi_masked(merge=...)enforces the same anti-broadcast guard as top-levelmerge=: a wrong-shaped result from a custom rule was re-broadcast by the mask fold and written silently. - A stitch bank's validity attrs land after its payload, and a tile's core write lands before its bank: both crash windows now surface as the loud "never banked" refusal at
finalize()instead of a valid-looking zero bank or a tile silently missing from the output. - A
finalize()interrupted inside thecompact=Truein-place renumbering is detected (aresolvingmarker on the scratch) and the retry refused with the recovery spelled out — it used to pass every guard and permanently split objects straddling the crash point.prepare_jobsdeliberately starts over and clears the marker. - Distributed
measure/detectpartials record each ROI's/tile's own column set and dtypes, and the gather restores them: the merge used to union columns across units (NaN-filled, ints promoted to float), so column- or dtype-sensitive custom joins diverged from serial and integer detection extras came back as floats.
Behaviour changes¶
RoiandRoiSliceare frozen — assigning to a field (roi.label = 2,roi.get("z").length = 5.0) raises a pydanticValidationErrorinstead of mutating. In-place mutation had started to silently diverge from what a containing ROI table serialized (the table's frame rebuilds only throughadd()), so stale values were written with no error. Build a changed ROI withupdate_sliceormodel_copy(update=...)and put it back withadd(roi, overwrite=True)— both unchanged.slicesis a tuple and every mutator (update_slice,remove_slice,zoom, ...) re-validates and carries extra fields, so element-level surgery and invalid copies are no longer possible.- The
dask[array]floor rises to 2025.12 — the first release whoseto_zarrboth aligns writes to the target's write unit and warns (rather than raises, dask#12159) on the rechunk it does so. - The promotion out of
experimentalrenames the iterator surface — one-time renames, no shims (the 1.0 surface was explicitly experimental):grid()→by_grid(),by_chunks(grid="write")→by_write_units(),overlap_xy→overlap_x/overlap_y,check_if_chunks_overlap→check_if_write_units_overlapandrequire_no_chunks_overlap→require_no_write_units_overlap(both now measured on the write target, at shard granularity when sharded),post_consolidate()→finalize(),ImageProcessingIterator(input_channel_selection=)→channel_selection=.MapperProtocolchanged shape the same way ((func, units); no known custom mappers exist). - (beta-only break) The 1.1.0b* iterator surface consolidated into the final verb table; none of this ever shipped stable. Gone, behavior absorbed:
measure_to_partial/detect_to_partial/merge_partials(afor_jobslice'smeasure/detectnow bank the partial and returnNone— return typesTable | None/RoiTable | None— andfinalize()is the gather, returning the table on the read-only iterators and raising on a slice or with nothing banked);iter_batched(→iter(batch_size=...)); thestitch=/nms=constructor arguments and thecoalesce=kwargs (→ the chain declarationswith_stitch/with_nms/with_join, declared beforefor_job; a slice's declared join is inert); and the nameNmsConfig(→GreedyNms). Every join — serial or distributed — now receives the same normalized list (DataFrames with alabelcolumn, stampedroi_index/roi_name, empty ROIs as empty column-less frames); functions returning_ngio_index/roi_index/roi_nameare refused, and the defaultmeasuretable gains the two provenance columns.AbstractIteratorBuildergained a third Generic parameter (thefinalizeresult) and became the read-only root: the writer surface (mapverbs, setter builders, write-unit gates) moved to a newWritingIteratorBuilder, so the read-only iterators no longer carry methods that could only raise, and theiriterdefaults to readonly. The clone hookget_init_kwargs()became the private_get_init_kwargs()(defining the old name fails at class definition). - (beta-only break) Segmentation refuses undeclared overlapping write footprints: without
with_stitchoron_overlap, overlapping label writes raise at every writing verb instead of silently last-writer-winning. The check is pixel-exact (a halo never triggers it; sharing a chunk without sharing pixels passes); masked writes are exempt by construction; image processing keeps its permissive default. - (beta-only break) Contested-pixel write order is now a defined, declared contract instead of an undocumented by-product of greedy colouring: schedule-owned by default (
write_order="any"), later-ROI-wins on request ("roi"). Earlier 1.1 betas could put an earlier ROI after a later one while the docs claimed "the later wave wins". Pixel-disjoint tilings keep their packed schedules under either value; the serial-degradation warning now fires on schedule width, not wave count. - A crashed stitched/distributed run can leave transient
_ngio_stitch/_ngio_partialsgroups beside the resolution levels — unregistered, ignored by older ngio, wiped by the next run. cache=Trueactually caches: metadata is held for the object's lifetime (outside writes needrefresh()), and caching composes with the atomic plate/well operations. Under both cache modes an image's derived values —dimensions,dataset,pixel_size— are pinned per object (refresh()or reopening un-pins; undercache=Falseany metadata read does too); elsewherecache=Falseis unchanged and remains the default.max_workers=0raises instead of running silently serial;by_grid(tail="drop")raises when it would drop every tile; a writingmapno longer holds every written patch in memory until it returns.- ngio no longer wraps a plain local store in
NgioStorewhen the retry policy is a no-op (the wrapper costzarrsfor nothing), and a codec pipeline that silently fell back now warns once per store type. measurereturns an emptyFeatureTablewhen zero objects are found — the same contract asdetect— instead of raising.stitch(compact=True)warns when the final renumbering reaches ids outside the iterated ROIs: the compaction walks the whole label, so external references keyed by those ids (feature tables, masking ROI tables) go stale.
Deprecated¶
All removals scheduled for ngio=1.2. The default-flip entries warn as NgioFutureWarning (Python hides DeprecationWarning from end users); the rest as NgioDeprecationWarning.
| Deprecated | Use instead |
|---|---|
set_axes_units (#232) |
set_space_unit / set_time_unit (the batch form silently resets the unit you omit) |
ngio.experimental.iterators |
ngio.iterators (one more release of forwarding for the Fractal tasks ecosystem) |
iter_as_dask, map_as_dask, reduce_as_dask, iter(data_mode="dask") |
the numpy verbs; Image.get_as_dask for lazy whole-region access |
bare iter() defaulting to dask |
pass data_mode="numpy" now; numpy becomes the default |
legacy transform methods (get_as_numpy_transform & co.) |
the two-method on_get/on_set contract |
| the ROI and masked pipe classes (8 names) | bare pipes with roi=, MaskTransform in transforms=, merge=MaskMerge(...) |
consolidate() without a mode |
becomes mode="auto"; pass mode="dask" to pin today's behaviour |
plate fan-outs without max_workers |
becomes "auto"; pass max_workers=1 to pin serial reads |
NgioCache.set(overwrite=) |
drop the argument — it was always ignored, set always overwrites |
Removed¶
- (beta-only) The 25 iterator names the betas added to the top level (the mappers, the scheduling/reconciliation primitives,
ObjectDetectionIterator,AbstractIteratorBuilder,StitchConfig, ...) live inngio.iteratorsonly, andZarrConfig/DaskConfig/ConsolidationConfiginngio.config. The pre-1.1 top-level names are unchanged. - (beta-only)
ZarrGroupHandler.refresh(): it had no callers — the containers'refresh()are the entry points;invalidate_meta()/clean_cache()remain. - The surfaces v1.0.0 deprecated for
ngio=1.1are gone (except theexperimental.iteratorsalias — see Deprecated): theconctatenate_tablestypo alias,set_axes_unit, thelevels_paths=/validate_paths=keyword aliases, and every*_asyncvariant —plate.get_images_async()becomesplate.get_images(max_workers="auto").
Performance¶
- Metadata is no longer re-read and re-decoded per access:
image.dimensions— read once per ROI by every iterator — drops from ~520µs to ~2µs, and to zero store reads undercache=True. - Plate walking is flat instead of quadratic:
create_empty_plate733 → 178 metadata reads (14.4 MB → 128 KB written on a 384-image plate); per-well image paths 75 → 9 reads. - Dask writes align to the write unit: sharded writes stop read-modify-writing whole shards (128 chunk reads → 2 on a 1 MB write); in-memory blocks are capped at 8 MiB for a ~75% peak-memory cut at no wall-clock cost.
zarrsactually engages on local stores:set_array76 → 17 ms,get_as_numpy39 → 9 ms on a 32 MB image.- Consolidation reads less: sharded sources read at their write unit (324 chunk reads → 8), the numpy path chains levels through memory, coarsening drops its float64 intermediate (~3× peak), and the dask mode stops computing every source chunk twice.
- Tables and overlap checks: type-filtered listings are one memoised pass under
cache=True(95 reads → 2 warm;cache=Falsere-reads per call, by contract), ROI tables rebuild their DataFrame only on change, andcheck_if_regions_overlapsweeps instead of scanning all pairs (2,048 ROIs: 1,083 ms → 31 ms;check_if_write_units_overlapremains all-pairs).
Docs¶
- The array setters (
set_array,set_roi,set_roi_masked) document that dask patches are serial-only: concurrent dask writes from several threads can silently lose updates. Enforcing it was deliberately left out — the guard's blast radius outweighs a hazard with no known concurrent callers. - The iterators getting-started page is now a concept guide: it keeps the design-system figures (the iterator walk, the five iterators side by side, tail policies, wave scheduling, halos, stitching, and detection) and the short core snippets, and hands the long worked examples to the tutorials.
- New Stitching tutorial: a tiled watershed segmentation on real microscopy data, with and without
with_stitch(), plus theStitchConfigtuning knobs. - New Distributed processing tutorial: the
prepare_jobs → for_job → finalizerecipe executable end to end — partition layouts, distributed stitching, and distributed measurement. - The object detection tutorial gained the NMS walkthrough (raw pre-NMS boxes against the suppressed result) and
Roi.anchor; the feature extraction tutorial ends with a scatter drawn from the feature table (area against mean intensity, rasterized marks so the page stays light). - The object detection snippet script joined the
test_snippetschain, where it had been missing.
Chores¶
- The exact operation-count baselines behind the numbers above are committed under
tests/performance/and gated in CI. - Fixed the Windows CI test failures from the
v1.1.0a1build: three tests asserted the bare-LocalStorebypass unconditionally, but on WindowsNgioStorealways wraps local stores (its sharing-violation retry is unconditional). The tests are now platform-aware, and the Windows-wrap decision is covered on every platform via a monkeypatched test. - Dropped
commitizen; releases are now tagged by hand (seeCONTRIBUTING.md).
[v1.0.1]¶
Concurrency and Windows fixes. No API change — every v1.0.0 call keeps working.
Behaviour changes¶
- Lock files moved out of the Zarr store into a
<store>.ngio-locks/directory beside it. Upgrade every writer to a plate at the same time: a≤1.0.0writer takes the old in-store paths, so mixed versions never exclude each other and one concurrent update is lost silently. Groups differing only after a dot (foo.bar,foo.baz) no longer share a lock. Locks are keyed on the store root, so a well opened directly withopen_ome_zarr_welland the same well reached through its plate no longer share one — take atomic well operations through the plate. Old.lockfiles left inside a store by an earlier version are not cleaned up. atomic_add_image/atomic_remove_imagewarn on Windows that their lock is best-effort:filelockcan hand it to two writers at once, so concurrent ones can lose an update —v1.0.0lost it silently. A single writer is unaffected. The warning is an error under-W errororfilterwarnings = ["error"].
Fixes¶
- Concurrent writers no longer race on creating a group:
get_group(create_mode=True)is a get-or-create, andatomic_add_imagecreates the well group under the plate lock. Two workers adding to the same well could fail withNgioFileExistsError. - Windows: concurrent reads of a metadata file no longer fail with
PermissionError.v1.0.0only retried the conflict when the error carried a Win32 code, whichos.replacesets butopen()does not. Label.consolidate(mode="coarsen")averaged label IDs instead of taking the maximum — the mean of labels 3 and 7 is 5, a label that never existed — and truncated on integer dtypes.on_disk_zoomdid not forwardorderto the coarsening path.- zarr 3.3 support. zarr 3.3 added a coalescing
get_rangesand a synchronous store surface (get_sync,set_sync,delete_sync); ngio's store inherited all of them fromWrapperStoreforwarded straight to the wrapped store, so they ran with no retry policy and no Windows sharing-violation retry — unlike every other IO operation. They are now routed through the same retry path. Nothing reached these methods with zarr's defaults, so no released version could lose data over it; sharded arrays and the opt-inFusedCodecPipelinedo.
Chores¶
- Added a performance gate at
tests/performance/: exact store-operation counts asserted against committed baselines, running in CI like any other test. Seetests/performance/README.md. - The performance gate counts zarr 3.3's new store surface, and its instrumentation check now covers
WrapperStoreas well asStore— the sync methods arrived on the former and aStore-only check could not see them. It also fails when zarr removes a hooked method, which would previously have zeroed a counter silently. Op counts are unchanged on zarr 3.1.6, 3.2.1 and 3.3.0. - The op-count assertion is skipped when the counts differ and zarr is not the version the baselines were generated on, so an upstream zarr release no longer fails
CI (pip), which installs dependencies unpinned. Thetest11environment still asserts strictly on every PR. - Linting moves from
pre-committoprek, a drop-in reimplementation.pixi run -e dev lintis still the entry point;pre-commit autoupdatebecomesprek auto-update. - CI no longer depends on any Node 20 action.
[v1.0.0]¶
First stable release. Everything deprecated in v0.5.0 (each warned "will be removed in ngio=0.6") is now removed — that release became 1.0.0.
Removed¶
| Removed | Use instead |
|---|---|
OmeZarrContainer.image_meta |
.meta |
.levels_paths |
.level_paths |
.set_channel_percentiles(a, b) |
.set_channel_windows_with_percentiles(percentiles=(a, b)) |
version= on the plate/well create and derive functions |
ngff_version= |
check_type= on get_table |
get_table_as(name, TableCls) or get_*_table(name) |
pixel_size=, xy_pixelsize= on create/derive |
pixelsize= (plus z_spacing=, time_spacing=) |
xy_scaling_factor=, z_scaling_factor= |
scaling_factors= |
labels=, channel_labels=, channel_wavelengths=, channel_colors=, channel_active= |
channels_meta= |
wavelength_id=, start=, end=, percentiles=, colors=, active= on set_channel_meta |
channel_meta=ChannelsMeta(...) |
pixelsize is now required on create_empty_ome_zarr and create_ome_zarr_from_array. The pixel_size= argument on the getters is unaffected: pixel_size selects a pyramid level, pixelsize is a value written on create.
Behaviour changes¶
Same call, different result:
derive_imageinheritsdtype,dimension_separatorandcompressorsfrom the reference image instead of forcinguint16— deriving from afloat32image no longer silently downcasts it.add_tableandwrite_tablekeep the source table's backend instead of rewriting it asanndata_v1(#207). Passbackend=to convert.- Opening a container no longer reads every pyramid level (
validate_arrays=Falseby default), so a bad array fails on first access rather than at open. open_imageandopen_labeldefault tostrict=False, matching every other getter.list_roi_tablesreturns[]instead of raising when there are no tables.get_masked_label(path=...)resolves the masking label at the label's own pixel size, matchingget_masked_image.PixelSizes with differenttime_units now compare unequal, and==against a non-PixelSizereturnsNotImplemented.
Deprecated, removal in ngio=1.1¶
conctatenate_tables → concatenate_tables, set_axes_unit → set_axes_units, levels_paths= → level_paths=, validate_paths= → validate_arrays=, ngio.experimental.iterators → ngio.iterators, and every *_async plate/table function → the sync form with max_workers=.
Migration (v0.5 → v1.0)¶
create_empty_plate(store, ngff_version="0.4") # was version=
create_empty_ome_zarr(store, pixelsize=0.5, # was xy_pixelsize=
scaling_factors=(1.0, 2.0, 2.0)) # was *_scaling_factor=
ome_zarr.get_table_as("roi", RoiTable) # was check_type="roi_table"
ome_zarr.derive_image(store, channels_meta=["DAPI"], pixelsize=(ps.y, ps.x))
ome_zarr.set_channel_meta(channel_meta=ChannelsMeta.default_init(labels=["DAPI"]))
from ngio import SegmentationIterator # was ngio.experimental
plate.get_images(max_workers=8) # was get_images_async()
Features¶
- The iterators are stable API:
from ngio import SegmentationIterator. - Configurable IO retries:
NgioConfig.io_retryplus thengio.utils.retry_iodecorator. ngio's ownNgioErrors are never retried. See the Configuration page. ngio.utils.NgioStorewraps every zarr store ngio opens and applies that retry policy to all IO.ZipStoreis now supported.max_workers=on the sync plate and table APIs replaces the separate async surface;Nonekeeps the serial behaviour.- A larger public namespace, including
MaskedImage,MaskedLabel,Channel,S3FSConfig,derive_ome_zarr_plate,__version__, theget_ngio_*_metareaders and every error class.AbstractBaseTable,ImplementedTablesandwrite_tableare exported fromngio.tables, so a custom table type can be registered without private imports. NgioTableValidationErrornow subclassesNgioValidationError, soexcept ValueErrorcatches it like its siblings; newNgioKeyError.
Fixes¶
- Dask writes could silently drop data.
da.store(..., lock=False)let two blocks read-modify-write the same chunk — or shard, when the target is sharded — concurrently, losing one update. This hit every region write that was not chunk-aligned and every sharded target, including pyramid consolidation. Allda.storecalls now share a lock; block compute stays parallel. - Windows: concurrent access to a store no longer fails with
PermissionError: [WinError 5]/[WinError 32]. A concurrent reader ofzarr.jsoncould break a writer's atomic rename; store operations now absorb these transient conflicts with a short bounded retry. No behaviour change on Linux or macOS. import ngiono longer raisesAttributeErrorwhen an s3fs older than 2026.2.0 is installed.concatenate_image_tablesbuilt a wrong index: unnamed, and duplicated undermode="lazy".Roi.union/intersectiondropped ROI name""and label0;Roi.from_valuesnow validates its inputs.- Plate and well metadata
add_*/remove_*mutated the receiver instead of returning a copy. AxesSetup.from_ordered_listsilently dropped a non-canonical axis in some orders.- Grid iterator ROIs now get unique names, and
by_chunkswith overlap ≥ chunk size raisesNgioValueError.
Packaging¶
- Ship
src/ngio/py.typed. The PEP 561 marker was missing, so downstream type checkers ignored ngio's annotations. - Real lower bounds on every dependency, exercised by a
test-min-depsCI leg:zarr>=3.1.6,numpy>=2.0,fsspec>=2025.3,anndata>=0.12.5,ome-zarr-models>=1.4and the rest.pandas3.x andanndata0.13 are now allowed, therequires-pythonupper cap is gone, and unusedrequests/distributedare dropped. - New
s3extra:pip install ngio[s3].
Docs¶
- Rebuilt on Zensical with every code block executed at build time, plus new landing, glossary and Configuration pages.
[v0.5.14]¶
Fix¶
- Fix saving empty tables (#99). Empty tables now round-trip through both backends instead of raising a cryptic pandas error: (1) an empty ROI/masking table (zero ROIs) keeps its schema columns; (2)
_validate_cast_index_dtype_dfcasts an empty index to the requestedstr/inttype instead of rejecting it; (3) an empty ROI table with no backend materializes as an empty table rather than raising; and (4)convert_pandas_to_anndatano longer drops the numeric columns of a zero-row table (it now checks for zero columns rather thanDataFrame.empty, which is also true for a zero-row frame). - Fix
write_tablewriting an empty table (or raisingFileNotFoundError) when given a table returned byopen_tablewhose data had not yet been loaded.write_tablenow materializes the table data before swapping to the destination backend, mirroringTablesContainer.add, so a table opened from one store can be copied to another viawrite_tablewith its data intact. - Fix tables with missing (
None/NaN) values in string columns failing to serialize to the AnnData backend. The_check_for_mixed_typesand_check_for_supported_typesguards now ignore missing values and classify a column from its non-null contents, so a string column containingNone(or an all-missing column) is accepted — matching AnnData's native handling, which stores such columns as a categorical withNaNfor the missing entries. This surfaced when copying a condition table (e.g.get_table+add_table), which re-serializes through the default AnnData backend. Note: the round trip normalizes missing-containing string columns fromobject/Nonetocategory/NaN.
[v0.5.13]¶
Feature¶
- Add a global
NgioConfig/get_config()configuration system, loaded from~/.ngio_config.jsonby default or a path set via theNGIO_CONFIG_PATHenv var (.jsonfile). Both are exported from the top-levelngiopackage. - Add configurable s3fs retry handling:
NgioConfig.s3fs.custom_retry_markerslists error substrings that trigger a retry via a customs3fs.set_custom_error_handler, applied through the newngio.utils.refresh_s3fs_config(). The motivating use case is AWS clock-skew errors, but any error substring can be configured.
Tests¶
- Migrate the S3 store test harness from a
moto[server]subprocess toaiomotoin server mode (aiomoto[pandas]in thetestextra). This also fixes a CI import crash on Python 3.13/3.14:aiomotocapsaiobotocore/motoand floorss3fs, so the universal (multi-platform) solve no longer backtrackss3fsto the ancient0.4.2(which lacksset_custom_error_handlerand crashedimport ngioat module load). CSV and Parquet table backends now round-trip on the S3 store under the mock.
Fix¶
- Fix
derive_label(andOmeZarrContainer.derive_label) rejecting an explicitshapethat omits the channel axis. When the reference image has acaxis andchannels_policyremoves or overrides it ("squeeze","singleton", or an integer), the up-front shape-length check failed before the channel policy was applied. The provided shape is now normalized to the reference dimensionality before pyramid computation, so a channel-less shape (e.g.(z, y, x)for a(c, z, y, x)image) is accepted.channels_policy="same"still requires the full shape. - Fix
is_group_listablewrongly reportingTruefor stores that cannot actually be listed (e.g. HTTP hosts without a directory index): zarr >= 3.1.6 swallows the listing error onFsspecStoreand yields an empty listing instead of raising. The check now verifies that the group's own metadata document (zarr.json/.zgroup) — which must exist for any group that was successfully opened — appears in the store listing, distinguishing a broken listing from a genuinely empty group on any store type. copy_groupnow raises an error when the source listing does not contain the group's metadata document, instead of silently producing an empty copy from a non-listable store.
Chores¶
- Harden GitHub Actions and scan workflows through
zizmor. - Rename the
pre-commitpixi dev task tolint: the old name shadowed thepre-commitbinary inpixi run, and its trailinggit add -usilently staged the working tree and masked hook failures in the task's exit code.
[v0.5.12]¶
Fix¶
- Fix loading v0.4 HCS plates where the
versionkey is absent from the plate-level metadata: the v0.4. V0.4 decoder now explicitly inject the version into the plate dict before constructingPlateWithVersion, so missing orNoneversion values no longer cause a validation error.
Refactor¶
- Remove redundant
versionfield fromNgioPlateMeta: the field is now a@computed_fieldproperty that delegates toself.plate.version, eliminating the need to keep two copies of the NGFF version in sync. The public.versionattribute andmodel_dump()output are unchanged.
[v0.5.11]¶
Fix¶
- Remove eager uniqueness check on
wavelength_idinChannelsMeta.default_init: duplicatewavelength_idvalues are now allowed at creation time.get_channel_idxraises a clear error if a lookup by an ambiguouswavelength_idis attempted, directing users to select by label instead.
[v0.5.10]¶
Fix¶
- Replace
da.to_zarrwithda.store(..., lock=False)in pyramid writes (_on_disk_dask_zoom,_on_disk_coarsen) and region slice writes (_ops_slices). Dask >=2025.11'sto_zarrre-derives chunks vianormalize_chunks(chunks="auto", ...)and emits aPerformanceWarning(treated as error by ngio's filterwarnings) when the result is not a multiple of the target's chunks;da.storewrites blocks 1:1. - Copy object/string-dtype zarr arrays directly when consolidating groups: dask >=2025.11 raises
NotImplementedErrorfrom auto-chunking for these dtypes, so they bypass dask and are copied via numpy. - Set
auto_shard_zarr_v3together withzarr_write_formatonanndata's global settings via a new_update_anndata_global_settingshelper, so reading/writing tables works correctly when mixing zarr v2 and v3 in the same session on anndata 0.12.
Chores¶
- Pin
anndatato>=0.12.0,<0.13.0. - Unpin
dask(remove the<2025.11.0upper bound introduced in v0.4.5).
[v0.5.9]¶
Fix¶
- Fix AnnData reading over HTTP when directory listing is disabled: skip optional Zarr groups (
uns,obsm,varm, etc.) that cannot be discovered without listing. - Fix
ngff_versionnot being propagated when deriving a plate:derive_plate()andderive_ome_zarr_plate()now defaultngff_versiontoNoneand inherit the source plate's version when no version is explicitly provided.
[v0.5.8]¶
Fix¶
- Change tolerance when converting Roi to pixel coordinates to avoid machine precision dependent rounding issues.
Tests¶
- Improve testing for ZoomTransform.
- Remove broad warnings filter for all tests.
Chores¶
- Replace custom logger warnings with standard Python warnings for better integration with user applications.
[v0.5.7]¶
Fix¶
- Add docstrings to
ChannelSelectionModelto allow for correct json schema generation.
[v0.5.6]¶
Fix¶
- Fix translation check in
_ngio_to_v04_multiscaleand_ngio_to_v05_multiscale: translations were incorrectly dropped when all values were negative or when positive and negative values cancelled out. - Fix shape compatibility check in
_check_compatibility_of_shapes: integer indices in the slicing tuple now correctly reduce the expected shape rank instead of inserting a spurious size-1 dimension.
[v0.5.5]¶
Features¶
Roinow supports dict-like slice access:roi["x"]returns the slice for axis"x"and raisesKeyErrorif the axis is not present.Roi.get(axis_name, default=None)now accepts an explicitdefaultvalue, following thedict.getconvention.- New
Roi.update_slice(name, new_slice)method: replaces the slice for an existing axis or appends a new one. Returns a newRoiinstance. - New
Roi.remove_slice(name)method: removes the slice for a named axis. Returns a newRoiinstance. RaisesNgioValueErrorif the axis is not present.
Chores¶
- Pin
mkdocsto version <2.0 to avoid build errors in CI due to breaking changes in mkdocs v2, and incompatibility with material design theme.
[0.5.4]¶
Fix¶
- Remove file locking remove in
ZarrGroupHandler, which was not used anywhere and is unnecessary in new lockfile release. - Correctly set Zarr array dtype to array dtype in
create_ome_zarr_from_array
[0.5.3]¶
Fix¶
- Fix bug in AnnData backend where "raw" entry with encoding-type "null" is written by default in newer anndata versions, which causes compatibility issues with older anndata versions. Now the "raw" entry is removed after writing if it has encoding-type "null".
[0.5.2]¶
Fix¶
- Fix critical bug in masking roi image handling causing incorrect results when image and mask have different pixel sizes.
- Fix bug in loading masking roi images when paths other than default are used.
[0.5.1]¶
Fix¶
- Fix bug causing incorrect channel metadata when creating an image.
- Fix correctly setting the space and time units when creating an image.
- Fix minor bug in
set_channel_windows_with_percentilesmethod.
Chores¶
- Improve logging consistency across the codebase.
[v0.5.0]¶
Features¶
- Add support for OME-NGFF v0.5
- Move to zarr-python v3
- API to delete labels and tables from OME-Zarr containers and HCS plates.
- Allow to explicitly set axes order when building masking roi tables.
- New metadata modification APIs for
Image,Label, andOmeZarrContainer: set_channel_labels- Update channel labelsset_channel_colors- Update channel colorsset_channel_windows- Update channel display windows (start/end values)set_channel_windows_with_percentiles- Update display windows based on data percentilesset_axes_names- Rename axes in the metadataset_axes_unit- Set space and time units for axesset_name- Set the image/label name in metadata- Add translation support in all image/label creation and derivation APIs.
API Breaking Changes¶
- New
Roimodels, now supporting arbitrary axes. - The
compressorargument has been renamed tocompressorsin all relevant functions and methods to reflect the support for multiple compressors in zarr v3. - The
versionargument has been renamed tongff_versionin all relevant functions and methods to specify the OME-NGFF version. - Remove the
parallel_safeargument from all zarr related functions and methods. The locking mechanism is now handled internally and only depends on thecache. - Remove the unused
parentargument fromZarrGroupHandler. - Internal changes to
ZarrGroupHandlerto support cleanup unused apis. - Remove
ngio_loggerin favor of standard warnings module.
Migration Guide (v0.4 → v0.5)¶
Roi API Changes¶
The Roi class now uses a flexible slice-based model supporting arbitrary axes:
# Old (v0.4)
roi = Roi(x=34.1, y=10, x_length=321.6, y_length=330)
# New (v0.5)
roi = Roi.from_values(slices={"x": (34.1, 321.6), "y": (10, 330)}, name=None)
# Accessing coordinates
# Old: roi.x, roi.y, roi.x_length, roi.y_length
# New: roi.get("x").start, roi.get("y").start, roi.get("x").length, roi.get("y").length
Argument Renames¶
# compressor → compressors
# Old (v0.4)
create_empty_ome_zarr(..., compressor=Blosc())
# New (v0.5)
create_empty_ome_zarr(..., compressors=Blosc())
# version → ngff_version
# Old (v0.4)
create_empty_ome_zarr(..., version="0.4")
# New (v0.5)
create_empty_ome_zarr(..., ngff_version="0.4")
Removed Arguments¶
parallel_safe: No longer needed, locking is handled internallyngio_logger: Use Python's standardwarningsmodule instead
Deprecations¶
- Standardized all deprecation warnings to indicate removal in
ngio=0.6. - Deprecated
set_channel_percentilesmethod, useset_channel_windows_with_percentilesinstead.
Fix¶
- Fix bug in
consolidatefunction when using coarsening mode with non power-of-two shapes. - Fix HCS plate column name formatting to use standardized zero-padding (e.g., column
3is now stored as"03"). - Fix
_stringify_columnnot passingnum_digitsparameter to_format_int_column.
Documentation¶
- Fix incorrect and incomplete docstrings across the codebase:
compute_masking_roi: Added Args/Returns, fixed description (supports 2D, 3D, 4D).lazy_compute_slices: Added Args/Returns sections.LabelsContainer.list: Fixed description (was "Create the /labels group").build_masking_roi_table: Added Args/Returns sections.TablesContainer: Fixed class and method descriptions (were referencing labels instead of tables).NgioPlateMeta.add_well: Fixed description (was "Add an image to the well").NgioPlateMeta.derive: Fixed type annotation in docstring (NgffVersion→NgffVersions).- Added missing docstrings to several HCS helper functions.
[v0.4.7]¶
Fix¶
- Fix bug adding time axis to masking roi tables.
- Fix channel selection from
wavelength_id - Fix table opening mode to stop writing groups when opening in append mode.
[v0.4.5]¶
Fix¶
- Pin Dask to version <2025.11 to avoid errors when writing zarr pyramids with dask (see https://github.com/dask/dask/issues/12159#issuecomment-3548421833)
[v0.4.4]¶
Fix¶
- Fix bug in channel visualization when using hex colors with leading '#'.
- Remove strict range check in channel window.
[v0.4.3]¶
Fix¶
- Fix bug in deriving labels and image from OME-Zarr with non standard path names.
- Add missing pillow dependency.
- Update pixi workspace config.
[v0.4.2]¶
API Changes¶
- Make roi.to_slicing_dict(pixel_size) always require pixel_size argument for consistency with other roi methods.
- Make PixelSize object a Pydantic model to allow for serialization.
Fix¶
- Improve robustness when rounding Rois to pixel coordinates.
[v0.4.1]¶
Fix¶
- Fix bug in zoom transform when input axes contain unknown axes (e.g. virtual axes). Now unknown axes are treated as virtual axes and set to 1 in the target shape.
[v0.4.0]¶
Features¶
- Add Iterators for image processing pipelines
- Add support for time in rois and roi-tables
- Building masking roi tables expanded to time series data
- Add zoom transformation
- Add support for rescaling on-the-fly masks for masked images
- Big refactor of the io pipeline to support iterators and lazy loading
- Add support for customize dimension separators and compression codecs
- Simplify AxesHandler and Dataset Classes
API Changes¶
- The image-like
get_*api have been slightly changed. Now if a single int is passed as slice_kwargs, it is interpreted as a single index. So the dimension is automatically squeezed. - Remove the
get_*_delayedmethods, now data cam only be loaded as numpy or dask array.Use theget_as_daskmethod instead, which returns a dask array that can be used with dask delayed. - A new model for channel selection is available. Now channels can be selected by name, index or with
ChannelSelectionModelobject. - Change
table_namekeyword argument tonamefor consistency in all table concatenation functions, e.g.concatenate_image_tables,concatenate_image_tables_as, etc. - Change to
Dimensionclass.get_shapeandget_canonical_shapehave been removed,getuses new keyword argumentsdefaultinstead ofstrict. - Image like objects now have a more clean API to load data. Instead of
get_arrayandset_array, they now useget_as_numpy, andget_as_daskfor delayed arrays. - Also for
get_roinow specific methods are available. For ROI objects, theget_roi_as_numpy, andget_roi_as_daskmethods. - Table ops moved to
ngio.images - int
labelas an explicit attribute inRoiobjects (previously only in stored in name and relying on convention) - Slight changes to
ImageandLabelobjects. Some minor attributes have been renamed for consistency.
Table specs¶
- Add
t_secondandlen_t_secondto ROI tables and masking ROI tables
[v0.3.5]¶
- Remove path normalization for images in wells. While the spec requires paths to be alphanumeric, this patch removes the normalization to allow for arbitrary image paths.
[v0.3.4]¶
- allow to write as
anndata_v1for backward compatibility with older ngio versions.
[v0.3.3]¶
Chores¶
- improve dataset download process and streamline the CI workflows
[v0.3.2]¶
API Changes¶
- change table backend default to
anndata_v1for backward compatibility. This will be chaanged again when ngiov0.2.xis no longer supported.