Skip to content

Named-axis kernel products

Two different axes can index a rectangular parameter table. For example, K[consumer,resource] has one row for each consumer and one column for each resource. State arrays need not have the same length. The named selectors identify which axis is summed, including when the matrix is used in the opposite direction.

Expression Independent array calculation
apply_along(K[consumer,resource:r]*P[resource:r], resource=r, kernel=sum) K @ P
apply_along(g[consumer:c]*K[consumer:c,resource]*Z[consumer:c], consumer=c, kernel=sum) K.T @ (g * Z)

The normalizer infers the parameter's axis order from its named references; validate_spec(spec).parameters reports the contract. Supply K with the reported shape and order. Distinct axis names make the intended row/column roles explicit, rather than relying on equal lengths to conceal a transpose.

The complete executable example includes both products and a per-coordinate choice of linear or quadratic loss:

"""Two distinct named axes, rectangular interactions and conditional losses.

Run ``uv run python examples/named_kernel_products.py``. All inputs are
synthetic. The independent reference uses ordinary matrix products.
"""

from __future__ import annotations

from typing import Any

import numpy as np

from op_system import compile_spec


def model_spec(resources: int = 3, consumers: int = 2) -> dict[str, Any]:
    """Describe interaction between two differently sized groups of variables.

    Returns:
        A specification with resource and consumer axes and a rectangular table.
    """
    return {
        "kind": "expr",
        "axes": [
            {"name": "resource", "coords": [f"r{i}" for i in range(resources)]},
            {"name": "consumer", "coords": [f"c{i}" for i in range(consumers)]},
        ],
        "state": ["P[resource]", "Z[consumer]"],
        "aliases": {
            "exposure[consumer]": (
                "apply_along(K[consumer,resource:r]*P[resource:r], "
                "resource=r, kernel=sum)"
            ),
            "removal[resource]": (
                "apply_along(g[consumer:c]*K[consumer:c,resource]*Z[consumer:c], "
                "consumer=c, kernel=sum)"
            ),
        },
        "equations": {
            "P[resource]": "-P[resource]*removal[resource]",
            "Z[consumer]": (
                "efficiency[consumer]*Z[consumer]*exposure[consumer] - "
                "np.where(linear[consumer] > 0, loss[consumer]*Z[consumer], "
                "loss[consumer]*Z[consumer]*Z[consumer])"
            ),
        },
    }


def inputs(resources: int = 3, consumers: int = 2) -> tuple[np.ndarray, dict[str, Any]]:
    """Return seeded states and a table ordered as consumer by resource."""
    rng = np.random.default_rng(312)
    state = rng.uniform(0.3, 2.0, size=resources + consumers)
    params = {
        "K": rng.uniform(0.0, 1.0, size=(consumers, resources)) / resources,
        "g": rng.uniform(0.2, 1.0, size=consumers),
        "efficiency": rng.uniform(0.2, 0.8, size=consumers),
        "loss": rng.uniform(0.05, 0.3, size=consumers),
        "linear": np.arange(consumers) % 2,
    }
    return state, params


def reference(state: np.ndarray, params: dict[str, Any]) -> np.ndarray:
    """Compute the same equations directly, without the specification compiler.

    Returns:
        Flat resource and consumer derivatives in declared state order.
    """
    consumers, resources = params["K"].shape
    p, z = state[:resources], state[resources : resources + consumers]
    resource_loss = -p * (params["K"].T @ (params["g"] * z))
    consumer_change = params["efficiency"] * z * (params["K"] @ p) - np.where(
        params["linear"] > 0, params["loss"] * z, params["loss"] * z**2
    )
    return np.concatenate([resource_loss, consumer_change])


def main() -> None:
    """Check the compiled equations against the independent rectangular product."""
    state, params = inputs()
    compiled = compile_spec(model_spec())
    result = compiled.eval_fn(0.0, state, **params)
    np.testing.assert_allclose(result, reference(state, params), rtol=1e-13, atol=1e-14)
    print(result)  # ruff: ignore[print] - command-line example output


if __name__ == "__main__":
    main()

Run uv run python examples/named_kernel_products.py. The example compares the compiled derivative with a separate NumPy implementation. Required backend qualification tests rectangular cases in NumPy, JAX and CPU PyTorch; JAX JIT and derivatives and PyTorch kernel gradients have independent checks.

A fixed-state example

A rectangular kernel with two consumers and three resources, and the compiled derivatives of the five state entries.

This synthetic example evaluates one state; it does not integrate a trajectory. Its binary-exact inputs are retained with the generated figure. The two consumer entries use different declared loss choices.

State Value Compiled derivative
P[r0] 1 -0.265625
P[r1] 2 -0.6875
P[r2] 4 -2.25
Z[c0] 0.5 0.0625
Z[c1] 1.5 1.40625

Vector PDF ยท Inputs and outputs

Conditional selection

np.where(condition, a, b) selects elementwise and dispatches through the state's array namespace. Conditions and branches can use shaped parameters, states and aliases. A condition such as linear[consumer] > 0 chooses the formula separately for each consumer.

Both branch expressions are evaluated. This is not lazy control flow: an inactive branch can still divide by zero, overflow or produce invalid gradients. Write expressions with valid numerical domains on both branches. Boolean parameters here choose between formulas; they do not create a stochastic event or change the reaction-network interpretation.

Lowering and cost

For a single unweighted, unfiltered reduction over one axis and one free output axis, the compiler recognizes a two-axis matrix multiplied by a one-axis vector or vector product. It emits namespace-native matmul, avoiding the broadcast matrix-vector temporary. Named axes supply the transpose when needed. More general layouts, weighted quadrature, filtered sums and repeated physical-axis forms retain the general reduction path.

Vectorized flat and PyTree evaluators cache generated expression functions at compile time. Each call binds fresh state and parameter values; it does not invoke eval for each alias or equation. State/parameter shape checks and namespace discovery still have per-call costs. Scalar fallback evaluation is unchanged, and small problems can still spend more time in dispatch than in arithmetic. No runtime speed ratio is guaranteed.

An optional warmed NumPy comparison records compilation separately from evaluation and checks the independent equations before timing:

OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 uv run python -m \
  scripts.benchmark_named_kernels --sizes 4 16 128 256 512 1024

Large normalization cases can consume substantial time and memory. Start with the default smaller sweep when exploring a machine. These measurements are outside CI; CI checks numerical behavior and contraction structure without a wall-time threshold.