77import xarray as xr
88from cftime import num2date
99
10+ from access_mopper .ocean_supergrid import Supergrid
1011from access_mopper .utilities import load_cmip6_mappings , type_mapping
1112from access_mopper .vocabulary_processors import CMIP6Vocabulary
1213
@@ -303,7 +304,10 @@ def write(self):
303304 for k , v in attrs .items ():
304305 dst .setncattr (k , v )
305306 for dim , size in self .ds .sizes .items ():
306- dst .createDimension (dim , size )
307+ if dim == "time" :
308+ dst .createDimension (dim , None ) # Unlimited dimension
309+ else :
310+ dst .createDimension (dim , size )
307311 for var in self .ds .variables :
308312 vdat = self .ds [var ]
309313 fill = None if var .endswith ("_bnds" ) else vdat .attrs .get ("_FillValue" )
@@ -329,6 +333,152 @@ def run(self):
329333 self .write ()
330334
331335
336+ class CMIP6_Ocean_CMORiser (CMIP6_CMORiser ):
337+ """
338+ CMORiser subclass for ocean variables using curvilinear supergrid coordinates.
339+ """
340+
341+ def __init__ (
342+ self ,
343+ input_paths : Union [str , List [str ]],
344+ output_path : str ,
345+ cmor_name : str ,
346+ cmip6_vocab : Any ,
347+ variable_mapping : Dict [str , Any ],
348+ supergrid_path : Union [str , Path ],
349+ drs_root : Optional [Path ] = None ,
350+ ):
351+ super ().__init__ (
352+ input_paths = input_paths ,
353+ output_path = output_path ,
354+ cmor_name = cmor_name ,
355+ cmip6_vocab = cmip6_vocab ,
356+ variable_mapping = variable_mapping ,
357+ drs_root = drs_root ,
358+ )
359+ self .supergrid = Supergrid (supergrid_path )
360+ self .grid_info = None
361+ self .grid_type = None
362+
363+ def infer_grid_type (self ):
364+ coord_sets = {
365+ "T" : {"xt_ocean" , "yt_ocean" },
366+ "U" : {"xu_ocean" , "yu_ocean" },
367+ "V" : {"xv_ocean" , "yv_ocean" },
368+ "Q" : {"xq_ocean" , "yq_ocean" },
369+ }
370+ present_coords = set (self .ds .coords )
371+ for grid , required in coord_sets .items ():
372+ if required .issubset (present_coords ):
373+ return grid
374+ raise ValueError ("Could not infer grid type from dataset coordinates." )
375+
376+ def select_and_process_variables (self ):
377+ input_vars = self .mapping [self .cmor_name ]["model_variables" ]
378+ calc = self .mapping [self .cmor_name ]["calculation" ]
379+
380+ self .ds = self .ds [input_vars ]
381+
382+ if calc ["type" ] == "direct" :
383+ self .ds [self .cmor_name ] = self .ds [input_vars [0 ]]
384+ elif calc ["type" ] == "formula" :
385+ local = {var : self .ds [var ] for var in input_vars }
386+ self .ds [self .cmor_name ] = eval (calc ["formula" ], {}, local )
387+ else :
388+ raise ValueError (f"Unsupported calculation type: { calc ['type' ]} " )
389+
390+ dim_rename = {
391+ "xt_ocean" : "i" ,
392+ "yt_ocean" : "j" ,
393+ "xu_ocean" : "i" ,
394+ "yu_ocean" : "j" ,
395+ "xq_ocean" : "i" ,
396+ "yq_ocean" : "j" ,
397+ "xv_ocean" : "i" ,
398+ "yv_ocean" : "j" ,
399+ }
400+ dims_to_rename = {
401+ k : v for k , v in dim_rename .items () if k in self .ds [self .cmor_name ].dims
402+ }
403+ self .ds [self .cmor_name ] = self .ds [self .cmor_name ].rename (dims_to_rename )
404+ self .ds [self .cmor_name ] = self .ds [self .cmor_name ].transpose ("time" , "j" , "i" )
405+
406+ self .grid_type = self .infer_grid_type ()
407+ # Drop all other data variables except the CMOR variable
408+ self .ds = self .ds [[self .cmor_name ]]
409+
410+ # Drop unused coordinates
411+ used_coords = set ()
412+ for dim in self .ds [self .cmor_name ].dims :
413+ if dim in self .ds .coords :
414+ used_coords .add (dim )
415+ else :
416+ # Might be implicit dimension (e.g. from formula), check all coords
417+ for coord in self .ds .coords :
418+ if dim in self .ds [coord ].dims :
419+ used_coords .add (coord )
420+ self .ds = self .ds .drop_vars ([c for c in self .ds .coords if c not in used_coords ])
421+
422+ def update_attributes (self ):
423+ grid_type = self .grid_type
424+ self .grid_info = self .supergrid .extract_grid (grid_type )
425+
426+ self .ds = self .ds .assign_coords (
427+ {
428+ "i" : self .grid_info ["i" ],
429+ "j" : self .grid_info ["j" ],
430+ "vertices" : self .grid_info ["vertices" ],
431+ }
432+ )
433+
434+ self .ds ["latitude" ] = self .grid_info ["latitude" ]
435+ self .ds ["longitude" ] = self .grid_info ["longitude" ]
436+ self .ds ["vertices_latitude" ] = self .grid_info ["vertices_latitude" ]
437+ self .ds ["vertices_longitude" ] = self .grid_info ["vertices_longitude" ]
438+
439+ self .ds ["latitude" ].attrs .update (
440+ {
441+ "standard_name" : "latitude" ,
442+ "units" : "degrees_north" ,
443+ "bounds" : "vertices_latitude" ,
444+ }
445+ )
446+ self .ds ["longitude" ].attrs .update (
447+ {
448+ "standard_name" : "longitude" ,
449+ "units" : "degrees_east" ,
450+ "bounds" : "vertices_longitude" ,
451+ }
452+ )
453+ self .ds ["vertices_latitude" ].attrs .update (
454+ {"standard_name" : "latitude" , "units" : "degrees_north" }
455+ )
456+ self .ds ["vertices_longitude" ].attrs .update (
457+ {"standard_name" : "longitude" , "units" : "degrees_east" }
458+ )
459+
460+ self .ds .attrs = {
461+ k : v
462+ for k , v in self .vocab .get_required_global_attributes ().items ()
463+ if v not in (None , "" )
464+ }
465+
466+ if "nv" in self .ds .dims :
467+ self .ds = self .ds .rename_dims ({"nv" : "bnds" }).rename_vars ({"nv" : "bnds" })
468+ self .ds ["bnds" ].attrs .update (
469+ {"long_name" : "vertex number of the bounds" , "units" : "1" }
470+ )
471+
472+ cmor_attrs = self .vocab .variable
473+ self .ds [self .cmor_name ].attrs .update (
474+ {k : v for k , v in cmor_attrs .items () if v not in (None , "" )}
475+ )
476+ var_type = cmor_attrs .get ("type" , "double" )
477+ self .ds [self .cmor_name ] = self .ds [self .cmor_name ].astype (
478+ self .type_mapping .get (var_type , np .float64 )
479+ )
480+
481+
332482class CMIP6Workflow :
333483 """
334484 Coordinates the CMORisation process using CMIP6Vocabulary and CMORiser.
0 commit comments