Morphisms

Base morphism classes, composition operations, and morphism utilities for all categorical structures.

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
90
91
92
93
94
95
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
123
124
125
126
@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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
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).
    """
    from quivers.core.objects import ProductSet

    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
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
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
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
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
595
596
597
598
599
600
601
602
603
604
605
606
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
645
646
647
648
649
650
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
696
697
698
699
700
701
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
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
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
809
810
811
812
813
814
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
872
873
874
875
876
877
878
879
880
881
882
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
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
def __init__(self, inner: Morphism, direction: str = "right") -> None:
    from quivers.core.objects import FreeResiduated, ProductSet
    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
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
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
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
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
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
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
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
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 therefore has no separate categorical object attached.

Source code in src/quivers/core/morphisms.py
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
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 therefore has no
    separate categorical object attached.
    """
    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
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
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.
    """
    from quivers.core.objects import FinSet, ProductSet

    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.

Source code in src/quivers/core/morphisms.py
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
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``.
    """
    from quivers.core.objects import FinSet, ProductSet

    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)