Source code for qibo.quantum_info.quantum_networks

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

import re
from functools import reduce
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 Choi operator of the quantum network as a tensor, which is an 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: matrix (ndarray): input Choi operator. partition (List[int] or Tuple[int]): partition of ``matrix``. system_output (List[bool] or Tuple[bool], optional): mask on the output system of the Choi operator. If ``None``, defaults to ``(False,True,False,True,...)``, where ``len(system_output)=len(partition)``. Defaults to ``None``. pure (bool, optional): ``True`` when ``matrix`` 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 :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. """ def __init__( self, matrix, partition: Union[List[int], Tuple[int]], system_output: Optional[Union[List[bool], Tuple[bool]]] = None, pure: bool = False, backend=None, ): self._run_checks(partition, system_output, pure) self._matrix = matrix self.partition = partition self.system_output = system_output self._pure = pure self._backend = backend self.dims = reduce(mul, self.partition) self._set_tensor_and_parameters()
[docs] def matrix(self, backend=None): """Returns the Choi operator of the quantum network in matrix form. 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 matrix of the quantum network. """ if backend is None: # pragma: no cover backend = self._backend return backend.cast(self._matrix, dtype=self._matrix.dtype)
[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{E}` of the network is Hermitian. Hermicity is calculated as distance between :math:`\\mathcal{E}` and :math:`\\mathcal{E}^{\\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 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" self._matrix = self._full() self._pure = False reshaped = self._backend.cast( np.reshape(self._matrix, (self.dims, self.dims)), dtype=self._matrix.dtype ) reshaped = self._backend.cast( np.transpose(np.conj(reshaped)) - reshaped, dtype=reshaped.dtype ) norm = self._backend.calculate_norm_density_matrix(reshaped, order=order) return float(norm) <= precision_tol
[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{E}` of the network is unital. Unitality is calculated as distance between the partial trace of :math:`\\mathcal{E}` 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" self._matrix = self._full() self._pure = False partial_trace = self._einsum("jkjl -> kl", self._matrix) identity = self._backend.cast( np.eye(partial_trace.shape[0]), dtype=partial_trace.dtype ) norm = self._backend.calculate_norm_density_matrix( partial_trace - identity, order=order, ) return float(norm) <= precision_tol
[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{E}` of the network satisfies the causal order condition. Causality is calculated as distance between partial trace of :math:`\\mathcal{E}` 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 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" self._matrix = self._full() self._pure = False partial_trace = self._einsum("jklk -> jl", self._matrix) identity = self._backend.cast( np.eye(partial_trace.shape[0]), dtype=partial_trace.dtype ) norm = self._backend.calculate_norm_density_matrix( partial_trace - identity, 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{E}` of the network is positive-semidefinite. Args: precision_tol (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}`. Note that this parameter can be set to negative values. Defaults to :math:`0.0`. Returns: bool: Positive-semidefinite condition. """ self._matrix = self._full() self._pure = False reshaped = np.reshape(self._matrix, (self.dims, self.dims)) if self.is_hermitian(): eigenvalues = np.linalg.eigvalsh(reshaped) else: if self._backend.__class__.__name__ in [ "CupyBackend", "CuQuantumBackend", ]: # pragma: no cover reshaped = np.array(reshaped.tolist(), dtype=reshaped.dtype) eigenvalues = np.linalg.eigvals(reshaped) return all(eigenvalue >= -precision_tol for eigenvalue in eigenvalues)
[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)`. """ matrix = self._backend.cast(self._matrix, copy=True) if self.is_pure(): return self._einsum("kj,ml,jl -> km", matrix, np.conj(matrix), state) return self._einsum("jklm,km -> jl", matrix, state)
[docs] def copy(self): """Returns a copy of the :class:`qibo.quantum_info.quantum_networks.QuantumNetwork` object.""" return self.__class__( np.copy(self._matrix), partition=self.partition, system_output=self.system_output, pure=self._pure, backend=self._backend, )
[docs] def to_full(self, backend=None): """Convert the internal representation to the full Choi operator of the network. Returns: (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): The full representation of the Quantum network. """ if backend is None: # pragma: no cover backend = self._backend if self.is_pure(): self._matrix = self._full() self._pure = False return self.matrix(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._matrix.shape} != " + f"{second_network.matrix(second_network._backend).shape}.", ) if self.system_output != second_network.system_output: raise_error(ValueError, "The networks must have the same output system.") new_first_matrix = self._full() new_second_matrix = second_network._full() return QuantumNetwork( new_first_matrix + new_second_matrix, self.partition, self.system_output, 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.matrix(backend=self._backend), partition=self.partition, system_output=self.system_output, pure=True, backend=self._backend, ) matrix = self._full() return QuantumNetwork( number * matrix, partition=self.partition, system_output=self.system_output, 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.matrix(backend=self._backend) / number, partition=self.partition, system_output=self.system_output, 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(second_network, subscripts=subscripts) def __str__(self): """Method to define how to print relevant information of the quantum network.""" string_in = ", ".join( [ str(self.partition[k]) for k in range(len(self.partition)) if not self.system_output[k] ] ) string_out = ", ".join( [ str(self.partition[k]) for k in range(len(self.partition)) if self.system_output[k] ] ) return f"J[{string_in} -> {string_out}]" def _run_checks(self, partition, system_output, 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_output is not None and len(system_output) != len(partition): raise_error( ValueError, "``len(system_output)`` must be the same as ``len(partition)``, " + f"but {len(system_output)} != {len(partition)}.", ) if not isinstance(pure, bool): raise_error( TypeError, f"``pure`` must be type ``bool``, but it is type ``{type(pure)}``.", ) def _set_tensor_and_parameters(self): """Sets tensor based on inputs.""" self._backend = _check_backend(self._backend) self._einsum = self._backend.np.einsum if isinstance(self.partition, list): self.partition = tuple(self.partition) try: if self._pure: self._matrix = np.reshape(self._matrix, self.partition) else: matrix_partition = self.partition * 2 self._matrix = np.reshape(self._matrix, matrix_partition) except: raise_error( ValueError, "``partition`` does not match the shape of the input matrix. " + f"Cannot reshape matrix of size {self._matrix.shape} to partition {self.partition}", ) if self.system_output is None: self.system_output = [ True, ] * len(self.partition) for k in range(len(self.partition) // 2): self.system_output[k * 2] = False self.system_output = tuple(self.system_output) else: self.system_output = tuple(self.system_output) def _full(self): """Reshapes input matrix based on purity.""" matrix = self._backend.cast(self._matrix, copy=True) if self.is_pure(): matrix = self._einsum("jk,lm -> kjml", matrix, np.conj(matrix)) return matrix def _check_subscript_pattern(self, subscripts: str): """Checks if input subscript match any implemented pattern.""" braket = "[a-z]" pattern_two = re.compile(braket * 2 + "," + braket * 2 + "->" + braket * 2) pattern_four = re.compile(braket * 4 + "," + braket * 2 + "->" + braket * 2) return bool(re.match(pattern_two, subscripts)), bool( re.match(pattern_four, subscripts) )