Skip to content

furax.core

Modules:

Classes:

Functions:

  • asoperator

    Wraps a function into an operator.

  • diagonal

    Mark an operator as diagonal (implies symmetric, which implies square).

  • idempotent

    Mark an operator as idempotent, i.e. P @ P = P (implies square).

  • lower_triangular

    Mark an operator as lower triangular (implies square).

  • negative_semidefinite

    Mark an operator as negative semi-definite (implies square).

  • orthogonal

    Mark an operator as orthogonal (implies square).

  • positive_semidefinite

    Mark an operator as positive semi-definite (implies square).

  • square

    Mark an operator as square.

  • symmetric

    Mark an operator as symmetric (implies square).

  • tridiagonal

    Mark an operator as tridiagonal (implies square).

  • upper_triangular

    Mark an operator as upper triangular (implies square).

  • dense_symmetric_band_toeplitz

    Returns a dense Symmetric Band Toeplitz matrix.

furax.core.MoveAxisOperator

Bases: AbstractLinearOperator

Operator that moves axes of pytree leaves: y = jnp.moveaxis(x, source, destination).

This operator is orthogonal: its transpose and inverse are the reverse axis move.

Attributes:

Examples:

>>> in_structure = jax.ShapeDtypeStruct((2, 3), jnp.float32)
>>> op = MoveAxisOperator(0, 1, in_structure=in_structure)
>>> op(jnp.array([[1., 1, 1], [2, 2, 2]]))
Array([[1., 2.],
       [1., 2.],
       [1., 2.]], dtype=float32)

Methods:

Source code in src/furax/core/_axes.py
class MoveAxisOperator(AbstractLinearOperator):
    """Operator that moves axes of pytree leaves: y = jnp.moveaxis(x, source, destination).

    This operator is orthogonal: its transpose and inverse are the reverse axis move.

    Attributes:
        source: The source axis or axes to move.
        destination: The destination axis or axes.

    Examples:
        >>> in_structure = jax.ShapeDtypeStruct((2, 3), jnp.float32)
        >>> op = MoveAxisOperator(0, 1, in_structure=in_structure)
        >>> op(jnp.array([[1., 1, 1], [2, 2, 2]]))
        Array([[1., 2.],
               [1., 2.],
               [1., 2.]], dtype=float32)
    """

    source: tuple[int, ...] = field(metadata={'static': True})
    destination: tuple[int, ...] = field(metadata={'static': True})

    def __init__(
        self,
        source: int | Sequence[int],
        destination: int | Sequence[int],
        *,
        in_structure: PyTree[jax.ShapeDtypeStruct],
    ) -> None:
        if isinstance(source, int):
            source = (source,)
        elif not isinstance(source, tuple):
            source = tuple(source)
        if isinstance(destination, int):
            destination = (destination,)
        elif not isinstance(destination, tuple):
            destination = tuple(destination)
        object.__setattr__(self, 'source', source)
        object.__setattr__(self, 'destination', destination)
        object.__setattr__(self, 'in_structure', in_structure)

    def mv(self, x: PyTree[Array, '...']) -> PyTree[Array, '...']:
        return jax.tree.map(lambda leaf: jnp.moveaxis(leaf, self.source, self.destination), x)

    def transpose(self) -> AbstractLinearOperator:
        return MoveAxisOperator(
            source=self.destination, destination=self.source, in_structure=self.out_structure
        )

    inverse = transpose

source = field(metadata={'static': True}) class-attribute instance-attribute

destination = field(metadata={'static': True}) class-attribute instance-attribute

inverse = transpose class-attribute instance-attribute

__init__(source, destination, *, in_structure)

Source code in src/furax/core/_axes.py
def __init__(
    self,
    source: int | Sequence[int],
    destination: int | Sequence[int],
    *,
    in_structure: PyTree[jax.ShapeDtypeStruct],
) -> None:
    if isinstance(source, int):
        source = (source,)
    elif not isinstance(source, tuple):
        source = tuple(source)
    if isinstance(destination, int):
        destination = (destination,)
    elif not isinstance(destination, tuple):
        destination = tuple(destination)
    object.__setattr__(self, 'source', source)
    object.__setattr__(self, 'destination', destination)
    object.__setattr__(self, 'in_structure', in_structure)

mv(x)

Source code in src/furax/core/_axes.py
def mv(self, x: PyTree[Array, '...']) -> PyTree[Array, '...']:
    return jax.tree.map(lambda leaf: jnp.moveaxis(leaf, self.source, self.destination), x)

transpose()

Source code in src/furax/core/_axes.py
def transpose(self) -> AbstractLinearOperator:
    return MoveAxisOperator(
        source=self.destination, destination=self.source, in_structure=self.out_structure
    )

furax.core.RavelOperator dataclass

Bases: AbstractRavelOrReshapeOperator

Operator that flattens pytree leaves: y = x.ravel().

By default, all dimensions are flattened. Use first_axis and last_axis to flatten only a subset of contiguous axes.

This operator is orthogonal: its transpose restores the original shape.

Attributes:

Examples:

To flatten the leaves of a pytree:

>>> in_structure = jax.ShapeDtypeStruct((2, 3), jnp.float32)
>>> op = RavelOperator(in_structure=in_structure)
>>> op.out_structure
ShapeDtypeStruct(shape=(6,), dtype=float32)

To flatten the first two axes of the leaves of a pytree:

>>> import furax as fx
>>> x = [jnp.ones((2, 2)), jnp.ones((2, 2, 8))]
>>> op = RavelOperator(0, 1, in_structure=fx.tree.as_structure(x))
>>> op.out_structure
[ShapeDtypeStruct(shape=(4,), dtype=float32),
ShapeDtypeStruct(shape=(4, 8), dtype=float32)]

To flatten the last two axes of the leaves of a pytree:

>>> import furax as fx
>>> x = [jnp.ones((2, 2, 3)), jnp.ones((2, 8))]
>>> op = RavelOperator(-2, -1, in_structure=fx.tree.as_structure(x))
>>> op.out_structure
[ShapeDtypeStruct(shape=(2, 6), dtype=float32),
ShapeDtypeStruct(shape=(16,), dtype=float32)]

Methods:

Source code in src/furax/core/_axes.py
class RavelOperator(AbstractRavelOrReshapeOperator):
    """Operator that flattens pytree leaves: y = x.ravel().

    By default, all dimensions are flattened. Use ``first_axis`` and ``last_axis``
    to flatten only a subset of contiguous axes.

    This operator is orthogonal: its transpose restores the original shape.

    Attributes:
        first_axis: The first axis to flatten (default: 0).
        last_axis: The last axis to flatten (default: -1).

    Examples:
        To flatten the leaves of a pytree:

        >>> in_structure = jax.ShapeDtypeStruct((2, 3), jnp.float32)
        >>> op = RavelOperator(in_structure=in_structure)
        >>> op.out_structure
        ShapeDtypeStruct(shape=(6,), dtype=float32)

        To flatten the first two axes of the leaves of a pytree:

        >>> import furax as fx
        >>> x = [jnp.ones((2, 2)), jnp.ones((2, 2, 8))]
        >>> op = RavelOperator(0, 1, in_structure=fx.tree.as_structure(x))
        >>> op.out_structure
        [ShapeDtypeStruct(shape=(4,), dtype=float32),
        ShapeDtypeStruct(shape=(4, 8), dtype=float32)]


        To flatten the last two axes of the leaves of a pytree:

        >>> import furax as fx
        >>> x = [jnp.ones((2, 2, 3)), jnp.ones((2, 8))]
        >>> op = RavelOperator(-2, -1, in_structure=fx.tree.as_structure(x))
        >>> op.out_structure
        [ShapeDtypeStruct(shape=(2, 6), dtype=float32),
        ShapeDtypeStruct(shape=(16,), dtype=float32)]
    """

    first_axis: int = field(default=0, metadata={'static': True})
    last_axis: int = field(default=-1, metadata={'static': True})

    def __post_init__(self) -> None:
        first_axis = self.first_axis
        last_axis = self.last_axis
        in_structure = self.in_structure

        if 0 <= last_axis < first_axis or last_axis < first_axis < 0:
            raise ValueError(
                f'the first axis ({first_axis}) to be flattened should be before the last one '
                f'({last_axis}).'
            )

        if first_axis < 0 <= last_axis or last_axis < 0 <= first_axis:
            for leaf in jax.tree.leaves(in_structure):
                first = leaf.ndim + first_axis if first_axis < 0 else first_axis
                last = leaf.ndim + last_axis if last_axis < 0 else last_axis
                if first > last:
                    raise ValueError(
                        f'there are no dimensions between {first_axis} and {last_axis} '
                        f'to be flattened in leaf of shape {leaf.shape}.'
                    )

    def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
        def func(leaf: Inexact[Array, ' _a']) -> Inexact[Array, ' _b']:
            first_axis = leaf.ndim + self.first_axis if self.first_axis < 0 else self.first_axis
            last_axis = leaf.ndim + self.last_axis if self.last_axis < 0 else self.last_axis
            if first_axis > last_axis:
                assert False, 'unreachable'  # noqa: B011
            if first_axis == last_axis:
                return leaf
            new_shape = leaf.shape[:first_axis] + (-1,) + leaf.shape[last_axis + 1 :]
            return leaf.reshape(new_shape)

        return jax.tree.map(func, x)

first_axis = field(default=0, metadata={'static': True}) class-attribute instance-attribute

last_axis = field(default=(-1), metadata={'static': True}) class-attribute instance-attribute

__post_init__()

Source code in src/furax/core/_axes.py
def __post_init__(self) -> None:
    first_axis = self.first_axis
    last_axis = self.last_axis
    in_structure = self.in_structure

    if 0 <= last_axis < first_axis or last_axis < first_axis < 0:
        raise ValueError(
            f'the first axis ({first_axis}) to be flattened should be before the last one '
            f'({last_axis}).'
        )

    if first_axis < 0 <= last_axis or last_axis < 0 <= first_axis:
        for leaf in jax.tree.leaves(in_structure):
            first = leaf.ndim + first_axis if first_axis < 0 else first_axis
            last = leaf.ndim + last_axis if last_axis < 0 else last_axis
            if first > last:
                raise ValueError(
                    f'there are no dimensions between {first_axis} and {last_axis} '
                    f'to be flattened in leaf of shape {leaf.shape}.'
                )

mv(x)

Source code in src/furax/core/_axes.py
def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
    def func(leaf: Inexact[Array, ' _a']) -> Inexact[Array, ' _b']:
        first_axis = leaf.ndim + self.first_axis if self.first_axis < 0 else self.first_axis
        last_axis = leaf.ndim + self.last_axis if self.last_axis < 0 else self.last_axis
        if first_axis > last_axis:
            assert False, 'unreachable'  # noqa: B011
        if first_axis == last_axis:
            return leaf
        new_shape = leaf.shape[:first_axis] + (-1,) + leaf.shape[last_axis + 1 :]
        return leaf.reshape(new_shape)

    return jax.tree.map(func, x)

furax.core.ReshapeOperator dataclass

Bases: AbstractRavelOrReshapeOperator

Operator that reshapes pytree leaves: y = x.reshape(shape).

This operator is orthogonal: its transpose restores the original shape.

Attributes:

  • shape (tuple[int, ...]) –

    The new shape of the pytree leaves. Use -1 for one inferred dimension.

Methods:

Source code in src/furax/core/_axes.py
class ReshapeOperator(AbstractRavelOrReshapeOperator):
    """Operator that reshapes pytree leaves: y = x.reshape(shape).

    This operator is orthogonal: its transpose restores the original shape.

    Attributes:
        shape: The new shape of the pytree leaves. Use -1 for one inferred dimension.
    """

    shape: tuple[int, ...] = field(metadata={'static': True})

    def __post_init__(self) -> None:
        super().__post_init__()
        for leaf in jax.tree.leaves(self.in_structure):
            new_shape = self._normalize_shape(self.shape, leaf.shape)
            if leaf.size != prod(new_shape):
                raise ValueError(f'invalid new shape {self.shape} for leaf of shape {leaf.shape}.')

    @staticmethod
    def _normalize_shape(shape: tuple[int, ...], leaf_shape: tuple[int, ...]) -> tuple[int, ...]:
        if any(_ < -1 for _ in shape):
            raise ValueError(f'reshape new sizes should be all positive, got {shape}.')
        try:
            index = shape.index(-1)
        except ValueError:
            return shape

        before = shape[:index]
        after = shape[index + 1 :]
        if -1 in after:
            raise ValueError('can only specify one unknown dimension.')
        unknown_dimension = -prod(leaf_shape) / prod(shape)
        if unknown_dimension != int(unknown_dimension):
            raise ValueError(f'cannot reshape array of shape {leaf_shape} into shape {shape}.')
        return before + (int(unknown_dimension),) + after

    def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
        return jax.tree.map(lambda leaf: leaf.reshape(self.shape), x)

shape = field(metadata={'static': True}) class-attribute instance-attribute

__post_init__()

Source code in src/furax/core/_axes.py
def __post_init__(self) -> None:
    super().__post_init__()
    for leaf in jax.tree.leaves(self.in_structure):
        new_shape = self._normalize_shape(self.shape, leaf.shape)
        if leaf.size != prod(new_shape):
            raise ValueError(f'invalid new shape {self.shape} for leaf of shape {leaf.shape}.')

mv(x)

Source code in src/furax/core/_axes.py
def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
    return jax.tree.map(lambda leaf: leaf.reshape(self.shape), x)

furax.core.AbstractLazyInverseOperator dataclass

Bases: _AbstractLazyDualOperator

Methods:

Source code in src/furax/core/_base.py
class AbstractLazyInverseOperator(_AbstractLazyDualOperator):
    def __call__(
        self, x: PyTree[jax.ShapeDtypeStruct] | None = None, /, **keywords: Any
    ) -> AbstractLinearOperator | PyTree[jax.ShapeDtypeStruct]:
        if x is not None:
            if keywords:
                raise ValueError(
                    'The application of a vector to inverse operator cannot be parametrized. '
                    'For example, instead of A.I(x, throw=True), use A.I(throw=True)(x).'
                )
            return self.mv(x)
        return self

    def __matmul__(self, other: Any) -> AbstractLinearOperator:
        if self.operator is other:
            return IdentityOperator(in_structure=self.in_structure)
        return super().__matmul__(other)

    def inverse(self) -> AbstractLinearOperator:
        return self.operator

    def as_matrix(self) -> Inexact[Array, 'a b']:
        matrix: Array = jnp.linalg.inv(self.operator.as_matrix())
        return matrix

__call__(x=None, /, **keywords)

Source code in src/furax/core/_base.py
def __call__(
    self, x: PyTree[jax.ShapeDtypeStruct] | None = None, /, **keywords: Any
) -> AbstractLinearOperator | PyTree[jax.ShapeDtypeStruct]:
    if x is not None:
        if keywords:
            raise ValueError(
                'The application of a vector to inverse operator cannot be parametrized. '
                'For example, instead of A.I(x, throw=True), use A.I(throw=True)(x).'
            )
        return self.mv(x)
    return self

__matmul__(other)

Source code in src/furax/core/_base.py
def __matmul__(self, other: Any) -> AbstractLinearOperator:
    if self.operator is other:
        return IdentityOperator(in_structure=self.in_structure)
    return super().__matmul__(other)

inverse()

Source code in src/furax/core/_base.py
def inverse(self) -> AbstractLinearOperator:
    return self.operator

as_matrix()

Source code in src/furax/core/_base.py
def as_matrix(self) -> Inexact[Array, 'a b']:
    matrix: Array = jnp.linalg.inv(self.operator.as_matrix())
    return matrix

furax.core.AbstractLazyInverseOrthogonalOperator dataclass

Bases: TransposeOperator, AbstractLazyInverseOperator

Source code in src/furax/core/_base.py
@orthogonal
class AbstractLazyInverseOrthogonalOperator(TransposeOperator, AbstractLazyInverseOperator):
    pass

furax.core.AbstractLinearOperator dataclass

Bases: ABC

Base class for linear operators.

Methods:

Attributes:

Source code in src/furax/core/_base.py
@register_dataclass_with_keys
@dataclass(frozen=True)
@dataclass_transform(frozen_default=True, field_specifiers=(field,))
class AbstractLinearOperator(ABC):
    """Base class for linear operators."""

    # Class-level tags (set by decorators)
    class_tags: ClassVar[OperatorTag] = OperatorTag.NONE

    in_structure: PyTree[jax.ShapeDtypeStruct] = field(
        kw_only=True, metadata={'static': True}, default=None
    )

    def __init_subclass__(cls, **kwargs: Any) -> None:
        dataclass(frozen=True)(cls)
        register_dataclass_with_keys(cls)

    def __post_init__(self) -> None:
        if self.in_structure is None:
            raise ValueError('The input structure of the operator is not defined.')

    @property
    def tags(self) -> OperatorTag:
        """Get the tags for this operator instance."""
        return self.class_tags

    # Operator properties with default False values
    @property
    def is_square(self) -> bool:
        return bool(self.tags & OperatorTag.SQUARE)

    @property
    def is_symmetric(self) -> bool:
        return bool(self.tags & OperatorTag.SYMMETRIC)

    @property
    def is_orthogonal(self) -> bool:
        return bool(self.tags & OperatorTag.ORTHOGONAL)

    @property
    def is_diagonal(self) -> bool:
        return bool(self.tags & OperatorTag.DIAGONAL)

    @property
    def is_tridiagonal(self) -> bool:
        return bool(self.tags & OperatorTag.TRIDIAGONAL)

    @property
    def is_lower_triangular(self) -> bool:
        return bool(self.tags & OperatorTag.LOWER_TRIANGULAR)

    @property
    def is_upper_triangular(self) -> bool:
        return bool(self.tags & OperatorTag.UPPER_TRIANGULAR)

    @property
    def is_positive_semidefinite(self) -> bool:
        return bool(self.tags & OperatorTag.POSITIVE_SEMIDEFINITE)

    @property
    def is_negative_semidefinite(self) -> bool:
        return bool(self.tags & OperatorTag.NEGATIVE_SEMIDEFINITE)

    @property
    def is_idempotent(self) -> bool:
        return bool(self.tags & OperatorTag.IDEMPOTENT)

    @overload
    def __call__(
        self, *, solver: lx.AbstractLinearSolver, **keywords: Any
    ) -> 'AbstractLinearOperator': ...

    @overload
    def __call__(self, x: PyTree[jax.ShapeDtypeStruct]) -> PyTree[jax.ShapeDtypeStruct]: ...

    def __call__(
        self, x: PyTree[jax.ShapeDtypeStruct] | None = None, **keywords: Any
    ) -> 'AbstractLinearOperator | PyTree[jax.ShapeDtypeStruct]':
        if keywords:
            raise TypeError('No keywords is allowed in AbstractLinearOperator __call__ method')
        if isinstance(x, AbstractLinearOperator):
            raise ValueError("Use '@' to compose operators")
        return self.mv(x)

    def __matmul__(self, other: Any) -> 'AbstractLinearOperator':
        if not isinstance(other, AbstractLinearOperator):
            return NotImplemented
        if not structure_equal(self.in_structure, other.out_structure):
            msg = (
                f'Incompatible linear operator structures: '
                f'self.in_structure={self.in_structure}, '
                f'other.out_structure={other.out_structure}'
            )
            raise ValueError(msg)
        if isinstance(other, CompositionOperator):
            return NotImplemented
        if isinstance(other, AbstractLazyInverseOperator):
            if other.operator is self:
                return IdentityOperator(in_structure=self.in_structure)

        return CompositionOperator([self, other])

    def __add__(self, other: Any) -> 'AbstractLinearOperator':
        if not isinstance(other, AbstractLinearOperator):
            return NotImplemented
        if not structure_equal(self.in_structure, other.in_structure):
            raise ValueError('Incompatible linear operator input structures')
        if not structure_equal(self.out_structure, other.out_structure):
            raise ValueError('Incompatible linear operator output structures')
        if isinstance(other, AdditionOperator):
            return NotImplemented

        return AdditionOperator([self, other])

    def __sub__(self, other: Any) -> 'AbstractLinearOperator':
        if not isinstance(other, AbstractLinearOperator):
            return NotImplemented
        if not structure_equal(self.in_structure, other.in_structure):
            raise ValueError('Incompatible linear operator input structures')
        if not structure_equal(self.out_structure, other.out_structure):
            raise ValueError('Incompatible linear operator output structures')

        result: AbstractLinearOperator = self + (-other)
        return result

    def __mul__(self, other: ScalarLike) -> 'AbstractLinearOperator':
        result = other * self
        assert isinstance(result, AbstractLinearOperator)  # mypy
        return result

    def __rmul__(self, other: ScalarLike) -> 'AbstractLinearOperator':
        other = jnp.asarray(other)
        if other.shape != ():
            raise ValueError('Can only multiply AbstractLinearOperators by scalars.')
        return HomothetyOperator(other, in_structure=self.out_structure) @ self

    def __truediv__(self, other: ScalarLike) -> 'AbstractLinearOperator':
        other = jnp.asarray(other)
        if other.shape != ():
            raise ValueError('Can only divide AbstractLinearOperators by scalars.')
        return HomothetyOperator(1 / other, in_structure=self.out_structure) @ self

    def __pos__(self) -> 'AbstractLinearOperator':
        return self

    def __neg__(self) -> 'AbstractLinearOperator':
        return (-1) * self

    @abstractmethod
    def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]: ...

    def reduce(self) -> 'AbstractLinearOperator':
        """Returns a linear operator with a reduced structure."""
        return self

    def as_matrix(self) -> Inexact[Array, 'a b']:
        """Returns the operator as a dense matrix.

        Input and output PyTrees are flattened and concatenated.
        """
        in_pytree = zeros_like(self.in_structure)
        in_leaves_ref, in_treedef = jax.tree.flatten(in_pytree)

        matrix = jnp.empty((self.out_size, self.in_size), dtype=self.out_promoted_dtype)
        jcounter = 0

        for ileaf, leaf in enumerate(in_leaves_ref):

            def body(index, carry):  # type: ignore[no-untyped-def]
                matrix, jcounter = carry
                zeros = in_leaves_ref.copy()
                zeros[ileaf] = leaf.ravel().at[index].set(1).reshape(leaf.shape)  # noqa: B023 (`body` consumed immediately, no deferred call)
                in_pytree = jax.tree.unflatten(in_treedef, zeros)
                out_pytree = self.mv(in_pytree)
                out_leaves = [leaf.ravel() for leaf in jax.tree.leaves(out_pytree)]
                matrix = matrix.at[:, jcounter].set(jnp.concatenate(out_leaves))
                jcounter += 1
                return matrix, jcounter

            matrix, jcounter = jax.lax.fori_loop(0, leaf.size, body, (matrix, jcounter))

        return matrix

    def transpose(self) -> 'AbstractLinearOperator':
        return TransposeOperator(self)

    @property
    def T(self) -> 'AbstractLinearOperator':
        return self.transpose()

    def inverse(self) -> 'AbstractLinearOperator':
        return InverseOperator(self)

    @property
    def I(self) -> 'AbstractLinearOperator':  # noqa: E743
        return self.inverse()

    @property
    def out_structure(self) -> PyTree[jax.ShapeDtypeStruct]:
        return jax.eval_shape(self.mv, self.in_structure)

    @property
    def in_size(self) -> int:
        """The number of elements in the input PyTree."""
        return sum(_.size for _ in jax.tree.leaves(self.in_structure))

    @property
    def out_size(self) -> int:
        """The number of elements in the output PyTree."""
        return sum(_.size for _ in jax.tree.leaves(self.out_structure))

    @property
    def in_promoted_dtype(self) -> DType[Any]:
        """Returns the promoted data type of the operator's input leaves."""
        leaves = jax.tree.leaves(self.in_structure)
        return jnp.result_type(*leaves)

    @property
    def out_promoted_dtype(self) -> DType[Any]:
        """Returns the promoted data type of the operator's output leaves."""
        leaves = jax.tree.leaves(self.out_structure)
        return jnp.result_type(*leaves)

class_tags = OperatorTag.NONE class-attribute

in_structure = field(kw_only=True, metadata={'static': True}, default=None) class-attribute instance-attribute

tags property

Get the tags for this operator instance.

is_square property

is_symmetric property

is_orthogonal property

is_diagonal property

is_tridiagonal property

is_lower_triangular property

is_upper_triangular property

is_positive_semidefinite property

is_negative_semidefinite property

is_idempotent property

T property

I property

out_structure property

in_size property

The number of elements in the input PyTree.

out_size property

The number of elements in the output PyTree.

in_promoted_dtype property

Returns the promoted data type of the operator's input leaves.

out_promoted_dtype property

Returns the promoted data type of the operator's output leaves.

__init_subclass__(**kwargs)

Source code in src/furax/core/_base.py
def __init_subclass__(cls, **kwargs: Any) -> None:
    dataclass(frozen=True)(cls)
    register_dataclass_with_keys(cls)

__post_init__()

Source code in src/furax/core/_base.py
def __post_init__(self) -> None:
    if self.in_structure is None:
        raise ValueError('The input structure of the operator is not defined.')

__call__(x=None, **keywords)

__call__(*, solver: lx.AbstractLinearSolver, **keywords: Any) -> AbstractLinearOperator
__call__(x: PyTree[jax.ShapeDtypeStruct]) -> PyTree[jax.ShapeDtypeStruct]
Source code in src/furax/core/_base.py
def __call__(
    self, x: PyTree[jax.ShapeDtypeStruct] | None = None, **keywords: Any
) -> 'AbstractLinearOperator | PyTree[jax.ShapeDtypeStruct]':
    if keywords:
        raise TypeError('No keywords is allowed in AbstractLinearOperator __call__ method')
    if isinstance(x, AbstractLinearOperator):
        raise ValueError("Use '@' to compose operators")
    return self.mv(x)

__matmul__(other)

Source code in src/furax/core/_base.py
def __matmul__(self, other: Any) -> 'AbstractLinearOperator':
    if not isinstance(other, AbstractLinearOperator):
        return NotImplemented
    if not structure_equal(self.in_structure, other.out_structure):
        msg = (
            f'Incompatible linear operator structures: '
            f'self.in_structure={self.in_structure}, '
            f'other.out_structure={other.out_structure}'
        )
        raise ValueError(msg)
    if isinstance(other, CompositionOperator):
        return NotImplemented
    if isinstance(other, AbstractLazyInverseOperator):
        if other.operator is self:
            return IdentityOperator(in_structure=self.in_structure)

    return CompositionOperator([self, other])

__add__(other)

Source code in src/furax/core/_base.py
def __add__(self, other: Any) -> 'AbstractLinearOperator':
    if not isinstance(other, AbstractLinearOperator):
        return NotImplemented
    if not structure_equal(self.in_structure, other.in_structure):
        raise ValueError('Incompatible linear operator input structures')
    if not structure_equal(self.out_structure, other.out_structure):
        raise ValueError('Incompatible linear operator output structures')
    if isinstance(other, AdditionOperator):
        return NotImplemented

    return AdditionOperator([self, other])

__sub__(other)

Source code in src/furax/core/_base.py
def __sub__(self, other: Any) -> 'AbstractLinearOperator':
    if not isinstance(other, AbstractLinearOperator):
        return NotImplemented
    if not structure_equal(self.in_structure, other.in_structure):
        raise ValueError('Incompatible linear operator input structures')
    if not structure_equal(self.out_structure, other.out_structure):
        raise ValueError('Incompatible linear operator output structures')

    result: AbstractLinearOperator = self + (-other)
    return result

__mul__(other)

Source code in src/furax/core/_base.py
def __mul__(self, other: ScalarLike) -> 'AbstractLinearOperator':
    result = other * self
    assert isinstance(result, AbstractLinearOperator)  # mypy
    return result

__rmul__(other)

Source code in src/furax/core/_base.py
def __rmul__(self, other: ScalarLike) -> 'AbstractLinearOperator':
    other = jnp.asarray(other)
    if other.shape != ():
        raise ValueError('Can only multiply AbstractLinearOperators by scalars.')
    return HomothetyOperator(other, in_structure=self.out_structure) @ self

__truediv__(other)

Source code in src/furax/core/_base.py
def __truediv__(self, other: ScalarLike) -> 'AbstractLinearOperator':
    other = jnp.asarray(other)
    if other.shape != ():
        raise ValueError('Can only divide AbstractLinearOperators by scalars.')
    return HomothetyOperator(1 / other, in_structure=self.out_structure) @ self

__pos__()

Source code in src/furax/core/_base.py
def __pos__(self) -> 'AbstractLinearOperator':
    return self

__neg__()

Source code in src/furax/core/_base.py
def __neg__(self) -> 'AbstractLinearOperator':
    return (-1) * self

mv(x) abstractmethod

Source code in src/furax/core/_base.py
@abstractmethod
def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]: ...

reduce()

Returns a linear operator with a reduced structure.

Source code in src/furax/core/_base.py
def reduce(self) -> 'AbstractLinearOperator':
    """Returns a linear operator with a reduced structure."""
    return self

as_matrix()

Returns the operator as a dense matrix.

Input and output PyTrees are flattened and concatenated.

Source code in src/furax/core/_base.py
def as_matrix(self) -> Inexact[Array, 'a b']:
    """Returns the operator as a dense matrix.

    Input and output PyTrees are flattened and concatenated.
    """
    in_pytree = zeros_like(self.in_structure)
    in_leaves_ref, in_treedef = jax.tree.flatten(in_pytree)

    matrix = jnp.empty((self.out_size, self.in_size), dtype=self.out_promoted_dtype)
    jcounter = 0

    for ileaf, leaf in enumerate(in_leaves_ref):

        def body(index, carry):  # type: ignore[no-untyped-def]
            matrix, jcounter = carry
            zeros = in_leaves_ref.copy()
            zeros[ileaf] = leaf.ravel().at[index].set(1).reshape(leaf.shape)  # noqa: B023 (`body` consumed immediately, no deferred call)
            in_pytree = jax.tree.unflatten(in_treedef, zeros)
            out_pytree = self.mv(in_pytree)
            out_leaves = [leaf.ravel() for leaf in jax.tree.leaves(out_pytree)]
            matrix = matrix.at[:, jcounter].set(jnp.concatenate(out_leaves))
            jcounter += 1
            return matrix, jcounter

        matrix, jcounter = jax.lax.fori_loop(0, leaf.size, body, (matrix, jcounter))

    return matrix

transpose()

Source code in src/furax/core/_base.py
def transpose(self) -> 'AbstractLinearOperator':
    return TransposeOperator(self)

inverse()

Source code in src/furax/core/_base.py
def inverse(self) -> 'AbstractLinearOperator':
    return InverseOperator(self)

__init__(*, in_structure=None)

furax.core.AdditionOperator

Bases: AbstractLinearOperator

An operator that adds two operators, as in C = A + B.

Methods:

Attributes:

Source code in src/furax/core/_base.py
class AdditionOperator(AbstractLinearOperator):
    """An operator that adds two operators, as in C = A + B."""

    operands: PyTree[AbstractLinearOperator]

    def __init__(self, operands: PyTree[AbstractLinearOperator]) -> None:
        object.__setattr__(self, 'operands', operands)
        super().__init__(in_structure=self.operand_leaves[0].in_structure)

    # Tag propagation properties
    @property
    def is_square(self) -> bool:
        return super().is_square or self.operand_leaves[0].is_square

    @property
    def is_symmetric(self) -> bool:
        return super().is_symmetric or all(op.is_symmetric for op in self.operand_leaves)

    @property
    def is_diagonal(self) -> bool:
        return super().is_diagonal or all(op.is_diagonal for op in self.operand_leaves)

    @property
    def is_positive_semidefinite(self) -> bool:
        return super().is_positive_semidefinite or all(
            op.is_positive_semidefinite for op in self.operand_leaves
        )

    @property
    def is_negative_semidefinite(self) -> bool:
        return super().is_negative_semidefinite or all(
            op.is_negative_semidefinite for op in self.operand_leaves
        )

    def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
        operands = self.operand_leaves
        y = operands[0](x)

        for operand in operands[1:]:
            y = jax.tree.map(jnp.add, y, operand(x))

        return y

    def transpose(self) -> AbstractLinearOperator:
        return AdditionOperator(self._tree_map(lambda operand: operand.T))

    def __add__(self, other: AbstractLinearOperator) -> 'AdditionOperator':
        if not isinstance(other, AbstractLinearOperator):
            return NotImplemented
        if not structure_equal(self.in_structure, other.in_structure):
            raise ValueError('Incompatible linear operator input structures')
        if not structure_equal(self.out_structure, other.out_structure):
            raise ValueError('Incompatible linear operator output structures')
        if isinstance(other, AdditionOperator):
            operands = other.operand_leaves
        else:
            operands = [other]
        return AdditionOperator(self.operand_leaves + operands)

    def __radd__(self, other: AbstractLinearOperator) -> 'AdditionOperator':
        if not isinstance(other, AbstractLinearOperator):
            return NotImplemented
        if not structure_equal(self.in_structure, other.in_structure):
            raise ValueError('Incompatible linear operator input structures')
        if not structure_equal(self.out_structure, other.out_structure):
            raise ValueError('Incompatible linear operator output structures')
        return AdditionOperator([other] + self.operand_leaves)

    def __neg__(self) -> 'AdditionOperator':
        return AdditionOperator(self._tree_map(lambda operand: (-1) * operand))

    @property
    def out_structure(self) -> PyTree[jax.ShapeDtypeStruct]:
        return self.operand_leaves[0].out_structure

    def as_matrix(self) -> Inexact[Array, 'a b']:
        return functools.reduce(jnp.add, (operand.as_matrix() for operand in self.operand_leaves))

    def reduce(self) -> AbstractLinearOperator:
        from .rules import AdditiveReductionRule

        operands = [operand.reduce() for operand in self.operand_leaves]
        operands = AdditiveReductionRule().apply(operands)
        if len(operands) == 1:
            return operands[0]
        return AdditionOperator(operands)

    @property
    def operand_leaves(self) -> list[AbstractLinearOperator]:
        """Returns the flat list of operators."""
        return jax.tree.leaves(
            self.operands, is_leaf=lambda x: isinstance(x, AbstractLinearOperator)
        )

    def _tree_map(self, f: Callable[..., Any], *args: Any) -> Any:
        return jax.tree.map(
            f,
            self.operands,
            *args,
            is_leaf=lambda x: isinstance(x, AbstractLinearOperator),
        )

operands instance-attribute

is_square property

is_symmetric property

is_diagonal property

is_positive_semidefinite property

is_negative_semidefinite property

out_structure property

operand_leaves property

Returns the flat list of operators.

__init__(operands)

Source code in src/furax/core/_base.py
def __init__(self, operands: PyTree[AbstractLinearOperator]) -> None:
    object.__setattr__(self, 'operands', operands)
    super().__init__(in_structure=self.operand_leaves[0].in_structure)

mv(x)

Source code in src/furax/core/_base.py
def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
    operands = self.operand_leaves
    y = operands[0](x)

    for operand in operands[1:]:
        y = jax.tree.map(jnp.add, y, operand(x))

    return y

transpose()

Source code in src/furax/core/_base.py
def transpose(self) -> AbstractLinearOperator:
    return AdditionOperator(self._tree_map(lambda operand: operand.T))

__add__(other)

Source code in src/furax/core/_base.py
def __add__(self, other: AbstractLinearOperator) -> 'AdditionOperator':
    if not isinstance(other, AbstractLinearOperator):
        return NotImplemented
    if not structure_equal(self.in_structure, other.in_structure):
        raise ValueError('Incompatible linear operator input structures')
    if not structure_equal(self.out_structure, other.out_structure):
        raise ValueError('Incompatible linear operator output structures')
    if isinstance(other, AdditionOperator):
        operands = other.operand_leaves
    else:
        operands = [other]
    return AdditionOperator(self.operand_leaves + operands)

__radd__(other)

Source code in src/furax/core/_base.py
def __radd__(self, other: AbstractLinearOperator) -> 'AdditionOperator':
    if not isinstance(other, AbstractLinearOperator):
        return NotImplemented
    if not structure_equal(self.in_structure, other.in_structure):
        raise ValueError('Incompatible linear operator input structures')
    if not structure_equal(self.out_structure, other.out_structure):
        raise ValueError('Incompatible linear operator output structures')
    return AdditionOperator([other] + self.operand_leaves)

__neg__()

Source code in src/furax/core/_base.py
def __neg__(self) -> 'AdditionOperator':
    return AdditionOperator(self._tree_map(lambda operand: (-1) * operand))

as_matrix()

Source code in src/furax/core/_base.py
def as_matrix(self) -> Inexact[Array, 'a b']:
    return functools.reduce(jnp.add, (operand.as_matrix() for operand in self.operand_leaves))

reduce()

Source code in src/furax/core/_base.py
def reduce(self) -> AbstractLinearOperator:
    from .rules import AdditiveReductionRule

    operands = [operand.reduce() for operand in self.operand_leaves]
    operands = AdditiveReductionRule().apply(operands)
    if len(operands) == 1:
        return operands[0]
    return AdditionOperator(operands)

furax.core.CompositionOperator

Bases: AbstractLinearOperator

An operator that composes two operators, as in C = B ∘ A.

Methods:

Attributes:

Source code in src/furax/core/_base.py
class CompositionOperator(AbstractLinearOperator):
    """An operator that composes two operators, as in C = B ∘ A."""

    operands: list[AbstractLinearOperator]

    def __init__(self, operands: list[AbstractLinearOperator]) -> None:
        object.__setattr__(self, 'operands', operands)
        super().__init__(in_structure=operands[-1].in_structure)

    # Tag propagation properties
    @property
    def is_square(self) -> bool:
        result: bool = super().is_square or structure_equal(
            self.operands[0].out_structure, self.operands[-1].in_structure
        )
        return result

    @property
    def is_diagonal(self) -> bool:
        return super().is_diagonal or all(op.is_diagonal for op in self.operands)

    def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
        for operand in reversed(self.operands):
            x = operand.mv(x)
        return x

    def transpose(self) -> AbstractLinearOperator:
        return CompositionOperator([_.T for _ in reversed(self.operands)])

    def __matmul__(self, other: AbstractLinearOperator) -> 'CompositionOperator':
        if not isinstance(other, AbstractLinearOperator):
            return NotImplemented
        if not structure_equal(self.in_structure, other.out_structure):
            msg = (
                f'Incompatible linear operator structures: '
                f'self.in_structure={self.in_structure}, '
                f'other.out_structure={other.out_structure}'
            )
            raise ValueError(msg)
        if isinstance(other, CompositionOperator):
            operands = other.operands
        else:
            operands = [other]
        return CompositionOperator(self.operands + operands)

    def __rmatmul__(self, other: AbstractLinearOperator) -> 'CompositionOperator':
        if not isinstance(other, AbstractLinearOperator):
            return NotImplemented
        if not structure_equal(self.out_structure, other.in_structure):
            msg = (
                f'Incompatible linear operator structures: '
                f'self.out_structure={self.out_structure}, '
                f'other.in_structure={other.in_structure}'
            )
            raise ValueError(msg)
        return CompositionOperator([other] + self.operands)

    def reduce(self) -> AbstractLinearOperator:
        """Returns a linear operator with a reduced structure."""
        from .rules import AlgebraicReductionRule

        operands = AlgebraicReductionRule().apply([operand.reduce() for operand in self.operands])
        if len(operands) == 0:
            return IdentityOperator(in_structure=self.in_structure)
        if len(operands) == 1:
            return operands[0]
        return CompositionOperator(operands)

    @property
    def out_structure(self) -> PyTree[jax.ShapeDtypeStruct]:
        return self.operands[0].out_structure

operands instance-attribute

is_square property

is_diagonal property

out_structure property

__init__(operands)

Source code in src/furax/core/_base.py
def __init__(self, operands: list[AbstractLinearOperator]) -> None:
    object.__setattr__(self, 'operands', operands)
    super().__init__(in_structure=operands[-1].in_structure)

mv(x)

Source code in src/furax/core/_base.py
def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
    for operand in reversed(self.operands):
        x = operand.mv(x)
    return x

transpose()

Source code in src/furax/core/_base.py
def transpose(self) -> AbstractLinearOperator:
    return CompositionOperator([_.T for _ in reversed(self.operands)])

__matmul__(other)

Source code in src/furax/core/_base.py
def __matmul__(self, other: AbstractLinearOperator) -> 'CompositionOperator':
    if not isinstance(other, AbstractLinearOperator):
        return NotImplemented
    if not structure_equal(self.in_structure, other.out_structure):
        msg = (
            f'Incompatible linear operator structures: '
            f'self.in_structure={self.in_structure}, '
            f'other.out_structure={other.out_structure}'
        )
        raise ValueError(msg)
    if isinstance(other, CompositionOperator):
        operands = other.operands
    else:
        operands = [other]
    return CompositionOperator(self.operands + operands)

__rmatmul__(other)

Source code in src/furax/core/_base.py
def __rmatmul__(self, other: AbstractLinearOperator) -> 'CompositionOperator':
    if not isinstance(other, AbstractLinearOperator):
        return NotImplemented
    if not structure_equal(self.out_structure, other.in_structure):
        msg = (
            f'Incompatible linear operator structures: '
            f'self.out_structure={self.out_structure}, '
            f'other.in_structure={other.in_structure}'
        )
        raise ValueError(msg)
    return CompositionOperator([other] + self.operands)

reduce()

Returns a linear operator with a reduced structure.

Source code in src/furax/core/_base.py
def reduce(self) -> AbstractLinearOperator:
    """Returns a linear operator with a reduced structure."""
    from .rules import AlgebraicReductionRule

    operands = AlgebraicReductionRule().apply([operand.reduce() for operand in self.operands])
    if len(operands) == 0:
        return IdentityOperator(in_structure=self.in_structure)
    if len(operands) == 1:
        return operands[0]
    return CompositionOperator(operands)

furax.core.HomothetyOperator dataclass

Bases: AbstractLinearOperator

Operator that multiplies its input by a scalar: H(x) = k * x.

The homothety operator is diagonal, symmetric and positive semi-definite (for positive values). Two consecutive homotheties compose by multiplying their scalars: H(k1) @ H(k2) = H(k1 * k2).

Attributes:

Examples:

>>> H = HomothetyOperator(2.0, in_structure=jax.ShapeDtypeStruct((3,), jnp.float32))
>>> x = jnp.array([1.0, 2.0, 3.0])
>>> H(x)
Array([2., 4., 6.], dtype=float32)
>>> H.I(x)  # Inverse: divides by 2
Array([0.5, 1. , 1.5], dtype=float32)

Methods:

Source code in src/furax/core/_base.py
@diagonal
class HomothetyOperator(AbstractLinearOperator):
    """Operator that multiplies its input by a scalar: H(x) = k * x.

    The homothety operator is diagonal, symmetric and positive semi-definite
    (for positive values). Two consecutive homotheties compose by multiplying
    their scalars: H(k1) @ H(k2) = H(k1 * k2).

    Attributes:
        value: The scalar multiplier.

    Examples:
        >>> H = HomothetyOperator(2.0, in_structure=jax.ShapeDtypeStruct((3,), jnp.float32))
        >>> x = jnp.array([1.0, 2.0, 3.0])
        >>> H(x)
        Array([2., 4., 6.], dtype=float32)
        >>> H.I(x)  # Inverse: divides by 2
        Array([0.5, 1. , 1.5], dtype=float32)
    """

    value: Scalar | int | float

    def __matmul__(self, other: Any) -> AbstractLinearOperator:
        if isinstance(other, HomothetyOperator):
            return HomothetyOperator(self.value * other.value, in_structure=self.in_structure)
        return super().__matmul__(other)

    def mv(self, x: PyTree[Inexact[Array, '...']]) -> PyTree[Inexact[Array, '...']]:
        return jax.tree.map(lambda leaf: self.value * leaf, x)

    def inverse(self) -> AbstractLinearOperator:
        return HomothetyOperator(1 / self.value, in_structure=self.in_structure)

    def as_matrix(self) -> Inexact[Array, 'a b']:
        return self.value * jnp.identity(self.in_size, dtype=self.out_promoted_dtype)

value instance-attribute

__matmul__(other)

Source code in src/furax/core/_base.py
def __matmul__(self, other: Any) -> AbstractLinearOperator:
    if isinstance(other, HomothetyOperator):
        return HomothetyOperator(self.value * other.value, in_structure=self.in_structure)
    return super().__matmul__(other)

mv(x)

Source code in src/furax/core/_base.py
def mv(self, x: PyTree[Inexact[Array, '...']]) -> PyTree[Inexact[Array, '...']]:
    return jax.tree.map(lambda leaf: self.value * leaf, x)

inverse()

Source code in src/furax/core/_base.py
def inverse(self) -> AbstractLinearOperator:
    return HomothetyOperator(1 / self.value, in_structure=self.in_structure)

as_matrix()

Source code in src/furax/core/_base.py
def as_matrix(self) -> Inexact[Array, 'a b']:
    return self.value * jnp.identity(self.in_size, dtype=self.out_promoted_dtype)

furax.core.IdentityOperator dataclass

Bases: AbstractLinearOperator

Operator that returns its input unchanged: I(x) = x.

The identity operator is diagonal, orthogonal and positive semi-definite. Its transpose and inverse are itself.

Examples:

>>> I = IdentityOperator(in_structure=jax.ShapeDtypeStruct((3,), jnp.float32))
>>> x = jnp.array([1.0, 2.0, 3.0])
>>> I(x)
Array([1., 2., 3.], dtype=float32)

Methods:

Source code in src/furax/core/_base.py
@orthogonal
@diagonal
@positive_semidefinite
@idempotent
class IdentityOperator(AbstractLinearOperator):
    """Operator that returns its input unchanged: I(x) = x.

    The identity operator is diagonal, orthogonal and positive semi-definite.
    Its transpose and inverse are itself.

    Examples:
        >>> I = IdentityOperator(in_structure=jax.ShapeDtypeStruct((3,), jnp.float32))
        >>> x = jnp.array([1.0, 2.0, 3.0])
        >>> I(x)
        Array([1., 2., 3.], dtype=float32)
    """

    def __matmul__(self, other: Any) -> AbstractLinearOperator:
        if not isinstance(other, AbstractLinearOperator):
            return NotImplemented
        return other

    def mv(self, x: PyTree[Inexact[Array, '...']]) -> PyTree[Inexact[Array, '...']]:
        return x

    def as_matrix(self) -> Inexact[Array, 'a b']:
        return jnp.identity(self.in_size, dtype=self.in_promoted_dtype)

__matmul__(other)

Source code in src/furax/core/_base.py
def __matmul__(self, other: Any) -> AbstractLinearOperator:
    if not isinstance(other, AbstractLinearOperator):
        return NotImplemented
    return other

mv(x)

Source code in src/furax/core/_base.py
def mv(self, x: PyTree[Inexact[Array, '...']]) -> PyTree[Inexact[Array, '...']]:
    return x

as_matrix()

Source code in src/furax/core/_base.py
def as_matrix(self) -> Inexact[Array, 'a b']:
    return jnp.identity(self.in_size, dtype=self.in_promoted_dtype)

furax.core.InverseOperator dataclass

Bases: AbstractLazyInverseOperator

Methods:

Attributes:

Source code in src/furax/core/_base.py
class InverseOperator(AbstractLazyInverseOperator):
    config: ConfigState = field(
        kw_only=True, metadata={'static': True}, default_factory=lambda: Config.instance()
    )

    def __post_init__(self) -> None:
        super().__post_init__()
        if not structure_equal(self.operator.in_structure, self.operator.out_structure):
            raise ValueError('Only square operators can be inverted.')
        object.__setattr__(self, 'operator', self.operator.reduce())

    def __call__(
        self,
        x: PyTree[jax.ShapeDtypeStruct] | None = None,
        /,
        *,
        solver: lx.AbstractLinearSolver | None = None,
        throw: bool | None = None,
        callback: Callable[[lx.Solution], None] | object = MISSING,
        **options: Any,
    ) -> AbstractLinearOperator | PyTree[jax.ShapeDtypeStruct]:
        config_options = {}
        if solver is not None:
            config_options['solver'] = solver
        if throw is not None:
            config_options['solver_throw'] = throw
        if callback is not MISSING:
            config_options['solver_callback'] = callback
        if options:
            if 'solver_options' in options:
                msg = 'pass solver_options (preconditioner, etc.) directly as keyword arguments'
                raise ValueError(msg)
            config_options['solver_options'] = options
        if x is None and config_options:
            with Config(**config_options):
                return InverseOperator(self.operator)
        return super().__call__(x, **config_options)

    def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
        from furax.interfaces.lineax import as_lineax_operator

        solver = self.config.solver
        throw = self.config.solver_throw
        options = self.config.solver_options.copy()
        A = as_lineax_operator(self.operator, OperatorTag.POSITIVE_SEMIDEFINITE)
        if preconditioner := options.get('preconditioner'):
            if not isinstance(preconditioner, AbstractLinearOperator):
                raise TypeError('The preconditioner must be an instance of AbstractLinearOperator.')
            options['preconditioner'] = as_lineax_operator(
                preconditioner, OperatorTag.POSITIVE_SEMIDEFINITE
            )
        solution = lx.linear_solve(A, x, solver=solver, throw=throw, options=options)
        jax.debug.callback(self.config.solver_callback, solution)
        return solution.value

config = field(kw_only=True, metadata={'static': True}, default_factory=(lambda: Config.instance())) class-attribute instance-attribute

__post_init__()

Source code in src/furax/core/_base.py
def __post_init__(self) -> None:
    super().__post_init__()
    if not structure_equal(self.operator.in_structure, self.operator.out_structure):
        raise ValueError('Only square operators can be inverted.')
    object.__setattr__(self, 'operator', self.operator.reduce())

__call__(x=None, /, *, solver=None, throw=None, callback=MISSING, **options)

Source code in src/furax/core/_base.py
def __call__(
    self,
    x: PyTree[jax.ShapeDtypeStruct] | None = None,
    /,
    *,
    solver: lx.AbstractLinearSolver | None = None,
    throw: bool | None = None,
    callback: Callable[[lx.Solution], None] | object = MISSING,
    **options: Any,
) -> AbstractLinearOperator | PyTree[jax.ShapeDtypeStruct]:
    config_options = {}
    if solver is not None:
        config_options['solver'] = solver
    if throw is not None:
        config_options['solver_throw'] = throw
    if callback is not MISSING:
        config_options['solver_callback'] = callback
    if options:
        if 'solver_options' in options:
            msg = 'pass solver_options (preconditioner, etc.) directly as keyword arguments'
            raise ValueError(msg)
        config_options['solver_options'] = options
    if x is None and config_options:
        with Config(**config_options):
            return InverseOperator(self.operator)
    return super().__call__(x, **config_options)

mv(x)

Source code in src/furax/core/_base.py
def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
    from furax.interfaces.lineax import as_lineax_operator

    solver = self.config.solver
    throw = self.config.solver_throw
    options = self.config.solver_options.copy()
    A = as_lineax_operator(self.operator, OperatorTag.POSITIVE_SEMIDEFINITE)
    if preconditioner := options.get('preconditioner'):
        if not isinstance(preconditioner, AbstractLinearOperator):
            raise TypeError('The preconditioner must be an instance of AbstractLinearOperator.')
        options['preconditioner'] = as_lineax_operator(
            preconditioner, OperatorTag.POSITIVE_SEMIDEFINITE
        )
    solution = lx.linear_solve(A, x, solver=solver, throw=throw, options=options)
    jax.debug.callback(self.config.solver_callback, solution)
    return solution.value

furax.core.OperatorTag

Bases: IntFlag

Flags representing properties of linear operators.

Attributes:

Source code in src/furax/core/_base.py
class OperatorTag(IntFlag):
    """Flags representing properties of linear operators."""

    NONE = 0
    SQUARE = auto()
    SYMMETRIC = auto()
    ORTHOGONAL = auto()
    DIAGONAL = auto()
    TRIDIAGONAL = auto()
    LOWER_TRIANGULAR = auto()
    UPPER_TRIANGULAR = auto()
    POSITIVE_SEMIDEFINITE = auto()
    NEGATIVE_SEMIDEFINITE = auto()
    IDEMPOTENT = auto()

NONE = 0 class-attribute instance-attribute

SQUARE = auto() class-attribute instance-attribute

SYMMETRIC = auto() class-attribute instance-attribute

ORTHOGONAL = auto() class-attribute instance-attribute

DIAGONAL = auto() class-attribute instance-attribute

TRIDIAGONAL = auto() class-attribute instance-attribute

LOWER_TRIANGULAR = auto() class-attribute instance-attribute

UPPER_TRIANGULAR = auto() class-attribute instance-attribute

POSITIVE_SEMIDEFINITE = auto() class-attribute instance-attribute

NEGATIVE_SEMIDEFINITE = auto() class-attribute instance-attribute

IDEMPOTENT = auto() class-attribute instance-attribute

furax.core.TransposeOperator dataclass

Bases: _AbstractLazyDualOperator

Methods:

Source code in src/furax/core/_base.py
class TransposeOperator(_AbstractLazyDualOperator):
    def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
        transpose = jax.linear_transpose(self.operator.mv, self.operator.in_structure)
        return transpose(x)[0]

    def transpose(self) -> AbstractLinearOperator:
        return self.operator

mv(x)

Source code in src/furax/core/_base.py
def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
    transpose = jax.linear_transpose(self.operator.mv, self.operator.in_structure)
    return transpose(x)[0]

transpose()

Source code in src/furax/core/_base.py
def transpose(self) -> AbstractLinearOperator:
    return self.operator

furax.core.BlockColumnOperator

Bases: AbstractBlockOperator

Operator that vertically stacks block operators: [A; B; C].

Applies each block to the same input and returns a pytree of outputs. All blocks must have the same input structure.

Transpose: BlockColumnOperator.T = BlockRowOperator

Attributes:

Examples:

>>> x = jnp.array([1, 2], jnp.float32)
>>> I = IdentityOperator(in_structure=jax.ShapeDtypeStruct((2,), jnp.float32))
>>> op_list = BlockColumnOperator([I, I, I])
>>> op_list.as_matrix()
Array([[1., 0.],
       [0., 1.],
       [1., 0.],
       [0., 1.],
       [1., 0.],
       [0., 1.]], dtype=float32)
>>> op_list(x)
[Array([1., 2.], dtype=float32),
 Array([1., 2.], dtype=float32),
 Array([1., 2.], dtype=float32)]
>>> op_dict = BlockColumnOperator({'a': I, 'b': I, 'c': I})
>>> op_dict(x)
{'a': Array([1., 2.], dtype=float32),
 'b': Array([1., 2.], dtype=float32),
 'c': Array([1., 2.], dtype=float32)}

Methods:

Source code in src/furax/core/_blocks.py
class BlockColumnOperator(AbstractBlockOperator):
    """Operator that vertically stacks block operators: [A; B; C].

    Applies each block to the same input and returns a pytree of outputs.
    All blocks must have the same input structure.

    Transpose: BlockColumnOperator.T = BlockRowOperator

    Attributes:
        blocks: A pytree of operators (all with identical input structure).

    Examples:
        >>> x = jnp.array([1, 2], jnp.float32)
        >>> I = IdentityOperator(in_structure=jax.ShapeDtypeStruct((2,), jnp.float32))
        >>> op_list = BlockColumnOperator([I, I, I])
        >>> op_list.as_matrix()
        Array([[1., 0.],
               [0., 1.],
               [1., 0.],
               [0., 1.],
               [1., 0.],
               [0., 1.]], dtype=float32)
        >>> op_list(x)
        [Array([1., 2.], dtype=float32),
         Array([1., 2.], dtype=float32),
         Array([1., 2.], dtype=float32)]

        >>> op_dict = BlockColumnOperator({'a': I, 'b': I, 'c': I})
        >>> op_dict(x)
        {'a': Array([1., 2.], dtype=float32),
         'b': Array([1., 2.], dtype=float32),
         'c': Array([1., 2.], dtype=float32)}
    """

    def __init__(self, blocks: PyTree[AbstractLinearOperator]) -> None:
        super().__init__(blocks)
        try:
            operators = self.block_leaves
            ref_structure = operators[0].in_structure
        except (AttributeError, TypeError):
            # During JAX/equinox tree operations, operators may have boolean placeholders
            return
        object.__setattr__(self, 'in_structure', ref_structure)
        invalid_structures = [
            structure
            for operator in operators[1:]
            if not structure_equal((structure := operator.in_structure), ref_structure)
        ]
        if len(invalid_structures) > 0:
            structures_as_str = '\n - '.join(str(structure) for structure in invalid_structures)
            raise ValueError(
                f'The operators in a BlockColumnOperator must have the same input structure:\n'
                f' - {ref_structure}\n'
                f' - {structures_as_str}'
            )

    def mv(self, vector: PyTree[Inexact[Array, ' _b']]) -> PyTree[Inexact[Array, ' _a']]:
        return self._tree_map(lambda op: op.mv(vector))

    def transpose(self) -> AbstractLinearOperator:
        return BlockRowOperator(self._tree_map(lambda op: op.T))

    def as_matrix(self) -> Inexact[Array, 'a b']:
        return jnp.vstack([op.as_matrix() for op in self.block_leaves])

__init__(blocks)

Source code in src/furax/core/_blocks.py
def __init__(self, blocks: PyTree[AbstractLinearOperator]) -> None:
    super().__init__(blocks)
    try:
        operators = self.block_leaves
        ref_structure = operators[0].in_structure
    except (AttributeError, TypeError):
        # During JAX/equinox tree operations, operators may have boolean placeholders
        return
    object.__setattr__(self, 'in_structure', ref_structure)
    invalid_structures = [
        structure
        for operator in operators[1:]
        if not structure_equal((structure := operator.in_structure), ref_structure)
    ]
    if len(invalid_structures) > 0:
        structures_as_str = '\n - '.join(str(structure) for structure in invalid_structures)
        raise ValueError(
            f'The operators in a BlockColumnOperator must have the same input structure:\n'
            f' - {ref_structure}\n'
            f' - {structures_as_str}'
        )

mv(vector)

Source code in src/furax/core/_blocks.py
def mv(self, vector: PyTree[Inexact[Array, ' _b']]) -> PyTree[Inexact[Array, ' _a']]:
    return self._tree_map(lambda op: op.mv(vector))

transpose()

Source code in src/furax/core/_blocks.py
def transpose(self) -> AbstractLinearOperator:
    return BlockRowOperator(self._tree_map(lambda op: op.T))

as_matrix()

Source code in src/furax/core/_blocks.py
def as_matrix(self) -> Inexact[Array, 'a b']:
    return jnp.vstack([op.as_matrix() for op in self.block_leaves])

furax.core.BlockDiagonalOperator

Bases: AbstractBlockOperator

Operator with independent diagonal blocks: diag(A, B, C).

Applies each block independently to the corresponding part of a pytree input. No constraints on block input/output structures.

The inverse is the block diagonal of individual inverses (if all blocks are square).

Attributes:

Examples:

>>> x = jnp.array([1, 2], jnp.float32)
>>> H = DenseBlockDiagonalOperator(
...     jnp.array([[0, 1], [1, 0]]),
...     jax.ShapeDtypeStruct((2,), jnp.float32)
... )
>>> H.as_matrix()
Array([[0., 1.],
       [1., 0.]], dtype=float32)
>>> op_list = BlockDiagonalOperator([H, 2*H, 3*H])
>>> op_list.as_matrix()
Array([[0., 1., 0., 0., 0., 0.],
       [1., 0., 0., 0., 0., 0.],
       [0., 0., 0., 2., 0., 0.],
       [0., 0., 2., 0., 0., 0.],
       [0., 0., 0., 0., 0., 3.],
       [0., 0., 0., 0., 3., 0.]], dtype=float32)
>>> op_list([x, x, x])
[Array([2., 1.], dtype=float32),
 Array([4., 2.], dtype=float32),
 Array([6., 3.], dtype=float32)]
>>> op_dict = BlockDiagonalOperator({'a': H, 'b': 2*H, 'c': 3*H})
>>> op_dict({'a': x, 'b': x, 'c': x})
{'a': Array([2., 1.], dtype=float32),
 'b': Array([4., 2.], dtype=float32),
 'c': Array([6., 3.], dtype=float32)}

Methods:

Source code in src/furax/core/_blocks.py
class BlockDiagonalOperator(AbstractBlockOperator):
    """Operator with independent diagonal blocks: diag(A, B, C).

    Applies each block independently to the corresponding part of a pytree input.
    No constraints on block input/output structures.

    The inverse is the block diagonal of individual inverses (if all blocks are square).

    Attributes:
        blocks: A pytree of operators.

    Examples:
        >>> x = jnp.array([1, 2], jnp.float32)
        >>> H = DenseBlockDiagonalOperator(
        ...     jnp.array([[0, 1], [1, 0]]),
        ...     jax.ShapeDtypeStruct((2,), jnp.float32)
        ... )
        >>> H.as_matrix()
        Array([[0., 1.],
               [1., 0.]], dtype=float32)
        >>> op_list = BlockDiagonalOperator([H, 2*H, 3*H])
        >>> op_list.as_matrix()
        Array([[0., 1., 0., 0., 0., 0.],
               [1., 0., 0., 0., 0., 0.],
               [0., 0., 0., 2., 0., 0.],
               [0., 0., 2., 0., 0., 0.],
               [0., 0., 0., 0., 0., 3.],
               [0., 0., 0., 0., 3., 0.]], dtype=float32)
        >>> op_list([x, x, x])
        [Array([2., 1.], dtype=float32),
         Array([4., 2.], dtype=float32),
         Array([6., 3.], dtype=float32)]

        >>> op_dict = BlockDiagonalOperator({'a': H, 'b': 2*H, 'c': 3*H})
        >>> op_dict({'a': x, 'b': x, 'c': x})
        {'a': Array([2., 1.], dtype=float32),
         'b': Array([4., 2.], dtype=float32),
         'c': Array([6., 3.], dtype=float32)}
    """

    def __init__(self, blocks: PyTree[AbstractLinearOperator]) -> None:
        # required: otherwise, the parent constructor would not be called by the dataclass-generated constructor
        super().__init__(blocks)

    def mv(self, vector: PyTree[Inexact[Array, ' _b']]) -> PyTree[Inexact[Array, ' _a']]:
        return self._tree_map(lambda op, vect: op.mv(vect), vector)

    def transpose(self) -> AbstractLinearOperator:
        return BlockDiagonalOperator(self._tree_map(lambda op: op.T))

    def inverse(self) -> AbstractLinearOperator:
        # if some of the blocks are not square, let's defer to the default inverse method
        if not jax.tree.all(
            self._tree_map(lambda op: structure_equal(op.in_structure, op.out_structure))
        ):
            return super().inverse()
        return BlockDiagonalOperator(self._tree_map(lambda op: op.I))

    def as_matrix(self) -> Inexact[Array, 'a b']:
        return jsl.block_diag(*[op.as_matrix() for op in self.block_leaves])  # type: ignore[no-any-return]  # noqa: E501

    def reduce(self) -> AbstractLinearOperator:
        """BlockDiagonalOperator([I, I, ...]) -> I."""
        op = super().reduce()
        assert isinstance(op, BlockDiagonalOperator)
        if all(isinstance(block, IdentityOperator) for block in op.block_leaves):
            return IdentityOperator(in_structure=self.in_structure)
        return op

__init__(blocks)

Source code in src/furax/core/_blocks.py
def __init__(self, blocks: PyTree[AbstractLinearOperator]) -> None:
    # required: otherwise, the parent constructor would not be called by the dataclass-generated constructor
    super().__init__(blocks)

mv(vector)

Source code in src/furax/core/_blocks.py
def mv(self, vector: PyTree[Inexact[Array, ' _b']]) -> PyTree[Inexact[Array, ' _a']]:
    return self._tree_map(lambda op, vect: op.mv(vect), vector)

transpose()

Source code in src/furax/core/_blocks.py
def transpose(self) -> AbstractLinearOperator:
    return BlockDiagonalOperator(self._tree_map(lambda op: op.T))

inverse()

Source code in src/furax/core/_blocks.py
def inverse(self) -> AbstractLinearOperator:
    # if some of the blocks are not square, let's defer to the default inverse method
    if not jax.tree.all(
        self._tree_map(lambda op: structure_equal(op.in_structure, op.out_structure))
    ):
        return super().inverse()
    return BlockDiagonalOperator(self._tree_map(lambda op: op.I))

as_matrix()

Source code in src/furax/core/_blocks.py
def as_matrix(self) -> Inexact[Array, 'a b']:
    return jsl.block_diag(*[op.as_matrix() for op in self.block_leaves])  # type: ignore[no-any-return]  # noqa: E501

reduce()

BlockDiagonalOperator([I, I, ...]) -> I.

Source code in src/furax/core/_blocks.py
def reduce(self) -> AbstractLinearOperator:
    """BlockDiagonalOperator([I, I, ...]) -> I."""
    op = super().reduce()
    assert isinstance(op, BlockDiagonalOperator)
    if all(isinstance(block, IdentityOperator) for block in op.block_leaves):
        return IdentityOperator(in_structure=self.in_structure)
    return op

furax.core.BlockRowOperator

Bases: AbstractBlockOperator

Operator that horizontally concatenates block operators: [A | B | C].

Applies each block to the corresponding part of a pytree input and sums the results. All blocks must have the same output structure.

Transpose: BlockRowOperator.T = BlockColumnOperator

Attributes:

Examples:

>>> x = jnp.array([1, 2], jnp.float32)
>>> I = IdentityOperator(in_structure=jax.ShapeDtypeStruct((2,), jnp.float32))
>>> op_list = BlockRowOperator([I, 2*I, 3*I])
>>> op_list.as_matrix()
Array([[1., 0., 2., 0., 3., 0.],
       [0., 1., 0., 2., 0., 3.]], dtype=float32)
>>> op_list([x, x, x])
Array([ 6., 12.], dtype=float32)
>>> op_dict = BlockRowOperator({'a': I, 'b': 2*I, 'c': 3*I})
>>> op_dict({'a': x, 'b': x, 'c': x})
Array([ 6., 12.], dtype=float32)

Methods:

Source code in src/furax/core/_blocks.py
class BlockRowOperator(AbstractBlockOperator):
    """Operator that horizontally concatenates block operators: [A | B | C].

    Applies each block to the corresponding part of a pytree input and sums
    the results. All blocks must have the same output structure.

    Transpose: BlockRowOperator.T = BlockColumnOperator

    Attributes:
        blocks: A pytree of operators (all with identical output structure).

    Examples:
        >>> x = jnp.array([1, 2], jnp.float32)
        >>> I = IdentityOperator(in_structure=jax.ShapeDtypeStruct((2,), jnp.float32))
        >>> op_list = BlockRowOperator([I, 2*I, 3*I])
        >>> op_list.as_matrix()
        Array([[1., 0., 2., 0., 3., 0.],
               [0., 1., 0., 2., 0., 3.]], dtype=float32)
        >>> op_list([x, x, x])
        Array([ 6., 12.], dtype=float32)

        >>> op_dict = BlockRowOperator({'a': I, 'b': 2*I, 'c': 3*I})
        >>> op_dict({'a': x, 'b': x, 'c': x})
        Array([ 6., 12.], dtype=float32)
    """

    def __init__(self, blocks: PyTree[AbstractLinearOperator]) -> None:
        super().__init__(blocks)
        try:
            operators = self.block_leaves
            ref_structure = operators[0].out_structure
        except (AttributeError, TypeError):
            # During JAX/equinox tree operations, operators may have boolean placeholders
            return
        invalid_structures = [
            structure
            for operator in operators[1:]
            if not structure_equal((structure := operator.out_structure), ref_structure)
        ]
        if len(invalid_structures) > 0:
            structures_as_str = '\n - '.join(str(structure) for structure in invalid_structures)
            raise ValueError(
                f'The operators in a BlockRowOperator must have the same output structure:\n'
                f' - {ref_structure}\n'
                f' - {structures_as_str}'
            )

    def mv(self, x: PyTree[Inexact[Array, ' _b']]) -> PyTree[Inexact[Array, ' _a']]:
        treedef: PyTreeDef = jax.tree.structure(
            self.blocks, is_leaf=lambda op: isinstance(op, AbstractLinearOperator)
        )
        output_leaves = (
            block(leaf) for block, leaf in zip(self.block_leaves, treedef.flatten_up_to(x))
        )
        return functools.reduce(lambda a, b: add(a, b), output_leaves)

    def transpose(self) -> AbstractLinearOperator:
        return BlockColumnOperator(self._tree_map(lambda op: op.T))

    @property
    def out_structure(self) -> PyTree[jax.ShapeDtypeStruct]:
        return self.block_leaves[0].out_structure

    def as_matrix(self) -> Inexact[Array, 'a b']:
        return jnp.hstack([op.as_matrix() for op in self.block_leaves])

out_structure property

__init__(blocks)

Source code in src/furax/core/_blocks.py
def __init__(self, blocks: PyTree[AbstractLinearOperator]) -> None:
    super().__init__(blocks)
    try:
        operators = self.block_leaves
        ref_structure = operators[0].out_structure
    except (AttributeError, TypeError):
        # During JAX/equinox tree operations, operators may have boolean placeholders
        return
    invalid_structures = [
        structure
        for operator in operators[1:]
        if not structure_equal((structure := operator.out_structure), ref_structure)
    ]
    if len(invalid_structures) > 0:
        structures_as_str = '\n - '.join(str(structure) for structure in invalid_structures)
        raise ValueError(
            f'The operators in a BlockRowOperator must have the same output structure:\n'
            f' - {ref_structure}\n'
            f' - {structures_as_str}'
        )

mv(x)

Source code in src/furax/core/_blocks.py
def mv(self, x: PyTree[Inexact[Array, ' _b']]) -> PyTree[Inexact[Array, ' _a']]:
    treedef: PyTreeDef = jax.tree.structure(
        self.blocks, is_leaf=lambda op: isinstance(op, AbstractLinearOperator)
    )
    output_leaves = (
        block(leaf) for block, leaf in zip(self.block_leaves, treedef.flatten_up_to(x))
    )
    return functools.reduce(lambda a, b: add(a, b), output_leaves)

transpose()

Source code in src/furax/core/_blocks.py
def transpose(self) -> AbstractLinearOperator:
    return BlockColumnOperator(self._tree_map(lambda op: op.T))

as_matrix()

Source code in src/furax/core/_blocks.py
def as_matrix(self) -> Inexact[Array, 'a b']:
    return jnp.hstack([op.as_matrix() for op in self.block_leaves])

furax.core.DenseBlockDiagonalOperator dataclass

Bases: AbstractLinearOperator

Operator that applies block diagonal dense matrices via einsum.

Only the diagonal blocks are stored, making this more memory-efficient than a full dense matrix. The operation is defined by einsum subscripts.

Attributes:

  • blocks (Inexact[Array, ...]) –

    The dense blocks as an array (at least 2D).

  • subscripts (str) –

    Einsum subscripts defining the operation (default: 'ij...,j...->i...').

Examples:

For a matrix made of three 2x4 diagonal blocks, and input block columns of three blocks of four elements each, the operator can be written as:

>>> blocks = jnp.arange(24).reshape(3, 2, 4)
>>> op = DenseBlockDiagonalOperator(
...     blocks, jax.ShapeDtypeStruct((3, 4), jnp.int32), 'imn,in->im')
>>> op.as_matrix()
Array([[ 0,  1,  2,  3,  0,  0,  0,  0,  0,  0,  0,  0],
       [ 4,  5,  6,  7,  0,  0,  0,  0,  0,  0,  0,  0],
       [ 0,  0,  0,  0,  8,  9, 10, 11,  0,  0,  0,  0],
       [ 0,  0,  0,  0, 12, 13, 14, 15,  0,  0,  0,  0],
       [ 0,  0,  0,  0,  0,  0,  0,  0, 16, 17, 18, 19],
       [ 0,  0,  0,  0,  0,  0,  0,  0, 20, 21, 22, 23]], dtype=int32)

The axes along which the operator is block diagonal can be non-leading dimensions. As a matter of fact, by default, the diagonal axes are assumed to be "on the right". The notion of block diagonality should be understood in a tensor context. The representation of this operator as a 2d matrix, which relies on the row-major layout, may not be block diagonal.

>>> blocks = jnp.arange(24).reshape(3, 2, 4)
>>> op = DenseBlockDiagonalOperator(blocks, jax.ShapeDtypeStruct((2, 4), jnp.int32))
>>> op.as_matrix()
Array([[ 0,  0,  0,  0,  4,  0,  0,  0],
       [ 0,  1,  0,  0,  0,  5,  0,  0],
       [ 0,  0,  2,  0,  0,  0,  6,  0],
       [ 0,  0,  0,  3,  0,  0,  0,  7],
       [ 8,  0,  0,  0, 12,  0,  0,  0],
       [ 0,  9,  0,  0,  0, 13,  0,  0],
       [ 0,  0, 10,  0,  0,  0, 14,  0],
       [ 0,  0,  0, 11,  0,  0,  0, 15],
       [16,  0,  0,  0, 20,  0,  0,  0],
       [ 0, 17,  0,  0,  0, 21,  0,  0],
       [ 0,  0, 18,  0,  0,  0, 22,  0],
       [ 0,  0,  0, 19,  0,  0,  0, 23]], dtype=int32)

Methods:

Source code in src/furax/core/_dense.py
class DenseBlockDiagonalOperator(AbstractLinearOperator):
    """Operator that applies block diagonal dense matrices via einsum.

    Only the diagonal blocks are stored, making this more memory-efficient than
    a full dense matrix. The operation is defined by einsum subscripts.

    Attributes:
        blocks: The dense blocks as an array (at least 2D).
        subscripts: Einsum subscripts defining the operation (default: 'ij...,j...->i...').

    Examples:
        For a matrix made of three 2x4 diagonal blocks, and input block columns of three blocks of
        four elements each, the operator can be written as:

        >>> blocks = jnp.arange(24).reshape(3, 2, 4)
        >>> op = DenseBlockDiagonalOperator(
        ...     blocks, jax.ShapeDtypeStruct((3, 4), jnp.int32), 'imn,in->im')
        >>> op.as_matrix()
        Array([[ 0,  1,  2,  3,  0,  0,  0,  0,  0,  0,  0,  0],
               [ 4,  5,  6,  7,  0,  0,  0,  0,  0,  0,  0,  0],
               [ 0,  0,  0,  0,  8,  9, 10, 11,  0,  0,  0,  0],
               [ 0,  0,  0,  0, 12, 13, 14, 15,  0,  0,  0,  0],
               [ 0,  0,  0,  0,  0,  0,  0,  0, 16, 17, 18, 19],
               [ 0,  0,  0,  0,  0,  0,  0,  0, 20, 21, 22, 23]], dtype=int32)

        The axes along which the operator is block diagonal can be non-leading dimensions.
        As a matter of fact, by default, the diagonal axes are assumed to be "on the right".
        The notion of block diagonality should be understood in a tensor context. The representation
        of this operator as a 2d matrix, which relies on the row-major layout, may not be block
        diagonal.

        >>> blocks = jnp.arange(24).reshape(3, 2, 4)
        >>> op = DenseBlockDiagonalOperator(blocks, jax.ShapeDtypeStruct((2, 4), jnp.int32))
        >>> op.as_matrix()
        Array([[ 0,  0,  0,  0,  4,  0,  0,  0],
               [ 0,  1,  0,  0,  0,  5,  0,  0],
               [ 0,  0,  2,  0,  0,  0,  6,  0],
               [ 0,  0,  0,  3,  0,  0,  0,  7],
               [ 8,  0,  0,  0, 12,  0,  0,  0],
               [ 0,  9,  0,  0,  0, 13,  0,  0],
               [ 0,  0, 10,  0,  0,  0, 14,  0],
               [ 0,  0,  0, 11,  0,  0,  0, 15],
               [16,  0,  0,  0, 20,  0,  0,  0],
               [ 0, 17,  0,  0,  0, 21,  0,  0],
               [ 0,  0, 18,  0,  0,  0, 22,  0],
               [ 0,  0,  0, 19,  0,  0,  0, 23]], dtype=int32)
    """

    blocks: Inexact[Array, '...']
    subscripts: str = field(default='ij...,j...->i...', metadata={'static': True})

    def __post_init__(self) -> None:
        subscripts = self.subscripts.replace(' ', '')
        if subscripts != self.subscripts:
            object.__setattr__(self, 'subscripts', subscripts)

        if not jax.tree.all(jax.tree.map(lambda leaf: len(leaf.shape) >= 2, self.blocks)):
            raise ValueError('The blocks should at least have 2 dimensions.')
        self._parse_subscripts(subscripts)

    def mv(self, x: PyTree[Array, '...']) -> PyTree[Array]:
        if is_leaf(x):
            return jnp.einsum(self.subscripts, self.blocks, x)
        leaves, treedef = jax.tree.flatten(x)
        if is_leaf(self.blocks):
            return jax.tree.unflatten(
                treedef, [jnp.einsum(self.subscripts, self.blocks, leaf) for leaf in leaves]
            )
        return jax.tree.map(ft.partial(jnp.einsum, self.subscripts), self.blocks, x)

    def transpose(self) -> AbstractLinearOperator:
        return DenseBlockDiagonalOperator(
            blocks=self.blocks,
            in_structure=self.out_structure,
            subscripts=self._get_transposed_subscripts(self.subscripts),
        )

    @staticmethod
    def _parse_subscripts(subscripts: str) -> tuple[str, str, str]:
        split_subscripts = subscripts.split(',')
        if len(split_subscripts) != 2:
            raise ValueError(f'There should be a single comma in the subscripts: {subscripts!r}."')
        left_subscripts, subscripts = split_subscripts
        split_subscripts = subscripts.split('->')
        if len(split_subscripts) != 2:
            raise ValueError('Explicit mode (with `->) is required for the einsum subscripts.')
        right_subscripts, result_subscripts = split_subscripts
        return left_subscripts, right_subscripts, result_subscripts

    @staticmethod
    def _get_transposed_subscripts(subscripts: str) -> PyTree[jax.ShapeDtypeStruct]:
        """Returns the einsum subscripts for the transpose operation.

        Examples:
            ij...,j...->i...    gives ji...,j...->i...
            hij...,hj...->hi... gives hji...,hj...->hi...
            ikj,kj->ki          gives jki,kj->ki
        """
        lefts, rights, results = DenseBlockDiagonalOperator._parse_subscripts(subscripts)
        lefts_as_set = set(lefts.replace('...', ''))
        rights_as_set = set(rights.replace('...', ''))
        results_as_set = set(results.replace('...', ''))

        # the sum axis is in the subscripts left and right but not in result
        sum_axis_as_set = lefts_as_set & rights_as_set - results_as_set
        if len(sum_axis_as_set) != 1:
            raise ValueError(f'The summation should be performed in one axis {subscripts!r}.')
        sum_axis = sum_axis_as_set.pop()

        # the transpose axis is in the subscripts left and result but not in right
        transpose_axis_as_set = lefts_as_set & results_as_set - rights_as_set
        if len(transpose_axis_as_set) == 0:
            raise ValueError(f'No transposition axis has been specified {subscripts!r}.')
        if len(transpose_axis_as_set) > 1:
            raise ValueError(f'Several transposition axes have been specified: {subscripts!r}.')
        transpose_axis = transpose_axis_as_set.pop()

        # we swap the transpose and sum axes
        sum_axis_number = lefts.index(sum_axis)
        transpose_axis_number = lefts.index(transpose_axis)
        lefts_as_list = list(lefts)
        lefts_as_list[sum_axis_number] = transpose_axis
        lefts_as_list[transpose_axis_number] = sum_axis
        lefts = ''.join(lefts_as_list)

        transpose_axis_number = results.index(transpose_axis)
        results_as_list = list(results)
        results_as_list[transpose_axis_number] = sum_axis
        expected_results = ''.join(results_as_list)
        if expected_results != rights:
            raise ValueError(
                f'The dimensions of the inputs {rights!r} cannot be reordered '
                f'into {expected_results!r}.'
            )

        return f'{lefts},{rights}->{results}'

blocks instance-attribute

subscripts = field(default='ij...,j...->i...', metadata={'static': True}) class-attribute instance-attribute

__post_init__()

Source code in src/furax/core/_dense.py
def __post_init__(self) -> None:
    subscripts = self.subscripts.replace(' ', '')
    if subscripts != self.subscripts:
        object.__setattr__(self, 'subscripts', subscripts)

    if not jax.tree.all(jax.tree.map(lambda leaf: len(leaf.shape) >= 2, self.blocks)):
        raise ValueError('The blocks should at least have 2 dimensions.')
    self._parse_subscripts(subscripts)

mv(x)

Source code in src/furax/core/_dense.py
def mv(self, x: PyTree[Array, '...']) -> PyTree[Array]:
    if is_leaf(x):
        return jnp.einsum(self.subscripts, self.blocks, x)
    leaves, treedef = jax.tree.flatten(x)
    if is_leaf(self.blocks):
        return jax.tree.unflatten(
            treedef, [jnp.einsum(self.subscripts, self.blocks, leaf) for leaf in leaves]
        )
    return jax.tree.map(ft.partial(jnp.einsum, self.subscripts), self.blocks, x)

transpose()

Source code in src/furax/core/_dense.py
def transpose(self) -> AbstractLinearOperator:
    return DenseBlockDiagonalOperator(
        blocks=self.blocks,
        in_structure=self.out_structure,
        subscripts=self._get_transposed_subscripts(self.subscripts),
    )

furax.core.BroadcastDiagonalOperator

Bases: AbstractLinearOperator

Operator that performs element-wise multiplication with broadcasting.

Unlike DiagonalOperator, this operator can change the output shape through broadcasting, making it non-square. Depending on the broadcasting direction:

  • Left broadcasting (extending dimensions on the left): equivalent to a block row operator with diagonal blocks.
  • Right broadcasting (extending dimensions on the right): equivalent to a block diagonal operator with column blocks.

Attributes:

  • diagonal (Inexact[Array, ...]) –

    The diagonal values.

  • axis_destination (int | tuple[int, ...]) –

    The axes along which the generalized diagonal values are applied to the input. If the type is a sequence, there should be as many axes as the number of dimensions in the diagonal input. If the type is a non-negative scalar integer, the dimensions will be (axis, ..., axis + diagonal.ndim - 1). If the type is a negative scalar integer, the dimensions will be (axis - diagonal.ndim, ..., axis).

Examples:

>>> import furax as fx
>>> import jax.numpy as jnp
>>> from numpy.testing import assert_allclose
>>> x = jnp.array([1, 2, 3])
>>> values = jnp.array([[1, 1, 1], [2, 1, 0]])
>>> op = BroadcastDiagonalOperator(
...     values, in_structure=fx.tree.as_structure(x), axis_destination=-1
... )
>>> assert_allclose(op(x), jnp.array([[1, 2, 3], [2, 2, 0]]))
>>> op.as_matrix()
Array([[1, 0, 0],
       [0, 1, 0],
       [0, 0, 1],
       [2, 0, 0],
       [0, 1, 0],
       [0, 0, 0]], dtype=int32)
>>> x = jnp.array([1, 2])
>>> values = jnp.array([[2, 3, 1], [1, 0, 1]])
>>> op = BroadcastDiagonalOperator(
...     values, in_structure=fx.tree.as_structure(x), axis_destination=0
... )
>>> assert_allclose(op(x), jnp.array([[2, 3, 1], [2, 0, 2]]))
>>> op.as_matrix()
Array([[2, 0],
       [3, 0],
       [1, 0],
       [0, 1],
       [0, 0],
       [0, 1]], dtype=int32)
>>> x = jnp.array([[0, 1, 2], [2, 3, 4]])
>>> values = jnp.array([2, 1])
>>> op = BroadcastDiagonalOperator(
...     values, in_structure=fx.tree.as_structure(x), axis_destination=0
... )
>>> assert_allclose(op(x), jnp.array([[0, 2, 4], [2, 3, 4]]))
>>> op.as_matrix()
Array([[2, 0, 0, 0, 0, 0],
       [0, 2, 0, 0, 0, 0],
       [0, 0, 2, 0, 0, 0],
       [0, 0, 0, 1, 0, 0],
       [0, 0, 0, 0, 1, 0],
       [0, 0, 0, 0, 0, 1]], dtype=int32)

Methods:

Source code in src/furax/core/_diagonal.py
class BroadcastDiagonalOperator(AbstractLinearOperator):
    """Operator that performs element-wise multiplication with broadcasting.

    Unlike `DiagonalOperator`, this operator can change the output shape through
    broadcasting, making it non-square. Depending on the broadcasting direction:

    - Left broadcasting (extending dimensions on the left): equivalent to a
      block row operator with diagonal blocks.
    - Right broadcasting (extending dimensions on the right): equivalent to a
      block diagonal operator with column blocks.

    Attributes:
        diagonal: The diagonal values.
        axis_destination: The axes along which the generalized diagonal values are applied to the
            input. If the type is a sequence, there should be as many axes as the number of
            dimensions in the ``diagonal`` input. If the type is a non-negative scalar integer, the
            dimensions will be ``(axis, ..., axis + diagonal.ndim - 1)``. If the type is a negative
            scalar integer, the dimensions will be ``(axis - diagonal.ndim, ..., axis)``.

    Examples:
        >>> import furax as fx
        >>> import jax.numpy as jnp
        >>> from numpy.testing import assert_allclose
        >>> x = jnp.array([1, 2, 3])
        >>> values = jnp.array([[1, 1, 1], [2, 1, 0]])
        >>> op = BroadcastDiagonalOperator(
        ...     values, in_structure=fx.tree.as_structure(x), axis_destination=-1
        ... )
        >>> assert_allclose(op(x), jnp.array([[1, 2, 3], [2, 2, 0]]))
        >>> op.as_matrix()
        Array([[1, 0, 0],
               [0, 1, 0],
               [0, 0, 1],
               [2, 0, 0],
               [0, 1, 0],
               [0, 0, 0]], dtype=int32)

        >>> x = jnp.array([1, 2])
        >>> values = jnp.array([[2, 3, 1], [1, 0, 1]])
        >>> op = BroadcastDiagonalOperator(
        ...     values, in_structure=fx.tree.as_structure(x), axis_destination=0
        ... )
        >>> assert_allclose(op(x), jnp.array([[2, 3, 1], [2, 0, 2]]))
        >>> op.as_matrix()
        Array([[2, 0],
               [3, 0],
               [1, 0],
               [0, 1],
               [0, 0],
               [0, 1]], dtype=int32)

        >>> x = jnp.array([[0, 1, 2], [2, 3, 4]])
        >>> values = jnp.array([2, 1])
        >>> op = BroadcastDiagonalOperator(
        ...     values, in_structure=fx.tree.as_structure(x), axis_destination=0
        ... )
        >>> assert_allclose(op(x), jnp.array([[0, 2, 4], [2, 3, 4]]))
        >>> op.as_matrix()
        Array([[2, 0, 0, 0, 0, 0],
               [0, 2, 0, 0, 0, 0],
               [0, 0, 2, 0, 0, 0],
               [0, 0, 0, 1, 0, 0],
               [0, 0, 0, 0, 1, 0],
               [0, 0, 0, 0, 0, 1]], dtype=int32)
    """

    _diagonal: Inexact[Array, '...']
    axis_destination: int | tuple[int, ...] = field(
        default=-1, kw_only=True, metadata={'static': True}
    )

    def __init__(
        self,
        diagonal: ArrayLike,
        *,
        axis_destination: int | Sequence[int] = -1,
        in_structure: PyTree[jax.ShapeDtypeStruct] | None = None,
    ):
        # Validation
        if not is_leaf(diagonal):
            raise ValueError(
                'the diagonal values cannot be a pytree. Use a BlockDiagonalOperator with'
                'DiagonalOperators or BroadcastDiagonalOperators instead.'
            )
        diagonal = jnp.asarray(diagonal)

        if diagonal.ndim == 0:
            raise ValueError('the diagonal values are scalar. Use HomothetyOperator instead.')

        # Normalize axis_destination if needed
        if isinstance(axis_destination, int):
            ndim = diagonal.ndim
            if axis_destination >= 0:
                axis_destination = tuple(range(axis_destination, axis_destination + ndim))
            else:
                axis_destination = tuple(range(axis_destination - ndim + 1, axis_destination + 1))
        elif not isinstance(axis_destination, tuple):
            axis_destination = tuple(axis_destination)

        object.__setattr__(self, '_diagonal', diagonal)
        object.__setattr__(self, 'axis_destination', tuple(axis_destination))
        super().__init__(in_structure=in_structure)

        # check dimensions by computing the actual output structure (not the @square shortcut)
        _ = jax.eval_shape(self.mv, in_structure)

    @property
    def diagonal(self) -> Inexact[Array, '...']:
        return self._diagonal

    def _reshape_leaves(
        self,
        input_leaf: Inexact[Array, '...'],
    ) -> tuple[Inexact[Array, '#b'], Inexact[Array, '#b']]:
        axes = self._normalize_axes(input_leaf.shape)
        reshaped_diagonal = self._reshape_diagonal(axes, input_leaf.ndim)
        reshaped_input_leaf = self._reshape_input_leaf(axes, input_leaf)
        self._check_leaf_shapes(
            reshaped_diagonal.shape, reshaped_input_leaf.shape, input_leaf.shape
        )
        return reshaped_diagonal, reshaped_input_leaf

    def _normalize_axes(self, input_leaf_shape: tuple[int, ...]) -> tuple[int, ...]:
        """Returns positive axes according to the input leaf shape."""
        assert isinstance(self.axis_destination, tuple)
        axes = tuple(
            axis if axis >= 0 else len(input_leaf_shape) + axis for axis in self.axis_destination
        )
        dups = [k for k, v in Counter(axes).items() if v > 1]
        if dups:
            raise ValueError(
                f'duplicated axis destination {list(self.axis_destination)} for leaf of shape '
                f'{input_leaf_shape}.'
            )
        return axes

    def _reshape_diagonal(
        self, axes: tuple[int, ...], input_leaf_ndim: int
    ) -> Inexact[Array, '...']:
        left_broadcast_dimensions = -min(0, min(axes))
        right_broadcast_dimensions = max(0, max(axes) - input_leaf_ndim + 1)
        reshaped_diagonal_leaf = self.diagonal.reshape(
            self._diagonal.shape
            + (
                left_broadcast_dimensions
                + right_broadcast_dimensions
                + input_leaf_ndim
                - self._diagonal.ndim
            )
            * (1,)
        )
        axes = tuple(axis + left_broadcast_dimensions for axis in axes)
        return jnp.moveaxis(reshaped_diagonal_leaf, range(len(axes)), axes)

    def _reshape_input_leaf(
        self, axes: tuple[int, ...], input_leaf: Inexact[Array, '...']
    ) -> Inexact[Array, '...']:
        right_broadcast_dimensions = max(0, max(axes) - input_leaf.ndim + 1)
        reshaped_input_leaf = input_leaf.reshape(
            input_leaf.shape + right_broadcast_dimensions * (1,)
        )
        return reshaped_input_leaf

    def _check_leaf_shapes(
        self,
        diagonal_shape: tuple[int, ...],
        leaf_shape: tuple[int, ...],
        input_shape: tuple[int, ...],
    ) -> None:
        _ = jnp.broadcast_shapes(diagonal_shape, leaf_shape)

    def mv(self, x: PyTree[Inexact[Array, '...']]) -> PyTree[Inexact[Array, '...']]:
        def func(input_leaf: Inexact[Array, '...']) -> Inexact[Array, '...']:
            reshaped_diagonal_leaf, reshaped_input_leaf = self._reshape_leaves(input_leaf)
            return reshaped_diagonal_leaf * reshaped_input_leaf

        return jax.tree.map(func, x)

axis_destination = field(default=(-1), kw_only=True, metadata={'static': True}) class-attribute instance-attribute

diagonal property

__init__(diagonal, *, axis_destination=-1, in_structure=None)

Source code in src/furax/core/_diagonal.py
def __init__(
    self,
    diagonal: ArrayLike,
    *,
    axis_destination: int | Sequence[int] = -1,
    in_structure: PyTree[jax.ShapeDtypeStruct] | None = None,
):
    # Validation
    if not is_leaf(diagonal):
        raise ValueError(
            'the diagonal values cannot be a pytree. Use a BlockDiagonalOperator with'
            'DiagonalOperators or BroadcastDiagonalOperators instead.'
        )
    diagonal = jnp.asarray(diagonal)

    if diagonal.ndim == 0:
        raise ValueError('the diagonal values are scalar. Use HomothetyOperator instead.')

    # Normalize axis_destination if needed
    if isinstance(axis_destination, int):
        ndim = diagonal.ndim
        if axis_destination >= 0:
            axis_destination = tuple(range(axis_destination, axis_destination + ndim))
        else:
            axis_destination = tuple(range(axis_destination - ndim + 1, axis_destination + 1))
    elif not isinstance(axis_destination, tuple):
        axis_destination = tuple(axis_destination)

    object.__setattr__(self, '_diagonal', diagonal)
    object.__setattr__(self, 'axis_destination', tuple(axis_destination))
    super().__init__(in_structure=in_structure)

    # check dimensions by computing the actual output structure (not the @square shortcut)
    _ = jax.eval_shape(self.mv, in_structure)

mv(x)

Source code in src/furax/core/_diagonal.py
def mv(self, x: PyTree[Inexact[Array, '...']]) -> PyTree[Inexact[Array, '...']]:
    def func(input_leaf: Inexact[Array, '...']) -> Inexact[Array, '...']:
        reshaped_diagonal_leaf, reshaped_input_leaf = self._reshape_leaves(input_leaf)
        return reshaped_diagonal_leaf * reshaped_input_leaf

    return jax.tree.map(func, x)

furax.core.DiagonalOperator

Bases: BroadcastDiagonalOperator

Operator that performs element-wise multiplication: D(x) = d * x.

The diagonal operator is symmetric and square. Its inverse divides by the diagonal values (zeros are handled by returning zero).

The multiplication axes can be specified via axis_destination: - axis_destination=0: diagonal[:, None] * x (multiply along first axis) - axis_destination=-1: diagonal * x (standard broadcasting, default)

Attributes:

  • diagonal (Inexact[Array, ...]) –

    The diagonal values.

  • axis_destination (int | tuple[int, ...]) –

    The axes along which the diagonal values are applied to the input. If the type is a sequence, there should be as many axes as the number of dimensions in the diagonal input. If the type is a non-negative scalar integer, the dimensions will be (axis, ..., axis + diagonal.ndim - 1). If the type is a negative scalar integer, the dimensions will be (axis - diagonal.ndim, ..., axis).

Examples:

>>> import furax as fx
>>> from numpy.testing import assert_allclose
>>> key_gain, key_tod, key_common = jax.random.split(jax.random.PRNGKey(0), 3)
>>> detector_count = 3
>>> sample_count = 10
>>> x = {
...     'tod': jax.random.normal(key_tod, (detector_count, sample_count)),
...     'ground': jax.random.normal(key_common, (detector_count,)),
... }
>>> detector_gains = jax.random.normal(key_gain, (detector_count,)) / 100 + 1
>>> op = DiagonalOperator(
...     detector_gains, axis_destination=0, in_structure=fx.tree.as_structure(x)
... )
>>> y = op(x)
>>> assert_allclose(x['tod'] * detector_gains[:, None], y['tod'])
>>> assert_allclose(x['ground'] * detector_gains, y['ground'])

Methods:

Source code in src/furax/core/_diagonal.py
@diagonal
class DiagonalOperator(BroadcastDiagonalOperator):
    """Operator that performs element-wise multiplication: D(x) = d * x.

    The diagonal operator is symmetric and square. Its inverse divides by the
    diagonal values (zeros are handled by returning zero).

    The multiplication axes can be specified via ``axis_destination``:
        - ``axis_destination=0``: ``diagonal[:, None] * x`` (multiply along first axis)
        - ``axis_destination=-1``: ``diagonal * x`` (standard broadcasting, default)

    Attributes:
        diagonal: The diagonal values.
        axis_destination: The axes along which the diagonal values are applied to the input.
            If the type is a sequence, there should be as many axes as the number of dimensions in
            the ``diagonal`` input. If the type is a non-negative scalar integer, the dimensions
            will be ``(axis, ..., axis + diagonal.ndim - 1)``. If the type is a negative scalar
            integer, the dimensions will be ``(axis - diagonal.ndim, ..., axis)``.

    Examples:
        >>> import furax as fx
        >>> from numpy.testing import assert_allclose
        >>> key_gain, key_tod, key_common = jax.random.split(jax.random.PRNGKey(0), 3)
        >>> detector_count = 3
        >>> sample_count = 10
        >>> x = {
        ...     'tod': jax.random.normal(key_tod, (detector_count, sample_count)),
        ...     'ground': jax.random.normal(key_common, (detector_count,)),
        ... }
        >>> detector_gains = jax.random.normal(key_gain, (detector_count,)) / 100 + 1
        >>> op = DiagonalOperator(
        ...     detector_gains, axis_destination=0, in_structure=fx.tree.as_structure(x)
        ... )
        >>> y = op(x)
        >>> assert_allclose(x['tod'] * detector_gains[:, None], y['tod'])
        >>> assert_allclose(x['ground'] * detector_gains, y['ground'])
    """

    def __init__(
        self,
        diagonal: ArrayLike,
        *,
        axis_destination: int | Sequence[int] = -1,
        in_structure: PyTree[jax.ShapeDtypeStruct] | None = None,
    ):
        if in_structure is None:
            in_structure = as_structure(diagonal)
        super().__init__(diagonal, axis_destination=axis_destination, in_structure=in_structure)

    def _check_leaf_shapes(
        self,
        diagonal_shape: tuple[int, ...],
        leaf_shape: tuple[int, ...],
        input_shape: tuple[int, ...],
    ) -> None:
        shape = jnp.broadcast_shapes(diagonal_shape, leaf_shape)
        if shape != input_shape:
            raise ValueError(
                f'the input shape {input_shape} cannot be changed to {shape} '
                f'by a DiagonalOperator. For broadcasting, use BroadcastDiagonalOperator.'
            )

    def inverse(self) -> AbstractLinearOperator:
        return DiagonalInverseOperator(self)

    def as_matrix(self) -> Inexact[Array, 'a b']:
        diagonals = [
            jnp.broadcast_to(
                self._reshape_diagonal(self._normalize_axes(leaf.shape), leaf.ndim), leaf.shape
            ).ravel()
            for leaf in jax.tree.leaves(self.in_structure)
        ]
        matrix = jnp.diag(jnp.concatenate(diagonals, dtype=self.out_promoted_dtype))
        return matrix

__init__(diagonal, *, axis_destination=-1, in_structure=None)

Source code in src/furax/core/_diagonal.py
def __init__(
    self,
    diagonal: ArrayLike,
    *,
    axis_destination: int | Sequence[int] = -1,
    in_structure: PyTree[jax.ShapeDtypeStruct] | None = None,
):
    if in_structure is None:
        in_structure = as_structure(diagonal)
    super().__init__(diagonal, axis_destination=axis_destination, in_structure=in_structure)

inverse()

Source code in src/furax/core/_diagonal.py
def inverse(self) -> AbstractLinearOperator:
    return DiagonalInverseOperator(self)

as_matrix()

Source code in src/furax/core/_diagonal.py
def as_matrix(self) -> Inexact[Array, 'a b']:
    diagonals = [
        jnp.broadcast_to(
            self._reshape_diagonal(self._normalize_axes(leaf.shape), leaf.ndim), leaf.shape
        ).ravel()
        for leaf in jax.tree.leaves(self.in_structure)
    ]
    matrix = jnp.diag(jnp.concatenate(diagonals, dtype=self.out_promoted_dtype))
    return matrix

furax.core.FourierOperator

Bases: AbstractLinearOperator

Operator that applies a frequency-domain kernel via FFT.

Performs element-wise multiplication in Fourier space: Y = IFFT(FFT(x) * kernel). The filter is applied along the last axis. Supports optional apodization to reduce edge artifacts.

Attributes:

  • kernel_func (Callable[[Array], Array]) –

    Function that returns the kernel given frequencies.

  • sample_rate (float) –

    Sample rate of the input signal [Hz].

  • apodize (bool) –

    Whether to apply Hamming window apodization.

  • fft_size (int) –

    Size of the FFT (power of 2 for efficiency).

Examples:

>>> import jax.numpy as jnp
>>> n = 1000
>>> fs = 200.0  # sampling frequency
>>> cutoff = 10.0  # cutoff frequency
>>> op = FourierOperator(
...     lambda f: f < cutoff,  # low-pass filter
...     in_structure=jax.ShapeDtypeStruct((n,), float),
...     sample_rate=fs,
... )
>>> signal = jnp.ones(n)
>>> filtered = op(signal)

Methods:

Source code in src/furax/core/_fourier.py
@square
class FourierOperator(AbstractLinearOperator):
    """Operator that applies a frequency-domain kernel via FFT.

    Performs element-wise multiplication in Fourier space: Y = IFFT(FFT(x) * kernel).
    The filter is applied along the last axis. Supports optional apodization to
    reduce edge artifacts.

    Attributes:
        kernel_func: Function that returns the kernel given frequencies.
        sample_rate: Sample rate of the input signal [Hz].
        apodize: Whether to apply Hamming window apodization.
        fft_size: Size of the FFT (power of 2 for efficiency).

    Examples:
        >>> import jax.numpy as jnp
        >>> n = 1000
        >>> fs = 200.0  # sampling frequency
        >>> cutoff = 10.0  # cutoff frequency
        >>> op = FourierOperator(
        ...     lambda f: f < cutoff,  # low-pass filter
        ...     in_structure=jax.ShapeDtypeStruct((n,), float),
        ...     sample_rate=fs,
        ... )
        >>> signal = jnp.ones(n)
        >>> filtered = op(signal)
    """

    kernel_func: Callable[[Array], Array] = field(metadata={'static': True})
    fft_size: int = field(metadata={'static': True})
    apodize: bool = field(metadata={'static': True})
    padding_width: int = field(metadata={'static': True})
    sample_rate: float

    def __init__(
        self,
        kernel_func: Callable[[Float[Array, '...']], Inexact[Array, '...']],
        *,
        in_structure: PyTree[jax.ShapeDtypeStruct],
        sample_rate: float = 1.0,
        apodize: bool = False,
        padding_width: int | None = None,
    ) -> None:
        """Create a FourierOperator.

        Args:
            kernel_func: Function that generates the Fourier kernel as a function of frequency.
            in_structure: Input structure of the operator.
            sample_rate: Sample rate of the input signal [Hz].
                Important if the kernel function depends on physical frequency units.
            apodize: Pad and apply Hamming window to both ends to reduce edge artifacts.
            padding_width: Padding width in samples on each end.
                Defaults is 5% of data length (rounded up).
        """
        # Data length
        n = in_structure.shape[-1]

        # Set padding_width if unspecified
        if apodize:
            if padding_width is None:
                padding_width = int(np.ceil(0.05 * n))  # 5% of data length
        else:
            padding_width = 0

        # Use a power-of-2 FFT size for efficiency
        # Add an extra factor of 2 to avoid edge effects
        fft_size = _next_power_of_2(n + 2 * padding_width, additional_power=1)

        # Compile the kernel function and check its output shape is correct
        jitted_kernel = jax.jit(kernel_func)
        freqs = jnp.fft.rfftfreq(fft_size, d=1 / sample_rate)
        kernel = jitted_kernel(freqs)
        if kernel.shape[-1] != freqs.size:
            raise ValueError('Bad kernel shape')

        object.__setattr__(self, 'kernel_func', jitted_kernel)
        object.__setattr__(self, 'fft_size', fft_size)
        object.__setattr__(self, 'apodize', apodize)
        object.__setattr__(self, 'padding_width', padding_width)
        object.__setattr__(self, 'sample_rate', sample_rate)
        object.__setattr__(self, 'in_structure', in_structure)

    def mv(self, x: Float[Array, '...']) -> Float[Array, '...']:
        """Apply Fourier kernel to input array.

        For multidimensional inputs, the filter is applied along the last axis.
        """
        kernel = self.get_kernel()
        func = jnp.vectorize(self._apply, signature='(n),(m)->(n)')
        return func(x, kernel)  # type: ignore[no-any-return]

    def get_kernel(self) -> Inexact[Array, '...']:
        freqs = jnp.fft.rfftfreq(self.fft_size, d=1 / self.sample_rate)
        return self.kernel_func(freqs)

    def as_matrix(self) -> Inexact[Array, 'a a']:
        """Returns the operator as a dense matrix.

        Warning: This can be memory-intensive for large inputs.
        """
        from functools import partial

        @partial(jnp.vectorize, signature='(n)->(n,n)')
        def func(x: Array) -> Array:
            """Create matrix by applying operator to each basis vector."""
            n = x.size
            matrix = jnp.zeros((n, n), dtype=x.dtype)

            for i in range(n):
                e_i = jnp.zeros(n, dtype=x.dtype)
                e_i = e_i.at[i].set(1.0)
                matrix = matrix.at[:, i].set(self.mv(e_i))

            return matrix

        x = jnp.zeros(self.in_structure.shape, self.in_structure.dtype)
        blocks: Array = func(x)

        # Handle multidimensional case
        if blocks.ndim > 2:
            # Return block diagonal matrix
            import jax.scipy.linalg as jsl

            blocks = blocks.reshape(-1, blocks.shape[-1], blocks.shape[-1])
            matrix: Array = jsl.block_diag(*blocks)
            return matrix

        return blocks

    @classmethod
    def create_bandpass_operator(
        cls,
        f_low: float,
        f_high: float,
        sample_rate: float,
        in_structure: PyTree[jax.ShapeDtypeStruct],
        *,
        filter_type: str = 'square',
        apodize: bool = True,
    ) -> 'FourierOperator':
        """Creates a bandpass filtering operator.

        Examples:
            >>> import jax.numpy as jnp
            >>> tod = jnp.sin(2 * jnp.pi * 10 * jnp.linspace(0, 1, 1000))
            >>> op = FourierOperator.create_bandpass_operator(
            ...     f_low=5.0,
            ...     f_high=15.0,
            ...     sample_rate=1000.0,
            ...     in_structure=jax.ShapeDtypeStruct(tod.shape, tod.dtype)
            ... )
            >>> filtered = op(tod)

        Args:
            f_low: Lower frequency cutoff (inclusive) [Hz].
            f_high: Upper frequency cutoff (inclusive) [Hz].
            sample_rate: Sampling rate of the input signal [Hz].
            in_structure: Input structure specification.
            filter_type: Filter shape type. Options: 'square', 'butter4', 'cos2'.
                Default: 'square'.
                - 'square': Sharp cutoff (ideal brick-wall filter)
                - 'butter4': 4th-order Butterworth filter (smooth rolloff)
                - 'cos2': Cosine-squared transition (smooth rolloff)
            apodize: Apply Hamming window to reduce edge artifacts.

        Returns:
            FourierOperator configured with bandpass kernel.

        Raises:
            ValueError: If frequency parameters or filter_type are invalid.
        """
        # Validate filter_type
        valid_filter_types = ('square', 'butter4', 'cos2')
        if filter_type not in valid_filter_types:
            raise ValueError(
                f'Invalid filter_type {filter_type}. Choose from: {", ".join(valid_filter_types)}'
            )

        if f_low < 0:
            raise ValueError(f'f_low must be non-negative, got {f_low}')
        if f_high > sample_rate / 2:
            raise ValueError(
                f'f_high must be less than Nyquist frequency {sample_rate / 2}, got {f_high}'
            )
        if f_low >= f_high:
            raise ValueError(f'f_low must be less than f_high, got f_low={f_low}, f_high={f_high}')

        # Create filter kernel based on filter_type
        if filter_type == 'square':

            def kernel_func(freqs):  # type: ignore[no-untyped-def]
                # Sharp cutoff (ideal brick-wall filter)
                mask = (freqs >= f_low) & (freqs <= f_high)
                return mask.astype(jnp.complex128)

        elif filter_type == 'butter4':
            import scipy.signal

            def kernel_func(freqs):  # type: ignore[no-untyped-def]
                # 4th-order Butterworth bandpass filter using scipy

                # Design Butterworth bandpass filter
                # butter returns (b, a) coefficients for digital filter
                # Use 4th order bandpass (becomes 8th order total - 4th for each band edge)
                sos = scipy.signal.butter(
                    4, [f_low, f_high], btype='bandpass', fs=sample_rate, output='sos'
                )

                # Convert to frequency response
                # Use freqs for the actual frequency points we need
                w = 2 * jnp.pi * freqs

                # Compute frequency response from second-order sections
                # H(w) = product of all second-order section responses
                H = jnp.ones_like(freqs, dtype=jnp.complex128)
                for section in sos:
                    b0, b1, b2, a0, a1, a2 = section
                    # H(z) = (b0 + b1*z^-1 + b2*z^-2) / (a0 + a1*z^-1 + a2*z^-2)
                    # For frequency response, substitute z = exp(j*w*T) where T = 1/fs
                    z = jnp.exp(1j * w / sample_rate)
                    z_inv = 1.0 / z
                    z_inv2 = z_inv * z_inv

                    numerator = b0 + b1 * z_inv + b2 * z_inv2
                    denominator = a0 + a1 * z_inv + a2 * z_inv2
                    H = H * (numerator / denominator)

                # Take magnitude for real-valued filter
                return jnp.abs(H).astype(jnp.complex128)

        elif filter_type == 'cos2':

            def kernel_func(freqs):  # type: ignore[no-untyped-def]
                # Cosine-squared transition outside the passband
                # Define transition width (10% of bandwidth on each side, outside the band)
                bandwidth = f_high - f_low
                transition_width = 0.1 * bandwidth

                # Transition regions are OUTSIDE the passband [f_low, f_high]
                # Lower transition: [max(0, f_low - transition_width), f_low]
                # Upper transition: [f_high, f_high + transition_width]
                f_low_transition_start = max(0.0, f_low - transition_width)

                # Initialize kernel
                kernel = jnp.zeros_like(freqs)

                # Passband (full transmission) - entire [f_low, f_high] range
                passband = (freqs >= f_low) & (freqs <= f_high)
                kernel = jnp.where(passband, 1.0, kernel)

                # Lower transition region (outside passband, below f_low)
                lower_transition = (freqs >= f_low_transition_start) & (freqs < f_low)
                # Cosine-squared from 0 to 1 as frequency increases toward f_low
                phase_low = (
                    (freqs - f_low_transition_start)
                    / (f_low - f_low_transition_start)
                    * (jnp.pi / 2)
                )
                kernel = jnp.where(lower_transition, jnp.sin(phase_low) ** 2, kernel)

                # Upper transition region (outside passband, above f_high)
                f_high_transition_end = f_high + transition_width
                upper_transition = (freqs > f_high) & (freqs <= f_high_transition_end)
                # Cosine-squared from 1 to 0 as frequency increases away from f_high
                phase_high = (f_high_transition_end - freqs) / transition_width * (jnp.pi / 2)
                kernel = jnp.where(upper_transition, jnp.sin(phase_high) ** 2, kernel)

                return kernel.astype(jnp.complex128)

        else:
            raise NotImplementedError(f'Filter type {filter_type} not implemented')

        # Validate that the filter has at least one unity gain point in the passband
        # This ensures the passband is actually represented in the frequency grid
        n = in_structure.shape[-1]
        kernel = kernel_func(jnp.fft.rfftfreq(n, d=1.0 / sample_rate))
        max_gain = jnp.max(jnp.abs(kernel))
        if max_gain < 0.99:  # Use 0.99 to account for numerical precision
            msg = (
                f'Filter passband not represented in frequency grid. '
                f'Maximum filter gain is {max_gain:.4f}, expected ~1.0. '
                f'The bandwidth [{f_low}, {f_high}] Hz may be too narrow for the given sample '
                'rate and input size.'
            )
            raise ValueError(msg)

        return cls(
            kernel_func,
            in_structure=in_structure,
            sample_rate=sample_rate,
            apodize=apodize,
        )

    def _apply(self, x: Array, kernel: Array) -> Array:
        """Apply Fourier kernel using FFT on the entire signal.

        This method transforms the entire signal to Fourier domain, applies
        the kernel, and transforms back.

        If apodization is enabled, the signal is padded on both ends and a
        Hamming window is applied to the padded regions to reduce edge artifacts.
        """
        n = x.shape[-1]

        if self.apodize:
            # Compute actual padding needed to match fft_size
            # This may differ from self.padding_width by 1 due to odd/even rounding
            total_padding = self.fft_size - n
            pad_left = total_padding // 2
            pad_right = total_padding - pad_left

            # Pad the signal
            x_padded = jnp.pad(x, (pad_left, pad_right), mode='edge')

            # Create apodization window
            # The window tapers from 0 to 1 in the padded regions
            # Use the actual padding amounts
            window = self._create_apodization_window(self.fft_size, pad_left)

            # Apply window to padded signal
            x_windowed = x_padded * window

            # Forward FFT
            X = jnp.fft.rfft(x_windowed)

            # Apply Fourier kernel (already validated to match size)
            X_filtered = X * kernel

            # Inverse FFT
            y_padded = jnp.fft.irfft(X_filtered, n=self.fft_size)

            # Extract the original signal region (remove padding)
            y = y_padded[pad_left : pad_left + n]
        else:
            # No apodization
            # Forward FFT (use rfft for real signals)
            X = jnp.fft.rfft(x, n=self.fft_size)

            # Apply Fourier kernel (already validated to match size)
            X_filtered = X * kernel

            # Inverse FFT
            y = jnp.fft.irfft(X_filtered, n=self.fft_size)

            # Extract the original signal length
            y = y[:n]

        return y

    @staticmethod
    def _create_apodization_window(fft_size: int, overlap: int) -> Array:
        """Create a Hamming window for apodization in overlap regions.

        The window is constructed such that:
        - It is 1.0 in the middle (valid output region)
        - It smoothly tapers to 0 in the overlap regions on both sides

        Args:
            fft_size: Size of the FFT block
            overlap: Size of the overlap region on each side

        Returns:
            Window array of shape (fft_size,)
        """
        # Create full Hamming window
        window = jnp.ones(fft_size)

        if overlap > 0:
            # Create Hamming window for the overlap region
            # The Hamming window goes from 0 to 1 over the overlap region
            hamming = 0.54 - 0.46 * jnp.cos(jnp.pi * jnp.arange(overlap) / overlap)

            # Apply taper at the beginning (left overlap)
            window = window.at[:overlap].set(hamming)

            # Apply taper at the end (right overlap)
            window = window.at[-overlap:].set(hamming[::-1])

        return window

kernel_func = field(metadata={'static': True}) class-attribute instance-attribute

fft_size = field(metadata={'static': True}) class-attribute instance-attribute

apodize = field(metadata={'static': True}) class-attribute instance-attribute

padding_width = field(metadata={'static': True}) class-attribute instance-attribute

sample_rate instance-attribute

__init__(kernel_func, *, in_structure, sample_rate=1.0, apodize=False, padding_width=None)

Parameters:

  • kernel_func (Callable[[Float[Array, ...]], Inexact[Array, ...]]) –

    Function that generates the Fourier kernel as a function of frequency.

  • in_structure (PyTree[ShapeDtypeStruct]) –

    Input structure of the operator.

  • sample_rate (float, default: 1.0 ) –

    Sample rate of the input signal [Hz]. Important if the kernel function depends on physical frequency units.

  • apodize (bool, default: False ) –

    Pad and apply Hamming window to both ends to reduce edge artifacts.

  • padding_width (int | None, default: None ) –

    Padding width in samples on each end. Defaults is 5% of data length (rounded up).

Source code in src/furax/core/_fourier.py
def __init__(
    self,
    kernel_func: Callable[[Float[Array, '...']], Inexact[Array, '...']],
    *,
    in_structure: PyTree[jax.ShapeDtypeStruct],
    sample_rate: float = 1.0,
    apodize: bool = False,
    padding_width: int | None = None,
) -> None:
    """Create a FourierOperator.

    Args:
        kernel_func: Function that generates the Fourier kernel as a function of frequency.
        in_structure: Input structure of the operator.
        sample_rate: Sample rate of the input signal [Hz].
            Important if the kernel function depends on physical frequency units.
        apodize: Pad and apply Hamming window to both ends to reduce edge artifacts.
        padding_width: Padding width in samples on each end.
            Defaults is 5% of data length (rounded up).
    """
    # Data length
    n = in_structure.shape[-1]

    # Set padding_width if unspecified
    if apodize:
        if padding_width is None:
            padding_width = int(np.ceil(0.05 * n))  # 5% of data length
    else:
        padding_width = 0

    # Use a power-of-2 FFT size for efficiency
    # Add an extra factor of 2 to avoid edge effects
    fft_size = _next_power_of_2(n + 2 * padding_width, additional_power=1)

    # Compile the kernel function and check its output shape is correct
    jitted_kernel = jax.jit(kernel_func)
    freqs = jnp.fft.rfftfreq(fft_size, d=1 / sample_rate)
    kernel = jitted_kernel(freqs)
    if kernel.shape[-1] != freqs.size:
        raise ValueError('Bad kernel shape')

    object.__setattr__(self, 'kernel_func', jitted_kernel)
    object.__setattr__(self, 'fft_size', fft_size)
    object.__setattr__(self, 'apodize', apodize)
    object.__setattr__(self, 'padding_width', padding_width)
    object.__setattr__(self, 'sample_rate', sample_rate)
    object.__setattr__(self, 'in_structure', in_structure)

mv(x)

Apply Fourier kernel to input array.

For multidimensional inputs, the filter is applied along the last axis.

Source code in src/furax/core/_fourier.py
def mv(self, x: Float[Array, '...']) -> Float[Array, '...']:
    """Apply Fourier kernel to input array.

    For multidimensional inputs, the filter is applied along the last axis.
    """
    kernel = self.get_kernel()
    func = jnp.vectorize(self._apply, signature='(n),(m)->(n)')
    return func(x, kernel)  # type: ignore[no-any-return]

get_kernel()

Source code in src/furax/core/_fourier.py
def get_kernel(self) -> Inexact[Array, '...']:
    freqs = jnp.fft.rfftfreq(self.fft_size, d=1 / self.sample_rate)
    return self.kernel_func(freqs)

as_matrix()

Returns the operator as a dense matrix.

Warning: This can be memory-intensive for large inputs.

Source code in src/furax/core/_fourier.py
def as_matrix(self) -> Inexact[Array, 'a a']:
    """Returns the operator as a dense matrix.

    Warning: This can be memory-intensive for large inputs.
    """
    from functools import partial

    @partial(jnp.vectorize, signature='(n)->(n,n)')
    def func(x: Array) -> Array:
        """Create matrix by applying operator to each basis vector."""
        n = x.size
        matrix = jnp.zeros((n, n), dtype=x.dtype)

        for i in range(n):
            e_i = jnp.zeros(n, dtype=x.dtype)
            e_i = e_i.at[i].set(1.0)
            matrix = matrix.at[:, i].set(self.mv(e_i))

        return matrix

    x = jnp.zeros(self.in_structure.shape, self.in_structure.dtype)
    blocks: Array = func(x)

    # Handle multidimensional case
    if blocks.ndim > 2:
        # Return block diagonal matrix
        import jax.scipy.linalg as jsl

        blocks = blocks.reshape(-1, blocks.shape[-1], blocks.shape[-1])
        matrix: Array = jsl.block_diag(*blocks)
        return matrix

    return blocks

create_bandpass_operator(f_low, f_high, sample_rate, in_structure, *, filter_type='square', apodize=True) classmethod

Creates a bandpass filtering operator.

Examples:

>>> import jax.numpy as jnp
>>> tod = jnp.sin(2 * jnp.pi * 10 * jnp.linspace(0, 1, 1000))
>>> op = FourierOperator.create_bandpass_operator(
...     f_low=5.0,
...     f_high=15.0,
...     sample_rate=1000.0,
...     in_structure=jax.ShapeDtypeStruct(tod.shape, tod.dtype)
... )
>>> filtered = op(tod)

Parameters:

  • f_low (float) –

    Lower frequency cutoff (inclusive) [Hz].

  • f_high (float) –

    Upper frequency cutoff (inclusive) [Hz].

  • sample_rate (float) –

    Sampling rate of the input signal [Hz].

  • in_structure (PyTree[ShapeDtypeStruct]) –

    Input structure specification.

  • filter_type (str, default: 'square' ) –

    Filter shape type. Options: 'square', 'butter4', 'cos2'. Default: 'square'. - 'square': Sharp cutoff (ideal brick-wall filter) - 'butter4': 4th-order Butterworth filter (smooth rolloff) - 'cos2': Cosine-squared transition (smooth rolloff)

  • apodize (bool, default: True ) –

    Apply Hamming window to reduce edge artifacts.

Returns:

Raises:

  • ValueError

    If frequency parameters or filter_type are invalid.

Source code in src/furax/core/_fourier.py
@classmethod
def create_bandpass_operator(
    cls,
    f_low: float,
    f_high: float,
    sample_rate: float,
    in_structure: PyTree[jax.ShapeDtypeStruct],
    *,
    filter_type: str = 'square',
    apodize: bool = True,
) -> 'FourierOperator':
    """Creates a bandpass filtering operator.

    Examples:
        >>> import jax.numpy as jnp
        >>> tod = jnp.sin(2 * jnp.pi * 10 * jnp.linspace(0, 1, 1000))
        >>> op = FourierOperator.create_bandpass_operator(
        ...     f_low=5.0,
        ...     f_high=15.0,
        ...     sample_rate=1000.0,
        ...     in_structure=jax.ShapeDtypeStruct(tod.shape, tod.dtype)
        ... )
        >>> filtered = op(tod)

    Args:
        f_low: Lower frequency cutoff (inclusive) [Hz].
        f_high: Upper frequency cutoff (inclusive) [Hz].
        sample_rate: Sampling rate of the input signal [Hz].
        in_structure: Input structure specification.
        filter_type: Filter shape type. Options: 'square', 'butter4', 'cos2'.
            Default: 'square'.
            - 'square': Sharp cutoff (ideal brick-wall filter)
            - 'butter4': 4th-order Butterworth filter (smooth rolloff)
            - 'cos2': Cosine-squared transition (smooth rolloff)
        apodize: Apply Hamming window to reduce edge artifacts.

    Returns:
        FourierOperator configured with bandpass kernel.

    Raises:
        ValueError: If frequency parameters or filter_type are invalid.
    """
    # Validate filter_type
    valid_filter_types = ('square', 'butter4', 'cos2')
    if filter_type not in valid_filter_types:
        raise ValueError(
            f'Invalid filter_type {filter_type}. Choose from: {", ".join(valid_filter_types)}'
        )

    if f_low < 0:
        raise ValueError(f'f_low must be non-negative, got {f_low}')
    if f_high > sample_rate / 2:
        raise ValueError(
            f'f_high must be less than Nyquist frequency {sample_rate / 2}, got {f_high}'
        )
    if f_low >= f_high:
        raise ValueError(f'f_low must be less than f_high, got f_low={f_low}, f_high={f_high}')

    # Create filter kernel based on filter_type
    if filter_type == 'square':

        def kernel_func(freqs):  # type: ignore[no-untyped-def]
            # Sharp cutoff (ideal brick-wall filter)
            mask = (freqs >= f_low) & (freqs <= f_high)
            return mask.astype(jnp.complex128)

    elif filter_type == 'butter4':
        import scipy.signal

        def kernel_func(freqs):  # type: ignore[no-untyped-def]
            # 4th-order Butterworth bandpass filter using scipy

            # Design Butterworth bandpass filter
            # butter returns (b, a) coefficients for digital filter
            # Use 4th order bandpass (becomes 8th order total - 4th for each band edge)
            sos = scipy.signal.butter(
                4, [f_low, f_high], btype='bandpass', fs=sample_rate, output='sos'
            )

            # Convert to frequency response
            # Use freqs for the actual frequency points we need
            w = 2 * jnp.pi * freqs

            # Compute frequency response from second-order sections
            # H(w) = product of all second-order section responses
            H = jnp.ones_like(freqs, dtype=jnp.complex128)
            for section in sos:
                b0, b1, b2, a0, a1, a2 = section
                # H(z) = (b0 + b1*z^-1 + b2*z^-2) / (a0 + a1*z^-1 + a2*z^-2)
                # For frequency response, substitute z = exp(j*w*T) where T = 1/fs
                z = jnp.exp(1j * w / sample_rate)
                z_inv = 1.0 / z
                z_inv2 = z_inv * z_inv

                numerator = b0 + b1 * z_inv + b2 * z_inv2
                denominator = a0 + a1 * z_inv + a2 * z_inv2
                H = H * (numerator / denominator)

            # Take magnitude for real-valued filter
            return jnp.abs(H).astype(jnp.complex128)

    elif filter_type == 'cos2':

        def kernel_func(freqs):  # type: ignore[no-untyped-def]
            # Cosine-squared transition outside the passband
            # Define transition width (10% of bandwidth on each side, outside the band)
            bandwidth = f_high - f_low
            transition_width = 0.1 * bandwidth

            # Transition regions are OUTSIDE the passband [f_low, f_high]
            # Lower transition: [max(0, f_low - transition_width), f_low]
            # Upper transition: [f_high, f_high + transition_width]
            f_low_transition_start = max(0.0, f_low - transition_width)

            # Initialize kernel
            kernel = jnp.zeros_like(freqs)

            # Passband (full transmission) - entire [f_low, f_high] range
            passband = (freqs >= f_low) & (freqs <= f_high)
            kernel = jnp.where(passband, 1.0, kernel)

            # Lower transition region (outside passband, below f_low)
            lower_transition = (freqs >= f_low_transition_start) & (freqs < f_low)
            # Cosine-squared from 0 to 1 as frequency increases toward f_low
            phase_low = (
                (freqs - f_low_transition_start)
                / (f_low - f_low_transition_start)
                * (jnp.pi / 2)
            )
            kernel = jnp.where(lower_transition, jnp.sin(phase_low) ** 2, kernel)

            # Upper transition region (outside passband, above f_high)
            f_high_transition_end = f_high + transition_width
            upper_transition = (freqs > f_high) & (freqs <= f_high_transition_end)
            # Cosine-squared from 1 to 0 as frequency increases away from f_high
            phase_high = (f_high_transition_end - freqs) / transition_width * (jnp.pi / 2)
            kernel = jnp.where(upper_transition, jnp.sin(phase_high) ** 2, kernel)

            return kernel.astype(jnp.complex128)

    else:
        raise NotImplementedError(f'Filter type {filter_type} not implemented')

    # Validate that the filter has at least one unity gain point in the passband
    # This ensures the passband is actually represented in the frequency grid
    n = in_structure.shape[-1]
    kernel = kernel_func(jnp.fft.rfftfreq(n, d=1.0 / sample_rate))
    max_gain = jnp.max(jnp.abs(kernel))
    if max_gain < 0.99:  # Use 0.99 to account for numerical precision
        msg = (
            f'Filter passband not represented in frequency grid. '
            f'Maximum filter gain is {max_gain:.4f}, expected ~1.0. '
            f'The bandwidth [{f_low}, {f_high}] Hz may be too narrow for the given sample '
            'rate and input size.'
        )
        raise ValueError(msg)

    return cls(
        kernel_func,
        in_structure=in_structure,
        sample_rate=sample_rate,
        apodize=apodize,
    )

furax.core.IndexOperator

Bases: AbstractLinearOperator

Operator that extracts elements by indexing: y = x[indices].

Supports integer indices, slices, boolean masks, and advanced indexing. When indices are unique, the operator satisfies: I @ I.T = Identity.

Attributes:

Examples:

To extract the second element of the first axis:

>>> op = IndexOperator(1, in_structure=jax.ShapeDtypeStruct((10, 4), jax.numpy.float32))

To extract values from the penultimate axis given an array of indices:

>>> indices = jax.numpy.array([2, 4, 4, 5, 7])
>>> in_structure = jax.ShapeDtypeStruct((9, 8, 3), jax.numpy.float32)
>>> op = IndexOperator((..., indices, slice(None)), in_structure=in_structure)

In order to extract values using a boolean mask, it is required to specify an output structure:

>>> indices = jax.numpy.array([True, False, True, False])
>>> in_structure = jax.ShapeDtypeStruct((4,), jax.numpy.float32)
>>> out_structure = jax.ShapeDtypeStruct((2,), jax.numpy.float32)
>>> op = IndexOperator(indices, in_structure=in_structure, out_structure=out_structure)

So it is usually better to specify an index mask:

>>> op = IndexOperator(jnp.where(indices), in_structure=in_structure)

Methods:

Source code in src/furax/core/_indices.py
class IndexOperator(AbstractLinearOperator):
    """Operator that extracts elements by indexing: y = x[indices].

    Supports integer indices, slices, boolean masks, and advanced indexing.
    When indices are unique, the operator satisfies: I @ I.T = Identity.

    Attributes:
        indices: The indexing tuple (integers, slices, arrays, or Ellipsis).
        unique_indices: Whether the indices select unique elements (enables optimizations).

    Examples:
        To extract the second element of the first axis:

        >>> op = IndexOperator(1, in_structure=jax.ShapeDtypeStruct((10, 4), jax.numpy.float32))

        To extract values from the penultimate axis given an array of indices:

        >>> indices = jax.numpy.array([2, 4, 4, 5, 7])
        >>> in_structure = jax.ShapeDtypeStruct((9, 8, 3), jax.numpy.float32)
        >>> op = IndexOperator((..., indices, slice(None)), in_structure=in_structure)

        In order to extract values using a boolean mask, it is required to specify an output structure:

        >>> indices = jax.numpy.array([True, False, True, False])
        >>> in_structure = jax.ShapeDtypeStruct((4,), jax.numpy.float32)
        >>> out_structure = jax.ShapeDtypeStruct((2,), jax.numpy.float32)
        >>> op = IndexOperator(indices, in_structure=in_structure, out_structure=out_structure)

        So it is usually better to specify an index mask:

        >>> op = IndexOperator(jnp.where(indices), in_structure=in_structure)
    """

    indices: tuple[int | slice | Bool[Array, '...'] | Integer[Array, '...'] | EllipsisType, ...]
    _out_structure: PyTree[jax.ShapeDtypeStruct] = field(metadata={'static': True})
    unique_indices: bool = field(metadata={'static': True})

    def __init__(
        self,
        indices: (
            int
            | slice
            | Bool[Array, '...']
            | Integer[Array, '...']
            | tuple[int | slice | Bool[Array, '...'] | Integer[Array, '...'] | EllipsisType, ...]
        ),
        *,
        in_structure: PyTree[jax.ShapeDtypeStruct],
        out_structure: PyTree[jax.ShapeDtypeStruct] | None = None,
        unique_indices: bool | None = None,
        _out_structure: PyTree[jax.ShapeDtypeStruct] | None = None,
    ) -> None:
        # Support JAX unflattening which uses the field name _out_structure
        if _out_structure is not None:
            out_structure = _out_structure

        if not isinstance(indices, tuple):
            indices = (indices,)

        # When _out_structure is provided, we're being called from JAX unflattening
        # and should skip normalization/validation
        if _out_structure is None:
            self._check_indices(indices)

            if unique_indices is None:
                if all(
                    isinstance(_, int | slice | EllipsisType)
                    or isinstance(_, Array)
                    and _.dtype == bool
                    for _ in indices
                ):
                    unique_indices = True
                else:
                    unique_indices = False

            if out_structure is None and any(
                isinstance(_, Array) and _.dtype == bool for _ in indices
            ):
                raise ValueError(
                    'The output structure must be specified when the indices are determined using a '
                    'boolean array.'
                )

            if out_structure is None:
                # Compute output structure manually
                def temp_mv(x):  # type: ignore[no-untyped-def]
                    return jax.tree.map(lambda leaf: leaf[indices], x)

                out_structure = jax.eval_shape(temp_mv, in_structure)

        object.__setattr__(self, 'indices', indices)
        object.__setattr__(self, '_out_structure', out_structure)
        object.__setattr__(self, 'unique_indices', unique_indices)
        object.__setattr__(self, 'in_structure', in_structure)

    def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
        return jax.tree.map(lambda leaf: leaf[self.indices], x)

    @property
    def out_structure(self) -> PyTree[jax.ShapeDtypeStruct]:
        return self._out_structure

    def reduce(self) -> AbstractLinearOperator:
        if len(self.indexed_axes) == 0:
            return IdentityOperator(in_structure=self.in_structure)
        return self

    @staticmethod
    def _check_indices(
        indices: tuple[
            int | slice | Bool[Array, '...'] | Integer[Array, '...'] | EllipsisType, ...
        ],
    ) -> None:
        ellipsis_count = sum(index is Ellipsis for index in indices)
        if ellipsis_count > 1:
            raise ValueError('more than one Ellipsis in specified in the indices.')

    @property
    def indexed_axes(self) -> list[int]:
        """Returns the list of axes for which an indexing is performed.

        Example: for an indexing of (slice(None), 3, ..., jnp.array([1, 2])),
            it returns [1, -1].
        """
        try:
            ellipsis_index = self.indices.index(Ellipsis)
        except ValueError:
            ellipsis_index = len(self.indices)

        axes = []
        for axis, index in enumerate(self.indices):
            if axis >= ellipsis_index:
                break
            if isinstance(index, slice) and index == slice(None):
                continue
            axes.append(axis)
        for axis, index in enumerate(self.indices[ellipsis_index + 1 :], ellipsis_index + 1):
            if index == slice(None):
                continue
            axes.append(axis - len(self.indices))
        return axes

indices instance-attribute

unique_indices = field(metadata={'static': True}) class-attribute instance-attribute

out_structure property

indexed_axes property

Returns the list of axes for which an indexing is performed.

for an indexing of (slice(None), 3, ..., jnp.array([1, 2])),

it returns [1, -1].

__init__(indices, *, in_structure, out_structure=None, unique_indices=None, _out_structure=None)

Source code in src/furax/core/_indices.py
def __init__(
    self,
    indices: (
        int
        | slice
        | Bool[Array, '...']
        | Integer[Array, '...']
        | tuple[int | slice | Bool[Array, '...'] | Integer[Array, '...'] | EllipsisType, ...]
    ),
    *,
    in_structure: PyTree[jax.ShapeDtypeStruct],
    out_structure: PyTree[jax.ShapeDtypeStruct] | None = None,
    unique_indices: bool | None = None,
    _out_structure: PyTree[jax.ShapeDtypeStruct] | None = None,
) -> None:
    # Support JAX unflattening which uses the field name _out_structure
    if _out_structure is not None:
        out_structure = _out_structure

    if not isinstance(indices, tuple):
        indices = (indices,)

    # When _out_structure is provided, we're being called from JAX unflattening
    # and should skip normalization/validation
    if _out_structure is None:
        self._check_indices(indices)

        if unique_indices is None:
            if all(
                isinstance(_, int | slice | EllipsisType)
                or isinstance(_, Array)
                and _.dtype == bool
                for _ in indices
            ):
                unique_indices = True
            else:
                unique_indices = False

        if out_structure is None and any(
            isinstance(_, Array) and _.dtype == bool for _ in indices
        ):
            raise ValueError(
                'The output structure must be specified when the indices are determined using a '
                'boolean array.'
            )

        if out_structure is None:
            # Compute output structure manually
            def temp_mv(x):  # type: ignore[no-untyped-def]
                return jax.tree.map(lambda leaf: leaf[indices], x)

            out_structure = jax.eval_shape(temp_mv, in_structure)

    object.__setattr__(self, 'indices', indices)
    object.__setattr__(self, '_out_structure', out_structure)
    object.__setattr__(self, 'unique_indices', unique_indices)
    object.__setattr__(self, 'in_structure', in_structure)

mv(x)

Source code in src/furax/core/_indices.py
def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
    return jax.tree.map(lambda leaf: leaf[self.indices], x)

reduce()

Source code in src/furax/core/_indices.py
def reduce(self) -> AbstractLinearOperator:
    if len(self.indexed_axes) == 0:
        return IdentityOperator(in_structure=self.in_structure)
    return self

furax.core.MaskOperator dataclass

Bases: AbstractLinearOperator

Operator that zeros out values according to a boolean mask: M(x) = x * mask.

The mask is symmetric and idempotent (M @ M = M). A True value means the data point is valid and preserved; False values are set to zero.

The mask is bit-packed internally to save memory. Use from_boolean_mask() to create from a standard boolean array.

Attributes:

  • mask (PyTree[UInt8[Array, ...]]) –

    Bit-packed mask as a pytree matching in_structure.

Methods:

  • __post_init__
  • from_boolean_mask

    Create a MaskOperator from a boolean mask.

  • to_boolean_mask

    Return the unpacked boolean mask as a pytree matching in_structure.

  • complement

    Return the complementary mask: valid where this one is invalid, and vice versa.

  • restrict

    Return a new MaskOperator with samples additionally masked out where condition is False.

  • mv
Source code in src/furax/core/_mask.py
@symmetric
@idempotent
class MaskOperator(AbstractLinearOperator):
    """Operator that zeros out values according to a boolean mask: M(x) = x * mask.

    The mask is symmetric and idempotent (M @ M = M). A True value means the
    data point is valid and preserved; False values are set to zero.

    The mask is bit-packed internally to save memory. Use ``from_boolean_mask()``
    to create from a standard boolean array.

    Attributes:
        mask: Bit-packed mask as a pytree matching ``in_structure``.
    """

    mask: PyTree[UInt8[Array, '...']]
    """Bit-packed sample mask as a pytree matching ``in_structure``."""

    def __post_init__(self) -> None:
        if not all(leaf.dtype == jnp.uint8 for leaf in jax.tree.leaves(self.mask)):
            msg = (
                'Expected boolean masks of unsigned byte data type. '
                'You might be looking for the `MaskOperator.from_boolean_mask()` factory.'
            )
            raise ValueError(msg)

    @classmethod
    def from_boolean_mask(
        cls,
        boolean_mask: PyTree[Bool[Array, '...']],
        *,
        in_structure: PyTree[jax.ShapeDtypeStruct],
    ) -> Self:
        """Create a MaskOperator from a boolean mask.

        Args:
            boolean_mask: Either a single boolean array (broadcast to all leaves of
                ``in_structure``) or a pytree of boolean arrays matching the structure
                of ``in_structure``.
            in_structure: The pytree structure of the operator's input.
        """
        mask_treedef = jax.tree.structure(boolean_mask)
        struct_treedef = jax.tree.structure(in_structure)

        if mask_treedef == struct_treedef:  # type: ignore[operator]
            # Pytree of masks matching in_structure: pack each leaf
            packed_mask = jax.tree.map(_check_and_pack, boolean_mask, in_structure)
        else:
            # Single mask broadcast to all leaves
            packed_mask = jax.tree.map(
                lambda struct_leaf: _check_and_pack(boolean_mask, struct_leaf),
                in_structure,
            )

        return cls(mask=packed_mask, in_structure=in_structure)

    def to_boolean_mask(self) -> PyTree[Bool[Array, '...']]:
        """Return the unpacked boolean mask as a pytree matching ``in_structure``."""
        return jax.tree.map(
            lambda m, s: jnp.unpackbits(m, axis=-1, count=s.shape[-1]).astype(bool),
            self.mask,
            self.in_structure,
        )

    def complement(self) -> 'MaskOperator':
        """Return the complementary mask: valid where this one is invalid, and vice versa.

        Computed directly on the packed bytes via bitwise NOT (no unpack/repack). Padding bits in
        the last byte are flipped too, but ``to_boolean_mask`` discards them (it truncates to the
        sample count), so they never surface.
        """
        flipped = jax.tree.map(jnp.bitwise_not, self.mask)
        return MaskOperator(flipped, in_structure=self.in_structure)

    def restrict(self, condition: Bool[Array, '...']) -> 'MaskOperator':
        """Return a new MaskOperator with samples additionally masked out where ``condition`` is False.

        A scalar ``condition`` gates the whole operator on or off.
        """
        gated = jax.tree.map(lambda m: m & condition, self.to_boolean_mask())
        return MaskOperator.from_boolean_mask(gated, in_structure=self.in_structure)

    def mv(self, x: PyTree[Inexact[Array, '...']]) -> PyTree[Inexact[Array, '...']]:
        boolean_mask = self.to_boolean_mask()
        return jax.tree.map(lambda m, leaf: jnp.where(m, leaf, 0), boolean_mask, x)

mask instance-attribute

Bit-packed sample mask as a pytree matching in_structure.

__post_init__()

Source code in src/furax/core/_mask.py
def __post_init__(self) -> None:
    if not all(leaf.dtype == jnp.uint8 for leaf in jax.tree.leaves(self.mask)):
        msg = (
            'Expected boolean masks of unsigned byte data type. '
            'You might be looking for the `MaskOperator.from_boolean_mask()` factory.'
        )
        raise ValueError(msg)

from_boolean_mask(boolean_mask, *, in_structure) classmethod

Create a MaskOperator from a boolean mask.

Parameters:

  • boolean_mask (PyTree[Bool[Array, ...]]) –

    Either a single boolean array (broadcast to all leaves of in_structure) or a pytree of boolean arrays matching the structure of in_structure.

  • in_structure (PyTree[ShapeDtypeStruct]) –

    The pytree structure of the operator's input.

Source code in src/furax/core/_mask.py
@classmethod
def from_boolean_mask(
    cls,
    boolean_mask: PyTree[Bool[Array, '...']],
    *,
    in_structure: PyTree[jax.ShapeDtypeStruct],
) -> Self:
    """Create a MaskOperator from a boolean mask.

    Args:
        boolean_mask: Either a single boolean array (broadcast to all leaves of
            ``in_structure``) or a pytree of boolean arrays matching the structure
            of ``in_structure``.
        in_structure: The pytree structure of the operator's input.
    """
    mask_treedef = jax.tree.structure(boolean_mask)
    struct_treedef = jax.tree.structure(in_structure)

    if mask_treedef == struct_treedef:  # type: ignore[operator]
        # Pytree of masks matching in_structure: pack each leaf
        packed_mask = jax.tree.map(_check_and_pack, boolean_mask, in_structure)
    else:
        # Single mask broadcast to all leaves
        packed_mask = jax.tree.map(
            lambda struct_leaf: _check_and_pack(boolean_mask, struct_leaf),
            in_structure,
        )

    return cls(mask=packed_mask, in_structure=in_structure)

to_boolean_mask()

Return the unpacked boolean mask as a pytree matching in_structure.

Source code in src/furax/core/_mask.py
def to_boolean_mask(self) -> PyTree[Bool[Array, '...']]:
    """Return the unpacked boolean mask as a pytree matching ``in_structure``."""
    return jax.tree.map(
        lambda m, s: jnp.unpackbits(m, axis=-1, count=s.shape[-1]).astype(bool),
        self.mask,
        self.in_structure,
    )

complement()

Return the complementary mask: valid where this one is invalid, and vice versa.

Computed directly on the packed bytes via bitwise NOT (no unpack/repack). Padding bits in the last byte are flipped too, but to_boolean_mask discards them (it truncates to the sample count), so they never surface.

Source code in src/furax/core/_mask.py
def complement(self) -> 'MaskOperator':
    """Return the complementary mask: valid where this one is invalid, and vice versa.

    Computed directly on the packed bytes via bitwise NOT (no unpack/repack). Padding bits in
    the last byte are flipped too, but ``to_boolean_mask`` discards them (it truncates to the
    sample count), so they never surface.
    """
    flipped = jax.tree.map(jnp.bitwise_not, self.mask)
    return MaskOperator(flipped, in_structure=self.in_structure)

restrict(condition)

Return a new MaskOperator with samples additionally masked out where condition is False.

A scalar condition gates the whole operator on or off.

Source code in src/furax/core/_mask.py
def restrict(self, condition: Bool[Array, '...']) -> 'MaskOperator':
    """Return a new MaskOperator with samples additionally masked out where ``condition`` is False.

    A scalar ``condition`` gates the whole operator on or off.
    """
    gated = jax.tree.map(lambda m: m & condition, self.to_boolean_mask())
    return MaskOperator.from_boolean_mask(gated, in_structure=self.in_structure)

mv(x)

Source code in src/furax/core/_mask.py
def mv(self, x: PyTree[Inexact[Array, '...']]) -> PyTree[Inexact[Array, '...']]:
    boolean_mask = self.to_boolean_mask()
    return jax.tree.map(lambda m, leaf: jnp.where(m, leaf, 0), boolean_mask, x)

furax.core.SumOperator dataclass

Bases: AbstractLinearOperator

Operator that sums pytree leaves along specified axes.

Follows NumPy conventions for axis specification
  • None: sum all dimensions
  • int: sum a single axis
  • (): no reduction (identity)
  • tuple[int, ...]: sum multiple axes

The axis specification can be a pytree matching the input structure to apply different reductions to different leaves.

Attributes:

  • axis (PyTree[tuple[int, ...] | None]) –

    The axes along which to sum, as a pytree or single value.

Examples:

To sum along every dimension of all leaves:

>>> from furax.tree import as_structure
>>> x = {'a': jnp.array([[0, 0, 0], [1, 1, 1]]),
...      'b': jnp.array([[1, 1, 1], [2, 2, 2]])}
>>> op = SumOperator(axis=None, in_structure=as_structure(x))
>>> op(x)
{'a': Array(3, dtype=int32), 'b': Array(9, dtype=int32)}

To sum along every dimension of only one leaf:

>>> op = SumOperator(axis={'a': None, 'b': ()}, in_structure=as_structure(x))
>>> op(x)
{'a': Array(3, dtype=int32), 'b': Array([[1, 1, 1], [2, 2, 2]], dtype=int32)}

To sum the leaves along different axes:

>>> op = SumOperator(axis={'a': 0, 'b': 1}, in_structure=as_structure(x))
>>> op(x)
{'a': Array([1, 1, 1], dtype=int32), 'b': Array([3, 6], dtype=int32)}

Methods:

Source code in src/furax/core/_sum.py
class SumOperator(AbstractLinearOperator):
    """Operator that sums pytree leaves along specified axes.

    Follows NumPy conventions for axis specification:
        - ``None``: sum all dimensions
        - ``int``: sum a single axis
        - ``()``: no reduction (identity)
        - ``tuple[int, ...]``: sum multiple axes

    The axis specification can be a pytree matching the input structure to
    apply different reductions to different leaves.

    Attributes:
        axis: The axes along which to sum, as a pytree or single value.

    Examples:
        To sum along every dimension of all leaves:

        >>> from furax.tree import as_structure
        >>> x = {'a': jnp.array([[0, 0, 0], [1, 1, 1]]),
        ...      'b': jnp.array([[1, 1, 1], [2, 2, 2]])}
        >>> op = SumOperator(axis=None, in_structure=as_structure(x))
        >>> op(x)
        {'a': Array(3, dtype=int32), 'b': Array(9, dtype=int32)}

        To sum along every dimension of only one leaf:

        >>> op = SumOperator(axis={'a': None, 'b': ()}, in_structure=as_structure(x))
        >>> op(x)
        {'a': Array(3, dtype=int32), 'b': Array([[1, 1, 1], [2, 2, 2]], dtype=int32)}

        To sum the leaves along different axes:

        >>> op = SumOperator(axis={'a': 0, 'b': 1}, in_structure=as_structure(x))
        >>> op(x)
        {'a': Array([1, 1, 1], dtype=int32), 'b': Array([3, 6], dtype=int32)}
    """

    axis: PyTree[tuple[int, ...] | None] = field(metadata={'static': True})

    def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
        return jax.tree.map(
            self._sum_leaf,
            self.axis,
            x,
            is_leaf=lambda leaf: (
                leaf is None
                or isinstance(leaf, tuple)
                and all(isinstance(element, int) for element in leaf)
            ),
        )

    @staticmethod
    def _sum_leaf(
        axes: tuple[int, ...] | None, leaf: PyTree[Inexact[Array, ' _a']]
    ) -> PyTree[Inexact[Array, ' _b']]:
        if isinstance(axes, tuple) and len(axes) == 0:
            return leaf
        return jax.tree.map(lambda leaf: jnp.sum(leaf, axis=axes), leaf)

axis = field(metadata={'static': True}) class-attribute instance-attribute

mv(x)

Source code in src/furax/core/_sum.py
def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
    return jax.tree.map(
        self._sum_leaf,
        self.axis,
        x,
        is_leaf=lambda leaf: (
            leaf is None
            or isinstance(leaf, tuple)
            and all(isinstance(element, int) for element in leaf)
        ),
    )

furax.core.SymmetricBandToeplitzOperator

Bases: AbstractLinearOperator

Operator for symmetric band Toeplitz convolution.

A Toeplitz matrix has constant diagonals. This operator is symmetric and exploits the band structure for efficient computation. For multidimensional band values, the operator is block diagonal.

Available methods (N = matrix size, K = number of bands): - dense: dense matrix multiplication - direct: direct convolution - fft: FFT on the whole input - overlap_save_sequential: sequential FFT on chunks - overlap_save_parallel: batched FFT on chunks (default)

============================ ========= ====== Method Time Memory ============================ ========= ====== dense O(N^2) N^2 direct O(NK) 2N fft O(N log N) 3N overlap_save_sequential O(N log K) 2N overlap_save_parallel O(N log K) 4N ============================ ========= ======

Attributes:

  • band_values (Float[Array, ...]) –

    The band values (first element is the diagonal).

  • method (str) –

    The computation method.

  • fft_size (int | None) –

    FFT size for the fft and overlap methods.

Examples:

>>> tod = jnp.ones((2, 5))
>>> op = SymmetricBandToeplitzOperator(
...     jnp.array([[1., 0.5], [1, 0.25]]),
...     in_structure=jax.ShapeDtypeStruct(tod.shape, tod.dtype))
>>> op(tod)
Array([[1.5 , 2.  , 2.  , 2.  , 1.5 ],
       [1.25, 1.5 , 1.5 , 1.5 , 1.25]], dtype=float64)
>>> op.as_matrix()
Array([[1.  , 0.5 , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  ],
       [0.5 , 1.  , 0.5 , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  ],
       [0.  , 0.5 , 1.  , 0.5 , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  ],
       [0.  , 0.  , 0.5 , 1.  , 0.5 , 0.  , 0.  , 0.  , 0.  , 0.  ],
       [0.  , 0.  , 0.  , 0.5 , 1.  , 0.  , 0.  , 0.  , 0.  , 0.  ],
       [0.  , 0.  , 0.  , 0.  , 0.  , 1.  , 0.25, 0.  , 0.  , 0.  ],
       [0.  , 0.  , 0.  , 0.  , 0.  , 0.25, 1.  , 0.25, 0.  , 0.  ],
       [0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.25, 1.  , 0.25, 0.  ],
       [0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.25, 1.  , 0.25],
       [0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.25, 1.  ]],      dtype=float64)

Methods:

Source code in src/furax/core/_toeplitz.py
@symmetric
class SymmetricBandToeplitzOperator(AbstractLinearOperator):
    """Operator for symmetric band Toeplitz convolution.

    A Toeplitz matrix has constant diagonals. This operator is symmetric and
    exploits the band structure for efficient computation. For multidimensional
    band values, the operator is block diagonal.

    Available methods (N = matrix size, K = number of bands):
        - ``dense``: dense matrix multiplication
        - ``direct``: direct convolution
        - ``fft``: FFT on the whole input
        - ``overlap_save_sequential``: sequential FFT on chunks
        - ``overlap_save_parallel``: batched FFT on chunks (default)

    ============================  =========  ======
    Method                        Time       Memory
    ============================  =========  ======
    ``dense``                     O(N^2)     N^2
    ``direct``                    O(NK)      2N
    ``fft``                       O(N log N) 3N
    ``overlap_save_sequential``   O(N log K) 2N
    ``overlap_save_parallel``     O(N log K) 4N
    ============================  =========  ======

    Attributes:
        band_values: The band values (first element is the diagonal).
        method: The computation method.
        fft_size: FFT size for the fft and overlap methods.

    Examples:
        >>> tod = jnp.ones((2, 5))
        >>> op = SymmetricBandToeplitzOperator(
        ...     jnp.array([[1., 0.5], [1, 0.25]]),
        ...     in_structure=jax.ShapeDtypeStruct(tod.shape, tod.dtype))
        >>> op(tod)
        Array([[1.5 , 2.  , 2.  , 2.  , 1.5 ],
               [1.25, 1.5 , 1.5 , 1.5 , 1.25]], dtype=float64)
        >>> op.as_matrix()
        Array([[1.  , 0.5 , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  ],
               [0.5 , 1.  , 0.5 , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  ],
               [0.  , 0.5 , 1.  , 0.5 , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  ],
               [0.  , 0.  , 0.5 , 1.  , 0.5 , 0.  , 0.  , 0.  , 0.  , 0.  ],
               [0.  , 0.  , 0.  , 0.5 , 1.  , 0.  , 0.  , 0.  , 0.  , 0.  ],
               [0.  , 0.  , 0.  , 0.  , 0.  , 1.  , 0.25, 0.  , 0.  , 0.  ],
               [0.  , 0.  , 0.  , 0.  , 0.  , 0.25, 1.  , 0.25, 0.  , 0.  ],
               [0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.25, 1.  , 0.25, 0.  ],
               [0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.25, 1.  , 0.25],
               [0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.25, 1.  ]],      dtype=float64)
    """

    METHODS: ClassVar[tuple[str, ...]] = (
        'dense',
        'direct',
        'fft',
        'overlap_save_parallel',
        'overlap_save_sequential',
    )
    band_values: Float[Array, '...']
    method: str = field(metadata={'static': True})
    fft_size: int | None = field(metadata={'static': True})

    def __init__(
        self,
        band_values: Float[Array, ' a'],
        *,
        in_structure: PyTree[jax.ShapeDtypeStruct],
        method: str = 'overlap_save_parallel',
        fft_size: int | None = None,
    ) -> None:
        if method not in self.METHODS:
            raise ValueError(f'Invalid method {method}. Choose from: {", ".join(self.METHODS)}')

        band_number = 2 * band_values.shape[-1] - 1
        if fft_size is not None:
            if method not in ('fft', 'overlap_save_parallel', 'overlap_save_sequential'):
                raise ValueError('The FFT size is only used by the fft and overlap methods.')
            if fft_size < band_number:
                raise ValueError('The FFT size should not be less than the number of bands.')

        elif method == 'fft':
            signal_length = in_structure.shape[-1]
            padded_length = signal_length + band_number - 1
            fft_size = self._get_next_power_of_two(padded_length)

        elif method.startswith('overlap_save'):
            overlap = band_number - 1  # 2 * (K - 1) where K = band_values.shape[-1]
            fft_size = self._get_optimal_fft_size(overlap)

        object.__setattr__(self, 'band_values', band_values)
        object.__setattr__(self, 'method', method)
        object.__setattr__(self, 'fft_size', fft_size)
        object.__setattr__(self, 'in_structure', in_structure)

    @staticmethod
    def _get_next_power_of_two(n: int) -> int:
        """Return the smallest power of 2 >= n."""
        if n <= 1:
            return 1
        return int(2 ** np.ceil(np.log2(n)))

    @staticmethod
    def _get_optimal_fft_size(overlap: int) -> int:
        """Return the power-of-2 FFT size that minimizes the OLS computation cost.

        The cost per sample is proportional to F*log2(F) / (F - overlap), where F
        is the FFT size and (F - overlap) is the number of new samples per block.
        """
        if overlap <= 0:
            return 2
        min_power = int(np.ceil(np.log2(overlap + 1)))
        best_f: int = 2**min_power
        best_cost = best_f * min_power / (best_f - overlap)
        for p in range(min_power + 1, min_power + 30):
            f = 2**p
            cost = f * p / (f - overlap)
            if cost < best_cost:
                best_cost = cost
                best_f = f
            else:
                break
        return best_f

    def _get_func(self) -> Callable[[Array, Array], Array]:
        if self.method == 'dense':
            return self._apply_dense
        if self.method == 'direct':
            return self._apply_direct
        if self.method == 'fft':
            return self._apply_fft
        if self.method == 'overlap_save_parallel':
            return self._apply_overlap_save_parallel
        if self.method == 'overlap_save_sequential':
            return self._apply_overlap_save_sequential

        raise NotImplementedError

    def _apply_dense(self, x: Array, band_values: Array) -> Array:
        matrix = dense_symmetric_band_toeplitz(x.shape[-1], band_values)
        return matrix @ x

    def _apply_direct(self, x: Array, band_values: Array) -> Array:
        kernel = self._get_kernel(band_values)
        half_band_width = kernel.size // 2
        return jnp.convolve(jnp.pad(x, (half_band_width, half_band_width)), kernel, mode='valid')

    def _apply_fft(self, x: Array, band_values: Array) -> Array:
        assert self.fft_size is not None
        kernel = self._get_kernel(band_values)
        half_band_width = kernel.size // 2
        signal_length = x.shape[-1]
        H = jnp.fft.rfft(kernel, n=self.fft_size)
        x_padded = jnp.pad(x, (0, 2 * half_band_width), mode='constant')
        X_padded = jnp.fft.rfft(x_padded, n=self.fft_size)
        Y_padded = jnp.fft.irfft(X_padded * H, n=self.fft_size)
        if half_band_width == 0:
            return Y_padded[:signal_length]
        return Y_padded[half_band_width : half_band_width + signal_length]

    def _apply_overlap_save_parallel(self, x: Array, band_values: Array) -> Array:
        """Overlap-and-save with batched rfft via vmap. All blocks processed in parallel."""
        assert self.fft_size is not None
        kernel = self._get_kernel(band_values)
        half_band_width = kernel.size // 2
        H = jnp.fft.rfft(kernel, n=self.fft_size)
        l = x.shape[-1]
        overlap = 2 * half_band_width
        step_size = self.fft_size - overlap
        nblock = int(np.ceil((l + overlap) / step_size))
        total_length = (nblock - 1) * step_size + self.fft_size
        x_padding_start = overlap
        x_padding_end = total_length - overlap - l
        x_padded = jnp.pad(x, (x_padding_start, x_padding_end), mode='constant')
        y_length = l + x_padding_end

        block_starts = jnp.arange(nblock) * step_size

        def extract_block(start_idx: Array) -> Array:
            return lax.dynamic_slice(x_padded, (start_idx,), (self.fft_size,))

        blocks = jax.vmap(extract_block)(block_starts)
        blocks_fft = jnp.fft.rfft(blocks, n=self.fft_size, axis=-1)
        blocks_filtered = jnp.fft.irfft(blocks_fft * H[None, :], n=self.fft_size, axis=-1)
        y_full = blocks_filtered[:, overlap:].ravel()[:y_length]

        return y_full[half_band_width : half_band_width + l]

    def _apply_overlap_save_sequential(self, x: Array, band_values: Array) -> Array:
        """Overlap-and-save with sequential rfft via fori_loop. Low memory usage."""
        assert self.fft_size is not None
        kernel = self._get_kernel(band_values)
        half_band_width = kernel.size // 2
        H = jnp.fft.rfft(kernel, n=self.fft_size)
        l = x.shape[-1]
        overlap = 2 * half_band_width
        step_size = self.fft_size - overlap
        nblock = int(np.ceil((l + overlap) / step_size))
        total_length = (nblock - 1) * step_size + self.fft_size
        x_padding_start = overlap
        x_padding_end = total_length - overlap - l
        x_padded = jnp.pad(x, (x_padding_start, x_padding_end), mode='constant')
        y = jnp.zeros(l + x_padding_end, dtype=x.dtype)

        def func(iblock: int, y: Array) -> Array:
            position = iblock * step_size
            x_block = lax.dynamic_slice(x_padded, (position,), (self.fft_size,))
            X = jnp.fft.rfft(x_block, n=self.fft_size)
            y_block = jnp.fft.irfft(X * H, n=self.fft_size)
            y = lax.dynamic_update_slice(
                y, lax.dynamic_slice(y_block, (overlap,), (step_size,)), (position,)
            )
            return y

        y = lax.fori_loop(0, nblock, func, y)
        return y[half_band_width : half_band_width + l]  # type: ignore[no-any-return]

    def _get_kernel(self, band_values: Array) -> Array:
        """[4, 3, 2, 1] -> [1, 2, 3, 4, 3, 2, 1]"""
        return jnp.concatenate((band_values[-1:0:-1], band_values))

    def mv(self, x: Float[Array, '...']) -> Float[Array, '...']:
        func = jnp.vectorize(self._get_func(), signature='(n),(k)->(n)')
        return func(x, self.band_values)  # type: ignore[no-any-return]

    def as_matrix(self) -> Inexact[Array, 'a a']:
        @partial(jnp.vectorize, signature='(n),(k)->(n,n)')
        def func(x: Array, band_values: Array) -> Array:
            return dense_symmetric_band_toeplitz(x.size, band_values)

        x = jnp.zeros(self.in_structure.shape, self.in_structure.dtype)
        blocks: Array = func(x, self.band_values)
        if blocks.ndim > 2:
            blocks = blocks.reshape(-1, blocks.shape[-1], blocks.shape[-1])
            matrix: Array = jsl.block_diag(*blocks)
            return matrix
        return blocks

METHODS = ('dense', 'direct', 'fft', 'overlap_save_parallel', 'overlap_save_sequential') class-attribute

band_values instance-attribute

method = field(metadata={'static': True}) class-attribute instance-attribute

fft_size = field(metadata={'static': True}) class-attribute instance-attribute

__init__(band_values, *, in_structure, method='overlap_save_parallel', fft_size=None)

Source code in src/furax/core/_toeplitz.py
def __init__(
    self,
    band_values: Float[Array, ' a'],
    *,
    in_structure: PyTree[jax.ShapeDtypeStruct],
    method: str = 'overlap_save_parallel',
    fft_size: int | None = None,
) -> None:
    if method not in self.METHODS:
        raise ValueError(f'Invalid method {method}. Choose from: {", ".join(self.METHODS)}')

    band_number = 2 * band_values.shape[-1] - 1
    if fft_size is not None:
        if method not in ('fft', 'overlap_save_parallel', 'overlap_save_sequential'):
            raise ValueError('The FFT size is only used by the fft and overlap methods.')
        if fft_size < band_number:
            raise ValueError('The FFT size should not be less than the number of bands.')

    elif method == 'fft':
        signal_length = in_structure.shape[-1]
        padded_length = signal_length + band_number - 1
        fft_size = self._get_next_power_of_two(padded_length)

    elif method.startswith('overlap_save'):
        overlap = band_number - 1  # 2 * (K - 1) where K = band_values.shape[-1]
        fft_size = self._get_optimal_fft_size(overlap)

    object.__setattr__(self, 'band_values', band_values)
    object.__setattr__(self, 'method', method)
    object.__setattr__(self, 'fft_size', fft_size)
    object.__setattr__(self, 'in_structure', in_structure)

mv(x)

Source code in src/furax/core/_toeplitz.py
def mv(self, x: Float[Array, '...']) -> Float[Array, '...']:
    func = jnp.vectorize(self._get_func(), signature='(n),(k)->(n)')
    return func(x, self.band_values)  # type: ignore[no-any-return]

as_matrix()

Source code in src/furax/core/_toeplitz.py
def as_matrix(self) -> Inexact[Array, 'a a']:
    @partial(jnp.vectorize, signature='(n),(k)->(n,n)')
    def func(x: Array, band_values: Array) -> Array:
        return dense_symmetric_band_toeplitz(x.size, band_values)

    x = jnp.zeros(self.in_structure.shape, self.in_structure.dtype)
    blocks: Array = func(x, self.band_values)
    if blocks.ndim > 2:
        blocks = blocks.reshape(-1, blocks.shape[-1], blocks.shape[-1])
        matrix: Array = jsl.block_diag(*blocks)
        return matrix
    return blocks

furax.core.TreeOperator

Bases: AbstractLinearOperator

Operator defined by a generalized matrix as a pytree of pytrees.

More memory-efficient than dense matrices when the structure allows XLA optimizations (symmetries, zeros, shared elements, broadcasting).

The structure of the generalized matrix is the tree product: outer_treedef x inner_treedef, analogous to a matrix where rows are represented by an outer tree and columns by the inner tree.

Leaves within the same row must be broadcastable; no such constraint applies across rows.

Attributes:

Examples:

To represent the Mueller Matrix of a quarter-wave plate with a vertical fast-axis:

>>> from furax.obs.stokes import StokesIQUV
>>> op = TreeOperator(
...     StokesIQUV(
            StokesIQUV(1, 0, 0,  0),
            StokesIQUV(0, 1, 0,  0),
            StokesIQUV(0, 0, 0, -1),
            StokesIQUV(0, 0, 1,  0),
        ),
        in_structure=StokesIQUV.structure_for((), jnp.float32)
... )
>>> op.as_matrix()
Array([[ 1.,  0.,  0.,  0.],
       [ 0.,  1.,  0.,  0.],
       [ 0.,  0.,  0., -1.],
       [ 0.,  0.,  1.,  0.]], dtype=float32)
>>> op(StokesIQUV(1., 1., 1., 1.))
StokesIQUV(i=1.0, q=1.0, u=-1.0, v=1.0)

Methods:

Source code in src/furax/core/_trees.py
class TreeOperator(AbstractLinearOperator):
    """Operator defined by a generalized matrix as a pytree of pytrees.

    More memory-efficient than dense matrices when the structure allows XLA
    optimizations (symmetries, zeros, shared elements, broadcasting).

    The structure of the generalized matrix is the tree product: outer_treedef x inner_treedef,
    analogous to a matrix where rows are represented by an outer tree and columns by the inner tree.

    Leaves within the same row must be broadcastable; no such constraint applies across rows.

    Attributes:
        tree: The generalized matrix as a pytree of pytrees.
        outer_treedef: PyTreeDef for rows.
        inner_treedef: PyTreeDef for columns.
        tree_shape: (num_rows, num_cols) in terms of tree leaves.

    Examples:
        To represent the Mueller Matrix of a quarter-wave plate with a vertical fast-axis:

        >>> from furax.obs.stokes import StokesIQUV
        >>> op = TreeOperator(
        ...     StokesIQUV(
                    StokesIQUV(1, 0, 0,  0),
                    StokesIQUV(0, 1, 0,  0),
                    StokesIQUV(0, 0, 0, -1),
                    StokesIQUV(0, 0, 1,  0),
                ),
                in_structure=StokesIQUV.structure_for((), jnp.float32)
        ... )
        >>> op.as_matrix()
        Array([[ 1.,  0.,  0.,  0.],
               [ 0.,  1.,  0.,  0.],
               [ 0.,  0.,  0., -1.],
               [ 0.,  0.,  1.,  0.]], dtype=float32)
        >>> op(StokesIQUV(1., 1., 1., 1.))
        StokesIQUV(i=1.0, q=1.0, u=-1.0, v=1.0)
    """

    tree: PyTree[Array]
    inner_treedef: PyTreeDef = field(metadata={'static': True})
    outer_treedef: PyTreeDef = field(metadata={'static': True})

    def __init__(
        self,
        tree: PyTree[PyTree[Any]],
        *,
        in_structure: PyTree[jax.ShapeDtypeStruct],
        inner_treedef: PyTreeDef | None = None,
        outer_treedef: PyTreeDef | None = None,
    ) -> None:
        if inner_treedef is None:
            inner_treedef = jax.tree.structure(in_structure)
        if outer_treedef is None:
            outer_treedef = _get_outer_treedef(in_structure, tree)
        object.__setattr__(self, 'tree', tree)
        object.__setattr__(self, 'inner_treedef', inner_treedef)
        object.__setattr__(self, 'outer_treedef', outer_treedef)
        object.__setattr__(self, 'in_structure', in_structure)

    @property
    def tree_shape(self) -> tuple[int, int]:
        """Return the number of leaves of the outer and inner structures."""
        return self.outer_treedef.num_leaves, self.inner_treedef.num_leaves

    def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
        return matvec(self.outer_treedef, self.tree, x)

    def transpose(self) -> AbstractLinearOperator:
        transposed_tree = jax.tree.transpose(self.outer_treedef, self.inner_treedef, self.tree)
        return TreeOperator(transposed_tree, in_structure=self.out_structure)

    def inverse(self) -> AbstractLinearOperator:
        dense = _tree_to_dense(self.outer_treedef, self.inner_treedef, self.tree)
        dense_pinv = jnp.linalg.pinv(dense)
        tree = _dense_to_tree(self.inner_treedef, self.outer_treedef, dense_pinv)
        return TreeOperator(tree, in_structure=self.out_structure)

tree instance-attribute

inner_treedef = field(metadata={'static': True}) class-attribute instance-attribute

outer_treedef = field(metadata={'static': True}) class-attribute instance-attribute

tree_shape property

Return the number of leaves of the outer and inner structures.

__init__(tree, *, in_structure, inner_treedef=None, outer_treedef=None)

Source code in src/furax/core/_trees.py
def __init__(
    self,
    tree: PyTree[PyTree[Any]],
    *,
    in_structure: PyTree[jax.ShapeDtypeStruct],
    inner_treedef: PyTreeDef | None = None,
    outer_treedef: PyTreeDef | None = None,
) -> None:
    if inner_treedef is None:
        inner_treedef = jax.tree.structure(in_structure)
    if outer_treedef is None:
        outer_treedef = _get_outer_treedef(in_structure, tree)
    object.__setattr__(self, 'tree', tree)
    object.__setattr__(self, 'inner_treedef', inner_treedef)
    object.__setattr__(self, 'outer_treedef', outer_treedef)
    object.__setattr__(self, 'in_structure', in_structure)

mv(x)

Source code in src/furax/core/_trees.py
def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
    return matvec(self.outer_treedef, self.tree, x)

transpose()

Source code in src/furax/core/_trees.py
def transpose(self) -> AbstractLinearOperator:
    transposed_tree = jax.tree.transpose(self.outer_treedef, self.inner_treedef, self.tree)
    return TreeOperator(transposed_tree, in_structure=self.out_structure)

inverse()

Source code in src/furax/core/_trees.py
def inverse(self) -> AbstractLinearOperator:
    dense = _tree_to_dense(self.outer_treedef, self.inner_treedef, self.tree)
    dense_pinv = jnp.linalg.pinv(dense)
    tree = _dense_to_tree(self.inner_treedef, self.outer_treedef, dense_pinv)
    return TreeOperator(tree, in_structure=self.out_structure)

furax.core.asoperator(func, *, in_structure, **keywords)

Wraps a function into an operator.

Parameters:

  • func (Callable[..., Any]) –

    The function to wrap.

  • in_structure (PyTree[ShapeDtypeStruct]) –

    The structure (shapes and dtypes) of the operator's input.

  • **keywords (Any, default: {} ) –

    Keyword arguments to pass to the function.

Usage

op = asoperator(lambda x: 2*x, in_structure=jax.ShapeDtypeStruct((), jnp.float32)) op.I(1.) Array(0.5, dtype=float32)

Returns:

Source code in src/furax/core/_base.py
def asoperator(
    func: Callable[..., Any],
    *,
    in_structure: PyTree[jax.ShapeDtypeStruct],
    **keywords: Any,
) -> AbstractLinearOperator:
    """Wraps a function into an operator.

    Args:
        func: The function to wrap.
        in_structure: The structure (shapes and dtypes) of the operator's input.
        **keywords: Keyword arguments to pass to the function.

    Usage:
        >>> op = asoperator(lambda x: 2*x, in_structure=jax.ShapeDtypeStruct((), jnp.float32))
        >>> op.I(1.)
        Array(0.5, dtype=float32)

    Returns:
        An operator wrapping the function.
    """

    class Operator(AbstractLinearOperator):
        def mv(self, x: PyTree[Inexact[Array, ' _a']]) -> PyTree[Inexact[Array, ' _b']]:
            return partial_func(x)

    if not hasattr(func, 'lower'):
        func = jax.jit(func)
    partial_func = Partial(func, **keywords)
    _check_params(partial_func)
    return Operator(in_structure=in_structure)

furax.core.diagonal(cls)

Mark an operator as diagonal (implies symmetric, which implies square).

Source code in src/furax/core/_base.py
def diagonal(cls: type[T]) -> type[T]:
    """Mark an operator as diagonal (implies symmetric, which implies square)."""
    symmetric(cls)
    cls.class_tags |= OperatorTag.DIAGONAL
    return cls

furax.core.idempotent(cls)

Mark an operator as idempotent, i.e. P @ P = P (implies square).

Source code in src/furax/core/_base.py
def idempotent(cls: type[T]) -> type[T]:
    """Mark an operator as idempotent, i.e. P @ P = P (implies square)."""
    square(cls)
    cls.class_tags |= OperatorTag.IDEMPOTENT
    return cls

furax.core.lower_triangular(cls)

Mark an operator as lower triangular (implies square).

Source code in src/furax/core/_base.py
def lower_triangular(cls: type[T]) -> type[T]:
    """Mark an operator as lower triangular (implies square)."""
    square(cls)
    cls.class_tags |= OperatorTag.LOWER_TRIANGULAR
    return cls

furax.core.negative_semidefinite(cls)

Mark an operator as negative semi-definite (implies square).

Source code in src/furax/core/_base.py
def negative_semidefinite(cls: type[T]) -> type[T]:
    """Mark an operator as negative semi-definite (implies square)."""
    square(cls)
    cls.class_tags |= OperatorTag.NEGATIVE_SEMIDEFINITE
    return cls

furax.core.orthogonal(cls)

Mark an operator as orthogonal (implies square).

Source code in src/furax/core/_base.py
def orthogonal(cls: type[T]) -> type[T]:
    """Mark an operator as orthogonal (implies square)."""
    square(cls)
    cls.class_tags |= OperatorTag.ORTHOGONAL
    cls.inverse = cls.transpose  # type: ignore[method-assign]
    return cls

furax.core.positive_semidefinite(cls)

Mark an operator as positive semi-definite (implies square).

Source code in src/furax/core/_base.py
def positive_semidefinite(cls: type[T]) -> type[T]:
    """Mark an operator as positive semi-definite (implies square)."""
    square(cls)
    cls.class_tags |= OperatorTag.POSITIVE_SEMIDEFINITE
    return cls

furax.core.square(cls)

Mark an operator as square.

Source code in src/furax/core/_base.py
def square(cls: type[T]) -> type[T]:
    """Mark an operator as square."""
    cls.class_tags |= OperatorTag.SQUARE
    cls.out_structure = property(lambda self: self.in_structure)  # type: ignore[assignment,method-assign]
    return cls

furax.core.symmetric(cls)

Mark an operator as symmetric (implies square).

Source code in src/furax/core/_base.py
def symmetric(cls: type[T]) -> type[T]:
    """Mark an operator as symmetric (implies square)."""
    square(cls)
    cls.class_tags |= OperatorTag.SYMMETRIC
    cls.transpose = lambda self: self  # type: ignore[method-assign]
    return cls

furax.core.tridiagonal(cls)

Mark an operator as tridiagonal (implies square).

Source code in src/furax/core/_base.py
def tridiagonal(cls: type[T]) -> type[T]:
    """Mark an operator as tridiagonal (implies square)."""
    square(cls)
    cls.class_tags |= OperatorTag.TRIDIAGONAL
    return cls

furax.core.upper_triangular(cls)

Mark an operator as upper triangular (implies square).

Source code in src/furax/core/_base.py
def upper_triangular(cls: type[T]) -> type[T]:
    """Mark an operator as upper triangular (implies square)."""
    square(cls)
    cls.class_tags |= OperatorTag.UPPER_TRIANGULAR
    return cls

furax.core.dense_symmetric_band_toeplitz(n, band_values)

Returns a dense Symmetric Band Toeplitz matrix.

Source code in src/furax/core/_toeplitz.py
def dense_symmetric_band_toeplitz(n: int, band_values: ArrayLike) -> Array:
    """Returns a dense Symmetric Band Toeplitz matrix."""
    band_values = jnp.asarray(band_values)
    output = jnp.zeros(n**2, dtype=band_values.dtype)
    band_width = band_values.size - 1
    for j in range(-band_width, band_width + 1):
        value = band_values[abs(j)]
        m = n - j
        if j >= 0:
            indices = j + jnp.arange(m) * (n + 1)
        else:
            indices = -n * j + jnp.arange(m) * (n + 1)
        output = output.at[indices].set(value)
    return output.reshape(n, n)

furax.core.rules

Classes:

Attributes:

T = TypeVar('T', bound=AbstractRule) module-attribute

COMPOSITION_RULE_REGISTRY = RuleRegistry[AbstractBinaryRule]() module-attribute

ADDITION_RULE_REGISTRY = RuleRegistry[AbstractBinaryRule]() module-attribute

NoReduction

Bases: BaseException

Raised when no algebraic reduction is applied.

Source code in src/furax/core/rules.py
class NoReduction(BaseException):
    """Raised when no algebraic reduction is applied."""

AbstractRule

The base class for all algebraic reduction rules.

Source code in src/furax/core/rules.py
class AbstractRule:
    """The base class for all algebraic reduction rules."""

RuleRegistry

Bases: Generic[T]

Registry for AbstractRules.

The rules are automatically registered when a class is encountered by the Python interpreter. The registry can be iterated over to retrieve the registered rules.

Methods:

Source code in src/furax/core/rules.py
class RuleRegistry(Generic[T]):
    """Registry for AbstractRules.

    The rules are automatically registered when a class is encountered by the Python interpreter.
    The registry can be iterated over to retrieve the registered rules.
    """

    def __init__(self) -> None:
        self._registry: list[T] = []

    def register(self, rule: T) -> None:
        """Register a rule.

        Args:
            rule: The rule to register.
        """
        self._registry.append(rule)

    def __iter__(self) -> Iterator[T]:
        return iter(self._registry)

__init__()

Source code in src/furax/core/rules.py
def __init__(self) -> None:
    self._registry: list[T] = []

register(rule)

Register a rule.

Parameters:

  • rule (T) –

    The rule to register.

Source code in src/furax/core/rules.py
def register(self, rule: T) -> None:
    """Register a rule.

    Args:
        rule: The rule to register.
    """
    self._registry.append(rule)

__iter__()

Source code in src/furax/core/rules.py
def __iter__(self) -> Iterator[T]:
    return iter(self._registry)

AbstractNaryRule

Bases: AbstractRule, ABC

A binary algebraic rule to reduce op1 @ op2 @ op3 @ ....

Methods:

  • apply

    Applies the algebraic rule to reduce the composition op1 @ op2 @ op3 @ ....

Attributes:

Source code in src/furax/core/rules.py
class AbstractNaryRule(AbstractRule, ABC):
    """A binary algebraic rule to reduce `op1 @ op2 @ op3 @ ...`."""

    operator_class: type[AbstractLinearOperator]

    @abstractmethod
    def apply(self, operands: list[AbstractLinearOperator]) -> list[AbstractLinearOperator]:
        """Applies the algebraic rule to reduce the composition `op1 @ op2 @ op3 @ ...`."""

operator_class instance-attribute

apply(operands) abstractmethod

Applies the algebraic rule to reduce the composition op1 @ op2 @ op3 @ ....

Source code in src/furax/core/rules.py
@abstractmethod
def apply(self, operands: list[AbstractLinearOperator]) -> list[AbstractLinearOperator]:
    """Applies the algebraic rule to reduce the composition `op1 @ op2 @ op3 @ ...`."""

AlgebraicReductionRule

Bases: AbstractNaryRule

N-ary rule to apply algebraic reductions.

Methods:

Source code in src/furax/core/rules.py
class AlgebraicReductionRule(AbstractNaryRule):
    """N-ary rule to apply algebraic reductions."""

    def apply(self, operands: list[AbstractLinearOperator]) -> list[AbstractLinearOperator]:
        if len(operands) < 2:
            return operands

        in_structure = operands[-1].in_structure

        identity_rule = IdentityRule()
        homothety_rule = HomothetyRule()

        operands = identity_rule.apply(operands)
        operands = homothety_rule.apply(operands)

        index = 0
        while index < len(operands) - 1:
            left, right = operands[index], operands[index + 1]

            for rule in COMPOSITION_RULE_REGISTRY:
                try:
                    rule.check(left, right)
                    new_ops = rule.apply(left, right)
                except NoReduction:
                    continue
                operands[index : index + 2] = new_ops

                # if the rule produces a HomothetyOperator, we deal with it first
                if any(isinstance(op, HomothetyOperator) for op in new_ops):
                    operands = homothety_rule.apply(operands)
                    index = 0
                if index > 0:
                    index -= 1
                break
            else:
                index += 1

        if len(operands) == 0:
            return [IdentityOperator(in_structure=in_structure)]
        return operands

apply(operands)

Source code in src/furax/core/rules.py
def apply(self, operands: list[AbstractLinearOperator]) -> list[AbstractLinearOperator]:
    if len(operands) < 2:
        return operands

    in_structure = operands[-1].in_structure

    identity_rule = IdentityRule()
    homothety_rule = HomothetyRule()

    operands = identity_rule.apply(operands)
    operands = homothety_rule.apply(operands)

    index = 0
    while index < len(operands) - 1:
        left, right = operands[index], operands[index + 1]

        for rule in COMPOSITION_RULE_REGISTRY:
            try:
                rule.check(left, right)
                new_ops = rule.apply(left, right)
            except NoReduction:
                continue
            operands[index : index + 2] = new_ops

            # if the rule produces a HomothetyOperator, we deal with it first
            if any(isinstance(op, HomothetyOperator) for op in new_ops):
                operands = homothety_rule.apply(operands)
                index = 0
            if index > 0:
                index -= 1
            break
        else:
            index += 1

    if len(operands) == 0:
        return [IdentityOperator(in_structure=in_structure)]
    return operands

HomothetyRule

Bases: AbstractNaryRule

N-ary rule to combine HomothetyOperators in compositions.

The resulting HomothetyOperator becomes the leftmost (rightmost) operator when the composed operator is wide (tall).

Methods:

Attributes:

Source code in src/furax/core/rules.py
class HomothetyRule(AbstractNaryRule):
    """N-ary rule to combine HomothetyOperators in compositions.

    The resulting HomothetyOperator becomes the leftmost (rightmost) operator when the composed
    operator is wide (tall).
    """

    operator_class = HomothetyOperator

    def apply(self, operands: list[AbstractLinearOperator]) -> list[AbstractLinearOperator]:
        if len(operands) < 2:
            return operands

        first, *_, last = operands
        homothety_number = 0
        value: Scalar = jnp.array(1)
        new_operands = []
        for operand in operands:
            if isinstance(operand, HomothetyOperator):
                value *= operand.value
                homothety_number += 1
            else:
                new_operands.append(operand)

        if homothety_number == 0:
            return operands

        # apply the homothety on the smallest number of elements
        apply_on_left = first.out_size <= last.in_size
        if homothety_number == 1:
            if apply_on_left and isinstance(first, HomothetyOperator):
                return operands
            elif not apply_on_left and isinstance(last, HomothetyOperator):
                return operands

        if apply_on_left:
            return [HomothetyOperator(value, in_structure=first.out_structure)] + new_operands
        return new_operands + [HomothetyOperator(value, in_structure=last.in_structure)]

operator_class = HomothetyOperator class-attribute instance-attribute

apply(operands)

Source code in src/furax/core/rules.py
def apply(self, operands: list[AbstractLinearOperator]) -> list[AbstractLinearOperator]:
    if len(operands) < 2:
        return operands

    first, *_, last = operands
    homothety_number = 0
    value: Scalar = jnp.array(1)
    new_operands = []
    for operand in operands:
        if isinstance(operand, HomothetyOperator):
            value *= operand.value
            homothety_number += 1
        else:
            new_operands.append(operand)

    if homothety_number == 0:
        return operands

    # apply the homothety on the smallest number of elements
    apply_on_left = first.out_size <= last.in_size
    if homothety_number == 1:
        if apply_on_left and isinstance(first, HomothetyOperator):
            return operands
        elif not apply_on_left and isinstance(last, HomothetyOperator):
            return operands

    if apply_on_left:
        return [HomothetyOperator(value, in_structure=first.out_structure)] + new_operands
    return new_operands + [HomothetyOperator(value, in_structure=last.in_structure)]

IdentityRule

Bases: AbstractNaryRule

N-ary rule to discard IdentityOperator in compositions.

Methods:

Attributes:

Source code in src/furax/core/rules.py
class IdentityRule(AbstractNaryRule):
    """N-ary rule to discard IdentityOperator in compositions."""

    operator_class = IdentityOperator

    def apply(self, operands: list[AbstractLinearOperator]) -> list[AbstractLinearOperator]:
        return [_ for _ in operands if not isinstance(_, IdentityOperator)]

operator_class = IdentityOperator class-attribute instance-attribute

apply(operands)

Source code in src/furax/core/rules.py
def apply(self, operands: list[AbstractLinearOperator]) -> list[AbstractLinearOperator]:
    return [_ for _ in operands if not isinstance(_, IdentityOperator)]

AdditiveReductionRule

Bases: AbstractNaryRule

Pairwise reduction of the operands of an AdditionOperator.

Methods:

Source code in src/furax/core/rules.py
class AdditiveReductionRule(AbstractNaryRule):
    """Pairwise reduction of the operands of an ``AdditionOperator``."""

    def apply(self, operands: list[AbstractLinearOperator]) -> list[AbstractLinearOperator]:
        # Addition is commutative and associative, so the operands are reduced by repeatedly
        # fusing any pair for which an `AbstractAdditionRule` applies (trying both orders),
        # until a fixed point is reached. Operands left unfused are returned as-is.
        operands = list(operands)
        changed = True
        while changed and len(operands) > 1:
            changed = False
            for a in range(len(operands)):
                for b in range(a + 1, len(operands)):
                    fused = self._fuse(operands[a], operands[b])
                    if fused is not None:
                        operands = operands[:a] + fused + operands[a + 1 : b] + operands[b + 1 :]
                        changed = True
                        break
                if changed:
                    break
        return operands

    @staticmethod
    def _fuse(
        left: AbstractLinearOperator, right: AbstractLinearOperator
    ) -> list[AbstractLinearOperator] | None:
        for rule in ADDITION_RULE_REGISTRY:
            for first, second in ((left, right), (right, left)):
                try:
                    rule.check(first, second)
                    return rule.apply(first, second)
                except NoReduction:
                    continue
        return None

apply(operands)

Source code in src/furax/core/rules.py
def apply(self, operands: list[AbstractLinearOperator]) -> list[AbstractLinearOperator]:
    # Addition is commutative and associative, so the operands are reduced by repeatedly
    # fusing any pair for which an `AbstractAdditionRule` applies (trying both orders),
    # until a fixed point is reached. Operands left unfused are returned as-is.
    operands = list(operands)
    changed = True
    while changed and len(operands) > 1:
        changed = False
        for a in range(len(operands)):
            for b in range(a + 1, len(operands)):
                fused = self._fuse(operands[a], operands[b])
                if fused is not None:
                    operands = operands[:a] + fused + operands[a + 1 : b] + operands[b + 1 :]
                    changed = True
                    break
            if changed:
                break
    return operands

AbstractBinaryRule

Bases: AbstractRule, ABC

A binary algebraic rule to reduce two operators.

Subclasses implement different flavours of reduction, e.g. op1 @ op2 or op1 + op2.

Methods:

Attributes:

Source code in src/furax/core/rules.py
class AbstractBinaryRule(AbstractRule, ABC):
    """A binary algebraic rule to reduce two operators.

    Subclasses implement different flavours of reduction, e.g. `op1 @ op2` or `op1 + op2`.
    """

    registry: 'RuleRegistry[AbstractBinaryRule]'  # set by the flavour subclass

    operator_class: (
        type[AbstractLinearOperator] | tuple[type[AbstractLinearOperator], ...] | None
    ) = None
    """Operator class of either left or right operator for the rule to apply"""

    left_operator_class: (
        type[AbstractLinearOperator] | tuple[type[AbstractLinearOperator], ...] | None
    ) = None
    """Operator class that left operator must be for the rule to apply"""

    right_operator_class: (
        type[AbstractLinearOperator] | tuple[type[AbstractLinearOperator], ...] | None
    ) = None
    """Operator class that right operator must be for the rule to apply"""

    def __init_subclass__(cls) -> None:
        if cls.__name__.startswith('Abstract'):
            return
        cls.registry.register(cls())
        if (
            cls.operator_class is None
            and cls.left_operator_class is None
            and cls.right_operator_class is None
        ):
            raise ValueError('The operator classes are not specified in the binary rule.')
        if cls.operator_class is not None:
            if cls.left_operator_class is not None or cls.right_operator_class is not None:
                raise ValueError(
                    'Either operator_class or left_operator_class and right_operator_class must be '
                    'specified in the binary rule.'
                )

    def _check_operands(self, left: AbstractLinearOperator, right: AbstractLinearOperator) -> None:
        """Shared operand-class matching: raises [`NoReduction`][] if the rule does not apply."""
        if self.operator_class is not None:
            if not isinstance(left, self.operator_class) and not isinstance(
                right, self.operator_class
            ):
                raise NoReduction
        elif self.left_operator_class is not None and not isinstance(
            left, self.left_operator_class
        ):
            raise NoReduction
        elif self.right_operator_class is not None and not isinstance(
            right, self.right_operator_class
        ):
            raise NoReduction

    @abstractmethod
    def check(self, left: AbstractLinearOperator, right: AbstractLinearOperator) -> None:
        """Raises [`NoReduction`][] if the rule does not apply to ``(left, right)``."""

    @abstractmethod
    def apply(
        self, left: AbstractLinearOperator, right: AbstractLinearOperator
    ) -> list[AbstractLinearOperator]:
        """Applies the algebraic rule to reduce the binary combination of left and right."""

registry instance-attribute

operator_class = None class-attribute instance-attribute

Operator class of either left or right operator for the rule to apply

left_operator_class = None class-attribute instance-attribute

Operator class that left operator must be for the rule to apply

right_operator_class = None class-attribute instance-attribute

Operator class that right operator must be for the rule to apply

__init_subclass__()

Source code in src/furax/core/rules.py
def __init_subclass__(cls) -> None:
    if cls.__name__.startswith('Abstract'):
        return
    cls.registry.register(cls())
    if (
        cls.operator_class is None
        and cls.left_operator_class is None
        and cls.right_operator_class is None
    ):
        raise ValueError('The operator classes are not specified in the binary rule.')
    if cls.operator_class is not None:
        if cls.left_operator_class is not None or cls.right_operator_class is not None:
            raise ValueError(
                'Either operator_class or left_operator_class and right_operator_class must be '
                'specified in the binary rule.'
            )

check(left, right) abstractmethod

Raises NoReduction if the rule does not apply to (left, right).

Source code in src/furax/core/rules.py
@abstractmethod
def check(self, left: AbstractLinearOperator, right: AbstractLinearOperator) -> None:
    """Raises [`NoReduction`][] if the rule does not apply to ``(left, right)``."""

apply(left, right) abstractmethod

Applies the algebraic rule to reduce the binary combination of left and right.

Source code in src/furax/core/rules.py
@abstractmethod
def apply(
    self, left: AbstractLinearOperator, right: AbstractLinearOperator
) -> list[AbstractLinearOperator]:
    """Applies the algebraic rule to reduce the binary combination of left and right."""

AbstractCompositionRule

Bases: AbstractBinaryRule

A binary rule to reduce a composition op1 @ op2.

Methods:

Attributes:

Source code in src/furax/core/rules.py
class AbstractCompositionRule(AbstractBinaryRule):
    """A binary rule to reduce a composition ``op1 @ op2``."""

    registry = COMPOSITION_RULE_REGISTRY

    def check(self, left: AbstractLinearOperator, right: AbstractLinearOperator) -> None:
        self._check_operands(left, right)
        # In addition to checking operands, handle the case of TransposeOperator
        if self.left_operator_class is TransposeOperator and left.operator is not right:  # type: ignore[attr-defined]
            raise NoReduction
        if self.right_operator_class is TransposeOperator and right.operator is not left:  # type: ignore[attr-defined]
            raise NoReduction

registry = COMPOSITION_RULE_REGISTRY class-attribute instance-attribute

check(left, right)

Source code in src/furax/core/rules.py
def check(self, left: AbstractLinearOperator, right: AbstractLinearOperator) -> None:
    self._check_operands(left, right)
    # In addition to checking operands, handle the case of TransposeOperator
    if self.left_operator_class is TransposeOperator and left.operator is not right:  # type: ignore[attr-defined]
        raise NoReduction
    if self.right_operator_class is TransposeOperator and right.operator is not left:  # type: ignore[attr-defined]
        raise NoReduction

InverseBinaryRule

Bases: AbstractCompositionRule

Binary rule for op.I @ op = I and op.I @ op = I.

Methods:

Attributes:

Source code in src/furax/core/rules.py
class InverseBinaryRule(AbstractCompositionRule):
    """Binary rule for `op.I @ op = I` and `op.I @ op = I`."""

    operator_class = AbstractLazyInverseOperator

    def check(self, left: AbstractLinearOperator, right: AbstractLinearOperator) -> None:
        super().check(left, right)
        if isinstance(left, self.operator_class):
            if left.operator is not right:
                raise NoReduction
        else:
            assert isinstance(right, self.operator_class)  # mypy assert
            if right.operator is not left:
                raise NoReduction

    def apply(
        self, left: AbstractLinearOperator, right: AbstractLinearOperator
    ) -> list[AbstractLinearOperator]:
        return []

operator_class = AbstractLazyInverseOperator class-attribute instance-attribute

check(left, right)

Source code in src/furax/core/rules.py
def check(self, left: AbstractLinearOperator, right: AbstractLinearOperator) -> None:
    super().check(left, right)
    if isinstance(left, self.operator_class):
        if left.operator is not right:
            raise NoReduction
    else:
        assert isinstance(right, self.operator_class)  # mypy assert
        if right.operator is not left:
            raise NoReduction

apply(left, right)

Source code in src/furax/core/rules.py
def apply(
    self, left: AbstractLinearOperator, right: AbstractLinearOperator
) -> list[AbstractLinearOperator]:
    return []

IdempotentRule

Bases: AbstractCompositionRule

Binary rule for P @ P = P when P is idempotent.

Methods:

Attributes:

Source code in src/furax/core/rules.py
class IdempotentRule(AbstractCompositionRule):
    """Binary rule for `P @ P = P` when `P` is idempotent."""

    operator_class = AbstractLinearOperator

    def check(self, left: AbstractLinearOperator, right: AbstractLinearOperator) -> None:
        super().check(left, right)
        if not (left.is_idempotent and left is right):
            raise NoReduction

    def apply(
        self, left: AbstractLinearOperator, right: AbstractLinearOperator
    ) -> list[AbstractLinearOperator]:
        return [left]

operator_class = AbstractLinearOperator class-attribute instance-attribute

check(left, right)

Source code in src/furax/core/rules.py
def check(self, left: AbstractLinearOperator, right: AbstractLinearOperator) -> None:
    super().check(left, right)
    if not (left.is_idempotent and left is right):
        raise NoReduction

apply(left, right)

Source code in src/furax/core/rules.py
def apply(
    self, left: AbstractLinearOperator, right: AbstractLinearOperator
) -> list[AbstractLinearOperator]:
    return [left]

AbstractAdditionRule

Bases: AbstractBinaryRule

A binary rule to reduce a sum op1 + op2.

Methods:

Attributes:

Source code in src/furax/core/rules.py
class AbstractAdditionRule(AbstractBinaryRule):
    """A binary rule to reduce a sum ``op1 + op2``."""

    registry = ADDITION_RULE_REGISTRY

    def check(self, left: AbstractLinearOperator, right: AbstractLinearOperator) -> None:
        self._check_operands(left, right)

registry = ADDITION_RULE_REGISTRY class-attribute instance-attribute

check(left, right)

Source code in src/furax/core/rules.py
def check(self, left: AbstractLinearOperator, right: AbstractLinearOperator) -> None:
    self._check_operands(left, right)

HomothetyAdditionRule

Bases: AbstractAdditionRule

Additive rule H(a) + H(b) = H(a + b) (the additive analog of HomothetyRule).

Methods:

Attributes:

Source code in src/furax/core/rules.py
class HomothetyAdditionRule(AbstractAdditionRule):
    """Additive rule ``H(a) + H(b) = H(a + b)`` (the additive analog of [`HomothetyRule`][])."""

    left_operator_class = HomothetyOperator
    right_operator_class = HomothetyOperator

    def apply(
        self, left: AbstractLinearOperator, right: AbstractLinearOperator
    ) -> list[AbstractLinearOperator]:
        assert isinstance(left, HomothetyOperator)  # mypy
        assert isinstance(right, HomothetyOperator)  # mypy
        return [HomothetyOperator(left.value + right.value, in_structure=left.in_structure)]

left_operator_class = HomothetyOperator class-attribute instance-attribute

right_operator_class = HomothetyOperator class-attribute instance-attribute

apply(left, right)

Source code in src/furax/core/rules.py
def apply(
    self, left: AbstractLinearOperator, right: AbstractLinearOperator
) -> list[AbstractLinearOperator]:
    assert isinstance(left, HomothetyOperator)  # mypy
    assert isinstance(right, HomothetyOperator)  # mypy
    return [HomothetyOperator(left.value + right.value, in_structure=left.in_structure)]

furax.core.utils

Classes:

Functions:

Attributes:

  • T

T = TypeVar('T') module-attribute

DefaultIdentityDict

Bases: dict[T, T]

A dict whose default factory is the identity.

Examples:

>>> d = DefaultIdentityDict({'a': 'b'})
>>> d['c']
'c'

Methods:

Source code in src/furax/core/utils.py
class DefaultIdentityDict(dict[T, T]):
    """A dict whose default factory is the identity.

    Examples:
        >>> d = DefaultIdentityDict({'a': 'b'})
        >>> d['c']
        'c'
    """

    def __getitem__(self, key: T) -> T:
        try:
            return super().__getitem__(key)
        except KeyError:
            return key

__getitem__(key)

Source code in src/furax/core/utils.py
def __getitem__(self, key: T) -> T:
    try:
        return super().__getitem__(key)
    except KeyError:
        return key

register_dataclass_with_keys(cls)

Register a dataclass as a pytree node, bypassing init during unflatten.

The motivation to reimplement jax.tree_util.register_dataclass comes from the fact that it does not handle dataclasses with an init constructor that does not match the fields of the dataclass.

Source code in src/furax/core/utils.py
def register_dataclass_with_keys(cls: type[_T]) -> type[_T]:
    """Register a dataclass as a pytree node, bypassing __init__ during unflatten.

    The motivation to reimplement jax.tree_util.register_dataclass comes from the fact that it does not handle
    dataclasses with an __init__ constructor that does not match the fields of the dataclass.
    """
    fields = dataclasses.fields(cls)  # type: ignore[arg-type]
    data_fields = tuple(f.name for f in fields if not f.metadata.get('static', False))
    meta_fields = tuple(f.name for f in fields if f.metadata.get('static', False))

    def flatten_with_keys(obj: _T) -> tuple[list[tuple[GetAttrKey, Any]], tuple[Any, ...]]:
        data = [(GetAttrKey(name), getattr(obj, name)) for name in data_fields]
        meta = tuple(getattr(obj, name) for name in meta_fields)
        return data, meta

    def unflatten(meta: tuple[Any, ...], data: Iterable[Any]) -> _T:
        obj = object.__new__(cls)
        for name, value in zip(data_fields, data):
            object.__setattr__(obj, name, value)
        for name, value in zip(meta_fields, meta):
            object.__setattr__(obj, name, value)
        return obj

    def flatten(obj: _T) -> tuple[list[Any], tuple[Any, ...]]:
        data = [getattr(obj, name) for name in data_fields]
        meta = tuple(getattr(obj, name) for name in meta_fields)
        return data, meta

    register_pytree_with_keys(cls, flatten_with_keys, unflatten, flatten)
    return cls