Skip to content

Commit 7e950b4

Browse files
#723: Add soil thickness processor (#758)
* #723: add a soil thickness calculation function. * #723: Create functional test for slthick processor
1 parent 6f94c16 commit 7e950b4

File tree

6 files changed

+134
-18
lines changed

6 files changed

+134
-18
lines changed

cdds/cdds/common/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2019-2025, Met Office.
1+
# (C) British Crown Copyright 2019-2026, Met Office.
22
# Please see LICENSE.md for license details.
33
"""The :mod:`constants` module contains constants (values that should never be changed by a user and exist for
44
readability and maintainability purposes) for all CDDS components.
@@ -8,6 +8,7 @@
88

99
# Note that orography (m01s00i033) must not go in this list
1010
ANCIL_VARIABLES = [
11+
# soil moisture content
1112
# root depth
1213
'm01s00i009',
1314
'm01s00i216',

mip_convert/mip_convert/plugins/base/data/processors.py

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2016-2025, Met Office.
1+
# (C) British Crown Copyright 2016-2026, Met Office.
22
# Please see LICENSE.md for license details.
33

44
"""Module containing processor functions. These processors can be referred
@@ -560,7 +560,7 @@ def fix_packing_division(numerator, denominator):
560560
def _z_axis(cube):
561561
z_coords = [coord for coord in cube.coords() if guess_coord_axis(coord) == 'Z']
562562
if len(z_coords) != 1:
563-
raise ValueError("cube should have exactly one 'Z' axis")
563+
raise ValueError(f"cube should have exactly one 'Z' axis but found {len(z_coords)}")
564564
return z_coords[0]
565565

566566

@@ -2321,19 +2321,77 @@ def tos_ORCA12(tos_con, tossq_con):
23212321
return tos_con
23222322

23232323

2324-
def calc_rootd(soil_cube, frac_cube, ice_class=None):
2325-
"""
2326-
Returns a cube of the maximum rooting depth calculated from a cube on a model's standard latitude-longitude grid and
2324+
def calc_slthick(soil_cube, frac_cube, ice_class=None):
2325+
"""Returns a cube of soil layer thickness calculated from a cube on a model's standard latitude-longitude grid and
23272326
on soil levels.
23282327
23292328
Parameters
23302329
----------
23312330
soil_cube: :class:`iris.cube.Cube`
23322331
A cube containing data on soil levels for that model.
2333-
23342332
frac_cube: :class:`iris.cube.Cube`
23352333
A cube containing JULES tile fractions for that model.
2334+
ice_class: str
2335+
The tile ID string for land ice. If present, the soil layer thickness is set to zero on land ice points.
2336+
2337+
Returns
2338+
-------
2339+
:class:`iris.cube.Cube`
2340+
A cube containing the thickness of each soil level of that model.
23362341
2342+
Raises
2343+
------
2344+
RuntimeError
2345+
If the given soil cube does not contain cell bounds on the z axis or the soil cube is not masked.
2346+
ValueError
2347+
If the soil is identified as having a negative thickness.
2348+
"""
2349+
# Calculate the thickness of a soil layer from the cell bounds of the vertical coord of the input cube.
2350+
depth_coord = _z_axis(soil_cube)
2351+
slthick_data = soil_cube.data.copy()
2352+
for cell, data in zip(depth_coord, slthick_data):
2353+
if not cell.has_bounds():
2354+
raise RuntimeError("The provided cube does not contian cell bounds on the identified Z axis.")
2355+
data[:] = cell.bounds[0][1] - cell.bounds[0][0]
2356+
if np.any(data < 0):
2357+
raise ValueError("Soil cannot have a negative thickness.")
2358+
2359+
# Set max root depth to 0.0 m on land ice points.
2360+
if ice_class:
2361+
ice_frac = frac_cube.extract(_pseudo_constraint(ice_class))
2362+
ice_cube = _collapse_pseudo(ice_frac, SUM)
2363+
ice_mask = ice_cube.data.data > 1e-6
2364+
slthick_data[:, ice_mask] = 0.0
2365+
2366+
# Copy the land/sea mask from the source cube to the new array.
2367+
if not is_masked(soil_cube.data):
2368+
raise RuntimeError("The soil cube is not masked, any land/sea masks will not be copied to the new array.")
2369+
2370+
slthick_data.mask = soil_cube.data.mask
2371+
2372+
# Create a Cube of the soil level thickness data.
2373+
dim_coords_and_dims = [(coord, k) for (k, coord) in enumerate(soil_cube.dim_coords)]
2374+
2375+
slthick_cube = iris.cube.Cube(slthick_data,
2376+
standard_name="cell_thickness",
2377+
long_name="Thickness of Soil Layers",
2378+
units=depth_coord.units,
2379+
dim_coords_and_dims=dim_coords_and_dims,
2380+
)
2381+
2382+
return slthick_cube
2383+
2384+
2385+
def calc_rootd(soil_cube, frac_cube, ice_class=None):
2386+
"""Returns a cube of the maximum rooting depth calculated from a cube on a model's standard latitude-longitude grid
2387+
and on soil levels.
2388+
2389+
Parameters
2390+
----------
2391+
soil_cube: :class:`iris.cube.Cube`
2392+
A cube containing data on soil levels for that model.
2393+
frac_cube: :class:`iris.cube.Cube`
2394+
A cube containing JULES tile fractions for that model.
23372395
ice_class: str
23382396
Tile ID string for land ice. If present, max root depth is set to zero on land ice points.
23392397

mip_convert/mip_convert/plugins/hadgem3_gc5/data/HadGEM3GC5_mappings.cfg

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2018-2025, Met Office.
1+
# (C) British Crown Copyright 2018-2026, Met Office.
22
# Please see LICENSE.md for license details.
33
#
44
# This 'model to MIP mappings' configuration file contains sections
@@ -42,8 +42,14 @@ expression = m01s03i210[lbproc=128]
4242
mip_table_id = atmos
4343
units = m s-1
4444

45+
[slthick_ti-sl-hxy-lnd]
46+
dimension = longitude latitude sdepth
47+
expression = calc_slthick(m01s00i009, m01s00i216, ice_class='ice')
48+
mip_table_id = land
49+
units = m
50+
4551
[rootd_ti-u-hxy-lnd]
4652
dimension = longitude latitude
4753
expression = calc_rootd(m01s00i009, m01s00i216, ice_class='ice')
4854
mip_table_id = land
49-
units = m
55+
units = m
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# (C) British Crown Copyright 2022-2026, Met Office.
2+
# Please see LICENSE.md for license details.
3+
import os
4+
5+
import pytest
6+
7+
from mip_convert.tests.test_functional.test_command_line import AbstractFunctionalTests
8+
from mip_convert.tests.test_functional.utils.configurations import Cmip7TestData, SpecificInfo
9+
from mip_convert.tests.test_functional.utils.directories import (get_cmor_log, get_output_dir,
10+
MODEL_OUTPUT_DIR,
11+
ROOT_OUTPUT_CASES_DIR)
12+
13+
14+
class TestCmip7_fx_slthick_ti_sl_hxy_lnd(AbstractFunctionalTests):
15+
16+
def get_test_data(self):
17+
test_location = os.path.join(ROOT_OUTPUT_CASES_DIR, 'test_CMIP7_land_slthick_ti-sl-hxy-lnd')
18+
return Cmip7TestData(
19+
mip_table='land',
20+
variables=['slthick_ti-sl-hxy-lnd'],
21+
specific_info=SpecificInfo(
22+
common={
23+
'test_location': test_location
24+
},
25+
cmor_setup={
26+
'cmor_log_file': get_cmor_log(test_location)
27+
},
28+
cmor_dataset={
29+
'output_dir': get_output_dir(test_location)
30+
},
31+
request={
32+
'ancil_files': ' '.join([
33+
os.path.join(MODEL_OUTPUT_DIR, 'u-dk469', 'ancil', 'test_input_land_fx_u-dk469.pp'),
34+
]),
35+
'model_output_dir': MODEL_OUTPUT_DIR,
36+
'run_bounds': '2257-01-01T00:00:00 2257-02-01T00:00:00',
37+
'suite_id': 'u-dk469',
38+
'mip_convert_plugin': 'HadGEM3GC5'
39+
},
40+
streams={
41+
'ancil': {'CMIP7_land@fx': 'slthick_ti-sl-hxy-lnd'}
42+
},
43+
other={
44+
'reference_version': 'v1',
45+
'filenames': ['slthick_ti-sl-hxy-lnd_fx_glb_gn_PCMDI-test-1-0_1pctCO2_r1i1dp1f1.nc'],
46+
'ignore_history': True,
47+
}
48+
)
49+
)
50+
51+
@pytest.mark.slow
52+
def test_cmip7_efx_slthick(self):
53+
self.maxDiff = True
54+
self.check_convert(plugin_id="CMIP7")

mip_convert/mip_convert/tests/test_functional/utils/directories.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2022-2025, Met Office.
1+
# (C) British Crown Copyright 2022-2026, Met Office.
22
# Please see LICENSE.md for license details.
33
import os
44

@@ -17,7 +17,7 @@
1717
CORDEX_MIP_TABLE_DIR = '{}/CORDEX/cordex-cmip6-cmor-tables/Tables'.format(ROOT_MIP_TABLES_DIR)
1818
ARISE_MIP_TABLE_DIR = '{}/ARISE/for_functional_tests'.format(ROOT_MIP_TABLES_DIR)
1919
CMIP6_MIP_TABLE_DIR = '{}/CMIP6/for_functional_tests'.format(ROOT_MIP_TABLES_DIR)
20-
CMIP7_MIP_TABLE_DIR = '{}/CMIP7/DR-1.2.2.2-v1.0.1'.format(ROOT_MIP_TABLES_DIR)
20+
CMIP7_MIP_TABLE_DIR = '{}/CMIP7/for_functional_tests'.format(ROOT_MIP_TABLES_DIR)
2121
SEASONAL_MIP_TABLE_DIR = '{}/SEASONAL/for_functional_tests'.format(ROOT_MIP_TABLES_DIR)
2222
NAHOSMIP_MIP_TABLE_DIR = '{}/GCModelDev/for_functional_tests'.format(ROOT_MIP_TABLES_DIR)
2323

mip_convert/mip_convert/tests/test_process/test_processors.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2016-2025, Met Office.
1+
# (C) British Crown Copyright 2016-2026, Met Office.
22
# Please see LICENSE.md for license details.
33
# pylint: disable = missing-docstring, invalid-name, too-many-public-methods
44
# pylint: disable = no-member, no-value-for-parameter
@@ -16,12 +16,9 @@
1616
import numpy as np
1717

1818
from mip_convert.plugins.base.data.processors import (
19-
area_mean, areacella, calc_rho_mean, calc_zostoga,
20-
combine_cubes_to_basin_coord, eos_insitu, fix_clmisr_height,
21-
land_class_area, land_class_mean, level_sum, mask_copy,
22-
mask_zeros, mask_polar_column_zonal_means,
23-
mon_mean_from_day,
24-
ocean_quasi_barotropic_streamfunc, tile_ids_for_class, volcello, vortmean,
19+
area_mean, areacella, calc_rho_mean, calc_zostoga, combine_cubes_to_basin_coord, eos_insitu, fix_clmisr_height,
20+
land_class_area, land_class_mean, level_sum, mask_copy, mask_zeros, mask_polar_column_zonal_means,
21+
mon_mean_from_day, ocean_quasi_barotropic_streamfunc, tile_ids_for_class, volcello, vortmean,
2522
annual_from_monthly_2d, annual_from_monthly_3d, calculate_thkcello_weights, check_data_is_monthly)
2623
from mip_convert.tests.common import dummy_cube
2724
from functools import reduce

0 commit comments

Comments
 (0)