# -*- coding: utf-8 -*-
"""
Implements the free dagger monoidal category
and strong dagger monoidal functors.
The syntax for diagrams is given by the following grammar::
diagram ::= Box(name, dom=type, cod=type)
| diagram[::-1]
| diagram @ diagram
| diagram >> diagram
| Id(type)
where :code:`[::-1]`, :code:`@` and :code:`>>` denote the dagger, tensor and
composition respectively. The syntax for types is given by::
type ::= Ty(name) | type @ type | Ty()
Notes
-----
We can check the axioms for dagger monoidal categories, up to interchanger.
>>> x, y, z, w = Ty('x'), Ty('y'), Ty('z'), Ty('w')
>>> f0, f1 = Box('f0', x, y), Box('f1', z, w)
>>> d = Id(x) @ f1 >> f0 @ Id(w)
>>> assert d == (f0 @ f1).interchange(0, 1)
>>> assert f0 @ f1 == d.interchange(0, 1)
>>> assert (f0 @ f1)[::-1][::-1] == f0 @ f1
>>> assert (f0 @ f1)[::-1].interchange(0, 1) == f0[::-1] @ f1[::-1]
We can check the Eckmann-Hilton argument, up to interchanger.
>>> s0, s1 = Box('s0', Ty(), Ty()), Box('s1', Ty(), Ty())
>>> assert s0 @ s1 == s0 >> s1 == (s1 @ s0).interchange(0, 1)
>>> assert s1 @ s0 == s1 >> s0 == (s0 @ s1).interchange(0, 1)
.. image:: ../_static/imgs/EckmannHilton.gif
:align: center
"""
from discopy import cat, messages, drawing, rewriting
from discopy.cat import Ob
from discopy.messages import WarnOnce
from discopy.utils import factory_name, from_tree, unbiased
warn_permutation = WarnOnce()
[docs]class Ty(Ob):
"""
Implements a type as a list of :class:`discopy.cat.Ob`, used as domain and
codomain for :class:`monoidal.Diagram`.
Types are the free monoid on objects with product
:code:`@` and unit :code:`Ty()`.
Parameters
----------
objects : list of :class:`discopy.cat.Ob`
List of objects or object names.
Important
---------
Elements that are not instance of :class:`discopy.cat.Ob` are implicitly
taken to be the name of an object, i.e.
:code:`Ty('x', 'y') == Ty(Ob('x'), Ob('y'))`.
Notes
-----
We can check the axioms for a monoid.
>>> x, y, z, unit = Ty('x'), Ty('y'), Ty('z'), Ty()
>>> assert x @ unit == x == unit @ x
>>> assert (x @ y) @ z == x @ y @ z == x @ (y @ z)
"""
def __init__(self, *objects):
self._objects = tuple(
x if isinstance(x, Ob) else Ob(x) for x in objects)
super().__init__(self)
@property
def objects(self):
"""
List of objects forming a type.
Note
----
A type may be sliced into subtypes.
>>> t = Ty('x', 'y', 'z')
>>> assert t[0] == Ob('x')
>>> assert t[:1] == Ty('x')
>>> assert t[1:] == Ty('y', 'z')
"""
return list(self._objects)
[docs] def tensor(self, *others):
"""
Returns the tensor of types, i.e. the concatenation of their lists
of objects. This is called with the binary operator `@`.
>>> Ty('x') @ Ty('y', 'z')
Ty('x', 'y', 'z')
Parameters
----------
other : monoidal.Ty
Returns
-------
t : monoidal.Ty
such that :code:`t.objects == self.objects + other.objects`.
Note
----
We can take the sum of a list of type, specifying the unit `Ty()`.
>>> types = Ty('x'), Ty('y'), Ty('z')
>>> Ty().tensor(*types)
Ty('x', 'y', 'z')
We can take the exponent of a type by any natural number.
>>> Ty('x') ** 3
Ty('x', 'x', 'x')
"""
for other in others:
if not isinstance(other, Ty):
raise TypeError(messages.type_err(Ty, other))
objects = self.objects + [x for t in others for x in t.objects]
return self.upgrade(Ty(*objects))
[docs] def count(self, obj):
"""
Counts the occurrence of a given object.
Parameters
----------
obj : :class:`Ty` or :class:`Ob`
either a type of length 1 or an object
Returns
-------
n : int
such that :code:`n == self.objects.count(ob)`.
Examples
--------
>>> x = Ty('x')
>>> xs = x ** 5
>>> assert xs.count(x) == xs.count(x[0]) == xs.objects.count(Ob('x'))
"""
obj, = obj if isinstance(obj, Ty) else (obj, )
return self._objects.count(obj)
[docs] @staticmethod
def upgrade(old):
""" Allows class inheritance for tensor and __getitem__. """
return old
[docs] def downgrade(self):
""" Downgrades to :class:`discopy.monoidal.Ty`. """
return Ty(*self)
def __eq__(self, other):
return isinstance(other, Ty) and self._objects == other._objects
def __hash__(self):
return hash(repr(self))
def __repr__(self):
return "Ty({})".format(', '.join(repr(x.name) for x in self._objects))
def __str__(self):
return ' @ '.join(map(str, self._objects)) or 'Ty()'
def __len__(self):
return len(self._objects)
def __iter__(self):
for i in range(len(self)):
yield self[i]
def __getitem__(self, key):
if isinstance(key, slice):
return self.upgrade(Ty(*self._objects[key]))
return self._objects[key]
def __matmul__(self, other):
return self.tensor(other)
def __pow__(self, n_times):
if not isinstance(n_times, int):
raise TypeError(messages.type_err(int, n_times))
result = type(self)()
for _ in range(n_times):
result = result @ self
return result
def to_tree(self):
return {
'factory': factory_name(self),
'objects': [x.to_tree() for x in self.objects]}
@classmethod
def from_tree(cls, tree):
return cls(*map(from_tree, tree['objects']))
def types(names):
""" Transforms strings into instances of :class:`discopy.monoidal.Ty`.
Examples
--------
>>> x, y, z = types("x y z")
>>> x, y, z
(Ty('x'), Ty('y'), Ty('z'))
"""
return list(map(Ty, names.split()))
[docs]class PRO(Ty):
""" Implements the objects of a PRO, i.e. a non-symmetric PROP.
Wraps a natural number n into a unary type Ty(1, ..., 1) of length n.
Parameters
----------
n : int
Number of wires.
Examples
--------
>>> PRO(1) @ PRO(1)
PRO(2)
>>> assert PRO(3) == Ty(1, 1, 1)
>>> assert PRO(1) == PRO(Ob(1))
"""
@staticmethod
def upgrade(old):
for obj in old:
if obj.name != 1:
raise TypeError(messages.type_err(int, obj.name))
return PRO(len(old))
def __init__(self, n=0):
if isinstance(n, PRO):
n = len(n)
if isinstance(n, Ob):
n = n.name
super().__init__(*(n * [1]))
@classmethod
def from_tree(cls, tree):
return cls(len(tree["objects"]))
def __repr__(self):
return "PRO({})".format(len(self))
def __str__(self):
return repr(len(self))
[docs]class Layer(cat.Box):
"""
Layer of a diagram, i.e. a box with wires to the left and right.
Parameters
----------
left : monoidal.Ty
Left wires.
box : monoidal.Box
Middle box.
right : monoidal.Ty
Right wires.
Examples
--------
>>> x, y, z = Ty('x'), Ty('y'), Ty('z')
>>> f, g = Box('f', y, z), Box('g', z, x)
>>> Layer(x, f, z)
Layer(Ty('x'), Box('f', Ty('y'), Ty('z')), Ty('z'))
>>> first, then = Layer(x, f, z), Layer(x, g, z)
>>> print(first >> then)
Id(x) @ f @ Id(z) >> Id(x) @ g @ Id(z)
"""
def __init__(self, left, box, right):
self._left, self._box, self._right = left, box, right
dom, cod = left @ box.dom @ right, left @ box.cod @ right
super().__init__("Layer", dom, cod)
def __iter__(self):
yield self._left
yield self._box
yield self._right
def __repr__(self):
return "Layer({}, {}, {})".format(
*map(repr, (self._left, self._box, self._right)))
def __str__(self):
left, box, right = self
return ("{} @ ".format(box.id(left)) if left else "")\
+ str(box)\
+ (" @ {}".format(box.id(right)) if right else "")
def __getitem__(self, key):
if key == slice(None, None, -1):
return Layer(self._left, self._box[::-1], self._right)
return super().__getitem__(key)
[docs]class Diagram(cat.Arrow):
"""
Defines a diagram given dom, cod, a list of boxes and offsets.
Parameters
----------
dom : monoidal.Ty
Domain of the diagram.
cod : monoidal.Ty
Codomain of the diagram.
boxes : list of :class:`Diagram`
Boxes of the diagram.
offsets : list of int
Offsets of each box in the diagram.
layers : list of :class:`Layer`, optional
Layers of the diagram,
computed from boxes and offsets if :code:`None`.
Raises
------
:class:`AxiomError`
Whenever the boxes do not compose.
Examples
--------
>>> x, y, z, w = Ty('x'), Ty('y'), Ty('z'), Ty('w')
>>> f0, f1, g = Box('f0', x, y), Box('f1', z, w), Box('g', y @ w, y)
>>> d = Diagram(x @ z, y, [f0, f1, g], [0, 1, 0])
>>> assert d == f0 @ f1 >> g
>>> d.draw(figsize=(2, 2),
... path='docs/_static/imgs/monoidal/arrow-example.png')
.. image:: ../_static/imgs/monoidal/arrow-example.png
:align: center
"""
@staticmethod
def upgrade(old):
return old
[docs] def downgrade(self):
""" Downcasting to :class:`discopy.monoidal.Diagram`. """
dom, cod = Ty(*self.dom), Ty(*self.cod)
boxes, offsets = [box.downgrade() for box in self.boxes], self.offsets
return Diagram(dom, cod, boxes, offsets)
def __init__(self, dom, cod, boxes, offsets, layers=None):
if not isinstance(dom, Ty):
raise TypeError(messages.type_err(Ty, dom))
if not isinstance(cod, Ty):
raise TypeError(messages.type_err(Ty, cod))
if len(boxes) != len(offsets):
raise ValueError(messages.boxes_and_offsets_must_have_same_len())
if layers is None:
layers = cat.Id(dom)
for box, off in zip(boxes, offsets):
if not isinstance(box, Diagram):
raise TypeError(messages.type_err(Diagram, box))
if not isinstance(off, int):
raise TypeError(messages.type_err(int, off))
left = layers.cod[:off] if layers else dom[:off]
right = layers.cod[off + len(box.dom):]\
if layers else dom[off + len(box.dom):]
layers = layers >> self.layer_factory(left, box, right)
layers = layers >> cat.Id(cod)
self._layers, self._offsets = layers, tuple(offsets)
super().__init__(dom, cod, boxes, _scan=False)
def to_tree(self):
return dict(cat.Arrow.to_tree(self), offsets=self.offsets)
@classmethod
def from_tree(cls, tree):
arrow = cat.Arrow.from_tree(tree)
return cls(arrow.dom, arrow.cod, arrow.boxes, tree['offsets'])
@property
def offsets(self):
""" The offset of a box is the number of wires to its left. """
return list(self._offsets)
@property
def layers(self):
"""
A :class:`discopy.cat.Arrow` with :class:`Layer` boxes such that::
diagram == Id(diagram.dom).then(*[
Id(left) @ box @ Id(right)
for left, box, right in diagram.layers])
This is accessed using python slices::
diagram[i:j] == Diagram(
dom=diagram.layers[i].dom,
cod=diagram.layers[j - 1].cod,
boxes=diagram.boxes[i:j],
offsets=diagram.offsets[i:j],
layers=diagram.layers[i:j])
"""
return self._layers
@unbiased
def then(self, other):
if isinstance(other, Sum):
return super().then(other)
layers = self.layers.then(other.layers)
boxes = [box for _, box, _ in layers]
offsets = [len(left) for left, _, _ in layers]
return self.upgrade(
Diagram(self.dom, other.cod, boxes, offsets, layers))
[docs] def tensor(self, other=None, *rest):
"""
Returns the horizontal composition of 'self' with a diagram 'other'.
This method is called using the binary operator `@`:
>>> x, y, z, w = Ty('x'), Ty('y'), Ty('z'), Ty('w')
>>> f0, f1 = Box('f0', x, y), Box('f1', z, w)
>>> assert f0 @ f1 == f0.tensor(f1) == f0 @ Id(z) >> Id(y) @ f1
>>> (f0 @ f1).draw(
... figsize=(2, 2),
... path='docs/_static/imgs/monoidal/tensor-example.png')
.. image:: ../_static/imgs/monoidal/tensor-example.png
:align: center
Parameters
----------
other : :class:`Diagram`
Returns
-------
diagram : :class:`Diagram`
the tensor of 'self' and 'other'.
"""
if other is None:
return self
if rest:
return self.tensor(other).tensor(*rest)
if isinstance(other, Sum):
return self.sum([self]).tensor(other)
if not isinstance(other, Diagram):
raise TypeError(messages.type_err(Diagram, other))
dom, cod = self.dom @ other.dom, self.cod @ other.cod
boxes = self.boxes + other.boxes
offsets = self.offsets + [n + len(self.cod) for n in other.offsets]
layers = cat.Id(dom)
for left, box, right in self.layers:
layers = layers >> self.layer_factory(left, box, right @ other.dom)
for left, box, right in other.layers:
layers = layers >> self.layer_factory(self.cod @ left, box, right)
return self.upgrade(Diagram(dom, cod, boxes, offsets, layers=layers))
def __matmul__(self, other):
return self.tensor(other)
def __eq__(self, other):
if not isinstance(other, Diagram):
return False
return all(getattr(self, attr) == getattr(other, attr)
for attr in ['dom', 'cod', 'boxes', 'offsets'])
def __repr__(self):
if not self.boxes: # i.e. self is identity.
return repr(self.id(self.dom))
if len(self.boxes) == 1 and self.dom == self.boxes[0].dom:
return repr(self.boxes[0]) # i.e. self is a generator.
return "Diagram(dom={}, cod={}, boxes={}, offsets={})".format(
repr(self.dom), repr(self.cod),
repr(self.boxes), repr(self.offsets))
def __hash__(self):
return hash(repr(self))
def __iter__(self):
for left, box, right in self.layers:
yield self.id(left) @ box @ self.id(right)
def __str__(self):
return ' >> '.join(map(str, self.layers)) or str(self.id(self.dom))
def __getitem__(self, key):
if isinstance(key, slice):
layers = self.layers[key]
boxes_and_offsets = tuple(zip(*(
(box, len(left)) for left, box, _ in layers))) or ([], [])
inputs = (layers.dom, layers.cod) + boxes_and_offsets
return self.upgrade(Diagram(*inputs, layers=layers))
left, box, right = self.layers[key]
return self.id(left) @ box @ self.id(right)
def subs(self, *args):
return self.id(self.dom).then(*(
self.id(left) @ box.subs(*args) @ self.id(right)
for left, box, right in self.layers))
def lambdify(self, *symbols, **kwargs):
return lambda *xs: self.id(self.dom).then(*(
self.id(left) @ box.lambdify(*symbols, **kwargs)(*xs)
[docs] @ self.id(right) for left, box, right in self.layers))
@staticmethod
def swap(left, right, ar_factory=None, swap_factory=None):
"""
Returns a diagram that swaps the left with the right wires.
Parameters
----------
left : monoidal.Ty
left hand-side of the domain.
right : monoidal.Ty
right hand-side of the domain.
Returns
-------
diagram : monoidal.Diagram
with :code:`diagram.dom == left @ right`
"""
ar_factory = ar_factory or Diagram
swap_factory = swap_factory or Swap
if not left:
return ar_factory.id(right)
if len(left) == 1:
boxes = [
swap_factory(left, right[i: i + 1])
for i, _ in enumerate(right)]
offsets = range(len(right))
return ar_factory(left @ right, right @ left, boxes, offsets)
return ar_factory.id(left[:1]) @ ar_factory.swap(left[1:], right)\
>> ar_factory.swap(left[:1], right) @ ar_factory.id(left[1:])
[docs] @staticmethod
def permutation(perm, dom=None, ar_factory=None, inverse=False):
"""
Returns the diagram that encodes a permutation of wires.
.. warning::
This method used to return the inverse permutation up to and
including discopy v0.4.2.
Parameters
----------
perm : list of int
such that :code:`i` goes to :code:`perm[i]`
dom : monoidal.Ty, optional
of the same length as :code:`perm`,
default is :code:`PRO(len(perm))`.
inverse : bool
whether to return the inverse permutation.
Returns
-------
diagram : monoidal.Diagram
"""
ar_factory = ar_factory or Diagram
if set(range(len(perm))) != set(perm):
raise ValueError("Input should be a permutation of range(n).")
if dom is None:
dom = PRO(len(perm))
if not inverse:
warn_permutation.warn(
'Since discopy v0.4.3 the behaviour of '
'permutation has changed. Pass inverse=False '
'to get the default behaviour.')
perm = [perm.index(i) for i in range(len(perm))]
if len(dom) != len(perm):
raise ValueError(
"Domain and permutation should have the same length.")
diagram = ar_factory.id(dom)
for i in range(len(dom)):
j = perm.index(i)
diagram = diagram >> ar_factory.id(diagram.cod[:i])\
@ ar_factory.swap(diagram.cod[i:j], diagram.cod[j:j + 1])\
@ ar_factory.id(diagram.cod[j + 1:])
perm = perm[:i] + [i] + perm[i:j] + perm[j + 1:]
return diagram
[docs] def permute(self, *perm, inverse=False):
"""
Returns :code:`self >> self.permutation(perm, self.dom)`.
Parameters
----------
perm : list of int
such that :code:`i` goes to :code:`perm[i]`
inverse : bool
whether to return the inverse permutation.
Examples
--------
>>> x, y, z = Ty('x'), Ty('y'), Ty('z')
>>> assert Id(x @ y @ z).permute(2, 1, 0).cod == z @ y @ x
>>> assert Id(x @ y @ z).permute(2, 0).cod == z @ y @ x
"""
if min(perm) < 0 or max(perm) >= len(self.cod):
raise IndexError(f'{self} index out of bounds.')
if len(set(perm)) != len(perm):
raise ValueError('{perm} is not a permutation.')
sorted_perm = sorted(perm)
perm = [
i if i not in perm else sorted_perm[perm.index(i)]
for i in range(len(self.cod))]
return self >> self.permutation(list(perm), self.cod, inverse)
[docs] @staticmethod
def subclass(ar_factory):
""" Decorator for subclasses of Diagram. """
def upgrade(old):
ob_upgrade = type(ar_factory.id().dom).upgrade # Is this Yoneda?
dom, cod = ob_upgrade(old.dom), ob_upgrade(old.cod)
return ar_factory(dom, cod, old.boxes, old.offsets, old.layers)
ar_factory.upgrade = staticmethod(upgrade)
return ar_factory
[docs] def open_bubbles(self):
"""
Called when drawing bubbles. Replace each bubble by::
open_bubble\\
>> Id(left) @ open_bubbles(bubble.inside) @ Id(right)\\
>> close_bubble
for :code:`left = Ty(bubble.drawing_name)` and :code:`right = Ty("")`.
:meth:`Diagram.downgrade` gets called in the process.
"""
if not any(isinstance(box, Bubble) for box in self.boxes):
return self.downgrade()
class OpenBubbles(Functor):
def __call__(self, diagram):
diagram = diagram.downgrade()
if isinstance(diagram, Bubble):
obj = Ob(diagram.drawing_name)
obj.draw_as_box = True
left, right = Ty(obj), Ty("")
open_bubble = Box(
"open_bubble",
diagram.dom, left @ diagram.inside.dom @ right)
close_bubble = Box(
"_close",
left @ diagram.inside.cod @ right, diagram.cod)
open_bubble.draw_as_wires = True
close_bubble.draw_as_wires = True
# Wires can go straight only if types have the same length.
if len(diagram.dom) == len(diagram.inside.dom):
open_bubble.bubble_opening = True
if len(diagram.cod) == len(diagram.inside.cod):
close_bubble.bubble_closing = True
return open_bubble\
>> Id(left) @ self(diagram.inside) @ Id(right)\
>> close_bubble
return super().__call__(diagram)
return OpenBubbles(lambda x: x, lambda f: f)(self)
draw = drawing.draw
to_gif = drawing.to_gif
interchange = rewriting.interchange
normalize = rewriting.normalize
normal_form = rewriting.normal_form
foliate = rewriting.foliate
flatten = rewriting.flatten
foliation = rewriting.foliation
depth = rewriting.depth
width = rewriting.width
layer_factory = Layer
[docs]class Id(cat.Id, Diagram):
""" Implements the identity diagram of a given type.
>>> s, t = Ty('x', 'y'), Ty('z', 'w')
>>> f = Box('f', s, t)
>>> assert f >> Id(t) == f == Id(s) >> f
"""
def __init__(self, dom=Ty()):
cat.Id.__init__(self, dom)
Diagram.__init__(self, dom, dom, [], [], layers=cat.Id(dom))
from_tree = Diagram.from_tree
Diagram.id = Id
[docs]class Box(cat.Box, Diagram):
"""
A box is a diagram with :code:`boxes==[self]` and :code:`offsets==[0]`.
Parameters
----------
name : any
Name of the box.
dom : :class:`discopy.monoidal.Ty`
Domain of the box.
cod : :class:`discopy.monoidal.Ty`
Codomain of the box.
data : any, optional
Extra data in the box.
Other parameters
----------------
draw_as_spider : bool, optional
Whether to draw the box as a spider.
draw_as_wires : bool, optional
Whether to draw the box as wires, e.g. :class:`discopy.monoidal.Swap`.
drawing_name : str, optional
The name to use when drawing the box.
tikzstyle_name : str, optional
The name of the style when tikzing the box.
color : str, optional
The color to use when drawing the box, one of
:code:`"white", "red", "green", "blue", "yellow", "black"`.
Default is :code:`"red" if draw_as_spider else "white"`.
shape : str, optional
The shape to use when drawing a spider,
one of :code:`"circle", "rectangle"`.
Examples
--------
>>> f = Box('f', Ty('x', 'y'), Ty('z'))
>>> assert Id(Ty('x', 'y')) >> f == f == f >> Id(Ty('z'))
>>> assert Id(Ty()) @ f == f == f @ Id(Ty())
>>> assert f == f[::-1][::-1]
"""
[docs] def downgrade(self):
""" Downcasting to :class:`discopy.monoidal.Box`. """
box = Box.__new__(Box)
for attr, value in self.__dict__.items():
setattr(box, attr, value)
dom, cod = self.dom.downgrade(), self.cod.downgrade()
box._dom, box._cod, box._boxes = dom, cod, [box]
layer = Layer(box._dom[0:0], box, box._dom[0:0])
box._layers = cat.Arrow(dom, cod, [layer], _scan=False)
return box
def __init__(self, name, dom, cod, **params):
cat.Box.__init__(self, name, dom, cod, **params)
layer = self.layer_factory(dom[0:0], self, dom[0:0])
layers = cat.Arrow(dom, cod, [layer], _scan=False)
Diagram.__init__(self, dom, cod, [self], [0], layers=layers)
for attr, value in params.items():
if attr in drawing.ATTRIBUTES:
setattr(self, attr, value)
def __eq__(self, other):
if isinstance(other, Box):
return cat.Box.__eq__(self, other)
if isinstance(other, Diagram):
return len(other) == 1 and other.boxes[0] == self\
and (other.dom, other.cod) == (self.dom, self.cod)
return False
def __hash__(self):
return hash(repr(self))
class BinaryBoxConstructor:
""" Box constructor with left and right as input. """
def __init__(self, left, right):
self.left, self.right = left, right
def to_tree(self):
left, right = self.left.to_tree(), self.right.to_tree()
return dict(Box.to_tree(self), left=left, right=right)
@classmethod
def from_tree(cls, tree):
return cls(*map(from_tree, (tree['left'], tree['right'])))
[docs]class Swap(BinaryBoxConstructor, Box):
"""
Implements the symmetry of atomic types.
Parameters
----------
left : monoidal.Ty
of length 1.
right : monoidal.Ty
of length 1.
"""
def __init__(self, left, right):
if len(left) != 1 or len(right) != 1:
raise ValueError(messages.swap_vs_swaps(left, right))
name, dom, cod =\
"Swap({}, {})".format(left, right), left @ right, right @ left
BinaryBoxConstructor.__init__(self, left, right)
Box.__init__(self, name, dom, cod)
self.draw_as_wires = True
def __repr__(self):
return "Swap({}, {})".format(repr(self.left), repr(self.right))
def dagger(self):
return type(self)(self.right, self.left)
[docs]class Sum(cat.Sum, Box):
""" Sum of monoidal diagrams. """
@staticmethod
def upgrade(old):
if not isinstance(old, cat.Sum):
raise TypeError(messages.type_err(cat.Sum, old))
return Sum(old.terms, old.dom, old.cod)
def tensor(self, *others):
if len(others) != 1:
return super().tensor(*others)
other = others[0] if isinstance(others[0], Sum) else Sum(others)
unit = Sum([], self.dom @ other.dom, self.cod @ other.cod)
terms = [f.tensor(g) for f in self.terms for g in other.terms]
return self.upgrade(sum(terms, unit))
[docs] def draw(self, **params):
""" Drawing a sum as an equation with :code:`symbol='+'`. """
return drawing.equation(*self.terms, symbol='+', **params)
[docs]class Bubble(cat.Bubble, Box):
"""
Bubble in a monoidal diagram, i.e. a unary operator on homsets.
Parameters
----------
inside : discopy.monoidal.Diagram
The diagram inside the bubble.
dom : discopy.monoidal.Ty, optional
The domain of the bubble, default is :code:`inside.dom`.
cod : discopy.monoidal.Ty, optional
The codomain of the bubble, default is :code:`inside.cod`.
Examples
--------
>>> x, y = Ty('x'), Ty('y')
>>> f, g = Box('f', x, y ** 3), Box('g', y, y @ y)
>>> d = (f.bubble(dom=x @ x, cod=y) >> g).bubble()
>>> d.draw(path='docs/_static/imgs/monoidal/bubble-example.png')
.. image:: ../_static/imgs/monoidal/bubble-example.png
:align: center
"""
def __init__(self, inside, dom=None, cod=None, **params):
self.drawing_name = params.get("drawing_name", "")
cat.Bubble.__init__(self, inside, dom, cod)
Box.__init__(self, self._name, self.dom, self.cod, data=self.data)
[docs] def downgrade(self):
""" Downcasting to :class:`discopy.monoidal.Bubble`. """
result = Bubble(self.inside.downgrade(), Ty(*self.dom), Ty(*self.cod))
result.drawing_name = self.drawing_name
return result
Diagram.sum = Sum
Diagram.bubble_factory = Bubble
[docs]class Functor(cat.Functor):
"""
Implements a monoidal functor given its image on objects and arrows.
One may define monoidal functors into custom categories by overriding
the defaults ob_factory=Ty and ar_factory=Diagram.
>>> x, y, z, w = Ty('x'), Ty('y'), Ty('z'), Ty('w')
>>> f0, f1 = Box('f0', x, y, data=[0.1]), Box('f1', z, w, data=[1.1])
>>> F = Functor({x: z, y: w, z: x, w: y}, {f0: f1, f1: f0})
>>> assert F(f0) == f1 and F(f1) == f0
>>> assert F(F(f0)) == f0
>>> assert F(f0 @ f1) == f1 @ f0
>>> assert F(f0 >> f0[::-1]) == f1 >> f1[::-1]
>>> source, target = f0 >> f0[::-1], F(f0 >> f0[::-1])
>>> drawing.equation(
... source, target, symbol='$\\\\mapsto$', figsize=(4, 2),
... path='docs/_static/imgs/monoidal/functor-example.png')
.. image:: ../_static/imgs/monoidal/functor-example.png
:align: center
"""
def __init__(self, ob, ar, ob_factory=None, ar_factory=None):
if ob_factory is None:
ob_factory = Ty
if ar_factory is None:
ar_factory = Diagram
super().__init__(ob, ar, ob_factory=ob_factory, ar_factory=ar_factory)
def __call__(self, diagram):
if isinstance(diagram, (Sum, Bubble)):
super().__call__(diagram)
if isinstance(diagram, Ty):
return self.ob_factory().tensor(*[
self.ob[type(diagram)(x)] for x in diagram])
if isinstance(diagram, Swap):
return self.ar_factory.swap(
self(diagram.left), self(diagram.right))
if isinstance(diagram, Box):
return super().__call__(diagram)
if isinstance(diagram, Diagram):
scan, result = diagram.dom, self.ar_factory.id(self(diagram.dom))
for box, off in zip(diagram.boxes, diagram.offsets):
id_l = self.ar_factory.id(self(scan[:off]))
id_r = self.ar_factory.id(self(scan[off + len(box.dom):]))
result = result >> id_l @ self(box) @ id_r
scan = scan[:off] @ box.cod @ scan[off + len(box.dom):]
return result
raise TypeError(messages.type_err(Diagram, diagram))