diff --git a/docs/src/further_topics/um_files_loading.rst b/docs/src/further_topics/um_files_loading.rst index c5238e6b70..2d2eb973e4 100644 --- a/docs/src/further_topics/um_files_loading.rst +++ b/docs/src/further_topics/um_files_loading.rst @@ -315,6 +315,12 @@ the derived ``altitude``. it produces basic coordinates 'model_level_number', 'sigma' and 'level_pressure', and a manufactured 3D 'air_pressure' coordinate. +**Surface Fields** + +In order for surface fields to be recognised when saving, you must include +`label_surface_fields=True` to :func:`iris.fileformats.pp.save` or +:func:`iris.save`. When surface fields are encountered with this flag set to True, +LBLEV will be set to 9999 and LBVC to 129. .. _um_time_metadata: diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index edc6ebfe2d..c862ef192e 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -50,6 +50,11 @@ This document explains the changes made to Iris for this release references are split across multiple input fields, and :meth:`~iris.LOAD_POLICY` to control it, as requested in :issue:`5369`, actioned in :pull:`6168`. +#. `@ESadek-MO`_ has updated :mod:`iris.fileformats.pp_save_rules` and + :mod:`iris.fileformats.pp` to include the `label_surface_fields` flag across + relevant functions, most notably :func:`iris.fileformats.pp.save`. + This allows the user to choose whether or not surface fields are recognised + and handled appropriately. (:issue:`3280`, :pull:`5734`) 🐛 Bugs Fixed ============= @@ -116,6 +121,10 @@ This document explains the changes made to Iris for this release #. `@bouweandela`_ added type hints for :class:`~iris.cube.Cube`. (:pull:`6037`) +#. `@ESadek-MO`_ has updated :ref:`um_files_loading` to include a short description + of the new `label_surface_fields` functionality. (:pull:`5734`) + + 💼 Internal =========== diff --git a/lib/iris/fileformats/pp.py b/lib/iris/fileformats/pp.py index ce92d4456e..3b38304f00 100644 --- a/lib/iris/fileformats/pp.py +++ b/lib/iris/fileformats/pp.py @@ -2171,7 +2171,7 @@ def _load_cubes_variable_loader( return result -def save(cube, target, append=False, field_coords=None): +def save(cube, target, append=False, field_coords=None, label_surface_fields=False): """Use the PP saving rules (and any user rules) to save a cube to a PP file. Parameters @@ -2192,6 +2192,11 @@ def save(cube, target, append=False, field_coords=None): coordinates of the resulting fields. If None, the final two dimensions are chosen for slicing. + label_surface_fields : bool, default=False + Whether you wish pp_save_rules to recognise surface fields or not. + When true, if surface fields are encountered, LBLEV will be set to 9999 + and LBVC to 129. + Default is False. Notes ----- @@ -2200,11 +2205,11 @@ def save(cube, target, append=False, field_coords=None): of cubes to be saved to a PP file. """ - fields = as_fields(cube, field_coords) + fields = as_fields(cube, field_coords, label_surface_fields=label_surface_fields) save_fields(fields, target, append=append) -def save_pairs_from_cube(cube, field_coords=None): +def save_pairs_from_cube(cube, field_coords=None, label_surface_fields=False): """Use the PP saving rules to generate (2D cube, PP field) pairs from a cube. Parameters @@ -2316,12 +2321,12 @@ def save_pairs_from_cube(cube, field_coords=None): # Run the PP save rules on the slice2D, to fill the PPField, # recording the rules that were used - pp_field = verify(slice2D, pp_field) + pp_field = verify(slice2D, pp_field, label_surface_fields=label_surface_fields) yield (slice2D, pp_field) -def as_fields(cube, field_coords=None): +def as_fields(cube, field_coords=None, label_surface_fields=False): """Use the PP saving rules to convert a cube to an iterable of PP fields. Use the PP saving rules (and any user rules) to convert a cube to @@ -2335,9 +2340,19 @@ def as_fields(cube, field_coords=None): reducing the given cube into 2d slices, which will ultimately determine the x and y coordinates of the resulting fields. If None, the final two dimensions are chosen for slicing. + label_surface_fields : bool, default=False + Whether you wish pp_save_rules to recognise surface fields or not. + When true, if surface fields are encountered, LBLEV will be set to 9999 + and LBVC to 129. + Default is False. """ - return (field for _, field in save_pairs_from_cube(cube, field_coords=field_coords)) + return ( + field + for _, field in save_pairs_from_cube( + cube, field_coords=field_coords, label_surface_fields=label_surface_fields + ) + ) def save_fields(fields, target, append: bool = False): diff --git a/lib/iris/fileformats/pp_save_rules.py b/lib/iris/fileformats/pp_save_rules.py index b8e95d2160..b156260f72 100644 --- a/lib/iris/fileformats/pp_save_rules.py +++ b/lib/iris/fileformats/pp_save_rules.py @@ -663,7 +663,7 @@ def _lbproc_rules(cube, pp): return pp -def _vertical_rules(cube, pp): +def _vertical_rules(cube, pp, label_surface_fields=False): """Rule for setting vertical levels for the PP field. Parameters @@ -773,6 +773,22 @@ def _vertical_rules(cube, pp): pp.brsvd[0] = depth_coord.bounds[0, 0] pp.brlev = depth_coord.bounds[0, 1] + # Surface field. + if ( + height_coord is None + and depth_coord is None + and pressure_coord is None + and soil_mln_coord is None + and apt_coord is None + and air_pres_coord is None + and level_height_coord is None + and mln_coord is None + and sigma_coord is None + and label_surface_fields + ): + pp.lbvc = 129 + pp.lblev = 9999 + # Single potential-temperature level. if ( apt_coord is not None @@ -883,7 +899,7 @@ def _all_other_rules(cube, pp): return pp -def verify(cube, field): +def verify(cube, field, label_surface_fields=False): # Rules functions. field = _basic_coord_system_rules(cube, field) field = _um_version_rules(cube, field) @@ -893,7 +909,7 @@ def verify(cube, field): field = _grid_and_pole_rules(cube, field) field = _non_std_cross_section_rules(cube, field) field = _lbproc_rules(cube, field) - field = _vertical_rules(cube, field) + field = _vertical_rules(cube, field, label_surface_fields=label_surface_fields) field = _all_other_rules(cube, field) return field diff --git a/lib/iris/tests/test_cube_to_pp.py b/lib/iris/tests/test_cube_to_pp.py index 6ae4567f49..fa06ba553f 100644 --- a/lib/iris/tests/test_cube_to_pp.py +++ b/lib/iris/tests/test_cube_to_pp.py @@ -370,6 +370,31 @@ def test_lbvc(self): self.assertEqual(field.lblev, lblev) self.assertEqual(field.blev, blev) + def test_surface_field(self): + def setup_cube(coord=None): + cube = stock.lat_lon_cube() + if coord: + cube.add_aux_coord(coord) + temp_pp_path = iris.util.create_temp_filename(".pp") + iris.fileformats.pp.save( + cube, target=temp_pp_path, label_surface_fields=True + ) + cube = iris.fileformats.pp.load(temp_pp_path) + return cube + + # check surface fields are correctly applied + cube = setup_cube() + for field in cube: + self.assertEqual(field.lbvc, 129) + self.assertEqual(field.lblev, 9999) + + # check surface fields aren't incorrectly applied + v_coord = iris.coords.DimCoord(standard_name="depth", units="m", points=[-5]) + cube = setup_cube(v_coord) + for field in cube: + self.assertNotEqual(field.lbvc, 129) + self.assertNotEqual(field.lblev, 9999) + def fields_from_cube(cubes): """Return an iterator of PP fields generated from saving the given cube(s)