Array typing & docstring conventions¶
This note is the standard the codebase is being swept toward. It makes the numpy ↔ xarray boundary explicit and self-documenting.
Two layers¶
xrtoolz keeps a strict two-layer split inside every domain submodule:
| Layer | Speaks | Signature style | Lives in |
|---|---|---|---|
| Operators / primitives | xr.DataArray / xr.Dataset |
dim: str (named dims), coords & attrs preserved |
operators.py, _src/<topic>.py |
| numpy kernels | np.ndarray |
axis: int (positional axes), pure array math |
_src/_<topic>_kernels.py |
Everything users touch — operators and the pure-function primitives — takes and returns xarray objects. Raw numpy only appears in the private kernels, which the xarray layer wraps (adding coord/attr handling and dim→axis translation).
numpy kernels MUST be jaxtyped¶
Every array parameter and return of a numpy kernel is annotated with
jaxtyping so the dtype and shape
are part of the signature:
from __future__ import annotations
import numpy as np
from jaxtyping import Float
def partial(
x: Float[np.ndarray, "*shape"],
*,
axis: int,
spacing: float = 1.0,
) -> Float[np.ndarray, "*shape"]:
"""Partial derivative ∂x/∂<axis> via 2nd-order central differences.
Args:
x: Real-valued field of arbitrary shape ``(*shape)``.
axis: Axis to differentiate along.
spacing: Uniform sample spacing along ``axis``.
Returns:
The derivative, same shape ``(*shape)`` as ``x``.
"""
...
Rules:
- Annotations only. We do not wrap kernels in
@jaxtyped/runtime checkers — the annotations are documentation and static types, so there is zero runtime cost.from __future__ import annotationskeeps them as strings. - Dtype picks the narrowest correct jaxtyping class:
Float,Int,Bool,Complex, orNum(any numeric) — e.g. an FFT kernel returnsComplex[np.ndarray, ...]. - Shape axes are named to carry meaning and express relationships:
- semantic names when the kernel is dimension-aware:
Float[np.ndarray, "lat lon"]; - a shared name across params/returns expresses "same shape": reuse
"*shape"(or"n","lat lon") on both sides; *namefor a variadic run of axes (leading batch dims, arbitrary rank):Float[np.ndarray, "*batch n"],Float[np.ndarray, "*shape"].- Document the shape in the docstring too (Args/Returns), so it reads without knowing jaxtyping.
Tooling¶
jaxtyping is a core dependency (it only pulls wadler-lindig; no JAX). ruff
ignores F722, F821, and UP037 repo-wide because pyflakes / pyupgrade
read the shape strings ("lat lon", "*shape") as forward-ref annotations
and would otherwise flag the named axes as undefined or strip their required
quotes — this is jaxtyping's recommended config. ty understands the
annotations with no extra configuration and remains the undefined-name safety
net.
Docstrings¶
Google-style, on every public operator, primitive, and kernel. Operators
follow this template (see geo.Reduce / interpolate.Coarsen as exemplars):
class Thing(Operator):
"""One-sentence purpose.
Optional 1–3 sentence extended description.
Args:
param: Type + meaning; say if required/optional and the default.
Returns:
The xarray result — name the produced variables / dims when useful.
"""
Kernels additionally state array shapes in Args/Returns (mirroring the jaxtyping axes).
Reference implementation¶
xrtoolz.calc is the worked example of this convention end-to-end:
_src/operators.py and the geometry modules (_src/cartesian.py,
_src/rectilinear.py, _src/spherical.py) are the xarray layer with full
Google-style docstrings; the stencil arithmetic itself is delegated to
finitediffx rather than an in-repo kernel module, and the small
module-private numpy helpers (e.g. _difference, _broadcast_along) carry
jaxtyped shape annotations. New/updated modules should match it.