Operators
See Linear operators for formulas and
interpretations, and Implementing a custom operator
for a complete extension example.
Custom operators
A LinearOperator implements output_shape(input_dimension) and
apply(function, point). The point has shape (d,). For a scalar function,
apply must return the declared output shape. It must also preserve any
output axes already present on the supplied function, because covariance
construction applies operators to both kernel arguments.
The operation must be linear in function. Its locations, weights, and other
parameters must be independent of the function's values. Use JAX-compatible
operations for jit, vmap, and differentiation. Store differentiable numerical
parameters as array fields. Structural settings such as quadrature order can
be static Equinox fields.
The default derivative_order is zero. Override it when the operator takes
derivatives of the function. gpex checks it against the kernel's supported
GP derivative order. A finite difference made from function evaluations
has order zero under this contract.
Quadrature interfaces
QuadratureOperator.quadrature(point) returns absolute evaluation points with
shape (q, d) and weights with shape (q,). Its inherited apply evaluates
and weights the function while preserving existing output axes. The default
scalar output_shape may be overridden for input-dimension restrictions.
Weights may be negative and need not sum to one. Include normalization and
change-of-variable factors in the rule. Points and weights may depend on the
query location and operator parameters. Keep the number of nodes fixed under
tracing.
TensorQuadratureOperator.coordinate_quadrature(point) returns one
(nodes, weights) pair per coordinate in input order. Each array is
one-dimensional. Node counts can differ between coordinates and must remain
fixed under tracing. The full weight is the product of the coordinate weights.
Any override of quadrature or apply must represent that same product rule.
This interface enables efficient covariance evaluation with SeparableKernel.
Built-in constructors and methods
Identity(), Gradient(), Hessian(), and Laplacian() are stateless.
PartialDerivative(axis) selects one coordinate axis, which is static under
JAX transformations. Numerical directions, displacements, radii, and smoothing
scales are dynamic array parameters.
gpex.operators
Linear operators for observations and posterior queries.
LinearOperator
Bases: Module
Base class for linear measurements and query operators.
Subclasses declare the result produced at one input location with
output_shape and
implement the linear action with apply.
apply must remain linear in function and use JAX-compatible
operations.
derivative_order
property
Required derivative order of the latent function.
The default, zero, is for operators using only function values. Custom
differential operators must override this so covariance compilation can
check the kernel's smoothness before applying automatic differentiation.
output_shape
abstractmethod
output_shape(input_dimension: int) -> tuple[int, ...]
Return the operator output shape at one input location.
Source code in src/gpex/operators/base.py
| @abstractmethod
def output_shape(self, input_dimension: int) -> tuple[int, ...]:
"""Return the operator output shape at one input location."""
|
apply
abstractmethod
apply(
function: PointFunction, point: Point
) -> FunctionalValue
Apply the operator to function at point.
Source code in src/gpex/operators/base.py
| @abstractmethod
def apply(self, function: PointFunction, point: Point) -> FunctionalValue:
"""Apply the operator to ``function`` at ``point``."""
|
QuadratureOperator
Bases: LinearOperator
A scalar linear measurement defined by weighted function evaluations.
Subclasses implement quadrature(point) to return evaluation points of
shape (q, d) and weights of shape (q,). Both may depend on the query
location and operator parameters, but not on the function being evaluated.
Weights may be signed and are not normalized by this class.
The rule must use JAX-compatible operations and keep q fixed under
tracing. apply supports JIT, batching, and differentiation through the
points and weights. It also preserves trailing function-output axes when
operators are nested during covariance compilation.
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Return scalar output; subclasses may validate the input dimension.
Source code in src/gpex/operators/quadrature.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
"""Return scalar output; subclasses may validate the input dimension."""
return ()
|
quadrature
abstractmethod
quadrature(point: Point) -> QuadratureRule
Return absolute evaluation points (q, d) and weights (q,).
Include the rule's normalization and any change-of-variable factors
in the weights. The point count and input dimension are static.
Source code in src/gpex/operators/quadrature.py
| @abstractmethod
def quadrature(self, point: Point) -> QuadratureRule:
"""Return absolute evaluation points ``(q, d)`` and weights ``(q,)``.
Include the rule's normalization and any change-of-variable factors
in the weights. The point count and input dimension are static.
"""
|
apply
apply(
function: PointFunction, point: Point
) -> FunctionalValue
Source code in src/gpex/operators/quadrature.py
| def apply(self, function: PointFunction, point: Point) -> FunctionalValue:
self.output_shape(point.shape[-1])
points, weights = self.quadrature(point)
values = jax.vmap(function)(points)
return jnp.tensordot(
weights, values, axes=(0, 0), precision=jax.lax.Precision.HIGHEST
)
|
TensorQuadratureOperator
Bases: QuadratureOperator
A scalar quadrature rule that factors over input coordinates.
Implement coordinate_quadrature(point) to return one pair of node and
weight vectors per coordinate. Nodes are absolute coordinate values, and
full weights are products of coordinate weights. Both may depend on the
entire query point and operator parameters, independently of the function.
Node counts are static under tracing and may differ between coordinates.
The inherited apply evaluates the Cartesian-product rule. Covariance
evaluation with a SeparableKernel contracts coordinate rules directly,
preserving the same approximation without constructing the full grid.
Overrides of apply or quadrature must represent that same rule.
coordinate_quadrature
abstractmethod
coordinate_quadrature(point: Point) -> TensorQuadratureRule
Return ((nodes_0, weights_0), ...) in input-coordinate order.
Each pair contains equally sized one-dimensional arrays. Include
normalization and change-of-variable factors in the weights; signed
and unnormalized rules are supported.
Source code in src/gpex/operators/quadrature.py
| @abstractmethod
def coordinate_quadrature(self, point: Point) -> TensorQuadratureRule:
"""Return ``((nodes_0, weights_0), ...)`` in input-coordinate order.
Each pair contains equally sized one-dimensional arrays. Include
normalization and change-of-variable factors in the weights; signed
and unnormalized rules are supported.
"""
|
quadrature
quadrature(point: Point) -> QuadratureRule
Source code in src/gpex/operators/quadrature.py
| def quadrature(self, point: Point) -> QuadratureRule:
rules = self.coordinate_quadrature(point)
points = jnp.stack(
jnp.meshgrid(*(nodes for nodes, _ in rules), indexing="ij"), axis=-1
).reshape(-1, point.shape[-1])
weights = jnp.prod(
jnp.stack(
jnp.meshgrid(*(weights for _, weights in rules), indexing="ij"),
axis=-1,
),
axis=-1,
).reshape(-1)
return points, weights
|
Identity
Bases: LinearOperator
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/local.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
return ()
|
apply
apply(
function: PointFunction, point: Point
) -> FunctionalValue
Source code in src/gpex/operators/local.py
| def apply(self, function: PointFunction, point: Point) -> FunctionalValue:
return function(point)
|
Increment
Increment(displacement: Displacement)
Bases: LinearOperator
Return the unnormalized increment f(x + displacement) - f(x).
Source code in src/gpex/operators/differences.py
| def __init__(self, displacement: Displacement) -> None:
displacement = jnp.asarray(displacement)
if displacement.ndim != 1 or displacement.size == 0:
raise ValueError("displacement must be a non-empty one-dimensional array")
self.displacement = displacement
|
displacement
instance-attribute
displacement: Displacement = displacement
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/differences.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
if self.displacement.shape != (input_dimension,):
raise ValueError(
"displacement shape must match the locations' input dimension"
)
return ()
|
apply
apply(
function: PointFunction, point: Point
) -> FunctionalValue
Source code in src/gpex/operators/differences.py
| def apply(self, function: PointFunction, point: Point) -> FunctionalValue:
self.output_shape(point.shape[-1])
return function(point + self.displacement) - function(point)
|
PartialDerivative
Bases: LinearOperator
axis
class-attribute
instance-attribute
axis: int = eqx.field(static=True)
derivative_order
property
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/local.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
if not 0 <= self.axis < input_dimension:
raise ValueError(
f"axis {self.axis} is invalid for dimension {input_dimension}"
)
return ()
|
apply
apply(
function: PointFunction, point: Point
) -> FunctionalValue
Source code in src/gpex/operators/local.py
| def apply(self, function: PointFunction, point: Point) -> FunctionalValue:
self.output_shape(point.shape[-1])
tangent = jnp.zeros_like(point).at[self.axis].set(1)
return jax.jvp(function, (point,), (tangent,))[1]
|
DirectionalDerivative
DirectionalDerivative(direction: Direction)
Bases: LinearOperator
Source code in src/gpex/operators/local.py
| def __init__(self, direction: Direction) -> None:
direction = jnp.asarray(direction)
if direction.ndim != 1:
raise ValueError("direction must be a one-dimensional array")
self.direction = direction
|
direction
instance-attribute
direction: Direction = direction
derivative_order
property
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/local.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
if self.direction.shape != (input_dimension,):
raise ValueError(
"direction shape must match the locations' input dimension"
)
return ()
|
apply
apply(
function: PointFunction, point: Point
) -> FunctionalValue
Source code in src/gpex/operators/local.py
| def apply(self, function: PointFunction, point: Point) -> FunctionalValue:
return jax.jvp(function, (point,), (self.direction,))[1]
|
Gradient
Bases: LinearOperator
derivative_order
property
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/local.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
return (input_dimension,)
|
apply
apply(
function: PointFunction, point: Point
) -> FunctionalValue
Source code in src/gpex/operators/local.py
| def apply(self, function: PointFunction, point: Point) -> FunctionalValue:
return jax.jacfwd(function)(point)
|
Hessian
Bases: LinearOperator
derivative_order
property
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/local.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
return (input_dimension, input_dimension)
|
apply
apply(
function: PointFunction, point: Point
) -> FunctionalValue
Source code in src/gpex/operators/local.py
| def apply(self, function: PointFunction, point: Point) -> FunctionalValue:
return jax.jacfwd(jax.jacfwd(function))(point)
|
Laplacian
Bases: LinearOperator
Return the trace of the input Hessian at each location.
Sum second coordinate derivatives without constructing the full Hessian.
derivative_order
property
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/local.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
return ()
|
apply
apply(
function: PointFunction, point: Point
) -> FunctionalValue
Source code in src/gpex/operators/local.py
| def apply(self, function: PointFunction, point: Point) -> FunctionalValue:
def second_derivative(axis: int) -> FunctionalValue:
tangent = jax.nn.one_hot(axis, point.shape[0])
def first_derivative(variable: Point) -> FunctionalValue:
return jax.jvp(function, (variable,), (tangent,))[1]
return jax.jvp(first_derivative, (point,), (tangent,))[1]
def accumulate(axis: int, total: FunctionalValue) -> FunctionalValue:
return total + second_derivative(axis)
# Preserve array-valued intermediate functions from another operator.
initial = jnp.zeros_like(function(point))
return jax.lax.fori_loop(0, point.shape[0], accumulate, initial)
|
BoxAverage
BoxAverage(
radius: BoxRadius | float, *, quadrature_order: int = 8
)
Bases: TensorQuadratureOperator
Average a scalar function over a box centered at each input location.
A scalar radius defines a square or hypercube. A radius vector defines one
half-width per input dimension. The integral is approximated by a symmetric
tensor-product midpoint rule with quadrature_order ** dimension points.
Construction and evaluation support JIT, batching, and differentiation of
finite positive radius values. Quadrature order remains static.
Source code in src/gpex/operators/quadrature.py
| def __init__(self, radius: BoxRadius | float, *, quadrature_order: int = 8) -> None:
radius = jnp.asarray(radius)
if radius.ndim > 1 or radius.size == 0:
raise ValueError("box-average radius must be scalar or one-dimensional")
if quadrature_order < 1:
raise ValueError("quadrature order must be positive")
self.radius = error_if(
radius,
jnp.any(~jnp.isfinite(radius) | (radius <= 0)),
"box-average radius must be finite and strictly positive",
)
self.quadrature_order = quadrature_order
|
radius
instance-attribute
radius: BoxRadius = error_if(
radius,
jnp.any(~jnp.isfinite(radius) | (radius <= 0)),
"box-average radius must be finite and strictly positive",
)
quadrature_order
class-attribute
instance-attribute
quadrature_order: int = quadrature_order
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/quadrature.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
if self.radius.ndim == 1 and self.radius.shape != (input_dimension,):
raise ValueError("radius shape must match the locations' input dimension")
return ()
|
coordinate_quadrature
coordinate_quadrature(point: Point) -> TensorQuadratureRule
Source code in src/gpex/operators/quadrature.py
| def coordinate_quadrature(self, point: Point) -> TensorQuadratureRule:
input_dimension = point.shape[-1]
radius = jnp.broadcast_to(self.radius, (input_dimension,))
normalized_nodes = (
2.0 * (jnp.arange(self.quadrature_order) + 0.5) / self.quadrature_order
- 1.0
)
weights = jnp.full((self.quadrature_order,), 1.0 / self.quadrature_order)
return tuple(
(point[axis] + normalized_nodes * radius[axis], weights)
for axis in range(input_dimension)
)
|
GaussianSmoothing
GaussianSmoothing(
scale: GaussianScale | float,
*,
quadrature_order: int = 8,
)
Bases: TensorQuadratureOperator
Average a scalar function under a centered Gaussian density.
A scalar scale defines an isotropic Gaussian. A scale vector defines one
standard deviation per input dimension. The expectation is approximated
with tensor-product Gauss--Hermite quadrature using
quadrature_order ** dimension points.
Construction and evaluation support JIT, batching, and differentiation of
finite positive scale values. Quadrature order remains static.
Source code in src/gpex/operators/quadrature.py
| def __init__(
self, scale: GaussianScale | float, *, quadrature_order: int = 8
) -> None:
scale = jnp.asarray(scale)
if scale.ndim > 1 or scale.size == 0:
raise ValueError(
"Gaussian-smoothing scale must be scalar or one-dimensional"
)
nodes, weights = np.polynomial.hermite.hermgauss(quadrature_order)
self.scale = error_if(
scale,
jnp.any(~jnp.isfinite(scale) | (scale <= 0)),
"Gaussian-smoothing scale must be finite and strictly positive",
)
self.quadrature_order = quadrature_order
self._nodes = tuple(float(node * np.sqrt(2.0)) for node in nodes)
self._weights = tuple(float(weight / np.sqrt(np.pi)) for weight in weights)
|
scale
instance-attribute
scale: GaussianScale = error_if(
scale,
jnp.any(~jnp.isfinite(scale) | (scale <= 0)),
"Gaussian-smoothing scale must be finite and strictly positive",
)
quadrature_order
class-attribute
instance-attribute
quadrature_order: int = quadrature_order
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/quadrature.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
if self.scale.ndim == 1 and self.scale.shape != (input_dimension,):
raise ValueError("scale shape must match the locations' input dimension")
return ()
|
coordinate_quadrature
coordinate_quadrature(point: Point) -> TensorQuadratureRule
Source code in src/gpex/operators/quadrature.py
| def coordinate_quadrature(self, point: Point) -> TensorQuadratureRule:
input_dimension = point.shape[-1]
scale = jnp.broadcast_to(self.scale, (input_dimension,))
nodes = jnp.asarray(self._nodes)
weights = jnp.asarray(self._weights)
return tuple(
(point[axis] + nodes * scale[axis], weights)
for axis in range(input_dimension)
)
|
CumulativeIntegral1D
CumulativeIntegral1D(
base: IntegralBase | float, *, quadrature_order: int = 8
)
Bases: QuadratureOperator
Integrate a one-dimensional function from base to each location.
Gauss--Legendre quadrature is mapped from [-1, 1] to the oriented
interval from base to x. Locations below base therefore produce
the correctly signed integral, and the value at base is exactly zero.
Source code in src/gpex/operators/quadrature.py
| def __init__(
self, base: IntegralBase | float, *, quadrature_order: int = 8
) -> None:
base = jnp.asarray(base)
if base.ndim != 0:
raise ValueError("integration base must be scalar")
nodes, weights = np.polynomial.legendre.leggauss(quadrature_order)
self.base = base
self.quadrature_order = quadrature_order
self._nodes = tuple(float(node) for node in nodes)
self._weights = tuple(float(weight) for weight in weights)
|
base
instance-attribute
base: IntegralBase = base
quadrature_order
class-attribute
instance-attribute
quadrature_order: int = quadrature_order
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/quadrature.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
if input_dimension != 1:
raise ValueError(
"CumulativeIntegral1D only supports one-dimensional inputs"
)
return ()
|
quadrature
quadrature(point: Point) -> QuadratureRule
Source code in src/gpex/operators/quadrature.py
| def quadrature(self, point: Point) -> QuadratureRule:
nodes = jnp.asarray(self._nodes)
weights = jnp.asarray(self._weights)
half_interval = (point[0] - self.base) / 2.0
midpoint = (point[0] + self.base) / 2.0
quadrature_points = midpoint + half_interval * nodes
return quadrature_points[:, None], half_interval * weights
|
HaarWavelet1D
HaarWavelet1D(
scale: WaveletScale | float,
*,
quadrature_order: int = 8,
)
Bases: QuadratureOperator
A translated, fixed-scale Haar-wavelet coefficient in one dimension.
scale is the full width of the wavelet support. The analyzing wavelet
is positive on the left half, negative on the right half, has unit
\(L^2\) norm, and therefore annihilates constants. Each half-integral
is approximated with a symmetric midpoint rule.
Construction and evaluation support JIT, batching, and differentiation of
finite positive scale values. Quadrature order remains static.
Source code in src/gpex/operators/quadrature.py
| def __init__(
self, scale: WaveletScale | float, *, quadrature_order: int = 8
) -> None:
scale = jnp.asarray(scale)
if scale.ndim != 0:
raise ValueError("Haar-wavelet scale must be scalar")
if quadrature_order < 1:
raise ValueError("quadrature order must be positive")
self.scale = error_if(
scale,
~jnp.isfinite(scale) | (scale <= 0),
"Haar-wavelet scale must be finite and strictly positive",
)
self.quadrature_order = quadrature_order
|
scale
instance-attribute
scale: WaveletScale = error_if(
scale,
~jnp.isfinite(scale) | (scale <= 0),
"Haar-wavelet scale must be finite and strictly positive",
)
quadrature_order
class-attribute
instance-attribute
quadrature_order: int = quadrature_order
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/quadrature.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
if input_dimension != 1:
raise ValueError("HaarWavelet1D currently supports one-dimensional inputs")
return ()
|
quadrature
quadrature(point: Point) -> QuadratureRule
Source code in src/gpex/operators/quadrature.py
| def quadrature(self, point: Point) -> QuadratureRule:
nodes = (jnp.arange(self.quadrature_order) + 0.5) / self.quadrature_order
half_width = self.scale / 2.0
left_offsets = -half_width + nodes * half_width
right_offsets = nodes * half_width
offsets = jnp.concatenate((left_offsets, right_offsets))[:, None]
signs = jnp.concatenate(
(jnp.ones(self.quadrature_order), -jnp.ones(self.quadrature_order))
)
weights = signs * jnp.sqrt(self.scale) / (2 * self.quadrature_order)
return point + offsets, weights
|
LinearCombination
Bases: LinearOperator
A weighted sum of operators with a common output shape.
Source code in src/gpex/operators/base.py
| def __init__(
self,
operators: tuple[LinearOperator, ...] | list[LinearOperator],
coefficients: OperatorCoefficients,
) -> None:
operators = tuple(operators)
coefficients = jnp.asarray(coefficients)
if not operators:
raise ValueError("at least one operator is required")
if coefficients.shape != (len(operators),):
raise ValueError("coefficients must contain one value per operator")
self.operators = operators
self.coefficients = coefficients
|
operators
instance-attribute
coefficients
instance-attribute
coefficients: OperatorCoefficients = coefficients
derivative_order
property
output_shape
output_shape(input_dimension: int) -> tuple[int, ...]
Source code in src/gpex/operators/base.py
| def output_shape(self, input_dimension: int) -> tuple[int, ...]:
shapes = [operator.output_shape(input_dimension) for operator in self.operators]
if any(shape != shapes[0] for shape in shapes[1:]):
raise ValueError("combined operators must have the same output shape")
return shapes[0]
|
apply
apply(
function: PointFunction, point: Point
) -> FunctionalValue
Source code in src/gpex/operators/base.py
| def apply(self, function: PointFunction, point: Point) -> FunctionalValue:
values = jnp.stack(
[operator.apply(function, point) for operator in self.operators]
)
coefficient_shape = (len(self.operators),) + (1,) * (values.ndim - 1)
return jnp.sum(values * self.coefficients.reshape(coefficient_shape), axis=0)
|