Feature: Write provenance to zarr from parent file + run details#63
Feature: Write provenance to zarr from parent file + run details#63BrianWhitneyAI wants to merge 11 commits into
Conversation
Add an include_provenance flag (default off) that writes a top-level "bioio" attribute block beside the NGFF "ome" block in each scene's store, plus the source metadata XML as sidecars under bioio/. The block records the source file name, UTC conversion timestamp, reader plugin, bioio package versions (including bioio-conversion), the cross-format StandardMetadata for that scene, and pointers to the native/OME XML sidecars. Provenance assembly lives in a new bioio_conversion/provenance.py (ProvenanceBuilder), kept out of the converter proper. It is built once per conversion and cached: the metadata reader and whole-file XML are scene-independent, so only StandardMetadata is re-read per scene. For formats that surface richer metadata under non-default reader options, provenance opens a dedicated metadata reader (pixel I/O is unaffected): CZI uses an aicspylibczi reader with subblock metadata (embedding <Subblocks> into the native XML and deriving extra StandardMetadata such as total_time_duration), and ND2 uses the standard 96-well plate geometry so each scene's stage position is assigned a well row/column. The dedicated reader is best-effort and falls back to the pixel reader if it can't open or its scene names diverge. Provenance is keyed by scene name, not index, so a different metadata backend can't misattach a scene. Add a "provenance" install extra (bioio-czi, bioio-nd2) and pin bioio-nd2 in the test extras. Tests in tests/converters/test_provenance.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Example Provenance OutputStore layoutSource metadata sidecarsFull
|
SeanDuHare
left a comment
There was a problem hiding this comment.
need to finish, but here are some initial thoughts
| ``"bioio"`` attributes block (a sibling of ``"ome"``): the source's | ||
| cross-format ``standard_metadata``, the reader plugin and package | ||
| versions, and the native/OME metadata XML as sidecars under | ||
| ``bioio/``. For CZI this also captures per-subblock metadata. Off by |
There was a problem hiding this comment.
CZI bit feels unnecessary to mention - there will likely be quirks with each reader
| dtype: Optional[Union[str, np.dtype]] = None, | ||
| n_workers: Optional[int] = None, | ||
| shard_limit_bytes: int = DEFAULT_SHARD_LIMIT_BYTES, | ||
| include_provenance: bool = False, |
There was a problem hiding this comment.
provenance is such an overloaded term especially in biology - i feel like maybe "include_source_metadata" might help?
There was a problem hiding this comment.
We also include creation metadata so I dont think "include_source_metadata" would cover it, we could split to two flags?
| self._metadata_xml_cache: Optional[Tuple[Optional[str], Optional[str]]] = None | ||
| self._whole_file_cache: Optional[Tuple[Dict[str, Any], Dict[str, str]]] = None | ||
|
|
||
| def _metadata_reader(self) -> BioImage: |
There was a problem hiding this comment.
Why does this not re-use the bioimage reference from the caller? I think you did it this way because there are specific plugin parameters you want to use but that feels like it would be hard to discover as a user. Instead, providing warnings when certain information is missing could be a way to inform the user of the best route - I'll think about this more
There was a problem hiding this comment.
It is very hard to discover as a user. The real problem is that you need to be a super user to get well info out of CZIs or ND2s, you need to know that CZI needs aicspylibczi to get subblock data to get well and nd2 needs a plate definition. Then you also need to know that aicspylibczi doesnt have the same efficiency as pylibczirw. It is not an easy problem but a "metadata probe" that uses the optimal metadata read and a "pixel data probe" for optimal reads was the pattern I was thinking.
| """ | ||
| Whole-file ``(ome_xml, native_xml)`` for the source, serialized once. | ||
| """ | ||
| if self._metadata_xml_cache is None: |
There was a problem hiding this comment.
How necessary is this, is it just used per scene? Hos big can this get? I'm curious if memory is more precious here than a single file access
There was a problem hiding this comment.
otherwise we do it for every scene. Chris Fricks most recent had 47 scenes, Ive seen as high as 180
There was a problem hiding this comment.
I also think the subblock collection is somewhat expensive
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace semicolons in f-strings in provenance.py to avoid flake8 E702 false positives - Fix mypy error: pass Path(out_path) to write_sidecars instead of str, which also fixes sidecar files not being written (str / str fails silently inside write_sidecars) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove _metadata_reader_kwargs and its hardcoded aicspylibczi/nd2 logic from ProvenanceBuilder. Callers now pass metadata_reader_kwargs directly. OmeZarrConverter exposes this as provenance_reader_kwargs. Tests updated to pass aicspylibczi kwargs explicitly for the subblock test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add xmltodict>=0.13.0 dependency - Replace raw XML sidecar files with proper JSON via xmltodict.parse(); _OME_FORCE_LIST ensures repeating OME elements (Channel, Image, Plane, etc.) are always arrays regardless of count - Rename path constants: NATIVE_XML_PATH/OME_XML_PATH -> NATIVE_METADATA_PATH/OME_METADATA_PATH (.json extensions) - Update tests: read sidecars as JSON directly, no ET parsing needed; check root dict key instead of element tag; navigate Subblocks by key Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move sidecars from bioio/ subdirectory to the store root as metadata.native.json and metadata.ome.json - Replace xmltodict + _OME_FORCE_LIST for OME metadata with ome-types model_dump(mode='json') for cleaner, well-typed output - CZI native (ET.Element) still goes through xmltodict - Add provenance_reader_kwargs passthrough to OmeZarrConverter and ProvenanceBuilder, removing hardcoded aicspylibczi/PLATE_96 logic - Update test_metadata_xml_sidecars -> test_metadata_json_sidecars to assert on model_dump key structure and dedup behaviour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No longer needed now that plugin-specific kwargs are user-supplied via provenance_reader_kwargs instead of hardcoded in the package. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No nd2 fixtures or test references exist — it was carried over from the old provenance extra. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AcquisitionTime is not guaranteed to be present in the test fixture; assert all subblocks are dicts instead, which proves include_subblock_metadata produced structured per-subblock data. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes #64
Closes #65
Closes #66
Summary
Adds an opt-in
include_provenanceflag (default off) toOmeZarrConverterthat captures where a converted store came from. When enabled, each scene's store gets:"bioio"attribute block in the rootzarr.jsonwith: source file name, UTC conversion timestamp, reader plugin, bioio package versions,StandardMetadata, and pointers to the metadata sidecars.metadata.native.json(source-native metadata) andmetadata.ome.json(normalized OME metadata), deduped to a single file when byte-identical (e.g. OME-TIFF).Reader-specific kwargs can be forwarded to the provenance metadata reader via
provenance_reader_kwargs— for example, to enable format-specific metadata extraction without affecting pixel reads.Example output
zarr.jsonattributes:{ "bioio": { "source_file": "my_image.czi", "converted": "2026-07-24T18:00:00+00:00", "plugin": "bioio-czi", "bioio_python_versions": { "bioio": "3.0.0", "bioio-czi": "2.8.0", "bioio-conversion": "0.5.0" }, "source_metadata": "metadata.native.json", "ome_metadata": "metadata.ome.json", "standard_metadata": { "physical_size_x": 0.108, "physical_size_y": 0.108, "...": "..." } } }🤖 Generated with Claude Code