Kan Extensions

Left and right Kan extensions in enriched categories.

kan_extensions

Kan extensions: generalized (re)indexing of morphisms.

The left Kan extension of a morphism R: A → B along a map p: A → A' produces a morphism Lan_p(R): A' → B via:

(Lan_p R)(a', b) = ⋁{a : p(a) = a'} R(a, b)

The right Kan extension uses meet (⋀) instead of join (⋁):

(Ran_p R)(a', b) = ⋀{a : p(a) = a'} R(a, b)

Marginalization is a special case: left Kan extension along a projection π: A₁×...×Aₙ → A_{i₁}×...×A_{iₖ}.

This module provides:

ObjectMap (abstract) — maps between finite sets
├── Projection       — π: A₁×...×Aₙ → some subset of components
└── Inclusion        — ι: A → A + B (coproduct injection)

left_kan()  — left Kan extension
right_kan() — right Kan extension

ObjectMap

Bases: ABC

An abstract deterministic map between finite sets.

Represents a function p: A → A' at the element level, used as the map along which to compute Kan extensions.

source abstractmethod property

source: SetObject

The source object A.

target abstractmethod property

target: SetObject

The target object A'.

apply abstractmethod

apply(source_idx: tuple[int, ...]) -> tuple[int, ...]

Map a source index to a target index.

PARAMETER DESCRIPTION
source_idx

An element of A as a multi-index.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
tuple[int, ...]

The corresponding element of A'.

Source code in src/quivers/enriched/kan_extensions.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@abstractmethod
def apply(self, source_idx: tuple[int, ...]) -> tuple[int, ...]:
    """Map a source index to a target index.

    Parameters
    ----------
    source_idx : tuple[int, ...]
        An element of A as a multi-index.

    Returns
    -------
    tuple[int, ...]
        The corresponding element of A'.
    """
    ...

fiber_indices

fiber_indices(target_idx: tuple[int, ...]) -> list[tuple[int, ...]]

Return all source indices that map to the given target index.

PARAMETER DESCRIPTION
target_idx

An element of A'.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
list[tuple[int, ...]]

All a ∈ A such that p(a) = target_idx.

Source code in src/quivers/enriched/kan_extensions.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def fiber_indices(self, target_idx: tuple[int, ...]) -> list[tuple[int, ...]]:
    """Return all source indices that map to the given target index.

    Parameters
    ----------
    target_idx : tuple[int, ...]
        An element of A'.

    Returns
    -------
    list[tuple[int, ...]]
        All a ∈ A such that p(a) = target_idx.
    """
    result: list[tuple[int, ...]] = []
    for src_idx in itertools.product(*(range(s) for s in self.source.shape)):
        if self.apply(src_idx) == target_idx:
            result.append(src_idx)
    return result

Projection

Projection(product: ProductSet, keep_indices: tuple[int, ...])

Bases: ObjectMap

Projection from a product to a subset of its components.

Given ProductSet(A₁, ..., Aₙ), projects onto the components at the specified indices: π(a₁, ..., aₙ) = (a_{i₁}, ..., a_{iₖ}).

PARAMETER DESCRIPTION
product

The source product set.

TYPE: ProductSet

keep_indices

Indices of the components to keep (0-based).

TYPE: tuple[int, ...]

Source code in src/quivers/enriched/kan_extensions.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def __init__(self, product: ProductSet, keep_indices: tuple[int, ...]) -> None:
    if not isinstance(product, ProductSet):
        raise TypeError(
            f"Projection requires ProductSet, got {type(product).__name__}"
        )
    n = len(product.components)
    for idx in keep_indices:
        if not 0 <= idx < n:
            raise ValueError(f"component index {idx} out of range [0, {n})")
    self._product = product
    self._keep_indices = keep_indices
    self._drop_indices = tuple((i for i in range(n) if i not in keep_indices))
    kept = [product.components[i] for i in keep_indices]
    if len(kept) == 1:
        self._target = kept[0]
    else:
        self._target = ProductSet(components=tuple(kept))
    self._dim_offsets: list[int] = []
    offset = 0
    for comp in product.components:
        self._dim_offsets.append(offset)
        offset += comp.ndim

source property

source: ProductSet

The source product set.

target property

target: SetObject

The target (projected) set.

keep_indices property

keep_indices: tuple[int, ...]

Indices of the kept components.

drop_indices property

drop_indices: tuple[int, ...]

Indices of the dropped (marginalized) components.

apply

apply(source_idx: tuple[int, ...]) -> tuple[int, ...]

Project by keeping only the selected component indices.

Source code in src/quivers/enriched/kan_extensions.py
146
147
148
149
150
151
152
153
154
def apply(self, source_idx: tuple[int, ...]) -> tuple[int, ...]:
    """Project by keeping only the selected component indices."""
    result: list[int] = []
    for comp_idx in self._keep_indices:
        comp = self._product.components[comp_idx]
        offset = self._dim_offsets[comp_idx]
        for d in range(comp.ndim):
            result.append(source_idx[offset + d])
    return tuple(result)

Inclusion

Inclusion(coproduct: CoproductSet, component_index: int)

Bases: ObjectMap

Coproduct inclusion ι_k: Aₖ → A₁ + ... + Aₙ.

Embeds the k-th component of a CoproductSet into the full set.

PARAMETER DESCRIPTION
coproduct

The target coproduct set.

TYPE: CoproductSet

component_index

Which component to include (0-based).

TYPE: int

Source code in src/quivers/enriched/kan_extensions.py
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, coproduct: CoproductSet, component_index: int) -> None:
    if not isinstance(coproduct, CoproductSet):
        raise TypeError(
            f"Inclusion requires CoproductSet, got {type(coproduct).__name__}"
        )
    n = len(coproduct.components)
    if not 0 <= component_index < n:
        raise ValueError(f"component_index {component_index} out of range [0, {n})")
    self._coproduct = coproduct
    self._component_index = component_index
    self._component = coproduct.components[component_index]
    self._offset = coproduct.offset(component_index)

source property

source: SetObject

The source component.

target property

target: CoproductSet

The target coproduct set.

apply

apply(source_idx: tuple[int, ...]) -> tuple[int, ...]

Embed into the coproduct with offset.

Source code in src/quivers/enriched/kan_extensions.py
193
194
195
196
def apply(self, source_idx: tuple[int, ...]) -> tuple[int, ...]:
    """Embed into the coproduct with offset."""
    (flat_idx,) = source_idx
    return (self._offset + flat_idx,)

left_kan

left_kan(morph: Morphism, along: ObjectMap, quantale: Quantale | None = None) -> ObservedMorphism

Left Kan extension of a morphism along an object map.

Computes: (Lan_p R)(a', b) = ⋁{a : p(a) = a'} R(a, b)

PARAMETER DESCRIPTION
morph

The morphism R: A → B.

TYPE: Morphism

along

The map p: A → A'.

TYPE: ObjectMap

quantale

The enrichment algebra. Defaults to PRODUCT_FUZZY.

TYPE: Quantale or None DEFAULT: None

RETURNS DESCRIPTION
ObservedMorphism

The left Kan extension Lan_p(R): A' → B.

Source code in src/quivers/enriched/kan_extensions.py
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
def left_kan(
    morph: Morphism, along: ObjectMap, quantale: Quantale | None = None
) -> ObservedMorphism:
    """Left Kan extension of a morphism along an object map.

    Computes: (Lan_p R)(a', b) = ⋁{a : p(a) = a'} R(a, b)

    Parameters
    ----------
    morph : Morphism
        The morphism R: A → B.
    along : ObjectMap
        The map p: A → A'.
    quantale : Quantale or None
        The enrichment algebra. Defaults to PRODUCT_FUZZY.

    Returns
    -------
    ObservedMorphism
        The left Kan extension Lan_p(R): A' → B.
    """
    q = quantale if quantale is not None else PRODUCT_FUZZY
    target = along.target
    codomain = morph.codomain
    result_shape = (*target.shape, *codomain.shape)
    result = torch.full(result_shape, q.zero)
    source_tensor = morph.tensor.detach()
    for tgt_idx in itertools.product(*(range(s) for s in target.shape)):
        fiber = along.fiber_indices(tgt_idx)
        if not fiber:
            continue
        slices = torch.stack([source_tensor[src_idx] for src_idx in fiber])
        joined = q.join(slices, dim=0)
        result[tgt_idx] = joined
    return observed(target, codomain, result, quantale=q)

right_kan

right_kan(morph: Morphism, along: ObjectMap, quantale: Quantale | None = None) -> ObservedMorphism

Right Kan extension of a morphism along an object map.

Computes: (Ran_p R)(a', b) = ⋀{a : p(a) = a'} R(a, b)

PARAMETER DESCRIPTION
morph

The morphism R: A → B.

TYPE: Morphism

along

The map p: A → A'.

TYPE: ObjectMap

quantale

The enrichment algebra. Defaults to PRODUCT_FUZZY.

TYPE: Quantale or None DEFAULT: None

RETURNS DESCRIPTION
ObservedMorphism

The right Kan extension Ran_p(R): A' → B.

Source code in src/quivers/enriched/kan_extensions.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def right_kan(
    morph: Morphism, along: ObjectMap, quantale: Quantale | None = None
) -> ObservedMorphism:
    """Right Kan extension of a morphism along an object map.

    Computes: (Ran_p R)(a', b) = ⋀{a : p(a) = a'} R(a, b)

    Parameters
    ----------
    morph : Morphism
        The morphism R: A → B.
    along : ObjectMap
        The map p: A → A'.
    quantale : Quantale or None
        The enrichment algebra. Defaults to PRODUCT_FUZZY.

    Returns
    -------
    ObservedMorphism
        The right Kan extension Ran_p(R): A' → B.
    """
    q = quantale if quantale is not None else PRODUCT_FUZZY
    target = along.target
    codomain = morph.codomain
    result_shape = (*target.shape, *codomain.shape)
    result = torch.full(result_shape, q.unit)
    source_tensor = morph.tensor.detach()
    for tgt_idx in itertools.product(*(range(s) for s in target.shape)):
        fiber = along.fiber_indices(tgt_idx)
        if not fiber:
            continue
        slices = torch.stack([source_tensor[src_idx] for src_idx in fiber])
        met = q.meet(slices, dim=0)
        result[tgt_idx] = met
    return observed(target, codomain, result, quantale=q)