Source code for qibo.quantum_info.quantum_networks

"""Module defining the `QuantumNetwork` class and adjacent functions."""

from functools import reduce
from logging import warning
from operator import mul
from typing import List, Optional, Tuple, Union

import numpy as np

from qibo.backends import _check_backend
from qibo.config import raise_error


[docs]class QuantumNetwork: """This class stores the representation of the quantum network as a tensor. This is a unique representation of the quantum network. A minimum quantum network is a quantum channel, which is a quantum network of the form :math:`J[n \\to m]`, where :math:`n` is the dimension of the input system , and :math:`m` is the dimension of the output system. A quantum state is a quantum network of the form :math:`J: 1 \\to n`, such that the input system is trivial. An observable is a quantum network of the form :math:`J: n \\to 1`, such that the output system is trivial. A quantum network may contain multiple input and output systems. For example, a "quantum comb" is a quantum network of the form :math:`J: n', n \\to m, m'`, which convert a quantum channel of the form :math:`J: n \\to m` to a quantum channel of the form :math:`J: n' \\to m'`. Args: tensor (ndarray): input Choi operator. partition (List[int] or Tuple[int]): partition of ``tensor``. system_input (List[bool] or Tuple[bool], optional): mask on the output system of the Choi operator. If ``None``, defaults to ``(True,False,True,False,...)``, where ``len(system_input)=len(partition)``. Defaults to ``None``. pure (bool, optional): ``True`` when ``tensor`` is a "pure" representation (e.g. a pure state, a unitary operator, etc.), ``False`` otherwise. Defaults to ``False``. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in calculations. If ``None``, defaults to the current backend. Defaults to ``None``. """ def __init__( self, tensor, partition: Optional[Union[List[int], Tuple[int]]] = None, system_input: Optional[Union[List[bool], Tuple[bool]]] = None, pure: bool = False, backend=None, ): self._tensor = tensor self.partition = tuple(partition) self.system_input = system_input self._pure = pure self._backend = backend self._run_checks(self.partition, self.system_input, self._pure) self._set_parameters() self.dims = reduce(mul, self.partition) if len(self.partition) > 0 else 1 @staticmethod def _order_tensor_to_operator(dims: int): """Returns the order to reshape a tensor into an operator. Given a tenosr of ``2 * dims`` leads, the order is :math:`[0, 2, 4, ..., 1, 3, 5, ...]`. Args: dims (int): dimension. Returns: list: order to reshape tensor into an operator. """ return list(range(0, 2 * dims, 2)) + list(range(1, 2 * dims, 2)) @staticmethod def _order_operator_to_tensor(nsystems: int): """Returns the order to reshape an operator to a tensor. Given a operator of :math:`2n` systems, the order is :math:`[0, n, 1, n+1, 2, n+2, ...]`. Args: nsystems (int): number of systems. Returns: list: order to reshape operator into tensor. """ return list( sum(zip(list(range(0, nsystems)), list(range(nsystems, nsystems * 2))), ()) ) @classmethod def _operator_to_tensor(cls, operator, partition: List[int]): n = len(partition) order = cls._order_operator_to_tensor(n) # Check if the `partition` matches the shape of the input matrix if np.prod(tuple(operator.shape)) != np.prod( tuple(dim**2 for dim in partition) ): raise_error( ValueError, "``partition`` does not match the shape of the input matrix. " + f"Cannot reshape matrix of size {operator.shape} to partition {partition}", ) # Check if `operator` is a pytourch tensor tensor = operator.reshape(list(partition) * 2) if operator.__class__.__name__ == "Tensor": tensor = tensor.permute(order) else: tensor = tensor.transpose(order) return tensor.reshape([dim**2 for dim in partition])
[docs] @classmethod def from_operator( cls, operator, partition: Optional[Union[List[int], Tuple[int]]] = None, system_input: Optional[Union[List[bool], Tuple[bool]]] = None, pure: bool = False, backend=None, ): """Construct a :class:`qibo.quantum_info.QuantumNetwork` object from a ndarray. This method converts a Choi operator to the internal representation of :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`. The input array can be a pure state, a Choi operator, a unitary operator, etc. Args: arr (ndarray): input numpy array. partition (List[int] or Tuple[int], optional): partition of ``arr``. If ``None``, defaults to the shape of ``arr``. Defaults to ``None``. system_input (List[bool] or Tuple[bool], optional): mask on the input system of the Choi operator. If ``None``, defaults to ``(True,False,True,False...)``, where ``len(system_input)=len(partition)``. Defaults to ``None``. pure (bool, optional): ``True`` when ``arr`` is a "pure" representation (e.g. a pure state, a unitary operator, etc.), ``False`` otherwise. Defaults to ``False``. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in calculations. If ``None``, defaults to the current backend. Defaults to ``None``. Returns: :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: quantum network constructed from the input Choi operator. """ if pure: if partition is None: partition = tuple(operator.shape) tensor = operator else: if tuple(partition) not in [ tuple(operator.shape), tuple(int(np.sqrt(dim)) for dim in operator.shape) * 2, ]: raise_error( ValueError, "``partition`` does not match the shape of the input matrix. " + f"Cannot reshape matrix of size {operator.shape} " + f"to partition {partition}", ) tensor = operator.reshape(partition) else: # check if arr is a valid choi operator len_sys = len(operator.shape) if (len_sys % 2 != 0) or ( operator.shape[: len_sys // 2] != operator.shape[len_sys // 2 :] ): raise_error( ValueError, "The opertor must be a square operator where the first half of the shape " + "is the same as the second half of the shape. " + f"However, the shape of the input is {operator.shape}. " + "If the input is pure, set `pure=True`.", ) if partition is None: partition = operator.shape[: len_sys // 2] tensor = cls._operator_to_tensor(operator, partition) return cls( tensor, partition=partition, system_input=system_input, pure=pure, backend=backend, )
[docs] def operator(self, full: bool = False, backend=None): """Returns the Choi operator of the quantum network. The shape of the returned operator is :math:`(*self.partition, *self.partition)`. Args: full (bool, optional): If this is ``False``, and the network is pure, the method will only return the eigenvector (unique when the network is pure). If ``True``, returns the full tensor of the quantum network. Defaults to ``False``. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used to return the Choi operator. If ``None``, defaults to the backend defined when initializing the :class:`qibo.quantum_info.quantum_networks.QuantumNetwork` object. Defaults to ``None``. Returns: ndarray: Choi operator of the quantum network. """ if backend is None: # pragma: no cover backend = self._backend if self.is_pure() and not full: return backend.cast(self._tensor, dtype=self._tensor.dtype) tensor = self.full(backend) if self.is_pure() else self._tensor n = len(self.partition) order = self._order_tensor_to_operator(n) operator = self._backend.np.transpose( tensor.reshape(tuple(np.repeat(self.partition, 2))), order ) return backend.cast(operator, dtype=self._tensor.dtype)
[docs] def matrix(self, backend=None): """Returns the Choi operator of the quantum network in the matrix form. The shape of the returned operator is :math:`(self.dims, self.dims)`. Args: backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used to return the Choi operator. If ``None``, defaults to the backend defined when initializing the :class:`qibo.quantum_info.quantum_networks.QuantumNetwork` object. Defaults to ``None``. Returns: ndarray: Choi operator of the quantum network. """ return self.operator(full=True, backend=backend).reshape((self.dims, self.dims))
[docs] def is_pure(self): """Returns bool indicading if the Choi operator of the network is pure.""" return self._pure
[docs] def is_hermitian( self, order: Optional[Union[int, str]] = None, precision_tol: float = 1e-8 ): """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` is Hermitian. Hermicity is calculated as distance between :math:`\\mathcal{J}` and :math:`\\mathcal{J}^{\\dagger}` with respect to a given norm. Default is the ``Hilbert-Schmidt`` norm (also known as ``Frobenius`` norm). For specifications on the other possible values of the parameter ``order`` for the ``tensorflow`` backend, please refer to `tensorflow.norm <https://www.tensorflow.org/api_docs/python/tf/norm>`_. For all other backends, please refer to `numpy.linalg.norm <https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html>`_. Args: order (str or int, optional): order of the norm. Defaults to ``None``. precision_tol (float, optional): threshold :math:`\\epsilon` that defines if Choi operator of the network is :math:`\\epsilon`-close to Hermicity in the norm given by ``order``. Defaults to :math:`10^{-8}`. Returns: bool: Hermiticity condition. If the adjoint of the Choi operator is equal to the Choi operator, the method returns ``True``. If the input is pure, the its always Hermitian. """ if precision_tol < 0.0: raise_error( ValueError, f"``precision_tol`` must be non-negative float, but it is {precision_tol}", ) if order is None and self._backend.__class__.__name__ == "TensorflowBackend": order = "euclidean" if self.is_pure(): # if the input is pure, it is always hermitian return True reshaped = self._backend.cast( self.matrix(), dtype=self._tensor.dtype, ) if self._backend.__class__.__name__ == "PyTorchBackend": adjoint = self._backend.np.transpose(reshaped, (1, 0)) else: adjoint = self._backend.np.transpose(reshaped) mat_diff = self._backend.np.conj(adjoint) - reshaped norm = self._backend.calculate_matrix_norm(mat_diff, order=order) return float(norm) <= precision_tol
[docs] def is_positive_semidefinite(self, precision_tol: float = 1e-8): """Returns bool indicating if Choi operator :math:`\\mathcal{J}` is positive-semidefinite. Args: precision_tol (float, optional): threshold value used to check if eigenvalues of the Choi operator :math:`\\mathcal{J}` are such that :math:`\\textup{eigenvalues}(\\mathcal{J}) >= - \\textup{precision_tol}`. Note that this parameter can be set to negative values. Defaults to :math:`0.0`. Returns: bool: Positive-semidefinite condition. """ if precision_tol < 0.0: raise_error( ValueError, f"``precision_tol`` must be non-negative float, but it is {precision_tol}", ) if self.is_pure(): # if the input is pure, it is always positive semidefinite return True reshaped = self._backend.cast( self.matrix(), dtype=self._tensor.dtype, ) if self.is_hermitian(): eigenvalues = self._backend.calculate_eigenvalues(reshaped) else: return False return all( self._backend.np.real(eigenvalue) >= -precision_tol for eigenvalue in eigenvalues )
[docs] def copy(self): """Returns a copy of the :class:`qibo.quantum_info.QuantumNetwork` object.""" return self.__class__( self._backend.np.copy(self._tensor), partition=self.partition, system_input=self.system_input, pure=self._pure, backend=self._backend, )
[docs] def conj(self): """Returns the conjugate of the quantum network.""" return self.__class__( self._backend.np.conj(self._tensor), partition=self.partition, system_input=self.system_input, pure=self._pure, backend=self._backend, )
def __add__(self, second_network): """Add two Quantum Networks by adding their Choi operators. This operation always returns a non-pure Quantum Network. Args: second_network (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum network to be added to the original network. Returns: (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum network resulting from the summation of two Choi operators. """ if not isinstance(second_network, QuantumNetwork): raise_error( TypeError, "It is not possible to add a object of type ``QuantumNetwork`` " + f"and and object of type ``{type(second_network)}``.", ) if self.full().shape != second_network.full().shape: raise_error( ValueError, f"The Choi operators must have the same shape, but {self.full().shape} != " + f"{second_network.full().shape}.", ) if self.system_input != second_network.system_input: raise_error(ValueError, "The networks must have the same input systems.") new_first_tensor = self.full() new_second_tensor = second_network.full() return QuantumNetwork( new_first_tensor + new_second_tensor, self.partition, self.system_input, pure=False, backend=self._backend, ) def __mul__(self, number: Union[float, int]): """Returns quantum network with its Choi operator multiplied by a scalar. If the quantum network is pure and ``number > 0.0``, the method returns a pure quantum network with its Choi operator multiplied by the square root of ``number``. This is equivalent to multiplying `self.to_full()` by the ``number``. Otherwise, this method will return a full quantum network. Args: number (float or int): scalar to multiply the Choi operator of the network with. Returns: :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network with its Choi operator multiplied by ``number``. """ if not isinstance(number, (float, int)): raise_error( TypeError, "It is not possible to multiply a ``QuantumNetwork`` by a non-scalar.", ) if self.is_pure() and number > 0.0: return QuantumNetwork( np.sqrt(number) * self._tensor, partition=self.partition, system_input=self.system_input, pure=True, backend=self._backend, ) tensor = self.full() return QuantumNetwork( number * tensor, partition=self.partition, system_input=self.system_input, pure=False, backend=self._backend, ) def __rmul__(self, number: Union[float, int]): """""" return self.__mul__(number) def __truediv__(self, number: Union[float, int]): """Returns quantum network with its Choi operator divided by a scalar. If the quantum network is pure and ``number > 0.0``, the method returns a pure quantum network with its Choi operator divided by the square root of ``number``. This is equivalent to dividing `self.to_full()` by the ``number``. Otherwise, this method will return a full quantum network. Args: number (float or int): scalar to divide the Choi operator of the network with. Returns: :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network with its Choi operator divided by ``number``. """ if not isinstance(number, (float, int)): raise_error( TypeError, "It is not possible to divide a ``QuantumNetwork`` by a non-scalar.", ) number = np.sqrt(number) if self.is_pure() and number > 0.0 else number return QuantumNetwork( self._tensor / number, partition=self.partition, system_input=self.system_input, pure=self.is_pure(), backend=self._backend, ) def __matmul__(self, second_network): """Defines matrix multiplication between two ``QuantumNetwork`` objects. If ``self.partition == second_network.partition in [2, 4]``, this method is overwritten by :meth:`qibo.quantum_info.quantum_networks.QuantumNetwork.link_product`. Args: second_network (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Returns: :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network resulting from the link product operation. """ if not isinstance(second_network, QuantumNetwork): raise_error( TypeError, "It is not possible to implement matrix multiplication of a " + "``QuantumNetwork`` by a non-``QuantumNetwork``.", ) if len(self.partition) == 2: # `self` is a channel if len(second_network.partition) != 2: raise_error( ValueError, f"`QuantumNetwork {second_network} is assumed to be a channel, but it is not. " + "Use `link_product` method to specify the subscript.", ) if self.partition[1] != second_network.partition[0]: raise_error( ValueError, "partitions of the networks do not match: " + f"{self.partition[1]} != {second_network.partition[0]}.", ) subscripts = "jk,kl -> jl" elif len(self.partition) == 4: # `self` is a super-channel if len(second_network.partition) != 2: raise_error( ValueError, f"`QuantumNetwork {second_network} is assumed to be a channel, but it is not. " + "Use `link_product` method to specify the subscript.", ) if self.partition[1] != second_network.partition[0]: raise_error( ValueError, "Systems of the channel do not match the super-channel: " + f"{self.partition[1], self.partition[2]} != " + f"{second_network.partition[0],second_network.partition[1]}.", ) subscripts = "jklm,kl -> jm" else: raise_error( NotImplementedError, "`partitions` do not match any implemented pattern``. " + "Use `link_product` method to specify the subscript.", ) return self.link_product(subscripts, second_network) # pylint: disable=E0606 def __str__(self): """Method to define how to print relevant information of the quantum network.""" systems = [] for i, dim in enumerate(self.partition): if self.system_input[i]: systems.append(f"┍{dim}┑") else: systems.append(f"┕{dim}┙") return f"J[{', '.join(systems)}]" def _run_checks(self, partition, system_input, pure): """Checks if all inputs are correct in type and value.""" if not isinstance(partition, (list, tuple)): raise_error( TypeError, "``partition`` must be type ``tuple`` or ``list``, " + f"but it is type ``{type(partition)}``.", ) if any(not isinstance(party, int) for party in partition): raise_error( ValueError, "``partition`` must be a ``tuple`` or ``list`` of positive integers, " + "but contains non-integers.", ) if any(party <= 0 for party in partition): raise_error( ValueError, "``partition`` must be a ``tuple`` or ``list`` of positive integers, " + "but contains non-positive integers.", ) if system_input is not None and len(system_input) != len(partition): raise_error( ValueError, "``len(system_input)`` must be the same as ``len(partition)``, " + f"but {len(system_input)} != {len(partition)}.", ) if not isinstance(pure, bool): raise_error( TypeError, f"``pure`` must be type ``bool``, but it is type ``{type(pure)}``.", ) @staticmethod def _check_system_input(system_input, partition) -> Tuple[bool]: """ If `system_input` not defined, assume the network follows the order of a quantum Comb. """ if system_input is None: system_input = [ False, ] * len(partition) for k in range(len(partition) // 2): system_input[k * 2] = True return tuple(system_input) def _set_parameters(self): """Standarize the parameters.""" self._backend = _check_backend(self._backend) self.partition = tuple(self.partition) self.system_input = self._check_system_input(self.system_input, self.partition) self._einsum = self._backend.np.einsum self._tensordot = self._backend.np.tensordot self._tensor = self._backend.cast(self._tensor, dtype=self._tensor.dtype) if self._pure: if np.prod(tuple(self._tensor.shape)) != np.prod(tuple(self.partition)): raise_error( ValueError, "``partition`` does not match the shape of the input matrix. " + f"Cannot reshape matrix of size {self._tensor.shape} " + f"to partition {self.partition}.", ) self._tensor = self._backend.np.reshape(self._tensor, self.partition) else: if np.prod(tuple(self._tensor.shape)) != np.prod( tuple(dim**2 for dim in self.partition) ): raise_error( ValueError, "``partition`` does not match the shape of the input matrix. " + f"Cannot reshape matrix of size {self._tensor.shape} " + f"to partition {self.partition}.", ) matrix_partition = [dim**2 for dim in self.partition] self._tensor = self._backend.np.reshape(self._tensor, matrix_partition)
[docs] def full(self, update: bool = False, backend=None): """Convert the internal representation to the full tensor of the network. Args: update (bool, optional): If ``True``, updates the internal representation of the network to the full tensor. Defaults to ``False``. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in calculations. If ``None``, defaults to the current backend. Defaults to ``None``. Returns: ndarray: full reprentation of the quantum network. """ if backend is None: # pragma: no cover backend = self._backend tensor = self._backend.np.copy(self._tensor) tensor = backend.cast(tensor, dtype=self._tensor.dtype) conj = backend.np.conj if self.is_pure(): # Reshapes input matrix based on purity. tensor.reshape(self.dims) if self._backend.__class__.__name__ == "PyTorchBackend": tensor = self._tensordot(tensor, conj(tensor), dims=0) else: tensor = self._tensordot(tensor, conj(tensor), axes=0) tensor = self._operator_to_tensor(tensor, self.partition) if update: self._tensor = tensor self._pure = False return tensor
[docs]class QuantumComb(QuantumNetwork): """Stores a Quantum comb, which is a network in which the systems follows a sequential order. It is also called the *non-Markovian quantum process* in many literatures. A quantum comb is a quantum network of the form :math:`J[┍i1┑,┕o1┙,┍i2┑,┕o2┙, ...]`, where the process first take an input state from system :math:`i1`, then output a state to system :math:`o1`, and so on. This is a non-Markovian process as the output of the system :math:`o2` may depend on what happened in systems :math:`i1`, and :math:`o1`. A quantum channel is a special case of quantum comb, where there are only one input system and one output system. Args: tensor (ndarray): the tensor representations of the quantum Comb. partition (List[int] or Tuple[int]): partition of ``matrix``. system_input (List[bool] or Tuple[bool], optional): mask on the input system of the Choi operator. If ``None``, defaults to ``(True,False,True,False,...)``, where ``len(system_input)=len(partition)``. Defaults to ``None``. pure (bool, optional): ``True`` when ``tensor`` is a "pure" representation (e.g. a pure state, a unitary operator, etc.), ``False`` otherwise. Defaults to ``False``. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in calculations. If ``None``, defaults to the current backend. Defaults to ``None``. """ def __init__( self, tensor, partition: Optional[Union[List[int], Tuple[int]]] = None, system_input: Optional[Union[List[bool], Tuple[bool]]] = None, pure: bool = False, backend=None, ): if partition is None: if pure: partition = tensor.shape else: partition = tuple(int(np.sqrt(d)) for d in tensor.shape) if len(partition) % 2 != 0: raise_error( ValueError, "A quantum comb should only contain equal number of input and output systems. " + "For general quantum networks, one should use the ``QuantumNetwork`` class.", ) if system_input is not None: warning("system_input is ignored for QuantumComb") super().__init__( tensor, partition, [True, False] * (len(partition) // 2), pure, backend )
[docs] def is_causal( self, order: Optional[Union[int, str]] = None, precision_tol: float = 1e-8 ): """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` satisfies causal order Causality is calculated based on a recursive constrains. This method reduce a n-comb to a (n-1)-comb at each step, and checks if the reduced comb is independent on the last output system. Args: order (str or int, optional): order of the norm. Defaults to ``None``. precision_tol (float, optional): threshold :math:`\\epsilon` that defines if Choi operator of the network is :math:`\\epsilon`-close to causality in the norm given by ``order``. Defaults to :math:`10^{-8}`. Returns: bool: Causal order condition. """ if precision_tol < 0.0: raise_error( ValueError, f"``precision_tol`` must be non-negative float, but it is {precision_tol}", ) if order is None and self._backend.__class__.__name__ == "TensorflowBackend": order = "euclidean" backend = self._backend dim_out = self.partition[-1] dim_in = self.partition[-2] trace_out = TraceOperation(dim_out, backend=backend).full() trace_in = TraceOperation(dim_in, backend=backend).full() if self._backend.__class__.__name__ == "PyTorchBackend": reduced = self._tensordot(self.full(), trace_out, dims=([-1], [0])) sub_comb = self._tensordot(reduced, trace_in, dims=([-1], [0])) expected = self._tensordot(sub_comb, trace_in / dim_in, dims=0) else: reduced = self._tensordot(self.full(), trace_out, axes=(-1, 0)) sub_comb = self._tensordot(reduced, trace_in, axes=(-1, 0)) expected = self._tensordot(sub_comb, trace_in / dim_in, axes=0) norm = self._backend.calculate_vector_norm(reduced - expected, order=order) if float(norm) > precision_tol: return False if len(self.partition) == 2: return True return QuantumComb( sub_comb, self.partition[:-2], pure=False, backend=self._backend ).is_causal(order, precision_tol)
[docs] @classmethod def from_operator( cls, operator, partition=None, inverse=False, pure=False, backend=None ): # pylint: disable=W0237 comb = super().from_operator(operator, partition, None, pure, backend) if inverse: # Convert mathmetical convention of Choi operator to physical convention comb.partition = comb.partition[::-1] comb._tensor = comb._tensor.T # pylint: disable=W0212 return comb
[docs]class QuantumChannel(QuantumComb): """Stores a Quantum channel, which is a special case of quantum comb. A quantum channel is a quantum comb with only one input and one output. This class includes all quantum channels, unitary operators, and quantum states. To construct a `QuantumChannel` object, one can use the `QuantumNetwork.from_nparray` method. **Note**: if one try to construct a quantum network from a unitary operator or Choi operator, the first system will be the output. However, here we assume the first system is the input system. It is important to specify `inverse=True` when constructing by `QuantumNetwork.from_nparray`. Args: tensor (ndarray): the tensor representations of the quantum comb. partition (List[int] or Tuple[int], optional): partition of ``matrix``. If not provided and `system_input` is `None`, assume the input is a quantum state, whose input is a trivial system. If `system_input` is set to `True`, assume the input is an observable, whose output is a trivial system. system_input (List[bool] or Tuple[bool], optional): mask on the input system of the Choi operator. If ``None`` the default is ``(True,False)``. Defaults to ``None``. pure (bool, optional): ``True`` when ``tensor`` is a "pure" representation (e.g. a pure state, a unitary operator, etc.), ``False`` otherwise. Defaults to ``False``. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in calculations. If ``None``, defaults to the current backend. Defaults to ``None``. """ def __init__( self, tensor, partition: Optional[Union[List[int], Tuple[int]]] = None, system_input: Optional[Union[List[bool], Tuple[bool]]] = None, pure: bool = False, backend=None, ): if isinstance(partition, int): partition = (partition,) if partition is not None: if len(partition) > 2: raise_error( ValueError, "A quantum channel should only contain one input system and one output system." + "For general quantum networks, one should use the ``QuantumNetwork`` class.", ) if len(partition) == 1: if system_input is None: # Assume the input is a quantum state partition = (1, partition[0]) else: if isinstance(system_input, bool): system_input = (system_input,) partition = ( (partition[0], 1) if system_input[0] else (1, partition[0]) ) super().__init__(tensor, partition, pure=pure, backend=backend)
[docs] def is_unital( self, order: Optional[Union[int, str]] = None, precision_tol: float = 1e-8 ): """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` is unital. A map is unital if it preserves the identity operator. Unitality is calculated as distance between the partial trace of :math:`\\mathcal{J}` and the Identity operator :math:`I`, with respect to a given norm. Default is the ``Hilbert-Schmidt`` norm (also known as ``Frobenius`` norm). For specifications on the other possible values of the parameter ``order`` for the ``tensorflow`` backend, please refer to `tensorflow.norm <https://www.tensorflow.org/api_docs/python/tf/norm>`_. For all other backends, please refer to `numpy.linalg.norm <https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html>`_. Args: order (str or int, optional): order of the norm. Defaults to ``None``. precision_tol (float, optional): threshold :math:`\\epsilon` that defines if Choi operator of the network is :math:`\\epsilon`-close to unitality in the norm given by ``order``. Defaults to :math:`10^{-8}`. Returns: bool: Unitality condition. """ if precision_tol < 0.0: raise_error( ValueError, f"``precision_tol`` must be non-negative float, but it is {precision_tol}", ) if order is None and self._backend.__class__.__name__ == "TensorflowBackend": order = "euclidean" backend = self._backend dim_out = self.partition[1] dim_in = self.partition[0] trace_out = TraceOperation(dim_out, backend=backend).full() trace_in = TraceOperation(dim_in, backend=backend).full() if self._backend.__class__.__name__ == "PyTorchBackend": reduced = self._tensordot(self.full(), trace_in, dims=([0], [0])) sub_comb = self._tensordot( reduced, trace_out, dims=([0], [0]), ) expected = self._tensordot(trace_out / dim_out, sub_comb, dims=0) else: reduced = self._tensordot(self.full(), trace_in, axes=(0, 0)) sub_comb = self._tensordot(reduced, trace_out, axes=(0, 0)) expected = self._tensordot(trace_out / dim_out, sub_comb, axes=0) norm = self._backend.calculate_vector_norm((reduced - expected), order=order) if float(norm) > precision_tol: return False if len(self.partition) == 2: return True # Unital is defined for quantum channels only. # But we can extend it to quantum combs as follows: return QuantumChannel( # pragma: no cover sub_comb, self.partition[2:], pure=False, backend=self._backend ).is_unital(order, precision_tol)
[docs] def is_channel( self, order: Optional[Union[int, str]] = None, precision_tol_causal: float = 1e-8, precision_tol_psd: float = 1e-8, ): """Returns bool indicating if Choi operator :math:`\\mathcal{E}` is a channel. Args: order (int or str, optional): order of the norm used to calculate causality. Defaults to ``None``. precision_tol_causal (float, optional): threshold :math:`\\epsilon` that defines if Choi operator of the network is :math:`\\epsilon`-close to causality in the norm given by ``order``. Defaults to :math:`10^{-8}`. precision_tol_psd (float, optional): threshold value used to check if eigenvalues of the Choi operator :math:`\\mathcal{E}` are such that :math:`\\textup{eigenvalues}(\\mathcal{E}) >= \\textup{precision_tol_psd}`. Note that this parameter can be set to negative values. Defaults to :math:`0.0`. Returns: bool: Channel condition. """ return self.is_causal( order, precision_tol_causal ) and self.is_positive_semidefinite(precision_tol_psd)
[docs] def apply(self, state): """Apply the Choi operator :math:`\\mathcal{E}` to ``state`` :math:`\\varrho`. It is assumed that ``state`` :math:`\\varrho` is a density matrix. Args: state (ndarray): density matrix of a ``state``. Returns: ndarray: Resulting state :math:`\\mathcal{E}(\\varrho)`. """ operator = self.copy().operator() conj = self._backend.np.conj if self.is_pure(): return self._einsum("ij,lk,il", operator, conj(operator), state) return self._einsum("ijkl, jl", operator, state)
def link_product( subscripts: str, *operands: QuantumNetwork, backend=None, surpress_warning=False, ): """Link product between two quantum networks. The link product is not commutative. Here, we assume that :math:`A.\\textup{link_product}(B)` means "applying :math:`B` to :math:`A`". However, the ``link_product`` is associative, so we override the `@` operation in order to simplify notation. Args: subscripts (str, optional): Specifies the subscript for summation using the Einstein summation convention. For more details, please refer to `numpy.einsum <https://numpy.org/doc/stable/reference/generated/numpy.einsum.html>`_. operands (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum networks to be contracted. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in calculations. If ``None``, defaults to the current backend. Defaults to ``None``. surpress_warning (bool, optional): If ``True``, surpresses the warning regarding if the same index connects two input or two output systems. Defaults to ``False``. Returns: :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network resulting from the link product between two quantum networks. """ if not isinstance(subscripts, str): raise_error( TypeError, f"subscripts must be type str, but it is type {type(subscripts)}.", ) for i, operand in enumerate(operands): if not isinstance(operand, QuantumNetwork): raise_error(TypeError, f"The {i}-th operator is not a ``QuantumNetwork``.") if backend is None: # pragma: no cover backend = operands[0]._backend # pylint: disable=W0212 tensors = [ ( backend.to_numpy(operand.full()) if operand.is_pure() else backend.to_numpy(operand._tensor) # pylint: disable=W0212 ) for operand in operands ] # keep track of the `partition` and `system_input` of the network _, contracrtion_list = np.einsum_path( subscripts, *tensors, optimize=False, einsum_call=True ) inds, idx_rm, einsum_str, _, _ = contracrtion_list[0] input_str, results_index = einsum_str.split("->") inputs = input_str.split(",") # Warning if the same index connects two input or two output systems if not surpress_warning: for ind in idx_rm: found = 0 for i, script in enumerate(inputs): index = script.find(ind) if index < 0: continue found += 1 if found > 1 and is_input == operands[inds[i]].system_input[index]: warning( f"Index {ind} connects two {'input' if is_input else 'output'} systems." ) is_input = operands[inds[i]].system_input[index] if found > 2: warning( f"Index {ind} appears multiple times in the input subscripts {input_str}." ) # set correct order of the `partition` and `system_input` partition = [] system_input = [] for ind in results_index: for i, script in enumerate(inputs): index = script.find(ind) if index < 0: continue partition.append(operands[inds[i]].partition[index]) system_input.append(operands[inds[i]].system_input[index]) new_tensor = np.einsum(subscripts, *tensors) return QuantumNetwork(new_tensor, partition, system_input, backend=backend) class IdentityChannel(QuantumChannel): """The Identity channel with the given dimension. Args: dim (int): Dimension of the Identity operator. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in calculations. If ``None``, defaults to the current backend. Defaults to ``None``. """ def __init__(self, dim: int, backend=None): identity = np.eye(dim, dtype=complex) identity = backend.cast(identity, dtype=identity.dtype) super().__init__(identity, [dim, dim], pure=True, backend=backend) class TraceOperation(QuantumNetwork): """The Trace operator with the given dimension. Args: dim (int): Dimension of the Trace operator. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in calculations. If ``None``, defaults to the current backend. Defaults to ``None``. """ def __init__(self, dim: int, backend=None): identity = np.eye(dim, dtype=complex) identity = backend.cast(identity, dtype=identity.dtype) super().__init__(identity, [dim], [True], pure=False, backend=backend)