11"""Fixes for ICON-ESM-LR model."""
22
3+ import numpy as np
4+ from iris .coords import AuxCoord
5+ from iris .mesh import Connectivity , MeshXY
6+
37from esmvalcore .cmor ._fixes .fix import Fix
8+ from esmvalcore .iris_helpers import has_unstructured_grid
9+
10+ # deduplicate decimals - round cartesian coords to merge
11+ # identical vertices (especially pole vertices)
12+ CARTESIAN_COORDINATE_DECIMALS = 12
413
514
615class AllVars (Fix ):
7- """Fixes for all variables."""
16+ """Adapt the native ICON mesh fix for ICON-ESM-LR outputs.
17+
18+ Like the native ICON fix (`esmvalcore.cmor._fixes.icon._base_fixes`).
19+ this avoids ``MeshXY.from_coords`` because shared
20+ polygon vertices would be duplicated. Since CMIP6 ICON-ESM-LR files don't
21+ have ``vertex_of_cell``, recreate the connectivity from coordinate
22+ bounds.
23+ """
24+
25+ @staticmethod
26+ def _can_create_mesh (cube ):
27+ # check if mesh is there
28+ if cube .mesh is not None :
29+ return False
30+ # unstructured?
31+ if not has_unstructured_grid (cube ):
32+ return False
33+
34+ lat = cube .coord ("latitude" )
35+ lon = cube .coord ("longitude" )
36+
37+ # check bounds
38+ if not lat .has_bounds () or not lon .has_bounds ():
39+ return False
40+ if lat .bounds .shape != lon .bounds .shape :
41+ return False
42+ return lat .bounds .ndim == 2
43+
44+ @staticmethod
45+ def _get_node_coords_and_connectivity (lat_bounds , lon_bounds ):
46+ """Build unique mesh nodes and face-node connectivity.
47+
48+ Cell vertices are converted to cartesian coordinates to
49+ identify shared nodes. Duplicate vertices are
50+ removed and a face-node connectivity array is generated.
51+ """
52+ lat_rad = np .deg2rad (lat_bounds )
53+ lon_rad = np .deg2rad (lon_bounds )
54+
55+ cartesian = np .stack (
56+ [
57+ np .cos (lat_rad ) * np .cos (lon_rad ),
58+ np .cos (lat_rad ) * np .sin (lon_rad ),
59+ np .sin (lat_rad ),
60+ ],
61+ axis = - 1 ,
62+ )
63+
64+ # round coords to avoid floating point diffs
65+ rounded = np .round (
66+ cartesian .reshape (- 1 , 3 ),
67+ decimals = CARTESIAN_COORDINATE_DECIMALS ,
68+ )
69+ # unique mesh nodes
70+ unique_nodes , inverse = np .unique (
71+ rounded ,
72+ axis = 0 ,
73+ return_inverse = True ,
74+ )
75+
76+ # we create face-node connectivity and back to lon/lat
77+ connectivity = inverse .reshape (lat_bounds .shape )
78+ norm = np .linalg .norm (unique_nodes , axis = 1 )
79+ unit_nodes = unique_nodes / norm [:, np .newaxis ]
80+ node_lat = np .rad2deg (np .arcsin (unit_nodes [:, 2 ]))
81+ node_lon = (
82+ np .rad2deg (np .arctan2 (unit_nodes [:, 1 ], unit_nodes [:, 0 ])) % 360.0
83+ )
84+
85+ return node_lat , node_lon , connectivity
86+
87+ def _fix_unstructured_mesh (self , cube ):
88+ """Create and attach the Iris mesh.
89+
90+ Constructs node coordinates and face-node connectivity
91+ from latitude longitude bounds and replaces the original
92+ coordinate representation with Iris mesh coordinates.
93+ """
94+ if not self ._can_create_mesh (cube ):
95+ return
96+
97+ lat = cube .coord ("latitude" )
98+ lon = cube .coord ("longitude" )
99+ mesh_dim = cube .coord_dims (lat )
100+
101+ # construct the face_node_connectivity from bounds
102+ node_lat_points , node_lon_points , face_node_connectivity = (
103+ self ._get_node_coords_and_connectivity (lat .bounds , lon .bounds )
104+ )
105+
106+ node_lat = AuxCoord (
107+ node_lat_points ,
108+ standard_name = "latitude" ,
109+ long_name = "node latitude" ,
110+ var_name = "nlat" ,
111+ units = lat .units ,
112+ )
113+ node_lon = AuxCoord (
114+ node_lon_points ,
115+ standard_name = "longitude" ,
116+ long_name = "node longitude" ,
117+ var_name = "nlon" ,
118+ units = lon .units ,
119+ )
120+
121+ face_lat = lat .copy ()
122+ face_lon = lon .copy ()
123+ # Update face bounds using the deduplicated node coords.
124+ face_lat .bounds = node_lat .points [face_node_connectivity ]
125+ face_lon .bounds = node_lon .points [face_node_connectivity ]
126+
127+ # Same create mesh logic with native ICON fix (Iris mesh object).
128+ connectivity = Connectivity (
129+ indices = face_node_connectivity ,
130+ cf_role = "face_node_connectivity" ,
131+ start_index = 0 ,
132+ location_axis = 0 ,
133+ )
134+ mesh = MeshXY (
135+ topology_dimension = 2 ,
136+ node_coords_and_axes = [(node_lat , "y" ), (node_lon , "x" )],
137+ face_coords_and_axes = [(face_lat , "y" ), (face_lon , "x" )],
138+ connectivities = [connectivity ],
139+ )
140+
141+ cube .remove_coord ("latitude" )
142+ cube .remove_coord ("longitude" )
143+ for mesh_coord in mesh .to_MeshCoords ("face" ):
144+ cube .add_aux_coord (mesh_coord , mesh_dim )
8145
9146 def fix_metadata (self , cubes ):
10147 """Rename ``var_name`` of latitude and longitude.
@@ -29,5 +166,6 @@ def fix_metadata(self, cubes):
29166 for std_name , var_name in varnames_to_change .items ():
30167 if cube .coords (std_name ):
31168 cube .coord (std_name ).var_name = var_name
169+ self ._fix_unstructured_mesh (cube )
32170
33171 return cubes
0 commit comments