DisCoPy is a Python toolkit for computing with string diagrams.
- Organisation: https://discopy.org
- Documentation: https://docs.discopy.org
- Source code: https://github.com/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.
- 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, JAX and Quimb
- 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 categories - a
Stream
data structure, an implementation of monoidal streams as a category with delayed feedback - the
Int
-construction, also called the geometry of interaction, i.e. the free tortile/compact closed category on a balanced/symmetric traced category
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:
We're keen to welcome new contributors!
First, read the contributing guidelines then open an issue.
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
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()
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.
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)
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.
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.
The vertical categorification of a cancellative monoid is called a traced
category, where the diagrams can have feedback loops:
Given a traced category
The structure theorem of Joyal-Street-Verity says that the embedding
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()
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
- 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:
where
This comes with a delay
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()
Now if we use the python
module to interpret each box as a call to a chatbot with the prompt as input, we can get an output along the following lines:
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