Python values and signatures

The explicit inputs and outputs declarations define the callable’s interface. Input entries must name distinct local bindings. Declare outputs even for a constant query. Omit inputs when no runtime arguments are needed.

Records, tables, and arrays

FlatPPL value

Python input

Python result

Scalar

Python scalar or scalar array

Scalar JAX array

Array

NumPy/JAX array or numeric list with the declared shape

JAX array

Record

Dictionary with exactly the declared fields

Dictionary

Table

Dictionary of column arrays, pandas/Polars DataFrame, or PyArrow table/record batch

Dictionary of JAX arrays

Tuple

Tuple or list of components

Tuple

Multiple outputs

—

Tuple in declared order

Records and tables also accept other mapping objects. Field order in a Python dictionary does not change the tensor layout. Nested records follow the same rules.

import jax
import jax.numpy as jnp
from flatppl import flatppl

score = flatppl(r"""
    p = elementof(cartprod(offset = reals, values = cartpow(reals, 3)))
    inputs = p
    outputs = p.offset + sum(p.values)
""").compile()

point = {
    "values": jnp.array([1.0, 2.0, 3.0], dtype=jnp.float32),
    "offset": jnp.float32(4.0),
}
value, gradient = jax.value_and_grad(score)(point)
print(float(value))
print(gradient["values"].tolist())
10.0
[1.0, 1.0, 1.0]

Real inputs convert to the query’s precision, including NumPy arrays that default to float64. Host integer arrays and lists convert when every value fits the query’s integer dtype. Overflow raises ValueError before conversion. JAX integer arrays and Boolean arrays must match the declared dtype. Complex and ragged arrays are not accepted by this adapter.

Tables use the standard Arrow PyCapsule interface. Any producer implementing __arrow_c_stream__ or tabular __arrow_c_array__ works, including pandas, Polars, and PyArrow. The older __dataframe__ protocol is a fallback. PyArrow consumes the protocol; pandas and Polars are optional.

Columns must have unique names and Boolean, integer, or real values. Names come from the exported Arrow schema and must match the query. Row order is preserved, including sliced and chunked data. Row-index columns identified by Arrow’s pandas metadata are ignored. Null values raise an error; fill or filter missing data explicitly before binding it. Both direct calls and query.set(data=table) accept these tables.

import polars as pl

table = pl.DataFrame({"x": [1.0, 2.0, 3.0], "y": [3.0, 7.0, 5.0]})
residual = flatppl("""
    data = external(cartpow(cartprod(x=reals, y=reals), 3))
    inputs = data
    outputs = sum(data.y .- data.x)
""").compile()
print(float(residual(table)))
print(float(residual(table.to_arrow())))
9.0
9.0

Conversion borrows host buffers when possible. Combining chunks, unpacking Boolean columns, changing precision, or transferring to a GPU can require copies.

Frames work at the host boundary only

A DataFrame or Arrow table is a host object. The compiled function converts it when you call the function directly, outside any JAX transformation. Inside jax.jit, jax.grad or jax.vmap, arguments must be JAX pytrees. Convert the frame once to a dictionary of JAX column arrays and pass that dictionary instead:

columns = {name: jnp.asarray(table[name].to_numpy()) for name in table.columns}
shifted = jax.jit(lambda data, shift: residual(data) + shift)
print(float(shifted(columns, 1.0)))
10.0

The dictionary lives on the device and is reused by every later call, so conversion runs once rather than per call.

Bind sizes before compilation

Array shapes are static. Declare size constants as external and bind them with query.set(**kwargs) before compiling. The same source can serve different sizes.

import numpy as np

query = flatppl(r"""
    n = external(posintegers)
    data = external(cartpow(reals, n))
    inputs = data
    outputs = sum(data) / lengthof(data)
""")
data = np.array([1.0, 2.0, 6.0])
summarize = query.set(**{"n": len(data)}).compile()
print(float(summarize(data)))
print(float(query.set(n=2).compile()([2.0, 4.0])))
3.0
3.0

lengthof(data) uses the declared vector length or table row count. For a matrix, declare cartpow(reals, [n, p]) and bind both sizes. FlatPPL distinguishes a multidimensional array from a vector of vectors. sizeof(data) returns the outer array dimensions. zeros(size), ones(size), and eye(n) use these static sizes.

.set(...) returns a new module. It fixes only declared external bindings, checks their domains, and removes bound values from inputs if present. The original module and earlier compiled functions keep their own snapshots. Bind a different size on the original module and compile a new function.

Bind arrays, records, or tables the same way when their values must be fixed at compile time. Binding copies the supplied values, including device arrays, to host constants. Pass changing data through runtime inputs to reuse compilation. Fixed numeric arrays must be nonempty. Python binding currently accepts finite real constants, integers, and Booleans; complex constants are unsupported.

Runtime indices and categories

values[index] uses one-based FlatPPL indices. get0(values, index) uses zero-based indices. Indices can change between calls without recompiling. Scalar selection also works inside FlatPPL broadcasts over vector cells. Array selectors must remain within the declared extent.

Categorical observations can be runtime integers, vectors, or table columns. Categorical uses categories 1 through n. Categorical0 uses 0 through n-1. Both return negative infinity outside that support.

category_score = flatppl("""
    p = elementof(stdsimplex(3))
    observed = external(cartpow(integers, 3))
    inputs = (p, observed)
    outputs = logdensityof(iid(Categorical(p), 3), observed)
""").compile()

observed = np.array([2, 3, 3])
value, gradient = jax.value_and_grad(lambda p: category_score(p, observed))(
    jnp.array([0.0, 0.25, 0.75])
)
print(round(float(value), 6))
print([round(float(g), 6) for g in gradient])
-1.961658
[0.0, 4.0, 2.666667]

Supply data without reading a file

Promote a named data binding into the query signature:

summarize = flatppl(r"""
    data = load_data("observations.csv", cartpow(cartprod(x = reals, y = reals), 3))
    inputs = data
    outputs = record(total = sum(data.x), residual = data.y .- data.x)
""").compile(autodiff=False)

result = summarize(data={
    "x": jnp.array([1.0, 2.0, 3.0], dtype=jnp.float32),
    "y": jnp.array([3.0, 7.0, 5.0], dtype=jnp.float32),
})
print(float(result["total"]), result["residual"].tolist())
6.0 [2.0, 5.0, 2.0]

The declared input supplies data, so this query does not read observations.csv. The filename records the source’s usual data location. Call the same function with new column values to reuse the compiled computation.

Inspect the interface

Every compiled function has an immutable schema. It records input names, logical structures, tensor positions, shapes, dtypes, and authored output names.

print(tuple(field["name"] for field in score.schema["inputs"]))
print(score.schema["inputs"][0]["value"]["kind"])
('p',)
record

The wrapper uses this schema to pack arguments and reconstruct results. See the API reference for its fields.