Group-to-Array Inheritance¶
The proj: and spatial: conventions support group-to-array inheritance: when metadata is defined at the group level, it applies to all direct child arrays. The multiscales convention does not support inheritance — it is group-level only.
This notebook covers:
- How inheritance works for proj: and spatial:
- Array-level overrides
- Why multiscales is different
Prerequisites: The proj: Convention
Inheritance Model¶
The proj: and spatial: conventions share the same inheritance rules:
- Group-level definition: metadata defined at the group level applies to all direct child arrays
- Direct children only: inheritance does not cascade to grandchildren or deeper levels
- Array-level override: any child array can define its own attributes to override the inherited values
| Convention | Inherits? | Override behavior |
|---|---|---|
| proj: | Yes | Full replacement — array's proj: replaces group's entirely |
| spatial: | Yes | Override or supplement — array can replace individual properties |
| multiscales | No | Group-level only; each group defines its own pyramid independently |
Sentinel-2 scenes are a natural fit for this pattern: all bands share the same UTM CRS and bounding box, so defining proj: and shared spatial: properties once at the group level avoids repeating identical metadata across every band.
import json
from geozarr_toolkit import (
ProjConventionMetadata,
SpatialConventionMetadata,
create_proj_attrs,
create_spatial_attrs,
create_zarr_conventions,
)
proj: Inheritance¶
When proj: is defined at the group level, the CRS applies to all direct child arrays. This is useful when multiple arrays (bands, variables, time steps) share the same coordinate reference system.
For a Sentinel-2 scene, the CRS (EPSG:32612) is the same for every band, so we define it once at the group level.
# Group-level CRS applies to all direct child arrays
group_attrs = create_proj_attrs(code="EPSG:32612")
group_attrs["zarr_conventions"] = create_zarr_conventions(ProjConventionMetadata())
print("Group attributes (shared by all child arrays):")
print(json.dumps(group_attrs, indent=2))
# Visualize the inheritance hierarchy
print()
print("Sentinel-2 scene group/ <- proj:code = EPSG:32612")
print(" ├── TCI (10m) <- inherits EPSG:32612")
print(" ├── B02 (10m) <- inherits EPSG:32612")
print(" ├── B05 (20m) <- inherits EPSG:32612")
print(" └── B01 (60m) <- inherits EPSG:32612")
Group attributes (shared by all child arrays):
{
"proj:code": "EPSG:32612",
"zarr_conventions": [
{
"uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f",
"schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json",
"spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md",
"name": "proj:",
"description": "Coordinate reference system information for geospatial data"
}
]
}
Sentinel-2 scene group/ <- proj:code = EPSG:32612
├── TCI (10m) <- inherits EPSG:32612
├── B02 (10m) <- inherits EPSG:32612
├── B05 (20m) <- inherits EPSG:32612
└── B01 (60m) <- inherits EPSG:32612
spatial: Inheritance¶
The spatial: convention also inherits from group to direct children. Group-level properties like spatial:dimensions and spatial:bbox apply to all child arrays. However, unlike proj:, spatial: allows partial override — a child array can supplement or replace individual properties while inheriting the rest.
This is particularly useful with multi-resolution data: the bounding box and dimension names are shared, but spatial:shape and spatial:transform vary per resolution level.
# Group-level spatial: properties shared by all bands
group_spatial = create_spatial_attrs(
dimensions=["Y", "X"],
bbox=[300000.0, 3990240.0, 409800.0, 4100040.0],
)
print("Group spatial: attributes (shared):")
print(json.dumps(group_spatial, indent=2))
# Each band supplements with its own shape and transform
print()
print("Array-level supplements (per resolution):")
tci_attrs = create_spatial_attrs(
dimensions=["Y", "X"],
transform=[10.0, 0.0, 300000.0, 0.0, -10.0, 4100040.0],
shape=[10980, 10980],
)
print(
f" TCI (10m): shape={tci_attrs['spatial:shape']}, transform a={tci_attrs['spatial:transform'][0]}"
)
b05_attrs = create_spatial_attrs(
dimensions=["Y", "X"],
transform=[20.0, 0.0, 300000.0, 0.0, -20.0, 4100040.0],
shape=[5490, 5490],
)
print(
f" B05 (20m): shape={b05_attrs['spatial:shape']}, transform a={b05_attrs['spatial:transform'][0]}"
)
b01_attrs = create_spatial_attrs(
dimensions=["Y", "X"],
transform=[60.0, 0.0, 300000.0, 0.0, -60.0, 4100040.0],
shape=[1830, 1830],
)
print(
f" B01 (60m): shape={b01_attrs['spatial:shape']}, transform a={b01_attrs['spatial:transform'][0]}"
)
Group spatial: attributes (shared):
{
"spatial:dimensions": [
"Y",
"X"
],
"spatial:bbox": [
300000.0,
3990240.0,
409800.0,
4100040.0
],
"spatial:transform_type": "affine",
"spatial:registration": "pixel"
}
Array-level supplements (per resolution):
TCI (10m): shape=[10980, 10980], transform a=10.0
B05 (20m): shape=[5490, 5490], transform a=20.0
B01 (60m): shape=[1830, 1830], transform a=60.0
Array-Level Override (proj:)¶
A child array can fully replace the inherited CRS by defining its own proj: attributes. The override is complete — the array's proj: entirely replaces the group's.
# Example: a child array that overrides the group CRS
array_attrs = create_proj_attrs(code="EPSG:4326")
print("Group: proj:code = EPSG:32612 (UTM zone 12N)")
print("Array: proj:code = EPSG:4326 (WGS 84 geographic)")
print()
print("Array-level attributes:")
print(json.dumps(array_attrs, indent=2))
Group: proj:code = EPSG:32612 (UTM zone 12N)
Array: proj:code = EPSG:4326 (WGS 84 geographic)
Array-level attributes:
{
"proj:code": "EPSG:4326"
}
Why Multiscales Does Not Inherit¶
The multiscales convention is fundamentally different: it describes the structure of a pyramid (which child arrays exist, their scale relationships), not a property of individual arrays. It can only be defined at the group level and does not propagate to child groups.
Each multiscale group defines its own pyramid independently. If you have nested groups that each contain pyramids, each group carries its own multiscales metadata — there is no cascading.
root/ <- multiscales: layout for [0, 1, 2, 3, 4]
├── 0 (10980x10980) <- no multiscales (it's an array, not a group)
├── 1 (5490x5490)
├── subgroup/ <- would need its own multiscales if it's a pyramid
│ ├── 0
│ └── 1
...
Summary¶
| Convention | Inherits? | Scope | Override |
|---|---|---|---|
| proj: | Yes | Direct children only | Full replacement |
| spatial: | Yes | Direct children only | Override or supplement |
| multiscales | No | Group-level only | N/A |
Key rules:
- Inheritance is one level deep — direct children only, never grandchildren
- proj: override is all-or-nothing: the array's CRS fully replaces the group's
- spatial: allows partial override: a child can define
spatial:shapeandspatial:transformwhile inheritingspatial:bboxandspatial:dimensionsfrom the group - multiscales describes pyramid structure, not array properties, so inheritance doesn't apply
Next: Composition | COG to Zarr