Source code for stilt.particles.accessor
"""
The ``.stilt`` accessor on a particle table: endpoints, enhancement, plots.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
import pandas as pd
import xarray as xr
from stilt.sampling import sample_field
if TYPE_CHECKING:
from stilt.visualization import ParticlesPlotAccessor
[docs]
@pd.api.extensions.register_dataframe_accessor("stilt")
class ParticlesAccessor:
"""
PYSTILT methods on a particle table, as ``particles.stilt``.
Examples
--------
>>> particles = sim.particles # doctest: +SKIP
>>> particles.stilt.endpoints() # doctest: +SKIP
>>> particles.stilt.enhancement(flux).mean() # doctest: +SKIP
>>> particles.stilt.plot.map() # doctest: +SKIP
"""
def __init__(self, particles: pd.DataFrame) -> None:
self._particles = particles
[docs]
def endpoints(self) -> pd.DataFrame:
"""
Return the last row of each particle, the one farthest in time from release.
For a backward run this is where the air came from, which is where
to sample a background concentration field. For a forward run it is
where the air went. A particle that left the meteorology domain
early ends where it left.
Returns
-------
pandas.DataFrame
One row per particle, with every column of the particle table.
``age`` is minutes since release and ``time`` the UTC time.
"""
p = self._particles.reset_index(drop=True)
if p.empty:
return p
reach = p["age"].abs()
last = reach.groupby(p["particle"], sort=False).idxmax().to_numpy(dtype=int)
return p.iloc[last]
[docs]
def enhancement(self, flux: xr.DataArray) -> pd.Series:
"""
Return each particle's enhancement, ``foot`` times flux summed along its trajectory.
The mean over particles, after any weighting, is the modeled
enhancement at the receptor. Unlike ``foot.stilt.enhancement``, the
flux is taken at each particle position, with no gridding or
smoothing.
Parameters
----------
flux : xarray.DataArray
Surface flux on a ``lat``/``lon`` grid, in µmol m⁻² s⁻¹ for an
enhancement in ppm. A flux with a ``time`` dimension is read as
steps that start at their labels, as ``foot.stilt.enhancement``
reads it: each particle row takes the last flux step that
starts at or before the row's ``time``. A row before the first
label gets zero flux, and a row after the last label takes the
last step. A flux labeled at the middle of its steps must be
relabeled first (see ``foot.stilt.enhancement``). Points outside
the flux grid, and missing cells, count as zero flux.
Returns
-------
pandas.Series
Enhancement indexed by ``particle``. A particle that never crosses
the flux field gets 0.
Raises
------
ValueError
If the flux varies in time and the particles have no
``time`` column.
"""
p = self._particles
times = p["time"].to_numpy() if "time" in p.columns else None
if "time" in flux.dims and times is None:
raise ValueError(
"flux varies in time but the particles have no 'time' column."
)
sampled = sample_field(
flux,
p["lon"].to_numpy(),
p["lat"].to_numpy(),
times=times,
fill_value=0.0,
step_starts=True,
)
contribution = p["foot"].to_numpy(dtype=float) * sampled
ids = p["particle"].to_numpy()
unique, inverse = np.unique(ids, return_inverse=True)
sums = np.bincount(inverse, weights=contribution, minlength=unique.size)
return pd.Series(
sums, index=pd.Index(unique, name="particle"), name="enhancement"
)
@property
def plot(self) -> ParticlesPlotAccessor:
"""Plotting methods, such as ``particles.stilt.plot.map()``."""
from stilt.visualization import ParticlesPlotAccessor
return ParticlesPlotAccessor(self._particles)