Skip to content

Commit 23e69e9

Browse files
schlunmaflicj191
andauthored
Add support for variables phcint and amoc to ICON-XPP CMORizer (#3025)
Co-authored-by: Felicity Chun <32269066+flicj191@users.noreply.github.com>
1 parent 4f65a7b commit 23e69e9

9 files changed

Lines changed: 297 additions & 6 deletions

File tree

esmvalcore/cmor/_fixes/icon/icon_xpp.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44

5+
from iris.coords import AuxCoord
56
from iris.cube import CubeList
67
from scipy import constants
78

@@ -56,6 +57,51 @@ def fix_metadata(self, cubes: CubeList) -> CubeList:
5657
Hfss = NegateData
5758

5859

60+
class Msftmz(IconFix):
61+
"""Fixes for ``msftmz``."""
62+
63+
def fix_metadata(self, cubes: CubeList) -> CubeList:
64+
"""Fix metadata."""
65+
preprocessed_cubes = CubeList([])
66+
basin_coord = AuxCoord(
67+
"placeholder",
68+
standard_name="region",
69+
long_name="ocean basin",
70+
var_name="basin",
71+
)
72+
var_names = {
73+
"atlantic_moc": "atlantic_arctic_ocean",
74+
"pacific_moc": "indian_pacific_ocean",
75+
"global_moc": "global_ocean",
76+
}
77+
for var_name, basin in var_names.items():
78+
cube = self.get_cube(cubes, var_name=var_name)
79+
cube.var_name = "msftmz"
80+
cube.long_name = None
81+
cube.attributes.locals = {}
82+
83+
# Remove longitude coordinate (with length 1)
84+
cube = cube[..., 0]
85+
cube.remove_coord("longitude")
86+
87+
# Add scalar basin coordinate
88+
cube.add_aux_coord(basin_coord.copy(basin), ())
89+
preprocessed_cubes.append(cube)
90+
91+
msftmz_cube = preprocessed_cubes.merge_cube()
92+
93+
# Swap time and basin coordinates
94+
msftmz_cube.transpose([1, 0, 2, 3])
95+
96+
# By default, merge_cube() sorts the coordinate alphabetically (i.e.,
97+
# atlantic_arctic_ocean -> global_ocean -> indian_pacific_ocean). Thus,
98+
# we need to restore the desired order (atlantic_arctic_ocean ->
99+
# indian_pacific_ocean -> global_ocean).
100+
msftmz_cube = msftmz_cube[:, [0, 2, 1], ...]
101+
102+
return CubeList([msftmz_cube])
103+
104+
59105
Rlut = NegateData
60106

61107

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
SOURCE: CMIP7
2+
generic_levels: olevel
3+
!============
4+
variable_entry: phcint
5+
!============
6+
modeling_realm: ocean
7+
!----------------------------------
8+
! Variable attributes:
9+
!----------------------------------
10+
standard_name: integral_wrt_depth_of_sea_water_potential_temperature_expressed_as_heat_content
11+
units: J m-2
12+
cell_methods: area: time: mean where sea
13+
cell_measures: area: areacello
14+
long_name: Integrated Ocean Heat Content from Potential Temperature
15+
comment: This is the vertically-integrated heat content derived from potential temperature (thetao).
16+
!----------------------------------
17+
! Additional variable information:
18+
!----------------------------------
19+
dimensions: longitude latitude time olevel
20+
type: real
21+
!----------------------------------
22+
!

esmvalcore/cmor/tables/cmip6-custom/CMIP6_custom.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,18 @@
767767
"type": "real",
768768
"units": "kg s-1"
769769
},
770+
"phcint": {
771+
"cell_measures": "area: areacello",
772+
"cell_methods": "area: time: mean where sea",
773+
"comment": "This is the vertically-integrated heat content derived from potential temperature (thetao).",
774+
"dimensions": "longitude latitude time olevel",
775+
"long_name": "Integrated Ocean Heat Content from Potential Temperature",
776+
"modeling_realm": "ocean",
777+
"out_name": "phcint",
778+
"standard_name": "integral_wrt_depth_of_sea_water_potential_temperature_expressed_as_heat_content",
779+
"type": "real",
780+
"units": "J m-2"
781+
},
770782
"ptype": {
771783
"cell_measures": "area: areacella",
772784
"cell_methods": "time: mean",

esmvalcore/config/configurations/defaults/extra_facets_icon.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ projects:
132132
gpp: {raw_name: assimi_gross_assimilation_box, var_type: jsb_2d_ml}
133133
lai: {raw_name: pheno_lai_box, var_type: jsb_2d_ml, raw_units: '1'}
134134

135+
# 1D ocean variables
136+
amoc: {var_type: oce_moc}
137+
135138
# 2D ocean variables
136139
hfds: {raw_name: HeatFlux_Total, var_type: oce_dbg}
137140
mlotst: {raw_name: mld, var_type: oce_dbg}
@@ -143,6 +146,8 @@ projects:
143146
zos: {raw_name: zos, var_type: oce_dbg}
144147

145148
# 3D ocean variables
149+
msftmz: {var_type: oce_moc, lat_var: lat}
150+
phcint: {var_type: oce_def}
146151
so: {var_type: oce_def, raw_units: "0.001"}
147152
thetao: {raw_name: to, var_type: oce_def, raw_units: degC}
148153
uo: {raw_name: u, var_type: oce_def}

esmvalcore/preprocessor/_derive/amoc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ def required(project):
2121
{"short_name": "msftmz", "optional": True},
2222
{"short_name": "msftyz", "optional": True},
2323
]
24+
elif project == "ICON":
25+
required = [{"short_name": "msftmz", "mip": "Omon"}]
2426
else:
2527
msg = f"Project {project} can not be used for Amoc derivation."
2628
raise ValueError(msg)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Derivation of variable ``phcint``."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from cf_units import Unit
8+
from iris import NameConstraint
9+
10+
from esmvalcore.preprocessor._shared import get_coord_weights
11+
12+
from ._baseclass import DerivedVariableBase
13+
14+
if TYPE_CHECKING:
15+
from iris.cube import Cube, CubeList
16+
17+
from esmvalcore.typing import Facets
18+
19+
RHO_CP = 4.09169e6
20+
RHO_CP_UNIT = Unit("kg m-3 J kg-1 K-1")
21+
22+
23+
class DerivedVariable(DerivedVariableBase):
24+
"""Derivation of variable `ohc`."""
25+
26+
@staticmethod
27+
def required(project: str) -> list[Facets]: # noqa: ARG004
28+
"""Declare the variables needed for derivation."""
29+
return [{"short_name": "thetao"}]
30+
31+
@staticmethod
32+
def calculate(cubes: CubeList) -> Cube:
33+
"""Compute vertically-integrated heat content.
34+
35+
Use c_p * rho_0 = 4.09169e+6 J m-3 K-1 (Kuhlbrodt et al., 2015, Clim.
36+
Dyn.)
37+
38+
Arguments
39+
---------
40+
cubes:
41+
Input cubes.
42+
43+
Returns
44+
-------
45+
:
46+
Output cube.
47+
48+
"""
49+
cube = cubes.extract_cube(NameConstraint(var_name="thetao"))
50+
cube.convert_units("K")
51+
52+
# In the following, we modify the cube's data and units instead of the
53+
# cube directly to avoid dropping cell measures and ancillary variables
54+
# (https://scitools-iris.readthedocs.io/en/stable/further_topics/lenient_maths.html#finer-detail)
55+
56+
# Multiply by c_p * rho_0 -> J m-3
57+
cube.data = cube.core_data() * RHO_CP
58+
cube.units *= RHO_CP_UNIT
59+
60+
# Multiply by layer depth -> J m-2
61+
z_coord = cube.coord(axis="z")
62+
layer_depth = get_coord_weights(cube, z_coord, broadcast=True)
63+
cube.data = cube.core_data() * layer_depth
64+
cube.units *= z_coord.units
65+
66+
return cube

tests/integration/cmor/_fixes/icon/test_icon_xpp.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import pytest
66
from cf_units import Unit
77
from iris import NameConstraint
8-
from iris.coords import DimCoord
8+
from iris.coords import AuxCoord, DimCoord
99
from iris.cube import Cube, CubeList
10+
from iris.util import new_axis
1011

1112
import esmvalcore.cmor._fixes.icon.icon_xpp
1213
from esmvalcore.cmor._fixes.fix import GenericFix
@@ -18,6 +19,7 @@
1819
Gpp,
1920
Hfls,
2021
Hfss,
22+
Msftmz,
2123
Rlut,
2224
Rlutcs,
2325
Rsutcs,
@@ -704,6 +706,64 @@ def test_hfss_fix(cubes_regular_grid):
704706
np.testing.assert_allclose(fixed_cube.data, [[[0.0, -1.0], [-2.0, -3.0]]])
705707

706708

709+
# Test msftmz (for extra fix)
710+
711+
712+
def test_get_msftmz_fix():
713+
"""Test getting of fix."""
714+
fix = Fix.get_fixes("ICON", "ICON-XPP", "Omon", "msftmz")
715+
assert fix == [Msftmz(None), AllVars(None), GenericFix(None)]
716+
717+
718+
def test_msftmz_fix(cubes_regular_grid):
719+
"""Test fix."""
720+
depth_coord = AuxCoord(
721+
10.0,
722+
standard_name="depth",
723+
long_name="depth below sea",
724+
units="m",
725+
attributes={"positive": "down"},
726+
)
727+
cube = cubes_regular_grid[0][..., [0]]
728+
cube.coord("latitude").var_name = "lat"
729+
cube.add_aux_coord(depth_coord, ())
730+
cube = new_axis(cube, "depth")
731+
cube.transpose([1, 0, 2, 3])
732+
cubes = CubeList([cube.copy() * 0.0, cube.copy() * 1.0, cube.copy() * 2.0])
733+
cubes[0].var_name = "atlantic_moc"
734+
cubes[0].units = "kg s-1"
735+
cubes[1].var_name = "pacific_moc"
736+
cubes[1].units = "kg s-1"
737+
cubes[2].var_name = "global_moc"
738+
cubes[2].units = "kg s-1"
739+
740+
fixed_cubes = fix_metadata(cubes, "Omon", "msftmz")
741+
742+
assert len(fixed_cubes) == 1
743+
cube = fixed_cubes[0]
744+
assert cube.var_name == "msftmz"
745+
assert (
746+
cube.standard_name
747+
== "ocean_meridional_overturning_mass_streamfunction"
748+
)
749+
assert cube.long_name == "Ocean Meridional Overturning Mass Streamfunction"
750+
751+
assert cube.units == "kg s-1"
752+
assert "positive" not in cube.attributes
753+
assert "invalid_units" not in cube.attributes
754+
755+
np.testing.assert_equal(
756+
cube.coord("region").points,
757+
["atlantic_arctic_ocean", "indian_pacific_ocean", "global_ocean"],
758+
)
759+
760+
assert cube.shape == (1, 3, 1, 2)
761+
np.testing.assert_allclose(
762+
cube.data,
763+
[[[[0.0, 0.0]], [[0.0, 2.0]], [[0.0, 4.0]]]],
764+
)
765+
766+
707767
# Test rlut (for extra fix)
708768

709769

tests/unit/preprocessor/_derive/test_amoc.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,16 @@ def cubes():
4848
def test_amoc_preamble(cubes):
4949
derived_var = amoc.DerivedVariable()
5050

51-
cmip5_required = derived_var.required("CMIP5")
52-
assert cmip5_required[0]["short_name"] == "msftmyz"
53-
cmip6_required = derived_var.required("CMIP6")
54-
assert cmip6_required[0]["short_name"] == "msftmz"
55-
assert cmip6_required[1]["short_name"] == "msftyz"
51+
assert derived_var.required("CMIP5") == [
52+
{"short_name": "msftmyz", "mip": "Omon"},
53+
]
54+
assert derived_var.required("CMIP6") == [
55+
{"short_name": "msftmz", "optional": True},
56+
{"short_name": "msftyz", "optional": True},
57+
]
58+
assert derived_var.required("ICON") == [
59+
{"short_name": "msftmz", "mip": "Omon"},
60+
]
5661

5762
# if project s neither CMIP5 nor CMIP6
5863
with pytest.raises(ValueError, match="Project CMIPX can not be used"):
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Test derivation of ``phcint``."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
import numpy as np
8+
import pytest
9+
from iris.coords import DimCoord
10+
from iris.cube import CubeList
11+
12+
from esmvalcore.preprocessor._derive import derive, get_required
13+
14+
if TYPE_CHECKING:
15+
from iris.cube import Cube
16+
17+
18+
@pytest.fixture
19+
def cubes(realistic_4d_cube: Cube) -> CubeList:
20+
depth_coord = DimCoord(
21+
[500.0],
22+
bounds=[[0.0, 1000.0]],
23+
standard_name="depth",
24+
units="m",
25+
attributes={"positive": "down"},
26+
)
27+
realistic_4d_cube.remove_coord("air_pressure")
28+
realistic_4d_cube.add_dim_coord(depth_coord, 1)
29+
realistic_4d_cube.var_name = "thetao"
30+
return CubeList([realistic_4d_cube])
31+
32+
33+
@pytest.mark.parametrize("project", ["CMIP3", "CMIP5", "CMIP6", "CMIP7"])
34+
def test_get_required(project: str) -> None:
35+
assert get_required("phcint", project) == [{"short_name": "thetao"}]
36+
37+
38+
def test_derive(cubes: CubeList) -> None:
39+
short_name = "phcint"
40+
long_name = "Integrated Ocean Heat Content from Potential Temperature"
41+
units = "J m-2"
42+
standard_name = "integral_wrt_depth_of_sea_water_potential_temperature_expressed_as_heat_content"
43+
44+
derived_cube = derive(
45+
cubes,
46+
short_name=short_name,
47+
long_name=long_name,
48+
units=units,
49+
standard_name=standard_name,
50+
)
51+
52+
assert derived_cube.standard_name == standard_name
53+
assert derived_cube.long_name == long_name
54+
assert derived_cube.var_name == short_name
55+
assert derived_cube.units == units
56+
assert derived_cube.shape == (2, 1, 2, 3)
57+
expected_data = np.ma.masked_invalid(
58+
[
59+
[
60+
[
61+
[0.0, np.nan, np.nan],
62+
[np.nan, 16366760000.0, 20458450000.0],
63+
],
64+
],
65+
[
66+
[
67+
[24550140000.0, 28641830000.0, 32733520000.0],
68+
[36825210000.0, 40916900000.0, 45008590000.0],
69+
],
70+
],
71+
],
72+
)
73+
np.testing.assert_allclose(derived_cube.data, expected_data)

0 commit comments

Comments
 (0)