Source code for carpet_concentrations.gridders.latitude_seasonality_gridder

"""
Gridder based on pre-calculated latitudinal gradient and seasonality
"""
from __future__ import annotations

from functools import partial
from typing import TYPE_CHECKING, Any

import numpy as np
from attrs import define, field

from carpet_concentrations.attrs_utils import (
    make_attrs_validator_compatible_value_instance_input,
)
from carpet_concentrations.xarray_pint_utils import (
    check_pint_quantified_dataset,
    check_pint_quantified_dataset_attrs,
)
from carpet_concentrations.xarray_utils import (
    calculate_weighted_area_mean_latitude_only,
    check_all_units_compatible_attrs,
    check_dimensions,
)

if TYPE_CHECKING:
    import xarray as xr


def _attribute_has_year_month_lat_coords(
    instance: Any,
    value: xr.Dataset,
    attr_to_get: str,
) -> None:
    check_dimensions(
        value[getattr(instance, attr_to_get)], ("year", "month", "lat"), extras_ok=True
    )


_seasonality_has_year_month_lat_coords = (
    make_attrs_validator_compatible_value_instance_input(
        partial(_attribute_has_year_month_lat_coords, attr_to_get="seasonality_name")
    )
)


_latitudinal_gradient_has_year_month_lat_coords = (
    make_attrs_validator_compatible_value_instance_input(
        partial(
            _attribute_has_year_month_lat_coords,
            attr_to_get="latitudinal_gradient_name",
        )
    )
)


def _seasonality_annual_mean_zero(
    instance: Any,
    value: xr.Dataset,
) -> None:
    seasonality = value[instance.seasonality_name]
    np.testing.assert_allclose(
        seasonality.mean("month").data.magnitude,
        0,
        atol=1e-8,
        err_msg="seasonality must have an annual-mean of zero in all years",
    )


_seasonality_annual_mean_zero_attrs = (
    make_attrs_validator_compatible_value_instance_input(_seasonality_annual_mean_zero)
)


def _latitudinal_gradient_spatial_mean(
    instance: Any,
    value: xr.Dataset,
) -> None:
    np.testing.assert_allclose(
        calculate_weighted_area_mean_latitude_only(
            value, [instance.latitudinal_gradient_name]
        )[instance.latitudinal_gradient_name].data.magnitude,
        0,
        atol=1e-8,
        err_msg=(
            "latitudinal gradient must have an area-weighted spatial-mean "
            "of zero in all timesteps"
        ),
    )


_latitudinal_gradient_spatial_mean_zero_attrs = (
    make_attrs_validator_compatible_value_instance_input(
        _latitudinal_gradient_spatial_mean
    )
)


[docs]@define class LatitudeSeasonalityGridder: """ Gridder based on specified latitudinal gradient and seasonality """ gridding_values: xr.Dataset = field( validator=[ check_pint_quantified_dataset_attrs, _seasonality_has_year_month_lat_coords, _seasonality_annual_mean_zero_attrs, _latitudinal_gradient_has_year_month_lat_coords, _latitudinal_gradient_spatial_mean_zero_attrs, check_all_units_compatible_attrs, ] ) """ Values to use to convert global-mean concentrations to a grid This should have already been turned into a pint-compatiable quantity using ``pint.quantify`` or similar. It should also contain variables that match the value of ``seasonality_name`` and ``latitudinal_gradient_name``. Both the variables must have at least the dimensions ("year", "month", "lat"). The seasonality variable must have an annual-mean of zero. The latitudinal gradient must have a spatial-mean of zero. """ seasonality_name: str = "seasonality" """ Name of the seasonality variable (in ``gridding_values`` but also used elsewhere) """ latitudinal_gradient_name: str = "latitudinal_gradient" """ Name of the latitudinal gradient variable (in ``gridding_values`` but also used elsewhere) """
[docs] def calculate(self, global_means: xr.Dataset) -> xr.Dataset: r""" Calculate gridded values Parameters ---------- global_means Global-mean values. These should have already been interpolated such that their dimensions are ``("year", "month")`` (other dimensions e.g. ``"scenario"`` are also ok) to obtain sensible results (if not, there may be jumps at the start and end of years). ``global_means`` should have also already been turned into a pint-compatiable quantity using ``pint.quantify`` or similar. Returns ------- Gridded values on a ``("latitude", "year", "month")`` grid (plus any other dimensions present in ``global_means``) Raises ------ CoordinateError ``global_means`` does not have at least the dimensions of ``("year", "month")`` (other dimensions e.g. ``"scenario"`` are also ok) NotPintQuantifiedError ``global_means`` has not been quantified using ``pint.quantify`` or similar Notes ----- Eq. 1 of :footcite:t:`meinshausen_et_al_2017_concentrations`. We use slightly different notation here for clarity. .. math:: C(l, m, y, ...) = \overline{C(m, y, ...)} + S(l, m, y) + L(l, m, y) i.e. the concentration at latitude :math:`l` in year :math:`y` and month :math:`m` is the sum of the global-, annual-mean concentration in that year (already interpolated down to a monthly timestep), the seasonality at that latitude in that year in that month (varies with year and latitude as often scaled with something else) and the latitudinal gradient at that latitude in that year and month (varies with year and month as often scaled with something else, must have monthly information to avoid step changes in output). Note that the global-, annual-mean concentrations :math:`C(y, m)` must be pre-interpolated onto monthly steps using a mean-preserving alogrithm [#11 for function which will do this] to avoid spurious steps in the outputs. The ellipses represent extra dimensions (e.g. scenario, greenhouse gas) that might be in :math:`C(y, m)`. Any such dimensions are preserved in the output. """ check_pint_quantified_dataset(global_means) for darray in global_means.values(): check_dimensions(darray, ("year", "month"), extras_ok=True) res = ( global_means + self.gridding_values["seasonality"] + self.gridding_values["latitudinal_gradient"] ) return res