# Monoidal categories in DisCoPy

In order to use the advanced features of `lambeq`

and extend it, an understanding of DisCoPy is required. DisCoPy is the tool of choice for dealing with monoidal categories in Python, and it is widely used for DisCoCat and categorical quantum mechanics (CQM) research.

## Categories

A *category* consists of a collection of *objects* \(A, B, C, \ldots\) and a collection of *morphisms* between objects of the form \(f: A \to B, g: B \to C, h: C \to D, \ldots\), such that:

Morphisms with matching types compose. For example, \(f: A \to B\) and \(g: B \to C\) can compose to make \(g \circ f: A \to C\), but not \(f \circ g\).

Morphisms compose in an associative way: \((h \circ g) \circ f = h \circ (g \circ f)\)

Each object has an identity arrow: \(1_B \circ f = f = f \circ 1_A\)

These definitions are implicitly encoded in this *commutative diagram*: any directed path between two specific objects represents equal morphisms.

The `discopy.cat`

module is a framework for *free* categories: we first define generating objects with the `Ob`

class and generating morphisms with the `Box`

class, then build composite morphisms by freely combining the generating morphisms using forward composition `<<`

(equivalent to \(\circ\)) and backward composition `>>`

.

```
[1]:
```

```
from discopy.cat import Box, Ob, Id
A, B, C, D = Ob('A'), Ob('B'), Ob('C'), Ob('D')
f = Box('f', A, B)
g = Box('g', B, C)
h = Box('h', C, D)
# the codomain of f and domain of g match, so f and g compose
g << f
assert f.cod == g.dom == B
assert g << f == f >> g
# associativity
assert h << (g << f) == h << g << f == (h << g) << f
# identity
assert f << Id(A) == f == Id(B) << f
```

As mentioned above, in DisCoPy the generating morphisms are defined using the `Box`

class. When morphisms are composed, they combine to become an `Arrow`

. Compared to traditional category theory notation, DisCoPy prefers to use backwards composition `>>`

, where `f >> g`

should be read as “`f`

followed by `g`

”.

```
[2]:
```

```
# only arrows that 'type-check' can be composed
arrow = h << g << f
assert arrow == f >> g >> h
print(repr(arrow))
```

```
Arrow(dom=Ob('A'), cod=Ob('D'), boxes=[Box('f', Ob('A'), Ob('B')), Box('g', Ob('B'), Ob('C')), Box('h', Ob('C'), Ob('D'))])
```

An `Arrow`

behaves like a `List[Box]`

: it can be indexed, sliced, or even reversed. Reversing a morphism actually performs the *dagger* operation, which is the abstract notion of a dagger in quantum mechanics and linear algebra.

```
[3]:
```

```
print(arrow)
print(f'Indexing:', arrow[0])
print(f'Slicing:', arrow[1:])
print(f'Reversing (dagger):', arrow[::-1])
```

```
f >> g >> h
Indexing: f
Slicing: g >> h
Reversing (dagger): h[::-1] >> g[::-1] >> f[::-1]
```

## Monoidal categories

A *monoidal category* is a category equipped with the *monoidal product* \(\otimes\) and *monoidal unit* \(I\) and has the following properties:

objects can be combined to return another object (e.g \(A \otimes B\))

morphisms can be combined to return another morphism (\((f: A \to B) \otimes (g: C \to D) = f \otimes g: A \otimes C \to B \otimes D\)).

\(\otimes\) is associative on objects: \((A \otimes B) \otimes C = A \otimes (B \otimes C)\)

\(\otimes\) is associative on morphisms: \((f \otimes g) \otimes h = f \otimes (g \otimes h)\)

\(I\) is the identity on objects for \(\otimes\): \(A \otimes I= A = I \otimes A\)

\(1_I\) is the identity on arrows for \(\otimes\): \(f \otimes 1_I = f = 1_I \otimes f\)

Built upon `discopy.cat`

, the `discopy.monoidal`

module can be used to define and manipulate free monoidal categories: the generating objects are defined with the `Ty`

class, and the generating morphisms with the `Box`

class; the composite objects are built using `@`

and the composite morphisms using `>>`

and `<<`

.

```
[4]:
```

```
from discopy.monoidal import Box, Id, Ty
A, B, C = Ty('A'), Ty('B'), Ty('C')
f = Box('f', A, B)
g = Box('g', B, C)
h = Box('h', B, A)
# combining objects
A @ B
# combining arrows
f @ g
# associativity
assert (A @ B) @ C == A @ B @ C == A @ (B @ C)
assert (f @ g) @ h == f @ g @ h == f @ (g @ h)
# monoidal unit
assert A @ Ty() == A == Ty() @ A
assert f @ Id(Ty()) == f == Id(Ty()) @ f
```

Monoidal categories have an elegant graphical calculus, which allow them to be drawn and manipulated graphically.

```
[5]:
```

```
x = Box('x', A, A)
y = Box('y', A @ A, B)
diagram = x @ Id(A) >> y
print(repr(diagram))
diagram.draw(figsize=(5, 3))
```

```
Diagram(dom=Ty('A', 'A'), cod=Ty('B'), boxes=[Box('x', Ty('A'), Ty('A')), Box('y', Ty('A', 'A'), Ty('B'))], offsets=[0, 0])
```

A `Ty`

can be indexed, sliced, or even reversed, just like a `List[Ob]`

.

```
[6]:
```

```
t = A @ B @ C
print(t)
print(repr(t))
print('Indexing:', t[0])
print(f'Slicing:', t[1:])
print(f'Reversing:', t[::-1])
```

```
A @ B @ C
Ty('A', 'B', 'C')
Indexing: A
Slicing: B @ C
Reversing: C @ B @ A
```

It is often desirable to select a single object from a compound `Ty`

and use it as a type, since a `monoidal.Box`

expects its domain and codomain to be types (`Ty`

) rather than Objects (`Ob`

). This can be achieved using a 1-element slice, as in the following code:

```
[7]:
```

```
# indexing a Ty gives an Ob back
print('Indexing (Ob):', repr(t[0]))
# trick to get a Ty with a single object
print('Indexing (Ty):', repr(t[0:1]))
```

```
Indexing (Ob): Ob('A')
Indexing (Ty): Ty('A')
```

Similarly, a `monoidal.Diagram`

behaves like a `List[Box]`

, so it can be indexed, sliced, and reversed. Reversing a diagram performs the dagger operation.

```
[8]:
```

```
print(diagram)
print(f'Indexing:', diagram[0])
print(f'Slicing:', diagram[1:])
print(f'Reversing (dagger):', diagram[::-1])
from discopy import drawing
print('\nDagger operation:')
# boxes are drawn as trapeziums to demonstrate the reflection along the horizontal axis
drawing.equation(diagram, diagram[::-1], symbol='->', asymmetry=0.2, figsize=(8, 3))
```

```
x @ Id(A) >> y
Indexing: x @ Id(A)
Slicing: y
Reversing (dagger): y[::-1] >> x[::-1] @ Id(A)
Dagger operation:
```

The internal representation of a diagram contains the domain (input type), codomain (output type), the list of boxes that make up the diagram, and a list of offsets for the boxes. For example, by adjusting the offset of the first box to 1, the box is then connected to the second wire rather than the first.

```
[9]:
```

```
from discopy.monoidal import Diagram
offset_diagram = Diagram(
dom=Ty('A', 'A'), cod=Ty('B'),
boxes=[Box('x', Ty('A'), Ty('A')), Box('y', Ty('A', 'A'), Ty('B'))],
offsets=[1, 0])
offset_diagram.draw(figsize=(5, 3))
```

A monoidal category equipped with a `Swap`

is known as a symmetric monoidal category. Nested swaps can be defined using the `swap()`

method.

```
[10]:
```

```
from discopy.monoidal import Swap
Swap(A, B).draw(figsize=(1, 1))
Diagram.swap(A @ B, C).draw(figsize=(2, 2))
```

Note

In a strict mathematical sense, the associativity and unit rules of \(\otimes\) in a monoidal category only hold up to *isomorphism*. As a consequence, this definition requires extra morphisms such as unitors and associators, as well as complicated coherence conditions. Instead, DisCoPy strictly enforces the rules to hold up to equality, so such coherence conditions are unnecessary. This greatly simplifies its practical use.

## Rigid monoidal categories

A *rigid category* is a monoidal category where every object \(A\) has a *left adjoint* \(A^l\) and *right adjoint* \(A^r\). The left adjoint of the right adjoint of a type is equal to the type itself, and vice versa: \((A^r)^l = A = (A^l)^r\)

In the `discopy.rigid`

module, the adjoint of a rigid type `Ty`

is obtained using the `.l`

and `.r`

properties:

```
[11]:
```

```
from discopy.rigid import Box, Id, Ty
A = Ty('A')
print(A.l, 'is represented as', repr(A.l))
print(A.r, 'is represented as', repr(A.r))
assert A.r.l == A == A.l.r
```

```
A.l is represented as Ty(Ob('A', z=-1))
A.r is represented as Ty(Ob('A', z=1))
```

The key property of a rigid category is the existence of cups and caps between an object and its adjoint: these are special morphisms that are drawn as bent wires in diagrammatic notation.

```
[12]:
```

```
from discopy.rigid import Cap, Cup
drawing.equation(Cup(A.r, A.r.r), Cup(A, A.r), Cup(A.l, A), figsize=(8, 1), symbol='...')
drawing.equation(Cap(A.l, A.l.l), Cap(A, A.l), Cap(A.r, A), figsize=(8, 1), symbol='...')
```

Cups and caps satisfy the so-called snake equations:

```
[13]:
```

```
snake1 = Id(A) @ Cap(A.r, A) >> Cup(A, A.r) @ Id(A)
snake2 = Cap(A, A.l) @ Id(A) >> Id(A) @ Cup(A.l, A)
assert snake1.normal_form() == Id(A) == snake2.normal_form()
print('Snake Equations - For any object', A, ':')
drawing.equation(snake1, Id(A), snake2, figsize=(8, 2))
```

```
Snake Equations - For any object A :
```

Note

The `normal_form()`

method used above also applies on standard monoidal diagrams.

Nested cups and caps can be created using the `cups()`

, `caps()`

methods.

```
[14]:
```

```
from discopy.rigid import Diagram
A, B = Ty('A'), Ty('B')
nested_cup = Diagram.cups(A @ B, (A @ B).r)
nested_cap = Diagram.caps((A @ B).r, A @ B)
nested_snake = Id(A @ B) @ nested_cap >> nested_cup @ Id(A @ B)
assert nested_snake.normal_form() == Id(A @ B)
drawing.equation(nested_snake, nested_snake.normal_form())
```