Source code for qnet.algebra.library.spin_algebra

"""Definitions for an algebra on spin (angular momentum) Hilbert spaces, both
for integer and half-integer spin"""
from abc import ABCMeta
from collections.__init__ import OrderedDict

import sympy
from sympy import sympify, sqrt

from ..core.hilbert_space_algebra import LocalSpace
from ..core.state_algebra import BasisKet
from ..core.operator_algebra import LocalOperator, PseudoInverse
from ...utils.indices import SpinIndex

__all__ = [
    'SpinSpace', 'SpinBasisKet', 'SpinOperator', 'Jz', 'Jplus', 'Jminus']
__private__ = ['Jpjmcoeff', 'Jzjmcoeff', 'Jmjmcoeff']


[docs]class SpinSpace(LocalSpace): """A Hilbert space for an integer or half-integer spin system For a given spin $N$, the resulting Hilbert space has dimension $2 N + 1$ with levels labeled from $-N$ to $+N$ (as strings) For an integer spin:: >>> hs = SpinSpace(label=0, spin=1) >>> hs.dimension 3 >>> hs.basis_labels ('-1', '0', '+1') For a half-integer spin:: >>> hs = SpinSpace(label=0, spin=sympy.Rational(3, 2)) >>> hs.spin 3/2 >>> hs.dimension 4 >>> hs.basis_labels ('-3/2', '-1/2', '+1/2', '+3/2') For convenience, you may also give `spin` as a tuple or a string:: >>> hs = SpinSpace(label=0, spin=(3, 2)) >>> assert hs == SpinSpace(label=0, spin=sympy.Rational(3, 2)) >>> hs = SpinSpace(label=0, spin='3/2') >>> assert hs == SpinSpace(label=0, spin=(3, 2)) You may use custom labels, e.g.:: >>> hs = SpinSpace(label='s', spin='1/2', basis=('-', '+')) >>> hs.basis_labels ('-', '+') The labels "up" and "down" are recognized and printed as the appropritate arrow symbols:: >>> hs = SpinSpace(label='s', spin='1/2', basis=('down', 'up')) >>> unicode(BasisKet('up', hs=hs)) '|↑⟩⁽ˢ⁾' >>> unicode(BasisKet('down', hs=hs)) '|↓⟩⁽ˢ⁾' Raises: ValueError: if `spin` is not an integer or half-integer greater than zero """ _basis_label_types = (str, SpinIndex) # acceptable types for labels def __init__( self, label, *, spin, basis=None, local_identifiers=None, order_index=None): if isinstance(spin, tuple): spin = sympy.sympify(spin[0]) / spin[1] else: spin = sympy.sympify(spin) self._spin = spin if not (2 * spin).is_integer: raise ValueError( "spin %s must be an integer or half-integer" % spin) try: dimension = int(2 * spin) + 1 except TypeError: raise ValueError( "spin %s must be an integer or half-integer" % spin) if dimension <= 1: raise ValueError("spin %s must be greater than zero" % spin) bottom = -spin if basis is None: basis = tuple([ SpinIndex._static_render(bottom + n) for n in range(dimension)]) else: # sometimes people don't think and use some of the "canonical" TLS # labels in the wrong order. We can catch it, so why not? if basis == ('up', 'down') or basis == ('+', '-'): raise ValueError( "Invalid basis: you've switched %s and %s" % basis) super().__init__( label=label, basis=basis, dimension=dimension, local_identifiers=local_identifiers, order_index=order_index) # rewrite the kwargs from super() self._kwargs = OrderedDict([ ('spin', self._spin), ('local_identifiers', self._sorted_local_identifiers), ('order_index', self._order_index)]) self._minimal_kwargs = self._kwargs.copy() if local_identifiers is None: del self._minimal_kwargs['local_identifiers'] if order_index is None: del self._minimal_kwargs['order_index']
[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 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. Returns a :class:`str` label if `label_or_index` is a :class:`str` or :class:`int`, or a :class:`SpinIndex` if `label_or_index` is a :class:`SpinIndex`. Args: label_or_index (int or str or SpinIndex): If `int`, the zero-based 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:`SpinIndex` Note: This differs from its super-method only by never returning an integer index (which is not accepted when instantiating a :class:`BasisKet` for a :class:`SpinSpace`) """ if isinstance(label_or_index, int): new_index = label_or_index + n if new_index < 0: raise IndexError("index %d < 0" % new_index) if new_index >= self.dimension: raise IndexError( "index %d out of range for basis %s" % (new_index, self._basis)) return self.basis_labels[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_labels[new_index] elif isinstance(label_or_index, SpinIndex): return label_or_index.__class__(expr=label_or_index.expr + n)
@property def spin(self) -> sympy.Rational: """The spin-number associated with the :class:`SpinSpace` This can be a SymPy integer or a half-integer. """ return self._spin @property def multiplicity(self) -> int: """The multiplicity of the Hilbert space, $2 S + 1$. This is equivalent to the dimension:: >>> hs = SpinSpace('s', spin=sympy.Rational(3, 2)) >>> hs.multiplicity == 4 == hs.dimension True """ return int(2 * self._spin) + 1
[docs]def SpinBasisKet(*numer_denom, hs): """Constructor for a :class:`BasisKet` for a :class:`SpinSpace` For a half-integer spin system:: >>> hs = SpinSpace('s', spin=(3, 2)) >>> assert SpinBasisKet(1, 2, hs=hs) == BasisKet("+1/2", hs=hs) For an integer spin system:: >>> hs = SpinSpace('s', spin=1) >>> assert SpinBasisKet(1, hs=hs) == BasisKet("+1", hs=hs) Note that ``BasisKet(1, hs=hs)`` with an integer index (which would hypothetically refer to ``BasisKet("0", hs=hs)`` is not allowed for spin systems:: >>> BasisKet(1, hs=hs) Traceback (most recent call last): ... TypeError: label_or_index must be an instance of one of str, SpinIndex; not int Raises: TypeError: if `hs` is not a :class:`SpinSpace` or the wrong number of positional arguments is given ValueError: if any of the positional arguments are out range for the given `hs` """ try: spin_numer, spin_denom = hs.spin.as_numer_denom() except AttributeError: raise TypeError( "hs=%s for SpinBasisKet must be a SpinSpace instance" % hs) assert spin_denom in (1, 2) if spin_denom == 1: # integer spin if len(numer_denom) != 1: raise TypeError( "SpinBasisKet requires exactly one positional argument for an " "integer-spin Hilbert space") numer = numer_denom[0] if numer < -spin_numer or numer > spin_numer: raise ValueError( "spin quantum number %s must be in range (%s, %s)" % (numer, -spin_numer, spin_numer)) label = str(numer) if numer > 0: label = "+" + label return BasisKet(label, hs=hs) else: # half-integer spin if len(numer_denom) != 2: raise TypeError( "SpinBasisKet requires exactly two positional arguments for a " "half-integer-spin Hilbert space") numer, denom = numer_denom numer = int(numer) denom = int(denom) if denom != 2: raise ValueError( "The second positional argument (denominator of the spin " "quantum number) must be 2, not %s" % denom) if numer < -spin_numer or numer > spin_numer: raise ValueError( "spin quantum number %s/%s must be in range (%s/2, %s/2)" % (numer, denom, -spin_numer, spin_numer)) label = str(numer) if numer > 0: label = "+" + label label = label + "/2" return BasisKet(label, hs=hs)
[docs]class SpinOperator(LocalOperator, metaclass=ABCMeta): """Base class for operators in a spin space""" _hs_cls = SpinSpace def __init__(self, *args, hs): super().__init__(*args, hs=hs) if not isinstance(self.space, SpinSpace): raise TypeError( "hs %s must be an instance of SpinSpace" % self.space)
[docs]class Jz(SpinOperator): """Spin (angular momentum) operator in z-direction $\Op{J}_z$ is the $z$ component of a general spin operator acting on a particular :class:`SpinSpace` `hs` of freedom with well defined spin quantum number $s$. It is Hermitian:: >>> hs = SpinSpace(1, spin=(1, 2)) >>> print(ascii(Jz(hs=hs).adjoint())) J_z^(1) :class:`Jz`, :class:`Jplus` and :class:`Jminus` satisfy the angular momentum commutator algebra:: >>> print(ascii((Jz(hs=hs) * Jplus(hs=hs) - ... Jplus(hs=hs)*Jz(hs=hs)).expand())) J_+^(1) >>> print(ascii((Jz(hs=hs) * Jminus(hs=hs) - ... Jminus(hs=hs)*Jz(hs=hs)).expand())) -J_-^(1) >>> print(ascii((Jplus(hs=hs) * Jminus(hs=hs) ... - Jminus(hs=hs)*Jplus(hs=hs)).expand())) 2 * J_z^(1) >>> Jplus(hs=hs).dag() == Jminus(hs=hs) True >>> Jminus(hs=hs).dag() == Jplus(hs=hs) True Printers should represent this operator with the default identifier:: >>> Jz._identifier 'J_z' A custom identifier may be define using `hs`'s `local_identifiers` argument. """ _identifier = 'J_z' def __init__(self, *, hs): super().__init__(hs=hs) def _adjoint(self): return self def _pseudo_inverse(self): return PseudoInverse(self)
[docs]class Jplus(SpinOperator): """Raising operator of a spin space $\Op{J}_{+} = \Op{J}_x + i \op{J}_y$ is the raising ladder operator of a general spin operator acting on a particular :class:`SpinSpace` `hs` with well defined spin quantum number $s$. It's adjoint is the lowering operator:: >>> hs = SpinSpace(1, spin=(1, 2)) >>> print(ascii(Jplus(hs=hs).adjoint())) J_-^(1) :class:`Jz`, :class:`Jplus` and :class:`Jminus` satisfy that angular momentum commutator algebra, see :class:`Jz` Printers should represent this operator with the default identifier:: >>> Jplus._identifier 'J_+' A custom identifier may be define using `hs`'s `local_identifiers` argument. """ _identifier = 'J_+' def __init__(self, *, hs): super().__init__(hs=hs) def _adjoint(self): return Jminus(hs=self.space) def _pseudo_inverse(self): return PseudoInverse(self)
[docs]class Jminus(SpinOperator): """Lowering operator on a spin space $\Op{J}_{-} = \Op{J}_x - i \op{J}_y$ is the lowering ladder operator of a general spin operator acting on a particular :class:`SpinSpace` `hs` with well defined spin quantum number $s$. It's adjoint is the raising operator:: >>> hs = SpinSpace(1, spin=(1, 2)) >>> print(ascii(Jminus(hs=hs).adjoint())) J_+^(1) :class:`Jz`, :class:`Jplus` and :class:`Jminus` satisfy that angular momentum commutator algebra, see :class:`Jz`. Printers should represent this operator with the default identifier:: >>> Jminus._identifier 'J_-' A custom identifier may be define using `hs`'s `local_identifiers` argument. """ _identifier = 'J_-' def __init__(self, *, hs): super().__init__(hs=hs) def _adjoint(self): return Jplus(hs=self.space) def _pseudo_inverse(self): return PseudoInverse(self)
[docs]def Jpjmcoeff(ls, m, shift=False) -> sympy.Expr: r'''Eigenvalue of the $\Op{J}_{+}$ (:class:`Jplus`) operator .. math:: \Op{J}_{+} \ket{s, m} = \sqrt{s (s+1) - m (m+1)} \ket{s, m} where the multiplicity $s$ is implied by the size of the Hilbert space `ls`: there are $2s+1$ eigenstates with $m = -s, -s+1, \dots, s$. Args: ls (LocalSpace): The Hilbert space in which the $\Op{J}_{+}$ operator acts. m (str or int): If str, the label of the basis state of `hs` to which the operator is applied. If integer together with ``shift=True``, the zero-based index of the basis state. Otherwise, directly the quantum number $m$. shift (bool): If True for a integer value of `m`, treat `m` as the zero-based index of the basis state (i.e., shift `m` down by $s$ to obtain the quantum number $m$) ''' assert isinstance(ls, SpinSpace) n = ls.dimension s = sympify(n - 1) / 2 assert n == int(2 * s + 1) if isinstance(m, str): m = ls.basis_labels.index(m) - s # m is now Sympy expression elif isinstance(m, int): if shift: assert 0 <= m < n m = m - s return sqrt(s * (s + 1) - m * (m + 1))
[docs]def Jzjmcoeff(ls, m, shift) -> sympy.Expr: r'''Eigenvalue of the $\Op{J}_z$ (:class:`Jz`) operator .. math:: \Op{J}_{z} \ket{s, m} = m \ket{s, m} See also :func:`Jpjmcoeff`. ''' assert isinstance(ls, SpinSpace) n = ls.dimension s = sympify(n - 1) / 2 assert n == int(2 * s + 1) if isinstance(m, str): return ls.basis.index(m) - s elif isinstance(m, int): if shift: assert 0 <= m < n return m - s else: return sympify(m)
[docs]def Jmjmcoeff(ls, m, shift) -> sympy.Expr: r'''Eigenvalue of the $\Op{J}_{-}$ (:class:`Jminus`) operator .. math:: \Op{J}_{-} \ket{s, m} = \sqrt{s (s+1) - m (m-1)} \ket{s, m} See also :func:`Jpjmcoeff`. ''' assert isinstance(ls, SpinSpace) n = ls.dimension s = sympify(n - 1) / 2 assert n == int(2 * s + 1) if isinstance(m, str): m = ls.basis.index(m) - s # m is now Sympy expression elif isinstance(m, int): if shift: assert 0 <= m < n m = m - s return sqrt(s * (s + 1) - m * (m - 1))