r"""Core class hierarchy for Hilbert spaces
This module defines some simple classes to describe simple and
*composite/tensor* (i.e., multiple degree of freedom)
Hilbert spaces of quantum systems.
For more details see :ref:`hilbert_space_algebra`.
"""
import functools
import operator
import re
from abc import ABCMeta, abstractmethod
from collections import OrderedDict
from itertools import product as cartesian_product
from .abstract_algebra import (
Expression, Operation, )
from .algebraic_properties import (
assoc, idem, filter_neutral, convert_to_spaces, empty_trivial, )
from .exceptions import AlgebraError, BasisNotSetError
from ...utils.indices import SymbolicLabelBase, FockIndex, FockLabel, StrLabel
from ...utils.ordering import KeyTuple
from ...utils.singleton import Singleton, singleton_object
__all__ = [
'HilbertSpace', 'LocalSpace', 'ProductSpace',
'FullSpace', 'TrivialSpace']
__private__ = [] # anything not in __all__ must be in __private__
###############################################################################
# Abstract base classes
###############################################################################
[docs]class HilbertSpace(metaclass=ABCMeta):
"""Base class for Hilbert spaces"""
[docs] def tensor(self, *others):
"""Tensor product between Hilbert spaces"""
return ProductSpace.create(self, *others)
[docs] @abstractmethod
def remove(self, other):
"""Remove a particular factor from a tensor product space."""
raise NotImplementedError(self.__class__.__name__)
[docs] @abstractmethod
def intersect(self, other):
"""Find the mutual tensor factors of two Hilbert spaces."""
raise NotImplementedError(self.__class__.__name__)
@property
@abstractmethod
def local_factors(self):
"""Return tuple of LocalSpace objects that tensored together yield this
Hilbert space."""
raise NotImplementedError(self.__class__.__name__)
[docs] def isdisjoint(self, other):
"""Check whether two Hilbert spaces are disjoint (do not have any
common local factors). Note that `FullSpace` is *not* disjoint with any
other Hilbert space, while `TrivialSpace` *is* disjoint with any other
HilbertSpace (even itself)
"""
if other == FullSpace:
return False
else:
for ls in self.local_factors:
if isinstance(ls.label, StrLabel):
return False
for ls in other.local_factors:
if isinstance(ls.label, StrLabel):
return False
return set(self.local_factors).isdisjoint(set(other.local_factors))
[docs] def is_tensor_factor_of(self, other):
"""Test if a space is included within a larger tensor product space.
Also ``True`` if ``self == other``.
:param other: Other Hilbert space
:type other: HilbertSpace
:rtype: bool
"""
return self <= other
[docs] def is_strict_tensor_factor_of(self, other):
"""Test if a space is included within a larger tensor product space.
Not ``True`` if ``self == other``."""
return self < other
@property
def dimension(self):
"""Full dimension of the Hilbert space.
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
"""
raise BasisNotSetError(
"Hilbert space %s has no defined basis" % str(self))
@property
def has_basis(self):
"""True if the Hilbert space has a basis"""
return False
@property
def basis_states(self):
"""Yield an iterator over the states (:class:`.State` instances) that
form the canonical basis of the Hilbert space
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
"""
raise BasisNotSetError(
"Hilbert space %s has no defined basis" % str(self))
[docs] def basis_state(self, index_or_label):
"""Return the basis state with the given index or label.
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
IndexError: if there is no basis state with the given index
KeyError: if there is not basis state with the given label
"""
raise BasisNotSetError(
"Hilbert space %s has no defined basis" % str(self))
@property
def basis_labels(self):
"""Tuple of basis labels.
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
"""
raise BasisNotSetError(
"Hilbert space %s has no defined basis" % str(self))
[docs] @abstractmethod
def is_strict_subfactor_of(self, other):
"""Test whether a Hilbert space occures as a strict sub-factor in
a (larger) Hilbert space"""
raise NotImplementedError(self.__class__.__name__)
[docs] def __len__(self):
"""The number of LocalSpace factors / degrees of freedom."""
return len(self.local_factors)
def __mul__(self, other):
return self.tensor(other)
def __truediv__(self, other):
return self.remove(other)
def __and__(self, other):
return self.intersect(other)
def __lt__(self, other):
return self.is_strict_subfactor_of(other)
def __gt__(self, other):
return other.is_strict_subfactor_of(self)
def __le__(self, other):
return self == other or self < other
def __ge__(self, other):
return self == other or self > other
###############################################################################
# Hilbert space algebra elements
###############################################################################
[docs]class LocalSpace(HilbertSpace, Expression):
"""Hilbert space for a single degree of freedom.
Args:
label (str or int or StrLabel): label (subscript) of the
Hilbert space
basis (tuple or None): Set an explicit basis for the Hilbert space
(tuple of labels for the basis states)
dimension (int or None): Specify the dimension $n$ of the Hilbert
space. This implies a basis numbered from 0 to $n-1$.
local_identifiers (dict): Mapping of class names of
:class:`.LocalOperator` subclasses to identifier names. Used e.g.
'b' instead of the default 'a' for the anihilation operator. This
can be a dict or a dict-compatible structure, e.g. a list/tuple of
key-value tuples.
order_index (int or None): An optional key that determines the
preferred order of Hilbert spaces. This also changes the order of
e.g. sums or products of Operators. Hilbert spaces will be ordered
from left to right be increasing `order_index`; Hilbert spaces
without an explicit `order_index` are sorted by their label
A :class:`LocalSpace` fundamentally has a Fock-space like structure,
in that its basis states may be understood as an "excitation".
The spectrum can be infinite, with levels labeled by integers 0, 1, ...::
>>> hs = LocalSpace(label=0)
or truncated to a finite dimension::
>>> hs = LocalSpace(0, dimension=5)
>>> hs.basis_labels
('0', '1', '2', '3', '4')
For finite-dimensional (truncated) Hilbert spaces, we also allow an
arbitrary alternative labeling of the canonical basis::
>>> hs = LocalSpace('rydberg', dimension=3, basis=('g', 'e', 'r'))
"""
_rx_label = re.compile('^[A-Za-z0-9.+-]+(_[A-Za-z0-9().+-]+)?$')
_basis_label_types = (int, str, FockIndex, FockLabel)
# acceptable types for labels
def __init__(
self, label, *, basis=None, dimension=None, local_identifiers=None,
order_index=None):
default_args = []
if basis is None:
default_args.append('basis')
else:
basis = tuple([str(label) for label in basis])
if dimension is None:
default_args.append('dimension')
else:
dimension = int(dimension)
if order_index in [None, float('inf')]:
default_args.append('order_index')
order_index = float('inf') # ensure sort as last
else:
order_index = int(order_index)
if local_identifiers is None:
default_args.append('local_identifiers')
local_identifiers = {}
else:
local_identifiers = dict(local_identifiers)
try:
# we want to normalize the local_identifiers to an arbitrary stable
# order
self._sorted_local_identifiers = tuple(
sorted(tuple(local_identifiers.items())))
except TypeError:
# this will happen e.g. if the keys in local_identifier are types
# instead of class names
raise TypeError(
"local_identifier must map class names to identifier strings")
if not isinstance(label, StrLabel):
label = str(label)
if not self._rx_label.match(label):
raise ValueError(
"label '%s' does not match pattern '%s'"
% (label, self._rx_label.pattern))
if basis is None:
if dimension is not None:
basis = tuple([str(i) for i in range(dimension)])
else:
if dimension is None:
dimension = len(basis)
else:
if dimension != len(basis):
raise ValueError("basis and dimension are incompatible")
self._label = label
self._order_key = KeyTuple((
order_index, label, str(dimension), basis,
self._sorted_local_identifiers))
self._basis = basis
self._dimension = dimension
self._local_identifiers = local_identifiers
self._order_index = order_index
self._kwargs = OrderedDict([
('basis', self._basis), ('dimension', self._dimension),
('local_identifiers', self._sorted_local_identifiers),
('order_index', self._order_index)])
self._minimal_kwargs = self._kwargs.copy()
for key in default_args:
del self._minimal_kwargs[key]
super().__init__(
label, basis=basis, dimension=dimension,
local_identifiers=self._sorted_local_identifiers,
order_index=order_index)
@classmethod
def _check_basis_label_type(cls, label_or_index):
"""Every object (BasisKet, LocalSigma) that contains a label or index
for an eigenstate of some LocalSpace should call this routine to check
the type of that label or index (or, use
:meth:`_unpack_basis_label_or_index`"""
if not isinstance(label_or_index, cls._basis_label_types):
raise TypeError(
"label_or_index must be an instance of one of %s; not %s" % (
", ".join([t.__name__ for t in cls._basis_label_types]),
label_or_index.__class__.__name__))
def _unpack_basis_label_or_index(self, label_or_index):
"""return tuple (label, ind) from `label_or_index`
If `label_or_int` is a :class:`.SymbolicLabelBase` sub-instance, it
will be stored in the `label` attribute, and the `ind` attribute will
return the value of the label's :attr:`.FockIndex.fock_index`
attribute. No checks are performed for symbolic labels.
:meth:`_check_basis_label_type` is called on `label_or_index`.
Raises:
ValueError: if `label_or_index` is a :class:`str` referencing an
invalid basis state; or, if `label_or_index` is an :class:`int`
< 0 or >= the dimension of the Hilbert space
BasisNotSetError: if `label_or_index` is a :class:`str`, but the
Hilbert space has no defined basis
TypeError: if `label_or_int` is not a :class:`str`, :class:`int`,
or :class:`.SymbolicLabelBase`, or more generally whatever
types are allowed through the `_basis_label_types` attribute of
the Hilbert space.
"""
self._check_basis_label_type(label_or_index)
if isinstance(label_or_index, str):
label = label_or_index
try:
ind = self.basis_labels.index(label)
# the above line may also raise BasisNotSetError, which we
# don't want to catch here
except ValueError:
# a less confusing error message:
raise ValueError(
"%r is not one of the basis labels %r"
% (label, self.basis_labels))
elif isinstance(label_or_index, int):
ind = label_or_index
if ind < 0:
raise ValueError("Index %d must be >= 0" % ind)
if self.has_basis:
if ind >= self.dimension:
raise ValueError(
"Index %s must be < the dimension %d of Hilbert "
"space %s" % (ind, self.dimension, self))
label = self.basis_labels[label_or_index]
else:
label = str(label_or_index)
elif isinstance(label_or_index, SymbolicLabelBase):
label = label_or_index
try:
ind = label_or_index.fock_index
except AttributeError:
raise TypeError(
"label_or_index must define a fock_index attribute in "
"order to be used for identifying a level in a Hilbert "
"space")
else:
raise TypeError(
"label_or_index must be an int or str, or SymbolicLabelBase, "
"not %s" % type(label_or_index))
return label, ind
@property
def args(self):
"""List of arguments, consisting only of `label`"""
return (self._label, )
@property
def label(self):
"""Label of the Hilbert space"""
return self._label
@property
def has_basis(self):
"""True if the Hilbert space has a basis"""
return self._basis is not None
@property
def basis_states(self):
"""Yield an iterator over the states (:class:`.BasisKet` instances)
that form the canonical basis of the Hilbert space
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
"""
from qnet.algebra.core.state_algebra import BasisKet # avoid circ. import
for label in self.basis_labels:
yield BasisKet(label, hs=self)
[docs] def basis_state(self, index_or_label):
"""Return the basis state with the given index or label.
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
IndexError: if there is no basis state with the given index
KeyError: if there is not basis state with the given label
"""
from qnet.algebra.core.state_algebra import BasisKet # avoid circ. import
try:
return BasisKet(index_or_label, hs=self)
except ValueError as exc_info:
if isinstance(index_or_label, int):
raise IndexError(str(exc_info))
else:
raise KeyError(str(exc_info))
@property
def basis_labels(self):
"""Tuple of basis labels (strings).
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
"""
if self._basis is None:
raise BasisNotSetError(
"Hilbert space %s has no defined basis" % str(self))
return self._basis
@property
def dimension(self):
"""Dimension of the Hilbert space.
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
"""
if self._dimension is not None:
return self._dimension
else:
raise BasisNotSetError(
"Hilbert space %s has no defined basis" % str(self))
@property
def kwargs(self):
return self._kwargs
@property
def minimal_kwargs(self):
return self._minimal_kwargs
[docs] def remove(self, other):
if other == self:
return TrivialSpace
return self
[docs] def intersect(self, other):
if self in other.local_factors:
return self
return TrivialSpace
@property
def local_factors(self):
return (self, )
[docs] def is_strict_subfactor_of(self, other):
if isinstance(other, ProductSpace) and self in other.operands:
assert len(other.operands) > 1
return True
if other is FullSpace:
return True
return False
[docs] def next_basis_label_or_index(self, label_or_index, n=1):
"""Given the label or index of a basis state, return the label/index of
the next basis state.
More generally, if `n` is given, return the `n`'th next basis state
label/index; `n` may also be negative to obtain previous basis state
labels/indices.
The return type is the same as the type of `label_or_index`.
Args:
label_or_index (int or str or SymbolicLabelBase): If `int`, the
index of a basis state; if `str`, the label of a basis state
n (int): The increment
Raises:
IndexError: If going beyond the last or first basis state
ValueError: If `label` is not a label for any basis state in the
Hilbert space
.BasisNotSetError: If the Hilbert space has no defined basis
TypeError: if `label_or_index` is neither a :class:`str` nor an
:class:`int`, nor a :class:`SymbolicLabelBase`
"""
if isinstance(label_or_index, int):
new_index = label_or_index + n
if new_index < 0:
raise IndexError("index %d < 0" % new_index)
if self.has_basis:
if new_index >= self.dimension:
raise IndexError("index %d out of range for basis %s"
% (new_index, self._basis))
return new_index
elif isinstance(label_or_index, str):
label_index = self.basis_labels.index(label_or_index)
new_index = label_index + n
if (new_index < 0) or (new_index >= len(self._basis)):
raise IndexError("index %d out of range for basis %s"
% (new_index, self._basis))
return self._basis[new_index]
elif isinstance(label_or_index, SymbolicLabelBase):
return label_or_index.__class__(expr=label_or_index.expr + n)
else:
raise TypeError(
"Invalid type for label_or_index: %s"
% label_or_index.__class__.__name__)
[docs]@singleton_object
class TrivialSpace(HilbertSpace, Expression, metaclass=Singleton):
"""The 'nullspace', i.e. a one dimensional Hilbert space, which is a factor
space of every other Hilbert space.
This is the Hilbert space of scalars.
"""
def __hash__(self):
return hash(self.__class__)
@property
def args(self):
"""Empty tuple (no arguments)"""
return ()
@property
def _order_key(self):
return KeyTuple((-2, '_'))
@property
def dimension(self):
return 1
@property
def has_basis(self):
"""True, by definition (the basis is defined as
:obj:`~qnet.algebra.state_algebra.TrivialKet`)"""
return True
@property
def basis_states(self):
"""Yield the :obj:`~qnet.algebra.state_algebra.TrivialKet`"""
from qnet.algebra.core.state_algebra import TrivialKet
yield TrivialKet
def basis_state(self, index_or_label):
"""Return the basis state with the given index 0 or label "0".
All other indices or labels raise an exception.
Raises:
IndexError: if index is different from 0
KeyError: if label is differnt from ""
"""
from qnet.algebra.core.state_algebra import TrivialKet
if index_or_label in [0, "0"]:
return TrivialKet
else:
if isinstance(index_or_label, int):
raise IndexError(
"No index %d in basis for TrivialSpace" % index_or_label)
else:
raise KeyError(
"No label %d in basis for TrivialSpace" % index_or_label)
@property
def basis_labels(self):
"""The one-element tuple containing the label '0'"""
return tuple(["0", ])
def remove(self, other):
"""Removing any Hilbert space from the trivial space yields the trivial
space again"""
return self
def intersect(self, other):
"""The intersection of the trivial space with any other space is only
the trivial space"""
return self
@property
def local_factors(self):
"""Empty list (the trivial space has no factors)"""
return ()
def is_strict_subfactor_of(self, other):
"""The trivial space is a subfactor of any other space (except
itself)"""
if other is TrivialSpace:
return False
return True
@property
def label(self):
return "null"
[docs]@singleton_object
class FullSpace(HilbertSpace, Expression, metaclass=Singleton):
"""The 'full space', i.e. a Hilbert space that includes any other Hilbert
space as a tensor factor.
The `FullSpace` has no defined basis, any related properties will raise
:class:`.BasisNotSetError`
"""
@property
def args(self):
"""Empty tuple (no arguments)"""
return ()
def __hash__(self):
return hash(self.__class__)
@property
def _order_key(self):
return KeyTuple((-1, '_'))
def remove(self, other):
"""Raise AlgebraError, as the remaining space is undefined"""
raise AlgebraError("Cannot remove anything from FullSpace")
@property
def local_factors(self):
"""Raise AlgebraError, as the the factors of the full space are
undefined"""
raise AlgebraError("FullSpace has no local_factors")
def intersect(self, other):
"""Return `other`"""
return other
def is_strict_subfactor_of(self, other):
"""False, as the full space by definition is not contained in any other
space"""
return False
@property
def label(self):
return "total"
###############################################################################
# Algebra Operations
###############################################################################
[docs]class ProductSpace(HilbertSpace, Operation):
"""Tensor product of local Hilbert spaces
>>> hs1 = LocalSpace('1', basis=(0,1))
>>> hs2 = LocalSpace('2', basis=(0,1))
>>> hs = hs1 * hs2
>>> hs.basis_labels
('0,0', '0,1', '1,0', '1,1')
"""
_neutral_element = TrivialSpace
simplifications = [empty_trivial, assoc, convert_to_spaces, idem,
filter_neutral]
def __init__(self, *local_spaces):
if len(set(local_spaces)) != len(local_spaces):
raise ValueError("Nondistinct spaces: %s" % repr(local_spaces))
try:
self._dimension = functools.reduce(
operator.mul,
[ls.dimension for ls in local_spaces], 1)
except BasisNotSetError:
self._dimension = None
# determine the basis labels automatically
try:
ls_bases = [ls.basis_labels for ls in local_spaces]
basis = []
for label_tuple in cartesian_product(*ls_bases):
basis.append(",".join([str(l) for l in label_tuple]))
self._basis = tuple(basis)
except BasisNotSetError:
self._basis = None
op_keys = [space._order_key for space in local_spaces]
self._order_key = KeyTuple([v for op_key in op_keys for v in op_key])
super().__init__(*local_spaces) # Operation __init__
[docs] @classmethod
def create(cls, *local_spaces):
if any(local_space is FullSpace for local_space in local_spaces):
return FullSpace
return super().create(*local_spaces)
@property
def has_basis(self):
"""True if the all the local factors of the `ProductSpace` have a
defined basis"""
return self._basis is not None
@property
def basis_states(self):
"""Yield an iterator over the states (:class:`.TensorKet` instances)
that form the canonical basis of the Hilbert space
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
"""
from qnet.algebra.core.state_algebra import BasisKet, TensorKet
# importing locally avoids circular import
ls_bases = [ls.basis_labels for ls in self.local_factors]
for label_tuple in cartesian_product(*ls_bases):
yield TensorKet(
*[BasisKet(label, hs=ls) for (ls, label)
in zip(self.local_factors, label_tuple)])
@property
def basis_labels(self):
"""Tuple of basis labels. Each basis label consists of the labels of
the :class:`.BasisKet` states that factor the basis state, separated by
commas.
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
"""
if self._basis is None:
raise BasisNotSetError(
"Hilbert space %s has no defined basis" % str(self))
return self._basis
[docs] def basis_state(self, index_or_label):
"""Return the basis state with the given index or label.
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
IndexError: if there is no basis state with the given index
KeyError: if there is not basis state with the given label
"""
from qnet.algebra.core.state_algebra import BasisKet, TensorKet
if isinstance(index_or_label, int): # index
ls_bases = [ls.basis_labels for ls in self.local_factors]
label_tuple = list(cartesian_product(*ls_bases))[index_or_label]
try:
return TensorKet(
*[BasisKet(label, hs=ls) for (ls, label)
in zip(self.local_factors, label_tuple)])
except ValueError as exc_info:
raise IndexError(str(exc_info))
else: # label
local_labels = index_or_label.split(",")
if len(local_labels) != len(self.local_factors):
raise KeyError(
"label %s for Hilbert space %s must be comma-separated "
"concatenation of local labels" % (index_or_label, self))
try:
return TensorKet(
*[BasisKet(label, hs=ls) for (ls, label)
in zip(self.local_factors, local_labels)])
except ValueError as exc_info:
raise KeyError(str(exc_info))
@property
def dimension(self):
"""Dimension of the Hilbert space.
Raises:
.BasisNotSetError: if the Hilbert space has no defined basis
"""
if self._dimension is not None:
return self._dimension
else:
raise BasisNotSetError(
"Hilbert space %s has no defined basis" % str(self))
[docs] def remove(self, other):
"""Remove a particular factor from a tensor product space."""
if other is FullSpace:
return TrivialSpace
if other is TrivialSpace:
return self
if isinstance(other, ProductSpace):
oops = set(other.operands)
else:
oops = {other}
return ProductSpace.create(
*sorted(set(self.operands).difference(oops)))
@property
def local_factors(self):
"""The :class:`LocalSpace` instances that make up the product"""
return self.operands
[docs] @classmethod
def order_key(cls, obj):
"""Key by which operands are sorted"""
assert isinstance(obj, HilbertSpace)
return obj._order_key
[docs] def intersect(self, other):
"""Find the mutual tensor factors of two Hilbert spaces."""
if other is FullSpace:
return self
if other is TrivialSpace:
return TrivialSpace
if isinstance(other, ProductSpace):
other_ops = set(other.operands)
else:
other_ops = {other}
return ProductSpace.create(
*sorted(set(self.operands).intersection(other_ops)))
[docs] def is_strict_subfactor_of(self, other):
"""Test if a space is included within a larger tensor product space.
Not ``True`` if ``self == other``."""
if isinstance(other, ProductSpace):
return set(self.operands) < set(other.operands)
if other is FullSpace:
return True
return False