From a286451ce7e69889c0ddb4b9a77e1bf7b5b28fb3 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Tue, 10 May 2022 16:49:27 +0200 Subject: [PATCH 1/4] Added special case for unit conversion of pr --- doc/conf.py | 1 + doc/recipe/preprocessor.rst | 38 ++++++++---- esmvalcore/preprocessor/_units.py | 62 ++++++++++++++++++- .../preprocessor/_units/test_convert_units.py | 50 +++++++++++++++ 4 files changed, 138 insertions(+), 13 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index c5da062688..7769667c74 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -416,6 +416,7 @@ # Configuration for intersphinx intersphinx_mapping = { + 'cf_units': ('https://cf-units.readthedocs.io/en/latest/', None), 'esmvalcore': (f'https://docs.esmvaltool.org/projects/esmvalcore/en/{rtd_version}/', None), diff --git a/doc/recipe/preprocessor.rst b/doc/recipe/preprocessor.rst index fe7808642c..c12381bc64 100644 --- a/doc/recipe/preprocessor.rst +++ b/doc/recipe/preprocessor.rst @@ -1921,20 +1921,36 @@ See also :func:`esmvalcore.preprocessor.detrend`. Unit conversion =============== -Converting units is also supported. This is particularly useful in -cases where different datasets might have different units, for example -when comparing CMIP5 and CMIP6 variables where the units have changed -or in case of observational datasets that are delivered in different -units. - +Converting units is also supported. +This is particularly useful in cases where different datasets might have +different units, for example when comparing CMIP5 and CMIP6 variables where the +units have changed or in case of observational datasets that are delivered in +different units. In these cases, having a unit conversion at the end of the processing will guarantee homogeneous input for the diagnostics. -.. note:: - Conversion is only supported between compatible units! In other - words, converting temperature units from ``degC`` to ``Kelvin`` works - fine, changing precipitation units from a rate based unit to an - amount based unit is not supported at the moment. +Conversion is only supported between compatible units! +In other words, converting temperature units from ``degC`` to ``Kelvin`` works +fine, while changing units from ``kg`` to ``m`` will not work. + +However, there are some well-defined exceptions from this rule for specific +variables (i.e., the ``short_name`` of the corresponding data needs to match +the ones given in the table below or the pattern given in brackets needs to be +present in the data's ``standard_name`` or ``long_name``): + +==================== ==================== ================= ================= +Variable Expected Expected source Expected target + ``short_name`` units units + (or pattern) +==================== ==================== ================= ================= +Precipitation flux ``pr`` ``kg m-2 s-1`` ``mm day-1`` + (``precipitation``) +==================== ==================== ================= ================= + + +.. hint:: + Source or target units convertible to the ones given in the table above are + also supported. See also :func:`esmvalcore.preprocessor.convert_units`. diff --git a/esmvalcore/preprocessor/_units.py b/esmvalcore/preprocessor/_units.py index dbcaa87cc9..bae39fe742 100644 --- a/esmvalcore/preprocessor/_units.py +++ b/esmvalcore/preprocessor/_units.py @@ -5,6 +5,8 @@ """ import logging +from cf_units import Unit + logger = logging.getLogger(__name__) @@ -14,11 +16,23 @@ def convert_units(cube, units): This converts units of a cube. + Note + ---- + Allows some special unit conversions which are usually not allowed by + :mod:`cf_units` for certain variables (based on matching ``var_name`` or + patterns in ``standard_name`` or ``long_name``, see brackets below). Note + that any other units convertible to the ones given below also work, e.g., + for precipitation ``[kg m-2 yr-1]`` --> ``[m s-1]`` would also work: + + - Variable ``pr`` (``precipitation``): ``[kg m-2 s-1]`` --> ``[mm + day-1]`` + + For precipitation variables, a water density of 1000 kg m-3 is assumed. + Arguments --------- cube: iris.cube.Cube input cube - units: str new units in udunits form @@ -26,6 +40,50 @@ def convert_units(cube, units): ------- iris.cube.Cube converted cube. + """ - cube.convert_units(units) + # Dictionary containing special cases + # Dictionary keys: (expected var_name, expected pattern which needs to + # appear in standard_name or long_name) + # Dictionary values: (special source units, special target units) --> both + # need to be "identical", e.g., 1 kg m-2 s-1 "equals" 1 + # mm s-1 for precipitation + special_cases = { + ('pr', 'precipitation'): ('kg m-2 s-1', 'mm s-1'), + } + try: + cube.convert_units(units) + except ValueError: + var_name = '' if cube.var_name is None else cube.var_name + std_name = '' if cube.standard_name is None else cube.standard_name + long_name = '' if cube.long_name is None else cube.long_name + for (special_names, special_units) in special_cases.items(): + # Special unit conversion only works if all of the following + # criteria are met: + # - the units in the cubes are convertible to the special source + # units + # - the target units given by the user are convertible to the + # special target units + # - the cube has the correct names, i.e., either the cube's + # var_name matches the expected var_name or the expected pattern + # appears in the cube's standard_name or long_name + is_special_case = ( + cube.units.is_convertible(special_units[0]) and + Unit(units).is_convertible(special_units[1]) and + any([ + var_name == special_names[0], + special_names[1] in std_name.lower(), + special_names[1] in long_name.lower(), + ]) + ) + if is_special_case: + cube.convert_units(special_units[0]) + cube.units = special_units[1] + cube.convert_units(units) + break + + # If no special case has been detected, raise the original error + else: + raise + return cube diff --git a/tests/unit/preprocessor/_units/test_convert_units.py b/tests/unit/preprocessor/_units/test_convert_units.py index bceb52550f..5e28e8a2ba 100644 --- a/tests/unit/preprocessor/_units/test_convert_units.py +++ b/tests/unit/preprocessor/_units/test_convert_units.py @@ -4,6 +4,7 @@ import cf_units import iris +import iris.fileformats import numpy as np import tests @@ -43,6 +44,55 @@ def test_convert_compatible_units(self): self.assertEqual(result.units, expected_units) self.assert_array_equal(result.data, expected_data) + def test_convert_special_pr_from_var_name(self): + """Test special conversion of pr.""" + self.arr.var_name = 'pr' + self.arr.units = 'kg m-2 s-1' + result = convert_units(self.arr, 'mm day-1') + self.assertEqual(result.units, 'mm day-1') + np.testing.assert_allclose( + result.data, + [[0.0, 86400.0], [172800.0, 259200.0]], + ) + + def test_convert_special_pr_from_standard_name(self): + """Test special conversion of pr.""" + self.arr.standard_name = 'precipitation_flux' + self.arr.units = 'g m-2 s-1' + result = convert_units(self.arr, 'mm day-1') + self.assertEqual(result.units, 'mm day-1') + np.testing.assert_allclose( + result.data, + [[0.0, 86.4], [172.8, 259.200]], + ) + + def test_convert_special_pr_from_long_name(self): + """Test special conversion of pr.""" + self.arr.long_name = 'Convective Precipitation Flux' + self.arr.units = 'g m-2 yr-1' + result = convert_units(self.arr, 'm yr-1') + self.assertEqual(result.units, 'm yr-1') + np.testing.assert_allclose( + result.data, + [[0.0, 1.0e-6], [2.0e-6, 3.0e-6]], + ) + + def test_convert_special_pr_fail_invalid_name(self): + """Test special conversion of pr.""" + self.arr.units = 'kg m-2 s-1' + self.assertRaises(ValueError, convert_units, self.arr, 'mm day-1') + + def test_convert_special_pr_fail_invalid_source_units(self): + """Test special conversion of pr.""" + self.arr.var_name = 'pr' + self.assertRaises(ValueError, convert_units, self.arr, 'mm day-1') + + def test_convert_special_pr_fail_invalid_target_units(self): + """Test special conversion of pr.""" + self.arr.var_name = 'pr' + self.arr.units = 'kg m-2 s-1' + self.assertRaises(ValueError, convert_units, self.arr, 'K') + if __name__ == '__main__': unittest.main() From 4fb63019b018bf6df37f7bd511f436d6fc4b1d56 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Wed, 11 May 2022 15:57:41 +0200 Subject: [PATCH 2/4] Only allowed special unit conversion when standard_name matches pre-defined cases --- doc/recipe/preprocessor.rst | 22 ++++---- esmvalcore/preprocessor/_units.py | 54 +++++++++---------- .../preprocessor/_units/test_convert_units.py | 29 ++++------ 3 files changed, 43 insertions(+), 62 deletions(-) diff --git a/doc/recipe/preprocessor.rst b/doc/recipe/preprocessor.rst index c12381bc64..18cce6b1f5 100644 --- a/doc/recipe/preprocessor.rst +++ b/doc/recipe/preprocessor.rst @@ -1934,19 +1934,15 @@ In other words, converting temperature units from ``degC`` to ``Kelvin`` works fine, while changing units from ``kg`` to ``m`` will not work. However, there are some well-defined exceptions from this rule for specific -variables (i.e., the ``short_name`` of the corresponding data needs to match -the ones given in the table below or the pattern given in brackets needs to be -present in the data's ``standard_name`` or ``long_name``): - -==================== ==================== ================= ================= -Variable Expected Expected source Expected target - ``short_name`` units units - (or pattern) -==================== ==================== ================= ================= -Precipitation flux ``pr`` ``kg m-2 s-1`` ``mm day-1`` - (``precipitation``) -==================== ==================== ================= ================= - +variables (defined by their ``standard_name``). +In these cases, the data's ``standard_name`` is also adapted so that it is +consistent with the CF conventions. + +=========================== ========================== ====================== ====================== +Expected ``standard_name`` Output ``standard_name`` Expected source units Expected source units +=========================== ========================== ====================== ====================== +``precipitation_flux`` ``lwe_precipitation_rate`` ``kg m-2 s-1`` ``mm day-1`` +=========================== ========================== ====================== ====================== .. hint:: Source or target units convertible to the ones given in the table above are diff --git a/esmvalcore/preprocessor/_units.py b/esmvalcore/preprocessor/_units.py index bae39fe742..43fd8927c8 100644 --- a/esmvalcore/preprocessor/_units.py +++ b/esmvalcore/preprocessor/_units.py @@ -18,23 +18,25 @@ def convert_units(cube, units): Note ---- - Allows some special unit conversions which are usually not allowed by - :mod:`cf_units` for certain variables (based on matching ``var_name`` or - patterns in ``standard_name`` or ``long_name``, see brackets below). Note + Allows special unit conversions which are usually not allowed by + :mod:`cf_units` for certain variables (based on matching + ``standard_name``). This also adapts the ``standard_name`` of the data so + that is consistent with the CF conventions (names in brackets below). Note that any other units convertible to the ones given below also work, e.g., - for precipitation ``[kg m-2 yr-1]`` --> ``[m s-1]`` would also work: + for ``precipitation_flux``, ``[kg m-2 yr-1]`` --> ``[m s-1]`` would also + work. - - Variable ``pr`` (``precipitation``): ``[kg m-2 s-1]`` --> ``[mm - day-1]`` + - ``precipitation_flux`` (--> ``lwe_precipitation_rate``): ``[kg m-2 + s-1]`` --> ``[mm day-1]`` For precipitation variables, a water density of 1000 kg m-3 is assumed. Arguments --------- - cube: iris.cube.Cube - input cube - units: str - new units in udunits form + cube: iris.cube.Cube + input cube + units: str + new units in udunits form Returns ------- @@ -43,40 +45,32 @@ def convert_units(cube, units): """ # Dictionary containing special cases - # Dictionary keys: (expected var_name, expected pattern which needs to - # appear in standard_name or long_name) + # Dictionary keys: (expected standard_name, target standard_name) # Dictionary values: (special source units, special target units) --> both # need to be "identical", e.g., 1 kg m-2 s-1 "equals" 1 # mm s-1 for precipitation special_cases = { - ('pr', 'precipitation'): ('kg m-2 s-1', 'mm s-1'), + ('precipitation_flux', 'lwe_precipitation_rate'): ('kg m-2 s-1', + 'mm s-1'), } try: cube.convert_units(units) except ValueError: - var_name = '' if cube.var_name is None else cube.var_name - std_name = '' if cube.standard_name is None else cube.standard_name - long_name = '' if cube.long_name is None else cube.long_name for (special_names, special_units) in special_cases.items(): # Special unit conversion only works if all of the following # criteria are met: - # - the units in the cubes are convertible to the special source + # - the units in the cube are convertible to the special source # units - # - the target units given by the user are convertible to the + # - the target units desired by the user are convertible to the # special target units - # - the cube has the correct names, i.e., either the cube's - # var_name matches the expected var_name or the expected pattern - # appears in the cube's standard_name or long_name - is_special_case = ( - cube.units.is_convertible(special_units[0]) and - Unit(units).is_convertible(special_units[1]) and - any([ - var_name == special_names[0], - special_names[1] in std_name.lower(), - special_names[1] in long_name.lower(), - ]) - ) + # - the cube has the correct standard_name + is_special_case = all([ + cube.units.is_convertible(special_units[0]), + Unit(units).is_convertible(special_units[1]), + cube.standard_name == special_names[0], + ]) if is_special_case: + cube.standard_name = special_names[1] cube.convert_units(special_units[0]) cube.units = special_units[1] cube.convert_units(units) diff --git a/tests/unit/preprocessor/_units/test_convert_units.py b/tests/unit/preprocessor/_units/test_convert_units.py index 5e28e8a2ba..99e9f2d2e5 100644 --- a/tests/unit/preprocessor/_units/test_convert_units.py +++ b/tests/unit/preprocessor/_units/test_convert_units.py @@ -44,52 +44,43 @@ def test_convert_compatible_units(self): self.assertEqual(result.units, expected_units) self.assert_array_equal(result.data, expected_data) - def test_convert_special_pr_from_var_name(self): - """Test special conversion of pr.""" - self.arr.var_name = 'pr' + def test_convert_precipitation_flux(self): + """Test special conversion of precipitation_flux.""" + self.arr.standard_name = 'precipitation_flux' self.arr.units = 'kg m-2 s-1' result = convert_units(self.arr, 'mm day-1') + self.assertEqual(result.standard_name, 'lwe_precipitation_rate') self.assertEqual(result.units, 'mm day-1') np.testing.assert_allclose( result.data, [[0.0, 86400.0], [172800.0, 259200.0]], ) - def test_convert_special_pr_from_standard_name(self): + def test_convert_precipitation_flux_convertible(self): """Test special conversion of pr.""" self.arr.standard_name = 'precipitation_flux' - self.arr.units = 'g m-2 s-1' - result = convert_units(self.arr, 'mm day-1') - self.assertEqual(result.units, 'mm day-1') - np.testing.assert_allclose( - result.data, - [[0.0, 86.4], [172.8, 259.200]], - ) - - def test_convert_special_pr_from_long_name(self): - """Test special conversion of pr.""" - self.arr.long_name = 'Convective Precipitation Flux' self.arr.units = 'g m-2 yr-1' result = convert_units(self.arr, 'm yr-1') + self.assertEqual(result.standard_name, 'lwe_precipitation_rate') self.assertEqual(result.units, 'm yr-1') np.testing.assert_allclose( result.data, [[0.0, 1.0e-6], [2.0e-6, 3.0e-6]], ) - def test_convert_special_pr_fail_invalid_name(self): + def test_convert_precipitation_flux_fail_invalid_name(self): """Test special conversion of pr.""" self.arr.units = 'kg m-2 s-1' self.assertRaises(ValueError, convert_units, self.arr, 'mm day-1') - def test_convert_special_pr_fail_invalid_source_units(self): + def test_convert_precipitation_flux_fail_invalid_source_units(self): """Test special conversion of pr.""" - self.arr.var_name = 'pr' + self.arr.standard_name = 'precipitation_flux' self.assertRaises(ValueError, convert_units, self.arr, 'mm day-1') def test_convert_special_pr_fail_invalid_target_units(self): """Test special conversion of pr.""" - self.arr.var_name = 'pr' + self.arr.standard_name = 'precipitation_flux' self.arr.units = 'kg m-2 s-1' self.assertRaises(ValueError, convert_units, self.arr, 'K') From 465435e5927bbe696fb6788e191624252713a1e6 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Wed, 11 May 2022 16:51:23 +0200 Subject: [PATCH 3/4] Fixed docstring of test --- tests/unit/preprocessor/_units/test_convert_units.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/preprocessor/_units/test_convert_units.py b/tests/unit/preprocessor/_units/test_convert_units.py index 99e9f2d2e5..e06050c86c 100644 --- a/tests/unit/preprocessor/_units/test_convert_units.py +++ b/tests/unit/preprocessor/_units/test_convert_units.py @@ -57,7 +57,7 @@ def test_convert_precipitation_flux(self): ) def test_convert_precipitation_flux_convertible(self): - """Test special conversion of pr.""" + """Test special conversion of precipitation_flux.""" self.arr.standard_name = 'precipitation_flux' self.arr.units = 'g m-2 yr-1' result = convert_units(self.arr, 'm yr-1') @@ -69,17 +69,17 @@ def test_convert_precipitation_flux_convertible(self): ) def test_convert_precipitation_flux_fail_invalid_name(self): - """Test special conversion of pr.""" + """Test special conversion of precipitation_flux.""" self.arr.units = 'kg m-2 s-1' self.assertRaises(ValueError, convert_units, self.arr, 'mm day-1') def test_convert_precipitation_flux_fail_invalid_source_units(self): - """Test special conversion of pr.""" + """Test special conversion of precipitation_flux.""" self.arr.standard_name = 'precipitation_flux' self.assertRaises(ValueError, convert_units, self.arr, 'mm day-1') def test_convert_special_pr_fail_invalid_target_units(self): - """Test special conversion of pr.""" + """Test special conversion of precipitation_flux.""" self.arr.standard_name = 'precipitation_flux' self.arr.units = 'kg m-2 s-1' self.assertRaises(ValueError, convert_units, self.arr, 'K') From a9be71c8718244dde374274629f2d42b55dc8ab3 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Fri, 13 May 2022 15:12:30 +0200 Subject: [PATCH 4/4] Suggestions from code review --- doc/recipe/preprocessor.rst | 33 +++-- esmvalcore/preprocessor/_units.py | 117 +++++++++++------- .../preprocessor/_units/test_convert_units.py | 42 ++++++- 3 files changed, 136 insertions(+), 56 deletions(-) diff --git a/doc/recipe/preprocessor.rst b/doc/recipe/preprocessor.rst index 18cce6b1f5..3d3b7f310d 100644 --- a/doc/recipe/preprocessor.rst +++ b/doc/recipe/preprocessor.rst @@ -1933,20 +1933,29 @@ Conversion is only supported between compatible units! In other words, converting temperature units from ``degC`` to ``Kelvin`` works fine, while changing units from ``kg`` to ``m`` will not work. -However, there are some well-defined exceptions from this rule for specific -variables (defined by their ``standard_name``). -In these cases, the data's ``standard_name`` is also adapted so that it is -consistent with the CF conventions. - -=========================== ========================== ====================== ====================== -Expected ``standard_name`` Output ``standard_name`` Expected source units Expected source units -=========================== ========================== ====================== ====================== -``precipitation_flux`` ``lwe_precipitation_rate`` ``kg m-2 s-1`` ``mm day-1`` -=========================== ========================== ====================== ====================== +However, there are some well-defined exceptions from this rule in order to +transform one quantity to another (physically related) quantity. +These quantities are identified via their ``standard_name`` and their ``units`` +(units convertible to the ones defined are also supported). +For example, this enables conversions between precipitation fluxes measured in +``kg m-2 s-1`` and precipitation rates measured in ``mm day-1`` (and vice +versa). +Currently, the following special conversions are supported: + +* ``precipitation_flux`` (``kg m-2 s-1``) -- + ``lwe_precipitation_rate`` (``mm day-1``) .. hint:: - Source or target units convertible to the ones given in the table above are - also supported. + Names in the list correspond to ``standard_names`` of the input data. + Conversions are allowed from each quantity to any other quantity given in a + bullet point. + The corresponding target quantity is inferred from the desired target units. + In addition, any other units convertible to the ones given are also + supported (e.g., instead of ``mm day-1``, ``m s-1`` is also supported). + +.. note:: + For the transformation between the different precipitation variables, a + water density of ``1000 kg m-3`` is assumed. See also :func:`esmvalcore.preprocessor.convert_units`. diff --git a/esmvalcore/preprocessor/_units.py b/esmvalcore/preprocessor/_units.py index 43fd8927c8..1a14944676 100644 --- a/esmvalcore/preprocessor/_units.py +++ b/esmvalcore/preprocessor/_units.py @@ -10,6 +10,58 @@ logger = logging.getLogger(__name__) +# List containing special cases for convert_units. Each list item is another +# list. Each of these sublists defines one special conversion. Each element in +# the sublists is a tuple (standard_name, units). Note: All units for a single +# special case need to be "physically identical", e.g., 1 kg m-2 s-1 "equals" 1 +# mm s-1 for precipitation +SPECIAL_CASES = [ + [ + ('precipitation_flux', 'kg m-2 s-1'), + ('lwe_precipitation_rate', 'mm s-1'), + ], +] + + +def _try_special_conversions(cube, units): + """Try special conversion.""" + for special_case in SPECIAL_CASES: + for (std_name, special_units) in special_case: + # Special unit conversion only works if all of the following + # criteria are met: + # - the cube's standard_name is one of the supported + # standard_names + # - the cube's units are convertible to the ones defined for + # that given standard_name + # - the desired target units are convertible to the units of + # one of the other standard_names in that special case + + # Step 1: find suitable source name and units + if (cube.standard_name == std_name and + cube.units.is_convertible(special_units)): + for (target_std_name, target_units) in special_case: + if target_std_name == std_name: + continue + + # Step 2: find suitable target name and units + if Unit(units).is_convertible(target_units): + cube.standard_name = target_std_name + + # In order to avoid two calls to cube.convert_units, + # determine the conversion factor between the cube's + # units and the source units first and simply add this + # factor to the target units (remember that the source + # units and the target units should be "physically + # identical"). + factor = cube.units.convert(1.0, special_units) + cube.units = f"{factor} {target_units}" + cube.convert_units(units) + return True + + # If no special case has been detected, return False + return False + + def convert_units(cube, units): """ Convert the units of a cube to new ones. @@ -18,25 +70,34 @@ def convert_units(cube, units): Note ---- - Allows special unit conversions which are usually not allowed by - :mod:`cf_units` for certain variables (based on matching - ``standard_name``). This also adapts the ``standard_name`` of the data so - that is consistent with the CF conventions (names in brackets below). Note - that any other units convertible to the ones given below also work, e.g., - for ``precipitation_flux``, ``[kg m-2 yr-1]`` --> ``[m s-1]`` would also - work. + Allows special unit conversions which transforms one quantity to another + (physically related) quantity. These quantities are identified via their + ``standard_name`` and their ``units`` (units convertible to the ones + defined are also supported). For example, this enables conversions between + precipitation fluxes measured in ``kg m-2 s-1`` and precipitation rates + measured in ``mm day-1`` (and vice versa). + + Currently, the following special conversions are supported: - - ``precipitation_flux`` (--> ``lwe_precipitation_rate``): ``[kg m-2 - s-1]`` --> ``[mm day-1]`` + * ``precipitation_flux`` (``kg m-2 s-1``) -- + ``lwe_precipitation_rate`` (``mm day-1``) - For precipitation variables, a water density of 1000 kg m-3 is assumed. + Names in the list correspond to ``standard_names`` of the input data. + Conversions are allowed from each quantity to any other quantity given in a + bullet point. The corresponding target quantity is inferred from the + desired target units. In addition, any other units convertible to the ones + given are also supported (e.g., instead of ``mm day-1``, ``m s-1`` is also + supported). + + Note that for precipitation variables, a water density of ``1000 kg m-3`` + is assumed. Arguments --------- cube: iris.cube.Cube - input cube + Input cube. units: str - new units in udunits form + New units in udunits form. Returns ------- @@ -44,40 +105,10 @@ def convert_units(cube, units): converted cube. """ - # Dictionary containing special cases - # Dictionary keys: (expected standard_name, target standard_name) - # Dictionary values: (special source units, special target units) --> both - # need to be "identical", e.g., 1 kg m-2 s-1 "equals" 1 - # mm s-1 for precipitation - special_cases = { - ('precipitation_flux', 'lwe_precipitation_rate'): ('kg m-2 s-1', - 'mm s-1'), - } try: cube.convert_units(units) except ValueError: - for (special_names, special_units) in special_cases.items(): - # Special unit conversion only works if all of the following - # criteria are met: - # - the units in the cube are convertible to the special source - # units - # - the target units desired by the user are convertible to the - # special target units - # - the cube has the correct standard_name - is_special_case = all([ - cube.units.is_convertible(special_units[0]), - Unit(units).is_convertible(special_units[1]), - cube.standard_name == special_names[0], - ]) - if is_special_case: - cube.standard_name = special_names[1] - cube.convert_units(special_units[0]) - cube.units = special_units[1] - cube.convert_units(units) - break - - # If no special case has been detected, raise the original error - else: + if not _try_special_conversions(cube, units): raise return cube diff --git a/tests/unit/preprocessor/_units/test_convert_units.py b/tests/unit/preprocessor/_units/test_convert_units.py index e06050c86c..875100d1b5 100644 --- a/tests/unit/preprocessor/_units/test_convert_units.py +++ b/tests/unit/preprocessor/_units/test_convert_units.py @@ -78,12 +78,52 @@ def test_convert_precipitation_flux_fail_invalid_source_units(self): self.arr.standard_name = 'precipitation_flux' self.assertRaises(ValueError, convert_units, self.arr, 'mm day-1') - def test_convert_special_pr_fail_invalid_target_units(self): + def test_convert_precipitation_flux_fail_invalid_target_units(self): """Test special conversion of precipitation_flux.""" self.arr.standard_name = 'precipitation_flux' self.arr.units = 'kg m-2 s-1' self.assertRaises(ValueError, convert_units, self.arr, 'K') + def test_convert_lwe_precipitation_rate(self): + """Test special conversion of lwe_precipitation_rate.""" + self.arr.standard_name = 'lwe_precipitation_rate' + self.arr.units = 'mm s-1' + result = convert_units(self.arr, 'kg m-2 s-1') + self.assertEqual(result.standard_name, 'precipitation_flux') + self.assertEqual(result.units, 'kg m-2 s-1') + np.testing.assert_allclose( + result.data, + [[0.0, 1.0], [2.0, 3.0]], + ) + + def test_convert_lwe_precipitation_rate_convertible(self): + """Test special conversion of lwe_precipitation_rate.""" + self.arr.standard_name = 'lwe_precipitation_rate' + self.arr.units = 'm yr-1' + result = convert_units(self.arr, 'g m-2 yr-1') + self.assertEqual(result.standard_name, 'precipitation_flux') + self.assertEqual(result.units, 'g m-2 yr-1') + np.testing.assert_allclose( + result.data, + [[0.0, 1.0e6], [2.0e6, 3.0e6]], + ) + + def test_convert_lwe_precipitation_rate_fail_invalid_name(self): + """Test special conversion of lwe_precipitation_rate.""" + self.arr.units = 'mm s-1' + self.assertRaises(ValueError, convert_units, self.arr, 'kg m-2 s-1') + + def test_convert_lwe_precipitation_rate_fail_invalid_source_units(self): + """Test special conversion of lwe_precipitation_rate.""" + self.arr.standard_name = 'lwe_precipitation_rate' + self.assertRaises(ValueError, convert_units, self.arr, 'kg m-2 s-1') + + def test_convert_lwe_precipitation_rate_fail_invalid_target_units(self): + """Test special conversion of lwe_precipitation_rate.""" + self.arr.standard_name = 'lwe_precipitation_rate' + self.arr.units = 'mm s-1' + self.assertRaises(ValueError, convert_units, self.arr, 'K') + if __name__ == '__main__': unittest.main()