Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion mip_convert/mip_convert/requested_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

@mo-gill Ed (mo-gill) Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, putting the call to apply_cell_measures in save rather than in the block before the time_slice loop was due to problems with cmor such as it requiring the grid axes when making a varid in _makeVarid, and other issues - which would cause errors like this:

2026-06-10 14:02:07 mip_convert.save.cmor.cmor_wrapper.CmorWrapper._debug_on_args DEBUG: frequency arg: ["<class 'str'>"] "mon"
2026-06-10 14:02:07 mip_convert.save.cmor.cmor_wrapper.CmorWrapper._debug_on_args DEBUG: apply_cell_measures arg: ["<class 'str'>"] "CMIP7"
2026-06-10 14:02:07 mip_convert.save.cmor.cmor_wrapper.CmorWrapper._debug_on_args DEBUG: apply_cell_measures arg: ["<class 'str'>"] "for_functional_tests_2"
2026-06-10 14:02:07 mip_convert.save.cmor.cmor_wrapper.CmorWrapper._debug_on_args DEBUG: apply_cell_measures arg: ["<class 'str'>"] "atmos"
2026-06-10 14:02:07 mip_convert.save.cmor.cmor_wrapper.CmorWrapper._debug_on_args DEBUG: apply_cell_measures arg: ["<class 'str'>"] "uas_tavg-h10m-hxy-u"
2026-06-10 14:02:07 mip_convert.save.cmor.cmor_wrapper.CmorWrapper._debug_on_args DEBUG: apply_cell_measures arg: ["<class 'str'>"] "mon"
2026-06-10 14:02:07 mip_convert.save.cmor.cmor_wrapper.CmorWrapper._debug_on_args DEBUG: apply_cell_measures arg: ["<class 'str'>"] "glb"
2026-06-10 14:02:07 mip_convert.save.cmor.cmor_wrapper.CmorWrapper._debug_on_args DEBUG: apply_cell_measures arg: ["<class 'NoneType'>"] "None"
2026-06-10 14:02:07 mip_convert.request.convert CRITICAL: Unable to produce MIP requested variable "uas_tavg-h10m-hxy-u" for "CMIP7_atmos": 'NoneType' object cannot be interpreted as an integer
2026-06-10 14:02:07 mip_convert.request.convert ERROR: 'NoneType' object cannot be interpreted as an integer
Traceback (most recent call last):
  File "CDDS/mip_convert/mip_convert/request.py", line 124, in convert
    produce_mip_requested_variable(variable_name, stream_id, substream, mip_table, user_config,
  File "/CDDS/mip_convert/mip_convert/requested_variables.py", line 129, in produce_mip_requested_variable
    saver.cmor.apply_cell_measures(
  File "CDDS/mip_convert/mip_convert/save/cmor/cmor_wrapper.py", line 140, in apply_cell_measures
    retval = cmor.cmor.set_variable_attribute(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/cmor/pywrapper.py", line 1060, in set_variable_attribute
    return _cmor.set_variable_attribute(var_id, name, data_type, value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: 'NoneType' object cannot be interpreted as an integer
2026-06-10 14:02:07 mip_convert.request.convert INFO: Critical Errors found in all variables, therefore MIP convert has failed.
2026-06-10 14:02:07 mip_convert.save.cmor.cmor_wrapper.CmorWrapper._debug_on_args DEBUG: close arg: ["<class 'NoneType'>"] "None"
2026-06-10 14:02:07 mip_convert.save.cmor.cmor_wrapper.CmorWrapper.close DEBUG: close return "0"


# Close the 'output netCDF file'.
cmor_lite.close(saver.varid)
Expand Down
15 changes: 14 additions & 1 deletion mip_convert/mip_convert/save/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import copy
import logging
from operator import attrgetter
from typing import Optional, Tuple

import iris
import numpy as np
Expand All @@ -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|.

Expand All @@ -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:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and saver.varid is None here should ensure this block isn't repeated unnecessarily on subsequent time slices, as varid is preserved each time the file is closed:

def _close_file(self):
self.cmor.close(self.varid, preserve=True)

saver._getVarId(cmor_variable)
saver.cmor.apply_cell_measures(*cell_measures_config, saver.varid)
saver(cmor_variable)


Expand Down
3 changes: 2 additions & 1 deletion mip_convert/mip_convert/save/cmor/cmor_outputter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -219,7 +220,7 @@ class AbstractCmorOutputter(object):
"""

def __init__(self, entry, cmor_wrapper):
self.cmor = cmor_wrapper
self.cmor: CmorWrapper = cmor_wrapper

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly slightly unnecessary but this fixed the fact i couldn't get any hover info in vscode on apply_cell_measures here saver.cmor.apply_cell_measures(*cell_measures_config, saver.varid) in mip_convert/mip_convert/save/__init__.py, which was bugging me.

self.entry = entry
self._name_space = MoNameSpace()
self.varid = None
Expand Down
59 changes: 59 additions & 0 deletions mip_convert/mip_convert/save/cmor/cmor_wrapper.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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')
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand Down