I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,59 @@
# Names exposed by 'from sympy.physics.quantum import *'
__all__ = [
'AntiCommutator',
'qapply',
'Commutator',
'Dagger',
'HilbertSpaceError', 'HilbertSpace', 'TensorProductHilbertSpace',
'TensorPowerHilbertSpace', 'DirectSumHilbertSpace', 'ComplexSpace', 'L2',
'FockSpace',
'InnerProduct',
'Operator', 'HermitianOperator', 'UnitaryOperator', 'IdentityOperator',
'OuterProduct', 'DifferentialOperator',
'represent', 'rep_innerproduct', 'rep_expectation', 'integrate_result',
'get_basis', 'enumerate_states',
'KetBase', 'BraBase', 'StateBase', 'State', 'Ket', 'Bra', 'TimeDepState',
'TimeDepBra', 'TimeDepKet', 'OrthogonalKet', 'OrthogonalBra',
'OrthogonalState', 'Wavefunction',
'TensorProduct', 'tensor_product_simp',
'hbar', 'HBar',
]
from .anticommutator import AntiCommutator
from .qapply import qapply
from .commutator import Commutator
from .dagger import Dagger
from .hilbert import (HilbertSpaceError, HilbertSpace,
TensorProductHilbertSpace, TensorPowerHilbertSpace,
DirectSumHilbertSpace, ComplexSpace, L2, FockSpace)
from .innerproduct import InnerProduct
from .operator import (Operator, HermitianOperator, UnitaryOperator,
IdentityOperator, OuterProduct, DifferentialOperator)
from .represent import (represent, rep_innerproduct, rep_expectation,
integrate_result, get_basis, enumerate_states)
from .state import (KetBase, BraBase, StateBase, State, Ket, Bra,
TimeDepState, TimeDepBra, TimeDepKet, OrthogonalKet,
OrthogonalBra, OrthogonalState, Wavefunction)
from .tensorproduct import TensorProduct, tensor_product_simp
from .constants import hbar, HBar

View File

@ -0,0 +1,149 @@
"""The anti-commutator: ``{A,B} = A*B + B*A``."""
from sympy.core.expr import Expr
from sympy.core.mul import Mul
from sympy.core.numbers import Integer
from sympy.core.singleton import S
from sympy.printing.pretty.stringpict import prettyForm
from sympy.physics.quantum.operator import Operator
from sympy.physics.quantum.dagger import Dagger
__all__ = [
'AntiCommutator'
]
#-----------------------------------------------------------------------------
# Anti-commutator
#-----------------------------------------------------------------------------
class AntiCommutator(Expr):
"""The standard anticommutator, in an unevaluated state.
Explanation
===========
Evaluating an anticommutator is defined [1]_ as: ``{A, B} = A*B + B*A``.
This class returns the anticommutator in an unevaluated form. To evaluate
the anticommutator, use the ``.doit()`` method.
Canonical ordering of an anticommutator is ``{A, B}`` for ``A < B``. The
arguments of the anticommutator are put into canonical order using
``__cmp__``. If ``B < A``, then ``{A, B}`` is returned as ``{B, A}``.
Parameters
==========
A : Expr
The first argument of the anticommutator {A,B}.
B : Expr
The second argument of the anticommutator {A,B}.
Examples
========
>>> from sympy import symbols
>>> from sympy.physics.quantum import AntiCommutator
>>> from sympy.physics.quantum import Operator, Dagger
>>> x, y = symbols('x,y')
>>> A = Operator('A')
>>> B = Operator('B')
Create an anticommutator and use ``doit()`` to multiply them out.
>>> ac = AntiCommutator(A,B); ac
{A,B}
>>> ac.doit()
A*B + B*A
The commutator orders it arguments in canonical order:
>>> ac = AntiCommutator(B,A); ac
{A,B}
Commutative constants are factored out:
>>> AntiCommutator(3*x*A,x*y*B)
3*x**2*y*{A,B}
Adjoint operations applied to the anticommutator are properly applied to
the arguments:
>>> Dagger(AntiCommutator(A,B))
{Dagger(A),Dagger(B)}
References
==========
.. [1] https://en.wikipedia.org/wiki/Commutator
"""
is_commutative = False
def __new__(cls, A, B):
r = cls.eval(A, B)
if r is not None:
return r
obj = Expr.__new__(cls, A, B)
return obj
@classmethod
def eval(cls, a, b):
if not (a and b):
return S.Zero
if a == b:
return Integer(2)*a**2
if a.is_commutative or b.is_commutative:
return Integer(2)*a*b
# [xA,yB] -> xy*[A,B]
ca, nca = a.args_cnc()
cb, ncb = b.args_cnc()
c_part = ca + cb
if c_part:
return Mul(Mul(*c_part), cls(Mul._from_args(nca), Mul._from_args(ncb)))
# Canonical ordering of arguments
#The Commutator [A,B] is on canonical form if A < B.
if a.compare(b) == 1:
return cls(b, a)
def doit(self, **hints):
""" Evaluate anticommutator """
A = self.args[0]
B = self.args[1]
if isinstance(A, Operator) and isinstance(B, Operator):
try:
comm = A._eval_anticommutator(B, **hints)
except NotImplementedError:
try:
comm = B._eval_anticommutator(A, **hints)
except NotImplementedError:
comm = None
if comm is not None:
return comm.doit(**hints)
return (A*B + B*A).doit(**hints)
def _eval_adjoint(self):
return AntiCommutator(Dagger(self.args[0]), Dagger(self.args[1]))
def _sympyrepr(self, printer, *args):
return "%s(%s,%s)" % (
self.__class__.__name__, printer._print(
self.args[0]), printer._print(self.args[1])
)
def _sympystr(self, printer, *args):
return "{%s,%s}" % (
printer._print(self.args[0]), printer._print(self.args[1]))
def _pretty(self, printer, *args):
pform = printer._print(self.args[0], *args)
pform = prettyForm(*pform.right(prettyForm(',')))
pform = prettyForm(*pform.right(printer._print(self.args[1], *args)))
pform = prettyForm(*pform.parens(left='{', right='}'))
return pform
def _latex(self, printer, *args):
return "\\left\\{%s,%s\\right\\}" % tuple([
printer._print(arg, *args) for arg in self.args])

View File

@ -0,0 +1,259 @@
"""Bosonic quantum operators."""
from sympy.core.mul import Mul
from sympy.core.numbers import Integer
from sympy.core.singleton import S
from sympy.functions.elementary.complexes import conjugate
from sympy.functions.elementary.exponential import exp
from sympy.functions.elementary.miscellaneous import sqrt
from sympy.physics.quantum import Operator
from sympy.physics.quantum import HilbertSpace, FockSpace, Ket, Bra, IdentityOperator
from sympy.functions.special.tensor_functions import KroneckerDelta
__all__ = [
'BosonOp',
'BosonFockKet',
'BosonFockBra',
'BosonCoherentKet',
'BosonCoherentBra'
]
class BosonOp(Operator):
"""A bosonic operator that satisfies [a, Dagger(a)] == 1.
Parameters
==========
name : str
A string that labels the bosonic mode.
annihilation : bool
A bool that indicates if the bosonic operator is an annihilation (True,
default value) or creation operator (False)
Examples
========
>>> from sympy.physics.quantum import Dagger, Commutator
>>> from sympy.physics.quantum.boson import BosonOp
>>> a = BosonOp("a")
>>> Commutator(a, Dagger(a)).doit()
1
"""
@property
def name(self):
return self.args[0]
@property
def is_annihilation(self):
return bool(self.args[1])
@classmethod
def default_args(self):
return ("a", True)
def __new__(cls, *args, **hints):
if not len(args) in [1, 2]:
raise ValueError('1 or 2 parameters expected, got %s' % args)
if len(args) == 1:
args = (args[0], S.One)
if len(args) == 2:
args = (args[0], Integer(args[1]))
return Operator.__new__(cls, *args)
def _eval_commutator_BosonOp(self, other, **hints):
if self.name == other.name:
# [a^\dagger, a] = -1
if not self.is_annihilation and other.is_annihilation:
return S.NegativeOne
elif 'independent' in hints and hints['independent']:
# [a, b] = 0
return S.Zero
return None
def _eval_commutator_FermionOp(self, other, **hints):
return S.Zero
def _eval_anticommutator_BosonOp(self, other, **hints):
if 'independent' in hints and hints['independent']:
# {a, b} = 2 * a * b, because [a, b] = 0
return 2 * self * other
return None
def _eval_adjoint(self):
return BosonOp(str(self.name), not self.is_annihilation)
def __mul__(self, other):
if other == IdentityOperator(2):
return self
if isinstance(other, Mul):
args1 = tuple(arg for arg in other.args if arg.is_commutative)
args2 = tuple(arg for arg in other.args if not arg.is_commutative)
x = self
for y in args2:
x = x * y
return Mul(*args1) * x
return Mul(self, other)
def _print_contents_latex(self, printer, *args):
if self.is_annihilation:
return r'{%s}' % str(self.name)
else:
return r'{{%s}^\dagger}' % str(self.name)
def _print_contents(self, printer, *args):
if self.is_annihilation:
return r'%s' % str(self.name)
else:
return r'Dagger(%s)' % str(self.name)
def _print_contents_pretty(self, printer, *args):
from sympy.printing.pretty.stringpict import prettyForm
pform = printer._print(self.args[0], *args)
if self.is_annihilation:
return pform
else:
return pform**prettyForm('\N{DAGGER}')
class BosonFockKet(Ket):
"""Fock state ket for a bosonic mode.
Parameters
==========
n : Number
The Fock state number.
"""
def __new__(cls, n):
return Ket.__new__(cls, n)
@property
def n(self):
return self.label[0]
@classmethod
def dual_class(self):
return BosonFockBra
@classmethod
def _eval_hilbert_space(cls, label):
return FockSpace()
def _eval_innerproduct_BosonFockBra(self, bra, **hints):
return KroneckerDelta(self.n, bra.n)
def _apply_from_right_to_BosonOp(self, op, **options):
if op.is_annihilation:
return sqrt(self.n) * BosonFockKet(self.n - 1)
else:
return sqrt(self.n + 1) * BosonFockKet(self.n + 1)
class BosonFockBra(Bra):
"""Fock state bra for a bosonic mode.
Parameters
==========
n : Number
The Fock state number.
"""
def __new__(cls, n):
return Bra.__new__(cls, n)
@property
def n(self):
return self.label[0]
@classmethod
def dual_class(self):
return BosonFockKet
@classmethod
def _eval_hilbert_space(cls, label):
return FockSpace()
class BosonCoherentKet(Ket):
"""Coherent state ket for a bosonic mode.
Parameters
==========
alpha : Number, Symbol
The complex amplitude of the coherent state.
"""
def __new__(cls, alpha):
return Ket.__new__(cls, alpha)
@property
def alpha(self):
return self.label[0]
@classmethod
def dual_class(self):
return BosonCoherentBra
@classmethod
def _eval_hilbert_space(cls, label):
return HilbertSpace()
def _eval_innerproduct_BosonCoherentBra(self, bra, **hints):
if self.alpha == bra.alpha:
return S.One
else:
return exp(-(abs(self.alpha)**2 + abs(bra.alpha)**2 - 2 * conjugate(bra.alpha) * self.alpha)/2)
def _apply_from_right_to_BosonOp(self, op, **options):
if op.is_annihilation:
return self.alpha * self
else:
return None
class BosonCoherentBra(Bra):
"""Coherent state bra for a bosonic mode.
Parameters
==========
alpha : Number, Symbol
The complex amplitude of the coherent state.
"""
def __new__(cls, alpha):
return Bra.__new__(cls, alpha)
@property
def alpha(self):
return self.label[0]
@classmethod
def dual_class(self):
return BosonCoherentKet
def _apply_operator_BosonOp(self, op, **options):
if not op.is_annihilation:
return self.alpha * self
else:
return None

View File

@ -0,0 +1,341 @@
"""Operators and states for 1D cartesian position and momentum.
TODO:
* Add 3D classes to mappings in operatorset.py
"""
from sympy.core.numbers import (I, pi)
from sympy.core.singleton import S
from sympy.functions.elementary.exponential import exp
from sympy.functions.elementary.miscellaneous import sqrt
from sympy.functions.special.delta_functions import DiracDelta
from sympy.sets.sets import Interval
from sympy.physics.quantum.constants import hbar
from sympy.physics.quantum.hilbert import L2
from sympy.physics.quantum.operator import DifferentialOperator, HermitianOperator
from sympy.physics.quantum.state import Ket, Bra, State
__all__ = [
'XOp',
'YOp',
'ZOp',
'PxOp',
'X',
'Y',
'Z',
'Px',
'XKet',
'XBra',
'PxKet',
'PxBra',
'PositionState3D',
'PositionKet3D',
'PositionBra3D'
]
#-------------------------------------------------------------------------
# Position operators
#-------------------------------------------------------------------------
class XOp(HermitianOperator):
"""1D cartesian position operator."""
@classmethod
def default_args(self):
return ("X",)
@classmethod
def _eval_hilbert_space(self, args):
return L2(Interval(S.NegativeInfinity, S.Infinity))
def _eval_commutator_PxOp(self, other):
return I*hbar
def _apply_operator_XKet(self, ket, **options):
return ket.position*ket
def _apply_operator_PositionKet3D(self, ket, **options):
return ket.position_x*ket
def _represent_PxKet(self, basis, *, index=1, **options):
states = basis._enumerate_state(2, start_index=index)
coord1 = states[0].momentum
coord2 = states[1].momentum
d = DifferentialOperator(coord1)
delta = DiracDelta(coord1 - coord2)
return I*hbar*(d*delta)
class YOp(HermitianOperator):
""" Y cartesian coordinate operator (for 2D or 3D systems) """
@classmethod
def default_args(self):
return ("Y",)
@classmethod
def _eval_hilbert_space(self, args):
return L2(Interval(S.NegativeInfinity, S.Infinity))
def _apply_operator_PositionKet3D(self, ket, **options):
return ket.position_y*ket
class ZOp(HermitianOperator):
""" Z cartesian coordinate operator (for 3D systems) """
@classmethod
def default_args(self):
return ("Z",)
@classmethod
def _eval_hilbert_space(self, args):
return L2(Interval(S.NegativeInfinity, S.Infinity))
def _apply_operator_PositionKet3D(self, ket, **options):
return ket.position_z*ket
#-------------------------------------------------------------------------
# Momentum operators
#-------------------------------------------------------------------------
class PxOp(HermitianOperator):
"""1D cartesian momentum operator."""
@classmethod
def default_args(self):
return ("Px",)
@classmethod
def _eval_hilbert_space(self, args):
return L2(Interval(S.NegativeInfinity, S.Infinity))
def _apply_operator_PxKet(self, ket, **options):
return ket.momentum*ket
def _represent_XKet(self, basis, *, index=1, **options):
states = basis._enumerate_state(2, start_index=index)
coord1 = states[0].position
coord2 = states[1].position
d = DifferentialOperator(coord1)
delta = DiracDelta(coord1 - coord2)
return -I*hbar*(d*delta)
X = XOp('X')
Y = YOp('Y')
Z = ZOp('Z')
Px = PxOp('Px')
#-------------------------------------------------------------------------
# Position eigenstates
#-------------------------------------------------------------------------
class XKet(Ket):
"""1D cartesian position eigenket."""
@classmethod
def _operators_to_state(self, op, **options):
return self.__new__(self, *_lowercase_labels(op), **options)
def _state_to_operators(self, op_class, **options):
return op_class.__new__(op_class,
*_uppercase_labels(self), **options)
@classmethod
def default_args(self):
return ("x",)
@classmethod
def dual_class(self):
return XBra
@property
def position(self):
"""The position of the state."""
return self.label[0]
def _enumerate_state(self, num_states, **options):
return _enumerate_continuous_1D(self, num_states, **options)
def _eval_innerproduct_XBra(self, bra, **hints):
return DiracDelta(self.position - bra.position)
def _eval_innerproduct_PxBra(self, bra, **hints):
return exp(-I*self.position*bra.momentum/hbar)/sqrt(2*pi*hbar)
class XBra(Bra):
"""1D cartesian position eigenbra."""
@classmethod
def default_args(self):
return ("x",)
@classmethod
def dual_class(self):
return XKet
@property
def position(self):
"""The position of the state."""
return self.label[0]
class PositionState3D(State):
""" Base class for 3D cartesian position eigenstates """
@classmethod
def _operators_to_state(self, op, **options):
return self.__new__(self, *_lowercase_labels(op), **options)
def _state_to_operators(self, op_class, **options):
return op_class.__new__(op_class,
*_uppercase_labels(self), **options)
@classmethod
def default_args(self):
return ("x", "y", "z")
@property
def position_x(self):
""" The x coordinate of the state """
return self.label[0]
@property
def position_y(self):
""" The y coordinate of the state """
return self.label[1]
@property
def position_z(self):
""" The z coordinate of the state """
return self.label[2]
class PositionKet3D(Ket, PositionState3D):
""" 3D cartesian position eigenket """
def _eval_innerproduct_PositionBra3D(self, bra, **options):
x_diff = self.position_x - bra.position_x
y_diff = self.position_y - bra.position_y
z_diff = self.position_z - bra.position_z
return DiracDelta(x_diff)*DiracDelta(y_diff)*DiracDelta(z_diff)
@classmethod
def dual_class(self):
return PositionBra3D
# XXX: The type:ignore here is because mypy gives Definition of
# "_state_to_operators" in base class "PositionState3D" is incompatible with
# definition in base class "BraBase"
class PositionBra3D(Bra, PositionState3D): # type: ignore
""" 3D cartesian position eigenbra """
@classmethod
def dual_class(self):
return PositionKet3D
#-------------------------------------------------------------------------
# Momentum eigenstates
#-------------------------------------------------------------------------
class PxKet(Ket):
"""1D cartesian momentum eigenket."""
@classmethod
def _operators_to_state(self, op, **options):
return self.__new__(self, *_lowercase_labels(op), **options)
def _state_to_operators(self, op_class, **options):
return op_class.__new__(op_class,
*_uppercase_labels(self), **options)
@classmethod
def default_args(self):
return ("px",)
@classmethod
def dual_class(self):
return PxBra
@property
def momentum(self):
"""The momentum of the state."""
return self.label[0]
def _enumerate_state(self, *args, **options):
return _enumerate_continuous_1D(self, *args, **options)
def _eval_innerproduct_XBra(self, bra, **hints):
return exp(I*self.momentum*bra.position/hbar)/sqrt(2*pi*hbar)
def _eval_innerproduct_PxBra(self, bra, **hints):
return DiracDelta(self.momentum - bra.momentum)
class PxBra(Bra):
"""1D cartesian momentum eigenbra."""
@classmethod
def default_args(self):
return ("px",)
@classmethod
def dual_class(self):
return PxKet
@property
def momentum(self):
"""The momentum of the state."""
return self.label[0]
#-------------------------------------------------------------------------
# Global helper functions
#-------------------------------------------------------------------------
def _enumerate_continuous_1D(*args, **options):
state = args[0]
num_states = args[1]
state_class = state.__class__
index_list = options.pop('index_list', [])
if len(index_list) == 0:
start_index = options.pop('start_index', 1)
index_list = list(range(start_index, start_index + num_states))
enum_states = [0 for i in range(len(index_list))]
for i, ind in enumerate(index_list):
label = state.args[0]
enum_states[i] = state_class(str(label) + "_" + str(ind), **options)
return enum_states
def _lowercase_labels(ops):
if not isinstance(ops, set):
ops = [ops]
return [str(arg.label[0]).lower() for arg in ops]
def _uppercase_labels(ops):
if not isinstance(ops, set):
ops = [ops]
new_args = [str(arg.label[0])[0].upper() +
str(arg.label[0])[1:] for arg in ops]
return new_args

View File

@ -0,0 +1,754 @@
#TODO:
# -Implement Clebsch-Gordan symmetries
# -Improve simplification method
# -Implement new simplifications
"""Clebsch-Gordon Coefficients."""
from sympy.concrete.summations import Sum
from sympy.core.add import Add
from sympy.core.expr import Expr
from sympy.core.function import expand
from sympy.core.mul import Mul
from sympy.core.power import Pow
from sympy.core.relational import Eq
from sympy.core.singleton import S
from sympy.core.symbol import (Wild, symbols)
from sympy.core.sympify import sympify
from sympy.functions.elementary.miscellaneous import sqrt
from sympy.functions.elementary.piecewise import Piecewise
from sympy.printing.pretty.stringpict import prettyForm, stringPict
from sympy.functions.special.tensor_functions import KroneckerDelta
from sympy.physics.wigner import clebsch_gordan, wigner_3j, wigner_6j, wigner_9j
from sympy.printing.precedence import PRECEDENCE
__all__ = [
'CG',
'Wigner3j',
'Wigner6j',
'Wigner9j',
'cg_simp'
]
#-----------------------------------------------------------------------------
# CG Coefficients
#-----------------------------------------------------------------------------
class Wigner3j(Expr):
"""Class for the Wigner-3j symbols.
Explanation
===========
Wigner 3j-symbols are coefficients determined by the coupling of
two angular momenta. When created, they are expressed as symbolic
quantities that, for numerical parameters, can be evaluated using the
``.doit()`` method [1]_.
Parameters
==========
j1, m1, j2, m2, j3, m3 : Number, Symbol
Terms determining the angular momentum of coupled angular momentum
systems.
Examples
========
Declare a Wigner-3j coefficient and calculate its value
>>> from sympy.physics.quantum.cg import Wigner3j
>>> w3j = Wigner3j(6,0,4,0,2,0)
>>> w3j
Wigner3j(6, 0, 4, 0, 2, 0)
>>> w3j.doit()
sqrt(715)/143
See Also
========
CG: Clebsch-Gordan coefficients
References
==========
.. [1] Varshalovich, D A, Quantum Theory of Angular Momentum. 1988.
"""
is_commutative = True
def __new__(cls, j1, m1, j2, m2, j3, m3):
args = map(sympify, (j1, m1, j2, m2, j3, m3))
return Expr.__new__(cls, *args)
@property
def j1(self):
return self.args[0]
@property
def m1(self):
return self.args[1]
@property
def j2(self):
return self.args[2]
@property
def m2(self):
return self.args[3]
@property
def j3(self):
return self.args[4]
@property
def m3(self):
return self.args[5]
@property
def is_symbolic(self):
return not all(arg.is_number for arg in self.args)
# This is modified from the _print_Matrix method
def _pretty(self, printer, *args):
m = ((printer._print(self.j1), printer._print(self.m1)),
(printer._print(self.j2), printer._print(self.m2)),
(printer._print(self.j3), printer._print(self.m3)))
hsep = 2
vsep = 1
maxw = [-1]*3
for j in range(3):
maxw[j] = max(m[j][i].width() for i in range(2))
D = None
for i in range(2):
D_row = None
for j in range(3):
s = m[j][i]
wdelta = maxw[j] - s.width()
wleft = wdelta //2
wright = wdelta - wleft
s = prettyForm(*s.right(' '*wright))
s = prettyForm(*s.left(' '*wleft))
if D_row is None:
D_row = s
continue
D_row = prettyForm(*D_row.right(' '*hsep))
D_row = prettyForm(*D_row.right(s))
if D is None:
D = D_row
continue
for _ in range(vsep):
D = prettyForm(*D.below(' '))
D = prettyForm(*D.below(D_row))
D = prettyForm(*D.parens())
return D
def _latex(self, printer, *args):
label = map(printer._print, (self.j1, self.j2, self.j3,
self.m1, self.m2, self.m3))
return r'\left(\begin{array}{ccc} %s & %s & %s \\ %s & %s & %s \end{array}\right)' % \
tuple(label)
def doit(self, **hints):
if self.is_symbolic:
raise ValueError("Coefficients must be numerical")
return wigner_3j(self.j1, self.j2, self.j3, self.m1, self.m2, self.m3)
class CG(Wigner3j):
r"""Class for Clebsch-Gordan coefficient.
Explanation
===========
Clebsch-Gordan coefficients describe the angular momentum coupling between
two systems. The coefficients give the expansion of a coupled total angular
momentum state and an uncoupled tensor product state. The Clebsch-Gordan
coefficients are defined as [1]_:
.. math ::
C^{j_3,m_3}_{j_1,m_1,j_2,m_2} = \left\langle j_1,m_1;j_2,m_2 | j_3,m_3\right\rangle
Parameters
==========
j1, m1, j2, m2 : Number, Symbol
Angular momenta of states 1 and 2.
j3, m3: Number, Symbol
Total angular momentum of the coupled system.
Examples
========
Define a Clebsch-Gordan coefficient and evaluate its value
>>> from sympy.physics.quantum.cg import CG
>>> from sympy import S
>>> cg = CG(S(3)/2, S(3)/2, S(1)/2, -S(1)/2, 1, 1)
>>> cg
CG(3/2, 3/2, 1/2, -1/2, 1, 1)
>>> cg.doit()
sqrt(3)/2
>>> CG(j1=S(1)/2, m1=-S(1)/2, j2=S(1)/2, m2=+S(1)/2, j3=1, m3=0).doit()
sqrt(2)/2
Compare [2]_.
See Also
========
Wigner3j: Wigner-3j symbols
References
==========
.. [1] Varshalovich, D A, Quantum Theory of Angular Momentum. 1988.
.. [2] `Clebsch-Gordan Coefficients, Spherical Harmonics, and d Functions
<https://pdg.lbl.gov/2020/reviews/rpp2020-rev-clebsch-gordan-coefs.pdf>`_
in P.A. Zyla *et al.* (Particle Data Group), Prog. Theor. Exp. Phys.
2020, 083C01 (2020).
"""
precedence = PRECEDENCE["Pow"] - 1
def doit(self, **hints):
if self.is_symbolic:
raise ValueError("Coefficients must be numerical")
return clebsch_gordan(self.j1, self.j2, self.j3, self.m1, self.m2, self.m3)
def _pretty(self, printer, *args):
bot = printer._print_seq(
(self.j1, self.m1, self.j2, self.m2), delimiter=',')
top = printer._print_seq((self.j3, self.m3), delimiter=',')
pad = max(top.width(), bot.width())
bot = prettyForm(*bot.left(' '))
top = prettyForm(*top.left(' '))
if not pad == bot.width():
bot = prettyForm(*bot.right(' '*(pad - bot.width())))
if not pad == top.width():
top = prettyForm(*top.right(' '*(pad - top.width())))
s = stringPict('C' + ' '*pad)
s = prettyForm(*s.below(bot))
s = prettyForm(*s.above(top))
return s
def _latex(self, printer, *args):
label = map(printer._print, (self.j3, self.m3, self.j1,
self.m1, self.j2, self.m2))
return r'C^{%s,%s}_{%s,%s,%s,%s}' % tuple(label)
class Wigner6j(Expr):
"""Class for the Wigner-6j symbols
See Also
========
Wigner3j: Wigner-3j symbols
"""
def __new__(cls, j1, j2, j12, j3, j, j23):
args = map(sympify, (j1, j2, j12, j3, j, j23))
return Expr.__new__(cls, *args)
@property
def j1(self):
return self.args[0]
@property
def j2(self):
return self.args[1]
@property
def j12(self):
return self.args[2]
@property
def j3(self):
return self.args[3]
@property
def j(self):
return self.args[4]
@property
def j23(self):
return self.args[5]
@property
def is_symbolic(self):
return not all(arg.is_number for arg in self.args)
# This is modified from the _print_Matrix method
def _pretty(self, printer, *args):
m = ((printer._print(self.j1), printer._print(self.j3)),
(printer._print(self.j2), printer._print(self.j)),
(printer._print(self.j12), printer._print(self.j23)))
hsep = 2
vsep = 1
maxw = [-1]*3
for j in range(3):
maxw[j] = max(m[j][i].width() for i in range(2))
D = None
for i in range(2):
D_row = None
for j in range(3):
s = m[j][i]
wdelta = maxw[j] - s.width()
wleft = wdelta //2
wright = wdelta - wleft
s = prettyForm(*s.right(' '*wright))
s = prettyForm(*s.left(' '*wleft))
if D_row is None:
D_row = s
continue
D_row = prettyForm(*D_row.right(' '*hsep))
D_row = prettyForm(*D_row.right(s))
if D is None:
D = D_row
continue
for _ in range(vsep):
D = prettyForm(*D.below(' '))
D = prettyForm(*D.below(D_row))
D = prettyForm(*D.parens(left='{', right='}'))
return D
def _latex(self, printer, *args):
label = map(printer._print, (self.j1, self.j2, self.j12,
self.j3, self.j, self.j23))
return r'\left\{\begin{array}{ccc} %s & %s & %s \\ %s & %s & %s \end{array}\right\}' % \
tuple(label)
def doit(self, **hints):
if self.is_symbolic:
raise ValueError("Coefficients must be numerical")
return wigner_6j(self.j1, self.j2, self.j12, self.j3, self.j, self.j23)
class Wigner9j(Expr):
"""Class for the Wigner-9j symbols
See Also
========
Wigner3j: Wigner-3j symbols
"""
def __new__(cls, j1, j2, j12, j3, j4, j34, j13, j24, j):
args = map(sympify, (j1, j2, j12, j3, j4, j34, j13, j24, j))
return Expr.__new__(cls, *args)
@property
def j1(self):
return self.args[0]
@property
def j2(self):
return self.args[1]
@property
def j12(self):
return self.args[2]
@property
def j3(self):
return self.args[3]
@property
def j4(self):
return self.args[4]
@property
def j34(self):
return self.args[5]
@property
def j13(self):
return self.args[6]
@property
def j24(self):
return self.args[7]
@property
def j(self):
return self.args[8]
@property
def is_symbolic(self):
return not all(arg.is_number for arg in self.args)
# This is modified from the _print_Matrix method
def _pretty(self, printer, *args):
m = (
(printer._print(
self.j1), printer._print(self.j3), printer._print(self.j13)),
(printer._print(
self.j2), printer._print(self.j4), printer._print(self.j24)),
(printer._print(self.j12), printer._print(self.j34), printer._print(self.j)))
hsep = 2
vsep = 1
maxw = [-1]*3
for j in range(3):
maxw[j] = max(m[j][i].width() for i in range(3))
D = None
for i in range(3):
D_row = None
for j in range(3):
s = m[j][i]
wdelta = maxw[j] - s.width()
wleft = wdelta //2
wright = wdelta - wleft
s = prettyForm(*s.right(' '*wright))
s = prettyForm(*s.left(' '*wleft))
if D_row is None:
D_row = s
continue
D_row = prettyForm(*D_row.right(' '*hsep))
D_row = prettyForm(*D_row.right(s))
if D is None:
D = D_row
continue
for _ in range(vsep):
D = prettyForm(*D.below(' '))
D = prettyForm(*D.below(D_row))
D = prettyForm(*D.parens(left='{', right='}'))
return D
def _latex(self, printer, *args):
label = map(printer._print, (self.j1, self.j2, self.j12, self.j3,
self.j4, self.j34, self.j13, self.j24, self.j))
return r'\left\{\begin{array}{ccc} %s & %s & %s \\ %s & %s & %s \\ %s & %s & %s \end{array}\right\}' % \
tuple(label)
def doit(self, **hints):
if self.is_symbolic:
raise ValueError("Coefficients must be numerical")
return wigner_9j(self.j1, self.j2, self.j12, self.j3, self.j4, self.j34, self.j13, self.j24, self.j)
def cg_simp(e):
"""Simplify and combine CG coefficients.
Explanation
===========
This function uses various symmetry and properties of sums and
products of Clebsch-Gordan coefficients to simplify statements
involving these terms [1]_.
Examples
========
Simplify the sum over CG(a,alpha,0,0,a,alpha) for all alpha to
2*a+1
>>> from sympy.physics.quantum.cg import CG, cg_simp
>>> a = CG(1,1,0,0,1,1)
>>> b = CG(1,0,0,0,1,0)
>>> c = CG(1,-1,0,0,1,-1)
>>> cg_simp(a+b+c)
3
See Also
========
CG: Clebsh-Gordan coefficients
References
==========
.. [1] Varshalovich, D A, Quantum Theory of Angular Momentum. 1988.
"""
if isinstance(e, Add):
return _cg_simp_add(e)
elif isinstance(e, Sum):
return _cg_simp_sum(e)
elif isinstance(e, Mul):
return Mul(*[cg_simp(arg) for arg in e.args])
elif isinstance(e, Pow):
return Pow(cg_simp(e.base), e.exp)
else:
return e
def _cg_simp_add(e):
#TODO: Improve simplification method
"""Takes a sum of terms involving Clebsch-Gordan coefficients and
simplifies the terms.
Explanation
===========
First, we create two lists, cg_part, which is all the terms involving CG
coefficients, and other_part, which is all other terms. The cg_part list
is then passed to the simplification methods, which return the new cg_part
and any additional terms that are added to other_part
"""
cg_part = []
other_part = []
e = expand(e)
for arg in e.args:
if arg.has(CG):
if isinstance(arg, Sum):
other_part.append(_cg_simp_sum(arg))
elif isinstance(arg, Mul):
terms = 1
for term in arg.args:
if isinstance(term, Sum):
terms *= _cg_simp_sum(term)
else:
terms *= term
if terms.has(CG):
cg_part.append(terms)
else:
other_part.append(terms)
else:
cg_part.append(arg)
else:
other_part.append(arg)
cg_part, other = _check_varsh_871_1(cg_part)
other_part.append(other)
cg_part, other = _check_varsh_871_2(cg_part)
other_part.append(other)
cg_part, other = _check_varsh_872_9(cg_part)
other_part.append(other)
return Add(*cg_part) + Add(*other_part)
def _check_varsh_871_1(term_list):
# Sum( CG(a,alpha,b,0,a,alpha), (alpha, -a, a)) == KroneckerDelta(b,0)
a, alpha, b, lt = map(Wild, ('a', 'alpha', 'b', 'lt'))
expr = lt*CG(a, alpha, b, 0, a, alpha)
simp = (2*a + 1)*KroneckerDelta(b, 0)
sign = lt/abs(lt)
build_expr = 2*a + 1
index_expr = a + alpha
return _check_cg_simp(expr, simp, sign, lt, term_list, (a, alpha, b, lt), (a, b), build_expr, index_expr)
def _check_varsh_871_2(term_list):
# Sum((-1)**(a-alpha)*CG(a,alpha,a,-alpha,c,0),(alpha,-a,a))
a, alpha, c, lt = map(Wild, ('a', 'alpha', 'c', 'lt'))
expr = lt*CG(a, alpha, a, -alpha, c, 0)
simp = sqrt(2*a + 1)*KroneckerDelta(c, 0)
sign = (-1)**(a - alpha)*lt/abs(lt)
build_expr = 2*a + 1
index_expr = a + alpha
return _check_cg_simp(expr, simp, sign, lt, term_list, (a, alpha, c, lt), (a, c), build_expr, index_expr)
def _check_varsh_872_9(term_list):
# Sum( CG(a,alpha,b,beta,c,gamma)*CG(a,alpha',b,beta',c,gamma), (gamma, -c, c), (c, abs(a-b), a+b))
a, alpha, alphap, b, beta, betap, c, gamma, lt = map(Wild, (
'a', 'alpha', 'alphap', 'b', 'beta', 'betap', 'c', 'gamma', 'lt'))
# Case alpha==alphap, beta==betap
# For numerical alpha,beta
expr = lt*CG(a, alpha, b, beta, c, gamma)**2
simp = S.One
sign = lt/abs(lt)
x = abs(a - b)
y = abs(alpha + beta)
build_expr = a + b + 1 - Piecewise((x, x > y), (0, Eq(x, y)), (y, y > x))
index_expr = a + b - c
term_list, other1 = _check_cg_simp(expr, simp, sign, lt, term_list, (a, alpha, b, beta, c, gamma, lt), (a, alpha, b, beta), build_expr, index_expr)
# For symbolic alpha,beta
x = abs(a - b)
y = a + b
build_expr = (y + 1 - x)*(x + y + 1)
index_expr = (c - x)*(x + c) + c + gamma
term_list, other2 = _check_cg_simp(expr, simp, sign, lt, term_list, (a, alpha, b, beta, c, gamma, lt), (a, alpha, b, beta), build_expr, index_expr)
# Case alpha!=alphap or beta!=betap
# Note: this only works with leading term of 1, pattern matching is unable to match when there is a Wild leading term
# For numerical alpha,alphap,beta,betap
expr = CG(a, alpha, b, beta, c, gamma)*CG(a, alphap, b, betap, c, gamma)
simp = KroneckerDelta(alpha, alphap)*KroneckerDelta(beta, betap)
sign = S.One
x = abs(a - b)
y = abs(alpha + beta)
build_expr = a + b + 1 - Piecewise((x, x > y), (0, Eq(x, y)), (y, y > x))
index_expr = a + b - c
term_list, other3 = _check_cg_simp(expr, simp, sign, S.One, term_list, (a, alpha, alphap, b, beta, betap, c, gamma), (a, alpha, alphap, b, beta, betap), build_expr, index_expr)
# For symbolic alpha,alphap,beta,betap
x = abs(a - b)
y = a + b
build_expr = (y + 1 - x)*(x + y + 1)
index_expr = (c - x)*(x + c) + c + gamma
term_list, other4 = _check_cg_simp(expr, simp, sign, S.One, term_list, (a, alpha, alphap, b, beta, betap, c, gamma), (a, alpha, alphap, b, beta, betap), build_expr, index_expr)
return term_list, other1 + other2 + other4
def _check_cg_simp(expr, simp, sign, lt, term_list, variables, dep_variables, build_index_expr, index_expr):
""" Checks for simplifications that can be made, returning a tuple of the
simplified list of terms and any terms generated by simplification.
Parameters
==========
expr: expression
The expression with Wild terms that will be matched to the terms in
the sum
simp: expression
The expression with Wild terms that is substituted in place of the CG
terms in the case of simplification
sign: expression
The expression with Wild terms denoting the sign that is on expr that
must match
lt: expression
The expression with Wild terms that gives the leading term of the
matched expr
term_list: list
A list of all of the terms is the sum to be simplified
variables: list
A list of all the variables that appears in expr
dep_variables: list
A list of the variables that must match for all the terms in the sum,
i.e. the dependent variables
build_index_expr: expression
Expression with Wild terms giving the number of elements in cg_index
index_expr: expression
Expression with Wild terms giving the index terms have when storing
them to cg_index
"""
other_part = 0
i = 0
while i < len(term_list):
sub_1 = _check_cg(term_list[i], expr, len(variables))
if sub_1 is None:
i += 1
continue
if not build_index_expr.subs(sub_1).is_number:
i += 1
continue
sub_dep = [(x, sub_1[x]) for x in dep_variables]
cg_index = [None]*build_index_expr.subs(sub_1)
for j in range(i, len(term_list)):
sub_2 = _check_cg(term_list[j], expr.subs(sub_dep), len(variables) - len(dep_variables), sign=(sign.subs(sub_1), sign.subs(sub_dep)))
if sub_2 is None:
continue
if not index_expr.subs(sub_dep).subs(sub_2).is_number:
continue
cg_index[index_expr.subs(sub_dep).subs(sub_2)] = j, expr.subs(lt, 1).subs(sub_dep).subs(sub_2), lt.subs(sub_2), sign.subs(sub_dep).subs(sub_2)
if not any(i is None for i in cg_index):
min_lt = min(*[ abs(term[2]) for term in cg_index ])
indices = [ term[0] for term in cg_index]
indices.sort()
indices.reverse()
[ term_list.pop(j) for j in indices ]
for term in cg_index:
if abs(term[2]) > min_lt:
term_list.append( (term[2] - min_lt*term[3])*term[1] )
other_part += min_lt*(sign*simp).subs(sub_1)
else:
i += 1
return term_list, other_part
def _check_cg(cg_term, expr, length, sign=None):
"""Checks whether a term matches the given expression"""
# TODO: Check for symmetries
matches = cg_term.match(expr)
if matches is None:
return
if sign is not None:
if not isinstance(sign, tuple):
raise TypeError('sign must be a tuple')
if not sign[0] == (sign[1]).subs(matches):
return
if len(matches) == length:
return matches
def _cg_simp_sum(e):
e = _check_varsh_sum_871_1(e)
e = _check_varsh_sum_871_2(e)
e = _check_varsh_sum_872_4(e)
return e
def _check_varsh_sum_871_1(e):
a = Wild('a')
alpha = symbols('alpha')
b = Wild('b')
match = e.match(Sum(CG(a, alpha, b, 0, a, alpha), (alpha, -a, a)))
if match is not None and len(match) == 2:
return ((2*a + 1)*KroneckerDelta(b, 0)).subs(match)
return e
def _check_varsh_sum_871_2(e):
a = Wild('a')
alpha = symbols('alpha')
c = Wild('c')
match = e.match(
Sum((-1)**(a - alpha)*CG(a, alpha, a, -alpha, c, 0), (alpha, -a, a)))
if match is not None and len(match) == 2:
return (sqrt(2*a + 1)*KroneckerDelta(c, 0)).subs(match)
return e
def _check_varsh_sum_872_4(e):
alpha = symbols('alpha')
beta = symbols('beta')
a = Wild('a')
b = Wild('b')
c = Wild('c')
cp = Wild('cp')
gamma = Wild('gamma')
gammap = Wild('gammap')
cg1 = CG(a, alpha, b, beta, c, gamma)
cg2 = CG(a, alpha, b, beta, cp, gammap)
match1 = e.match(Sum(cg1*cg2, (alpha, -a, a), (beta, -b, b)))
if match1 is not None and len(match1) == 6:
return (KroneckerDelta(c, cp)*KroneckerDelta(gamma, gammap)).subs(match1)
match2 = e.match(Sum(cg1**2, (alpha, -a, a), (beta, -b, b)))
if match2 is not None and len(match2) == 4:
return S.One
return e
def _cg_list(term):
if isinstance(term, CG):
return (term,), 1, 1
cg = []
coeff = 1
if not isinstance(term, (Mul, Pow)):
raise NotImplementedError('term must be CG, Add, Mul or Pow')
if isinstance(term, Pow) and term.exp.is_number:
if term.exp.is_number:
[ cg.append(term.base) for _ in range(term.exp) ]
else:
return (term,), 1, 1
if isinstance(term, Mul):
for arg in term.args:
if isinstance(arg, CG):
cg.append(arg)
else:
coeff *= arg
return cg, coeff, coeff/abs(coeff)

View File

@ -0,0 +1,370 @@
"""Matplotlib based plotting of quantum circuits.
Todo:
* Optimize printing of large circuits.
* Get this to work with single gates.
* Do a better job checking the form of circuits to make sure it is a Mul of
Gates.
* Get multi-target gates plotting.
* Get initial and final states to plot.
* Get measurements to plot. Might need to rethink measurement as a gate
issue.
* Get scale and figsize to be handled in a better way.
* Write some tests/examples!
"""
from __future__ import annotations
from sympy.core.mul import Mul
from sympy.external import import_module
from sympy.physics.quantum.gate import Gate, OneQubitGate, CGate, CGateS
__all__ = [
'CircuitPlot',
'circuit_plot',
'labeller',
'Mz',
'Mx',
'CreateOneQubitGate',
'CreateCGate',
]
np = import_module('numpy')
matplotlib = import_module(
'matplotlib', import_kwargs={'fromlist': ['pyplot']},
catch=(RuntimeError,)) # This is raised in environments that have no display.
if np and matplotlib:
pyplot = matplotlib.pyplot
Line2D = matplotlib.lines.Line2D
Circle = matplotlib.patches.Circle
#from matplotlib import rc
#rc('text',usetex=True)
class CircuitPlot:
"""A class for managing a circuit plot."""
scale = 1.0
fontsize = 20.0
linewidth = 1.0
control_radius = 0.05
not_radius = 0.15
swap_delta = 0.05
labels: list[str] = []
inits: dict[str, str] = {}
label_buffer = 0.5
def __init__(self, c, nqubits, **kwargs):
if not np or not matplotlib:
raise ImportError('numpy or matplotlib not available.')
self.circuit = c
self.ngates = len(self.circuit.args)
self.nqubits = nqubits
self.update(kwargs)
self._create_grid()
self._create_figure()
self._plot_wires()
self._plot_gates()
self._finish()
def update(self, kwargs):
"""Load the kwargs into the instance dict."""
self.__dict__.update(kwargs)
def _create_grid(self):
"""Create the grid of wires."""
scale = self.scale
wire_grid = np.arange(0.0, self.nqubits*scale, scale, dtype=float)
gate_grid = np.arange(0.0, self.ngates*scale, scale, dtype=float)
self._wire_grid = wire_grid
self._gate_grid = gate_grid
def _create_figure(self):
"""Create the main matplotlib figure."""
self._figure = pyplot.figure(
figsize=(self.ngates*self.scale, self.nqubits*self.scale),
facecolor='w',
edgecolor='w'
)
ax = self._figure.add_subplot(
1, 1, 1,
frameon=True
)
ax.set_axis_off()
offset = 0.5*self.scale
ax.set_xlim(self._gate_grid[0] - offset, self._gate_grid[-1] + offset)
ax.set_ylim(self._wire_grid[0] - offset, self._wire_grid[-1] + offset)
ax.set_aspect('equal')
self._axes = ax
def _plot_wires(self):
"""Plot the wires of the circuit diagram."""
xstart = self._gate_grid[0]
xstop = self._gate_grid[-1]
xdata = (xstart - self.scale, xstop + self.scale)
for i in range(self.nqubits):
ydata = (self._wire_grid[i], self._wire_grid[i])
line = Line2D(
xdata, ydata,
color='k',
lw=self.linewidth
)
self._axes.add_line(line)
if self.labels:
init_label_buffer = 0
if self.inits.get(self.labels[i]): init_label_buffer = 0.25
self._axes.text(
xdata[0]-self.label_buffer-init_label_buffer,ydata[0],
render_label(self.labels[i],self.inits),
size=self.fontsize,
color='k',ha='center',va='center')
self._plot_measured_wires()
def _plot_measured_wires(self):
ismeasured = self._measurements()
xstop = self._gate_grid[-1]
dy = 0.04 # amount to shift wires when doubled
# Plot doubled wires after they are measured
for im in ismeasured:
xdata = (self._gate_grid[ismeasured[im]],xstop+self.scale)
ydata = (self._wire_grid[im]+dy,self._wire_grid[im]+dy)
line = Line2D(
xdata, ydata,
color='k',
lw=self.linewidth
)
self._axes.add_line(line)
# Also double any controlled lines off these wires
for i,g in enumerate(self._gates()):
if isinstance(g, (CGate, CGateS)):
wires = g.controls + g.targets
for wire in wires:
if wire in ismeasured and \
self._gate_grid[i] > self._gate_grid[ismeasured[wire]]:
ydata = min(wires), max(wires)
xdata = self._gate_grid[i]-dy, self._gate_grid[i]-dy
line = Line2D(
xdata, ydata,
color='k',
lw=self.linewidth
)
self._axes.add_line(line)
def _gates(self):
"""Create a list of all gates in the circuit plot."""
gates = []
if isinstance(self.circuit, Mul):
for g in reversed(self.circuit.args):
if isinstance(g, Gate):
gates.append(g)
elif isinstance(self.circuit, Gate):
gates.append(self.circuit)
return gates
def _plot_gates(self):
"""Iterate through the gates and plot each of them."""
for i, gate in enumerate(self._gates()):
gate.plot_gate(self, i)
def _measurements(self):
"""Return a dict ``{i:j}`` where i is the index of the wire that has
been measured, and j is the gate where the wire is measured.
"""
ismeasured = {}
for i,g in enumerate(self._gates()):
if getattr(g,'measurement',False):
for target in g.targets:
if target in ismeasured:
if ismeasured[target] > i:
ismeasured[target] = i
else:
ismeasured[target] = i
return ismeasured
def _finish(self):
# Disable clipping to make panning work well for large circuits.
for o in self._figure.findobj():
o.set_clip_on(False)
def one_qubit_box(self, t, gate_idx, wire_idx):
"""Draw a box for a single qubit gate."""
x = self._gate_grid[gate_idx]
y = self._wire_grid[wire_idx]
self._axes.text(
x, y, t,
color='k',
ha='center',
va='center',
bbox={"ec": 'k', "fc": 'w', "fill": True, "lw": self.linewidth},
size=self.fontsize
)
def two_qubit_box(self, t, gate_idx, wire_idx):
"""Draw a box for a two qubit gate. Does not work yet.
"""
# x = self._gate_grid[gate_idx]
# y = self._wire_grid[wire_idx]+0.5
print(self._gate_grid)
print(self._wire_grid)
# unused:
# obj = self._axes.text(
# x, y, t,
# color='k',
# ha='center',
# va='center',
# bbox=dict(ec='k', fc='w', fill=True, lw=self.linewidth),
# size=self.fontsize
# )
def control_line(self, gate_idx, min_wire, max_wire):
"""Draw a vertical control line."""
xdata = (self._gate_grid[gate_idx], self._gate_grid[gate_idx])
ydata = (self._wire_grid[min_wire], self._wire_grid[max_wire])
line = Line2D(
xdata, ydata,
color='k',
lw=self.linewidth
)
self._axes.add_line(line)
def control_point(self, gate_idx, wire_idx):
"""Draw a control point."""
x = self._gate_grid[gate_idx]
y = self._wire_grid[wire_idx]
radius = self.control_radius
c = Circle(
(x, y),
radius*self.scale,
ec='k',
fc='k',
fill=True,
lw=self.linewidth
)
self._axes.add_patch(c)
def not_point(self, gate_idx, wire_idx):
"""Draw a NOT gates as the circle with plus in the middle."""
x = self._gate_grid[gate_idx]
y = self._wire_grid[wire_idx]
radius = self.not_radius
c = Circle(
(x, y),
radius,
ec='k',
fc='w',
fill=False,
lw=self.linewidth
)
self._axes.add_patch(c)
l = Line2D(
(x, x), (y - radius, y + radius),
color='k',
lw=self.linewidth
)
self._axes.add_line(l)
def swap_point(self, gate_idx, wire_idx):
"""Draw a swap point as a cross."""
x = self._gate_grid[gate_idx]
y = self._wire_grid[wire_idx]
d = self.swap_delta
l1 = Line2D(
(x - d, x + d),
(y - d, y + d),
color='k',
lw=self.linewidth
)
l2 = Line2D(
(x - d, x + d),
(y + d, y - d),
color='k',
lw=self.linewidth
)
self._axes.add_line(l1)
self._axes.add_line(l2)
def circuit_plot(c, nqubits, **kwargs):
"""Draw the circuit diagram for the circuit with nqubits.
Parameters
==========
c : circuit
The circuit to plot. Should be a product of Gate instances.
nqubits : int
The number of qubits to include in the circuit. Must be at least
as big as the largest ``min_qubits`` of the gates.
"""
return CircuitPlot(c, nqubits, **kwargs)
def render_label(label, inits={}):
"""Slightly more flexible way to render labels.
>>> from sympy.physics.quantum.circuitplot import render_label
>>> render_label('q0')
'$\\\\left|q0\\\\right\\\\rangle$'
>>> render_label('q0', {'q0':'0'})
'$\\\\left|q0\\\\right\\\\rangle=\\\\left|0\\\\right\\\\rangle$'
"""
init = inits.get(label)
if init:
return r'$\left|%s\right\rangle=\left|%s\right\rangle$' % (label, init)
return r'$\left|%s\right\rangle$' % label
def labeller(n, symbol='q'):
"""Autogenerate labels for wires of quantum circuits.
Parameters
==========
n : int
number of qubits in the circuit.
symbol : string
A character string to precede all gate labels. E.g. 'q_0', 'q_1', etc.
>>> from sympy.physics.quantum.circuitplot import labeller
>>> labeller(2)
['q_1', 'q_0']
>>> labeller(3,'j')
['j_2', 'j_1', 'j_0']
"""
return ['%s_%d' % (symbol,n-i-1) for i in range(n)]
class Mz(OneQubitGate):
"""Mock-up of a z measurement gate.
This is in circuitplot rather than gate.py because it's not a real
gate, it just draws one.
"""
measurement = True
gate_name='Mz'
gate_name_latex='M_z'
class Mx(OneQubitGate):
"""Mock-up of an x measurement gate.
This is in circuitplot rather than gate.py because it's not a real
gate, it just draws one.
"""
measurement = True
gate_name='Mx'
gate_name_latex='M_x'
class CreateOneQubitGate(type):
def __new__(mcl, name, latexname=None):
if not latexname:
latexname = name
return type(name + "Gate", (OneQubitGate,),
{'gate_name': name, 'gate_name_latex': latexname})
def CreateCGate(name, latexname=None):
"""Use a lexical closure to make a controlled gate.
"""
if not latexname:
latexname = name
onequbitgate = CreateOneQubitGate(name, latexname)
def ControlledGate(ctrls,target):
return CGate(tuple(ctrls),onequbitgate(target))
return ControlledGate

View File

@ -0,0 +1,488 @@
"""Primitive circuit operations on quantum circuits."""
from functools import reduce
from sympy.core.sorting import default_sort_key
from sympy.core.containers import Tuple
from sympy.core.mul import Mul
from sympy.core.symbol import Symbol
from sympy.core.sympify import sympify
from sympy.utilities import numbered_symbols
from sympy.physics.quantum.gate import Gate
__all__ = [
'kmp_table',
'find_subcircuit',
'replace_subcircuit',
'convert_to_symbolic_indices',
'convert_to_real_indices',
'random_reduce',
'random_insert'
]
def kmp_table(word):
"""Build the 'partial match' table of the Knuth-Morris-Pratt algorithm.
Note: This is applicable to strings or
quantum circuits represented as tuples.
"""
# Current position in subcircuit
pos = 2
# Beginning position of candidate substring that
# may reappear later in word
cnd = 0
# The 'partial match' table that helps one determine
# the next location to start substring search
table = []
table.append(-1)
table.append(0)
while pos < len(word):
if word[pos - 1] == word[cnd]:
cnd = cnd + 1
table.append(cnd)
pos = pos + 1
elif cnd > 0:
cnd = table[cnd]
else:
table.append(0)
pos = pos + 1
return table
def find_subcircuit(circuit, subcircuit, start=0, end=0):
"""Finds the subcircuit in circuit, if it exists.
Explanation
===========
If the subcircuit exists, the index of the start of
the subcircuit in circuit is returned; otherwise,
-1 is returned. The algorithm that is implemented
is the Knuth-Morris-Pratt algorithm.
Parameters
==========
circuit : tuple, Gate or Mul
A tuple of Gates or Mul representing a quantum circuit
subcircuit : tuple, Gate or Mul
A tuple of Gates or Mul to find in circuit
start : int
The location to start looking for subcircuit.
If start is the same or past end, -1 is returned.
end : int
The last place to look for a subcircuit. If end
is less than 1 (one), then the length of circuit
is taken to be end.
Examples
========
Find the first instance of a subcircuit:
>>> from sympy.physics.quantum.circuitutils import find_subcircuit
>>> from sympy.physics.quantum.gate import X, Y, Z, H
>>> circuit = X(0)*Z(0)*Y(0)*H(0)
>>> subcircuit = Z(0)*Y(0)
>>> find_subcircuit(circuit, subcircuit)
1
Find the first instance starting at a specific position:
>>> find_subcircuit(circuit, subcircuit, start=1)
1
>>> find_subcircuit(circuit, subcircuit, start=2)
-1
>>> circuit = circuit*subcircuit
>>> find_subcircuit(circuit, subcircuit, start=2)
4
Find the subcircuit within some interval:
>>> find_subcircuit(circuit, subcircuit, start=2, end=2)
-1
"""
if isinstance(circuit, Mul):
circuit = circuit.args
if isinstance(subcircuit, Mul):
subcircuit = subcircuit.args
if len(subcircuit) == 0 or len(subcircuit) > len(circuit):
return -1
if end < 1:
end = len(circuit)
# Location in circuit
pos = start
# Location in the subcircuit
index = 0
# 'Partial match' table
table = kmp_table(subcircuit)
while (pos + index) < end:
if subcircuit[index] == circuit[pos + index]:
index = index + 1
else:
pos = pos + index - table[index]
index = table[index] if table[index] > -1 else 0
if index == len(subcircuit):
return pos
return -1
def replace_subcircuit(circuit, subcircuit, replace=None, pos=0):
"""Replaces a subcircuit with another subcircuit in circuit,
if it exists.
Explanation
===========
If multiple instances of subcircuit exists, the first instance is
replaced. The position to being searching from (if different from
0) may be optionally given. If subcircuit cannot be found, circuit
is returned.
Parameters
==========
circuit : tuple, Gate or Mul
A quantum circuit.
subcircuit : tuple, Gate or Mul
The circuit to be replaced.
replace : tuple, Gate or Mul
The replacement circuit.
pos : int
The location to start search and replace
subcircuit, if it exists. This may be used
if it is known beforehand that multiple
instances exist, and it is desirable to
replace a specific instance. If a negative number
is given, pos will be defaulted to 0.
Examples
========
Find and remove the subcircuit:
>>> from sympy.physics.quantum.circuitutils import replace_subcircuit
>>> from sympy.physics.quantum.gate import X, Y, Z, H
>>> circuit = X(0)*Z(0)*Y(0)*H(0)*X(0)*H(0)*Y(0)
>>> subcircuit = Z(0)*Y(0)
>>> replace_subcircuit(circuit, subcircuit)
(X(0), H(0), X(0), H(0), Y(0))
Remove the subcircuit given a starting search point:
>>> replace_subcircuit(circuit, subcircuit, pos=1)
(X(0), H(0), X(0), H(0), Y(0))
>>> replace_subcircuit(circuit, subcircuit, pos=2)
(X(0), Z(0), Y(0), H(0), X(0), H(0), Y(0))
Replace the subcircuit:
>>> replacement = H(0)*Z(0)
>>> replace_subcircuit(circuit, subcircuit, replace=replacement)
(X(0), H(0), Z(0), H(0), X(0), H(0), Y(0))
"""
if pos < 0:
pos = 0
if isinstance(circuit, Mul):
circuit = circuit.args
if isinstance(subcircuit, Mul):
subcircuit = subcircuit.args
if isinstance(replace, Mul):
replace = replace.args
elif replace is None:
replace = ()
# Look for the subcircuit starting at pos
loc = find_subcircuit(circuit, subcircuit, start=pos)
# If subcircuit was found
if loc > -1:
# Get the gates to the left of subcircuit
left = circuit[0:loc]
# Get the gates to the right of subcircuit
right = circuit[loc + len(subcircuit):len(circuit)]
# Recombine the left and right side gates into a circuit
circuit = left + replace + right
return circuit
def _sympify_qubit_map(mapping):
new_map = {}
for key in mapping:
new_map[key] = sympify(mapping[key])
return new_map
def convert_to_symbolic_indices(seq, start=None, gen=None, qubit_map=None):
"""Returns the circuit with symbolic indices and the
dictionary mapping symbolic indices to real indices.
The mapping is 1 to 1 and onto (bijective).
Parameters
==========
seq : tuple, Gate/Integer/tuple or Mul
A tuple of Gate, Integer, or tuple objects, or a Mul
start : Symbol
An optional starting symbolic index
gen : object
An optional numbered symbol generator
qubit_map : dict
An existing mapping of symbolic indices to real indices
All symbolic indices have the format 'i#', where # is
some number >= 0.
"""
if isinstance(seq, Mul):
seq = seq.args
# A numbered symbol generator
index_gen = numbered_symbols(prefix='i', start=-1)
cur_ndx = next(index_gen)
# keys are symbolic indices; values are real indices
ndx_map = {}
def create_inverse_map(symb_to_real_map):
rev_items = lambda item: (item[1], item[0])
return dict(map(rev_items, symb_to_real_map.items()))
if start is not None:
if not isinstance(start, Symbol):
msg = 'Expected Symbol for starting index, got %r.' % start
raise TypeError(msg)
cur_ndx = start
if gen is not None:
if not isinstance(gen, numbered_symbols().__class__):
msg = 'Expected a generator, got %r.' % gen
raise TypeError(msg)
index_gen = gen
if qubit_map is not None:
if not isinstance(qubit_map, dict):
msg = ('Expected dict for existing map, got ' +
'%r.' % qubit_map)
raise TypeError(msg)
ndx_map = qubit_map
ndx_map = _sympify_qubit_map(ndx_map)
# keys are real indices; keys are symbolic indices
inv_map = create_inverse_map(ndx_map)
sym_seq = ()
for item in seq:
# Nested items, so recurse
if isinstance(item, Gate):
result = convert_to_symbolic_indices(item.args,
qubit_map=ndx_map,
start=cur_ndx,
gen=index_gen)
sym_item, new_map, cur_ndx, index_gen = result
ndx_map.update(new_map)
inv_map = create_inverse_map(ndx_map)
elif isinstance(item, (tuple, Tuple)):
result = convert_to_symbolic_indices(item,
qubit_map=ndx_map,
start=cur_ndx,
gen=index_gen)
sym_item, new_map, cur_ndx, index_gen = result
ndx_map.update(new_map)
inv_map = create_inverse_map(ndx_map)
elif item in inv_map:
sym_item = inv_map[item]
else:
cur_ndx = next(gen)
ndx_map[cur_ndx] = item
inv_map[item] = cur_ndx
sym_item = cur_ndx
if isinstance(item, Gate):
sym_item = item.__class__(*sym_item)
sym_seq = sym_seq + (sym_item,)
return sym_seq, ndx_map, cur_ndx, index_gen
def convert_to_real_indices(seq, qubit_map):
"""Returns the circuit with real indices.
Parameters
==========
seq : tuple, Gate/Integer/tuple or Mul
A tuple of Gate, Integer, or tuple objects or a Mul
qubit_map : dict
A dictionary mapping symbolic indices to real indices.
Examples
========
Change the symbolic indices to real integers:
>>> from sympy import symbols
>>> from sympy.physics.quantum.circuitutils import convert_to_real_indices
>>> from sympy.physics.quantum.gate import X, Y, H
>>> i0, i1 = symbols('i:2')
>>> index_map = {i0 : 0, i1 : 1}
>>> convert_to_real_indices(X(i0)*Y(i1)*H(i0)*X(i1), index_map)
(X(0), Y(1), H(0), X(1))
"""
if isinstance(seq, Mul):
seq = seq.args
if not isinstance(qubit_map, dict):
msg = 'Expected dict for qubit_map, got %r.' % qubit_map
raise TypeError(msg)
qubit_map = _sympify_qubit_map(qubit_map)
real_seq = ()
for item in seq:
# Nested items, so recurse
if isinstance(item, Gate):
real_item = convert_to_real_indices(item.args, qubit_map)
elif isinstance(item, (tuple, Tuple)):
real_item = convert_to_real_indices(item, qubit_map)
else:
real_item = qubit_map[item]
if isinstance(item, Gate):
real_item = item.__class__(*real_item)
real_seq = real_seq + (real_item,)
return real_seq
def random_reduce(circuit, gate_ids, seed=None):
"""Shorten the length of a quantum circuit.
Explanation
===========
random_reduce looks for circuit identities in circuit, randomly chooses
one to remove, and returns a shorter yet equivalent circuit. If no
identities are found, the same circuit is returned.
Parameters
==========
circuit : Gate tuple of Mul
A tuple of Gates representing a quantum circuit
gate_ids : list, GateIdentity
List of gate identities to find in circuit
seed : int or list
seed used for _randrange; to override the random selection, provide a
list of integers: the elements of gate_ids will be tested in the order
given by the list
"""
from sympy.core.random import _randrange
if not gate_ids:
return circuit
if isinstance(circuit, Mul):
circuit = circuit.args
ids = flatten_ids(gate_ids)
# Create the random integer generator with the seed
randrange = _randrange(seed)
# Look for an identity in the circuit
while ids:
i = randrange(len(ids))
id = ids.pop(i)
if find_subcircuit(circuit, id) != -1:
break
else:
# no identity was found
return circuit
# return circuit with the identity removed
return replace_subcircuit(circuit, id)
def random_insert(circuit, choices, seed=None):
"""Insert a circuit into another quantum circuit.
Explanation
===========
random_insert randomly chooses a location in the circuit to insert
a randomly selected circuit from amongst the given choices.
Parameters
==========
circuit : Gate tuple or Mul
A tuple or Mul of Gates representing a quantum circuit
choices : list
Set of circuit choices
seed : int or list
seed used for _randrange; to override the random selections, give
a list two integers, [i, j] where i is the circuit location where
choice[j] will be inserted.
Notes
=====
Indices for insertion should be [0, n] if n is the length of the
circuit.
"""
from sympy.core.random import _randrange
if not choices:
return circuit
if isinstance(circuit, Mul):
circuit = circuit.args
# get the location in the circuit and the element to insert from choices
randrange = _randrange(seed)
loc = randrange(len(circuit) + 1)
choice = choices[randrange(len(choices))]
circuit = list(circuit)
circuit[loc: loc] = choice
return tuple(circuit)
# Flatten the GateIdentity objects (with gate rules) into one single list
def flatten_ids(ids):
collapse = lambda acc, an_id: acc + sorted(an_id.equivalent_ids,
key=default_sort_key)
ids = reduce(collapse, ids, [])
ids.sort(key=default_sort_key)
return ids

View File

@ -0,0 +1,239 @@
"""The commutator: [A,B] = A*B - B*A."""
from sympy.core.add import Add
from sympy.core.expr import Expr
from sympy.core.mul import Mul
from sympy.core.power import Pow
from sympy.core.singleton import S
from sympy.printing.pretty.stringpict import prettyForm
from sympy.physics.quantum.dagger import Dagger
from sympy.physics.quantum.operator import Operator
__all__ = [
'Commutator'
]
#-----------------------------------------------------------------------------
# Commutator
#-----------------------------------------------------------------------------
class Commutator(Expr):
"""The standard commutator, in an unevaluated state.
Explanation
===========
Evaluating a commutator is defined [1]_ as: ``[A, B] = A*B - B*A``. This
class returns the commutator in an unevaluated form. To evaluate the
commutator, use the ``.doit()`` method.
Canonical ordering of a commutator is ``[A, B]`` for ``A < B``. The
arguments of the commutator are put into canonical order using ``__cmp__``.
If ``B < A``, then ``[B, A]`` is returned as ``-[A, B]``.
Parameters
==========
A : Expr
The first argument of the commutator [A,B].
B : Expr
The second argument of the commutator [A,B].
Examples
========
>>> from sympy.physics.quantum import Commutator, Dagger, Operator
>>> from sympy.abc import x, y
>>> A = Operator('A')
>>> B = Operator('B')
>>> C = Operator('C')
Create a commutator and use ``.doit()`` to evaluate it:
>>> comm = Commutator(A, B)
>>> comm
[A,B]
>>> comm.doit()
A*B - B*A
The commutator orders it arguments in canonical order:
>>> comm = Commutator(B, A); comm
-[A,B]
Commutative constants are factored out:
>>> Commutator(3*x*A, x*y*B)
3*x**2*y*[A,B]
Using ``.expand(commutator=True)``, the standard commutator expansion rules
can be applied:
>>> Commutator(A+B, C).expand(commutator=True)
[A,C] + [B,C]
>>> Commutator(A, B+C).expand(commutator=True)
[A,B] + [A,C]
>>> Commutator(A*B, C).expand(commutator=True)
[A,C]*B + A*[B,C]
>>> Commutator(A, B*C).expand(commutator=True)
[A,B]*C + B*[A,C]
Adjoint operations applied to the commutator are properly applied to the
arguments:
>>> Dagger(Commutator(A, B))
-[Dagger(A),Dagger(B)]
References
==========
.. [1] https://en.wikipedia.org/wiki/Commutator
"""
is_commutative = False
def __new__(cls, A, B):
r = cls.eval(A, B)
if r is not None:
return r
obj = Expr.__new__(cls, A, B)
return obj
@classmethod
def eval(cls, a, b):
if not (a and b):
return S.Zero
if a == b:
return S.Zero
if a.is_commutative or b.is_commutative:
return S.Zero
# [xA,yB] -> xy*[A,B]
ca, nca = a.args_cnc()
cb, ncb = b.args_cnc()
c_part = ca + cb
if c_part:
return Mul(Mul(*c_part), cls(Mul._from_args(nca), Mul._from_args(ncb)))
# Canonical ordering of arguments
# The Commutator [A, B] is in canonical form if A < B.
if a.compare(b) == 1:
return S.NegativeOne*cls(b, a)
def _expand_pow(self, A, B, sign):
exp = A.exp
if not exp.is_integer or not exp.is_constant() or abs(exp) <= 1:
# nothing to do
return self
base = A.base
if exp.is_negative:
base = A.base**-1
exp = -exp
comm = Commutator(base, B).expand(commutator=True)
result = base**(exp - 1) * comm
for i in range(1, exp):
result += base**(exp - 1 - i) * comm * base**i
return sign*result.expand()
def _eval_expand_commutator(self, **hints):
A = self.args[0]
B = self.args[1]
if isinstance(A, Add):
# [A + B, C] -> [A, C] + [B, C]
sargs = []
for term in A.args:
comm = Commutator(term, B)
if isinstance(comm, Commutator):
comm = comm._eval_expand_commutator()
sargs.append(comm)
return Add(*sargs)
elif isinstance(B, Add):
# [A, B + C] -> [A, B] + [A, C]
sargs = []
for term in B.args:
comm = Commutator(A, term)
if isinstance(comm, Commutator):
comm = comm._eval_expand_commutator()
sargs.append(comm)
return Add(*sargs)
elif isinstance(A, Mul):
# [A*B, C] -> A*[B, C] + [A, C]*B
a = A.args[0]
b = Mul(*A.args[1:])
c = B
comm1 = Commutator(b, c)
comm2 = Commutator(a, c)
if isinstance(comm1, Commutator):
comm1 = comm1._eval_expand_commutator()
if isinstance(comm2, Commutator):
comm2 = comm2._eval_expand_commutator()
first = Mul(a, comm1)
second = Mul(comm2, b)
return Add(first, second)
elif isinstance(B, Mul):
# [A, B*C] -> [A, B]*C + B*[A, C]
a = A
b = B.args[0]
c = Mul(*B.args[1:])
comm1 = Commutator(a, b)
comm2 = Commutator(a, c)
if isinstance(comm1, Commutator):
comm1 = comm1._eval_expand_commutator()
if isinstance(comm2, Commutator):
comm2 = comm2._eval_expand_commutator()
first = Mul(comm1, c)
second = Mul(b, comm2)
return Add(first, second)
elif isinstance(A, Pow):
# [A**n, C] -> A**(n - 1)*[A, C] + A**(n - 2)*[A, C]*A + ... + [A, C]*A**(n-1)
return self._expand_pow(A, B, 1)
elif isinstance(B, Pow):
# [A, C**n] -> C**(n - 1)*[C, A] + C**(n - 2)*[C, A]*C + ... + [C, A]*C**(n-1)
return self._expand_pow(B, A, -1)
# No changes, so return self
return self
def doit(self, **hints):
""" Evaluate commutator """
A = self.args[0]
B = self.args[1]
if isinstance(A, Operator) and isinstance(B, Operator):
try:
comm = A._eval_commutator(B, **hints)
except NotImplementedError:
try:
comm = -1*B._eval_commutator(A, **hints)
except NotImplementedError:
comm = None
if comm is not None:
return comm.doit(**hints)
return (A*B - B*A).doit(**hints)
def _eval_adjoint(self):
return Commutator(Dagger(self.args[1]), Dagger(self.args[0]))
def _sympyrepr(self, printer, *args):
return "%s(%s,%s)" % (
self.__class__.__name__, printer._print(
self.args[0]), printer._print(self.args[1])
)
def _sympystr(self, printer, *args):
return "[%s,%s]" % (
printer._print(self.args[0]), printer._print(self.args[1]))
def _pretty(self, printer, *args):
pform = printer._print(self.args[0], *args)
pform = prettyForm(*pform.right(prettyForm(',')))
pform = prettyForm(*pform.right(printer._print(self.args[1], *args)))
pform = prettyForm(*pform.parens(left='[', right=']'))
return pform
def _latex(self, printer, *args):
return "\\left[%s,%s\\right]" % tuple([
printer._print(arg, *args) for arg in self.args])

View File

@ -0,0 +1,59 @@
"""Constants (like hbar) related to quantum mechanics."""
from sympy.core.numbers import NumberSymbol
from sympy.core.singleton import Singleton
from sympy.printing.pretty.stringpict import prettyForm
import mpmath.libmp as mlib
#-----------------------------------------------------------------------------
# Constants
#-----------------------------------------------------------------------------
__all__ = [
'hbar',
'HBar',
]
class HBar(NumberSymbol, metaclass=Singleton):
"""Reduced Plank's constant in numerical and symbolic form [1]_.
Examples
========
>>> from sympy.physics.quantum.constants import hbar
>>> hbar.evalf()
1.05457162000000e-34
References
==========
.. [1] https://en.wikipedia.org/wiki/Planck_constant
"""
is_real = True
is_positive = True
is_negative = False
is_irrational = True
__slots__ = ()
def _as_mpf_val(self, prec):
return mlib.from_float(1.05457162e-34, prec)
def _sympyrepr(self, printer, *args):
return 'HBar()'
def _sympystr(self, printer, *args):
return 'hbar'
def _pretty(self, printer, *args):
if printer._use_unicode:
return prettyForm('\N{PLANCK CONSTANT OVER TWO PI}')
return prettyForm('hbar')
def _latex(self, printer, *args):
return r'\hbar'
# Create an instance for everyone to use.
hbar = HBar()

View File

@ -0,0 +1,97 @@
"""Hermitian conjugation."""
from sympy.core import Expr, Mul, sympify
from sympy.functions.elementary.complexes import adjoint
__all__ = [
'Dagger'
]
class Dagger(adjoint):
"""General Hermitian conjugate operation.
Explanation
===========
Take the Hermetian conjugate of an argument [1]_. For matrices this
operation is equivalent to transpose and complex conjugate [2]_.
Parameters
==========
arg : Expr
The SymPy expression that we want to take the dagger of.
evaluate : bool
Whether the resulting expression should be directly evaluated.
Examples
========
Daggering various quantum objects:
>>> from sympy.physics.quantum.dagger import Dagger
>>> from sympy.physics.quantum.state import Ket, Bra
>>> from sympy.physics.quantum.operator import Operator
>>> Dagger(Ket('psi'))
<psi|
>>> Dagger(Bra('phi'))
|phi>
>>> Dagger(Operator('A'))
Dagger(A)
Inner and outer products::
>>> from sympy.physics.quantum import InnerProduct, OuterProduct
>>> Dagger(InnerProduct(Bra('a'), Ket('b')))
<b|a>
>>> Dagger(OuterProduct(Ket('a'), Bra('b')))
|b><a|
Powers, sums and products::
>>> A = Operator('A')
>>> B = Operator('B')
>>> Dagger(A*B)
Dagger(B)*Dagger(A)
>>> Dagger(A+B)
Dagger(A) + Dagger(B)
>>> Dagger(A**2)
Dagger(A)**2
Dagger also seamlessly handles complex numbers and matrices::
>>> from sympy import Matrix, I
>>> m = Matrix([[1,I],[2,I]])
>>> m
Matrix([
[1, I],
[2, I]])
>>> Dagger(m)
Matrix([
[ 1, 2],
[-I, -I]])
References
==========
.. [1] https://en.wikipedia.org/wiki/Hermitian_adjoint
.. [2] https://en.wikipedia.org/wiki/Hermitian_transpose
"""
def __new__(cls, arg, evaluate=True):
if hasattr(arg, 'adjoint') and evaluate:
return arg.adjoint()
elif hasattr(arg, 'conjugate') and hasattr(arg, 'transpose') and evaluate:
return arg.conjugate().transpose()
return Expr.__new__(cls, sympify(arg))
def __mul__(self, other):
from sympy.physics.quantum import IdentityOperator
if isinstance(other, IdentityOperator):
return self
return Mul(self, other)
adjoint.__name__ = "Dagger"
adjoint._sympyrepr = lambda a, b: "Dagger(%s)" % b._print(a.args[0])

View File

@ -0,0 +1,319 @@
from itertools import product
from sympy.core.add import Add
from sympy.core.containers import Tuple
from sympy.core.function import expand
from sympy.core.mul import Mul
from sympy.core.singleton import S
from sympy.functions.elementary.exponential import log
from sympy.matrices.dense import MutableDenseMatrix as Matrix
from sympy.printing.pretty.stringpict import prettyForm
from sympy.physics.quantum.dagger import Dagger
from sympy.physics.quantum.operator import HermitianOperator
from sympy.physics.quantum.represent import represent
from sympy.physics.quantum.matrixutils import numpy_ndarray, scipy_sparse_matrix, to_numpy
from sympy.physics.quantum.tensorproduct import TensorProduct, tensor_product_simp
from sympy.physics.quantum.trace import Tr
class Density(HermitianOperator):
"""Density operator for representing mixed states.
TODO: Density operator support for Qubits
Parameters
==========
values : tuples/lists
Each tuple/list should be of form (state, prob) or [state,prob]
Examples
========
Create a density operator with 2 states represented by Kets.
>>> from sympy.physics.quantum.state import Ket
>>> from sympy.physics.quantum.density import Density
>>> d = Density([Ket(0), 0.5], [Ket(1),0.5])
>>> d
Density((|0>, 0.5),(|1>, 0.5))
"""
@classmethod
def _eval_args(cls, args):
# call this to qsympify the args
args = super()._eval_args(args)
for arg in args:
# Check if arg is a tuple
if not (isinstance(arg, Tuple) and len(arg) == 2):
raise ValueError("Each argument should be of form [state,prob]"
" or ( state, prob )")
return args
def states(self):
"""Return list of all states.
Examples
========
>>> from sympy.physics.quantum.state import Ket
>>> from sympy.physics.quantum.density import Density
>>> d = Density([Ket(0), 0.5], [Ket(1),0.5])
>>> d.states()
(|0>, |1>)
"""
return Tuple(*[arg[0] for arg in self.args])
def probs(self):
"""Return list of all probabilities.
Examples
========
>>> from sympy.physics.quantum.state import Ket
>>> from sympy.physics.quantum.density import Density
>>> d = Density([Ket(0), 0.5], [Ket(1),0.5])
>>> d.probs()
(0.5, 0.5)
"""
return Tuple(*[arg[1] for arg in self.args])
def get_state(self, index):
"""Return specific state by index.
Parameters
==========
index : index of state to be returned
Examples
========
>>> from sympy.physics.quantum.state import Ket
>>> from sympy.physics.quantum.density import Density
>>> d = Density([Ket(0), 0.5], [Ket(1),0.5])
>>> d.states()[1]
|1>
"""
state = self.args[index][0]
return state
def get_prob(self, index):
"""Return probability of specific state by index.
Parameters
===========
index : index of states whose probability is returned.
Examples
========
>>> from sympy.physics.quantum.state import Ket
>>> from sympy.physics.quantum.density import Density
>>> d = Density([Ket(0), 0.5], [Ket(1),0.5])
>>> d.probs()[1]
0.500000000000000
"""
prob = self.args[index][1]
return prob
def apply_op(self, op):
"""op will operate on each individual state.
Parameters
==========
op : Operator
Examples
========
>>> from sympy.physics.quantum.state import Ket
>>> from sympy.physics.quantum.density import Density
>>> from sympy.physics.quantum.operator import Operator
>>> A = Operator('A')
>>> d = Density([Ket(0), 0.5], [Ket(1),0.5])
>>> d.apply_op(A)
Density((A*|0>, 0.5),(A*|1>, 0.5))
"""
new_args = [(op*state, prob) for (state, prob) in self.args]
return Density(*new_args)
def doit(self, **hints):
"""Expand the density operator into an outer product format.
Examples
========
>>> from sympy.physics.quantum.state import Ket
>>> from sympy.physics.quantum.density import Density
>>> from sympy.physics.quantum.operator import Operator
>>> A = Operator('A')
>>> d = Density([Ket(0), 0.5], [Ket(1),0.5])
>>> d.doit()
0.5*|0><0| + 0.5*|1><1|
"""
terms = []
for (state, prob) in self.args:
state = state.expand() # needed to break up (a+b)*c
if (isinstance(state, Add)):
for arg in product(state.args, repeat=2):
terms.append(prob*self._generate_outer_prod(arg[0],
arg[1]))
else:
terms.append(prob*self._generate_outer_prod(state, state))
return Add(*terms)
def _generate_outer_prod(self, arg1, arg2):
c_part1, nc_part1 = arg1.args_cnc()
c_part2, nc_part2 = arg2.args_cnc()
if (len(nc_part1) == 0 or len(nc_part2) == 0):
raise ValueError('Atleast one-pair of'
' Non-commutative instance required'
' for outer product.')
# Muls of Tensor Products should be expanded
# before this function is called
if (isinstance(nc_part1[0], TensorProduct) and len(nc_part1) == 1
and len(nc_part2) == 1):
op = tensor_product_simp(nc_part1[0]*Dagger(nc_part2[0]))
else:
op = Mul(*nc_part1)*Dagger(Mul(*nc_part2))
return Mul(*c_part1)*Mul(*c_part2) * op
def _represent(self, **options):
return represent(self.doit(), **options)
def _print_operator_name_latex(self, printer, *args):
return r'\rho'
def _print_operator_name_pretty(self, printer, *args):
return prettyForm('\N{GREEK SMALL LETTER RHO}')
def _eval_trace(self, **kwargs):
indices = kwargs.get('indices', [])
return Tr(self.doit(), indices).doit()
def entropy(self):
""" Compute the entropy of a density matrix.
Refer to density.entropy() method for examples.
"""
return entropy(self)
def entropy(density):
"""Compute the entropy of a matrix/density object.
This computes -Tr(density*ln(density)) using the eigenvalue decomposition
of density, which is given as either a Density instance or a matrix
(numpy.ndarray, sympy.Matrix or scipy.sparse).
Parameters
==========
density : density matrix of type Density, SymPy matrix,
scipy.sparse or numpy.ndarray
Examples
========
>>> from sympy.physics.quantum.density import Density, entropy
>>> from sympy.physics.quantum.spin import JzKet
>>> from sympy import S
>>> up = JzKet(S(1)/2,S(1)/2)
>>> down = JzKet(S(1)/2,-S(1)/2)
>>> d = Density((up,S(1)/2),(down,S(1)/2))
>>> entropy(d)
log(2)/2
"""
if isinstance(density, Density):
density = represent(density) # represent in Matrix
if isinstance(density, scipy_sparse_matrix):
density = to_numpy(density)
if isinstance(density, Matrix):
eigvals = density.eigenvals().keys()
return expand(-sum(e*log(e) for e in eigvals))
elif isinstance(density, numpy_ndarray):
import numpy as np
eigvals = np.linalg.eigvals(density)
return -np.sum(eigvals*np.log(eigvals))
else:
raise ValueError(
"numpy.ndarray, scipy.sparse or SymPy matrix expected")
def fidelity(state1, state2):
""" Computes the fidelity [1]_ between two quantum states
The arguments provided to this function should be a square matrix or a
Density object. If it is a square matrix, it is assumed to be diagonalizable.
Parameters
==========
state1, state2 : a density matrix or Matrix
Examples
========
>>> from sympy import S, sqrt
>>> from sympy.physics.quantum.dagger import Dagger
>>> from sympy.physics.quantum.spin import JzKet
>>> from sympy.physics.quantum.density import fidelity
>>> from sympy.physics.quantum.represent import represent
>>>
>>> up = JzKet(S(1)/2,S(1)/2)
>>> down = JzKet(S(1)/2,-S(1)/2)
>>> amp = 1/sqrt(2)
>>> updown = (amp*up) + (amp*down)
>>>
>>> # represent turns Kets into matrices
>>> up_dm = represent(up*Dagger(up))
>>> down_dm = represent(down*Dagger(down))
>>> updown_dm = represent(updown*Dagger(updown))
>>>
>>> fidelity(up_dm, up_dm)
1
>>> fidelity(up_dm, down_dm) #orthogonal states
0
>>> fidelity(up_dm, updown_dm).evalf().round(3)
0.707
References
==========
.. [1] https://en.wikipedia.org/wiki/Fidelity_of_quantum_states
"""
state1 = represent(state1) if isinstance(state1, Density) else state1
state2 = represent(state2) if isinstance(state2, Density) else state2
if not isinstance(state1, Matrix) or not isinstance(state2, Matrix):
raise ValueError("state1 and state2 must be of type Density or Matrix "
"received type=%s for state1 and type=%s for state2" %
(type(state1), type(state2)))
if state1.shape != state2.shape and state1.is_square:
raise ValueError("The dimensions of both args should be equal and the "
"matrix obtained should be a square matrix")
sqrt_state1 = state1**S.Half
return Tr((sqrt_state1*state2*sqrt_state1)**S.Half).doit()

View File

@ -0,0 +1,191 @@
"""Fermionic quantum operators."""
from sympy.core.numbers import Integer
from sympy.core.singleton import S
from sympy.physics.quantum import Operator
from sympy.physics.quantum import HilbertSpace, Ket, Bra
from sympy.functions.special.tensor_functions import KroneckerDelta
__all__ = [
'FermionOp',
'FermionFockKet',
'FermionFockBra'
]
class FermionOp(Operator):
"""A fermionic operator that satisfies {c, Dagger(c)} == 1.
Parameters
==========
name : str
A string that labels the fermionic mode.
annihilation : bool
A bool that indicates if the fermionic operator is an annihilation
(True, default value) or creation operator (False)
Examples
========
>>> from sympy.physics.quantum import Dagger, AntiCommutator
>>> from sympy.physics.quantum.fermion import FermionOp
>>> c = FermionOp("c")
>>> AntiCommutator(c, Dagger(c)).doit()
1
"""
@property
def name(self):
return self.args[0]
@property
def is_annihilation(self):
return bool(self.args[1])
@classmethod
def default_args(self):
return ("c", True)
def __new__(cls, *args, **hints):
if not len(args) in [1, 2]:
raise ValueError('1 or 2 parameters expected, got %s' % args)
if len(args) == 1:
args = (args[0], S.One)
if len(args) == 2:
args = (args[0], Integer(args[1]))
return Operator.__new__(cls, *args)
def _eval_commutator_FermionOp(self, other, **hints):
if 'independent' in hints and hints['independent']:
# [c, d] = 0
return S.Zero
return None
def _eval_anticommutator_FermionOp(self, other, **hints):
if self.name == other.name:
# {a^\dagger, a} = 1
if not self.is_annihilation and other.is_annihilation:
return S.One
elif 'independent' in hints and hints['independent']:
# {c, d} = 2 * c * d, because [c, d] = 0 for independent operators
return 2 * self * other
return None
def _eval_anticommutator_BosonOp(self, other, **hints):
# because fermions and bosons commute
return 2 * self * other
def _eval_commutator_BosonOp(self, other, **hints):
return S.Zero
def _eval_adjoint(self):
return FermionOp(str(self.name), not self.is_annihilation)
def _print_contents_latex(self, printer, *args):
if self.is_annihilation:
return r'{%s}' % str(self.name)
else:
return r'{{%s}^\dagger}' % str(self.name)
def _print_contents(self, printer, *args):
if self.is_annihilation:
return r'%s' % str(self.name)
else:
return r'Dagger(%s)' % str(self.name)
def _print_contents_pretty(self, printer, *args):
from sympy.printing.pretty.stringpict import prettyForm
pform = printer._print(self.args[0], *args)
if self.is_annihilation:
return pform
else:
return pform**prettyForm('\N{DAGGER}')
def _eval_power(self, exp):
from sympy.core.singleton import S
if exp == 0:
return S.One
elif exp == 1:
return self
elif (exp > 1) == True and exp.is_integer == True:
return S.Zero
elif (exp < 0) == True or exp.is_integer == False:
raise ValueError("Fermionic operators can only be raised to a"
" positive integer power")
return Operator._eval_power(self, exp)
class FermionFockKet(Ket):
"""Fock state ket for a fermionic mode.
Parameters
==========
n : Number
The Fock state number.
"""
def __new__(cls, n):
if n not in (0, 1):
raise ValueError("n must be 0 or 1")
return Ket.__new__(cls, n)
@property
def n(self):
return self.label[0]
@classmethod
def dual_class(self):
return FermionFockBra
@classmethod
def _eval_hilbert_space(cls, label):
return HilbertSpace()
def _eval_innerproduct_FermionFockBra(self, bra, **hints):
return KroneckerDelta(self.n, bra.n)
def _apply_from_right_to_FermionOp(self, op, **options):
if op.is_annihilation:
if self.n == 1:
return FermionFockKet(0)
else:
return S.Zero
else:
if self.n == 0:
return FermionFockKet(1)
else:
return S.Zero
class FermionFockBra(Bra):
"""Fock state bra for a fermionic mode.
Parameters
==========
n : Number
The Fock state number.
"""
def __new__(cls, n):
if n not in (0, 1):
raise ValueError("n must be 0 or 1")
return Bra.__new__(cls, n)
@property
def n(self):
return self.label[0]
@classmethod
def dual_class(self):
return FermionFockKet

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,345 @@
"""Grover's algorithm and helper functions.
Todo:
* W gate construction (or perhaps -W gate based on Mermin's book)
* Generalize the algorithm for an unknown function that returns 1 on multiple
qubit states, not just one.
* Implement _represent_ZGate in OracleGate
"""
from sympy.core.numbers import pi
from sympy.core.sympify import sympify
from sympy.core.basic import Atom
from sympy.functions.elementary.integers import floor
from sympy.functions.elementary.miscellaneous import sqrt
from sympy.matrices.dense import eye
from sympy.core.numbers import NegativeOne
from sympy.physics.quantum.qapply import qapply
from sympy.physics.quantum.qexpr import QuantumError
from sympy.physics.quantum.hilbert import ComplexSpace
from sympy.physics.quantum.operator import UnitaryOperator
from sympy.physics.quantum.gate import Gate
from sympy.physics.quantum.qubit import IntQubit
__all__ = [
'OracleGate',
'WGate',
'superposition_basis',
'grover_iteration',
'apply_grover'
]
def superposition_basis(nqubits):
"""Creates an equal superposition of the computational basis.
Parameters
==========
nqubits : int
The number of qubits.
Returns
=======
state : Qubit
An equal superposition of the computational basis with nqubits.
Examples
========
Create an equal superposition of 2 qubits::
>>> from sympy.physics.quantum.grover import superposition_basis
>>> superposition_basis(2)
|0>/2 + |1>/2 + |2>/2 + |3>/2
"""
amp = 1/sqrt(2**nqubits)
return sum(amp*IntQubit(n, nqubits=nqubits) for n in range(2**nqubits))
class OracleGateFunction(Atom):
"""Wrapper for python functions used in `OracleGate`s"""
def __new__(cls, function):
if not callable(function):
raise TypeError('Callable expected, got: %r' % function)
obj = Atom.__new__(cls)
obj.function = function
return obj
def _hashable_content(self):
return type(self), self.function
def __call__(self, *args):
return self.function(*args)
class OracleGate(Gate):
"""A black box gate.
The gate marks the desired qubits of an unknown function by flipping
the sign of the qubits. The unknown function returns true when it
finds its desired qubits and false otherwise.
Parameters
==========
qubits : int
Number of qubits.
oracle : callable
A callable function that returns a boolean on a computational basis.
Examples
========
Apply an Oracle gate that flips the sign of ``|2>`` on different qubits::
>>> from sympy.physics.quantum.qubit import IntQubit
>>> from sympy.physics.quantum.qapply import qapply
>>> from sympy.physics.quantum.grover import OracleGate
>>> f = lambda qubits: qubits == IntQubit(2)
>>> v = OracleGate(2, f)
>>> qapply(v*IntQubit(2))
-|2>
>>> qapply(v*IntQubit(3))
|3>
"""
gate_name = 'V'
gate_name_latex = 'V'
#-------------------------------------------------------------------------
# Initialization/creation
#-------------------------------------------------------------------------
@classmethod
def _eval_args(cls, args):
if len(args) != 2:
raise QuantumError(
'Insufficient/excessive arguments to Oracle. Please ' +
'supply the number of qubits and an unknown function.'
)
sub_args = (args[0],)
sub_args = UnitaryOperator._eval_args(sub_args)
if not sub_args[0].is_Integer:
raise TypeError('Integer expected, got: %r' % sub_args[0])
function = args[1]
if not isinstance(function, OracleGateFunction):
function = OracleGateFunction(function)
return (sub_args[0], function)
@classmethod
def _eval_hilbert_space(cls, args):
"""This returns the smallest possible Hilbert space."""
return ComplexSpace(2)**args[0]
#-------------------------------------------------------------------------
# Properties
#-------------------------------------------------------------------------
@property
def search_function(self):
"""The unknown function that helps find the sought after qubits."""
return self.label[1]
@property
def targets(self):
"""A tuple of target qubits."""
return sympify(tuple(range(self.args[0])))
#-------------------------------------------------------------------------
# Apply
#-------------------------------------------------------------------------
def _apply_operator_Qubit(self, qubits, **options):
"""Apply this operator to a Qubit subclass.
Parameters
==========
qubits : Qubit
The qubit subclass to apply this operator to.
Returns
=======
state : Expr
The resulting quantum state.
"""
if qubits.nqubits != self.nqubits:
raise QuantumError(
'OracleGate operates on %r qubits, got: %r'
% (self.nqubits, qubits.nqubits)
)
# If function returns 1 on qubits
# return the negative of the qubits (flip the sign)
if self.search_function(qubits):
return -qubits
else:
return qubits
#-------------------------------------------------------------------------
# Represent
#-------------------------------------------------------------------------
def _represent_ZGate(self, basis, **options):
"""
Represent the OracleGate in the computational basis.
"""
nbasis = 2**self.nqubits # compute it only once
matrixOracle = eye(nbasis)
# Flip the sign given the output of the oracle function
for i in range(nbasis):
if self.search_function(IntQubit(i, nqubits=self.nqubits)):
matrixOracle[i, i] = NegativeOne()
return matrixOracle
class WGate(Gate):
"""General n qubit W Gate in Grover's algorithm.
The gate performs the operation ``2|phi><phi| - 1`` on some qubits.
``|phi> = (tensor product of n Hadamards)*(|0> with n qubits)``
Parameters
==========
nqubits : int
The number of qubits to operate on
"""
gate_name = 'W'
gate_name_latex = 'W'
@classmethod
def _eval_args(cls, args):
if len(args) != 1:
raise QuantumError(
'Insufficient/excessive arguments to W gate. Please ' +
'supply the number of qubits to operate on.'
)
args = UnitaryOperator._eval_args(args)
if not args[0].is_Integer:
raise TypeError('Integer expected, got: %r' % args[0])
return args
#-------------------------------------------------------------------------
# Properties
#-------------------------------------------------------------------------
@property
def targets(self):
return sympify(tuple(reversed(range(self.args[0]))))
#-------------------------------------------------------------------------
# Apply
#-------------------------------------------------------------------------
def _apply_operator_Qubit(self, qubits, **options):
"""
qubits: a set of qubits (Qubit)
Returns: quantum object (quantum expression - QExpr)
"""
if qubits.nqubits != self.nqubits:
raise QuantumError(
'WGate operates on %r qubits, got: %r'
% (self.nqubits, qubits.nqubits)
)
# See 'Quantum Computer Science' by David Mermin p.92 -> W|a> result
# Return (2/(sqrt(2^n)))|phi> - |a> where |a> is the current basis
# state and phi is the superposition of basis states (see function
# create_computational_basis above)
basis_states = superposition_basis(self.nqubits)
change_to_basis = (2/sqrt(2**self.nqubits))*basis_states
return change_to_basis - qubits
def grover_iteration(qstate, oracle):
"""Applies one application of the Oracle and W Gate, WV.
Parameters
==========
qstate : Qubit
A superposition of qubits.
oracle : OracleGate
The black box operator that flips the sign of the desired basis qubits.
Returns
=======
Qubit : The qubits after applying the Oracle and W gate.
Examples
========
Perform one iteration of grover's algorithm to see a phase change::
>>> from sympy.physics.quantum.qapply import qapply
>>> from sympy.physics.quantum.qubit import IntQubit
>>> from sympy.physics.quantum.grover import OracleGate
>>> from sympy.physics.quantum.grover import superposition_basis
>>> from sympy.physics.quantum.grover import grover_iteration
>>> numqubits = 2
>>> basis_states = superposition_basis(numqubits)
>>> f = lambda qubits: qubits == IntQubit(2)
>>> v = OracleGate(numqubits, f)
>>> qapply(grover_iteration(basis_states, v))
|2>
"""
wgate = WGate(oracle.nqubits)
return wgate*oracle*qstate
def apply_grover(oracle, nqubits, iterations=None):
"""Applies grover's algorithm.
Parameters
==========
oracle : callable
The unknown callable function that returns true when applied to the
desired qubits and false otherwise.
Returns
=======
state : Expr
The resulting state after Grover's algorithm has been iterated.
Examples
========
Apply grover's algorithm to an even superposition of 2 qubits::
>>> from sympy.physics.quantum.qapply import qapply
>>> from sympy.physics.quantum.qubit import IntQubit
>>> from sympy.physics.quantum.grover import apply_grover
>>> f = lambda qubits: qubits == IntQubit(2)
>>> qapply(apply_grover(f, 2))
|2>
"""
if nqubits <= 0:
raise QuantumError(
'Grover\'s algorithm needs nqubits > 0, received %r qubits'
% nqubits
)
if iterations is None:
iterations = floor(sqrt(2**nqubits)*(pi/4))
v = OracleGate(nqubits, oracle)
iterated = superposition_basis(nqubits)
for iter in range(iterations):
iterated = grover_iteration(iterated, v)
iterated = qapply(iterated)
return iterated

View File

@ -0,0 +1,653 @@
"""Hilbert spaces for quantum mechanics.
Authors:
* Brian Granger
* Matt Curry
"""
from functools import reduce
from sympy.core.basic import Basic
from sympy.core.singleton import S
from sympy.core.sympify import sympify
from sympy.sets.sets import Interval
from sympy.printing.pretty.stringpict import prettyForm
from sympy.physics.quantum.qexpr import QuantumError
__all__ = [
'HilbertSpaceError',
'HilbertSpace',
'TensorProductHilbertSpace',
'TensorPowerHilbertSpace',
'DirectSumHilbertSpace',
'ComplexSpace',
'L2',
'FockSpace'
]
#-----------------------------------------------------------------------------
# Main objects
#-----------------------------------------------------------------------------
class HilbertSpaceError(QuantumError):
pass
#-----------------------------------------------------------------------------
# Main objects
#-----------------------------------------------------------------------------
class HilbertSpace(Basic):
"""An abstract Hilbert space for quantum mechanics.
In short, a Hilbert space is an abstract vector space that is complete
with inner products defined [1]_.
Examples
========
>>> from sympy.physics.quantum.hilbert import HilbertSpace
>>> hs = HilbertSpace()
>>> hs
H
References
==========
.. [1] https://en.wikipedia.org/wiki/Hilbert_space
"""
def __new__(cls):
obj = Basic.__new__(cls)
return obj
@property
def dimension(self):
"""Return the Hilbert dimension of the space."""
raise NotImplementedError('This Hilbert space has no dimension.')
def __add__(self, other):
return DirectSumHilbertSpace(self, other)
def __radd__(self, other):
return DirectSumHilbertSpace(other, self)
def __mul__(self, other):
return TensorProductHilbertSpace(self, other)
def __rmul__(self, other):
return TensorProductHilbertSpace(other, self)
def __pow__(self, other, mod=None):
if mod is not None:
raise ValueError('The third argument to __pow__ is not supported \
for Hilbert spaces.')
return TensorPowerHilbertSpace(self, other)
def __contains__(self, other):
"""Is the operator or state in this Hilbert space.
This is checked by comparing the classes of the Hilbert spaces, not
the instances. This is to allow Hilbert Spaces with symbolic
dimensions.
"""
if other.hilbert_space.__class__ == self.__class__:
return True
else:
return False
def _sympystr(self, printer, *args):
return 'H'
def _pretty(self, printer, *args):
ustr = '\N{LATIN CAPITAL LETTER H}'
return prettyForm(ustr)
def _latex(self, printer, *args):
return r'\mathcal{H}'
class ComplexSpace(HilbertSpace):
"""Finite dimensional Hilbert space of complex vectors.
The elements of this Hilbert space are n-dimensional complex valued
vectors with the usual inner product that takes the complex conjugate
of the vector on the right.
A classic example of this type of Hilbert space is spin-1/2, which is
``ComplexSpace(2)``. Generalizing to spin-s, the space is
``ComplexSpace(2*s+1)``. Quantum computing with N qubits is done with the
direct product space ``ComplexSpace(2)**N``.
Examples
========
>>> from sympy import symbols
>>> from sympy.physics.quantum.hilbert import ComplexSpace
>>> c1 = ComplexSpace(2)
>>> c1
C(2)
>>> c1.dimension
2
>>> n = symbols('n')
>>> c2 = ComplexSpace(n)
>>> c2
C(n)
>>> c2.dimension
n
"""
def __new__(cls, dimension):
dimension = sympify(dimension)
r = cls.eval(dimension)
if isinstance(r, Basic):
return r
obj = Basic.__new__(cls, dimension)
return obj
@classmethod
def eval(cls, dimension):
if len(dimension.atoms()) == 1:
if not (dimension.is_Integer and dimension > 0 or dimension is S.Infinity
or dimension.is_Symbol):
raise TypeError('The dimension of a ComplexSpace can only'
'be a positive integer, oo, or a Symbol: %r'
% dimension)
else:
for dim in dimension.atoms():
if not (dim.is_Integer or dim is S.Infinity or dim.is_Symbol):
raise TypeError('The dimension of a ComplexSpace can only'
' contain integers, oo, or a Symbol: %r'
% dim)
@property
def dimension(self):
return self.args[0]
def _sympyrepr(self, printer, *args):
return "%s(%s)" % (self.__class__.__name__,
printer._print(self.dimension, *args))
def _sympystr(self, printer, *args):
return "C(%s)" % printer._print(self.dimension, *args)
def _pretty(self, printer, *args):
ustr = '\N{LATIN CAPITAL LETTER C}'
pform_exp = printer._print(self.dimension, *args)
pform_base = prettyForm(ustr)
return pform_base**pform_exp
def _latex(self, printer, *args):
return r'\mathcal{C}^{%s}' % printer._print(self.dimension, *args)
class L2(HilbertSpace):
"""The Hilbert space of square integrable functions on an interval.
An L2 object takes in a single SymPy Interval argument which represents
the interval its functions (vectors) are defined on.
Examples
========
>>> from sympy import Interval, oo
>>> from sympy.physics.quantum.hilbert import L2
>>> hs = L2(Interval(0,oo))
>>> hs
L2(Interval(0, oo))
>>> hs.dimension
oo
>>> hs.interval
Interval(0, oo)
"""
def __new__(cls, interval):
if not isinstance(interval, Interval):
raise TypeError('L2 interval must be an Interval instance: %r'
% interval)
obj = Basic.__new__(cls, interval)
return obj
@property
def dimension(self):
return S.Infinity
@property
def interval(self):
return self.args[0]
def _sympyrepr(self, printer, *args):
return "L2(%s)" % printer._print(self.interval, *args)
def _sympystr(self, printer, *args):
return "L2(%s)" % printer._print(self.interval, *args)
def _pretty(self, printer, *args):
pform_exp = prettyForm('2')
pform_base = prettyForm('L')
return pform_base**pform_exp
def _latex(self, printer, *args):
interval = printer._print(self.interval, *args)
return r'{\mathcal{L}^2}\left( %s \right)' % interval
class FockSpace(HilbertSpace):
"""The Hilbert space for second quantization.
Technically, this Hilbert space is a infinite direct sum of direct
products of single particle Hilbert spaces [1]_. This is a mess, so we have
a class to represent it directly.
Examples
========
>>> from sympy.physics.quantum.hilbert import FockSpace
>>> hs = FockSpace()
>>> hs
F
>>> hs.dimension
oo
References
==========
.. [1] https://en.wikipedia.org/wiki/Fock_space
"""
def __new__(cls):
obj = Basic.__new__(cls)
return obj
@property
def dimension(self):
return S.Infinity
def _sympyrepr(self, printer, *args):
return "FockSpace()"
def _sympystr(self, printer, *args):
return "F"
def _pretty(self, printer, *args):
ustr = '\N{LATIN CAPITAL LETTER F}'
return prettyForm(ustr)
def _latex(self, printer, *args):
return r'\mathcal{F}'
class TensorProductHilbertSpace(HilbertSpace):
"""A tensor product of Hilbert spaces [1]_.
The tensor product between Hilbert spaces is represented by the
operator ``*`` Products of the same Hilbert space will be combined into
tensor powers.
A ``TensorProductHilbertSpace`` object takes in an arbitrary number of
``HilbertSpace`` objects as its arguments. In addition, multiplication of
``HilbertSpace`` objects will automatically return this tensor product
object.
Examples
========
>>> from sympy.physics.quantum.hilbert import ComplexSpace, FockSpace
>>> from sympy import symbols
>>> c = ComplexSpace(2)
>>> f = FockSpace()
>>> hs = c*f
>>> hs
C(2)*F
>>> hs.dimension
oo
>>> hs.spaces
(C(2), F)
>>> c1 = ComplexSpace(2)
>>> n = symbols('n')
>>> c2 = ComplexSpace(n)
>>> hs = c1*c2
>>> hs
C(2)*C(n)
>>> hs.dimension
2*n
References
==========
.. [1] https://en.wikipedia.org/wiki/Hilbert_space#Tensor_products
"""
def __new__(cls, *args):
r = cls.eval(args)
if isinstance(r, Basic):
return r
obj = Basic.__new__(cls, *args)
return obj
@classmethod
def eval(cls, args):
"""Evaluates the direct product."""
new_args = []
recall = False
#flatten arguments
for arg in args:
if isinstance(arg, TensorProductHilbertSpace):
new_args.extend(arg.args)
recall = True
elif isinstance(arg, (HilbertSpace, TensorPowerHilbertSpace)):
new_args.append(arg)
else:
raise TypeError('Hilbert spaces can only be multiplied by \
other Hilbert spaces: %r' % arg)
#combine like arguments into direct powers
comb_args = []
prev_arg = None
for new_arg in new_args:
if prev_arg is not None:
if isinstance(new_arg, TensorPowerHilbertSpace) and \
isinstance(prev_arg, TensorPowerHilbertSpace) and \
new_arg.base == prev_arg.base:
prev_arg = new_arg.base**(new_arg.exp + prev_arg.exp)
elif isinstance(new_arg, TensorPowerHilbertSpace) and \
new_arg.base == prev_arg:
prev_arg = prev_arg**(new_arg.exp + 1)
elif isinstance(prev_arg, TensorPowerHilbertSpace) and \
new_arg == prev_arg.base:
prev_arg = new_arg**(prev_arg.exp + 1)
elif new_arg == prev_arg:
prev_arg = new_arg**2
else:
comb_args.append(prev_arg)
prev_arg = new_arg
elif prev_arg is None:
prev_arg = new_arg
comb_args.append(prev_arg)
if recall:
return TensorProductHilbertSpace(*comb_args)
elif len(comb_args) == 1:
return TensorPowerHilbertSpace(comb_args[0].base, comb_args[0].exp)
else:
return None
@property
def dimension(self):
arg_list = [arg.dimension for arg in self.args]
if S.Infinity in arg_list:
return S.Infinity
else:
return reduce(lambda x, y: x*y, arg_list)
@property
def spaces(self):
"""A tuple of the Hilbert spaces in this tensor product."""
return self.args
def _spaces_printer(self, printer, *args):
spaces_strs = []
for arg in self.args:
s = printer._print(arg, *args)
if isinstance(arg, DirectSumHilbertSpace):
s = '(%s)' % s
spaces_strs.append(s)
return spaces_strs
def _sympyrepr(self, printer, *args):
spaces_reprs = self._spaces_printer(printer, *args)
return "TensorProductHilbertSpace(%s)" % ','.join(spaces_reprs)
def _sympystr(self, printer, *args):
spaces_strs = self._spaces_printer(printer, *args)
return '*'.join(spaces_strs)
def _pretty(self, printer, *args):
length = len(self.args)
pform = printer._print('', *args)
for i in range(length):
next_pform = printer._print(self.args[i], *args)
if isinstance(self.args[i], (DirectSumHilbertSpace,
TensorProductHilbertSpace)):
next_pform = prettyForm(
*next_pform.parens(left='(', right=')')
)
pform = prettyForm(*pform.right(next_pform))
if i != length - 1:
if printer._use_unicode:
pform = prettyForm(*pform.right(' ' + '\N{N-ARY CIRCLED TIMES OPERATOR}' + ' '))
else:
pform = prettyForm(*pform.right(' x '))
return pform
def _latex(self, printer, *args):
length = len(self.args)
s = ''
for i in range(length):
arg_s = printer._print(self.args[i], *args)
if isinstance(self.args[i], (DirectSumHilbertSpace,
TensorProductHilbertSpace)):
arg_s = r'\left(%s\right)' % arg_s
s = s + arg_s
if i != length - 1:
s = s + r'\otimes '
return s
class DirectSumHilbertSpace(HilbertSpace):
"""A direct sum of Hilbert spaces [1]_.
This class uses the ``+`` operator to represent direct sums between
different Hilbert spaces.
A ``DirectSumHilbertSpace`` object takes in an arbitrary number of
``HilbertSpace`` objects as its arguments. Also, addition of
``HilbertSpace`` objects will automatically return a direct sum object.
Examples
========
>>> from sympy.physics.quantum.hilbert import ComplexSpace, FockSpace
>>> c = ComplexSpace(2)
>>> f = FockSpace()
>>> hs = c+f
>>> hs
C(2)+F
>>> hs.dimension
oo
>>> list(hs.spaces)
[C(2), F]
References
==========
.. [1] https://en.wikipedia.org/wiki/Hilbert_space#Direct_sums
"""
def __new__(cls, *args):
r = cls.eval(args)
if isinstance(r, Basic):
return r
obj = Basic.__new__(cls, *args)
return obj
@classmethod
def eval(cls, args):
"""Evaluates the direct product."""
new_args = []
recall = False
#flatten arguments
for arg in args:
if isinstance(arg, DirectSumHilbertSpace):
new_args.extend(arg.args)
recall = True
elif isinstance(arg, HilbertSpace):
new_args.append(arg)
else:
raise TypeError('Hilbert spaces can only be summed with other \
Hilbert spaces: %r' % arg)
if recall:
return DirectSumHilbertSpace(*new_args)
else:
return None
@property
def dimension(self):
arg_list = [arg.dimension for arg in self.args]
if S.Infinity in arg_list:
return S.Infinity
else:
return reduce(lambda x, y: x + y, arg_list)
@property
def spaces(self):
"""A tuple of the Hilbert spaces in this direct sum."""
return self.args
def _sympyrepr(self, printer, *args):
spaces_reprs = [printer._print(arg, *args) for arg in self.args]
return "DirectSumHilbertSpace(%s)" % ','.join(spaces_reprs)
def _sympystr(self, printer, *args):
spaces_strs = [printer._print(arg, *args) for arg in self.args]
return '+'.join(spaces_strs)
def _pretty(self, printer, *args):
length = len(self.args)
pform = printer._print('', *args)
for i in range(length):
next_pform = printer._print(self.args[i], *args)
if isinstance(self.args[i], (DirectSumHilbertSpace,
TensorProductHilbertSpace)):
next_pform = prettyForm(
*next_pform.parens(left='(', right=')')
)
pform = prettyForm(*pform.right(next_pform))
if i != length - 1:
if printer._use_unicode:
pform = prettyForm(*pform.right(' \N{CIRCLED PLUS} '))
else:
pform = prettyForm(*pform.right(' + '))
return pform
def _latex(self, printer, *args):
length = len(self.args)
s = ''
for i in range(length):
arg_s = printer._print(self.args[i], *args)
if isinstance(self.args[i], (DirectSumHilbertSpace,
TensorProductHilbertSpace)):
arg_s = r'\left(%s\right)' % arg_s
s = s + arg_s
if i != length - 1:
s = s + r'\oplus '
return s
class TensorPowerHilbertSpace(HilbertSpace):
"""An exponentiated Hilbert space [1]_.
Tensor powers (repeated tensor products) are represented by the
operator ``**`` Identical Hilbert spaces that are multiplied together
will be automatically combined into a single tensor power object.
Any Hilbert space, product, or sum may be raised to a tensor power. The
``TensorPowerHilbertSpace`` takes two arguments: the Hilbert space; and the
tensor power (number).
Examples
========
>>> from sympy.physics.quantum.hilbert import ComplexSpace, FockSpace
>>> from sympy import symbols
>>> n = symbols('n')
>>> c = ComplexSpace(2)
>>> hs = c**n
>>> hs
C(2)**n
>>> hs.dimension
2**n
>>> c = ComplexSpace(2)
>>> c*c
C(2)**2
>>> f = FockSpace()
>>> c*f*f
C(2)*F**2
References
==========
.. [1] https://en.wikipedia.org/wiki/Hilbert_space#Tensor_products
"""
def __new__(cls, *args):
r = cls.eval(args)
if isinstance(r, Basic):
return r
return Basic.__new__(cls, *r)
@classmethod
def eval(cls, args):
new_args = args[0], sympify(args[1])
exp = new_args[1]
#simplify hs**1 -> hs
if exp is S.One:
return args[0]
#simplify hs**0 -> 1
if exp is S.Zero:
return S.One
#check (and allow) for hs**(x+42+y...) case
if len(exp.atoms()) == 1:
if not (exp.is_Integer and exp >= 0 or exp.is_Symbol):
raise ValueError('Hilbert spaces can only be raised to \
positive integers or Symbols: %r' % exp)
else:
for power in exp.atoms():
if not (power.is_Integer or power.is_Symbol):
raise ValueError('Tensor powers can only contain integers \
or Symbols: %r' % power)
return new_args
@property
def base(self):
return self.args[0]
@property
def exp(self):
return self.args[1]
@property
def dimension(self):
if self.base.dimension is S.Infinity:
return S.Infinity
else:
return self.base.dimension**self.exp
def _sympyrepr(self, printer, *args):
return "TensorPowerHilbertSpace(%s,%s)" % (printer._print(self.base,
*args), printer._print(self.exp, *args))
def _sympystr(self, printer, *args):
return "%s**%s" % (printer._print(self.base, *args),
printer._print(self.exp, *args))
def _pretty(self, printer, *args):
pform_exp = printer._print(self.exp, *args)
if printer._use_unicode:
pform_exp = prettyForm(*pform_exp.left(prettyForm('\N{N-ARY CIRCLED TIMES OPERATOR}')))
else:
pform_exp = prettyForm(*pform_exp.left(prettyForm('x')))
pform_base = printer._print(self.base, *args)
return pform_base**pform_exp
def _latex(self, printer, *args):
base = printer._print(self.base, *args)
exp = printer._print(self.exp, *args)
return r'{%s}^{\otimes %s}' % (base, exp)

View File

@ -0,0 +1,853 @@
from collections import deque
from sympy.core.random import randint
from sympy.external import import_module
from sympy.core.basic import Basic
from sympy.core.mul import Mul
from sympy.core.numbers import Number, equal_valued
from sympy.core.power import Pow
from sympy.core.singleton import S
from sympy.physics.quantum.represent import represent
from sympy.physics.quantum.dagger import Dagger
__all__ = [
# Public interfaces
'generate_gate_rules',
'generate_equivalent_ids',
'GateIdentity',
'bfs_identity_search',
'random_identity_search',
# "Private" functions
'is_scalar_sparse_matrix',
'is_scalar_nonsparse_matrix',
'is_degenerate',
'is_reducible',
]
np = import_module('numpy')
scipy = import_module('scipy', import_kwargs={'fromlist': ['sparse']})
def is_scalar_sparse_matrix(circuit, nqubits, identity_only, eps=1e-11):
"""Checks if a given scipy.sparse matrix is a scalar matrix.
A scalar matrix is such that B = bI, where B is the scalar
matrix, b is some scalar multiple, and I is the identity
matrix. A scalar matrix would have only the element b along
it's main diagonal and zeroes elsewhere.
Parameters
==========
circuit : Gate tuple
Sequence of quantum gates representing a quantum circuit
nqubits : int
Number of qubits in the circuit
identity_only : bool
Check for only identity matrices
eps : number
The tolerance value for zeroing out elements in the matrix.
Values in the range [-eps, +eps] will be changed to a zero.
"""
if not np or not scipy:
pass
matrix = represent(Mul(*circuit), nqubits=nqubits,
format='scipy.sparse')
# In some cases, represent returns a 1D scalar value in place
# of a multi-dimensional scalar matrix
if (isinstance(matrix, int)):
return matrix == 1 if identity_only else True
# If represent returns a matrix, check if the matrix is diagonal
# and if every item along the diagonal is the same
else:
# Due to floating pointing operations, must zero out
# elements that are "very" small in the dense matrix
# See parameter for default value.
# Get the ndarray version of the dense matrix
dense_matrix = matrix.todense().getA()
# Since complex values can't be compared, must split
# the matrix into real and imaginary components
# Find the real values in between -eps and eps
bool_real = np.logical_and(dense_matrix.real > -eps,
dense_matrix.real < eps)
# Find the imaginary values between -eps and eps
bool_imag = np.logical_and(dense_matrix.imag > -eps,
dense_matrix.imag < eps)
# Replaces values between -eps and eps with 0
corrected_real = np.where(bool_real, 0.0, dense_matrix.real)
corrected_imag = np.where(bool_imag, 0.0, dense_matrix.imag)
# Convert the matrix with real values into imaginary values
corrected_imag = corrected_imag * complex(1j)
# Recombine the real and imaginary components
corrected_dense = corrected_real + corrected_imag
# Check if it's diagonal
row_indices = corrected_dense.nonzero()[0]
col_indices = corrected_dense.nonzero()[1]
# Check if the rows indices and columns indices are the same
# If they match, then matrix only contains elements along diagonal
bool_indices = row_indices == col_indices
is_diagonal = bool_indices.all()
first_element = corrected_dense[0][0]
# If the first element is a zero, then can't rescale matrix
# and definitely not diagonal
if (first_element == 0.0 + 0.0j):
return False
# The dimensions of the dense matrix should still
# be 2^nqubits if there are elements all along the
# the main diagonal
trace_of_corrected = (corrected_dense/first_element).trace()
expected_trace = pow(2, nqubits)
has_correct_trace = trace_of_corrected == expected_trace
# If only looking for identity matrices
# first element must be a 1
real_is_one = abs(first_element.real - 1.0) < eps
imag_is_zero = abs(first_element.imag) < eps
is_one = real_is_one and imag_is_zero
is_identity = is_one if identity_only else True
return bool(is_diagonal and has_correct_trace and is_identity)
def is_scalar_nonsparse_matrix(circuit, nqubits, identity_only, eps=None):
"""Checks if a given circuit, in matrix form, is equivalent to
a scalar value.
Parameters
==========
circuit : Gate tuple
Sequence of quantum gates representing a quantum circuit
nqubits : int
Number of qubits in the circuit
identity_only : bool
Check for only identity matrices
eps : number
This argument is ignored. It is just for signature compatibility with
is_scalar_sparse_matrix.
Note: Used in situations when is_scalar_sparse_matrix has bugs
"""
matrix = represent(Mul(*circuit), nqubits=nqubits)
# In some cases, represent returns a 1D scalar value in place
# of a multi-dimensional scalar matrix
if (isinstance(matrix, Number)):
return matrix == 1 if identity_only else True
# If represent returns a matrix, check if the matrix is diagonal
# and if every item along the diagonal is the same
else:
# Added up the diagonal elements
matrix_trace = matrix.trace()
# Divide the trace by the first element in the matrix
# if matrix is not required to be the identity matrix
adjusted_matrix_trace = (matrix_trace/matrix[0]
if not identity_only
else matrix_trace)
is_identity = equal_valued(matrix[0], 1) if identity_only else True
has_correct_trace = adjusted_matrix_trace == pow(2, nqubits)
# The matrix is scalar if it's diagonal and the adjusted trace
# value is equal to 2^nqubits
return bool(
matrix.is_diagonal() and has_correct_trace and is_identity)
if np and scipy:
is_scalar_matrix = is_scalar_sparse_matrix
else:
is_scalar_matrix = is_scalar_nonsparse_matrix
def _get_min_qubits(a_gate):
if isinstance(a_gate, Pow):
return a_gate.base.min_qubits
else:
return a_gate.min_qubits
def ll_op(left, right):
"""Perform a LL operation.
A LL operation multiplies both left and right circuits
with the dagger of the left circuit's leftmost gate, and
the dagger is multiplied on the left side of both circuits.
If a LL is possible, it returns the new gate rule as a
2-tuple (LHS, RHS), where LHS is the left circuit and
and RHS is the right circuit of the new rule.
If a LL is not possible, None is returned.
Parameters
==========
left : Gate tuple
The left circuit of a gate rule expression.
right : Gate tuple
The right circuit of a gate rule expression.
Examples
========
Generate a new gate rule using a LL operation:
>>> from sympy.physics.quantum.identitysearch import ll_op
>>> from sympy.physics.quantum.gate import X, Y, Z
>>> x = X(0); y = Y(0); z = Z(0)
>>> ll_op((x, y, z), ())
((Y(0), Z(0)), (X(0),))
>>> ll_op((y, z), (x,))
((Z(0),), (Y(0), X(0)))
"""
if (len(left) > 0):
ll_gate = left[0]
ll_gate_is_unitary = is_scalar_matrix(
(Dagger(ll_gate), ll_gate), _get_min_qubits(ll_gate), True)
if (len(left) > 0 and ll_gate_is_unitary):
# Get the new left side w/o the leftmost gate
new_left = left[1:len(left)]
# Add the leftmost gate to the left position on the right side
new_right = (Dagger(ll_gate),) + right
# Return the new gate rule
return (new_left, new_right)
return None
def lr_op(left, right):
"""Perform a LR operation.
A LR operation multiplies both left and right circuits
with the dagger of the left circuit's rightmost gate, and
the dagger is multiplied on the right side of both circuits.
If a LR is possible, it returns the new gate rule as a
2-tuple (LHS, RHS), where LHS is the left circuit and
and RHS is the right circuit of the new rule.
If a LR is not possible, None is returned.
Parameters
==========
left : Gate tuple
The left circuit of a gate rule expression.
right : Gate tuple
The right circuit of a gate rule expression.
Examples
========
Generate a new gate rule using a LR operation:
>>> from sympy.physics.quantum.identitysearch import lr_op
>>> from sympy.physics.quantum.gate import X, Y, Z
>>> x = X(0); y = Y(0); z = Z(0)
>>> lr_op((x, y, z), ())
((X(0), Y(0)), (Z(0),))
>>> lr_op((x, y), (z,))
((X(0),), (Z(0), Y(0)))
"""
if (len(left) > 0):
lr_gate = left[len(left) - 1]
lr_gate_is_unitary = is_scalar_matrix(
(Dagger(lr_gate), lr_gate), _get_min_qubits(lr_gate), True)
if (len(left) > 0 and lr_gate_is_unitary):
# Get the new left side w/o the rightmost gate
new_left = left[0:len(left) - 1]
# Add the rightmost gate to the right position on the right side
new_right = right + (Dagger(lr_gate),)
# Return the new gate rule
return (new_left, new_right)
return None
def rl_op(left, right):
"""Perform a RL operation.
A RL operation multiplies both left and right circuits
with the dagger of the right circuit's leftmost gate, and
the dagger is multiplied on the left side of both circuits.
If a RL is possible, it returns the new gate rule as a
2-tuple (LHS, RHS), where LHS is the left circuit and
and RHS is the right circuit of the new rule.
If a RL is not possible, None is returned.
Parameters
==========
left : Gate tuple
The left circuit of a gate rule expression.
right : Gate tuple
The right circuit of a gate rule expression.
Examples
========
Generate a new gate rule using a RL operation:
>>> from sympy.physics.quantum.identitysearch import rl_op
>>> from sympy.physics.quantum.gate import X, Y, Z
>>> x = X(0); y = Y(0); z = Z(0)
>>> rl_op((x,), (y, z))
((Y(0), X(0)), (Z(0),))
>>> rl_op((x, y), (z,))
((Z(0), X(0), Y(0)), ())
"""
if (len(right) > 0):
rl_gate = right[0]
rl_gate_is_unitary = is_scalar_matrix(
(Dagger(rl_gate), rl_gate), _get_min_qubits(rl_gate), True)
if (len(right) > 0 and rl_gate_is_unitary):
# Get the new right side w/o the leftmost gate
new_right = right[1:len(right)]
# Add the leftmost gate to the left position on the left side
new_left = (Dagger(rl_gate),) + left
# Return the new gate rule
return (new_left, new_right)
return None
def rr_op(left, right):
"""Perform a RR operation.
A RR operation multiplies both left and right circuits
with the dagger of the right circuit's rightmost gate, and
the dagger is multiplied on the right side of both circuits.
If a RR is possible, it returns the new gate rule as a
2-tuple (LHS, RHS), where LHS is the left circuit and
and RHS is the right circuit of the new rule.
If a RR is not possible, None is returned.
Parameters
==========
left : Gate tuple
The left circuit of a gate rule expression.
right : Gate tuple
The right circuit of a gate rule expression.
Examples
========
Generate a new gate rule using a RR operation:
>>> from sympy.physics.quantum.identitysearch import rr_op
>>> from sympy.physics.quantum.gate import X, Y, Z
>>> x = X(0); y = Y(0); z = Z(0)
>>> rr_op((x, y), (z,))
((X(0), Y(0), Z(0)), ())
>>> rr_op((x,), (y, z))
((X(0), Z(0)), (Y(0),))
"""
if (len(right) > 0):
rr_gate = right[len(right) - 1]
rr_gate_is_unitary = is_scalar_matrix(
(Dagger(rr_gate), rr_gate), _get_min_qubits(rr_gate), True)
if (len(right) > 0 and rr_gate_is_unitary):
# Get the new right side w/o the rightmost gate
new_right = right[0:len(right) - 1]
# Add the rightmost gate to the right position on the right side
new_left = left + (Dagger(rr_gate),)
# Return the new gate rule
return (new_left, new_right)
return None
def generate_gate_rules(gate_seq, return_as_muls=False):
"""Returns a set of gate rules. Each gate rules is represented
as a 2-tuple of tuples or Muls. An empty tuple represents an arbitrary
scalar value.
This function uses the four operations (LL, LR, RL, RR)
to generate the gate rules.
A gate rule is an expression such as ABC = D or AB = CD, where
A, B, C, and D are gates. Each value on either side of the
equal sign represents a circuit. The four operations allow
one to find a set of equivalent circuits from a gate identity.
The letters denoting the operation tell the user what
activities to perform on each expression. The first letter
indicates which side of the equal sign to focus on. The
second letter indicates which gate to focus on given the
side. Once this information is determined, the inverse
of the gate is multiplied on both circuits to create a new
gate rule.
For example, given the identity, ABCD = 1, a LL operation
means look at the left value and multiply both left sides by the
inverse of the leftmost gate A. If A is Hermitian, the inverse
of A is still A. The resulting new rule is BCD = A.
The following is a summary of the four operations. Assume
that in the examples, all gates are Hermitian.
LL : left circuit, left multiply
ABCD = E -> AABCD = AE -> BCD = AE
LR : left circuit, right multiply
ABCD = E -> ABCDD = ED -> ABC = ED
RL : right circuit, left multiply
ABC = ED -> EABC = EED -> EABC = D
RR : right circuit, right multiply
AB = CD -> ABD = CDD -> ABD = C
The number of gate rules generated is n*(n+1), where n
is the number of gates in the sequence (unproven).
Parameters
==========
gate_seq : Gate tuple, Mul, or Number
A variable length tuple or Mul of Gates whose product is equal to
a scalar matrix
return_as_muls : bool
True to return a set of Muls; False to return a set of tuples
Examples
========
Find the gate rules of the current circuit using tuples:
>>> from sympy.physics.quantum.identitysearch import generate_gate_rules
>>> from sympy.physics.quantum.gate import X, Y, Z
>>> x = X(0); y = Y(0); z = Z(0)
>>> generate_gate_rules((x, x))
{((X(0),), (X(0),)), ((X(0), X(0)), ())}
>>> generate_gate_rules((x, y, z))
{((), (X(0), Z(0), Y(0))), ((), (Y(0), X(0), Z(0))),
((), (Z(0), Y(0), X(0))), ((X(0),), (Z(0), Y(0))),
((Y(0),), (X(0), Z(0))), ((Z(0),), (Y(0), X(0))),
((X(0), Y(0)), (Z(0),)), ((Y(0), Z(0)), (X(0),)),
((Z(0), X(0)), (Y(0),)), ((X(0), Y(0), Z(0)), ()),
((Y(0), Z(0), X(0)), ()), ((Z(0), X(0), Y(0)), ())}
Find the gate rules of the current circuit using Muls:
>>> generate_gate_rules(x*x, return_as_muls=True)
{(1, 1)}
>>> generate_gate_rules(x*y*z, return_as_muls=True)
{(1, X(0)*Z(0)*Y(0)), (1, Y(0)*X(0)*Z(0)),
(1, Z(0)*Y(0)*X(0)), (X(0)*Y(0), Z(0)),
(Y(0)*Z(0), X(0)), (Z(0)*X(0), Y(0)),
(X(0)*Y(0)*Z(0), 1), (Y(0)*Z(0)*X(0), 1),
(Z(0)*X(0)*Y(0), 1), (X(0), Z(0)*Y(0)),
(Y(0), X(0)*Z(0)), (Z(0), Y(0)*X(0))}
"""
if isinstance(gate_seq, Number):
if return_as_muls:
return {(S.One, S.One)}
else:
return {((), ())}
elif isinstance(gate_seq, Mul):
gate_seq = gate_seq.args
# Each item in queue is a 3-tuple:
# i) first item is the left side of an equality
# ii) second item is the right side of an equality
# iii) third item is the number of operations performed
# The argument, gate_seq, will start on the left side, and
# the right side will be empty, implying the presence of an
# identity.
queue = deque()
# A set of gate rules
rules = set()
# Maximum number of operations to perform
max_ops = len(gate_seq)
def process_new_rule(new_rule, ops):
if new_rule is not None:
new_left, new_right = new_rule
if new_rule not in rules and (new_right, new_left) not in rules:
rules.add(new_rule)
# If haven't reached the max limit on operations
if ops + 1 < max_ops:
queue.append(new_rule + (ops + 1,))
queue.append((gate_seq, (), 0))
rules.add((gate_seq, ()))
while len(queue) > 0:
left, right, ops = queue.popleft()
# Do a LL
new_rule = ll_op(left, right)
process_new_rule(new_rule, ops)
# Do a LR
new_rule = lr_op(left, right)
process_new_rule(new_rule, ops)
# Do a RL
new_rule = rl_op(left, right)
process_new_rule(new_rule, ops)
# Do a RR
new_rule = rr_op(left, right)
process_new_rule(new_rule, ops)
if return_as_muls:
# Convert each rule as tuples into a rule as muls
mul_rules = set()
for rule in rules:
left, right = rule
mul_rules.add((Mul(*left), Mul(*right)))
rules = mul_rules
return rules
def generate_equivalent_ids(gate_seq, return_as_muls=False):
"""Returns a set of equivalent gate identities.
A gate identity is a quantum circuit such that the product
of the gates in the circuit is equal to a scalar value.
For example, XYZ = i, where X, Y, Z are the Pauli gates and
i is the imaginary value, is considered a gate identity.
This function uses the four operations (LL, LR, RL, RR)
to generate the gate rules and, subsequently, to locate equivalent
gate identities.
Note that all equivalent identities are reachable in n operations
from the starting gate identity, where n is the number of gates
in the sequence.
The max number of gate identities is 2n, where n is the number
of gates in the sequence (unproven).
Parameters
==========
gate_seq : Gate tuple, Mul, or Number
A variable length tuple or Mul of Gates whose product is equal to
a scalar matrix.
return_as_muls: bool
True to return as Muls; False to return as tuples
Examples
========
Find equivalent gate identities from the current circuit with tuples:
>>> from sympy.physics.quantum.identitysearch import generate_equivalent_ids
>>> from sympy.physics.quantum.gate import X, Y, Z
>>> x = X(0); y = Y(0); z = Z(0)
>>> generate_equivalent_ids((x, x))
{(X(0), X(0))}
>>> generate_equivalent_ids((x, y, z))
{(X(0), Y(0), Z(0)), (X(0), Z(0), Y(0)), (Y(0), X(0), Z(0)),
(Y(0), Z(0), X(0)), (Z(0), X(0), Y(0)), (Z(0), Y(0), X(0))}
Find equivalent gate identities from the current circuit with Muls:
>>> generate_equivalent_ids(x*x, return_as_muls=True)
{1}
>>> generate_equivalent_ids(x*y*z, return_as_muls=True)
{X(0)*Y(0)*Z(0), X(0)*Z(0)*Y(0), Y(0)*X(0)*Z(0),
Y(0)*Z(0)*X(0), Z(0)*X(0)*Y(0), Z(0)*Y(0)*X(0)}
"""
if isinstance(gate_seq, Number):
return {S.One}
elif isinstance(gate_seq, Mul):
gate_seq = gate_seq.args
# Filter through the gate rules and keep the rules
# with an empty tuple either on the left or right side
# A set of equivalent gate identities
eq_ids = set()
gate_rules = generate_gate_rules(gate_seq)
for rule in gate_rules:
l, r = rule
if l == ():
eq_ids.add(r)
elif r == ():
eq_ids.add(l)
if return_as_muls:
convert_to_mul = lambda id_seq: Mul(*id_seq)
eq_ids = set(map(convert_to_mul, eq_ids))
return eq_ids
class GateIdentity(Basic):
"""Wrapper class for circuits that reduce to a scalar value.
A gate identity is a quantum circuit such that the product
of the gates in the circuit is equal to a scalar value.
For example, XYZ = i, where X, Y, Z are the Pauli gates and
i is the imaginary value, is considered a gate identity.
Parameters
==========
args : Gate tuple
A variable length tuple of Gates that form an identity.
Examples
========
Create a GateIdentity and look at its attributes:
>>> from sympy.physics.quantum.identitysearch import GateIdentity
>>> from sympy.physics.quantum.gate import X, Y, Z
>>> x = X(0); y = Y(0); z = Z(0)
>>> an_identity = GateIdentity(x, y, z)
>>> an_identity.circuit
X(0)*Y(0)*Z(0)
>>> an_identity.equivalent_ids
{(X(0), Y(0), Z(0)), (X(0), Z(0), Y(0)), (Y(0), X(0), Z(0)),
(Y(0), Z(0), X(0)), (Z(0), X(0), Y(0)), (Z(0), Y(0), X(0))}
"""
def __new__(cls, *args):
# args should be a tuple - a variable length argument list
obj = Basic.__new__(cls, *args)
obj._circuit = Mul(*args)
obj._rules = generate_gate_rules(args)
obj._eq_ids = generate_equivalent_ids(args)
return obj
@property
def circuit(self):
return self._circuit
@property
def gate_rules(self):
return self._rules
@property
def equivalent_ids(self):
return self._eq_ids
@property
def sequence(self):
return self.args
def __str__(self):
"""Returns the string of gates in a tuple."""
return str(self.circuit)
def is_degenerate(identity_set, gate_identity):
"""Checks if a gate identity is a permutation of another identity.
Parameters
==========
identity_set : set
A Python set with GateIdentity objects.
gate_identity : GateIdentity
The GateIdentity to check for existence in the set.
Examples
========
Check if the identity is a permutation of another identity:
>>> from sympy.physics.quantum.identitysearch import (
... GateIdentity, is_degenerate)
>>> from sympy.physics.quantum.gate import X, Y, Z
>>> x = X(0); y = Y(0); z = Z(0)
>>> an_identity = GateIdentity(x, y, z)
>>> id_set = {an_identity}
>>> another_id = (y, z, x)
>>> is_degenerate(id_set, another_id)
True
>>> another_id = (x, x)
>>> is_degenerate(id_set, another_id)
False
"""
# For now, just iteratively go through the set and check if the current
# gate_identity is a permutation of an identity in the set
for an_id in identity_set:
if (gate_identity in an_id.equivalent_ids):
return True
return False
def is_reducible(circuit, nqubits, begin, end):
"""Determines if a circuit is reducible by checking
if its subcircuits are scalar values.
Parameters
==========
circuit : Gate tuple
A tuple of Gates representing a circuit. The circuit to check
if a gate identity is contained in a subcircuit.
nqubits : int
The number of qubits the circuit operates on.
begin : int
The leftmost gate in the circuit to include in a subcircuit.
end : int
The rightmost gate in the circuit to include in a subcircuit.
Examples
========
Check if the circuit can be reduced:
>>> from sympy.physics.quantum.identitysearch import is_reducible
>>> from sympy.physics.quantum.gate import X, Y, Z
>>> x = X(0); y = Y(0); z = Z(0)
>>> is_reducible((x, y, z), 1, 0, 3)
True
Check if an interval in the circuit can be reduced:
>>> is_reducible((x, y, z), 1, 1, 3)
False
>>> is_reducible((x, y, y), 1, 1, 3)
True
"""
current_circuit = ()
# Start from the gate at "end" and go down to almost the gate at "begin"
for ndx in reversed(range(begin, end)):
next_gate = circuit[ndx]
current_circuit = (next_gate,) + current_circuit
# If a circuit as a matrix is equivalent to a scalar value
if (is_scalar_matrix(current_circuit, nqubits, False)):
return True
return False
def bfs_identity_search(gate_list, nqubits, max_depth=None,
identity_only=False):
"""Constructs a set of gate identities from the list of possible gates.
Performs a breadth first search over the space of gate identities.
This allows the finding of the shortest gate identities first.
Parameters
==========
gate_list : list, Gate
A list of Gates from which to search for gate identities.
nqubits : int
The number of qubits the quantum circuit operates on.
max_depth : int
The longest quantum circuit to construct from gate_list.
identity_only : bool
True to search for gate identities that reduce to identity;
False to search for gate identities that reduce to a scalar.
Examples
========
Find a list of gate identities:
>>> from sympy.physics.quantum.identitysearch import bfs_identity_search
>>> from sympy.physics.quantum.gate import X, Y, Z
>>> x = X(0); y = Y(0); z = Z(0)
>>> bfs_identity_search([x], 1, max_depth=2)
{GateIdentity(X(0), X(0))}
>>> bfs_identity_search([x, y, z], 1)
{GateIdentity(X(0), X(0)), GateIdentity(Y(0), Y(0)),
GateIdentity(Z(0), Z(0)), GateIdentity(X(0), Y(0), Z(0))}
Find a list of identities that only equal to 1:
>>> bfs_identity_search([x, y, z], 1, identity_only=True)
{GateIdentity(X(0), X(0)), GateIdentity(Y(0), Y(0)),
GateIdentity(Z(0), Z(0))}
"""
if max_depth is None or max_depth <= 0:
max_depth = len(gate_list)
id_only = identity_only
# Start with an empty sequence (implicitly contains an IdentityGate)
queue = deque([()])
# Create an empty set of gate identities
ids = set()
# Begin searching for gate identities in given space.
while (len(queue) > 0):
current_circuit = queue.popleft()
for next_gate in gate_list:
new_circuit = current_circuit + (next_gate,)
# Determines if a (strict) subcircuit is a scalar matrix
circuit_reducible = is_reducible(new_circuit, nqubits,
1, len(new_circuit))
# In many cases when the matrix is a scalar value,
# the evaluated matrix will actually be an integer
if (is_scalar_matrix(new_circuit, nqubits, id_only) and
not is_degenerate(ids, new_circuit) and
not circuit_reducible):
ids.add(GateIdentity(*new_circuit))
elif (len(new_circuit) < max_depth and
not circuit_reducible):
queue.append(new_circuit)
return ids
def random_identity_search(gate_list, numgates, nqubits):
"""Randomly selects numgates from gate_list and checks if it is
a gate identity.
If the circuit is a gate identity, the circuit is returned;
Otherwise, None is returned.
"""
gate_size = len(gate_list)
circuit = ()
for i in range(numgates):
next_gate = gate_list[randint(0, gate_size - 1)]
circuit = circuit + (next_gate,)
is_scalar = is_scalar_matrix(circuit, nqubits, False)
return circuit if is_scalar else None

View File

@ -0,0 +1,137 @@
"""Symbolic inner product."""
from sympy.core.expr import Expr
from sympy.functions.elementary.complexes import conjugate
from sympy.printing.pretty.stringpict import prettyForm
from sympy.physics.quantum.dagger import Dagger
from sympy.physics.quantum.state import KetBase, BraBase
__all__ = [
'InnerProduct'
]
# InnerProduct is not an QExpr because it is really just a regular commutative
# number. We have gone back and forth about this, but we gain a lot by having
# it subclass Expr. The main challenges were getting Dagger to work
# (we use _eval_conjugate) and represent (we can use atoms and subs). Having
# it be an Expr, mean that there are no commutative QExpr subclasses,
# which simplifies the design of everything.
class InnerProduct(Expr):
"""An unevaluated inner product between a Bra and a Ket [1].
Parameters
==========
bra : BraBase or subclass
The bra on the left side of the inner product.
ket : KetBase or subclass
The ket on the right side of the inner product.
Examples
========
Create an InnerProduct and check its properties:
>>> from sympy.physics.quantum import Bra, Ket
>>> b = Bra('b')
>>> k = Ket('k')
>>> ip = b*k
>>> ip
<b|k>
>>> ip.bra
<b|
>>> ip.ket
|k>
In simple products of kets and bras inner products will be automatically
identified and created::
>>> b*k
<b|k>
But in more complex expressions, there is ambiguity in whether inner or
outer products should be created::
>>> k*b*k*b
|k><b|*|k>*<b|
A user can force the creation of a inner products in a complex expression
by using parentheses to group the bra and ket::
>>> k*(b*k)*b
<b|k>*|k>*<b|
Notice how the inner product <b|k> moved to the left of the expression
because inner products are commutative complex numbers.
References
==========
.. [1] https://en.wikipedia.org/wiki/Inner_product
"""
is_complex = True
def __new__(cls, bra, ket):
if not isinstance(ket, KetBase):
raise TypeError('KetBase subclass expected, got: %r' % ket)
if not isinstance(bra, BraBase):
raise TypeError('BraBase subclass expected, got: %r' % ket)
obj = Expr.__new__(cls, bra, ket)
return obj
@property
def bra(self):
return self.args[0]
@property
def ket(self):
return self.args[1]
def _eval_conjugate(self):
return InnerProduct(Dagger(self.ket), Dagger(self.bra))
def _sympyrepr(self, printer, *args):
return '%s(%s,%s)' % (self.__class__.__name__,
printer._print(self.bra, *args), printer._print(self.ket, *args))
def _sympystr(self, printer, *args):
sbra = printer._print(self.bra)
sket = printer._print(self.ket)
return '%s|%s' % (sbra[:-1], sket[1:])
def _pretty(self, printer, *args):
# Print state contents
bra = self.bra._print_contents_pretty(printer, *args)
ket = self.ket._print_contents_pretty(printer, *args)
# Print brackets
height = max(bra.height(), ket.height())
use_unicode = printer._use_unicode
lbracket, _ = self.bra._pretty_brackets(height, use_unicode)
cbracket, rbracket = self.ket._pretty_brackets(height, use_unicode)
# Build innerproduct
pform = prettyForm(*bra.left(lbracket))
pform = prettyForm(*pform.right(cbracket))
pform = prettyForm(*pform.right(ket))
pform = prettyForm(*pform.right(rbracket))
return pform
def _latex(self, printer, *args):
bra_label = self.bra._print_contents_latex(printer, *args)
ket = printer._print(self.ket, *args)
return r'\left\langle %s \right. %s' % (bra_label, ket)
def doit(self, **hints):
try:
r = self.ket._eval_innerproduct(self.bra, **hints)
except NotImplementedError:
try:
r = conjugate(
self.bra.dual._eval_innerproduct(self.ket.dual, **hints)
)
except NotImplementedError:
r = None
if r is not None:
return r
return self

View File

@ -0,0 +1,103 @@
"""A cache for storing small matrices in multiple formats."""
from sympy.core.numbers import (I, Rational, pi)
from sympy.core.power import Pow
from sympy.functions.elementary.exponential import exp
from sympy.matrices.dense import Matrix
from sympy.physics.quantum.matrixutils import (
to_sympy, to_numpy, to_scipy_sparse
)
class MatrixCache:
"""A cache for small matrices in different formats.
This class takes small matrices in the standard ``sympy.Matrix`` format,
and then converts these to both ``numpy.matrix`` and
``scipy.sparse.csr_matrix`` matrices. These matrices are then stored for
future recovery.
"""
def __init__(self, dtype='complex'):
self._cache = {}
self.dtype = dtype
def cache_matrix(self, name, m):
"""Cache a matrix by its name.
Parameters
----------
name : str
A descriptive name for the matrix, like "identity2".
m : list of lists
The raw matrix data as a SymPy Matrix.
"""
try:
self._sympy_matrix(name, m)
except ImportError:
pass
try:
self._numpy_matrix(name, m)
except ImportError:
pass
try:
self._scipy_sparse_matrix(name, m)
except ImportError:
pass
def get_matrix(self, name, format):
"""Get a cached matrix by name and format.
Parameters
----------
name : str
A descriptive name for the matrix, like "identity2".
format : str
The format desired ('sympy', 'numpy', 'scipy.sparse')
"""
m = self._cache.get((name, format))
if m is not None:
return m
raise NotImplementedError(
'Matrix with name %s and format %s is not available.' %
(name, format)
)
def _store_matrix(self, name, format, m):
self._cache[(name, format)] = m
def _sympy_matrix(self, name, m):
self._store_matrix(name, 'sympy', to_sympy(m))
def _numpy_matrix(self, name, m):
m = to_numpy(m, dtype=self.dtype)
self._store_matrix(name, 'numpy', m)
def _scipy_sparse_matrix(self, name, m):
# TODO: explore different sparse formats. But sparse.kron will use
# coo in most cases, so we use that here.
m = to_scipy_sparse(m, dtype=self.dtype)
self._store_matrix(name, 'scipy.sparse', m)
sqrt2_inv = Pow(2, Rational(-1, 2), evaluate=False)
# Save the common matrices that we will need
matrix_cache = MatrixCache()
matrix_cache.cache_matrix('eye2', Matrix([[1, 0], [0, 1]]))
matrix_cache.cache_matrix('op11', Matrix([[0, 0], [0, 1]])) # |1><1|
matrix_cache.cache_matrix('op00', Matrix([[1, 0], [0, 0]])) # |0><0|
matrix_cache.cache_matrix('op10', Matrix([[0, 0], [1, 0]])) # |1><0|
matrix_cache.cache_matrix('op01', Matrix([[0, 1], [0, 0]])) # |0><1|
matrix_cache.cache_matrix('X', Matrix([[0, 1], [1, 0]]))
matrix_cache.cache_matrix('Y', Matrix([[0, -I], [I, 0]]))
matrix_cache.cache_matrix('Z', Matrix([[1, 0], [0, -1]]))
matrix_cache.cache_matrix('S', Matrix([[1, 0], [0, I]]))
matrix_cache.cache_matrix('T', Matrix([[1, 0], [0, exp(I*pi/4)]]))
matrix_cache.cache_matrix('H', sqrt2_inv*Matrix([[1, 1], [1, -1]]))
matrix_cache.cache_matrix('Hsqrt2', Matrix([[1, 1], [1, -1]]))
matrix_cache.cache_matrix(
'SWAP', Matrix([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]))
matrix_cache.cache_matrix('ZX', sqrt2_inv*Matrix([[1, 1], [1, -1]]))
matrix_cache.cache_matrix('ZY', Matrix([[I, 0], [0, -I]]))

View File

@ -0,0 +1,272 @@
"""Utilities to deal with sympy.Matrix, numpy and scipy.sparse."""
from sympy.core.expr import Expr
from sympy.core.numbers import I
from sympy.core.singleton import S
from sympy.matrices.matrixbase import MatrixBase
from sympy.matrices import eye, zeros
from sympy.external import import_module
__all__ = [
'numpy_ndarray',
'scipy_sparse_matrix',
'sympy_to_numpy',
'sympy_to_scipy_sparse',
'numpy_to_sympy',
'scipy_sparse_to_sympy',
'flatten_scalar',
'matrix_dagger',
'to_sympy',
'to_numpy',
'to_scipy_sparse',
'matrix_tensor_product',
'matrix_zeros'
]
# Conditionally define the base classes for numpy and scipy.sparse arrays
# for use in isinstance tests.
np = import_module('numpy')
if not np:
class numpy_ndarray:
pass
else:
numpy_ndarray = np.ndarray # type: ignore
scipy = import_module('scipy', import_kwargs={'fromlist': ['sparse']})
if not scipy:
class scipy_sparse_matrix:
pass
sparse = None
else:
sparse = scipy.sparse
scipy_sparse_matrix = sparse.spmatrix # type: ignore
def sympy_to_numpy(m, **options):
"""Convert a SymPy Matrix/complex number to a numpy matrix or scalar."""
if not np:
raise ImportError
dtype = options.get('dtype', 'complex')
if isinstance(m, MatrixBase):
return np.array(m.tolist(), dtype=dtype)
elif isinstance(m, Expr):
if m.is_Number or m.is_NumberSymbol or m == I:
return complex(m)
raise TypeError('Expected MatrixBase or complex scalar, got: %r' % m)
def sympy_to_scipy_sparse(m, **options):
"""Convert a SymPy Matrix/complex number to a numpy matrix or scalar."""
if not np or not sparse:
raise ImportError
dtype = options.get('dtype', 'complex')
if isinstance(m, MatrixBase):
return sparse.csr_matrix(np.array(m.tolist(), dtype=dtype))
elif isinstance(m, Expr):
if m.is_Number or m.is_NumberSymbol or m == I:
return complex(m)
raise TypeError('Expected MatrixBase or complex scalar, got: %r' % m)
def scipy_sparse_to_sympy(m, **options):
"""Convert a scipy.sparse matrix to a SymPy matrix."""
return MatrixBase(m.todense())
def numpy_to_sympy(m, **options):
"""Convert a numpy matrix to a SymPy matrix."""
return MatrixBase(m)
def to_sympy(m, **options):
"""Convert a numpy/scipy.sparse matrix to a SymPy matrix."""
if isinstance(m, MatrixBase):
return m
elif isinstance(m, numpy_ndarray):
return numpy_to_sympy(m)
elif isinstance(m, scipy_sparse_matrix):
return scipy_sparse_to_sympy(m)
elif isinstance(m, Expr):
return m
raise TypeError('Expected sympy/numpy/scipy.sparse matrix, got: %r' % m)
def to_numpy(m, **options):
"""Convert a sympy/scipy.sparse matrix to a numpy matrix."""
dtype = options.get('dtype', 'complex')
if isinstance(m, (MatrixBase, Expr)):
return sympy_to_numpy(m, dtype=dtype)
elif isinstance(m, numpy_ndarray):
return m
elif isinstance(m, scipy_sparse_matrix):
return m.todense()
raise TypeError('Expected sympy/numpy/scipy.sparse matrix, got: %r' % m)
def to_scipy_sparse(m, **options):
"""Convert a sympy/numpy matrix to a scipy.sparse matrix."""
dtype = options.get('dtype', 'complex')
if isinstance(m, (MatrixBase, Expr)):
return sympy_to_scipy_sparse(m, dtype=dtype)
elif isinstance(m, numpy_ndarray):
if not sparse:
raise ImportError
return sparse.csr_matrix(m)
elif isinstance(m, scipy_sparse_matrix):
return m
raise TypeError('Expected sympy/numpy/scipy.sparse matrix, got: %r' % m)
def flatten_scalar(e):
"""Flatten a 1x1 matrix to a scalar, return larger matrices unchanged."""
if isinstance(e, MatrixBase):
if e.shape == (1, 1):
e = e[0]
if isinstance(e, (numpy_ndarray, scipy_sparse_matrix)):
if e.shape == (1, 1):
e = complex(e[0, 0])
return e
def matrix_dagger(e):
"""Return the dagger of a sympy/numpy/scipy.sparse matrix."""
if isinstance(e, MatrixBase):
return e.H
elif isinstance(e, (numpy_ndarray, scipy_sparse_matrix)):
return e.conjugate().transpose()
raise TypeError('Expected sympy/numpy/scipy.sparse matrix, got: %r' % e)
# TODO: Move this into sympy.matricies.
def _sympy_tensor_product(*matrices):
"""Compute the kronecker product of a sequence of SymPy Matrices.
"""
from sympy.matrices.expressions.kronecker import matrix_kronecker_product
return matrix_kronecker_product(*matrices)
def _numpy_tensor_product(*product):
"""numpy version of tensor product of multiple arguments."""
if not np:
raise ImportError
answer = product[0]
for item in product[1:]:
answer = np.kron(answer, item)
return answer
def _scipy_sparse_tensor_product(*product):
"""scipy.sparse version of tensor product of multiple arguments."""
if not sparse:
raise ImportError
answer = product[0]
for item in product[1:]:
answer = sparse.kron(answer, item)
# The final matrices will just be multiplied, so csr is a good final
# sparse format.
return sparse.csr_matrix(answer)
def matrix_tensor_product(*product):
"""Compute the matrix tensor product of sympy/numpy/scipy.sparse matrices."""
if isinstance(product[0], MatrixBase):
return _sympy_tensor_product(*product)
elif isinstance(product[0], numpy_ndarray):
return _numpy_tensor_product(*product)
elif isinstance(product[0], scipy_sparse_matrix):
return _scipy_sparse_tensor_product(*product)
def _numpy_eye(n):
"""numpy version of complex eye."""
if not np:
raise ImportError
return np.array(np.eye(n, dtype='complex'))
def _scipy_sparse_eye(n):
"""scipy.sparse version of complex eye."""
if not sparse:
raise ImportError
return sparse.eye(n, n, dtype='complex')
def matrix_eye(n, **options):
"""Get the version of eye and tensor_product for a given format."""
format = options.get('format', 'sympy')
if format == 'sympy':
return eye(n)
elif format == 'numpy':
return _numpy_eye(n)
elif format == 'scipy.sparse':
return _scipy_sparse_eye(n)
raise NotImplementedError('Invalid format: %r' % format)
def _numpy_zeros(m, n, **options):
"""numpy version of zeros."""
dtype = options.get('dtype', 'float64')
if not np:
raise ImportError
return np.zeros((m, n), dtype=dtype)
def _scipy_sparse_zeros(m, n, **options):
"""scipy.sparse version of zeros."""
spmatrix = options.get('spmatrix', 'csr')
dtype = options.get('dtype', 'float64')
if not sparse:
raise ImportError
if spmatrix == 'lil':
return sparse.lil_matrix((m, n), dtype=dtype)
elif spmatrix == 'csr':
return sparse.csr_matrix((m, n), dtype=dtype)
def matrix_zeros(m, n, **options):
""""Get a zeros matrix for a given format."""
format = options.get('format', 'sympy')
if format == 'sympy':
return zeros(m, n)
elif format == 'numpy':
return _numpy_zeros(m, n, **options)
elif format == 'scipy.sparse':
return _scipy_sparse_zeros(m, n, **options)
raise NotImplementedError('Invaild format: %r' % format)
def _numpy_matrix_to_zero(e):
"""Convert a numpy zero matrix to the zero scalar."""
if not np:
raise ImportError
test = np.zeros_like(e)
if np.allclose(e, test):
return 0.0
else:
return e
def _scipy_sparse_matrix_to_zero(e):
"""Convert a scipy.sparse zero matrix to the zero scalar."""
if not np:
raise ImportError
edense = e.todense()
test = np.zeros_like(edense)
if np.allclose(edense, test):
return 0.0
else:
return e
def matrix_to_zero(e):
"""Convert a zero matrix to the scalar zero."""
if isinstance(e, MatrixBase):
if zeros(*e.shape) == e:
e = S.Zero
elif isinstance(e, numpy_ndarray):
e = _numpy_matrix_to_zero(e)
elif isinstance(e, scipy_sparse_matrix):
e = _scipy_sparse_matrix_to_zero(e)
return e

View File

@ -0,0 +1,657 @@
"""Quantum mechanical operators.
TODO:
* Fix early 0 in apply_operators.
* Debug and test apply_operators.
* Get cse working with classes in this file.
* Doctests and documentation of special methods for InnerProduct, Commutator,
AntiCommutator, represent, apply_operators.
"""
from typing import Optional
from sympy.core.add import Add
from sympy.core.expr import Expr
from sympy.core.function import (Derivative, expand)
from sympy.core.mul import Mul
from sympy.core.numbers import oo
from sympy.core.singleton import S
from sympy.printing.pretty.stringpict import prettyForm
from sympy.physics.quantum.dagger import Dagger
from sympy.physics.quantum.qexpr import QExpr, dispatch_method
from sympy.matrices import eye
__all__ = [
'Operator',
'HermitianOperator',
'UnitaryOperator',
'IdentityOperator',
'OuterProduct',
'DifferentialOperator'
]
#-----------------------------------------------------------------------------
# Operators and outer products
#-----------------------------------------------------------------------------
class Operator(QExpr):
"""Base class for non-commuting quantum operators.
An operator maps between quantum states [1]_. In quantum mechanics,
observables (including, but not limited to, measured physical values) are
represented as Hermitian operators [2]_.
Parameters
==========
args : tuple
The list of numbers or parameters that uniquely specify the
operator. For time-dependent operators, this will include the time.
Examples
========
Create an operator and examine its attributes::
>>> from sympy.physics.quantum import Operator
>>> from sympy import I
>>> A = Operator('A')
>>> A
A
>>> A.hilbert_space
H
>>> A.label
(A,)
>>> A.is_commutative
False
Create another operator and do some arithmetic operations::
>>> B = Operator('B')
>>> C = 2*A*A + I*B
>>> C
2*A**2 + I*B
Operators do not commute::
>>> A.is_commutative
False
>>> B.is_commutative
False
>>> A*B == B*A
False
Polymonials of operators respect the commutation properties::
>>> e = (A+B)**3
>>> e.expand()
A*B*A + A*B**2 + A**2*B + A**3 + B*A*B + B*A**2 + B**2*A + B**3
Operator inverses are handle symbolically::
>>> A.inv()
A**(-1)
>>> A*A.inv()
1
References
==========
.. [1] https://en.wikipedia.org/wiki/Operator_%28physics%29
.. [2] https://en.wikipedia.org/wiki/Observable
"""
is_hermitian: Optional[bool] = None
is_unitary: Optional[bool] = None
@classmethod
def default_args(self):
return ("O",)
#-------------------------------------------------------------------------
# Printing
#-------------------------------------------------------------------------
_label_separator = ','
def _print_operator_name(self, printer, *args):
return self.__class__.__name__
_print_operator_name_latex = _print_operator_name
def _print_operator_name_pretty(self, printer, *args):
return prettyForm(self.__class__.__name__)
def _print_contents(self, printer, *args):
if len(self.label) == 1:
return self._print_label(printer, *args)
else:
return '%s(%s)' % (
self._print_operator_name(printer, *args),
self._print_label(printer, *args)
)
def _print_contents_pretty(self, printer, *args):
if len(self.label) == 1:
return self._print_label_pretty(printer, *args)
else:
pform = self._print_operator_name_pretty(printer, *args)
label_pform = self._print_label_pretty(printer, *args)
label_pform = prettyForm(
*label_pform.parens(left='(', right=')')
)
pform = prettyForm(*pform.right(label_pform))
return pform
def _print_contents_latex(self, printer, *args):
if len(self.label) == 1:
return self._print_label_latex(printer, *args)
else:
return r'%s\left(%s\right)' % (
self._print_operator_name_latex(printer, *args),
self._print_label_latex(printer, *args)
)
#-------------------------------------------------------------------------
# _eval_* methods
#-------------------------------------------------------------------------
def _eval_commutator(self, other, **options):
"""Evaluate [self, other] if known, return None if not known."""
return dispatch_method(self, '_eval_commutator', other, **options)
def _eval_anticommutator(self, other, **options):
"""Evaluate [self, other] if known."""
return dispatch_method(self, '_eval_anticommutator', other, **options)
#-------------------------------------------------------------------------
# Operator application
#-------------------------------------------------------------------------
def _apply_operator(self, ket, **options):
return dispatch_method(self, '_apply_operator', ket, **options)
def _apply_from_right_to(self, bra, **options):
return None
def matrix_element(self, *args):
raise NotImplementedError('matrix_elements is not defined')
def inverse(self):
return self._eval_inverse()
inv = inverse
def _eval_inverse(self):
return self**(-1)
def __mul__(self, other):
if isinstance(other, IdentityOperator):
return self
return Mul(self, other)
class HermitianOperator(Operator):
"""A Hermitian operator that satisfies H == Dagger(H).
Parameters
==========
args : tuple
The list of numbers or parameters that uniquely specify the
operator. For time-dependent operators, this will include the time.
Examples
========
>>> from sympy.physics.quantum import Dagger, HermitianOperator
>>> H = HermitianOperator('H')
>>> Dagger(H)
H
"""
is_hermitian = True
def _eval_inverse(self):
if isinstance(self, UnitaryOperator):
return self
else:
return Operator._eval_inverse(self)
def _eval_power(self, exp):
if isinstance(self, UnitaryOperator):
# so all eigenvalues of self are 1 or -1
if exp.is_even:
from sympy.core.singleton import S
return S.One # is identity, see Issue 24153.
elif exp.is_odd:
return self
# No simplification in all other cases
return Operator._eval_power(self, exp)
class UnitaryOperator(Operator):
"""A unitary operator that satisfies U*Dagger(U) == 1.
Parameters
==========
args : tuple
The list of numbers or parameters that uniquely specify the
operator. For time-dependent operators, this will include the time.
Examples
========
>>> from sympy.physics.quantum import Dagger, UnitaryOperator
>>> U = UnitaryOperator('U')
>>> U*Dagger(U)
1
"""
is_unitary = True
def _eval_adjoint(self):
return self._eval_inverse()
class IdentityOperator(Operator):
"""An identity operator I that satisfies op * I == I * op == op for any
operator op.
Parameters
==========
N : Integer
Optional parameter that specifies the dimension of the Hilbert space
of operator. This is used when generating a matrix representation.
Examples
========
>>> from sympy.physics.quantum import IdentityOperator
>>> IdentityOperator()
I
"""
is_hermitian = True
is_unitary = True
@property
def dimension(self):
return self.N
@classmethod
def default_args(self):
return (oo,)
def __init__(self, *args, **hints):
if not len(args) in (0, 1):
raise ValueError('0 or 1 parameters expected, got %s' % args)
self.N = args[0] if (len(args) == 1 and args[0]) else oo
def _eval_commutator(self, other, **hints):
return S.Zero
def _eval_anticommutator(self, other, **hints):
return 2 * other
def _eval_inverse(self):
return self
def _eval_adjoint(self):
return self
def _apply_operator(self, ket, **options):
return ket
def _apply_from_right_to(self, bra, **options):
return bra
def _eval_power(self, exp):
return self
def _print_contents(self, printer, *args):
return 'I'
def _print_contents_pretty(self, printer, *args):
return prettyForm('I')
def _print_contents_latex(self, printer, *args):
return r'{\mathcal{I}}'
def __mul__(self, other):
if isinstance(other, (Operator, Dagger)):
return other
return Mul(self, other)
def _represent_default_basis(self, **options):
if not self.N or self.N == oo:
raise NotImplementedError('Cannot represent infinite dimensional' +
' identity operator as a matrix')
format = options.get('format', 'sympy')
if format != 'sympy':
raise NotImplementedError('Representation in format ' +
'%s not implemented.' % format)
return eye(self.N)
class OuterProduct(Operator):
"""An unevaluated outer product between a ket and bra.
This constructs an outer product between any subclass of ``KetBase`` and
``BraBase`` as ``|a><b|``. An ``OuterProduct`` inherits from Operator as they act as
operators in quantum expressions. For reference see [1]_.
Parameters
==========
ket : KetBase
The ket on the left side of the outer product.
bar : BraBase
The bra on the right side of the outer product.
Examples
========
Create a simple outer product by hand and take its dagger::
>>> from sympy.physics.quantum import Ket, Bra, OuterProduct, Dagger
>>> from sympy.physics.quantum import Operator
>>> k = Ket('k')
>>> b = Bra('b')
>>> op = OuterProduct(k, b)
>>> op
|k><b|
>>> op.hilbert_space
H
>>> op.ket
|k>
>>> op.bra
<b|
>>> Dagger(op)
|b><k|
In simple products of kets and bras outer products will be automatically
identified and created::
>>> k*b
|k><b|
But in more complex expressions, outer products are not automatically
created::
>>> A = Operator('A')
>>> A*k*b
A*|k>*<b|
A user can force the creation of an outer product in a complex expression
by using parentheses to group the ket and bra::
>>> A*(k*b)
A*|k><b|
References
==========
.. [1] https://en.wikipedia.org/wiki/Outer_product
"""
is_commutative = False
def __new__(cls, *args, **old_assumptions):
from sympy.physics.quantum.state import KetBase, BraBase
if len(args) != 2:
raise ValueError('2 parameters expected, got %d' % len(args))
ket_expr = expand(args[0])
bra_expr = expand(args[1])
if (isinstance(ket_expr, (KetBase, Mul)) and
isinstance(bra_expr, (BraBase, Mul))):
ket_c, kets = ket_expr.args_cnc()
bra_c, bras = bra_expr.args_cnc()
if len(kets) != 1 or not isinstance(kets[0], KetBase):
raise TypeError('KetBase subclass expected'
', got: %r' % Mul(*kets))
if len(bras) != 1 or not isinstance(bras[0], BraBase):
raise TypeError('BraBase subclass expected'
', got: %r' % Mul(*bras))
if not kets[0].dual_class() == bras[0].__class__:
raise TypeError(
'ket and bra are not dual classes: %r, %r' %
(kets[0].__class__, bras[0].__class__)
)
# TODO: make sure the hilbert spaces of the bra and ket are
# compatible
obj = Expr.__new__(cls, *(kets[0], bras[0]), **old_assumptions)
obj.hilbert_space = kets[0].hilbert_space
return Mul(*(ket_c + bra_c)) * obj
op_terms = []
if isinstance(ket_expr, Add) and isinstance(bra_expr, Add):
for ket_term in ket_expr.args:
for bra_term in bra_expr.args:
op_terms.append(OuterProduct(ket_term, bra_term,
**old_assumptions))
elif isinstance(ket_expr, Add):
for ket_term in ket_expr.args:
op_terms.append(OuterProduct(ket_term, bra_expr,
**old_assumptions))
elif isinstance(bra_expr, Add):
for bra_term in bra_expr.args:
op_terms.append(OuterProduct(ket_expr, bra_term,
**old_assumptions))
else:
raise TypeError(
'Expected ket and bra expression, got: %r, %r' %
(ket_expr, bra_expr)
)
return Add(*op_terms)
@property
def ket(self):
"""Return the ket on the left side of the outer product."""
return self.args[0]
@property
def bra(self):
"""Return the bra on the right side of the outer product."""
return self.args[1]
def _eval_adjoint(self):
return OuterProduct(Dagger(self.bra), Dagger(self.ket))
def _sympystr(self, printer, *args):
return printer._print(self.ket) + printer._print(self.bra)
def _sympyrepr(self, printer, *args):
return '%s(%s,%s)' % (self.__class__.__name__,
printer._print(self.ket, *args), printer._print(self.bra, *args))
def _pretty(self, printer, *args):
pform = self.ket._pretty(printer, *args)
return prettyForm(*pform.right(self.bra._pretty(printer, *args)))
def _latex(self, printer, *args):
k = printer._print(self.ket, *args)
b = printer._print(self.bra, *args)
return k + b
def _represent(self, **options):
k = self.ket._represent(**options)
b = self.bra._represent(**options)
return k*b
def _eval_trace(self, **kwargs):
# TODO if operands are tensorproducts this may be will be handled
# differently.
return self.ket._eval_trace(self.bra, **kwargs)
class DifferentialOperator(Operator):
"""An operator for representing the differential operator, i.e. d/dx
It is initialized by passing two arguments. The first is an arbitrary
expression that involves a function, such as ``Derivative(f(x), x)``. The
second is the function (e.g. ``f(x)``) which we are to replace with the
``Wavefunction`` that this ``DifferentialOperator`` is applied to.
Parameters
==========
expr : Expr
The arbitrary expression which the appropriate Wavefunction is to be
substituted into
func : Expr
A function (e.g. f(x)) which is to be replaced with the appropriate
Wavefunction when this DifferentialOperator is applied
Examples
========
You can define a completely arbitrary expression and specify where the
Wavefunction is to be substituted
>>> from sympy import Derivative, Function, Symbol
>>> from sympy.physics.quantum.operator import DifferentialOperator
>>> from sympy.physics.quantum.state import Wavefunction
>>> from sympy.physics.quantum.qapply import qapply
>>> f = Function('f')
>>> x = Symbol('x')
>>> d = DifferentialOperator(1/x*Derivative(f(x), x), f(x))
>>> w = Wavefunction(x**2, x)
>>> d.function
f(x)
>>> d.variables
(x,)
>>> qapply(d*w)
Wavefunction(2, x)
"""
@property
def variables(self):
"""
Returns the variables with which the function in the specified
arbitrary expression is evaluated
Examples
========
>>> from sympy.physics.quantum.operator import DifferentialOperator
>>> from sympy import Symbol, Function, Derivative
>>> x = Symbol('x')
>>> f = Function('f')
>>> d = DifferentialOperator(1/x*Derivative(f(x), x), f(x))
>>> d.variables
(x,)
>>> y = Symbol('y')
>>> d = DifferentialOperator(Derivative(f(x, y), x) +
... Derivative(f(x, y), y), f(x, y))
>>> d.variables
(x, y)
"""
return self.args[-1].args
@property
def function(self):
"""
Returns the function which is to be replaced with the Wavefunction
Examples
========
>>> from sympy.physics.quantum.operator import DifferentialOperator
>>> from sympy import Function, Symbol, Derivative
>>> x = Symbol('x')
>>> f = Function('f')
>>> d = DifferentialOperator(Derivative(f(x), x), f(x))
>>> d.function
f(x)
>>> y = Symbol('y')
>>> d = DifferentialOperator(Derivative(f(x, y), x) +
... Derivative(f(x, y), y), f(x, y))
>>> d.function
f(x, y)
"""
return self.args[-1]
@property
def expr(self):
"""
Returns the arbitrary expression which is to have the Wavefunction
substituted into it
Examples
========
>>> from sympy.physics.quantum.operator import DifferentialOperator
>>> from sympy import Function, Symbol, Derivative
>>> x = Symbol('x')
>>> f = Function('f')
>>> d = DifferentialOperator(Derivative(f(x), x), f(x))
>>> d.expr
Derivative(f(x), x)
>>> y = Symbol('y')
>>> d = DifferentialOperator(Derivative(f(x, y), x) +
... Derivative(f(x, y), y), f(x, y))
>>> d.expr
Derivative(f(x, y), x) + Derivative(f(x, y), y)
"""
return self.args[0]
@property
def free_symbols(self):
"""
Return the free symbols of the expression.
"""
return self.expr.free_symbols
def _apply_operator_Wavefunction(self, func, **options):
from sympy.physics.quantum.state import Wavefunction
var = self.variables
wf_vars = func.args[1:]
f = self.function
new_expr = self.expr.subs(f, func(*var))
new_expr = new_expr.doit()
return Wavefunction(new_expr, *wf_vars)
def _eval_derivative(self, symbol):
new_expr = Derivative(self.expr, symbol)
return DifferentialOperator(new_expr, self.args[-1])
#-------------------------------------------------------------------------
# Printing
#-------------------------------------------------------------------------
def _print(self, printer, *args):
return '%s(%s)' % (
self._print_operator_name(printer, *args),
self._print_label(printer, *args)
)
def _print_pretty(self, printer, *args):
pform = self._print_operator_name_pretty(printer, *args)
label_pform = self._print_label_pretty(printer, *args)
label_pform = prettyForm(
*label_pform.parens(left='(', right=')')
)
pform = prettyForm(*pform.right(label_pform))
return pform

View File

@ -0,0 +1,290 @@
"""Functions for reordering operator expressions."""
import warnings
from sympy.core.add import Add
from sympy.core.mul import Mul
from sympy.core.numbers import Integer
from sympy.core.power import Pow
from sympy.physics.quantum import Commutator, AntiCommutator
from sympy.physics.quantum.boson import BosonOp
from sympy.physics.quantum.fermion import FermionOp
__all__ = [
'normal_order',
'normal_ordered_form'
]
def _expand_powers(factors):
"""
Helper function for normal_ordered_form and normal_order: Expand a
power expression to a multiplication expression so that that the
expression can be handled by the normal ordering functions.
"""
new_factors = []
for factor in factors.args:
if (isinstance(factor, Pow)
and isinstance(factor.args[1], Integer)
and factor.args[1] > 0):
for n in range(factor.args[1]):
new_factors.append(factor.args[0])
else:
new_factors.append(factor)
return new_factors
def _normal_ordered_form_factor(product, independent=False, recursive_limit=10,
_recursive_depth=0):
"""
Helper function for normal_ordered_form_factor: Write multiplication
expression with bosonic or fermionic operators on normally ordered form,
using the bosonic and fermionic commutation relations. The resulting
operator expression is equivalent to the argument, but will in general be
a sum of operator products instead of a simple product.
"""
factors = _expand_powers(product)
new_factors = []
n = 0
while n < len(factors) - 1:
current, next = factors[n], factors[n + 1]
if any(not isinstance(f, (FermionOp, BosonOp)) for f in (current, next)):
new_factors.append(current)
n += 1
continue
key_1 = (current.is_annihilation, str(current.name))
key_2 = (next.is_annihilation, str(next.name))
if key_1 <= key_2:
new_factors.append(current)
n += 1
continue
n += 2
if current.is_annihilation and not next.is_annihilation:
if isinstance(current, BosonOp) and isinstance(next, BosonOp):
if current.args[0] != next.args[0]:
if independent:
c = 0
else:
c = Commutator(current, next)
new_factors.append(next * current + c)
else:
new_factors.append(next * current + 1)
elif isinstance(current, FermionOp) and isinstance(next, FermionOp):
if current.args[0] != next.args[0]:
if independent:
c = 0
else:
c = AntiCommutator(current, next)
new_factors.append(-next * current + c)
else:
new_factors.append(-next * current + 1)
elif (current.is_annihilation == next.is_annihilation and
isinstance(current, FermionOp) and isinstance(next, FermionOp)):
new_factors.append(-next * current)
else:
new_factors.append(next * current)
if n == len(factors) - 1:
new_factors.append(factors[-1])
if new_factors == factors:
return product
else:
expr = Mul(*new_factors).expand()
return normal_ordered_form(expr,
recursive_limit=recursive_limit,
_recursive_depth=_recursive_depth + 1,
independent=independent)
def _normal_ordered_form_terms(expr, independent=False, recursive_limit=10,
_recursive_depth=0):
"""
Helper function for normal_ordered_form: loop through each term in an
addition expression and call _normal_ordered_form_factor to perform the
factor to an normally ordered expression.
"""
new_terms = []
for term in expr.args:
if isinstance(term, Mul):
new_term = _normal_ordered_form_factor(
term, recursive_limit=recursive_limit,
_recursive_depth=_recursive_depth, independent=independent)
new_terms.append(new_term)
else:
new_terms.append(term)
return Add(*new_terms)
def normal_ordered_form(expr, independent=False, recursive_limit=10,
_recursive_depth=0):
"""Write an expression with bosonic or fermionic operators on normal
ordered form, where each term is normally ordered. Note that this
normal ordered form is equivalent to the original expression.
Parameters
==========
expr : expression
The expression write on normal ordered form.
independent : bool (default False)
Whether to consider operator with different names as operating in
different Hilbert spaces. If False, the (anti-)commutation is left
explicit.
recursive_limit : int (default 10)
The number of allowed recursive applications of the function.
Examples
========
>>> from sympy.physics.quantum import Dagger
>>> from sympy.physics.quantum.boson import BosonOp
>>> from sympy.physics.quantum.operatorordering import normal_ordered_form
>>> a = BosonOp("a")
>>> normal_ordered_form(a * Dagger(a))
1 + Dagger(a)*a
"""
if _recursive_depth > recursive_limit:
warnings.warn("Too many recursions, aborting")
return expr
if isinstance(expr, Add):
return _normal_ordered_form_terms(expr,
recursive_limit=recursive_limit,
_recursive_depth=_recursive_depth,
independent=independent)
elif isinstance(expr, Mul):
return _normal_ordered_form_factor(expr,
recursive_limit=recursive_limit,
_recursive_depth=_recursive_depth,
independent=independent)
else:
return expr
def _normal_order_factor(product, recursive_limit=10, _recursive_depth=0):
"""
Helper function for normal_order: Normal order a multiplication expression
with bosonic or fermionic operators. In general the resulting operator
expression will not be equivalent to original product.
"""
factors = _expand_powers(product)
n = 0
new_factors = []
while n < len(factors) - 1:
if (isinstance(factors[n], BosonOp) and
factors[n].is_annihilation):
# boson
if not isinstance(factors[n + 1], BosonOp):
new_factors.append(factors[n])
else:
if factors[n + 1].is_annihilation:
new_factors.append(factors[n])
else:
if factors[n].args[0] != factors[n + 1].args[0]:
new_factors.append(factors[n + 1] * factors[n])
else:
new_factors.append(factors[n + 1] * factors[n])
n += 1
elif (isinstance(factors[n], FermionOp) and
factors[n].is_annihilation):
# fermion
if not isinstance(factors[n + 1], FermionOp):
new_factors.append(factors[n])
else:
if factors[n + 1].is_annihilation:
new_factors.append(factors[n])
else:
if factors[n].args[0] != factors[n + 1].args[0]:
new_factors.append(-factors[n + 1] * factors[n])
else:
new_factors.append(-factors[n + 1] * factors[n])
n += 1
else:
new_factors.append(factors[n])
n += 1
if n == len(factors) - 1:
new_factors.append(factors[-1])
if new_factors == factors:
return product
else:
expr = Mul(*new_factors).expand()
return normal_order(expr,
recursive_limit=recursive_limit,
_recursive_depth=_recursive_depth + 1)
def _normal_order_terms(expr, recursive_limit=10, _recursive_depth=0):
"""
Helper function for normal_order: look through each term in an addition
expression and call _normal_order_factor to perform the normal ordering
on the factors.
"""
new_terms = []
for term in expr.args:
if isinstance(term, Mul):
new_term = _normal_order_factor(term,
recursive_limit=recursive_limit,
_recursive_depth=_recursive_depth)
new_terms.append(new_term)
else:
new_terms.append(term)
return Add(*new_terms)
def normal_order(expr, recursive_limit=10, _recursive_depth=0):
"""Normal order an expression with bosonic or fermionic operators. Note
that this normal order is not equivalent to the original expression, but
the creation and annihilation operators in each term in expr is reordered
so that the expression becomes normal ordered.
Parameters
==========
expr : expression
The expression to normal order.
recursive_limit : int (default 10)
The number of allowed recursive applications of the function.
Examples
========
>>> from sympy.physics.quantum import Dagger
>>> from sympy.physics.quantum.boson import BosonOp
>>> from sympy.physics.quantum.operatorordering import normal_order
>>> a = BosonOp("a")
>>> normal_order(a * Dagger(a))
Dagger(a)*a
"""
if _recursive_depth > recursive_limit:
warnings.warn("Too many recursions, aborting")
return expr
if isinstance(expr, Add):
return _normal_order_terms(expr, recursive_limit=recursive_limit,
_recursive_depth=_recursive_depth)
elif isinstance(expr, Mul):
return _normal_order_factor(expr, recursive_limit=recursive_limit,
_recursive_depth=_recursive_depth)
else:
return expr

View File

@ -0,0 +1,279 @@
""" A module for mapping operators to their corresponding eigenstates
and vice versa
It contains a global dictionary with eigenstate-operator pairings.
If a new state-operator pair is created, this dictionary should be
updated as well.
It also contains functions operators_to_state and state_to_operators
for mapping between the two. These can handle both classes and
instances of operators and states. See the individual function
descriptions for details.
TODO List:
- Update the dictionary with a complete list of state-operator pairs
"""
from sympy.physics.quantum.cartesian import (XOp, YOp, ZOp, XKet, PxOp, PxKet,
PositionKet3D)
from sympy.physics.quantum.operator import Operator
from sympy.physics.quantum.state import StateBase, BraBase, Ket
from sympy.physics.quantum.spin import (JxOp, JyOp, JzOp, J2Op, JxKet, JyKet,
JzKet)
__all__ = [
'operators_to_state',
'state_to_operators'
]
#state_mapping stores the mappings between states and their associated
#operators or tuples of operators. This should be updated when new
#classes are written! Entries are of the form PxKet : PxOp or
#something like 3DKet : (ROp, ThetaOp, PhiOp)
#frozenset is used so that the reverse mapping can be made
#(regular sets are not hashable because they are mutable
state_mapping = { JxKet: frozenset((J2Op, JxOp)),
JyKet: frozenset((J2Op, JyOp)),
JzKet: frozenset((J2Op, JzOp)),
Ket: Operator,
PositionKet3D: frozenset((XOp, YOp, ZOp)),
PxKet: PxOp,
XKet: XOp }
op_mapping = {v: k for k, v in state_mapping.items()}
def operators_to_state(operators, **options):
""" Returns the eigenstate of the given operator or set of operators
A global function for mapping operator classes to their associated
states. It takes either an Operator or a set of operators and
returns the state associated with these.
This function can handle both instances of a given operator or
just the class itself (i.e. both XOp() and XOp)
There are multiple use cases to consider:
1) A class or set of classes is passed: First, we try to
instantiate default instances for these operators. If this fails,
then the class is simply returned. If we succeed in instantiating
default instances, then we try to call state._operators_to_state
on the operator instances. If this fails, the class is returned.
Otherwise, the instance returned by _operators_to_state is returned.
2) An instance or set of instances is passed: In this case,
state._operators_to_state is called on the instances passed. If
this fails, a state class is returned. If the method returns an
instance, that instance is returned.
In both cases, if the operator class or set does not exist in the
state_mapping dictionary, None is returned.
Parameters
==========
arg: Operator or set
The class or instance of the operator or set of operators
to be mapped to a state
Examples
========
>>> from sympy.physics.quantum.cartesian import XOp, PxOp
>>> from sympy.physics.quantum.operatorset import operators_to_state
>>> from sympy.physics.quantum.operator import Operator
>>> operators_to_state(XOp)
|x>
>>> operators_to_state(XOp())
|x>
>>> operators_to_state(PxOp)
|px>
>>> operators_to_state(PxOp())
|px>
>>> operators_to_state(Operator)
|psi>
>>> operators_to_state(Operator())
|psi>
"""
if not (isinstance(operators, (Operator, set)) or issubclass(operators, Operator)):
raise NotImplementedError("Argument is not an Operator or a set!")
if isinstance(operators, set):
for s in operators:
if not (isinstance(s, Operator)
or issubclass(s, Operator)):
raise NotImplementedError("Set is not all Operators!")
ops = frozenset(operators)
if ops in op_mapping: # ops is a list of classes in this case
#Try to get an object from default instances of the
#operators...if this fails, return the class
try:
op_instances = [op() for op in ops]
ret = _get_state(op_mapping[ops], set(op_instances), **options)
except NotImplementedError:
ret = op_mapping[ops]
return ret
else:
tmp = [type(o) for o in ops]
classes = frozenset(tmp)
if classes in op_mapping:
ret = _get_state(op_mapping[classes], ops, **options)
else:
ret = None
return ret
else:
if operators in op_mapping:
try:
op_instance = operators()
ret = _get_state(op_mapping[operators], op_instance, **options)
except NotImplementedError:
ret = op_mapping[operators]
return ret
elif type(operators) in op_mapping:
return _get_state(op_mapping[type(operators)], operators, **options)
else:
return None
def state_to_operators(state, **options):
""" Returns the operator or set of operators corresponding to the
given eigenstate
A global function for mapping state classes to their associated
operators or sets of operators. It takes either a state class
or instance.
This function can handle both instances of a given state or just
the class itself (i.e. both XKet() and XKet)
There are multiple use cases to consider:
1) A state class is passed: In this case, we first try
instantiating a default instance of the class. If this succeeds,
then we try to call state._state_to_operators on that instance.
If the creation of the default instance or if the calling of
_state_to_operators fails, then either an operator class or set of
operator classes is returned. Otherwise, the appropriate
operator instances are returned.
2) A state instance is returned: Here, state._state_to_operators
is called for the instance. If this fails, then a class or set of
operator classes is returned. Otherwise, the instances are returned.
In either case, if the state's class does not exist in
state_mapping, None is returned.
Parameters
==========
arg: StateBase class or instance (or subclasses)
The class or instance of the state to be mapped to an
operator or set of operators
Examples
========
>>> from sympy.physics.quantum.cartesian import XKet, PxKet, XBra, PxBra
>>> from sympy.physics.quantum.operatorset import state_to_operators
>>> from sympy.physics.quantum.state import Ket, Bra
>>> state_to_operators(XKet)
X
>>> state_to_operators(XKet())
X
>>> state_to_operators(PxKet)
Px
>>> state_to_operators(PxKet())
Px
>>> state_to_operators(PxBra)
Px
>>> state_to_operators(XBra)
X
>>> state_to_operators(Ket)
O
>>> state_to_operators(Bra)
O
"""
if not (isinstance(state, StateBase) or issubclass(state, StateBase)):
raise NotImplementedError("Argument is not a state!")
if state in state_mapping: # state is a class
state_inst = _make_default(state)
try:
ret = _get_ops(state_inst,
_make_set(state_mapping[state]), **options)
except (NotImplementedError, TypeError):
ret = state_mapping[state]
elif type(state) in state_mapping:
ret = _get_ops(state,
_make_set(state_mapping[type(state)]), **options)
elif isinstance(state, BraBase) and state.dual_class() in state_mapping:
ret = _get_ops(state,
_make_set(state_mapping[state.dual_class()]))
elif issubclass(state, BraBase) and state.dual_class() in state_mapping:
state_inst = _make_default(state)
try:
ret = _get_ops(state_inst,
_make_set(state_mapping[state.dual_class()]))
except (NotImplementedError, TypeError):
ret = state_mapping[state.dual_class()]
else:
ret = None
return _make_set(ret)
def _make_default(expr):
# XXX: Catching TypeError like this is a bad way of distinguishing between
# classes and instances. The logic using this function should be rewritten
# somehow.
try:
ret = expr()
except TypeError:
ret = expr
return ret
def _get_state(state_class, ops, **options):
# Try to get a state instance from the operator INSTANCES.
# If this fails, get the class
try:
ret = state_class._operators_to_state(ops, **options)
except NotImplementedError:
ret = _make_default(state_class)
return ret
def _get_ops(state_inst, op_classes, **options):
# Try to get operator instances from the state INSTANCE.
# If this fails, just return the classes
try:
ret = state_inst._state_to_operators(op_classes, **options)
except NotImplementedError:
if isinstance(op_classes, (set, tuple, frozenset)):
ret = tuple(_make_default(x) for x in op_classes)
else:
ret = _make_default(op_classes)
if isinstance(ret, set) and len(ret) == 1:
return ret[0]
return ret
def _make_set(ops):
if isinstance(ops, (tuple, list, frozenset)):
return set(ops)
else:
return ops

View File

@ -0,0 +1,675 @@
"""Pauli operators and states"""
from sympy.core.add import Add
from sympy.core.mul import Mul
from sympy.core.numbers import I
from sympy.core.power import Pow
from sympy.core.singleton import S
from sympy.functions.elementary.exponential import exp
from sympy.physics.quantum import Operator, Ket, Bra
from sympy.physics.quantum import ComplexSpace
from sympy.matrices import Matrix
from sympy.functions.special.tensor_functions import KroneckerDelta
__all__ = [
'SigmaX', 'SigmaY', 'SigmaZ', 'SigmaMinus', 'SigmaPlus', 'SigmaZKet',
'SigmaZBra', 'qsimplify_pauli'
]
class SigmaOpBase(Operator):
"""Pauli sigma operator, base class"""
@property
def name(self):
return self.args[0]
@property
def use_name(self):
return bool(self.args[0]) is not False
@classmethod
def default_args(self):
return (False,)
def __new__(cls, *args, **hints):
return Operator.__new__(cls, *args, **hints)
def _eval_commutator_BosonOp(self, other, **hints):
return S.Zero
class SigmaX(SigmaOpBase):
"""Pauli sigma x operator
Parameters
==========
name : str
An optional string that labels the operator. Pauli operators with
different names commute.
Examples
========
>>> from sympy.physics.quantum import represent
>>> from sympy.physics.quantum.pauli import SigmaX
>>> sx = SigmaX()
>>> sx
SigmaX()
>>> represent(sx)
Matrix([
[0, 1],
[1, 0]])
"""
def __new__(cls, *args, **hints):
return SigmaOpBase.__new__(cls, *args, **hints)
def _eval_commutator_SigmaY(self, other, **hints):
if self.name != other.name:
return S.Zero
else:
return 2 * I * SigmaZ(self.name)
def _eval_commutator_SigmaZ(self, other, **hints):
if self.name != other.name:
return S.Zero
else:
return - 2 * I * SigmaY(self.name)
def _eval_commutator_BosonOp(self, other, **hints):
return S.Zero
def _eval_anticommutator_SigmaY(self, other, **hints):
return S.Zero
def _eval_anticommutator_SigmaZ(self, other, **hints):
return S.Zero
def _eval_adjoint(self):
return self
def _print_contents_latex(self, printer, *args):
if self.use_name:
return r'{\sigma_x^{(%s)}}' % str(self.name)
else:
return r'{\sigma_x}'
def _print_contents(self, printer, *args):
return 'SigmaX()'
def _eval_power(self, e):
if e.is_Integer and e.is_positive:
return SigmaX(self.name).__pow__(int(e) % 2)
def _represent_default_basis(self, **options):
format = options.get('format', 'sympy')
if format == 'sympy':
return Matrix([[0, 1], [1, 0]])
else:
raise NotImplementedError('Representation in format ' +
format + ' not implemented.')
class SigmaY(SigmaOpBase):
"""Pauli sigma y operator
Parameters
==========
name : str
An optional string that labels the operator. Pauli operators with
different names commute.
Examples
========
>>> from sympy.physics.quantum import represent
>>> from sympy.physics.quantum.pauli import SigmaY
>>> sy = SigmaY()
>>> sy
SigmaY()
>>> represent(sy)
Matrix([
[0, -I],
[I, 0]])
"""
def __new__(cls, *args, **hints):
return SigmaOpBase.__new__(cls, *args)
def _eval_commutator_SigmaZ(self, other, **hints):
if self.name != other.name:
return S.Zero
else:
return 2 * I * SigmaX(self.name)
def _eval_commutator_SigmaX(self, other, **hints):
if self.name != other.name:
return S.Zero
else:
return - 2 * I * SigmaZ(self.name)
def _eval_anticommutator_SigmaX(self, other, **hints):
return S.Zero
def _eval_anticommutator_SigmaZ(self, other, **hints):
return S.Zero
def _eval_adjoint(self):
return self
def _print_contents_latex(self, printer, *args):
if self.use_name:
return r'{\sigma_y^{(%s)}}' % str(self.name)
else:
return r'{\sigma_y}'
def _print_contents(self, printer, *args):
return 'SigmaY()'
def _eval_power(self, e):
if e.is_Integer and e.is_positive:
return SigmaY(self.name).__pow__(int(e) % 2)
def _represent_default_basis(self, **options):
format = options.get('format', 'sympy')
if format == 'sympy':
return Matrix([[0, -I], [I, 0]])
else:
raise NotImplementedError('Representation in format ' +
format + ' not implemented.')
class SigmaZ(SigmaOpBase):
"""Pauli sigma z operator
Parameters
==========
name : str
An optional string that labels the operator. Pauli operators with
different names commute.
Examples
========
>>> from sympy.physics.quantum import represent
>>> from sympy.physics.quantum.pauli import SigmaZ
>>> sz = SigmaZ()
>>> sz ** 3
SigmaZ()
>>> represent(sz)
Matrix([
[1, 0],
[0, -1]])
"""
def __new__(cls, *args, **hints):
return SigmaOpBase.__new__(cls, *args)
def _eval_commutator_SigmaX(self, other, **hints):
if self.name != other.name:
return S.Zero
else:
return 2 * I * SigmaY(self.name)
def _eval_commutator_SigmaY(self, other, **hints):
if self.name != other.name:
return S.Zero
else:
return - 2 * I * SigmaX(self.name)
def _eval_anticommutator_SigmaX(self, other, **hints):
return S.Zero
def _eval_anticommutator_SigmaY(self, other, **hints):
return S.Zero
def _eval_adjoint(self):
return self
def _print_contents_latex(self, printer, *args):
if self.use_name:
return r'{\sigma_z^{(%s)}}' % str(self.name)
else:
return r'{\sigma_z}'
def _print_contents(self, printer, *args):
return 'SigmaZ()'
def _eval_power(self, e):
if e.is_Integer and e.is_positive:
return SigmaZ(self.name).__pow__(int(e) % 2)
def _represent_default_basis(self, **options):
format = options.get('format', 'sympy')
if format == 'sympy':
return Matrix([[1, 0], [0, -1]])
else:
raise NotImplementedError('Representation in format ' +
format + ' not implemented.')
class SigmaMinus(SigmaOpBase):
"""Pauli sigma minus operator
Parameters
==========
name : str
An optional string that labels the operator. Pauli operators with
different names commute.
Examples
========
>>> from sympy.physics.quantum import represent, Dagger
>>> from sympy.physics.quantum.pauli import SigmaMinus
>>> sm = SigmaMinus()
>>> sm
SigmaMinus()
>>> Dagger(sm)
SigmaPlus()
>>> represent(sm)
Matrix([
[0, 0],
[1, 0]])
"""
def __new__(cls, *args, **hints):
return SigmaOpBase.__new__(cls, *args)
def _eval_commutator_SigmaX(self, other, **hints):
if self.name != other.name:
return S.Zero
else:
return -SigmaZ(self.name)
def _eval_commutator_SigmaY(self, other, **hints):
if self.name != other.name:
return S.Zero
else:
return I * SigmaZ(self.name)
def _eval_commutator_SigmaZ(self, other, **hints):
return 2 * self
def _eval_commutator_SigmaMinus(self, other, **hints):
return SigmaZ(self.name)
def _eval_anticommutator_SigmaZ(self, other, **hints):
return S.Zero
def _eval_anticommutator_SigmaX(self, other, **hints):
return S.One
def _eval_anticommutator_SigmaY(self, other, **hints):
return I * S.NegativeOne
def _eval_anticommutator_SigmaPlus(self, other, **hints):
return S.One
def _eval_adjoint(self):
return SigmaPlus(self.name)
def _eval_power(self, e):
if e.is_Integer and e.is_positive:
return S.Zero
def _print_contents_latex(self, printer, *args):
if self.use_name:
return r'{\sigma_-^{(%s)}}' % str(self.name)
else:
return r'{\sigma_-}'
def _print_contents(self, printer, *args):
return 'SigmaMinus()'
def _represent_default_basis(self, **options):
format = options.get('format', 'sympy')
if format == 'sympy':
return Matrix([[0, 0], [1, 0]])
else:
raise NotImplementedError('Representation in format ' +
format + ' not implemented.')
class SigmaPlus(SigmaOpBase):
"""Pauli sigma plus operator
Parameters
==========
name : str
An optional string that labels the operator. Pauli operators with
different names commute.
Examples
========
>>> from sympy.physics.quantum import represent, Dagger
>>> from sympy.physics.quantum.pauli import SigmaPlus
>>> sp = SigmaPlus()
>>> sp
SigmaPlus()
>>> Dagger(sp)
SigmaMinus()
>>> represent(sp)
Matrix([
[0, 1],
[0, 0]])
"""
def __new__(cls, *args, **hints):
return SigmaOpBase.__new__(cls, *args)
def _eval_commutator_SigmaX(self, other, **hints):
if self.name != other.name:
return S.Zero
else:
return SigmaZ(self.name)
def _eval_commutator_SigmaY(self, other, **hints):
if self.name != other.name:
return S.Zero
else:
return I * SigmaZ(self.name)
def _eval_commutator_SigmaZ(self, other, **hints):
if self.name != other.name:
return S.Zero
else:
return -2 * self
def _eval_commutator_SigmaMinus(self, other, **hints):
return SigmaZ(self.name)
def _eval_anticommutator_SigmaZ(self, other, **hints):
return S.Zero
def _eval_anticommutator_SigmaX(self, other, **hints):
return S.One
def _eval_anticommutator_SigmaY(self, other, **hints):
return I
def _eval_anticommutator_SigmaMinus(self, other, **hints):
return S.One
def _eval_adjoint(self):
return SigmaMinus(self.name)
def _eval_mul(self, other):
return self * other
def _eval_power(self, e):
if e.is_Integer and e.is_positive:
return S.Zero
def _print_contents_latex(self, printer, *args):
if self.use_name:
return r'{\sigma_+^{(%s)}}' % str(self.name)
else:
return r'{\sigma_+}'
def _print_contents(self, printer, *args):
return 'SigmaPlus()'
def _represent_default_basis(self, **options):
format = options.get('format', 'sympy')
if format == 'sympy':
return Matrix([[0, 1], [0, 0]])
else:
raise NotImplementedError('Representation in format ' +
format + ' not implemented.')
class SigmaZKet(Ket):
"""Ket for a two-level system quantum system.
Parameters
==========
n : Number
The state number (0 or 1).
"""
def __new__(cls, n):
if n not in (0, 1):
raise ValueError("n must be 0 or 1")
return Ket.__new__(cls, n)
@property
def n(self):
return self.label[0]
@classmethod
def dual_class(self):
return SigmaZBra
@classmethod
def _eval_hilbert_space(cls, label):
return ComplexSpace(2)
def _eval_innerproduct_SigmaZBra(self, bra, **hints):
return KroneckerDelta(self.n, bra.n)
def _apply_from_right_to_SigmaZ(self, op, **options):
if self.n == 0:
return self
else:
return S.NegativeOne * self
def _apply_from_right_to_SigmaX(self, op, **options):
return SigmaZKet(1) if self.n == 0 else SigmaZKet(0)
def _apply_from_right_to_SigmaY(self, op, **options):
return I * SigmaZKet(1) if self.n == 0 else (-I) * SigmaZKet(0)
def _apply_from_right_to_SigmaMinus(self, op, **options):
if self.n == 0:
return SigmaZKet(1)
else:
return S.Zero
def _apply_from_right_to_SigmaPlus(self, op, **options):
if self.n == 0:
return S.Zero
else:
return SigmaZKet(0)
def _represent_default_basis(self, **options):
format = options.get('format', 'sympy')
if format == 'sympy':
return Matrix([[1], [0]]) if self.n == 0 else Matrix([[0], [1]])
else:
raise NotImplementedError('Representation in format ' +
format + ' not implemented.')
class SigmaZBra(Bra):
"""Bra for a two-level quantum system.
Parameters
==========
n : Number
The state number (0 or 1).
"""
def __new__(cls, n):
if n not in (0, 1):
raise ValueError("n must be 0 or 1")
return Bra.__new__(cls, n)
@property
def n(self):
return self.label[0]
@classmethod
def dual_class(self):
return SigmaZKet
def _qsimplify_pauli_product(a, b):
"""
Internal helper function for simplifying products of Pauli operators.
"""
if not (isinstance(a, SigmaOpBase) and isinstance(b, SigmaOpBase)):
return Mul(a, b)
if a.name != b.name:
# Pauli matrices with different labels commute; sort by name
if a.name < b.name:
return Mul(a, b)
else:
return Mul(b, a)
elif isinstance(a, SigmaX):
if isinstance(b, SigmaX):
return S.One
if isinstance(b, SigmaY):
return I * SigmaZ(a.name)
if isinstance(b, SigmaZ):
return - I * SigmaY(a.name)
if isinstance(b, SigmaMinus):
return (S.Half + SigmaZ(a.name)/2)
if isinstance(b, SigmaPlus):
return (S.Half - SigmaZ(a.name)/2)
elif isinstance(a, SigmaY):
if isinstance(b, SigmaX):
return - I * SigmaZ(a.name)
if isinstance(b, SigmaY):
return S.One
if isinstance(b, SigmaZ):
return I * SigmaX(a.name)
if isinstance(b, SigmaMinus):
return -I * (S.One + SigmaZ(a.name))/2
if isinstance(b, SigmaPlus):
return I * (S.One - SigmaZ(a.name))/2
elif isinstance(a, SigmaZ):
if isinstance(b, SigmaX):
return I * SigmaY(a.name)
if isinstance(b, SigmaY):
return - I * SigmaX(a.name)
if isinstance(b, SigmaZ):
return S.One
if isinstance(b, SigmaMinus):
return - SigmaMinus(a.name)
if isinstance(b, SigmaPlus):
return SigmaPlus(a.name)
elif isinstance(a, SigmaMinus):
if isinstance(b, SigmaX):
return (S.One - SigmaZ(a.name))/2
if isinstance(b, SigmaY):
return - I * (S.One - SigmaZ(a.name))/2
if isinstance(b, SigmaZ):
# (SigmaX(a.name) - I * SigmaY(a.name))/2
return SigmaMinus(b.name)
if isinstance(b, SigmaMinus):
return S.Zero
if isinstance(b, SigmaPlus):
return S.Half - SigmaZ(a.name)/2
elif isinstance(a, SigmaPlus):
if isinstance(b, SigmaX):
return (S.One + SigmaZ(a.name))/2
if isinstance(b, SigmaY):
return I * (S.One + SigmaZ(a.name))/2
if isinstance(b, SigmaZ):
#-(SigmaX(a.name) + I * SigmaY(a.name))/2
return -SigmaPlus(a.name)
if isinstance(b, SigmaMinus):
return (S.One + SigmaZ(a.name))/2
if isinstance(b, SigmaPlus):
return S.Zero
else:
return a * b
def qsimplify_pauli(e):
"""
Simplify an expression that includes products of pauli operators.
Parameters
==========
e : expression
An expression that contains products of Pauli operators that is
to be simplified.
Examples
========
>>> from sympy.physics.quantum.pauli import SigmaX, SigmaY
>>> from sympy.physics.quantum.pauli import qsimplify_pauli
>>> sx, sy = SigmaX(), SigmaY()
>>> sx * sy
SigmaX()*SigmaY()
>>> qsimplify_pauli(sx * sy)
I*SigmaZ()
"""
if isinstance(e, Operator):
return e
if isinstance(e, (Add, Pow, exp)):
t = type(e)
return t(*(qsimplify_pauli(arg) for arg in e.args))
if isinstance(e, Mul):
c, nc = e.args_cnc()
nc_s = []
while nc:
curr = nc.pop(0)
while (len(nc) and
isinstance(curr, SigmaOpBase) and
isinstance(nc[0], SigmaOpBase) and
curr.name == nc[0].name):
x = nc.pop(0)
y = _qsimplify_pauli_product(curr, x)
c1, nc1 = y.args_cnc()
curr = Mul(*nc1)
c = c + c1
nc_s.append(curr)
return Mul(*c) * Mul(*nc_s)
return e

View File

@ -0,0 +1,72 @@
"""1D quantum particle in a box."""
from sympy.core.numbers import pi
from sympy.core.singleton import S
from sympy.core.symbol import Symbol
from sympy.functions.elementary.miscellaneous import sqrt
from sympy.functions.elementary.trigonometric import sin
from sympy.sets.sets import Interval
from sympy.physics.quantum.operator import HermitianOperator
from sympy.physics.quantum.state import Ket, Bra
from sympy.physics.quantum.constants import hbar
from sympy.functions.special.tensor_functions import KroneckerDelta
from sympy.physics.quantum.hilbert import L2
m = Symbol('m')
L = Symbol('L')
__all__ = [
'PIABHamiltonian',
'PIABKet',
'PIABBra'
]
class PIABHamiltonian(HermitianOperator):
"""Particle in a box Hamiltonian operator."""
@classmethod
def _eval_hilbert_space(cls, label):
return L2(Interval(S.NegativeInfinity, S.Infinity))
def _apply_operator_PIABKet(self, ket, **options):
n = ket.label[0]
return (n**2*pi**2*hbar**2)/(2*m*L**2)*ket
class PIABKet(Ket):
"""Particle in a box eigenket."""
@classmethod
def _eval_hilbert_space(cls, args):
return L2(Interval(S.NegativeInfinity, S.Infinity))
@classmethod
def dual_class(self):
return PIABBra
def _represent_default_basis(self, **options):
return self._represent_XOp(None, **options)
def _represent_XOp(self, basis, **options):
x = Symbol('x')
n = Symbol('n')
subs_info = options.get('subs', {})
return sqrt(2/L)*sin(n*pi*x/L).subs(subs_info)
def _eval_innerproduct_PIABBra(self, bra):
return KroneckerDelta(bra.label[0], self.label[0])
class PIABBra(Bra):
"""Particle in a box eigenbra."""
@classmethod
def _eval_hilbert_space(cls, label):
return L2(Interval(S.NegativeInfinity, S.Infinity))
@classmethod
def dual_class(self):
return PIABKet

View File

@ -0,0 +1,212 @@
"""Logic for applying operators to states.
Todo:
* Sometimes the final result needs to be expanded, we should do this by hand.
"""
from sympy.core.add import Add
from sympy.core.mul import Mul
from sympy.core.power import Pow
from sympy.core.singleton import S
from sympy.core.sympify import sympify
from sympy.physics.quantum.anticommutator import AntiCommutator
from sympy.physics.quantum.commutator import Commutator
from sympy.physics.quantum.dagger import Dagger
from sympy.physics.quantum.innerproduct import InnerProduct
from sympy.physics.quantum.operator import OuterProduct, Operator
from sympy.physics.quantum.state import State, KetBase, BraBase, Wavefunction
from sympy.physics.quantum.tensorproduct import TensorProduct
__all__ = [
'qapply'
]
#-----------------------------------------------------------------------------
# Main code
#-----------------------------------------------------------------------------
def qapply(e, **options):
"""Apply operators to states in a quantum expression.
Parameters
==========
e : Expr
The expression containing operators and states. This expression tree
will be walked to find operators acting on states symbolically.
options : dict
A dict of key/value pairs that determine how the operator actions
are carried out.
The following options are valid:
* ``dagger``: try to apply Dagger operators to the left
(default: False).
* ``ip_doit``: call ``.doit()`` in inner products when they are
encountered (default: True).
Returns
=======
e : Expr
The original expression, but with the operators applied to states.
Examples
========
>>> from sympy.physics.quantum import qapply, Ket, Bra
>>> b = Bra('b')
>>> k = Ket('k')
>>> A = k * b
>>> A
|k><b|
>>> qapply(A * b.dual / (b * b.dual))
|k>
>>> qapply(k.dual * A / (k.dual * k), dagger=True)
<b|
>>> qapply(k.dual * A / (k.dual * k))
<k|*|k><b|/<k|k>
"""
from sympy.physics.quantum.density import Density
dagger = options.get('dagger', False)
if e == 0:
return S.Zero
# This may be a bit aggressive but ensures that everything gets expanded
# to its simplest form before trying to apply operators. This includes
# things like (A+B+C)*|a> and A*(|a>+|b>) and all Commutators and
# TensorProducts. The only problem with this is that if we can't apply
# all the Operators, we have just expanded everything.
# TODO: don't expand the scalars in front of each Mul.
e = e.expand(commutator=True, tensorproduct=True)
# If we just have a raw ket, return it.
if isinstance(e, KetBase):
return e
# We have an Add(a, b, c, ...) and compute
# Add(qapply(a), qapply(b), ...)
elif isinstance(e, Add):
result = 0
for arg in e.args:
result += qapply(arg, **options)
return result.expand()
# For a Density operator call qapply on its state
elif isinstance(e, Density):
new_args = [(qapply(state, **options), prob) for (state,
prob) in e.args]
return Density(*new_args)
# For a raw TensorProduct, call qapply on its args.
elif isinstance(e, TensorProduct):
return TensorProduct(*[qapply(t, **options) for t in e.args])
# For a Pow, call qapply on its base.
elif isinstance(e, Pow):
return qapply(e.base, **options)**e.exp
# We have a Mul where there might be actual operators to apply to kets.
elif isinstance(e, Mul):
c_part, nc_part = e.args_cnc()
c_mul = Mul(*c_part)
nc_mul = Mul(*nc_part)
if isinstance(nc_mul, Mul):
result = c_mul*qapply_Mul(nc_mul, **options)
else:
result = c_mul*qapply(nc_mul, **options)
if result == e and dagger:
return Dagger(qapply_Mul(Dagger(e), **options))
else:
return result
# In all other cases (State, Operator, Pow, Commutator, InnerProduct,
# OuterProduct) we won't ever have operators to apply to kets.
else:
return e
def qapply_Mul(e, **options):
ip_doit = options.get('ip_doit', True)
args = list(e.args)
# If we only have 0 or 1 args, we have nothing to do and return.
if len(args) <= 1 or not isinstance(e, Mul):
return e
rhs = args.pop()
lhs = args.pop()
# Make sure we have two non-commutative objects before proceeding.
if (not isinstance(rhs, Wavefunction) and sympify(rhs).is_commutative) or \
(not isinstance(lhs, Wavefunction) and sympify(lhs).is_commutative):
return e
# For a Pow with an integer exponent, apply one of them and reduce the
# exponent by one.
if isinstance(lhs, Pow) and lhs.exp.is_Integer:
args.append(lhs.base**(lhs.exp - 1))
lhs = lhs.base
# Pull OuterProduct apart
if isinstance(lhs, OuterProduct):
args.append(lhs.ket)
lhs = lhs.bra
# Call .doit() on Commutator/AntiCommutator.
if isinstance(lhs, (Commutator, AntiCommutator)):
comm = lhs.doit()
if isinstance(comm, Add):
return qapply(
e.func(*(args + [comm.args[0], rhs])) +
e.func(*(args + [comm.args[1], rhs])),
**options
)
else:
return qapply(e.func(*args)*comm*rhs, **options)
# Apply tensor products of operators to states
if isinstance(lhs, TensorProduct) and all(isinstance(arg, (Operator, State, Mul, Pow)) or arg == 1 for arg in lhs.args) and \
isinstance(rhs, TensorProduct) and all(isinstance(arg, (Operator, State, Mul, Pow)) or arg == 1 for arg in rhs.args) and \
len(lhs.args) == len(rhs.args):
result = TensorProduct(*[qapply(lhs.args[n]*rhs.args[n], **options) for n in range(len(lhs.args))]).expand(tensorproduct=True)
return qapply_Mul(e.func(*args), **options)*result
# Now try to actually apply the operator and build an inner product.
try:
result = lhs._apply_operator(rhs, **options)
except NotImplementedError:
result = None
if result is None:
_apply_right = getattr(rhs, '_apply_from_right_to', None)
if _apply_right is not None:
try:
result = _apply_right(lhs, **options)
except NotImplementedError:
result = None
if result is None:
if isinstance(lhs, BraBase) and isinstance(rhs, KetBase):
result = InnerProduct(lhs, rhs)
if ip_doit:
result = result.doit()
# TODO: I may need to expand before returning the final result.
if result == 0:
return S.Zero
elif result is None:
if len(args) == 0:
# We had two args to begin with so args=[].
return e
else:
return qapply_Mul(e.func(*(args + [lhs])), **options)*rhs
elif isinstance(result, InnerProduct):
return result*qapply_Mul(e.func(*args), **options)
else: # result is a scalar times a Mul, Add or TensorProduct
return qapply(e.func(*args)*result, **options)

View File

@ -0,0 +1,224 @@
"""
qasm.py - Functions to parse a set of qasm commands into a SymPy Circuit.
Examples taken from Chuang's page: https://web.archive.org/web/20220120121541/https://www.media.mit.edu/quanta/qasm2circ/
The code returns a circuit and an associated list of labels.
>>> from sympy.physics.quantum.qasm import Qasm
>>> q = Qasm('qubit q0', 'qubit q1', 'h q0', 'cnot q0,q1')
>>> q.get_circuit()
CNOT(1,0)*H(1)
>>> q = Qasm('qubit q0', 'qubit q1', 'cnot q0,q1', 'cnot q1,q0', 'cnot q0,q1')
>>> q.get_circuit()
CNOT(1,0)*CNOT(0,1)*CNOT(1,0)
"""
__all__ = [
'Qasm',
]
from math import prod
from sympy.physics.quantum.gate import H, CNOT, X, Z, CGate, CGateS, SWAP, S, T,CPHASE
from sympy.physics.quantum.circuitplot import Mz
def read_qasm(lines):
return Qasm(*lines.splitlines())
def read_qasm_file(filename):
return Qasm(*open(filename).readlines())
def flip_index(i, n):
"""Reorder qubit indices from largest to smallest.
>>> from sympy.physics.quantum.qasm import flip_index
>>> flip_index(0, 2)
1
>>> flip_index(1, 2)
0
"""
return n-i-1
def trim(line):
"""Remove everything following comment # characters in line.
>>> from sympy.physics.quantum.qasm import trim
>>> trim('nothing happens here')
'nothing happens here'
>>> trim('something #happens here')
'something '
"""
if '#' not in line:
return line
return line.split('#')[0]
def get_index(target, labels):
"""Get qubit labels from the rest of the line,and return indices
>>> from sympy.physics.quantum.qasm import get_index
>>> get_index('q0', ['q0', 'q1'])
1
>>> get_index('q1', ['q0', 'q1'])
0
"""
nq = len(labels)
return flip_index(labels.index(target), nq)
def get_indices(targets, labels):
return [get_index(t, labels) for t in targets]
def nonblank(args):
for line in args:
line = trim(line)
if line.isspace():
continue
yield line
return
def fullsplit(line):
words = line.split()
rest = ' '.join(words[1:])
return fixcommand(words[0]), [s.strip() for s in rest.split(',')]
def fixcommand(c):
"""Fix Qasm command names.
Remove all of forbidden characters from command c, and
replace 'def' with 'qdef'.
"""
forbidden_characters = ['-']
c = c.lower()
for char in forbidden_characters:
c = c.replace(char, '')
if c == 'def':
return 'qdef'
return c
def stripquotes(s):
"""Replace explicit quotes in a string.
>>> from sympy.physics.quantum.qasm import stripquotes
>>> stripquotes("'S'") == 'S'
True
>>> stripquotes('"S"') == 'S'
True
>>> stripquotes('S') == 'S'
True
"""
s = s.replace('"', '') # Remove second set of quotes?
s = s.replace("'", '')
return s
class Qasm:
"""Class to form objects from Qasm lines
>>> from sympy.physics.quantum.qasm import Qasm
>>> q = Qasm('qubit q0', 'qubit q1', 'h q0', 'cnot q0,q1')
>>> q.get_circuit()
CNOT(1,0)*H(1)
>>> q = Qasm('qubit q0', 'qubit q1', 'cnot q0,q1', 'cnot q1,q0', 'cnot q0,q1')
>>> q.get_circuit()
CNOT(1,0)*CNOT(0,1)*CNOT(1,0)
"""
def __init__(self, *args, **kwargs):
self.defs = {}
self.circuit = []
self.labels = []
self.inits = {}
self.add(*args)
self.kwargs = kwargs
def add(self, *lines):
for line in nonblank(lines):
command, rest = fullsplit(line)
if self.defs.get(command): #defs come first, since you can override built-in
function = self.defs.get(command)
indices = self.indices(rest)
if len(indices) == 1:
self.circuit.append(function(indices[0]))
else:
self.circuit.append(function(indices[:-1], indices[-1]))
elif hasattr(self, command):
function = getattr(self, command)
function(*rest)
else:
print("Function %s not defined. Skipping" % command)
def get_circuit(self):
return prod(reversed(self.circuit))
def get_labels(self):
return list(reversed(self.labels))
def plot(self):
from sympy.physics.quantum.circuitplot import CircuitPlot
circuit, labels = self.get_circuit(), self.get_labels()
CircuitPlot(circuit, len(labels), labels=labels, inits=self.inits)
def qubit(self, arg, init=None):
self.labels.append(arg)
if init: self.inits[arg] = init
def indices(self, args):
return get_indices(args, self.labels)
def index(self, arg):
return get_index(arg, self.labels)
def nop(self, *args):
pass
def x(self, arg):
self.circuit.append(X(self.index(arg)))
def z(self, arg):
self.circuit.append(Z(self.index(arg)))
def h(self, arg):
self.circuit.append(H(self.index(arg)))
def s(self, arg):
self.circuit.append(S(self.index(arg)))
def t(self, arg):
self.circuit.append(T(self.index(arg)))
def measure(self, arg):
self.circuit.append(Mz(self.index(arg)))
def cnot(self, a1, a2):
self.circuit.append(CNOT(*self.indices([a1, a2])))
def swap(self, a1, a2):
self.circuit.append(SWAP(*self.indices([a1, a2])))
def cphase(self, a1, a2):
self.circuit.append(CPHASE(*self.indices([a1, a2])))
def toffoli(self, a1, a2, a3):
i1, i2, i3 = self.indices([a1, a2, a3])
self.circuit.append(CGateS((i1, i2), X(i3)))
def cx(self, a1, a2):
fi, fj = self.indices([a1, a2])
self.circuit.append(CGate(fi, X(fj)))
def cz(self, a1, a2):
fi, fj = self.indices([a1, a2])
self.circuit.append(CGate(fi, Z(fj)))
def defbox(self, *args):
print("defbox not supported yet. Skipping: ", args)
def qdef(self, name, ncontrols, symbol):
from sympy.physics.quantum.circuitplot import CreateOneQubitGate, CreateCGate
ncontrols = int(ncontrols)
command = fixcommand(name)
symbol = stripquotes(symbol)
if ncontrols > 0:
self.defs[command] = CreateCGate(symbol)
else:
self.defs[command] = CreateOneQubitGate(symbol)

View File

@ -0,0 +1,413 @@
from sympy.core.expr import Expr
from sympy.core.symbol import Symbol
from sympy.core.sympify import sympify
from sympy.matrices.dense import Matrix
from sympy.printing.pretty.stringpict import prettyForm
from sympy.core.containers import Tuple
from sympy.utilities.iterables import is_sequence
from sympy.physics.quantum.dagger import Dagger
from sympy.physics.quantum.matrixutils import (
numpy_ndarray, scipy_sparse_matrix,
to_sympy, to_numpy, to_scipy_sparse
)
__all__ = [
'QuantumError',
'QExpr'
]
#-----------------------------------------------------------------------------
# Error handling
#-----------------------------------------------------------------------------
class QuantumError(Exception):
pass
def _qsympify_sequence(seq):
"""Convert elements of a sequence to standard form.
This is like sympify, but it performs special logic for arguments passed
to QExpr. The following conversions are done:
* (list, tuple, Tuple) => _qsympify_sequence each element and convert
sequence to a Tuple.
* basestring => Symbol
* Matrix => Matrix
* other => sympify
Strings are passed to Symbol, not sympify to make sure that variables like
'pi' are kept as Symbols, not the SymPy built-in number subclasses.
Examples
========
>>> from sympy.physics.quantum.qexpr import _qsympify_sequence
>>> _qsympify_sequence((1,2,[3,4,[1,]]))
(1, 2, (3, 4, (1,)))
"""
return tuple(__qsympify_sequence_helper(seq))
def __qsympify_sequence_helper(seq):
"""
Helper function for _qsympify_sequence
This function does the actual work.
"""
#base case. If not a list, do Sympification
if not is_sequence(seq):
if isinstance(seq, Matrix):
return seq
elif isinstance(seq, str):
return Symbol(seq)
else:
return sympify(seq)
# base condition, when seq is QExpr and also
# is iterable.
if isinstance(seq, QExpr):
return seq
#if list, recurse on each item in the list
result = [__qsympify_sequence_helper(item) for item in seq]
return Tuple(*result)
#-----------------------------------------------------------------------------
# Basic Quantum Expression from which all objects descend
#-----------------------------------------------------------------------------
class QExpr(Expr):
"""A base class for all quantum object like operators and states."""
# In sympy, slots are for instance attributes that are computed
# dynamically by the __new__ method. They are not part of args, but they
# derive from args.
# The Hilbert space a quantum Object belongs to.
__slots__ = ('hilbert_space', )
is_commutative = False
# The separator used in printing the label.
_label_separator = ''
@property
def free_symbols(self):
return {self}
def __new__(cls, *args, **kwargs):
"""Construct a new quantum object.
Parameters
==========
args : tuple
The list of numbers or parameters that uniquely specify the
quantum object. For a state, this will be its symbol or its
set of quantum numbers.
Examples
========
>>> from sympy.physics.quantum.qexpr import QExpr
>>> q = QExpr(0)
>>> q
0
>>> q.label
(0,)
>>> q.hilbert_space
H
>>> q.args
(0,)
>>> q.is_commutative
False
"""
# First compute args and call Expr.__new__ to create the instance
args = cls._eval_args(args, **kwargs)
if len(args) == 0:
args = cls._eval_args(tuple(cls.default_args()), **kwargs)
inst = Expr.__new__(cls, *args)
# Now set the slots on the instance
inst.hilbert_space = cls._eval_hilbert_space(args)
return inst
@classmethod
def _new_rawargs(cls, hilbert_space, *args, **old_assumptions):
"""Create new instance of this class with hilbert_space and args.
This is used to bypass the more complex logic in the ``__new__``
method in cases where you already have the exact ``hilbert_space``
and ``args``. This should be used when you are positive these
arguments are valid, in their final, proper form and want to optimize
the creation of the object.
"""
obj = Expr.__new__(cls, *args, **old_assumptions)
obj.hilbert_space = hilbert_space
return obj
#-------------------------------------------------------------------------
# Properties
#-------------------------------------------------------------------------
@property
def label(self):
"""The label is the unique set of identifiers for the object.
Usually, this will include all of the information about the state
*except* the time (in the case of time-dependent objects).
This must be a tuple, rather than a Tuple.
"""
if len(self.args) == 0: # If there is no label specified, return the default
return self._eval_args(list(self.default_args()))
else:
return self.args
@property
def is_symbolic(self):
return True
@classmethod
def default_args(self):
"""If no arguments are specified, then this will return a default set
of arguments to be run through the constructor.
NOTE: Any classes that override this MUST return a tuple of arguments.
Should be overridden by subclasses to specify the default arguments for kets and operators
"""
raise NotImplementedError("No default arguments for this class!")
#-------------------------------------------------------------------------
# _eval_* methods
#-------------------------------------------------------------------------
def _eval_adjoint(self):
obj = Expr._eval_adjoint(self)
if obj is None:
obj = Expr.__new__(Dagger, self)
if isinstance(obj, QExpr):
obj.hilbert_space = self.hilbert_space
return obj
@classmethod
def _eval_args(cls, args):
"""Process the args passed to the __new__ method.
This simply runs args through _qsympify_sequence.
"""
return _qsympify_sequence(args)
@classmethod
def _eval_hilbert_space(cls, args):
"""Compute the Hilbert space instance from the args.
"""
from sympy.physics.quantum.hilbert import HilbertSpace
return HilbertSpace()
#-------------------------------------------------------------------------
# Printing
#-------------------------------------------------------------------------
# Utilities for printing: these operate on raw SymPy objects
def _print_sequence(self, seq, sep, printer, *args):
result = []
for item in seq:
result.append(printer._print(item, *args))
return sep.join(result)
def _print_sequence_pretty(self, seq, sep, printer, *args):
pform = printer._print(seq[0], *args)
for item in seq[1:]:
pform = prettyForm(*pform.right(sep))
pform = prettyForm(*pform.right(printer._print(item, *args)))
return pform
# Utilities for printing: these operate prettyForm objects
def _print_subscript_pretty(self, a, b):
top = prettyForm(*b.left(' '*a.width()))
bot = prettyForm(*a.right(' '*b.width()))
return prettyForm(binding=prettyForm.POW, *bot.below(top))
def _print_superscript_pretty(self, a, b):
return a**b
def _print_parens_pretty(self, pform, left='(', right=')'):
return prettyForm(*pform.parens(left=left, right=right))
# Printing of labels (i.e. args)
def _print_label(self, printer, *args):
"""Prints the label of the QExpr
This method prints self.label, using self._label_separator to separate
the elements. This method should not be overridden, instead, override
_print_contents to change printing behavior.
"""
return self._print_sequence(
self.label, self._label_separator, printer, *args
)
def _print_label_repr(self, printer, *args):
return self._print_sequence(
self.label, ',', printer, *args
)
def _print_label_pretty(self, printer, *args):
return self._print_sequence_pretty(
self.label, self._label_separator, printer, *args
)
def _print_label_latex(self, printer, *args):
return self._print_sequence(
self.label, self._label_separator, printer, *args
)
# Printing of contents (default to label)
def _print_contents(self, printer, *args):
"""Printer for contents of QExpr
Handles the printing of any unique identifying contents of a QExpr to
print as its contents, such as any variables or quantum numbers. The
default is to print the label, which is almost always the args. This
should not include printing of any brackets or parentheses.
"""
return self._print_label(printer, *args)
def _print_contents_pretty(self, printer, *args):
return self._print_label_pretty(printer, *args)
def _print_contents_latex(self, printer, *args):
return self._print_label_latex(printer, *args)
# Main printing methods
def _sympystr(self, printer, *args):
"""Default printing behavior of QExpr objects
Handles the default printing of a QExpr. To add other things to the
printing of the object, such as an operator name to operators or
brackets to states, the class should override the _print/_pretty/_latex
functions directly and make calls to _print_contents where appropriate.
This allows things like InnerProduct to easily control its printing the
printing of contents.
"""
return self._print_contents(printer, *args)
def _sympyrepr(self, printer, *args):
classname = self.__class__.__name__
label = self._print_label_repr(printer, *args)
return '%s(%s)' % (classname, label)
def _pretty(self, printer, *args):
pform = self._print_contents_pretty(printer, *args)
return pform
def _latex(self, printer, *args):
return self._print_contents_latex(printer, *args)
#-------------------------------------------------------------------------
# Represent
#-------------------------------------------------------------------------
def _represent_default_basis(self, **options):
raise NotImplementedError('This object does not have a default basis')
def _represent(self, *, basis=None, **options):
"""Represent this object in a given basis.
This method dispatches to the actual methods that perform the
representation. Subclases of QExpr should define various methods to
determine how the object will be represented in various bases. The
format of these methods is::
def _represent_BasisName(self, basis, **options):
Thus to define how a quantum object is represented in the basis of
the operator Position, you would define::
def _represent_Position(self, basis, **options):
Usually, basis object will be instances of Operator subclasses, but
there is a chance we will relax this in the future to accommodate other
types of basis sets that are not associated with an operator.
If the ``format`` option is given it can be ("sympy", "numpy",
"scipy.sparse"). This will ensure that any matrices that result from
representing the object are returned in the appropriate matrix format.
Parameters
==========
basis : Operator
The Operator whose basis functions will be used as the basis for
representation.
options : dict
A dictionary of key/value pairs that give options and hints for
the representation, such as the number of basis functions to
be used.
"""
if basis is None:
result = self._represent_default_basis(**options)
else:
result = dispatch_method(self, '_represent', basis, **options)
# If we get a matrix representation, convert it to the right format.
format = options.get('format', 'sympy')
result = self._format_represent(result, format)
return result
def _format_represent(self, result, format):
if format == 'sympy' and not isinstance(result, Matrix):
return to_sympy(result)
elif format == 'numpy' and not isinstance(result, numpy_ndarray):
return to_numpy(result)
elif format == 'scipy.sparse' and \
not isinstance(result, scipy_sparse_matrix):
return to_scipy_sparse(result)
return result
def split_commutative_parts(e):
"""Split into commutative and non-commutative parts."""
c_part, nc_part = e.args_cnc()
c_part = list(c_part)
return c_part, nc_part
def split_qexpr_parts(e):
"""Split an expression into Expr and noncommutative QExpr parts."""
expr_part = []
qexpr_part = []
for arg in e.args:
if not isinstance(arg, QExpr):
expr_part.append(arg)
else:
qexpr_part.append(arg)
return expr_part, qexpr_part
def dispatch_method(self, basename, arg, **options):
"""Dispatch a method to the proper handlers."""
method_name = '%s_%s' % (basename, arg.__class__.__name__)
if hasattr(self, method_name):
f = getattr(self, method_name)
# This can raise and we will allow it to propagate.
result = f(arg, **options)
if result is not None:
return result
raise NotImplementedError(
"%s.%s cannot handle: %r" %
(self.__class__.__name__, basename, arg)
)

View File

@ -0,0 +1,215 @@
"""An implementation of qubits and gates acting on them.
Todo:
* Update docstrings.
* Update tests.
* Implement apply using decompose.
* Implement represent using decompose or something smarter. For this to
work we first have to implement represent for SWAP.
* Decide if we want upper index to be inclusive in the constructor.
* Fix the printing of Rk gates in plotting.
"""
from sympy.core.expr import Expr
from sympy.core.numbers import (I, Integer, pi)
from sympy.core.symbol import Symbol
from sympy.functions.elementary.exponential import exp
from sympy.matrices.dense import Matrix
from sympy.functions import sqrt
from sympy.physics.quantum.qapply import qapply
from sympy.physics.quantum.qexpr import QuantumError, QExpr
from sympy.matrices import eye
from sympy.physics.quantum.tensorproduct import matrix_tensor_product
from sympy.physics.quantum.gate import (
Gate, HadamardGate, SwapGate, OneQubitGate, CGate, PhaseGate, TGate, ZGate
)
from sympy.functions.elementary.complexes import sign
__all__ = [
'QFT',
'IQFT',
'RkGate',
'Rk'
]
#-----------------------------------------------------------------------------
# Fourier stuff
#-----------------------------------------------------------------------------
class RkGate(OneQubitGate):
"""This is the R_k gate of the QTF."""
gate_name = 'Rk'
gate_name_latex = 'R'
def __new__(cls, *args):
if len(args) != 2:
raise QuantumError(
'Rk gates only take two arguments, got: %r' % args
)
# For small k, Rk gates simplify to other gates, using these
# substitutions give us familiar results for the QFT for small numbers
# of qubits.
target = args[0]
k = args[1]
if k == 1:
return ZGate(target)
elif k == 2:
return PhaseGate(target)
elif k == 3:
return TGate(target)
args = cls._eval_args(args)
inst = Expr.__new__(cls, *args)
inst.hilbert_space = cls._eval_hilbert_space(args)
return inst
@classmethod
def _eval_args(cls, args):
# Fall back to this, because Gate._eval_args assumes that args is
# all targets and can't contain duplicates.
return QExpr._eval_args(args)
@property
def k(self):
return self.label[1]
@property
def targets(self):
return self.label[:1]
@property
def gate_name_plot(self):
return r'$%s_%s$' % (self.gate_name_latex, str(self.k))
def get_target_matrix(self, format='sympy'):
if format == 'sympy':
return Matrix([[1, 0], [0, exp(sign(self.k)*Integer(2)*pi*I/(Integer(2)**abs(self.k)))]])
raise NotImplementedError(
'Invalid format for the R_k gate: %r' % format)
Rk = RkGate
class Fourier(Gate):
"""Superclass of Quantum Fourier and Inverse Quantum Fourier Gates."""
@classmethod
def _eval_args(self, args):
if len(args) != 2:
raise QuantumError(
'QFT/IQFT only takes two arguments, got: %r' % args
)
if args[0] >= args[1]:
raise QuantumError("Start must be smaller than finish")
return Gate._eval_args(args)
def _represent_default_basis(self, **options):
return self._represent_ZGate(None, **options)
def _represent_ZGate(self, basis, **options):
"""
Represents the (I)QFT In the Z Basis
"""
nqubits = options.get('nqubits', 0)
if nqubits == 0:
raise QuantumError(
'The number of qubits must be given as nqubits.')
if nqubits < self.min_qubits:
raise QuantumError(
'The number of qubits %r is too small for the gate.' % nqubits
)
size = self.size
omega = self.omega
#Make a matrix that has the basic Fourier Transform Matrix
arrayFT = [[omega**(
i*j % size)/sqrt(size) for i in range(size)] for j in range(size)]
matrixFT = Matrix(arrayFT)
#Embed the FT Matrix in a higher space, if necessary
if self.label[0] != 0:
matrixFT = matrix_tensor_product(eye(2**self.label[0]), matrixFT)
if self.min_qubits < nqubits:
matrixFT = matrix_tensor_product(
matrixFT, eye(2**(nqubits - self.min_qubits)))
return matrixFT
@property
def targets(self):
return range(self.label[0], self.label[1])
@property
def min_qubits(self):
return self.label[1]
@property
def size(self):
"""Size is the size of the QFT matrix"""
return 2**(self.label[1] - self.label[0])
@property
def omega(self):
return Symbol('omega')
class QFT(Fourier):
"""The forward quantum Fourier transform."""
gate_name = 'QFT'
gate_name_latex = 'QFT'
def decompose(self):
"""Decomposes QFT into elementary gates."""
start = self.label[0]
finish = self.label[1]
circuit = 1
for level in reversed(range(start, finish)):
circuit = HadamardGate(level)*circuit
for i in range(level - start):
circuit = CGate(level - i - 1, RkGate(level, i + 2))*circuit
for i in range((finish - start)//2):
circuit = SwapGate(i + start, finish - i - 1)*circuit
return circuit
def _apply_operator_Qubit(self, qubits, **options):
return qapply(self.decompose()*qubits)
def _eval_inverse(self):
return IQFT(*self.args)
@property
def omega(self):
return exp(2*pi*I/self.size)
class IQFT(Fourier):
"""The inverse quantum Fourier transform."""
gate_name = 'IQFT'
gate_name_latex = '{QFT^{-1}}'
def decompose(self):
"""Decomposes IQFT into elementary gates."""
start = self.args[0]
finish = self.args[1]
circuit = 1
for i in range((finish - start)//2):
circuit = SwapGate(i + start, finish - i - 1)*circuit
for level in range(start, finish):
for i in reversed(range(level - start)):
circuit = CGate(level - i - 1, RkGate(level, -i - 2))*circuit
circuit = HadamardGate(level)*circuit
return circuit
def _eval_inverse(self):
return QFT(*self.args)
@property
def omega(self):
return exp(-2*pi*I/self.size)

View File

@ -0,0 +1,811 @@
"""Qubits for quantum computing.
Todo:
* Finish implementing measurement logic. This should include POVM.
* Update docstrings.
* Update tests.
"""
import math
from sympy.core.add import Add
from sympy.core.mul import Mul
from sympy.core.numbers import Integer
from sympy.core.power import Pow
from sympy.core.singleton import S
from sympy.functions.elementary.complexes import conjugate
from sympy.functions.elementary.exponential import log
from sympy.core.basic import _sympify
from sympy.external.gmpy import SYMPY_INTS
from sympy.matrices import Matrix, zeros
from sympy.printing.pretty.stringpict import prettyForm
from sympy.physics.quantum.hilbert import ComplexSpace
from sympy.physics.quantum.state import Ket, Bra, State
from sympy.physics.quantum.qexpr import QuantumError
from sympy.physics.quantum.represent import represent
from sympy.physics.quantum.matrixutils import (
numpy_ndarray, scipy_sparse_matrix
)
from mpmath.libmp.libintmath import bitcount
__all__ = [
'Qubit',
'QubitBra',
'IntQubit',
'IntQubitBra',
'qubit_to_matrix',
'matrix_to_qubit',
'matrix_to_density',
'measure_all',
'measure_partial',
'measure_partial_oneshot',
'measure_all_oneshot'
]
#-----------------------------------------------------------------------------
# Qubit Classes
#-----------------------------------------------------------------------------
class QubitState(State):
"""Base class for Qubit and QubitBra."""
#-------------------------------------------------------------------------
# Initialization/creation
#-------------------------------------------------------------------------
@classmethod
def _eval_args(cls, args):
# If we are passed a QubitState or subclass, we just take its qubit
# values directly.
if len(args) == 1 and isinstance(args[0], QubitState):
return args[0].qubit_values
# Turn strings into tuple of strings
if len(args) == 1 and isinstance(args[0], str):
args = tuple( S.Zero if qb == "0" else S.One for qb in args[0])
else:
args = tuple( S.Zero if qb == "0" else S.One if qb == "1" else qb for qb in args)
args = tuple(_sympify(arg) for arg in args)
# Validate input (must have 0 or 1 input)
for element in args:
if element not in (S.Zero, S.One):
raise ValueError(
"Qubit values must be 0 or 1, got: %r" % element)
return args
@classmethod
def _eval_hilbert_space(cls, args):
return ComplexSpace(2)**len(args)
#-------------------------------------------------------------------------
# Properties
#-------------------------------------------------------------------------
@property
def dimension(self):
"""The number of Qubits in the state."""
return len(self.qubit_values)
@property
def nqubits(self):
return self.dimension
@property
def qubit_values(self):
"""Returns the values of the qubits as a tuple."""
return self.label
#-------------------------------------------------------------------------
# Special methods
#-------------------------------------------------------------------------
def __len__(self):
return self.dimension
def __getitem__(self, bit):
return self.qubit_values[int(self.dimension - bit - 1)]
#-------------------------------------------------------------------------
# Utility methods
#-------------------------------------------------------------------------
def flip(self, *bits):
"""Flip the bit(s) given."""
newargs = list(self.qubit_values)
for i in bits:
bit = int(self.dimension - i - 1)
if newargs[bit] == 1:
newargs[bit] = 0
else:
newargs[bit] = 1
return self.__class__(*tuple(newargs))
class Qubit(QubitState, Ket):
"""A multi-qubit ket in the computational (z) basis.
We use the normal convention that the least significant qubit is on the
right, so ``|00001>`` has a 1 in the least significant qubit.
Parameters
==========
values : list, str
The qubit values as a list of ints ([0,0,0,1,1,]) or a string ('011').
Examples
========
Create a qubit in a couple of different ways and look at their attributes:
>>> from sympy.physics.quantum.qubit import Qubit
>>> Qubit(0,0,0)
|000>
>>> q = Qubit('0101')
>>> q
|0101>
>>> q.nqubits
4
>>> len(q)
4
>>> q.dimension
4
>>> q.qubit_values
(0, 1, 0, 1)
We can flip the value of an individual qubit:
>>> q.flip(1)
|0111>
We can take the dagger of a Qubit to get a bra:
>>> from sympy.physics.quantum.dagger import Dagger
>>> Dagger(q)
<0101|
>>> type(Dagger(q))
<class 'sympy.physics.quantum.qubit.QubitBra'>
Inner products work as expected:
>>> ip = Dagger(q)*q
>>> ip
<0101|0101>
>>> ip.doit()
1
"""
@classmethod
def dual_class(self):
return QubitBra
def _eval_innerproduct_QubitBra(self, bra, **hints):
if self.label == bra.label:
return S.One
else:
return S.Zero
def _represent_default_basis(self, **options):
return self._represent_ZGate(None, **options)
def _represent_ZGate(self, basis, **options):
"""Represent this qubits in the computational basis (ZGate).
"""
_format = options.get('format', 'sympy')
n = 1
definite_state = 0
for it in reversed(self.qubit_values):
definite_state += n*it
n = n*2
result = [0]*(2**self.dimension)
result[int(definite_state)] = 1
if _format == 'sympy':
return Matrix(result)
elif _format == 'numpy':
import numpy as np
return np.array(result, dtype='complex').transpose()
elif _format == 'scipy.sparse':
from scipy import sparse
return sparse.csr_matrix(result, dtype='complex').transpose()
def _eval_trace(self, bra, **kwargs):
indices = kwargs.get('indices', [])
#sort index list to begin trace from most-significant
#qubit
sorted_idx = list(indices)
if len(sorted_idx) == 0:
sorted_idx = list(range(0, self.nqubits))
sorted_idx.sort()
#trace out for each of index
new_mat = self*bra
for i in range(len(sorted_idx) - 1, -1, -1):
# start from tracing out from leftmost qubit
new_mat = self._reduced_density(new_mat, int(sorted_idx[i]))
if (len(sorted_idx) == self.nqubits):
#in case full trace was requested
return new_mat[0]
else:
return matrix_to_density(new_mat)
def _reduced_density(self, matrix, qubit, **options):
"""Compute the reduced density matrix by tracing out one qubit.
The qubit argument should be of type Python int, since it is used
in bit operations
"""
def find_index_that_is_projected(j, k, qubit):
bit_mask = 2**qubit - 1
return ((j >> qubit) << (1 + qubit)) + (j & bit_mask) + (k << qubit)
old_matrix = represent(matrix, **options)
old_size = old_matrix.cols
#we expect the old_size to be even
new_size = old_size//2
new_matrix = Matrix().zeros(new_size)
for i in range(new_size):
for j in range(new_size):
for k in range(2):
col = find_index_that_is_projected(j, k, qubit)
row = find_index_that_is_projected(i, k, qubit)
new_matrix[i, j] += old_matrix[row, col]
return new_matrix
class QubitBra(QubitState, Bra):
"""A multi-qubit bra in the computational (z) basis.
We use the normal convention that the least significant qubit is on the
right, so ``|00001>`` has a 1 in the least significant qubit.
Parameters
==========
values : list, str
The qubit values as a list of ints ([0,0,0,1,1,]) or a string ('011').
See also
========
Qubit: Examples using qubits
"""
@classmethod
def dual_class(self):
return Qubit
class IntQubitState(QubitState):
"""A base class for qubits that work with binary representations."""
@classmethod
def _eval_args(cls, args, nqubits=None):
# The case of a QubitState instance
if len(args) == 1 and isinstance(args[0], QubitState):
return QubitState._eval_args(args)
# otherwise, args should be integer
elif not all(isinstance(a, (int, Integer)) for a in args):
raise ValueError('values must be integers, got (%s)' % (tuple(type(a) for a in args),))
# use nqubits if specified
if nqubits is not None:
if not isinstance(nqubits, (int, Integer)):
raise ValueError('nqubits must be an integer, got (%s)' % type(nqubits))
if len(args) != 1:
raise ValueError(
'too many positional arguments (%s). should be (number, nqubits=n)' % (args,))
return cls._eval_args_with_nqubits(args[0], nqubits)
# For a single argument, we construct the binary representation of
# that integer with the minimal number of bits.
if len(args) == 1 and args[0] > 1:
#rvalues is the minimum number of bits needed to express the number
rvalues = reversed(range(bitcount(abs(args[0]))))
qubit_values = [(args[0] >> i) & 1 for i in rvalues]
return QubitState._eval_args(qubit_values)
# For two numbers, the second number is the number of bits
# on which it is expressed, so IntQubit(0,5) == |00000>.
elif len(args) == 2 and args[1] > 1:
return cls._eval_args_with_nqubits(args[0], args[1])
else:
return QubitState._eval_args(args)
@classmethod
def _eval_args_with_nqubits(cls, number, nqubits):
need = bitcount(abs(number))
if nqubits < need:
raise ValueError(
'cannot represent %s with %s bits' % (number, nqubits))
qubit_values = [(number >> i) & 1 for i in reversed(range(nqubits))]
return QubitState._eval_args(qubit_values)
def as_int(self):
"""Return the numerical value of the qubit."""
number = 0
n = 1
for i in reversed(self.qubit_values):
number += n*i
n = n << 1
return number
def _print_label(self, printer, *args):
return str(self.as_int())
def _print_label_pretty(self, printer, *args):
label = self._print_label(printer, *args)
return prettyForm(label)
_print_label_repr = _print_label
_print_label_latex = _print_label
class IntQubit(IntQubitState, Qubit):
"""A qubit ket that store integers as binary numbers in qubit values.
The differences between this class and ``Qubit`` are:
* The form of the constructor.
* The qubit values are printed as their corresponding integer, rather
than the raw qubit values. The internal storage format of the qubit
values in the same as ``Qubit``.
Parameters
==========
values : int, tuple
If a single argument, the integer we want to represent in the qubit
values. This integer will be represented using the fewest possible
number of qubits.
If a pair of integers and the second value is more than one, the first
integer gives the integer to represent in binary form and the second
integer gives the number of qubits to use.
List of zeros and ones is also accepted to generate qubit by bit pattern.
nqubits : int
The integer that represents the number of qubits.
This number should be passed with keyword ``nqubits=N``.
You can use this in order to avoid ambiguity of Qubit-style tuple of bits.
Please see the example below for more details.
Examples
========
Create a qubit for the integer 5:
>>> from sympy.physics.quantum.qubit import IntQubit
>>> from sympy.physics.quantum.qubit import Qubit
>>> q = IntQubit(5)
>>> q
|5>
We can also create an ``IntQubit`` by passing a ``Qubit`` instance.
>>> q = IntQubit(Qubit('101'))
>>> q
|5>
>>> q.as_int()
5
>>> q.nqubits
3
>>> q.qubit_values
(1, 0, 1)
We can go back to the regular qubit form.
>>> Qubit(q)
|101>
Please note that ``IntQubit`` also accepts a ``Qubit``-style list of bits.
So, the code below yields qubits 3, not a single bit ``1``.
>>> IntQubit(1, 1)
|3>
To avoid ambiguity, use ``nqubits`` parameter.
Use of this keyword is recommended especially when you provide the values by variables.
>>> IntQubit(1, nqubits=1)
|1>
>>> a = 1
>>> IntQubit(a, nqubits=1)
|1>
"""
@classmethod
def dual_class(self):
return IntQubitBra
def _eval_innerproduct_IntQubitBra(self, bra, **hints):
return Qubit._eval_innerproduct_QubitBra(self, bra)
class IntQubitBra(IntQubitState, QubitBra):
"""A qubit bra that store integers as binary numbers in qubit values."""
@classmethod
def dual_class(self):
return IntQubit
#-----------------------------------------------------------------------------
# Qubit <---> Matrix conversion functions
#-----------------------------------------------------------------------------
def matrix_to_qubit(matrix):
"""Convert from the matrix repr. to a sum of Qubit objects.
Parameters
----------
matrix : Matrix, numpy.matrix, scipy.sparse
The matrix to build the Qubit representation of. This works with
SymPy matrices, numpy matrices and scipy.sparse sparse matrices.
Examples
========
Represent a state and then go back to its qubit form:
>>> from sympy.physics.quantum.qubit import matrix_to_qubit, Qubit
>>> from sympy.physics.quantum.represent import represent
>>> q = Qubit('01')
>>> matrix_to_qubit(represent(q))
|01>
"""
# Determine the format based on the type of the input matrix
format = 'sympy'
if isinstance(matrix, numpy_ndarray):
format = 'numpy'
if isinstance(matrix, scipy_sparse_matrix):
format = 'scipy.sparse'
# Make sure it is of correct dimensions for a Qubit-matrix representation.
# This logic should work with sympy, numpy or scipy.sparse matrices.
if matrix.shape[0] == 1:
mlistlen = matrix.shape[1]
nqubits = log(mlistlen, 2)
ket = False
cls = QubitBra
elif matrix.shape[1] == 1:
mlistlen = matrix.shape[0]
nqubits = log(mlistlen, 2)
ket = True
cls = Qubit
else:
raise QuantumError(
'Matrix must be a row/column vector, got %r' % matrix
)
if not isinstance(nqubits, Integer):
raise QuantumError('Matrix must be a row/column vector of size '
'2**nqubits, got: %r' % matrix)
# Go through each item in matrix, if element is non-zero, make it into a
# Qubit item times the element.
result = 0
for i in range(mlistlen):
if ket:
element = matrix[i, 0]
else:
element = matrix[0, i]
if format in ('numpy', 'scipy.sparse'):
element = complex(element)
if element != 0.0:
# Form Qubit array; 0 in bit-locations where i is 0, 1 in
# bit-locations where i is 1
qubit_array = [int(i & (1 << x) != 0) for x in range(nqubits)]
qubit_array.reverse()
result = result + element*cls(*qubit_array)
# If SymPy simplified by pulling out a constant coefficient, undo that.
if isinstance(result, (Mul, Add, Pow)):
result = result.expand()
return result
def matrix_to_density(mat):
"""
Works by finding the eigenvectors and eigenvalues of the matrix.
We know we can decompose rho by doing:
sum(EigenVal*|Eigenvect><Eigenvect|)
"""
from sympy.physics.quantum.density import Density
eigen = mat.eigenvects()
args = [[matrix_to_qubit(Matrix(
[vector, ])), x[0]] for x in eigen for vector in x[2] if x[0] != 0]
if (len(args) == 0):
return S.Zero
else:
return Density(*args)
def qubit_to_matrix(qubit, format='sympy'):
"""Converts an Add/Mul of Qubit objects into it's matrix representation
This function is the inverse of ``matrix_to_qubit`` and is a shorthand
for ``represent(qubit)``.
"""
return represent(qubit, format=format)
#-----------------------------------------------------------------------------
# Measurement
#-----------------------------------------------------------------------------
def measure_all(qubit, format='sympy', normalize=True):
"""Perform an ensemble measurement of all qubits.
Parameters
==========
qubit : Qubit, Add
The qubit to measure. This can be any Qubit or a linear combination
of them.
format : str
The format of the intermediate matrices to use. Possible values are
('sympy','numpy','scipy.sparse'). Currently only 'sympy' is
implemented.
Returns
=======
result : list
A list that consists of primitive states and their probabilities.
Examples
========
>>> from sympy.physics.quantum.qubit import Qubit, measure_all
>>> from sympy.physics.quantum.gate import H
>>> from sympy.physics.quantum.qapply import qapply
>>> c = H(0)*H(1)*Qubit('00')
>>> c
H(0)*H(1)*|00>
>>> q = qapply(c)
>>> measure_all(q)
[(|00>, 1/4), (|01>, 1/4), (|10>, 1/4), (|11>, 1/4)]
"""
m = qubit_to_matrix(qubit, format)
if format == 'sympy':
results = []
if normalize:
m = m.normalized()
size = max(m.shape) # Max of shape to account for bra or ket
nqubits = int(math.log(size)/math.log(2))
for i in range(size):
if m[i] != 0.0:
results.append(
(Qubit(IntQubit(i, nqubits=nqubits)), m[i]*conjugate(m[i]))
)
return results
else:
raise NotImplementedError(
"This function cannot handle non-SymPy matrix formats yet"
)
def measure_partial(qubit, bits, format='sympy', normalize=True):
"""Perform a partial ensemble measure on the specified qubits.
Parameters
==========
qubits : Qubit
The qubit to measure. This can be any Qubit or a linear combination
of them.
bits : tuple
The qubits to measure.
format : str
The format of the intermediate matrices to use. Possible values are
('sympy','numpy','scipy.sparse'). Currently only 'sympy' is
implemented.
Returns
=======
result : list
A list that consists of primitive states and their probabilities.
Examples
========
>>> from sympy.physics.quantum.qubit import Qubit, measure_partial
>>> from sympy.physics.quantum.gate import H
>>> from sympy.physics.quantum.qapply import qapply
>>> c = H(0)*H(1)*Qubit('00')
>>> c
H(0)*H(1)*|00>
>>> q = qapply(c)
>>> measure_partial(q, (0,))
[(sqrt(2)*|00>/2 + sqrt(2)*|10>/2, 1/2), (sqrt(2)*|01>/2 + sqrt(2)*|11>/2, 1/2)]
"""
m = qubit_to_matrix(qubit, format)
if isinstance(bits, (SYMPY_INTS, Integer)):
bits = (int(bits),)
if format == 'sympy':
if normalize:
m = m.normalized()
possible_outcomes = _get_possible_outcomes(m, bits)
# Form output from function.
output = []
for outcome in possible_outcomes:
# Calculate probability of finding the specified bits with
# given values.
prob_of_outcome = 0
prob_of_outcome += (outcome.H*outcome)[0]
# If the output has a chance, append it to output with found
# probability.
if prob_of_outcome != 0:
if normalize:
next_matrix = matrix_to_qubit(outcome.normalized())
else:
next_matrix = matrix_to_qubit(outcome)
output.append((
next_matrix,
prob_of_outcome
))
return output
else:
raise NotImplementedError(
"This function cannot handle non-SymPy matrix formats yet"
)
def measure_partial_oneshot(qubit, bits, format='sympy'):
"""Perform a partial oneshot measurement on the specified qubits.
A oneshot measurement is equivalent to performing a measurement on a
quantum system. This type of measurement does not return the probabilities
like an ensemble measurement does, but rather returns *one* of the
possible resulting states. The exact state that is returned is determined
by picking a state randomly according to the ensemble probabilities.
Parameters
----------
qubits : Qubit
The qubit to measure. This can be any Qubit or a linear combination
of them.
bits : tuple
The qubits to measure.
format : str
The format of the intermediate matrices to use. Possible values are
('sympy','numpy','scipy.sparse'). Currently only 'sympy' is
implemented.
Returns
-------
result : Qubit
The qubit that the system collapsed to upon measurement.
"""
import random
m = qubit_to_matrix(qubit, format)
if format == 'sympy':
m = m.normalized()
possible_outcomes = _get_possible_outcomes(m, bits)
# Form output from function
random_number = random.random()
total_prob = 0
for outcome in possible_outcomes:
# Calculate probability of finding the specified bits
# with given values
total_prob += (outcome.H*outcome)[0]
if total_prob >= random_number:
return matrix_to_qubit(outcome.normalized())
else:
raise NotImplementedError(
"This function cannot handle non-SymPy matrix formats yet"
)
def _get_possible_outcomes(m, bits):
"""Get the possible states that can be produced in a measurement.
Parameters
----------
m : Matrix
The matrix representing the state of the system.
bits : tuple, list
Which bits will be measured.
Returns
-------
result : list
The list of possible states which can occur given this measurement.
These are un-normalized so we can derive the probability of finding
this state by taking the inner product with itself
"""
# This is filled with loads of dirty binary tricks...You have been warned
size = max(m.shape) # Max of shape to account for bra or ket
nqubits = int(math.log2(size) + .1) # Number of qubits possible
# Make the output states and put in output_matrices, nothing in them now.
# Each state will represent a possible outcome of the measurement
# Thus, output_matrices[0] is the matrix which we get when all measured
# bits return 0. and output_matrices[1] is the matrix for only the 0th
# bit being true
output_matrices = []
for i in range(1 << len(bits)):
output_matrices.append(zeros(2**nqubits, 1))
# Bitmasks will help sort how to determine possible outcomes.
# When the bit mask is and-ed with a matrix-index,
# it will determine which state that index belongs to
bit_masks = []
for bit in bits:
bit_masks.append(1 << bit)
# Make possible outcome states
for i in range(2**nqubits):
trueness = 0 # This tells us to which output_matrix this value belongs
# Find trueness
for j in range(len(bit_masks)):
if i & bit_masks[j]:
trueness += j + 1
# Put the value in the correct output matrix
output_matrices[trueness][i] = m[i]
return output_matrices
def measure_all_oneshot(qubit, format='sympy'):
"""Perform a oneshot ensemble measurement on all qubits.
A oneshot measurement is equivalent to performing a measurement on a
quantum system. This type of measurement does not return the probabilities
like an ensemble measurement does, but rather returns *one* of the
possible resulting states. The exact state that is returned is determined
by picking a state randomly according to the ensemble probabilities.
Parameters
----------
qubits : Qubit
The qubit to measure. This can be any Qubit or a linear combination
of them.
format : str
The format of the intermediate matrices to use. Possible values are
('sympy','numpy','scipy.sparse'). Currently only 'sympy' is
implemented.
Returns
-------
result : Qubit
The qubit that the system collapsed to upon measurement.
"""
import random
m = qubit_to_matrix(qubit)
if format == 'sympy':
m = m.normalized()
random_number = random.random()
total = 0
result = 0
for i in m:
total += i*i.conjugate()
if total > random_number:
break
result += 1
return Qubit(IntQubit(result, int(math.log2(max(m.shape)) + .1)))
else:
raise NotImplementedError(
"This function cannot handle non-SymPy matrix formats yet"
)

View File

@ -0,0 +1,574 @@
"""Logic for representing operators in state in various bases.
TODO:
* Get represent working with continuous hilbert spaces.
* Document default basis functionality.
"""
from sympy.core.add import Add
from sympy.core.expr import Expr
from sympy.core.mul import Mul
from sympy.core.numbers import I
from sympy.core.power import Pow
from sympy.integrals.integrals import integrate
from sympy.physics.quantum.dagger import Dagger
from sympy.physics.quantum.commutator import Commutator
from sympy.physics.quantum.anticommutator import AntiCommutator
from sympy.physics.quantum.innerproduct import InnerProduct
from sympy.physics.quantum.qexpr import QExpr
from sympy.physics.quantum.tensorproduct import TensorProduct
from sympy.physics.quantum.matrixutils import flatten_scalar
from sympy.physics.quantum.state import KetBase, BraBase, StateBase
from sympy.physics.quantum.operator import Operator, OuterProduct
from sympy.physics.quantum.qapply import qapply
from sympy.physics.quantum.operatorset import operators_to_state, state_to_operators
__all__ = [
'represent',
'rep_innerproduct',
'rep_expectation',
'integrate_result',
'get_basis',
'enumerate_states'
]
#-----------------------------------------------------------------------------
# Represent
#-----------------------------------------------------------------------------
def _sympy_to_scalar(e):
"""Convert from a SymPy scalar to a Python scalar."""
if isinstance(e, Expr):
if e.is_Integer:
return int(e)
elif e.is_Float:
return float(e)
elif e.is_Rational:
return float(e)
elif e.is_Number or e.is_NumberSymbol or e == I:
return complex(e)
raise TypeError('Expected number, got: %r' % e)
def represent(expr, **options):
"""Represent the quantum expression in the given basis.
In quantum mechanics abstract states and operators can be represented in
various basis sets. Under this operation the follow transforms happen:
* Ket -> column vector or function
* Bra -> row vector of function
* Operator -> matrix or differential operator
This function is the top-level interface for this action.
This function walks the SymPy expression tree looking for ``QExpr``
instances that have a ``_represent`` method. This method is then called
and the object is replaced by the representation returned by this method.
By default, the ``_represent`` method will dispatch to other methods
that handle the representation logic for a particular basis set. The
naming convention for these methods is the following::
def _represent_FooBasis(self, e, basis, **options)
This function will have the logic for representing instances of its class
in the basis set having a class named ``FooBasis``.
Parameters
==========
expr : Expr
The expression to represent.
basis : Operator, basis set
An object that contains the information about the basis set. If an
operator is used, the basis is assumed to be the orthonormal
eigenvectors of that operator. In general though, the basis argument
can be any object that contains the basis set information.
options : dict
Key/value pairs of options that are passed to the underlying method
that finds the representation. These options can be used to
control how the representation is done. For example, this is where
the size of the basis set would be set.
Returns
=======
e : Expr
The SymPy expression of the represented quantum expression.
Examples
========
Here we subclass ``Operator`` and ``Ket`` to create the z-spin operator
and its spin 1/2 up eigenstate. By defining the ``_represent_SzOp``
method, the ket can be represented in the z-spin basis.
>>> from sympy.physics.quantum import Operator, represent, Ket
>>> from sympy import Matrix
>>> class SzUpKet(Ket):
... def _represent_SzOp(self, basis, **options):
... return Matrix([1,0])
...
>>> class SzOp(Operator):
... pass
...
>>> sz = SzOp('Sz')
>>> up = SzUpKet('up')
>>> represent(up, basis=sz)
Matrix([
[1],
[0]])
Here we see an example of representations in a continuous
basis. We see that the result of representing various combinations
of cartesian position operators and kets give us continuous
expressions involving DiracDelta functions.
>>> from sympy.physics.quantum.cartesian import XOp, XKet, XBra
>>> X = XOp()
>>> x = XKet()
>>> y = XBra('y')
>>> represent(X*x)
x*DiracDelta(x - x_2)
>>> represent(X*x*y)
x*DiracDelta(x - x_3)*DiracDelta(x_1 - y)
"""
format = options.get('format', 'sympy')
if format == 'numpy':
import numpy as np
if isinstance(expr, QExpr) and not isinstance(expr, OuterProduct):
options['replace_none'] = False
temp_basis = get_basis(expr, **options)
if temp_basis is not None:
options['basis'] = temp_basis
try:
return expr._represent(**options)
except NotImplementedError as strerr:
#If no _represent_FOO method exists, map to the
#appropriate basis state and try
#the other methods of representation
options['replace_none'] = True
if isinstance(expr, (KetBase, BraBase)):
try:
return rep_innerproduct(expr, **options)
except NotImplementedError:
raise NotImplementedError(strerr)
elif isinstance(expr, Operator):
try:
return rep_expectation(expr, **options)
except NotImplementedError:
raise NotImplementedError(strerr)
else:
raise NotImplementedError(strerr)
elif isinstance(expr, Add):
result = represent(expr.args[0], **options)
for args in expr.args[1:]:
# scipy.sparse doesn't support += so we use plain = here.
result = result + represent(args, **options)
return result
elif isinstance(expr, Pow):
base, exp = expr.as_base_exp()
if format in ('numpy', 'scipy.sparse'):
exp = _sympy_to_scalar(exp)
base = represent(base, **options)
# scipy.sparse doesn't support negative exponents
# and warns when inverting a matrix in csr format.
if format == 'scipy.sparse' and exp < 0:
from scipy.sparse.linalg import inv
exp = - exp
base = inv(base.tocsc()).tocsr()
if format == 'numpy':
return np.linalg.matrix_power(base, exp)
return base ** exp
elif isinstance(expr, TensorProduct):
new_args = [represent(arg, **options) for arg in expr.args]
return TensorProduct(*new_args)
elif isinstance(expr, Dagger):
return Dagger(represent(expr.args[0], **options))
elif isinstance(expr, Commutator):
A = expr.args[0]
B = expr.args[1]
return represent(Mul(A, B) - Mul(B, A), **options)
elif isinstance(expr, AntiCommutator):
A = expr.args[0]
B = expr.args[1]
return represent(Mul(A, B) + Mul(B, A), **options)
elif isinstance(expr, InnerProduct):
return represent(Mul(expr.bra, expr.ket), **options)
elif not isinstance(expr, (Mul, OuterProduct)):
# For numpy and scipy.sparse, we can only handle numerical prefactors.
if format in ('numpy', 'scipy.sparse'):
return _sympy_to_scalar(expr)
return expr
if not isinstance(expr, (Mul, OuterProduct)):
raise TypeError('Mul expected, got: %r' % expr)
if "index" in options:
options["index"] += 1
else:
options["index"] = 1
if "unities" not in options:
options["unities"] = []
result = represent(expr.args[-1], **options)
last_arg = expr.args[-1]
for arg in reversed(expr.args[:-1]):
if isinstance(last_arg, Operator):
options["index"] += 1
options["unities"].append(options["index"])
elif isinstance(last_arg, BraBase) and isinstance(arg, KetBase):
options["index"] += 1
elif isinstance(last_arg, KetBase) and isinstance(arg, Operator):
options["unities"].append(options["index"])
elif isinstance(last_arg, KetBase) and isinstance(arg, BraBase):
options["unities"].append(options["index"])
next_arg = represent(arg, **options)
if format == 'numpy' and isinstance(next_arg, np.ndarray):
# Must use np.matmult to "matrix multiply" two np.ndarray
result = np.matmul(next_arg, result)
else:
result = next_arg*result
last_arg = arg
# All three matrix formats create 1 by 1 matrices when inner products of
# vectors are taken. In these cases, we simply return a scalar.
result = flatten_scalar(result)
result = integrate_result(expr, result, **options)
return result
def rep_innerproduct(expr, **options):
"""
Returns an innerproduct like representation (e.g. ``<x'|x>``) for the
given state.
Attempts to calculate inner product with a bra from the specified
basis. Should only be passed an instance of KetBase or BraBase
Parameters
==========
expr : KetBase or BraBase
The expression to be represented
Examples
========
>>> from sympy.physics.quantum.represent import rep_innerproduct
>>> from sympy.physics.quantum.cartesian import XOp, XKet, PxOp, PxKet
>>> rep_innerproduct(XKet())
DiracDelta(x - x_1)
>>> rep_innerproduct(XKet(), basis=PxOp())
sqrt(2)*exp(-I*px_1*x/hbar)/(2*sqrt(hbar)*sqrt(pi))
>>> rep_innerproduct(PxKet(), basis=XOp())
sqrt(2)*exp(I*px*x_1/hbar)/(2*sqrt(hbar)*sqrt(pi))
"""
if not isinstance(expr, (KetBase, BraBase)):
raise TypeError("expr passed is not a Bra or Ket")
basis = get_basis(expr, **options)
if not isinstance(basis, StateBase):
raise NotImplementedError("Can't form this representation!")
if "index" not in options:
options["index"] = 1
basis_kets = enumerate_states(basis, options["index"], 2)
if isinstance(expr, BraBase):
bra = expr
ket = (basis_kets[1] if basis_kets[0].dual == expr else basis_kets[0])
else:
bra = (basis_kets[1].dual if basis_kets[0]
== expr else basis_kets[0].dual)
ket = expr
prod = InnerProduct(bra, ket)
result = prod.doit()
format = options.get('format', 'sympy')
return expr._format_represent(result, format)
def rep_expectation(expr, **options):
"""
Returns an ``<x'|A|x>`` type representation for the given operator.
Parameters
==========
expr : Operator
Operator to be represented in the specified basis
Examples
========
>>> from sympy.physics.quantum.cartesian import XOp, PxOp, PxKet
>>> from sympy.physics.quantum.represent import rep_expectation
>>> rep_expectation(XOp())
x_1*DiracDelta(x_1 - x_2)
>>> rep_expectation(XOp(), basis=PxOp())
<px_2|*X*|px_1>
>>> rep_expectation(XOp(), basis=PxKet())
<px_2|*X*|px_1>
"""
if "index" not in options:
options["index"] = 1
if not isinstance(expr, Operator):
raise TypeError("The passed expression is not an operator")
basis_state = get_basis(expr, **options)
if basis_state is None or not isinstance(basis_state, StateBase):
raise NotImplementedError("Could not get basis kets for this operator")
basis_kets = enumerate_states(basis_state, options["index"], 2)
bra = basis_kets[1].dual
ket = basis_kets[0]
return qapply(bra*expr*ket)
def integrate_result(orig_expr, result, **options):
"""
Returns the result of integrating over any unities ``(|x><x|)`` in
the given expression. Intended for integrating over the result of
representations in continuous bases.
This function integrates over any unities that may have been
inserted into the quantum expression and returns the result.
It uses the interval of the Hilbert space of the basis state
passed to it in order to figure out the limits of integration.
The unities option must be
specified for this to work.
Note: This is mostly used internally by represent(). Examples are
given merely to show the use cases.
Parameters
==========
orig_expr : quantum expression
The original expression which was to be represented
result: Expr
The resulting representation that we wish to integrate over
Examples
========
>>> from sympy import symbols, DiracDelta
>>> from sympy.physics.quantum.represent import integrate_result
>>> from sympy.physics.quantum.cartesian import XOp, XKet
>>> x_ket = XKet()
>>> X_op = XOp()
>>> x, x_1, x_2 = symbols('x, x_1, x_2')
>>> integrate_result(X_op*x_ket, x*DiracDelta(x-x_1)*DiracDelta(x_1-x_2))
x*DiracDelta(x - x_1)*DiracDelta(x_1 - x_2)
>>> integrate_result(X_op*x_ket, x*DiracDelta(x-x_1)*DiracDelta(x_1-x_2),
... unities=[1])
x*DiracDelta(x - x_2)
"""
if not isinstance(result, Expr):
return result
options['replace_none'] = True
if "basis" not in options:
arg = orig_expr.args[-1]
options["basis"] = get_basis(arg, **options)
elif not isinstance(options["basis"], StateBase):
options["basis"] = get_basis(orig_expr, **options)
basis = options.pop("basis", None)
if basis is None:
return result
unities = options.pop("unities", [])
if len(unities) == 0:
return result
kets = enumerate_states(basis, unities)
coords = [k.label[0] for k in kets]
for coord in coords:
if coord in result.free_symbols:
#TODO: Add support for sets of operators
basis_op = state_to_operators(basis)
start = basis_op.hilbert_space.interval.start
end = basis_op.hilbert_space.interval.end
result = integrate(result, (coord, start, end))
return result
def get_basis(expr, *, basis=None, replace_none=True, **options):
"""
Returns a basis state instance corresponding to the basis specified in
options=s. If no basis is specified, the function tries to form a default
basis state of the given expression.
There are three behaviors:
1. The basis specified in options is already an instance of StateBase. If
this is the case, it is simply returned. If the class is specified but
not an instance, a default instance is returned.
2. The basis specified is an operator or set of operators. If this
is the case, the operator_to_state mapping method is used.
3. No basis is specified. If expr is a state, then a default instance of
its class is returned. If expr is an operator, then it is mapped to the
corresponding state. If it is neither, then we cannot obtain the basis
state.
If the basis cannot be mapped, then it is not changed.
This will be called from within represent, and represent will
only pass QExpr's.
TODO (?): Support for Muls and other types of expressions?
Parameters
==========
expr : Operator or StateBase
Expression whose basis is sought
Examples
========
>>> from sympy.physics.quantum.represent import get_basis
>>> from sympy.physics.quantum.cartesian import XOp, XKet, PxOp, PxKet
>>> x = XKet()
>>> X = XOp()
>>> get_basis(x)
|x>
>>> get_basis(X)
|x>
>>> get_basis(x, basis=PxOp())
|px>
>>> get_basis(x, basis=PxKet)
|px>
"""
if basis is None and not replace_none:
return None
if basis is None:
if isinstance(expr, KetBase):
return _make_default(expr.__class__)
elif isinstance(expr, BraBase):
return _make_default(expr.dual_class())
elif isinstance(expr, Operator):
state_inst = operators_to_state(expr)
return (state_inst if state_inst is not None else None)
else:
return None
elif (isinstance(basis, Operator) or
(not isinstance(basis, StateBase) and issubclass(basis, Operator))):
state = operators_to_state(basis)
if state is None:
return None
elif isinstance(state, StateBase):
return state
else:
return _make_default(state)
elif isinstance(basis, StateBase):
return basis
elif issubclass(basis, StateBase):
return _make_default(basis)
else:
return None
def _make_default(expr):
# XXX: Catching TypeError like this is a bad way of distinguishing
# instances from classes. The logic using this function should be
# rewritten somehow.
try:
expr = expr()
except TypeError:
return expr
return expr
def enumerate_states(*args, **options):
"""
Returns instances of the given state with dummy indices appended
Operates in two different modes:
1. Two arguments are passed to it. The first is the base state which is to
be indexed, and the second argument is a list of indices to append.
2. Three arguments are passed. The first is again the base state to be
indexed. The second is the start index for counting. The final argument
is the number of kets you wish to receive.
Tries to call state._enumerate_state. If this fails, returns an empty list
Parameters
==========
args : list
See list of operation modes above for explanation
Examples
========
>>> from sympy.physics.quantum.cartesian import XBra, XKet
>>> from sympy.physics.quantum.represent import enumerate_states
>>> test = XKet('foo')
>>> enumerate_states(test, 1, 3)
[|foo_1>, |foo_2>, |foo_3>]
>>> test2 = XBra('bar')
>>> enumerate_states(test2, [4, 5, 10])
[<bar_4|, <bar_5|, <bar_10|]
"""
state = args[0]
if len(args) not in (2, 3):
raise NotImplementedError("Wrong number of arguments!")
if not isinstance(state, StateBase):
raise TypeError("First argument is not a state!")
if len(args) == 3:
num_states = args[2]
options['start_index'] = args[1]
else:
num_states = len(args[1])
options['index_list'] = args[1]
try:
ret = state._enumerate_state(num_states, **options)
except NotImplementedError:
ret = []
return ret

View File

@ -0,0 +1,679 @@
"""Simple Harmonic Oscillator 1-Dimension"""
from sympy.core.numbers import (I, Integer)
from sympy.core.singleton import S
from sympy.core.symbol import Symbol
from sympy.functions.elementary.miscellaneous import sqrt
from sympy.physics.quantum.constants import hbar
from sympy.physics.quantum.operator import Operator
from sympy.physics.quantum.state import Bra, Ket, State
from sympy.physics.quantum.qexpr import QExpr
from sympy.physics.quantum.cartesian import X, Px
from sympy.functions.special.tensor_functions import KroneckerDelta
from sympy.physics.quantum.hilbert import ComplexSpace
from sympy.physics.quantum.matrixutils import matrix_zeros
#------------------------------------------------------------------------------
class SHOOp(Operator):
"""A base class for the SHO Operators.
We are limiting the number of arguments to be 1.
"""
@classmethod
def _eval_args(cls, args):
args = QExpr._eval_args(args)
if len(args) == 1:
return args
else:
raise ValueError("Too many arguments")
@classmethod
def _eval_hilbert_space(cls, label):
return ComplexSpace(S.Infinity)
class RaisingOp(SHOOp):
"""The Raising Operator or a^dagger.
When a^dagger acts on a state it raises the state up by one. Taking
the adjoint of a^dagger returns 'a', the Lowering Operator. a^dagger
can be rewritten in terms of position and momentum. We can represent
a^dagger as a matrix, which will be its default basis.
Parameters
==========
args : tuple
The list of numbers or parameters that uniquely specify the
operator.
Examples
========
Create a Raising Operator and rewrite it in terms of position and
momentum, and show that taking its adjoint returns 'a':
>>> from sympy.physics.quantum.sho1d import RaisingOp
>>> from sympy.physics.quantum import Dagger
>>> ad = RaisingOp('a')
>>> ad.rewrite('xp').doit()
sqrt(2)*(m*omega*X - I*Px)/(2*sqrt(hbar)*sqrt(m*omega))
>>> Dagger(ad)
a
Taking the commutator of a^dagger with other Operators:
>>> from sympy.physics.quantum import Commutator
>>> from sympy.physics.quantum.sho1d import RaisingOp, LoweringOp
>>> from sympy.physics.quantum.sho1d import NumberOp
>>> ad = RaisingOp('a')
>>> a = LoweringOp('a')
>>> N = NumberOp('N')
>>> Commutator(ad, a).doit()
-1
>>> Commutator(ad, N).doit()
-RaisingOp(a)
Apply a^dagger to a state:
>>> from sympy.physics.quantum import qapply
>>> from sympy.physics.quantum.sho1d import RaisingOp, SHOKet
>>> ad = RaisingOp('a')
>>> k = SHOKet('k')
>>> qapply(ad*k)
sqrt(k + 1)*|k + 1>
Matrix Representation
>>> from sympy.physics.quantum.sho1d import RaisingOp
>>> from sympy.physics.quantum.represent import represent
>>> ad = RaisingOp('a')
>>> represent(ad, basis=N, ndim=4, format='sympy')
Matrix([
[0, 0, 0, 0],
[1, 0, 0, 0],
[0, sqrt(2), 0, 0],
[0, 0, sqrt(3), 0]])
"""
def _eval_rewrite_as_xp(self, *args, **kwargs):
return (S.One/sqrt(Integer(2)*hbar*m*omega))*(
S.NegativeOne*I*Px + m*omega*X)
def _eval_adjoint(self):
return LoweringOp(*self.args)
def _eval_commutator_LoweringOp(self, other):
return S.NegativeOne
def _eval_commutator_NumberOp(self, other):
return S.NegativeOne*self
def _apply_operator_SHOKet(self, ket, **options):
temp = ket.n + S.One
return sqrt(temp)*SHOKet(temp)
def _represent_default_basis(self, **options):
return self._represent_NumberOp(None, **options)
def _represent_XOp(self, basis, **options):
# This logic is good but the underlying position
# representation logic is broken.
# temp = self.rewrite('xp').doit()
# result = represent(temp, basis=X)
# return result
raise NotImplementedError('Position representation is not implemented')
def _represent_NumberOp(self, basis, **options):
ndim_info = options.get('ndim', 4)
format = options.get('format','sympy')
matrix = matrix_zeros(ndim_info, ndim_info, **options)
for i in range(ndim_info - 1):
value = sqrt(i + 1)
if format == 'scipy.sparse':
value = float(value)
matrix[i + 1, i] = value
if format == 'scipy.sparse':
matrix = matrix.tocsr()
return matrix
#--------------------------------------------------------------------------
# Printing Methods
#--------------------------------------------------------------------------
def _print_contents(self, printer, *args):
arg0 = printer._print(self.args[0], *args)
return '%s(%s)' % (self.__class__.__name__, arg0)
def _print_contents_pretty(self, printer, *args):
from sympy.printing.pretty.stringpict import prettyForm
pform = printer._print(self.args[0], *args)
pform = pform**prettyForm('\N{DAGGER}')
return pform
def _print_contents_latex(self, printer, *args):
arg = printer._print(self.args[0])
return '%s^{\\dagger}' % arg
class LoweringOp(SHOOp):
"""The Lowering Operator or 'a'.
When 'a' acts on a state it lowers the state up by one. Taking
the adjoint of 'a' returns a^dagger, the Raising Operator. 'a'
can be rewritten in terms of position and momentum. We can
represent 'a' as a matrix, which will be its default basis.
Parameters
==========
args : tuple
The list of numbers or parameters that uniquely specify the
operator.
Examples
========
Create a Lowering Operator and rewrite it in terms of position and
momentum, and show that taking its adjoint returns a^dagger:
>>> from sympy.physics.quantum.sho1d import LoweringOp
>>> from sympy.physics.quantum import Dagger
>>> a = LoweringOp('a')
>>> a.rewrite('xp').doit()
sqrt(2)*(m*omega*X + I*Px)/(2*sqrt(hbar)*sqrt(m*omega))
>>> Dagger(a)
RaisingOp(a)
Taking the commutator of 'a' with other Operators:
>>> from sympy.physics.quantum import Commutator
>>> from sympy.physics.quantum.sho1d import LoweringOp, RaisingOp
>>> from sympy.physics.quantum.sho1d import NumberOp
>>> a = LoweringOp('a')
>>> ad = RaisingOp('a')
>>> N = NumberOp('N')
>>> Commutator(a, ad).doit()
1
>>> Commutator(a, N).doit()
a
Apply 'a' to a state:
>>> from sympy.physics.quantum import qapply
>>> from sympy.physics.quantum.sho1d import LoweringOp, SHOKet
>>> a = LoweringOp('a')
>>> k = SHOKet('k')
>>> qapply(a*k)
sqrt(k)*|k - 1>
Taking 'a' of the lowest state will return 0:
>>> from sympy.physics.quantum import qapply
>>> from sympy.physics.quantum.sho1d import LoweringOp, SHOKet
>>> a = LoweringOp('a')
>>> k = SHOKet(0)
>>> qapply(a*k)
0
Matrix Representation
>>> from sympy.physics.quantum.sho1d import LoweringOp
>>> from sympy.physics.quantum.represent import represent
>>> a = LoweringOp('a')
>>> represent(a, basis=N, ndim=4, format='sympy')
Matrix([
[0, 1, 0, 0],
[0, 0, sqrt(2), 0],
[0, 0, 0, sqrt(3)],
[0, 0, 0, 0]])
"""
def _eval_rewrite_as_xp(self, *args, **kwargs):
return (S.One/sqrt(Integer(2)*hbar*m*omega))*(
I*Px + m*omega*X)
def _eval_adjoint(self):
return RaisingOp(*self.args)
def _eval_commutator_RaisingOp(self, other):
return S.One
def _eval_commutator_NumberOp(self, other):
return self
def _apply_operator_SHOKet(self, ket, **options):
temp = ket.n - Integer(1)
if ket.n is S.Zero:
return S.Zero
else:
return sqrt(ket.n)*SHOKet(temp)
def _represent_default_basis(self, **options):
return self._represent_NumberOp(None, **options)
def _represent_XOp(self, basis, **options):
# This logic is good but the underlying position
# representation logic is broken.
# temp = self.rewrite('xp').doit()
# result = represent(temp, basis=X)
# return result
raise NotImplementedError('Position representation is not implemented')
def _represent_NumberOp(self, basis, **options):
ndim_info = options.get('ndim', 4)
format = options.get('format', 'sympy')
matrix = matrix_zeros(ndim_info, ndim_info, **options)
for i in range(ndim_info - 1):
value = sqrt(i + 1)
if format == 'scipy.sparse':
value = float(value)
matrix[i,i + 1] = value
if format == 'scipy.sparse':
matrix = matrix.tocsr()
return matrix
class NumberOp(SHOOp):
"""The Number Operator is simply a^dagger*a
It is often useful to write a^dagger*a as simply the Number Operator
because the Number Operator commutes with the Hamiltonian. And can be
expressed using the Number Operator. Also the Number Operator can be
applied to states. We can represent the Number Operator as a matrix,
which will be its default basis.
Parameters
==========
args : tuple
The list of numbers or parameters that uniquely specify the
operator.
Examples
========
Create a Number Operator and rewrite it in terms of the ladder
operators, position and momentum operators, and Hamiltonian:
>>> from sympy.physics.quantum.sho1d import NumberOp
>>> N = NumberOp('N')
>>> N.rewrite('a').doit()
RaisingOp(a)*a
>>> N.rewrite('xp').doit()
-1/2 + (m**2*omega**2*X**2 + Px**2)/(2*hbar*m*omega)
>>> N.rewrite('H').doit()
-1/2 + H/(hbar*omega)
Take the Commutator of the Number Operator with other Operators:
>>> from sympy.physics.quantum import Commutator
>>> from sympy.physics.quantum.sho1d import NumberOp, Hamiltonian
>>> from sympy.physics.quantum.sho1d import RaisingOp, LoweringOp
>>> N = NumberOp('N')
>>> H = Hamiltonian('H')
>>> ad = RaisingOp('a')
>>> a = LoweringOp('a')
>>> Commutator(N,H).doit()
0
>>> Commutator(N,ad).doit()
RaisingOp(a)
>>> Commutator(N,a).doit()
-a
Apply the Number Operator to a state:
>>> from sympy.physics.quantum import qapply
>>> from sympy.physics.quantum.sho1d import NumberOp, SHOKet
>>> N = NumberOp('N')
>>> k = SHOKet('k')
>>> qapply(N*k)
k*|k>
Matrix Representation
>>> from sympy.physics.quantum.sho1d import NumberOp
>>> from sympy.physics.quantum.represent import represent
>>> N = NumberOp('N')
>>> represent(N, basis=N, ndim=4, format='sympy')
Matrix([
[0, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 2, 0],
[0, 0, 0, 3]])
"""
def _eval_rewrite_as_a(self, *args, **kwargs):
return ad*a
def _eval_rewrite_as_xp(self, *args, **kwargs):
return (S.One/(Integer(2)*m*hbar*omega))*(Px**2 + (
m*omega*X)**2) - S.Half
def _eval_rewrite_as_H(self, *args, **kwargs):
return H/(hbar*omega) - S.Half
def _apply_operator_SHOKet(self, ket, **options):
return ket.n*ket
def _eval_commutator_Hamiltonian(self, other):
return S.Zero
def _eval_commutator_RaisingOp(self, other):
return other
def _eval_commutator_LoweringOp(self, other):
return S.NegativeOne*other
def _represent_default_basis(self, **options):
return self._represent_NumberOp(None, **options)
def _represent_XOp(self, basis, **options):
# This logic is good but the underlying position
# representation logic is broken.
# temp = self.rewrite('xp').doit()
# result = represent(temp, basis=X)
# return result
raise NotImplementedError('Position representation is not implemented')
def _represent_NumberOp(self, basis, **options):
ndim_info = options.get('ndim', 4)
format = options.get('format', 'sympy')
matrix = matrix_zeros(ndim_info, ndim_info, **options)
for i in range(ndim_info):
value = i
if format == 'scipy.sparse':
value = float(value)
matrix[i,i] = value
if format == 'scipy.sparse':
matrix = matrix.tocsr()
return matrix
class Hamiltonian(SHOOp):
"""The Hamiltonian Operator.
The Hamiltonian is used to solve the time-independent Schrodinger
equation. The Hamiltonian can be expressed using the ladder operators,
as well as by position and momentum. We can represent the Hamiltonian
Operator as a matrix, which will be its default basis.
Parameters
==========
args : tuple
The list of numbers or parameters that uniquely specify the
operator.
Examples
========
Create a Hamiltonian Operator and rewrite it in terms of the ladder
operators, position and momentum, and the Number Operator:
>>> from sympy.physics.quantum.sho1d import Hamiltonian
>>> H = Hamiltonian('H')
>>> H.rewrite('a').doit()
hbar*omega*(1/2 + RaisingOp(a)*a)
>>> H.rewrite('xp').doit()
(m**2*omega**2*X**2 + Px**2)/(2*m)
>>> H.rewrite('N').doit()
hbar*omega*(1/2 + N)
Take the Commutator of the Hamiltonian and the Number Operator:
>>> from sympy.physics.quantum import Commutator
>>> from sympy.physics.quantum.sho1d import Hamiltonian, NumberOp
>>> H = Hamiltonian('H')
>>> N = NumberOp('N')
>>> Commutator(H,N).doit()
0
Apply the Hamiltonian Operator to a state:
>>> from sympy.physics.quantum import qapply
>>> from sympy.physics.quantum.sho1d import Hamiltonian, SHOKet
>>> H = Hamiltonian('H')
>>> k = SHOKet('k')
>>> qapply(H*k)
hbar*k*omega*|k> + hbar*omega*|k>/2
Matrix Representation
>>> from sympy.physics.quantum.sho1d import Hamiltonian
>>> from sympy.physics.quantum.represent import represent
>>> H = Hamiltonian('H')
>>> represent(H, basis=N, ndim=4, format='sympy')
Matrix([
[hbar*omega/2, 0, 0, 0],
[ 0, 3*hbar*omega/2, 0, 0],
[ 0, 0, 5*hbar*omega/2, 0],
[ 0, 0, 0, 7*hbar*omega/2]])
"""
def _eval_rewrite_as_a(self, *args, **kwargs):
return hbar*omega*(ad*a + S.Half)
def _eval_rewrite_as_xp(self, *args, **kwargs):
return (S.One/(Integer(2)*m))*(Px**2 + (m*omega*X)**2)
def _eval_rewrite_as_N(self, *args, **kwargs):
return hbar*omega*(N + S.Half)
def _apply_operator_SHOKet(self, ket, **options):
return (hbar*omega*(ket.n + S.Half))*ket
def _eval_commutator_NumberOp(self, other):
return S.Zero
def _represent_default_basis(self, **options):
return self._represent_NumberOp(None, **options)
def _represent_XOp(self, basis, **options):
# This logic is good but the underlying position
# representation logic is broken.
# temp = self.rewrite('xp').doit()
# result = represent(temp, basis=X)
# return result
raise NotImplementedError('Position representation is not implemented')
def _represent_NumberOp(self, basis, **options):
ndim_info = options.get('ndim', 4)
format = options.get('format', 'sympy')
matrix = matrix_zeros(ndim_info, ndim_info, **options)
for i in range(ndim_info):
value = i + S.Half
if format == 'scipy.sparse':
value = float(value)
matrix[i,i] = value
if format == 'scipy.sparse':
matrix = matrix.tocsr()
return hbar*omega*matrix
#------------------------------------------------------------------------------
class SHOState(State):
"""State class for SHO states"""
@classmethod
def _eval_hilbert_space(cls, label):
return ComplexSpace(S.Infinity)
@property
def n(self):
return self.args[0]
class SHOKet(SHOState, Ket):
"""1D eigenket.
Inherits from SHOState and Ket.
Parameters
==========
args : tuple
The list of numbers or parameters that uniquely specify the ket
This is usually its quantum numbers or its symbol.
Examples
========
Ket's know about their associated bra:
>>> from sympy.physics.quantum.sho1d import SHOKet
>>> k = SHOKet('k')
>>> k.dual
<k|
>>> k.dual_class()
<class 'sympy.physics.quantum.sho1d.SHOBra'>
Take the Inner Product with a bra:
>>> from sympy.physics.quantum import InnerProduct
>>> from sympy.physics.quantum.sho1d import SHOKet, SHOBra
>>> k = SHOKet('k')
>>> b = SHOBra('b')
>>> InnerProduct(b,k).doit()
KroneckerDelta(b, k)
Vector representation of a numerical state ket:
>>> from sympy.physics.quantum.sho1d import SHOKet, NumberOp
>>> from sympy.physics.quantum.represent import represent
>>> k = SHOKet(3)
>>> N = NumberOp('N')
>>> represent(k, basis=N, ndim=4)
Matrix([
[0],
[0],
[0],
[1]])
"""
@classmethod
def dual_class(self):
return SHOBra
def _eval_innerproduct_SHOBra(self, bra, **hints):
result = KroneckerDelta(self.n, bra.n)
return result
def _represent_default_basis(self, **options):
return self._represent_NumberOp(None, **options)
def _represent_NumberOp(self, basis, **options):
ndim_info = options.get('ndim', 4)
format = options.get('format', 'sympy')
options['spmatrix'] = 'lil'
vector = matrix_zeros(ndim_info, 1, **options)
if isinstance(self.n, Integer):
if self.n >= ndim_info:
return ValueError("N-Dimension too small")
if format == 'scipy.sparse':
vector[int(self.n), 0] = 1.0
vector = vector.tocsr()
elif format == 'numpy':
vector[int(self.n), 0] = 1.0
else:
vector[self.n, 0] = S.One
return vector
else:
return ValueError("Not Numerical State")
class SHOBra(SHOState, Bra):
"""A time-independent Bra in SHO.
Inherits from SHOState and Bra.
Parameters
==========
args : tuple
The list of numbers or parameters that uniquely specify the ket
This is usually its quantum numbers or its symbol.
Examples
========
Bra's know about their associated ket:
>>> from sympy.physics.quantum.sho1d import SHOBra
>>> b = SHOBra('b')
>>> b.dual
|b>
>>> b.dual_class()
<class 'sympy.physics.quantum.sho1d.SHOKet'>
Vector representation of a numerical state bra:
>>> from sympy.physics.quantum.sho1d import SHOBra, NumberOp
>>> from sympy.physics.quantum.represent import represent
>>> b = SHOBra(3)
>>> N = NumberOp('N')
>>> represent(b, basis=N, ndim=4)
Matrix([[0, 0, 0, 1]])
"""
@classmethod
def dual_class(self):
return SHOKet
def _represent_default_basis(self, **options):
return self._represent_NumberOp(None, **options)
def _represent_NumberOp(self, basis, **options):
ndim_info = options.get('ndim', 4)
format = options.get('format', 'sympy')
options['spmatrix'] = 'lil'
vector = matrix_zeros(1, ndim_info, **options)
if isinstance(self.n, Integer):
if self.n >= ndim_info:
return ValueError("N-Dimension too small")
if format == 'scipy.sparse':
vector[0, int(self.n)] = 1.0
vector = vector.tocsr()
elif format == 'numpy':
vector[0, int(self.n)] = 1.0
else:
vector[0, self.n] = S.One
return vector
else:
return ValueError("Not Numerical State")
ad = RaisingOp('a')
a = LoweringOp('a')
H = Hamiltonian('H')
N = NumberOp('N')
omega = Symbol('omega')
m = Symbol('m')

View File

@ -0,0 +1,173 @@
"""Shor's algorithm and helper functions.
Todo:
* Get the CMod gate working again using the new Gate API.
* Fix everything.
* Update docstrings and reformat.
"""
import math
import random
from sympy.core.mul import Mul
from sympy.core.singleton import S
from sympy.functions.elementary.exponential import log
from sympy.functions.elementary.miscellaneous import sqrt
from sympy.core.intfunc import igcd
from sympy.ntheory import continued_fraction_periodic as continued_fraction
from sympy.utilities.iterables import variations
from sympy.physics.quantum.gate import Gate
from sympy.physics.quantum.qubit import Qubit, measure_partial_oneshot
from sympy.physics.quantum.qapply import qapply
from sympy.physics.quantum.qft import QFT
from sympy.physics.quantum.qexpr import QuantumError
class OrderFindingException(QuantumError):
pass
class CMod(Gate):
"""A controlled mod gate.
This is black box controlled Mod function for use by shor's algorithm.
TODO: implement a decompose property that returns how to do this in terms
of elementary gates
"""
@classmethod
def _eval_args(cls, args):
# t = args[0]
# a = args[1]
# N = args[2]
raise NotImplementedError('The CMod gate has not been completed.')
@property
def t(self):
"""Size of 1/2 input register. First 1/2 holds output."""
return self.label[0]
@property
def a(self):
"""Base of the controlled mod function."""
return self.label[1]
@property
def N(self):
"""N is the type of modular arithmetic we are doing."""
return self.label[2]
def _apply_operator_Qubit(self, qubits, **options):
"""
This directly calculates the controlled mod of the second half of
the register and puts it in the second
This will look pretty when we get Tensor Symbolically working
"""
n = 1
k = 0
# Determine the value stored in high memory.
for i in range(self.t):
k += n*qubits[self.t + i]
n *= 2
# The value to go in low memory will be out.
out = int(self.a**k % self.N)
# Create array for new qbit-ket which will have high memory unaffected
outarray = list(qubits.args[0][:self.t])
# Place out in low memory
for i in reversed(range(self.t)):
outarray.append((out >> i) & 1)
return Qubit(*outarray)
def shor(N):
"""This function implements Shor's factoring algorithm on the Integer N
The algorithm starts by picking a random number (a) and seeing if it is
coprime with N. If it is not, then the gcd of the two numbers is a factor
and we are done. Otherwise, it begins the period_finding subroutine which
finds the period of a in modulo N arithmetic. This period, if even, can
be used to calculate factors by taking a**(r/2)-1 and a**(r/2)+1.
These values are returned.
"""
a = random.randrange(N - 2) + 2
if igcd(N, a) != 1:
return igcd(N, a)
r = period_find(a, N)
if r % 2 == 1:
shor(N)
answer = (igcd(a**(r/2) - 1, N), igcd(a**(r/2) + 1, N))
return answer
def getr(x, y, N):
fraction = continued_fraction(x, y)
# Now convert into r
total = ratioize(fraction, N)
return total
def ratioize(list, N):
if list[0] > N:
return S.Zero
if len(list) == 1:
return list[0]
return list[0] + ratioize(list[1:], N)
def period_find(a, N):
"""Finds the period of a in modulo N arithmetic
This is quantum part of Shor's algorithm. It takes two registers,
puts first in superposition of states with Hadamards so: ``|k>|0>``
with k being all possible choices. It then does a controlled mod and
a QFT to determine the order of a.
"""
epsilon = .5
# picks out t's such that maintains accuracy within epsilon
t = int(2*math.ceil(log(N, 2)))
# make the first half of register be 0's |000...000>
start = [0 for x in range(t)]
# Put second half into superposition of states so we have |1>x|0> + |2>x|0> + ... |k>x>|0> + ... + |2**n-1>x|0>
factor = 1/sqrt(2**t)
qubits = 0
for arr in variations(range(2), t, repetition=True):
qbitArray = list(arr) + start
qubits = qubits + Qubit(*qbitArray)
circuit = (factor*qubits).expand()
# Controlled second half of register so that we have:
# |1>x|a**1 %N> + |2>x|a**2 %N> + ... + |k>x|a**k %N >+ ... + |2**n-1=k>x|a**k % n>
circuit = CMod(t, a, N)*circuit
# will measure first half of register giving one of the a**k%N's
circuit = qapply(circuit)
for i in range(t):
circuit = measure_partial_oneshot(circuit, i)
# Now apply Inverse Quantum Fourier Transform on the second half of the register
circuit = qapply(QFT(t, t*2).decompose()*circuit, floatingPoint=True)
for i in range(t):
circuit = measure_partial_oneshot(circuit, i + t)
if isinstance(circuit, Qubit):
register = circuit
elif isinstance(circuit, Mul):
register = circuit.args[-1]
else:
register = circuit.args[-1].args[-1]
n = 1
answer = 0
for i in range(len(register)/2):
answer += n*register[i + t]
n = n << 1
if answer == 0:
raise OrderFindingException(
"Order finder returned 0. Happens with chance %f" % epsilon)
#turn answer into r using continued fractions
g = getr(answer, 2**t, N)
return g

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,425 @@
"""Abstract tensor product."""
from sympy.core.add import Add
from sympy.core.expr import Expr
from sympy.core.mul import Mul
from sympy.core.power import Pow
from sympy.core.sympify import sympify
from sympy.matrices.dense import DenseMatrix as Matrix
from sympy.matrices.immutable import ImmutableDenseMatrix as ImmutableMatrix
from sympy.printing.pretty.stringpict import prettyForm
from sympy.physics.quantum.qexpr import QuantumError
from sympy.physics.quantum.dagger import Dagger
from sympy.physics.quantum.commutator import Commutator
from sympy.physics.quantum.anticommutator import AntiCommutator
from sympy.physics.quantum.state import Ket, Bra
from sympy.physics.quantum.matrixutils import (
numpy_ndarray,
scipy_sparse_matrix,
matrix_tensor_product
)
from sympy.physics.quantum.trace import Tr
__all__ = [
'TensorProduct',
'tensor_product_simp'
]
#-----------------------------------------------------------------------------
# Tensor product
#-----------------------------------------------------------------------------
_combined_printing = False
def combined_tensor_printing(combined):
"""Set flag controlling whether tensor products of states should be
printed as a combined bra/ket or as an explicit tensor product of different
bra/kets. This is a global setting for all TensorProduct class instances.
Parameters
----------
combine : bool
When true, tensor product states are combined into one ket/bra, and
when false explicit tensor product notation is used between each
ket/bra.
"""
global _combined_printing
_combined_printing = combined
class TensorProduct(Expr):
"""The tensor product of two or more arguments.
For matrices, this uses ``matrix_tensor_product`` to compute the Kronecker
or tensor product matrix. For other objects a symbolic ``TensorProduct``
instance is returned. The tensor product is a non-commutative
multiplication that is used primarily with operators and states in quantum
mechanics.
Currently, the tensor product distinguishes between commutative and
non-commutative arguments. Commutative arguments are assumed to be scalars
and are pulled out in front of the ``TensorProduct``. Non-commutative
arguments remain in the resulting ``TensorProduct``.
Parameters
==========
args : tuple
A sequence of the objects to take the tensor product of.
Examples
========
Start with a simple tensor product of SymPy matrices::
>>> from sympy import Matrix
>>> from sympy.physics.quantum import TensorProduct
>>> m1 = Matrix([[1,2],[3,4]])
>>> m2 = Matrix([[1,0],[0,1]])
>>> TensorProduct(m1, m2)
Matrix([
[1, 0, 2, 0],
[0, 1, 0, 2],
[3, 0, 4, 0],
[0, 3, 0, 4]])
>>> TensorProduct(m2, m1)
Matrix([
[1, 2, 0, 0],
[3, 4, 0, 0],
[0, 0, 1, 2],
[0, 0, 3, 4]])
We can also construct tensor products of non-commutative symbols:
>>> from sympy import Symbol
>>> A = Symbol('A',commutative=False)
>>> B = Symbol('B',commutative=False)
>>> tp = TensorProduct(A, B)
>>> tp
AxB
We can take the dagger of a tensor product (note the order does NOT reverse
like the dagger of a normal product):
>>> from sympy.physics.quantum import Dagger
>>> Dagger(tp)
Dagger(A)xDagger(B)
Expand can be used to distribute a tensor product across addition:
>>> C = Symbol('C',commutative=False)
>>> tp = TensorProduct(A+B,C)
>>> tp
(A + B)xC
>>> tp.expand(tensorproduct=True)
AxC + BxC
"""
is_commutative = False
def __new__(cls, *args):
if isinstance(args[0], (Matrix, ImmutableMatrix, numpy_ndarray,
scipy_sparse_matrix)):
return matrix_tensor_product(*args)
c_part, new_args = cls.flatten(sympify(args))
c_part = Mul(*c_part)
if len(new_args) == 0:
return c_part
elif len(new_args) == 1:
return c_part * new_args[0]
else:
tp = Expr.__new__(cls, *new_args)
return c_part * tp
@classmethod
def flatten(cls, args):
# TODO: disallow nested TensorProducts.
c_part = []
nc_parts = []
for arg in args:
cp, ncp = arg.args_cnc()
c_part.extend(list(cp))
nc_parts.append(Mul._from_args(ncp))
return c_part, nc_parts
def _eval_adjoint(self):
return TensorProduct(*[Dagger(i) for i in self.args])
def _eval_rewrite(self, rule, args, **hints):
return TensorProduct(*args).expand(tensorproduct=True)
def _sympystr(self, printer, *args):
length = len(self.args)
s = ''
for i in range(length):
if isinstance(self.args[i], (Add, Pow, Mul)):
s = s + '('
s = s + printer._print(self.args[i])
if isinstance(self.args[i], (Add, Pow, Mul)):
s = s + ')'
if i != length - 1:
s = s + 'x'
return s
def _pretty(self, printer, *args):
if (_combined_printing and
(all(isinstance(arg, Ket) for arg in self.args) or
all(isinstance(arg, Bra) for arg in self.args))):
length = len(self.args)
pform = printer._print('', *args)
for i in range(length):
next_pform = printer._print('', *args)
length_i = len(self.args[i].args)
for j in range(length_i):
part_pform = printer._print(self.args[i].args[j], *args)
next_pform = prettyForm(*next_pform.right(part_pform))
if j != length_i - 1:
next_pform = prettyForm(*next_pform.right(', '))
if len(self.args[i].args) > 1:
next_pform = prettyForm(
*next_pform.parens(left='{', right='}'))
pform = prettyForm(*pform.right(next_pform))
if i != length - 1:
pform = prettyForm(*pform.right(',' + ' '))
pform = prettyForm(*pform.left(self.args[0].lbracket))
pform = prettyForm(*pform.right(self.args[0].rbracket))
return pform
length = len(self.args)
pform = printer._print('', *args)
for i in range(length):
next_pform = printer._print(self.args[i], *args)
if isinstance(self.args[i], (Add, Mul)):
next_pform = prettyForm(
*next_pform.parens(left='(', right=')')
)
pform = prettyForm(*pform.right(next_pform))
if i != length - 1:
if printer._use_unicode:
pform = prettyForm(*pform.right('\N{N-ARY CIRCLED TIMES OPERATOR}' + ' '))
else:
pform = prettyForm(*pform.right('x' + ' '))
return pform
def _latex(self, printer, *args):
if (_combined_printing and
(all(isinstance(arg, Ket) for arg in self.args) or
all(isinstance(arg, Bra) for arg in self.args))):
def _label_wrap(label, nlabels):
return label if nlabels == 1 else r"\left\{%s\right\}" % label
s = r", ".join([_label_wrap(arg._print_label_latex(printer, *args),
len(arg.args)) for arg in self.args])
return r"{%s%s%s}" % (self.args[0].lbracket_latex, s,
self.args[0].rbracket_latex)
length = len(self.args)
s = ''
for i in range(length):
if isinstance(self.args[i], (Add, Mul)):
s = s + '\\left('
# The extra {} brackets are needed to get matplotlib's latex
# rendered to render this properly.
s = s + '{' + printer._print(self.args[i], *args) + '}'
if isinstance(self.args[i], (Add, Mul)):
s = s + '\\right)'
if i != length - 1:
s = s + '\\otimes '
return s
def doit(self, **hints):
return TensorProduct(*[item.doit(**hints) for item in self.args])
def _eval_expand_tensorproduct(self, **hints):
"""Distribute TensorProducts across addition."""
args = self.args
add_args = []
for i in range(len(args)):
if isinstance(args[i], Add):
for aa in args[i].args:
tp = TensorProduct(*args[:i] + (aa,) + args[i + 1:])
c_part, nc_part = tp.args_cnc()
# Check for TensorProduct object: is the one object in nc_part, if any:
# (Note: any other object type to be expanded must be added here)
if len(nc_part) == 1 and isinstance(nc_part[0], TensorProduct):
nc_part = (nc_part[0]._eval_expand_tensorproduct(), )
add_args.append(Mul(*c_part)*Mul(*nc_part))
break
if add_args:
return Add(*add_args)
else:
return self
def _eval_trace(self, **kwargs):
indices = kwargs.get('indices', None)
exp = tensor_product_simp(self)
if indices is None or len(indices) == 0:
return Mul(*[Tr(arg).doit() for arg in exp.args])
else:
return Mul(*[Tr(value).doit() if idx in indices else value
for idx, value in enumerate(exp.args)])
def tensor_product_simp_Mul(e):
"""Simplify a Mul with TensorProducts.
Current the main use of this is to simplify a ``Mul`` of ``TensorProduct``s
to a ``TensorProduct`` of ``Muls``. It currently only works for relatively
simple cases where the initial ``Mul`` only has scalars and raw
``TensorProduct``s, not ``Add``, ``Pow``, ``Commutator``s of
``TensorProduct``s.
Parameters
==========
e : Expr
A ``Mul`` of ``TensorProduct``s to be simplified.
Returns
=======
e : Expr
A ``TensorProduct`` of ``Mul``s.
Examples
========
This is an example of the type of simplification that this function
performs::
>>> from sympy.physics.quantum.tensorproduct import \
tensor_product_simp_Mul, TensorProduct
>>> from sympy import Symbol
>>> A = Symbol('A',commutative=False)
>>> B = Symbol('B',commutative=False)
>>> C = Symbol('C',commutative=False)
>>> D = Symbol('D',commutative=False)
>>> e = TensorProduct(A,B)*TensorProduct(C,D)
>>> e
AxB*CxD
>>> tensor_product_simp_Mul(e)
(A*C)x(B*D)
"""
# TODO: This won't work with Muls that have other composites of
# TensorProducts, like an Add, Commutator, etc.
# TODO: This only works for the equivalent of single Qbit gates.
if not isinstance(e, Mul):
return e
c_part, nc_part = e.args_cnc()
n_nc = len(nc_part)
if n_nc == 0:
return e
elif n_nc == 1:
if isinstance(nc_part[0], Pow):
return Mul(*c_part) * tensor_product_simp_Pow(nc_part[0])
return e
elif e.has(TensorProduct):
current = nc_part[0]
if not isinstance(current, TensorProduct):
if isinstance(current, Pow):
if isinstance(current.base, TensorProduct):
current = tensor_product_simp_Pow(current)
else:
raise TypeError('TensorProduct expected, got: %r' % current)
n_terms = len(current.args)
new_args = list(current.args)
for next in nc_part[1:]:
# TODO: check the hilbert spaces of next and current here.
if isinstance(next, TensorProduct):
if n_terms != len(next.args):
raise QuantumError(
'TensorProducts of different lengths: %r and %r' %
(current, next)
)
for i in range(len(new_args)):
new_args[i] = new_args[i] * next.args[i]
else:
if isinstance(next, Pow):
if isinstance(next.base, TensorProduct):
new_tp = tensor_product_simp_Pow(next)
for i in range(len(new_args)):
new_args[i] = new_args[i] * new_tp.args[i]
else:
raise TypeError('TensorProduct expected, got: %r' % next)
else:
raise TypeError('TensorProduct expected, got: %r' % next)
current = next
return Mul(*c_part) * TensorProduct(*new_args)
elif e.has(Pow):
new_args = [ tensor_product_simp_Pow(nc) for nc in nc_part ]
return tensor_product_simp_Mul(Mul(*c_part) * TensorProduct(*new_args))
else:
return e
def tensor_product_simp_Pow(e):
"""Evaluates ``Pow`` expressions whose base is ``TensorProduct``"""
if not isinstance(e, Pow):
return e
if isinstance(e.base, TensorProduct):
return TensorProduct(*[ b**e.exp for b in e.base.args])
else:
return e
def tensor_product_simp(e, **hints):
"""Try to simplify and combine TensorProducts.
In general this will try to pull expressions inside of ``TensorProducts``.
It currently only works for relatively simple cases where the products have
only scalars, raw ``TensorProducts``, not ``Add``, ``Pow``, ``Commutators``
of ``TensorProducts``. It is best to see what it does by showing examples.
Examples
========
>>> from sympy.physics.quantum import tensor_product_simp
>>> from sympy.physics.quantum import TensorProduct
>>> from sympy import Symbol
>>> A = Symbol('A',commutative=False)
>>> B = Symbol('B',commutative=False)
>>> C = Symbol('C',commutative=False)
>>> D = Symbol('D',commutative=False)
First see what happens to products of tensor products:
>>> e = TensorProduct(A,B)*TensorProduct(C,D)
>>> e
AxB*CxD
>>> tensor_product_simp(e)
(A*C)x(B*D)
This is the core logic of this function, and it works inside, powers, sums,
commutators and anticommutators as well:
>>> tensor_product_simp(e**2)
(A*C)x(B*D)**2
"""
if isinstance(e, Add):
return Add(*[tensor_product_simp(arg) for arg in e.args])
elif isinstance(e, Pow):
if isinstance(e.base, TensorProduct):
return tensor_product_simp_Pow(e)
else:
return tensor_product_simp(e.base) ** e.exp
elif isinstance(e, Mul):
return tensor_product_simp_Mul(e)
elif isinstance(e, Commutator):
return Commutator(*[tensor_product_simp(arg) for arg in e.args])
elif isinstance(e, AntiCommutator):
return AntiCommutator(*[tensor_product_simp(arg) for arg in e.args])
else:
return e

Some files were not shown because too many files have changed in this diff Show More