<jupyter_start><jupyter_text>Advanced: Extending lambeq<jupyter_code>In this tutorial you will find examples of how to extend ``lambeq`` to add more :term:`readers <reader>`, :term:`rewrite rules <rewrite rule>` and :term:`ansätze <ansatz (plural: ansätze)>`, so you can start making your own `contributions <https://github.com/CQCL/lambeq/pulls>`_ to the toolkit.

:download:`Download code <../_code/extend-lambeq.ipynb>`<jupyter_output><empty_output><jupyter_text>Creating readers<jupyter_code>The :py:class:`.Reader` class is an abstract base class for converting sentences to diagrams. Each :term:`reader` can be seen as a different :term:`compositional model`, and ``lambeq`` can accommodate any compositional model that represents sentences in a :term:`string diagram`/\ :term:`tensor network` form.

A concrete subclass of :py:class:`.Reader` should implement the :py:meth:`.Reader.sentence2diagram` method, which converts a single sentence into a rigid diagram.<jupyter_output><empty_output><jupyter_text>Reader example: "Comb" readerIn this example we will create a reader that, given a sentence, it generates the following tensor network:Note that the particular compositional model is not appropriate for classical experiments, since the tensor that implements the layer can become very large for long sentences. However, the model can be implemented without problems on a quantum computer.<jupyter_code>from lambeq import AtomicType, Reader
from discopy import Box, Id, Word

N = AtomicType.NOUN

class CombReader(Reader):
    def sentence2diagram(self, sentence):
        words = Id().tensor(*[Word(w, N) for w in sentence.split()])
        layer = Box('LAYER', words.cod, N)
        return words >> layer

diagram = CombReader().sentence2diagram('John gave Mary a flower')
diagram.draw()<jupyter_output><empty_output><jupyter_text>Note that, in the above code, the method :py:meth:`~discopy.monoidal.Diagram.tensor` refers to the monoidal product and not to a physical tensor object. What the specific line does, using the monoidal identity :py:obj:`Id()` as a starting point, is to tensor one-by-one the boxes of the words in the sentence accumulatively, from left to right, into a single diagram, as in a standard fold operation.<jupyter_code>Id().tensor(*[Word(w, N) for w in ['John', 'gave', 'Mary', 'a', 'flower']]).draw()<jupyter_output><empty_output><jupyter_text>This diagram is then combined with the ``layer`` box to create the final output of the :term:`reader`... note::   In an actual implementation, the ``layer`` box should be shared among all sentences so it can be trained properly.<jupyter_code>## Creating rewrite rules<jupyter_output><empty_output><jupyter_text>from lambeq import BobcatParserparser = BobcatParser(verbose='text')d = parser.sentence2diagram('The food is fresh')<jupyter_code>### SimpleRewriteRule example: Negation functor<jupyter_output><empty_output><jupyter_text>The :py:class:`.SimpleRewriteRule` class contains functionality that facilitates the creation of simple :term:`rewrite rules `, without the need to define a new :py:class:`.RewriteRule` class from scratch. A :py:class:`.SimpleRewriteRule` finds words with codomain ``cod`` and name in list ``words``, then replaces their boxes with the diagram in ``template``. Here is an example of a negation :term:`functor` using :py:class:`.SimpleRewriteRule`. The functor adds a "NOT" box to the wire of certain auxiliary verbs:<jupyter_code>from lambeq import AtomicType, SimpleRewriteRule
from discopy.rigid import Box, Id

N = AtomicType.NOUN
S = AtomicType.SENTENCE
adj = N @ N.l

NOT = Box('NOT', S, S)

negation_rewrite = SimpleRewriteRule(
    cod=N.r @ S @ S.l @ N,
    template=SimpleRewriteRule.placeholder(N.r @ S @ S.l @ N) >> Id(N.r) @ NOT @ Id(S.l @ N),
    words=['is', 'was', 'has', 'have'])<jupyter_output><empty_output><jupyter_text>.. note::   The placeholder ``SimpleRewriteRule.placeholder(t)`` in the template above will be replaced by a box with the same name as the original box and type ``t``.A list of :py:class:`.RewriteRule`\ s can be passed to :py:class:`.Rewriter` to create a rewriting :term:`functor`. If no list is provided, then the default rewriting rules are used (see `Diagram Rewriting `_).<jupyter_code>from lambeq import Rewriter
from discopy import drawing

not_d = Rewriter([negation_rewrite])(d)
drawing.equation(d, not_d, symbol='->', figsize=(14, 4))<jupyter_output><empty_output><jupyter_text>RewriteRule example: "Past" functor<jupyter_code>Sometimes, a :term:`rewrite rule` may become too complicated to be implemented using the :py:class:`.SimpleRewriteRule` class, so the more general :py:class:`.RewriteRule` class should be used instead. A concrete subclass of a :py:class:`.RewriteRule` should implement the methods :py:meth:`~.RewriteRule.matches` and :py:meth:`~.RewriteRule.rewrite`.

A rewriter uses the :py:meth:`~.RewriteRule.matches` methods of its :py:class:`.RewriteRule`\ s to detect if a rule can be applied. If there is a match, then the matching box is replaced with the result of ``rewrite(box)``. 

In the following example, a :term:`functor` that changes the tense of certain auxiliary verbs is implemented by directly subclassing :py:class:`.RewriteRule`:<jupyter_output><empty_output><jupyter_text>from lambeq import RewriteRuleclass PastRewriteRule(RewriteRule):    mapping = {        'is': 'was',        'are': 'were',        'has': 'had'    }    def matches(self, box):        return box.name in self.mapping        def rewrite(self, box):        new_name = self.mapping[box.name]        return type(box)(name=new_name, dom=box.dom, cod=box.cod) past_d = Rewriter([PastRewriteRule()])(d)drawing.equation(d, past_d, symbol='->', figsize=(14, 4))<jupyter_code>## Creating ansätze<jupyter_output><empty_output><jupyter_text>d = parser.sentence2diagram('We will go')<jupyter_code>:term:`Ansätze <ansatz (plural: ansätze)>` for the quantum pipeline are implemented by extending the :py:class:`.CircuitAnsatz` class, while ansätze for the classical pipeline need to extend the :py:class:`.TensorAnsatz` class. Both classes extend :py:class:`.BaseAnsatz`, sharing a common interface. Once an :term:`ansatz <ansatz (plural: ansätze)>` is instantiated, it can be used as a :term:`functor` to convert diagrams to either a :term:`circuit <quantum circuit>` or a tensor diagram.

An :term:`ansatz <ansatz (plural: ansätze)>` should be initialised with an ``ob_map`` argument, a dictionary which maps a rigid type to the number of :term:`qubits <qubit>` in the quantum case, or to a dimension size (e.g. ``Dim(2, 2)``) for the classical case. Some :term:`ansätze <ansatz (plural: ansätze)>` may require additional arguments (see the `API documentation <../root-api.rst>`_ for more details).

In :term:`DisCoPy`, a :term:`functor` is defined by specifying the mappings for objects ``ob`` and arrows ``ar``. The :py:class:`.CircuitAnsatz` and :py:class:`.TensorAnsatz` classes already implement the ``_ob`` method, which extends ``ob_map`` to map not just base (atomic) types, but also compound types, into :term:`qubits <qubit>` and dimensions respectively. Therefore, to complete a new :term:`ansatz <ansatz (plural: ansätze)>` class, you only need to implement the ``_ar`` method, i.e. the mapping from rigid boxes to diagrams. This typically involves the following steps:

1. Obtain the label of the box using the ``_summarise_box`` method. This provides a unique token which can be used to parameterise the box.
2. Apply the :term:`functor` to the domain and the codomain of the box using the ``_ob`` method.
3. Construct and return an :term:`ansatz <ansatz (plural: ansätze)>` with new domain and codomain -- see how to construct diagrams using :term:`DisCoPy` `here <https://discopy.readthedocs.io/en/main/notebooks.html>`_.<jupyter_output><empty_output><jupyter_text>CircuitAnsatz example: "Real-valued" ansatz<jupyter_code>This :term:`ansatz <ansatz (plural: ansätze)>` always returns a tensor with real-valued entries, since the ansatz is constructed using only the CNOT and Y rotation gates, which both implement real-valued unitaries.<jupyter_output><empty_output><jupyter_text>from discopy.quantum.circuit import Functor, Idfrom discopy.quantum.gates import Bra, CX, Ket, Ryfrom lambeq import CircuitAnsatzfrom lambeq.ansatz import Symbolclass RealAnsatz(CircuitAnsatz):    def __init__(self, ob_map, n_layers):        super().__init__(ob_map=ob_map, n_layers=n_layers)        self.n_layers = n_layers        self.functor = Functor(ob=self.ob_map, ar=self._ar)    def _ar(self, box):         step 1: obtain label        label = self._summarise_box(box)         step 2: map domain and codomain        dom, cod = self._ob(box.dom), self._ob(box.cod)        n_qubits = max(dom, cod)        n_layers = self.n_layers                 step 3: construct and return ansatz        if n_qubits == 1:            circuit = Ry(Symbol(f'{label}_0'))        else:             this also deals with the n_qubits == 0 case correctly            circuit = Id(n_qubits)            for i in range(n_layers):                offset = i * n_qubits                syms = [Symbol(f'{label}_{offset + j}') for j in range(n_qubits)]                 adds a ladder of CNOTs                for j in range(n_qubits - 1):                    circuit >>= Id(j) @ CX @ Id(n_qubits - j - 2)                 adds a layer of Y rotations                circuit >>= Id().tensor(*[Ry(sym) for sym in syms])        if cod <= dom:            circuit >>= Id(cod) @ Bra(*[0]*(dom - cod))        else:            circuit <<= Id(dom) @ Ket(*[0]*(cod - dom))        return circuit real_d = RealAnsatz({N: 1, S: 1}, n_layers=2)(d)real_d.draw(figsize=(12, 10))<jupyter_code>### TensorAnsatz example: "Positive" ansatz<jupyter_output><empty_output><jupyter_text>This :term:`ansatz ` returns a positive tensor, since the individual tensors are element-wise squared before contracted.<jupyter_code>from lambeq import TensorAnsatz
from discopy import rigid, tensor
from functools import reduce

class PositiveAnsatz(TensorAnsatz):
    def _ar(self, box):
        # step 1: obtain label
        label = self._summarise_box(box)

        # step 2: map domain and codomain
        dom, cod = self._ob(box.dom), self._ob(box.cod)

        # step 3: construct and return ansatz
        name = self._summarise_box(box)
        n_params = reduce(lambda x, y: x * y, dom @ cod, 1)
        syms = Symbol(name, size=n_params)
        return tensor.Box(box.name, dom, cod, syms ** 2)
from discopy import Dim

ansatz = PositiveAnsatz({N: Dim(2), S: Dim(2)})
positive_d = ansatz(d)
positive_d.draw()
import numpy as np
from sympy import default_sort_key

syms = sorted(positive_d.free_symbols, key=default_sort_key)
sym_dict = {k: -np.ones(k.size) for k in syms}
subbed_diagram = positive_d.lambdify(*syms)(*sym_dict.values())

subbed_diagram.eval()<jupyter_output><empty_output><jupyter_text>Contributions<jupyter_code>We encourage you to implement your own :term:`readers <reader>`, :term:`rewrite rules <rewrite rule>` and :term:`ansätze <ansatz (plural: ansätze)>` and `contribute to lambeq <https://github.com/CQCL/lambeq/pulls>`_ -- detailed guidelines are available `here <../CONTRIBUTING.rst>`_. Below you can find some sources of inspiration:

* rewrites for relative pronouns: [SCC2014a]_ [SCC2014b]_
* rewrites to deal with coordination: [Kar2016]_
* rewrites to reduce the dimension size of verbs: [Kea2014]_
* rewrites to language circuits (DisCoCirc): [CW2021]_

* ansätze benchmarked by their expressibility: [SJA2019]_
* high-level examples of ansätze: `[link] <https://pennylane.ai/qml/glossary/circuit_ansatz.html>`_
.. rubric:: See also:

- :ref:`General information about sub-packages <sec-package-api>`
- :ref:`UML diagrams for sub-packages <uml-diagrams>`<jupyter_output><empty_output>