# -*- coding: utf-8 -*-
"""
Implements classical-quantum maps.
:class:`CQMap` implements the classical-quantum processes of
*Picturing Quantum Processes*, Coecke and Kissinger (2018).
Objects are given by a quantum dimension :class:`Q` (a.k.a. double wires)
and a classical dimension :class:`C` (a.k.a. single wires).
Arrows are given by arrays of the appropriate shape, see :class:`CQMap`.
For example, states of type :class:`Q` are density matrices:
>>> from discopy.quantum import Ket, H
>>> (Ket(0) >> H).eval(mixed=True).round(1)
CQMap(dom=CQ(), cod=Q(Dim(2)), array=[0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j])
"""
from discopy import monoidal, rigid, messages, tensor
from discopy.cat import AxiomError
from discopy.rigid import Ob, Ty, Diagram
from discopy.tensor import Dim, Tensor
from discopy.quantum.circuit import (
bit, qubit, Digit, Qudit,
Box, Sum, Swap, Discard, Measure, MixedState, Encode)
from discopy.quantum.gates import Scalar
[docs]class CQ(Ty):
"""
Implements the dimensions of classical-quantum systems.
Parameters
----------
classical : :class:`discopy.tensor.Dim`
Classical dimension.
quantum : :class:`discopy.tensor.Dim`
Quantum dimension.
Note
----
In the category of monoids, :class:`CQ` is the product of :class:`C` and
:class:`Q`, which are both isomorphic to :class:`discopy.tensor.Dim`.
Examples
--------
>>> CQ(Dim(2), Dim(2))
C(Dim(2)) @ Q(Dim(2))
>>> CQ(Dim(2), Dim(2)) @ CQ(Dim(2), Dim(2))
C(Dim(2, 2)) @ Q(Dim(2, 2))
"""
def __init__(self, classical=Dim(1), quantum=Dim(1)):
self.classical, self.quantum = classical, quantum
types = [Ob("C({})".format(dim)) for dim in classical]\
+ [Ob("Q({})".format(dim)) for dim in quantum]
super().__init__(*types)
def __repr__(self):
if not self:
return "CQ()"
if not self.classical:
return "Q({})".format(repr(self.quantum))
if not self.quantum:
return "C({})".format(repr(self.classical))
return "C({}) @ Q({})".format(repr(self.classical), repr(self.quantum))
def __str__(self):
return repr(self)
def tensor(self, *others):
classical = self.classical.tensor(*(x.classical for x in others))
quantum = self.quantum.tensor(*(x.quantum for x in others))
return CQ(classical, quantum)
@property
def l(self):
return CQ(self.classical[::-1], self.quantum[::-1])
@property
def r(self):
return self.l
[docs]class C(CQ):
"""
Implements the classical dimension of a classical-quantum system,
see :class:`CQ`.
"""
def __init__(self, dim=Dim(1)):
super().__init__(dim, Dim(1))
[docs]class Q(CQ):
"""
Implements the quantum dimension of a classical-quantum system,
see :class:`CQ`.
"""
def __init__(self, dim=Dim(1)):
super().__init__(Dim(1), dim)
[docs]class CQMap(Tensor):
"""
Implements classical-quantum maps.
Parameters
----------
dom : :class:`CQ`
Domain.
cod : :class:`CQ`
Codomain.
array : list, optional
Array of size :code:`product(utensor.dom @ utensor.cod)`.
utensor : :class:`discopy.tensor.Tensor`, optional
Underlying tensor with domain
:code:`dom.classical @ dom.quantum ** 2` and codomain
:code:`cod.classical @ cod.quantum ** 2``.
"""
@property
def utensor(self):
""" Underlying tensor. """
return Tensor(self._udom, self._ucod, self.array)
def __init__(self, dom, cod, array=None, utensor=None):
if array is None and utensor is None:
raise ValueError("One of array or utensor must be given.")
if utensor is None:
udom = dom.classical @ dom.quantum @ dom.quantum
ucod = cod.classical @ cod.quantum @ cod.quantum
else:
udom, ucod = utensor.dom, utensor.cod
super().__init__(udom, ucod, utensor.array if array is None else array)
self._dom, self._cod, self._udom, self._ucod = dom, cod, udom, ucod
def __repr__(self):
return super().__repr__().replace("Tensor", "CQMap")
def __add__(self, other):
if other == 0:
return self
if (self.dom, self.cod) != (other.dom, other.cod):
raise AxiomError(messages.cannot_add(self, other))
return CQMap(self.dom, self.cod, self.array + other.array)
def __radd__(self, other):
return self.__add__(other)
@staticmethod
def id(dom=CQ()):
utensor = Tensor.id(dom.classical @ dom.quantum @ dom.quantum)
return CQMap(dom, dom, utensor=utensor)
def then(self, *others):
if len(others) != 1:
return Tensor.then(self, *others)
other, = others
return CQMap(
self.dom, other.cod, utensor=self.utensor >> other.utensor)
def dagger(self):
return CQMap(self.cod, self.dom, utensor=self.utensor.dagger())
def tensor(self, *others):
if len(others) != 1:
return monoidal.Diagram.tensor(self, *others)
other, = others
f = rigid.Box('f', Ty('c00', 'q00', 'q00'), Ty('c10', 'q10', 'q10'))
g = rigid.Box('g', Ty('c01', 'q01', 'q01'), Ty('c11', 'q11', 'q11'))
above = Diagram.id(f.dom[:1] @ g.dom[:1] @ f.dom[1:2])\
@ Diagram.swap(g.dom[1:2], f.dom[2:]) @ Diagram.id(g.dom[2:])\
>> Diagram.id(f.dom[:1]) @ Diagram.swap(g.dom[:1], f.dom[1:])\
@ Diagram.id(g.dom[1:])
below =\
Diagram.id(f.cod[:1]) @ Diagram.swap(f.cod[1:], g.cod[:1])\
@ Diagram.id(g.cod[1:])\
>> Diagram.id(f.cod[:1] @ g.cod[:1] @ f.cod[1:2])\
@ Diagram.swap(f.cod[2:], g.cod[1:2]) @ Diagram.id(g.cod[2:])
diagram2tensor = tensor.Functor(
ob={Ty("{}{}{}".format(a, b, c)):
z.__getattribute__(y).__getattribute__(x)
for a, x in zip(['c', 'q'], ['classical', 'quantum'])
for b, y in zip([0, 1], ['dom', 'cod'])
for c, z in zip([0, 1], [self, other])},
ar={f: self.utensor.array, g: other.utensor.array})
return CQMap(self.dom @ other.dom, self.cod @ other.cod,
utensor=diagram2tensor(above >> f @ g >> below))
@staticmethod
def swap(left, right):
utensor = Tensor.swap(left.classical, right.classical)\
@ Tensor.swap(left.quantum, right.quantum)\
@ Tensor.swap(left.quantum, right.quantum)
return CQMap(left @ right, right @ left, utensor=utensor)
[docs] @staticmethod
def measure(dim, destructive=True):
""" Measure a quantum dimension into a classical dimension. """
if not dim:
return CQMap(CQ(), CQ(), Tensor.np.array(1))
if len(dim) == 1:
if destructive:
array = Tensor.np.array([
int(i == j == k)
for i in range(dim[0])
for j in range(dim[0])
for k in range(dim[0])])
return CQMap(Q(dim), C(dim), array)
array = Tensor.np.array([
int(i == j == k == l == m)
for i in range(dim[0])
for j in range(dim[0])
for k in range(dim[0])
for l in range(dim[0])
for m in range(dim[0])])
return CQMap(Q(dim), C(dim) @ Q(dim), array)
return CQMap.measure(dim[:1], destructive=destructive)\
@ CQMap.measure(dim[1:], destructive=destructive)
[docs] @staticmethod
def encode(dim, constructive=True):
""" Encode a classical dimension into a quantum dimension. """
return CQMap.measure(dim, destructive=constructive).dagger()
[docs] @staticmethod
def double(utensor):
""" Takes a tensor, returns a pure quantum CQMap. """
density = (utensor.conjugate(diagrammatic=False) @ utensor).array
return CQMap(Q(utensor.dom), Q(utensor.cod), density)
[docs] @staticmethod
def classical(utensor):
""" Takes a tensor, returns a classical CQMap. """
return CQMap(C(utensor.dom), C(utensor.cod), utensor.array)
[docs] @staticmethod
def discard(dom):
""" Discard a quantum dimension or take the marginal distribution. """
array = Tensor.np.tensordot(
Tensor.np.ones(dom.classical), Tensor.id(dom.quantum).array, 0)
return CQMap(dom, CQ(), array)
@staticmethod
def cups(left, right):
return CQMap.classical(Tensor.cups(left.classical, right.classical))\
@ CQMap.double(Tensor.cups(left.quantum, right.quantum))
@staticmethod
def caps(left, right):
return CQMap.cups(left, right).dagger()
[docs] def round(self, decimals=0):
""" Rounds the entries of a CQMap up to a number of decimals. """
return CQMap(self.dom, self.cod, utensor=self.utensor.round(decimals))
[docs]class Functor(rigid.Functor):
"""
Functors from :class:`Circuit` into :class:`CQMap`.
"""
def __init__(self, ob=None, ar=None):
self.__ob, self.__ar = ob or {}, ar or {}
super().__init__(self._ob, self._ar, ob_factory=CQ, ar_factory=CQMap)
def __repr__(self):
return "cqmap.Functor(ob={}, ar={})".format(self.__ob, self.__ar)
def _ob(self, typ):
""" Overrides the input mapping on objects for Digit and Qudit. """
obj, = typ
if isinstance(obj, Digit):
return C(Dim(obj.dim))
if isinstance(obj, Qudit):
return Q(Dim(obj.dim))
return self.__ob[typ]
def _ar(self, box):
""" Overrides the input mapping on arrows. """
if isinstance(box, Discard):
return CQMap.discard(self(box.dom))
if isinstance(box, Measure):
measure = CQMap.measure(
self(box.dom).quantum, destructive=box.destructive)
measure = measure @ CQMap.discard(self(box.dom).classical)\
if box.override_bits else measure
return measure
if isinstance(box, (MixedState, Encode)):
return self(box.dagger()).dagger()
if isinstance(box, Scalar):
scalar = box.array if box.is_mixed else abs(box.array) ** 2
return CQMap(CQ(), CQ(), scalar)
if not box.is_mixed and box.classical:
return CQMap(self(box.dom), self(box.cod), box.array)
if not box.is_mixed:
dom, cod = self(box.dom).quantum, self(box.cod).quantum
return CQMap.double(Tensor(dom, cod, box.array))
if hasattr(box, "array"):
return CQMap(self(box.dom), self(box.cod), box.array)
return self.__ar[box]