Skip to content

Morphisms

Base morphism classes, composition operations, and shared morphism utilities.

morphisms

Morphism hierarchy: V-enriched relations between finite sets.

A morphism from domain D to codomain C is represented as a tensor of shape (D.shape, C.shape) with entries in the lattice L of an algebra. Composition uses the algebra's operations (join over tensor product).

The hierarchy:

Morphism (abstract)
├── ObservedMorphism    — fixed tensor (not learned)
├── LatentMorphism      — nn.Parameter with sigmoid constraint
├── ComposedMorphism    — f >> g (V-enriched composition)
├── ProductMorphism     — f @ g (tensor / parallel product)
├── MarginalizedMorphism — contract codomain dims via join
├── FunctorMorphism     — lazy image of a morphism under a functor
└── RepeatMorphism      — runtime-variable iterated composition (T^n)

PyTorch boundary

The categorical hierarchy is intentionally not a subclass of torch.nn.Module: a Morphism is a categorical object and a Module is a PyTorch parameter container. When a Morphism needs to be bound into a parameter-tracking context (a MonadicProgram, a FanMorphism, a parametric-program parameter slot), the adapter as_torch_module produces a backend-agnostic nn.Module wrapping the morphism's parameters. Every binding site funnels through this adapter so the categorical / PyTorch boundary stays explicit and a JAX or numpy backend can replace as_torch_module without touching the morphism hierarchy itself.

Morphism

Morphism(domain: SetObject, codomain: SetObject, algebra: Algebra | None = None)

Bases: ABC

Abstract base for morphisms between finite sets.

Subclasses must implement tensor (returns the materialized tensor with values in the algebra's lattice) and module (returns the nn.Module tree for parameter collection).

PARAMETER DESCRIPTION
domain

Source object.

TYPE: SetObject

codomain

Target object.

TYPE: SetObject

algebra

The enrichment algebra. Defaults to PRODUCT_FUZZY.

TYPE: Algebra or None DEFAULT: None

Source code in src/quivers/core/morphisms.py
def __init__(
    self, domain: SetObject, codomain: SetObject, algebra: Algebra | None = None
) -> None:
    self._domain = domain
    self._codomain = codomain
    self._algebra = algebra if algebra is not None else PRODUCT_FUZZY

domain property

domain: SetObject

Source object.

codomain property

codomain: SetObject

Target object.

algebra property

algebra: Algebra

The enrichment algebra for this morphism.

tensor_shape property

tensor_shape: tuple[int, ...]

Expected shape of the materialized tensor.

tensor abstractmethod property

tensor: Tensor

Materialize the morphism as a tensor with values in L.

dagger property

dagger: 'TransformedMorphism'

Transpose / dagger of self : A → B, producing a morphism B → A whose tensor has the domain and codomain axes swapped.

The compact-closed structure of V-Cat means every object is self-dual (A^* = A), so the dagger is well-defined for every algebra. The semantic interpretation depends on the algebra: ProductFuzzyAlgebra gives the tensor transpose, Markov the Bayes-uniform-prior inversion, Viterbi the max-plus reversal, Boolean the relational converse.

Categorically: f^† = (ε_B ⊗ id_A) ∘ (id_B ⊗ f^* ⊗ id_A) ∘ (id_B ⊗ η_A) — but for finite-set objects the unit / counit collapse to the diagonal / co-diagonal, and the dagger reduces to a tensor transpose along the domain/codomain axes.

RETURNS DESCRIPTION
ObservedMorphism

Morphism B → A whose tensor is the axis-swapped tensor of self. Gradients propagate to the original morphism's parameters through the transpose.

module abstractmethod

module() -> Module

Return an nn.Module wrapping all learnable parameters.

Source code in src/quivers/core/morphisms.py
@abstractmethod
def module(self) -> nn.Module:
    """Return an nn.Module wrapping all learnable parameters."""
    ...

__rshift__

__rshift__(other: Morphism) -> ComposedMorphism

V-enriched composition: self >> other.

Composes self: A -> B with other: B -> C to yield a morphism A -> C, contracting over B using the algebra.

PARAMETER DESCRIPTION
other

Right morphism whose domain must match self's codomain.

TYPE: Morphism

RETURNS DESCRIPTION
ComposedMorphism

The composed morphism.

Source code in src/quivers/core/morphisms.py
def __rshift__(self, other: Morphism) -> ComposedMorphism:
    """V-enriched composition: self >> other.

    Composes self: A -> B with other: B -> C to yield
    a morphism A -> C, contracting over B using the algebra.

    Parameters
    ----------
    other : Morphism
        Right morphism whose domain must match self's codomain.

    Returns
    -------
    ComposedMorphism
        The composed morphism.
    """
    if not isinstance(other, Morphism):
        return NotImplemented
    if self.codomain != other.domain:
        raise TypeError(
            f"cannot compose: codomain {self.codomain!r} != domain {other.domain!r}"
        )
    if not self._algebra.is_compatible(other._algebra):
        raise TypeError(
            f"incompatible algebras: {self._algebra!r} and {other._algebra!r}"
        )
    return ComposedMorphism(self, other)

__matmul__

__matmul__(other: Morphism) -> ProductMorphism

Tensor (parallel) product: self @ other.

Given self: A -> B and other: C -> D, produces a morphism A×C -> B×D whose tensor is the outer product via the algebra's tensor_op.

PARAMETER DESCRIPTION
other

Right morphism.

TYPE: Morphism

RETURNS DESCRIPTION
ProductMorphism

The product morphism.

Source code in src/quivers/core/morphisms.py
def __matmul__(self, other: Morphism) -> ProductMorphism:
    """Tensor (parallel) product: self @ other.

    Given self: A -> B and other: C -> D, produces
    a morphism A×C -> B×D whose tensor is the outer product
    via the algebra's tensor_op.

    Parameters
    ----------
    other : Morphism
        Right morphism.

    Returns
    -------
    ProductMorphism
        The product morphism.
    """
    if not isinstance(other, Morphism):
        return NotImplemented
    return ProductMorphism(self, other)

change_base

change_base(phi) -> 'TransformedMorphism'

Transport this morphism along a change-of-base functor.

phi may be either a quivers.core.algebra_morphisms.AlgebraHomomorphism (pointwise: the action factors entry-by-entry through phi.apply) or a quivers.core.morphism_transformations.MorphismTransformation (shape-aware: the action consumes the whole tensor plus the morphism for axis resolution).

The result is a TransformedMorphism whose tensor is recomputed on each access by applying phi to the source's current tensor. The source's learnable parameters are exposed through .module() so torch.nn.Module.parameters walks reach them, and each backward through a fresh .tensor access rebuilds a graph rooted at those parameters; multi-step optimisation through change-of-base is supported.

PARAMETER DESCRIPTION
phi

The change-of-base functor. Its source must match self.algebra.

TYPE: AlgebraHomomorphism or MorphismTransformation

RETURNS DESCRIPTION
TransformedMorphism

A morphism over phi.target whose tensor is the transported tensor of self. For pointwise transformations the domain and codomain are preserved; for shape-aware ones (e.g. BayesInvert) the transformation may swap them.

Source code in src/quivers/core/morphisms.py
def change_base(self, phi) -> "TransformedMorphism":
    """Transport this morphism along a change-of-base functor.

    ``phi`` may be either a
    `quivers.core.algebra_morphisms.AlgebraHomomorphism`
    (pointwise: the action factors entry-by-entry through
    ``phi.apply``) or a
    `quivers.core.morphism_transformations.MorphismTransformation`
    (shape-aware: the action consumes the whole tensor plus
    the morphism for axis resolution).

    The result is a `TransformedMorphism` whose
    `tensor` is recomputed on each access by applying
    ``phi`` to the source's current tensor. The source's
    learnable parameters are exposed through ``.module()`` so
    `torch.nn.Module.parameters` walks reach them, and
    each backward through a fresh ``.tensor`` access rebuilds
    a graph rooted at those parameters; multi-step optimisation
    through change-of-base is supported.

    Parameters
    ----------
    phi : AlgebraHomomorphism or MorphismTransformation
        The change-of-base functor. Its ``source`` must match
        ``self.algebra``.

    Returns
    -------
    TransformedMorphism
        A morphism over ``phi.target`` whose tensor is the
        transported tensor of ``self``. For pointwise
        transformations the domain and codomain are preserved;
        for shape-aware ones (e.g. ``BayesInvert``) the
        transformation may swap them.
    """
    if isinstance(phi, TransSeq):
        # Apply the steps in order; each step's change_base
        # type-checks its own source against the current
        # algebra, so a malformed seam surfaces with the
        # same error a hand-written sequence would produce.
        current: Morphism = self
        for step in phi.steps:
            current = current.change_base(step)
        return cast(TransformedMorphism, current)
    if not isinstance(phi, (AlgebraHomomorphism, MorphismTransformation)):
        raise TypeError(
            f"change_base: expected AlgebraHomomorphism, "
            f"MorphismTransformation, or TransSeq; got "
            f"{type(phi).__name__}"
        )
    if type(phi.source) is not type(self._algebra):
        raise TypeError(
            f"change_base: source algebra "
            f"{phi.source.name!r} does not match this morphism's "
            f"algebra {self._algebra.name!r}"
        )
    if isinstance(phi, AlgebraHomomorphism):
        transform = _build_algebra_homomorphism_transform(phi)
        new_domain = self._domain
        new_codomain = self._codomain
    else:
        transform = _build_morphism_transformation_transform(phi, self)
        new_domain = phi.new_domain(self)
        new_codomain = phi.new_codomain(self)
    return TransformedMorphism(
        self,
        transform,
        new_domain,
        new_codomain,
        algebra=phi.target,
    )

refactor

refactor(*, domain=None, codomain=None) -> 'TransformedMorphism'

Switch between flat and product views of this morphism.

Given a morphism f : A -> B whose tensor storage has total numel matching the requested domain / codomain objects, return an equivalent morphism f' : A' -> B' whose tensor is the same data reshaped to the new factored layout. A' and B' must be isomorphic to A and B as objects — same cardinality, possibly different product structure.

Categorically this is the action of an object iso B ≅ B' on the morphism's tensor. Semantically it is a no-op; presentation only.

PARAMETER DESCRIPTION
domain

New domain. Must have prod(shape) == prod(self.domain.shape).

TYPE: SetObject DEFAULT: None

codomain

New codomain. Same numel constraint.

TYPE: SetObject DEFAULT: None

RETURNS DESCRIPTION
ObservedMorphism

The reshape of self into the requested type.

Source code in src/quivers/core/morphisms.py
def refactor(self, *, domain=None, codomain=None) -> "TransformedMorphism":
    """Switch between flat and product views of this morphism.

    Given a morphism ``f : A -> B`` whose tensor storage has
    total numel matching the requested ``domain`` / ``codomain``
    objects, return an equivalent morphism ``f' : A' -> B'``
    whose tensor is the same data reshaped to the new factored
    layout. ``A'`` and ``B'`` must be isomorphic to ``A`` and
    ``B`` as objects — same cardinality, possibly different
    product structure.

    Categorically this is the action of an object iso
    ``B ≅ B'`` on the morphism's tensor. Semantically it is
    a no-op; presentation only.

    Parameters
    ----------
    domain : SetObject, optional
        New domain. Must have ``prod(shape) == prod(self.domain.shape)``.
    codomain : SetObject, optional
        New codomain. Same numel constraint.

    Returns
    -------
    ObservedMorphism
        The reshape of ``self`` into the requested type.
    """
    new_domain = domain if domain is not None else self._domain
    new_codomain = codomain if codomain is not None else self._codomain

    def _numel(shape) -> int:
        n = 1
        for s in shape:
            n *= int(s)
        return n

    if _numel(new_domain.shape) != _numel(self._domain.shape):
        raise ValueError(
            f"refactor: domain numel {_numel(new_domain.shape)} "
            f"does not match {_numel(self._domain.shape)} for "
            f"{self._domain!r} -> {new_domain!r}"
        )
    if _numel(new_codomain.shape) != _numel(self._codomain.shape):
        raise ValueError(
            f"refactor: codomain numel "
            f"{_numel(new_codomain.shape)} does not match "
            f"{_numel(self._codomain.shape)} for "
            f"{self._codomain!r} -> {new_codomain!r}"
        )
    target_shape = tuple(new_domain.shape) + tuple(new_codomain.shape)

    def _transform(t: torch.Tensor, _shape=target_shape) -> torch.Tensor:
        return t.reshape(_shape)

    return TransformedMorphism(
        self,
        _transform,
        new_domain,
        new_codomain,
        algebra=self._algebra,
    )

marginalize

marginalize(*sets: SetObject) -> MarginalizedMorphism

Marginalize (join-reduce) over codomain components.

The codomain must be a ProductSet containing the sets to marginalize over. The result has a codomain with those components removed. Uses the algebra's join operation.

PARAMETER DESCRIPTION
*sets

Codomain components to marginalize over.

TYPE: SetObject DEFAULT: ()

RETURNS DESCRIPTION
MarginalizedMorphism

The marginalized morphism.

Source code in src/quivers/core/morphisms.py
def marginalize(self, *sets: SetObject) -> MarginalizedMorphism:
    """Marginalize (join-reduce) over codomain components.

    The codomain must be a ProductSet containing the sets to
    marginalize over. The result has a codomain with those
    components removed. Uses the algebra's join operation.

    Parameters
    ----------
    *sets : SetObject
        Codomain components to marginalize over.

    Returns
    -------
    MarginalizedMorphism
        The marginalized morphism.
    """
    return MarginalizedMorphism(self, sets)

trace

trace(obj: SetObject) -> 'TransformedMorphism'

Trace of self : X ⊗ A → A ⊗ Y along obj = A, producing a morphism X → Y.

Concretely the trace contracts the A axis on the domain side with the A axis on the codomain side via the algebra's tensor_op and then joins (self._algebra.join) over the contracted axis. This is the categorical trace tr_A(f) : X → Y = (ε_A ⊗ id_Y) ∘ (id_A ⊗ f) ∘ (η_A ⊗ id_X).

The morphism's domain must be a product set whose first component is obj; the codomain must be a product set whose first component is obj. If this is not the case a TypeError is raised — the user should call ProductSet.swap style helpers (not yet exposed) to reorder axes before tracing.

PARAMETER DESCRIPTION
obj

The object to contract over. Must appear at the start of both the domain and codomain product sets.

TYPE: SetObject

RETURNS DESCRIPTION
ObservedMorphism

Morphism X → Y (the trace).

Source code in src/quivers/core/morphisms.py
def trace(self, obj: SetObject) -> "TransformedMorphism":
    """Trace of ``self : X ⊗ A → A ⊗ Y`` along ``obj = A``,
    producing a morphism ``X → Y``.

    Concretely the trace contracts the A axis on the domain
    side with the A axis on the codomain side via the algebra's
    ``tensor_op`` and then joins (``self._algebra.join``) over
    the contracted axis. This is the categorical trace
    ``tr_A(f) : X → Y = (ε_A ⊗ id_Y) ∘ (id_A ⊗ f) ∘ (η_A ⊗ id_X)``.

    The morphism's domain must be a product set whose first
    component is ``obj``; the codomain must be a product set
    whose first component is ``obj``. If this is not the case
    a TypeError is raised — the user should call
    `ProductSet.swap` style helpers (not yet exposed) to
    reorder axes before tracing.

    Parameters
    ----------
    obj : SetObject
        The object to contract over. Must appear at the start
        of both the domain and codomain product sets.

    Returns
    -------
    ObservedMorphism
        Morphism ``X → Y`` (the trace).
    """
    if not isinstance(self._domain, ProductSet) or not isinstance(
        self._codomain, ProductSet
    ):
        raise TypeError(
            "trace: requires the morphism's domain and codomain "
            "to both be ProductSets with the contracted object "
            f"at the front; got domain={self._domain!r}, "
            f"codomain={self._codomain!r}"
        )
    if self._domain.components[0] != obj:
        raise TypeError(
            f"trace: domain's first component {self._domain.components[0]!r} "
            f"!= contraction object {obj!r}"
        )
    if self._codomain.components[0] != obj:
        raise TypeError(
            f"trace: codomain's first component {self._codomain.components[0]!r} "
            f"!= contraction object {obj!r}"
        )
    d_ndim = len(self._domain.shape)
    a_ndim = len(obj.shape)
    algebra = self._algebra

    def _transform(
        t: torch.Tensor, _d_ndim=d_ndim, _a_ndim=a_ndim, _q=algebra
    ) -> torch.Tensor:
        # Domain axes: 0..d_ndim-1 with A at 0..a_ndim-1.
        # Codomain axes: d_ndim..d_ndim+c_ndim-1 with A at
        # d_ndim..d_ndim+a_ndim-1.  Take diagonals pairing each
        # A axis on the domain side with the corresponding one
        # on the codomain side, then join along the surviving
        # diagonal axes.
        out = t
        for k in range(_a_ndim):
            out = torch.diagonal(out, dim1=0, dim2=_d_ndim - k)
        trailing = tuple(range(out.dim() - _a_ndim, out.dim()))
        return _q.join(out, dim=trailing)

    # Recover the X and Y product sets.
    x_components = tuple(self._domain.components[1:])
    y_components = tuple(self._codomain.components[1:])
    if len(x_components) == 1:
        new_domain = x_components[0]
    else:
        new_domain = ProductSet(components=x_components)
    if len(y_components) == 1:
        new_codomain = y_components[0]
    else:
        new_codomain = ProductSet(components=y_components)
    return TransformedMorphism(
        self, _transform, new_domain, new_codomain, algebra=self._algebra
    )

TransformedMorphism

TransformedMorphism(source: 'Morphism', transform, domain: SetObject, codomain: SetObject, algebra: Algebra | None = None)

Bases: Morphism

A morphism whose tensor is a transformation of a source morphism's tensor, recomputed on each tensor access.

The source morphism is registered as a submodule so self.module().parameters() includes the source's learnable parameters; the transform is applied lazily, so each backward through a fresh .tensor access gets its own autograd graph and multi-step optimisation propagates gradients correctly.

PARAMETER DESCRIPTION
source

Underlying morphism whose tensor feeds the transform.

TYPE: Morphism

transform

Pure function applied to source.tensor to produce the new tensor. Must depend on source only through the tensor; any additional context (e.g. the source morphism for shape resolution) must be captured by closure.

TYPE: Callable[[Tensor], Tensor]

domain

Target domain (may differ from source.domain for shape-aware transformations like the dagger or BayesInvert).

TYPE: SetObject

codomain

Target codomain.

TYPE: SetObject

algebra

Target algebra. Defaults to the source's algebra.

TYPE: Algebra or None DEFAULT: None

Source code in src/quivers/core/morphisms.py
def __init__(
    self,
    source: "Morphism",
    transform,  # Callable[[torch.Tensor], torch.Tensor]
    domain: SetObject,
    codomain: SetObject,
    algebra: Algebra | None = None,
) -> None:
    super().__init__(
        domain,
        codomain,
        algebra=algebra if algebra is not None else source.algebra,
    )
    self._source = source
    self._transform = transform
    self._module = _SourcedModule(source.module())

source property

source: 'Morphism'

The underlying morphism feeding this transform.

ObservedMorphism

ObservedMorphism(domain: SetObject, codomain: SetObject, data: Tensor, algebra: Algebra | None = None)

Bases: Morphism

A morphism with a fixed (non-learnable) tensor.

PARAMETER DESCRIPTION
domain

Source object.

TYPE: SetObject

codomain

Target object.

TYPE: SetObject

data

Fixed tensor of shape (domain.shape, codomain.shape).

TYPE: Tensor

algebra

The enrichment algebra. Defaults to PRODUCT_FUZZY.

TYPE: Algebra or None DEFAULT: None

Source code in src/quivers/core/morphisms.py
def __init__(
    self,
    domain: SetObject,
    codomain: SetObject,
    data: torch.Tensor,
    algebra: Algebra | None = None,
) -> None:
    super().__init__(domain, codomain, algebra=algebra)
    expected = self.tensor_shape
    if data.shape != expected:
        raise ValueError(
            f"data shape {data.shape} does not match expected {expected}"
        )
    self._module = _MorphismModule()
    self._module.register_buffer("data", data)

LatentMorphism

LatentMorphism(domain: SetObject, codomain: SetObject, init_scale: float = 0.5, algebra: Algebra | None = None)

Bases: Morphism

A learnable morphism backed by an nn.Parameter.

Stores unconstrained real-valued parameters and applies sigmoid to produce [0,1]-valued fuzzy relation entries.

PARAMETER DESCRIPTION
domain

Source object.

TYPE: SetObject

codomain

Target object.

TYPE: SetObject

init_scale

Standard deviation of the normal initialization for the unconstrained parameters. Default 0.5 (sigmoid maps this to roughly uniform over [0.3, 0.7]).

TYPE: float DEFAULT: 0.5

algebra

The enrichment algebra. Defaults to PRODUCT_FUZZY.

TYPE: Algebra or None DEFAULT: None

Source code in src/quivers/core/morphisms.py
def __init__(
    self,
    domain: SetObject,
    codomain: SetObject,
    init_scale: float = 0.5,
    algebra: Algebra | None = None,
) -> None:
    super().__init__(domain, codomain, algebra=algebra)
    shape = self.tensor_shape
    self._module = _MorphismModule()
    raw = nn.Parameter(torch.randn(shape) * init_scale)
    self._module.register_parameter("raw", raw)

raw property

raw: Parameter

Unconstrained parameter tensor.

tensor property

tensor: Tensor

Sigmoid-constrained tensor with values in (0, 1).

ComposedMorphism

ComposedMorphism(left: Morphism, right: Morphism)

Bases: Morphism

V-enriched composition of two morphisms.

Given left: A -> B and right: B -> C, produces A -> C by contracting over B using the algebra's compose method.

PARAMETER DESCRIPTION
left

Left morphism (applied first).

TYPE: Morphism

right

Right morphism (applied second).

TYPE: Morphism

Source code in src/quivers/core/morphisms.py
def __init__(self, left: Morphism, right: Morphism) -> None:
    n_contract = left.codomain.ndim
    super().__init__(left.domain, right.codomain, algebra=left._algebra)
    self._left = left
    self._right = right
    self._n_contract = n_contract

left property

left: Morphism

Left (first) morphism.

right property

right: Morphism

Right (second) morphism.

ProductMorphism

ProductMorphism(left: Morphism, right: Morphism)

Bases: Morphism

Tensor (parallel) product of two morphisms.

Given left: A -> B and right: C -> D, produces A×C -> B×D. The tensor is the outer product of the two component tensors via the algebra's tensor_op.

PARAMETER DESCRIPTION
left

Left morphism.

TYPE: Morphism

right

Right morphism.

TYPE: Morphism

Source code in src/quivers/core/morphisms.py
def __init__(self, left: Morphism, right: Morphism) -> None:
    domain = ProductSet(components=(left.domain, right.domain))
    codomain = ProductSet(components=(left.codomain, right.codomain))
    super().__init__(domain, codomain, algebra=left._algebra)
    self._left = left
    self._right = right

MarginalizedMorphism

MarginalizedMorphism(inner: Morphism, sets_to_marginalize: tuple[SetObject, ...] | list[SetObject])

Bases: Morphism

Morphism with codomain dimensions marginalized via the algebra's join.

Given an inner morphism A -> B × C and a set B to marginalize, produces A -> C by join-reduction over B's dimensions.

PARAMETER DESCRIPTION
inner

The morphism to marginalize.

TYPE: Morphism

sets_to_marginalize

Codomain components to marginalize over.

TYPE: tuple of SetObject

Source code in src/quivers/core/morphisms.py
def __init__(
    self,
    inner: Morphism,
    sets_to_marginalize: tuple[SetObject, ...] | list[SetObject],
) -> None:
    codomain = inner.codomain
    sets_to_marginalize = tuple(sets_to_marginalize)
    if not isinstance(codomain, ProductSet):
        raise TypeError(
            f"can only marginalize over ProductSet codomain, got {type(codomain).__name__}"
        )
    n_domain = inner.domain.ndim
    remaining_components: list[SetObject] = []
    dims_to_reduce: list[int] = []
    offset = n_domain
    for component in codomain.components:
        if component in sets_to_marginalize:
            for d in range(component.ndim):
                dims_to_reduce.append(offset + d)
        else:
            remaining_components.append(component)
        offset += component.ndim
    if not dims_to_reduce:
        raise ValueError("none of the specified sets found in codomain components")
    if len(remaining_components) == 0:
        raise ValueError("cannot marginalize all codomain components")
    elif len(remaining_components) == 1:
        new_codomain = remaining_components[0]
    else:
        new_codomain = ProductSet(components=tuple(remaining_components))
    super().__init__(inner.domain, new_codomain, algebra=inner._algebra)
    self._inner = inner
    self._dims_to_reduce = tuple(dims_to_reduce)

FunctorMorphism

FunctorMorphism(functor: Functor, inner: Morphism, domain: SetObject, codomain: SetObject)

Bases: Morphism

Lazy image of a morphism under a functor.

Recomputes the tensor on each access from the inner morphism, preserving gradient flow through the functor's map_tensor method. No additional parameters beyond those of the inner morphism.

PARAMETER DESCRIPTION
functor

The functor that produced this morphism.

TYPE: Functor

inner

The original morphism being mapped.

TYPE: Morphism

domain

The image of the inner morphism's domain under the functor.

TYPE: SetObject

codomain

The image of the inner morphism's codomain under the functor.

TYPE: SetObject

Source code in src/quivers/core/morphisms.py
def __init__(
    self, functor: Functor, inner: Morphism, domain: SetObject, codomain: SetObject
) -> None:
    super().__init__(domain, codomain, algebra=inner._algebra)
    self._functor = functor
    self._inner = inner

inner property

inner: Morphism

The original morphism being mapped.

RepeatMorphism

RepeatMorphism(inner: Morphism, n: int = 1)

Bases: Morphism

Runtime-variable iterated composition (matrix power).

Wraps an endomorphism f : X -> X and computes f^n at runtime, where n can be changed between calls. Uses repeated squaring for O(log n) algebra compositions.

For an endomorphism T : S -> S under an algebra, T^n is the n-fold Kleisli composition. Under the product_fuzzy algebra with stochastic matrices, this is standard matrix power.

PARAMETER DESCRIPTION
inner

An endomorphism (domain must equal codomain).

TYPE: Morphism

n

Initial number of repetitions (default 1). Can be changed later via the n_steps property.

TYPE: int DEFAULT: 1

RAISES DESCRIPTION
TypeError

If the inner morphism is not an endomorphism.

ValueError

If n < 1.

Examples:

>>> T = morphism(S, S)
>>> rep = RepeatMorphism(T, n=5)
>>> rep.tensor  # computes T^5
>>> rep.n_steps = 10
>>> rep.tensor  # now computes T^10
Source code in src/quivers/core/morphisms.py
def __init__(self, inner: Morphism, n: int = 1) -> None:
    if inner.domain != inner.codomain:
        raise TypeError(
            f"repeat requires an endomorphism, got {inner.domain!r} -> {inner.codomain!r}"
        )
    if n < 1:
        raise ValueError(f"n must be >= 1, got {n}")
    super().__init__(inner.domain, inner.codomain, algebra=inner._algebra)
    self._inner = inner
    self._n = n
    self._n_contract = inner.codomain.ndim

inner property

inner: Morphism

The base endomorphism.

n_steps property writable

n_steps: int

Number of iterated compositions.

tensor property

tensor: Tensor

Compute the n-fold composition via repeated squaring.

RETURNS DESCRIPTION
Tensor

The tensor for f^n, same shape as the inner morphism.

CurriedMorphism

CurriedMorphism(inner: Morphism, direction: str = 'right')

Bases: Morphism

Residuation-witness curried morphism.

For an inner morphism f : X * Y -> Z whose codomain Z lives in a residuated universe (a FreeResiduated), produces the morphism corresponding to the relevant residuation isomorphism:

  • direction='right' realises the right-residuation X * Y -> Z ≅ X -> Z/Y (counit of the right-residual adjunction),
  • direction='left' realises the left-residuation X * Y -> Z ≅ Y -> X\Z.

The underlying tensor data is reinterpreted, not recomputed: the same V-relation is presented under a different domain/codomain factoring in the residuated universe.

PARAMETER DESCRIPTION
inner

The base morphism. Must have a domain that factors as a non-trivial product (ProductSet with at least two components) and a codomain that inhabits a residuated universe.

TYPE: Morphism

direction

Which residuation to apply.

TYPE: Literal['right', 'left'] DEFAULT: 'right'

RAISES DESCRIPTION
TypeError

If inner.domain does not factor as a product.

Source code in src/quivers/core/morphisms.py
def __init__(self, inner: Morphism, direction: str = "right") -> None:
    from quivers.stochastic.categories import (
        AtomicCategory,
        SlashCategory,
    )

    if direction not in ("right", "left"):
        raise ValueError(f"direction must be 'right' or 'left', got {direction!r}")
    if not isinstance(inner.domain, ProductSet) or len(inner.domain.components) < 2:
        raise TypeError(
            f"curry requires inner morphism with product domain, "
            f"got {type(inner.domain).__name__}"
        )

    # Split off the first or last factor of the domain product
    # depending on direction; the residuation moves it into the
    # codomain via the slash constructor.
    components = inner.domain.components
    if direction == "right":
        new_domain_components = components[:-1]
        absorbed = components[-1]
    else:
        new_domain_components = components[1:]
        absorbed = components[0]

    if len(new_domain_components) == 1:
        new_domain = new_domain_components[0]
    else:
        new_domain = ProductSet(components=new_domain_components)

    # Codomain is the residuation of inner.codomain by `absorbed`.
    # When inner.codomain is a FreeResiduated universe, the new
    # codomain is the same universe (closed under residuation).
    # Otherwise, the construction is interpreted in the implicit
    # residuated structure on the existing codomain.
    if isinstance(inner.codomain, FreeResiduated):
        new_codomain: SetObject = inner.codomain
    else:
        slash = "/" if direction == "right" else "\\"
        cat = SlashCategory(
            result=AtomicCategory(name=str(inner.codomain)),
            argument=AtomicCategory(name=str(absorbed)),
            direction=slash,  # type: ignore[arg-type]
        )
        new_codomain = inner.codomain  # underlying type unchanged
        self._slash_category = cat

    super().__init__(new_domain, new_codomain, algebra=inner._algebra)
    self._inner = inner
    self._direction = direction

morphism

morphism(domain: SetObject, codomain: SetObject, init_scale: float = 0.5, algebra: Algebra | None = None) -> LatentMorphism

Create a latent (learnable) morphism.

PARAMETER DESCRIPTION
domain

Source object.

TYPE: SetObject

codomain

Target object.

TYPE: SetObject

init_scale

Initialization scale for unconstrained parameters.

TYPE: float DEFAULT: 0.5

algebra

The enrichment algebra. Defaults to PRODUCT_FUZZY.

TYPE: Algebra or None DEFAULT: None

RETURNS DESCRIPTION
LatentMorphism

A learnable morphism.

Source code in src/quivers/core/morphisms.py
def morphism(
    domain: SetObject,
    codomain: SetObject,
    init_scale: float = 0.5,
    algebra: Algebra | None = None,
) -> LatentMorphism:
    """Create a latent (learnable) morphism.

    Parameters
    ----------
    domain : SetObject
        Source object.
    codomain : SetObject
        Target object.
    init_scale : float
        Initialization scale for unconstrained parameters.
    algebra : Algebra or None
        The enrichment algebra. Defaults to PRODUCT_FUZZY.

    Returns
    -------
    LatentMorphism
        A learnable morphism.
    """
    return LatentMorphism(domain, codomain, init_scale=init_scale, algebra=algebra)

observed

observed(domain: SetObject, codomain: SetObject, data: Tensor, algebra: Algebra | None = None) -> ObservedMorphism

Create an observed (fixed) morphism.

PARAMETER DESCRIPTION
domain

Source object.

TYPE: SetObject

codomain

Target object.

TYPE: SetObject

data

Fixed tensor.

TYPE: Tensor

algebra

The enrichment algebra. Defaults to PRODUCT_FUZZY.

TYPE: Algebra or None DEFAULT: None

RETURNS DESCRIPTION
ObservedMorphism

A fixed morphism.

Source code in src/quivers/core/morphisms.py
def observed(
    domain: SetObject,
    codomain: SetObject,
    data: torch.Tensor,
    algebra: Algebra | None = None,
) -> ObservedMorphism:
    """Create an observed (fixed) morphism.

    Parameters
    ----------
    domain : SetObject
        Source object.
    codomain : SetObject
        Target object.
    data : torch.Tensor
        Fixed tensor.
    algebra : Algebra or None
        The enrichment algebra. Defaults to PRODUCT_FUZZY.

    Returns
    -------
    ObservedMorphism
        A fixed morphism.
    """
    return ObservedMorphism(domain, codomain, data, algebra=algebra)

identity

identity(obj: SetObject, algebra: Algebra | None = None) -> ObservedMorphism

Create the identity morphism on an object.

Returns an observed morphism obj -> obj whose tensor is the identity: unit on the diagonal, zero elsewhere.

PARAMETER DESCRIPTION
obj

The object to create an identity for.

TYPE: SetObject

algebra

The enrichment algebra. Defaults to PRODUCT_FUZZY.

TYPE: Algebra or None DEFAULT: None

RETURNS DESCRIPTION
ObservedMorphism

The identity morphism.

Source code in src/quivers/core/morphisms.py
def identity(obj: SetObject, algebra: Algebra | None = None) -> ObservedMorphism:
    """Create the identity morphism on an object.

    Returns an observed morphism obj -> obj whose tensor is the
    identity: unit on the diagonal, zero elsewhere.

    Parameters
    ----------
    obj : SetObject
        The object to create an identity for.
    algebra : Algebra or None
        The enrichment algebra. Defaults to PRODUCT_FUZZY.

    Returns
    -------
    ObservedMorphism
        The identity morphism.
    """
    q = algebra if algebra is not None else PRODUCT_FUZZY
    data = q.identity_tensor(obj.shape)
    return ObservedMorphism(obj, obj, data, algebra=q)

as_torch_module

as_torch_module(m: object) -> Module

Coerce a Morphism into an nn.Module for parameter tracking at a binding site.

The adapter draws the line between the categorical morphism hierarchy (backend-agnostic; lives in quivers.core) and PyTorch's parameter-container infrastructure. Every site that needs to register a morphism's parameters with a parent nn.Module (the MonadicProgram step list, the submodule list of a FanMorphism / StackMorphism composite, a parametric-program parameter slot bound to a morphism) calls this function once and stores the result.

PARAMETER DESCRIPTION
m

The morphism (or other object) to wrap. Accepted forms:

  • Already an nn.Module — returned unchanged so a continuous MonadicProgram step can pass its ContinuousMorphism straight through.
  • A Morphism with a .module() method (every subclass of Morphism defined in this file implements it) — the result of m.module() is returned. The morphism object itself is attached to the wrapper under the synthetic attribute _morphism so downstream code that needs the categorical object (e.g. to compute tensor or apply a Functor) can recover it without rebuilding.

TYPE: object

RETURNS DESCRIPTION
Module

A module whose parameters / buffers are exactly the morphism's, suitable for add_module on a parent.

RAISES DESCRIPTION
TypeError

m is neither an nn.Module nor a Morphism-shaped object with a .module() method.

Source code in src/quivers/core/morphisms.py
def as_torch_module(m: object) -> nn.Module:
    """Coerce a Morphism into an `nn.Module` for parameter
    tracking at a binding site.

    The adapter draws the line between the categorical morphism
    hierarchy (backend-agnostic; lives in `quivers.core`) and
    PyTorch's parameter-container infrastructure. Every site that
    needs to register a morphism's parameters with a parent
    `nn.Module` (the `MonadicProgram` step list, the
    submodule list of a `FanMorphism` / `StackMorphism`
    composite, a parametric-program parameter slot bound to a
    morphism) calls this function once and stores the result.

    Parameters
    ----------
    m : object
        The morphism (or other object) to wrap. Accepted forms:

        * Already an `nn.Module` — returned unchanged so a
          continuous `MonadicProgram` step can pass its
          `ContinuousMorphism` straight through.
        * A `Morphism` with a ``.module()`` method (every
          subclass of `Morphism` defined in this file
          implements it) — the result of ``m.module()`` is
          returned. The morphism object itself is attached to the
          wrapper under the synthetic attribute ``_morphism`` so
          downstream code that needs the categorical object (e.g.
          to compute ``tensor`` or apply a ``Functor``) can recover
          it without rebuilding.

    Returns
    -------
    nn.Module
        A module whose parameters / buffers are exactly the
        morphism's, suitable for ``add_module`` on a parent.

    Raises
    ------
    TypeError
        ``m`` is neither an `nn.Module` nor a
        `Morphism`-shaped object with a ``.module()`` method.
    """
    if isinstance(m, nn.Module):
        return m
    if isinstance(m, Morphism):
        wrapper = m.module()
        if not isinstance(wrapper, nn.Module):
            raise TypeError(
                f"{type(m).__name__}.module() returned "
                f"{type(wrapper).__name__}; expected nn.Module"
            )
        # Attach the original morphism on the wrapper so downstream
        # code that needs the categorical object can recover it
        # without rebuilding from the wrapped parameters.
        wrapper._morphism = m  # type: ignore[attr-defined]
        return wrapper
    raise TypeError(
        f"as_torch_module: cannot adapt {type(m).__name__} to "
        f"nn.Module; expected an nn.Module or a Morphism with a "
        f".module() method"
    )

extract_morphism

extract_morphism(module: Module) -> Morphism | None

Recover the Morphism previously bound through as_torch_module.

Returns the categorical morphism stored on the wrapper, or None if the module was registered directly (i.e. was already an nn.Module subclass) and thus has no separate categorical object attached.

PARAMETER DESCRIPTION
module

A module bound into a parameter-tracking context.

TYPE: Module

RETURNS DESCRIPTION
Morphism or None

The wrapped morphism, or None when module wraps none.

Source code in src/quivers/core/morphisms.py
def extract_morphism(module: nn.Module) -> Morphism | None:
    """Recover the `Morphism` previously bound through
    `as_torch_module`.

    Returns the categorical morphism stored on the wrapper, or
    ``None`` if the module was registered directly (i.e. was
    already an `nn.Module` subclass) and thus has no
    separate categorical object attached.

    Parameters
    ----------
    module : nn.Module
        A module bound into a parameter-tracking context.

    Returns
    -------
    Morphism or None
        The wrapped morphism, or ``None`` when ``module`` wraps none.
    """
    return getattr(module, "_morphism", None)

cup

cup(obj: SetObject, algebra: Algebra | None = None) -> ObservedMorphism

The compact-closed unit η_A : I → A ⊗ A.

For finite-set objects with their natural product, η_A is the diagonal: every entry (a, a) carries the algebra's monoidal unit and the off-diagonal entries carry the join unit (zero). The Kronecker-like tensor produced is the identity morphism's tensor reshaped from (*A.shape, *A.shape) to (1, *A.shape, *A.shape) so that the leading axis is the singleton input I.

Categorically the cup and the trivial-domain identity satisfy ε ∘ (id ⊗ η) = id; the snake equation that makes V-Cat compact-closed.

PARAMETER DESCRIPTION
obj

The object whose dual is being introduced. Every algebra ships an identity tensor; this morphism reshapes it as a Kleisli arrow from the singleton domain.

TYPE: SetObject

algebra

Override the default (ProductFuzzyAlgebra) algebra.

TYPE: Algebra DEFAULT: None

RETURNS DESCRIPTION
ObservedMorphism

Morphism I → A ⊗ A whose tensor is the diagonal of the target.

Source code in src/quivers/core/morphisms.py
def cup(obj: SetObject, algebra: Algebra | None = None) -> ObservedMorphism:
    """The compact-closed unit ``η_A : I → A ⊗ A``.

    For finite-set objects with their natural product, ``η_A`` is
    the *diagonal*: every entry ``(a, a)`` carries the algebra's
    monoidal unit and the off-diagonal entries carry the join unit
    (``zero``). The Kronecker-like tensor produced is the identity
    morphism's tensor reshaped from ``(*A.shape, *A.shape)`` to
    ``(1, *A.shape, *A.shape)`` so that the leading axis is the
    singleton input ``I``.

    Categorically the cup and the trivial-domain identity satisfy
    ``ε ∘ (id ⊗ η) = id``; the snake equation that makes V-Cat
    compact-closed.

    Parameters
    ----------
    obj : SetObject
        The object whose dual is being introduced. Every algebra
        ships an identity tensor; this morphism reshapes it as a
        Kleisli arrow from the singleton domain.
    algebra : Algebra, optional
        Override the default (ProductFuzzyAlgebra) algebra.

    Returns
    -------
    ObservedMorphism
        Morphism ``I → A ⊗ A`` whose tensor is the diagonal of the
        target.
    """
    q = algebra if algebra is not None else PRODUCT_FUZZY
    diag = q.identity_tensor(obj.shape)
    # Wrap in a leading singleton axis so the morphism's domain is
    # the unit object I (the singleton finite set).
    I = FinSet(name="1", cardinality=1)
    cod = ProductSet(components=(obj, obj))
    return ObservedMorphism(I, cod, diag.unsqueeze(0), algebra=q)

cap

cap(obj: SetObject, algebra: Algebra | None = None) -> ObservedMorphism

The compact-closed counit ε_A : A ⊗ A → I.

The dual of cup. The tensor is the diagonal flattened into (*A.shape, *A.shape, 1) so the trailing axis is the unit codomain I.

PARAMETER DESCRIPTION
obj

The object A.

TYPE: SetObject

algebra

Override the default (ProductFuzzyAlgebra) algebra.

TYPE: Algebra DEFAULT: None

RETURNS DESCRIPTION
ObservedMorphism

Morphism A ⊗ A → I whose tensor is the diagonal of the source.

Source code in src/quivers/core/morphisms.py
def cap(obj: SetObject, algebra: Algebra | None = None) -> ObservedMorphism:
    """The compact-closed counit ``ε_A : A ⊗ A → I``.

    The dual of `cup`. The tensor is the diagonal flattened
    into ``(*A.shape, *A.shape, 1)`` so the trailing axis is the
    unit codomain ``I``.

    Parameters
    ----------
    obj : SetObject
        The object ``A``.
    algebra : Algebra, optional
        Override the default (ProductFuzzyAlgebra) algebra.

    Returns
    -------
    ObservedMorphism
        Morphism ``A ⊗ A → I`` whose tensor is the diagonal of the
        source.
    """
    q = algebra if algebra is not None else PRODUCT_FUZZY
    diag = q.identity_tensor(obj.shape)
    I = FinSet(name="1", cardinality=1)
    dom = ProductSet(components=(obj, obj))
    return ObservedMorphism(dom, I, diag.unsqueeze(-1), algebra=q)