Source code for quantarhei.qm.liouvillespace.superoperator

"""Class representing superoperators


This class represents operators on the space of Hilbert space operators.
Usually, we refer to such operators as superoperators.




Class Details
-------------

"""

from __future__ import annotations

import copy as _copy_module
from typing import Any

# dependencies imports
import numpy

# quantarhei imports
from ... import COMPLEX, REAL
from ...core.managers import BasisManaged
from ...exceptions import QuantarheiError
from ...utils.types import BasisManagedComplexArray


[docs] class SuperOperator(BasisManaged): """Class representing superoperators Parameters ---------- dim : int Dimension of the superoperator data : array Data of the superoperator real : bool Is this data real? False if they are complex Examples -------- Creation of an empty SuperOperator is allowed >>> So = SuperOperator() >>> print(So.dim is None) True But calling uninitialize data attribute raises an exception >>> print(So.data is None) # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: 'SuperOperator' object has no attribute '_data'... Creating with `dim` arguments creates a zero superoperator >>> So = SuperOperator(dim=3) >>> print(So.dim) 3 >>> print(So.data.shape) (3, 3, 3, 3) Creating superoperator from data checks, if the data object has the correct shape >>> data = numpy.zeros((3,3,3)) >>> So = SuperOperator(data=data) Traceback (most recent call last): ... quantarhei.exceptions.QuantarheiError: The data do not represent a superoperator >>> data = numpy.zeros((3,1,3,2)) >>> So = SuperOperator(data=data) Traceback (most recent call last): ... quantarhei.exceptions.QuantarheiError: `data` has to be `square` four-dimensional matrix """ data: numpy.ndarray = BasisManagedComplexArray("data") # type: ignore[assignment] _data: numpy.ndarray name: str = "" def __init__( self, dim: int | None = None, data: Any = None, real: bool = False ) -> None: # Set the currently used basis cb = self.manager.get_current_basis() self.set_current_basis(cb) # unless it is the basis outside any context if cb != 0: self.manager.register_with_basis(cb, self) self._data_initialized = False self.dim = dim if dim is not None: if real: self.data = numpy.zeros((dim, dim, dim, dim), dtype=REAL) else: self.data = numpy.zeros((dim, dim, dim, dim), dtype=COMPLEX) elif data is not None: self.data = data if len(data.shape) != 4: raise QuantarheiError("The data do not represent a superoperator") Nd = data.shape[0] if numpy.any(numpy.array(data.shape) - Nd): raise QuantarheiError( "`data` has to be `square` four-dimensional matrix" ) self.dim = data.shape[0]
[docs] def apply(self, oper: Any, target: Any = None, copy: bool = True) -> Any: """Applies superoperator to an operator Parameters ---------- oper : Operator Operator on which the present superoperator is applied Examples -------- >>> import quantarhei as qr >>> import numpy >>> op = qr.ReducedDensityMatrix(data=[[0.0, 1.0], [1.0, 0.0]]) Let's take a matrix A >>> A = numpy.zeros((2,2)) >>> A[0,1] = 1.0 >>> A[1,0] = 1.0 and create a superoperator equivalent to matrix multiplication .. math:: A\\rho = \\sum_{i}A_{ik}\\rho_{kj} = \\ \\sum_{kl}\\delta_{jl}A_{ik}\\rho_{kl} = \\ \\sum_{kl}D_{ijkl}\\rho_{kl} D_{ijkl} = \\delta_{jl}A_{ik} >>> data = numpy.zeros((2,2,2,2)) >>> for i in range(2): ... for j in range(2): ... for k in range(2): ... for l in range(2): ... if j == l: ... data[i,j,k,l] = A[i,k] ... else: ... data[i,j,k,l] = 0.0 >>> Dd = SuperOperator(data=data) Now we can check that maultiplication and application of the superoperator lead to the same result. >>> B1 = Dd.apply(op) >>> B2 = numpy.dot(A,op.data) >>> numpy.allclose(B1.data, B2) True Every time you do this, new density matrix object is created. To avoid this, we can apply the operation without copying the object. >>> B3 = Dd.apply(op, copy=False) >>> print(B3 == op) True `B3` is now a different reference to the same object as `op`. We can check that the result of the operation is still correct: >>> numpy.allclose(B3.data, B2) True """ if copy: oper_ven = _copy_module.copy(oper) try: # if oper is Quantarhei operator oper_ven.data = numpy.tensordot(self.data, oper.data) except AttributeError: # if oper is numpy array oper_ven = numpy.tensordot(self.data, oper) return oper_ven oper.data = numpy.tensordot(self.data, oper.data) return oper
[docs] def transform(self, SS: numpy.ndarray, inv: numpy.ndarray | None = None) -> None: """Transforms the superoperator to a new basis Transformation of the superoperator follows similar rules as the one of operators. An operar :math:`A` is transformed as follows: .. math:: A_{\\alpha\\beta} = \\sum_{ij}(s^{-1})_{\\alpha i}A_{ij}s_{j\\beta} and .. math:: A_{ij} = \\sum_{\\alpha\\beta} s_{i\\alpha} \\ A_{\\alpha\\beta}(s^{-1})_{\\beta j} The transformation matrix :math:`s` is obtained by standard diagonalization routines and its elements can be expressed in Dirac notation as .. math:: s_{i\\alpha} = \\langle i | \\alpha \\rangle The inverse matrix :math:`s^{-1}` is obtained by transposition, because .. math:: \\langle i | j \\rangle = \\delta_{ij} = \\ \\sum_{\\alpha} \\langle i | \\alpha \\rangle \\ \\langle \\alpha | j \\rangle and we see that .. math:: (s^{-1})_{\\alpha i} = s_{i \\alpha}. Given an operator :math:`A` which is a result of an action of the superoperator :math:`R` on operar :math:`A` we can see that the transformation occurs as follows: .. math:: A_{ij} = \\sum_{kl}R_{ijkl}B_{kl} .. math:: A_{\\alpha\\beta} = \\ \\sum_{ij}(s^{-1})_{\\alpha i}A_{ij}s_{j\\beta} = \\ \\sum_{ijkl} (s^{-1})_{\\alpha i} s_{j\\beta} R_{ijkl} B_{kl} Using the back transformation of the operator :math:`B` we obtaine .. math:: A_{\\alpha\\beta} = \\ \\sum_{ij}(s^{-1})_{\\alpha i}A_{ij}s_{j\\beta} = \\ \\sum_{\\gamma\\delta} \\left [ \\right ] B_{\\gamma\\delta} which translates into .. math:: R_{\\alpha\\beta\\gamma\\delta} = \\ \\sum_{ijkl} s_{i \\alpha} \\ s_{j\\beta} R_{ijkl} \\ s_{k\\gamma}s_{l\\delta} Parameters ---------- SS : float matrix Transformation matrix inv : float matrix, optional Inverse of the transformation matrix Examples -------- """ if self.manager.warn_about_basis_change: print(f"\nQr >>> SuperOperator '{self.name}' changes basis") # # if inverse matrix not present, we create it # if inv is None: S1 = numpy.linalg.inv(SS) else: S1 = inv # dimension of the transformation matrix dim = SS.shape[0] # # Dimension 4 means a single, time independent superoperator # if self._data.ndim == 4: for c in range(dim): for d in range(dim): self._data[:, :, c, d] = numpy.dot( S1, numpy.dot(self._data[:, :, c, d], SS) ) for a in range(dim): for b in range(dim): self._data[a, b, :, :] = numpy.dot( S1, numpy.dot(self._data[a, b, :, :], SS) ) # # Larger dimension means more superoperators or time dependence # else: for tt in range(self._data.shape[0]): for c in range(dim): for d in range(dim): self._data[tt, :, :, c, d] = numpy.dot( S1, numpy.dot(self._data[tt, :, :, c, d], SS) ) for a in range(dim): for b in range(dim): self._data[tt, a, b, :, :] = numpy.dot( S1, numpy.dot(self._data[tt, a, b, :, :], SS) )