Skip to content

Encoders

Feature encodings for coordinates and time — useful as inputs to learned models. Cyclical time encoding maps a periodic coordinate onto the unit circle, \(\bigl(\sin(2\pi t/T),\cos(2\pi t/T)\bigr)\); Fourier features lift coordinates into a higher-dimensional sinusoidal basis.

Operators

EncodeTimeCyclical

EncodeTimeCyclical(components: Sequence[str] = ('dayofyear', 'hour'), time: str = 'time')

Bases: Operator

Wrap :func:xrops.transforms.encoders.encode_time_cyclical.

Source code in packages/xrtoolz-ops/src/xrops/transforms/operators.py
def __init__(
    self,
    components: Sequence[str] = ("dayofyear", "hour"),
    time: str = "time",
) -> None:
    self.components = tuple(components)
    self.time = time

EncodeTimeOrdinal

EncodeTimeOrdinal(reference_date: str | datetime64 | None = None, time: str = 'time', unit: str = 'D')

Bases: Operator

Wrap :func:xrops.transforms.encoders.encode_time_ordinal.

Source code in packages/xrtoolz-ops/src/xrops/transforms/operators.py
def __init__(
    self,
    reference_date: str | np.datetime64 | None = None,
    time: str = "time",
    unit: str = "D",
) -> None:
    self.reference_date = reference_date
    self.time = time
    self.unit = unit

CyclicalEncode

CyclicalEncode(variable: str, period: float)

Bases: Operator

Sin/cos embedding of a periodic variable.

Reads ds[variable], calls :func:cyclical_encode, and attaches two new data variables {variable}_sin and {variable}_cos to the dataset (preserving all other variables).

Source code in packages/xrtoolz-ops/src/xrops/transforms/operators.py
def __init__(self, variable: str, period: float) -> None:
    self.variable = variable
    self.period = float(period)

FourierFeatures

FourierFeatures(variable: str, num_freqs: int, scale: float = 1.0, *, output_name: str | None = None, feature_dim: str = 'feature')

Bases: Operator

Deterministic Fourier-feature encoding of ds[variable].

Output is a new variable with one extra trailing feature_dim axis of length 2 * num_freqs.

Source code in packages/xrtoolz-ops/src/xrops/transforms/operators.py
def __init__(
    self,
    variable: str,
    num_freqs: int,
    scale: float = 1.0,
    *,
    output_name: str | None = None,
    feature_dim: str = "feature",
) -> None:
    self.variable = variable
    self.num_freqs = int(num_freqs)
    self.scale = float(scale)
    self.output_name = output_name
    self.feature_dim = feature_dim

RandomFourierFeatures

RandomFourierFeatures(variable: str, num_features: int, sigma: float = 1.0, seed: int | None = None, *, output_name: str | None = None, feature_dim: str = 'feature')

Bases: Operator

Random Fourier features (Rahimi & Recht, 2007) of ds[variable].

Source code in packages/xrtoolz-ops/src/xrops/transforms/operators.py
def __init__(
    self,
    variable: str,
    num_features: int,
    sigma: float = 1.0,
    seed: int | None = None,
    *,
    output_name: str | None = None,
    feature_dim: str = "feature",
) -> None:
    self.variable = variable
    self.num_features = int(num_features)
    self.sigma = float(sigma)
    self.seed = seed
    self.output_name = output_name
    self.feature_dim = feature_dim

PositionalEncoding

PositionalEncoding(variable: str, num_freqs: int, include_input: bool = True, *, output_name: str | None = None, feature_dim: str = 'feature')

Bases: Operator

NeRF-style positional encoding of ds[variable].

Source code in packages/xrtoolz-ops/src/xrops/transforms/operators.py
def __init__(
    self,
    variable: str,
    num_freqs: int,
    include_input: bool = True,
    *,
    output_name: str | None = None,
    feature_dim: str = "feature",
) -> None:
    self.variable = variable
    self.num_freqs = int(num_freqs)
    self.include_input = bool(include_input)
    self.output_name = output_name
    self.feature_dim = feature_dim

TimeRescale

TimeRescale(freq_dt: float = 1.0, freq_unit: str = 's', t0: str | datetime64 | None = None, time: str = 'time')

Bases: Operator

Wrap :func:xrops.transforms.encoders.time_rescale.

Source code in packages/xrtoolz-ops/src/xrops/transforms/operators.py
def __init__(
    self,
    freq_dt: float = 1.0,
    freq_unit: str = "s",
    t0: str | np.datetime64 | None = None,
    time: str = "time",
) -> None:
    self.freq_dt = float(freq_dt)
    self.freq_unit = freq_unit
    self.t0 = t0
    self.time = time

TimeUnrescale

TimeUnrescale(time: str = 'time')

Bases: Operator

Wrap :func:xrops.transforms.encoders.time_unrescale.

Source code in packages/xrtoolz-ops/src/xrops/transforms/operators.py
def __init__(self, time: str = "time") -> None:
    self.time = time

Functional primitives (Layer 0)

These pure functions back the operators above; each takes xr.DataArray/xr.Dataset and a dim: argument.

encode_time_cyclical

encode_time_cyclical(time: DataArray, *, components: Sequence[str] = ('dayofyear', 'hour')) -> Dataset

Sin/cos encodings of datetime components as a Dataset of new variables.

For each component in components (any name accepted by xarray's .dt accessor), two variables are produced: {component}_sin and {component}_cos. The Layer-1 EncodeTimeCyclical operator merges these into the input Dataset as new coordinates.

Parameters:

Name Type Description Default
time DataArray

Time coordinate / variable.

required
components Sequence[str]

Iterable of datetime attributes to encode.

('dayofyear', 'hour')

Returns:

Type Description
Dataset

Dataset whose data variables are the requested sin/cos pairs,

Dataset

each carrying the same dims as time.

Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/coord_time.py
def encode_time_cyclical(
    time: xr.DataArray,
    *,
    components: Sequence[str] = ("dayofyear", "hour"),
) -> xr.Dataset:
    """Sin/cos encodings of datetime components as a Dataset of new variables.

    For each ``component`` in ``components`` (any name accepted by
    xarray's ``.dt`` accessor), two variables are produced:
    ``{component}_sin`` and ``{component}_cos``. The Layer-1
    ``EncodeTimeCyclical`` operator merges these into the input Dataset
    as new coordinates.

    Args:
        time: Time coordinate / variable.
        components: Iterable of datetime attributes to encode.

    Returns:
        Dataset whose data variables are the requested sin/cos pairs,
        each carrying the same dims as ``time``.
    """
    periods = {
        "dayofyear": 366.0,
        "day": 31.0,
        "month": 12.0,
        "hour": 24.0,
        "minute": 60.0,
        "second": 60.0,
        "weekday": 7.0,
    }
    out_vars: dict[str, xr.DataArray] = {}
    for name in components:
        if name not in periods:
            raise ValueError(
                f"Unknown time component {name!r}; known: {sorted(periods)}."
            )
        values = getattr(time.dt, name).values.astype(float)
        sin, cos = _sin_cos(values, periods[name])
        out_vars[f"{name}_sin"] = xr.DataArray(sin, dims=time.dims, coords=time.coords)
        out_vars[f"{name}_cos"] = xr.DataArray(cos, dims=time.dims, coords=time.coords)
    # Carry the source time coords on the output Dataset so direct
    # callers can still do label-based selection / alignment.
    return xr.Dataset(out_vars, coords=time.coords)

encode_time_ordinal

encode_time_ordinal(time: DataArray, *, reference_date: str | datetime64 | None = None, unit: str = 'D') -> DataArray

Ordinal float encoding of a time coordinate.

Parameters:

Name Type Description Default
time DataArray

Time coordinate / variable.

required
reference_date str | datetime64 | None

Reference time. Defaults to time.min().

None
unit str

Pandas timedelta unit used to scale the offset.

'D'

Returns:

Type Description
DataArray

DataArray of ordinal floats with the same dims as time.

DataArray

The Layer-1 EncodeTimeOrdinal operator attaches this as a

DataArray

{time.name}_ordinal coordinate on the input Dataset.

Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/coord_time.py
def encode_time_ordinal(
    time: xr.DataArray,
    *,
    reference_date: str | np.datetime64 | None = None,
    unit: str = "D",
) -> xr.DataArray:
    """Ordinal float encoding of a time coordinate.

    Args:
        time: Time coordinate / variable.
        reference_date: Reference time. Defaults to ``time.min()``.
        unit: Pandas timedelta unit used to scale the offset.

    Returns:
        DataArray of ordinal floats with the same dims as ``time``.
        The Layer-1 ``EncodeTimeOrdinal`` operator attaches this as a
        ``{time.name}_ordinal`` coordinate on the input Dataset.
    """
    ref = (
        np.datetime64(time.min().values)
        if reference_date is None
        else np.datetime64(reference_date)
    )
    delta = pd.Timedelta(1, unit=unit)
    ordinal = ((time.values - ref) / delta).astype(np.float64)
    # Keep every input coord (including the source timestamp coord at
    # ``time.name``) so direct callers can still ``out.sel(time=...)``.
    return xr.DataArray(
        ordinal,
        dims=time.dims,
        coords=time.coords,
        name=f"{time.name}_ordinal" if time.name is not None else "time_ordinal",
    )

cyclical_encode

cyclical_encode(da: DataArray, *, period: float) -> Dataset

Sin/cos embedding of a periodic variable.

Parameters:

Name Type Description Default
da DataArray

Input values with period period.

required
period float

Period length (e.g. 365.25 for day-of-year, 24 for hour of day, 2 * np.pi for radians).

required

Returns:

Type Description
Dataset

Dataset with sin and cos data variables, each the same

Dataset

shape as da.

Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/basis.py
def cyclical_encode(da: xr.DataArray, *, period: float) -> xr.Dataset:
    """Sin/cos embedding of a periodic variable.

    Args:
        da: Input values with period ``period``.
        period: Period length (e.g. 365.25 for day-of-year, 24 for hour
            of day, ``2 * np.pi`` for radians).

    Returns:
        Dataset with ``sin`` and ``cos`` data variables, each the same
        shape as ``da``.
    """
    x = 2.0 * np.pi * da / period
    return xr.Dataset({"sin": np.sin(x), "cos": np.cos(x)})

fourier_features

fourier_features(da: DataArray, *, num_freqs: int, scale: float = 1.0, feature_dim: str = 'feature') -> DataArray

Deterministic Fourier-feature encoding.

Adds a trailing feature_dim of length 2 * num_freqs whose entries alternate sin(2^k * scale * x) and cos(2^k * scale * x).

Parameters:

Name Type Description Default
da DataArray

Input DataArray.

required
num_freqs int

Number of frequency octaves to use.

required
scale float

Base frequency.

1.0
feature_dim str

Name of the appended feature dimension.

'feature'
Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/basis.py
def fourier_features(
    da: xr.DataArray,
    *,
    num_freqs: int,
    scale: float = 1.0,
    feature_dim: str = "feature",
) -> xr.DataArray:
    """Deterministic Fourier-feature encoding.

    Adds a trailing ``feature_dim`` of length ``2 * num_freqs`` whose
    entries alternate ``sin(2^k * scale * x)`` and ``cos(2^k * scale * x)``.

    Args:
        da: Input DataArray.
        num_freqs: Number of frequency octaves to use.
        scale: Base frequency.
        feature_dim: Name of the appended feature dimension.
    """
    return xr.apply_ufunc(
        _fourier_features_array,
        da,
        input_core_dims=[[]],
        output_core_dims=[[feature_dim]],
        kwargs={"num_freqs": num_freqs, "scale": scale},
        keep_attrs=True,
        dask="parallelized",
        dask_gufunc_kwargs={"output_sizes": {feature_dim: 2 * num_freqs}},
        output_dtypes=[np.promote_types(da.dtype, np.float32)],
    )

random_fourier_features

random_fourier_features(da: DataArray, *, num_features: int, sigma: float = 1.0, seed: int | None = None, input_dim: Hashable | None = None, feature_dim: str = 'feature') -> DataArray

Random Fourier features in the style of Rahimi & Recht, 2007.

Approximates the RBF kernel feature map. Adds a trailing feature_dim of length num_features.

Parameters:

Name Type Description Default
da DataArray

Input DataArray.

required
num_features int

Output feature dimension (must be even).

required
sigma float

Bandwidth of the underlying RBF kernel.

1.0
seed int | None

Seed for the RNG used to draw the random frequencies.

None
input_dim Hashable | None

Optional existing dimension of da to treat as the channel/feature axis to project from. When None each element is a scalar feature (d = 1); when given, that dimension is replaced by feature_dim.

None
feature_dim str

Name of the appended feature dimension.

'feature'
Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/basis.py
def random_fourier_features(
    da: xr.DataArray,
    *,
    num_features: int,
    sigma: float = 1.0,
    seed: int | None = None,
    input_dim: Hashable | None = None,
    feature_dim: str = "feature",
) -> xr.DataArray:
    """Random Fourier features in the style of Rahimi & Recht, 2007.

    Approximates the RBF kernel feature map. Adds a trailing
    ``feature_dim`` of length ``num_features``.

    Args:
        da: Input DataArray.
        num_features: Output feature dimension (must be even).
        sigma: Bandwidth of the underlying RBF kernel.
        seed: Seed for the RNG used to draw the random frequencies.
        input_dim: Optional existing dimension of ``da`` to treat as the
            channel/feature axis to project from. When ``None`` each
            element is a scalar feature (``d = 1``); when given, that
            dimension is replaced by ``feature_dim``.
        feature_dim: Name of the appended feature dimension.
    """
    if num_features % 2 != 0:
        raise ValueError("num_features must be even.")
    core_in = [[input_dim]] if input_dim is not None else [[]]
    return xr.apply_ufunc(
        _random_fourier_features_array,
        da,
        input_core_dims=core_in,
        output_core_dims=[[feature_dim]],
        kwargs={
            "num_features": num_features,
            "sigma": sigma,
            "seed": seed,
            "projected": input_dim is not None,
        },
        keep_attrs=True,
        dask="parallelized",
        dask_gufunc_kwargs={"output_sizes": {feature_dim: num_features}},
        output_dtypes=[np.promote_types(da.dtype, np.float32)],
    )

positional_encoding

positional_encoding(da: DataArray, *, num_freqs: int, include_input: bool = True, feature_dim: str = 'feature') -> DataArray

NeRF-style positional encoding.

Adds a trailing feature_dim of length (2 * num_freqs) + include_input.

Parameters:

Name Type Description Default
da DataArray

Input DataArray.

required
num_freqs int

Number of octave frequencies.

required
include_input bool

If True, prepend the raw input as an additional feature column.

True
feature_dim str

Name of the appended feature dimension.

'feature'
Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/basis.py
def positional_encoding(
    da: xr.DataArray,
    *,
    num_freqs: int,
    include_input: bool = True,
    feature_dim: str = "feature",
) -> xr.DataArray:
    """NeRF-style positional encoding.

    Adds a trailing ``feature_dim`` of length
    ``(2 * num_freqs) + include_input``.

    Args:
        da: Input DataArray.
        num_freqs: Number of octave frequencies.
        include_input: If ``True``, prepend the raw input as an
            additional feature column.
        feature_dim: Name of the appended feature dimension.
    """
    out_size = 2 * num_freqs + int(include_input)
    return xr.apply_ufunc(
        _positional_encoding_array,
        da,
        input_core_dims=[[]],
        output_core_dims=[[feature_dim]],
        kwargs={"num_freqs": num_freqs, "include_input": include_input},
        keep_attrs=True,
        dask="parallelized",
        dask_gufunc_kwargs={"output_sizes": {feature_dim: out_size}},
        output_dtypes=[np.promote_types(da.dtype, np.float32)],
    )

time_rescale

time_rescale(time: DataArray, *, freq_dt: float = 1.0, freq_unit: str = 's', t0: str | datetime64 | None = None) -> DataArray

Rescale a datetime64 time axis to a float offset.

t' = (t - t_0) / freq_dt, where freq_dt is expressed in freq_unit. Stores t0, freq, and units as attrs so that :func:time_unrescale can round-trip back to datetime64.

Parameters:

Name Type Description Default
time DataArray

Time coordinate / variable to rescale.

required
freq_dt float

Size of the time step in freq_unit units.

1.0
freq_unit str

Pandas timedelta unit ("s", "m", "h", "D", ...).

's'
t0 str | datetime64 | None

Reference time. Defaults to time.min().

None

Returns:

Type Description
DataArray

DataArray of float32 offsets, with units / freq /

DataArray

t0 attrs attached for :func:time_unrescale.

Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/coord_time.py
def time_rescale(
    time: xr.DataArray,
    *,
    freq_dt: float = 1.0,
    freq_unit: str = "s",
    t0: str | np.datetime64 | None = None,
) -> xr.DataArray:
    """Rescale a ``datetime64`` time axis to a float offset.

    ``t' = (t - t_0) / freq_dt``, where ``freq_dt`` is expressed in
    ``freq_unit``. Stores ``t0``, ``freq``, and ``units`` as attrs so
    that :func:`time_unrescale` can round-trip back to ``datetime64``.

    Args:
        time: Time coordinate / variable to rescale.
        freq_dt: Size of the time step in ``freq_unit`` units.
        freq_unit: Pandas timedelta unit (``"s"``, ``"m"``, ``"h"``,
            ``"D"``, ...).
        t0: Reference time. Defaults to ``time.min()``.

    Returns:
        DataArray of ``float32`` offsets, with ``units`` / ``freq`` /
        ``t0`` attrs attached for :func:`time_unrescale`.
    """
    delta = pd.Timedelta(freq_dt, unit=freq_unit)
    delta_ns = np.int64(delta.asm8.astype("timedelta64[ns]").astype(np.int64))

    if t0 is None:
        t0_val = np.datetime64(time.min().values, "ns")
    else:
        t0_val = np.datetime64(t0, "ns")

    td_ns = (
        (time.values.astype("datetime64[ns]") - t0_val)
        .astype("timedelta64[ns]")
        .astype(np.int64)
    )
    rescaled = (td_ns.astype(np.float64) / float(delta_ns)).astype(np.float32)
    out = xr.DataArray(
        rescaled,
        dims=time.dims,
        coords={cname: c for cname, c in time.coords.items() if cname != time.name},
        name=time.name,
        attrs={
            **dict(time.attrs),
            "units": freq_unit,
            "freq": float(freq_dt),
            "t0": str(t0_val),
        },
    )
    return out

time_unrescale

time_unrescale(time: DataArray) -> DataArray

Inverse of :func:time_rescale using its stored attrs.

Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/coord_time.py
def time_unrescale(time: xr.DataArray) -> xr.DataArray:
    """Inverse of :func:`time_rescale` using its stored attrs."""
    attrs = time.attrs
    if "t0" not in attrs or "freq" not in attrs or "units" not in attrs:
        raise ValueError(
            "time coord is missing t0/freq/units attrs — rescale with "
            "time_rescale first."
        )
    delta = pd.Timedelta(attrs["freq"], unit=attrs["units"])
    t0 = np.datetime64(attrs["t0"], "ns")
    delta_ns = np.int64(delta.asm8.astype("timedelta64[ns]").astype(np.int64))
    offsets = (time.values.astype(np.float64) * delta_ns).astype("timedelta64[ns]")
    restored = t0 + offsets
    return xr.DataArray(
        restored,
        dims=time.dims,
        coords={cname: c for cname, c in time.coords.items() if cname != time.name},
        name=time.name,
        attrs={},
    )

Coordinate-range conversions

Convert longitude between \([-180, 180)\) and \([0, 360)\), and latitude between the \([-90, 90]\) and \([0, 180]\) conventions.

lon_180_to_360

lon_180_to_360(coord: Float[ndarray, '*shape']) -> Float[ndarray, '*shape']

Wrap longitudes from [-180, 180) into [0, 360).

Parameters:

Name Type Description Default
coord Float[ndarray, '*shape']

Array of longitude values in the [-180, 180) convention.

required

Returns:

Type Description
Float[ndarray, '*shape']

Array of longitude values in the [0, 360) convention, with the

Float[ndarray, '*shape']

same shape as the input.

Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/coord_space.py
def lon_180_to_360(coord: Float[np.ndarray, "*shape"]) -> Float[np.ndarray, "*shape"]:
    """Wrap longitudes from ``[-180, 180)`` into ``[0, 360)``.

    Args:
        coord: Array of longitude values in the ``[-180, 180)`` convention.

    Returns:
        Array of longitude values in the ``[0, 360)`` convention, with the
        same shape as the input.
    """
    return np.asarray(coord) % 360.0

lon_360_to_180

lon_360_to_180(coord: Float[ndarray, '*shape']) -> Float[ndarray, '*shape']

Wrap longitudes from [0, 360) into [-180, 180).

Parameters:

Name Type Description Default
coord Float[ndarray, '*shape']

Array of longitude values in the [0, 360) convention.

required

Returns:

Type Description
Float[ndarray, '*shape']

Array of longitude values in the [-180, 180) convention, with

Float[ndarray, '*shape']

the same shape as the input.

Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/coord_space.py
def lon_360_to_180(coord: Float[np.ndarray, "*shape"]) -> Float[np.ndarray, "*shape"]:
    """Wrap longitudes from ``[0, 360)`` into ``[-180, 180)``.

    Args:
        coord: Array of longitude values in the ``[0, 360)`` convention.

    Returns:
        Array of longitude values in the ``[-180, 180)`` convention, with
        the same shape as the input.
    """
    return (np.asarray(coord) + 180.0) % 360.0 - 180.0

lat_90_to_180

lat_90_to_180(coord: Float[ndarray, '*shape']) -> Float[ndarray, '*shape']

Wrap latitudes from [-90, 90) into [0, 180).

Parameters:

Name Type Description Default
coord Float[ndarray, '*shape']

Array of latitude values in the [-90, 90) convention.

required

Returns:

Type Description
Float[ndarray, '*shape']

Array of latitude values in the [0, 180) convention.

Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/coord_space.py
def lat_90_to_180(coord: Float[np.ndarray, "*shape"]) -> Float[np.ndarray, "*shape"]:
    """Wrap latitudes from ``[-90, 90)`` into ``[0, 180)``.

    Args:
        coord: Array of latitude values in the ``[-90, 90)`` convention.

    Returns:
        Array of latitude values in the ``[0, 180)`` convention.
    """
    return np.asarray(coord) % 180.0

lat_180_to_90

lat_180_to_90(coord: Float[ndarray, '*shape']) -> Float[ndarray, '*shape']

Wrap latitudes from [0, 180) into [-90, 90).

Useful when a dataset stores latitude as a 0-based index rather than the standard geographic convention.

Parameters:

Name Type Description Default
coord Float[ndarray, '*shape']

Array of latitude values in the [0, 180) convention.

required

Returns:

Type Description
Float[ndarray, '*shape']

Array of latitude values in the [-90, 90) convention.

Source code in packages/xrtoolz-ops/src/xrops/transforms/_src/encoders/coord_space.py
def lat_180_to_90(coord: Float[np.ndarray, "*shape"]) -> Float[np.ndarray, "*shape"]:
    """Wrap latitudes from ``[0, 180)`` into ``[-90, 90)``.

    Useful when a dataset stores latitude as a 0-based index rather than
    the standard geographic convention.

    Args:
        coord: Array of latitude values in the ``[0, 180)`` convention.

    Returns:
        Array of latitude values in the ``[-90, 90)`` convention.
    """
    return (np.asarray(coord) + 90.0) % 180.0 - 90.0