diff --git a/mip_convert/mip_convert/requested_variables.py b/mip_convert/mip_convert/requested_variables.py index 31c07e6c4..18f8fe3f7 100644 --- a/mip_convert/mip_convert/requested_variables.py +++ b/mip_convert/mip_convert/requested_variables.py @@ -125,13 +125,22 @@ def produce_mip_requested_variable( if frequency: saver.cmor.set_frequency(frequency) + # Assemble the arguments needed to apply cell measures when saving. + cell_measures_config = ( + user_config.mip_era, + user_config.inpath, + mip_table.id, + variable_name, + frequency, + user_config.global_attributes.get('region', '')) + # Process the data by performing the appropriate 'model to MIP mapping', then save the 'MIP output variable' # to an 'output netCDF file'. period = user_config.slicing.get(stream_id, 'year') for time_slice in variable.slices_over(period): time_slice.process() logger.debug('MIP output variable contains: {}'.format(time_slice.info)) - save(time_slice, saver) + save(time_slice, saver, cell_measures_config=cell_measures_config) # Close the 'output netCDF file'. cmor_lite.close(saver.varid) diff --git a/mip_convert/mip_convert/save/__init__.py b/mip_convert/mip_convert/save/__init__.py index e5127823a..08bcee2bb 100644 --- a/mip_convert/mip_convert/save/__init__.py +++ b/mip_convert/mip_convert/save/__init__.py @@ -6,6 +6,7 @@ import copy import logging from operator import attrgetter +from typing import Optional, Tuple import iris import numpy as np @@ -14,12 +15,17 @@ DEFAULT_FILL_VALUE, has_auxiliary_latitude_longitude, check_values_equal) from mip_convert.load.pp.pp_axis import (BoundedAxis, AxisHybridHeight, TimeSeriesSiteAxis, ReferenceTimeAxis) +from mip_convert.save.cmor.cmor_outputter import AbstractCmorOutputter from mip_convert.variable import Variable as CMORVariable from mip_convert.variable import (CoordinateDomain, PolePoint, TripolarGrid, make_masked) -def save(mip_output_variable, saver): +def save( + mip_output_variable, + saver: AbstractCmorOutputter, + cell_measures_config: Optional[Tuple[str, str, str, str, Optional[str], str]] = None, +): """Save the |MIP output variable| to an |output netCDF file| using |CMOR|. @@ -41,10 +47,17 @@ def save(mip_output_variable, saver): The |MIP output variable|. saver: callable A function with the signature ``function(object)`` + cell_measures_config: tuple, optional + Arguments for :meth:`cmor_wrapper.apply_cell_measures` (excluding + ``variable_id``). When provided, cell measures are applied once, + after the CMOR variable is registered but before data is written. """ logger = logging.getLogger(__name__) logger.debug('Saving MIP output variable to an output netCDF file') cmor_variable = create_cmor_variable(mip_output_variable) + if cell_measures_config is not None and saver.varid is None: + saver._getVarId(cmor_variable) + saver.cmor.apply_cell_measures(*cell_measures_config, saver.varid) saver(cmor_variable) diff --git a/mip_convert/mip_convert/save/cmor/cmor_outputter.py b/mip_convert/mip_convert/save/cmor/cmor_outputter.py index b9007455c..797267184 100644 --- a/mip_convert/mip_convert/save/cmor/cmor_outputter.py +++ b/mip_convert/mip_convert/save/cmor/cmor_outputter.py @@ -38,6 +38,7 @@ from mip_convert import mip_parser import mip_convert.common from mip_convert.common import RelativePathChecker +from mip_convert.save.cmor.cmor_wrapper import CmorWrapper from mip_convert.save.mip_config import MipTableFactory @@ -219,7 +220,7 @@ class AbstractCmorOutputter(object): """ def __init__(self, entry, cmor_wrapper): - self.cmor = cmor_wrapper + self.cmor: CmorWrapper = cmor_wrapper self.entry = entry self._name_space = MoNameSpace() self.varid = None diff --git a/mip_convert/mip_convert/save/cmor/cmor_wrapper.py b/mip_convert/mip_convert/save/cmor/cmor_wrapper.py index 56c5a6463..32c2ae4c6 100644 --- a/mip_convert/mip_convert/save/cmor/cmor_wrapper.py +++ b/mip_convert/mip_convert/save/cmor/cmor_wrapper.py @@ -1,6 +1,8 @@ # (C) British Crown Copyright 2009-2025, Met Office. # Please see LICENSE.md for license details. from collections import OrderedDict +import os +import json import cmor @@ -115,3 +117,60 @@ def zfactor(self, *args, **kwargs): def set_frequency(self, frequency, **kwargs): self._debug_on_args('frequency', [frequency], kwargs) cmor.cmor.set_cur_dataset_attribute('frequency', frequency) + + def apply_cell_measures(self, mip_era, mip_table_dir, realm, variable, frequency, region, variable_id): + """Set the ``cell_measures`` attribute on a registered CMOR variable. + + Looks up ``{mip_table_dir}/{mip_era}_cell_measures.json`` and, if a + matching entry is found, calls ``cmor.set_variable_attribute`` to + attach the cell-measures string to the variable before any data are + written. If the file does not exist, or no matching key is found, + the method returns silently. + + This method must be called after ``cmor.variable()`` has registered + the variable (so that ``variable_id`` is valid) and before + ``cmor.write()`` is called. + + Parameters + ---------- + mip_era: str + The MIP era (e.g. ``"CMIP7"``). + mip_table_dir: str + Directory containing the MIP tables and the cell-measures JSON. + realm: str + The MIP table identifier (e.g. ``"Amon"``). + variable: str + The variable name in ``{root_label}_{branding}`` form. + frequency: str or None + The output frequency (e.g. ``"mon"``). + region: str + The region string (e.g. ``""`` for global). + variable_id: int + The integer handle returned by ``cmor.variable()``. + """ + self._debug_on_args( + "apply_cell_measures", [mip_era, mip_table_dir, realm, variable, frequency, region, variable_id], {} + ) + + cell_measures_file = os.path.join(mip_table_dir, f'{mip_era}_cell_measures.json') + + if os.path.exists(cell_measures_file): + with open(cell_measures_file) as fh: + cell_measures = json.load(fh) + if 'cell_measures' not in cell_measures: + self.logger.debug(f'"cell_measures" key not found in {cell_measures_file}') + return + cell_measures = cell_measures['cell_measures'] + + root_label, branding = variable.split('_') + key = f'{realm}.{root_label}.{branding}.{frequency}.{region}' + if key in cell_measures: + retval = cmor.cmor.set_variable_attribute( + variable_id, + 'cell_measures', + 'c', + cell_measures[key]) + if retval != 0: + self.logger.debug('cell_measures assignment failed. Check cmor log file for details') + else: + self.logger.debug(f'Cell_measures file "{cell_measures_file}" not found. Continuing') diff --git a/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_fx_rootd_ti_u_hxy_lnd.py b/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_fx_rootd_ti_u_hxy_lnd.py index 83c556a28..b77ba29cd 100644 --- a/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_fx_rootd_ti_u_hxy_lnd.py +++ b/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_fx_rootd_ti_u_hxy_lnd.py @@ -44,7 +44,7 @@ def get_test_data(self): 'ancil': {'CMIP7_land@fx': 'rootd_ti-u-hxy-lnd'} }, other={ - 'reference_version': 'v5', + 'reference_version': 'v6', 'filenames': ['rootd_ti-u-hxy-lnd_fx_glb_g100_UKESM1-3-LL_1pctCO2_r1i1p1f3.nc'], 'ignore_history': True, } diff --git a/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_fx_slthick_ti_sl_hxy_lnd.py b/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_fx_slthick_ti_sl_hxy_lnd.py index 2d2a69443..5d8d774ba 100644 --- a/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_fx_slthick_ti_sl_hxy_lnd.py +++ b/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_fx_slthick_ti_sl_hxy_lnd.py @@ -44,7 +44,7 @@ def get_test_data(self): 'ancil': {'CMIP7_land@fx': 'slthick_ti-sl-hxy-lnd'} }, other={ - 'reference_version': 'v5', + 'reference_version': 'v6', 'filenames': ['slthick_ti-sl-hxy-lnd_fx_glb_g100_UKESM1-3-LL_1pctCO2_r1i1p1f3.nc'], 'ignore_history': True, } diff --git a/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_mon_mcd_tavg-alh-hxy-u.py b/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_mon_mcd_tavg-alh-hxy-u.py index 25e6d279e..b1210d7fa 100644 --- a/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_mon_mcd_tavg-alh-hxy-u.py +++ b/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_mon_mcd_tavg-alh-hxy-u.py @@ -41,7 +41,7 @@ def get_test_data(self): 'ap5': {'CMIP7_atmos@mon': 'mcd_tavg-alh-hxy-u'} }, other={ - 'reference_version': 'v3', + 'reference_version': 'v4', 'filenames': ['mcd_tavg-alh-hxy-u_mon_glb_g100_UKESM1-3-LL_1pctCO2_r1i1p1f3_200001-200002.nc'], 'ignore_history': True, } diff --git a/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_mon_uas_tavg_h10m_hxy_u.py b/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_mon_uas_tavg_h10m_hxy_u.py index b28c9ec8a..95b4bcec4 100644 --- a/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_mon_uas_tavg_h10m_hxy_u.py +++ b/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_mon_uas_tavg_h10m_hxy_u.py @@ -41,7 +41,7 @@ def get_test_data(self): 'ap5': {'CMIP7_atmos@mon': 'uas_tavg-h10m-hxy-u'} }, other={ - 'reference_version': 'v7', + 'reference_version': 'v8', 'filenames': ['uas_tavg-h10m-hxy-u_mon_glb_g100_UKESM1-3-LL_1pctCO2_r1i1p1f3_196002-196003.nc'], 'ignore_history': True, } diff --git a/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_rlucs4co2_tavg-alh-hxy-u.py b/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_rlucs4co2_tavg-alh-hxy-u.py index e298cbebf..c957f02cf 100644 --- a/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_rlucs4co2_tavg-alh-hxy-u.py +++ b/mip_convert/mip_convert/tests/test_functional/test_functional_cmip7/test_cmip7_rlucs4co2_tavg-alh-hxy-u.py @@ -47,7 +47,7 @@ def get_test_data(self): }, streams={"ap5": {"CMIP7_atmos@mon": "rlucs4co2_tavg-alh-hxy-u"}}, other={ - "reference_version": "v3", + "reference_version": "v4", "filenames": [ "rlucs4co2_tavg-alh-hxy-u_mon_glb_g100_UKESM1-3-LL_esm-piControl_r1i1p1f1_190001-190002.nc" ],