Skip to content

Building functions

Declare the shape and structure of a function with L, G, 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 G introduces a tuple, so a one-leaf tree is the bare leaf and never a one-element tuple. See L 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 ``G`` introduces a tuple, so a one-leaf tree is the bare leaf
  and never a one-element tuple. See ``L`` for what that means at a call site.
  """

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

  @property
  def shapes(self) -> tuple[tuple[int, ...], ...]:
    """The leaf shapes in C-signature order."""
    if any(decl is Ellipsis 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 Ellipsis 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 Ellipsis 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[np.ndarray, ...]:
    """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}")

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 Ellipsis 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[np.ndarray, ...]

Validate and flatten a numerical value.

Source code in src/scaly/function/tree.py
def flatten_numerical(self, value: Numerical, what: str) -> tuple[np.ndarray, ...]:
  """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

L

Bases: Tree[Expr, ndarray]

Declare one named tensor.

The declared name is external metadata and need not match the local name used by a decorated function body. Pass a TensorType to set dtype or differentiability explicitly.

A single leaf is passed and returned unpacked. An L input tree takes the tensor itself, not (tensor,), and an L output tree returns the tensor itself, not a one-element tuple. Do not destructure a single-leaf result: (y,) = fn(x) does not raise, it iterates the returned tensor along its first axis exactly as NumPy would.

Source code in src/scaly/function/tree.py
class L(Tree[Expr, np.ndarray]):
  """Declare one named tensor.

  The declared name is external metadata and need not match the local name used by a decorated
  function body. Pass a ``TensorType`` to set dtype or differentiability explicitly.

  **A single leaf is passed and returned unpacked.** An ``L`` input tree takes the tensor itself,
  not ``(tensor,)``, and an ``L`` output tree returns the tensor itself, not a one-element tuple.
  Do not destructure a single-leaf result: ``(y,) = fn(x)`` does not raise, it iterates the
  returned tensor along its first axis exactly as NumPy would.
  """

  def __init__(self, name: str, shape: ShapeDecl, /) -> None:
    if not isinstance(name, str) or not name:
      raise ValueError("L needs a non-empty name")
    self.names = (name,)
    if shape is Ellipsis:
      self.decls = (Ellipsis,)
    elif isinstance(shape, TensorType):
      self.decls = (shape,)
    else:
      self.decls = (TensorType(as_shape(shape)),)

  def symbols(self, *, diff: bool | None = None) -> Expr:
    type_ = self.types[0]
    if diff is not None:
      type_ = TensorType(type_.shape, type_.dtype, diff)
    return Expr(ExprOp.INPUT, type=type_, name=self.names[0])

  def relabel(self, prefix: str) -> L:
    return L(prefix + self.names[0], self.decls[0])

  def with_types(self, types: tuple[TensorType, ...]) -> L:
    if len(types) != 1:
      raise ValueError(f"L expects one resolved type, got {len(types)}")
    return L(self.names[0], types[0])

  def flatten_symbolic(self, value: Expr, what: str, *, allow_scalar: bool = False) -> tuple[Expr, ...]:
    if not isinstance(value, Expr):
      raise ValueError(f"{what}: expected an Expr for {self.names[0]!r}, got {type(value).__name__}")
    decl = self.decls[0]
    if decl is not Ellipsis and value.shape != decl.shape and not (allow_scalar and value.shape == ()):
      raise ValueError(f"{what}: expected shape {decl.shape} for {self.names[0]!r}, got {value.shape}")
    return (value,)

  def flatten_numerical(self, value: np.ndarray, what: str) -> tuple[np.ndarray, ...]:
    if isinstance(value, Expr):
      raise ValueError(f"{what}: expected a numerical value for {self.names[0]!r}, got Expr")
    array = np.asarray(value, dtype=self.types[0].dtype.numpy())
    if array.shape != self.shapes[0]:
      raise ValueError(f"{what}: expected shape {self.shapes[0]} for {self.names[0]!r}, got {array.shape}")
    return (np.require(array, requirements="C"),)

  def unflatten(self, values: tuple[Any, ...]) -> Any:
    if len(values) != 1:
      raise ValueError(f"L expects one flat value, got {len(values)}")
    return values[0]

G

G(a: Tree[SA, NA], b: Tree[SB, NB]) -> Tree[tuple[SA, SB], tuple[NA, NB]]
G(a: Tree[SA, NA], b: Tree[SB, NB], c: Tree[SC, NC]) -> Tree[tuple[SA, SB, SC], tuple[NA, NB, NC]]
G(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]]
G(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]]
G(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]]
G(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]]
G(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]]
G(*parts: Tree[Any, Any]) -> Tree[Any, Any]

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

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

The decorator

function

function(inputs: Tree[SI, NI], outputs: Tree[SO, NO], /, *, name: str | None = None) -> Callable[[Callable[[SI], SO]], Function[SI, NI, SO, NO]]

Trace a callable over declared input and output pytrees.

Source code in src/scaly/function/api.py
def function[SI, NI, SO, NO](
  inputs: Tree[SI, NI], outputs: Tree[SO, NO], /, *, name: str | None = None
) -> Callable[[Callable[[SI], SO]], Function[SI, NI, SO, NO]]:
  """Trace a callable over declared input and output pytrees."""

  def decorate(fn: Callable[[SI], SO]) -> Function[SI, NI, SO, NO]:
    return Function(name or getattr(fn, "__name__", "fn"), fn, 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 Function.factory.

Source code in src/scaly/function/model.py
@dataclass(frozen=True, slots=True)
class DerivSpec:
  """Base class for typed requests passed to Function.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], of: str, wrt: str, *, name: str | None = None) -> Function[SI, NI, Expr, np.ndarray]
jacobian(source: Expr | Function, *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.

Source code in src/scaly/function/api.py
def jacobian(
  source: Expr | Function,
  *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."""
  if isinstance(source, Expr):
    return _jacobian_expr(source, _expr_wrt("jacobian", args, wrt, of, name))
  if not isinstance(source, Function):
    raise TypeError("jacobian source must be an Expr or Function")
  function_of, function_wrt = _function_names("jacobian", args, wrt, of)
  return _unseeded(source, name or f"{source.name}_jac_{function_of}_{function_wrt}", function_of, function_wrt, Jac(function_of, function_wrt))

gradient

gradient(source: Expr, wrt: Expr) -> Expr
gradient(source: Function[SI, NI, SO, NO], of: str, wrt: str, *, name: str | None = None) -> Function[SI, NI, Expr, np.ndarray]
gradient(source: Expr | Function, *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.

Source code in src/scaly/function/api.py
def gradient(
  source: Expr | Function,
  *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."""
  if isinstance(source, Expr):
    return _gradient_expr(source, _expr_wrt("gradient", args, wrt, of, name))
  if not isinstance(source, Function):
    raise TypeError("gradient source must be an Expr or Function")
  function_of, function_wrt = _function_names("gradient", args, wrt, of)
  return _unseeded(source, name or f"{source.name}_grad_{function_of}_{function_wrt}", function_of, function_wrt, Grad(function_of, function_wrt))

hessian

hessian(source: Expr, wrt: Expr) -> Expr
hessian(source: Function[SI, NI, SO, NO], of: str, wrt: str, *, name: str | None = None) -> Function[SI, NI, Expr, np.ndarray]
hessian(source: Expr | Function, *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.

Source code in src/scaly/function/api.py
def hessian(
  source: Expr | Function,
  *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."""
  if isinstance(source, Expr):
    return _hessian_expr(source, _expr_wrt("hessian", args, wrt, of, name))
  if not isinstance(source, Function):
    raise TypeError("hessian source must be an Expr or Function")
  function_of, function_wrt = _function_names("hessian", args, wrt, of)
  return _unseeded(
    source, name or f"{source.name}_hess_{function_of}_{function_wrt}_{function_wrt}", function_of, function_wrt, Hess(function_of, function_wrt)
  )

sparse_jacobian

sparse_jacobian(source: Expr, wrt: Expr) -> SparseJacobian
sparse_jacobian(source: Function[SI, NI, SO, NO], of: str, wrt: str, *, name: str | None = None) -> Function[SI, NI, Expr, np.ndarray]
sparse_jacobian(source: Expr | Function, *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.

Source code in src/scaly/function/api.py
def sparse_jacobian(
  source: Expr | Function,
  *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."""
  if isinstance(source, Expr):
    return _expr_sparse_jacobian(source, _expr_wrt("sparse_jacobian", args, wrt, of, name))
  if not isinstance(source, Function):
    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 or f"{source.name}_spjac_{function_of}_{function_wrt}", function_of, function_wrt, SpJac(function_of, function_wrt))

sparse_hessian

sparse_hessian(source: Expr, wrt: Expr, *, triangle: Triangle = 'full') -> SparseJacobian
sparse_hessian(source: Function[SI, NI, SO, NO], of: str, wrt: str, *, name: str | None = None, triangle: Triangle = 'full') -> Function[SI, NI, Expr, np.ndarray]
sparse_hessian(source: Expr | Function, *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.

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,
  *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.

  ``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):
    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 or f"{source.name}_sphess_{function_of}_{function_wrt}_{function_wrt}",
    function_of,
    function_wrt,
    SpHess(function_of, function_wrt, triangle=triangle),
  )

forward

forward(fn: Function[SI, NI, SO, NO], of: str, wrt: str, *, name: str | None = None) -> Function[tuple[SI, Expr], tuple[NI, np.ndarray], Expr, np.ndarray]

Create a seeded forward-mode Function computing J(of, wrt) @ fwd:wrt.

Source code in src/scaly/function/api.py
def forward[SI, NI, SO, NO](
  fn: Function[SI, NI, SO, NO],
  of: str,
  wrt: str,
  *,
  name: str | None = None,
) -> Function[tuple[SI, Expr], tuple[NI, np.ndarray], Expr, np.ndarray]:
  """Create a seeded forward-mode Function computing J(of, wrt) @ fwd:wrt."""
  _checked_names(fn, of, wrt)
  seed = L(f"fwd:{wrt}", fn.inputs[fn.input_tree.index(wrt)].type)
  result = fn.factory(name or f"{fn.name}_fwd_{of}_{wrt}", [*fn.input_names, f"fwd:{wrt}"], [Fwd(of, wrt)])
  return _typed_result(result, G(fn.input_tree, _factory_input_tree(result, seed)))

adjoint

adjoint(fn: Function[SI, NI, SO, NO], of: str, wrt: str, *, name: str | None = None) -> Function[tuple[SI, Expr], tuple[NI, np.ndarray], Expr, np.ndarray]

Create a seeded reverse-mode Function computing J(of, wrt).T @ lam:of.

Source code in src/scaly/function/api.py
def adjoint[SI, NI, SO, NO](
  fn: Function[SI, NI, SO, NO],
  of: str,
  wrt: str,
  *,
  name: str | None = None,
) -> Function[tuple[SI, Expr], tuple[NI, np.ndarray], Expr, np.ndarray]:
  """Create a seeded reverse-mode Function computing J(of, wrt).T @ lam:of."""
  _checked_names(fn, of, wrt)
  seed = L(f"lam:{of}", fn.outputs[fn.output_tree.index(of)].type)
  result = fn.factory(name or f"{fn.name}_adj_{of}_{wrt}", [*fn.input_names, f"lam:{of}"], [Adj(of, wrt)])
  return _typed_result(result, G(fn.input_tree, _factory_input_tree(result, seed)))

lagrangian_hessian

lagrangian_hessian(fn: Function[SI, NI, SO, NO], wrt: str, *, name: str | None = None, aux_name: str = 'gamma') -> Function[tuple[SI, SO], tuple[NI, NO], Expr, np.ndarray]

Create the dense Hessian of all outputs weighted by the declared output tree.

Source code in src/scaly/function/api.py
def lagrangian_hessian[SI, NI, SO, NO](
  fn: Function[SI, NI, SO, NO],
  wrt: str,
  *,
  name: str | None = None,
  aux_name: str = "gamma",
) -> Function[tuple[SI, SO], tuple[NI, NO], Expr, np.ndarray]:
  """Create the dense Hessian of all outputs weighted by the declared output tree."""
  fn.input_tree.index(wrt)
  output_names = list(fn.output_names)
  inputs = [*fn.input_names, *(f"lam:{out}" for out in output_names)]
  result = fn.factory(
    name or f"{fn.name}_hess_{aux_name}_{wrt}_{wrt}",
    inputs,
    [Hess(aux_name, wrt)],
    aux={aux_name: output_names},
  )
  seed_tree = _factory_input_tree(result, fn.output_tree.relabel("lam:"))
  return _typed_result(result, G(fn.input_tree, seed_tree))

sparse_lagrangian_hessian

sparse_lagrangian_hessian(fn: Function[SI, NI, SO, NO], wrt: str, *, name: str | None = None, aux_name: str = 'gamma', triangle: Triangle = 'full') -> Function[tuple[SI, SO], tuple[NI, NO], Expr, np.ndarray]

Create compact values for the weighted Hessian of all declared outputs.

Source code in src/scaly/function/api.py
def sparse_lagrangian_hessian[SI, NI, SO, NO](
  fn: Function[SI, NI, SO, NO],
  wrt: str,
  *,
  name: str | None = None,
  aux_name: str = "gamma",
  triangle: Triangle = "full",
) -> Function[tuple[SI, SO], tuple[NI, NO], Expr, np.ndarray]:
  """Create compact values for the weighted Hessian of all declared outputs."""
  fn.input_tree.index(wrt)
  output_names = list(fn.output_names)
  inputs = [*fn.input_names, *(f"lam:{out}" for out in output_names)]
  result = fn.factory(
    name or f"{fn.name}_sphess_{aux_name}_{wrt}_{wrt}",
    inputs,
    [SpHess(aux_name, wrt, triangle=triangle)],
    aux={aux_name: output_names},
  )
  seed_tree = _factory_input_tree(result, fn.output_tree.relabel("lam:"))
  return _typed_result(result, G(fn.input_tree, seed_tree))