Tutorials

Batch polynomial calculations

Use poly.batch to apply a function to a shaped collection of polynomials, points, or results. Each item remains a complete value. A batch of three polynomials has shape (3,) even when their coefficient counts differ.

Reuse a scalar function

array_batching.mojo Download
"""Array batching."""
from poly import dense, batch
from polynomials import Budget, PolyError


def add_polynomials(a: dense.Array[0], b: dense.Array[0], mut budget: Budget) raises PolyError -> dense.Array[0]:
    return dense.add(a, b, budget)


def main() raises:
    var p = dense.asarray([1, 2])
    var q = dense.asarray([3])
    var rows = batch.asarray([p.copy(), q.copy()], shape=List[Int]([2, 1]))
    var columns = batch.asarray([p.copy(), q.copy(), p.copy()], shape=List[Int]([1, 3]))
    var grid = batch.vmap(add_polynomials, rows, columns)
    print("Batch dimensions:", grid.shape()[0], grid.shape()[1])
    print("Middle item in row two:", grid[4][0])
    var total = batch.reduce(add_polynomials, rows, dense.asarray(List[Int]()))
    print("Sum coefficients:", total[0], total[1])

Run from the repository root pixi run mojo run -I build docs/examples/array_batching.mojo

Output

Batch dimensions: 2 3
Middle item in row two: 6
Sum coefficients: 4 2

The two input batches have shapes (2, 1) and (1, 3). The binary map returns a (2, 3) grid: each row uses one polynomial from the first batch, and each column uses one from the second. The function itself handles one pair and passes the shared budget into dense.add.

batch.asarray(items) makes a one-dimensional batch from a raw iterable. Passing a prepared batch without shape= shares its values and preserves its shape. Set shape= to interpret a flat iterable in row-major order. The product of the dimensions must equal the number of items. batch.scalar(value) makes a zero-dimensional batch, which broadcasts to any shape. Polynomial coefficients are never interpreted as extra batch axes.

Broadcast compatible shapes

Shapes align from the last axis. Corresponding dimensions must match or one must be 1; missing leading axes act as dimensions of size 1. For example, (4, 1, 2) and (3, 2) produce (4, 3, 2). A dimension of size 1 can also broadcast to an empty dimension of size 0. Incompatible shapes raise DOMAIN.

broadcast_to(values, shape) creates a shared view. It records zero strides for repeated axes instead of copying polynomial coefficients. Indexing and iteration visit logical items in row-major order; shape() returns an immutable view of the dimensions.

Maps, pairwise calculations, and folds

Function Result
vmap(function, values) One output per item, preserving shape
vmap(function, a, b) Binary map over the broadcast shape
outer(function, a, b) Every pair; shape is a.shape + b.shape
reduce(function, values, initial) One row-major left fold, returning a value
reduce(function, values, initial, axis=..., keepdims=...) One fold per group of batch items, returning a batch
accumulate(function, values, initial) Every running fold result, in the original shape

A fold always has an explicit initial value. Reducing an empty batch returns that value; accumulating an empty batch returns an empty batch. No operation reassociates a fold, so numerical rounding follows the stated traversal order.

Reduce selected batch dimensions

Use axis=1 to combine the columns within each row of a two-dimensional batch. With keepdims=True, each reduced dimension stays in the result with size 1, so the result can broadcast against the original batch in a later binary map.

array_batch_reductions.mojo Download
"""Reduce rows of polynomials while keeping their batch dimensions."""
from poly import batch, dense
from polynomials import Budget, PolyError
from std.testing import assert_equal


def add_polynomials(a: dense.Array[0], b: dense.Array[0], mut budget: Budget) raises PolyError -> dense.Array[0]:
    return dense.add(a, b, budget)


def main() raises:
    var grid = batch.asarray([
        dense.asarray([1, 1]), dense.asarray([2]), dense.asarray([3, -1]),
        dense.asarray([4]), dense.asarray([5, 1]), dense.asarray([6]),
    ], shape=List[Int]([2, 3]))
    var zero = dense.asarray(List[Int]())
    var rows = batch.reduce(add_polynomials, grid, zero, axis=1, keepdims=True)
    assert_equal(rows.shape(), List[Int]([2, 1]))
    assert_equal(dense.evaluate(rows[0], 2), 6)
    assert_equal(dense.evaluate(rows[1], 2), 17)
    print("Row sums at x=2:", dense.evaluate(rows[0], 2), dense.evaluate(rows[1], 2))
    var total = batch.reduce(add_polynomials, grid, zero, axis=None)
    assert_equal(total.ndim(), 0)
    assert_equal(dense.evaluate(total.item(), 2), 23)
    print("Total at x=2:", dense.evaluate(total.item(), 2))

Run from the repository root pixi run mojo run -I build docs/examples/array_batch_reductions.mojo

Output

Row sums at x=2: 6 17
Total at x=2: 23

The two row sums are the polynomials \(6\) and \(15+x\). Their batch has shape (2, 1); omitting keepdims=True would give (2,). The final reduction combines all six polynomials into \(21+x\). None of these operations reduces a polynomial's coefficient array or changes its variables.

axis accepts an integer, a tuple or finite iterable of integers, or None:

Argument Meaning
axis=0 Reduce the first batch dimension
axis=-1 Reduce the last batch dimension
axis=(0, 2) Reduce two dimensions together
axis=None Reduce every batch dimension
axis=() Keep every dimension; apply the callback to initial and each item

You can pass Optional[Int] when an axis is chosen at runtime. Duplicate axes, including aliases such as 0 and -2 for a two-dimensional batch, raise DOMAIN. Out-of-range axes also raise DOMAIN. A zero-dimensional batch has no numbered axes; use None or an empty tuple for it.

The call without axis or keepdims keeps its original scalar return type. Supplying either keyword selects a batch result, even for axis=None or keepdims=False. This keeps the return type fixed when the options change at runtime. Use result.item() to extract the only value from a result of any shape containing exactly one item; empty or larger results raise DOMAIN.

Each group starts with its own copy of initial. If a reduced dimension is empty, the group's result is that initial value. For example, reducing axis 1 of shape (2, 0, 3) produces shape (2, 3) containing six initial values. If a dimension that remains in the result is empty, the result is empty too.

Output groups follow the retained axes in row-major order. Within a group, items follow the reduced axes in their original row-major order; writing axis=(2, 0) instead of (0, 2) does not change the fold. This order also holds for broadcast views and callbacks whose result depends on evaluation order.

Keep a running result along an axis

batch.accumulate(function, values, initial, axis=...) returns every running fold in the original shape. Each line along the selected axis starts with its own initial. Negative axes and Optional[Int] work as they do for reductions. A scan accepts one axis; tuples and axis lists are not scan arguments.

array_workflow_accumulate.mojo Download
"""Accumulate each row while preserving the batch shape."""
from poly import batch
from polynomials import Budget, PolyError
from std.testing import assert_equal

def add(a: Int, b: Int, mut budget: Budget) raises PolyError -> Int:
    budget.charge(1)
    return a + b

def main() raises:
    var values = batch.asarray([1, 2, 3, 4, 5, 6], shape=List[Int]([2, 3]))
    var running = batch.accumulate(add, values, 0, axis=-1)
    assert_equal(List(running), List[Int]([1, 3, 6, 4, 9, 15]))
    assert_equal(running.shape(), values.shape())
    print("Row totals:", running[2], running[5])

Run from the repository root pixi run mojo run -I build docs/examples/array_workflow_accumulate.mojo

Output

Row totals: 6 15

The first row contains 1, 3, 6; the second starts again from zero and contains 4, 9, 15. axis=0 would accumulate down the columns. Callbacks visit items in row-major order, and broadcast views use their logical values. Empty batches stay empty. Omitting axis, or passing None, scans the flattened batch while preserving its original shape. A scalar batch accepts None and has no numbered axes. Accumulation preserves every dimension, so it does not take keepdims.

Use an array library for layout

Keep transpose, stack, and concatenate operations in your multidimensional array library. The boundary with poly.batch is an iterable of complete items and an explicit shape. For APN numerical batches, pass the shape when preparing the items, then construct an APN batch from the result when you need its array operations again.

array_batch_interop.mojo Download
"""Use APN's array layout operations around a polynomial batch reduction."""
from apn_mojo import Batch, Integer
from poly import batch
from polynomials import Budget, PolyError, PolyErrorKind
from std.testing import assert_equal


def add_integers(a: Integer, b: Integer, mut budget: Budget) raises PolyError -> Integer:
    budget.charge(1)
    try:
        return a + b
    except error:
        raise PolyError(PolyErrorKind.CAPACITY, String(error))


def main() raises:
    var source = Batch[Integer]([1, 2, 3, 4, 5, 6]).reshape([2, 3])
    var arranged = source.transpose()
    var prepared = batch.asarray(arranged, shape=arranged.shape())
    assert_equal(prepared.shape(), List[Int]([3, 2]))
    var sums = batch.reduce(add_integers, prepared, Integer(0), axis=1)
    var restored = Batch[Integer](sums).reshape(sums.shape())
    assert_equal(restored.shape(), List[Int]([3]))
    assert_equal(restored[0], Integer(5))
    assert_equal(restored[1], Integer(7))
    assert_equal(restored[2], Integer(9))
    print("Column sums:", restored[0], restored[1], restored[2])

Run from the repository root pixi run mojo run -I build docs/examples/array_batch_interop.mojo

Output

Column sums: 5 7 9

APN transposes the original (2, 3) numerical array. The polynomial package then folds each row of the (3, 2) view and returns the sums to an APN vector. Conversion consumes logical values in row-major order; it materializes new container storage and does not preserve view strides. Scalar value buffers may still be shared by their value types. The poly.batch construction and reduction share their usual budgets; APN's construction has its own resource contract.

This bridge applies to scalar types supported by APN batches. A whole polynomial or a result with evidence is one poly.batch item, but is not an APN numerical scalar. Supporting those objects in another array library requires that library's object-storage interface.

Pass work through the callback

Callbacks are named Mojo functions with a final mut budget: Budget argument and raises PolyError. Unary and binary maps can return another value type, including a polynomial, scalar, tuple, or result with evidence. The pinned Mojo toolchain uses thin function values here; captured closures are not accepted. To pass runtime data, include it in batch items or broadcast a batch.scalar as the second argument.

Maps and folds run sequentially and eagerly. They share one work allowance across shape preparation, allocation, and every callback. A callback must charge its own computation, usually by forwarding the budget into a library operation. Output storage is allocated under that allowance; failed calls do not change input arrays. See the batch reference for signatures.