Source code for huracanpy.tc._ace

"""
Module containing functions to compute ACE
"""

from numpy.polynomial.polynomial import Polynomial
from metpy.xarray import preprocess_and_wrap
from metpy.units import units
from sklearn.base import BaseEstimator

from .._metpy import dequantify_results, validate_units


[docs] def ace( wind, sum_by=None, threshold=34 * units("knots"), wind_units="m s-1", ): r"""Calculate accumulate cyclone energy (ACE) .. math:: \mathrm{ACE} = 10^{-4} \sum v_\mathrm{max}^2 \quad (v_\mathrm{max} \ge 34 \mathrm{kn}) By default, this function will return the "ACE" for each individual point in `wind`. To calculate more useful quantities of ACE, use the `sum_by` keyword. For example, to calculate the ACE of each individual track, doing >>> ace_by_track = huracanpy.tc.ace(tracks.wind, sum_by=tracks.track_id) will return a DataArray with track_id as a coordinate and the sum of ACE for each track as the data. Note that this is equivalent to using groupby: >>> ace_by_point = huracanpy.tc.ace(tracks.wind) >>> ace_by_track = ace_by_point.groupby(tracks.track_id).sum() To calculate the average ACE by track, you can do >>> ace_by_track_mean = ace_by_track.mean() Similarly to calculate a climatological mean ACE by year, run >>> climatological_ace = huracanpy.tc.ace( >>> tracks.wind, sum_by=tracks.time.dt.year >>> ).mean() Parameters ---------- wind : array_like Maximum velocity of a tropical cyclone associated with the tracks dataset sum_by : array_like Variable to take the sum of ACE values across. Must have the same length as wind threshold : scalar, default=34 knots ACE is set to zero below this threshold wind speed. The default argument is in knots. To pass an argument with units, use :py:mod:`metpy.units`, otherwise any non-default argument will be assumed to have the units of "wind_units" which is "m s-1" by default. wind_units : str, default="m s-1" If the units of wind are not specified in the attributes then the function will assume it is in these units before converting to knots Returns ------- array_like The ACE for each track in wind """ ace_values = _ace(wind, threshold, wind_units) if sum_by is not None: ace_values = ace_values.groupby(sum_by).sum() return ace_values
@dequantify_results @preprocess_and_wrap(wrap_like="wind") def _ace(wind, threshold=34 * units("knots"), wind_units="m s-1"): wind = validate_units(wind, wind_units) wind = wind.to(units("knots")) if threshold is not None: threshold = validate_units(threshold, wind_units) wind[wind < threshold] = 0 * units("knots") ace_values = (wind**2.0) * 1e-4 return ace_values
[docs] def pace( pressure, wind=None, model=None, sum_by=None, threshold_wind=None, threshold_pressure=None, wind_units="m s-1", pressure_units="hPa", **kwargs, ): """Calculate a pressure-based accumulated cyclone energy (PACE) for each individual point PACE is calculated the same way as ACE, but the wind is derived from fitting a pressure-wind relationship and calculating wind values from pressure using this fit Example ------- This function can be called in two ways 1. Pass the pressure and wind to fit a pressure-wind relationship to the data and then calculate pace from the winds derived from this fit >>> pace, pw_model = get_pace(pressure, wind) The default model to fit is a quadratic polynomial (:py:class:`numpy.polynomial.polynomial.Polynomial` with `deg=2`) 2. Pass just the pressure and an already fit model to calculate the wind speeds from this model >>> pace, _ = get_pace(pressure, model=pw_model) Parameters ---------- pressure : array_like Cyclone minimum sea-level pressure wind : array_like, optional Cyclone wind. Only include if you want to train a model to fit the pressure-wind relation model : str, class, or object, optional The model to fit the pressure wind relation or a model with preset parameters to derive wind from pressure. Can also be set to "z2021" or "holland" for those preset models. The object must have a `fit` function that returns a trained model, consistent with numpy and scikit-learn models. Default is py:class:`numpy.polynomial.polynomial.Polynomial` with `deg=2` sum_by : array_like Variable to take the sum of PACE values across. Must have the same length as pressure/wind. For examples, see the documentation for `huracanpy.tc.ace` threshold_wind : scalar, optional PACE is set to zero below this threshold wind speed threshold_pressure : scalar, optional Similar to threshold wind, set PACE to zero where pressure is above this threshold wind_units : str, default="m s-1" If the units of wind are not specified in the attributes then the function will assume it is in these units before converting to knots pressure_units : str, default="hPa" If the units of pressure are not specified in the attributes then the function will assume it is in these units **kwargs Remaining keywords are passed to the `fit` function of the model Returns ------- tuple (array_like, object) : Array of pace_values and the trained model for mapping from pressure values to wind values from :py:func:`pressure_wind_relation` """ pace_values, model = _pace( pressure, wind=wind, model=model, threshold_wind=threshold_wind, threshold_pressure=threshold_pressure, wind_units=wind_units, pressure_units=pressure_units, **kwargs, ) if sum_by is not None: pace_values = pace_values.groupby(sum_by).sum() return pace_values, model
@dequantify_results @preprocess_and_wrap(wrap_like=("pressure", None)) def _pace( pressure, wind=None, model=None, threshold_wind=None, threshold_pressure=None, wind_units="m s-1", pressure_units="hPa", **kwargs, ): if wind is None and model is None: raise ValueError( "Need to specify either wind or model to calculate pressure-wind relation" ) pressure = validate_units(pressure, pressure_units) if wind is not None: wind = validate_units(wind, wind_units) model = pressure_wind_relation( pressure, wind, model=model, pressure_units=pressure_units, wind_units=wind_units, **kwargs, ) pace_values = _ace(model.predict(pressure), threshold=threshold_wind) if threshold_pressure is not None: threshold_pressure = validate_units(threshold_pressure, pressure_units) pace_values[pressure > threshold_pressure] = 0.0 * pace_values.units return pace_values, model
[docs] @preprocess_and_wrap() def pressure_wind_relation( pressure, wind, model=None, pressure_units="hPa", wind_units="m s-1", **kwargs, ): """Fit a model pressure wind relation and return the trained model To get the predicted wind from the call the predict function (scikit-learn API) >>> wind = model.predict(pressure) or call the model as a function (numpy API) >>> wind = model(pressure) Note that the returned model supports inputs and outputs with units. Parameters ---------- pressure : array_like Cyclone minimum sea-level pressure wind : array_like Cyclone max wind model : str, class, or object, optional The model to fit the pressure wind relation or a model with preset parameters to derive wind from pressure. Can also be set to "z2021" or "holland" for those preset models. The object must have a `fit` function that returns a trained model, consistent with numpy and scikit-learn models. Default is py:class:`numpy.polynomial.polynomial.Polynomial` with `deg=2` wind_units : str, default="m s-1" If the units of wind are not specified in the attributes then the function will assume it is in these units before converting to knots pressure_units : str, default="hPa" If the units of pressure are not specified in the attributes then the function will assume it is in these units **kwargs Remaining keywords are passed to the `fit` function of the model Returns ------- object The model for mapping from pressure to wind. """ pressure = validate_units(pressure, pressure_units) wind = validate_units(wind, wind_units) if isinstance(model, str): if model.lower() == "z2021": model = Z2021() elif model.lower() == "holland": model = Holland() elif model is None: model = Polynomial if "deg" not in kwargs: kwargs["deg"] = 2 model = ModelWithUnits(model) model.fit(x=pressure, y=wind, **kwargs) return model
# Pre-determined pressure-wind relationships # Input pressure in hPa, output wind in knots class Z2021(Polynomial): def __init__(self): self.units = dict(x=units("hPa"), y=units("knots")) super().__init__([1.43290190e01, 5.68356519e-01, -1.05371378e-03]) def fit(self, *args, **kwargs): # noqa: ARG002 self.units = dict(x=units("hPa"), y=units("knots")) return self def predict(self, pressure): return self.__call__(pressure) def __call__(self, pressure): return super().__call__(1010.0 - pressure) class Holland: def __init__(self): self.units = dict(x=units("hPa"), y=units("knots")) def fit(self, *args, **kwargs): # noqa: ARG002 self.units = dict(x=units("hPa"), y=units("knots")) return self def predict(self, pressure): return self.__call__(pressure) def __call__(self, pressure): return 2.3 * (1010.0 - pressure) ** 0.76 class ModelWithUnits: def __init__(self, model): if not hasattr(model, "units"): model.units = dict(x=None, y=None) self.model = model @preprocess_and_wrap() def fit(self, x, y, **kws): self.model.units["x"] = x.units self.model.units["y"] = y.units if issubclass(self.model.__class__, BaseEstimator) and x.ndim <= 1: x = x.reshape(-1, 1) self.model = self.model.fit(x.magnitude, y.magnitude, **kws) return self @dequantify_results @preprocess_and_wrap(wrap_like="x") def predict(self, x, x_units=None): if x_units is None: x_units = str(self.model.units["x"]) x = validate_units(x, x_units).to(self.model.units["x"]).magnitude if issubclass(self.model.__class__, BaseEstimator) and x.ndim <= 1: x = x.reshape(-1, 1) try: y = self.model.predict(x) except AttributeError: y = self.model(x) return y * self.model.units["y"] def __call__(self, *args, **kwargs): return self.predict(*args, **kwargs)