Code Monkey home page Code Monkey logo

discopy's Introduction

Snake equation

DisCoPy

build readthedocs PyPI version DOI: 10.4204/EPTCS.333.13

DisCoPy is a Python toolkit for computing with string diagrams.

DisCoPy began as an implementation of DisCoCat and QNLP. This has now become its own library: lambeq.

Features

Example: Cooking

This example is inspired from Pawel Sobocinski's blog post Crema di Mascarpone and Diagrammatic Reasoning.

from discopy.symmetric import Ty as Ingredient, Box as Step, Diagram as Recipe

egg, white, yolk = Ingredient("egg"), Ingredient("white"), Ingredient("yolk")
crack = Step("crack", egg, white @ yolk)
merge = lambda x: Step("merge", x @ x, x)

# DisCoPy allows string diagrams to be defined as Python functions

@Recipe.from_callable(egg @ egg, white @ yolk)
def crack_two_eggs(left_egg, right_egg):
    left_white, left_yolk = crack(left_egg)
    right_white, right_yolk = crack(right_egg)
    return (merge(white)(left_white, right_white),
            merge(yolk)(left_yolk, right_yolk))

# ... or in point-free style using parallel (@) and sequential (>>) composition

assert crack_two_eggs == crack @ crack\
  >> white @ Recipe.swap(yolk, white) @ yolk\
  >> merge(white) @ merge(yolk)

crack_two_eggs.draw()

crack_two_eggs.draw()

Quickstart

pip install discopy

If you want to see DisCoPy in action, check out the QNLP tutorial!

Contribute

We're keen to welcome new contributors!

First, read the contributing guidelines. Then get in touch on Discord or open an issue.

How to cite

If you used 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

discopy's People

Contributors

alexkoziell avatar aljabr0 avatar annanpearson avatar boldar99 avatar colltoaction avatar dapingq avatar dependabot[bot] avatar discopy avatar ginamuuss avatar giodefelice avatar ianyfan avatar irene-r avatar kinianlo avatar konstantinosmei avatar le-big-mac avatar majidaldo avatar nikhilkhatri avatar oxford-quantum-group avatar thommy257 avatar toumix avatar y-richie-y avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

discopy's Issues

Bug in `Diagram.from_pyzx`

from discopy.quantum.zx import circuit2zx, Diagram, Ket

orig_zx = circuit2zx(Ket(0, 0).H(1).CX(0, 1))
new_zx = Diagram.from_pyzx(orig_zx.to_pyzx())

orig_zx.draw()
new_zx.draw()

Besides the missing scalars, the Hadamard is missing in the new diagram.

image

image

Suggested fix:
Add node = scan[source] to the function move, where is seems to be undefined.
https://github.com/oxford-quantum-group/discopy/blob/30eabdbcf1a911c79e89c73f206a08401cf0c1a6/discopy/quantum/zx.py#L158

We should add more tests to make sure the back and forth conversion yields a semantically equivalent zx diagram. This involves extending zx diagrams to have an eval function.

Bra and Ket should only accept integers

assert Ket('0') == Ket(0)
assert (Ket('0').array != Ket(0).array).any()

>>> Ket('1').array
array([0., 1.])
>>> Ket('0').array
array([0., 1.])

This is bad behaviour. The obvious thing to do is to make sure only integers can be passed to Bra and Ket.

Symmetric Monoidal Categories

For now, the Diagram class is based on lists of boxes and offsets, it allows only to encode planar diagrams.

In order to encode diagrams in symmetric monoidal categories, we would need a graph-based data structure instead, i.e. we want a class with the following interface:

SDiagram(dom, cod, boxes, graph)

The vertices of the graph are given by inputs + codomains + domains + outputs.
Note that scalar boxes do not appear anywhere in the graph: they are not connected to anything.
The edges of the graph should satisfy some condition depending on the kind of structure we want:

  • For hypergraph categories, there are no restrictions: everything can connect to arbitrarily many things using spiders.
  • For compact-closed categories, the graph should be monogamous: everything is connected to exactly one thing.
  • For traced categories, the graph should be monogamous plus it only connects inputs to outputs.
  • For symmetric monoidal categories proper, the graph should be a monogamous DAG: boxes should be given in a topological ordering, and edges must be monotone.

There should be a forgetful functor from these SDiagram to the usual planar Diagram, so that we can actually draw them. The functor sends a graph-based diagram to a list-based one, adding the necessary swaps, cups, caps and spiders. The adjoint takes a list-based diagram and computes it graph-based form.

This graph-based format would make the encoding of some diagrams smaller: swaps, cups, caps and spiders need not be encoded as boxes anymore. It would also make more equalities hold on the nose: e.g. the Yang-Baxter equation, snake equation and spider fusion. The interchanger reorders the vertices in the graph, it would now encode the naturality equation for symmetry, i.e. we can slide a box through a swap. Another killer app that this format allows is pattern-matching and double-pushout rewriting, see e.g. arXiv:1602.06771 and cartographer.id.

Type hints

DisCoPy would benefit from adding type hints.
The main challenges:

  • the fact that arrows have lists of boxes inside, which are subclasses of arrows
  • the inheritance of type hints along the hierarchy cat, monoidal, rigid, etc.
  • is it even possible to check DisCoPy statically with mypy?

We may use the Self type of PEP 673.

The documentation also needs to be updated to use type hints to generate API automatically.

See the DisCoPy version of my thesis where the type hints are included (not checked with mypy though).

AxiomError while trying to adapt a tutorial

Hey everyone, I'm new to QNLP. I recently discovered this tutorial on the DisCoPy docs website. I want to adapt this to my own blog tutorial.

To add a little variation, I'm following an eggless recipe of crema di mascarpone, replacing the yolk with yoghurt and the whites with a little applesauce.
Warning: I haven't tested this recipe.
With the magic of quantum computers, I have the possibility of executing this experiment virtually.

The process followed would be a little different as there would be no process to crack an egg. Instead, the directions would be as follows:

  1. Beat sugar and yoghurt together to make a yoghurt mix.
  2. Stir a little applesauce with mascarpone cheese.
  3. Fold the mixture of yoghurt mix and stirred mascarpone cheese to get crema di mascarpone.

Here's the diagrammatic representation of it
image

Here's the code I have tried to implement:

Method 1

from discopy import Ob, Ty, Id, Box

yoghurt, milk, = Ty('yoghurt'), Ty('milk')

assert yoghurt.objects == [Ob('yoghurt')]
assert yoghurt @ Ty() == yoghurt == Ty() @ yoghurt
sugar = Ty('sugar')
assert yoghurt @ sugar == Ty(Ob('yoghurt'), Ob('sugar'))
yoghurty_paste = Ty('yoghurty_paste')
beat = Box('beat', yoghurt @ sugar, yoghurty_paste)
beat.draw(aspect='auto')

Method 2

from discopy import Ty, Box, Id

mixture, yoghurt, applesauce = Ty('mixture'), Ty('yoghurt'), Ty('applesauce')
sugar, mascarpone = Ty('sugar'), Ty('mascarpone')
yoghurty_paste, thick_paste = Ty('yoghurty_paste'), Ty('thick_paste')
crema_di_mascarpone = Ty('crema_di_mascarpone')

sep2 = Box('separate', mixture, yoghurt @ applesauce)
beat2 = Box('beat', yoghurt @ sugar, yoghurty_paste)
whisk2 = Box('whisk', applesauce, applesauce)
stir2 = Box('stir', yoghurty_paste @ mascarpone, thick_paste)
fold2 = Box('fold', applesauce @ thick_paste, crema_di_mascarpone)


recipe2 = Id(sugar) @ Id(yoghurt) @ Id(applesauce) @ Id(mascarpone)\
        >> beat2 @ Id(applesauce) @ Id(mascarpone)

recipe2.draw(aspect='auto')

AxiomError: Id(sugar @ yoghurt @ applesauce @ mascarpone) does not compose with beat @ Id(applesauce @ mascarpone).

I'd appreciate any help/ direction to resources that can make this clearer.

Thank you so much!

Drawing doesn't work for complicated pregroup diagrams

Take this diagram:

diagram = Diagram(dom=Ty(), cod=Ty('s'), boxes=[Word('Bruce', Ty('n')), Word('is', Ty(Ob('n', z=1), 's', Ob('n', z=-1))), Word('a', Ty('n', Ob('n', z=-1))), Word('beagle', Ty('n')), Cup(Ty(Ob('n', z=-1)), Ty('n')), Word('from', Ty(Ob('n', z=1), 'n', Ob('n', z=-1))), Word('Brazil', Ty('n')), Cup(Ty(Ob('n', z=-1)), Ty('n')), Cup(Ty('n'), Ty(Ob('n', z=1))), Cup(Ty(Ob('n', z=-1)), Ty('n')), Cup(Ty('n'), Ty(Ob('n', z=1)))], offsets=[0, 1, 4, 6, 5, 5, 8, 7, 4, 3, 0])
grammar.draw(diagram)

This cannot currently be drawn using diagram.draw(), which shows the diagram flattened with triangle states.
This is due to the way we currently do pregroup detection, which uses diagram.foliation() to check whether the first slice of the diagram contain only words as boxes and the rest of the slices contain only cups as boxes respectively.

Dynamically compose a sequence

Hi, is there a function that appends >> in a dynamic way ?

For example, I have a list of grammars / diagrams:
glist = [g1, g2, g3, g4]

I want the result = g1 >> g2 >> g3 >> g4

It's similar to a for loop:

for g in glist:
result + >> g

Thanks.

Pattern Matching and Rewriting

Pattern matching and rewriting methods would be needed. That is, given a diagram and a pair of parallel diagrams left and right, we want:

  1. A method diagram.match(left) which generates pairs of ints (i, j) such that left occurs in diagram at depth i and offset j.
  2. A method diagram.rewrite(left, right, i, j) which returns diagram with the subdiagram left at position (i, j) has been substituted by right.

Then, if we want all the possible rewrites for a diagram, we can write:

diagram.rewrite(left, right, i, j) for i, j in diagram.match(left)

Note that at first, we only want to find left as an exact subdiagram, i.e. without any interchangers to apply. Ideally, we would also want another method imatch which tries to apply interchangers until left becomes an exact subdiagram. This would be a great generalisation of the snake removal algorithm.

Drawing quantum circuits.

Quantum circuits should be drawn as closely as possible to the standard notation for controlled gates, bra and kets, measurements etc.

example

Ideally, we want identity wires for bits and qubits to be drawn differently, e.g. as single and double lines respectively. Note that this will also allow to draw bastard spiders in ZX diagrams.

New Feature: Normal form for symmetric monoidal categories

Currently diagram.normal_form() only applies normal form to monoidal categories.
As a first step towards a symmetric monoidal normal form, we should just remove all double swaps and apply the Yang-Baxter equation from left to right.

Refactor drawing

The drawing module suffers from some technical debt.
It needs to be refactored so that:

  • it has a PlaneGraph data structure for storing graphs with positions and labels
  • drawing takes a Diagram, constructs the PlaneGraph and passes it to the backend
  • the backend constructs outputs either TikZ or matplotlib
  • boxes may override the default drawing in a modular way

See Section 1.3 of my thesis and a draft implementation here.

Diagramize: function-like notation for diagrams

For now, DisCoPy has two ways to define diagrams: either directly as a list of boxes and offsets, or as an expression involving composition, tensor and identities. When drawing diagrams that represent a function, these can be a lot less intuitive than Python's syntax for function.

We need a decorator @diagramize(dom, cod, boxes) that can translate function syntax into a diagram. Each box becomes a function which takes input wires and returns output wires. In the case of states, i.e. boxes with no inputs, we still need to specify the offset of the box. For example:

from discopy import Ty, Cup, Cap
x = Ty('x')
cup, cap = Cup(x, x.r), Cap(x.r, x)
@diagramize(dom=x, cod=x, boxes=[cup, cap])
def snake(left):
    middle, right = cap(offset=1)
    cup(left, middle)
    return right
snake.draw()

image

The function syntax allows one to define morphisms where wires can swap, split or terminate: Python variables can be used in arbitrary order, they can be copied or discarded. If we really to build a planar diagram, we may either throw an AxiomError, or introduce the necessary swap, copy or discard boxes. This problem doesn't arise if we have a graph-like data structure for symmetric monoidal diagrams, see issue https://github.com/oxford-quantum-group/discopy/issues/25.

Memory overflow when using backend=AerBackend()

Hey,
I came across a weird memory leak when using the AerBackend. Here's a minimal example to reproduce:

import os, psutil
from random import random
from discopy.quantum import Ket, Rz, Rx, Circuit
from pytket.extensions.qiskit import AerBackend

process = psutil.Process(os.getpid())


def verb_ansatz(p):
    return Ket(0) >> Rx(p)

def noun_ansatz(p):
    return Ket(0) >> Rz(p)

def eval(backend = AerBackend()):

    circuits = [verb_ansatz(random()) >> noun_ansatz(random()).dagger() for i in range(10)]
    results = [
        Circuit.eval(
            circuit,
            backend=backend,
            n_shots=2**10,
            seed=0,
            compilation=backend.default_compilation_pass(2))
             for circuit in circuits
        ]

    return results

if __name__=="__main__":
    i = 0
    while True:
        f = eval()
        i+=1
        print(i,psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2)

It appears that the backend variable accumulates memory over time, eventually leading to an overflow. A solution that worked for me was to initialize the backend every time in the eval() function:

def eval():

    backend = AerBackend()
    circuits = [verb_ansatz(random()) >> noun_ansatz(random()).dagger() for i in range(10)]
    results = [
        Circuit.eval(
            circuit,
            backend=backend,
            n_shots=2**10,
            seed=0,
            compilation=backend.default_compilation_pass(2))
             for circuit in circuits
        ]

    return results

I'm not sure if this is optimal but it seems to fix the memory leak.

I'm sending you this because I've seen the above syntax in the qnlp-experiment notebook.

Applying swaps

Currently, if we want to apply a swap between wires i and j, we need to do define the relevant swaps and add identity wires to the left and right of the swaps, or come up with the exact permutation, both of which is not very clean.

Proposal:
Implement diagram.apply_swap(i, j), which applies swaps (and the relevant identities) to the ith and jth wires.

`circuit.subs` breaks `circuit.eval`

Code

from sympy.abc import phi
from discopy import Rz
Rz(phi).subs(phi, 0.1).eval().array

Error:

.../discopy/quantum/gates.py in array(self)
    435         half_theta = self.modules.pi * self.phase
    436         return Tensor.np.array(
--> 437             [[self.modules.exp(-1j * half_theta), 0],
    438              [0, self.modules.exp(1j * half_theta)]])
    439
TypeError: loop of ufunc does not support argument 0 of type Mul which has no callable exp method

TypeError: loop of ufunc does not support argument 0 of type Mul which has no callable exp method
The problem is once converted the float using subs the number becomes <class 'sympy.core.numbers.Float'> and modules is still <module 'numpy' from '.../env/lib/python3.8/site-packages/numpy/__init__.py'>. This is because we choose which module to use based on whether there are any free symbols in the diagram. Therefore, we need a more robust way of choosing which module to use.

https://github.com/oxford-quantum-group/discopy/blob/472faa8fd8ed0b1ee24ca0ed7859fe6b3e45171b/discopy/quantum/gates.py#L347

For now, please use lambdify instead of subs.

quantum.circuit.Sum behaviour different between 1 and n arguments

Hi, I found the following unexpected behaviour of the entity mentioned in the subject, here the first case:

from discopy.quantum import *
Sum([Id(1), Id(1)], dom=qubit, cod=qubit).eval()

This returns Tensor(dom=Dim(2), cod=Dim(2), array=[2., 0., 0., 2.]), also the same result can be obtained through
sum([Id(1), Id(1)]).eval() and (Id(1) + Id(1)).eval().

Instead, if we pass a list containing a single term

Sum([Id(1)], dom=qubit, cod=qubit).eval()

we obtain array([1., 1.]) (same result with sum([Id(1)]).eval() and (Id(1)+0).eval()).
According to my understanding the second case should return something like Tensor(dom=Dim(2), cod=Dim(2), array=[1., 0., 0., 1.]), that is the result of Id(1).eval().
Thanks

Undesirable behaviour in Diagram.permute()

The permute function seems to permute exactly inverse to intuition.
Consider the following code:

perm = [1, 2, 3, 0]
diag.permute(*perm).draw() 

We would expect this to give the following permutation:
perm

Sending the first wire ('a') into index 1.
However, we get the following:
inv_perm

Sending the wire with index 1 into the 0th position.

Refactor inheritance

The inheritance mechanism between cat, monoidal, rigid, etc. can be simplified.
One necessary refactor is to change the initialisation of diagrams to always have the signature inside, dom, cod, i.e.

  • rename Arrow.boxes to Arrow.inside
  • rename Diagram.layers to Diagram.inside
  • make Diagram.boxes and Diagram.offsets a property, computed from inside

Then we can get rid of Diagram.subclass and use classmethod decorators instead.
We can also make Id a syntactic sugar for the class method Diagram.id.

Dagger of bubble

The dagger of bubbles is broken. Indeed, Bubble.dagger is inherited from Box.dagger which assumes that the subclasses keep the same arguments name, dom, cod, _dagger for initialisation.

>>> f = Box("f", Ty("x"), Ty("y"))
>>> f.bubble().dagger()

... in Box.dagger(self)
    576 def dagger(self):
--> 577     return type(self)(
    578         name=self.name, dom=self.cod, cod=self.dom,
    579         data=self.data, _dagger=not self._dagger)

TypeError: __init__() missing 1 required positional argument: 'inside'

`Circuit.subs()` converts `quantum.circuit.Swap` to `monoidal.Swap`

import discopy
from discopy.quantum import qubit, H
from discopy.quantum.circuit import Swap as CircuitSwap

example_circuit = CircuitSwap(qubit, qubit) >> H @ H
subbed_circuit = example_circuit.subs([])

assert all(isinstance(box, discopy.quantum.Box) for box in example_circuit.boxes)
assert not all(isinstance(box, discopy.quantum.Box) for box in subbed_circuit.boxes)

print(type(example_circuit.boxes[0]), type(subbed_circuit.boxes[0]))
subbed_circuit.eval()

Output:

<class 'discopy.quantum.circuit.Swap'> <class 'discopy.monoidal.Swap'>
AttributeError: 'Swap' object has no attribute 'is_mixed'

Python functions

DisCoPy needs a proper implementation of the category of Python types and functions.
For now, this is half-way implemented in the cartesian module.

We need a python module similar to the tensor module: it needs a diagram class where each box holds some data, and a Function class where the composition of Python functions happens.

Docstrings for core modules

Docstrings for core modules need to be standardised.
They need to contain:

  • a one sentence abstract-nonsense definition of the category
  • a one sentence intuition for what one can do in the category
  • the shortest natural language sentences that define each class
  • simple examples of functor applications (into diagrams, python, matrix, tensor, etc.)
  • the general abstract nonsense behind the implementation, axioms written in LaTex with reference to Selinger's survey and links to the nLab
  • the rewriting and normal form methods, if applicable

See here for an example with the ribbon module.

Conjugation of diagrams

DisCoPy already has a dagger method, implementing the vertical reflection of a diagram.
It would be useful to also have horizontal reflections, i.e. diagrammatic conjugation.

This requires each box to hold two Booleans, _dagger and _conjugate.
Setting _dagger = _conjugate = True we get the diagrammatic transpose for free.

Inconsistent gradients

Hi, according to my calculations , I found an inconsistency while obtaining the gradient of a rotation gate, here the code:

from sympy.abc import theta
from discopy.quantum import *

f = Ket(0) >> Rx(theta) >> Bra(1)
f = f >> f[::-1]

# We use the following definition for $R_x(\theta)$
# R_x(\theta) = \cos(\pi \theta) I - i \sin(\pi \theta) \sigma_x
#
# Now, we define a function f that represents our circuit:
# f(\theta) = \left|\bra{1} R_x(\theta) \ket{0} \right|^2 = \sin^2(\pi \theta)
# thus
# \pdv{f}{\theta} = \pi \sin(2\pi \theta)
#
# Some sample values:
# f(1/4) = \sin^2(\pi/4) = 1/2
# \left[\pdv{f}{\theta}\right](1/4) = \pi
#
# f(-1/4) = \sin^2(-\pi/4) = 1/2
# \left[\pdv{f}{\theta}\right](-1/4) = -\pi
#
# Also note that the function f is real whereas the gradient
# resulting from the code below is purely imaginary for the sample
# values indicated above.

p = -1/4    # Value used to eval f and df
print(f.subs(theta, p).eval()) # Here the result is as expected

print(f.grad(theta).subs(theta, p).eval())

The value corresponding to the second print (the one relative to grad) does not match my calculations, see above in the code the manual calculation of grad and the expected values. My results match the calculations of the library when I change in gates.Rotation.grad(...) the following line

  def grad(self, var):
    # ...
    return scalar(-np.pi * gradient) @ type(self)(self.phase - .5)

Thanks

Define `Diagram`s via method chaining

Relates to #17 (and potentially #54).

For #54, assuming the new boxes have wires added to them, we could do:

from discopy import Ty, Word
n, s, p = map(Ty, "nsp")
A = Word('A', n @ p)
V = Word('V', n.r @ s @ n.l)
B = Word('B', p.r @ n)

(A @ V @ B).cup(1, 5).cup(0, 1).cup(1, 2)

image

For quantum circuits, now that we have non-adjacent gates, we could define circuits using chaining notation:

from discopy.quantum import Id
circuit = Id(5).CX(0, 2).X(4).CRz(0.2, 4, 2).H(2)

image

Points of discussion:

  • Is this a good idea?
  • Diagram.swap() already exists. Should we change the current method to Diagram.swaps(), to be consistent with Diagram.cups() and Diagram.caps() in a new version?

More helpful `does_not_compose` error message

Currently, when one tries to compose two Diagrams that do not compose, an error message displays the two diagrams.
E.g.

A = Box('A', Ty(), Ty('s'))
B = Box('B', Ty('n'), Ty())
A >> B

gives
AxiomError: A does not compose with B.
In this case, the error message is helpful, as we can immediately see that the problem is due to Ty('n') != Ty('s').

However, when the two diagrams are complicated, it is not very helpful to see all these:

AxiomError: Cap(n.r, n) >> Id(n.r) @ Cap(s, s.l) @ Id(n) >> Id(n.r @ s @ s.l @ n) @ too >> Id(n.r @ s @ s.l @ n @ n.r @ s @ s.l @ n) @ large. >> Id(n.r @ s @ s.l @ n @ n.r @ s @ s.l) @ Cup(n, n.r) @ Id(s) >> Id(n.r @ s @ s.l @ n @ n.r @ s) @ Cup(s.l, s) >> Id(n.r @ s @ s.l) @ Cup(n, n.r) @ Id(s) >> Id(n.r @ s) @ Cup(s.l, s) does not compose with The @ Id(n.l @ s) >> Id(n @ n.l) @ trophy @ Id(s) >> Id(n @ n.l @ n @ n.l) @ doesn't @ Id(s) >> Id(n @ n.l @ n @ n.l @ n) @ fit @ Id(s) >> Id(n @ n.l @ n @ n.l @ n @ n.r @ s @ p.l) @ into @ Id(s) >> Id(n @ n.l @ n @ n.l @ n @ n.r @ s @ p.l @ p @ n.l) @ the @ Id(s) >> Id(n @ n.l @ n @ n.l @ n @ n.r @ s @ p.l @ p @ n.l @ n @ n.l) @ brown @ Id(s) >> Id(n @ n.l @ n @ n.l @ n @ n.r @ s @ p.l @ p @ n.l @ n @ n.l @ n @ n.l) @ suitcase @ Id(s) >> Id(n @ n.l @ n @ n.l @ n @ n.r @ s @ p.l @ p @ n.l @ n @ n.l @ n @ n.l @ n) @ because @ Id(s) >> Id(n @ n.l @ n) @ Cup(n.l, n) @ Id(n.r @ s @ p.l @ p @ n.l @ n @ n.l @ n @ n.l @ n @ s.r @ n.r.r @ n.r @ s @ s.l @ s) >> Id(n) @ Cup(n.l, n) @ Id(n.r @ s @ p.l @ p @ n.l @ n @ n.l @ n @ n.l @ n @ s.r @ n.r.r @ n.r @ s @ s.l @ s) >> Id(n @ n.r @ s @ p.l @ p @ n.l @ n @ n.l @ n) @ Cup(n.l, n) @ Id(s.r @ n.r.r @ n.r @ s @ s.l @ s) >> Id(n @ n.r @ s @ p.l @ p @ n.l @ n) @ Cup(n.l, n) @ Id(s.r @ n.r.r @ n.r @ s @ s.l @ s) >> Id(n @ n.r @ s @ p.l @ p) @ Cup(n.l, n) @ Id(s.r @ n.r.r @ n.r @ s @ s.l @ s) >> Id(n @ n.r @ s) @ Cup(p.l, p) @ Id(s.r @ n.r.r @ n.r @ s @ s.l @ s) >> Id(n @ n.r @ s @ s.r @ n.r.r @ n.r @ s) @ Cup(s.l, s) >> Id(n @ n.r) @ Cup(s, s.r) @ Id(n.r.r @ n.r @ s) >> Id(n) @ Cup(n.r, n.r.r) @ Id(n.r @ s) >> Cup(n, n.r) @ Id(s).

A helpful error message in these situations should help the users to figure out why don't the diagrams compose. I suggest that one way to make the message clearer is to also include A.cod and B.dom in the error message.
E.g.

A = Box('A', Ty(), Ty('s'))
B = Box('B', Ty('n'), Ty())
A >> B

AxiomError: A does not compose with B. Codomain Ty('s') is not compatible with domain Ty('n')

`to_pennylane` does not allow choosing the device

Hi,
I'm opening an issue because I don't see the "Discussion" section. I strongly suggest enabling it, to allow a better interaction with users and contributors πŸ˜„

Going to the actual content, I'm using the pennylane compatibility functions and class you implemented in the last months to write my own model for lambeq using pennylane. However I'm "struggling" with some of your design choices.

  • in to_pennylane you retrieve a PennyLaneCircuit object, which requires a qml.Device object to be constructed. However you only use default.qubit without giving the user the possibility to choose the device and the number of shots

https://github.com/oxford-quantum-group/discopy/blob/7e5f5758432744b6549f22a6f6866f85e694d7a8/discopy/quantum/pennylane.py#L186

I guess you do that because you want to enforce the number of wires to be appropriately set to the number of qubits of the tk circuit, which is fair. However, in my case I'm forced to do something like this

  pennylane_circuit = to_pennylane(diagram, probabilities=True)
  pennylane_circuit.device = THE_DEVICE_I_ACTUALLY_WANT

Can't you just introduce a device parameter (defaulted to None) to allow the user to specify one and check the number of qubits, raising an exception if not correctly set, and using the default one if None ?

def to_pennylane(disco_circuit: Circuit,  device: qml.Device=None, probabilities=False):
     ...
     if device is None:
          device = qml.device('default.qubit', wires=tk_circ.n_qubits, shots=None)
     else:
          if device.num_wires != tk_circ.n_qubits:
               raise ValueError("...")
     
     # if you get here, device is appropriate, so you can pass it to PennyLaneCircuit         

Alternatively, one might consider allowing to specify only the name and the number of shots (params of the function).

  • since there's a little of mix between torch.Tensors in discopy signatures and numpy.ndarrays in lambeq signatures, to pass the weights to PennyLaneCircuit.eval() I'm forced to cast to a tensor, otherwise I get the expected exception
    return [torch.cat(p) if len(p) > 0 else p
TypeError: expected Tensor as element 0 in argument 0, but got numpy.float64

This however raises the following exception:

RuntimeError: zero-dimensional tensor (at position 0) cannot be concatenated

the only workaround I found is replacing torch.cat with torch.stack here https://github.com/oxford-quantum-group/discopy/blob/7e5f5758432744b6549f22a6f6866f85e694d7a8/discopy/quantum/pennylane.py#L367-L368

Normalisation of mixed circuits

When I create a circuit, discard one of the wires and then compose it with its dagger and .eval(), I would expect it to be 1. The following code is a minimal example where the result of eval() deviates from 1.

from discopy.quantum import Ket, Discard, Id, qubit
state = Ket(0, 0).Ry(0.1, 0).CRx(0.3, 0, 1) 
new_state = state >> Discard() @ Id(qubit)
(new_state >> new_state.dagger()).draw()
(new_state >> new_state.dagger()).eval(mixed=True)

braided

We need an implementation of braided categories, see here.

Rewrite rule for removing redundant swaps

I am wondering if discopy has any rewriting tools for removing redundant swaps? Specifically, I am trying to automatically remove this kind of redundant swaps:
download

which is, I believe, equivalent to the followings up to symmetries:
download
download
download

Categories

DisCoPy is a library for applied category theory, but it has no explicit definition of categories. I propose to create a class Category which is just a named tuple for a par of Python types ob and ar.

cat.Category would have Ob and Arrow as default values, monoidal.Category would have Ty and Diagram, etc. We can also define e.g. tensor.Category with Dim and Tensor as default.

ModuleNotFoundError: No module named 'discopy.moncat'


ModuleNotFoundError Traceback (most recent call last)
in
----> 1 from circuit import Circuit, Rx, CX, H, sqrt
2
3 c0 = Rx(0.5) @ Circuit.id(2) >> CX @ Rx(0.125)
4 print("From discopy:\n{}\n".format(c0))
5

D:\M.Tech\Semester - 3\Review 3\discopy-ab2b356bd3cad1dfb55ca6606d6c4b4181fe590c\notebooks\circuit.py in
23 from discopy import messages
24 from discopy.cat import Quiver
---> 25 from discopy.moncat import InterchangerError
26 from discopy.rigidcat import Ob, Ty, PRO, Box, Diagram, Functor
27 from discopy.tensor import np, Dim, Tensor, TensorFunctor

ModuleNotFoundError: No module named 'discopy.moncat'

Compatability with pyzx >= 0.7.0

It seems that version 0.7.0 of pyzx has broken the integration due to the API change mentioned
here concerning Graph.inputs and Graph.outputs.

Minimal non working example (stolen from the documentation-notebooks):

With discopy 0.4.2 (and also the master) and pyzx 0.7.0 the following code:

from discopy.quantum.zx import *
from pyzx import draw
bialgebra = Z(1, 2, .25) @ Z(1, 2, .75) >> Id(1) @ SWAP @ Id(1) >> X(2, 1) @ X(2, 1, .5)
bialgebra.to_pyzx()

Throws an error:

discopy/discopy/quantum/zx.py", line 96, in to_pyzx
    graph.inputs().append(node)
AttributeError: 'tuple' object has no attribute 'append'

Proposals

I noticed that for the tests the pyzx version is pinned to 0.6.4 and with that version it works.
Would it be an option for you to add pyzx as an optional dependency, like this https://setuptools.pypa.io/en/latest/userguide/dependency_management.html?highlight=extras_require#optional-dependencies and fix the version?
That would make things a bit easier for new users (like me).

A different solution would be adapting discopy to work with the new pyzx version. Would a pull-request implementing this change be considered? Since pyzx changed from saving inputs/outputs as list to tuples, this change is sadly not completely trivial.

No modules named 'pytket' and 'jax'

We completed the install of discopy and ran alice-loves-bob.ipynb. When running cells 4 and 9, no modules named 'pytket' was rendered. When running cells 14, 15, and 19, no modules named 'jax' was rendered.

pip install pytket
ERROR: Could not find a version that satisfies the requirement pytket (from versions: none)
ERROR: No matching distribution found for pytket

pip install --upgrade jax jaxlib
Collecting jax
Downloading jax-0.1.68.tar.gz (345 kB)
|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 345 kB 1.6 MB/s
ERROR: Could not find a version that satisfies the requirement jaxlib (from versions: none)
ERROR: No matching distribution found for jaxlib

Please assist,

Weird behaviour of Functor in biclosed

discopy version: 0.4.2

I would expect the biclosed functor F defined as below to be an identity functor.

from discopy.biclosed import Functor
iden = lambda x: x
F = Functor(ob=iden, ar=iden)

However, this is not the case when you put a biclosed.FA through the said functor F.

from discopy.biclosed import Ty, FA, Over
a, b = map(Ty, 'ab')
fa = FA(Over(a, b))
Ffa = F(fa)
assert fa == Ffa
>>> AssertionError

Indeed, they are surprisingly not the same:

print(fa)
>>> FA(a << b)
print(Ffa)
>>> FA((a << b) << b)

Cause of the problem

In the definition of biclosed.Functor.__call__(self, diagram), the part that is responsible for dealing with a biclosed.FA is:

        for cls, method in [(FA, 'fa'), (BA, 'ba')]:
            if isinstance(diagram, cls):
                return getattr(self.ar_factory, method)(
                    self(diagram.dom[:1]), self(diagram.dom[1:]))

here the variable diagram would be, for instance, FA(Over(a, b)) which has domain (a << b) @ b and codomain a. Now note that the definition of the __call__ function would return biclosed.Diagram.fa(a << b, b). But biclosed.Diagram.fa actually expects (a, b) as arugements!

Interestingly, rigid.Diagram.fa (wrongfully?) expects (a << b, b) as arguments. So I guess that is why the bug has gone unnoticed? Or it's a feature, not a bug?

Suggestion

For me, I think the problem lies within biclosed.Functor as FA(Over(a, b)).dom[0] contains all the information inFA(Over(a, b)).dom[1] (i.e. one can infer .dom[1] from .dom[0], e.g. from a << b you'd know .dom[1] == b. So the second argument diagram.dom[1:] is redundant. Of course, if the behaviour of biclosed.Functor is to be changed, the corresponding factory functions in rigid.Diagram has to be changed accordingly.

Tensor product of pregroup diagrams

Currently, in version 0.3.7.2, the tensor product of two pregroup diagrams is not a pregroup diagram. (pregroup diagrams are those with all the words at the top). This behaviour is evident when trying to draw the tensored diagram with discopy.grammar.draw.

Example

from discopy import Word, Cup, Id
from discopy.grammar import draw 

n, s = Ty('n'), Ty('s')
john = Word('John', n)
talks = Word('talks', n.r @ s)
walks = Word('walks', n.r @ s)

sent1 = john @ talks >> Cup(n, n.r) @ Id(S)
sent2 = john @ walks >> Cup(n, n.r) @ Id(S)

draw(sent1 @ sent2)

ouptut:

ValueError: Expected a pregroup diagram of shape`word @ ... @ word >> cups_and_swaps`, use diagram.draw() instead.

The reason is that the method discopy.rigid.Diagram.tensor puts the boxes of sent2 after all the boxes of sent1, so that the word boxes of sent2 will appear after the cups in sent1.

If I am not mistaken, there is currently no function in discopy that would tensor together two pregroup diagrams while making sure the output is also a pregroup diagram.

I'd suggest adding a function discopy.grammar.tensor that have the required behaviour.

Controlled gates between non-adjacent qubits.

We need controlled gates between non-adjacent qubits, so that we don't clutter our diagrams with swaps. That is, we want an abstract base class:

ControlledGate(controlled, target=0)

where controlled is a gate, e.g. CX = ControlledGate(X), and target is an int that specifies the relative position of the target w.r.t the control qubit.

If we set target >= 0 then there is target qubit wires running between control on the left and target on the right.
If we set target < 0 then the control is on the right of the target instead.

We also want these new boxes to be drawn nicely, see issue https://github.com/oxford-quantum-group/discopy/issues/27.

Permutation boxes

We can already draw arbitrary permutations as sequences of swaps, but this can clutter diagrams unnecessarily.
An easy fix is to define Permutation boxes, with an appropriate refactoring of the drawing module.

These could then be constructed by calling diagram.permute as proposed in issue https://github.com/oxford-quantum-group/discopy/issues/17.

One design decision that needs to be taken: should Swap be a subclass of Permutation?

Extension of tk.Circuit applies same scalar to all circuits passed in

In tk.py on line 134, the scalar of self is being applied to the results of all circuits passed in, including the list of others. The scalar properties of the circuits in others are being ignored. Thus the scalar of only the first circuit in the list is considered.

snippet form tk.py (131 - 134):

if scale:
    for i, _ in enumerate((self, ) + others):
          for bitstring in counts[i]:
               counts[i][bitstring] *= self.scalar <- I don't think this is intended.

I believe this is not intended.

Error installing discopy

As I tried to install discopy, using pip3 install discopΓ½ I got this error:
(NLP-project) [herlimenezes@localhost ~]$ pip3 install discopy
Collecting discopy
Using cached https://files.pythonhosted.org/packages/09/8e/e6f47ca5b82f4c90675e63b57c5cb60b909473d8999114c1677f0cb3e6ba/discopy-0.2.4.tar.gz
Complete output from command python setup.py egg_info:
Traceback (most recent call last):
File "", line 1, in
File "/tmp/pip-install-pystp9a6/discopy/setup.py", line 16, in
TEST_REQ = [l.strip() for l in open('tests/requirements.txt').readlines()]
FileNotFoundError: [Errno 2] No such file or directory: 'tests/requirements.txt'

----------------------------------------

Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-install-pystp9a6/discopy/

Whiskering

I propose to define x @ f and f @ x as syntactic sugar for Id(x) @ f and f @ Id(x).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.