From 82be35fc91e84b8d7bde069e0f436e5759ff8014 Mon Sep 17 00:00:00 2001 From: Henry Wright Date: Fri, 28 Feb 2025 17:49:54 +0000 Subject: [PATCH] reuploading changes for bounds of derived variables --- lib/iris/__init__.py | 5 ++ lib/iris/fileformats/cf.py | 132 ++++++++++++++++++++++++++++++------- 2 files changed, 113 insertions(+), 24 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index d622bd18b0..0dbdc40510 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -169,6 +169,7 @@ def __init__( pandas_ndim=False, save_split_attrs=False, date_microseconds=False, + derived_bounds=False, ): """Container for run-time options controls. @@ -202,6 +203,8 @@ def __init__( behaviour, such as when using :class:`~iris.Constraint`, and you may need to defend against floating point precision issues where you didn't need to before. + derived_bounds : bool, default=False + When True, uses the new form for deriving bounds with the load. """ # The flag 'example_future_flag' is provided as a reference for the @@ -215,6 +218,7 @@ def __init__( self.__dict__["pandas_ndim"] = pandas_ndim self.__dict__["save_split_attrs"] = save_split_attrs self.__dict__["date_microseconds"] = date_microseconds + self.__dict__["derived_bounds"] = derived_bounds # TODO: next major release: set IrisDeprecation to subclass # DeprecationWarning instead of UserWarning. @@ -228,6 +232,7 @@ def __repr__(self): self.pandas_ndim, self.save_split_attrs, self.date_microseconds, + self.derived_bounds, ) # deprecated_options = {'example_future_flag': 'warning',} diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 82be010c6e..69072f274a 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -16,6 +16,7 @@ from abc import ABCMeta, abstractmethod from collections.abc import Iterable, MutableMapping +import contextlib import os import re from typing import ClassVar, Optional @@ -94,6 +95,8 @@ def __init__(self, name, data): #: CF-netCDF formula terms that his variable participates in. self.cf_terms_by_root = {} + self._to_be_promoted = False + self.cf_attrs_reset() @staticmethod @@ -1416,16 +1419,66 @@ def _translate(self, variables): # Identify and register all CF formula terms. formula_terms = _CFFormulaTermsVariable.identify(variables) - for cf_var in formula_terms.values(): - for cf_root, cf_term in cf_var.cf_terms_by_root.items(): - # Ignore formula terms owned by a bounds variable. - if cf_root not in self.cf_group.bounds: - cf_name = cf_var.cf_name - if cf_var.cf_name not in self.cf_group: - self.cf_group[cf_name] = CFAuxiliaryCoordinateVariable( - cf_name, cf_var.cf_data - ) - self.cf_group[cf_name].add_formula_term(cf_root, cf_term) + if iris.FUTURE.derived_bounds: + # cf_var = CFFormulaTermsVariable (loops through everything that appears in formula terms) + for cf_var in formula_terms.values(): + # eg. eta:'a' | cf_root = eta and cf_term = a. cf_var.cf_terms_by_root = {'eta': 'a'} (looking at all appearances in formula terms) + for cf_root, cf_term in cf_var.cf_terms_by_root.items(): + # gets set to the bounds of the coord from cf_root_coord + bounds_name = None + # cf_root_coord = CFCoordinateVariable of the coordinate relating to the root + cf_root_coord = self.cf_group.coordinates.get(cf_root) + if cf_root_coord is None: + cf_root_coord = self.cf_group.auxiliary_coordinates.get(cf_root) + with contextlib.suppress(AttributeError): + # Copes with cf_root_coord not existing, OR not having + # `bounds` attribute. + bounds_name = cf_root_coord.bounds + + if bounds_name is not None: + try: + # This will error if more or less than 1 variable is found. + # TODO: try a try/except here or logical alternative + (bounds_var,) = [ + # loop through all formula terms and add them if they have a cf_term_by_root + # where (bounds of cf_root): cf_term (same as before) + f + for f in formula_terms.values() + if f.cf_terms_by_root.get(bounds_name) == cf_term + ] + if bounds_var != cf_var: + cf_var.bounds = bounds_var.cf_name + new_var = CFBoundaryVariable( + bounds_var.cf_name, bounds_var.cf_data + ) + new_var.add_formula_term(bounds_name, cf_term) + self.cf_group[bounds_var.cf_name] = new_var + except ValueError: + # Modify the boundary_variable set _to_be_promoted to True + self.cf_group.get(bounds_name)._to_be_promoted = True + + if cf_root not in self.cf_group.bounds: + cf_name = cf_var.cf_name + if cf_var.cf_name not in self.cf_group: + new_var = CFAuxiliaryCoordinateVariable( + cf_name, cf_var.cf_data + ) + if hasattr(cf_var, "bounds"): + new_var.bounds = cf_var.bounds + new_var.add_formula_term(cf_root, cf_term) + self.cf_group[cf_name] = new_var + + else: + for cf_var in formula_terms.values(): + for cf_root, cf_term in cf_var.cf_terms_by_root.items(): + # Ignore formula terms owned by a bounds variable. + if cf_root not in self.cf_group.bounds: + cf_name = cf_var.cf_name + if cf_var.cf_name not in self.cf_group: + self.cf_group[cf_name] = CFAuxiliaryCoordinateVariable( + cf_name, cf_var.cf_data + ) + self.cf_group[cf_name].add_formula_term(cf_root, cf_term) # Determine the CF data variables. data_variable_names = ( @@ -1454,7 +1507,7 @@ def _span_check( """Sanity check dimensionality.""" var = self.cf_group[var_name] # No span check is necessary if variable is attached to a mesh. - if is_mesh_var or var.spans(cf_variable): + if (is_mesh_var or var.spans(cf_variable)) and not var._to_be_promoted: cf_group[var_name] = var else: # Register the ignored variable. @@ -1498,6 +1551,14 @@ def _span_check( for cf_name in match: _span_check(cf_name) + if iris.FUTURE.derived_bounds: + if hasattr(cf_variable, "bounds"): + if cf_variable.bounds not in cf_group: + bounds_var = self.cf_group[cf_variable.bounds] + # TODO: warning if span fails + if bounds_var.spans(cf_variable): + cf_group[cf_variable.bounds] = bounds_var + # Build CF data variable relationships. if isinstance(cf_variable, CFDataVariable): # Add global netCDF attributes. @@ -1539,19 +1600,42 @@ def _span_check( # Determine whether there are any formula terms that # may be promoted to a CFDataVariable and restrict promotion to only # those formula terms that are reference surface/phenomenon. - for cf_var in self.cf_group.formula_terms.values(): - for cf_root, cf_term in cf_var.cf_terms_by_root.items(): - cf_root_var = self.cf_group[cf_root] - name = cf_root_var.standard_name or cf_root_var.long_name - terms = reference_terms.get(name, []) - if isinstance(terms, str) or not isinstance(terms, Iterable): - terms = [terms] - cf_var_name = cf_var.cf_name - if cf_term in terms and cf_var_name not in self.cf_group.promoted: - data_var = CFDataVariable(cf_var_name, cf_var.cf_data) - self.cf_group.promoted[cf_var_name] = data_var - _build(data_var) - break + if iris.FUTURE.derived_bounds: + for cf_var in self.cf_group.formula_terms.values(): + if self.cf_group[cf_var.cf_name] is CFBoundaryVariable: + continue + else: + for cf_root, cf_term in cf_var.cf_terms_by_root.items(): + cf_root_var = self.cf_group[cf_root] + if not hasattr(cf_root_var, "standard_name"): + continue + name = cf_root_var.standard_name or cf_root_var.long_name + terms = reference_terms.get(name, []) + if isinstance(terms, str) or not isinstance(terms, Iterable): + terms = [terms] + cf_var_name = cf_var.cf_name + if ( + cf_term in terms + and cf_var_name not in self.cf_group.promoted + ): + data_var = CFDataVariable(cf_var_name, cf_var.cf_data) + self.cf_group.promoted[cf_var_name] = data_var + _build(data_var) + break + else: + for cf_var in self.cf_group.formula_terms.values(): + for cf_root, cf_term in cf_var.cf_terms_by_root.items(): + cf_root_var = self.cf_group[cf_root] + name = cf_root_var.standard_name or cf_root_var.long_name + terms = reference_terms.get(name, []) + if isinstance(terms, str) or not isinstance(terms, Iterable): + terms = [terms] + cf_var_name = cf_var.cf_name + if cf_term in terms and cf_var_name not in self.cf_group.promoted: + data_var = CFDataVariable(cf_var_name, cf_var.cf_data) + self.cf_group.promoted[cf_var_name] = data_var + _build(data_var) + break # Promote any ignored variables. promoted = set() not_promoted = ignored.difference(promoted)