Skip to content

JAX and observation capacity

A sequential experiment adds observations over time. JAX specializes compiled functions to array shapes, so growing an array can require another compilation at every step. gpex observation blocks can reserve space in advance, keeping their shapes fixed as observations arrive.

Observed locations and reserved locations

Suppose an experiment starts with three observations and will collect at most twenty. Reserve twenty locations when constructing the observation block:

import jax.numpy as jnp

from gpex import ObservationBlock

observations = ObservationBlock.from_arrays(
    points=jnp.array([[-1.0], [0.0], [1.0]]),
    y=jnp.array([-0.8, 0.0, 0.8]),
    variance=0.01,
    name="measurements",
    max_size=20,
)

This block has observations.count == 3 active locations and observations.capacity == 20 reserved locations. Only the three observations inform the posterior. Appending two observations increases the count to five and leaves the capacity at twenty. An append beyond capacity raises an error.

max_size counts locations. Each location can contribute several scalar outputs, as specified by the operator. A gradient block in two dimensions with capacity twenty reserves forty scalar outputs. Queries use their supplied locations without reserving additional space.

The default capacity is max(1024, number_of_supplied_locations). For a fixed dataset, pass max_size=points.shape[0] to use compact storage. For an experiment, choose the capacity from its planned number of measurements. Dense conditioning uses quadratic memory and cubic factorization work in the total reserved scalar capacity, including unused space. Each observation block contributes its own reserve.

Reusing compiled code

A compiled conditioning function can accept blocks whose active counts change while their reserved shapes stay fixed. Locations, readings, and noise values may also change. Keep the compiled function and pass the current data to it. The sequential observations tutorial shows how to append readings to an existing posterior.

Block names, operator structure, capacities, array shapes, and dtypes determine which compiled program can be reused. Changing those can trigger compilation. Prediction also specializes to query structure and shapes. A different append batch size can compile another update function even when it fits within the same capacity.

When model parameters change, pass the model as an argument to the compiled function. Compiling gp.condition directly captures that particular immutable GP. Constructing and compiling a new closure on every iteration loses the opportunity to reuse the existing compiled function.

Updating a dataset and updating a posterior

An observation block stores the measurements. A conditioned posterior stores the result of inference. Their update methods have different purposes:

Operation Result
observations.append(locations, readings) A new observation block containing the additional data
observations.replace(points, readings) A new block with replacement data and the same capacity
gp.condition(dataset) A posterior calculated from the dataset and current model parameters
posterior.append(locations, readings, block="measurements") A posterior updated using its existing factorization

All four return new objects. Keep the dataset if you will later fit hyperparameters or replace observations.

Posterior append assumes the kernel, mean, operator parameters, noise, and numerical settings stay fixed. Under those conditions, it gives the same posterior as conditioning on all observations, up to floating-point error. A single scalar append costs quadratic work in reserved scalar capacity. Reusing a compiled condition call still performs the full factorization.

After fitting hyperparameters, replacing readings, or introducing another named block, condition on the complete dataset again. Rebuild any PreparedQuery when its conditioning state or target changes.

Blocks for future observations

An experiment can declare a block before any of its readings are available. Use empty arrays with their intended shapes, choose a capacity, and provide a padding_point valid for the kernel and operator:

future = ObservationBlock.from_arrays(
    points=jnp.empty((0, 1)),
    y=jnp.empty((0,)),
    variance=0.01,
    name="future_measurements",
    max_size=10,
    padding_point=jnp.array([0.0]),
)

The block contributes no observations until it receives readings. Its name, operator, and noise variance are already available to the compiled update. See the block reference for constructor arguments and shapes.