Skip to content

Building functions

Declare the shape and structure of a function with arg, group, and @function. The derivative wrappers, such as gradient, then create functions from that declaration. See the functions guide for examples and the derivatives guide for choosing a derivative.

Declared trees

Tree

A pytree declaration whose leaves are Expr symbolically and NumPy arrays numerically.

A tree is its structure: only group introduces a tuple, so a one-leaf tree is the bare leaf and never a one-element tuple. See arg for what that means at a call site.

has_holes property

has_holes: bool

Whether any leaf needs binding.

shapes property

shapes: tuple[tuple[int, ...], ...]

The leaf shapes in C-signature order.

types property

types: tuple[TensorType, ...]

The leaf tensor types in C-signature order.

size property

size: int

The number of leaves.

symbols

symbols(*, diff: bool | None = None) -> Symbolic

Create named input expressions with this tree's structure.

relabel

relabel(prefix: str) -> Tree[Symbolic, Numerical]

Return the same structure with prefix added to every leaf name.

with_types

with_types(types: tuple[TensorType, ...]) -> Tree[Symbolic, Numerical]

Return the same structure with resolved leaf types.

resolved

resolved(traced: tuple[TensorType, ...]) -> tuple[TensorType, ...]

Check traced output types against this declaration and resolve inferred shapes.

index

index(name: str) -> int

Return the flat index for name, or raise with the declared choices.

is_symbolic

is_symbolic(value: Symbolic | Numerical) -> TypeGuard[Symbolic]

Whether value has at least one leaf and every leaf is an Expr.

This is the leaf-kind half of Function.__call__'s dispatch. It deliberately ignores structure so that a wrongly-shaped tree is reported by flatten_symbolic against the declared names instead of being rejected here as a kind mismatch.

is_numerical

is_numerical(value: Symbolic | Numerical) -> TypeGuard[Numerical]

Whether no leaf of value is an Expr.

The numerical side is the fallback: array-likes are coerced by flatten_numerical, so a leaf only has to not be symbolic. Values with no leaves land here and are reported as a structure error rather than as a mixed call.

flatten_symbolic

flatten_symbolic(value: Symbolic, what: str, *, allow_scalar: bool = False) -> tuple[Expr, ...]

Validate and flatten a symbolic value.

flatten_numerical

flatten_numerical(value: Numerical, what: str) -> tuple[Array, ...]

Validate and flatten a numerical value.

unflatten

unflatten(values: tuple[Any, ...]) -> Any

Rebuild this tree's structure from flat values.

arg(name, shape=None, /, *, layout=None, pattern=None)

arg
arg
arg
arg

Declare one named tensor, optionally leaving its shape to a call or trace.

A leaf is passed and returned as a bare value. Use group to declare tuple structure. A TensorType sets the dtype and differentiability explicitly, and a stored pattern for a sparse matrix. Dense leaves use row-major buffers by default. layout="column-major" orders each matrix's values by column, keeping leading batch axes in row-major order. A sparse leaf's layout is "csc" (the default), "csr", or the coordinate order of a SparsityPattern. pattern=... leaves the pattern to the call, with a "csc" or "csr" layout.

group

group(a: Tree[SA, NA]) -> Tree[tuple[SA], tuple[NA]]
group(a: Tree[SA, NA], b: Tree[SB, NB]) -> Tree[tuple[SA, SB], tuple[NA, NB]]
group(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC]) -> Tree[tuple[SA, SB, SC], tuple[NA, NB, NC]]
group(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND]) -> Tree[tuple[SA, SB, SC, SD], tuple[NA, NB, NC, ND]]
group(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE]) -> Tree[tuple[SA, SB, SC, SD, SE], tuple[NA, NB, NC, ND, NE]]
group(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE], f: Tree[SF, NF]) -> Tree[tuple[SA, SB, SC, SD, SE, SF], tuple[NA, NB, NC, ND, NE, NF]]
group(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE], f: Tree[SF, NF], g: Tree[SG, NG]) -> Tree[tuple[SA, SB, SC, SD, SE, SF, SG], tuple[NA, NB, NC, ND, NE, NF, NG]]
group(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE], f: Tree[SF, NF], g: Tree[SG, NG], h: Tree[SH, NH]) -> Tree[tuple[SA, SB, SC, SD, SE, SF, SG, SH], tuple[NA, NB, NC, ND, NE, NF, NG, NH]]
group(a: Tree[S, N], /, *parts: Tree[S, N]) -> Tree[tuple[S, ...], tuple[N, ...]]
group(a: Tree[Any, Any], /, *parts: Tree[Any, Any]) -> Tree[tuple[Any, ...], tuple[Any, ...]]
group(*parts: Tree[Any, Any]) -> Tree[Any, Any]

Group one to eight trees side by side. Nest groups for greater widths.

Source code in src/scaly/function/tree.py
def group(*parts: Tree[Any, Any]) -> Tree[Any, Any]:
  """Group one to eight trees side by side. Nest groups for greater widths."""
  return _G(parts)

The decorator

function

function(*, outputs: Tree[SO, NO], name: str | None = None) -> Callable[[Callable[[], SO]], Function[tuple[], tuple[], SO, NO]]
function(a: Tree[SA, NA], /, *, outputs: Tree[SO, NO], name: str | None = None) -> Callable[[Callable[[SA], SO]], Function[tuple[SA], tuple[NA], SO, NO]]
function(a: Tree[SA, NA], b: Tree[SB, NB], /, *, outputs: Tree[SO, NO], name: str | None = None) -> Callable[[Callable[[SA, SB], SO]], Function[tuple[SA, SB], tuple[NA, NB], SO, NO]]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], /, *, outputs: Tree[SO, NO], name: str | None = None) -> Callable[[Callable[[SA, SB, SC], SO]], Function[tuple[SA, SB, SC], tuple[NA, NB, NC], SO, NO]]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], /, *, outputs: Tree[SO, NO], name: str | None = None) -> Callable[[Callable[[SA, SB, SC, SD], SO]], Function[tuple[SA, SB, SC, SD], tuple[NA, NB, NC, ND], SO, NO]]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE], /, *, outputs: Tree[SO, NO], name: str | None = None) -> Callable[[Callable[[SA, SB, SC, SD, SE], SO]], Function[tuple[SA, SB, SC, SD, SE], tuple[NA, NB, NC, ND, NE], SO, NO]]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE], f: Tree[SF, NF], /, *, outputs: Tree[SO, NO], name: str | None = None) -> Callable[[Callable[[SA, SB, SC, SD, SE, SF], SO]], Function[tuple[SA, SB, SC, SD, SE, SF], tuple[NA, NB, NC, ND, NE, NF], SO, NO]]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE], f: Tree[SF, NF], g: Tree[SG, NG], /, *, outputs: Tree[SO, NO], name: str | None = None) -> Callable[[Callable[[SA, SB, SC, SD, SE, SF, SG], SO]], Function[tuple[SA, SB, SC, SD, SE, SF, SG], tuple[NA, NB, NC, ND, NE, NF, NG], SO, NO]]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE], f: Tree[SF, NF], g: Tree[SG, NG], h: Tree[SH, NH], /, *, outputs: Tree[SO, NO], name: str | None = None) -> Callable[[Callable[[SA, SB, SC, SD, SE, SF, SG, SH], SO]], Function[tuple[SA, SB, SC, SD, SE, SF, SG, SH], tuple[NA, NB, NC, ND, NE, NF, NG, NH], SO, NO]]
function(a: Tree[SA, NA], /, *, name: str | None = None) -> _InferredOutputs[tuple[NA], SA]
function(a: Tree[SA, NA], b: Tree[SB, NB], /, *, name: str | None = None) -> _InferredOutputs[tuple[NA, NB], SA, SB]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], /, *, name: str | None = None) -> _InferredOutputs[tuple[NA, NB, NC], SA, SB, SC]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], /, *, name: str | None = None) -> _InferredOutputs[tuple[NA, NB, NC, ND], SA, SB, SC, SD]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE], /, *, name: str | None = None) -> _InferredOutputs[tuple[NA, NB, NC, ND, NE], SA, SB, SC, SD, SE]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE], f: Tree[SF, NF], /, *, name: str | None = None) -> _InferredOutputs[tuple[NA, NB, NC, ND, NE, NF], SA, SB, SC, SD, SE, SF]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE], f: Tree[SF, NF], g: Tree[SG, NG], /, *, name: str | None = None) -> _InferredOutputs[tuple[NA, NB, NC, ND, NE, NF, NG], SA, SB, SC, SD, SE, SF, SG]
function(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC], d: Tree[SD, ND], e: Tree[SE, NE], f: Tree[SF, NF], g: Tree[SG, NG], h: Tree[SH, NH], /, *, name: str | None = None) -> _InferredOutputs[tuple[NA, NB, NC, ND, NE, NF, NG, NH], SA, SB, SC, SD, SE, SF, SG, SH]
function(*, name: str | None = None) -> _Bare
function(*inputs: Tree[Any, Any], outputs: Tree[Any, Any] | None = None, name: str | None = None) -> Callable[[Callable[..., Any]], Function[Any, Any, Any, Any]]

Trace a callable with one declaration per parameter and an optional output tree.

Source code in src/scaly/function/api.py
def function(
  *inputs: Tree[Any, Any], outputs: Tree[Any, Any] | None = None, name: str | None = None
) -> Callable[[Callable[..., Any]], Function[Any, Any, Any, Any]]:
  """Trace a callable with one declaration per parameter and an optional output tree."""

  def decorate(fn: Callable[..., Any]) -> Function[Any, Any, Any, Any]:
    function_name = name or getattr(fn, "__name__", "fn")
    parameters = tuple(inspect.signature(fn).parameters.values())
    if (inputs or outputs is not None) and (
      len(parameters) != len(inputs) or any(p.kind not in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD) for p in parameters)
    ):
      raise TypeError(f"{function_name}: declare one tree per positional parameter, got {len(inputs)} declarations for {len(parameters)} parameters")
    return _traced(Function(function_name, fn, None if not inputs and outputs is None and parameters else parameter_list(inputs), outputs))

  return decorate

Derivative requests

Pass these request objects to Function.factory when you need several outputs or derivatives in one function. For a single derivative, the named wrappers below are usually more convenient.

DerivSpec dataclass

Base class for typed requests passed to ConcreteFunction.factory.

Source code in src/scaly/function/concrete.py
@dataclass(frozen=True, slots=True)
class DerivSpec:
  """Base class for typed requests passed to ConcreteFunction.factory."""

  kind: ClassVar[str]
  of: str
  wrt: str

  @property
  def output_name(self) -> str:
    return f"{self.kind}_{self.of}_{self.wrt}"

  def build(self, inputs: Mapping[str, Expr], outputs: Mapping[str, Expr]) -> tuple[Expr, int | None]:
    raise NotImplementedError

  def _in(self, inputs: Mapping[str, Expr], name: str) -> Expr:
    if name not in inputs:
      raise ValueError(f"unknown factory input {name!r} in output {self}")
    return inputs[name]

  def _out(self, outputs: Mapping[str, Expr], name: str) -> Expr:
    if name not in outputs:
      raise ValueError(f"unknown factory output {name!r} in output {self}")
    return outputs[name]

Jac dataclass

Bases: DerivSpec

Dense Jacobian d of / d wrt.

Source code in src/scaly/function/factory.py
@dataclass(frozen=True, slots=True)
class Jac(DerivSpec):
  """Dense Jacobian ``d of / d wrt``."""

  kind = "jac"

  def build(self, inputs: Mapping[str, Expr], outputs: Mapping[str, Expr]) -> tuple[Expr, int | None]:
    return jacobian(self._out(outputs, self.of), self._in(inputs, self.wrt)), None

Grad dataclass

Bases: DerivSpec

Gradient of a scalar output.

Source code in src/scaly/function/factory.py
@dataclass(frozen=True, slots=True)
class Grad(DerivSpec):
  """Gradient of a scalar output."""

  kind = "grad"

  def build(self, inputs: Mapping[str, Expr], outputs: Mapping[str, Expr]) -> tuple[Expr, int | None]:
    return gradient(self._out(outputs, self.of), self._in(inputs, self.wrt)), None

Hess dataclass

Bases: DerivSpec

Dense Hessian of a scalar output.

Source code in src/scaly/function/factory.py
@dataclass(frozen=True, slots=True)
class Hess(DerivSpec):
  """Dense Hessian of a scalar output."""

  kind = "hess"

  @property
  def output_name(self) -> str:
    return f"{self.kind}_{self.of}_{self.wrt}_{self.wrt}"

  def build(self, inputs: Mapping[str, Expr], outputs: Mapping[str, Expr]) -> tuple[Expr, int | None]:
    y = self._out(outputs, self.of)
    x0 = self._in(inputs, self.wrt)
    return hessian(y, x0), None

SpJac dataclass

Bases: DerivSpec

Sparse Jacobian, stored in layout.

Source code in src/scaly/function/factory.py
@dataclass(frozen=True, slots=True)
class SpJac(DerivSpec):
  """Sparse Jacobian, stored in ``layout``."""

  kind = "spjac"
  layout: Layout = field(default="csc", kw_only=True)

  def build(self, inputs: Mapping[str, Expr], outputs: Mapping[str, Expr]) -> tuple[Expr, int | None]:
    sj = _sparse_jacobian(self._out(outputs, self.of), self._in(inputs, self.wrt))
    return sj.leaf(), sj.coloring_width

SpHess dataclass

Bases: DerivSpec

Sparse Hessian of a scalar output, one triangle of it stored in layout.

Source code in src/scaly/function/factory.py
@dataclass(frozen=True, slots=True)
class SpHess(DerivSpec):
  """Sparse Hessian of a scalar output, one ``triangle`` of it stored in ``layout``."""

  kind = "sphess"
  triangle: Triangle = field(default="full", kw_only=True)
  layout: Layout = field(default="csc", kw_only=True)

  def __post_init__(self) -> None:
    _validate_triangle(self.triangle)

  @property
  def output_name(self) -> str:
    return f"{self.kind}_{self.of}_{self.wrt}_{self.wrt}"

  def build(self, inputs: Mapping[str, Expr], outputs: Mapping[str, Expr]) -> tuple[Expr, int | None]:
    y = self._out(outputs, self.of)
    x0 = self._in(inputs, self.wrt)
    sh = _sparse_hessian(y, x0, triangle=self.triangle)
    return sh.leaf(), sh.coloring_width

Fwd dataclass

Bases: DerivSpec

Seeded forward mode: J(of, wrt) @ fwd:wrt.

Source code in src/scaly/function/factory.py
@dataclass(frozen=True, slots=True)
class Fwd(DerivSpec):
  """Seeded forward mode: ``J(of, wrt) @ fwd:wrt``."""

  kind = "fwd"

  def build(self, inputs: Mapping[str, Expr], outputs: Mapping[str, Expr]) -> tuple[Expr, int | None]:
    return jvp(self._out(outputs, self.of), self._in(inputs, self.wrt), self._in(inputs, f"fwd:{self.wrt}")), None

Adj dataclass

Bases: DerivSpec

Seeded reverse mode: J(of, wrt).T @ lam:of.

Source code in src/scaly/function/factory.py
@dataclass(frozen=True, slots=True)
class Adj(DerivSpec):
  """Seeded reverse mode: ``J(of, wrt).T @ lam:of``."""

  kind = "adj"

  def build(self, inputs: Mapping[str, Expr], outputs: Mapping[str, Expr]) -> tuple[Expr, int | None]:
    return vjp((self._out(outputs, self.of),), (self._in(inputs, self.wrt),), (self._in(inputs, f"lam:{self.of}"),))[0], None

Named wrappers

The common derivative functions accept either an Expr and an Expr input, or a Function, an output name, and an input name.

jacobian

jacobian(source: Expr, wrt: Expr) -> Expr
jacobian(source: Function[SI, NI, SO, NO] | ConcreteFunction[SI, NI, SO, NO], of: str | None = None, wrt: str | None = None, *, name: str | None = None) -> Function[SI, NI, Expr, Array]
jacobian(source: Expr | Function | ConcreteFunction, *args: Expr | str, wrt: Expr | str | None = None, of: str | None = None, name: str | None = None) -> Expr | Function

Build a dense Jacobian for an Expr or a named Function output.

For an expression source, wrt must be an independent input.

Source code in src/scaly/function/api.py
def jacobian(
  source: Expr | Function | ConcreteFunction,
  *args: Expr | str,
  wrt: Expr | str | None = None,
  of: str | None = None,
  name: str | None = None,
) -> Expr | Function:
  """Build a dense Jacobian for an Expr or a named Function output.

  For an expression source, ``wrt`` must be an independent input.
  """
  if isinstance(source, Expr):
    return _jacobian_expr(source, _expr_wrt("jacobian", args, wrt, of, name))
  if not isinstance(source, Function | ConcreteFunction):
    raise TypeError("jacobian source must be an Expr or Function")
  function_of, function_wrt = _function_names("jacobian", args, wrt, of)
  return _unseeded(source, name, function_of, function_wrt, Jac)

gradient

gradient(source: Expr | Function | ConcreteFunction, *args: Expr | str, wrt: Expr | str | None = None, of: str | None = None, name: str | None = None) -> Expr | Function

Build a gradient for an Expr or a named Function output.

For an expression source, wrt must be an independent input.

Source code in src/scaly/function/api.py
def gradient(
  source: Expr | Function | ConcreteFunction,
  *args: Expr | str,
  wrt: Expr | str | None = None,
  of: str | None = None,
  name: str | None = None,
) -> Expr | Function:
  """Build a gradient for an Expr or a named Function output.

  For an expression source, ``wrt`` must be an independent input.
  """
  if isinstance(source, Expr):
    return _gradient_expr(source, _expr_wrt("gradient", args, wrt, of, name))
  if not isinstance(source, Function | ConcreteFunction):
    raise TypeError("gradient source must be an Expr or Function")
  function_of, function_wrt = _function_names("gradient", args, wrt, of)
  return _unseeded(source, name, function_of, function_wrt, Grad)

hessian

hessian(source: Expr, wrt: Expr) -> Expr
hessian(source: Function[SI, NI, SO, NO] | ConcreteFunction[SI, NI, SO, NO], of: str | None = None, wrt: str | None = None, *, name: str | None = None) -> Function[SI, NI, Expr, Array]
hessian(source: Expr | Function | ConcreteFunction, *args: Expr | str, wrt: Expr | str | None = None, of: str | None = None, name: str | None = None) -> Expr | Function

Build a Hessian for an Expr or a named Function output.

For an expression source, wrt must be an independent input.

Source code in src/scaly/function/api.py
def hessian(
  source: Expr | Function | ConcreteFunction,
  *args: Expr | str,
  wrt: Expr | str | None = None,
  of: str | None = None,
  name: str | None = None,
) -> Expr | Function:
  """Build a Hessian for an Expr or a named Function output.

  For an expression source, ``wrt`` must be an independent input.
  """
  if isinstance(source, Expr):
    return _hessian_expr(source, _expr_wrt("hessian", args, wrt, of, name))
  if not isinstance(source, Function | ConcreteFunction):
    raise TypeError("hessian source must be an Expr or Function")
  function_of, function_wrt = _function_names("hessian", args, wrt, of)
  return _unseeded(source, name, function_of, function_wrt, Hess)

sparse_jacobian

sparse_jacobian(source: Expr, wrt: Expr) -> Expr
sparse_jacobian(source: Function[SI, NI, SO, NO] | ConcreteFunction[SI, NI, SO, NO], of: str | None = None, wrt: str | None = None, *, name: str | None = None, layout: Layout = 'csc') -> Function[SI, NI, Expr, NumericalArray]
sparse_jacobian(source: Expr | Function | ConcreteFunction, *args: Expr | str, wrt: Expr | str | None = None, of: str | None = None, name: str | None = None, layout: Layout = 'csc') -> Expr | Function

Build a sparse Jacobian that stores only its structural nonzeros, for an Expr or a named Function output.

For an expression source, wrt must be an independent input, and the result is a sparse tensor of shape (source.size, wrt.size). For a Function, the output is a sparse leaf whose values are stored in layout: "csc" (the default), "csr", or a SparsityPattern listing the same coordinates in the order the caller wants.

Source code in src/scaly/function/api.py
def sparse_jacobian(
  source: Expr | Function | ConcreteFunction,
  *args: Expr | str,
  wrt: Expr | str | None = None,
  of: str | None = None,
  name: str | None = None,
  layout: Layout = "csc",
) -> Expr | Function:
  """Build a sparse Jacobian that stores only its structural nonzeros, for an Expr or a named Function output.

  For an expression source, ``wrt`` must be an independent input, and the result is a sparse
  tensor of shape ``(source.size, wrt.size)``. For a Function, the output is a sparse leaf whose
  values are stored in ``layout``: ``"csc"`` (the default), ``"csr"``, or a ``SparsityPattern``
  listing the same coordinates in the order the caller wants.
  """
  if isinstance(source, Expr):
    if layout != "csc":
      raise TypeError("sparse_jacobian Expr form does not accept layout")
    return _expr_sparse_jacobian(source, _expr_wrt("sparse_jacobian", args, wrt, of, name))
  if not isinstance(source, Function | ConcreteFunction):
    raise TypeError("sparse_jacobian source must be an Expr or Function")
  function_of, function_wrt = _function_names("sparse_jacobian", args, wrt, of)
  return _unseeded(source, name, function_of, function_wrt, SpJac, layout=layout)

sparse_hessian

sparse_hessian(source: Expr, wrt: Expr, *, triangle: Triangle = 'full') -> Expr
sparse_hessian(source: Function[SI, NI, SO, NO] | ConcreteFunction[SI, NI, SO, NO], of: str | None = None, wrt: str | None = None, *, name: str | None = None, triangle: Triangle = 'full', layout: Layout = 'csc') -> Function[SI, NI, Expr, NumericalArray]
sparse_hessian(source: Expr | Function | ConcreteFunction, *args: Expr | str, wrt: Expr | str | None = None, of: str | None = None, name: str | None = None, triangle: Triangle = 'full', layout: Layout = 'csc') -> Expr | Function

Build a sparse Hessian that stores only its structural nonzeros, for an Expr or a named scalar Function output.

For an expression source, wrt must be an independent input, and the result is a sparse tensor of shape (wrt.size, wrt.size). triangle selects the stored entries: "full" keeps the whole symmetric pattern, while "lower" and "upper" keep one triangle with the diagonal. For a Function, the output is a sparse leaf whose values are stored in layout, as for sparse_jacobian.

Source code in src/scaly/function/api.py
def sparse_hessian(
  source: Expr | Function | ConcreteFunction,
  *args: Expr | str,
  wrt: Expr | str | None = None,
  of: str | None = None,
  name: str | None = None,
  triangle: Triangle = "full",
  layout: Layout = "csc",
) -> Expr | Function:
  """Build a sparse Hessian that stores only its structural nonzeros, for an Expr or a named scalar Function output.

  For an expression source, ``wrt`` must be an independent input, and the result is a sparse
  tensor of shape ``(wrt.size, wrt.size)``. ``triangle`` selects the stored entries: ``"full"``
  keeps the whole symmetric pattern, while ``"lower"`` and ``"upper"`` keep one triangle with the
  diagonal. For a Function, the output is a sparse leaf whose values are stored in ``layout``, as
  for ``sparse_jacobian``.
  """
  if isinstance(source, Expr):
    if layout != "csc":
      raise TypeError("sparse_hessian Expr form does not accept layout")
    return _expr_sparse_hessian(source, _expr_wrt("sparse_hessian", args, wrt, of, name), triangle=triangle)
  if not isinstance(source, Function | ConcreteFunction):
    raise TypeError("sparse_hessian source must be an Expr or Function")
  function_of, function_wrt = _function_names("sparse_hessian", args, wrt, of)
  return _unseeded(source, name, function_of, function_wrt, SpHess, triangle=triangle, layout=layout)

forward

forward(fn: Function | ConcreteFunction, of: str | None = None, wrt: str | None = None, *, name: str | None = None) -> Function

Create a forward derivative with one seed parameter shaped and typed as wrt.

Source code in src/scaly/function/api.py
def forward(fn: Function | ConcreteFunction, of: str | None = None, wrt: str | None = None, *, name: str | None = None) -> Function:
  """Create a forward derivative with one seed parameter shaped and typed as ``wrt``."""
  return _seeded(fn, of, wrt, name, "fwd")

adjoint

adjoint(fn: Function | ConcreteFunction, of: str | None = None, wrt: str | None = None, *, name: str | None = None) -> Function

Create an adjoint derivative with one seed parameter shaped and typed as of.

Source code in src/scaly/function/api.py
def adjoint(fn: Function | ConcreteFunction, of: str | None = None, wrt: str | None = None, *, name: str | None = None) -> Function:
  """Create an adjoint derivative with one seed parameter shaped and typed as ``of``."""
  return _seeded(fn, of, wrt, name, "adj")

lagrangian_hessian

lagrangian_hessian(fn: Function[tuple[*Ss,], tuple[*Ns,], SO, NO], wrt: str | None = None, *, name: str | None = None, aux_name: str = 'gamma') -> Function[tuple[*Ss, SO], tuple[*Ns, NO], Expr, Array]

Create the dense weighted Hessian with one multiplier parameter matching the output tree.

Source code in src/scaly/function/api.py
def lagrangian_hessian[*Ss, *Ns, SO, NO](
  fn: Function[tuple[*Ss], tuple[*Ns], SO, NO], wrt: str | None = None, *, name: str | None = None, aux_name: str = "gamma"
) -> Function[tuple[*Ss, SO], tuple[*Ns, NO], Expr, Array]:
  """Create the dense weighted Hessian with one multiplier parameter matching the output tree."""
  return _seeded(fn, None, wrt, name, "hess", aux_name)

sparse_lagrangian_hessian

sparse_lagrangian_hessian(fn: Function[tuple[*Ss,], tuple[*Ns,], SO, NO], wrt: str | None = None, *, name: str | None = None, aux_name: str = 'gamma', triangle: Triangle = 'full', layout: Layout = 'csc') -> Function[tuple[*Ss, SO], tuple[*Ns, NO], Expr, NumericalArray]

Create the sparse weighted Hessian with multipliers matching the output tree, stored in layout.

Source code in src/scaly/function/api.py
def sparse_lagrangian_hessian[*Ss, *Ns, SO, NO](
  fn: Function[tuple[*Ss], tuple[*Ns], SO, NO],
  wrt: str | None = None,
  *,
  name: str | None = None,
  aux_name: str = "gamma",
  triangle: Triangle = "full",
  layout: Layout = "csc",
) -> Function[tuple[*Ss, SO], tuple[*Ns, NO], Expr, NumericalArray]:
  """Create the sparse weighted Hessian with multipliers matching the output tree, stored in ``layout``."""
  return _seeded(fn, None, wrt, name, "sphess", aux_name, triangle, layout)

Custom derivative rules

custom_derivative

custom_derivative(fn: Function[SI, NI, SO, NO] | ConcreteFunction[SI, NI, SO, NO], *, jvp: _Rule | None = None, fwd: _Rule | None = None, bwd: _Rule | None = None, sparsity: Callable[[str, str, tuple[int, int]], SparsityPattern | ndarray] | None = None) -> Function[SI, NI, SO, NO]

Return fn with derivative rules that replace differentiating its body.

jvp(*inputs, *directions) is the forward rule, the product of the Jacobian with a direction. It takes fn's parameters and then one direction per parameter, and returns the change of each output along that direction, in fn's output structure. Several directions apply the rule to each direction in one mapped loop, and a direction the rule never reads is never computed.

fwd and bwd form the reverse rule, the product of output weights with the Jacobian. fwd(*inputs) returns (outputs, residuals), and the outputs become fn's. bwd(residuals, weights) takes one weight per output and returns one weighted derivative per parameter of fn. A call computes its outputs and residuals together, so the reverse rule reuses them instead of computing them again, and a second derivative still differentiates them.

A mode without a rule, forward or reverse, differentiates the body. Rules may be shape templates, which take the shapes of each call of fn. sparsity(of, wrt, shape) gives the Jacobian pattern of output of in input wrt, as a SparsityPattern or a boolean mask of shape, which is (of.size, wrt.size). Without it, the pattern of a function with rules is dense.

Source code in src/scaly/function/api.py
def custom_derivative[SI, NI, SO, NO](
  fn: Function[SI, NI, SO, NO] | ConcreteFunction[SI, NI, SO, NO],
  *,
  jvp: _Rule | None = None,
  fwd: _Rule | None = None,
  bwd: _Rule | None = None,
  sparsity: Callable[[str, str, tuple[int, int]], SparsityPattern | np.ndarray] | None = None,
) -> Function[SI, NI, SO, NO]:
  """Return ``fn`` with derivative rules that replace differentiating its body.

  ``jvp(*inputs, *directions)`` is the forward rule, the product of the Jacobian with a direction.
  It takes ``fn``'s parameters and then one direction per parameter, and returns the change of
  each output along that direction, in ``fn``'s output structure. Several directions apply the
  rule to each direction in one mapped loop, and a direction the rule never reads is never computed.

  ``fwd`` and ``bwd`` form the reverse rule, the product of output weights with the Jacobian.
  ``fwd(*inputs)`` returns ``(outputs, residuals)``, and the outputs become ``fn``'s.
  ``bwd(residuals, weights)`` takes one weight per output and returns one weighted derivative per
  parameter of ``fn``. A call computes its outputs and residuals together, so the reverse rule
  reuses them instead of computing them again, and a second derivative still differentiates them.

  A mode without a rule, forward or reverse, differentiates the body. Rules may be shape templates,
  which take the shapes of each call of ``fn``. ``sparsity(of, wrt, shape)`` gives the Jacobian pattern of output
  ``of`` in input ``wrt``, as a ``SparsityPattern`` or a boolean mask of ``shape``, which is
  ``(of.size, wrt.size)``. Without it, the pattern of a function with rules is dense.
  """
  if (fwd is None) != (bwd is None):
    raise TypeError("custom_derivative takes fwd and bwd together")
  if jvp is None and fwd is None and sparsity is None:
    raise TypeError("custom_derivative needs at least one of jvp, fwd and bwd, or sparsity")
  if not isinstance(fn, Function | ConcreteFunction):
    raise TypeError("sc.custom_derivative needs a Function; wrap an extern call in a Function first")
  source = Function._from_instance(fn) if isinstance(fn, ConcreteFunction) else fn
  return lift(source, source.inputs, source.outputs, lambda concrete: _with_rules(concrete, jvp, fwd, bwd, sparsity), name=source.name)

Mapped calls

vmap

vmap(callee: Function[SI, NI, SO, NO] | ConcreteFunction[SI, NI, SO, NO], length: int) -> Function[SI, NI, SO, NO]

Map independent calls over a leading axis, preserving parameter and output trees.

Each output leaf has shape (length, *callee_shape). Use broadcast for a whole shared tensor and window for rank-1 overlapping slices of a declared width. Affine views are read from their base in place. Other views preserve their element order.

broadcast

broadcast(value: T) -> T

Mark one tensor argument to be passed whole to every mapped iteration.

Source code in src/scaly/function/sugar.py
def broadcast[T](value: T, /) -> T:
  """Mark one tensor argument to be passed whole to every mapped iteration."""
  return cast(T, _Broadcast(value))

window

window(value: T, start: int, stride: int) -> T

Mark rank-1 windows whose width comes from the callee's declared shape.

Iteration i reads at start + i * stride. For a shape hole, use a leading-axis view or explicitly instantiate the callee to specify the window width.

Source code in src/scaly/function/sugar.py
def window[T](value: T, start: int, stride: int, /) -> T:
  """Mark rank-1 windows whose width comes from the callee's declared shape.

  Iteration ``i`` reads at ``start + i * stride``. For a shape hole, use a leading-axis
  view or explicitly instantiate the callee to specify the window width.
  """
  start, stride = int(start), int(stride)
  if start < 0 or stride < 0:
    raise ValueError("window start and stride must be non-negative")
  return cast(T, _Window(value, start, stride))

Control flow

scan(body, init, xs=(), *, length, trip=None, index=False, reverse=False, in_place=(), max_trajectory=None)

scan

Run body for length steps, threading a carry and stacking per-step outputs.

body takes one parameter per entry of init (one carry when init is not a tuple), then the step number as an int64 scalar when index is true, then one parameter per entry of xs. It returns the new carries first, each with its input's shape and dtype, followed by any per-step outputs. Each entry of xs is sliced per step as vmap slices its arguments: a leading axis of length, broadcast for a value shared by every step, or window.

Returns (carry, ys): the final carry, shaped like init, and a tuple of the per-step outputs, each with a leading axis of length. trip, an int64 scalar, runs only the first max(0, min(trip, length)) steps. The unused entries of the stacked outputs are zero. With reverse the steps run from the last to the first, and step k still reads slice k and writes output k. in_place names carry positions or input names that lowering must prove safe to update in place. Forward derivatives follow the same steps as the primal loop. Reverse derivatives store the entering carries and traverse the steps backwards. max_trajectory limits the number of stored carry elements for this loop, using the static length even when trip is smaller. None leaves it unlimited. Exceeding the limit raises ValueError and suggests sc.custom_derivative. Derivative loops do not inherit the limit.

while_loop

while_loop(cond: Any, body: Any, init: Any, *, max_iter: int, params: tuple[Any, ...] = (), index: bool = False, max_trajectory: int | None = None) -> tuple[Any, Expr]

Apply body to a carry while cond holds, at most max_iter times.

body and cond take the same parameters: one per entry of init (one carry when init is not a tuple), then the step number as an int64 scalar when index is true, then one per entry of params, which every step reads whole. cond returns a bool scalar and is evaluated before each step. body returns the new carries, each with its input's shape and dtype. Returns (carry, count): the final carry, shaped like init, and the number of steps taken as an int64 scalar. Forward derivatives take the same steps, with the condition evaluated on primal carries. Reverse derivatives traverse exactly the steps taken backwards. max_trajectory limits stored carry elements using max_iter times the stored carry size, even when the loop exits early. Each nested loop has its own limit. None leaves storage unlimited. Exceeding the limit raises ValueError and suggests sc.custom_derivative. Derivative loops do not inherit the limit.

cond

cond(selector: Any, branches: tuple[Function | ConcreteFunction, ...], *operands: Any) -> Any

Call the branch selected by a scalar boolean or integer, with operands as its inputs.

False selects branch zero and True selects branch one. A boolean requires exactly two branches. Integer selectors clamp to [0, len(branches) - 1]. Only the selected branch runs, including its prints. Branches must have matching input and output structures, shapes, dtypes and stored patterns. Results follow the first branch's output tree. The selector has no derivative. Derivatives follow the selected branch. Structural sparsity is the union of the branches.