# -*- coding: utf-8 -*-
"""
The abstract base classes for categories.
These mirror the concrete hierarchy of :mod:`discopy` modules: each class adds
the characteristic generator of its categorical structure as an
:func:`abc.abstractmethod`, e.g. :class:`BraidedCategory` is a
:class:`MonoidalCategory` with an abstract :meth:`BraidedCategory.braid`.
Summary
-------
.. autosummary::
:template: class.rst
:nosignatures:
:toctree:
Category
MonoidalCategory
BraidedCategory
TracedCategory
BalancedCategory
SymmetricCategory
MarkovCategory
FeedbackCategory
ClosedCategory
RigidCategory
PivotalCategory
RibbonCategory
NamedGeneric
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Generic, Optional, Type, TypeVar
from discopy.utils import get_origin
[docs]
class Category[T](ABC):
"""
A category is a Python class with methods :code:`dom, cod, id, then`,
together with an attribute :attr:`ob` for its objects.
This base class also implements syntactic sugar :code:`>>` and :code:`<<`
for forward and backward composition with the method :code:`then`.
Example
-------
>>> class List(list, Category):
... ob, dom, cod = type(None), None, None
... def then(self, other):
... return self + other
>>> assert List([1, 2]) >> List([3]) == List([1, 2, 3])
>>> assert List([3]) << List([1, 2]) == List([1, 2, 3])
"""
ar: Type[Category]
sum_factory: Type[Category]
ob: Type[T]
dom: T
cod: T
[docs]
@abstractmethod
def then(self, other: Optional[Category[T]], *others: Category[T]
) -> Category[T]:
"""
Sequential composition, to be instantiated.
Parameters:
other : The other arrow to compose sequentially.
"""
[docs]
def is_composable(self, other: Category) -> bool:
"""
Whether two arrows are composable, i.e. the codomain of the first is
the domain of the second.
Parameters:
other : The other arrow.
"""
return self.cod == other.dom
[docs]
def is_parallel(self, other: Category) -> bool:
"""
Whether two arrows are parallel, i.e. they have the same
domain and codomain.
Parameters:
other : The other arrow.
"""
return (self.dom, self.cod) == (other.dom, other.cod)
__rshift__ = __llshift__ = lambda self, other: self.then(other)
__lshift__ = __lrshift__ = lambda self, other: other.then(self)
[docs]
class MonoidalCategory[T](Category[T]):
"""
A monoidal category is a :class:`Category` with a method :code:`tensor` for
both its objects and its arrows.
This base class also implements syntactic sugar :code:`@` for whiskering.
"""
[docs]
@classmethod
@abstractmethod
def id(cls, dom: T) -> MonoidalCategory:
"""
Identity on a given domain, to be instantiated.
Parameters:
dom : The object on which to take the identity.
"""
[docs]
@abstractmethod
def tensor(self, other: MonoidalCategory) -> MonoidalCategory:
"""
Parallel composition, to be instantiated.
Parameters:
other : The other arrow to compose in parallel.
"""
[docs]
@classmethod
def whisker(cls, other: T) -> MonoidalCategory:
"""
Apply :meth:`MonoidalCategory.id` if :code:`other` is not tensorable
else do nothing.
Parameters:
other : The whiskering object.
"""
return other if isinstance(other, MonoidalCategory) else cls.id(other)
def __matmul__(self, other):
return self.tensor(self.whisker(other))
def __rmatmul__(self, other):
return self.whisker(other).tensor(self)
[docs]
class TracedCategory[T](MonoidalCategory[T]):
"""
A traced category is a :class:`MonoidalCategory` with a method
:code:`trace` for the partial trace of a morphism over some objects.
"""
[docs]
@abstractmethod
def trace(self, n: int = 1, left: bool = False) -> TracedCategory:
"""
The trace of a morphism, to be instantiated.
Parameters:
n : The number of objects to trace over.
left : Whether to trace the wires on the left or right.
"""
class BiclosedCategory[T](MonoidalCategory[T]):
"""
A biclosed category is a :class:`MonoidalCategory` with methods :code:`ev`
and :code:`curry` for the evaluation and currying of morphisms.
We also assume the type for objects comes with methods for left and right
exponentials :code`x << y` and :code`x >> y`.
"""
@classmethod
@abstractmethod
def ev(cls, base: T, exponent: T, left: bool = True) -> BiclosedCategory:
"""
The evaluation of an exponential type, to be instantiated.
Parameters:
base : The base of the exponential type.
exponent : The exponent of the exponential type.
left : Whether to take the left or right evaluation.
"""
@abstractmethod
def curry(self, n: int = 1, left: bool = True) -> BiclosedCategory:
"""
The currying of a morphism, to be instantiated.
Parameters:
n : The number of objects to curry.
left : Whether to curry on the left or right.
"""
[docs]
class RigidCategory[T](BiclosedCategory[T]):
"""
A rigid category is a :class:`BiclosedCategory` where every object has a
left and right adjoint, witnessed by methods :code:`cups` and :code:`caps`.
"""
[docs]
@classmethod
@abstractmethod
def cups(cls, left: T, right: T) -> RigidCategory:
"""
The cups witnessing :code:`right` as the adjoint of :code:`left`.
Parameters:
left : The left-hand side of the cups.
right : Its adjoint, i.e. the right-hand side of the cups.
"""
[docs]
@classmethod
@abstractmethod
def caps(cls, left: T, right: T) -> RigidCategory:
"""
The caps witnessing :code:`right` as the adjoint of :code:`left`.
Parameters:
left : The left-hand side of the caps.
right : Its adjoint, i.e. the right-hand side of the caps.
"""
[docs]
class PivotalCategory[T](RigidCategory[T], TracedCategory[T]):
"""
A pivotal category is a :class:`RigidCategory` where the left and right
adjoints coincide, hence it is also a :class:`TracedCategory`.
"""
[docs]
class BraidedCategory[T](MonoidalCategory[T]):
"""
A braided category is a :class:`MonoidalCategory` with a method
:code:`braid` for the natural isomorphism :code:`x @ y -> y @ x`.
"""
[docs]
@classmethod
@abstractmethod
def braid(cls, left: T, right: T) -> BraidedCategory:
"""
The braid of two objects, to be instantiated.
Parameters:
left : The object on the left of the braid.
right : The object on the right of the braid.
"""
[docs]
class BalancedCategory[T](BraidedCategory[T], TracedCategory[T]):
"""
A balanced category is a :class:`BraidedCategory` and a
:class:`TracedCategory` with a method :code:`twist` for the natural
automorphism :code:`x -> x`.
"""
[docs]
@classmethod
@abstractmethod
def twist(cls, dom: T) -> BalancedCategory:
"""
The twist on an object, to be instantiated.
Parameters:
dom : The object on which to take the twist.
"""
[docs]
class SymmetricCategory[T](BalancedCategory[T]):
"""
A symmetric category is a :class:`BalancedCategory` where the braid is its
own inverse called :code:`swap` for the symmetry :code:`x @ y -> y @ x`.
"""
[docs]
@classmethod
@abstractmethod
def swap(cls, left: T, right: T) -> SymmetricCategory:
"""
The swap of two objects, to be instantiated.
Parameters:
left : The object on the left of the swap.
right : The object on the right of the swap.
"""
[docs]
class MarkovCategory[T](SymmetricCategory[T]):
"""
A Markov category is a :class:`SymmetricCategory` with methods
:code:`copy` and :code:`merge` for the supply of commutative comonoids.
"""
[docs]
@classmethod
@abstractmethod
def copy(cls, x: T, n: int = 2) -> MarkovCategory:
"""
Make :code:`n` copies of a given object :code:`x`.
Parameters:
x : The object to copy.
n : The number of copies.
"""
[docs]
@classmethod
@abstractmethod
def merge(cls, x: T, n: int = 2) -> MarkovCategory:
"""
Merge :code:`n` copies of a given object :code:`x`.
Parameters:
x : The object to merge.
n : The number of copies.
"""
[docs]
class ClosedCategory[T](BiclosedCategory[T], MarkovCategory[T]):
"""
A closed category is a symmetric :class:`BiclosedCategory`. We also assume
it comes with copy and discard so it is also a :class:`MarkovCategory`.
"""
[docs]
class FeedbackCategory[T](MarkovCategory[T]):
"""
A feedback category is a :class:`MarkovCategory` with a :code:`delay`
endofunctor and a :code:`feedback` operator.
"""
[docs]
@abstractmethod
def delay(self, n_steps: int = 1) -> FeedbackCategory:
"""
The delay endofunctor applied to a morphism.
Parameters:
n_steps : The number of time steps to delay.
"""
[docs]
@abstractmethod
def feedback(self, dom=None, cod=None, mem=None) -> FeedbackCategory:
"""
The feedback operator on a morphism.
Parameters:
dom : The domain of the feedback.
cod : The codomain of the feedback.
mem : The memory type to trace over.
"""
[docs]
class RibbonCategory[T](PivotalCategory[T], BalancedCategory[T]):
"""
A ribbon category is a :class:`PivotalCategory` which is also a
:class:`BalancedCategory`, i.e. where diagrams can draw knots and links.
"""
class CompactCategory[T](RibbonCategory[T], SymmetricCategory[T]):
"""
A compact category is a :class:`RibbonCategory` which is also a
:class:`SymmetricCategory`, i.e. with cups, caps and swaps.
"""
class HypergraphCategory[T](CompactCategory[T], MarkovCategory[T]):
"""
A hypergraph category is a symmetric category with a supply of spiders,
i.e. special commutative Frobenius algebras on each objects.
This makes it both a :class:`CompactCategory` and a :class:`MarkovCategory`
"""
@classmethod
@abstractmethod
def spiders(cls, n_legs_in: int, n_legs_out: int, typ: T, phases=None
) -> HypergraphCategory:
"""
The spiders on a given type with ``n_legs_in`` and ``n_legs_out`` and
some optional vector of ``phases``.
Parameters:
n_legs_in : The number of legs in for each spider.
n_legs_out : The number of legs out for each spider.
typ : The type of the spiders.
phases : The phase for each spider.
"""
[docs]
class NamedGeneric(Generic[TypeVar('T')]):
"""
A ``NamedGeneric`` is a ``Generic`` where the type parameter has a name.
Parameters:
attr : The name of the type parameter.
Note
----
In a standard ``Generic`` class, the type parameter disappears when the
member of the class is instantiated, e.g.
>>> assert list[int]([1, 2, 3])\\
... == list[float]([1, 2, 3])\\
... == [1, 2, 3]
In a ``NamedGeneric``, the type parameter is attached to the members of the
class so that we have access to it.
Example
-------
>>> from dataclasses import dataclass
>>> @dataclass
... class L(NamedGeneric["dtype"]):
... inside: list
>>> assert L[int]([1, 2, 3]).dtype == int
>>> assert L[int]([1, 2, 3]) != L[float]([1, 2, 3])
"""
_cache = dict()
def __class_getitem__(_, attributes):
if not isinstance(attributes, tuple):
attributes = (attributes,)
G = Generic.__class_getitem__(tuple(map(TypeVar, attributes)))
class Result(G):
def __class_getitem__(cls, values):
if hasattr(cls, "__is_named_generic__"):
cls = cls.__bases__[0]
values = values if isinstance(values, tuple) else (values,)
cls_values = tuple(
getattr(cls, attr, None) for attr in attributes)
if cls not in NamedGeneric._cache:
NamedGeneric._cache[cls] = {cls_values: cls}
if values not in NamedGeneric._cache[cls]:
origin = get_origin(cls)
class C(origin):
__is_named_generic__ = True
# We need this to fix pickling of nested classes
# https://stackoverflow.com/questions/1947904/how-can-i-pickle-a-dynamically-created-nested-class-in-python
def __reduce__(self):
func, args, data = super().__reduce__()
# Check if class name is of the form:
# *ClassName*[*type*]
if '[' in args[0].__name__:
args = (origin, ) + args[1:]
data |= {"__class_getitem__values__": values}
return func, args, data
C.__module__ = origin.__module__
names = [getattr(v, "__name__", str(v)) for v in values]
C.__name__ = C.__qualname__ = origin.__name__\
+ f"[{', '.join(names)}]"
C.__origin__ = cls
for attr, value in zip(attributes, values):
setattr(C, attr, value)
NamedGeneric._cache[cls][values] = C
return NamedGeneric._cache[cls][values]
__name__ = __qualname__\
= f"NamedGeneric[{', '.join(map(repr, attributes))}]"
for attr in attributes:
setattr(Result, attr, getattr(Result, attr, None))
return Result
def __setstate__(self, state):
if "__class_getitem__values__" in state:
new_cls = self.__class__[state["__class_getitem__values__"]]
self.__class__ = new_cls