# -*- coding: utf-8 -*-
"""
Implements classical-quantum circuits.
Objects are :class:`Ty` generated by two basic types
:code:`bit` and :code:`qubit`.
Arrows are diagrams generated by :class:`QuantumGate`, :class:`ClassicalGate`,
:class:`Discard`, :class:`Measure` and :class:`Encode`.
>>> from discopy.quantum.gates import Ket, CX, H, X, Rz, sqrt, Controlled
>>> circuit = Ket(0, 0) >> CX >> Controlled(Rz(0.25)) >> Measure() @ Discard()
>>> circuit.draw(
... figsize=(3, 6),
... path='docs/_static/imgs/quantum/circuit-example.png')
.. image:: ../_static/imgs/quantum/circuit-example.png
:align: center
>>> from discopy.grammar.pregroup import Word
>>> from discopy.rigid import Ty, Cup, Id
>>> s, n = Ty('s'), Ty('n')
>>> Alice = Word('Alice', n)
>>> loves = Word('loves', n.r @ s @ n.l)
>>> Bob = Word('Bob', n)
>>> grammar = Cup(n, n.r) @ Id(s) @ Cup(n.l, n)
>>> sentence = grammar << Alice @ loves @ Bob
>>> ob = {s: 0, n: 1}
>>> ar = {Alice: Ket(0),
... loves: CX << sqrt(2) @ H @ X << Ket(0, 0),
... Bob: Ket(1)}
>>> F = Functor(ob, ar)
>>> assert abs(F(sentence).eval().array) ** 2
>>> from discopy import drawing
>>> drawing.equation(
... sentence, F(sentence), symbol='$\\\\mapsto$',
... figsize=(6, 3), nodesize=.5,
... path='docs/_static/imgs/quantum/functor-example.png')
.. image:: ../_static/imgs/quantum/functor-example.png
:align: center
"""
import random
from itertools import takewhile, chain
from collections.abc import Mapping
from discopy import messages, monoidal, rigid, tensor
from discopy.cat import AxiomError
from discopy.rigid import Diagram
from discopy.tensor import Dim, Tensor
from math import pi
from functools import reduce, partial
class AntiConjugate:
def conjugate(self):
return type(self)(-self.phase)
l = r = property(conjugate)
class RealConjugate:
def conjugate(self):
return self
l = r = property(conjugate)
class Anti2QubitConjugate:
def conjugate(self):
algebraic_conj = type(self)(-self.phase)
return Swap(qubit, qubit) >> algebraic_conj >> Swap(qubit, qubit)
l = r = property(conjugate)
def index2bitstring(i, length):
""" Turns an index into a bitstring of a given length. """
if i >= 2 ** length:
raise ValueError("Index should be less than 2 ** length.")
if not i and not length:
return ()
return tuple(map(int, '{{:0{}b}}'.format(length).format(i)))
def bitstring2index(bitstring):
""" Turns a bitstring into an index. """
return sum(value * 2 ** i for i, value in enumerate(bitstring[::-1]))
[docs]class Ob(RealConjugate, rigid.Ob):
"""
Implement the generating objects of :class:`Circuit`, i.e.
information units of some integer dimension greater than 1.
Examples
--------
>>> assert bit.objects == [Ob("bit", dim=2)]
>>> assert qubit.objects == [Ob("qubit", dim=2)]
"""
def __init__(self, name, dim=2, z=0):
super().__init__(name)
if z != 0:
raise AxiomError("circuit.Ob are self-dual.")
if not isinstance(dim, int) or dim < 2:
raise ValueError("Dimension should be an int greater than 1.")
self._dim = dim
@property
def dim(self):
""" Dimension of the unit, e.g. :code:`dim=2` for bits and qubits. """
return self._dim
def __repr__(self):
return self.name
@classmethod
def from_tree(cls, tree: dict) -> "Ob":
dim, z = tree['dim'], tree.get('z', 0)
return cls(dim=dim, z=z)
def to_tree(self) -> dict:
return dict(dim=self.dim, **super().to_tree())
[docs]class Digit(Ob):
"""
Classical unit of information of some dimension :code:`dim`.
Examples
--------
>>> assert bit.objects == [Digit(2)] == [Ob("bit", dim=2)]
"""
def __init__(self, dim, z=0):
name = "bit" if dim == 2 else "Digit({})".format(dim)
super().__init__(name, dim)
[docs]class Qudit(Ob):
"""
Quantum unit of information of some dimension :code:`dim`.
Examples
--------
>>> assert qubit.objects == [Qudit(2)] == [Ob("qubit", dim=2)]
"""
def __init__(self, dim, z=0):
name = "qubit" if dim == 2 else "Qudit({})".format(dim)
super().__init__(name, dim)
[docs]class Ty(rigid.Ty):
"""
Implement the input and output types of :class:`Circuit`.
Examples
--------
>>> assert bit == Ty(Digit(2))
>>> assert qubit == Ty(Qudit(2))
>>> assert bit @ qubit != qubit @ bit
You can construct :code:`n` qubits by taking powers of :code:`qubit`:
>>> print(bit ** 2 @ qubit ** 3)
bit @ bit @ qubit @ qubit @ qubit
"""
@staticmethod
def upgrade(old):
return Ty(*old.objects)
def __repr__(self):
return str(self)
bit, qubit = Ty(Digit(2)), Ty(Qudit(2))
[docs]@monoidal.Diagram.subclass
class Circuit(tensor.Diagram):
""" Classical-quantum circuits. """
def __repr__(self):
return super().__repr__().replace('Diagram', 'Circuit')
def conjugate(self):
return self.l
@property
def is_mixed(self):
"""
Whether the circuit is mixed, i.e. it contains both bits and qubits
or it discards qubits. Mixed circuits can be evaluated only by a
:class:`CQMapFunctor` not a :class:`discopy.tensor.Functor`.
"""
both_bits_and_qubits = self.dom.count(bit) and self.dom.count(qubit)\
or any(layer.cod.count(bit) and layer.cod.count(qubit)
for layer in self.layers)
return both_bits_and_qubits or any(box.is_mixed for box in self.boxes)
[docs] def init_and_discard(self):
""" Returns a circuit with empty domain and only bits as codomain. """
from discopy.quantum.gates import Bits, Ket
circuit = self
if circuit.dom:
init = Id(0).tensor(*(
Bits(0) if x.name == "bit" else Ket(0) for x in circuit.dom))
circuit = init >> circuit
if circuit.cod != bit ** len(circuit.cod):
discards = Id(0).tensor(*(
Discard() if x.name == "qubit"
else Id(bit) for x in circuit.cod))
circuit = circuit >> discards
return circuit
[docs] def eval(self, *others, backend=None, mixed=False,
contractor=None, **params):
"""
Evaluate a circuit on a backend, or simulate it with numpy.
Parameters
----------
others : :class:`discopy.quantum.circuit.Circuit`
Other circuits to process in batch.
backend : pytket.Backend, optional
Backend on which to run the circuit, if none then we apply
:class:`discopy.tensor.Functor` or :class:`CQMapFunctor` instead.
mixed : bool, optional
Whether to apply :class:`discopy.tensor.Functor`
or :class:`CQMapFunctor`.
contractor : callable, optional
Use :class:`tensornetwork` contraction
instead of discopy's basic eval feature.
params : kwargs, optional
Get passed to Circuit.get_counts.
Returns
-------
tensor : :class:`discopy.tensor.Tensor`
If :code:`backend is not None` or :code:`mixed=False`.
cqmap : :class:`CQMap`
Otherwise.
Examples
--------
We can evaluate a pure circuit (i.e. with :code:`not circuit.is_mixed`)
as a unitary :class:`discopy.tensor.Tensor` or as a :class:`CQMap`:
>>> from discopy.quantum import *
>>> H.eval().round(2) # doctest: +ELLIPSIS
Tensor(dom=Dim(2), cod=Dim(2), array=[0.71+0.j, ..., -0.71+0.j])
>>> H.eval(mixed=True).round(1) # doctest: +ELLIPSIS
CQMap(dom=Q(Dim(2)), cod=Q(Dim(2)), array=[0.5+0.j, ..., 0.5+0.j])
We can evaluate a mixed circuit as a :class:`CQMap`:
>>> assert Measure().eval()\\
... == CQMap(dom=Q(Dim(2)), cod=C(Dim(2)),
... array=[1, 0, 0, 0, 0, 0, 0, 1])
>>> circuit = Bits(1, 0) @ Ket(0) >> Discard(bit ** 2 @ qubit)
>>> assert circuit.eval() == CQMap(dom=CQ(), cod=CQ(), array=[1])
We can execute any circuit on a `pytket.Backend`:
>>> circuit = Ket(0, 0) >> sqrt(2) @ H @ X >> CX >> Measure() @ Bra(0)
>>> from discopy.quantum.tk import mockBackend
>>> backend = mockBackend({(0, 1): 512, (1, 0): 512})
>>> assert circuit.eval(backend, n_shots=2**10).round()\\
... == Tensor(dom=Dim(1), cod=Dim(2), array=[0., 1.])
"""
from discopy.quantum import cqmap
if contractor is not None:
array = contractor(*self.to_tn(mixed=mixed)).tensor
if self.is_mixed or mixed:
f = cqmap.Functor()
return cqmap.CQMap(f(self.dom), f(self.cod), array)
f = tensor.Functor(lambda x: x[0].dim, {})
return Tensor(f(self.dom), f(self.cod), array)
from discopy import cqmap
from discopy.quantum.gates import Bits, scalar
if len(others) == 1 and not isinstance(others[0], Circuit):
# This allows the syntax :code:`circuit.eval(backend)`
return self.eval(backend=others[0], mixed=mixed, **params)
if backend is None:
if others:
return [circuit.eval(mixed=mixed, **params)
for circuit in (self, ) + others]
functor = cqmap.Functor() if mixed or self.is_mixed\
else tensor.Functor(lambda x: x[0].dim, lambda f: f.array)
box = functor(self)
return type(box)(box.dom, box.cod, box.array + 0j)
circuits = [circuit.to_tk() for circuit in (self, ) + others]
results, counts = [], circuits[0].get_counts(
*circuits[1:], backend=backend, **params)
for i, circuit in enumerate(circuits):
n_bits = len(circuit.post_processing.dom)
result = Tensor.zeros(Dim(1), Dim(*(n_bits * (2, ))))
for bitstring, count in counts[i].items():
result += (scalar(count) @ Bits(*bitstring)).eval()
if circuit.post_processing:
result = result >> circuit.post_processing.eval()
results.append(result)
return results if len(results) > 1 else results[0]
[docs] def get_counts(self, *others, backend=None, **params):
"""
Get counts from a backend, or simulate them with numpy.
Parameters
----------
others : :class:`discopy.quantum.circuit.Circuit`
Other circuits to process in batch.
backend : pytket.Backend, optional
Backend on which to run the circuit, if none then `numpy`.
n_shots : int, optional
Number of shots, default is :code:`2**10`.
measure_all : bool, optional
Whether to measure all qubits, default is :code:`False`.
normalize : bool, optional
Whether to normalize the counts, default is :code:`True`.
post_select : bool, optional
Whether to perform post-selection, default is :code:`True`.
scale : bool, optional
Whether to scale the output, default is :code:`True`.
seed : int, optional
Seed to feed the backend, default is :code:`None`.
compilation : callable, optional
Compilation function to apply before getting counts.
Returns
-------
counts : dict
From bitstrings to counts.
Examples
--------
>>> from discopy.quantum import *
>>> circuit = H @ X >> CX >> Measure(2)
>>> from discopy.quantum.tk import mockBackend
>>> backend = mockBackend({(0, 1): 512, (1, 0): 512})
>>> circuit.get_counts(backend, n_shots=2**10)
{(0, 1): 0.5, (1, 0): 0.5}
"""
if len(others) == 1 and not isinstance(others[0], Circuit):
# This allows the syntax :code:`circuit.get_counts(backend)`
return self.get_counts(backend=others[0], **params)
if backend is None:
if others:
return [circuit.get_counts(**params)
for circuit in (self, ) + others]
utensor, counts = self.init_and_discard().eval(), dict()
for i in range(2**len(utensor.cod)):
bits = index2bitstring(i, len(utensor.cod))
if utensor.array[bits]:
counts[bits] = utensor.array[bits].real
return counts
counts = self.to_tk().get_counts(
*(other.to_tk() for other in others), backend=backend, **params)
return counts if len(counts) > 1 else counts[0]
[docs] def measure(self, mixed=False):
"""
Measure a circuit on the computational basis using :code:`numpy`.
Parameters
----------
mixed : bool, optional
Whether to apply :class:`tensor.Functor` or :class:`cqmap.Functor`.
Returns
-------
array : numpy.ndarray
"""
from discopy.quantum.gates import Bra, Ket
if mixed or self.is_mixed:
return self.init_and_discard().eval(mixed=True).array.real
state = (Ket(*(len(self.dom) * [0])) >> self).eval()
effects = [Bra(*index2bitstring(j, len(self.cod))).eval()
for j in range(2 ** len(self.cod))]
array = Tensor.np.zeros(len(self.cod) * (2, )) + 0j
for effect in effects:
array +=\
effect.array * Tensor.np.absolute((state >> effect).array) ** 2
return array
[docs] def to_tn(self, mixed=False):
"""
Send a diagram to a mixed :code:`tensornetwork`.
Parameters
----------
mixed : bool, default: False
Whether to perform mixed (also known as density matrix) evaluation
of the circuit.
Returns
-------
nodes : :class:`tensornetwork.Node`
Nodes of the network.
output_edge_order : list of :class:`tensornetwork.Edge`
Output edges of the network.
"""
if not mixed and not self.is_mixed:
return super().to_tn()
import tensornetwork as tn
from discopy.quantum import (
qubit, bit, ClassicalGate, Copy, Match, Discard, SWAP)
for box in self.boxes + [self]:
if set(box.dom @ box.cod) - set(bit @ qubit):
raise ValueError(
"Only circuits with qubits and bits are supported.")
# try to decompose some gates
diag = Id(self.dom)
last_i = 0
for i, box in enumerate(self.boxes):
if hasattr(box, '_decompose'):
decomp = box._decompose()
diag >>= self[last_i:i]
left, _, right = self.layers[i]
diag >>= Id(left) @ decomp @ Id(right)
last_i = i + 1
diag >>= self[last_i:]
self = diag
c_nodes = [tn.CopyNode(2, 2, f'c_input_{i}', dtype=complex)
for i in range(self.dom.count(bit))]
q_nodes1 = [tn.CopyNode(2, 2, f'q1_input_{i}', dtype=complex)
for i in range(self.dom.count(qubit))]
q_nodes2 = [tn.CopyNode(2, 2, f'q2_input_{i}', dtype=complex)
for i in range(self.dom.count(qubit))]
inputs = [n[0] for n in c_nodes + q_nodes1 + q_nodes2]
c_scan = [n[1] for n in c_nodes]
q_scan1 = [n[1] for n in q_nodes1]
q_scan2 = [n[1] for n in q_nodes2]
nodes = c_nodes + q_nodes1 + q_nodes2
for box, layer, offset in zip(self.boxes, self.layers, self.offsets):
if box == Circuit.swap(bit, bit):
left, _, _ = layer
c_offset = left.count(bit)
c_scan[c_offset], c_scan[c_offset + 1] =\
c_scan[c_offset + 1], c_scan[c_offset]
elif box.is_mixed or isinstance(box, ClassicalGate):
c_dom = box.dom.count(bit)
q_dom = box.dom.count(qubit)
c_cod = box.cod.count(bit)
q_cod = box.cod.count(qubit)
left, _, _ = layer
c_offset = left.count(bit)
q_offset = left.count(qubit)
if isinstance(box, Discard):
assert box.n_qubits == 1
tn.connect(q_scan1[q_offset], q_scan2[q_offset])
del q_scan1[q_offset]
del q_scan2[q_offset]
continue
if isinstance(box, (Copy, Match, Measure, Encode)):
assert len(box.dom) == 1 or len(box.cod) == 1
node = tn.CopyNode(3, 2, 'cq_' + str(box), dtype=complex)
else:
# only unoptimised gate is MixedState()
array = box.eval(mixed=True).array
node = tn.Node(array + 0j, 'cq_' + str(box))
for i in range(c_dom):
tn.connect(c_scan[c_offset + i], node[i])
for i in range(q_dom):
tn.connect(q_scan1[q_offset + i], node[c_dom + i])
for i in range(q_dom):
tn.connect(q_scan2[q_offset + i], node[c_dom + q_dom + i])
cq_dom = c_dom + 2 * q_dom
c_edges = node[cq_dom:cq_dom + c_cod]
q_edges1 = node[cq_dom + c_cod:cq_dom + c_cod + q_cod]
q_edges2 = node[cq_dom + c_cod + q_cod:]
c_scan[c_offset:c_offset + c_dom] = c_edges
q_scan1[q_offset:q_offset + q_dom] = q_edges1
q_scan2[q_offset:q_offset + q_dom] = q_edges2
nodes.append(node)
else:
left, _, _ = layer
q_offset = left[:offset + 1].count(qubit)
if box == SWAP:
q_scan1[q_offset], q_scan1[q_offset + 1] =\
q_scan1[q_offset + 1], q_scan1[q_offset]
q_scan2[q_offset], q_scan2[q_offset + 1] =\
q_scan2[q_offset + 1], q_scan2[q_offset]
continue
utensor = box.array
node1 = tn.Node(utensor + 0j, 'q1_' + str(box))
node2 = tn.Node(Tensor.np.conj(utensor) + 0j, 'q2_' + str(box))
for i in range(len(box.dom)):
tn.connect(q_scan1[q_offset + i], node1[i])
tn.connect(q_scan2[q_offset + i], node2[i])
edges1 = node1[len(box.dom):]
edges2 = node2[len(box.dom):]
q_scan1[q_offset:q_offset + len(box.dom)] = edges1
q_scan2[q_offset:q_offset + len(box.dom)] = edges2
nodes.extend([node1, node2])
outputs = c_scan + q_scan1 + q_scan2
return nodes, inputs + outputs
[docs] def to_tk(self):
"""
Export to t|ket>.
Returns
-------
tk_circuit : pytket.Circuit
A :class:`pytket.Circuit`.
Note
----
* No measurements are performed.
* SWAP gates are treated as logical swaps.
* If the circuit contains scalars or a :class:`Bra`,
then :code:`tk_circuit` will hold attributes
:code:`post_selection` and :code:`scalar`.
Examples
--------
>>> from discopy.quantum import *
>>> bell_test = H @ Id(1) >> CX >> Measure() @ Measure()
>>> bell_test.to_tk()
tk.Circuit(2, 2).H(0).CX(0, 1).Measure(0, 0).Measure(1, 1)
>>> circuit0 = sqrt(2) @ H @ Rx(0.5) >> CX >> Measure() @ Discard()
>>> circuit0.to_tk()
tk.Circuit(2, 1).H(0).Rx(1.0, 1).CX(0, 1).Measure(0, 0).scale(2)
>>> circuit1 = Ket(1, 0) >> CX >> Id(1) @ Ket(0) @ Id(1)
>>> circuit1.to_tk()
tk.Circuit(3).X(0).CX(0, 2)
>>> circuit2 = X @ Id(2) >> Id(1) @ SWAP >> CX @ Id(1) >> Id(1) @ SWAP
>>> circuit2.to_tk()
tk.Circuit(3).X(0).CX(0, 2)
>>> circuit3 = Ket(0, 0)\\
... >> H @ Id(1)\\
... >> Id(1) @ X\\
... >> CX\\
... >> Id(1) @ Bra(0)
>>> print(repr(circuit3.to_tk()))
tk.Circuit(2, 1).H(0).X(1).CX(0, 1).Measure(1, 0).post_select({0: 0})
"""
# pylint: disable=import-outside-toplevel
from discopy.quantum.tk import to_tk
return to_tk(self)
[docs] def to_pennylane(self, probabilities=False, backend_config=None,
diff_method='best'):
"""
Export DisCoPy circuit to PennylaneCircuit.
Parameters
----------
probabilties : bool, default: False
If True, the PennylaneCircuit will return the normalized
probabilties of measuring the computational basis states
when run. If False, it returns the unnormalized quantum
states in the computational basis.
Returns
-------
:class:`discopy.quantum.pennylane.PennylaneCircuit`
"""
# pylint: disable=import-outside-toplevel
from discopy.quantum.pennylane import to_pennylane
return to_pennylane(self, probabilities=probabilities,
backend_config=backend_config,
diff_method=diff_method)
[docs] @staticmethod
def from_tk(*tk_circuits):
"""
Translate a :class:`pytket.Circuit` into a :class:`Circuit`, or
a list of :class:`pytket` circuits into a :class:`Sum`.
Parameters
----------
tk_circuits : pytket.Circuit
potentially with :code:`scalar` and
:code:`post_selection` attributes.
Returns
-------
circuit : :class:`Circuit`
Such that :code:`Circuit.from_tk(circuit.to_tk()) == circuit`.
Note
----
* :meth:`Circuit.init_and_discard` is applied beforehand.
* SWAP gates are introduced when applying gates to non-adjacent qubits.
Examples
--------
>>> from discopy.quantum import *
>>> import pytket as tk
>>> c = Rz(0.5) @ Id(1) >> Id(1) @ Rx(0.25) >> CX
>>> assert Circuit.from_tk(c.to_tk()) == c.init_and_discard()
>>> tk_GHZ = tk.Circuit(3).H(1).CX(1, 2).CX(1, 0)
>>> pprint = lambda c: print(str(c).replace(' >>', '\\n >>'))
>>> pprint(Circuit.from_tk(tk_GHZ))
Ket(0)
>> Id(1) @ Ket(0)
>> Id(2) @ Ket(0)
>> Id(1) @ H @ Id(1)
>> Id(1) @ CX
>> SWAP @ Id(1)
>> CX @ Id(1)
>> SWAP @ Id(1)
>> Discard(qubit) @ Id(2)
>> Discard(qubit) @ Id(1)
>> Discard(qubit)
>>> circuit = Ket(1, 0) >> CX >> Id(1) @ Ket(0) @ Id(1)
>>> print(Circuit.from_tk(circuit.to_tk())[3:-3])
X @ Id(2) >> Id(1) @ SWAP >> CX @ Id(1) >> Id(1) @ SWAP
>>> bell_state = Circuit.caps(qubit, qubit)
>>> bell_effect = bell_state[::-1]
>>> circuit = bell_state @ Id(1) >> Id(1) @ bell_effect >> Bra(0)
>>> pprint(Circuit.from_tk(circuit.to_tk())[3:])
H @ Id(2)
>> CX @ Id(1)
>> Id(1) @ CX
>> Id(1) @ H @ Id(1)
>> Bra(0) @ Id(2)
>> Bra(0) @ Id(1)
>> Bra(0)
>> scalar(4)
"""
# pylint: disable=import-outside-toplevel
from discopy.quantum.tk import from_tk
if not tk_circuits:
return Sum([], qubit ** 0, qubit ** 0)
if len(tk_circuits) == 1:
return from_tk(tk_circuits[0])
return sum(Circuit.from_tk(c) for c in tk_circuits)
[docs] def grad(self, var, **params):
"""
Gradient with respect to :code:`var`.
Parameters
----------
var : sympy.Symbol
Differentiated variable.
Returns
-------
circuit : `discopy.quantum.circuit.Sum`
Examples
--------
>>> from sympy.abc import phi
>>> from discopy.quantum import *
>>> circuit = Rz(phi / 2) @ Rz(phi + 1) >> CX
>>> assert circuit.grad(phi, mixed=False)\\
... == (Rz(phi / 2) @ scalar(pi) @ Rz(phi + 1.5) >> CX)\\
... + (scalar(pi/2) @ Rz(phi/2 + .5) @ Rz(phi + 1) >> CX)
"""
return super().grad(var, **params)
[docs] def jacobian(self, variables, **params):
"""
Jacobian with respect to :code:`variables`.
Parameters
----------
variables : List[sympy.Symbol]
Differentiated variables.
Returns
-------
circuit : `discopy.quantum.circuit.Sum`
with :code:`circuit.dom == self.dom`
and :code:`circuit.cod == Digit(len(variables)) @ self.cod`.
Examples
--------
>>> from sympy.abc import x, y
>>> from discopy.quantum.gates import Bits, Ket, Rx, Rz
>>> circuit = Ket(0) >> Rx(x) >> Rz(y)
>>> assert circuit.jacobian([x, y])\\
... == (Bits(0) @ circuit.grad(x)) + (Bits(1) @ circuit.grad(y))
>>> assert not circuit.jacobian([])
>>> assert circuit.jacobian([x]) == circuit.grad(x)
"""
if not variables:
return Sum([], self.dom, self.cod)
if len(variables) == 1:
return self.grad(variables[0], **params)
from discopy.quantum.gates import Digits
return sum(Digits(i, dim=len(variables)) @ self.grad(x, **params)
for i, x in enumerate(variables))
[docs] def draw(self, **params):
""" We draw the labels of a circuit whenever it's mixed. """
draw_type_labels = params.get('draw_type_labels') or self.is_mixed
params = dict({'draw_type_labels': draw_type_labels}, **params)
return super().draw(**params)
@staticmethod
def swap(left, right):
return monoidal.Diagram.swap(
left, right, ar_factory=Circuit, swap_factory=Swap)
@staticmethod
def permutation(perm, dom=None, inverse=False):
if dom is None:
dom = qubit ** len(perm)
return monoidal.Diagram.permutation(
perm, dom, ar_factory=Circuit, inverse=inverse)
@staticmethod
def cups(left, right):
from discopy.quantum.gates import CX, H, sqrt, Bra, Match
def cup_factory(left, right):
if left == right == qubit:
return CX >> H @ sqrt(2) @ Id(1) >> Bra(0, 0)
if left == right == bit:
return Match() >> Discard(bit)
raise ValueError
return rigid.cups(
left, right, ar_factory=Circuit, cup_factory=cup_factory)
@staticmethod
def caps(left, right):
return Circuit.cups(left, right).dagger()
@staticmethod
def spiders(n_legs_in, n_legs_out, dim):
from discopy.quantum.gates import CX, H, Bra, sqrt
t = rigid.Ty('PRO')
if len(dim) == 0:
return Id()
def decomp_ar(spider):
return spider.decompose()
def spider_ar(spider):
dom, cod = len(spider.dom), len(spider.cod)
if dom < cod:
return spider_ar(spider.dagger()).dagger()
circ = Id(qubit)
if dom == 2:
circ = CX >> Id(qubit) @ Bra(0)
if cod == 0:
circ >>= H >> Bra(0) @ sqrt(2)
return circ
diag = Diagram.spiders(n_legs_in, n_legs_out, t ** len(dim))
decomp = monoidal.Functor(ob={t: t}, ar=decomp_ar)
to_circ = monoidal.Functor(ob={t: qubit}, ar=spider_ar,
ar_factory=Circuit, ob_factory=Ty)
circ = to_circ(decomp(diag))
return circ
def _apply_gate(self, gate, position):
""" Apply gate at position """
if position < 0 or position >= len(self.cod):
raise ValueError(f'Index {position} out of range.')
left = Id(position)
right = Id(len(self.cod) - position - len(gate.dom))
return self >> left @ gate @ right
def _apply_controlled(self, base_gate, *xs):
from discopy.quantum import Controlled
if len(set(xs)) != len(xs):
raise ValueError(f'Indices {xs} not unique.')
if min(xs) < 0 or max(xs) >= len(self.cod):
raise ValueError(f'Indices {xs} out of range.')
# example multi-controlled gate o-o---o-o-x--o-o
before = sorted(filter(lambda x: x < xs[-1], xs[:-1]))
after = sorted(filter(lambda x: x > xs[-1], xs[:-1]))
gate = base_gate
last_x = xs[-1]
for x in before[::-1]:
gate = Controlled(gate, distance=last_x - x)
last_x = x
last_x = xs[-1]
for x in after:
gate = Controlled(gate, distance=last_x - x)
last_x = x
return self._apply_gate(gate, min(xs))
[docs] def H(self, x):
""" Apply Hadamard gate to circuit. """
from discopy.quantum import H
return self._apply_gate(H, x)
[docs] def S(self, x):
""" Apply S gate to circuit. """
from discopy.quantum import S
return self._apply_gate(S, x)
[docs] def X(self, x):
""" Apply Pauli X gate to circuit. """
from discopy.quantum import X
return self._apply_gate(X, x)
[docs] def Y(self, x):
""" Apply Pauli Y gate to circuit. """
from discopy.quantum import Y
return self._apply_gate(Y, x)
[docs] def Z(self, x):
""" Apply Pauli Z gate to circuit. """
from discopy.quantum import Z
return self._apply_gate(Z, x)
[docs] def Rx(self, phase, x):
""" Apply Rx gate to circuit. """
from discopy.quantum import Rx
return self._apply_gate(Rx(phase), x)
[docs] def Ry(self, phase, x):
""" Apply Rx gate to circuit. """
from discopy.quantum import Ry
return self._apply_gate(Ry(phase), x)
[docs] def Rz(self, phase, x):
""" Apply Rz gate to circuit. """
from discopy.quantum import Rz
return self._apply_gate(Rz(phase), x)
[docs] def CX(self, x, y):
""" Apply Controlled X / CNOT gate to circuit. """
from discopy.quantum import X
return self._apply_controlled(X, x, y)
[docs] def CY(self, x, y):
""" Apply Controlled Y gate to circuit. """
from discopy.quantum import Y
return self._apply_controlled(Y, x, y)
[docs] def CZ(self, x, y):
""" Apply Controlled Z gate to circuit. """
from discopy.quantum import Z
return self._apply_controlled(Z, x, y)
[docs] def CCX(self, x, y, z):
""" Apply Controlled CX / Toffoli gate to circuit. """
from discopy.quantum import X
return self._apply_controlled(X, x, y, z)
[docs] def CCZ(self, x, y, z):
""" Apply Controlled CZ gate to circuit. """
from discopy.quantum import Z
return self._apply_controlled(Z, x, y, z)
[docs] def CRx(self, phase, x, y):
""" Apply Controlled Rx gate to circuit. """
from discopy.quantum import Rx
return self._apply_controlled(Rx(phase), x, y)
[docs] def CRz(self, phase, x, y):
""" Apply Controlled Rz gate to circuit. """
from discopy.quantum import Rz
return self._apply_controlled(Rz(phase), x, y)
[docs]class Id(rigid.Id, Circuit):
""" Identity circuit. """
def __init__(self, dom=0):
if isinstance(dom, int):
dom = qubit ** dom
self._qubit_only = all(x.name == "qubit" for x in dom)
rigid.Id.__init__(self, dom)
Circuit.__init__(self, dom, dom, [], [])
def __repr__(self):
return "Id({})".format(len(self.dom) if self._qubit_only else self.dom)
def __str__(self):
return repr(self)
Circuit.id = Id
[docs]class Box(rigid.Box, Circuit):
"""
Boxes in a circuit diagram.
Parameters
----------
name : any
dom : discopy.quantum.circuit.Ty
cod : discopy.quantum.circuit.Ty
is_mixed : bool, optional
Whether the box is mixed, default is :code:`True`.
_dagger : bool, optional
If set to :code:`None` then the box is self-adjoint.
"""
def __init__(self, name, dom, cod,
is_mixed=True, data=None, _dagger=False, _conjugate=False):
if dom and not isinstance(dom, Ty):
raise TypeError(messages.type_err(Ty, dom))
if cod and not isinstance(cod, Ty):
raise TypeError(messages.type_err(Ty, cod))
z = 1 if _conjugate else 0
self._conjugate = _conjugate
rigid.Box.__init__(
self, name, dom, cod, data=data, _dagger=_dagger, _z=z)
Circuit.__init__(self, dom, cod, [self], [0])
if not is_mixed:
if all(isinstance(x, Digit) for x in dom @ cod):
self.classical = True
elif all(isinstance(x, Qudit) for x in dom @ cod):
self.classical = False
else:
raise ValueError(
"dom and cod should be Digits only or Qudits only.")
self._mixed = is_mixed
def grad(self, var, **params):
if var not in self.free_symbols:
return Sum([], self.dom, self.cod)
raise NotImplementedError
@property
def is_mixed(self):
return self._mixed
def __repr__(self):
return self.name
[docs]class Sum(tensor.Sum, Box):
""" Sums of circuits. """
@staticmethod
def upgrade(old):
return Sum(old.terms, old.dom, old.cod)
@property
def is_mixed(self):
return any(circuit.is_mixed for circuit in self.terms)
def get_counts(self, backend=None, **params):
if not self.terms:
return {}
if len(self.terms) == 1:
return self.terms[0].get_counts(backend=backend, **params)
counts = Circuit.get_counts(*self.terms, backend=backend, **params)
result = {}
for circuit_counts in counts:
for bitstring, count in circuit_counts.items():
result[bitstring] = result.get(bitstring, 0) + count
return result
def eval(self, backend=None, mixed=False, **params):
mixed = mixed or any(t.is_mixed for t in self.terms)
if not self.terms:
return 0
if len(self.terms) == 1:
return self.terms[0].eval(backend=backend, mixed=mixed, **params)
return sum(
Circuit.eval(*self.terms, backend=backend, mixed=mixed, **params))
def grad(self, var, **params):
return sum(circuit.grad(var, **params) for circuit in self.terms)
def to_tk(self):
return [circuit.to_tk() for circuit in self.terms]
Circuit.sum = Sum
[docs]class Swap(rigid.Swap, Box):
""" Implements swaps of circuit wires. """
def __init__(self, left, right):
rigid.Swap.__init__(self, left, right)
Box.__init__(
self, self.name, self.dom, self.cod, is_mixed=left != right)
def dagger(self):
return Swap(self.right, self.left)
def conjugate(self):
return Swap(self.right, self.left)
l = r = property(conjugate)
def __repr__(self):
return "SWAP"\
if self.left == self.right == qubit else super().__repr__()
def __str__(self):
return repr(self)
[docs]class Discard(RealConjugate, Box):
""" Discard n qubits. If :code:`dom == bit` then marginal distribution. """
def __init__(self, dom=1):
if isinstance(dom, int):
dom = qubit ** dom
super().__init__(
"Discard({})".format(dom), dom, qubit ** 0, is_mixed=True)
self.draw_as_discards = True
self.n_qubits = len(dom)
def dagger(self):
return MixedState(self.dom)
def _decompose(self):
return Id().tensor(*[Discard()] * self.n_qubits)
[docs]class MixedState(RealConjugate, Box):
"""
Maximally-mixed state on n qubits.
If :code:`cod == bit` then uniform distribution.
"""
def __init__(self, cod=1):
if isinstance(cod, int):
cod = qubit ** cod
super().__init__(
"MixedState({})".format(cod), qubit ** 0, cod, is_mixed=True)
self.drawing_name = "MixedState"
if cod == bit:
self.drawing_name = ""
self.draw_as_spider, self.color = True, "black"
def dagger(self):
return Discard(self.cod)
def _decompose(self):
return Id().tensor(*[MixedState()] * len(self.cod))
[docs]class Measure(RealConjugate, Box):
"""
Measure n qubits into n bits.
Parameters
----------
n_qubits : int
Number of qubits to measure.
destructive : bool, optional
Whether to do a non-destructive measurement instead.
override_bits : bool, optional
Whether to override input bits, this is the standard behaviour of tket.
"""
def __init__(self, n_qubits=1, destructive=True, override_bits=False):
dom, cod = qubit ** n_qubits, bit ** n_qubits
name = "Measure({})".format("" if n_qubits == 1 else n_qubits)
if not destructive:
cod = qubit ** n_qubits @ cod
name = name\
.replace("()", "(1)").replace(')', ", destructive=False)")
if override_bits:
dom = dom @ bit ** n_qubits
name = name\
.replace("()", "(1)").replace(')', ", override_bits=True)")
super().__init__(name, dom, cod, is_mixed=True)
self.destructive, self.override_bits = destructive, override_bits
self.n_qubits = n_qubits
self.draw_as_measures = True
def dagger(self):
return Encode(self.n_qubits,
constructive=self.destructive,
reset_bits=self.override_bits)
def _decompose(self):
return Id().tensor(*[
Measure(destructive=self.destructive,
override_bits=self.override_bits)] * self.n_qubits)
[docs]class Encode(RealConjugate, Box):
"""
Controlled preparation, i.e. encode n bits into n qubits.
Parameters
----------
n_bits : int
Number of bits to encode.
constructive : bool, optional
Whether to do a classically-controlled correction instead.
reset_bits : bool, optional
Whether to reset the bits to the uniform distribution.
"""
def __init__(self, n_bits=1, constructive=True, reset_bits=False):
dom, cod = bit ** n_bits, qubit ** n_bits
name = Measure(n_bits, constructive, reset_bits).name\
.replace("Measure", "Encode")\
.replace("destructive", "constructive")\
.replace("override_bits", "reset_bits")
super().__init__(name, dom, cod, is_mixed=True)
self.constructive, self.reset_bits = constructive, reset_bits
self.n_bits = n_bits
def dagger(self):
return Measure(self.n_bits,
destructive=self.constructive,
override_bits=self.reset_bits)
def _decompose(self):
return Id().tensor(*[
Encode(constructive=self.constructive,
reset_bits=self.reset_bits)] * self.n_bits)
[docs]class Functor(rigid.Functor):
""" Functors into :class:`Circuit`. """
def __init__(self, ob, ar):
if isinstance(ob, Mapping):
ob = {x: qubit ** y if isinstance(y, int) else y
for x, y in ob.items()}
super().__init__(ob, ar, ob_factory=Ty, ar_factory=Circuit)
def __repr__(self):
return super().__repr__().replace("Functor", "circuit.Functor")
[docs]class IQPansatz(Circuit):
"""
Build an IQP ansatz on n qubits, if n = 1 returns an Euler decomposition
>>> pprint = lambda c: print(str(c).replace(' >>', '\\n >>'))
>>> pprint(IQPansatz(3, [[0.1, 0.2], [0.3, 0.4]]))
H @ Id(2)
>> Id(1) @ H @ Id(1)
>> Id(2) @ H
>> CRz(0.1) @ Id(1)
>> Id(1) @ CRz(0.2)
>> H @ Id(2)
>> Id(1) @ H @ Id(1)
>> Id(2) @ H
>> CRz(0.3) @ Id(1)
>> Id(1) @ CRz(0.4)
>>> print(IQPansatz(1, [0.3, 0.8, 0.4]))
Rx(0.3) >> Rz(0.8) >> Rx(0.4)
"""
def __init__(self, n_qubits, params):
from discopy.quantum.gates import H, Rx, Rz, CRz
if n_qubits == 1:
circuit = Rx(params[0]) >> Rz(params[1]) >> Rx(params[2])
elif len(Tensor.np.shape(params)) != 2\
or Tensor.np.shape(params)[1] != n_qubits - 1:
raise ValueError(
"Expected params of shape (depth, {})".format(n_qubits - 1))
else:
depth = Tensor.np.shape(params)[0]
circuit = Id(n_qubits)
for thetas in params:
hadamards = Id().tensor(*(n_qubits * [H]))
rotations = Id(n_qubits).then(*(
Id(i) @ CRz(thetas[i]) @ Id(n_qubits - 2 - i)
for i in range(n_qubits - 1)))
circuit >>= hadamards >> rotations
super().__init__(
circuit.dom, circuit.cod, circuit.boxes, circuit.offsets)
class Sim14ansatz(Circuit):
"""
Builds a modified version of circuit 14 from arXiv:1905.10876
Replaces circuit-block construction with two rings of CRx gates, in
opposite orientation.
>>> pprint = lambda c: print(str(c).replace(' >>', '\\n >>'))
>>> pprint(Sim14ansatz(3, [[i/10 for i in range(12)]]))
Ry(0) @ Id(2)
>> Id(1) @ Ry(0.1) @ Id(1)
>> Id(2) @ Ry(0.2)
>> CRx(0.3)
>> CRx(0.4) @ Id(1)
>> Id(1) @ CRx(0.5)
>> Ry(0.6) @ Id(2)
>> Id(1) @ Ry(0.7) @ Id(1)
>> Id(2) @ Ry(0.8)
>> CRx(0.9) @ Id(1)
>> CRx(1)
>> Id(1) @ CRx(1.1)
>>> pprint(Sim14ansatz(1, [0.1, 0.2, 0.3]))
Rx(0.1)
>> Rz(0.2)
>> Rx(0.3)
"""
def __init__(self, n_qubits, params):
from discopy.quantum.gates import Rx, Ry, Rz
params_shape = Tensor.np.shape(params)
if n_qubits == 1:
circuit = Rx(params[0]) >> Rz(params[1]) >> Rx(params[2])
elif (len(params_shape) != 2) or (params_shape[1] != 4 * n_qubits):
raise ValueError(
"Expected params of shape (depth, {})".format(4 * n_qubits))
else:
depth = params_shape[0]
circuit = Id(n_qubits)
for thetas in params:
sublayer1 = Id().tensor(*map(Ry, thetas[:n_qubits]))
for i in range(n_qubits):
src = i
tgt = (i - 1) % n_qubits
sublayer1 = sublayer1.CRx(thetas[n_qubits + i], src, tgt)
sublayer2 = Id().tensor(
*map(Ry, thetas[2 * n_qubits: 3 * n_qubits]))
for i in range(n_qubits, 0, -1):
src = i % n_qubits
tgt = (i + 1) % n_qubits
sublayer2 = sublayer2.CRx(thetas[-i], src, tgt)
circuit >>= sublayer1 >> sublayer2
super().__init__(
circuit.dom, circuit.cod, circuit.boxes, circuit.offsets)
class Sim15ansatz(Circuit):
"""
Builds a modified version of circuit 15 from arXiv:1905.10876
Replaces circuit-block construction with two rings of CNOT gates, in
opposite orientation.
>>> pprint = lambda c: print(str(c).replace(' >>', '\\n >>'))
>>> pprint(Sim15ansatz(3, [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]]))
Ry(0.1) @ Id(2)
>> Id(1) @ Ry(0.2) @ Id(1)
>> Id(2) @ Ry(0.3)
>> CX
>> CX @ Id(1)
>> Id(1) @ CX
>> Ry(0.4) @ Id(2)
>> Id(1) @ Ry(0.5) @ Id(1)
>> Id(2) @ Ry(0.6)
>> CX @ Id(1)
>> CX
>> Id(1) @ CX
>>> pprint(Sim15ansatz(1, [0.1, 0.2, 0.3]))
Rx(0.1)
>> Rz(0.2)
>> Rx(0.3)
"""
def __init__(self, n_qubits, params):
from discopy.quantum.gates import Rx, Ry, Rz
params_shape = Tensor.np.shape(params)
if n_qubits == 1:
circuit = Rx(params[0]) >> Rz(params[1]) >> Rx(params[2])
elif (len(params_shape) != 2) or (params_shape[1] != 2 * n_qubits):
raise ValueError(
"Expected params of shape (depth, {})".format(2 * n_qubits))
else:
depth = params_shape[0]
circuit = Id(n_qubits)
for thetas in params:
sublayer1 = Id().tensor(*map(Ry, thetas[:n_qubits]))
for i in range(n_qubits):
src = i
tgt = (i - 1) % n_qubits
sublayer1 = sublayer1.CX(src, tgt)
sublayer2 = Id().tensor(*map(Ry, thetas[n_qubits:]))
for i in range(n_qubits, 0, -1):
src = i % n_qubits
tgt = (i + 1) % n_qubits
sublayer2 = sublayer2.CX(src, tgt)
circuit >>= sublayer1 >> sublayer2
super().__init__(
circuit.dom, circuit.cod, circuit.boxes, circuit.offsets)
class Sim8ansatz(Circuit):
"""
Builds circuit 8 from arXiv:1905.10876
>>> pprint = lambda c: print(str(c).replace(' >>', '\\n >>'))
>>> pprint(Sim8ansatz(3, [[i/10 for i in range(14)]]))
Rx(0) @ Id(2)
>> Id(1) @ Rx(0.1) @ Id(1)
>> Id(2) @ Rx(0.2)
>> Rz(0.3) @ Id(2)
>> Id(1) @ Rz(0.4) @ Id(1)
>> Id(2) @ Rz(0.5)
>> CRx(0.6) @ Id(1)
>> Rx(0.7) @ Id(2)
>> Id(1) @ Rx(0.8) @ Id(1)
>> Id(2) @ Rx(0.9)
>> Rz(1) @ Id(2)
>> Id(1) @ Rz(1.1) @ Id(1)
>> Id(2) @ Rz(1.2)
>> Id(1) @ CRx(1.3)
>>> pprint(Sim8ansatz(1, [0.1, 0.2, 0.3]))
Rx(0.1)
>> Rz(0.2)
>> Rx(0.3)
"""
def __init__(self, n_qubits, params):
from discopy.quantum.gates import Rx, Rz
params_shape = Tensor.np.shape(params)
if n_qubits == 1:
circuit = Rx(params[0]) >> Rz(params[1]) >> Rx(params[2])
elif (len(params_shape) != 2) or (params_shape[1] != 5 * n_qubits - 1):
raise ValueError(
"Expected params of shape (depth, {})".format(
5 * n_qubits - 1))
else:
depth = params_shape[0]
circuit = Id(n_qubits)
for thetas in params:
RXs_1 = Id().tensor(*map(Rx, thetas[:n_qubits]))
RZs_1 = Id().tensor(*map(Rz, thetas[n_qubits: 2 * n_qubits]))
bricks_1 = Id(n_qubits)
for i, tgt in enumerate(range(1, n_qubits, 2)):
src = tgt - 1
bricks_1 = bricks_1.CRx(thetas[2 * n_qubits + i], src, tgt)
sublayer1 = RXs_1 >> RZs_1 >> bricks_1
# Params used by the first sublayer
# Used as offset for second sublayer
sl1_thetas = 2 * n_qubits + i + 1
RXs_2 = Id().tensor(
*map(Rx, thetas[sl1_thetas: sl1_thetas + n_qubits]))
RZs_2 = Id().tensor(
*map(Rz,
thetas[sl1_thetas + n_qubits:
sl1_thetas + 2 * n_qubits]))
thetas_used = sl1_thetas + 2 * n_qubits
bricks_2 = Id(n_qubits)
for i, tgt in enumerate(range(2, n_qubits, 2)):
src = tgt - 1
bricks_2 = bricks_2.CRx(thetas[thetas_used + i], src, tgt)
sublayer2 = RXs_2 >> RZs_2 >> bricks_2
circuit >>= sublayer1 >> sublayer2
super().__init__(
circuit.dom, circuit.cod, circuit.boxes, circuit.offsets)
def real_amp_ansatz(params: Tensor.np.ndarray, *, entanglement='full'):
"""
The real-amplitudes 2-local circuit. The shape of the params determines
the number of layers and the number of qubits respectively (layers, qubit).
This heuristic generates orthogonal operators so the imaginary part of the
correponding matrix is always the zero matrix.
:param params: A 2D numpy array of parameters.
:param entanglement: Configuration for the entaglement, currently either
'full' (default), 'linear' or 'circular'.
"""
from discopy.quantum.gates import CX, Ry, rewire
ext_cx = partial(rewire, CX)
assert entanglement in ('linear', 'circular', 'full')
params = Tensor.np.asarray(params)
assert params.ndim == 2
dom = qubit**params.shape[1]
n_qbs = params.shape[1]
circuit = Id(n_qbs)
for v in params[:-1]:
rys = Id().tensor(*(Ry(v[k]) for k in range(n_qbs)))
if entanglement == 'full':
cxs = [[ext_cx(k1, k2, dom=dom) for k2 in range(k1 + 1, n_qbs)] for
k1 in range(n_qbs - 1)]
cxs = reduce(lambda a, b: a >> b, chain(*cxs))
else:
cxs = [ext_cx(k, k + 1, dom=dom) for k in range(n_qbs - 1)]
cxs = reduce(lambda a, b: a >> b, cxs)
if entanglement == 'circular':
cxs = ext_cx(n_qbs - 1, 0, dom=dom) >> cxs
circuit >>= rys >> cxs
circuit >>= Id().tensor(*(Ry(params[-1][k]) for k in range(n_qbs)))
return circuit
[docs]def random_tiling(n_qubits, depth=3, gateset=None, seed=None):
""" Returns a random Euler decomposition if n_qubits == 1,
otherwise returns a random tiling with the given depth and gateset.
>>> from discopy.quantum.gates import CX, H, T, Rx, Rz
>>> c = random_tiling(1, seed=420)
>>> print(c)
Rx(0.0263) >> Rz(0.781) >> Rx(0.273)
>>> print(random_tiling(2, 2, gateset=[CX, H, T], seed=420))
CX >> T @ Id(1) >> Id(1) @ T
>>> print(random_tiling(3, 2, gateset=[CX, H, T], seed=420))
CX @ Id(1) >> Id(2) @ T >> H @ Id(2) >> Id(1) @ H @ Id(1) >> Id(2) @ H
>>> print(random_tiling(2, 1, gateset=[Rz, Rx], seed=420))
Rz(0.673) @ Id(1) >> Id(1) @ Rx(0.273)
"""
from discopy.quantum.gates import H, CX, Rx, Rz, Parametrized
gateset = gateset or [H, Rx, CX]
if seed is not None:
random.seed(seed)
if n_qubits == 1:
phases = [random.random() for _ in range(3)]
return Rx(phases[0]) >> Rz(phases[1]) >> Rx(phases[2])
result = Id(n_qubits)
for _ in range(depth):
line, n_affected = Id(0), 0
while n_affected < n_qubits:
gate = random.choice(
gateset if n_qubits - n_affected > 1 else [
g for g in gateset
if g is Rx or g is Rz or len(g.dom) == 1])
if isinstance(gate, type) and issubclass(gate, Parametrized):
gate = gate(random.random())
line = line @ gate
n_affected += len(gate.dom)
result = result >> line
return result