Skip to content

API Reference

Note: The triple colon (:::) syntax below is used by mkdocstrings for API auto-documentation. If your documentation build does not support mkdocstrings, replace these with standard markdown/code blocks as needed.

Top-level Functions

monet

MONET: Model and Observation Evaluation Toolkit

A Python toolkit for comparing air quality models with observations. Provides utilities for pairing model output with observational data, statistical analysis, and visualization.

Main Components

plots : module Visualization tools for model-observation comparisons met_funcs : module Meteorological calculation utilities monet_accessor : module xarray and pandas accessor methods for MONET functionality util : module General utility functions and statistical tools

Functions

dataset_to_monet(*args, **kwargs)

Placeholder function for docs build.

Source code in monet/__init__.py
def dataset_to_monet(*args, **kwargs):
    """Placeholder function for docs build."""
    pass

rename_to_monet_latlon(*args, **kwargs)

Placeholder function for docs build.

Source code in monet/__init__.py
def rename_to_monet_latlon(*args, **kwargs):
    """Placeholder function for docs build."""
    pass

rename_latlon(*args, **kwargs)

Placeholder function for docs build.

Source code in monet/__init__.py
def rename_latlon(*args, **kwargs):
    """Placeholder function for docs build."""
    pass

Plotting Functions

The plotting functions are available in the monet.plots module.

monet.plots

Attributes

taylordiagram_function = create_taylor_diagram module-attribute

Functions

cmap_discretize(cmap, N)

Return a discrete colormap from a continuous colormap.

Creates a new colormap by discretizing an existing continuous colormap into N distinct colors while preserving the color transitions.

Parameters

cmap : str or matplotlib.colors.Colormap Colormap instance or registered colormap name to discretize. Example: cm.jet, 'viridis', etc. N : int Number of discrete colors to use in the new colormap.

Returns

matplotlib.colors.LinearSegmentedColormap A new colormap object with N discrete colors based on the input colormap. The name will be the original colormap name with "_N" appended.

Examples

from numpy import arange from numpy.ma import resize from matplotlib.pyplot import imshow from matplotlib.cm import jet x = resize(arange(100), (5, 100)) djet = cmap_discretize(jet, 5) imshow(x, cmap=djet)

Source code in monet/plots/colorbars.py
def cmap_discretize(cmap, N):
    """Return a discrete colormap from a continuous colormap.

    Creates a new colormap by discretizing an existing continuous colormap
    into N distinct colors while preserving the color transitions.

    Parameters
    ----------
    cmap : str or matplotlib.colors.Colormap
        Colormap instance or registered colormap name to discretize.
        Example: cm.jet, 'viridis', etc.
    N : int
        Number of discrete colors to use in the new colormap.

    Returns
    -------
    matplotlib.colors.LinearSegmentedColormap
        A new colormap object with N discrete colors based on the input colormap.
        The name will be the original colormap name with "_N" appended.

    Examples
    --------
    >>> from numpy import arange
    >>> from numpy.ma import resize
    >>> from matplotlib.pyplot import imshow
    >>> from matplotlib.cm import jet
    >>> x = resize(arange(100), (5, 100))
    >>> djet = cmap_discretize(jet, 5)
    >>> imshow(x, cmap=djet)
    """
    import matplotlib.colors as mcolors
    import numpy as np

    if isinstance(cmap, str):
        cmap = plt.get_cmap(cmap)
    colors_i = np.concatenate((np.linspace(0, 1.0, N), (0.0, 0.0, 0.0, 0.0)))
    colors_rgba = cmap(colors_i)
    indices = np.linspace(0, 1.0, N + 1)
    cdict = {}
    for ki, key in enumerate(("red", "green", "blue")):
        cdict[key] = [(indices[i], colors_rgba[i - 1, ki], colors_rgba[i, ki]) for i in range(N + 1)]
    # Return colormap object.
    return mcolors.LinearSegmentedColormap(f"{cmap.name}_{N}", cdict, 1024)

colorbar_index(ncolors, cmap, minval=None, maxval=None, dtype='int', ax=None, **kwargs)

Create a colorbar with discrete colors and custom tick labels.

Parameters

ncolors : int Number of discrete colors to use in the colorbar. cmap : str or matplotlib.colors.Colormap Colormap to discretize and use for the colorbar. minval : float, optional Minimum value for the colorbar tick labels. If None and maxval is None, tick labels will range from 0 to ncolors. If None and maxval is provided, tick labels will range from 0 to maxval. maxval : float, optional Maximum value for the colorbar tick labels. If None, tick labels will range from 0 or minval to ncolors. dtype : str or type, default "int" Data type for tick label values (e.g., "int", "float"). ax : matplotlib.axes.Axes, optional Axes to attach the colorbar to. **kwargs Additional keyword arguments to pass to plt.colorbar.

Returns

tuple (colorbar, discretized_cmap) where: - colorbar is the matplotlib.colorbar.Colorbar instance - discretized_cmap is the discretized colormap

Source code in monet/plots/colorbars.py
def colorbar_index(ncolors, cmap, minval=None, maxval=None, dtype="int", ax=None, **kwargs):
    """Create a colorbar with discrete colors and custom tick labels.

    Parameters
    ----------
    ncolors : int
        Number of discrete colors to use in the colorbar.
    cmap : str or matplotlib.colors.Colormap
        Colormap to discretize and use for the colorbar.
    minval : float, optional
        Minimum value for the colorbar tick labels. If None and maxval is None,
        tick labels will range from 0 to ncolors. If None and maxval is provided,
        tick labels will range from 0 to maxval.
    maxval : float, optional
        Maximum value for the colorbar tick labels. If None, tick labels
        will range from 0 or minval to ncolors.
    dtype : str or type, default "int"
        Data type for tick label values (e.g., "int", "float").
    ax : matplotlib.axes.Axes, optional
        Axes to attach the colorbar to.
    **kwargs
        Additional keyword arguments to pass to `plt.colorbar`.

    Returns
    -------
    tuple
        (colorbar, discretized_cmap) where:
        - colorbar is the matplotlib.colorbar.Colorbar instance
        - discretized_cmap is the discretized colormap
    """
    import matplotlib.cm as cm
    import numpy as np

    cmap = cmap_discretize(cmap, ncolors)
    mappable = cm.ScalarMappable(cmap=cmap)
    mappable.set_array([])
    mappable.set_clim(-0.5, ncolors + 0.5)
    colorbar = plt.colorbar(mappable, ax=ax, format="%1.2g", **kwargs)
    colorbar.set_ticks(np.linspace(0, ncolors, ncolors))
    if (minval is None) & (maxval is not None):
        colorbar.set_ticklabels(np.around(np.linspace(0, maxval, ncolors).astype(dtype), 2))
    elif (minval is None) & (maxval is None):
        colorbar.set_ticklabels(np.around(np.linspace(0, ncolors, ncolors).astype(dtype), 2))
    else:
        colorbar.set_ticklabels(np.around(np.linspace(minval, maxval, ncolors).astype(dtype), 2))

    return colorbar, cmap

kdeplot(df, title=None, label=None, ax=None, **kwargs)

Create a kernel density estimate plot.

Parameters

df : pandas.Series or array-like Data to plot the distribution of. title : str, optional Title for the plot. label : str, optional Label for the plotted line (for legend). ax : matplotlib.axes.Axes, optional Axes to plot on. If None, creates a new figure and axes. **kwargs Additional arguments passed to seaborn's kdeplot. Common options include 'shade', 'bw', and 'color'.

Returns

matplotlib.axes.Axes The axes containing the plot.

Source code in monet/plots/plots.py
@_default_sns_context
def kdeplot(df, title=None, label=None, ax=None, **kwargs):
    """Create a kernel density estimate plot.

    Parameters
    ----------
    df : pandas.Series or array-like
        Data to plot the distribution of.
    title : str, optional
        Title for the plot.
    label : str, optional
        Label for the plotted line (for legend).
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure and axes.
    **kwargs
        Additional arguments passed to seaborn's kdeplot.
        Common options include 'shade', 'bw', and 'color'.

    Returns
    -------
    matplotlib.axes.Axes
        The axes containing the plot.
    """
    with sns.axes_style("ticks"):
        if ax is None:
            f, ax = plt.subplots(figsize=(11, 6), frameon=False)
            sns.despine()
        ax = sns.kdeplot(df, ax=ax, label=label, **kwargs)

    return ax

normval(vmin, vmax, cmap)

Create a BoundaryNorm for discrete colormaps with specific bounds.

Parameters

vmin : float Minimum value for the colormap. vmax : float Maximum value for the colormap. cmap : matplotlib.colors.Colormap The colormap to create bounds for.

Returns

matplotlib.colors.BoundaryNorm A boundary norm with evenly spaced bounds from vmin to vmax in steps of 5.0.

Source code in monet/plots/plots.py
def normval(vmin, vmax, cmap):
    """Create a BoundaryNorm for discrete colormaps with specific bounds.

    Parameters
    ----------
    vmin : float
        Minimum value for the colormap.
    vmax : float
        Maximum value for the colormap.
    cmap : matplotlib.colors.Colormap
        The colormap to create bounds for.

    Returns
    -------
    matplotlib.colors.BoundaryNorm
        A boundary norm with evenly spaced bounds from vmin to vmax in steps of 5.0.
    """
    from matplotlib.colors import BoundaryNorm
    from numpy import arange

    bounds = arange(vmin, vmax + 5.0, 5.0)
    norm = BoundaryNorm(boundaries=bounds, ncolors=cmap.N)
    return norm

savefig(fname, *, fig=None, loc=1, decorate=True, logo=None, logo_height=None, **kwargs)

Save figure and add logo.

Parameters

fname : str Output file name or path. Passed to plt.savefig. Must include desired file extension (.jpg or .png). fig : matplotlib.figure.Figure, optional The figure to save. If None, the current figure (plt.gcf()) is used. loc : int The location for the logo.

* 1 -- bottom left (default)
* 2 -- bottom right
* 3 -- top right
* 4 -- top left

decorate : bool, default: True Whether to add the logo. logo : str, optional Path to the logo to be used. If not provided, the MONET logo is used. logo_height : float or int, optional Desired logo height in pixels. If not provided, the original logo image dimensions are used. Modify to scale the logo. **kwargs : dict Passed to the plt.savefig function.

Returns

None

Notes

This function replaces the previous pydecorate implementation with a direct Pillow implementation for greater flexibility and reduced dependencies.

Examples

import matplotlib.pyplot as plt plt.plot([1, 2], [3, 4]) savefig("plot.png", loc=1)

Source code in monet/plots/__init__.py
def savefig(
    fname: str,
    *,
    fig: Figure | None = None,
    loc: int = 1,
    decorate: bool = True,
    logo: str | None = None,
    logo_height: float | int | None = None,
    **kwargs: Any,
) -> None:
    """Save figure and add logo.

    Parameters
    ----------
    fname : str
        Output file name or path. Passed to ``plt.savefig``.
        Must include desired file extension (``.jpg`` or ``.png``).
    fig : matplotlib.figure.Figure, optional
        The figure to save. If None, the current figure (plt.gcf()) is used.
    loc : int
        The location for the logo.

        * 1 -- bottom left (default)
        * 2 -- bottom right
        * 3 -- top right
        * 4 -- top left
    decorate : bool, default: True
        Whether to add the logo.
    logo : str, optional
        Path to the logo to be used.
        If not provided, the MONET logo is used.
    logo_height : float or int, optional
        Desired logo height in pixels.
        If not provided, the original logo image dimensions are used.
        Modify to scale the logo.
    **kwargs : dict
        Passed to the ``plt.savefig`` function.

    Returns
    -------
    None

    Notes
    -----
    This function replaces the previous ``pydecorate`` implementation with a direct
    ``Pillow`` implementation for greater flexibility and reduced dependencies.

    Examples
    --------
    >>> import matplotlib.pyplot as plt
    >>> plt.plot([1, 2], [3, 4])
    >>> savefig("plot.png", loc=1)
    """
    parts = fname.split(".")
    if not len(parts) > 1:
        raise ValueError("`fname` must include a file extension, e.g. '.png'")
    ext = fname.split(".")[-1]

    # Save current figure
    if fig is None:
        fig = plt.gcf()
    fig.savefig(fname, **kwargs)

    # Add logo
    if decorate:
        if logo is None:
            logo = Path(__file__).parent / "../data/MONET-logo.png"
        if ext.lower() not in {"png", "jpg", "jpeg"}:
            raise ValueError(f"only PNG and JPEG supported, but detected extension is {ext!r}")

        img = Image.open(fname)
        logo_img = Image.open(logo)

        # Resize logo if requested
        if logo_height is not None:
            aspect_ratio = logo_img.width / logo_img.height
            logo_width = int(logo_height * aspect_ratio)
            logo_img = logo_img.resize((logo_width, int(logo_height)), Image.Resampling.LANCZOS)

        # Calculate position
        # 1 -- bottom left (default)
        # 2 -- bottom right
        # 3 -- top right
        # 4 -- top left
        if loc == 1:
            pos = (0, img.height - logo_img.height)
        elif loc == 2:
            pos = (img.width - logo_img.width, img.height - logo_img.height)
        elif loc == 3:
            pos = (img.width - logo_img.width, 0)
        elif loc == 4:
            pos = (0, 0)
        else:
            raise ValueError(f"invalid `loc` {loc!r}")

        # Paste logo (using the logo itself as mask for transparency if it has alpha)
        mask = logo_img if logo_img.mode in ("RGBA", "LA", "P") else None
        img.paste(logo_img, pos, mask)

        # PIL.Image will determine format from the filename extension
        img.save(fname)

        img.close()
        logo_img.close()

scatter(df, x=None, y=None, title=None, label=None, ax=None, **kwargs)

Create a scatter plot with regression line.

Parameters

df : pandas.DataFrame DataFrame containing the data to plot. x : str, optional Column name for x-axis values. y : str, optional Column name for y-axis values. title : str, optional Title for the plot. label : str, optional Label for the plot (for legend). ax : matplotlib.axes.Axes, optional Axes to plot on. If None, creates a new figure and axes. **kwargs Additional arguments passed to seaborn's regplot. Common options include 'scatter_kws', 'line_kws', and 'ci'.

Returns

matplotlib.axes.Axes The axes containing the plot.

Source code in monet/plots/plots.py
@_default_sns_context
def scatter(df, x=None, y=None, title=None, label=None, ax=None, **kwargs):
    """Create a scatter plot with regression line.

    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame containing the data to plot.
    x : str, optional
        Column name for x-axis values.
    y : str, optional
        Column name for y-axis values.
    title : str, optional
        Title for the plot.
    label : str, optional
        Label for the plot (for legend).
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure and axes.
    **kwargs
        Additional arguments passed to seaborn's regplot.
        Common options include 'scatter_kws', 'line_kws', and 'ci'.

    Returns
    -------
    matplotlib.axes.Axes
        The axes containing the plot.
    """
    with sns.axes_style("ticks"):
        if ax is None:
            f, ax = plt.subplots(figsize=(8, 6), frameon=False)
        ax = sns.regplot(data=df, x=x, y=y, label=label, **kwargs)
        plt.title(title)

    return ax

sp_scatter_bias(df, col1=None, col2=None, ax=None, outline=False, tight=True, global_map=True, map_kwargs=None, cbar_kwargs=None, val_max=None, val_min=None, **kwargs)

Create a spatial scatter plot showing the bias (difference) between two columns in a DataFrame.

Parameters

df : pandas.DataFrame DataFrame containing latitude, longitude, and data columns to compare. col1 : str Name of the first column (reference value). col2 : str Name of the second column (comparison value). ax : matplotlib.axes.Axes, optional Axes to plot on. If None, creates a new map using draw_map. outline : bool, default False Whether to show the map outline. tight : bool, default True Whether to apply tight_layout to the figure. global_map : bool, default True Whether to set global map boundaries (-180 to 180 longitude, -90 to 90 latitude). map_kwargs : dict, default {} Keyword arguments passed to draw_map if creating a new map. cbar_kwargs : dict, default {} Keyword arguments for colorbar customization. val_max : float, optional Maximum value for color scaling. If None, uses 95th percentile of absolute differences. val_min : float, optional Minimum value for color scaling (not currently used). **kwargs : dict Additional keyword arguments passed to DataFrame.plot.scatter.

Returns

matplotlib.axes.Axes The axes object containing the plot.

Notes

The point size is scaled by the magnitude of the difference between col2 and col1, making larger differences more visually prominent. Differences are capped at 300 units for display purposes.

Examples

import pandas as pd df = pd.DataFrame({'latitude': [40, 41], 'longitude': [-70, -71], 'obs': [1, 2], 'mod': [1.1, 1.9]}) ax = sp_scatter_bias(df, col1='obs', col2='mod')

Source code in monet/plots/__init__.py
def sp_scatter_bias(
    df: pd.DataFrame,
    col1: str | None = None,
    col2: str | None = None,
    ax: Axes | None = None,
    outline: bool = False,
    tight: bool = True,
    global_map: bool = True,
    map_kwargs: dict[str, Any] | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    val_max: float | None = None,
    val_min: float | None = None,
    **kwargs: Any,
) -> Axes:
    """Create a spatial scatter plot showing the bias (difference) between two columns in a DataFrame.

    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame containing latitude, longitude, and data columns to compare.
    col1 : str
        Name of the first column (reference value).
    col2 : str
        Name of the second column (comparison value).
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new map using draw_map.
    outline : bool, default False
        Whether to show the map outline.
    tight : bool, default True
        Whether to apply tight_layout to the figure.
    global_map : bool, default True
        Whether to set global map boundaries (-180 to 180 longitude, -90 to 90 latitude).
    map_kwargs : dict, default {}
        Keyword arguments passed to draw_map if creating a new map.
    cbar_kwargs : dict, default {}
        Keyword arguments for colorbar customization.
    val_max : float, optional
        Maximum value for color scaling. If None, uses 95th percentile of absolute differences.
    val_min : float, optional
        Minimum value for color scaling (not currently used).
    **kwargs : dict
        Additional keyword arguments passed to DataFrame.plot.scatter.

    Returns
    -------
    matplotlib.axes.Axes
        The axes object containing the plot.

    Notes
    -----
    The point size is scaled by the magnitude of the difference between col2 and col1,
    making larger differences more visually prominent. Differences are capped at 300 units
    for display purposes.

    Examples
    --------
    >>> import pandas as pd
    >>> df = pd.DataFrame({'latitude': [40, 41], 'longitude': [-70, -71], 'obs': [1, 2], 'mod': [1.1, 1.9]})
    >>> ax = sp_scatter_bias(df, col1='obs', col2='mod')
    """
    if map_kwargs is None:
        map_kwargs = {}
    if cbar_kwargs is None:
        cbar_kwargs = {}

    if ax is None:
        ax = draw_map(**map_kwargs)

    if col1 is None or col2 is None:
        raise ValueError("User must specify col1 and col2 in the dataframe")

    dfnew = df[["latitude", "longitude", col1, col2]].dropna().copy(deep=True)
    dfnew["sp_diff"] = dfnew[col2] - dfnew[col1]
    top = score(dfnew["sp_diff"].abs(), per=95)
    if val_max is not None:
        top = val_max
    # x, y = df.longitude.values, df.latitude.values
    dfnew["sp_diff_size"] = dfnew["sp_diff"].abs() / top * 100.0
    dfnew.loc[dfnew["sp_diff_size"] > 300, "sp_diff_size"] = 300.0
    dfnew.plot.scatter(
        x="longitude",
        y="latitude",
        c=dfnew["sp_diff"],
        s=dfnew["sp_diff_size"],
        vmin=-1 * top,
        vmax=top,
        ax=ax,
        colorbar=True,
        **kwargs,
    )
    if not outline:
        _set_outline_patch_alpha(ax)
    if global_map:
        plt.xlim([-180, 180])
        plt.ylim([-90, 90])
    if tight:
        plt.tight_layout(pad=0)
    return ax

spatial(da, fig=None, ax=None, **kwargs)

Create a spatial plot from an xarray.DataArray.

A convenience wrapper for xarray's plot method with consistent styling.

.. deprecated:: 24.8.1 This function is deprecated and will be removed in a future version. Please use spatial_plot and add map features like coastlines and gridlines manually for more control.

Parameters

da : xr.DataArray The data to plot spatially. fig : matplotlib.figure.Figure, optional Figure to plot on. ax : plt.Axes, optional Axes to plot on. If None, a new figure and axes will be created. **kwargs Additional keyword arguments passed to xarray's plot method.

Returns

t.Tuple[plt.Figure, plt.Axes] The figure and axes containing the plot.

Source code in monet/plots/plots.py
@_default_sns_context
def spatial(
    da: xr.DataArray,
    fig: plt.Figure | None = None,
    ax: plt.Axes | None = None,
    **kwargs,
) -> tuple[plt.Figure, plt.Axes]:
    """Create a spatial plot from an xarray.DataArray.

    A convenience wrapper for xarray's plot method with consistent styling.

    .. deprecated:: 24.8.1
        This function is deprecated and will be removed in a future version.
        Please use `spatial_plot` and add map features like coastlines
        and gridlines manually for more control.

    Parameters
    ----------
    da : xr.DataArray
        The data to plot spatially.
    fig : matplotlib.figure.Figure, optional
        Figure to plot on.
    ax : plt.Axes, optional
        Axes to plot on. If None, a new figure and axes will be created.
    **kwargs
        Additional keyword arguments passed to xarray's plot method.

    Returns
    -------
    t.Tuple[plt.Figure, plt.Axes]
        The figure and axes containing the plot.
    """
    warnings.warn(
        "The function `spatial` is deprecated and will be removed in a future version. Please use `spatial_plot` instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    fig, ax = spatial_plot(da, fig=fig, ax=ax, **kwargs)
    ax.coastlines()
    ax.gridlines()
    return fig, ax

spatial_bias_scatter(ds, *, vmin=None, vmax=None, savename='', cmap='RdBu_r', fig=None, ax=None, **kwargs)

Create a scatter plot showing bias on a map.

Parameters

ds : xr.Dataset Dataset containing 'obs' and 'model' variables, and 'latitude' and 'longitude' coordinates. vmin : float, optional Minimum value for colorscale. If None, automatically determined. vmax : float, optional Maximum value for colorscale. If None, automatically determined. savename : str, default "" If provided, save the figure to this path. cmap : str or matplotlib.colors.Colormap, default "RdBu_r" Colormap to use for bias values. fig : matplotlib.figure.Figure, optional Figure to plot on. ax : matplotlib.axes.Axes, optional Axes to plot on. **kwargs Additional keyword arguments to pass to xarray.plot.scatter.

Returns

t.Tuple[plt.Figure, plt.Axes] The figure and axes containing the plot.

Notes

The scatter points are colored by the difference (model - obs) and sized by the absolute magnitude of this difference, making larger biases more visible.

Source code in monet/plots/plots.py
@_default_sns_context
def spatial_bias_scatter(
    ds: xr.Dataset,
    *,
    vmin: float | None = None,
    vmax: float | None = None,
    savename: str = "",
    cmap: str = "RdBu_r",
    fig: plt.Figure | None = None,
    ax: plt.Axes | None = None,
    **kwargs,
) -> tuple[plt.Figure, plt.Axes]:
    """Create a scatter plot showing bias on a map.

    Parameters
    ----------
    ds : xr.Dataset
        Dataset containing 'obs' and 'model' variables, and 'latitude' and
        'longitude' coordinates.
    vmin : float, optional
        Minimum value for colorscale. If None, automatically determined.
    vmax : float, optional
        Maximum value for colorscale. If None, automatically determined.
    savename : str, default ""
        If provided, save the figure to this path.
    cmap : str or matplotlib.colors.Colormap, default "RdBu_r"
        Colormap to use for bias values.
    fig : matplotlib.figure.Figure, optional
        Figure to plot on.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on.
    **kwargs
        Additional keyword arguments to pass to `xarray.plot.scatter`.

    Returns
    -------
    t.Tuple[plt.Figure, plt.Axes]
        The figure and axes containing the plot.

    Notes
    -----
    The scatter points are colored by the difference (model - obs) and sized
    by the absolute magnitude of this difference, making larger biases more visible.
    """
    fig, ax = _create_map(fig=fig, ax=ax)
    ax.set_facecolor("white")

    # Create a new dataset for plotting to avoid modifying the original
    plot_ds = ds.copy(deep=False)
    plot_ds["difference"] = ds["model"] - ds["obs"]

    # Calculate size based on absolute difference.
    # A zero size is invisible, so we add a minimum size and scale.
    # The scaling factor is arbitrary and can be adjusted for better visualization.
    size = np.abs(plot_ds["difference"])
    # Avoid division by zero if all differences are zero
    if size.max() > 0:
        size = (size / size.max()) * 200 + 20
    else:
        size = xr.full_like(size, 20)

    plot_ds.plot.scatter(
        ax=ax,
        x="longitude",
        y="latitude",
        hue="difference",
        s=size,
        vmin=vmin,
        vmax=vmax,
        cmap=cmap,
        edgecolors="k",
        linewidths=0.25,
        alpha=0.7,
        transform=ccrs.PlateCarree(),
        **kwargs,
    )

    _savefig(fig, save_name=savename)
    return fig, ax

spatial_contourf(da, ax=None, **kwargs)

Create a spatial plot from an xarray.DataArray using contourf.

Parameters

da : xarray.DataArray The data to plot. ax : matplotlib.axes.Axes, optional Axes to plot on. If None, a new figure and axes will be created. **kwargs Additional keyword arguments to pass to xarray's plot.contourf() method.

Returns

t.Tuple[plt.Figure, plt.Axes] The figure and axes containing the plot.

Source code in monet/plots/plots.py
@_default_sns_context
def spatial_contourf(
    da: xr.DataArray,
    ax: plt.Axes | None = None,
    **kwargs,
) -> tuple[plt.Figure, plt.Axes]:
    """Create a spatial plot from an xarray.DataArray using contourf.

    Parameters
    ----------
    da : xarray.DataArray
        The data to plot.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, a new figure and axes will be created.
    **kwargs
        Additional keyword arguments to pass to xarray's plot.contourf() method.

    Returns
    -------
    t.Tuple[plt.Figure, plt.Axes]
        The figure and axes containing the plot.
    """
    fig, ax = _create_map(ax=ax)
    da.plot.contourf(ax=ax, transform=ccrs.PlateCarree(), **kwargs)
    ax.coastlines()
    ax.gridlines()
    return fig, ax

spatial_imshow(da, ax=None, **kwargs)

Create a spatial plot from an xarray.DataArray using imshow.

Parameters

da : xarray.DataArray The data to plot. ax : matplotlib.axes.Axes, optional Axes to plot on. If None, a new figure and axes will be created. **kwargs Additional keyword arguments to pass to xarray's plot.imshow() method.

Returns

t.Tuple[plt.Figure, plt.Axes] The figure and axes containing the plot.

Source code in monet/plots/plots.py
@_default_sns_context
def spatial_imshow(
    da: xr.DataArray,
    ax: plt.Axes | None = None,
    **kwargs,
) -> tuple[plt.Figure, plt.Axes]:
    """Create a spatial plot from an xarray.DataArray using imshow.

    Parameters
    ----------
    da : xarray.DataArray
        The data to plot.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, a new figure and axes will be created.
    **kwargs
        Additional keyword arguments to pass to xarray's plot.imshow() method.

    Returns
    -------
    t.Tuple[plt.Figure, plt.Axes]
        The figure and axes containing the plot.
    """
    fig, ax = _create_map(ax=ax)
    da.plot.imshow(ax=ax, transform=ccrs.PlateCarree(), **kwargs)
    ax.coastlines()
    ax.gridlines()
    return fig, ax

timeseries(df, x='time', y='obs', ax=None, plotargs=None, fillargs=None, title='', ylabel=None, label=None)

Create a timeseries plot with shaded error bounds.

Parameters

df : pd.DataFrame DataFrame containing the data to plot. x : str, default "time" Column name to use for the x-axis (time). y : str, default "obs" Column name to use for the y-axis (values to plot). ax : plt.Axes, optional Axes to plot on. If None, creates a new figure and axes. plotargs : dict, optional Additional arguments to pass to DataFrame.plot(). fillargs : dict, optional Additional arguments to pass to fill_between for the error shading. Defaults to {"alpha": 0.2}. title : str, default "" Title for the plot. ylabel : str, optional Y-axis label. If None, uses variable name and units from DataFrame. label : str, optional Label for the plotted line (for legend). If None, uses y.

Returns

plt.Axes The axes containing the plot.

Notes

This function groups the data by time, plots the mean values, and adds shading for ±1 standard deviation around the mean.

Source code in monet/plots/plots.py
@_default_sns_context
def timeseries(
    df: "pd.DataFrame",
    x: str = "time",
    y: str = "obs",
    ax: plt.Axes | None = None,
    plotargs: dict[str, t.Any] | None = None,
    fillargs: dict[str, t.Any] | None = None,
    title: str = "",
    ylabel: str | None = None,
    label: str | None = None,
) -> plt.Axes:
    """Create a timeseries plot with shaded error bounds.

    Parameters
    ----------
    df : pd.DataFrame
        DataFrame containing the data to plot.
    x : str, default "time"
        Column name to use for the x-axis (time).
    y : str, default "obs"
        Column name to use for the y-axis (values to plot).
    ax : plt.Axes, optional
        Axes to plot on. If None, creates a new figure and axes.
    plotargs : dict, optional
        Additional arguments to pass to DataFrame.plot().
    fillargs : dict, optional
        Additional arguments to pass to fill_between for the error shading.
        Defaults to `{"alpha": 0.2}`.
    title : str, default ""
        Title for the plot.
    ylabel : str, optional
        Y-axis label. If None, uses variable name and units from DataFrame.
    label : str, optional
        Label for the plotted line (for legend). If None, uses `y`.

    Returns
    -------
    plt.Axes
        The axes containing the plot.

    Notes
    -----
    This function groups the data by time, plots the mean values, and adds
    shading for ±1 standard deviation around the mean.
    """

    if plotargs is None:
        plotargs = {}
    if fillargs is None:
        fillargs = {"alpha": 0.2}

    with sns.axes_style("ticks"):
        if ax is None:
            _, ax = plt.subplots(figsize=(11, 6), frameon=False)

        # Group by the specified time column
        grouped = df.groupby(x)
        m = grouped.mean(numeric_only=True)
        e = grouped.std(numeric_only=True)

        variable = df["variable"].iloc[0] if "variable" in df.columns else ""
        unit = df["units"].iloc[0] if "units" in df.columns else "None"

        upper = m[y] + e[y]
        lower = m[y] - e[y]
        lower.loc[lower < 0] = 0

        plot_label = label if label is not None else y
        m = m.rename(columns={y: plot_label})

        m[plot_label].plot(ax=ax, **plotargs)
        ax.fill_between(m.index, lower.values, upper.values, **fillargs)

        if ylabel is None:
            ax.set_ylabel(f"{variable} ({unit})")
        else:
            ax.set_ylabel(ylabel)

        ax.set_xlabel("")
        ax.legend()
        ax.set_title(title)
        plt.tight_layout()

    return ax

wind_barbs(u, v, ax=None, thin=15, **kwargs)

Create a barbs plot of wind on a map.

Parameters

u : xr.DataArray 2D array of u-component of wind. v : xr.DataArray 2D array of v-component of wind. ax : plt.Axes, optional Axes to plot on. thin : int, optional The thinning factor for the wind vectors. Default is 15. **kwargs Additional arguments to pass to barbs. Common options include 'length', 'pivot', 'barb_increments'.

Returns

t.Tuple[plt.Figure, plt.Axes] The figure and axes objects.

Source code in monet/plots/plots.py
@_default_sns_context
def wind_barbs(
    u: xr.DataArray,
    v: xr.DataArray,
    ax: plt.Axes = None,
    thin: int = 15,
    **kwargs,
) -> tuple[plt.Figure, plt.Axes]:
    """Create a barbs plot of wind on a map.

    Parameters
    ----------
    u : xr.DataArray
        2D array of u-component of wind.
    v : xr.DataArray
        2D array of v-component of wind.
    ax : plt.Axes, optional
        Axes to plot on.
    thin : int, optional
        The thinning factor for the wind vectors. Default is 15.
    **kwargs
        Additional arguments to pass to barbs. Common options include
        'length', 'pivot', 'barb_increments'.

    Returns
    -------
    t.Tuple[plt.Figure, plt.Axes]
        The figure and axes objects.
    """
    if ax is None:
        fig, ax = _create_map(ax=ax)
    else:
        fig = ax.figure

    u_thinned, v_thinned, x2d, y2d = _thin_data(u, v, thin)

    # define map and draw boundaries
    ax.barbs(
        x2d,
        y2d,
        u_thinned.values,
        v_thinned.values,
        transform=ccrs.PlateCarree(),
        **kwargs,
    )
    return fig, ax

wind_quiver(u, v, ax=None, thin=15, **kwargs)

Create a quiver plot of wind vectors on a map.

Parameters

u : xr.DataArray 2D array of u-component of wind. v : xr.DataArray 2D array of v-component of wind. ax : plt.Axes, optional Axes to plot on. thin : int, optional The thinning factor for the wind vectors. Default is 15. **kwargs Additional arguments to pass to quiver. Common options include 'scale', 'scale_units', and 'width'.

Returns

t.Tuple[plt.Figure, plt.Axes] The figure and axes objects.

Source code in monet/plots/plots.py
@_default_sns_context
def wind_quiver(
    u: xr.DataArray,
    v: xr.DataArray,
    ax: plt.Axes = None,
    thin: int = 15,
    **kwargs,
) -> tuple[plt.Figure, plt.Axes]:
    """Create a quiver plot of wind vectors on a map.

    Parameters
    ----------
    u : xr.DataArray
        2D array of u-component of wind.
    v : xr.DataArray
        2D array of v-component of wind.
    ax : plt.Axes, optional
        Axes to plot on.
    thin : int, optional
        The thinning factor for the wind vectors. Default is 15.
    **kwargs
        Additional arguments to pass to quiver. Common options include
        'scale', 'scale_units', and 'width'.

    Returns
    -------
    t.Tuple[plt.Figure, plt.Axes]
        The figure and axes objects.
    """
    if ax is None:
        fig, ax = _create_map(ax=ax)
    else:
        fig = ax.figure

    u_thinned, v_thinned, x2d, y2d = _thin_data(u, v, thin)

    # define map and draw boundaries
    ax.quiver(
        x2d,
        y2d,
        u_thinned.values,
        v_thinned.values,
        transform=ccrs.PlateCarree(),
        **kwargs,
    )
    return fig, ax

Accessors

MONET extends xarray and pandas objects via accessors. These methods are available through the .monet attribute on the respective objects.

DataArray Accessor

Bases: BaseAccessor

DataArray accessor for MONET functionality.

Source code in monet/accessors/dataarray_accessor.py
@xr.register_dataarray_accessor("monet")
class MONETAccessor(BaseAccessor):
    """DataArray accessor for MONET functionality."""

    def __init__(self, xray_obj):
        """Initialize the accessor.

        Parameters
        ----------
        xray_obj : xarray.DataArray
            The DataArray this accessor will work with.
        """
        self._obj = xray_obj

    def remap_nearest_parallel(self, data, radius_of_influence=1e6, n_processes=None, **kwargs):
        """Deprecated: Remap data using nearest neighbor interpolation with parallel processing."""
        warnings.warn(
            "remap_nearest_parallel is deprecated. xregrid uses dask for parallelization.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.remap(data, method="nearest", **kwargs)

    def stratify(self, levels, vertical, axis=1, tension=0.0):
        """Deprecated: use ``interpolate_vertical`` instead."""
        import warnings

        warnings.warn(
            "stratify() is deprecated. Use interpolate_vertical(target_levels, level_dim) instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        level_dim = vertical if isinstance(vertical, str) else (vertical.name or self._obj.dims[axis])
        return self.interpolate_vertical(np.asarray(levels), level_dim=level_dim, tension=tension)

    def interpolate_vertical(self, target_levels, level_dim: str = "level", tension: float = 0.0):
        """Vertically interpolate data to new levels using pytspack tension splines.

        Parameters
        ----------
        target_levels : array-like
            Target vertical level values.
        level_dim : str, default: ``"level"``
            Name of the vertical dimension in the DataArray.
        tension : float, default: ``0.0``
            Tension factor for the spline. ``0.0`` gives a standard cubic spline.

        Returns
        -------
        xarray.DataArray
            Data interpolated to ``target_levels``.

        Examples
        --------
        >>> da.monet.interpolate_vertical([850, 700, 500], level_dim='pressure')
        """
        from pytspack import interpolate_vertical

        da = self._obj
        if hasattr(da.data, "chunks") and level_dim in da.dims:
            da = da.chunk({level_dim: -1})
        return interpolate_vertical(da, target_levels, level_dim=level_dim, tension=tension)

    def nearest_ij(self, lat=None, lon=None, **kwargs):
        """Find the nearest grid indices to given lat/lon point(s).

        Parameters
        ----------
        lat : float or array-like, optional
            Latitude value(s).
        lon : float or array-like, optional
            Longitude value(s).
        **kwargs : dict
            Additional keyword arguments.

        Returns
        -------
        tuple
            (i, j) indices of nearest point(s).
        """
        raise NotImplementedError("nearest_ij is not yet implemented with xregrid")

    def quick_imshow(
        self,
        map_kws: dict[str, t.Any] | None = None,
        roll_dateline: bool = False,
        projection: t.Any | None = None,
        colorbar: bool = True,
        figsize: tuple[float, float] | None = None,
        **kwargs: t.Any,
    ) -> tuple[plt.Figure, plt.Axes]:
        """Create a quick imshow plot of the data with flexible options.
        Convention-aware: supports both CF/COARDS and UGRID.
        """
        from ..plots.cartopy_utils import plot_quick_imshow

        return plot_quick_imshow(
            self._obj,
            map_kws=map_kws,
            projection=projection,
            colorbar=colorbar,
            figsize=figsize,
            **kwargs,
        )

    def quick_map(
        self,
        map_kws: dict[str, t.Any] | None = None,
        roll_dateline: bool = False,
        projection: t.Any | None = None,
        colorbar: bool = True,
        figsize: tuple[float, float] | None = None,
        **kwargs: t.Any,
    ) -> tuple[plt.Figure, plt.Axes]:
        """Create a quick map plot of the data with flexible options.
        Convention-aware: supports both CF/COARDS and UGRID.
        """
        from ..plots.cartopy_utils import plot_quick_map

        return plot_quick_map(
            self._obj,
            map_kws=map_kws,
            projection=projection,
            colorbar=colorbar,
            figsize=figsize,
            **kwargs,
        )

    def quick_contourf(
        self,
        map_kws: dict[str, t.Any] | None = None,
        roll_dateline: bool = False,
        projection: t.Any | None = None,
        colorbar: bool = True,
        figsize: tuple[float, float] | None = None,
        **kwargs: t.Any,
    ) -> tuple[plt.Figure, plt.Axes]:
        """Create a quick filled contour plot of the data with flexible options.
        Convention-aware: supports both CF/COARDS and UGRID.
        """
        from ..plots.cartopy_utils import plot_quick_contourf

        return plot_quick_contourf(
            self._obj,
            map_kws=map_kws,
            projection=projection,
            colorbar=colorbar,
            figsize=figsize,
            **kwargs,
        )

    def _tight_layout(self):
        """Apply tight layout to the current figure.

        Returns
        -------
        None
        """
        from matplotlib.pyplot import subplots_adjust

        subplots_adjust(0, 0, 1, 1)

    def remap_nearest(self, data, radius_of_influence=1e6, **kwargs):
        """Deprecated: Remap data using nearest neighbor interpolation."""
        warnings.warn(
            "remap_nearest is deprecated and will be removed in a future version. "
            "Please use remap(data, method='nearest') instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.remap(data, method="nearest", radius_of_influence=radius_of_influence, **kwargs)

    def compare(
        self,
        other: xr.DataArray,
        stat: str | t.Callable = "diff",
        plot: bool = True,
        plot_method: str = "quick_map",
        stat_kwargs: dict[str, t.Any] | None = None,
        plot_kwargs: dict[str, t.Any] | None = None,
    ) -> xr.DataArray | tuple[plt.Figure, plt.Axes]:
        """
        Compute and optionally plot a statistic between this DataArray and another.
        Leverages MONET's monet_stats metrics and preserves Dask laziness.

        Parameters
        ----------
        other : xarray.DataArray
            The other DataArray to compare with.
        stat : str or callable, default: "diff"
            Statistic to compute. Can be any metric name from monet_stats
            (e.g., "RMSE", "MB", "NMB", "IOA", etc.), "diff", or a callable.
        plot : bool, default: True
            Whether to plot the result using a MONET quick plot method.
        plot_method : str, default: "quick_map"
            Which plotting method to use
            (e.g., "quick_map", "quick_imshow", "quick_contourf").
        stat_kwargs : dict, optional
            Additional kwargs for the statistic function.
        plot_kwargs : dict, optional
            Additional kwargs for the plotting function.

        Returns
        -------
        xarray.DataArray or (fig, ax)
            The statistic DataArray, or (fig, ax) if plot=True.
        """

        stat_kwargs = stat_kwargs or {}
        plot_kwargs = plot_kwargs or {}
        da1 = self._obj
        da2 = other
        # Align DataArrays
        da1, da2 = xr.align(da1, da2, join="inner")
        # Compute statistic
        stat_da = None
        if callable(stat):
            stat_da = stat(da1, da2, **stat_kwargs)
        elif isinstance(stat, str):
            if stat.lower() == "diff":
                stat_da = da1 - da2
            else:
                # Detect Dask-backed inputs — prefer lazy built-in implementations
                # when possible so the graph is never materialised prematurely.
                _is_dask = hasattr(da1.data, "chunks")
                _stat_lower = stat.lower()
                _lazy_stats = {"rmse", "mae", "mse"}

                if _is_dask and _stat_lower in _lazy_stats:
                    # Always use the lazy path for Dask inputs
                    if _stat_lower == "rmse":
                        stat_da = (((da1 - da2) ** 2).mean(dim=stat_kwargs.get("dim", None))) ** 0.5
                    elif _stat_lower == "mae":
                        stat_da = (da1 - da2).pipe(abs).mean(dim=stat_kwargs.get("dim", None))
                    elif _stat_lower == "mse":
                        stat_da = ((da1 - da2) ** 2).mean(dim=stat_kwargs.get("dim", None))
                else:
                    # Try monet_stats with case-insensitive lookup (uppercase
                    # variants like RMSE are lazy; lowercase are not).
                    try:
                        import monet_stats

                        # Prefer uppercase variant if available (lazy)
                        func = getattr(monet_stats, stat.upper(), None) or getattr(monet_stats, stat, None)
                        if func is None:
                            raise AttributeError(stat)
                        stat_da = func(da1, da2, **stat_kwargs)
                    except (ImportError, AttributeError) as e:
                        if _stat_lower == "rmse":
                            stat_da = (((da1 - da2) ** 2).mean(dim=stat_kwargs.get("dim", None))) ** 0.5
                        elif _stat_lower == "mae":
                            stat_da = (da1 - da2).pipe(abs).mean(dim=stat_kwargs.get("dim", None))
                        elif _stat_lower == "mse":
                            stat_da = ((da1 - da2) ** 2).mean(dim=stat_kwargs.get("dim", None))
                        else:
                            raise ValueError(f"Unknown stat: {stat}") from e
        else:
            raise ValueError(f"Unknown stat: {stat}")

        # Ensure stat_da is a DataArray and set name
        if not isinstance(stat_da, xr.DataArray):
            # Convert scalar to DataArray if needed
            stat_da = xr.DataArray(stat_da)

        stat_name = stat if isinstance(stat, str) else getattr(stat, "__name__", "statistic")
        stat_da.name = stat_name

        # Update history
        from ..util.conventions import update_history

        update_history(stat_da, f"Computed comparison statistic: {stat_name}")

        if plot:
            plot_func = getattr(stat_da.monet, plot_method)
            return plot_func(**plot_kwargs)
        else:
            return stat_da

    def quick_facet_time_map(
        self,
        map_kws: dict[str, t.Any] | None = None,
        projection: t.Any | None = None,
        colorbar: bool = True,
        figsize: tuple[float, float] | None = None,
        cmap: str | t.Any | None = None,
        vmin: float | None = None,
        vmax: float | None = None,
        norm: t.Any | None = None,
        dpi: int = 150,
        xlabel: str | None = None,
        ylabel: str | None = None,
        suptitle: str | None = None,
        cbar_label: str | None = None,
        xticks: list[float] | None = None,
        yticks: list[float] | None = None,
        annotations: list[dict[str, t.Any]] | None = None,
        export_path: str | None = None,
        export_formats: list[str] | None = None,
        time_dim: str = "time",
        ncols: int = 3,
        **kwargs: t.Any,
    ) -> tuple[plt.Figure, np.ndarray]:
        """
        Create a facet grid of map plots for each time slice in a DataArray using Cartopy.
        Convention-aware: supports both CF/COARDS and UGRID.

        Parameters
        ----------
        map_kws : dict, optional
            Dictionary of keyword arguments for map features.
        projection : cartopy.crs.Projection, optional
            Cartopy projection to use. Defaults to PlateCarree.
        colorbar : bool, default: True
            Whether to add a colorbar (shared).
        figsize : tuple, optional
            Figure size.
        cmap : str or Colormap, optional
            Colormap to use.
        vmin, vmax : float, optional
            Color limits.
        norm : Normalize, optional
            Matplotlib normalization.
        dpi : int, optional
            Dots per inch for export.
        xlabel, ylabel, suptitle : str, optional
            Axis labels and super title.
        cbar_label : str, optional
            Label for the colorbar.
        xticks, yticks : list, optional
            Custom tick locations.
        annotations : list of dict, optional
            List of annotation dicts for each subplot.
        export_path : str, optional
            Path to export the figure (without extension).
        export_formats : list, optional
            List of formats to export (e.g., ["png", "pdf"]).
        time_dim : str, default: "time"
            Name of the time dimension.
        ncols : int, default: 3
            Number of columns in the facet grid.
        **kwargs : dict
            Additional keyword arguments for plotting.

        Returns
        -------
        fig : matplotlib.figure.Figure
            The matplotlib figure object.
        axes : ndarray of matplotlib.axes.Axes
            The matplotlib axes objects.
        """
        from ..plots.cartopy_utils import facet_time_map

        return facet_time_map(
            self._obj,
            time_dim=time_dim,
            ncols=ncols,
            map_kws=map_kws,
            projection=projection,
            colorbar=colorbar,
            figsize=figsize,
            cmap=cmap,
            vmin=vmin,
            vmax=vmax,
            norm=norm,
            dpi=dpi,
            xlabel=xlabel,
            ylabel=ylabel,
            suptitle=suptitle,
            cbar_label=cbar_label,
            xticks=xticks,
            yticks=yticks,
            annotations=annotations,
            export_path=export_path,
            export_formats=export_formats,
            **kwargs,
        )

Functions

interpolate_vertical(target_levels, level_dim='level', tension=0.0)

Vertically interpolate data to new levels using pytspack tension splines.

Parameters

target_levels : array-like Target vertical level values. level_dim : str, default: "level" Name of the vertical dimension in the DataArray. tension : float, default: 0.0 Tension factor for the spline. 0.0 gives a standard cubic spline.

Returns

xarray.DataArray Data interpolated to target_levels.

Examples

da.monet.interpolate_vertical([850, 700, 500], level_dim='pressure')

Source code in monet/accessors/dataarray_accessor.py
def interpolate_vertical(self, target_levels, level_dim: str = "level", tension: float = 0.0):
    """Vertically interpolate data to new levels using pytspack tension splines.

    Parameters
    ----------
    target_levels : array-like
        Target vertical level values.
    level_dim : str, default: ``"level"``
        Name of the vertical dimension in the DataArray.
    tension : float, default: ``0.0``
        Tension factor for the spline. ``0.0`` gives a standard cubic spline.

    Returns
    -------
    xarray.DataArray
        Data interpolated to ``target_levels``.

    Examples
    --------
    >>> da.monet.interpolate_vertical([850, 700, 500], level_dim='pressure')
    """
    from pytspack import interpolate_vertical

    da = self._obj
    if hasattr(da.data, "chunks") and level_dim in da.dims:
        da = da.chunk({level_dim: -1})
    return interpolate_vertical(da, target_levels, level_dim=level_dim, tension=tension)

stratify(levels, vertical, axis=1, tension=0.0)

Deprecated: use interpolate_vertical instead.

Source code in monet/accessors/dataarray_accessor.py
def stratify(self, levels, vertical, axis=1, tension=0.0):
    """Deprecated: use ``interpolate_vertical`` instead."""
    import warnings

    warnings.warn(
        "stratify() is deprecated. Use interpolate_vertical(target_levels, level_dim) instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    level_dim = vertical if isinstance(vertical, str) else (vertical.name or self._obj.dims[axis])
    return self.interpolate_vertical(np.asarray(levels), level_dim=level_dim, tension=tension)

nearest_ij(lat=None, lon=None, **kwargs)

Find the nearest grid indices to given lat/lon point(s).

Parameters

lat : float or array-like, optional Latitude value(s). lon : float or array-like, optional Longitude value(s). **kwargs : dict Additional keyword arguments.

Returns

tuple (i, j) indices of nearest point(s).

Source code in monet/accessors/dataarray_accessor.py
def nearest_ij(self, lat=None, lon=None, **kwargs):
    """Find the nearest grid indices to given lat/lon point(s).

    Parameters
    ----------
    lat : float or array-like, optional
        Latitude value(s).
    lon : float or array-like, optional
        Longitude value(s).
    **kwargs : dict
        Additional keyword arguments.

    Returns
    -------
    tuple
        (i, j) indices of nearest point(s).
    """
    raise NotImplementedError("nearest_ij is not yet implemented with xregrid")

quick_facet_time_map(map_kws=None, projection=None, colorbar=True, figsize=None, cmap=None, vmin=None, vmax=None, norm=None, dpi=150, xlabel=None, ylabel=None, suptitle=None, cbar_label=None, xticks=None, yticks=None, annotations=None, export_path=None, export_formats=None, time_dim='time', ncols=3, **kwargs)

Create a facet grid of map plots for each time slice in a DataArray using Cartopy. Convention-aware: supports both CF/COARDS and UGRID.

Parameters

map_kws : dict, optional Dictionary of keyword arguments for map features. projection : cartopy.crs.Projection, optional Cartopy projection to use. Defaults to PlateCarree. colorbar : bool, default: True Whether to add a colorbar (shared). figsize : tuple, optional Figure size. cmap : str or Colormap, optional Colormap to use. vmin, vmax : float, optional Color limits. norm : Normalize, optional Matplotlib normalization. dpi : int, optional Dots per inch for export. xlabel, ylabel, suptitle : str, optional Axis labels and super title. cbar_label : str, optional Label for the colorbar. xticks, yticks : list, optional Custom tick locations. annotations : list of dict, optional List of annotation dicts for each subplot. export_path : str, optional Path to export the figure (without extension). export_formats : list, optional List of formats to export (e.g., ["png", "pdf"]). time_dim : str, default: "time" Name of the time dimension. ncols : int, default: 3 Number of columns in the facet grid. **kwargs : dict Additional keyword arguments for plotting.

Returns

fig : matplotlib.figure.Figure The matplotlib figure object. axes : ndarray of matplotlib.axes.Axes The matplotlib axes objects.

Source code in monet/accessors/dataarray_accessor.py
def quick_facet_time_map(
    self,
    map_kws: dict[str, t.Any] | None = None,
    projection: t.Any | None = None,
    colorbar: bool = True,
    figsize: tuple[float, float] | None = None,
    cmap: str | t.Any | None = None,
    vmin: float | None = None,
    vmax: float | None = None,
    norm: t.Any | None = None,
    dpi: int = 150,
    xlabel: str | None = None,
    ylabel: str | None = None,
    suptitle: str | None = None,
    cbar_label: str | None = None,
    xticks: list[float] | None = None,
    yticks: list[float] | None = None,
    annotations: list[dict[str, t.Any]] | None = None,
    export_path: str | None = None,
    export_formats: list[str] | None = None,
    time_dim: str = "time",
    ncols: int = 3,
    **kwargs: t.Any,
) -> tuple[plt.Figure, np.ndarray]:
    """
    Create a facet grid of map plots for each time slice in a DataArray using Cartopy.
    Convention-aware: supports both CF/COARDS and UGRID.

    Parameters
    ----------
    map_kws : dict, optional
        Dictionary of keyword arguments for map features.
    projection : cartopy.crs.Projection, optional
        Cartopy projection to use. Defaults to PlateCarree.
    colorbar : bool, default: True
        Whether to add a colorbar (shared).
    figsize : tuple, optional
        Figure size.
    cmap : str or Colormap, optional
        Colormap to use.
    vmin, vmax : float, optional
        Color limits.
    norm : Normalize, optional
        Matplotlib normalization.
    dpi : int, optional
        Dots per inch for export.
    xlabel, ylabel, suptitle : str, optional
        Axis labels and super title.
    cbar_label : str, optional
        Label for the colorbar.
    xticks, yticks : list, optional
        Custom tick locations.
    annotations : list of dict, optional
        List of annotation dicts for each subplot.
    export_path : str, optional
        Path to export the figure (without extension).
    export_formats : list, optional
        List of formats to export (e.g., ["png", "pdf"]).
    time_dim : str, default: "time"
        Name of the time dimension.
    ncols : int, default: 3
        Number of columns in the facet grid.
    **kwargs : dict
        Additional keyword arguments for plotting.

    Returns
    -------
    fig : matplotlib.figure.Figure
        The matplotlib figure object.
    axes : ndarray of matplotlib.axes.Axes
        The matplotlib axes objects.
    """
    from ..plots.cartopy_utils import facet_time_map

    return facet_time_map(
        self._obj,
        time_dim=time_dim,
        ncols=ncols,
        map_kws=map_kws,
        projection=projection,
        colorbar=colorbar,
        figsize=figsize,
        cmap=cmap,
        vmin=vmin,
        vmax=vmax,
        norm=norm,
        dpi=dpi,
        xlabel=xlabel,
        ylabel=ylabel,
        suptitle=suptitle,
        cbar_label=cbar_label,
        xticks=xticks,
        yticks=yticks,
        annotations=annotations,
        export_path=export_path,
        export_formats=export_formats,
        **kwargs,
    )

Dataset Accessor

Bases: BaseAccessor

Source code in monet/accessors/dataset_accessor.py
@xr.register_dataset_accessor("monet")
class MONETAccessorDataset(BaseAccessor):
    def __init__(self, xray_obj):
        """Initialize the accessor.

        Parameters
        ----------
        xray_obj : xarray.Dataset
            The Dataset this accessor will work with.
        """
        self._obj = xray_obj

    def remap_nearest_parallel(self, data, radius_of_influence=1e6, n_processes=None, **kwargs):
        """Deprecated: Remap data using nearest neighbor interpolation with parallel processing."""
        warnings.warn(
            "remap_nearest_parallel is deprecated. xregrid uses dask for parallelization.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.remap(data, method="nearest", **kwargs)

    def remap_nearest(self, data, radius_of_influence=1e6, **kwargs):
        """Deprecated: Remap data using nearest neighbor interpolation."""
        warnings.warn(
            "remap_nearest is deprecated and will be removed in a future version. "
            "Please use remap(data, method='nearest') instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.remap(data, method="nearest", radius_of_influence=radius_of_influence, **kwargs)

    def nearest_ij(self, lat=None, lon=None, **kwargs):
        """Find the nearest grid indices to given lat/lon point(s).

        Parameters
        ----------
        lat : float or array-like, optional
            Latitude value(s).
        lon : float or array-like, optional
            Longitude value(s).
        **kwargs : dict
            Additional keyword arguments.

        Returns
        -------
        tuple
            (i, j) indices of nearest point(s).
        """
        raise NotImplementedError("nearest_ij is not yet implemented with xregrid")

    def stratify(self, levels, vertical, axis=1, tension=0.0):
        """Deprecated: use ``interpolate_vertical`` instead."""
        import warnings

        warnings.warn(
            "stratify() is deprecated. Use interpolate_vertical(target_levels, level_dim) instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        level_dim = vertical if isinstance(vertical, str) else (vertical.name or self._obj.dims[axis])
        return self.interpolate_vertical(np.asarray(levels), level_dim=level_dim, tension=tension)

    def interpolate_vertical(
        self,
        target_levels,
        level_dim: str = "level",
        tension: float = 0.0,
    ) -> xr.Dataset:
        """Vertically interpolate all variables in the Dataset to new levels using pytspack tension splines.

        Parameters
        ----------
        target_levels : array-like
            Target vertical level values.
        level_dim : str, default: ``"level"``
            Name of the vertical dimension to interpolate along.
        tension : float, default: ``0.0``
            Tension factor for the spline. ``0.0`` gives a standard cubic spline.

        Returns
        -------
        xarray.Dataset
            Dataset with all variables that have ``level_dim`` interpolated to ``target_levels``.

        Examples
        --------
        >>> ds.monet.interpolate_vertical([850, 700, 500], level_dim='pressure')
        """
        from pytspack import interpolate_vertical

        ds = self._obj
        if any(hasattr(ds[v].data, "chunks") for v in ds.data_vars) and level_dim in ds.dims:
            ds = ds.chunk({level_dim: -1})
        return interpolate_vertical(ds, target_levels, level_dim=level_dim, tension=tension)

    def quick_facet_time_map(
        self,
        var: str,
        map_kws: dict[str, t.Any] | None = None,
        projection: t.Any | None = None,
        colorbar: bool = True,
        figsize: tuple[float, float] | None = None,
        cmap: str | t.Any | None = None,
        vmin: float | None = None,
        vmax: float | None = None,
        norm: t.Any | None = None,
        dpi: int = 150,
        xlabel: str | None = None,
        ylabel: str | None = None,
        suptitle: str | None = None,
        cbar_label: str | None = None,
        xticks: list[float] | None = None,
        yticks: list[float] | None = None,
        annotations: list[dict[str, t.Any]] | None = None,
        export_path: str | None = None,
        export_formats: list[str] | None = None,
        time_dim: str = "time",
        ncols: int = 3,
        **kwargs: t.Any,
    ) -> tuple[plt.Figure, np.ndarray]:
        """
        Create a facet grid of map plots for each time slice in a Dataset variable using Cartopy.
        Convention-aware: supports both CF/COARDS and UGRID.

        Parameters
        ----------
        var : str
            Name of the variable in the dataset to plot.
        map_kws : dict, optional
            Dictionary of keyword arguments for map features.
        projection : cartopy.crs.Projection, optional
            Cartopy projection to use. Defaults to PlateCarree.
        colorbar : bool, default: True
            Whether to add a colorbar (shared).
        figsize : tuple, optional
            Figure size.
        cmap : str or Colormap, optional
            Colormap to use.
        vmin, vmax : float, optional
            Color limits.
        norm : Normalize, optional
            Matplotlib normalization.
        dpi : int, optional
            Dots per inch for export.
        xlabel, ylabel, suptitle : str, optional
            Axis labels and super title.
        cbar_label : str, optional
            Label for the colorbar.
        xticks, yticks : list, optional
            Custom tick locations.
        annotations : list of dict, optional
            List of annotation dicts for each subplot.
        export_path : str, optional
            Path to export the figure (without extension).
        export_formats : list, optional
            List of formats to export (e.g., ["png", "pdf"]).
        time_dim : str, default: "time"
            Name of the time dimension.
        ncols : int, default: 3
            Number of columns in the facet grid.
        **kwargs : dict
            Additional keyword arguments for plotting.

        Returns
        -------
        fig : matplotlib.figure.Figure
            The matplotlib figure object.
        axes : ndarray of matplotlib.axes.Axes
            The matplotlib axes objects.
        """
        from ..plots.cartopy_utils import facet_time_map

        return facet_time_map(
            self._obj[var],
            time_dim=time_dim,
            ncols=ncols,
            map_kws=map_kws,
            projection=projection,
            colorbar=colorbar,
            figsize=figsize,
            cmap=cmap,
            vmin=vmin,
            vmax=vmax,
            norm=norm,
            dpi=dpi,
            xlabel=xlabel,
            ylabel=ylabel,
            suptitle=suptitle,
            cbar_label=cbar_label,
            xticks=xticks,
            yticks=yticks,
            annotations=annotations,
            export_path=export_path,
            export_formats=export_formats,
            **kwargs,
        )

Functions

interpolate_vertical(target_levels, level_dim='level', tension=0.0)

Vertically interpolate all variables in the Dataset to new levels using pytspack tension splines.

Parameters

target_levels : array-like Target vertical level values. level_dim : str, default: "level" Name of the vertical dimension to interpolate along. tension : float, default: 0.0 Tension factor for the spline. 0.0 gives a standard cubic spline.

Returns

xarray.Dataset Dataset with all variables that have level_dim interpolated to target_levels.

Examples

ds.monet.interpolate_vertical([850, 700, 500], level_dim='pressure')

Source code in monet/accessors/dataset_accessor.py
def interpolate_vertical(
    self,
    target_levels,
    level_dim: str = "level",
    tension: float = 0.0,
) -> xr.Dataset:
    """Vertically interpolate all variables in the Dataset to new levels using pytspack tension splines.

    Parameters
    ----------
    target_levels : array-like
        Target vertical level values.
    level_dim : str, default: ``"level"``
        Name of the vertical dimension to interpolate along.
    tension : float, default: ``0.0``
        Tension factor for the spline. ``0.0`` gives a standard cubic spline.

    Returns
    -------
    xarray.Dataset
        Dataset with all variables that have ``level_dim`` interpolated to ``target_levels``.

    Examples
    --------
    >>> ds.monet.interpolate_vertical([850, 700, 500], level_dim='pressure')
    """
    from pytspack import interpolate_vertical

    ds = self._obj
    if any(hasattr(ds[v].data, "chunks") for v in ds.data_vars) and level_dim in ds.dims:
        ds = ds.chunk({level_dim: -1})
    return interpolate_vertical(ds, target_levels, level_dim=level_dim, tension=tension)

stratify(levels, vertical, axis=1, tension=0.0)

Deprecated: use interpolate_vertical instead.

Source code in monet/accessors/dataset_accessor.py
def stratify(self, levels, vertical, axis=1, tension=0.0):
    """Deprecated: use ``interpolate_vertical`` instead."""
    import warnings

    warnings.warn(
        "stratify() is deprecated. Use interpolate_vertical(target_levels, level_dim) instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    level_dim = vertical if isinstance(vertical, str) else (vertical.name or self._obj.dims[axis])
    return self.interpolate_vertical(np.asarray(levels), level_dim=level_dim, tension=tension)

nearest_ij(lat=None, lon=None, **kwargs)

Find the nearest grid indices to given lat/lon point(s).

Parameters

lat : float or array-like, optional Latitude value(s). lon : float or array-like, optional Longitude value(s). **kwargs : dict Additional keyword arguments.

Returns

tuple (i, j) indices of nearest point(s).

Source code in monet/accessors/dataset_accessor.py
def nearest_ij(self, lat=None, lon=None, **kwargs):
    """Find the nearest grid indices to given lat/lon point(s).

    Parameters
    ----------
    lat : float or array-like, optional
        Latitude value(s).
    lon : float or array-like, optional
        Longitude value(s).
    **kwargs : dict
        Additional keyword arguments.

    Returns
    -------
    tuple
        (i, j) indices of nearest point(s).
    """
    raise NotImplementedError("nearest_ij is not yet implemented with xregrid")

quick_facet_time_map(var, map_kws=None, projection=None, colorbar=True, figsize=None, cmap=None, vmin=None, vmax=None, norm=None, dpi=150, xlabel=None, ylabel=None, suptitle=None, cbar_label=None, xticks=None, yticks=None, annotations=None, export_path=None, export_formats=None, time_dim='time', ncols=3, **kwargs)

Create a facet grid of map plots for each time slice in a Dataset variable using Cartopy. Convention-aware: supports both CF/COARDS and UGRID.

Parameters

var : str Name of the variable in the dataset to plot. map_kws : dict, optional Dictionary of keyword arguments for map features. projection : cartopy.crs.Projection, optional Cartopy projection to use. Defaults to PlateCarree. colorbar : bool, default: True Whether to add a colorbar (shared). figsize : tuple, optional Figure size. cmap : str or Colormap, optional Colormap to use. vmin, vmax : float, optional Color limits. norm : Normalize, optional Matplotlib normalization. dpi : int, optional Dots per inch for export. xlabel, ylabel, suptitle : str, optional Axis labels and super title. cbar_label : str, optional Label for the colorbar. xticks, yticks : list, optional Custom tick locations. annotations : list of dict, optional List of annotation dicts for each subplot. export_path : str, optional Path to export the figure (without extension). export_formats : list, optional List of formats to export (e.g., ["png", "pdf"]). time_dim : str, default: "time" Name of the time dimension. ncols : int, default: 3 Number of columns in the facet grid. **kwargs : dict Additional keyword arguments for plotting.

Returns

fig : matplotlib.figure.Figure The matplotlib figure object. axes : ndarray of matplotlib.axes.Axes The matplotlib axes objects.

Source code in monet/accessors/dataset_accessor.py
def quick_facet_time_map(
    self,
    var: str,
    map_kws: dict[str, t.Any] | None = None,
    projection: t.Any | None = None,
    colorbar: bool = True,
    figsize: tuple[float, float] | None = None,
    cmap: str | t.Any | None = None,
    vmin: float | None = None,
    vmax: float | None = None,
    norm: t.Any | None = None,
    dpi: int = 150,
    xlabel: str | None = None,
    ylabel: str | None = None,
    suptitle: str | None = None,
    cbar_label: str | None = None,
    xticks: list[float] | None = None,
    yticks: list[float] | None = None,
    annotations: list[dict[str, t.Any]] | None = None,
    export_path: str | None = None,
    export_formats: list[str] | None = None,
    time_dim: str = "time",
    ncols: int = 3,
    **kwargs: t.Any,
) -> tuple[plt.Figure, np.ndarray]:
    """
    Create a facet grid of map plots for each time slice in a Dataset variable using Cartopy.
    Convention-aware: supports both CF/COARDS and UGRID.

    Parameters
    ----------
    var : str
        Name of the variable in the dataset to plot.
    map_kws : dict, optional
        Dictionary of keyword arguments for map features.
    projection : cartopy.crs.Projection, optional
        Cartopy projection to use. Defaults to PlateCarree.
    colorbar : bool, default: True
        Whether to add a colorbar (shared).
    figsize : tuple, optional
        Figure size.
    cmap : str or Colormap, optional
        Colormap to use.
    vmin, vmax : float, optional
        Color limits.
    norm : Normalize, optional
        Matplotlib normalization.
    dpi : int, optional
        Dots per inch for export.
    xlabel, ylabel, suptitle : str, optional
        Axis labels and super title.
    cbar_label : str, optional
        Label for the colorbar.
    xticks, yticks : list, optional
        Custom tick locations.
    annotations : list of dict, optional
        List of annotation dicts for each subplot.
    export_path : str, optional
        Path to export the figure (without extension).
    export_formats : list, optional
        List of formats to export (e.g., ["png", "pdf"]).
    time_dim : str, default: "time"
        Name of the time dimension.
    ncols : int, default: 3
        Number of columns in the facet grid.
    **kwargs : dict
        Additional keyword arguments for plotting.

    Returns
    -------
    fig : matplotlib.figure.Figure
        The matplotlib figure object.
    axes : ndarray of matplotlib.axes.Axes
        The matplotlib axes objects.
    """
    from ..plots.cartopy_utils import facet_time_map

    return facet_time_map(
        self._obj[var],
        time_dim=time_dim,
        ncols=ncols,
        map_kws=map_kws,
        projection=projection,
        colorbar=colorbar,
        figsize=figsize,
        cmap=cmap,
        vmin=vmin,
        vmax=vmax,
        norm=norm,
        dpi=dpi,
        xlabel=xlabel,
        ylabel=ylabel,
        suptitle=suptitle,
        cbar_label=cbar_label,
        xticks=xticks,
        yticks=yticks,
        annotations=annotations,
        export_path=export_path,
        export_formats=export_formats,
        **kwargs,
    )

DataFrame Accessor

Bases: BaseAccessor

Pandas DataFrame accessor for MONET functionality.

This accessor adds MONET-specific methods to pandas DataFrames.

Source code in monet/accessors/pandas_accessor.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
@pd.api.extensions.register_dataframe_accessor("monet")
class MONETAccessorPandas(BaseAccessor):
    """Pandas DataFrame accessor for MONET functionality.

    This accessor adds MONET-specific methods to pandas DataFrames.
    """

    def __init__(self, pandas_obj):
        """Initialize the accessor.

        Parameters
        ----------
        pandas_obj : pandas.DataFrame
            The pandas DataFrame this accessor will work with.
        """
        self._validate(pandas_obj)
        self._obj = pandas_obj

    @staticmethod
    def _validate(obj: pd.DataFrame):
        """Verify there is a column for latitude and longitude.

        Parameters
        ----------
        obj : pandas.DataFrame
            Object to validate.

        Raises
        ------
        AttributeError
            If obj does not have latitude and longitude columns.
        """
        # More flexible validation to support common variants before rename_for_monet
        lat_names = ["latitude", "lat", "Latitude", "Lat", "LAT"]
        lon_names = ["longitude", "lon", "Longitude", "Lon", "LON"]

        has_lat = any(name in obj.columns for name in lat_names)
        has_lon = any(name in obj.columns for name in lon_names)

        if not (has_lat and has_lon):
            searched = f"latitude cols tried: {lat_names}; longitude cols tried: {lon_names}"
            raise AttributeError(f"Must have latitude and longitude columns. {searched}. Found columns: {list(obj.columns)}")

    @property
    def center(self) -> tuple[float, float]:
        """The geographic center point of this DataFrame.

        .. note::

           Currently just the lon and lat mean values,
           not necessarily representative of the geographic center.

        Returns
        -------
        tuple
            (lon, lat)
        """
        # Use detected names
        lat_names = ["latitude", "lat", "Latitude", "Lat", "LAT"]
        lon_names = ["longitude", "lon", "Longitude", "Lon", "LON"]

        lat_col = next((c for c in lat_names if c in self._obj.columns), None)
        lon_col = next((c for c in lon_names if c in self._obj.columns), None)

        if lat_col is None or lon_col is None:
            raise AttributeError("Could not detect latitude and longitude columns.")

        lat = self._obj[lat_col]
        lon = self._obj[lon_col]
        return (float(lon.mean()), float(lat.mean()))

    def _get_latlon_cols(self) -> tuple[str, str]:
        """Get the detected latitude and longitude column names."""
        lat_names = ["latitude", "lat", "Latitude", "Lat", "LAT"]
        lon_names = ["longitude", "lon", "Longitude", "Lon", "LON"]

        lat_col = next((c for c in lat_names if c in self._obj.columns), None)
        lon_col = next((c for c in lon_names if c in self._obj.columns), None)

        if lat_col is None or lon_col is None:
            raise AttributeError("Could not detect latitude and longitude columns.")
        return lat_col, lon_col

    def to_ascii2nc_df(
        self,
        grib_code=126,
        height_msl=0.0,
        column="aod_550nm",
        message_type="ADPUPA",
        pressure=1000.0,
        qc=None,
        height_agl=None,
    ):
        """Convert DataFrame to MET ASCII2NC format.

        Parameters
        ----------
        grib_code : int, default: 126
            GRIB code for the variable.
        height_msl : float or str, default: 0.0
            Height above mean sea level (meters).
        column : str, default: "aod_550nm"
            Column name from which to get observation values.
        message_type : str, default: "ADPUPA"
            Message type for MET.
        pressure : float or str, default: 1000.0
            Pressure level (mb).
        qc : str, optional
            Quality control value. If None, defaults to 0.
        height_agl : float or str, optional
            Height above ground level (meters). If None, defaults to 0.

        Returns
        -------
        pandas.DataFrame
            DataFrame formatted for MET ASCII2NC.
        """
        df = self._obj
        # Work on a local dict to avoid mutating the caller's DataFrame
        data: dict = {}
        data["ascii2nc_time"] = df.time.dt.strftime("%Y%m%d_%H%M%S")
        data["ascii2nc_gribcode"] = int(grib_code)
        if isinstance(height_msl, str):
            data["ascii2nc_elevation"] = df[height_msl]
        else:
            data["ascii2nc_elevation"] = height_msl
        data["ascii2nc_message"] = message_type
        if isinstance(pressure, str):
            data["ascii2nc_pressure"] = df[pressure]
        else:
            data["ascii2nc_pressure"] = pressure
        data["ascii2nc_value"] = df[column]
        if qc is None:
            qc_series = pd.Series("0", index=df.index)
            qc_series[data["ascii2nc_value"].isnull()] = "1"
            data["ascii2nc_qc"] = qc_series
        else:
            data["ascii2nc_qc"] = "0"
        elevation = data["ascii2nc_elevation"]
        if height_agl is None:
            data["ascii2nc_height_agl"] = elevation
        elif isinstance(height_agl, str):
            data["ascii2nc_height_agl"] = df[height_agl]
        else:
            data["ascii2nc_height_agl"] = height_agl
        lat_col, lon_col = self._get_latlon_cols()
        # Build the output DataFrame from the local data dict + needed original columns
        out = pd.DataFrame(
            {
                "ascii2nc_message": data["ascii2nc_message"],
                "siteid": df["siteid"],
                "ascii2nc_time": data["ascii2nc_time"],
                lat_col: df[lat_col],
                lon_col: df[lon_col],
                "ascii2nc_elevation": data["ascii2nc_elevation"],
                "ascii2nc_gribcode": data["ascii2nc_gribcode"],
                "ascii2nc_pressure": data["ascii2nc_pressure"],
                "ascii2nc_height_agl": data["ascii2nc_height_agl"],
                "ascii2nc_qc": data["ascii2nc_qc"],
                "ascii2nc_value": data["ascii2nc_value"],
            },
            index=df.index,
        )
        out = out.rename(
            dict(
                ascii2nc_message="typ",
                siteid="sid",
                ascii2nc_time="vld",
                **{lat_col: "lat", lon_col: "lon"},
                ascii2nc_elevation="elv",
                ascii2nc_gribcode="var",
                ascii2nc_pressure="lvl",
                ascii2nc_height_agl="lvl",
                ascii2nc_qc="qc",
                ascii2nc_value="obs",
            ),
            axis=1,
        )
        out = out.astype(dict(typ=str, sid=str, vld=str, var=str, qc=str))
        return out

    def to_ascii2nc_list(self, **kwargs):
        """Convert DataFrame to MET ASCII2NC list format.

        Parameters
        ----------
        **kwargs : dict
            Keyword arguments passed to to_ascii2nc_df().

        Returns
        -------
        list of list
            List of lists formatted for MET ASCII2NC.
        """
        out = self.to_ascii2nc_df(**kwargs)
        return out.values.tolist()

    @staticmethod
    def rename_for_monet(df: pd.DataFrame) -> pd.DataFrame:
        """Rename latitude and longitude columns in the DataFrame to MONET standard.

        Parameters
        ----------
        df : pandas.DataFrame
            DataFrame to rename columns in.

        Returns
        -------
        pandas.DataFrame
            DataFrame with renamed latitude/longitude columns.
        """
        out = df.copy()
        col_map = None
        if "lat" in out.columns and "lon" in out.columns:
            col_map = {"lat": "latitude", "lon": "longitude"}
        elif "Latitude" in out.columns and "Longitude" in out.columns:
            col_map = {"Latitude": "latitude", "Longitude": "longitude"}
        elif "Lat" in out.columns and "Lon" in out.columns:
            col_map = {"Lat": "latitude", "Lon": "longitude"}
        elif "LAT" in out.columns and "LON" in out.columns:
            col_map = {"LAT": "latitude", "LON": "longitude"}

        if col_map:
            out = out.rename(columns=col_map)

        # If neither, but already correct, do nothing
        # If neither, but only one present, add missing as NaN
        if "latitude" not in out.columns:
            out["latitude"] = np.nan
        if "longitude" not in out.columns:
            out["longitude"] = np.nan

        # Reorder columns to put latitude/longitude first if present
        cols = list(out.columns)
        for c in ["latitude", "longitude"]:
            if c in cols:
                cols.insert(0, cols.pop(cols.index(c)))
        out = out[cols]
        return out

    def _df_to_da(self, d: pd.DataFrame | None = None) -> xr.Dataset:  # TODO: should be `to_ds` or `to_xarray`
        """Convert DataFrame to xarray.
        Preserves detected spatial columns as coordinates.

        Parameters
        ----------
        d : pandas.DataFrame, optional
            To use instead of self.

        Returns
        -------
        xarray.Dataset
        """
        index_name = "index"
        if d is None:
            d = self._obj.copy()
        else:
            d = d.copy()

        # Avoid issues with Arrow-backed strings
        for col in d.columns:
            if pd.api.types.is_string_dtype(d[col]) and not pd.api.types.is_numeric_dtype(d[col]):
                d[col] = np.asarray(d[col], dtype=object)
        if pd.api.types.is_string_dtype(d.index) and not pd.api.types.is_numeric_dtype(d.index):
            d.index = pd.Index(np.asarray(d.index, dtype=object), name=d.index.name)

        if d.index.name is not None:
            index_name = d.index.name
        ds = d.to_xarray().rename({index_name: "x"}).expand_dims("y")
        if "time" in ds.data_vars.keys():
            ds["time"] = ds.time.squeeze()  # it is only 1D

        # Detect spatial columns to set as coords
        lat_names = ["latitude", "lat", "Latitude", "Lat", "LAT"]
        lon_names = ["longitude", "lon", "Longitude", "Lon", "LON"]
        lat_col = next((c for c in lat_names if c in ds.data_vars), None)
        lon_col = next((c for c in lon_names if c in ds.data_vars), None)

        coords_to_set = []
        if lat_col:
            coords_to_set.append(lat_col)
        if lon_col:
            coords_to_set.append(lon_col)
        if coords_to_set:
            ds = ds.set_coords(coords_to_set)

        return ds

    def remap_nearest(
        self,
        df: pd.DataFrame,
        radius_of_influence: float = 1e5,
        combine: bool = False,
    ) -> pd.DataFrame:
        """Remap data using nearest neighbor interpolation (xregrid).

        Parameters
        ----------
        df : pandas.DataFrame
            DataFrame to remap.
        radius_of_influence : float, default: 1e5
            Search radius in meters (unused in xregrid).
        combine : bool, default: False
            Whether to combine the remapped data with the original data.

        Returns
        -------
        pandas.DataFrame
            Remapped DataFrame.
        """
        if not has_xregrid:
            raise ImportError("xregrid (with esmpy) is required for this functionality")

        from ..util.resample import resample

        source_data = self.rename_for_monet(df)
        target_data = self.rename_for_monet(self._obj)

        # make fake index
        source_data = self._make_fake_index_var(source_data)
        source_data_da = self._df_to_da(source_data)
        target_data_da = self._df_to_da(target_data)

        # Use xregrid to resample
        da_source = source_data_da["monet_fake_index"]
        res = resample(da_source, target_data_da, method="nearest")

        r = res
        r.name = "monet_fake_index"

        # now merge back from original DataFrame
        q = r.compute()
        v = q.to_dataframe()

        # Ensure we have the fake index column to merge on
        if "monet_fake_index" not in v.columns:
            # It might be in the index if xarray conversion put it there
            v = v.reset_index()

        result = v.merge(source_data, how="left", on="monet_fake_index").drop("monet_fake_index", axis=1)

        # Restore index if it was lost
        result.index = target_data.index

        if combine:
            columns_to_use = result.columns.difference(target_data.columns)
            return pd.merge(
                target_data,
                result[columns_to_use],
                left_index=True,
                right_index=True,
                how="outer",
            )
        else:
            return result

    def cftime_to_datetime64(self, col: str | None = None) -> pd.DataFrame:
        """Convert cftime column to numpy datetime64.

        Parameters
        ----------
        col : str, optional
            Name of the column to convert. If None, tries to detect the time column.

        Returns
        -------
        pandas.DataFrame
            DataFrame with converted time column.
        """
        df = self._obj.copy()

        def cf_to_dt64(x):
            try:
                return pd.to_datetime(x.strftime("%Y-%m-%d %H:%M:%S"))
            except AttributeError:
                return x

        if col is None:  # assume 'time' is the column name to transform
            col = "time"

        if col in df.columns:
            df[col] = df[col].apply(cf_to_dt64)
        return df

    def _make_fake_index_var(self, df):
        """Create a fake index variable for the DataFrame.

        Parameters
        ----------
        df : pandas.DataFrame
            DataFrame to create index for.

        Returns
        -------
        pandas.DataFrame
            DataFrame with fake index column added.
        """
        from numpy import arange

        # column = df.columns[0]
        fake_index = arange(len(df))
        column_name = "monet_fake_index"
        r = pd.Series(fake_index.astype(float), index=df.index)
        r.name = column_name
        df[column_name] = r
        return df

    def quick_facet_time_map(self, *args, **kwargs):
        """
        Faceted map plotting is not supported for Pandas DataFrames.
        """
        raise NotImplementedError("Faceted map plotting is only available for xarray DataArray and Dataset accessors.")

    def plot_points_map(
        self,
        lon_col="longitude",
        lat_col="latitude",
        projection=None,
        color="C0",
        marker="o",
        size=40,
        edgecolor="k",
        alpha=0.8,
        map_kws=None,
        figsize=(8, 6),
        dpi=150,
        title=None,
        export_path=None,
        export_formats=None,
        **kwargs,
    ):
        """
        Plot points from this DataFrame on a Cartopy map.
        """
        from ..plots.cartopy_utils import plot_points_map

        return plot_points_map(
            self._obj,
            lon_col=lon_col,
            lat_col=lat_col,
            projection=projection,
            color=color,
            marker=marker,
            size=size,
            edgecolor=edgecolor,
            alpha=alpha,
            map_kws=map_kws,
            figsize=figsize,
            dpi=dpi,
            title=title,
            export_path=export_path,
            export_formats=export_formats,
            **kwargs,
        )

    def pair(self, model, **kwargs):
        """Pair this DataFrame with model data.

        Parameters
        ----------
        model : xarray.Dataset or xarray.DataArray
            Model data to pair with.
        **kwargs : dict
            Additional arguments passed to `monet.pair`.

        Returns
        -------
        pandas.DataFrame or dask.dataframe.DataFrame
            The DataFrame with paired model data.
        """
        from ..util.combinetool import pair

        return pair(model, self._obj, **kwargs)

    def plot_lines_map(
        self,
        lon_col="longitude",
        lat_col="latitude",
        group_col=None,
        projection=None,
        color="C0",
        linewidth=2,
        alpha=0.8,
        map_kws=None,
        figsize=(8, 6),
        dpi=150,
        title=None,
        export_path=None,
        export_formats=None,
        **kwargs,
    ):
        """
        Plot lines from this DataFrame on a Cartopy map. Optionally group by a column.
        """
        from ..plots.cartopy_utils import plot_lines_map

        return plot_lines_map(
            self._obj,
            lon_col=lon_col,
            lat_col=lat_col,
            group_col=group_col,
            projection=projection,
            color=color,
            linewidth=linewidth,
            alpha=alpha,
            map_kws=map_kws,
            figsize=figsize,
            dpi=dpi,
            title=title,
            export_path=export_path,
            export_formats=export_formats,
            **kwargs,
        )

Attributes

center property

The geographic center point of this DataFrame.

.. note::

Currently just the lon and lat mean values, not necessarily representative of the geographic center.

Returns

tuple (lon, lat)

Functions

to_ascii2nc_df(grib_code=126, height_msl=0.0, column='aod_550nm', message_type='ADPUPA', pressure=1000.0, qc=None, height_agl=None)

Convert DataFrame to MET ASCII2NC format.

Parameters

grib_code : int, default: 126 GRIB code for the variable. height_msl : float or str, default: 0.0 Height above mean sea level (meters). column : str, default: "aod_550nm" Column name from which to get observation values. message_type : str, default: "ADPUPA" Message type for MET. pressure : float or str, default: 1000.0 Pressure level (mb). qc : str, optional Quality control value. If None, defaults to 0. height_agl : float or str, optional Height above ground level (meters). If None, defaults to 0.

Returns

pandas.DataFrame DataFrame formatted for MET ASCII2NC.

Source code in monet/accessors/pandas_accessor.py
def to_ascii2nc_df(
    self,
    grib_code=126,
    height_msl=0.0,
    column="aod_550nm",
    message_type="ADPUPA",
    pressure=1000.0,
    qc=None,
    height_agl=None,
):
    """Convert DataFrame to MET ASCII2NC format.

    Parameters
    ----------
    grib_code : int, default: 126
        GRIB code for the variable.
    height_msl : float or str, default: 0.0
        Height above mean sea level (meters).
    column : str, default: "aod_550nm"
        Column name from which to get observation values.
    message_type : str, default: "ADPUPA"
        Message type for MET.
    pressure : float or str, default: 1000.0
        Pressure level (mb).
    qc : str, optional
        Quality control value. If None, defaults to 0.
    height_agl : float or str, optional
        Height above ground level (meters). If None, defaults to 0.

    Returns
    -------
    pandas.DataFrame
        DataFrame formatted for MET ASCII2NC.
    """
    df = self._obj
    # Work on a local dict to avoid mutating the caller's DataFrame
    data: dict = {}
    data["ascii2nc_time"] = df.time.dt.strftime("%Y%m%d_%H%M%S")
    data["ascii2nc_gribcode"] = int(grib_code)
    if isinstance(height_msl, str):
        data["ascii2nc_elevation"] = df[height_msl]
    else:
        data["ascii2nc_elevation"] = height_msl
    data["ascii2nc_message"] = message_type
    if isinstance(pressure, str):
        data["ascii2nc_pressure"] = df[pressure]
    else:
        data["ascii2nc_pressure"] = pressure
    data["ascii2nc_value"] = df[column]
    if qc is None:
        qc_series = pd.Series("0", index=df.index)
        qc_series[data["ascii2nc_value"].isnull()] = "1"
        data["ascii2nc_qc"] = qc_series
    else:
        data["ascii2nc_qc"] = "0"
    elevation = data["ascii2nc_elevation"]
    if height_agl is None:
        data["ascii2nc_height_agl"] = elevation
    elif isinstance(height_agl, str):
        data["ascii2nc_height_agl"] = df[height_agl]
    else:
        data["ascii2nc_height_agl"] = height_agl
    lat_col, lon_col = self._get_latlon_cols()
    # Build the output DataFrame from the local data dict + needed original columns
    out = pd.DataFrame(
        {
            "ascii2nc_message": data["ascii2nc_message"],
            "siteid": df["siteid"],
            "ascii2nc_time": data["ascii2nc_time"],
            lat_col: df[lat_col],
            lon_col: df[lon_col],
            "ascii2nc_elevation": data["ascii2nc_elevation"],
            "ascii2nc_gribcode": data["ascii2nc_gribcode"],
            "ascii2nc_pressure": data["ascii2nc_pressure"],
            "ascii2nc_height_agl": data["ascii2nc_height_agl"],
            "ascii2nc_qc": data["ascii2nc_qc"],
            "ascii2nc_value": data["ascii2nc_value"],
        },
        index=df.index,
    )
    out = out.rename(
        dict(
            ascii2nc_message="typ",
            siteid="sid",
            ascii2nc_time="vld",
            **{lat_col: "lat", lon_col: "lon"},
            ascii2nc_elevation="elv",
            ascii2nc_gribcode="var",
            ascii2nc_pressure="lvl",
            ascii2nc_height_agl="lvl",
            ascii2nc_qc="qc",
            ascii2nc_value="obs",
        ),
        axis=1,
    )
    out = out.astype(dict(typ=str, sid=str, vld=str, var=str, qc=str))
    return out

to_ascii2nc_list(**kwargs)

Convert DataFrame to MET ASCII2NC list format.

Parameters

**kwargs : dict Keyword arguments passed to to_ascii2nc_df().

Returns

list of list List of lists formatted for MET ASCII2NC.

Source code in monet/accessors/pandas_accessor.py
def to_ascii2nc_list(self, **kwargs):
    """Convert DataFrame to MET ASCII2NC list format.

    Parameters
    ----------
    **kwargs : dict
        Keyword arguments passed to to_ascii2nc_df().

    Returns
    -------
    list of list
        List of lists formatted for MET ASCII2NC.
    """
    out = self.to_ascii2nc_df(**kwargs)
    return out.values.tolist()

rename_for_monet(df) staticmethod

Rename latitude and longitude columns in the DataFrame to MONET standard.

Parameters

df : pandas.DataFrame DataFrame to rename columns in.

Returns

pandas.DataFrame DataFrame with renamed latitude/longitude columns.

Source code in monet/accessors/pandas_accessor.py
@staticmethod
def rename_for_monet(df: pd.DataFrame) -> pd.DataFrame:
    """Rename latitude and longitude columns in the DataFrame to MONET standard.

    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame to rename columns in.

    Returns
    -------
    pandas.DataFrame
        DataFrame with renamed latitude/longitude columns.
    """
    out = df.copy()
    col_map = None
    if "lat" in out.columns and "lon" in out.columns:
        col_map = {"lat": "latitude", "lon": "longitude"}
    elif "Latitude" in out.columns and "Longitude" in out.columns:
        col_map = {"Latitude": "latitude", "Longitude": "longitude"}
    elif "Lat" in out.columns and "Lon" in out.columns:
        col_map = {"Lat": "latitude", "Lon": "longitude"}
    elif "LAT" in out.columns and "LON" in out.columns:
        col_map = {"LAT": "latitude", "LON": "longitude"}

    if col_map:
        out = out.rename(columns=col_map)

    # If neither, but already correct, do nothing
    # If neither, but only one present, add missing as NaN
    if "latitude" not in out.columns:
        out["latitude"] = np.nan
    if "longitude" not in out.columns:
        out["longitude"] = np.nan

    # Reorder columns to put latitude/longitude first if present
    cols = list(out.columns)
    for c in ["latitude", "longitude"]:
        if c in cols:
            cols.insert(0, cols.pop(cols.index(c)))
    out = out[cols]
    return out

remap_nearest(df, radius_of_influence=100000.0, combine=False)

Remap data using nearest neighbor interpolation (xregrid).

Parameters

df : pandas.DataFrame DataFrame to remap. radius_of_influence : float, default: 1e5 Search radius in meters (unused in xregrid). combine : bool, default: False Whether to combine the remapped data with the original data.

Returns

pandas.DataFrame Remapped DataFrame.

Source code in monet/accessors/pandas_accessor.py
def remap_nearest(
    self,
    df: pd.DataFrame,
    radius_of_influence: float = 1e5,
    combine: bool = False,
) -> pd.DataFrame:
    """Remap data using nearest neighbor interpolation (xregrid).

    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame to remap.
    radius_of_influence : float, default: 1e5
        Search radius in meters (unused in xregrid).
    combine : bool, default: False
        Whether to combine the remapped data with the original data.

    Returns
    -------
    pandas.DataFrame
        Remapped DataFrame.
    """
    if not has_xregrid:
        raise ImportError("xregrid (with esmpy) is required for this functionality")

    from ..util.resample import resample

    source_data = self.rename_for_monet(df)
    target_data = self.rename_for_monet(self._obj)

    # make fake index
    source_data = self._make_fake_index_var(source_data)
    source_data_da = self._df_to_da(source_data)
    target_data_da = self._df_to_da(target_data)

    # Use xregrid to resample
    da_source = source_data_da["monet_fake_index"]
    res = resample(da_source, target_data_da, method="nearest")

    r = res
    r.name = "monet_fake_index"

    # now merge back from original DataFrame
    q = r.compute()
    v = q.to_dataframe()

    # Ensure we have the fake index column to merge on
    if "monet_fake_index" not in v.columns:
        # It might be in the index if xarray conversion put it there
        v = v.reset_index()

    result = v.merge(source_data, how="left", on="monet_fake_index").drop("monet_fake_index", axis=1)

    # Restore index if it was lost
    result.index = target_data.index

    if combine:
        columns_to_use = result.columns.difference(target_data.columns)
        return pd.merge(
            target_data,
            result[columns_to_use],
            left_index=True,
            right_index=True,
            how="outer",
        )
    else:
        return result

pair(model, **kwargs)

Pair this DataFrame with model data.

Parameters

model : xarray.Dataset or xarray.DataArray Model data to pair with. **kwargs : dict Additional arguments passed to monet.pair.

Returns

pandas.DataFrame or dask.dataframe.DataFrame The DataFrame with paired model data.

Source code in monet/accessors/pandas_accessor.py
def pair(self, model, **kwargs):
    """Pair this DataFrame with model data.

    Parameters
    ----------
    model : xarray.Dataset or xarray.DataArray
        Model data to pair with.
    **kwargs : dict
        Additional arguments passed to `monet.pair`.

    Returns
    -------
    pandas.DataFrame or dask.dataframe.DataFrame
        The DataFrame with paired model data.
    """
    from ..util.combinetool import pair

    return pair(model, self._obj, **kwargs)

cftime_to_datetime64(col=None)

Convert cftime column to numpy datetime64.

Parameters

col : str, optional Name of the column to convert. If None, tries to detect the time column.

Returns

pandas.DataFrame DataFrame with converted time column.

Source code in monet/accessors/pandas_accessor.py
def cftime_to_datetime64(self, col: str | None = None) -> pd.DataFrame:
    """Convert cftime column to numpy datetime64.

    Parameters
    ----------
    col : str, optional
        Name of the column to convert. If None, tries to detect the time column.

    Returns
    -------
    pandas.DataFrame
        DataFrame with converted time column.
    """
    df = self._obj.copy()

    def cf_to_dt64(x):
        try:
            return pd.to_datetime(x.strftime("%Y-%m-%d %H:%M:%S"))
        except AttributeError:
            return x

    if col is None:  # assume 'time' is the column name to transform
        col = "time"

    if col in df.columns:
        df[col] = df[col].apply(cf_to_dt64)
    return df

plot_points_map(lon_col='longitude', lat_col='latitude', projection=None, color='C0', marker='o', size=40, edgecolor='k', alpha=0.8, map_kws=None, figsize=(8, 6), dpi=150, title=None, export_path=None, export_formats=None, **kwargs)

Plot points from this DataFrame on a Cartopy map.

Source code in monet/accessors/pandas_accessor.py
def plot_points_map(
    self,
    lon_col="longitude",
    lat_col="latitude",
    projection=None,
    color="C0",
    marker="o",
    size=40,
    edgecolor="k",
    alpha=0.8,
    map_kws=None,
    figsize=(8, 6),
    dpi=150,
    title=None,
    export_path=None,
    export_formats=None,
    **kwargs,
):
    """
    Plot points from this DataFrame on a Cartopy map.
    """
    from ..plots.cartopy_utils import plot_points_map

    return plot_points_map(
        self._obj,
        lon_col=lon_col,
        lat_col=lat_col,
        projection=projection,
        color=color,
        marker=marker,
        size=size,
        edgecolor=edgecolor,
        alpha=alpha,
        map_kws=map_kws,
        figsize=figsize,
        dpi=dpi,
        title=title,
        export_path=export_path,
        export_formats=export_formats,
        **kwargs,
    )

plot_lines_map(lon_col='longitude', lat_col='latitude', group_col=None, projection=None, color='C0', linewidth=2, alpha=0.8, map_kws=None, figsize=(8, 6), dpi=150, title=None, export_path=None, export_formats=None, **kwargs)

Plot lines from this DataFrame on a Cartopy map. Optionally group by a column.

Source code in monet/accessors/pandas_accessor.py
def plot_lines_map(
    self,
    lon_col="longitude",
    lat_col="latitude",
    group_col=None,
    projection=None,
    color="C0",
    linewidth=2,
    alpha=0.8,
    map_kws=None,
    figsize=(8, 6),
    dpi=150,
    title=None,
    export_path=None,
    export_formats=None,
    **kwargs,
):
    """
    Plot lines from this DataFrame on a Cartopy map. Optionally group by a column.
    """
    from ..plots.cartopy_utils import plot_lines_map

    return plot_lines_map(
        self._obj,
        lon_col=lon_col,
        lat_col=lat_col,
        group_col=group_col,
        projection=projection,
        color=color,
        linewidth=linewidth,
        alpha=alpha,
        map_kws=map_kws,
        figsize=figsize,
        dpi=dpi,
        title=title,
        export_path=export_path,
        export_formats=export_formats,
        **kwargs,
    )

Utility Functions

General Utilities

monet.util.tools

Utility tools for MONET.

Functions

nearest(items, pivot)

Find the nearest value to pivot in a collection.

This implementation is backend-agnostic and supports Dask-backed xarray objects using xarray.apply_ufunc.

Parameters

items : array-like Collection of values to search through. pivot : float, int, or array-like The value(s) to find the nearest match to.

Returns

closest_value : same type as input The item from the collection that is closest to the pivot value.

Source code in monet/util/tools.py
def nearest(items: Any, pivot: Any) -> Any:
    """Find the nearest value to pivot in a collection.

    This implementation is backend-agnostic and supports Dask-backed
    xarray objects using xarray.apply_ufunc.

    Parameters
    ----------
    items : array-like
        Collection of values to search through.
    pivot : float, int, or array-like
        The value(s) to find the nearest match to.

    Returns
    -------
    closest_value : same type as input
        The item from the collection that is closest to the pivot value.
    """
    _, val = findclosest(items, pivot)
    return val

search_listinlist(array1, array2)

Find matching indices between two arrays.

Parameters

array1 : numpy.ndarray First array to search for matches array2 : numpy.ndarray Second array to search for matches

Returns

tuple (index1, index2) containing: - index1: sorted array of indices in array1 where matches were found - index2: sorted array of indices in array2 where matches were found

Source code in monet/util/tools.py
def search_listinlist(array1: np.ndarray, array2: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """Find matching indices between two arrays.

    Parameters
    ----------
    array1 : numpy.ndarray
        First array to search for matches
    array2 : numpy.ndarray
        Second array to search for matches

    Returns
    -------
    tuple
        (index1, index2) containing:
        - index1: sorted array of indices in array1 where matches were found
        - index2: sorted array of indices in array2 where matches were found
    """
    # Find the intersection of the two arrays
    inter = np.intersect1d(array1, array2)

    # Find the indices in array1
    index1 = np.where(np.isin(array1, inter))[0]

    # Find the indices in array2
    index2 = np.where(np.isin(array2, inter))[0]

    return np.sort(index1), np.sort(index2)

linregress(x, y, dim=None)

Perform a linear regression.

This implementation is backend-agnostic and supports Dask-backed xarray objects using xarray.apply_ufunc. If x and y are multi-dimensional, the regression is performed over the specified dimension (for xarray) or the last dimension (for numpy).

Parameters

x : numpy.ndarray or xarray.DataArray Independent variable values. y : numpy.ndarray or xarray.DataArray Dependent variable values. dim : str, optional The dimension along which to perform the regression. Only used if inputs are xarray objects. If None and inputs are xarray, the last dimension is used.

Returns

slope, intercept, r_squared, std_err : same type as input - slope: regression line slope - intercept: regression line y-intercept - r_squared: coefficient of determination - std_err: standard error of the residuals

Source code in monet/util/tools.py
def linregress(x: xr.DataArray | np.ndarray, y: xr.DataArray | np.ndarray, dim: str | None = None) -> Any:
    """Perform a linear regression.

    This implementation is backend-agnostic and supports Dask-backed
    xarray objects using xarray.apply_ufunc. If x and y are multi-dimensional,
    the regression is performed over the specified dimension (for xarray)
    or the last dimension (for numpy).

    Parameters
    ----------
    x : numpy.ndarray or xarray.DataArray
        Independent variable values.
    y : numpy.ndarray or xarray.DataArray
        Dependent variable values.
    dim : str, optional
        The dimension along which to perform the regression. Only used if
        inputs are xarray objects. If None and inputs are xarray, the last
        dimension is used.

    Returns
    -------
    slope, intercept, r_squared, std_err : same type as input
        - slope: regression line slope
        - intercept: regression line y-intercept
        - r_squared: coefficient of determination
        - std_err: standard error of the residuals
    """

    def _logic(x, y):
        # We use numpy for the core logic to avoid statsmodels dependency
        # and support vectorized operations across chunks.
        # Ensure we are working with at least 1D arrays
        x = np.asanyarray(x)
        y = np.asanyarray(y)

        # Handle multi-dimensional arrays by calculating along the last axis
        # This is compatible with apply_ufunc(..., input_core_dims=[['dim'], ['dim']])
        n = x.shape[-1]
        sum_x = np.sum(x, axis=-1)
        sum_y = np.sum(y, axis=-1)
        sum_xx = np.sum(x * x, axis=-1)
        sum_yy = np.sum(y * y, axis=-1)
        sum_xy = np.sum(x * y, axis=-1)

        denominator = n * sum_xx - sum_x**2
        # Use np.where to avoid division by zero
        # Ensure floating point division to match xr.apply_ufunc expectations and avoid casting errors
        slope = np.divide(
            (n * sum_xy - sum_x * sum_y).astype(float),
            denominator.astype(float),
            out=np.zeros_like(denominator, dtype=float),
            where=denominator != 0,
        )
        intercept = (sum_y.astype(float) - slope * sum_x.astype(float)) / n

        # R-squared
        # SS_tot = sum((y - y_mean)**2) = sum(y**2) - (sum(y)**2)/n
        # SS_res = sum((y - (slope*x + intercept))**2)
        ss_tot = sum_yy - (sum_y**2) / n
        y_pred = slope[..., np.newaxis] * x + intercept[..., np.newaxis]
        ss_res = np.sum((y - y_pred) ** 2, axis=-1)

        r_squared = np.divide(ss_tot - ss_res, ss_tot, out=np.zeros_like(ss_tot), where=ss_tot != 0)
        std_err = np.sqrt(np.divide(ss_res, n - 2, out=np.zeros_like(ss_res), where=n > 2))

        return slope, intercept, r_squared, std_err

    # Determine core dimension for xarray
    if dim is None and isinstance(x, xr.DataArray):
        dim = x.dims[-1]
    elif dim is None:
        dim = "core_dim"  # Placeholder for numpy

    return _apply_vectorized(
        _logic,
        x,
        y,
        name="linear regression",
        output_dtypes=[float, float, float, float],
        input_core_dims=[[dim], [dim]],
        output_core_dims=[[], [], [], []],
        source="monet.util.tools",
    )

findclosest(list_obj, value)

Find the index and value of the closest element to a target value.

This implementation is backend-agnostic and supports Dask-backed xarray objects.

Parameters

list_obj : array-like Collection of values to search through. value : float, int, or array-like The target value(s) to find the closest match to.

Returns

index, closest_value : same type as input - index: the position in list_obj of the closest value - closest_value: the value from list_obj that is closest to the target

Source code in monet/util/tools.py
def findclosest(list_obj: Any, value: Any) -> Any:
    """Find the index and value of the closest element to a target value.

    This implementation is backend-agnostic and supports Dask-backed
    xarray objects.

    Parameters
    ----------
    list_obj : array-like
        Collection of values to search through.
    value : float, int, or array-like
        The target value(s) to find the closest match to.

    Returns
    -------
    index, closest_value : same type as input
        - index: the position in list_obj of the closest value
        - closest_value: the value from list_obj that is closest to the target
    """
    if isinstance(list_obj, xr.DataArray | xr.Dataset) or isinstance(value, xr.DataArray | xr.Dataset):
        # Use xarray operations to preserve laziness and avoid apply_ufunc scalar issues
        # Ensure they are DataArrays for indexing
        if not isinstance(list_obj, xr.DataArray):
            list_obj = xr.DataArray(list_obj, dims=["search_dim"])
        if not isinstance(value, xr.DataArray):
            value = xr.DataArray(value)

        # Name search dimension if not already named
        if len(list_obj.dims) == 1 and list_obj.dims[0] == "dim_0":
            list_obj = list_obj.rename({"dim_0": "search_dim"})
        search_dim = list_obj.dims[0]

        diff = np.abs(list_obj - value)
        idx = diff.argmin(dim=search_dim)
        res = list_obj.isel({search_dim: idx})

        # Add history
        for out in (idx, res):
            if hasattr(out, "attrs"):
                update_history(out, "Found closest element via monet.util.tools")

        return idx, res

    # Fallback for non-xarray (NumPy or small lists)
    arr = np.asanyarray(list_obj)
    val = np.asanyarray(value)

    if arr.ndim == 1 and (val.ndim == 0 or val.size == 1):
        # Original simple path
        a = min((abs(x - float(val)), x, i) for i, x in enumerate(list_obj))
        return a[2], a[1]

    # Vectorized NumPy path
    diff = np.abs(arr[np.newaxis, :] - val[..., np.newaxis])
    idx = np.argmin(diff, axis=-1)
    return idx, arr[idx]

kolmogorov_zurbenko_filter(df, col, window, iterations)

Apply a Kolmogorov-Zurbenko filter to a specific column in a DataFrame.

A Kolmogorov-Zurbenko filter is a low-pass filter created by iteratively applying a moving average of specified window length. This implementation applies the filter to a DataFrame grouped by site ID.

Parameters

df : pandas.DataFrame DataFrame containing the data to filter. col : str Column name to apply the filter to. window : int Size of the moving average window. iterations : int Number of times to apply the moving average filter.

Returns

pandas.DataFrame DataFrame with original data and filtered values merged in.

Source code in monet/util/tools.py
def kolmogorov_zurbenko_filter(df: pd.DataFrame, col: str, window: int, iterations: int) -> pd.DataFrame:
    """Apply a Kolmogorov-Zurbenko filter to a specific column in a DataFrame.

    A Kolmogorov-Zurbenko filter is a low-pass filter created by iteratively
    applying a moving average of specified window length. This implementation
    applies the filter to a DataFrame grouped by site ID.

    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame containing the data to filter.
    col : str
        Column name to apply the filter to.
    window : int
        Size of the moving average window.
    iterations : int
        Number of times to apply the moving average filter.

    Returns
    -------
    pandas.DataFrame
        DataFrame with original data and filtered values merged in.
    """
    df.index = df.time_local
    z = df.copy()
    for i in range(iterations):
        z.index = z.time_local
        z = z.groupby("siteid")[col].rolling(window, center=True, min_periods=1).mean().reset_index().dropna()
    df = df.reset_index(drop=True)
    return df.merge(z, on=["siteid", "time_local"])

wsdir2uv(ws, wdir)

Convert wind speed and direction to U and V components.

This implementation is backend-agnostic and supports Dask-backed xarray objects using xarray.apply_ufunc.

Parameters

ws : float, numpy.ndarray, or xarray.DataArray Wind speed values. wdir : float, numpy.ndarray, or xarray.DataArray Wind direction values in degrees (meteorological convention: 0=North, 90=East).

Returns

u, v : same type as input - u is the zonal wind component (positive for eastward wind) - v is the meridional wind component (positive for northward wind)

Source code in monet/util/tools.py
def wsdir2uv(ws: Any, wdir: Any) -> Any:
    """Convert wind speed and direction to U and V components.

    This implementation is backend-agnostic and supports Dask-backed
    xarray objects using xarray.apply_ufunc.

    Parameters
    ----------
    ws : float, numpy.ndarray, or xarray.DataArray
        Wind speed values.
    wdir : float, numpy.ndarray, or xarray.DataArray
        Wind direction values in degrees (meteorological convention: 0=North, 90=East).

    Returns
    -------
    u, v : same type as input
        - u is the zonal wind component (positive for eastward wind)
        - v is the meridional wind component (positive for northward wind)
    """

    def _logic(ws, wdir):
        u = -ws * np.sin(wdir * np.pi / 180.0)
        v = -ws * np.cos(wdir * np.pi / 180.0)
        return u, v

    return _apply_vectorized(
        _logic,
        ws,
        wdir,
        name="U and V wind components",
        output_dtypes=[float, float],
        output_core_dims=[[], []],
        source="monet.util.tools.wsdir2uv",
    )

get_relhum(temp, press, vap)

Calculate relative humidity from temperature, pressure and vapor pressure.

This implementation is backend-agnostic and supports Dask-backed xarray objects using xarray.apply_ufunc.

Parameters

temp : float, numpy.ndarray, or xarray.DataArray Temperature in Kelvin press : float, numpy.ndarray, or xarray.DataArray Pressure in hPa/mb vap : float, numpy.ndarray, or xarray.DataArray Vapor pressure in hPa/mb

Returns

relhum : same type as input Relative humidity as a percentage (0-100)

Source code in monet/util/tools.py
def get_relhum(temp: Any, press: Any, vap: Any) -> Any:
    """Calculate relative humidity from temperature, pressure and vapor pressure.

    This implementation is backend-agnostic and supports Dask-backed
    xarray objects using xarray.apply_ufunc.

    Parameters
    ----------
    temp : float, numpy.ndarray, or xarray.DataArray
        Temperature in Kelvin
    press : float, numpy.ndarray, or xarray.DataArray
        Pressure in hPa/mb
    vap : float, numpy.ndarray, or xarray.DataArray
        Vapor pressure in hPa/mb

    Returns
    -------
    relhum : same type as input
        Relative humidity as a percentage (0-100)
    """

    def _logic(temp, press, vap):
        temp_o = 273.16
        es_vap = 611.0 * np.exp(17.67 * ((temp - temp_o) / (temp - 29.65)))
        ws_vap = 0.622 * (es_vap / press)
        return 100.0 * (vap / ws_vap)

    return _apply_vectorized(_logic, temp, press, vap, name="relative humidity", source="monet.util.tools")

long_to_wide(df)

Convert a DataFrame from long (stacked) to wide format.

Parameters

df : pandas.DataFrame DataFrame in long format with 'time', 'siteid', 'variable', 'obs', and 'units' columns

Returns

pandas.DataFrame DataFrame in wide format with variables as columns

Source code in monet/util/tools.py
def long_to_wide(df: pd.DataFrame) -> pd.DataFrame:
    """Convert a DataFrame from long (stacked) to wide format.

    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame in long format with 'time', 'siteid', 'variable',
        'obs', and 'units' columns

    Returns
    -------
    pandas.DataFrame
        DataFrame in wide format with variables as columns
    """
    w = df.pivot_table(values="obs", index=["time", "siteid"], columns="variable").reset_index()
    g = df.groupby("variable")
    for name, group in g:
        w[name + "_unit"] = group.units.unique()[0]
    return merge(w, df, on=["siteid", "time"])

calc_8hr_rolling_max(df, col=None, window=None)

Calculate 8-hour rolling maximum values.

Parameters

df : pandas.DataFrame Input data with 'time_local' and 'siteid' columns col : str Column name to calculate rolling max for window : int Rolling window size in hours

Returns

pandas.DataFrame DataFrame with added column containing 8-hour maxima

Source code in monet/util/tools.py
def calc_8hr_rolling_max(df: pd.DataFrame, col: str | None = None, window: int | None = None) -> pd.DataFrame:
    """Calculate 8-hour rolling maximum values.

    Parameters
    ----------
    df : pandas.DataFrame
        Input data with 'time_local' and 'siteid' columns
    col : str
        Column name to calculate rolling max for
    window : int
        Rolling window size in hours

    Returns
    -------
    pandas.DataFrame
        DataFrame with added column containing 8-hour maxima
    """
    if col is None or window is None:
        raise ValueError("col and window must be provided")

    df.index = df.time_local
    df_rolling = df.groupby("siteid")[col].rolling(window, center=True, win_type="boxcar").mean().reset_index().dropna()
    df_rolling_max = df_rolling.groupby("siteid").resample("D", on="time_local").max().reset_index(drop=True)
    df = df.reset_index(drop=True)
    return df.merge(df_rolling_max, on=["siteid", "time_local"])

calc_24hr_ave(df, col=None)

Calculate 24-hour averages.

Parameters

df : pandas.DataFrame Input data with 'time_local' and 'siteid' columns col : str Column name to average

Returns

pandas.DataFrame DataFrame with added column containing daily averages

Source code in monet/util/tools.py
def calc_24hr_ave(df: pd.DataFrame, col: str | None = None) -> pd.DataFrame:
    """Calculate 24-hour averages.

    Parameters
    ----------
    df : pandas.DataFrame
        Input data with 'time_local' and 'siteid' columns
    col : str
        Column name to average

    Returns
    -------
    pandas.DataFrame
        DataFrame with added column containing daily averages
    """
    if col is None:
        raise ValueError("col must be provided")

    df.index = df.time_local
    df_24hr_ave = df.groupby("siteid")[col].resample("D").mean().reset_index()
    df = df.reset_index(drop=True)
    return df.merge(df_24hr_ave, on=["siteid", "time_local"])

calc_3hr_ave(df, col=None)

Calculate 3-hour averages.

Parameters

df : pandas.DataFrame Input data with 'time_local' and 'siteid' columns col : str Column name to average

Returns

pandas.DataFrame DataFrame with added column containing 3-hour averages

Source code in monet/util/tools.py
def calc_3hr_ave(df: pd.DataFrame, col: str | None = None) -> pd.DataFrame:
    """Calculate 3-hour averages.

    Parameters
    ----------
    df : pandas.DataFrame
        Input data with 'time_local' and 'siteid' columns
    col : str
        Column name to average

    Returns
    -------
    pandas.DataFrame
        DataFrame with added column containing 3-hour averages
    """
    if col is None:
        raise ValueError("col must be provided")

    df.index = df.time_local
    df_3hr_ave = df.groupby("siteid")[col].resample("3H").mean().reset_index()
    df = df.reset_index(drop=True)
    return df.merge(df_3hr_ave, on=["siteid", "time_local"])

calc_annual_ave(df, col=None)

Calculate annual averages.

Parameters

df : pandas.DataFrame Input data with 'time_local' and 'siteid' columns col : str Column name to average

Returns

pandas.DataFrame DataFrame with added column containing annual averages

Source code in monet/util/tools.py
def calc_annual_ave(df: pd.DataFrame, col: str | None = None) -> pd.DataFrame:
    """Calculate annual averages.

    Parameters
    ----------
    df : pandas.DataFrame
        Input data with 'time_local' and 'siteid' columns
    col : str
        Column name to average

    Returns
    -------
    pandas.DataFrame
        DataFrame with added column containing annual averages
    """
    if col is None:
        raise ValueError("col must be provided")

    df.index = df.time_local
    df_annual_ave = df.groupby("siteid")[col].resample("A").mean().reset_index()
    df = df.reset_index(drop=True)
    return df.merge(df_annual_ave, on=["siteid", "time_local"])

get_giorgi_region_bounds(index=None, acronym=None)

Get lat/lon boundaries for a Giorgi region.

Giorgi regions are geographical regions defined for climate studies. Returns bounds for a region specified by index number or acronym.

Parameters

index : int, optional Region index number (1-22) acronym : str, optional Region acronym (e.g. 'NAU', 'SAU', etc)

Returns

numpy.ndarray Array containing [latmin, lonmin, latmax, lonmax, acronym]

Notes

Either index or acronym must be provided. For region definitions see: https://web.northeastern.edu/sds/web/demsos/images_002/subregions.jpg

Source code in monet/util/tools.py
def get_giorgi_region_bounds(index: int | None = None, acronym: str | None = None) -> np.ndarray:
    """Get lat/lon boundaries for a Giorgi region.

    Giorgi regions are geographical regions defined for climate studies.
    Returns bounds for a region specified by index number or acronym.

    Parameters
    ----------
    index : int, optional
        Region index number (1-22)
    acronym : str, optional
        Region acronym (e.g. 'NAU', 'SAU', etc)

    Returns
    -------
    numpy.ndarray
        Array containing [latmin, lonmin, latmax, lonmax, acronym]

    Notes
    -----
    Either index or acronym must be provided. For region definitions see:
    https://web.northeastern.edu/sds/web/demsos/images_002/subregions.jpg
    """
    df = pd.DataFrame(
        {
            "latmin": GIORGI_LATMIN,
            "lonmin": GIORGI_LONMIN,
            "latmax": GIORGI_LATMAX,
            "lonmax": GIORGI_LONMAX,
            "acronym": GIORGI_ACRONYMS,
        },
        index=GIORGI_INDICES,
    )

    if index is None and acronym is None:
        msg = (
            "either index or acronym needs to be supplied. "
            "look here https://web.northeastern.edu/sds/web/demsos/images_002/subregions.jpg"
        )
        raise ValueError(msg)
    elif index is not None:
        return df.loc[df.index == index].values.flatten()
    else:
        return df.loc[df.acronym == acronym.upper()].values.flatten()

get_giorgi_region_df(dset)

Add Giorgi region index and acronym to DataFrame or Dataset.

This implementation is backend-agnostic and supports Dask-backed xarray objects using xarray.apply_ufunc. Convention-aware: supports CF/COARDS and UGRID via MONET accessors.

Parameters

dset : pandas.DataFrame or xarray.Dataset DataFrame or Dataset containing latitude and longitude.

Returns

pandas.DataFrame or xarray.Dataset Input object with added columns/variables: - GIORGI_INDEX: region index number (float, to accommodate NaN) - GIORGI_ACRO: region acronym (str)

Source code in monet/util/tools.py
def get_giorgi_region_df(
    dset: pd.DataFrame | xr.Dataset,
) -> pd.DataFrame | xr.Dataset:
    """Add Giorgi region index and acronym to DataFrame or Dataset.

    This implementation is backend-agnostic and supports Dask-backed
    xarray objects using xarray.apply_ufunc.
    Convention-aware: supports CF/COARDS and UGRID via MONET accessors.

    Parameters
    ----------
    dset : pandas.DataFrame or xarray.Dataset
        DataFrame or Dataset containing latitude and longitude.

    Returns
    -------
    pandas.DataFrame or xarray.Dataset
        Input object with added columns/variables:
        - GIORGI_INDEX: region index number (float, to accommodate NaN)
        - GIORGI_ACRO: region acronym (str)
    """
    bounds = np.array([GIORGI_LONMIN, GIORGI_LATMIN, GIORGI_LONMAX, GIORGI_LATMAX]).T
    indices = np.array(GIORGI_INDICES)
    acronyms = np.array(GIORGI_ACRONYMS)

    lat = dset.monet.lat
    lon = dset.monet.lon

    if lat is None or lon is None:
        raise ValueError("Could not detect latitude and longitude coordinates.")

    if isinstance(dset, pd.DataFrame):
        dset["GIORGI_INDEX"] = _find_region_indices(lon.values, lat.values, bounds, indices)
        dset["GIORGI_ACRO"] = _find_region_acronyms(lon.values, lat.values, bounds, acronyms)
        return dset
    elif isinstance(dset, xr.Dataset):
        lat, lon = xr.broadcast(lat, lon)
        # Use apply_ufunc for Dask compatibility
        idx = _apply_vectorized(
            _find_region_indices,
            lon,
            lat,
            bounds=bounds,
            indices=indices,
            name="GIORGI region indices",
            source="monet.util.tools",
        )
        acro = _apply_vectorized(
            _find_region_acronyms,
            lon,
            lat,
            bounds=bounds,
            acronyms=acronyms,
            name="GIORGI region acronyms",
            output_dtypes=[object],
            source="monet.util.tools",
        )
        # Drop coords that already exist as data variables to avoid MergeError
        conflict = [v for v in list(idx.coords) if v in dset.data_vars]
        if conflict:
            idx = idx.drop_vars(conflict)
            acro = acro.drop_vars([v for v in conflict if v in acro.coords])
        dset["GIORGI_INDEX"] = idx
        dset["GIORGI_ACRO"] = acro

        return dset
    else:
        raise TypeError("dset must be a pandas.DataFrame or xarray.Dataset")

get_epa_region_bounds(index=None, acronym=None)

Get lat/lon boundaries for an EPA region.

Parameters

index : int, optional EPA region number acronym : str, optional EPA region acronym

Returns

list [latmin, lonmin, latmax, lonmax, acronym]

Source code in monet/util/tools.py
def get_epa_region_bounds(index: int | None = None, acronym: str | None = None) -> np.ndarray:
    """Get lat/lon boundaries for an EPA region.

    Parameters
    ----------
    index : int, optional
        EPA region number
    acronym : str, optional
        EPA region acronym

    Returns
    -------
    list
        [latmin, lonmin, latmax, lonmax, acronym]
    """
    df = pd.DataFrame(
        {
            "latmin": EPA_LATMIN,
            "lonmin": EPA_LONMIN,
            "latmax": EPA_LATMAX,
            "lonmax": EPA_LONMAX,
            "acronym": EPA_ACRONYMS,
        },
        index=EPA_INDICES,
    )

    if index is None and acronym is None:
        msg = (
            "either index or acronym needs to be supplied. "
            "Look here for more information: "
            "https://www.epa.gov/enviro/epa-regional-kml-download "
            "https://gist.github.com/jakebathman/719e8416191ba14bb6e700fc2d5fccc5"
        )
        raise ValueError(msg)
    elif index is not None:
        return df.loc[df.index == index].values.flatten()
    else:
        return df.loc[df.acronym == acronym.upper()].values.flatten()

get_epa_region_df(dset)

Add EPA region index and acronym to DataFrame or Dataset.

This implementation is backend-agnostic and supports Dask-backed xarray objects using xarray.apply_ufunc. Convention-aware: supports CF/COARDS and UGRID via MONET accessors.

Parameters

dset : pandas.DataFrame or xarray.Dataset DataFrame or Dataset containing latitude and longitude.

Returns

pandas.DataFrame or xarray.Dataset Input object with added columns/variables: - EPA_INDEX: region index number (float, to accommodate NaN) - EPA_ACRO: region acronym (str)

Source code in monet/util/tools.py
def get_epa_region_df(
    dset: pd.DataFrame | xr.Dataset,
) -> pd.DataFrame | xr.Dataset:
    """Add EPA region index and acronym to DataFrame or Dataset.

    This implementation is backend-agnostic and supports Dask-backed
    xarray objects using xarray.apply_ufunc.
    Convention-aware: supports CF/COARDS and UGRID via MONET accessors.

    Parameters
    ----------
    dset : pandas.DataFrame or xarray.Dataset
        DataFrame or Dataset containing latitude and longitude.

    Returns
    -------
    pandas.DataFrame or xarray.Dataset
        Input object with added columns/variables:
        - EPA_INDEX: region index number (float, to accommodate NaN)
        - EPA_ACRO: region acronym (str)
    """
    bounds = np.array([EPA_LONMIN, EPA_LATMIN, EPA_LONMAX, EPA_LATMAX]).T
    indices = np.array(EPA_INDICES)
    acronyms = np.array(EPA_ACRONYMS)

    lat = dset.monet.lat
    lon = dset.monet.lon

    if lat is None or lon is None:
        raise ValueError("Could not detect latitude and longitude coordinates.")

    if isinstance(dset, pd.DataFrame):
        dset["EPA_INDEX"] = _find_region_indices(lon.values, lat.values, bounds, indices)
        dset["EPA_ACRO"] = _find_region_acronyms(lon.values, lat.values, bounds, acronyms)
        return dset
    elif isinstance(dset, xr.Dataset):
        lat, lon = xr.broadcast(lat, lon)
        # Use apply_ufunc for Dask compatibility
        idx = _apply_vectorized(
            _find_region_indices,
            lon,
            lat,
            bounds=bounds,
            indices=indices,
            name="EPA region indices",
            source="monet.util.tools",
        )
        acro = _apply_vectorized(
            _find_region_acronyms,
            lon,
            lat,
            bounds=bounds,
            acronyms=acronyms,
            name="EPA region acronyms",
            output_dtypes=[object],
            source="monet.util.tools",
        )
        dset["EPA_INDEX"] = idx
        dset["EPA_ACRO"] = acro

        return dset
    else:
        raise TypeError("dset must be a pandas.DataFrame or xarray.Dataset")

add_mask(dset, mask_name, resolution=0.05, new_var=None)

Add a mask to a DataFrame or Dataset using the pre-computed mask system.

Parameters

dset : pandas.DataFrame or xarray.Dataset Input object containing latitude and longitude. mask_name : str Name of the mask (e.g., 'giorgi', 'ipcc_ar6', 'epa_eco', 'timezones', 'epa_admin', 'land'). resolution : float, default: 0.05 Resolution of the mask in degrees. new_var : str, optional Name of the new variable/column to create. Defaults to mask_name.

Returns

pandas.DataFrame or xarray.Dataset The input object with the added mask information.

Source code in monet/util/tools.py
def add_mask(
    dset: pd.DataFrame | xr.Dataset, mask_name: str, resolution: float = 0.05, new_var: str | None = None
) -> pd.DataFrame | xr.Dataset:
    """Add a mask to a DataFrame or Dataset using the pre-computed mask system.

    Parameters
    ----------
    dset : pandas.DataFrame or xarray.Dataset
        Input object containing latitude and longitude.
    mask_name : str
        Name of the mask (e.g., 'giorgi', 'ipcc_ar6', 'epa_eco', 'timezones', 'epa_admin', 'land').
    resolution : float, default: 0.05
        Resolution of the mask in degrees.
    new_var : str, optional
        Name of the new variable/column to create. Defaults to mask_name.

    Returns
    -------
    pandas.DataFrame or xarray.Dataset
        The input object with the added mask information.
    """
    from .mask import query_mask

    return query_mask(dset, mask_name, resolution=resolution, new_var=new_var)

calc_13_category_usda_soil_type(clay, sand, silt)

Calculate the 13 category USDA soil type from clay, sand and silt percentages.

Categories: 0 -- WATER 1 -- SAND 2 -- LOAMY SAND 3 -- SANDY LOAM 4 -- SILT LOAM 5 -- SILT 6 -- LOAM 7 -- SANDY CLAY LOAM 8 -- SILTY CLAY LOAM 9 -- CLAY LOAM 10 -- SANDY CLAY 11 -- SILTY CLAY 12 -- CLAY

Parameters

clay : float, numpy.ndarray, or xarray.DataArray Clay percentage (0-100). sand : float, numpy.ndarray, or xarray.DataArray Sand percentage (0-100). silt : float, numpy.ndarray, or xarray.DataArray Silt percentage (0-100).

Returns

stype : same type as input USDA soil type category (0-12).

Source code in monet/util/tools.py
def calc_13_category_usda_soil_type(clay: Any, sand: Any, silt: Any) -> Any:
    """Calculate the 13 category USDA soil type from clay, sand and silt percentages.

    Categories:
    0 -- WATER
    1 -- SAND
    2 -- LOAMY SAND
    3 -- SANDY LOAM
    4 -- SILT LOAM
    5 -- SILT
    6 -- LOAM
    7 -- SANDY CLAY LOAM
    8 -- SILTY CLAY LOAM
    9 -- CLAY LOAM
    10 -- SANDY CLAY
    11 -- SILTY CLAY
    12 -- CLAY

    Parameters
    ----------
    clay : float, numpy.ndarray, or xarray.DataArray
        Clay percentage (0-100).
    sand : float, numpy.ndarray, or xarray.DataArray
        Sand percentage (0-100).
    silt : float, numpy.ndarray, or xarray.DataArray
        Silt percentage (0-100).

    Returns
    -------
    stype : same type as input
        USDA soil type category (0-12).
    """

    def _logic(clay, sand, silt):
        stype = np.zeros(clay.shape)
        # 1 -- SAND
        stype[(silt + clay * 1.5 < 15.0) & (clay != 255)] = 1.0
        # 2 -- LOAMY SAND
        stype[(silt + 1.5 * clay >= 15.0) & (silt + 1.5 * clay < 30) & (clay != 255)] = 2.0
        # 3 -- SANDY LOAM
        stype[(clay >= 7.0) & (clay < 20) & (sand > 52) & (silt + 2 * clay >= 30) & (clay != 255)] = 3.0
        stype[(clay < 7) & (silt < 50) & (silt + 2 * clay >= 30) & (clay != 255)] = 3.0
        # 4 -- SILT LOAM
        stype[(silt >= 50) & (clay >= 12) & (clay < 27) & (clay != 255)] = 4.0
        stype[(silt >= 50) & (silt < 80) & (clay < 12) & (clay != 255)] = 4.0
        # 5 -- SILT
        stype[(silt >= 80) & (clay < 12) & (clay != 255)] = 5.0
        # 6 -- LOAM
        stype[(clay >= 7) & (clay < 27) & (silt >= 28) & (silt < 50) & (sand <= 52) & (clay != 255)] = 6.0
        # 7 -- SANDY CLAY LOAM
        stype[(clay >= 20) & (clay < 35) & (silt < 28) & (sand > 45) & (clay != 255)] = 7.0
        # 8 -- SILTY CLAY LOAM
        stype[(clay >= 27) & (clay < 40.0) & (sand > 40) & (clay != 255)] = 8.0
        # 9 -- CLAY LOAM
        stype[(clay >= 27) & (clay < 40.0) & (sand > 20) & (sand <= 45) & (clay != 255)] = 9.0
        # 10 -- SANDY CLAY
        stype[(clay >= 35) & (sand > 45) & (clay != 255)] = 10.0
        # 11 -- SILTY CLAY
        stype[(clay >= 40) & (silt >= 40) & (clay != 255)] = 11.0
        # 12 -- CLAY
        stype[(clay >= 40) & (sand <= 45) & (silt < 40) & (clay != 255)] = 12.0
        return stype

    return _apply_vectorized(_logic, clay, sand, silt, name="USDA soil type", source="monet.util.tools")

Meteorological Functions

monet.met_funcs

This package contains the main routines for estimating variables related to the Monin-Obukhov (MO) Similarity Theory, such as MO length, adiabatic correctors for heat and momentum transport. It requires the following package.

References

.. [Brutsaert2005] Brutsaert, W. (2005). Hydrology: an introduction (Vol. 61, No. 8). Cambridge: Cambridge University Press.

.. [Norman2000] Norman, J. M., W. P. Kustas, J. H. Prueger, and G. R. Diak (2000), Surface flux estimation using radiometric temperature: A dual-temperature-difference method to minimize measurement errors, Water Resour. Res., 36(8), 2263-2274, https://doi.org/10.1029/2000WR900033.

Functions

calc_pressure(z)

Calculates the barometric pressure above sea level.

Parameters

z: float or xarray.DataArray height above sea level (m).

Returns

p: float or xarray.DataArray air pressure (mb).

Source code in monet/met_funcs.py
def calc_pressure(z: ArrayLike) -> Any:
    """Calculates the barometric pressure above sea level.

    Parameters
    ----------
    z: float or xarray.DataArray
        height above sea level (m).

    Returns
    -------
    p: float or xarray.DataArray
        air pressure (mb)."""
    return _apply_vectorized(_pressure_logic, z, name="barometric pressure", source=_SOURCE)

calc_rho(p, ea, T_A_K)

Calculates the density of air.

Parameters

p : float or xarray.DataArray total air pressure (dry air + water vapour) (mb). ea : float or xarray.DataArray water vapor pressure at reference height above canopy (mb). T_A_K : float or xarray.DataArray air temperature at reference height (Kelvin).

Returns

rho : float or xarray.DataArray density of air (kg m-3).

References

based on equation (2.6) from Brutsaert (2005): Hydrology - An Introduction (pp 25).

Source code in monet/met_funcs.py
def calc_rho(p: ArrayLike, ea: ArrayLike, T_A_K: ArrayLike) -> Any:
    """Calculates the density of air.

    Parameters
    ----------
    p : float or xarray.DataArray
        total air pressure (dry air + water vapour) (mb).
    ea : float or xarray.DataArray
        water vapor pressure at reference height above canopy (mb).
    T_A_K : float or xarray.DataArray
        air temperature at reference height (Kelvin).

    Returns
    -------
    rho : float or xarray.DataArray
        density of air (kg m-3).

    References
    ----------
    based on equation (2.6) from Brutsaert (2005): Hydrology - An Introduction (pp 25)."""
    return _apply_vectorized(_rho_logic, p, ea, T_A_K, name="air density", source=_SOURCE)

calc_vapor_pressure(T_K)

Calculate the saturation water vapour pressure.

Parameters

T_K : float or xarray.DataArray temperature (K).

Returns

ea : float or xarray.DataArray saturation water vapour pressure (mb).

Source code in monet/met_funcs.py
def calc_vapor_pressure(T_K: ArrayLike) -> Any:
    """Calculate the saturation water vapour pressure.

    Parameters
    ----------
    T_K : float or xarray.DataArray
        temperature (K).

    Returns
    -------
    ea : float or xarray.DataArray
        saturation water vapour pressure (mb).
    """
    return _apply_vectorized(_vapor_pressure_logic, T_K, name="saturation vapor pressure", source=_SOURCE)

calc_mixing_ratio(ea, p)

Calculate ratio of mass of water vapour to the mass of dry air (-)

Parameters

ea : float or xarray.DataArray water vapor pressure at reference height (mb). p : float or xarray.DataArray total air pressure (dry air + water vapour) at reference height (mb).

Returns

r : float or xarray.DataArray mixing ratio (-)

References

https://glossary.ametsoc.org/wiki/Mixing_ratio

Source code in monet/met_funcs.py
def calc_mixing_ratio(ea: ArrayLike, p: ArrayLike) -> Any:
    """Calculate ratio of mass of water vapour to the mass of dry air (-)

    Parameters
    ----------
    ea : float or xarray.DataArray
        water vapor pressure at reference height (mb).
    p : float or xarray.DataArray
        total air pressure (dry air + water vapour) at reference height (mb).

    Returns
    -------
    r : float or xarray.DataArray
        mixing ratio (-)

    References
    ----------
    https://glossary.ametsoc.org/wiki/Mixing_ratio
    """
    return _apply_vectorized(_mixing_ratio_logic, ea, p, name="mixing ratio", source=_SOURCE)

calc_sun_angles(lat, lon, stdlon, doy, ftime)

Calculates the Sun Zenith and Azimuth Angles (SZA & SAA).

Parameters

lat : float or xarray.DataArray latitude of the site (degrees). lon : float or xarray.DataArray longitude of the site (degrees). stdlon : float or xarray.DataArray central longitude of the time zone of the site (degrees). doy : float or xarray.DataArray day of year of measurement (1-366). ftime : float or xarray.DataArray time of measurement (decimal hours).

Returns

sza, saa : float or xarray.DataArray Sun Zenith Angle (degrees) and Sun Azimuth Angle (degrees).

Source code in monet/met_funcs.py
def calc_sun_angles(lat: ArrayLike, lon: ArrayLike, stdlon: ArrayLike, doy: ArrayLike, ftime: ArrayLike) -> Any:
    """Calculates the Sun Zenith and Azimuth Angles (SZA & SAA).

    Parameters
    ----------
    lat : float or xarray.DataArray
        latitude of the site (degrees).
    lon : float or xarray.DataArray
        longitude of the site (degrees).
    stdlon : float or xarray.DataArray
        central longitude of the time zone of the site (degrees).
    doy : float or xarray.DataArray
        day of year of measurement (1-366).
    ftime : float or xarray.DataArray
        time of measurement (decimal hours).

    Returns
    -------
    sza, saa : float or xarray.DataArray
        Sun Zenith Angle (degrees) and Sun Azimuth Angle (degrees).
    """
    return _apply_vectorized(
        _sun_angles_logic,
        lat,
        lon,
        stdlon,
        doy,
        ftime,
        name="sun angles",
        output_dtypes=[float, float],
        input_core_dims=[[]] * 5,
        output_core_dims=[[], []],
        source=_SOURCE,
    )

calc_L(ustar, T_A_K, rho, c_p, H, LE)

Calculates the Monin-Obukhov length.

Parameters

ustar : float or xarray.DataArray friction velocity (m s-1). T_A_K : float or xarray.DataArray air temperature (Kelvin). rho : float or xarray.DataArray air density (kg m-3). c_p : float or xarray.DataArray Heat capacity of air at constant pressure (J kg-1 K-1). H : float or xarray.DataArray sensible heat flux (W m-2). LE : float or xarray.DataArray latent heat flux (W m-2).

Returns

L : float or xarray.DataArray Obukhov stability length (m).

References

[Brutsaert2005]_

Source code in monet/met_funcs.py
def calc_L(
    ustar: ArrayLike,
    T_A_K: ArrayLike,
    rho: ArrayLike,
    c_p: ArrayLike,
    H: ArrayLike,
    LE: ArrayLike,
) -> Any:
    """Calculates the Monin-Obukhov length.

    Parameters
    ----------
    ustar : float or xarray.DataArray
        friction velocity (m s-1).
    T_A_K : float or xarray.DataArray
        air temperature (Kelvin).
    rho : float or xarray.DataArray
        air density (kg m-3).
    c_p : float or xarray.DataArray
        Heat capacity of air at constant pressure (J kg-1 K-1).
    H : float or xarray.DataArray
        sensible heat flux (W m-2).
    LE : float or xarray.DataArray
        latent heat flux (W m-2).

    Returns
    -------
    L : float or xarray.DataArray
        Obukhov stability length (m).

    References
    ----------
    [Brutsaert2005]_
    """
    return _apply_vectorized(_L_logic, ustar, T_A_K, rho, c_p, H, LE, name="Obukhov stability length", source=_SOURCE)

calc_u_star(u, z_u, L, d_0, z_0M)

Friction velocity.

Parameters

u : float or xarray.DataArray wind speed above the surface (m s-1). z_u : float or xarray.DataArray wind speed measurement height (m). L : float or xarray.DataArray Monin Obukhov stability length (m). d_0 : float or xarray.DataArray zero-plane displacement height (m). z_0M : float or xarray.DataArray aerodynamic roughness length for momentum transport (m).

Returns

u_star : float or xarray.DataArray friction velocity (m s-1).

References

[Brutsaert2005]_

Source code in monet/met_funcs.py
def calc_u_star(u: ArrayLike, z_u: ArrayLike, L: ArrayLike, d_0: ArrayLike, z_0M: ArrayLike) -> Any:
    """Friction velocity.

    Parameters
    ----------
    u : float or xarray.DataArray
        wind speed above the surface (m s-1).
    z_u : float or xarray.DataArray
        wind speed measurement height (m).
    L : float or xarray.DataArray
        Monin Obukhov stability length (m).
    d_0 : float or xarray.DataArray
        zero-plane displacement height (m).
    z_0M : float or xarray.DataArray
        aerodynamic roughness length for momentum transport (m).

    Returns
    -------
    u_star : float or xarray.DataArray
        friction velocity (m s-1).

    References
    ----------
    [Brutsaert2005]_
    """
    return _apply_vectorized(_u_star_logic, u, z_u, L, d_0, z_0M, name="friction velocity (u*)", source=_SOURCE)

calc_Psi_H(zoL)

Calculates the adiabatic correction factor for heat transport.

Parameters

zoL : float or xarray.DataArray stability coefficient (unitless).

Returns

Psi_H : float or xarray.DataArray adiabatic corrector factor for heat transport (unitless).

References

[Brutsaert2005]_

Source code in monet/met_funcs.py
def calc_Psi_H(zoL: ArrayLike) -> Any:
    """Calculates the adiabatic correction factor for heat transport.

    Parameters
    ----------
    zoL : float or xarray.DataArray
        stability coefficient (unitless).

    Returns
    -------
    Psi_H : float or xarray.DataArray
        adiabatic corrector factor for heat transport (unitless).

    References
    ----------
    [Brutsaert2005]_
    """
    return _apply_vectorized(_Psi_H_logic, zoL, name="adiabatic correction factor (heat)", source=_SOURCE)

calc_Psi_M(zoL)

Adiabatic correction factor for momentum transport.

Parameters

zoL : float or xarray.DataArray stability coefficient (unitless).

Returns

Psi_M : float or xarray.DataArray adiabatic corrector factor for momentum transport (unitless).

References

[Brutsaert2005]_

Source code in monet/met_funcs.py
def calc_Psi_M(zoL: ArrayLike) -> Any:
    """Adiabatic correction factor for momentum transport.

    Parameters
    ----------
    zoL : float or xarray.DataArray
        stability coefficient (unitless).

    Returns
    -------
    Psi_M : float or xarray.DataArray
        adiabatic corrector factor for momentum transport (unitless).

    References
    ----------
    [Brutsaert2005]_
    """
    return _apply_vectorized(_Psi_M_logic, zoL, name="adiabatic correction factor (momentum)", source=_SOURCE)

Vertical Coordinate Utilities

monet.util.vertical

Vertical coordinate utilities for MONET.

Functions

calc_fv3_pressure(ak, bk, ps, dim='z')

Calculate pressure from FV3 hybrid coordinate coefficients.

The pressure at a given level is calculated using the formula: P = ak + bk * ps

Parameters

ak : xarray.DataArray or numpy.ndarray Hybrid coordinate coefficient 'ak'. Usually in Pa. bk : xarray.DataArray or numpy.ndarray Hybrid coordinate coefficient 'bk'. ps : xarray.DataArray or numpy.ndarray Surface pressure. Usually in Pa. dim : str, optional Name of the vertical dimension. Default is 'z'.

Returns

xarray.DataArray The calculated pressure.

Notes

This function is backend-agnostic and supports both NumPy and Dask-backed xarray objects.

Source code in monet/util/vertical.py
def calc_fv3_pressure(
    ak: xr.DataArray | np.ndarray,
    bk: xr.DataArray | np.ndarray,
    ps: xr.DataArray | np.ndarray,
    dim: str = "z",
) -> xr.DataArray:
    """Calculate pressure from FV3 hybrid coordinate coefficients.

    The pressure at a given level is calculated using the formula:
    P = ak + bk * ps

    Parameters
    ----------
    ak : xarray.DataArray or numpy.ndarray
        Hybrid coordinate coefficient 'ak'. Usually in Pa.
    bk : xarray.DataArray or numpy.ndarray
        Hybrid coordinate coefficient 'bk'.
    ps : xarray.DataArray or numpy.ndarray
        Surface pressure. Usually in Pa.
    dim : str, optional
        Name of the vertical dimension. Default is 'z'.

    Returns
    -------
    xarray.DataArray
        The calculated pressure.

    Notes
    -----
    This function is backend-agnostic and supports both NumPy and Dask-backed
    xarray objects.
    """
    if not isinstance(ak, xr.DataArray):
        ak = xr.DataArray(ak, dims=[dim])
    if not isinstance(bk, xr.DataArray):
        bk = xr.DataArray(bk, dims=[dim])
    if not isinstance(ps, xr.DataArray):
        ps = xr.DataArray(ps, dims=[f"__dim_{i}" for i in range(ps.ndim)])

    def _p_logic(a, b, s):
        # Core dimensions are moved to the end by apply_ufunc.
        # a, b: (..., N), s: (...)
        return a + b * s[..., np.newaxis]

    res = xr.apply_ufunc(
        _p_logic,
        ak,
        bk,
        ps,
        input_core_dims=[[dim], [dim], []],
        output_core_dims=[[dim]],
        dask="parallelized",
    )

    if isinstance(res, xr.DataArray):
        # Move vertical dim to the front to match standard MONET convention
        all_dims = [dim] + [d for d in res.dims if d != dim]
        res = res.transpose(*all_dims)

        from .conventions import update_history

        update_history(res, "Calculated pressure via calc_fv3_pressure")
        res.name = "pressure"
        if hasattr(ps, "attrs") and "units" in ps.attrs:
            res.attrs["units"] = ps.attrs["units"]

    return res

calc_fv3_height(temp, phalf, hsfc=0.0, dim='z')

Calculate geopotential height using the hypsometric equation.

Calculates geopotential height at layer interfaces (phalf levels) by integrating the hypsometric equation from the surface upwards.

Parameters

temp : xarray.DataArray or numpy.ndarray Air temperature at layer centers (K). Shape: (n_layers, ...) phalf : xarray.DataArray or numpy.ndarray Pressure at layer interfaces (Pa). Shape: (n_layers + 1, ...) hsfc : xarray.DataArray or numpy.ndarray, default 0.0 Surface geopotential height (m). dim : str, optional Name of the vertical dimension. Default is 'z'.

Returns

xarray.DataArray Geopotential height at interfaces (m). Shape: (n_layers + 1, ...)

Notes

This function assumes the vertical dimension is ordered from top to bottom (index 0 is the top of the atmosphere, last index is the surface). It is backend-agnostic and supports both NumPy and Dask-backed xarray objects.

Source code in monet/util/vertical.py
def calc_fv3_height(
    temp: xr.DataArray | np.ndarray,
    phalf: xr.DataArray | np.ndarray,
    hsfc: xr.DataArray | np.ndarray = 0.0,
    dim: str = "z",
) -> xr.DataArray:
    """Calculate geopotential height using the hypsometric equation.

    Calculates geopotential height at layer interfaces (phalf levels) by
    integrating the hypsometric equation from the surface upwards.

    Parameters
    ----------
    temp : xarray.DataArray or numpy.ndarray
        Air temperature at layer centers (K). Shape: (n_layers, ...)
    phalf : xarray.DataArray or numpy.ndarray
        Pressure at layer interfaces (Pa). Shape: (n_layers + 1, ...)
    hsfc : xarray.DataArray or numpy.ndarray, default 0.0
        Surface geopotential height (m).
    dim : str, optional
        Name of the vertical dimension. Default is 'z'.

    Returns
    -------
    xarray.DataArray
        Geopotential height at interfaces (m). Shape: (n_layers + 1, ...)

    Notes
    -----
    This function assumes the vertical dimension is ordered from top to bottom
    (index 0 is the top of the atmosphere, last index is the surface).
    It is backend-agnostic and supports both NumPy and Dask-backed xarray objects.
    """
    # --- Validate vertical ordering ----------------------------------------
    # phalf pressure should *decrease* from index 0 (TOA) to last (surface)
    # when ordered top-to-bottom.  Check the first slice to detect a
    # bottom-up array and raise early rather than produce a wrong result.
    if isinstance(phalf, xr.DataArray):
        _p_check = phalf.isel({dim: slice(None, 2)}).values
    else:
        _p_check = np.asarray(phalf).flat[:2] if np.asarray(phalf).ndim >= 1 else None
    if _p_check is not None and len(_p_check) >= 2:
        # Flatten to 1-D for the comparison (handles multi-dim arrays)
        _p_flat = np.asarray(_p_check).ravel()
        if _p_flat[0] < _p_flat[1]:
            raise ValueError(
                "calc_fv3_height expects phalf ordered top-to-bottom "
                "(pressure *decreasing* from index 0 toward the surface). "
                "Received an array where phalf[0] < phalf[1], which indicates "
                "bottom-up ordering. Reverse the vertical dimension before "
                "calling this function (e.g. phalf = phalf.isel(z=slice(None, None, -1)))."
            )
    # -----------------------------------------------------------------------
    if not isinstance(temp, xr.DataArray):
        temp = xr.DataArray(temp, dims=[dim] + [f"__dim_{i}" for i in range(temp.ndim - 1)])
    if not isinstance(phalf, xr.DataArray):
        phalf = xr.DataArray(phalf, dims=[dim] + [f"__dim_{i}" for i in range(phalf.ndim - 1)])

    if not isinstance(hsfc, xr.DataArray):
        # Attempt to match spatial dimensions of temp if hsfc is not a DataArray
        spatial_dims = [d for d in temp.dims if d != dim]
        if hsfc.ndim == len(spatial_dims):
            hsfc = xr.DataArray(hsfc, dims=spatial_dims)
        else:
            hsfc = xr.DataArray(hsfc)

    def _hydrostatic_logic(t, p_int, h_s):
        # Core dimensions are moved to the end.
        # t: (..., N), p_int: (..., N+1), h_s: (...)
        p_up = p_int[..., :-1]
        p_lo = p_int[..., 1:]
        # dz = (R_d * T / g) * ln(p_lo / p_up)
        dz = (R_d * t / g) * np.log(p_lo / p_up)

        h_int = np.empty_like(p_int)
        h_int[..., -1] = h_s
        # Summing from bottom up (along the last dimension)
        h_int[..., :-1] = h_s[..., np.newaxis] + np.cumsum(dz[..., ::-1], axis=-1)[..., ::-1]
        return h_int

    # Rename dims to allow different sizes in apply_ufunc
    temp_renamed = temp.rename({dim: "__layer"})
    phalf_renamed = phalf.rename({dim: "__interface"})

    res = xr.apply_ufunc(
        _hydrostatic_logic,
        temp_renamed,
        phalf_renamed,
        hsfc,
        input_core_dims=[["__layer"], ["__interface"], []],
        output_core_dims=[["__interface"]],
        dask="parallelized",
        output_dtypes=[temp.dtype],
    )

    res = res.rename({"__interface": dim})

    if isinstance(res, xr.DataArray):
        # Move vertical dim to the front to match standard MONET convention
        all_dims = [dim] + [d for d in res.dims if d != dim]
        res = res.transpose(*all_dims)

        from .conventions import update_history

        update_history(res, "Calculated geopotential height via calc_fv3_height")
        res.name = "height"
        res.attrs["units"] = "m"

    return res

Combine Tool

monet.util.combinetool

Functions

pair(model, obs, *, method='nearest', interp_time=False, suffix='_model', merge=True, **kwargs)

Unified interface for pairing model and observation data.

Supports xarray (Dataset/DataArray) and DataFrame (pandas/dask) objects. Maintains laziness for Dask-backed objects. Supports both CF/COARDS and UGRID conventions via automatic standardization.

Parameters

model : xarray.Dataset or xarray.DataArray Model data (usually gridded). obs : xarray.Dataset, xarray.DataArray, pandas.DataFrame, or dask.dataframe.DataFrame Observation data. method : str, default 'nearest' Spatial interpolation method. interp_time : bool, default False Whether to interpolate in time. suffix : str, default '_model' Suffix for model variables if names conflict. merge : bool, default True Whether to merge the paired data with the original observations. **kwargs : dict Additional arguments passed to regridding backend.

Returns

xarray.Dataset, xarray.DataArray, pandas.DataFrame, or dask.dataframe.DataFrame Matched object of the same type as obs.

Examples

paired_df = pair(model_ds, obs_df, method='bilinear')

Source code in monet/util/combinetool.py
def pair(
    model: xr.Dataset | xr.DataArray,
    obs: xr.Dataset | xr.DataArray | pd.DataFrame | t.Any,
    *,
    method: str = "nearest",
    interp_time: bool = False,
    suffix: str = "_model",
    merge: bool = True,
    **kwargs: t.Any,
) -> xr.Dataset | xr.DataArray | pd.DataFrame | t.Any:
    """Unified interface for pairing model and observation data.

    Supports xarray (Dataset/DataArray) and DataFrame (pandas/dask) objects.
    Maintains laziness for Dask-backed objects.
    Supports both CF/COARDS and UGRID conventions via automatic standardization.

    Parameters
    ----------
    model : xarray.Dataset or xarray.DataArray
        Model data (usually gridded).
    obs : xarray.Dataset, xarray.DataArray, pandas.DataFrame, or dask.dataframe.DataFrame
        Observation data.
    method : str, default 'nearest'
        Spatial interpolation method.
    interp_time : bool, default False
        Whether to interpolate in time.
    suffix : str, default '_model'
        Suffix for model variables if names conflict.
    merge : bool, default True
        Whether to merge the paired data with the original observations.
    **kwargs : dict
        Additional arguments passed to regridding backend.

    Returns
    -------
    xarray.Dataset, xarray.DataArray, pandas.DataFrame, or dask.dataframe.DataFrame
        Matched object of the same type as `obs`.

    Examples
    --------
    >>> paired_df = pair(model_ds, obs_df, method='bilinear')
    """
    if isinstance(obs, xr.Dataset | xr.DataArray):
        return _pair_xarray(model, obs, method=method, interp_time=interp_time, suffix=suffix, merge=merge, **kwargs)
    elif isinstance(obs, pd.DataFrame) or (has_dask_df and isinstance(obs, dd.DataFrame)):
        return _pair_dataframe(model, obs, method=method, interp_time=interp_time, suffix=suffix, merge=merge, **kwargs)
    else:
        raise TypeError(f"Unsupported type for obs: {type(obs)}")

combine_da_to_df(da, df, *, merge=True, suffix=None, **kwargs)

Combine xarray data with point observations in a dataframe.

Note: This is a backward compatibility wrapper for monet.pair.

Parameters

da : xarray.DataArray or xarray.Dataset Gridded data to be interpolated to target points. df : pandas.DataFrame Point observations. merge : bool, default True Whether to merge with original DataFrame. suffix : str, optional Suffix to add to variable names. **kwargs : dict Passed to pair.

Returns

pandas.DataFrame Combined DataFrame.

Source code in monet/util/combinetool.py
def combine_da_to_df(
    da: xr.DataArray | xr.Dataset,
    df: pd.DataFrame,
    *,
    merge: bool = True,
    suffix: str | None = None,
    **kwargs: t.Any,
) -> pd.DataFrame:
    """Combine xarray data with point observations in a dataframe.

    Note: This is a backward compatibility wrapper for `monet.pair`.

    Parameters
    ----------
    da : xarray.DataArray or xarray.Dataset
        Gridded data to be interpolated to target points.
    df : pandas.DataFrame
        Point observations.
    merge : bool, default True
        Whether to merge with original DataFrame.
    suffix : str, optional
        Suffix to add to variable names.
    **kwargs : dict
        Passed to `pair`.

    Returns
    -------
    pandas.DataFrame
        Combined DataFrame.
    """
    if suffix is not None:
        kwargs["suffix"] = suffix
    return pair(da, df, merge=merge, **kwargs)  # type: ignore

combine_da_to_da(source, target, *, merge=True, interp_time=False, **kwargs)

Combine gridded data with point observation data in xarray format.

Note: This is a backward compatibility wrapper. It restores the old behavior of expanding 1D coordinates to a 2D meshgrid for compatibility with legacy tests. For point-to-point pairing, use monet.pair.

Parameters

source : xarray.DataArray or xarray.Dataset Gridded data to interpolate from. target : xarray.DataArray or xarray.Dataset Target grid or point observation data. merge : bool, default True Whether to merge. interp_time : bool, default False Whether to interpolate in time. **kwargs : dict Additional arguments passed to resample.

Returns

xarray.Dataset or xarray.DataArray Combined Dataset.

Source code in monet/util/combinetool.py
def combine_da_to_da(
    source: xr.DataArray | xr.Dataset,
    target: xr.DataArray | xr.Dataset,
    *,
    merge: bool = True,
    interp_time: bool = False,
    **kwargs: t.Any,
) -> xr.Dataset | xr.DataArray:
    """Combine gridded data with point observation data in xarray format.

    Note: This is a backward compatibility wrapper. It restores the old behavior
    of expanding 1D coordinates to a 2D meshgrid for compatibility with legacy tests.
    For point-to-point pairing, use `monet.pair`.

    Parameters
    ----------
    source : xarray.DataArray or xarray.Dataset
        Gridded data to interpolate from.
    target : xarray.DataArray or xarray.Dataset
        Target grid or point observation data.
    merge : bool, default True
        Whether to merge.
    interp_time : bool, default False
        Whether to interpolate in time.
    **kwargs : dict
        Additional arguments passed to `resample`.

    Returns
    -------
    xarray.Dataset or xarray.DataArray
        Combined Dataset.
    """
    from .interp_util import lonlat_to_dataset
    from .resample import resample

    # Check for legacy meshgrid expansion (if lat/lon are 1D and share a dimension)
    target_grid = target

    # Direct coordinate detection for expansion to be more robust
    lat_names = ["latitude", "lat", "Latitude", "y"]
    lon_names = ["longitude", "lon", "Longitude", "x"]

    lat_da = None
    for n in lat_names:
        if n in target.coords:
            lat_da = target[n]
            break

    lon_da = None
    for n in lon_names:
        if n in target.coords:
            lon_da = target[n]
            break

    if lat_da is not None and lon_da is not None:
        if lat_da.ndim == 1 and lon_da.ndim == 1 and lat_da.dims == lon_da.dims:
            # Legacy behavior: expand to meshgrid
            target_grid = lonlat_to_dataset(lon_da.values, lat_da.values)

    # Use resample directly instead of pair to avoid point-mode logic in pair
    paired = resample(source, target_grid, **kwargs)

    if interp_time and "time" in target.coords:
        paired = paired.interp(time=target.time)

    if merge:
        # Note: Merging an expanded grid with the original points might lead to
        # unexpected results (broadcasting), but this matches legacy behavior if merge=True was used.
        return xr.merge([target, paired])
    else:
        return paired

combine_da_to_height_profile(da, dset, *, radius_of_influence=12000.0)

This function will combine an xarray.DataArray to a 2d dataset with dimensions (time,z)

Parameters

da : xarray.DataArray dset : xarray.Dataset Dataset containing vertical profile observations radius_of_influence : float, optional Search radius for nearest neighbor interpolation in meters. Default is 12km.

Returns

xarray.Dataset Combined dataset with interpolated model values at observation heights

Source code in monet/util/combinetool.py
def combine_da_to_height_profile(da, dset, *, radius_of_influence=12e3):
    """This function will combine an xarray.DataArray to a 2d dataset with
    dimensions (time,z)

    Parameters
    ----------
    da : xarray.DataArray
    dset : xarray.Dataset
        Dataset containing vertical profile observations
    radius_of_influence : float, optional
        Search radius for nearest neighbor interpolation in meters.
        Default is 12km.

    Returns
    -------
    xarray.Dataset
        Combined dataset with interpolated model values at observation heights
    """
    # from ..util.interp_util import nearest_point_swathdefinition
    lon, lat = dset.longitude, dset.latitude
    # target_grid = nearest_point_swathdefinition(longitude=lon, latitude=lat)
    da_interped = da.monet.nearest_latlon(lon=lon, lat=lat, radius_of_influence=radius_of_influence)

    # FIXME: interp to height here

    dset[da.name] = da_interped

    return dset

Error Metrics

Statistical metrics are provided by the monet-stats package, and exposed through monet.util.stats.

monet.util.stats

Statistics utilities module - compatibility layer for monet_stats.

This module imports monet_stats and re-exports all functions for backward compatibility. The actual statistical functions are now provided by the monet-stats package.