content: """The Quantum Object (Qobj) class, for representing quantum states and
  operators, and related functions.
  """
  from __future__ import annotations
  
  import functools
  import numbers
  import warnings
  from typing import Any, Literal, Union, overload
  import numpy as np
  from numpy.typing import ArrayLike
  import scipy.sparse
  
  from .. import __version__
  from ..settings import settings
  from . import data as _data
  from qutip.typing import LayerType, DimensionLike
  import qutip
  from .dimensions import (
      enumerate_flat, flatten, unflatten, Dimensions, to_tensor_rep
  )
  
  __all__ = ['Qobj', 'ptrace']
  
  
  _NORM_FUNCTION_LOOKUP = {
      'tr': _data.norm.trace,
      'one': _data.norm.one,
      'max': _data.norm.max,
      'fro': _data.norm.frobenius,
      'l2': _data.norm.l2,
  }
  _NORM_ALLOWED_MATRIX = {'tr', 'fro', 'one', 'max'}
  _NORM_ALLOWED_VECTOR = {'l2', 'max'}
  
  _CALL_ALLOWED = {
      ('super', 'oper'),
      ('super', 'ket'),
      ('oper', 'ket'),
  }
  
  
  def _require_equal_type(method):
      """
      Decorate a binary Qobj method to ensure both operands are Qobj and of the
      same type and dimensions.  Promote numeric scalar to identity matrices of
      the same type and shape.
      """
      @functools.wraps(method)
      def out(self, other):
          if isinstance(other, Qobj):
              if self._dims != other._dims:
                  msg = (
                      "incompatible dimensions "
                      + repr(self.dims) + " and " + repr(other.dims)
                  )
                  raise ValueError(msg)
              return method(self, other)
          if other == 0:
              return method(self, other)
          if self._dims.issquare and isinstance(other, numbers.Number):
              scale = complex(other)
              other = Qobj(_data.identity(self.shape[0], scale,
                                          dtype=type(self.data)),
                           dims=self._dims,
                           isherm=(scale.imag == 0),
                           isunitary=(abs(abs(scale)-1) < settings.core['atol']),
                           copy=False)
              return method(self, other)
          return NotImplemented
  
      return out
  
  
  def _latex_real(x):
      if not x:
          return "0"
      if not 0.001 <= abs(x) < 1000:
          base, exp = "{:.3e}".format(x).split('e')
          return base + r"\times10^{{ {:d} }}".format(int(exp))
      if abs(x - int(x)) < 0.001:
          return "{:d}".format(round(x))
      return "{:.3f}".format(x)
  
  
  def _latex_complex(x):
      if abs(x.imag) < 0.001:
          return _latex_real(x.real)
      if abs(x.real) < 0.001:
          return _latex_real(x.imag) + "j"
      sign = "+" if x.imag > 0 else "-"
      return "(" + _latex_real(x.real) + sign + _latex_real(abs(x.imag)) + "j)"
  
  
  def _latex_row(row, cols, data):
      if row is None:
          bits = (r"\ddots" if col is None else r"\vdots" for col in cols)
      else:
          bits = (r"\cdots" if col is None else _latex_complex(data[row, col])
                  for col in cols)
      return " & ".join(bits)
  
  
  class Qobj:
      """
      A class for representing quantum objects, such as quantum operators and
      states.
  
      The Qobj class is the QuTiP representation of quantum operators and state
      vectors. This class also implements math operations +,-,* between Qobj
      instances (and / by a C-number), as well as a collection of common
      operator/state operations.  The Qobj constructor optionally takes a
      dimension ``list`` and/or shape ``list`` as arguments.
  
      Parameters
      ----------
      arg: array_like, data object or :obj:`.Qobj`
          Data for vector/matrix representation of the quantum object.
      dims: list
          Dimensions of object used for tensor products.
      copy: bool
          Flag specifying whether Qobj should get a copy of the
          input data, or use the original.
  
  
      Attributes
      ----------
      data : object
          The data object storing the vector / matrix representation of the
          `Qobj`.
      dtype : type
          The data-layer type used for storing the data. The possible types are
          described in `Qobj.to <./classes.html#qutip.core.qobj.Qobj.to>`__.
      dims : list
          List of dimensions keeping track of the tensor structure.
      shape : list
          Shape of the underlying `data` array.
      type : str
          Type of quantum object: 'bra', 'ket', 'oper', 'operator-ket',
          'operator-bra', or 'super'.
      superrep : str
          Representation used if `type` is 'super'. One of 'super'
          (Liouville form), 'choi' (Choi matrix with tr = dimension),
          or 'chi' (chi-matrix representation).
      isherm : bool
          Indicates if quantum object represents Hermitian operator.
      isunitary : bool
          Indictaes if quantum object represents unitary operator.
      iscp : bool
          Indicates if the quantum object represents a map, and if that map is
          completely positive (CP).
      ishp : bool
          Indicates if the quantum object represents a map, and if that map is
          hermicity preserving (HP).
      istp : bool
          Indicates if the quantum object represents a map, and if that map is
          trace preserving (TP).
      iscptp : bool
          Indicates if the quantum object represents a map that is completely
          positive and trace preserving (CPTP).
      isket : bool
          Indicates if the quantum object represents a ket.
      isbra : bool
          Indicates if the quantum object represents a bra.
      isoper : bool
          Indicates if the quantum object represents an operator.
      issuper : bool
          Indicates if the quantum object represents a superoperator.
      isoperket : bool
          Indicates if the quantum object represents an operator in column vector
          form.
      isoperbra : bool
          Indicates if the quantum object represents an operator in row vector
          form.
  
      Methods
      -------
      copy()
          Create copy of Qobj
      conj()
          Conjugate of quantum object.
      contract()
          Contract subspaces of the tensor structure which are 1D.
      cosm()
          Cosine of quantum object.
      dag()
          Adjoint (dagger) of quantum object.
      data_as(format, copy)
          Vector / matrix representation of quantum object.
      diag()
          Diagonal elements of quantum object.
      dnorm()
          Diamond norm of quantum operator.
      drop_scalar_dims()
          Drops one-dimensional subsystems from the list of dimensions (``dims``)
      dual_chan()
          Dual channel of quantum object representing a CP map.
      eigenenergies(sparse=False, sort='low', eigvals=0, tol=0, maxiter=100000)
          Returns eigenenergies (eigenvalues) of a quantum object.
      eigenstates(sparse=False, sort='low', eigvals=0, tol=0, maxiter=100000)
          Returns eigenenergies and eigenstates of quantum object.
      expm()
          Matrix exponential of quantum object.
      full(order='C')
          Returns dense array of quantum object `data` attribute.
      groundstate(sparse=False, tol=0, maxiter=100000)
          Returns eigenvalue and eigenket for the groundstate of a quantum
          object.
      inv()
          Return a Qobj corresponding to the matrix inverse of the operator.
      logm()
          Matrix logarithm of quantum operator.
      matrix_element(bra, ket)
          Returns the matrix element of operator between `bra` and `ket` vectors.
      norm(norm='tr', sparse=False, tol=0, maxiter=100000)
          Returns norm of a ket or an operator.
      overlap(other)
          Overlap between two state vectors or two operators.
      permute(order)
          Returns composite qobj with indices reordered.
      basis_expansion(decimal_places=14, term_separator=" + ",
                      dim_separator="auto", sort="largest-first")
          Return a string-representation of the basis expansion
          E.g. ``"(0.5+0.15j) |010> + (0.25+0j) |000> + ..."``
      proj()
          Computes the projector for a ket or bra vector.
      ptrace(sel)
          Returns quantum object for selected dimensions after performing
          partial trace.
      purity()
          Calculates the purity of a quantum object.
      sinm()
          Sine of quantum object.
      sqrtm()
          Matrix square root of quantum object.
      tidyup(atol=1e-12)
          Removes small elements from quantum object.
      tr()
          Trace of quantum object.
      trans()
          Transpose of quantum object.
      transform(inpt, inverse=False)
          Performs a basis transformation defined by `inpt` matrix.
      trunc_neg(method='clip')
          Removes negative eigenvalues and returns a new Qobj that is
          a valid density operator.
      unit(norm='tr', sparse=False, tol=0, maxiter=100000)
          Returns normalized quantum object.
  
      """
      # Disable ufuncs from acting directly on Qobj.
      __array_ufunc__ = None
  
      def _initialize_data(self, arg, dims, copy):
          if isinstance(arg, _data.Data):
              self._data = arg.copy() if copy else arg
              self._dims = Dimensions(dims or [[arg.shape[0]], [arg.shape[1]]])
          elif isinstance(arg, Qobj):
              self._data = arg.data.copy() if copy else arg.data
              self._dims = Dimensions(dims or arg._dims)
              if self._isherm is None and arg._isherm is not None:
                  self._isherm = arg._isherm
              if self._isunitary is None and arg._isunitary is not None:
                  self._isunitary = arg._isunitary
          else:
              self._data = _data.create(arg, copy=copy)
              self._dims = Dimensions(
                  dims or [[self._data.shape[0]], [self._data.shape[1]]]
              )
          if self._dims.shape != self._data.shape:
              raise ValueError('Provided dimensions do not match the data: ' +
                               f"{self._dims.shape} vs {self._data.shape}")
  
      def __init__(
          self,
          arg: ArrayLike | Any = None,
          dims: DimensionLike = None,
          copy: bool = True,
          superrep: str = None,
          isherm: bool = None,
          isunitary: bool = None,
          dtype: type | str = None,
      ):
          self._isherm = isherm
          self._isunitary = isunitary
          self._initialize_data(arg, dims, copy)
  
          if superrep is not None:
              self.superrep = superrep
  
          if (
              isinstance(arg, list)
              or dtype
              or settings.core["default_dtype_scope"] == "full"
          ):
              dtype = dtype or settings.core["default_dtype"]
              if dtype is None or isinstance(self._data, _data.to.parse(dtype)):
                  return
              self._data = _data.to(dtype, self._data)
  
      def copy(self) -> Qobj:
          """Create identical copy"""
          return Qobj(arg=self._data,
                      dims=self._dims,
                      isherm=self._isherm,
                      isunitary=self._isunitary,
                      dtype=self.dtype,
                      copy=True)
  
      @property
      def dims(self) -> list[list[int]] | list[list[list[int]]]:
          return self._dims.as_list()
  
      @dims.setter
      def dims(self, dims: list[list[int]] | list[list[list[int]]] | Dimensions):
          dims = Dimensions(dims, rep=self.superrep)
          if dims.shape != self._data.shape:
              raise ValueError('Provided dimensions do not match the data: ' +
                               f"{dims.shape} vs {self._data.shape}")
          self._dims = dims
  
      @property
      def type(self) -> str:
          return self._dims.type
  
      @property
      def superrep(self) -> str:
          return self._dims.superrep
  
      @superrep.setter
      def superrep(self, super_rep: str):
          self._dims = self._dims.replace_superrep(super_rep)
  
      @property
      def data(self) -> _data.Data:
          return self._data
  
      @data.setter
      def data(self, data: _data.Data):
          if not isinstance(data, _data.Data):
              raise TypeError('Qobj data must be a data-layer format.')
          if self._dims.shape != data.shape:
              raise ValueError('Provided data do not match the dimensions: ' +
                               f"{self._dims.shape} vs {data.shape}")
          self._data = data
  
      @property
      def dtype(self):
          return type(self._data)
  
      def to(self, data_type: LayerType, copy: bool=False) -> Qobj:
          """
          Convert the underlying data store of this `Qobj` into a different
          storage representation.
  
          The different storage representations available are the "data-layer
          types" which are known to :obj:`qutip.core.data.to`.  By default, these
          are :class:`~qutip.core.data.CSR`, :class:`~qutip.core.data.Dense` and
          :class:`~qutip.core.data.Dia`, which respectively construct a
          compressed sparse row matrix, diagonal matrix and a dense one.  Certain
          algorithms and operations may be faster or more accurate when using a
          more appropriate data store.
  
          Parameters
          ----------
          data_type : type, str
              The data-layer type or its string alias that the data of this
              :class:`Qobj` should be converted to.
  
          copy : Bool
              If the data store is already in the format requested, whether the
              function should return returns `self` or a copy.
  
          Returns
          -------
          Qobj
              A :class:`Qobj` with the data stored in the requested format.
          """
          data_type = _data.to.parse(data_type)
          if type(self._data) is data_type and copy:
              return self.copy()
          elif type(self._data) is data_type:
              return self
          return Qobj(
              _data.to(data_type, self._data),
              dims=self._dims,
              isherm=self._isherm,
              isunitary=self._isunitary,
              dtype=data_type,
              copy=copy
          )
  
      @_require_equal_type
      def __add__(self, other: Qobj | complex) -> Qobj:
          if other == 0:
              return self.copy()
          return Qobj(_data.add(self._data, other._data),
                      dims=self._dims,
                      isherm=(self._isherm and other._isherm) or None,
                      copy=False)
  
      def __radd__(self, other: Qobj | complex) -> Qobj:
          return self.__add__(other)
  
      @_require_equal_type
      def __sub__(self, other: Qobj | complex) -> Qobj:
          if other == 0:
              return self.copy()
          return Qobj(_data.sub(self._data, other._data),
                      dims=self._dims,
                      isherm=(self._isherm and other._isherm) or None,
                      copy=False)
  
      def __rsub__(self, other: Qobj | complex) -> Qobj:
          return self.__neg__().__add__(other)
  
      def __mul__(self, other: complex) -> Qobj:
          """
          If other is a Qobj, we dispatch to __matmul__. If not, we
          check that other is a valid complex scalar, i.e., we can do
          complex(other). Otherwise, we return NotImplemented.
          """
  
          if isinstance(other, Qobj):
              return self.__matmul__(other)
  
          # We send other to mul instead of complex(other) to be more flexible.
          # The dispatcher can then decide how to handle other and return
          # TypeError if it does not know what to do with the type of other.
          try:
              out = _data.mul(self._data, other)
          except TypeError:
              return NotImplemented
  
          # Infer isherm and isunitary if possible
          try:
              multiplier = complex(other)
              isherm = (self._isherm and multiplier.imag == 0) or None
              isunitary = (abs(abs(multiplier) - 1) < settings.core['atol']
                           if self._isunitary else None)
          except TypeError:
              isherm = None
              isunitary = None
  
          return Qobj(out,
                      dims=self._dims,
                      isherm=isherm,
                      isunitary=isunitary,
                      copy=False)
  
      def __rmul__(self, other: complex) -> Qobj:
          # Shouldn't be here unless `other.__mul__` has already been tried, so
          # we _shouldn't_ check that `other` is `Qobj`.
          return self.__mul__(other)
  
      def __matmul__(self, other: Qobj) -> Qobj:
          if not isinstance(other, Qobj):
              try:
                  other = Qobj(other)
              except TypeError:
                  return NotImplemented
          new_dims = self._dims @ other._dims
          if new_dims.type == 'scalar':
              return _data.inner(self._data, other._data)
          if self.isket and other.isbra:
              return Qobj(
                  _data.matmul_outer(self._data, other._data),
                  dims=new_dims,
                  isunitary=False,
                  copy=False
              )
  
          return Qobj(
              _data.matmul(self._data, other._data),
              dims=new_dims,
              isunitary=self._isunitary and other._isunitary,
              copy=False
          )
  
      def __truediv__(self, other: complex) -> Qobj:
          return self.__mul__(1 / other)
  
      def __neg__(self) -> Qobj:
          return Qobj(_data.neg(self._data),
                      dims=self._dims,
                      isherm=self._isherm,
                      isunitary=self._isunitary,
                      copy=False)
  
      def __getitem__(self, ind):
          # TODO: should we require that data-layer types implement this?  This
          # isn't the right way of handling it, for sure.
          if isinstance(self._data, _data.CSR):
              data = self._data.as_scipy()
          elif isinstance(self._data, _data.Dense):
              data = self._data.as_ndarray()
          else:
              data = self._data
          try:
              out = data[ind]
              return out.toarray() if scipy.sparse.issparse(out) else out
          except TypeError:
              pass
          return data.to_array()[ind]
  
      def __eq__(self, other) -> bool:
          if self is other:
              return True
          if not isinstance(other, Qobj) or self._dims != other._dims:
              return False
          # isequal uses both atol and rtol from settings.core
          return _data.isequal(self._data, other._data)
  
      def __pow__(self, n: int, m=None) -> Qobj:  # calculates powers of Qobj
          if (
              self.type not in ('oper', 'super')
              or self._dims[0] != self._dims[1]
              or m is not None
              or not isinstance(n, numbers.Integral)
              or n < 0
          ):
              return NotImplemented
          return Qobj(_data.pow(self._data, n),
                      dims=self._dims,
                      isherm=self._isherm,
                      isunitary=self._isunitary,
                      copy=False)
  
      def _str_header(self):
          out = ", ".join([
              "Quantum object: dims=" + str(self.dims),
              "shape=" + str(self._data.shape),
              "type=" + repr(self.type),
              "dtype=" + self.dtype.__name__,
          ])
          if self.type in ('oper', 'super'):
              out += ", isherm=" + str(self.isherm)
          if self.issuper and self.superrep != 'super':
              out += ", superrep=" + repr(self.superrep)
          return out
  
      def basis_expansion(self, decimal_places: int = 14,
                          term_separator: str = " + ",
                          dim_separator: str = "auto",
                          sort: str = "largest-first") -> str:
          """
          Return a string-representation of the basis expansion
          for a ket or a bra, e.g.
  
          ``"(0.5+0.15j) |010> + (0.25+0j) |000> + ..."``
  
          Parameters
          ----------
          decimal_places : int
              The number of decimal places to show for the coefficients.
  
          term_separator: str
              Separator string between terms. E.g. use " +\n"
              to print terms on separate lines.
  
          dim_separator: str
              Separator string between dimension indices.
              E.g. use ", " to print as ``"|0, 0, 0>"``.
              If dim_separator="auto", then "" is used
              for qubits and ", " is used otherwise.
  
          sort: str
              Show largest elements (by magnitude) first
              if "largest-first". Unsorted otherwise.
  
          Returns
          -------
          basis_expansion : str
              The requested string representation.
          """
  
          if not (self.isket or self.isbra):
              raise ValueError(
                  f"Only implemented for states, not {self.type}."
              )
          dims = self._dims[0 if self.isket else 1]
  
          if dim_separator == "auto":
              # No separator for pure qubit states but comma-separator otherwise,
              # since bitstrings are nice, but e.g. |153> would be ambiguous.
              dim_separator = (
                  ", " if any([dim > 2 for dim in dims.flat()]) else ""
              )
  
          template = "{} |{}>" if self.isket else "{} <{}|"
  
          ket_full = np.round(self.full(squeeze=True), decimals=decimal_places)
  
          indices = range(len(ket_full))
          if sort == "largest-first":
              indices = np.argsort(np.abs(ket_full))[::-1]
  
          parts = []
          for i in indices:
              if np.abs(ket_full[i]) > 10**-(decimal_places):
                  coeff = ket_full.item(i)
                  basis_str = dim_separator.join(
                      map(str, dims.idx2dims(i))
                  )
                  parts.append(template.format(coeff, basis_str))
  
          if len(parts) == 0:  # return something for norm-zero states.
              return "0"
  
          return term_separator.join(parts)
  
      def __str__(self):
          if self.data.shape[0] * self.data.shape[0] > 100_000_000:
              # If the system is huge, don't attempt to convert to a dense matrix
              # and then to string, because it is pointless and is likely going
              # to produce memory errors. Instead print the sparse data string
              # representation.
              data = _data.to(_data.CSR, self.data).as_scipy()
          elif _data.iszero(_data.sub(self.data.conj(), self.data)):
              data = np.real(self.full())
          else:
              data = self.full()
          return self._str_header() + "\nQobj data =\n" + str(data)
  
      def __repr__(self):
          # give complete information on Qobj without print statement in
          # command-line we cant realistically serialize a Qobj into a string,
          # so we simply return the informal __str__ representation instead.)
          return self.__str__()
  
      def __call__(self, other: Qobj) -> Qobj:
          """
          Acts this Qobj on another Qobj either by left-multiplication,
          or by vectorization and devectorization, as
          appropriate.
          """
          if not isinstance(other, Qobj):
              raise TypeError("Only defined for quantum objects.")
          if (self.type, other.type) not in _CALL_ALLOWED:
              raise TypeError(self.type + " cannot act on " + other.type)
          if self.issuper:
              if other.isket:
                  other = other.proj()
              return qutip.vector_to_operator(self @ qutip.operator_to_vector(other))
          return self.__matmul__(other)
  
      def __getstate__(self):
          # defines what happens when Qobj object gets pickled
          self.__dict__.update({'qutip_version': __version__[:5]})
          return self.__dict__
  
      def __setstate__(self, state):
          # defines what happens when loading a pickled Qobj
          if 'qutip_version' in state.keys():
              del state['qutip_version']
          (self.__dict__).update(state)
  
      def _repr_latex_(self):
          """
          Generate a LaTeX representation of the Qobj instance. Can be used for
          formatted output in ipython notebook.
          """
          half_length = 5
          n_rows, n_cols = self.data.shape
          # Choose which rows and columns we're going to output, or None if that
          # element should be truncated.
          rows = list(range(min((half_length, n_rows))))
          if n_rows <= half_length * 2:
              rows += list(range(half_length, min((2*half_length, n_rows))))
          else:
              rows.append(None)
              rows += list(range(n_rows - half_length, n_rows))
          cols = list(range(min((half_length, n_cols))))
          if n_cols <= half_length * 2:
              cols += list(range(half_length, min((2*half_length, n_cols))))
          else:
              cols.append(None)
              cols += list(range(n_cols - half_length, n_cols))
          # Make the data array.
          data = r'$$\left(\begin{array}{cc}'
          data += r"\\".join(_latex_row(row, cols, self.data.to_array())
                             for row in rows)
          data += r'\end{array}\right)$$'
          return self._str_header() + data
  
      def __and__(self, other: Qobj) -> Qobj:
          """
          Syntax shortcut for tensor:
          A & B ==> tensor(A, B)
          """
          return qutip.tensor(self, other)
  
      def dag(self) -> Qobj:
          """Get the Hermitian adjoint of the quantum object."""
          if self._isherm:
              return self.copy()
          return Qobj(_data.adjoint(self._data),
                      dims=Dimensions(self._dims[0], self._dims[1]),
                      isherm=self._isherm,
                      isunitary=self._isunitary,
                      copy=False)
  
      def conj(self) -> Qobj:
          """Get the element-wise conjugation of the quantum object."""
          return Qobj(_data.conj(self._data),
                      dims=self._dims,
                      isherm=self._isherm,
                      isunitary=self._isunitary,
                      copy=False)
  
      def trans(self) -> Qobj:
          """Get the matrix transpose of the quantum operator.
  
          Returns
          -------
          oper : :class:`.Qobj`
              Transpose of input operator.
          """
          return Qobj(_data.transpose(self._data),
                      dims=Dimensions(self._dims[0], self._dims[1]),
                      isherm=self._isherm,
                      isunitary=self._isunitary,
                      copy=False)
  
      def dual_chan(self) -> Qobj:
          """Dual channel of quantum object representing a completely positive
          map.
          """
          # Uses the technique of Johnston and Kribs (arXiv:1102.0948), which
          # is only valid for completely positive maps.
          self._dims._require_pure_dims("dual channel")
          if not self.iscp:
              raise ValueError("Dual channels are only implemented for CP maps.")
          J = qutip.to_choi(self)
          tensor_idxs = enumerate_flat(J.dims)
          J_dual = qutip.tensor_swap(J, *(
                  list(zip(tensor_idxs[0][1], tensor_idxs[0][0])) +
                  list(zip(tensor_idxs[1][1], tensor_idxs[1][0]))
          )).trans()
          J_dual.superrep = 'choi'
          return J_dual
  
      def norm(
          self,
          norm: Literal["l2", "max", "fro", "tr", "one"] = None,
          kwargs: dict[str, Any] = None
      ) -> float:
          """
          Norm of a quantum object.
  
          Default norm is L2-norm for kets and trace-norm for operators.  Other
          ket and operator norms may be specified using the `norm` parameter.
  
          Parameters
          ----------
          norm : str
              Which type of norm to use.  Allowed values for vectors are 'l2' and
              'max'.  Allowed values for matrices are 'tr' for the trace norm,
              'fro' for the Frobenius norm, 'one' and 'max'.
  
          kwargs : dict
              Additional keyword arguments to pass on to the relevant norm
              solver.  See details for each norm function in :mod:`.data.norm`.
  
          Returns
          -------
          norm : float
              The requested norm of the operator or state quantum object.
          """
          if self.type in ('oper', 'super'):
              norm = norm or 'tr'
              if norm not in _NORM_ALLOWED_MATRIX:
                  raise ValueError(
                      "matrix norm must be in " + repr(_NORM_ALLOWED_MATRIX)
                  )
          else:
              norm = norm or 'l2'
              if norm not in _NORM_ALLOWED_VECTOR:
                  raise ValueError(
                      "vector norm must be in " + repr(_NORM_ALLOWED_VECTOR)
                  )
          kwargs = kwargs or {}
          return _NORM_FUNCTION_LOOKUP[norm](self._data, **kwargs)
  
      def proj(self) -> Qobj:
          """Form the projector from a given ket or bra vector.
  
          Parameters
          ----------
          Q : :class:`.Qobj`
              Input bra or ket vector
  
          Returns
          -------
          P : :class:`.Qobj`
              Projection operator.
          """
          if not (self.isket or self.isbra):
              raise TypeError("projection is only defined for bras and kets")
          dims = ([self._dims[0], self._dims[0]] if self.isket
                  else [self._dims[1], self._dims[1]])
          return Qobj(_data.project(self._data),
                      dims=dims,
                      isherm=True,
                      copy=False)
  
      def tr(self) -> complex:
          """Trace of a quantum object.
  
          Returns
          -------
          trace : float
              Returns the trace of the quantum object.
  
          """
          out = _data.trace(self._data)
          # This ensures that trace can return something that is not a number such
          # as a `tensorflow.Tensor` in qutip-tensorflow.
          if settings.core["auto_real_casting"] and self.isherm:
              out = out.real
          return out
  
      def purity(self) -> complex:
          """Calculate purity of a quantum object.
  
          Returns
          -------
          state_purity : float
              Returns the purity of a quantum object.
              For a pure state, the purity is 1.
              For a mixed state of dimension `d`, 1/d<=purity<1.
  
          """
          if self.type in ("super", "operator-ket", "operator-bra"):
              raise TypeError('purity is only defined for states.')
          if self.isket or self.isbra:
              return _data.norm.l2(self._data)**2
          return _data.trace(_data.matmul(self._data, self._data)).real
  
      def full(
          self,
          order: Literal['C', 'F'] = 'C',
          squeeze: bool = False
      ) -> np.ndarray:
          """Dense array from quantum object.
  
          Parameters
          ----------
          order : str {'C', 'F'}
              Return array in C (default) or Fortran ordering.
          squeeze : bool {False, True}
              Squeeze output array.
  
          Returns
          -------
          data : array
              Array of complex data from quantum objects `data` attribute.
          """
          out = np.asarray(self.data.to_array(), order=order)
          return out.squeeze() if squeeze else out
  
      def data_as(self, format: str = None, copy: bool = True) -> Any:
          """Matrix from quantum object.
  
          Parameters
          ----------
          format : str, default: None
              Type of the output, "ndarray" for ``Dense``, "csr_matrix" for
              ``CSR``. A ValueError will be raised if the format is not
              supported.
  
          copy : bool {False, True}
              Whether to return a copy
  
          Returns
          -------
          data : numpy.ndarray, scipy.sparse.matrix_csr, etc.
              Matrix in the type of the underlying libraries.
          """
          return _data.extract(self._data, format, copy)
  
      def diag(self) -> np.ndarray:
          """Diagonal elements of quantum object.
  
          Returns
          -------
          diags : array
              Returns array of ``real`` values if operators is Hermitian,
              otherwise ``complex`` values are returned.
          """
          # TODO: add a `diagonal` method to the data layer?
          out = _data.to(_data.CSR, self.data).as_scipy().diagonal()
          if settings.core["auto_real_casting"] and self.isherm:
              out = np.real(out)
          return out
  
      def expm(self, dtype: LayerType = None) -> Qobj:
          """Matrix exponential of quantum operator.
  
          Input operator must be square.
  
          Parameters
          ----------
          dtype : type
              The data-layer type that should be output.
  
          Returns
          -------
          oper : :class:`.Qobj`
              Exponentiated quantum operator.
  
          Raises
          ------
          TypeError
              Quantum operator is not square.
          """
          if not self._dims.issquare:
              raise TypeError("expm is only valid for square operators")
          if dtype is None and isinstance(self.data, (_data.CSR, _data.Dia)):
              dtype = _data.Dense
          return Qobj(_data.expm(self._data, dtype=dtype),
                      dims=self._dims,
                      isherm=self._isherm,
                      copy=False,
                      dtype=dtype)
  
      def logm(self) -> Qobj:
          """Matrix logarithm of quantum operator.
  
          Input operator must be square.
  
          Returns
          -------
          oper : :class:`.Qobj`
              Logarithm of the quantum operator.
  
          Raises
          ------
          TypeError
  