Skip to content

Commit a88d823

Browse files
committed
Add support for downloading supergrid
1 parent d6900dc commit a88d823

File tree

6 files changed

+443
-260
lines changed

6 files changed

+443
-260
lines changed

notebooks/GettingStartedOcean.ipynb

Lines changed: 80 additions & 58 deletions
Large diffs are not rendered by default.

notebooks/Getting_started.ipynb

Lines changed: 106 additions & 62 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ dependencies = [
2626
"cftime",
2727
"dask",
2828
"pyyaml",
29+
"tqdm",
30+
"requests"
2931
]
3032
dynamic = ["version"]
3133

src/access_mopper/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
from . import _version
22
from ._config import _creator
3-
from .cmip6_cmoriser import CMIP6Workflow
3+
from .cmip6_cmoriser import (
4+
ACCESS_ESM_CMORiser,
5+
CMIP6_Atmosphere_CMORiser,
6+
CMIP6_CMORiser,
7+
CMIP6_Ocean_CMORiser,
8+
)
49

510
__version__ = _version.get_versions()["version"]
611

src/access_mopper/cmip6_cmoriser.py

Lines changed: 164 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
class CMIP6_CMORiser:
1616
"""
17-
Handles CMORisation of NetCDF datasets using CMIP6 metadata.
17+
Base class for CMIP6 CMORisers, providing shared logic for CMORisation.
1818
"""
1919

2020
type_mapping = type_mapping
@@ -45,31 +45,9 @@ def load_dataset(self):
4545
)
4646

4747
def select_and_process_variables(self):
48-
bnds_required = [
49-
v["out_name"] + "_bnds"
50-
for v in self.vocab.axes.values()
51-
if v.get("must_have_bounds") == "yes"
52-
]
53-
54-
input_vars = self.mapping[self.cmor_name]["model_variables"]
55-
calc = self.mapping[self.cmor_name]["calculation"]
56-
self.ds = self.ds[input_vars + bnds_required]
57-
58-
if calc["type"] == "direct":
59-
self.ds = self.ds.rename({input_vars[0]: self.cmor_name})
60-
elif calc["type"] == "formula":
61-
local = {var: self.ds[var] for var in input_vars}
62-
self.ds[self.cmor_name] = eval(calc["formula"], {}, local)
63-
else:
64-
raise ValueError(f"Unsupported calculation type: {calc['type']}")
65-
66-
cmor_dims = self.vocab.variable["dimensions"].split()
67-
transpose_order = [
68-
self.vocab.axes[dim]["out_name"]
69-
for dim in cmor_dims
70-
if "value" not in self.vocab.axes[dim]
71-
]
72-
self.ds[self.cmor_name] = self.ds[self.cmor_name].transpose(*transpose_order)
48+
raise NotImplementedError(
49+
"Subclasses must implement select_and_process_variables."
50+
)
7351

7452
def _check_units(self, var: str, expected: str) -> bool:
7553
actual = self.ds[var].attrs.get("units")
@@ -118,106 +96,7 @@ def drop_intermediates(self):
11896
self.ds = self.ds.drop_vars(var)
11997

12098
def update_attributes(self):
121-
self.ds.attrs = {
122-
k: v
123-
for k, v in self.vocab.get_required_global_attributes().items()
124-
if v not in (None, "")
125-
}
126-
127-
self.ds = self.ds.rename(self.mapping[self.cmor_name]["dimensions"])
128-
129-
required_coords = {
130-
v["out_name"] for v in self.vocab.axes.values() if "value" in v
131-
}.union({v["out_name"] for v in self.vocab.axes.values()})
132-
self.ds = self.ds.drop_vars(
133-
[c for c in self.ds.coords if c not in required_coords], errors="ignore"
134-
)
135-
136-
cmor_attrs = self.vocab.variable
137-
self._check_units(self.cmor_name, cmor_attrs.get("units"))
138-
139-
self.ds[self.cmor_name].attrs.update(
140-
{k: v for k, v in cmor_attrs.items() if v not in (None, "")}
141-
)
142-
var_type = cmor_attrs.get("type", "double")
143-
self.ds[self.cmor_name] = self.ds[self.cmor_name].astype(
144-
self.type_mapping.get(var_type, np.float64)
145-
)
146-
147-
try:
148-
if cmor_attrs.get("valid_min") not in (None, "") and cmor_attrs.get(
149-
"valid_max"
150-
) not in (None, ""):
151-
vmin = self.type_mapping.get(var_type, np.float64)(
152-
cmor_attrs["valid_min"]
153-
)
154-
vmax = self.type_mapping.get(var_type, np.float64)(
155-
cmor_attrs["valid_max"]
156-
)
157-
self._check_range(self.cmor_name, vmin, vmax)
158-
except ValueError as e:
159-
raise ValueError(
160-
f"Failed to validate value range for {self.cmor_name}: {e}"
161-
)
162-
163-
for dim, meta in self.vocab.axes.items():
164-
name = meta["out_name"]
165-
dtype = self.type_mapping.get(meta.get("type", "double"), np.float64)
166-
if name in self.ds:
167-
self._check_units(name, meta.get("units", ""))
168-
if meta.get("standard_name") == "time":
169-
self._check_calendar(name)
170-
original_units = self.ds[name].attrs.get("units", "")
171-
coord_attrs = {
172-
k: v
173-
for k, v in {
174-
"standard_name": meta.get("standard_name"),
175-
"long_name": meta.get("long_name"),
176-
"units": meta.get("units"),
177-
"axis": meta.get("axis"),
178-
"positive": meta.get("positive"),
179-
"valid_min": dtype(meta["valid_min"])
180-
if "valid_min" in meta
181-
else None,
182-
"valid_max": dtype(meta["valid_max"])
183-
if "valid_max" in meta
184-
else None,
185-
}.items()
186-
if v is not None
187-
}
188-
if coord_attrs.get(
189-
"units"
190-
) == "days since ?" and original_units.lower().startswith("days since"):
191-
coord_attrs["units"] = original_units
192-
updated = self.ds[name].astype(dtype)
193-
updated.attrs.update(coord_attrs)
194-
self.ds[name] = updated
195-
elif "value" in meta:
196-
self.ds = self.ds.assign_coords(
197-
{
198-
name: xr.DataArray(
199-
dtype(meta["value"]),
200-
dims=(),
201-
attrs={
202-
k: v
203-
for k, v in {
204-
"standard_name": meta.get("standard_name"),
205-
"long_name": meta.get("long_name"),
206-
"units": meta.get("units"),
207-
"axis": meta.get("axis"),
208-
"positive": meta.get("positive"),
209-
"valid_min": dtype(meta["valid_min"])
210-
if "valid_min" in meta
211-
else None,
212-
"valid_max": dtype(meta["valid_max"])
213-
if "valid_max" in meta
214-
else None,
215-
}.items()
216-
if v is not None
217-
},
218-
)
219-
}
220-
)
99+
raise NotImplementedError("Subclasses must implement update_attributes.")
221100

222101
def reorder(self):
223102
def ordered(ds, core=("lat", "lon", "time", "height")):
@@ -333,6 +212,141 @@ def run(self):
333212
self.write()
334213

335214

215+
class CMIP6_Atmosphere_CMORiser(CMIP6_CMORiser):
216+
"""
217+
Handles CMORisation of NetCDF datasets using CMIP6 metadata (Atmosphere/Land).
218+
"""
219+
220+
def select_and_process_variables(self):
221+
bnds_required = [
222+
v["out_name"] + "_bnds"
223+
for v in self.vocab.axes.values()
224+
if v.get("must_have_bounds") == "yes"
225+
]
226+
227+
input_vars = self.mapping[self.cmor_name]["model_variables"]
228+
calc = self.mapping[self.cmor_name]["calculation"]
229+
self.ds = self.ds[input_vars + bnds_required]
230+
231+
if calc["type"] == "direct":
232+
self.ds = self.ds.rename({input_vars[0]: self.cmor_name})
233+
elif calc["type"] == "formula":
234+
local = {var: self.ds[var] for var in input_vars}
235+
self.ds[self.cmor_name] = eval(calc["formula"], {}, local)
236+
else:
237+
raise ValueError(f"Unsupported calculation type: {calc['type']}")
238+
239+
cmor_dims = self.vocab.variable["dimensions"].split()
240+
transpose_order = [
241+
self.vocab.axes[dim]["out_name"]
242+
for dim in cmor_dims
243+
if "value" not in self.vocab.axes[dim]
244+
]
245+
self.ds[self.cmor_name] = self.ds[self.cmor_name].transpose(*transpose_order)
246+
247+
def update_attributes(self):
248+
self.ds.attrs = {
249+
k: v
250+
for k, v in self.vocab.get_required_global_attributes().items()
251+
if v not in (None, "")
252+
}
253+
254+
self.ds = self.ds.rename(self.mapping[self.cmor_name]["dimensions"])
255+
256+
required_coords = {
257+
v["out_name"] for v in self.vocab.axes.values() if "value" in v
258+
}.union({v["out_name"] for v in self.vocab.axes.values()})
259+
self.ds = self.ds.drop_vars(
260+
[c for c in self.ds.coords if c not in required_coords], errors="ignore"
261+
)
262+
263+
cmor_attrs = self.vocab.variable
264+
self._check_units(self.cmor_name, cmor_attrs.get("units"))
265+
266+
self.ds[self.cmor_name].attrs.update(
267+
{k: v for k, v in cmor_attrs.items() if v not in (None, "")}
268+
)
269+
var_type = cmor_attrs.get("type", "double")
270+
self.ds[self.cmor_name] = self.ds[self.cmor_name].astype(
271+
self.type_mapping.get(var_type, np.float64)
272+
)
273+
274+
try:
275+
if cmor_attrs.get("valid_min") not in (None, "") and cmor_attrs.get(
276+
"valid_max"
277+
) not in (None, ""):
278+
vmin = self.type_mapping.get(var_type, np.float64)(
279+
cmor_attrs["valid_min"]
280+
)
281+
vmax = self.type_mapping.get(var_type, np.float64)(
282+
cmor_attrs["valid_max"]
283+
)
284+
self._check_range(self.cmor_name, vmin, vmax)
285+
except ValueError as e:
286+
raise ValueError(
287+
f"Failed to validate value range for {self.cmor_name}: {e}"
288+
)
289+
290+
for dim, meta in self.vocab.axes.items():
291+
name = meta["out_name"]
292+
dtype = self.type_mapping.get(meta.get("type", "double"), np.float64)
293+
if name in self.ds:
294+
self._check_units(name, meta.get("units", ""))
295+
if meta.get("standard_name") == "time":
296+
self._check_calendar(name)
297+
original_units = self.ds[name].attrs.get("units", "")
298+
coord_attrs = {
299+
k: v
300+
for k, v in {
301+
"standard_name": meta.get("standard_name"),
302+
"long_name": meta.get("long_name"),
303+
"units": meta.get("units"),
304+
"axis": meta.get("axis"),
305+
"positive": meta.get("positive"),
306+
"valid_min": dtype(meta["valid_min"])
307+
if "valid_min" in meta
308+
else None,
309+
"valid_max": dtype(meta["valid_max"])
310+
if "valid_max" in meta
311+
else None,
312+
}.items()
313+
if v is not None
314+
}
315+
if coord_attrs.get(
316+
"units"
317+
) == "days since ?" and original_units.lower().startswith("days since"):
318+
coord_attrs["units"] = original_units
319+
updated = self.ds[name].astype(dtype)
320+
updated.attrs.update(coord_attrs)
321+
self.ds[name] = updated
322+
elif "value" in meta:
323+
self.ds = self.ds.assign_coords(
324+
{
325+
name: xr.DataArray(
326+
dtype(meta["value"]),
327+
dims=(),
328+
attrs={
329+
k: v
330+
for k, v in {
331+
"standard_name": meta.get("standard_name"),
332+
"long_name": meta.get("long_name"),
333+
"units": meta.get("units"),
334+
"axis": meta.get("axis"),
335+
"positive": meta.get("positive"),
336+
"valid_min": dtype(meta["valid_min"])
337+
if "valid_min" in meta
338+
else None,
339+
"valid_max": dtype(meta["valid_max"])
340+
if "valid_max" in meta
341+
else None,
342+
}.items()
343+
if v is not None
344+
},
345+
)
346+
}
347+
)
348+
349+
336350
class CMIP6_Ocean_CMORiser(CMIP6_CMORiser):
337351
"""
338352
CMORiser subclass for ocean variables using curvilinear supergrid coordinates.
@@ -343,9 +357,8 @@ def __init__(
343357
input_paths: Union[str, List[str]],
344358
output_path: str,
345359
cmor_name: str,
346-
cmip6_vocab: Any,
360+
cmip6_vocab: CMIP6Vocabulary,
347361
variable_mapping: Dict[str, Any],
348-
supergrid_path: Union[str, Path],
349362
drs_root: Optional[Path] = None,
350363
):
351364
super().__init__(
@@ -356,7 +369,9 @@ def __init__(
356369
variable_mapping=variable_mapping,
357370
drs_root=drs_root,
358371
)
359-
self.supergrid = Supergrid(supergrid_path)
372+
373+
nominal_resolution = cmip6_vocab._get_nominal_resolution()
374+
self.supergrid = Supergrid(nominal_resolution)
360375
self.grid_info = None
361376
self.grid_type = None
362377

@@ -479,7 +494,7 @@ def update_attributes(self):
479494
)
480495

481496

482-
class CMIP6Workflow:
497+
class ACCESS_ESM_CMORiser:
483498
"""
484499
Coordinates the CMORisation process using CMIP6Vocabulary and CMORiser.
485500
Handles DRS, versioning, and orchestrates the workflow.
@@ -515,14 +530,25 @@ def __init__(
515530
parent_info=self.parent_info,
516531
)
517532

518-
self.cmoriser = CMIP6_CMORiser(
519-
input_paths=self.input_paths,
520-
output_path=str(self.output_path),
521-
cmor_name=self.vocab.cmor_name,
522-
cmip6_vocab=self.vocab,
523-
variable_mapping=self.variable_mapping,
524-
drs_root=drs_root if drs_root else None,
525-
)
533+
table, cmor_name = compound_name.split(".")
534+
if table in ("Amon", "Lmon", "SImon", "SImon"):
535+
self.cmoriser = CMIP6_Atmosphere_CMORiser(
536+
input_paths=self.input_paths,
537+
output_path=str(self.output_path),
538+
cmor_name=cmor_name,
539+
cmip6_vocab=self.vocab,
540+
variable_mapping=self.variable_mapping,
541+
drs_root=drs_root if drs_root else None,
542+
)
543+
elif table in ("Oyr", "Oday", "Omon", "Omon_curvilinear"):
544+
self.cmoriser = CMIP6_Ocean_CMORiser(
545+
input_paths=self.input_paths,
546+
output_path=str(self.output_path),
547+
cmor_name=cmor_name,
548+
cmip6_vocab=self.vocab,
549+
variable_mapping=self.variable_mapping,
550+
drs_root=drs_root if drs_root else None,
551+
)
526552

527553
def run(self):
528554
"""

0 commit comments

Comments
 (0)