Skip to content

Commit 5130514

Browse files
authored
Merge pull request #128 from paigem/vector-b-grid
Add B-grid vector Laplacian
2 parents 817aadc + 57fd8ad commit 5130514

11 files changed

Lines changed: 273 additions & 43 deletions

File tree

docs/basic_filtering.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ The following table provides an overview of these different grid type options: w
4646
+--------------------------------+-----------------------------------------------------------------+--------------+--------------------+------------------+------------------------------------------+
4747
| ``VECTOR_C_GRID`` | `Arakawa C-Grid <https://en.wikipedia.org/wiki/Arakawa_grids>`_ | yes | periodic | Vector Laplacian | :doc:`examples/example_vector_laplacian` |
4848
+--------------------------------+-----------------------------------------------------------------+--------------+--------------------+------------------+------------------------------------------+
49+
| ``VECTOR_B_GRID`` | `Arakawa B-Grid <https://en.wikipedia.org/wiki/Arakawa_grids>`_ | no | periodic | Vector Laplacian | |
50+
+--------------------------------+-----------------------------------------------------------------+--------------+--------------------+------------------+------------------------------------------+
4951

5052
Grid types with scalar Laplacians can be used for filtering scalar fields (such as temperature), and grid types with vector Laplacians can be used for filtering vector fields (such as velocity).
5153

gcm_filters/kernels.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"TRIPOLAR_REGULAR_WITH_LAND_AREA_WEIGHTED",
2424
"TRIPOLAR_POP_WITH_LAND",
2525
"VECTOR_C_GRID",
26+
"VECTOR_B_GRID",
2627
],
2728
)
2829

@@ -695,6 +696,147 @@ def __call__(self, ufield: ArrayType, vfield: ArrayType):
695696
ALL_KERNELS[GridType.VECTOR_C_GRID] = CgridVectorLaplacian
696697

697698

699+
@dataclass
700+
class BgridVectorLaplacian(BaseVectorLaplacian):
701+
"""̵Vector Laplacian on B-Grid. Implemented for viscosity operators on B-grids based on POP code. Assumes periodic boundary conditions.
702+
703+
Attributes
704+
----------
705+
706+
DXU: x-spacing centered at U points
707+
DYU: y-spacing centered at U points
708+
HUS: cell widths on south side of U cell
709+
HUW: cell widths on west side of U cell
710+
HTE: cell widths on east side of T cell
711+
HTN: cell widths on north side of T cell
712+
UAREA: U-cell area
713+
TAREA: T-cell area
714+
"""
715+
716+
is_dimensional = True
717+
718+
DXU: ArrayType
719+
DYU: ArrayType
720+
HUS: ArrayType
721+
HUW: ArrayType
722+
HTE: ArrayType
723+
HTN: ArrayType
724+
UAREA: ArrayType
725+
TAREA: ArrayType
726+
727+
def __post_init__(self):
728+
np = get_array_module(self.DXU)
729+
730+
# Derived quantities
731+
self.UAREA_R = 1 / self.UAREA
732+
self.TAREA_R = 1 / self.TAREA
733+
734+
self.DXUR = 1 / self.DXU
735+
self.DYUR = 1 / self.DYU
736+
737+
def __call__(self, ufield: ArrayType, vfield: ArrayType):
738+
np = get_array_module(ufield)
739+
740+
ufield = np.nan_to_num(ufield)
741+
vfield = np.nan_to_num(vfield)
742+
743+
# Constants
744+
c2 = 2
745+
p5 = 0.5
746+
747+
# Calculate coefficients for the stencil without metric terms
748+
WORK1 = self.HUS / self.HTE
749+
750+
DUS = (
751+
WORK1 * self.UAREA_R
752+
) # South coefficient of 5-point stencil for the Del**2 operator acting at U points
753+
DUN = (
754+
np.roll(WORK1, 1, axis=-1) * self.UAREA_R
755+
) # North coefficient of 5-point stencil
756+
757+
WORK1 = self.HUW / self.HTN
758+
759+
DUW = WORK1 * self.UAREA_R # West coefficient of 5-point stencil
760+
DUE = (
761+
np.roll(WORK1, 1, axis=-2) * self.UAREA_R
762+
) # East coefficient of 5-point stencil
763+
764+
# Calculate coefficients for metric terms and for metric advection terms (KXU,KYU)
765+
KXU = (
766+
np.roll(self.HUW, 1, axis=-2) - self.HUW
767+
) * self.UAREA_R # defined in (3.24) of POP manual
768+
KYU = (np.roll(self.HUS, 1, axis=-1) - self.HUS) * self.UAREA_R
769+
770+
WORK1 = (self.HTE - np.roll(self.HTE, -1, axis=-2)) * self.TAREA_R # KXT
771+
WORK2 = p5 * (WORK1 + np.roll(WORK1, 1, axis=-1))
772+
DXKX = (np.roll(WORK2, 1, axis=-2) - WORK2) * self.DXUR
773+
774+
WORK2 = p5 * (WORK1 + np.roll(WORK1, 1, axis=-2))
775+
DYKX = (np.roll(WORK2, 1, axis=-1) - WORK2) * self.DYUR
776+
777+
WORK1 = (self.HTN - np.roll(self.HTN, -1, axis=-1)) * self.TAREA_R # KYT
778+
WORK2 = p5 * (WORK1 + np.roll(WORK1, 1, axis=-2))
779+
DYKY = (np.roll(WORK2, 1, axis=-1) - WORK2) * self.DYUR
780+
781+
WORK2 = p5 * (WORK1 + np.roll(WORK1, 1, axis=-1))
782+
DXKY = (np.roll(WORK2, axis=-2, shift=1) - WORK2) * self.DXUR
783+
784+
DUM = -(
785+
DXKX + DYKY + c2 * (KXU * KXU + KYU * KYU)
786+
) # central coefficient for metric terms that do not mix U,V
787+
DMC = (
788+
DXKY - DYKX
789+
) # central coefficient of 5-point stencil for the metric terms that mix U,V
790+
791+
# Calculate the central coefficient for metric mixing terms that mix U,V
792+
DME = (c2 * KYU) / (
793+
self.HTN + np.roll(self.HTN, axis=-2, shift=1)
794+
) # East coefficient of 5-point stencil for the metric terms that mix U,V
795+
796+
DMN = -(c2 * KXU) / (
797+
self.HTE + np.roll(self.HTE, axis=-1, shift=1)
798+
) # North coefficient of 5-point stencil
799+
800+
DUC = -(DUN + DUS + DUE + DUW) # central coefficient of 5-point stencil
801+
DMW = -DME # West coefficient of 5-point stencil
802+
DMS = -DMN # East coefficient of 5-point stencil
803+
804+
# Compute the horizontal diffusion of momentum
805+
am = 1
806+
cc = DUC + DUM
807+
808+
u_component = am * (
809+
cc * ufield
810+
+ DUN * np.roll(ufield, -1, axis=-2)
811+
+ DUS * np.roll(ufield, 1, axis=-2)
812+
+ DUE * np.roll(ufield, -1, axis=-1)
813+
+ DUW * np.roll(ufield, 1, axis=-1)
814+
+ DMC * vfield
815+
+ DMN * np.roll(vfield, -1, axis=-2)
816+
+ DMS * np.roll(vfield, 1, axis=-2)
817+
+ DME * np.roll(vfield, -1, axis=-1)
818+
+ DMW * np.roll(vfield, 1, axis=-1)
819+
)
820+
821+
v_component = am * (
822+
cc * vfield
823+
+ DUN * np.roll(vfield, -1, axis=-2)
824+
+ DUS * np.roll(vfield, 1, axis=-2)
825+
+ DUE * np.roll(vfield, -1, axis=-1)
826+
+ DUW * np.roll(vfield, 1, axis=-1)
827+
+ DMC * ufield
828+
+ DMN * np.roll(ufield, -1, axis=-2)
829+
+ DMS * np.roll(ufield, 1, axis=-2)
830+
+ DME * np.roll(ufield, -1, axis=-1)
831+
+ DMW * np.roll(ufield, 1, axis=-1)
832+
)
833+
834+
return (u_component, v_component)
835+
836+
837+
ALL_KERNELS[GridType.VECTOR_B_GRID] = BgridVectorLaplacian
838+
839+
698840
def required_grid_vars(grid_type: GridType):
699841
"""Utility function for figuring out the required grid variables needed by each grid type.
700842

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 80 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@
3030
GridType.TRIPOLAR_POP_WITH_LAND: ["wet_mask", "dxe", "dye", "dxn", "dyn", "tarea"],
3131
}
3232

33+
_vector_grid_kwargs = {
34+
GridType.VECTOR_C_GRID: [
35+
"wet_mask_t",
36+
"wet_mask_q",
37+
"dxT",
38+
"dyT",
39+
"dxCu",
40+
"dyCu",
41+
"dxCv",
42+
"dyCv",
43+
"dxBu",
44+
"dyBu",
45+
"area_u",
46+
"area_v",
47+
"kappa_iso",
48+
"kappa_aniso",
49+
],
50+
GridType.VECTOR_B_GRID: [
51+
"DXU",
52+
"DYU",
53+
"HUS",
54+
"HUW",
55+
"HTE",
56+
"HTN",
57+
"UAREA",
58+
"TAREA",
59+
],
60+
}
3361

3462
scalar_grids = [
3563
GridType.REGULAR,
@@ -45,7 +73,7 @@
4573
GridType.TRIPOLAR_REGULAR_WITH_LAND_AREA_WEIGHTED,
4674
GridType.TRIPOLAR_POP_WITH_LAND,
4775
]
48-
vector_grids = [GridType.VECTOR_C_GRID]
76+
vector_grids = [GridType.VECTOR_C_GRID, GridType.VECTOR_B_GRID]
4977

5078

5179
def _make_random_data(shape: Tuple[int, int], seed: int) -> np.ndarray:
@@ -150,72 +178,90 @@ def grid_type_and_input_ds(request, scalar_grid_type_data_and_extra_kwargs):
150178

151179

152180
@pytest.fixture(scope="session")
181+
# compute latitudes and longitudes for a spherical C-grid geometry
153182
def spherical_geometry():
183+
154184
ny, nx = (128, 256)
155185

156186
# construct spherical coordinate system similar to MOM6 NeverWorld2 grid
157187
# define latitudes and longitudes
158188
lat_min = -70
159189
lat_max = 70
160-
lat_u = np.linspace(
190+
191+
# compute latitude of u-points on C-grid
192+
latCu = np.linspace(
161193
lat_min + 0.5 * (lat_max - lat_min) / ny,
162194
lat_max - 0.5 * (lat_max - lat_min) / ny,
163195
ny,
164196
)
165-
lat_v = np.linspace(lat_min + (lat_max - lat_min) / ny, lat_max, ny)
197+
# compute latitude of v-points on C-grid
198+
latCv = np.linspace(lat_min + (lat_max - lat_min) / ny, lat_max, ny)
199+
166200
lon_min = 0
167201
lon_max = 60
168-
lon_u = np.linspace(lon_min + (lon_max - lon_min) / nx, lon_max, nx)
169-
lon_v = np.linspace(
202+
203+
lonCu = np.linspace(lon_min + (lon_max - lon_min) / nx, lon_max, nx)
204+
lonCv = np.linspace(
170205
lon_min + 0.5 * (lon_max - lon_min) / nx,
171206
lon_max - 0.5 * (lon_max - lon_min) / nx,
172207
nx,
173208
)
174-
(geolon_u, geolat_u) = np.meshgrid(lon_u, lat_u)
175-
(geolon_v, geolat_v) = np.meshgrid(lon_v, lat_v)
176209

177-
return geolon_u, geolat_u, geolon_v, geolat_v
210+
(geolonCu, geolatCu) = np.meshgrid(lonCu, latCu)
211+
(geolonCv, geolatCv) = np.meshgrid(lonCv, latCv)
212+
213+
return geolonCu, geolatCu, geolonCv, geolatCv
178214

179215

180216
@pytest.fixture(scope="session", params=vector_grids)
181217
def vector_grid_type_data_and_extra_kwargs(request, spherical_geometry):
182218
grid_type = request.param
183-
geolon_u, geolat_u, geolon_v, geolat_v = spherical_geometry
184-
ny, nx = geolon_u.shape
219+
geolonCu, geolatCu, geolonCv, geolatCv = spherical_geometry
220+
ny, nx = geolonCu.shape
185221

186222
extra_kwargs = {}
187223

188-
# for now, we assume that the only implemented vector grid is VECTOR_C_GRID
189-
# we can relax this if we implement other vector grids
190-
assert grid_type == GridType.VECTOR_C_GRID
191-
192224
R = 6378000
225+
193226
# dx varies spatially
194-
extra_kwargs["dxCu"] = R * np.cos(geolat_u / 360 * 2 * np.pi)
195-
extra_kwargs["dxCv"] = R * np.cos(geolat_v / 360 * 2 * np.pi)
196-
extra_kwargs["dxBu"] = extra_kwargs["dxCv"] + np.roll(
197-
extra_kwargs["dxCv"], -1, axis=1
198-
)
199-
extra_kwargs["dxT"] = extra_kwargs["dxCu"] + np.roll(
200-
extra_kwargs["dxCu"], 1, axis=1
201-
)
202-
# dy is set constant, equal to dx at the equator
203-
dy = np.max(extra_kwargs["dxCu"]) * np.ones((ny, nx))
204-
extra_kwargs["dyCu"] = dy
205-
extra_kwargs["dyCv"] = dy
206-
extra_kwargs["dyBu"] = dy
207-
extra_kwargs["dyT"] = dy
227+
for name in _vector_grid_kwargs[grid_type]:
228+
if name in ["dxCu", "dxT", "HUS", "HTE"]:
229+
extra_kwargs[name] = R * np.cos(geolatCu / 360 * 2 * np.pi)
230+
# compute dy for later
231+
# dy is set constant, equal to dx at the equator
232+
dy = np.max(extra_kwargs[name]) * np.ones((ny, nx))
233+
if name in ["dxCv", "dxBu", "DXU", "HUW", "HTN"]:
234+
extra_kwargs[name] = R * np.cos(geolatCv / 360 * 2 * np.pi)
235+
236+
# dy
237+
for name in _vector_grid_kwargs[grid_type]:
238+
if name in ["dyCu", "dyCv", "dyBu", "dyT", "DYU"]:
239+
extra_kwargs[name] = dy
240+
208241
# compute grid cell areas
209-
extra_kwargs["area_u"] = extra_kwargs["dxCu"] * extra_kwargs["dyCu"]
210-
extra_kwargs["area_v"] = extra_kwargs["dxCv"] * extra_kwargs["dyCv"]
242+
for name in _vector_grid_kwargs[grid_type]:
243+
if name == "area_u":
244+
extra_kwargs[name] = extra_kwargs["dxCu"] * extra_kwargs["dyCu"]
245+
elif name == "area_v":
246+
extra_kwargs[name] = extra_kwargs["dxCv"] * extra_kwargs["dyCv"]
247+
elif name == "UAREA":
248+
extra_kwargs[name] = extra_kwargs["DXU"] * extra_kwargs["DYU"]
249+
elif name == "TAREA":
250+
# on a spherical grid, HTE is the same as x-spacing centered at tracer point
251+
# on a spherical grid, DYU is the same as y-spacing centered at tracer point
252+
extra_kwargs[name] = extra_kwargs["HTE"] * extra_kwargs["DYU"]
253+
211254
# set isotropic and anisotropic kappas
212-
extra_kwargs["kappa_iso"] = np.ones((ny, nx))
213-
extra_kwargs["kappa_aniso"] = np.ones((ny, nx))
255+
for name in _vector_grid_kwargs[grid_type]:
256+
if name in ["kappa_iso", "kappa_aniso"]:
257+
extra_kwargs[name] = np.ones((ny, nx))
258+
214259
# put a big island in the middle
215260
mask_data = np.ones((ny, nx))
216261
mask_data[: (ny // 2), : (nx // 2)] = 0
217-
extra_kwargs["wet_mask_t"] = mask_data
218-
extra_kwargs["wet_mask_q"] = mask_data
262+
for name in _vector_grid_kwargs[grid_type]:
263+
if name in ["wet_mask_t", "wet_mask_q"]:
264+
extra_kwargs[name] = mask_data
219265

220266
data_u = _make_random_data((ny, nx), 42)
221267
data_v = _make_random_data((ny, nx), 43)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"chunks": [
3+
2,
4+
128,
5+
256
6+
],
7+
"compressor": {
8+
"blocksize": 0,
9+
"clevel": 5,
10+
"cname": "lz4",
11+
"id": "blosc",
12+
"shuffle": 1
13+
},
14+
"dtype": "<f4",
15+
"fill_value": 0.0,
16+
"filters": null,
17+
"order": "C",
18+
"shape": [
19+
2,
20+
128,
21+
256
22+
],
23+
"zarr_format": 2
24+
}
233 KB
Binary file not shown.
0 Bytes
Binary file not shown.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"chunks": [
3+
2,
4+
128,
5+
256
6+
],
7+
"compressor": {
8+
"blocksize": 0,
9+
"clevel": 5,
10+
"cname": "lz4",
11+
"id": "blosc",
12+
"shuffle": 1
13+
},
14+
"dtype": "<f4",
15+
"fill_value": 0.0,
16+
"filters": null,
17+
"order": "C",
18+
"shape": [
19+
2,
20+
128,
21+
256
22+
],
23+
"zarr_format": 2
24+
}
238 KB
Binary file not shown.
-579 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)