Skip to content
Closed
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
5 changes: 5 additions & 0 deletions lib/iris/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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',}
Expand Down
132 changes: 108 additions & 24 deletions lib/iris/fileformats/cf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this is probably not necessary. Would you not want to do something like bounds_name = getattr(cf_root_coord, "bounds", default=None)?

# 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:
Copy link
Contributor

Choose a reason for hiding this comment

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

Ultimately, I think you'll probably want to replace this try-except call. I expect you ought to be able to replace this with something like:

bounds_vars = [...]
if len(bounds_vars) == 1:
  bounds_var = bounds_vars[0]
  ...
else:
  ...

# 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 = (
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

if you reach this point you will have a standard name, so or cf_root_var.long_name is unnecessary.

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)
Expand Down
Loading