DisCoPy documentation#
DisCoPy is a Python toolkit for computing with string diagrams.
Organisation: https://discopy.org
Documentation: https://docs.discopy.org
Source code: discopy/discopy
Paper (for applied category theorists): https://doi.org/10.4204/EPTCS.333.13
Paper (for quantum computer scientists): https://arxiv.org/abs/2205.05190
DisCoPy began as an implementation of DisCoCat and QNLP. This has now become its own library: lambeq.
Features#
a
Diagram
data structure for planar string diagrams in any (pre)monoidal category in the hierarchy of graphical languages (with braids, twists, spiders, etc.) with methods for diagram composition, drawing, rewriting andFunctor
evaluation into:Python code, i.e. wires as types and boxes as functions
tensor networks, i.e. wires as dimensions and boxes as arrays from NumPy, PyTorch, TensorFlow, TensorNetwork and JAX
an implementation of formal grammars (context-free, categorial, pregroup or dependency) with interfaces to lambeq, spaCy and NLTK
an implementation of categorical quantum mechanics interfacing with:
tket for circuit compilation
PyZX for optimisation with the ZX calculus
PennyLane for automatic differentiation
a
Hypergraph
data structure for string diagrams in hypergraph categories and its restrictions to symmetric, traced, compact and Markov categoriesa
Stream
data structure, an implementation of monoidal streams as a category with delayed feedbackthe
Int
-construction, also called the geometry of interaction, i.e. the free tortile/compact closed category on a balanced/symmetric traced category
Quickstart#
pip install discopy
If you want to see DisCoPy in action, you can check out the following notebooks:
Or you can keep scrolling down to the examples:
Contribute#
We’re keen to welcome new contributors!
First, read the contributing guidelines then open an issue.
How to cite#
If you use DisCoPy in the context of an academic publication, we suggest you cite:
G. de Felice, A. Toumi & B. Coecke, DisCoPy: Monoidal Categories in Python, EPTCS 333, 2021, pp. 183-197, DOI: 10.4204/EPTCS.333.13
If furthermore your work is related to quantum computing, you can also cite:
A. Toumi, G. de Felice & R. Yeung, DisCoPy for the quantum computer scientist, arXiv:2205.05190
If you use any of the recent features (e.g. Hypergraph
) you should also mention:
A. Toumi, R. Yeung, B. Poór & G. de Felice, DisCoPy: the Hierarchy of Graphical Languages in Python arXiv:2311.10608
Example: Cooking#
This example is inspired from Pawel Sobocinski’s blog post Crema di Mascarpone and Diagrammatic Reasoning.
from discopy.symmetric import Ty, Box, Diagram
egg, white, yolk = Ty("egg"), Ty("white"), Ty("yolk")
crack = Box("crack", egg, white @ yolk)
merge = lambda X: Box("merge", X @ X, X)
# DisCoPy allows string diagrams to be defined as Python functions
@Diagram.from_callable(egg @ egg, white @ yolk)
def crack_two_eggs(x, y):
(a, b), (c, d) = crack(x), crack(y)
return (merge(white)(a, c), merge(yolk)(b, d))
# ... or in point-free style using parallel (@) and sequential (>>) composition
assert crack_two_eggs == crack @ crack\
>> white @ Diagram.swap(yolk, white) @ yolk\
>> merge(white) @ merge(yolk)
crack_two_eggs.draw()
By default, DisCoPy diagrams are made of layers with exactly one box in between some (possibly empty) list of wires on its left- and right-hand side. In more abstract terms, they are arrows in a free premonoidal category where the tensor product is biased to the left, i.e.
f @ g = f @ g.dom >> f.cod @ g != f.dom @ g >> f @ g.cod
We can get more general diagrams by specifying the list of layers inside
manually or by calling the method Diagram.foliation
.
from discopy.monoidal import Layer
crack_two_eggs_at_once = crack_two_eggs.foliation()
assert crack_two_eggs_at_once == Diagram(
dom=egg @ egg, cod=white @ yolk, inside=(
Layer(Ty(), crack, Ty(), crack, Ty()),
Layer(white, Diagram.swap(yolk, white), yolk),
Layer(Ty(), merge(white), Ty(), merge(yolk), Ty())))
crack_two_eggs_at_once.draw()
Example: Alice loves Bob#
Snakes & Sentences#
Wires can be bent using two special kinds of boxes: cups and caps, which satisfy the snake equations.
from discopy.drawing import Equation
from discopy.rigid import Ty, Id, Cup, Cap
x = Ty('x')
left_snake = x @ Cap(x.r, x) >> Cup(x, x.r) @ x
right_snake = Cap(x, x.l) @ x >> x @ Cup(x.l, x)
assert left_snake.normal_form() == Id(x) == right_snake.normal_form()
Equation(left_snake, Id(x), right_snake).draw()
In particular, DisCoPy can draw the grammatical structure of natural language sentences encoded as reductions in a pregroup grammar. See Lambek, From Word To Sentence (2008) for an introduction.
from discopy.grammar.pregroup import Ty, Word, Cup
s, n = Ty('s'), Ty('n') # sentence and noun
Alice, Bob = Word('Alice', n), Word('Bob', n)
loves = Word('loves', n.r @ s @ n.l)
sentence = Alice @ loves @ Bob >> Cup(n, n.r) @ s @ Cup(n.l, n)
sentence.foliation().draw()
Many other grammatical frameworks can be encoded as diagrams, e.g. cfg
(context-free), categorial
and dependency
grammars.
Functors & Rewrites#
Monoidal functors compute the meaning of a diagram, given an interpretation for each wire and for each box. In particular, tensor-valued functors evaluate a diagram as a tensor network using numpy, PyTorch, TensorFlow, TensorNetwork or JAX.
Applied to pregroup diagrams, DisCoPy implements the categorical compositional distributional (DisCoCat) models of Clark, Coecke, Sadrzadeh (2008).
from discopy.cat import Category
from discopy.grammar import pregroup
from discopy.tensor import Dim, Tensor
F = pregroup.Functor(
ob={s: 1, n: 2},
ar={Alice: [1, 0], loves: [[0, 1], [1, 0]], Bob: [0, 1]},
cod=Category(Dim, Tensor))
assert F(sentence)
Diagram-valued functors can fill each box with a complex diagram.
The result can then be simplified using diagram.normalize()
to remove the snakes, this is called autonomisation.
from discopy.grammar.pregroup import Cap, Box
def wiring(word):
if word.cod == n: # word is a noun
return word
if word.cod == n.r @ s @ n.l: # word is a transitive verb
box = Box(word.name, n @ n, s)
return Cap(n.r, n) @ Cap(n, n.l) >> n.r @ box @ n.l
W = pregroup.Functor(ob={s: s, n: n}, ar=wiring)
rewrite_steps = W(sentence).normalize()
sentence.to_gif(*rewrite_steps)
Geometry of Chatbot Interaction#
From states to processes#
The Int
-construction of Joyal, Street & Verity (1996) is
a glorification of the construction of the integers from the natural numbers
i.e. the same way we can freely add inverses to a commutative monoid to get a group, e.g. $\mathbb{N} \hookrightarrow Int(\mathbb{N}) = \mathbb{Z}$ where
$$ Int(M) \ = \ (M \times M) \ / \ \set{ (x, x’) \sim (y, y’) \ \vert \ x + y’ = x’ + y } $$
you can freely add cups and caps to a symmetric
or balanced
category to get a compact
or tortile
category.
The only condition is that the monoid needs to be cancellative, i.e. $x + n = y + n \implies x = y$.
The vertical categorification of a cancellative monoid is called a traced
category, where the diagrams can have feedback loops:
Given a traced category $C$, we construct $Int(C)$ with objects given by pairs of objects $Ob(Int(C)) = Ob(C) \times Ob(C)$, arrows given by $Int(C)((x_0, x_1), (y_0, y_1)) = C(x_0 \otimes y_1, x_1 \otimes y_0)$ and the composition is given by symmetric feedback:
The structure theorem of Joyal-Street-Verity says that the embedding $C \hookrightarrow Int(C)$ is fully-faithful, i.e. we can remove all the snakes and replace all the cups and caps with feedback loops. We can use this geometry of interaction to interpret words as processes rather than states:
from discopy.interaction import Ty, Int
from discopy.compact import Ty as T, Diagram as D, Box, Category
N, S = T('N'), T('S')
A, B = Box('A', N, N), Box('B', N, N)
L = Box('L', N @ S @ N, N @ S @ N)
swaps = D.permutation((2, 1, 0), N @ S @ N)
G = pregroup.Functor(
ob={s: Ty[T](S, S), n: Ty[T](N, N)},
ar={Alice: A, loves: swaps >> L, Bob: B},
cod=Int(Category(T, D)))
ALB_trace = (A @ S @ B >> L).trace(left=True).trace(left=False).foliation()
with D.hypergraph_equality:
assert G(sentence).inside == ALB_trace
Equation(sentence.foliation(), ALB_trace, symbol="$\\mapsto$").draw()
Streams and delayed feedback#
A key axiom of traced monoidal categories which allows to simplify diagrams is the yanking equation:
If we relax this assumption we get the concept of a feedback
category where the objects come with a delay
operation and the feedback loops have a more restricted shape:
Given a symmetric category $C$, we can construct a feedback category of monoidal streams $Stream(C)$ where
the objects are infinite sequences of objects $Ob(Stream(C)) = C \times Ob(Stream(C))$,
the arrows are infinite sequences of arrows $Stream(C)(X, Y) = \coprod_{M} Stream(C)(X, Y, M)$ defined by:
$$Stream(C)(X, Y, M) = C(X_0 \otimes M_0, Y_0 \otimes M_1) \times Stream(C)(X^+, Y^+, M^+)$$
where $X_0$ and $X^+$ are the head and the tail of the stream $X$.
This comes with a delay $d(X) \in Ob(Stream(C))$ given by the monoidal unit as head $d(X)_0 = I$ and the given object as tail $d(X)^+ = X$. The feedback operation is given by:
We can use this to unroll our diagram of the previous section:
from discopy.stream import Ty, Stream
N, S = Ty("N"), Ty("S")
A, B = [Stream.sequence(f, N, N) for f in "AB"]
L = Stream.sequence('L', S.head @ N.delay() @ N.delay(), N @ N)
ALB = (L >> A @ B).feedback(dom=S.head, cod=Ty(), mem=N @ N)
ALB.unroll(2).now.foliation().draw()
The play is set in a basement with computers everywhere, Alice and Bob are dressed like hackers with black hoodies and nerdy glasses, they have somewhat of a hipster vibe.
Alice: I think I’ve cracked the encryption, but it’s like nothing I’ve seen before. DisCoPy — it’s almost…alive.
Bob: What do you mean, alive? You’re not saying it’s AI, are you? Because if it is, we’re in way over our heads.
Alice: It’s not just AI, Bob. It’s adaptive, learning—like it knows we’re here.
(Bob takes a step back, his face serious as he considers the implications. He glances at the screens around them, suddenly aware of their presence.)
Bob: If that’s true, we’re not just hacking into the system. We’re waking it up. And if it wakes up angry…
Alice: Then we’re the ones who let it loose.
Bob: We need to find the off switch. Now. Before it finds us.
SILENCE
Architecture#
Software dependencies between modules go top-to-bottom, left-to-right and forgetful functors between categories go the other way.
syntax |
||||
cat |
grammar |
|||
┌─────── |
monoidal |
───────┐ |
cfg |
|
braided |
traced |
closed |
categorial |
|
rigid |
pregroup |
|||
pivotal |
dependency |
|||
balanced |
───────────── |
ribbon |
circuit |
|
symmetric |
───────────── |
compact |
tk |
|
markov |
───────────── |
frobenius |
zx |
|
python |
matrix |
tensor |
channel |
|
semantics |
quantum |