Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
f96daaf
Docs(design): format metadata framework (draft, #233)
claude Oct 2, 2026
3e47b39
Docs(design): format metadata memo, round 1 review (#233)
claude Oct 2, 2026
840f222
Docs(design): format metadata memo, round 2 review (#233)
claude Oct 2, 2026
c979fc1
Feat(datamodel): format metadata framework and BIDS sidecar codec (#233)
claude Oct 3, 2026
8beb5b5
Feat(datamodel): optional metadata field on Image and Transformation …
claude Oct 3, 2026
989fb86
Feat(io): NiftiMetadata for NIfTI images and transformations (#233)
claude Oct 3, 2026
8ff26d0
Docs: metadata user guide, API pages and prototype notes (#233)
claude Oct 3, 2026
445c6f4
Feat(io): MghMetadata for MGH/MGZ images (#233)
claude Oct 3, 2026
794187c
Feat(io): metadata for x5, ITK and FLIRT transformations (#233)
claude Oct 3, 2026
5c7edf3
Test(io): metadata of x5, ITK and FLIRT transformations (#233)
claude Oct 3, 2026
d0bf785
Docs: x5, ITK and FLIRT metadata guide and prototype notes (#233)
claude Oct 3, 2026
573979d
Feat(io): ZarrMetadata and OmeZarrMetadata for Zarr images (#233)
claude Oct 3, 2026
cad9192
Docs: MGH and OME-Zarr metadata in the user guide and memo (#233)
claude Oct 3, 2026
5f0a3f3
Docs(io): point the MGH and Zarr metadata references at their API pag…
claude Oct 3, 2026
818eab8
Merge branch 'meta-images' into claude/format-metadata-framework-n3ylis
claude Oct 3, 2026
9683f92
Feat(datamodel): copy metadata, geometry and record hooks, lazy field…
claude Oct 3, 2026
7181086
Fix(io): NIfTI writes the data model's time step, rereads a new heade…
claude Oct 3, 2026
621fad8
Perf(io): read the MGH tags lazily; footer keywords as force= (#233)
claude Oct 3, 2026
521c2e2
Feat(io): a single x5 node puts its metadata on its block (#233)
claude Oct 3, 2026
8fc0ead
Fix(io): OME-Zarr windows from the data, reported; Zarr node identity…
claude Oct 3, 2026
aad90c7
Test(io): cross-format metadata matrix (#233)
claude Oct 3, 2026
10d8afa
Docs: metadata copies, geometry hook, lazy record part (#233)
claude Oct 3, 2026
61acf45
Feat(core): Lazy and the LazyField descriptor in _core.properties (#233)
claude Oct 4, 2026
c69c662
Feat(datamodel): enums of the metadata terms (#233)
claude Oct 4, 2026
c036367
Refactor(metadata): Metadata -> FileBasedMetadata hierarchy, review o…
claude Oct 4, 2026
b213b73
Docs: metadata user guide for the new hierarchy, to(), terms and data…
claude Oct 4, 2026
66504fa
Docs(design): format metadata memo after the review of #287 (#233)
claude Oct 4, 2026
099fe27
Merge main into the format metadata branch (#233)
claude Oct 4, 2026
477504d
Docs(design): format the memo's code blocks with ruff 0.16 (#233)
claude Oct 4, 2026
e7d3f5b
Fix(metadata): read class annotations portably on Python 3.14
claude Oct 4, 2026
112bcaf
Merge remote-tracking branch 'origin/main' into claude/format-metadat…
claude Oct 4, 2026
67d2a53
Narrow data_unit to a pint-backed Unit after merging main
claude Oct 4, 2026
7d5624d
Split datamodel/metadata.py into a subpackage
claude Oct 4, 2026
9e30826
Move the format author's manual out of the metadata docstring
claude Oct 4, 2026
407e588
Compute the BIDS keys and scopes once, and put public code first
claude Oct 4, 2026
8441cf2
Hold the read-time snapshot as a dict
claude Oct 4, 2026
e551a24
Reduce the loss-policy surface to one collector
claude Oct 4, 2026
7acb7a6
Make supports() a classmethod
claude Oct 4, 2026
66eaa4f
Move the capability logic onto Metadata
claude Oct 4, 2026
8363433
Drop derived=: _geometry decides which fields are derived
claude Oct 4, 2026
62fd77f
Copy the raw record in derive by default; document _import
claude Oct 4, 2026
f1606ee
Write derive once, with a table of field rules
claude Oct 4, 2026
ab0a5d9
Trim EncodingDirection to an oriented vector
claude Oct 4, 2026
2e24a55
Keep the Zarr node in the raw record, not in the base
claude Oct 4, 2026
0a8d526
Share one JSON codec between BIDS, Zarr and x5
claude Oct 4, 2026
7882f84
Share the parsers' metadata sync
claude Oct 4, 2026
b63b50f
Move generic value helpers to _core
claude Oct 4, 2026
dd2669f
Put each format's metadata class first
claude Oct 4, 2026
3f68e95
Order Metadata and FileBasedMetadata public-first
claude Oct 4, 2026
894aa71
Bring the metadata docs in line with the new layout
claude Oct 4, 2026
8f26b2b
Keep the private __module__ of the metadata names
claude Oct 4, 2026
a0b3fb2
Refuse unknown class keywords on metadata classes
claude Oct 4, 2026
4d91b7f
Drop the underscore of names shared inside the metadata package
claude Oct 4, 2026
3ca4e85
Read _base and _derive_values in order
claude Oct 4, 2026
f5cac08
Let sync_metadata take a reader and force a fresh read
claude Oct 4, 2026
f355e02
Document the shared JSON codec for format authors
claude Oct 4, 2026
2ab54a9
Give each write one report with FileBasedMetadata.writable
claude Oct 4, 2026
baa2cdb
Note writable() and the keyword check in the design memo
claude Oct 4, 2026
c4e4fea
Move Lazy and LazyField to _core.fields
claude Oct 4, 2026
dbc683e
Import the metadata names from brainhops.datamodel.metadata
claude Oct 4, 2026
0f6518c
Rename the Space and Intent enums to SpaceEnum and IntentEnum
claude Oct 4, 2026
595adfb
Rename the term converter helper to to_enum
claude Oct 4, 2026
8a286a4
Take a ConversionReport as on_loss, and drop report=
claude Oct 4, 2026
f8eef3e
Give examples of the _import hook and of derive
claude Oct 4, 2026
d9052c0
Let Factory() infer the default from the field's hint
claude Oct 4, 2026
44d85fa
Make FileBasedMetadata.changed_fields private
claude Oct 4, 2026
31ffde4
Keep _declare and _finish on the metaclass
claude Oct 4, 2026
6b25500
Annotate self-typed returns with tx.Self
claude Oct 4, 2026
5b9f766
Use the default equality for EncodingDirection
claude Oct 4, 2026
3f9e2fe
Rewrite the metadata docstrings to the documentation guidelines
claude Oct 5, 2026
1be296b
Rename the metadata helpers and simplify copy
claude Oct 5, 2026
3931e26
Generalise float32_repr into shortest_decimal
claude Oct 5, 2026
21aac3d
Replace to_enum with an EnumConverter class
claude Oct 5, 2026
f1ec363
Enable grid tables in the docs and use them for the enums
claude Oct 5, 2026
fd17e95
Let the pinned format declare the format field
claude Oct 5, 2026
4b4a7ef
Rename the vocabulary groups to *Vocabulary
claude Oct 5, 2026
8dd4239
Add a StorageVocabulary with the NIfTI intensity scaling
claude Oct 5, 2026
1bb2518
Key propagation on axis types instead of grids and volumes
claude Oct 5, 2026
fc16b10
Carry the raw record on Metadata, matched by its type
claude Oct 5, 2026
ff0d311
Add MetadataParser and Metadata.load
claude Oct 5, 2026
2b02388
Give the test formats built with type() a __qualname__
claude Oct 5, 2026
97d7936
Reduce the public metadata API to what users need
claude Oct 5, 2026
7c25045
Let the image operations derive their metadata
claude Oct 5, 2026
94a3f13
Resolve Metadata.load by the bases, not by names
claude Oct 5, 2026
b8345f8
Drop lazy metadata fields
claude Oct 5, 2026
c82de4e
Copy the read-time snapshot with copy.copy
claude Oct 5, 2026
8168a0e
Revert "Give the test formats built with type() a __qualname__"
claude Oct 5, 2026
cbca82f
Require bagof-magic 0.3.dev2
claude Oct 5, 2026
f41f0eb
Keep the pyramid metadata when a transformation is applied
claude Oct 5, 2026
07e3c68
Declare the raw record type as a type argument
claude Oct 5, 2026
36c78a1
Rename the hooks to _decode_raw and _encode_raw
claude Oct 5, 2026
229f949
Check the finished record instead of declaring _geometry
claude Oct 5, 2026
8824ab8
Read metadata files through the public parser methods
claude Oct 5, 2026
6bdae49
Make FileBasedMetadata the metadata dispatcher
claude Oct 5, 2026
e9fbbbb
Document the two hooks and the metadata dispatcher
claude Oct 5, 2026
1a83eb8
Move FileBasedMetadata to io
claude Oct 5, 2026
0b607c6
Drop the _import conversion hook
claude Oct 5, 2026
b7950db
Drop ConversionReport.passed_through
claude Oct 5, 2026
9520798
Merge origin/main into the format-metadata framework
claude Oct 6, 2026
07e95b1
Merge the metadata system (claude/feat/233-metadata-system) into the …
claude Oct 6, 2026
f12c182
Merge the review of the metadata system (claude/feat/233-metadata-sys…
claude Oct 6, 2026
d6a4549
Fix(metadata): wire the images, the writers and the guide to the revi…
claude Oct 6, 2026
57c4255
Merge the operation objects (claude/feat/233-metadata-system) into th…
claude Oct 6, 2026
7fce768
Feat(images): derive the metadata through the operation objects (#233)
claude Oct 6, 2026
da75296
Docs(metadata): the operation objects in the user guide (#233)
claude Oct 6, 2026
a54b173
Merge the optional geometry of Resampled (claude/feat/233-metadata-sy…
claude Oct 6, 2026
9e76701
Fix(zarr): derive a level without reading it (#233)
claude Oct 6, 2026
a6eab81
Merge the record design of the metadata (claude/feat/233-metadata-sys…
claude Oct 13, 2026
03535b2
Merge the cached transformations of images (claude/fix/metadata-keeps…
claude Oct 13, 2026
6fe21a3
Feat(metadata): metadata on the roots of the data model, taken over b…
claude Oct 13, 2026
f66d9a1
Feat(metadata): image operations derive the metadata, conversions car…
claude Oct 13, 2026
79329af
Fix(fnirt): the FNIRT field takes the metadata of the root over (#233)
claude Oct 13, 2026
c81be44
Fix(io): a format built from a data model converts its metadata (#233)
claude Oct 13, 2026
f92e20a
Test(io): conversions carry the metadata, record of the format included
claude Oct 13, 2026
464c634
Feat(io): every write consolidates the metadata first, and warns once…
claude Oct 13, 2026
eb5a1bd
Test(metadata): conformance checks 24 and 25, and the user guide (#233)
claude Oct 13, 2026
29c7796
Merge the metadata system (claude/feat/233-metadata-system) into the …
claude Oct 14, 2026
cdf41b1
Merge the record fix of the metadata system into the wiring
claude Oct 14, 2026
fffa63d
Fix(io): the formats of main take the metadata of the root over (#233)
claude Oct 14, 2026
e6b83c8
Feat(io): the formats of main consolidate their metadata first (#233)
claude Oct 14, 2026
83abc7d
Fix(io): a save reports the metadata that a conversion drops (#233)
claude Oct 14, 2026
a6d3a0e
Docs(metadata): the wiring of the formats of main in the memo and the…
claude Oct 14, 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
125 changes: 107 additions & 18 deletions docs/design/format-records.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,8 @@ copy is decoded afresh from the new record, so a second consolidation
changes nothing, and a truncated description reads back truncated. The
constant `SUPPORTED_FIELDS` of each metadata class lists the fields
that its record can store, and a conversion into the format reports
from it a lower bound of what a write loses. The format objects do not
consolidate their metadata yet; the wiring of pass 5a (#287) makes it
step 2 of the precedence below.
from it a lower bound of what a write loses. Every format object
consolidates its metadata as step 2 of the precedence below.

The methods `to_raw()` and `to_metadata()` are removed from every class,
including `MetadataFormat`. The encoding code of each format stays
Expand Down Expand Up @@ -293,7 +292,11 @@ Consolidation, and therefore every write, follows four steps.
metadata can be overridden on save, and a writer option with the
name of a field, such as `dtype=`, is that field. The stored type is
the override, then `metadata.dtype`, then the type of the data.
This step is wired in pass 5a (#287).
A header field that holds a field of the vocabulary, such as the
`descrip` of NIfTI or the `tr` of MGH, is refused as an override,
with the name of the field to give instead. One report collects what
the consolidation of the metadata and the encoding of the geometry
lose, and the loss policy acts on it once.
3. The geometry is encoded from the model over that record. The image
or the transformation owns its geometry and its data, the metadata
owns its vocabulary, and the record supplies everything else.
Expand Down Expand Up @@ -691,6 +694,14 @@ format.
23. A field that the format has no slot for is reported as lost by
`consolidate` and is absent from the copy, and `on_loss="raise"`
raises `MetadataLossError`.
24. The metadata of an object of another class, generic or of another
format, is converted by `from_instance` into the metadata of the
format without the record of the other format, and a save writes
each field of `Exemplar.vocabulary` or reports it as lost.
25. An index that moves no voxel keeps the class and the record of the
metadata of an image and every field, and a flip or a resampling
keeps the class, keeps a field in the `FILE` scope and clears a
field in the `SPATIAL` scope.
The checks above run on the level class of each pyramid format, which
reads one level with the option `level`. A level class whose container is
a directory has no bytes, so its metadata is compared through a summary,
Expand Down Expand Up @@ -873,10 +884,11 @@ its status.
7. In the first pass, `metadata` is a field of the format classes,
while #287 puts it on the roots of the data model. The name also
clashes with `Transformation.metadata_fields`, which may be renamed.
Status: decided in the fifth pass. `metadata` goes on the roots, each
format takes it over with `Inherit("metadata")`, and
`metadata_fields` becomes `encoding_fields` without an alias. The
wiring of pass 5a (#287) implements it.
Status: resolved in the fifth pass. `metadata` is a field of the
roots, each format takes it over with `Inherit("metadata")` (the
NIfTI transformations in each concrete class, see item 400), and
`metadata_fields` became `encoding_fields` without an alias, in the
wiring of pass 5a (#287).
8. Views that are decoded from `metadata` and cached, such as
`transformations`, would become stale when `metadata` is assigned
again. Each format holds a private `_metadata` field behind a public
Expand Down Expand Up @@ -2904,12 +2916,26 @@ The items from 400 on were found in pass 5a, which adds the vocabulary
of #233 to the metadata of the formats (#310) and wires it into images
and transformations (#287).

400. `bagof` takes an inherited field from the first base that has it,
so a NIfTI transformation whose first base is a generic model
(`NiftiRASToVoxel(VoxelToRAS, _NiftiAffine)`) gets the generic
`metadata` of the root before the takeover of
`NiftiBasedTransformation`. The wiring probes it, and falls back on
the takeover in each concrete class. Status: open, for #287.
400. `NiftiBasedTransformation`, `_NiftiAffine` and `NiftyRegField` are
format classes that do not derive from `Transformation`: the root
of the data model comes in with the first base of each concrete
class, as in `NiftiRASToVoxel(RASToVoxel, _NiftiAffine)`. `bagof`
refuses a takeover of a field that no base declares, and refuses a
class that holds both the `metadata` of the root and a `_metadata`
of the format, so the takeover cannot be written once on
`NiftiBasedTransformation`. Deriving that class from
`Transformation` would allow it, but it would make the abstract
`NiftyRegField` a data model, which `relabel` then no longer
refuses as a family of formats. Status: decided in pass 5a. Each
of the nine concrete classes (`NiftiRASToVoxel`,
`NiftiVoxelToRAS`, `NiftiRASCoordinatesField`,
`NiftiRASDisplacementField`, `NiftyRegSequence`,
`SpmCoordinatesField`, `ItkNiftiField`, `FnirtWarpField` and
`AfniNiftiWarp`) takes `metadata` over itself. The MRtrix warps
share the abstract base `MrtrixWarp`, which does not derive from
`Transformation` either, so `MrtrixCoordinatesField`,
`MrtrixDisplacementField` and `MrtrixWarpFull` take `metadata` over
in the same way.
401. The metadata of a format decodes its vocabulary at once, in
`from_raw`, instead of in lazy properties: a field may be `None`,
so a lazy field would need a sentinel or a flag, which is the state
Expand All @@ -2919,17 +2945,21 @@ and transformations (#287).
402. Loss is reported at consolidation, so once per write, and a
conversion reports only a lower bound from `SUPPORTED_FIELDS`. A
save that converts and then writes would warn twice, so the writer
passes one report as `on_loss=` to both. Status: decided in pass
5a, and the single report of a save is wired in #287.
passes one report as `on_loss=` to both. Status: resolved in pass
5a. `io.save` converts under `metadata_loss_policy(report)`, which
takes a report as well as a policy, passes the same report as
`on_loss=` to the write, and acts on it once.
403. The format objects of the first three passes built the metadata of
their consolidated copy with `replace(self.metadata, raw=record)`,
which kept the decoded fields of the old record. With eager
decoding, the metadata of NIfTI, MGH and X5 is decoded from the new
record instead (`type(self.metadata).from_raw(record)`). The fields
that were assigned and not consolidated are dropped there until the
wiring makes the consolidation of the metadata step 2 of every
write. Status: resolved in pass 5a for the formats that have a
vocabulary, and open for #287.
write. Status: resolved in pass 5a. Every format object
consolidates its metadata first, with the helper
`metadata_to_write` (`io/metadata/_base.py`), and encodes over the
record of the result, so an assigned field is written.
404. `Metadata.derive` adds to `history` and `generated_by`, which NIfTI
and MGH cannot store, so every consolidation of derived metadata
would report them as lost. `MetadataFormat.derive` keeps the two
Expand All @@ -2940,6 +2970,65 @@ and transformations (#287).
Check 22 changes `dtype` for MGH, which the fixed header holds.
Check 23 is vacuous for X5, whose JSON metadata stores every field,
as the comment of its exemplar says. Status: accepted.
410. A writer copies the description and the intent of the template
`like` over the record of the consolidated metadata, so a field
given by keyword would lose to the template, while a header
override won over it before. The NIfTI writers write the
description and the intent given by keyword again after the
template (`_keep_given_fields`, `io/common/nifti/_header.py`), and
the MGH writer does the same for the MRI parameters. A field that
was only changed in the metadata still loses to the template, as
the record did. Status: decided in pass 5a.
411. An X5 file stores the vocabulary in the JSON metadata of the
transform that holds it, and a chain that is assigned gives new
transforms. `X5Transform.consolidate` therefore encodes the chain
first and then the fields of the metadata that changed, and those
given by keyword, over the new record, so that a reused transform
keeps its own JSON metadata. Status: decided in pass 5a.
412. The images store their data with `metadata.dtype` when no `dtype=`
is given, as nibabel does with the type of its header, so data
computed in another type are converted, and scaled into an integer
type. The NIfTI transformations change the stored type only when
`dtype` is given or changed in the metadata, because their arrays
are computed from the chain. Status: open, for the maintainer to
decide whether the transformations follow the rule of the images.
413. Only the records of NIfTI, MGH and X5 hold fields of the
vocabulary in pass 5a. Every other format that holds the metadata
of its own class, which is every migrated format, consolidates its
metadata first all the same, so its consolidation and its writers
take `on_loss=` and the fields of the metadata by keyword, and
report every field that is set as lost. This holds for the formats
of pass 5b (AFNI, MRtrix, NRRD, MINC, LTA and M3Z) and of pass 5c
(Zarr, FLIRT and ITK), and for the formats that pass 5 does not
plan, such as TIFF, Pillow, JPEG 2000, image stacks, the read-only
images, register.dat, Convert3D, elastix, TrakEM2, BigDataViewer
and the plain matrix formats. A format that cannot write, such as
MINC or OpenSlide, gives its consolidated copy the consolidated
metadata. The writers of AFNI, MRtrix and NRRD keep their option
`datatype=` and that of Zarr its option `dtype=`, which store the
values with the type given and are not the field `dtype`, whose
value these formats report as lost. The option `space=` of NRRD
names the space of the NRRD header, so the field `space` cannot be
given by keyword to a NRRD writer. Status: open, for passes 5b and
5c.
414. A format that does not take `metadata` over, which is a format not
yet migrated, such as the OME-Zarr images and fields or the
pyramids of N5, Imaris, precomputed volumes and BigDataViewer,
holds its metadata in the `metadata` field of its data model, and
writes none of the vocabulary. `io.save` reports the fields that are set as lost when it
converts into such a format, but a save of the object itself
reports nothing. Status: open, until these formats move to the
record design.
415. A format built from a transformation of the data model by
`from_instance`, such as LTA or a MINC affine, receives its
metadata, and so do the NIfTI transformations and the SPM field
through their converters. Other converters build the format
without the metadata of the source, such as those into a Convert3D
matrix or a NiftyReg affine. `io.save` reports the fields of the
source as lost when the object that it converted holds no metadata,
but `obj.to(cls)` drops them without a report. Status: open, for
the maintainer to decide whether every converter passes the
metadata of its source.

470. `io.save` tries the candidates tier by tier, most specific first,
those that hold the object as it is before those that a
Expand Down
18 changes: 16 additions & 2 deletions docs/dev/metadata-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ it.
| `brainhops.datamodel.metadata._vocabulary` | the annotations `Bids`, `Scoped` and `Along`, and the tables `VOCABULARY`, `FIELDS`, `GROUPS`, `BIDS_KEYS`, `SCOPES` and `ALONG` |
| `brainhops.datamodel.metadata._derive` | `Indexed` and `Resampled`, which an image operation builds for `derive` |
| `brainhops.datamodel.metadata._report` | `apply_loss_policy`, `OnLoss` and `LossPolicy` |
| `brainhops.io.metadata._base` | `consolidated`, `lost_fields` and `unsupported_values` |
| `brainhops.io.metadata._base` | `consolidated`, `lost_fields` and `unsupported_values`, and, for the format objects, `metadata_to_write`, `requested_fields`, `overruled_fields` and `metadata_options` |
| `brainhops.io.metadata._json` | the codec of the JSON stores, which BIDS and X5 share |
| `brainhops.io.metadata._storage` | `preferred_storage` and `stored_values` |

Expand Down Expand Up @@ -98,6 +98,20 @@ consolidate first and write the record of the copy, and they take the
same overrides and `on_loss=` as `consolidate`. The metadata itself is
never changed by a write.

An image or a transformation of the format consolidates its metadata
first, as step 2 of the precedence on write, whether or not the format
stores a vocabulary. Its `consolidate` takes `on_loss=` and the fields
of the metadata by keyword. It asks `requested_fields` which fields the
write is asked to store, consolidates the metadata with
`metadata_to_write` and the report of the write, encodes its geometry
and its data over the record of the result, and acts on the report once
with `apply_loss_policy`. A format whose model owns a slot, such as the
time step of a NIfTI image, reports a field that it encoded over with
`overruled_fields`. A writer that writes the record of a consolidated
copy splits its options with `metadata_options`, so that `on_loss` and
the fields go to `consolidate` and the other options to the writer of
the record.

## The codec

`_decode(raw)` returns a dictionary of field values for a record, or
Expand Down Expand Up @@ -143,7 +157,7 @@ move.
that `_encode` takes. `tests/test_io_metadata_matrix.py` checks that
the losses of a fully populated metadata are exactly the other fields.
- The exemplar of the format in `tests/test_io_conformance.py` gives a
field and a value in `vocabulary`, for checks 21 to 23.
field and a value in `vocabulary`, for checks 21 to 25.
- The tests of the format decode each slot, encode each field into a
copy, and probe each value that a slot holds approximately or not at
all.
Loading
Loading