Skip to content

Commit e45d2ad

Browse files
fix: standardize spatial dimensions to lowercase in S1 RTC models and test cases
1 parent 1e6b6a0 commit e45d2ad

3 files changed

Lines changed: 87 additions & 71 deletions

File tree

src/eopf_geozarr/data_api/s1_rtc.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@
1515
├── ascending/
1616
│ ├── zarr.json # zarr_conventions, multiscales, proj:, spatial:
1717
│ ├── r10m/ # native resolution dataset
18-
│ │ ├── vv/ # (time, Y, X) float32
19-
│ │ ├── vh/ # (time, Y, X) float32
20-
│ │ ├── border_mask/ # (time, Y, X) uint8
18+
│ │ ├── vv/ # (time, y, x) float32
19+
│ │ ├── vh/ # (time, y, x) float32
20+
│ │ ├── border_mask/ # (time, y, x) uint8
2121
│ │ ├── time/ # (time,) int64 datetime
2222
│ │ ├── absolute_orbit/
2323
│ │ ├── relative_orbit/
2424
│ │ └── platform/
2525
│ ├── r20m/ … r720m/ # overview levels (vv, vh, border_mask only)
2626
│ └── conditions/
27-
│ └── gamma_area_{orbit}/ # (Y, X) float32
27+
│ └── gamma_area_{orbit}/ # (y, x) float32
2828
└── descending/
2929
└── (same structure)
3030
"""
@@ -61,14 +61,41 @@
6161
# ============================================================================
6262

6363

64+
class MultiscalesTransform(BaseModel):
65+
"""Scale/translation transform between resolution levels."""
66+
67+
scale: tuple[float, ...] | None = None
68+
translation: tuple[float, ...] | None = None
69+
70+
71+
class MultiscalesScaleLevel(BaseModel):
72+
"""A single resolution level in the multiscales layout."""
73+
74+
asset: str
75+
derived_from: str | None = None
76+
transform: MultiscalesTransform | None = None
77+
resampling_method: str | None = None
78+
79+
model_config = {"extra": "allow"}
80+
81+
82+
class Multiscales(BaseModel):
83+
"""Typed multiscales metadata (layout + optional resampling_method)."""
84+
85+
layout: tuple[MultiscalesScaleLevel, ...]
86+
resampling_method: str | None = None
87+
88+
model_config = {"extra": "allow"}
89+
90+
6491
class S1RtcOrbitGroupAttrs(BaseModel):
6592
"""Attributes for an orbit-direction group (ascending or descending).
6693
6794
Carries the three GeoZarr conventions plus proj:/spatial:/multiscales metadata.
6895
"""
6996

7097
zarr_conventions: list[dict[str, Any]]
71-
multiscales: dict[str, Any] # validated structurally below
98+
multiscales: Multiscales
7299
proj_code: str = Field(alias="proj:code")
73100
spatial_dimensions: list[str] = Field(alias="spatial:dimensions")
74101
spatial_bbox: list[float] = Field(alias="spatial:bbox")
@@ -84,22 +111,11 @@ def validate_zarr_conventions(self) -> Self:
84111
raise ValueError(f"Missing required zarr_conventions UUIDs: {missing}")
85112
return self
86113

87-
@model_validator(mode="after")
88-
def validate_multiscales_layout(self) -> Self:
89-
"""Ensure multiscales has a layout array with at least one entry."""
90-
layout = self.multiscales.get("layout")
91-
if not layout or not isinstance(layout, (list, tuple)):
92-
raise ValueError("multiscales must contain a non-empty 'layout' array")
93-
for entry in layout:
94-
if "asset" not in entry:
95-
raise ValueError("Each multiscales layout entry must have an 'asset' key")
96-
return self
97-
98114
@model_validator(mode="after")
99115
def validate_spatial_dimensions(self) -> Self:
100-
if self.spatial_dimensions != ["Y", "X"]:
116+
if self.spatial_dimensions != ["y", "x"]:
101117
raise ValueError(
102-
f"spatial:dimensions must be ['Y', 'X'], got {self.spatial_dimensions}"
118+
f"spatial:dimensions must be ['y', 'x'], got {self.spatial_dimensions}"
103119
)
104120
return self
105121

@@ -151,7 +167,7 @@ class S1RtcConditionsAttrs(BaseModel):
151167
class S1RtcNativeResolutionMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg]
152168
"""Members for the native resolution dataset (r10m).
153169
154-
Data variables (time, Y, X) plus 1-D coordinate variables (time,).
170+
Data variables (time, y, x) plus 1-D coordinate variables (time,).
155171
All fields optional since not all arrays are present during incremental construction.
156172
"""
157173

@@ -285,9 +301,9 @@ class S1RtcRoot(GroupSpec[DatasetAttrs, S1RtcRootMembers]): # type: ignore[type
285301
├── ascending/
286302
│ ├── zarr.json # zarr_conventions, multiscales, proj:, spatial:
287303
│ ├── r10m/
288-
│ │ ├── vv/ # (time, Y, X) float32
289-
│ │ ├── vh/ # (time, Y, X) float32
290-
│ │ ├── border_mask/ # (time, Y, X) uint8
304+
│ │ ├── vv/ # (time, y, x) float32
305+
│ │ ├── vh/ # (time, y, x) float32
306+
│ │ ├── border_mask/ # (time, y, x) uint8
291307
│ │ ├── time/ # (time,) int64
292308
│ │ ├── absolute_orbit/
293309
│ │ ├── relative_orbit/

tests/_test_data/s1_rtc_examples/s1-grd-rtc-31TCH.json

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@
186186
},
187187
"proj:code": "EPSG:32631",
188188
"spatial:dimensions": [
189-
"Y",
190-
"X"
189+
"y",
190+
"x"
191191
],
192192
"spatial:bbox": [
193193
500000.0,
@@ -283,8 +283,8 @@
283283
"storage_transformers": [],
284284
"dimension_names": [
285285
"time",
286-
"Y",
287-
"X"
286+
"y",
287+
"x"
288288
]
289289
},
290290
"vh": {
@@ -356,8 +356,8 @@
356356
"storage_transformers": [],
357357
"dimension_names": [
358358
"time",
359-
"Y",
360-
"X"
359+
"y",
360+
"x"
361361
]
362362
},
363363
"border_mask": {
@@ -429,8 +429,8 @@
429429
"storage_transformers": [],
430430
"dimension_names": [
431431
"time",
432-
"Y",
433-
"X"
432+
"y",
433+
"x"
434434
]
435435
},
436436
"time": {
@@ -665,8 +665,8 @@
665665
"storage_transformers": [],
666666
"dimension_names": [
667667
"time",
668-
"Y",
669-
"X"
668+
"y",
669+
"x"
670670
]
671671
},
672672
"vh": {
@@ -738,8 +738,8 @@
738738
"storage_transformers": [],
739739
"dimension_names": [
740740
"time",
741-
"Y",
742-
"X"
741+
"y",
742+
"x"
743743
]
744744
},
745745
"border_mask": {
@@ -811,8 +811,8 @@
811811
"storage_transformers": [],
812812
"dimension_names": [
813813
"time",
814-
"Y",
815-
"X"
814+
"y",
815+
"x"
816816
]
817817
}
818818
}
@@ -903,8 +903,8 @@
903903
"storage_transformers": [],
904904
"dimension_names": [
905905
"time",
906-
"Y",
907-
"X"
906+
"y",
907+
"x"
908908
]
909909
},
910910
"vh": {
@@ -976,8 +976,8 @@
976976
"storage_transformers": [],
977977
"dimension_names": [
978978
"time",
979-
"Y",
980-
"X"
979+
"y",
980+
"x"
981981
]
982982
},
983983
"border_mask": {
@@ -1049,8 +1049,8 @@
10491049
"storage_transformers": [],
10501050
"dimension_names": [
10511051
"time",
1052-
"Y",
1053-
"X"
1052+
"y",
1053+
"x"
10541054
]
10551055
}
10561056
}
@@ -1141,8 +1141,8 @@
11411141
"storage_transformers": [],
11421142
"dimension_names": [
11431143
"time",
1144-
"Y",
1145-
"X"
1144+
"y",
1145+
"x"
11461146
]
11471147
},
11481148
"vh": {
@@ -1214,8 +1214,8 @@
12141214
"storage_transformers": [],
12151215
"dimension_names": [
12161216
"time",
1217-
"Y",
1218-
"X"
1217+
"y",
1218+
"x"
12191219
]
12201220
},
12211221
"border_mask": {
@@ -1287,8 +1287,8 @@
12871287
"storage_transformers": [],
12881288
"dimension_names": [
12891289
"time",
1290-
"Y",
1291-
"X"
1290+
"y",
1291+
"x"
12921292
]
12931293
}
12941294
}
@@ -1379,8 +1379,8 @@
13791379
"storage_transformers": [],
13801380
"dimension_names": [
13811381
"time",
1382-
"Y",
1383-
"X"
1382+
"y",
1383+
"x"
13841384
]
13851385
},
13861386
"vh": {
@@ -1452,8 +1452,8 @@
14521452
"storage_transformers": [],
14531453
"dimension_names": [
14541454
"time",
1455-
"Y",
1456-
"X"
1455+
"y",
1456+
"x"
14571457
]
14581458
},
14591459
"border_mask": {
@@ -1525,8 +1525,8 @@
15251525
"storage_transformers": [],
15261526
"dimension_names": [
15271527
"time",
1528-
"Y",
1529-
"X"
1528+
"y",
1529+
"x"
15301530
]
15311531
}
15321532
}
@@ -1617,8 +1617,8 @@
16171617
"storage_transformers": [],
16181618
"dimension_names": [
16191619
"time",
1620-
"Y",
1621-
"X"
1620+
"y",
1621+
"x"
16221622
]
16231623
},
16241624
"vh": {
@@ -1690,8 +1690,8 @@
16901690
"storage_transformers": [],
16911691
"dimension_names": [
16921692
"time",
1693-
"Y",
1694-
"X"
1693+
"y",
1694+
"x"
16951695
]
16961696
},
16971697
"border_mask": {
@@ -1763,8 +1763,8 @@
17631763
"storage_transformers": [],
17641764
"dimension_names": [
17651765
"time",
1766-
"Y",
1767-
"X"
1766+
"y",
1767+
"x"
17681768
]
17691769
}
17701770
}
@@ -1774,8 +1774,8 @@
17741774
"attributes": {
17751775
"proj:code": "EPSG:32631",
17761776
"spatial:dimensions": [
1777-
"Y",
1778-
"X"
1777+
"y",
1778+
"x"
17791779
],
17801780
"spatial:transform": [
17811781
10.0,
@@ -1829,8 +1829,8 @@
18291829
],
18301830
"storage_transformers": [],
18311831
"dimension_names": [
1832-
"Y",
1833-
"X"
1832+
"y",
1833+
"x"
18341834
]
18351835
},
18361836
"gamma_area_037": {
@@ -1875,8 +1875,8 @@
18751875
],
18761876
"storage_transformers": [],
18771877
"dimension_names": [
1878-
"Y",
1879-
"X"
1878+
"y",
1879+
"x"
18801880
]
18811881
},
18821882
"gamma_area_110": {
@@ -1921,13 +1921,13 @@
19211921
],
19221922
"storage_transformers": [],
19231923
"dimension_names": [
1924-
"Y",
1925-
"X"
1924+
"y",
1925+
"x"
19261926
]
19271927
}
19281928
}
19291929
}
19301930
}
19311931
}
19321932
}
1933-
}
1933+
}

tests/test_data_api/test_s1_rtc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def test_s1_rtc_orbit_attrs(s1_rtc_json_example: dict[str, object]) -> None:
7474
attrs = model.descending.attributes
7575
assert len(attrs.zarr_conventions) == 3
7676
assert attrs.proj_code.startswith("EPSG:")
77-
assert attrs.spatial_dimensions == ["Y", "X"]
77+
assert attrs.spatial_dimensions == ["y", "x"]
7878
assert len(attrs.spatial_bbox) == 4
7979
layout = attrs.multiscales["layout"]
8080
assert len(layout) == 6
@@ -110,7 +110,7 @@ def test_s1_rtc_rejects_missing_convention_uuid(s1_rtc_json_example: dict[str, o
110110
def test_s1_rtc_rejects_bad_spatial_dimensions(s1_rtc_json_example: dict[str, object]) -> None:
111111
"""Reject orbit attrs with wrong spatial:dimensions."""
112112
data = copy.deepcopy(s1_rtc_json_example)
113-
data["members"]["descending"]["attributes"]["spatial:dimensions"] = ["x", "y"]
113+
data["members"]["descending"]["attributes"]["spatial:dimensions"] = ["lat", "lon"]
114114
with pytest.raises(Exception, match="spatial:dimensions"):
115115
S1RtcRoot(**data)
116116

0 commit comments

Comments
 (0)