man advect/tutorials/control-flow
CONTROL-FLOW(1)User CommandsCONTROL-FLOW(1)
────────────────────────────────────────────────────────────────────────────────────────

Dynamic Control Flow and Mutation

Dynamic transforms execute the Python function for every call and differentiate the path its inputs take. Conditions, loops, and helper functions remain ordinary Python.

Follow data-dependent branches

import numpy as np

import advect as ad


def piecewise_loss(x):
    if np.sum(x) > 0:
        return np.sum(np.sin(x))
    return np.sum(x * x)


gradient = ad.grad(piecewise_loss)
positive = np.array([0.2, 0.4])
negative = np.array([-0.2, -0.4])

print("positive branch:", gradient(positive))
print("negative branch:", gradient(negative))

The first call differentiates sin; the second differentiates the square. These are pathwise derivatives: Advect does not differentiate the discrete decision itself, so the derivative may jump where the branch changes.

Let loops run for the current input

Iteration counts can also depend on traced values. has_aux=True is handy when the function should report what happened without differentiating the report:

def settle_loss(x):
    state = x
    steps = 0
    while np.max(np.abs(state)) > 0.25:
        state = 0.5 * state
        steps += 1
    return np.sum(state * state), steps


gradient, steps = ad.grad(settle_loss, has_aux=True)(
    np.array([2.0, -1.0])
)
print(f"{steps} iterations; gradient:", gradient)

The loop is unrolled into this invocation's trace. A later call may run a different number of iterations and gets a fresh trace. Helper functions behave the same way: Advect records their supported numerical operations, not the Python call boundary.

Update an owned local array

Supported mutation syntax becomes immutable updates on the trace. Inputs are not writable, so copy first and update the owned local value:

def smooth(field):
    updated = field.copy()
    laplacian = field[2:] - 2 * field[1:-1] + field[:-2]
    updated[1:-1] += 0.1 * laplacian
    return updated


def stencil_loss(field):
    updated = smooth(field)
    return np.sum(updated * updated)


field = np.sin(np.linspace(0.0, 2 * np.pi, 128))
updated = smooth(field)
gradient = ad.grad(stencil_loss)(field)
print("largest local update:", np.max(np.abs(updated - field)))
print("edge and center gradients:", gradient[[0, len(gradient) // 2, -1]])

Basic indexed updates and direct named basic-slice views are supported. Mutating an input, updating through advanced indexing, or mutating through an ambiguous transformed view raises at the offending operation with a suggested rewrite. Troubleshooting collects the common fixes.

Dynamic tracing is the right model when the executed path is part of the computation. If iterations only search for a converged state, use implicit differentiation instead of recording the solver's steps.

$

[1:docs] [2:playground] $ man advect/tutorials/control-flow