Source code for qnet.algebra.core.state_algebra

r"""This module implements the algebra of states in a Hilbert space

For more details see :ref:`state_algebra`.
"""
import re
from abc import ABCMeta
from collections import OrderedDict
from itertools import product as cartesian_product

import sympy

from .abstract_algebra import Expression, Operation
from .scalar_algebra import is_scalar
from .abstract_quantum_algebra import (
    ScalarTimesQuantumExpression, QuantumExpression, QuantumSymbol,
    QuantumPlus, QuantumTimes, QuantumAdjoint, QuantumIndexedSum,
    QuantumDerivative, ensure_local_space, _series_expand_combine_prod)
from .algebraic_properties import (
    accept_bras, assoc, assoc_indexed, basis_ket_zero_outside_hs,
    filter_neutral, match_replace, match_replace_binary, orderby,
    collect_summands)
from .exceptions import OverlappingSpaces, UnequalSpaces, SpaceTooLargeError
from .hilbert_space_algebra import FullSpace, TrivialSpace
from qnet.algebra.core.algebraic_properties import (
    indexed_sum_over_const,
    indexed_sum_over_kronecker)
from .operator_algebra import Operator, OperatorPlus, PseudoInverse
from .scalar_algebra import ScalarExpression
from ...utils.indices import (
    FockIndex, IdxSym, IndexOverFockSpace, IndexOverRange, SymbolicLabelBase, )
from ...utils.ordering import FullCommutativeHSOrder
from ...utils.singleton import Singleton, singleton_object

__all__ = [
    'BasisKet', 'Bra', 'BraKet', 'CoherentStateKet', 'State', 'KetBra',
    'KetPlus', 'KetSymbol', 'LocalKet', 'OperatorTimesKet', 'ScalarTimesKet',
    'TensorKet', 'TrivialKet', 'ZeroKet', 'KetIndexedSum', 'StateDerivative']

__private__ = []  # anything not in __all__ must be in __private__


###############################################################################
# Algebraic properties
###############################################################################


[docs]class State(QuantumExpression, metaclass=ABCMeta): """Base class for states in a Hilbert space""" def _adjoint(self): if self.isket: return Bra(self) else: return self.ket @property def isket(self): """Whether the state represents a ket""" # We enforce that all sub-classes of Ket except Bra are normal # Hilbert space vectors. For example, KetPlus(Bra(...), Bra(...)) # is disallowed, and is represented as Bra(KetPlus(.., ...)). Thus, # all State instances that are not directly an instance of Bra are # kets. Name of property 'isket' is taken from QuTiP return not isinstance(self, Bra) @property def isbra(self): """Wether the state represents a bra (adjoint ket)""" return isinstance(self, Bra) @property def bra(self): """The bra associated with a ket""" if self.isbra: return self else: return self._adjoint() @property def ket(self): """The ket associated with a bra""" if self.isket: return self else: return self._adjoint() def __mul__(self, other): # In the State algebra, any adjoint is a Bra # instance at the top level. To enforce this, we need some custom # __mul__ implementations, in particular for KetIndexedSums def check_if_sum(*args): return [isinstance(arg, KetIndexedSum) for arg in args] if isinstance(other, State): isket = (self.isket, other.isket) if isket == (True, True): # Ket * Ket is_sum = check_if_sum(self, other) if is_sum == [True, True]: return QuantumIndexedSum.__mul__(self, other) elif is_sum == [True, False]: return QuantumIndexedSum.__mul__(self, other) elif is_sum == [False, True]: return QuantumIndexedSum.__rmul__(other, self) elif is_sum == [False, False]: return TensorKet.create(self, other) elif isket == (True, False): # Ket * Bra is_sum = check_if_sum(self, other.ket) if is_sum == [True, True]: other_ket = other.ket.make_disjunct_indices(self) new_term = self.term * Bra(other_ket.term) new_ranges = self.ranges + other_ket.ranges return new_term.__class__._indexed_sum_cls.create( new_term, *new_ranges) elif is_sum == [True, False]: return QuantumIndexedSum.__mul__(self, other) elif is_sum == [False, True]: new_term = self * Bra(other.ket.term) return new_term.__class__._indexed_sum_cls.create( new_term, *other.ket.ranges) elif is_sum == [False, False]: return KetBra.create(self, other.ket) elif isket == (False, True): # Bra * Ket is_sum = check_if_sum(self.ket, other) if is_sum == [True, True]: other = other.make_disjunct_indices(self.ket) new_term = Bra(self.ket.term) * other.term new_ranges = self.ket.ranges + other.ranges return new_term.__class__._indexed_sum_cls.create( new_term, *new_ranges) elif is_sum == [True, False]: new_term = Bra(self.ket.term) * other return new_term.__class__._indexed_sum_cls.create( new_term, *self.ket.ranges) elif is_sum == [False, True]: return QuantumIndexedSum.__rmul__(other, self) elif is_sum == [False, False]: return BraKet.create(self.ket, other) elif isket == (False, False): # Bra * Bra return Bra.create(self.ket * other.ket) elif isinstance(other, Operator): if self.isbra: return Bra.create(other.adjoint() * self.ket) try: return super().__mul__(other) except AttributeError: return NotImplemented def __rmul__(self, other): if self.isket and isinstance(other, Operator): return OperatorTimesKet.create(other, self) elif self.isbra and is_scalar(other): return Bra(other.conjugate() * self.ket) try: return super().__rmul__(other) except AttributeError: return NotImplemented
############################################################################### # Operator algebra elements ###############################################################################
[docs]class KetSymbol(QuantumSymbol, State): """Symbolic state See :class:`.QuantumSymbol`. """ _rx_label = re.compile( r'(^[+-]?\d+(/\d+)?$|' r'^[A-Za-z0-9+-]+([A-Za-z0-9()_,.+-=]+)?$)')
[docs]class LocalKet(State, metaclass=ABCMeta): """A state on a :class:`LocalSpace` This does not include operations, even if these operations only involve states acting on the same local space""" def __init__(self, *args, hs): hs = ensure_local_space(hs, cls=self._default_hs_cls) self._hs = hs super().__init__(*args, hs=hs) @property def space(self): return self._hs @property def kwargs(self): return {'hs': self._hs}
[docs]@singleton_object class ZeroKet(State, Expression, metaclass=Singleton): """ZeroKet constant (singleton) object for the null-state.""" _order_index = 2 @property def space(self): return FullSpace @property def args(self): return tuple() def _diff(self, sym): return self
[docs]@singleton_object class TrivialKet(State, Expression, metaclass=Singleton): """TrivialKet constant (singleton) object. This is the neutral element under the state tensor-product. """ _order_index = 2 @property def space(self): return TrivialSpace def _adjoint(self): return Bra(TrivialKet) @property def args(self): return tuple() def _diff(self, sym): return ZeroKet
[docs]class BasisKet(LocalKet, KetSymbol): """Local basis state, identified by index or label Basis kets are orthornormal, and the :meth:`next` and :meth:`prev` methods can be used to move between basis states. Args: label_or_index: If `str`, the label of the basis state (must be an element of `hs.basis_labels`). If `int`, the (zero-based) index of the basis state. This works if `hs` has an unknown dimension. For a symbolic index, `label_or_index` can be an instance of an appropriate subclass of :class:`~qnet.algebra.indices.SymbolicLabelBase` hs (LocalSpace): The Hilbert space in which the basis is defined Raises: ValueError: if `label_or_index` is not in the Hilbert space TypeError: if `label_or_index` is not of an appropriate type .BasisNotSetError: if `label_or_index` is a `str` but no basis is defined for `hs` Note: Basis states that are instantiated via a label or via an index are equivalent:: >>> hs = LocalSpace('tls', basis=('g', 'e')) >>> BasisKet('g', hs=hs) == BasisKet(0, hs=hs) True >>> print(ascii(BasisKet(0, hs=hs))) |g>^(tls) When instantiating the :class:`BasisKet` via :meth:`~qnet.algebra.abstract_algebra.Expression.create`, an integer label outside the range of the underlying Hilbert space results in a :class:`ZeroKet`:: >>> BasisKet.create(-1, hs=0) ZeroKet >>> BasisKet.create(2, hs=LocalSpace('tls', dimension=2)) ZeroKet """ simplifications = [basis_ket_zero_outside_hs] def __init__(self, label_or_index, *, hs): hs = ensure_local_space(hs, cls=self._default_hs_cls) label, ind = hs._unpack_basis_label_or_index(label_or_index) self._index = ind super().__init__(label, hs=hs) @property def args(self): """Tuple containing `label_or_index` as its only element.""" if self.space.has_basis or isinstance(self.label, SymbolicLabelBase): return (self.label, ) else: return (self.index, ) @property def index(self): """The index of the state in the Hilbert space basis >>> hs = LocalSpace('tls', basis=('g', 'e')) >>> BasisKet('g', hs=hs).index 0 >>> BasisKet('e', hs=hs).index 1 >>> BasisKet(1, hs=hs).index 1 For a :class:`BasisKet` with an indexed label, this may return a sympy expression:: >>> hs = SpinSpace('s', spin='3/2') >>> i = symbols('i', cls=IdxSym) >>> lbl = SpinIndex(i/2, hs) >>> ket = BasisKet(lbl, hs=hs) >>> ket.index i/2 + 3/2 """ return self._index
[docs] def next(self, n=1): """Move up by `n` steps in the Hilbert space:: >>> hs = LocalSpace('tls', basis=('g', 'e')) >>> ascii(BasisKet('g', hs=hs).next()) '|e>^(tls)' >>> ascii(BasisKet(0, hs=hs).next()) '|e>^(tls)' We can also go multiple steps: >>> hs = LocalSpace('ten', dimension=10) >>> ascii(BasisKet(0, hs=hs).next(2)) '|2>^(ten)' An increment that leads out of the Hilbert space returns zero:: >>> BasisKet(0, hs=hs).next(10) ZeroKet """ if isinstance(self.label, SymbolicLabelBase): next_label = self.space.next_basis_label_or_index( self.label, n) return BasisKet(next_label, hs=self.space) else: try: next_index = self.space.next_basis_label_or_index( self.index, n) return BasisKet(next_index, hs=self.space) except IndexError: return ZeroKet
[docs] def prev(self, n=1): """Move down by `n` steps in the Hilbert space, cf. :meth:`next`. >>> hs = LocalSpace('3l', basis=('g', 'e', 'r')) >>> ascii(BasisKet('r', hs=hs).prev(2)) '|g>^(3l)' >>> BasisKet('r', hs=hs).prev(3) ZeroKet """ return self.next(n=-n)
[docs]class CoherentStateKet(LocalKet): """Local coherent state, labeled by a complex amplitude Args: hs (LocalSpace): The local Hilbert space degree of freedom. ampl (Scalar): The coherent displacement amplitude. """ _rx_label = re.compile('^.*$') def __init__(self, ampl, *, hs): self._ampl = ampl super().__init__(ampl, hs=hs) @property def args(self): return (self._ampl, ) @property def ampl(self): return self._ampl def _diff(self, sym): from qnet.algebra.library.fock_operators import Destroy, Create hs = self.space return ( (self._ampl * Create(hs=hs) - self._ampl.conjugate() * Destroy(hs=hs)) .diff(sym) * self)
[docs] def to_fock_representation(self, index_symbol='n', max_terms=None): """Return the coherent state written out as an indexed sum over Fock basis states""" phase_factor = sympy.exp( sympy.Rational(-1, 2) * self.ampl * self.ampl.conjugate()) if not isinstance(index_symbol, IdxSym): index_symbol = IdxSym(index_symbol) n = index_symbol if max_terms is None: index_range = IndexOverFockSpace(n, hs=self._hs) else: index_range = IndexOverRange(n, 0, max_terms-1) term = ( (self.ampl**n / sympy.sqrt(sympy.factorial(n))) * BasisKet(FockIndex(n), hs=self._hs)) return phase_factor * KetIndexedSum(term, index_range)
############################################################################### # Algebra Operations ###############################################################################
[docs]class KetPlus(State, QuantumPlus): """Sum of states""" _neutral_element = ZeroKet _binary_rules = OrderedDict() simplifications = [ accept_bras, assoc, orderby, collect_summands, match_replace_binary] order_key = FullCommutativeHSOrder def __init__(self, *operands): _check_kets(*operands, same_space=True) super().__init__(*operands)
[docs]class TensorKet(State, QuantumTimes): """A tensor product of kets Each ket must belong to different degree of freedom (:class:`.LocalSpace`). """ _binary_rules = OrderedDict() _neutral_element = TrivialKet simplifications = [ accept_bras, assoc, orderby, filter_neutral, match_replace_binary] order_key = FullCommutativeHSOrder def __init__(self, *operands): _check_kets(*operands, disjunct_space=True) super().__init__(*operands)
[docs] @classmethod def create(cls, *ops): if any(o == ZeroKet for o in ops): return ZeroKet return super().create(*ops)
[docs]class ScalarTimesKet(State, ScalarTimesQuantumExpression): """Product of a :class:`.Scalar` coefficient and a ket Args: coeff (Scalar): coefficient term (State): the ket that is multiplied """ _rules = OrderedDict() simplifications = [match_replace, ]
[docs] @classmethod def create(cls, coeff, term): if term.isbra: scalar_times_ket = coeff.conjugate() * term.ket return Bra.create(scalar_times_ket) return super().create(coeff, term)
def __init__(self, coeff, term): _check_kets(term) super().__init__(coeff, term)
[docs]class OperatorTimesKet(State, Operation): """Product of an operator and a state.""" _rules = OrderedDict() simplifications = [match_replace] def __init__(self, operator, ket): _check_kets(ket) if not operator.space <= ket.space: raise SpaceTooLargeError( str(operator.space) + " <!= " + str(ket.space)) super().__init__(operator, ket) @property def space(self): return self.operands[1].space @property def operator(self): return self.operands[0] @property def ket(self): return self.operands[1] def _expand(self): c, t = self.operands ct = c.expand() et = t.expand() if isinstance(et, KetPlus): if isinstance(ct, OperatorPlus): return sum((cto * eto for eto in et.operands for cto in ct.operands), ZeroKet) else: return sum((c * eto for eto in et.operands), ZeroKet) elif isinstance(ct, OperatorPlus): return sum((cto * et for cto in ct.operands), ZeroKet) return ct * et def _diff(self, sym): return ( self.operator.diff(sym) * self.ket + self.operator * self.ket.diff(sym)) def _series_expand(self, param, about, order): ce = self.operator.series_expand(param, about, order) te = self.ket.series_expand(param, about, order) return tuple(ce[k] * te[n - k] for n in range(order + 1) for k in range(n + 1))
[docs]class StateDerivative(QuantumDerivative, State): """Symbolic partial derivative of a state See :class:`.QuantumDerivative`. """ pass
[docs]class Bra(State, QuantumAdjoint): """The associated dual/adjoint state for any ket""" def __init__(self, ket): if ket.isbra: raise TypeError("ket cannot be a Bra instance") super().__init__(ket) @property def ket(self): """The original :class:`State`""" return self.operands[0] @property def bra(self): return self operand = ket @property def isket(self): """False, by defintion""" return False @property def isbra(self): """True, by definition""" return True def _adjoint(self): return self.ket @property def label(self): return self.ket.label def __add__(self, other): if isinstance(other, Bra): return Bra.create(self.ket + other.ket) return NotImplemented def __sub__(self, other): if isinstance(other, Bra): return Bra.create(self.ket - other.ket) return NotImplemented def __truediv__(self, other): if is_scalar(other): return Bra.create(self.ket/other.conjugate()) return NotImplemented
[docs]class BraKet(ScalarExpression, Operation): r"""The symbolic inner product between two states This mathermatically corresponds to: .. math:: \langle b | k \rangle which we define to be linear in the state :math:`k` and anti-linear in :math:`b`. Args: bra (State): The anti-linear state argument. Note that this is *not* a :class:`Bra` instance. ket (State): The linear state argument. """ _rules = OrderedDict() _space = TrivialSpace simplifications = [match_replace] def __init__(self, bra, ket): _check_kets(bra, ket, same_space=True) super().__init__(bra, ket) @property def ket(self): """The ket of the braket""" return self.operands[1] @property def bra(self): """The bra of the braket (:class:`Bra` instance)""" return Bra(self.operands[0]) def _diff(self, sym): bra, ket = self.operands return ( self.__class__.create(bra.diff(sym), ket) + self.__class__.create(bra, ket.diff(sym))) def _adjoint(self): return BraKet.create(*reversed(self.operands)) def _expand(self): b, k = self.operands be, ke = b.expand(), k.expand() besummands = be.operands if isinstance(be, KetPlus) else (be,) kesummands = ke.operands if isinstance(ke, KetPlus) else (ke,) return sum(BraKet.create(bes, kes) for bes in besummands for kes in kesummands) def _series_expand(self, param, about, order): be = self.bra.series_expand(param, about, order) ke = self.ket.series_expand(param, about, order) return _series_expand_combine_prod(be, ke, order)
[docs]class KetBra(Operator, Operation): """Outer product of two states Args: ket (State): The left factor in the product bra (State): The right factor in the product. Note that this is *not* a :class:`Bra` instance. """ _rules = OrderedDict() simplifications = [match_replace] def __init__(self, ket, bra): _check_kets(ket, bra, same_space=True) super().__init__(ket, bra) @property def ket(self): """The left factor in the product""" return self.operands[0] @property def bra(self): """The co-state right factor in the product This is a :class:`Bra` instance (unlike the `bra` given to the constructor """ return Bra(self.operands[1]) @property def space(self): """The Hilbert space of the states being multiplied""" return self.operands[0].space def _adjoint(self): return KetBra.create(*reversed(self.operands)) def _pseudo_inverse(self): return PseudoInverse(self) def _diff(self, sym): ket, bra = self.operands return ( self.__class__.create(ket.diff(sym), bra) + self.__class__.create(ket, bra.diff(sym))) def _expand(self): k, b = self.ket, self.bra.ket be, ke = b.expand(), k.expand() kesummands = ke.operands if isinstance(ke, KetPlus) else (ke,) besummands = be.operands if isinstance(be, KetPlus) else (be,) res_summands = [] for (k, b) in cartesian_product(kesummands, besummands): res_summands.append(KetBra.create(k, b)) return OperatorPlus.create(*res_summands) def _series_expand(self, param, about, order): ke = self.ket.series_expand(param, about, order) be = self.bra.series_expand(param, about, order) return tuple(ke[k] * be[n - k] for n in range(order + 1) for k in range(n + 1))
[docs]class KetIndexedSum(State, QuantumIndexedSum): """Indexed sum over Kets""" # Must inherit from State first, so that proper __mul__ is used _rules = OrderedDict() simplifications = [ assoc_indexed, indexed_sum_over_kronecker, indexed_sum_over_const, match_replace, ]
[docs] @classmethod def create(cls, term, *ranges): if term.isbra: return Bra.create(KetIndexedSum.create(term.ket, *ranges)) else: return super().create(term, *ranges)
def __init__(self, term, *ranges): _check_kets(term) super().__init__(term, *ranges)
def _check_kets(*ops, same_space=False, disjunct_space=False): """Check that all operands are Kets from the same Hilbert space.""" if not all([(isinstance(o, State) and o.isket) for o in ops]): raise TypeError("All operands must be Kets") if same_space: if not len({o.space for o in ops if o is not ZeroKet}) == 1: raise UnequalSpaces(str(ops)) if disjunct_space: spc = TrivialSpace for o in ops: if o.space & spc > TrivialSpace: raise OverlappingSpaces(str(ops)) spc *= o.space State._zero = ZeroKet State._one = TrivialKet State._base_cls = State State._scalar_times_expr_cls = ScalarTimesKet State._plus_cls = KetPlus State._times_cls = TensorKet State._adjoint_cls = Bra State._indexed_sum_cls = KetIndexedSum State._derivative_cls = StateDerivative