Conversation
`metadata_sources` copies provenance from elsewhere; there was no way to record the metadata of the step doing the creating, so callers wrote it afterwards in a second pass over the plate. That second pass is not just slower -- it is unusable by a pipeline that scaffolds its stores up front. The source copy reads the upstream store's zattrs at plate-creation time, so if a step's own record only lands once its positions have been computed, a downstream `create_empty_plate` that runs before then inherits nothing and the provenance chain breaks. `extra_metadata` closes that, in the loop that already holds an open position handle for every key -- new ones from `create_position`, pre-existing ones from the channel-append branch -- so it costs no extra I/O. Two differences from `metadata_sources`, both deliberate: it is written to every position named rather than only newly created ones, so a re-run with a changed configuration refreshes the record; and it wins over an inherited key of the same name, since the caller is the authority on its own step. The name and semantics match `process_single_position`'s existing `extra_metadata` -- top-level zattrs keys, not nested -- and the two now share `_validated_extra_metadata` so they cannot disagree about which keys are legal.
srivarra
approved these changes
Sep 14, 2026
srivarra
left a comment
Collaborator
There was a problem hiding this comment.
It looks good overall, just one question about supporting other OME-Zarr objects.
I think it's worth formalizing some way to work with extra metadata via Pydantic and seeing if we can align it with the DCA specs somehow. But that's out of scope in this pr.
Comment on lines
+55
to
+56
| reserved = _OME_KEYS.intersection(extra_metadata) | ||
| if reserved: |
Collaborator
There was a problem hiding this comment.
These other keys are omitted: plate, well and bioformats2raw.layout, do we need them here?
Also, right now this won't work with OME-Zarr v0.4, but that's probably okay, we can just make sure the OME-Zarr is v0.5.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
extra_metadatatocreate_empty_plate: the caller's own per-position record, wheremetadata_sourcesis metadata from elsewhere. Entries land as top-level zattrs on every position named — the same spellingprocess_single_positionalready uses.Why
Downstream (biahub) writes this record today in a second pass over the plate after
create_empty_platereturns. That pass is not just slower — it is unusable by a pipeline that scaffolds its output stores up front.The source copy reads the upstream store's zattrs at plate-creation time, and only for newly created positions. So if a step's own record only lands once its positions have been computed, a downstream
create_empty_platethat runs before then inherits nothing, and the provenance chain is empty by the time anything runs. Writing it here is what makes a scaffold-everything-first pipeline keep its provenance (czbiohub-sf/biahub#304).Why inside rather than after the call
create_position, pre-existing ones from the channel-append branch — and already does azattrs.putfor new positions. A pass afterwards costs a second plate open plus N position re-opens inr+.metadata_sourcescopy deliberately does not. A re-run with a changed configuration has to refresh its own record; inherited metadata describes the store as it was made.extra_metadatabeats an inherited key of the same name, since the caller is the authority on its own step.Notes
_validated_extra_metadatais now shared withprocess_single_position, so the two seats that writeextra_metadatacannot disagree about which keys are legal (mapping, string keys, no OME-reserved names).create_zeros/append_channel, so the base dict is re-read rather than reused from before them.tests/ngff/test_ngff_utils.py, including one that chains twocreate_empty_platecalls and asserts the second inherits the first's record — the property this exists for.test_apply_transform_to_tczyx_and_savefails on this branch and equally onmain(a hypothesis-found sharding offset error); unrelated.🤖 Generated with Claude Code