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.

Source code in src/scaly/function/tree.py
class Tree[Symbolic, Numerical]:
  """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.
  """

  names: tuple[str, ...]
  decls: tuple[LeafDecl, ...]

  def __setattr__(self, name: str, value: Any) -> None:
    if name in self.__dict__:
      raise AttributeError(f"tree attribute {name!r} is read-only")
    object.__setattr__(self, name, value)

  def __delattr__(self, name: str) -> None:
    raise AttributeError(f"tree attribute {name!r} is read-only")

  @property
  def has_holes(self) -> bool:
    """Whether any leaf shape needs binding."""
    return any(decl is None for decl in self.decls)

  @property
  def shapes(self) -> tuple[tuple[int, ...], ...]:
    """The leaf shapes in C-signature order."""
    if any(decl is None for decl in self.decls):
      raise TypeError(f"tree {self.names} has inferred shapes; resolve them by tracing first")
    return tuple(cast(TensorType, decl).shape for decl in self.decls)

  @property
  def types(self) -> tuple[TensorType, ...]:
    """The leaf tensor types in C-signature order."""
    if any(decl is None for decl in self.decls):
      raise TypeError(f"tree {self.names} has inferred shapes; resolve them by tracing first")
    return cast(tuple[TensorType, ...], self.decls)

  @property
  def size(self) -> int:
    """The number of leaves."""
    return len(self.names)

  def symbols(self, *, diff: bool | None = None) -> Symbolic:
    """Create named input expressions with this tree's structure."""
    raise NotImplementedError

  def relabel(self, prefix: str) -> Tree[Symbolic, Numerical]:
    """Return the same structure with ``prefix`` added to every leaf name."""
    raise NotImplementedError

  def with_types(self, types: tuple[TensorType, ...]) -> Tree[Symbolic, Numerical]:
    """Return the same structure with resolved leaf types."""
    raise NotImplementedError

  def resolved(self, traced: tuple[TensorType, ...]) -> tuple[TensorType, ...]:
    """Check traced output types against this declaration and resolve inferred shapes."""
    if len(traced) != self.size:
      raise TypeError(f"declared {self.size} outputs {self.names}, body returned {len(traced)}")
    for name, decl, actual in zip(self.names, self.decls, traced, strict=True):
      if decl is not None and (decl.shape != actual.shape or decl.dtype != actual.dtype):
        raise TypeError(f"{name!r} declared with type {decl}, traced type {actual}")
    return traced

  def index(self, name: str) -> int:
    """Return the flat index for ``name``, or raise with the declared choices."""
    if name not in self.names:
      raise ValueError(f"unknown name {name!r}; declared {self.names}")
    return self.names.index(name)

  def is_symbolic(self, 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.
    """
    leaves = _leaves(value)
    return bool(leaves) and all(isinstance(leaf, Expr) for leaf in leaves)

  def is_numerical(self, 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.
    """
    return not any(isinstance(leaf, Expr) for leaf in _leaves(value))

  def flatten_symbolic(self, value: Symbolic, what: str, *, allow_scalar: bool = False) -> tuple[Expr, ...]:
    """Validate and flatten a symbolic value."""
    raise NotImplementedError

  def flatten_numerical(self, value: Numerical, what: str) -> tuple[Array, ...]:
    """Validate and flatten a numerical value."""
    raise NotImplementedError

  def unflatten(self, values: tuple[Any, ...]) -> Any:
    """Rebuild this tree's structure from flat values."""
    raise NotImplementedError

  def _check_unique(self) -> None:
    if len(set(self.names)) != len(self.names):
      raise ValueError(f"duplicate names in {self.names}")

has_holes property

has_holes: bool

Whether any leaf shape 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.

Source code in src/scaly/function/tree.py
def symbols(self, *, diff: bool | None = None) -> Symbolic:
  """Create named input expressions with this tree's structure."""
  raise NotImplementedError

relabel

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

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

Source code in src/scaly/function/tree.py
def relabel(self, prefix: str) -> Tree[Symbolic, Numerical]:
  """Return the same structure with ``prefix`` added to every leaf name."""
  raise NotImplementedError

with_types

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

Return the same structure with resolved leaf types.

Source code in src/scaly/function/tree.py
def with_types(self, types: tuple[TensorType, ...]) -> Tree[Symbolic, Numerical]:
  """Return the same structure with resolved leaf types."""
  raise NotImplementedError

resolved

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

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

Source code in src/scaly/function/tree.py
def resolved(self, traced: tuple[TensorType, ...]) -> tuple[TensorType, ...]:
  """Check traced output types against this declaration and resolve inferred shapes."""
  if len(traced) != self.size:
    raise TypeError(f"declared {self.size} outputs {self.names}, body returned {len(traced)}")
  for name, decl, actual in zip(self.names, self.decls, traced, strict=True):
    if decl is not None and (decl.shape != actual.shape or decl.dtype != actual.dtype):
      raise TypeError(f"{name!r} declared with type {decl}, traced type {actual}")
  return traced

index

index(name: str) -> int

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

Source code in src/scaly/function/tree.py
def index(self, name: str) -> int:
  """Return the flat index for ``name``, or raise with the declared choices."""
  if name not in self.names:
    raise ValueError(f"unknown name {name!r}; declared {self.names}")
  return self.names.index(name)

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.

Source code in src/scaly/function/tree.py
def is_symbolic(self, 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.
  """
  leaves = _leaves(value)
  return bool(leaves) and all(isinstance(leaf, Expr) for leaf in leaves)

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.

Source code in src/scaly/function/tree.py
def is_numerical(self, 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.
  """
  return not any(isinstance(leaf, Expr) for leaf in _leaves(value))

flatten_symbolic

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

Validate and flatten a symbolic value.

Source code in src/scaly/function/tree.py
def flatten_symbolic(self, value: Symbolic, what: str, *, allow_scalar: bool = False) -> tuple[Expr, ...]:
  """Validate and flatten a symbolic value."""
  raise NotImplementedError

flatten_numerical

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

Validate and flatten a numerical value.

Source code in src/scaly/function/tree.py
def flatten_numerical(self, value: Numerical, what: str) -> tuple[Array, ...]:
  """Validate and flatten a numerical value."""
  raise NotImplementedError

unflatten

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

Rebuild this tree's structure from flat values.

Source code in src/scaly/function/tree.py
def unflatten(self, values: tuple[Any, ...]) -> Any:
  """Rebuild this tree's structure from flat values."""
  raise NotImplementedError

arg

arg(name: str, shape: ShapeDecl = None) -> Tree[Expr, Array]

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.

Source code in src/scaly/function/tree.py
def arg(name: str, shape: ShapeDecl = None, /) -> Tree[Expr, Array]:
  """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.
  """
  return _Leaf(name, shape)

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, SparsityPattern | None, 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, SparsityPattern | None, int | None]:
    return jacobian(self._out(outputs, self.of), self._in(inputs, self.wrt)), None, 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, SparsityPattern | None, int | None]:
    return gradient(self._out(outputs, self.of), self._in(inputs, self.wrt)), None, 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, SparsityPattern | None, int | None]:
    y = self._out(outputs, self.of)
    x0 = self._in(inputs, self.wrt)
    return hessian(y, x0), None, None

SpJac dataclass

Bases: DerivSpec

Compact nonzero Jacobian values, with the sparsity that indexes them.

Source code in src/scaly/function/factory.py
@dataclass(frozen=True, slots=True)
class SpJac(DerivSpec):
  """Compact nonzero Jacobian values, with the sparsity that indexes them."""

  kind = "spjac"

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

SpHess dataclass

Bases: DerivSpec

Compact nonzero Hessian values, with the sparsity that indexes them.

Source code in src/scaly/function/factory.py
@dataclass(frozen=True, slots=True)
class SpHess(DerivSpec):
  """Compact nonzero Hessian values, with the sparsity that indexes them."""

  kind = "sphess"
  triangle: Triangle = field(default="full", 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, SparsityPattern | None, int | None]:
    y = self._out(outputs, self.of)
    x0 = self._in(inputs, self.wrt)
    sh = sparse_hessian(y, x0, triangle=self.triangle)
    return sh.values, sh.sparsity, 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, SparsityPattern | None, int | None]:
    return jvp(self._out(outputs, self.of), self._in(inputs, self.wrt), self._in(inputs, f"fwd:{self.wrt}")), None, 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, SparsityPattern | None, int | None]:
    return vjp((self._out(outputs, self.of),), (self._in(inputs, self.wrt),), (self._in(inputs, f"lam:{self.of}"),))[0], None, 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, wrt: Expr) -> Expr
gradient(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]
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) -> SparseJacobian
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) -> Function[SI, NI, Expr, Array]
sparse_jacobian(source: Expr | Function | ConcreteFunction, *args: Expr | str, wrt: Expr | str | None = None, of: str | None = None, name: str | None = None) -> SparseJacobian | Function

Build compact nonzero Jacobian values 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 sparse_jacobian(
  source: Expr | Function | ConcreteFunction,
  *args: Expr | str,
  wrt: Expr | str | None = None,
  of: str | None = None,
  name: str | None = None,
) -> SparseJacobian | Function:
  """Build compact nonzero Jacobian values for an Expr or a named Function output.

  For an expression source, ``wrt`` must be an independent input.
  """
  if isinstance(source, Expr):
    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)

sparse_hessian

sparse_hessian(source: Expr, wrt: Expr, *, triangle: Triangle = 'full') -> SparseJacobian
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') -> Function[SI, NI, Expr, Array]
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') -> SparseJacobian | Function

Build compact nonzero Hessian values for an Expr or a named Function output.

For an expression source, wrt must be an independent input. triangle selects the symmetric pattern returned by the sparse Hessian: "full" keeps every entry, while "lower" and "upper" keep one triangle in the full pattern's order.

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",
) -> SparseJacobian | Function:
  """Build compact nonzero Hessian values for an Expr or a named Function output.

  For an expression source, ``wrt`` must be an independent input.
  ``triangle`` selects the symmetric pattern returned by the sparse Hessian: ``"full"`` keeps
  every entry, while ``"lower"`` and ``"upper"`` keep one triangle in the full pattern's order.
  """
  if isinstance(source, Expr):
    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)

forward

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

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

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

adjoint

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

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

Source code in src/scaly/function/api.py
def adjoint[*Ss, *Ns, SO, NO](
  fn: Function[tuple[*Ss], tuple[*Ns], SO, NO], of: str | None = None, wrt: str | None = None, *, name: str | None = None
) -> Function[tuple[*Ss, Expr], tuple[*Ns, Array], Expr, Array]:
  """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') -> Function[tuple[*Ss, SO], tuple[*Ns, NO], Expr, Array]

Create compact weighted Hessian values with multipliers matching the output tree.

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",
) -> Function[tuple[*Ss, SO], tuple[*Ns, NO], Expr, Array]:
  """Create compact weighted Hessian values with multipliers matching the output tree."""
  return _seeded(fn, None, wrt, name, "sphess", aux_name, triangle)

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")
  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.

Source code in src/scaly/function/sugar.py
def vmap[SI, NI, SO, NO](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.
  """
  if not isinstance(callee, Function | ConcreteFunction):
    raise TypeError("vmap callee must be an scaly Function")
  if length < 0:
    raise ValueError("vmap length must be non-negative")
  source = Function._from_instance(callee) if isinstance(callee, ConcreteFunction) else callee

  def strip_axis(skeleton):
    if isinstance(skeleton, tuple):
      return tuple(strip_axis(part) for part in skeleton)
    if not skeleton.shape or skeleton.shape[0] != length:
      raise ValueError(f"vmap needs leading axis {length}, got {skeleton.shape}")
    return TensorType(skeleton.shape[1:], skeleton.dtype, skeleton.diff)

  def transform(concrete: ConcreteFunction) -> ConcreteFunction:
    if length in concrete._memo.maps:
      return concrete._memo.maps[length]
    inputs = _batch_tree(concrete.input_tree, length)
    outputs = _batch_tree(concrete.output_tree, length)

    def body(*args):
      actuals = inputs.flatten_symbolic(args, "vmap inputs")
      specs = [(actual if len(actual.shape) == 1 else actual.vec(), 0, formal.size) for actual, formal in zip(actuals, concrete.inputs, strict=True)]
      values = tuple(_mapped_call(concrete, length, specs, i).reshape((length, *out.shape)) for i, out in enumerate(concrete.outputs))
      return outputs.unflatten(values)

    concrete._memo.maps[length] = ConcreteFunction._trace(f"{concrete.name}_vmap{length}", body, inputs, outputs)
    return concrete._memo.maps[length]

  mapped = _Mapped(
    f"{source.name}_vmap{length}",
    source._fn,
    None if source.inputs is None else _batch_tree(source.inputs, length),
    None if source.outputs is None else _batch_tree(source.outputs, length),
    source,
    transform,
    strip_axis,
    length,
  )
  return cast(Function[SI, NI, SO, NO], _traced(mapped))

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.
  """
  if start < 0 or stride < 0:
    raise ValueError("window start and stride must be non-negative")
  return cast(T, _Window(value, start, stride))