Source code for esmvalcore.preprocessor._derive
"""Automatically derive variables."""
from __future__ import annotations
import importlib
import logging
from copy import deepcopy
from pathlib import Path
from typing import TYPE_CHECKING
from iris.cube import CubeList
from esmvalcore.preprocessor._units import convert_units
if TYPE_CHECKING:
from cf_units import Unit
from iris.cube import Cube
from esmvalcore.preprocessor._derive._baseclass import DerivedVariableBase
from esmvalcore.typing import Facets
logger = logging.getLogger(__name__)
def _get_all_derived_variables() -> dict[str, type[DerivedVariableBase]]:
"""Get all possible derived variables.
Returns
-------
dict
All derived variables with `short_name` (keys) and the associated
python classes (values).
"""
derivers = {}
for path in Path(__file__).parent.glob("[a-z]*.py"):
short_name = path.stem
module = importlib.import_module(
f"esmvalcore.preprocessor._derive.{short_name}",
)
derivers[short_name] = module.DerivedVariable
return derivers
ALL_DERIVED_VARIABLES: dict[str, type[DerivedVariableBase]] = (
_get_all_derived_variables()
)
__all__ = list(ALL_DERIVED_VARIABLES)
def get_required(short_name: str, project: str) -> list[Facets]:
"""Return all required variables for derivation.
Get all information (at least ``short_name``) required for derivation.
Parameters
----------
short_name:
Short name of the variable to derive.
project:
Project of the variable to derive.
Returns
-------
list[esmvalcore.typing.Facets]
List of facets (including at least the key ``short_name``).
"""
if short_name.lower() not in ALL_DERIVED_VARIABLES:
msg = (
f"Cannot derive variable '{short_name}': no derivation script "
f"available"
)
raise NotImplementedError(msg)
DerivedVariable = ALL_DERIVED_VARIABLES[short_name.lower()] # noqa: N806
return deepcopy(DerivedVariable().required(project))
[docs]
def derive(
cubes: CubeList,
short_name: str,
long_name: str,
units: str | Unit,
standard_name: str | None = None,
) -> Cube:
"""Derive variable.
Parameters
----------
cubes:
Includes all the needed variables for derivation defined in
:func:`get_required`.
short_name:
short_name
long_name:
long_name
units:
units
standard_name:
standard_name
Returns
-------
iris.cube.Cube
The new derived variable.
"""
if short_name == cubes[0].var_name:
return cubes[0]
cubes = CubeList(cubes)
# Derive variable
DerivedVariable = ALL_DERIVED_VARIABLES[short_name.lower()] # noqa: N806
try:
cube = DerivedVariable().calculate(cubes)
except Exception as exc:
msg = (
f"Derivation of variable '{short_name}' failed. If you used "
f"the option '--skip_nonexistent' for running your recipe, "
f"this might be caused by missing input data for derivation"
)
raise ValueError(msg) from exc
# Set standard attributes
cube.var_name = short_name
cube.standard_name = standard_name or None
cube.long_name = long_name
for temp in cubes:
if "source_file" in temp.attributes:
cube.attributes["source_file"] = temp.attributes["source_file"]
# Check/convert units
if cube.units is None or cube.units == units:
cube.units = units
elif cube.units.is_no_unit() or cube.units.is_unknown():
logger.warning(
"Units of cube after executing derivation script of '%s' are "
"'%s', automatically setting them to '%s'. This might lead to "
"incorrect data",
short_name,
cube.units,
units,
)
cube.units = units
else:
try:
convert_units(cube, units)
except ValueError as exc:
msg = (
f"Units '{cube.units}' after executing derivation script of "
f"'{short_name}' cannot be converted to target units '{units}'"
)
raise ValueError(msg) from exc
return cube