From 0dc496119a6111284021b5970ce7104cc6b86f14 Mon Sep 17 00:00:00 2001 From: Canoming Date: Sat, 2 Mar 2024 17:04:13 +0800 Subject: [PATCH 01/67] refactor `QuantumNetwork` to support general quantum networks --- src/qibo/quantum_info/quantum_networks.py | 745 ++++++++++++-------- tests/test_quantum_info_quantum_networks.py | 69 +- 2 files changed, 500 insertions(+), 314 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 8992169ae5..1c553791dd 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -1,6 +1,5 @@ """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 @@ -10,6 +9,7 @@ from qibo.backends import _check_backend from qibo.config import raise_error +from logging import warning class QuantumNetwork: """This class stores the Choi operator of the quantum network as a tensor, @@ -44,24 +44,106 @@ class QuantumNetwork: def __init__( self, - matrix, - partition: Union[List[int], Tuple[int]], - system_output: Optional[Union[List[bool], Tuple[bool]]] = None, + tensor, + partition: Optional[Union[List[int], Tuple[int]]] = None, + system_input: Optional[Union[List[bool], Tuple[bool]]] = None, pure: bool = False, backend=None, ): - self._run_checks(partition, system_output, pure) + self._run_checks(partition, system_input, pure) - self._matrix = matrix + self._tensor = tensor self.partition = partition - self.system_output = system_output + self.system_input = system_input self._pure = pure self._backend = backend - self.dims = reduce(mul, self.partition) - self._set_tensor_and_parameters() + self._set_parameters() + + self.dims = reduce(mul, self.partition) # should be after `_set_parameters` to ensure `self.partition` is not `None` + + @staticmethod + def _order_tensor2operator(n:int, system_input: Union[List[bool], Tuple[bool]]): + order = list(range(0, n*2, 2)) + list(range(1, n*2, 2)) + for i, is_input in enumerate(system_input): + if is_input: + order[i] = order[i] + 1 + order[i+n] = order[i+n] - 1 + return order + + @staticmethod + def _order_operator2tensor(n:int, system_input: Union[List[bool], Tuple[bool]]): + order = list(sum( + zip( + list(range(0, n)), list(range(n, n*2)) + ),())) + for i, is_input in enumerate(system_input): + if is_input: + temp = order[i*2] + order[i*2] = order[i*2+1] + order[i*2+1] = temp + return order + + @classmethod + def _operator2tensor(cls,operator,partition:List[int], system_input:List[bool]): + n = len(partition) + order = cls._order_operator2tensor(n, system_input) + try: + return operator.reshape( + list(partition) * 2 + ).transpose(order) + except: + raise_error( + ValueError, + "``partition`` does not match the shape of the input matrix. " + + f"Cannot reshape matrix of size {operator.shape} to partition {partition}", + ) + + @classmethod + def from_nparray(cls, + arr:np.ndarray, + partition: Optional[Union[List[int], Tuple[int]]] = None, + system_input: Optional[Union[List[bool], Tuple[bool]]] = None, + pure:bool=False, + backend=None, + ): + + if pure: + if partition is None: + partition = arr.shape + tensor = arr + else: + try: + tensor = arr.reshape(partition) + except: + raise_error( + ValueError, + "``partition`` does not match the shape of the input matrix. " + + f"Cannot reshape matrix of size {arr.shape} to partition {partition}", + ) + else: + # check if arr is a valid choi operator + len_sys = len(arr.shape) + if (len_sys % 2 != 0) or (arr.shape[:len_sys//2] != arr.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 {arr.shape}. '+ + 'If the input is pure, set `pure=True`.' + ) + + if partition is None: + partition = arr.shape[:len_sys//2] + + tensor = cls._operator2tensor(arr, partition, system_input) + + return cls(tensor, + partition=partition, + system_input=system_input, + pure=pure, + backend=backend) - def matrix(self, backend=None): + def operator(self, backend=None, full=False): """Returns the Choi operator of the quantum network in matrix form. Args: @@ -76,7 +158,22 @@ def matrix(self, backend=None): if backend is None: # pragma: no cover backend = self._backend - return backend.cast(self._matrix, dtype=self._matrix.dtype) + if self.is_pure() and not full: + return backend.cast(self._tensor, dtype=self._tensor.dtype) + + if self.is_pure(): + tensor = self.full(backend) + else: + tensor = self._tensor + + n = len(self.partition) + order = self._order_tensor2operator(n, self.system_input) + + operator = tensor.reshape( + np.repeat(self.partition, 2) + ).transpose(order) + + return backend.cast(operator, dtype=self._tensor.dtype) def is_pure(self): """Returns bool indicading if the Choi operator of the network is pure.""" @@ -106,6 +203,9 @@ def is_hermitian( Returns: bool: Hermiticity condition. """ + if self.is_pure(): # if the input is pure, it is always hermitian + return True + if precision_tol < 0.0: raise_error( ValueError, @@ -115,112 +215,13 @@ def is_hermitian( 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 + np.reshape(self.operator(), (self.dims, self.dims)), dtype=self._tensor.dtype ) - reshaped = self._backend.cast( + mat_diff = 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 - - 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 `_. - For all other backends, please refer to - `numpy.linalg.norm `_. - - 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 = np.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 - - 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 `_. - For all other backends, please refer to - `numpy.linalg.norm `_. - - 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 = np.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, - ) + norm = self._backend.calculate_norm_density_matrix(mat_diff, order=order) return float(norm) <= precision_tol @@ -237,69 +238,21 @@ def is_positive_semidefinite(self, precision_tol: float = 1e-8): Returns: bool: Positive-semidefinite condition. """ - self._matrix = self._full() - self._pure = False + if self.is_pure(): # if the input is pure, it is always positive semidefinite + return True - reshaped = np.reshape(self._matrix, (self.dims, self.dims)) + reshaped = self._backend.cast( + np.reshape(self.operator(), (self.dims, self.dims)), dtype=self._tensor.dtype + ) 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 False return all(eigenvalue >= -precision_tol for eigenvalue in eigenvalues) - 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) - - 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 = np.copy(self._matrix) - - if self.is_pure(): - return np.einsum("kj,ml,jl -> km", matrix, np.conj(matrix), state) - - return np.einsum("jklm,km -> jl", matrix, state) - - def link_product(self, second_network, subscripts: str = "ij,jk -> ik"): + def link_product(self, subscripts: str, second_network): """Link product between two quantum networks. The link product is not commutative. Here, we assume that @@ -318,84 +271,19 @@ def link_product(self, second_network, subscripts: str = "ij,jk -> ik"): :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network resulting from the link product between two quantum networks. """ - if not isinstance(second_network, QuantumNetwork): - raise_error( - TypeError, - "It is not possible to implement link product of a " - + "``QuantumNetwork`` with a non-``QuantumNetwork``.", - ) - - if not isinstance(subscripts, str): - raise_error( - TypeError, - f"subscripts must be type str, but it is type {type(subscripts)}.", - ) - subscripts = subscripts.replace(" ", "") - pattern_two, pattern_four = self._check_subscript_pattern(subscripts) - channel_subscripts = pattern_two and subscripts[1] == subscripts[3] - inv_subscripts = pattern_two and subscripts[0] == subscripts[4] - super_subscripts = ( - pattern_four - and subscripts[1] == subscripts[5] - and subscripts[2] == subscripts[6] - ) - - if not channel_subscripts and not inv_subscripts and not super_subscripts: - raise_error( - NotImplementedError, - "Subscripts do not match any implemented pattern.", - ) - - first_matrix = self._full() - second_matrix = second_network._full() # pylint: disable=W0212 - - if super_subscripts: - cexpr = "jklmnopq,klop->jmnq" - return QuantumNetwork( - np.einsum(cexpr, first_matrix, second_matrix), - [self.partition[0] + self.partition[-1]], - ) - - cexpr = "jkab,klbc->jlac" - - if inv_subscripts: - return QuantumNetwork( - np.einsum(cexpr, second_matrix, first_matrix), - [second_network.partition[0], self.partition[1]], - ) - - return QuantumNetwork( - np.einsum(cexpr, first_matrix, second_matrix), - [self.partition[0], second_network.partition[1]], - ) + return link_product(subscripts, self, second_network, backend=self._backend) def copy(self): """Returns a copy of the :class:`qibo.quantum_info.quantum_networks.QuantumNetwork` object.""" return self.__class__( - np.copy(self._matrix), + np.copy(self._tensor), partition=self.partition, - system_output=self.system_output, + system_output=self.system_input, pure=self._pure, backend=self._backend, ) - 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. @@ -416,23 +304,23 @@ def __add__(self, second_network): + f"and and object of type ``{type(second_network)}``.", ) - if self._full().shape != second_network._full().shape: + 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}.", + f"The Choi operators must have the same shape, but {self.full().shape} != " + + f"{second_network.full().shape}.", ) - if self.system_output != second_network.system_output: - raise_error(ValueError, "The networks must have the same output system.") + if self.system_input != second_network.system_input: + raise_error(ValueError, "The networks must have the same input systems.") - new_first_matrix = self._full() - new_second_matrix = second_network._full() + new_first_tensor = self.full() + new_second_tensor = second_network.full() return QuantumNetwork( - new_first_matrix + new_second_matrix, + new_first_tensor + new_second_tensor, self.partition, - self.system_output, + self.system_input, pure=False, backend=self._backend, ) @@ -460,19 +348,19 @@ def __mul__(self, number: Union[float, int]): if self.is_pure() and number > 0.0: return QuantumNetwork( - np.sqrt(number) * self.matrix(backend=self._backend), + np.sqrt(number) * self.operator(backend=self._backend), partition=self.partition, - system_output=self.system_output, + system_input=self.system_input, pure=True, backend=self._backend, ) - matrix = self._full() + tensor = self.full() return QuantumNetwork( - number * matrix, + number * tensor, partition=self.partition, - system_output=self.system_output, + system_input=self.system_input, pure=False, backend=self._backend, ) @@ -505,9 +393,9 @@ def __truediv__(self, number: Union[float, int]): number = np.sqrt(number) if self.is_pure() and number > 0.0 else number return QuantumNetwork( - self.matrix(backend=self._backend) / number, + self.operator(backend=self._backend) / number, partition=self.partition, - system_output=self.system_output, + system_input=self.system_input, pure=self.is_pure(), backend=self._backend, ) @@ -571,7 +459,7 @@ def __matmul__(self, second_network): + "Use `link_product` method to specify the subscript.", ) - return self.link_product(second_network, subscripts=subscripts) + return self.link_product(subscripts, second_network) def __str__(self): """Method to define how to print relevant information of the quantum network.""" @@ -579,7 +467,7 @@ def __str__(self): [ str(self.partition[k]) for k in range(len(self.partition)) - if not self.system_output[k] + if self.system_input[k] ] ) @@ -587,13 +475,13 @@ def __str__(self): [ str(self.partition[k]) for k in range(len(self.partition)) - if self.system_output[k] + if not self.system_input[k] ] ) return f"J[{string_in} -> {string_out}]" - def _run_checks(self, partition, system_output, pure): + 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( @@ -616,11 +504,11 @@ def _run_checks(self, partition, system_output, pure): + "but contains non-positive integers.", ) - if system_output is not None and len(system_output) != len(partition): + if system_input is not None and len(system_input) != len(partition): raise_error( ValueError, - "``len(system_output)`` must be the same as ``len(partition)``, " - + f"but {len(system_output)} != {len(partition)}.", + "``len(system_input)`` must be the same as ``len(partition)``, " + + f"but {len(system_input)} != {len(partition)}.", ) if not isinstance(pure, bool): @@ -628,53 +516,332 @@ def _run_checks(self, partition, system_output, pure): 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. + """ - def _set_tensor_and_parameters(self): - """Sets tensor based on inputs.""" + 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) - if isinstance(self.partition, list): - self.partition = tuple(self.partition) + self.partition = tuple(self.partition) + + self.system_input = self._check_system_input(self.system_input, + self.partition) try: if self._pure: - self._matrix = np.reshape(self._matrix, self.partition) + self._tensor = np.reshape(self._tensor, self.partition) else: - matrix_partition = self.partition * 2 - self._matrix = np.reshape(self._matrix, matrix_partition) + matrix_partition = [d**2 for d in self.partition] + self._tensor = np.reshape(self._tensor, 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}", + + f"Cannot reshape matrix of size {self._tensor.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, backend=None, update=False): + """Convert the internal representation to the full tensor of the network. + + Returns: + ndarray: The full reprentation of the quantum network. + """ + if backend is None: # pragma: no cover + backend = self._backend + tensor = np.copy(self._tensor) + + if self.is_pure(): + """Reshapes input matrix based on purity.""" + tensor.reshape([self.dims]) + tensor = np.tensordot(tensor, np.conj(tensor), axes=0) + tensor = self._operator2tensor(tensor, + self.partition, + self.system_input) + + if update: + self._tensor = tensor + self._pure = False + + return tensor + +class QuantumComb(QuantumNetwork): + + def __init__( + self, + tensor, + partition: Optional[Union[List[int], Tuple[int]]] = None, + system_output: Optional[Union[List[bool], Tuple[bool]]] = None, + pure: bool = False, + backend=None, + ): + if partition == None: + if pure: + partition = tensor.shape + else: + partition = (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_output is not None: + warning('system_output is ignored for QuantumComb') + + super().__init__(tensor, + partition, + [False, True]*(len(partition)//2), + pure, + backend) + + 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 `_. + For all other backends, please refer to + `numpy.linalg.norm `_. + + 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._tensor = self.full() + self._pure = False + + partial_trace = np.einsum("jklk -> jl", self._tensor) + 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 + +class QuantumChannel(QuantumNetwork): + + def __init__( + self, + tensor, + partition: Optional[Union[List[int], Tuple[int]]] = None, + system_output: Optional[Union[List[bool], Tuple[bool]]] = None, + pure: bool = False, + backend=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 or partition == None: + if system_output == None: # Assume the input is a quantum state + partition = (1, partition[0]) + elif len(system_output) == 1: + if system_output: + partition = (1, partition[0]) + else: + partition = (partition[0], 1) + + super().__init__(tensor, partition, [False,True], pure, backend) + + 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 `_. + For all other backends, please refer to + `numpy.linalg.norm `_. + + 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._tensor = self.full() + self._pure = False + + partial_trace = np.einsum("jkjl -> kl", self._tensor) + 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 + + 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) + + 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 = np.copy(self._tensor) - def _full(self): - """Reshapes input matrix based on purity.""" - matrix = np.copy(self._matrix) if self.is_pure(): - matrix = np.einsum("jk,lm -> kjml", matrix, np.conj(matrix)) + return np.einsum("kj,ml,jl -> km", matrix, np.conj(matrix), state) + + return np.einsum("jklm,km -> jl", matrix, state) - return matrix +class StochQuantumNetwork: + pass - return matrix +def link_product(subscripts:str = 'ij,jk -> ik' , *operands: QuantumNetwork, backend=None): + """Link product between two quantum networks. - 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) + 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. - return bool(re.match(pattern_two, subscripts)), bool( - re.match(pattern_four, subscripts) + Args: + second_network (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum + network to be applied to the original network. + subscripts (str, optional): Specifies the subscript for summation using + the Einstein summation convention. For more details, please refer to + `numpy.einsum `_. + + 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``." + ) + + tensors = (operand.full() if operand.is_pure() else operand._tensor 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, remaining, blas = contracrtion_list[0] + input_str, results_index = einsum_str.split('->') + inputs = input_str.split(',') + + partition = [] + system_input = [] + for ind in results_index: + found = False + for i, script in enumerate(inputs): + try: + index = script.index(ind) + if found: + warning( + f"Index {ind} is repeated in the output subscripts {results_index}." + ) + found = True + + partition.append(operands[inds[i]].partition[index]) + system_input.append(operands[inds[i]].partition[index]) + + except: + continue + + new_tensor = np.einsum(subscripts, *tensors) + + return QuantumNetwork(new_tensor, partition, system_input, backend=backend) + diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 1f20a97b45..82dbeb4bb3 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -32,7 +32,7 @@ def test_errors(backend): comb_sys_out = (False, True) * 2 comb = random_density_matrix(2**4, backend=backend) comb_choi = QuantumNetwork( - comb, comb_partition, system_output=comb_sys_out, backend=backend + comb, comb_partition, system_input=comb_sys_out, backend=backend ) with pytest.raises(TypeError): @@ -46,7 +46,7 @@ def test_errors(backend): with pytest.raises(ValueError): QuantumNetwork( - channel.to_choi(backend=backend), partition=(1, 2), system_output=(1, 2, 3) + channel.to_choi(backend=backend), partition=(1, 2), system_input=(1, 2, 3) ) with pytest.raises(TypeError): @@ -75,16 +75,16 @@ def test_errors(backend): network_2 = network.copy() with pytest.raises(ValueError): - network_2.system_output = (False,) + network_2.system_input = (False,) network += network_2 # Multiplying QuantumNetwork with non-QuantumNetwork with pytest.raises(TypeError): - network @ network.matrix(backend) + network @ network.operator(backend) # Linking QuantumNetwork with non-QuantumNetwork with pytest.raises(TypeError): - network.link_product(network.matrix(backend)) + network.link_product(network.operator(backend)) with pytest.raises(TypeError): network.link_product(network, subscripts=True) @@ -129,31 +129,31 @@ def test_operational_logic(backend): # Sum with itself has to match multiplying by int backend.assert_allclose( - (network + network).matrix(backend), (2 * network).matrix(backend) + (network + network).operator(backend), (2 * network).operator(backend) ) backend.assert_allclose( - (network_state_pure + network_state_pure).matrix(backend), - (2 * network_state_pure).to_full(), + (network_state_pure + network_state_pure).operator(backend), + (2 * network_state_pure).full(), ) # Sum with itself has to match multiplying by float backend.assert_allclose( - (network + network).matrix(backend), (2.0 * network).matrix(backend) + (network + network).operator(backend), (2.0 * network).operator(backend) ) backend.assert_allclose( - (network_state_pure + network_state_pure).matrix(backend), - (2.0 * network_state_pure).to_full(), + (network_state_pure + network_state_pure).operator(backend), + (2.0 * network_state_pure).full(), ) # Multiplying and dividing by same scalar has to bring back to original network backend.assert_allclose( - ((2.0 * network) / 2).matrix(backend), network.matrix(backend) + ((2.0 * network) / 2).operator(backend), network.operator(backend) ) unitary = random_unitary(dims, backend=backend) network_unitary = QuantumNetwork(unitary, (dims, dims), pure=True, backend=backend) backend.assert_allclose( - (network_unitary / 2).matrix(backend), unitary / np.sqrt(2), atol=1e-5 + (network_unitary / 2).operator(backend), unitary / np.sqrt(2), atol=1e-5 ) @@ -169,10 +169,10 @@ def test_parameters(backend): channel.to_choi(backend=backend), partition, backend=backend ) - backend.assert_allclose(network.matrix(backend=backend).shape, (2, 2, 2, 2)) + backend.assert_allclose(network.operator(backend=backend).shape, (2, 2, 2, 2)) backend.assert_allclose(network.dims, 4) backend.assert_allclose(network.partition, partition) - backend.assert_allclose(network.system_output, (False, True)) + backend.assert_allclose(network.system_input, (False, True)) assert network.is_causal() assert network.is_unital() @@ -202,7 +202,7 @@ def test_with_states(backend): backend.assert_allclose(state_output_network, state_output) backend.assert_allclose( - state_output_link.matrix(backend=backend).reshape((dims, dims)), state_output + state_output_link.operator(backend=backend).reshape((dims, dims)), state_output ) assert network_state.is_hermitian() @@ -226,19 +226,19 @@ def test_with_unitaries(backend, subscript): unitary_1 @ unitary_2, (dims, dims), pure=True, backend=backend ) - test = network_1.link_product(network_2, subscript).to_full(backend=backend) + test = network_1.link_product(network_2, subscript).full(backend=backend,update=True) if subscript[1] == subscript[3]: - backend.assert_allclose(test, network_3.to_full(backend), atol=1e-8) + backend.assert_allclose(test, network_3.full(), atol=1e-8) backend.assert_allclose( - test, (network_1 @ network_2).to_full(backend=backend), atol=1e-8 + test, (network_1 @ network_2).full(backend=backend), atol=1e-8 ) if subscript[0] == subscript[4]: - backend.assert_allclose(test, network_4.to_full(backend)) + backend.assert_allclose(test, network_4.full(backend)) - backend.assert_allclose(test, (network_2 @ network_1).to_full(backend=backend)) + backend.assert_allclose(test, (network_2 @ network_1).full(backend=backend)) def test_with_comb(backend): @@ -252,16 +252,16 @@ def test_with_comb(backend): channel = random_density_matrix(2**2, backend=backend) comb_choi = QuantumNetwork( - comb, comb_partition, system_output=comb_sys_out, backend=backend + comb, comb_partition, system_input=comb_sys_out, backend=backend ) channel_choi = QuantumNetwork( - channel, channel_partition, system_output=channel_sys_out, backend=backend + channel, channel_partition, system_input=channel_sys_out, backend=backend ) - test = comb_choi.link_product(channel_choi, subscript).to_full(backend) + test = comb_choi.link_product(channel_choi, subscript).full(backend,update=True) channel_choi2 = comb_choi @ channel_choi - backend.assert_allclose(test, channel_choi2.to_full(backend), atol=1e-5) + backend.assert_allclose(test, channel_choi2.full(backend), atol=1e-5) def test_apply(backend): @@ -291,3 +291,22 @@ def test_non_hermitian_and_prints(backend): assert not network.is_channel() assert network.__str__() == "J[4 -> 4]" + +def test_uility_func(): + old_shape = (0,10,1,11,2,12,3,13) + test_ls = np.ones(old_shape) + n = len(test_ls.shape) // 2 + + system_input = (False, True, False, True) + + order2op = QuantumNetwork._order_tensor2operator(n , system_input) + order2tensor = QuantumNetwork._order_operator2tensor(n , system_input) + new_shape = test_ls.transpose(order2op).shape + + for i in range(n): + if system_input[i]: + assert (new_shape[i] - new_shape[i+n]) == 10 + else: + assert (new_shape[i] - new_shape[i+n]) == -10 + + assert tuple(test_ls.transpose(order2op).transpose(order2tensor).shape) == old_shape From 93c7ca35cbe5dfd358c578b4642be5b638002821 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 09:30:43 +0000 Subject: [PATCH 02/67] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/qibo/quantum_info/quantum_networks.py | 150 ++++++++++---------- tests/test_quantum_info_quantum_networks.py | 19 +-- 2 files changed, 87 insertions(+), 82 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 1c553791dd..d612953dee 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -1,6 +1,7 @@ """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 @@ -9,7 +10,6 @@ from qibo.backends import _check_backend from qibo.config import raise_error -from logging import warning class QuantumNetwork: """This class stores the Choi operator of the quantum network as a tensor, @@ -60,38 +60,35 @@ def __init__( self._set_parameters() - self.dims = reduce(mul, self.partition) # should be after `_set_parameters` to ensure `self.partition` is not `None` + self.dims = reduce( + mul, self.partition + ) # should be after `_set_parameters` to ensure `self.partition` is not `None` @staticmethod - def _order_tensor2operator(n:int, system_input: Union[List[bool], Tuple[bool]]): - order = list(range(0, n*2, 2)) + list(range(1, n*2, 2)) + def _order_tensor2operator(n: int, system_input: Union[List[bool], Tuple[bool]]): + order = list(range(0, n * 2, 2)) + list(range(1, n * 2, 2)) for i, is_input in enumerate(system_input): if is_input: order[i] = order[i] + 1 - order[i+n] = order[i+n] - 1 + order[i + n] = order[i + n] - 1 return order @staticmethod - def _order_operator2tensor(n:int, system_input: Union[List[bool], Tuple[bool]]): - order = list(sum( - zip( - list(range(0, n)), list(range(n, n*2)) - ),())) + def _order_operator2tensor(n: int, system_input: Union[List[bool], Tuple[bool]]): + order = list(sum(zip(list(range(0, n)), list(range(n, n * 2))), ())) for i, is_input in enumerate(system_input): if is_input: - temp = order[i*2] - order[i*2] = order[i*2+1] - order[i*2+1] = temp + temp = order[i * 2] + order[i * 2] = order[i * 2 + 1] + order[i * 2 + 1] = temp return order @classmethod - def _operator2tensor(cls,operator,partition:List[int], system_input:List[bool]): + def _operator2tensor(cls, operator, partition: List[int], system_input: List[bool]): n = len(partition) order = cls._order_operator2tensor(n, system_input) try: - return operator.reshape( - list(partition) * 2 - ).transpose(order) + return operator.reshape(list(partition) * 2).transpose(order) except: raise_error( ValueError, @@ -100,13 +97,14 @@ def _operator2tensor(cls,operator,partition:List[int], system_input:List[bool]): ) @classmethod - def from_nparray(cls, - arr:np.ndarray, + def from_nparray( + cls, + arr: np.ndarray, partition: Optional[Union[List[int], Tuple[int]]] = None, system_input: Optional[Union[List[bool], Tuple[bool]]] = None, - pure:bool=False, + pure: bool = False, backend=None, - ): + ): if pure: if partition is None: @@ -124,24 +122,28 @@ def from_nparray(cls, else: # check if arr is a valid choi operator len_sys = len(arr.shape) - if (len_sys % 2 != 0) or (arr.shape[:len_sys//2] != arr.shape[len_sys//2:]): + if (len_sys % 2 != 0) or ( + arr.shape[: len_sys // 2] != arr.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 {arr.shape}. '+ - 'If the input is pure, set `pure=True`.' + "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 {arr.shape}. " + + "If the input is pure, set `pure=True`.", ) if partition is None: - partition = arr.shape[:len_sys//2] + partition = arr.shape[: len_sys // 2] tensor = cls._operator2tensor(arr, partition, system_input) - - return cls(tensor, - partition=partition, - system_input=system_input, - pure=pure, - backend=backend) + + return cls( + tensor, + partition=partition, + system_input=system_input, + pure=pure, + backend=backend, + ) def operator(self, backend=None, full=False): """Returns the Choi operator of the quantum network in matrix form. @@ -169,9 +171,7 @@ def operator(self, backend=None, full=False): n = len(self.partition) order = self._order_tensor2operator(n, self.system_input) - operator = tensor.reshape( - np.repeat(self.partition, 2) - ).transpose(order) + operator = tensor.reshape(np.repeat(self.partition, 2)).transpose(order) return backend.cast(operator, dtype=self._tensor.dtype) @@ -203,7 +203,7 @@ def is_hermitian( Returns: bool: Hermiticity condition. """ - if self.is_pure(): # if the input is pure, it is always hermitian + if self.is_pure(): # if the input is pure, it is always hermitian return True if precision_tol < 0.0: @@ -216,7 +216,8 @@ def is_hermitian( order = "euclidean" reshaped = self._backend.cast( - np.reshape(self.operator(), (self.dims, self.dims)), dtype=self._tensor.dtype + np.reshape(self.operator(), (self.dims, self.dims)), + dtype=self._tensor.dtype, ) mat_diff = self._backend.cast( np.transpose(np.conj(reshaped)) - reshaped, dtype=reshaped.dtype @@ -238,11 +239,12 @@ def is_positive_semidefinite(self, precision_tol: float = 1e-8): Returns: bool: Positive-semidefinite condition. """ - if self.is_pure(): # if the input is pure, it is always positive semidefinite + if self.is_pure(): # if the input is pure, it is always positive semidefinite return True reshaped = self._backend.cast( - np.reshape(self.operator(), (self.dims, self.dims)), dtype=self._tensor.dtype + np.reshape(self.operator(), (self.dims, self.dims)), + dtype=self._tensor.dtype, ) if self.is_hermitian(): @@ -516,7 +518,7 @@ def _run_checks(self, partition, system_input, pure): TypeError, f"``pure`` must be type ``bool``, but it is type ``{type(pure)}``.", ) - + @staticmethod def _check_system_input(system_input, partition) -> Tuple[bool]: """ @@ -537,8 +539,7 @@ def _set_parameters(self): self.partition = tuple(self.partition) - self.system_input = self._check_system_input(self.system_input, - self.partition) + self.system_input = self._check_system_input(self.system_input, self.partition) try: if self._pure: @@ -567,9 +568,7 @@ def full(self, backend=None, update=False): """Reshapes input matrix based on purity.""" tensor.reshape([self.dims]) tensor = np.tensordot(tensor, np.conj(tensor), axes=0) - tensor = self._operator2tensor(tensor, - self.partition, - self.system_input) + tensor = self._operator2tensor(tensor, self.partition, self.system_input) if update: self._tensor = tensor @@ -577,6 +576,7 @@ def full(self, backend=None, update=False): return tensor + class QuantumComb(QuantumNetwork): def __init__( @@ -591,21 +591,19 @@ def __init__( if pure: partition = tensor.shape else: - partition = (int(np.sqrt(d)) for d in tensor.shape) + partition = (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." + + "For general quantum networks, one should use the ``QuantumNetwork`` class.", ) if system_output is not None: - warning('system_output is ignored for QuantumComb') - - super().__init__(tensor, - partition, - [False, True]*(len(partition)//2), - pure, - backend) + warning("system_output is ignored for QuantumComb") + + super().__init__( + tensor, partition, [False, True] * (len(partition) // 2), pure, backend + ) def is_causal( self, order: Optional[Union[int, str]] = None, precision_tol: float = 1e-8 @@ -655,6 +653,7 @@ def is_causal( return float(norm) <= precision_tol + class QuantumChannel(QuantumNetwork): def __init__( @@ -669,19 +668,19 @@ def __init__( 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." + + "For general quantum networks, one should use the ``QuantumNetwork`` class.", ) - + if len(partition) == 1 or partition == None: - if system_output == None: # Assume the input is a quantum state + if system_output == None: # Assume the input is a quantum state partition = (1, partition[0]) elif len(system_output) == 1: if system_output: partition = (1, partition[0]) else: partition = (partition[0], 1) - - super().__init__(tensor, partition, [False,True], pure, backend) + + super().__init__(tensor, partition, [False, True], pure, backend) def is_unital( self, order: Optional[Union[int, str]] = None, precision_tol: float = 1e-8 @@ -776,10 +775,14 @@ def apply(self, state): return np.einsum("jklm,km -> jl", matrix, state) + class StochQuantumNetwork: pass -def link_product(subscripts:str = 'ij,jk -> ik' , *operands: QuantumNetwork, backend=None): + +def link_product( + subscripts: str = "ij,jk -> ik", *operands: QuantumNetwork, backend=None +): """Link product between two quantum networks. The link product is not commutative. Here, we assume that @@ -804,23 +807,23 @@ def link_product(subscripts:str = 'ij,jk -> ik' , *operands: QuantumNetwork, bac 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``." - ) - - tensors = (operand.full() if operand.is_pure() else operand._tensor for operand in operands) + raise_error(TypeError, f"The {i}th operator is not a ``QuantumNetwork``.") + + tensors = ( + operand.full() if operand.is_pure() else operand._tensor 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) - + _, contracrtion_list = np.einsum_path( + subscripts, *tensors, optimize=False, einsum_call=True + ) + inds, idx_rm, einsum_str, remaining, blas = contracrtion_list[0] - input_str, results_index = einsum_str.split('->') - inputs = input_str.split(',') + input_str, results_index = einsum_str.split("->") + inputs = input_str.split(",") partition = [] system_input = [] @@ -840,8 +843,7 @@ def link_product(subscripts:str = 'ij,jk -> ik' , *operands: QuantumNetwork, bac except: continue - + new_tensor = np.einsum(subscripts, *tensors) return QuantumNetwork(new_tensor, partition, system_input, backend=backend) - diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 82dbeb4bb3..e066763c7d 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -226,7 +226,9 @@ def test_with_unitaries(backend, subscript): unitary_1 @ unitary_2, (dims, dims), pure=True, backend=backend ) - test = network_1.link_product(network_2, subscript).full(backend=backend,update=True) + test = network_1.link_product(network_2, subscript).full( + backend=backend, update=True + ) if subscript[1] == subscript[3]: backend.assert_allclose(test, network_3.full(), atol=1e-8) @@ -258,7 +260,7 @@ def test_with_comb(backend): channel, channel_partition, system_input=channel_sys_out, backend=backend ) - test = comb_choi.link_product(channel_choi, subscript).full(backend,update=True) + test = comb_choi.link_product(channel_choi, subscript).full(backend, update=True) channel_choi2 = comb_choi @ channel_choi backend.assert_allclose(test, channel_choi2.full(backend), atol=1e-5) @@ -292,21 +294,22 @@ def test_non_hermitian_and_prints(backend): assert network.__str__() == "J[4 -> 4]" + def test_uility_func(): - old_shape = (0,10,1,11,2,12,3,13) + old_shape = (0, 10, 1, 11, 2, 12, 3, 13) test_ls = np.ones(old_shape) n = len(test_ls.shape) // 2 system_input = (False, True, False, True) - order2op = QuantumNetwork._order_tensor2operator(n , system_input) - order2tensor = QuantumNetwork._order_operator2tensor(n , system_input) + order2op = QuantumNetwork._order_tensor2operator(n, system_input) + order2tensor = QuantumNetwork._order_operator2tensor(n, system_input) new_shape = test_ls.transpose(order2op).shape for i in range(n): if system_input[i]: - assert (new_shape[i] - new_shape[i+n]) == 10 + assert (new_shape[i] - new_shape[i + n]) == 10 else: - assert (new_shape[i] - new_shape[i+n]) == -10 - + assert (new_shape[i] - new_shape[i + n]) == -10 + assert tuple(test_ls.transpose(order2op).transpose(order2tensor).shape) == old_shape From ee9d38f1a446eefc699332046fe9484c678b1b1a Mon Sep 17 00:00:00 2001 From: Canoming Date: Mon, 4 Mar 2024 21:04:39 +0800 Subject: [PATCH 03/67] gix typo --- src/qibo/quantum_info/quantum_networks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index d612953dee..2245edad9c 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -88,7 +88,7 @@ def _operator2tensor(cls, operator, partition: List[int], system_input: List[boo n = len(partition) order = cls._order_operator2tensor(n, system_input) try: - return operator.reshape(list(partition) * 2).transpose(order) + return operator.reshape(list(partition) * 2).transpose(order).reshape([dim**2 for dim in partition]) except: raise_error( ValueError, @@ -812,9 +812,9 @@ def link_product( if not isinstance(operand, QuantumNetwork): raise_error(TypeError, f"The {i}th operator is not a ``QuantumNetwork``.") - tensors = ( + tensors = [ operand.full() if operand.is_pure() else operand._tensor for operand in operands - ) + ] # keep track of the `partition` and `system_input` of the network _, contracrtion_list = np.einsum_path( @@ -839,7 +839,7 @@ def link_product( found = True partition.append(operands[inds[i]].partition[index]) - system_input.append(operands[inds[i]].partition[index]) + system_input.append(operands[inds[i]].system_input[index]) except: continue From 71764b83ea5391c28826f0934a193fa0d8f2eec9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:05:18 +0000 Subject: [PATCH 04/67] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/qibo/quantum_info/quantum_networks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 2245edad9c..b7f5b1ae97 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -88,7 +88,11 @@ def _operator2tensor(cls, operator, partition: List[int], system_input: List[boo n = len(partition) order = cls._order_operator2tensor(n, system_input) try: - return operator.reshape(list(partition) * 2).transpose(order).reshape([dim**2 for dim in partition]) + return ( + operator.reshape(list(partition) * 2) + .transpose(order) + .reshape([dim**2 for dim in partition]) + ) except: raise_error( ValueError, From 89a66428d2b6acddc66ea7fc786b2476a18223d0 Mon Sep 17 00:00:00 2001 From: Canoming Date: Mon, 4 Mar 2024 22:34:25 +0800 Subject: [PATCH 05/67] improve readability of the `__str__` of `QauantumNetwork` --- src/qibo/quantum_info/quantum_networks.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index b7f5b1ae97..0fee1a7a46 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -469,23 +469,15 @@ def __matmul__(self, second_network): 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 self.system_input[k] - ] - ) + systems = [] - string_out = ", ".join( - [ - str(self.partition[k]) - for k in range(len(self.partition)) - if not self.system_input[k] - ] - ) + for i, dim in enumerate(self.partition): + if self.system_input[i]: + systems.append(f'┍{dim}┑') + else: + systems.append(f'┕{dim}┙') - return f"J[{string_in} -> {string_out}]" + return f"J[{', '.join(systems)}]" def _run_checks(self, partition, system_input, pure): """Checks if all inputs are correct in type and value.""" From af3eeb5c90f94b16d6c30bfeb456fe1c32a95c33 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:36:07 +0000 Subject: [PATCH 06/67] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/qibo/quantum_info/quantum_networks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 0fee1a7a46..5520fe0aba 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -473,9 +473,9 @@ def __str__(self): for i, dim in enumerate(self.partition): if self.system_input[i]: - systems.append(f'┍{dim}┑') + systems.append(f"┍{dim}┑") else: - systems.append(f'┕{dim}┙') + systems.append(f"┕{dim}┙") return f"J[{', '.join(systems)}]" From 704aa0eedda49d4d8d11359bf50f0385f1c2065c Mon Sep 17 00:00:00 2001 From: Canoming Date: Wed, 6 Mar 2024 20:42:03 +0800 Subject: [PATCH 07/67] correct order of tensor --- src/qibo/quantum_info/quantum_networks.py | 69 +++++++++++++-------- tests/test_quantum_info_quantum_networks.py | 4 +- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 5520fe0aba..ec6fbfc4cb 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -60,33 +60,27 @@ def __init__( self._set_parameters() - self.dims = reduce( - mul, self.partition - ) # should be after `_set_parameters` to ensure `self.partition` is not `None` + if len(self.partition) > 0: + self.dims = reduce( + mul, self.partition + ) # should be after `_set_parameters` to ensure `self.partition` is not `None` + else: + self.dims = 1 @staticmethod - def _order_tensor2operator(n: int, system_input: Union[List[bool], Tuple[bool]]): + def _order_tensor2operator(n: int): order = list(range(0, n * 2, 2)) + list(range(1, n * 2, 2)) - for i, is_input in enumerate(system_input): - if is_input: - order[i] = order[i] + 1 - order[i + n] = order[i + n] - 1 return order @staticmethod - def _order_operator2tensor(n: int, system_input: Union[List[bool], Tuple[bool]]): + def _order_operator2tensor(n: int): order = list(sum(zip(list(range(0, n)), list(range(n, n * 2))), ())) - for i, is_input in enumerate(system_input): - if is_input: - temp = order[i * 2] - order[i * 2] = order[i * 2 + 1] - order[i * 2 + 1] = temp return order @classmethod def _operator2tensor(cls, operator, partition: List[int], system_input: List[bool]): n = len(partition) - order = cls._order_operator2tensor(n, system_input) + order = cls._order_operator2tensor(n) try: return ( operator.reshape(list(partition) * 2) @@ -173,11 +167,14 @@ def operator(self, backend=None, full=False): tensor = self._tensor n = len(self.partition) - order = self._order_tensor2operator(n, self.system_input) + order = self._order_tensor2operator(n) operator = tensor.reshape(np.repeat(self.partition, 2)).transpose(order) return backend.cast(operator, dtype=self._tensor.dtype) + + def matrix(self, backend=None): + return self.operator(backend, full=True).reshape((self.dims, self.dims)) def is_pure(self): """Returns bool indicading if the Choi operator of the network is pure.""" @@ -207,9 +204,6 @@ def is_hermitian( Returns: bool: Hermiticity condition. """ - if self.is_pure(): # if the input is pure, it is always hermitian - return True - if precision_tol < 0.0: raise_error( ValueError, @@ -219,8 +213,11 @@ def is_hermitian( 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( - np.reshape(self.operator(), (self.dims, self.dims)), + self.matrix(), dtype=self._tensor.dtype, ) mat_diff = self._backend.cast( @@ -247,7 +244,7 @@ def is_positive_semidefinite(self, precision_tol: float = 1e-8): return True reshaped = self._backend.cast( - np.reshape(self.operator(), (self.dims, self.dims)), + self.matrix(), dtype=self._tensor.dtype, ) @@ -572,7 +569,6 @@ def full(self, backend=None, update=False): return tensor - class QuantumComb(QuantumNetwork): def __init__( @@ -787,11 +783,11 @@ def link_product( in order to simplify notation. Args: - second_network (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum - network to be applied to the original network. subscripts (str, optional): Specifies the subscript for summation using the Einstein summation convention. For more details, please refer to `numpy.einsum `_. + operands (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum + network to be contracted. Returns: :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network resulting @@ -821,6 +817,29 @@ def link_product( input_str, results_index = einsum_str.split("->") inputs = input_str.split(",") + # Warning if the same index connects two input or two output systems + for ind in idx_rm: + found = 0 + for i, script in enumerate(inputs): + try: + index = script.index(ind) + found += 1 + if found > 1: + if is_input != operands[inds[i]].system_input[index]: + pass + else: + 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} is accores multiple times in the input subscripts {input_str}." + ) + except: + continue + + # check output systems partition = [] system_input = [] for ind in results_index: @@ -830,7 +849,7 @@ def link_product( index = script.index(ind) if found: warning( - f"Index {ind} is repeated in the output subscripts {results_index}." + f"Index {ind} is repeated in the input subscripts {input_str}." ) found = True diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index e066763c7d..b4fe2fd7b3 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -288,9 +288,9 @@ def test_non_hermitian_and_prints(backend): network = QuantumNetwork(matrix, (dims, dims), pure=False, backend=backend) assert not network.is_hermitian() - assert not network.is_causal() + # assert not network.is_causal() assert not network.is_positive_semidefinite() - assert not network.is_channel() + # assert not network.is_channel() assert network.__str__() == "J[4 -> 4]" From 443f199930b26c96ff895afd9d26bc723b5fabef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:42:30 +0000 Subject: [PATCH 08/67] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/qibo/quantum_info/quantum_networks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index ec6fbfc4cb..42a44981de 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -172,7 +172,7 @@ def operator(self, backend=None, full=False): operator = tensor.reshape(np.repeat(self.partition, 2)).transpose(order) return backend.cast(operator, dtype=self._tensor.dtype) - + def matrix(self, backend=None): return self.operator(backend, full=True).reshape((self.dims, self.dims)) @@ -569,6 +569,7 @@ def full(self, backend=None, update=False): return tensor + class QuantumComb(QuantumNetwork): def __init__( From 6f9b30a89aef826e59e40c5f943e891c168c6774 Mon Sep 17 00:00:00 2001 From: Canoming Date: Thu, 7 Mar 2024 19:10:30 +0800 Subject: [PATCH 09/67] add pre-defined tensors --- src/qibo/quantum_info/quantum_networks.py | 72 +++++++++++++++++------ 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 42a44981de..486a53782f 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -774,7 +774,10 @@ class StochQuantumNetwork: def link_product( - subscripts: str = "ij,jk -> ik", *operands: QuantumNetwork, backend=None + subscripts: str = "ij,jk -> ik", + *operands: QuantumNetwork, + backend=None, + surpress_warning=False, ): """Link product between two quantum networks. @@ -819,26 +822,27 @@ def link_product( inputs = input_str.split(",") # Warning if the same index connects two input or two output systems - for ind in idx_rm: - found = 0 - for i, script in enumerate(inputs): - try: - index = script.index(ind) - found += 1 - if found > 1: - if is_input != operands[inds[i]].system_input[index]: - pass - else: + if not surpress_warning: + for ind in idx_rm: + found = 0 + for i, script in enumerate(inputs): + try: + index = script.index(ind) + found += 1 + if found > 1: + if is_input != operands[inds[i]].system_input[index]: + pass + else: + 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} connects two {'input' if is_input else 'output'} systems." + f"Index {ind} is accores multiple times in the input subscripts {input_str}." ) - is_input = operands[inds[i]].system_input[index] - if found > 2: - warning( - f"Index {ind} is accores multiple times in the input subscripts {input_str}." - ) - except: - continue + except: + continue # check output systems partition = [] @@ -863,3 +867,33 @@ def link_product( new_tensor = np.einsum(subscripts, *tensors) return QuantumNetwork(new_tensor, partition, system_input, backend=backend) + + +def identity(dim: int, backend=None): + """Returns the identity channel. + + Args: + dim (int): Dimension of the identity operator. + backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used + to return the identity operator. If ``None``, defaults to the backend defined + when initializing the :class:`qibo.quant + """ + + return QuantumNetwork.from_nparray( + np.eye(dim), [dim, dim], [True, False], pure=True, backend=backend + ) + + +def trace(dim: int, backend=None): + """Returns the trace operator. + + Args: + dim (int): Dimension to be traced. + backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used + to return the identity operator. If ``None``, defaults to the backend defined + when initializing the :class:`qibo.quant + """ + + return QuantumNetwork.from_nparray( + np.eye(dim), [dim], [True], pure=False, backend=backend + ) From 9401fcddd5e0cc16bb5a7f4ec652d931c8516e35 Mon Sep 17 00:00:00 2001 From: Canoming Date: Sat, 16 Mar 2024 00:07:33 +0800 Subject: [PATCH 10/67] generalize the `is_causal` method to all combs --- src/qibo/quantum_info/quantum_networks.py | 71 +++++++++++++++-------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 486a53782f..0d6d8fdabe 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -576,7 +576,8 @@ def __init__( self, tensor, partition: Optional[Union[List[int], Tuple[int]]] = None, - system_output: Optional[Union[List[bool], Tuple[bool]]] = None, + *, + system_input: Optional[Union[List[bool], Tuple[bool]]] = None, pure: bool = False, backend=None, ): @@ -591,11 +592,11 @@ def __init__( "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_output is not None: + if system_input is not None: warning("system_output is ignored for QuantumComb") super().__init__( - tensor, partition, [False, True] * (len(partition) // 2), pure, backend + tensor, partition, [True, False] * (len(partition) // 2), pure, backend ) def is_causal( @@ -622,29 +623,37 @@ def is_causal( 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._tensor = self.full() - self._pure = False - - partial_trace = np.einsum("jklk -> jl", self._tensor) - 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, - ) + dim_out = self.partition[-1] + dim_in = self.partition[-2] + + reduced = np.tensordot(self.full(), trace(dim_out).full(), axes=1) + print(reduced) + sub_comb = np.tensordot(reduced, trace(dim_in).full(), axes=1) + print(sub_comb) + + expected = np.tensordot(sub_comb, trace(dim_in).full() / dim_in, axes=0) + print(expected) + norm = self._backend.calculate_norm((reduced - expected), order=order) + if norm > precision_tol: + return False + elif len(self.partition) == 2: + return True + else: + return QuantumComb( + sub_comb, self.partition[:-2], pure=False, backend=self._backend + ).is_causal(order, precision_tol) - return float(norm) <= precision_tol + @classmethod + def from_nparray( + cls, tensor, partition=None, pure=False, backend=None, inverse=False + ): + comb = super().from_nparray(tensor, partition, None, pure, backend) + if ( + inverse + ): # Convert mathmetical convention of Choi operator to physical convention + comb.partition = comb.partition[::-1] + comb._tensor = np.transpose(comb._tensor) + return comb class QuantumChannel(QuantumNetwork): @@ -778,6 +787,7 @@ def link_product( *operands: QuantumNetwork, backend=None, surpress_warning=False, + casting: bool = True, ): """Link product between two quantum networks. @@ -866,6 +876,17 @@ def link_product( new_tensor = np.einsum(subscripts, *tensors) + if casting: + try: + network = QuantumChannel(new_tensor, partition, backend=backend) + return network + except: + try: + network = QuantumComb(new_tensor, partition, backend=backend) + return network + except: + pass + return QuantumNetwork(new_tensor, partition, system_input, backend=backend) From 385bae6ca77c49fc3d5d7a1980db6aa02a6732e9 Mon Sep 17 00:00:00 2001 From: Canoming Date: Sat, 30 Mar 2024 23:20:55 +0800 Subject: [PATCH 11/67] resolve the type errors when multiplied by np.int or np.float --- src/qibo/quantum_info/quantum_networks.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 0d6d8fdabe..3c75b459a2 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -2,6 +2,7 @@ from functools import reduce from logging import warning +from numbers import Number from operator import mul from typing import List, Optional, Tuple, Union @@ -328,7 +329,7 @@ def __add__(self, second_network): backend=self._backend, ) - def __mul__(self, number: Union[float, int]): + def __mul__(self, number: Union[float, int, Number]): """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 @@ -343,7 +344,7 @@ def __mul__(self, number: Union[float, int]): :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network with its Choi operator multiplied by ``number``. """ - if not isinstance(number, (float, int)): + if not isinstance(number, Number): raise_error( TypeError, "It is not possible to multiply a ``QuantumNetwork`` by a non-scalar.", @@ -368,11 +369,11 @@ def __mul__(self, number: Union[float, int]): backend=self._backend, ) - def __rmul__(self, number: Union[float, int]): + def __rmul__(self, number: Union[float, int, Number]): """""" return self.__mul__(number) - def __truediv__(self, number: Union[float, int]): + def __truediv__(self, number: Union[float, int, Number]): """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 @@ -387,7 +388,7 @@ def __truediv__(self, number: Union[float, int]): :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network with its Choi operator divided by ``number``. """ - if not isinstance(number, (float, int)): + if not isinstance(number, Number): raise_error( TypeError, "It is not possible to divide a ``QuantumNetwork`` by a non-scalar.", From 89c28a6cccbaa5d9dec0304aa234da655e182e27 Mon Sep 17 00:00:00 2001 From: Canoming Date: Sat, 13 Apr 2024 21:35:43 +0800 Subject: [PATCH 12/67] fix existing tests --- src/qibo/quantum_info/quantum_networks.py | 80 ++++++++++------- tests/test_quantum_info_quantum_networks.py | 98 ++++++++++++--------- 2 files changed, 107 insertions(+), 71 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 3c75b459a2..5c410abea5 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -32,9 +32,9 @@ class QuantumNetwork: 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 + system_input (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)``. + ``(False,True,False,True,...)``, where ``len(system_input)=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``. @@ -241,6 +241,12 @@ def is_positive_semidefinite(self, precision_tol: float = 1e-8): 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 @@ -283,7 +289,17 @@ def copy(self): return self.__class__( np.copy(self._tensor), partition=self.partition, - system_output=self.system_input, + system_input=self.system_input, + pure=self._pure, + backend=self._backend, + ) + + def conj(self): + """Returns the conjugate of the quantum network.""" + return self.__class__( + np.conj(self._tensor), + partition=self.partition, + system_input=self.system_input, pure=self._pure, backend=self._backend, ) @@ -352,7 +368,7 @@ def __mul__(self, number: Union[float, int, Number]): if self.is_pure() and number > 0.0: return QuantumNetwork( - np.sqrt(number) * self.operator(backend=self._backend), + np.sqrt(number) * self._tensor, partition=self.partition, system_input=self.system_input, pure=True, @@ -397,7 +413,7 @@ def __truediv__(self, number: Union[float, int, Number]): number = np.sqrt(number) if self.is_pure() and number > 0.0 else number return QuantumNetwork( - self.operator(backend=self._backend) / number, + self._tensor / number, partition=self.partition, system_input=self.system_input, pure=self.is_pure(), @@ -594,7 +610,7 @@ def __init__( + "For general quantum networks, one should use the ``QuantumNetwork`` class.", ) if system_input is not None: - warning("system_output is ignored for QuantumComb") + warning("system_input is ignored for QuantumComb") super().__init__( tensor, partition, [True, False] * (len(partition) // 2), pure, backend @@ -624,16 +640,19 @@ def is_causal( 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}", + ) + dim_out = self.partition[-1] dim_in = self.partition[-2] reduced = np.tensordot(self.full(), trace(dim_out).full(), axes=1) - print(reduced) sub_comb = np.tensordot(reduced, trace(dim_in).full(), axes=1) - print(sub_comb) - expected = np.tensordot(sub_comb, trace(dim_in).full() / dim_in, axes=0) - print(expected) + norm = self._backend.calculate_norm((reduced - expected), order=order) if norm > precision_tol: return False @@ -657,13 +676,13 @@ def from_nparray( return comb -class QuantumChannel(QuantumNetwork): +class QuantumChannel(QuantumComb): def __init__( self, tensor, partition: Optional[Union[List[int], Tuple[int]]] = None, - system_output: Optional[Union[List[bool], Tuple[bool]]] = None, + system_input: Optional[Union[List[bool], Tuple[bool]]] = None, pure: bool = False, backend=None, ): @@ -675,15 +694,15 @@ def __init__( ) if len(partition) == 1 or partition == None: - if system_output == None: # Assume the input is a quantum state + if system_input == None: # Assume the input is a quantum state partition = (1, partition[0]) - elif len(system_output) == 1: - if system_output: + elif len(system_input) == 1: + if system_input: partition = (1, partition[0]) else: partition = (partition[0], 1) - super().__init__(tensor, partition, [False, True], pure, backend) + super().__init__(tensor, partition, pure=pure, backend=backend) def is_unital( self, order: Optional[Union[int, str]] = None, precision_tol: float = 1e-8 @@ -718,6 +737,18 @@ def is_unital( if order is None and self._backend.__class__.__name__ == "TensorflowBackend": order = "euclidean" + reduced = np.tensordot(self.full(), trace(self.partition[1]).full(), axes=1) + sub_comb = np.tensordot(reduced, trace(self.partition[0]).full(), axes=1) + expected = np.tensordot( + sub_comb, trace(self.partition[0]).full() / self.partition[0], axes=0 + ) + + norm = self._backend.calculate_norm((reduced - expected), order=order) + if norm > precision_tol: + return False + elif len(self.partition) == 2: + return True + self._tensor = self.full() self._pure = False @@ -771,12 +802,12 @@ def apply(self, state): Returns: ndarray: Resulting state :math:`\\mathcal{E}(\\varrho)`. """ - matrix = np.copy(self._tensor) + operator = np.copy(self.operator()) if self.is_pure(): - return np.einsum("kj,ml,jl -> km", matrix, np.conj(matrix), state) + return np.einsum("kj,ml,jl -> km", operator, np.conj(operator), state) - return np.einsum("jklm,km -> jl", matrix, state) + return np.einsum("jklm, km", operator, state) class StochQuantumNetwork: @@ -877,17 +908,6 @@ def link_product( new_tensor = np.einsum(subscripts, *tensors) - if casting: - try: - network = QuantumChannel(new_tensor, partition, backend=backend) - return network - except: - try: - network = QuantumComb(new_tensor, partition, backend=backend) - return network - except: - pass - return QuantumNetwork(new_tensor, partition, system_input, backend=backend) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index b4fe2fd7b3..5060d9d27a 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -4,7 +4,11 @@ import pytest from qibo import gates -from qibo.quantum_info.quantum_networks import QuantumNetwork +from qibo.quantum_info.quantum_networks import ( + QuantumChannel, + QuantumComb, + QuantumNetwork, +) from qibo.quantum_info.random_ensembles import ( random_density_matrix, random_gaussian_matrix, @@ -18,7 +22,13 @@ def test_errors(backend): nqubits = len(channel.target_qubits) dims = 2**nqubits partition = (dims, dims) - network = QuantumNetwork( + network = QuantumNetwork.from_nparray( + channel.to_choi(backend=backend), partition, backend=backend + ) + quantum_comb = QuantumComb.from_nparray( + channel.to_choi(backend=backend), partition, backend=backend + ) + quantum_channel = QuantumChannel.from_nparray( channel.to_choi(backend=backend), partition, backend=backend ) @@ -56,10 +66,13 @@ def test_errors(backend): network.is_hermitian(precision_tol=-1e-8) with pytest.raises(ValueError): - network.is_unital(precision_tol=-1e-8) + network.is_positive_semidefinite(precision_tol=-1e-8) with pytest.raises(ValueError): - network.is_causal(precision_tol=-1e-8) + quantum_comb.is_causal(precision_tol=-1e-8) + + with pytest.raises(ValueError): + quantum_channel.is_unital(precision_tol=-1e-8) with pytest.raises(TypeError): network + 1 @@ -89,9 +102,6 @@ def test_errors(backend): with pytest.raises(TypeError): network.link_product(network, subscripts=True) - with pytest.raises(NotImplementedError): - network.link_product(network, subscripts="jk,lm->no") - with pytest.raises(NotImplementedError): net @ net @@ -120,7 +130,7 @@ def test_operational_logic(backend): nqubits = len(channel.target_qubits) dims = 2**nqubits partition = (dims, dims) - network = QuantumNetwork( + network = QuantumNetwork.from_nparray( channel.to_choi(backend=backend), partition, backend=backend ) @@ -133,7 +143,7 @@ def test_operational_logic(backend): ) backend.assert_allclose( (network_state_pure + network_state_pure).operator(backend), - (2 * network_state_pure).full(), + (2 * network_state_pure).operator(backend, full=True), ) # Sum with itself has to match multiplying by float @@ -142,7 +152,7 @@ def test_operational_logic(backend): ) backend.assert_allclose( (network_state_pure + network_state_pure).operator(backend), - (2.0 * network_state_pure).full(), + (2.0 * network_state_pure).operator(backend, full=True), ) # Multiplying and dividing by same scalar has to bring back to original network @@ -165,20 +175,26 @@ def test_parameters(backend): dims = 2**nqubits partition = (dims, dims) - network = QuantumNetwork( + network = QuantumNetwork.from_nparray( + channel.to_choi(backend=backend), partition, backend=backend + ) + quantum_comb = QuantumComb.from_nparray( + channel.to_choi(backend=backend), partition, backend=backend + ) + quantum_channel = QuantumChannel.from_nparray( channel.to_choi(backend=backend), partition, backend=backend ) backend.assert_allclose(network.operator(backend=backend).shape, (2, 2, 2, 2)) backend.assert_allclose(network.dims, 4) backend.assert_allclose(network.partition, partition) - backend.assert_allclose(network.system_input, (False, True)) + backend.assert_allclose(network.system_input, (True, False)) - assert network.is_causal() - assert network.is_unital() + assert quantum_comb.is_causal() + assert quantum_channel.is_unital() assert network.is_hermitian() assert network.is_positive_semidefinite() - assert network.is_channel() + assert quantum_channel.is_channel() def test_with_states(backend): @@ -186,24 +202,20 @@ def test_with_states(backend): dims = 2**nqubits state = random_density_matrix(dims, backend=backend) - network_state = QuantumNetwork(state, (1, 2), backend=backend) + network_state = QuantumChannel.from_nparray(state, backend=backend) lamb = float(np.random.rand()) channel = gates.DepolarizingChannel(0, lamb) - network_channel = QuantumNetwork( - channel.to_choi(backend=backend), (dims, dims), backend=backend + network_channel = QuantumChannel.from_nparray( + channel.to_choi(backend=backend), (dims, dims), backend=backend, inverse=True ) state_output = channel.apply_density_matrix(backend, state, nqubits) state_output_network = network_channel.apply(state) - state_output_link = network_state.link_product( - network_channel, subscripts="ij,jk -> ik" - ) + state_output_link = network_state.link_product("ij,kj -> ik", network_channel) backend.assert_allclose(state_output_network, state_output) - backend.assert_allclose( - state_output_link.operator(backend=backend).reshape((dims, dims)), state_output - ) + backend.assert_allclose(state_output_link.matrix(backend=backend), state_output) assert network_state.is_hermitian() assert network_state.is_positive_semidefinite() @@ -217,16 +229,20 @@ def test_with_unitaries(backend, subscript): unitary_1 = random_unitary(dims, backend=backend) unitary_2 = random_unitary(dims, backend=backend) - network_1 = QuantumNetwork(unitary_1, (dims, dims), pure=True, backend=backend) - network_2 = QuantumNetwork(unitary_2, (dims, dims), pure=True, backend=backend) - network_3 = QuantumNetwork( - unitary_2 @ unitary_1, (dims, dims), pure=True, backend=backend + network_1 = QuantumComb.from_nparray( + unitary_1, (dims, dims), pure=True, backend=backend, inverse=True + ) + network_2 = QuantumComb.from_nparray( + unitary_2, (dims, dims), pure=True, backend=backend, inverse=True + ) + network_3 = QuantumComb.from_nparray( + unitary_2 @ unitary_1, (dims, dims), pure=True, backend=backend, inverse=True ) - network_4 = QuantumNetwork( - unitary_1 @ unitary_2, (dims, dims), pure=True, backend=backend + network_4 = QuantumComb.from_nparray( + unitary_1 @ unitary_2, (dims, dims), pure=True, backend=backend, inverse=True ) - test = network_1.link_product(network_2, subscript).full( + test = network_1.link_product(subscript, network_2).full( backend=backend, update=True ) @@ -260,7 +276,7 @@ def test_with_comb(backend): channel, channel_partition, system_input=channel_sys_out, backend=backend ) - test = comb_choi.link_product(channel_choi, subscript).full(backend, update=True) + test = comb_choi.link_product(subscript, channel_choi).full(backend, update=True) channel_choi2 = comb_choi @ channel_choi backend.assert_allclose(test, channel_choi2.full(backend), atol=1e-5) @@ -272,7 +288,7 @@ def test_apply(backend): state = random_density_matrix(dims, backend=backend) unitary = random_unitary(dims, backend=backend) - network = QuantumNetwork(unitary, (dims, dims), pure=True, backend=backend) + network = QuantumChannel(unitary, (dims, dims), pure=True, backend=backend) applied = network.apply(state) target = unitary @ state @ np.transpose(np.conj(unitary)) @@ -292,7 +308,7 @@ def test_non_hermitian_and_prints(backend): assert not network.is_positive_semidefinite() # assert not network.is_channel() - assert network.__str__() == "J[4 -> 4]" + assert network.__str__() == "J[┍4┑, ┕4┙]" def test_uility_func(): @@ -300,16 +316,16 @@ def test_uility_func(): test_ls = np.ones(old_shape) n = len(test_ls.shape) // 2 + order2op = QuantumNetwork._order_tensor2operator(n) + order2tensor = QuantumNetwork._order_operator2tensor(n) + system_input = (False, True, False, True) - order2op = QuantumNetwork._order_tensor2operator(n, system_input) - order2tensor = QuantumNetwork._order_operator2tensor(n, system_input) new_shape = test_ls.transpose(order2op).shape - for i in range(n): - if system_input[i]: - assert (new_shape[i] - new_shape[i + n]) == 10 - else: - assert (new_shape[i] - new_shape[i + n]) == -10 + # if system_input[i]: + assert (new_shape[i] - new_shape[i + n]) == -10 + # else: + # assert (new_shape[i] - new_shape[i + n]) == -10 assert tuple(test_ls.transpose(order2op).transpose(order2tensor).shape) == old_shape From a9b6a3b234cb8bc97cd1a83a64d9baf669f8e914 Mon Sep 17 00:00:00 2001 From: Canoming Date: Sun, 14 Apr 2024 20:53:17 +0800 Subject: [PATCH 13/67] update documents --- src/qibo/quantum_info/quantum_networks.py | 119 ++++++++++++++------ tests/test_quantum_info_quantum_networks.py | 39 +++++-- 2 files changed, 115 insertions(+), 43 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 5c410abea5..22c00e640d 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -13,30 +13,30 @@ class QuantumNetwork: - """This class stores the Choi operator of the quantum network as a tensor, - which is an unique representation of the quantum network. + """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]`, + 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]`, + 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']`. + 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``. + 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 - ``(False,True,False,True,...)``, where ``len(system_input)=len(partition)``. + ``(True,False,True,False,...)``, where ``len(system_input)=len(partition)``. Defaults to ``None``. - pure (bool, optional): ``True`` when ``matrix`` is a "pure" representation (e.g. a pure + 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 :class:`qibo.backends.GlobalBackend`. @@ -104,6 +104,28 @@ def from_nparray( pure: bool = False, backend=None, ): + """Construct a :class:`qibo.quantum_info.quantum_networks.QuantumNetwork` object from a numpy array. + 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 :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. + + Returns: + :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: + """ if pure: if partition is None: @@ -145,16 +167,20 @@ def from_nparray( ) def operator(self, backend=None, full=False): - """Returns the Choi operator of the quantum network in matrix form. + """Returns the Choi operator of the quantum network. + The shape of the returned operator is :math:`(*self.partition, *self.partition)`. 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``. + 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``. Returns: - ndarray: Choi matrix of the quantum network. + ndarray: Choi operator of the quantum network. """ if backend is None: # pragma: no cover backend = self._backend @@ -175,6 +201,18 @@ def operator(self, backend=None, full=False): return backend.cast(operator, dtype=self._tensor.dtype) 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(backend, full=True).reshape((self.dims, self.dims)) def is_pure(self): @@ -184,10 +222,10 @@ def is_pure(self): 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. + """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` 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. + 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 @@ -229,12 +267,12 @@ def is_hermitian( return float(norm) <= precision_tol 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. + """Returns bool indicating if Choi operator :math:`\\mathcal{J}` 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}`. + 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`. @@ -271,11 +309,11 @@ def link_product(self, subscripts: str, second_network): in order to simplify notation. Args: - second_network (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum - network to be applied to the original network. subscripts (str, optional): Specifies the subscript for summation using the Einstein summation convention. For more details, please refer to `numpy.einsum `_. + second_network (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum + network to be applied to the original network. Returns: :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network resulting @@ -588,6 +626,25 @@ def full(self, backend=None, update=False): class QuantumComb(QuantumNetwork): + """Quantum comb is a quantum network such that the systems follows a sequential order. + It is also called the 'non-Markovian quantum process' in many literatures. + + 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 :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. + """ def __init__( self, @@ -619,17 +676,11 @@ def __init__( 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. + """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` 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 `_. - For all other backends, please refer to - `numpy.linalg.norm `_. + 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``. @@ -833,7 +884,7 @@ def link_product( the Einstein summation convention. For more details, please refer to `numpy.einsum `_. operands (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum - network to be contracted. + networks to be contracted. Returns: :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network resulting @@ -921,8 +972,8 @@ def identity(dim: int, backend=None): when initializing the :class:`qibo.quant """ - return QuantumNetwork.from_nparray( - np.eye(dim), [dim, dim], [True, False], pure=True, backend=backend + return QuantumChannel.from_nparray( + np.eye(dim), [dim, dim], pure=True, backend=backend ) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 5060d9d27a..a59317053d 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -8,6 +8,9 @@ QuantumChannel, QuantumComb, QuantumNetwork, + identity, + link_product, + trace, ) from qibo.quantum_info.random_ensembles import ( random_density_matrix, @@ -269,10 +272,10 @@ def test_with_comb(backend): comb = random_density_matrix(2**4, backend=backend) channel = random_density_matrix(2**2, backend=backend) - comb_choi = QuantumNetwork( + comb_choi = QuantumNetwork.from_nparray( comb, comb_partition, system_input=comb_sys_out, backend=backend ) - channel_choi = QuantumNetwork( + channel_choi = QuantumNetwork.from_nparray( channel, channel_partition, system_input=channel_sys_out, backend=backend ) @@ -288,7 +291,9 @@ def test_apply(backend): state = random_density_matrix(dims, backend=backend) unitary = random_unitary(dims, backend=backend) - network = QuantumChannel(unitary, (dims, dims), pure=True, backend=backend) + network = QuantumChannel.from_nparray( + unitary, (dims, dims), pure=True, backend=backend + ) applied = network.apply(state) target = unitary @ state @ np.transpose(np.conj(unitary)) @@ -301,7 +306,9 @@ def test_non_hermitian_and_prints(backend): dims = 2**nqubits matrix = random_gaussian_matrix(dims**2, backend=backend) - network = QuantumNetwork(matrix, (dims, dims), pure=False, backend=backend) + network = QuantumNetwork.from_nparray( + matrix, (dims, dims), pure=False, backend=backend + ) assert not network.is_hermitian() # assert not network.is_causal() @@ -312,6 +319,8 @@ def test_non_hermitian_and_prints(backend): def test_uility_func(): + # _order_tensor2operator should convert + # (a0,a1,b0,b1,...) to (a0,b0,..., a1,b1,...) old_shape = (0, 10, 1, 11, 2, 12, 3, 13) test_ls = np.ones(old_shape) n = len(test_ls.shape) // 2 @@ -319,13 +328,25 @@ def test_uility_func(): order2op = QuantumNetwork._order_tensor2operator(n) order2tensor = QuantumNetwork._order_operator2tensor(n) - system_input = (False, True, False, True) - new_shape = test_ls.transpose(order2op).shape for i in range(n): - # if system_input[i]: assert (new_shape[i] - new_shape[i + n]) == -10 - # else: - # assert (new_shape[i] - new_shape[i + n]) == -10 assert tuple(test_ls.transpose(order2op).transpose(order2tensor).shape) == old_shape + + +def test_predefined(backend): + id = identity(2) + tr = trace(2) + + backend.assert_allclose( + id.matrix(backend=backend), + np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [1, 0, 0, 1]]), + atol=1e-8, + ) + + traced = link_product("ij,j", id, tr) + + backend.assert_allclose( + tr.matrix(backend=backend), traced.matrix(backend=backend), atol=1e-8 + ) From ed5651aa50c04eca5f70e53cee9f628fc03ea571 Mon Sep 17 00:00:00 2001 From: Canoming Date: Wed, 17 Apr 2024 16:15:19 +0800 Subject: [PATCH 14/67] fix issue: `tensorflow.norm` cannot take argument `order=None` in some versions --- src/qibo/quantum_info/quantum_networks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 22c00e640d..c036ff520b 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -704,7 +704,10 @@ def is_causal( sub_comb = np.tensordot(reduced, trace(dim_in).full(), axes=1) expected = np.tensordot(sub_comb, trace(dim_in).full() / dim_in, axes=0) - norm = self._backend.calculate_norm((reduced - expected), order=order) + if order == None: + norm = self._backend.calculate_norm(reduced - expected) + else: + norm = self._backend.calculate_norm(reduced - expected, order=order) if norm > precision_tol: return False elif len(self.partition) == 2: From f9eb604fe5a421e5155666078a0bc568a4e7c63e Mon Sep 17 00:00:00 2001 From: Canoming Date: Wed, 17 Apr 2024 18:47:52 +0800 Subject: [PATCH 15/67] fix issue: `tensorflow.norm` cannot take argument `order=None` --- src/qibo/quantum_info/quantum_networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index c036ff520b..b926b97474 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -705,7 +705,7 @@ def is_causal( expected = np.tensordot(sub_comb, trace(dim_in).full() / dim_in, axes=0) if order == None: - norm = self._backend.calculate_norm(reduced - expected) + norm = self._backend.calculate_norm(reduced - expected, order=2) else: norm = self._backend.calculate_norm(reduced - expected, order=order) if norm > precision_tol: From 1e0290f7927f5b0b35a2ab912538639764c112f9 Mon Sep 17 00:00:00 2001 From: Canoming Date: Wed, 17 Apr 2024 19:42:18 +0800 Subject: [PATCH 16/67] make `einsum` depends on backend --- src/qibo/quantum_info/quantum_networks.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index b926b97474..1709b2357a 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -589,6 +589,8 @@ def _set_parameters(self): self.system_input = self._check_system_input(self.system_input, self.partition) + self._einsum = self._backend.np.einsum + try: if self._pure: self._tensor = np.reshape(self._tensor, self.partition) @@ -806,7 +808,7 @@ def is_unital( self._tensor = self.full() self._pure = False - partial_trace = np.einsum("jkjl -> kl", self._tensor) + partial_trace = self._einsum("jkjl -> kl", self._tensor) identity = self._backend.cast( np.eye(partial_trace.shape[0]), dtype=partial_trace.dtype ) @@ -859,9 +861,9 @@ def apply(self, state): operator = np.copy(self.operator()) if self.is_pure(): - return np.einsum("kj,ml,jl -> km", operator, np.conj(operator), state) + return self._einsum("kj,ml,jl -> km", operator, np.conj(operator), state) - return np.einsum("jklm, km", operator, state) + return self._einsum("jklm, km", operator, state) class StochQuantumNetwork: @@ -960,7 +962,7 @@ def link_product( except: continue - new_tensor = np.einsum(subscripts, *tensors) + new_tensor = self._einsum(subscripts, *tensors) return QuantumNetwork(new_tensor, partition, system_input, backend=backend) From dd810ecccfabc50136c6aefcb447eee3d3142dba Mon Sep 17 00:00:00 2001 From: Canoming Date: Wed, 17 Apr 2024 19:49:43 +0800 Subject: [PATCH 17/67] fix order issue as in `master` --- src/qibo/quantum_info/quantum_networks.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 1709b2357a..e9721e5bf6 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -699,6 +699,9 @@ def is_causal( 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" + dim_out = self.partition[-1] dim_in = self.partition[-2] @@ -706,10 +709,8 @@ def is_causal( sub_comb = np.tensordot(reduced, trace(dim_in).full(), axes=1) expected = np.tensordot(sub_comb, trace(dim_in).full() / dim_in, axes=0) - if order == None: - norm = self._backend.calculate_norm(reduced - expected, order=2) - else: - norm = self._backend.calculate_norm(reduced - expected, order=order) + norm = self._backend.calculate_norm(reduced - expected, order=order) + if norm > precision_tol: return False elif len(self.partition) == 2: @@ -962,7 +963,7 @@ def link_product( except: continue - new_tensor = self._einsum(subscripts, *tensors) + new_tensor = np.einsum(subscripts, *tensors) return QuantumNetwork(new_tensor, partition, system_input, backend=backend) From f9b065494725175171f1fa0e22c350f7c990e175 Mon Sep 17 00:00:00 2001 From: Canoming Date: Thu, 18 Apr 2024 18:17:14 +0800 Subject: [PATCH 18/67] fix tensorflow transpose --- src/qibo/quantum_info/quantum_networks.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index e9721e5bf6..252dd87c67 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -7,6 +7,7 @@ from typing import List, Optional, Tuple, Union import numpy as np +import tensorflow as tf from qibo.backends import _check_backend from qibo.config import raise_error @@ -83,6 +84,12 @@ def _operator2tensor(cls, operator, partition: List[int], system_input: List[boo n = len(partition) order = cls._order_operator2tensor(n) try: + if isinstance(operator, tf.Tensor): + return ( + operator.reshape(list(partition) * 2) + .permute(order) + .reshape([dim**2 for dim in partition]) + ) return ( operator.reshape(list(partition) * 2) .transpose(order) @@ -196,7 +203,10 @@ def operator(self, backend=None, full=False): n = len(self.partition) order = self._order_tensor2operator(n) - operator = tensor.reshape(np.repeat(self.partition, 2)).transpose(order) + if backend.__class__.__name__ == "TensorflowBackend": + opertor = tensor.reshape(np.repeat(self.partition, 2)).permute(order) + else: + operator = tensor.reshape(np.repeat(self.partition, 2)).transpose(order) return backend.cast(operator, dtype=self._tensor.dtype) From 779b2ff81ffba6e9e9bc7c779c138d2d47609834 Mon Sep 17 00:00:00 2001 From: Canoming Date: Thu, 18 Apr 2024 21:03:26 +0800 Subject: [PATCH 19/67] try to fix tensorflow operations --- src/qibo/quantum_info/quantum_networks.py | 32 ++++++++++------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 252dd87c67..8324d25a67 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -7,7 +7,6 @@ from typing import List, Optional, Tuple, Union import numpy as np -import tensorflow as tf from qibo.backends import _check_backend from qibo.config import raise_error @@ -84,17 +83,16 @@ def _operator2tensor(cls, operator, partition: List[int], system_input: List[boo n = len(partition) order = cls._order_operator2tensor(n) try: - if isinstance(operator, tf.Tensor): - return ( - operator.reshape(list(partition) * 2) - .permute(order) - .reshape([dim**2 for dim in partition]) - ) return ( operator.reshape(list(partition) * 2) .transpose(order) .reshape([dim**2 for dim in partition]) ) + # return ( + # operator.reshape(list(partition) * 2) + # .permute(order) + # .reshape([dim**2 for dim in partition]) + # ) except: raise_error( ValueError, @@ -203,10 +201,7 @@ def operator(self, backend=None, full=False): n = len(self.partition) order = self._order_tensor2operator(n) - if backend.__class__.__name__ == "TensorflowBackend": - opertor = tensor.reshape(np.repeat(self.partition, 2)).permute(order) - else: - operator = tensor.reshape(np.repeat(self.partition, 2)).transpose(order) + operator = tensor.reshape(np.repeat(self.partition, 2)).transpose(order) return backend.cast(operator, dtype=self._tensor.dtype) @@ -600,6 +595,7 @@ def _set_parameters(self): self.system_input = self._check_system_input(self.system_input, self.partition) self._einsum = self._backend.np.einsum + self._tensordot = self._backend.np.tensordot try: if self._pure: @@ -627,7 +623,7 @@ def full(self, backend=None, update=False): if self.is_pure(): """Reshapes input matrix based on purity.""" tensor.reshape([self.dims]) - tensor = np.tensordot(tensor, np.conj(tensor), axes=0) + tensor = self._tensordot(tensor, np.conj(tensor), axes=0) tensor = self._operator2tensor(tensor, self.partition, self.system_input) if update: @@ -715,9 +711,9 @@ def is_causal( dim_out = self.partition[-1] dim_in = self.partition[-2] - reduced = np.tensordot(self.full(), trace(dim_out).full(), axes=1) - sub_comb = np.tensordot(reduced, trace(dim_in).full(), axes=1) - expected = np.tensordot(sub_comb, trace(dim_in).full() / dim_in, axes=0) + reduced = self._tensordot(self.full(), trace(dim_out).full(), axes=1) + sub_comb = self._tensordot(reduced, trace(dim_in).full(), axes=1) + expected = self._tensordot(sub_comb, trace(dim_in).full() / dim_in, axes=0) norm = self._backend.calculate_norm(reduced - expected, order=order) @@ -804,9 +800,9 @@ def is_unital( if order is None and self._backend.__class__.__name__ == "TensorflowBackend": order = "euclidean" - reduced = np.tensordot(self.full(), trace(self.partition[1]).full(), axes=1) - sub_comb = np.tensordot(reduced, trace(self.partition[0]).full(), axes=1) - expected = np.tensordot( + reduced = self._tensordot(self.full(), trace(self.partition[1]).full(), axes=1) + sub_comb = self._tensordot(reduced, trace(self.partition[0]).full(), axes=1) + expected = self._tensordot( sub_comb, trace(self.partition[0]).full() / self.partition[0], axes=0 ) From c9b41f05f76bf8e7264a1b76798003ab40cfd965 Mon Sep 17 00:00:00 2001 From: Canoming Date: Thu, 18 Apr 2024 21:48:33 +0800 Subject: [PATCH 20/67] fix the return of the `norm` in `tensorflow` cannot be compared with float --- src/qibo/quantum_info/quantum_networks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 8324d25a67..83c8e4d7eb 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -717,7 +717,7 @@ def is_causal( norm = self._backend.calculate_norm(reduced - expected, order=order) - if norm > precision_tol: + if float(norm) > precision_tol: return False elif len(self.partition) == 2: return True @@ -807,7 +807,7 @@ def is_unital( ) norm = self._backend.calculate_norm((reduced - expected), order=order) - if norm > precision_tol: + if float(norm) > precision_tol: return False elif len(self.partition) == 2: return True From 5f62d1983636a419a0a5acd7c64a5d3518498372 Mon Sep 17 00:00:00 2001 From: Canoming Date: Thu, 18 Apr 2024 22:06:33 +0800 Subject: [PATCH 21/67] rearrange `try` `except` block for better err log readability --- src/qibo/quantum_info/quantum_networks.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 83c8e4d7eb..a100eef718 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -82,17 +82,11 @@ def _order_operator2tensor(n: int): def _operator2tensor(cls, operator, partition: List[int], system_input: List[bool]): n = len(partition) order = cls._order_operator2tensor(n) + + tensor = operator.reshape(list(partition) * 2).transpose(order) + try: - return ( - operator.reshape(list(partition) * 2) - .transpose(order) - .reshape([dim**2 for dim in partition]) - ) - # return ( - # operator.reshape(list(partition) * 2) - # .permute(order) - # .reshape([dim**2 for dim in partition]) - # ) + return tensor.reshape([dim**2 for dim in partition]) except: raise_error( ValueError, From 908b753f7f1213c87414469f8f152ee27d662151 Mon Sep 17 00:00:00 2001 From: Canoming Date: Fri, 19 Apr 2024 20:32:15 +0800 Subject: [PATCH 22/67] try to resolve `transpose` by convert tensor to `numpy` --- src/qibo/quantum_info/quantum_networks.py | 8 ++++---- tests/test_quantum_info_quantum_networks.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index a100eef718..ad82eb468e 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -83,7 +83,7 @@ def _operator2tensor(cls, operator, partition: List[int], system_input: List[boo n = len(partition) order = cls._order_operator2tensor(n) - tensor = operator.reshape(list(partition) * 2).transpose(order) + tensor = np.transpose(operator.reshape(list(partition) * 2), order) try: return tensor.reshape([dim**2 for dim in partition]) @@ -195,7 +195,7 @@ def operator(self, backend=None, full=False): n = len(self.partition) order = self._order_tensor2operator(n) - operator = tensor.reshape(np.repeat(self.partition, 2)).transpose(order) + operator = np.transpose(tensor.reshape(np.repeat(self.partition, 2)), order) return backend.cast(operator, dtype=self._tensor.dtype) @@ -862,9 +862,9 @@ def apply(self, state): operator = np.copy(self.operator()) if self.is_pure(): - return self._einsum("kj,ml,jl -> km", operator, np.conj(operator), state) + return self._einsum("ij,lk,il", operator, np.conj(operator), state) - return self._einsum("jklm, km", operator, state) + return self._einsum("ijkl, jl", operator, state) class StochQuantumNetwork: diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index a59317053d..cb8e4ed762 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -215,7 +215,7 @@ def test_with_states(backend): state_output = channel.apply_density_matrix(backend, state, nqubits) state_output_network = network_channel.apply(state) - state_output_link = network_state.link_product("ij,kj -> ik", network_channel) + state_output_link = network_state.link_product("ij,jk -> ik", network_channel) backend.assert_allclose(state_output_network, state_output) backend.assert_allclose(state_output_link.matrix(backend=backend), state_output) @@ -292,7 +292,7 @@ def test_apply(backend): state = random_density_matrix(dims, backend=backend) unitary = random_unitary(dims, backend=backend) network = QuantumChannel.from_nparray( - unitary, (dims, dims), pure=True, backend=backend + unitary, (dims, dims), pure=True, backend=backend, inverse=True ) applied = network.apply(state) From a1fcc408a4dd2c97e338ccca25655946ba099dab Mon Sep 17 00:00:00 2001 From: Canoming Date: Fri, 19 Apr 2024 21:14:01 +0800 Subject: [PATCH 23/67] cast the argument `shape` in `reshape` to `tuple` for `pytorch` backend --- src/qibo/quantum_info/quantum_networks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index ad82eb468e..70d00a2ea7 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -195,7 +195,9 @@ def operator(self, backend=None, full=False): n = len(self.partition) order = self._order_tensor2operator(n) - operator = np.transpose(tensor.reshape(np.repeat(self.partition, 2)), order) + operator = np.transpose( + tensor.reshape(tuple(np.repeat(self.partition, 2))), order + ) return backend.cast(operator, dtype=self._tensor.dtype) @@ -616,7 +618,7 @@ def full(self, backend=None, update=False): if self.is_pure(): """Reshapes input matrix based on purity.""" - tensor.reshape([self.dims]) + tensor.reshape(self.dims) tensor = self._tensordot(tensor, np.conj(tensor), axes=0) tensor = self._operator2tensor(tensor, self.partition, self.system_input) From beddc35e8ece94a89bf0b54d6c8dfc149bd2bc73 Mon Sep 17 00:00:00 2001 From: Canoming Date: Fri, 19 Apr 2024 22:59:20 +0800 Subject: [PATCH 24/67] fix: the argument names in `pytorch` is different from `numpy` --- src/qibo/quantum_info/quantum_networks.py | 56 +++++++++++++++++---- tests/test_quantum_info_quantum_networks.py | 14 +++--- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 70d00a2ea7..f62c4df832 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -10,6 +10,7 @@ from qibo.backends import _check_backend from qibo.config import raise_error +from tests.conftest import backend class QuantumNetwork: @@ -615,11 +616,16 @@ def full(self, backend=None, update=False): if backend is None: # pragma: no cover backend = self._backend tensor = 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) - tensor = self._tensordot(tensor, np.conj(tensor), axes=0) + 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._operator2tensor(tensor, self.partition, self.system_input) if update: @@ -704,12 +710,26 @@ def is_causal( 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] - reduced = self._tensordot(self.full(), trace(dim_out).full(), axes=1) - sub_comb = self._tensordot(reduced, trace(dim_in).full(), axes=1) - expected = self._tensordot(sub_comb, trace(dim_in).full() / dim_in, axes=0) + trace_out = backend.cast( + trace(dim_out, backend=backend).full(), dtype=self._tensor.dtype + ) + trace_in = backend.cast( + trace(dim_in, backend=backend).full(), dtype=self._tensor.dtype + ) + + if self._backend.__class__.__name__ == "PyTorchBackend": + reduced = self._tensordot(self.full(), trace_out, dims=1) + sub_comb = self._tensordot(reduced, trace_in, dims=1) + expected = self._tensordot(sub_comb, trace_in / dim_in, dims=0) + else: + reduced = self._tensordot(self.full(), trace_out, axes=1) + sub_comb = self._tensordot(reduced, trace_in, axes=1) + expected = self._tensordot(sub_comb, trace_in / dim_in, axes=0) norm = self._backend.calculate_norm(reduced - expected, order=order) @@ -796,11 +816,26 @@ def is_unital( if order is None and self._backend.__class__.__name__ == "TensorflowBackend": order = "euclidean" - reduced = self._tensordot(self.full(), trace(self.partition[1]).full(), axes=1) - sub_comb = self._tensordot(reduced, trace(self.partition[0]).full(), axes=1) - expected = self._tensordot( - sub_comb, trace(self.partition[0]).full() / self.partition[0], axes=0 + backend = self._backend + + dim_out = self.partition[-1] + dim_in = self.partition[-2] + + trace_out = backend.cast( + trace(dim_out, backend=backend).full(), dtype=self._tensor.dtype ) + trace_in = backend.cast( + trace(dim_in, backend=backend).full(), dtype=self._tensor.dtype + ) + + if self._backend.__class__.__name__ == "PyTorchBackend": + reduced = self._tensordot(self.full(), trace_in, dims=0) + sub_comb = self._tensordot(reduced, trace_out, dims=0) + expected = self._tensordot(sub_comb, trace_out / dim_out, dims=1) + else: + reduced = self._tensordot(self.full(), trace_in, axes=0) + sub_comb = self._tensordot(reduced, trace_out, axes=0) + expected = self._tensordot(sub_comb, trace_out / dim_out, axes=1) norm = self._backend.calculate_norm((reduced - expected), order=order) if float(norm) > precision_tol: @@ -861,10 +896,11 @@ def apply(self, state): Returns: ndarray: Resulting state :math:`\\mathcal{E}(\\varrho)`. """ - operator = np.copy(self.operator()) + operator = self.copy().operator() + conj = self._backend.np.conj if self.is_pure(): - return self._einsum("ij,lk,il", operator, np.conj(operator), state) + return self._einsum("ij,lk,il", operator, conj(operator), state) return self._einsum("ijkl, jl", operator, state) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index cb8e4ed762..449fdfbad0 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -311,9 +311,7 @@ def test_non_hermitian_and_prints(backend): ) assert not network.is_hermitian() - # assert not network.is_causal() assert not network.is_positive_semidefinite() - # assert not network.is_channel() assert network.__str__() == "J[┍4┑, ┕4┙]" @@ -336,12 +334,16 @@ def test_uility_func(): def test_predefined(backend): - id = identity(2) - tr = trace(2) + id = identity(2, backend=backend) + id_mat = id.matrix() + tr = trace(2, backend=backend) backend.assert_allclose( - id.matrix(backend=backend), - np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [1, 0, 0, 1]]), + id_mat, + backend.cast( + np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [1, 0, 0, 1]]), + dtype=id_mat.dtype, + ), atol=1e-8, ) From d77bcca400b2e033d5a8f4f30f110d59f6e8cfa3 Mon Sep 17 00:00:00 2001 From: Canoming Date: Fri, 19 Apr 2024 23:05:24 +0800 Subject: [PATCH 25/67] delete unexpected auto import --- src/qibo/quantum_info/quantum_networks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index f62c4df832..f8eff32093 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -10,7 +10,6 @@ from qibo.backends import _check_backend from qibo.config import raise_error -from tests.conftest import backend class QuantumNetwork: From c7de13f7360e6eff68e7484d36ee11d74a4d015a Mon Sep 17 00:00:00 2001 From: Canoming Date: Fri, 19 Apr 2024 23:54:42 +0800 Subject: [PATCH 26/67] update doc for quantum comb and quantum channel --- src/qibo/quantum_info/quantum_networks.py | 28 ++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index f8eff32093..55f5162594 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -636,7 +636,10 @@ def full(self, backend=None, update=False): class QuantumComb(QuantumNetwork): """Quantum comb is a quantum network such that the systems follows a sequential order. - It is also called the 'non-Markovian quantum process' in many literatures. + It is also called the *non-Markovian quantum process* in many literatures. + Specifically, a quantum comb is a quantum network of the form :math:`J[┍i1┑,┕o1┙,┍i2┑,┕o2┙, ...]`, + where the 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. @@ -755,6 +758,29 @@ def from_nparray( class QuantumChannel(QuantumComb): + """Quantum channel is a special case of quantum comb, where there are only one input + and one output. + This class includes all quantum chnanels, including unitary operators, quantum states, etc. + 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]): 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 :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. + """ def __init__( self, From bf881caf63ff42c5aa86faaf50c5c048da3f1394 Mon Sep 17 00:00:00 2001 From: Canoming Date: Sun, 21 Apr 2024 18:30:29 +0800 Subject: [PATCH 27/67] increase test coverage --- src/qibo/quantum_info/quantum_networks.py | 7 +----- tests/test_quantum_info_quantum_networks.py | 27 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 55f5162594..dea2c05875 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -61,12 +61,7 @@ def __init__( self._set_parameters() - if len(self.partition) > 0: - self.dims = reduce( - mul, self.partition - ) # should be after `_set_parameters` to ensure `self.partition` is not `None` - else: - self.dims = 1 + self.dims = reduce(mul, self.partition) @staticmethod def _order_tensor2operator(n: int): diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 449fdfbad0..f1f3f98b0e 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -126,6 +126,16 @@ def test_errors(backend): with pytest.raises(ValueError): QuantumNetwork(matrix, (1, 2), backend=backend) + with pytest.raises(ValueError): + QuantumNetwork.from_nparray(matrix, (1, 2), pure=True, backend=backend) + + vec = np.random.rand(4) + with pytest.raises(ValueError): + QuantumNetwork.from_nparray(vec, backend=backend) + + with pytest.raises(ValueError): + QuantumComb.from_nparray(vec, pure=True, backend=backend) + def test_operational_logic(backend): lamb = float(np.random.rand()) @@ -169,6 +179,11 @@ def test_operational_logic(backend): (network_unitary / 2).operator(backend), unitary / np.sqrt(2), atol=1e-5 ) + # Complex conjugate of a network has to match the complex conjugate of the operator + backend.assert_allclose( + network.conj().operator(backend), backend.np.conj(network.operator(backend)) + ) + def test_parameters(backend): lamb = float(np.random.rand()) @@ -261,6 +276,11 @@ def test_with_unitaries(backend, subscript): backend.assert_allclose(test, (network_2 @ network_1).full(backend=backend)) + # Check properties for pure states + assert network_1.is_causal() + assert network_1.is_hermitian() + assert network_1.is_positive_semidefinite() + def test_with_comb(backend): subscript = "jklm,kl->jm" @@ -352,3 +372,10 @@ def test_predefined(backend): backend.assert_allclose( tr.matrix(backend=backend), traced.matrix(backend=backend), atol=1e-8 ) + + +def test_default_construction(backend): + vec = np.random.rand(4) + QuantumNetwork.from_nparray(vec, pure=True, backend=backend) + QuantumComb.from_nparray(vec, (4, 1), pure=True, backend=backend) + QuantumChannel.from_nparray(vec, pure=True, backend=backend) From 0792262f2ae62515d9dcf29c02c65e2d39c2bde2 Mon Sep 17 00:00:00 2001 From: Canoming Date: Sun, 21 Apr 2024 20:45:48 +0800 Subject: [PATCH 28/67] more tests --- src/qibo/quantum_info/quantum_networks.py | 14 ++++--- tests/test_quantum_info_quantum_networks.py | 42 ++++++++++++++++++--- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index dea2c05875..da678425a4 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -61,7 +61,10 @@ def __init__( self._set_parameters() - self.dims = reduce(mul, self.partition) + if len(self.partition) > 0: + self.dims = reduce(mul, self.partition) + else: + self.dims = 1 @staticmethod def _order_tensor2operator(n: int): @@ -74,13 +77,12 @@ def _order_operator2tensor(n: int): return order @classmethod - def _operator2tensor(cls, operator, partition: List[int], system_input: List[bool]): + def _operator2tensor(cls, operator, partition: List[int]): n = len(partition) order = cls._order_operator2tensor(n) - tensor = np.transpose(operator.reshape(list(partition) * 2), order) - try: + tensor = np.transpose(operator.reshape(list(partition) * 2), order) return tensor.reshape([dim**2 for dim in partition]) except: raise_error( @@ -150,7 +152,7 @@ def from_nparray( if partition is None: partition = arr.shape[: len_sys // 2] - tensor = cls._operator2tensor(arr, partition, system_input) + tensor = cls._operator2tensor(arr, partition) return cls( tensor, @@ -620,7 +622,7 @@ def full(self, backend=None, update=False): tensor = self._tensordot(tensor, conj(tensor), dims=0) else: tensor = self._tensordot(tensor, conj(tensor), axes=0) - tensor = self._operator2tensor(tensor, self.partition, self.system_input) + tensor = self._operator2tensor(tensor, self.partition) if update: self._tensor = tensor diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index f1f3f98b0e..8779584b6b 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -136,6 +136,23 @@ def test_errors(backend): with pytest.raises(ValueError): QuantumComb.from_nparray(vec, pure=True, backend=backend) + with pytest.raises(TypeError): + link_product(1, quantum_comb) + + with pytest.raises(TypeError): + link_product("ij, i", quantum_comb, matrix) + + # raise warning + link_product("ii", quantum_channel) + link_product("ij, kj", network_state, quantum_channel) + link_product("ij, jj", network_state, quantum_channel) + + +def test_class_methods(backend): + matrix = random_density_matrix(2**2, backend=backend) + with pytest.raises(ValueError): + QuantumNetwork._operator2tensor(matrix, (3,)) + def test_operational_logic(backend): lamb = float(np.random.rand()) @@ -265,10 +282,12 @@ def test_with_unitaries(backend, subscript): ) if subscript[1] == subscript[3]: - backend.assert_allclose(test, network_3.full(), atol=1e-8) + backend.assert_allclose( + test, network_3.full(backend=backend, update=True), atol=1e-8 + ) backend.assert_allclose( - test, (network_1 @ network_2).full(backend=backend), atol=1e-8 + test, (network_1 @ network_2).full(backend=backend, update=True), atol=1e-8 ) if subscript[0] == subscript[4]: @@ -375,7 +394,18 @@ def test_predefined(backend): def test_default_construction(backend): - vec = np.random.rand(4) - QuantumNetwork.from_nparray(vec, pure=True, backend=backend) - QuantumComb.from_nparray(vec, (4, 1), pure=True, backend=backend) - QuantumChannel.from_nparray(vec, pure=True, backend=backend) + vec = np.random.rand(4).reshape([4, 1]) + network = QuantumNetwork.from_nparray(vec, pure=True, backend=backend) + assert network.partition == (4, 1) + assert network.system_input == (True, False) + comb1 = QuantumComb.from_nparray(vec, (4, 1), pure=True, backend=backend) + assert comb1.system_input == (True, False) + comb2 = QuantumComb.from_nparray(vec, pure=True, backend=backend) + assert comb2.partition == (4, 1) + assert comb2.system_input == (True, False) + comb3 = QuantumComb(vec, system_input=(True, True), pure=True, backend=backend) + assert comb3.partition == (4, 1) + assert comb3.system_input == (True, False) + channel = QuantumChannel.from_nparray(vec, pure=True, backend=backend) + assert channel.partition == (4, 1) + assert channel.system_input == (True, False) From db58c297cc35496727f9dc038240c2db4cb35c28 Mon Sep 17 00:00:00 2001 From: Canoming Date: Sat, 27 Apr 2024 18:46:50 +0800 Subject: [PATCH 29/67] add test to cover deeper recursive steps in `is_causal` --- src/qibo/quantum_info/quantum_networks.py | 27 ++++++---- tests/test_quantum_info_quantum_networks.py | 60 ++++++++++++++++----- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index da678425a4..ae408b95ec 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -722,12 +722,12 @@ def is_causal( ) if self._backend.__class__.__name__ == "PyTorchBackend": - reduced = self._tensordot(self.full(), trace_out, dims=1) - sub_comb = self._tensordot(reduced, trace_in, dims=1) + 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) - sub_comb = self._tensordot(reduced, trace_in, axes=1) + 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_norm(reduced - expected, order=order) @@ -851,13 +851,20 @@ def is_unital( ) if self._backend.__class__.__name__ == "PyTorchBackend": - reduced = self._tensordot(self.full(), trace_in, dims=0) - sub_comb = self._tensordot(reduced, trace_out, dims=0) - expected = self._tensordot(sub_comb, trace_out / dim_out, dims=1) + 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) - sub_comb = self._tensordot(reduced, trace_out, axes=0) - expected = self._tensordot(sub_comb, trace_out / dim_out, axes=1) + 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_norm((reduced - expected), order=order) if float(norm) > precision_tol: diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 8779584b6b..5fa278de2e 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -210,14 +210,17 @@ def test_parameters(backend): dims = 2**nqubits partition = (dims, dims) - network = QuantumNetwork.from_nparray( - channel.to_choi(backend=backend), partition, backend=backend - ) - quantum_comb = QuantumComb.from_nparray( - channel.to_choi(backend=backend), partition, backend=backend - ) + choi = channel.to_choi(backend=backend) + + network = QuantumNetwork.from_nparray(choi, partition, backend=backend) + quantum_comb = QuantumComb.from_nparray(choi, partition, backend=backend) quantum_channel = QuantumChannel.from_nparray( - channel.to_choi(backend=backend), partition, backend=backend + choi, partition, backend=backend, inverse=True + ) + + rand = random_density_matrix(dims**2, backend=backend) + non_channel = QuantumChannel.from_nparray( + rand, partition, backend=backend, inverse=True ) backend.assert_allclose(network.operator(backend=backend).shape, (2, 2, 2, 2)) @@ -225,12 +228,16 @@ def test_parameters(backend): backend.assert_allclose(network.partition, partition) backend.assert_allclose(network.system_input, (True, False)) - assert quantum_comb.is_causal() - assert quantum_channel.is_unital() assert network.is_hermitian() assert network.is_positive_semidefinite() + assert quantum_comb.is_causal() + assert quantum_channel.is_unital() assert quantum_channel.is_channel() + # Test non-unital and non_causal + assert not non_channel.is_causal() + assert not non_channel.is_unital() + def test_with_states(backend): nqubits = 1 @@ -305,17 +312,41 @@ def test_with_comb(backend): subscript = "jklm,kl->jm" comb_partition = (2,) * 4 channel_partition = (2,) * 2 - comb_sys_out = (False, True) * 2 - channel_sys_out = (False, True) + comb_sys_in = (False, True) * 2 + channel_sys_in = (False, True) + + rand_choi = random_density_matrix(4**2, backend=backend) + unitary_1 = random_unitary(4, backend=backend) + non_channel = QuantumNetwork.from_nparray( + rand_choi, + (2, 2, 2, 2), + system_input=(True, True, False, False), + backend=backend, + ) + unitary_channel = QuantumNetwork.from_nparray( + unitary_1, + (2, 2, 2, 2), + system_input=(True, True, False, False), + pure=True, + backend=backend, + ) + + non_comb = link_product("ij kl, km on -> jl mn", non_channel, unitary_channel) + non_comb = QuantumComb( + non_comb.full(backend=backend), + (2, 2, 2, 2), + system_input=(True, True, False, False), + backend=backend, + ) comb = random_density_matrix(2**4, backend=backend) channel = random_density_matrix(2**2, backend=backend) comb_choi = QuantumNetwork.from_nparray( - comb, comb_partition, system_input=comb_sys_out, backend=backend + comb, comb_partition, system_input=comb_sys_in, backend=backend ) channel_choi = QuantumNetwork.from_nparray( - channel, channel_partition, system_input=channel_sys_out, backend=backend + channel, channel_partition, system_input=channel_sys_in, backend=backend ) test = comb_choi.link_product(subscript, channel_choi).full(backend, update=True) @@ -323,6 +354,9 @@ def test_with_comb(backend): backend.assert_allclose(test, channel_choi2.full(backend), atol=1e-5) + assert non_comb.is_hermitian() + assert not non_comb.is_causal() + def test_apply(backend): nqubits = 2 From c02f147e15667aa997a00b33e9f8d1e7eaad963b Mon Sep 17 00:00:00 2001 From: Canoming Date: Tue, 30 Apr 2024 11:10:22 +0800 Subject: [PATCH 30/67] increase test coverage --- src/qibo/quantum_info/quantum_networks.py | 33 +++++++---------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index ae408b95ec..23fb20b596 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -808,9 +808,10 @@ def __init__( 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. + """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` of the network is unital. + A map is unital if it preserves the identity operator. - Unitality is calculated as distance between the partial trace of :math:`\\mathcal{E}` + 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). @@ -840,8 +841,8 @@ def is_unital( backend = self._backend - dim_out = self.partition[-1] - dim_in = self.partition[-2] + dim_out = self.partition[1] + dim_in = self.partition[0] trace_out = backend.cast( trace(dim_out, backend=backend).full(), dtype=self._tensor.dtype @@ -855,10 +856,7 @@ def is_unital( sub_comb = self._tensordot( reduced, trace_out, - dims=( - [0], - [0], - ), + dims=([0], [0]), ) expected = self._tensordot(trace_out / dim_out, sub_comb, dims=0) else: @@ -871,21 +869,10 @@ def is_unital( return False elif len(self.partition) == 2: return True - - self._tensor = self.full() - self._pure = False - - partial_trace = self._einsum("jkjl -> kl", self._tensor) - 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 + else: + return QuantumChannel( + sub_comb, self.partition[2:], pure=False, backend=self._backend + ).is_unital(order, precision_tol) def is_channel( self, From f50bae6f5a4a266dac98ac7cec512d23c207140e Mon Sep 17 00:00:00 2001 From: Canoming Date: Tue, 14 May 2024 20:00:28 +0800 Subject: [PATCH 31/67] cover more tests --- src/qibo/quantum_info/quantum_networks.py | 22 +++++++++++------ tests/test_quantum_info_quantum_networks.py | 27 ++++++++++++++++++--- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 23fb20b596..e7cf338429 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -787,21 +787,26 @@ def __init__( pure: bool = False, backend=None, ): + if isinstance(partition, int): + partition = (partition,) + 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 or partition == None: + if len(partition) == 1: if system_input == None: # Assume the input is a quantum state - partition = (1, partition[0]) - elif len(system_input) == 1: - if system_input: - partition = (1, partition[0]) - else: + partition = (partition[0], 1) + else: + if isinstance(system_input, bool): + system_input = (system_input,) + + if system_input[0]: partition = (partition[0], 1) + else: + partition = (1, partition[0]) super().__init__(tensor, partition, pure=pure, backend=backend) @@ -870,7 +875,8 @@ def is_unital( elif len(self.partition) == 2: return True else: - return QuantumChannel( + # 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) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 5fa278de2e..94320c241e 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -136,6 +136,9 @@ def test_errors(backend): with pytest.raises(ValueError): QuantumComb.from_nparray(vec, pure=True, backend=backend) + with pytest.raises(ValueError): + QuantumChannel(matrix, partition=(2, 2, 2), pure=True, backend=backend) + with pytest.raises(TypeError): link_product(1, quantum_comb) @@ -146,6 +149,7 @@ def test_errors(backend): link_product("ii", quantum_channel) link_product("ij, kj", network_state, quantum_channel) link_product("ij, jj", network_state, quantum_channel) + link_product("ij, jj, jj", network_state, network_state, quantum_channel) def test_class_methods(backend): @@ -429,6 +433,7 @@ def test_predefined(backend): def test_default_construction(backend): vec = np.random.rand(4).reshape([4, 1]) + mat = np.random.rand(16).reshape([2, 2, 2, 2]) network = QuantumNetwork.from_nparray(vec, pure=True, backend=backend) assert network.partition == (4, 1) assert network.system_input == (True, False) @@ -437,9 +442,25 @@ def test_default_construction(backend): comb2 = QuantumComb.from_nparray(vec, pure=True, backend=backend) assert comb2.partition == (4, 1) assert comb2.system_input == (True, False) + comb3 = QuantumComb.from_nparray(mat, pure=False, backend=backend) + assert comb3.partition == (2, 2) + assert comb3.system_input == (True, False) comb3 = QuantumComb(vec, system_input=(True, True), pure=True, backend=backend) assert comb3.partition == (4, 1) assert comb3.system_input == (True, False) - channel = QuantumChannel.from_nparray(vec, pure=True, backend=backend) - assert channel.partition == (4, 1) - assert channel.system_input == (True, False) + channel1 = QuantumChannel.from_nparray(vec, pure=True, backend=backend) + assert channel1.partition == (4, 1) + assert channel1.system_input == (True, False) + channel2 = QuantumChannel( + vec, partition=4, system_input=True, pure=True, backend=backend + ) + assert channel2.partition == (4, 1) + assert channel2.system_input == (True, False) + channel3 = QuantumChannel(vec, partition=4, pure=True, backend=backend) + assert channel3.partition == (4, 1) + assert channel3.system_input == (True, False) + channel4 = QuantumChannel( + vec, partition=4, system_input=False, pure=True, backend=backend + ) + assert channel4.partition == (1, 4) + assert channel4.system_input == (True, False) From 95e27e389b528b1cd16810e80952a00b7e2dc78f Mon Sep 17 00:00:00 2001 From: Canoming Date: Tue, 14 May 2024 20:20:11 +0800 Subject: [PATCH 32/67] restore the correct default behavior of quantum channel --- src/qibo/quantum_info/quantum_networks.py | 2 +- tests/test_quantum_info_quantum_networks.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index e7cf338429..7e64e48693 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -798,7 +798,7 @@ def __init__( ) if len(partition) == 1: if system_input == None: # Assume the input is a quantum state - partition = (partition[0], 1) + partition = (1, partition[0]) else: if isinstance(system_input, bool): system_input = (system_input,) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 94320c241e..3f2293c7e1 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -149,7 +149,7 @@ def test_errors(backend): link_product("ii", quantum_channel) link_product("ij, kj", network_state, quantum_channel) link_product("ij, jj", network_state, quantum_channel) - link_product("ij, jj, jj", network_state, network_state, quantum_channel) + link_product("ij, jj, jj", network_state, quantum_channel, quantum_channel) def test_class_methods(backend): @@ -457,7 +457,7 @@ def test_default_construction(backend): assert channel2.partition == (4, 1) assert channel2.system_input == (True, False) channel3 = QuantumChannel(vec, partition=4, pure=True, backend=backend) - assert channel3.partition == (4, 1) + assert channel3.partition == (1, 4) assert channel3.system_input == (True, False) channel4 = QuantumChannel( vec, partition=4, system_input=False, pure=True, backend=backend From 9d8433231f8d2399d56a4f1d9a345f9c8d9b181b Mon Sep 17 00:00:00 2001 From: Canoming Date: Wed, 15 May 2024 00:26:39 +0800 Subject: [PATCH 33/67] remove unnecessary checks and cover the rest of lines --- src/qibo/quantum_info/quantum_networks.py | 10 ++------ tests/test_quantum_info_quantum_networks.py | 26 ++++++++++++++++++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 7e64e48693..77747b442f 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -998,24 +998,18 @@ def link_product( is_input = operands[inds[i]].system_input[index] if found > 2: warning( - f"Index {ind} is accores multiple times in the input subscripts {input_str}." + f"Index {ind} appears multiple times in the input subscripts {input_str}." ) except: continue - # check output systems + # set correct order of the `partition` and `system_input` partition = [] system_input = [] for ind in results_index: - found = False for i, script in enumerate(inputs): try: index = script.index(ind) - if found: - warning( - f"Index {ind} is repeated in the input subscripts {input_str}." - ) - found = True partition.append(operands[inds[i]].partition[index]) system_input.append(operands[inds[i]].system_input[index]) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 3f2293c7e1..a6590c2f37 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -321,6 +321,7 @@ def test_with_comb(backend): rand_choi = random_density_matrix(4**2, backend=backend) unitary_1 = random_unitary(4, backend=backend) + unitary_2 = random_unitary(4, backend=backend) non_channel = QuantumNetwork.from_nparray( rand_choi, (2, 2, 2, 2), @@ -334,12 +335,32 @@ def test_with_comb(backend): pure=True, backend=backend, ) + unitary_channel2 = QuantumNetwork.from_nparray( + unitary_2, + (2, 2, 2, 2), + system_input=(True, True, False, False), + pure=True, + backend=backend, + ) non_comb = link_product("ij kl, km on -> jl mn", non_channel, unitary_channel) non_comb = QuantumComb( non_comb.full(backend=backend), (2, 2, 2, 2), - system_input=(True, True, False, False), + system_input=(True, False, True, False), + backend=backend, + ) + two_comb = link_product( + "ij kl, km on, i, o", + unitary_channel, + unitary_channel2, + trace(2, backend=backend), + trace(2, backend=backend), + ) + two_comb = QuantumComb( + two_comb.full(backend=backend), + (2, 2, 2, 2), + system_input=(True, False, True, False), backend=backend, ) @@ -361,6 +382,9 @@ def test_with_comb(backend): assert non_comb.is_hermitian() assert not non_comb.is_causal() + assert two_comb.is_hermitian() + assert two_comb.is_causal() + def test_apply(backend): nqubits = 2 From 871ba16b4f28cec8f81c7bde4eec57211629d96d Mon Sep 17 00:00:00 2001 From: Canoming Date: Thu, 16 May 2024 15:28:59 +0800 Subject: [PATCH 34/67] more tests --- src/qibo/quantum_info/quantum_networks.py | 35 +++++++++++---------- tests/test_quantum_info_quantum_networks.py | 4 +++ 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 77747b442f..644f70ca3c 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -668,7 +668,7 @@ def __init__( if pure: partition = tensor.shape else: - partition = (int(np.sqrt(d)) for d in tensor.shape) + partition = tuple(int(np.sqrt(d)) for d in tensor.shape) if len(partition) % 2 != 0: raise_error( ValueError, @@ -790,23 +790,24 @@ def __init__( if isinstance(partition, int): partition = (partition,) - 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 == None: # Assume the input is a quantum state - partition = (1, partition[0]) - else: - if isinstance(system_input, bool): - system_input = (system_input,) - - if system_input[0]: - partition = (partition[0], 1) - else: + 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 == None: # Assume the input is a quantum state partition = (1, partition[0]) + else: + if isinstance(system_input, bool): + system_input = (system_input,) + + if system_input[0]: + partition = (partition[0], 1) + else: + partition = (1, partition[0]) super().__init__(tensor, partition, pure=pure, backend=backend) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index a6590c2f37..50bd268427 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -458,6 +458,7 @@ def test_predefined(backend): def test_default_construction(backend): vec = np.random.rand(4).reshape([4, 1]) mat = np.random.rand(16).reshape([2, 2, 2, 2]) + tensor = np.random.rand(16).reshape([4, 4]) network = QuantumNetwork.from_nparray(vec, pure=True, backend=backend) assert network.partition == (4, 1) assert network.system_input == (True, False) @@ -488,3 +489,6 @@ def test_default_construction(backend): ) assert channel4.partition == (1, 4) assert channel4.system_input == (True, False) + channel5 = QuantumChannel(tensor, pure=False, backend=backend) + assert channel5.partition == (2, 2) + assert channel5.system_input == (True, False) From a8174f6f7d1734e9e9fc43732e9ef6343e138d2d Mon Sep 17 00:00:00 2001 From: Canoming Date: Sun, 2 Jun 2024 16:36:59 +0800 Subject: [PATCH 35/67] Update according to (Alejandro's comments)[https://github.com/qiboteam/qibo/pull/1244#discussion_r1617400588] --- doc/source/api-reference/qibo.rst | 8 ++ .../tutorials/quantum_networks/README.md | 122 ------------------ src/qibo/quantum_info/quantum_networks.py | 12 +- tests/test_quantum_info_quantum_networks.py | 21 +-- 4 files changed, 31 insertions(+), 132 deletions(-) delete mode 100644 doc/source/code-examples/tutorials/quantum_networks/README.md diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index c6b44d2645..9a107c3c92 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -1903,6 +1903,14 @@ For more details, see G. Chiribella *et al.*, *Theoretical framework for quantum :members: :member-order: bysource +.. autoclass:: qibo.quantum_info.quantum_networks.QuantumComb + :members: + :member-order: bysource + +.. autoclass:: qibo.quantum_info.quantum_networks.QuantumChannel + :members: + :member-order: bysource + Random Ensembles ^^^^^^^^^^^^^^^^ diff --git a/doc/source/code-examples/tutorials/quantum_networks/README.md b/doc/source/code-examples/tutorials/quantum_networks/README.md deleted file mode 100644 index e0b074640c..0000000000 --- a/doc/source/code-examples/tutorials/quantum_networks/README.md +++ /dev/null @@ -1,122 +0,0 @@ -# Quantum Networks - -## The Quantum Network Model - -The quantum network model is a mathematical framework that allows us to uniquely describe quantum information processing that involves multiple points in time and space. -Each distinguished point in time and space is treated as a linear system $\mathcal{H}_ i$. -A quantum network involving $n$ points in time and space is a Hermitian operator $\mathcal{N}$ that acts on the tensor product of the linear systems $\mathcal{H}_ 0 \otimes \mathcal{H}_ 1 \otimes \cdots \otimes \mathcal{H}_ {n-1}$. -Each system $\mathcal{H}_ {i}$ is either an input or an output of the network. - -A physically implementable quantum network is described by a semi-positive definite operator $\mathcal{N}$ that satisfies the causal constraints. - -A simple example is a quantum channel $\Gamma: \mathcal{H}_ 0 \to \mathcal{H}_ 1$, where $\mathcal{H}_ 0$ is the input system and $\mathcal{H}_ 1$ is the output system. -The quantum channel is a linear map, such that it maps any input quantum state to an output quantum state, which is a sufficient and necessary condition for the map to be physical. -A Hermitian operator $J^\Gamma$ acting on $\mathcal{H}_ 0\otimes \mathcal{H}_ 1$ is associated with a quantum channel $\Gamma$, if $J^\Gamma$ satisfies the following conditions: -$$J^\Gamma \geq 0, \quad \text{and} \quad \text{Tr}_ {\mathcal{H}_ 1} J^\Gamma = \mathbb{I}_ {\mathcal{H} _0} .$$ - -The first condition is called *complete positivity*, and the second condition is called *trace-preserving*. -In particular, the second condition ensures that the information of the input system is only accessible through the output system. - -In particular, a quantum state $\rho$ may be also considered as a quantum network, where the input system is the trivial system $\mathbb{C}$, and the output system is the quantum system $\mathcal{H}$. -The constraints on the quantum channels are then equivalent to the constraints on the quantum states: -$$\rho \geq 0, \quad \text{and} \quad \text{Tr} \rho = \mathbb{I}_ \mathbb{C} = 1\ .$$ - -> For more details, see G. Chiribella *et al.*, *Theoretical framework for quantum networks*, -> [Physical Review A 80.2 (2009): 022339](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.80.022339). - -## Quantum Network in `qibo` - -The manipulation of quantum networks in `qibo` is done through the `QuantumNetwork` class. - -```python -from qibo.quantum_info.quantum_networks import QuantumNetwork -``` - -A quantum state is a quantum network with a single input system and a single output system, where the input system is the trivial 1-dimensional system. -We need to specify the dimensions of each system in the `partition` argument. - -```python -from qibo.quantum_info import random_density_matrix, random_unitary - -state = random_density_matrix(2) -state_choi = QuantumNetwork(state, (1,2)) -print(f'A quantum state is a quantum netowrk of the form {state_choi}') -``` - -``` ->>> A quantum state is a quantum netowrk of the form J[1 -> 2] -``` - -A general quantum channel can be created in a similar way. - -```python -from qibo.gates import DepolarizingChannel - -test_ch = DepolarizingChannel(0,0.5) -N = len(test_ch.target_qubits) -partition = (2**N, 2**N) - -depolar_choi = QuantumNetwork(test_ch.to_choi(), partition) -print(f'A quantum channel is a quantum netowrk of the form {depolar_choi}') -``` - -``` ->>> A quantum channel is a quantum netowrk of the form J[2 -> 2] -``` - -One may apply a quantum channel to a quantum state, or compose two quantum channels, using the `@` operator. - -```python -new_state = depolar_choi @ depolar_choi @ state_choi -``` - -## Example - -For 3-dimensional systems, an unital channel may not be a mixed unitary channel. - -> Example 4.3 in (Watrous, John. The theory of quantum information. Cambridge university press, 2018.) - -```python -A1 = np.array([ - [0,0,0], - [0,0,1/np.sqrt(2)], - [0,-1/np.sqrt(2),0], -]) -A2 = np.array([ - [0,0,1/np.sqrt(2)], - [0,0,0], - [-1/np.sqrt(2),0,0], -]) -A3 = np.array([ - [0,1/np.sqrt(2),0], - [-1/np.sqrt(2),0,0], - [0,0,0], -]) - -Choi1 = QuantumNetwork(A1, (3,3), pure=True) * 3 -Choi2 = QuantumNetwork(A2, (3,3), pure=True)*3 -Choi3 = QuantumNetwork(A3, (3,3), pure=True)*3 -``` - -The three channels are pure but not unital. Which means they are not unitary. - -```python -print(f"Choi1 is unital: {Choi1.unital()}") -print(f"Choi2 is unital: {Choi2.unital()}") -print(f"Choi3 is unital: {Choi3.unital()}") -``` - -``` ->>> Choi1 is unital: False -Choi2 is unital: False -Choi3 is unital: False -``` - -However, the mixture of the three operators is unital. -As the matrices are orthogonal, they are the extreme points of the convex set of the unital channels. -Therefore, this mixed channel is not a mixed unitary channel. - -```python -Choi = Choi1/3 + Choi2/3 + Choi3/3 -print(f"The mixed channel is unital: {Choi.unital()}") -``` diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 644f70ca3c..76f6b4c94f 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -635,7 +635,7 @@ class QuantumComb(QuantumNetwork): """Quantum comb is a quantum network such that the systems follows a sequential order. It is also called the *non-Markovian quantum process* in many literatures. Specifically, a quantum comb is a quantum network of the form :math:`J[┍i1┑,┕o1┙,┍i2┑,┕o2┙, ...]`, - where the the process first take an input state from system :math:`i1`, then output a state to system :math:`o1`, and so on. + 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 @@ -968,8 +968,16 @@ def link_product( if not isinstance(operand, QuantumNetwork): raise_error(TypeError, f"The {i}th operator is not a ``QuantumNetwork``.") + if backend is None: + backend = operands[0]._backend + tensors = [ - operand.full() if operand.is_pure() else operand._tensor for operand in operands + ( + backend.to_numpy(operand.full()) + if operand.is_pure() + else backend.to_numpy(operand._tensor) + ) + for operand in operands ] # keep track of the `partition` and `system_input` of the network diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 50bd268427..a2ad45a2f0 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -140,16 +140,18 @@ def test_errors(backend): QuantumChannel(matrix, partition=(2, 2, 2), pure=True, backend=backend) with pytest.raises(TypeError): - link_product(1, quantum_comb) + link_product(1, quantum_comb, backend=backend) with pytest.raises(TypeError): - link_product("ij, i", quantum_comb, matrix) + link_product("ij, i", quantum_comb, matrix, backend=backend) # raise warning - link_product("ii", quantum_channel) - link_product("ij, kj", network_state, quantum_channel) - link_product("ij, jj", network_state, quantum_channel) - link_product("ij, jj, jj", network_state, quantum_channel, quantum_channel) + link_product("ii", quantum_channel, backend=backend) + link_product("ij, kj", network_state, quantum_channel, backend=backend) + link_product("ij, jj", network_state, quantum_channel, backend=backend) + link_product( + "ij, jj, jj", network_state, quantum_channel, quantum_channel, backend=backend + ) def test_class_methods(backend): @@ -343,7 +345,9 @@ def test_with_comb(backend): backend=backend, ) - non_comb = link_product("ij kl, km on -> jl mn", non_channel, unitary_channel) + non_comb = link_product( + "ij kl, km on -> jl mn", non_channel, unitary_channel, backend=backend + ) non_comb = QuantumComb( non_comb.full(backend=backend), (2, 2, 2, 2), @@ -356,6 +360,7 @@ def test_with_comb(backend): unitary_channel2, trace(2, backend=backend), trace(2, backend=backend), + backend=backend, ) two_comb = QuantumComb( two_comb.full(backend=backend), @@ -448,7 +453,7 @@ def test_predefined(backend): atol=1e-8, ) - traced = link_product("ij,j", id, tr) + traced = link_product("ij,j", id, tr, backend=backend) backend.assert_allclose( tr.matrix(backend=backend), traced.matrix(backend=backend), atol=1e-8 From ef848607b1735389e22f7dc07e70900d6df29d63 Mon Sep 17 00:00:00 2001 From: Canoming <36161480+Canoming@users.noreply.github.com> Date: Thu, 6 Jun 2024 00:02:23 +0800 Subject: [PATCH 36/67] Update src/qibo/quantum_info/quantum_networks.py no cover Co-authored-by: Alejandro Sopena <44305203+AlejandroSopena@users.noreply.github.com> --- src/qibo/quantum_info/quantum_networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 76f6b4c94f..e92f459725 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -968,7 +968,7 @@ def link_product( if not isinstance(operand, QuantumNetwork): raise_error(TypeError, f"The {i}th operator is not a ``QuantumNetwork``.") - if backend is None: + if backend is None: # pragma: no cover backend = operands[0]._backend tensors = [ From d3798e1a95087d1e5e2a854e3c5cdac1fc1affca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:02:57 +0000 Subject: [PATCH 37/67] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/qibo/quantum_info/quantum_networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index e92f459725..bac07574bf 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -968,7 +968,7 @@ def link_product( if not isinstance(operand, QuantumNetwork): raise_error(TypeError, f"The {i}th operator is not a ``QuantumNetwork``.") - if backend is None: # pragma: no cover + if backend is None: # pragma: no cover backend = operands[0]._backend tensors = [ From 1aff6240663200c459dd4fd375c18c14be7ad148 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 19 Jun 2024 13:12:21 +0400 Subject: [PATCH 38/67] small spacing change --- doc/source/api-reference/qibo.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index a07e5d8170..cd7f5ba420 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -1906,14 +1906,17 @@ For more details, see G. Chiribella *et al.*, *Theoretical framework for quantum `Physical Review A 80.2 (2009): 022339 `_. + .. autoclass:: qibo.quantum_info.quantum_networks.QuantumNetwork :members: :member-order: bysource + .. autoclass:: qibo.quantum_info.quantum_networks.QuantumComb :members: :member-order: bysource + .. autoclass:: qibo.quantum_info.quantum_networks.QuantumChannel :members: :member-order: bysource From 8253485f28038b08792dde8e9b178a21555d4072 Mon Sep 17 00:00:00 2001 From: Canoming Date: Mon, 24 Jun 2024 04:27:07 +0800 Subject: [PATCH 39/67] update according to the comments from @renatomello --- src/qibo/quantum_info/quantum_networks.py | 90 ++++++++++----------- tests/test_quantum_info_quantum_networks.py | 77 +++++++++--------- 2 files changed, 80 insertions(+), 87 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index bac07574bf..8fb3b1f7e1 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -92,7 +92,7 @@ def _operator2tensor(cls, operator, partition: List[int]): ) @classmethod - def from_nparray( + def from_operator( cls, arr: np.ndarray, partition: Optional[Union[List[int], Tuple[int]]] = None, @@ -715,10 +715,10 @@ def is_causal( dim_in = self.partition[-2] trace_out = backend.cast( - trace(dim_out, backend=backend).full(), dtype=self._tensor.dtype + TraceOperation(dim_out, backend=backend).full(), dtype=self._tensor.dtype ) trace_in = backend.cast( - trace(dim_in, backend=backend).full(), dtype=self._tensor.dtype + TraceOperation(dim_in, backend=backend).full(), dtype=self._tensor.dtype ) if self._backend.__class__.__name__ == "PyTorchBackend": @@ -742,10 +742,10 @@ def is_causal( ).is_causal(order, precision_tol) @classmethod - def from_nparray( + def from_operator( cls, tensor, partition=None, pure=False, backend=None, inverse=False ): - comb = super().from_nparray(tensor, partition, None, pure, backend) + comb = super().from_operator(tensor, partition, None, pure, backend) if ( inverse ): # Convert mathmetical convention of Choi operator to physical convention @@ -851,10 +851,10 @@ def is_unital( dim_in = self.partition[0] trace_out = backend.cast( - trace(dim_out, backend=backend).full(), dtype=self._tensor.dtype + TraceOperation(dim_out, backend=backend).full(), dtype=self._tensor.dtype ) trace_in = backend.cast( - trace(dim_in, backend=backend).full(), dtype=self._tensor.dtype + TraceOperation(dim_in, backend=backend).full(), dtype=self._tensor.dtype ) if self._backend.__class__.__name__ == "PyTorchBackend": @@ -928,16 +928,11 @@ def apply(self, state): return self._einsum("ijkl, jl", operator, state) -class StochQuantumNetwork: - pass - - def link_product( subscripts: str = "ij,jk -> ik", *operands: QuantumNetwork, backend=None, surpress_warning=False, - casting: bool = True, ): """Link product between two quantum networks. @@ -985,7 +980,7 @@ def link_product( subscripts, *tensors, optimize=False, einsum_call=True ) - inds, idx_rm, einsum_str, remaining, blas = contracrtion_list[0] + inds, idx_rm, einsum_str, _, _ = contracrtion_list[0] input_str, results_index = einsum_str.split("->") inputs = input_str.split(",") @@ -994,45 +989,42 @@ def link_product( for ind in idx_rm: found = 0 for i, script in enumerate(inputs): - try: - index = script.index(ind) - found += 1 - if found > 1: - if is_input != operands[inds[i]].system_input[index]: - pass - else: - warning( - f"Index {ind} connects two {'input' if is_input else 'output'} systems." - ) - is_input = operands[inds[i]].system_input[index] - if found > 2: + index = script.find(ind) + if index < 0: + continue + found += 1 + if found > 1: + if is_input != operands[inds[i]].system_input[index]: + pass + else: warning( - f"Index {ind} appears multiple times in the input subscripts {input_str}." + f"Index {ind} connects two {'input' if is_input else 'output'} systems." ) - except: - continue + 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): - try: - index = script.index(ind) - - partition.append(operands[inds[i]].partition[index]) - system_input.append(operands[inds[i]].system_input[index]) - - except: + 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) -def identity(dim: int, backend=None): - """Returns the identity channel. +class IdentityChannel(QuantumChannel): + """The identity channel with the given dimension. Args: dim (int): Dimension of the identity operator. @@ -1041,21 +1033,21 @@ def identity(dim: int, backend=None): when initializing the :class:`qibo.quant """ - return QuantumChannel.from_nparray( - np.eye(dim), [dim, dim], pure=True, backend=backend - ) + def __init__(self, dim: int, backend=None): + + super().__init__(np.eye(dim), [dim, dim], pure=True, backend=backend) -def trace(dim: int, backend=None): - """Returns the trace operator. +class TraceOperation(QuantumNetwork): + """The trace operator with the given dimension. Args: - dim (int): Dimension to be traced. - backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used - to return the identity operator. If ``None``, defaults to the backend defined - when initializing the :class:`qibo.quant + dim (int): Dimension of the trace operator. + backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in + calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. """ - return QuantumNetwork.from_nparray( - np.eye(dim), [dim], [True], pure=False, backend=backend - ) + def __init__(self, dim: int, backend=None): + + super().__init__(np.eye(dim), [dim], [True], pure=False, backend=backend) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index a2ad45a2f0..6ac96d1e38 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -5,12 +5,12 @@ from qibo import gates from qibo.quantum_info.quantum_networks import ( + IdentityChannel, QuantumChannel, QuantumComb, QuantumNetwork, - identity, + TraceOperation, link_product, - trace, ) from qibo.quantum_info.random_ensembles import ( random_density_matrix, @@ -25,13 +25,13 @@ def test_errors(backend): nqubits = len(channel.target_qubits) dims = 2**nqubits partition = (dims, dims) - network = QuantumNetwork.from_nparray( + network = QuantumNetwork.from_operator( channel.to_choi(backend=backend), partition, backend=backend ) - quantum_comb = QuantumComb.from_nparray( + quantum_comb = QuantumComb.from_operator( channel.to_choi(backend=backend), partition, backend=backend ) - quantum_channel = QuantumChannel.from_nparray( + quantum_channel = QuantumChannel.from_operator( channel.to_choi(backend=backend), partition, backend=backend ) @@ -127,14 +127,14 @@ def test_errors(backend): QuantumNetwork(matrix, (1, 2), backend=backend) with pytest.raises(ValueError): - QuantumNetwork.from_nparray(matrix, (1, 2), pure=True, backend=backend) + QuantumNetwork.from_operator(matrix, (1, 2), pure=True, backend=backend) vec = np.random.rand(4) with pytest.raises(ValueError): - QuantumNetwork.from_nparray(vec, backend=backend) + QuantumNetwork.from_operator(vec, backend=backend) with pytest.raises(ValueError): - QuantumComb.from_nparray(vec, pure=True, backend=backend) + QuantumComb.from_operator(vec, pure=True, backend=backend) with pytest.raises(ValueError): QuantumChannel(matrix, partition=(2, 2, 2), pure=True, backend=backend) @@ -166,7 +166,7 @@ def test_operational_logic(backend): nqubits = len(channel.target_qubits) dims = 2**nqubits partition = (dims, dims) - network = QuantumNetwork.from_nparray( + network = QuantumNetwork.from_operator( channel.to_choi(backend=backend), partition, backend=backend ) @@ -218,14 +218,14 @@ def test_parameters(backend): choi = channel.to_choi(backend=backend) - network = QuantumNetwork.from_nparray(choi, partition, backend=backend) - quantum_comb = QuantumComb.from_nparray(choi, partition, backend=backend) - quantum_channel = QuantumChannel.from_nparray( + network = QuantumNetwork.from_operator(choi, partition, backend=backend) + quantum_comb = QuantumComb.from_operator(choi, partition, backend=backend) + quantum_channel = QuantumChannel.from_operator( choi, partition, backend=backend, inverse=True ) rand = random_density_matrix(dims**2, backend=backend) - non_channel = QuantumChannel.from_nparray( + non_channel = QuantumChannel.from_operator( rand, partition, backend=backend, inverse=True ) @@ -250,11 +250,11 @@ def test_with_states(backend): dims = 2**nqubits state = random_density_matrix(dims, backend=backend) - network_state = QuantumChannel.from_nparray(state, backend=backend) + network_state = QuantumChannel.from_operator(state, backend=backend) lamb = float(np.random.rand()) channel = gates.DepolarizingChannel(0, lamb) - network_channel = QuantumChannel.from_nparray( + network_channel = QuantumChannel.from_operator( channel.to_choi(backend=backend), (dims, dims), backend=backend, inverse=True ) @@ -277,16 +277,16 @@ def test_with_unitaries(backend, subscript): unitary_1 = random_unitary(dims, backend=backend) unitary_2 = random_unitary(dims, backend=backend) - network_1 = QuantumComb.from_nparray( + network_1 = QuantumComb.from_operator( unitary_1, (dims, dims), pure=True, backend=backend, inverse=True ) - network_2 = QuantumComb.from_nparray( + network_2 = QuantumComb.from_operator( unitary_2, (dims, dims), pure=True, backend=backend, inverse=True ) - network_3 = QuantumComb.from_nparray( + network_3 = QuantumComb.from_operator( unitary_2 @ unitary_1, (dims, dims), pure=True, backend=backend, inverse=True ) - network_4 = QuantumComb.from_nparray( + network_4 = QuantumComb.from_operator( unitary_1 @ unitary_2, (dims, dims), pure=True, backend=backend, inverse=True ) @@ -324,20 +324,20 @@ def test_with_comb(backend): rand_choi = random_density_matrix(4**2, backend=backend) unitary_1 = random_unitary(4, backend=backend) unitary_2 = random_unitary(4, backend=backend) - non_channel = QuantumNetwork.from_nparray( + non_channel = QuantumNetwork.from_operator( rand_choi, (2, 2, 2, 2), system_input=(True, True, False, False), backend=backend, ) - unitary_channel = QuantumNetwork.from_nparray( + unitary_channel = QuantumNetwork.from_operator( unitary_1, (2, 2, 2, 2), system_input=(True, True, False, False), pure=True, backend=backend, ) - unitary_channel2 = QuantumNetwork.from_nparray( + unitary_channel2 = QuantumNetwork.from_operator( unitary_2, (2, 2, 2, 2), system_input=(True, True, False, False), @@ -358,8 +358,8 @@ def test_with_comb(backend): "ij kl, km on, i, o", unitary_channel, unitary_channel2, - trace(2, backend=backend), - trace(2, backend=backend), + TraceOperation(2, backend=backend), + TraceOperation(2, backend=backend), backend=backend, ) two_comb = QuantumComb( @@ -372,10 +372,10 @@ def test_with_comb(backend): comb = random_density_matrix(2**4, backend=backend) channel = random_density_matrix(2**2, backend=backend) - comb_choi = QuantumNetwork.from_nparray( + comb_choi = QuantumNetwork.from_operator( comb, comb_partition, system_input=comb_sys_in, backend=backend ) - channel_choi = QuantumNetwork.from_nparray( + channel_choi = QuantumNetwork.from_operator( channel, channel_partition, system_input=channel_sys_in, backend=backend ) @@ -397,7 +397,7 @@ def test_apply(backend): state = random_density_matrix(dims, backend=backend) unitary = random_unitary(dims, backend=backend) - network = QuantumChannel.from_nparray( + network = QuantumChannel.from_operator( unitary, (dims, dims), pure=True, backend=backend, inverse=True ) @@ -412,7 +412,7 @@ def test_non_hermitian_and_prints(backend): dims = 2**nqubits matrix = random_gaussian_matrix(dims**2, backend=backend) - network = QuantumNetwork.from_nparray( + network = QuantumNetwork.from_operator( matrix, (dims, dims), pure=False, backend=backend ) @@ -440,9 +440,10 @@ def test_uility_func(): def test_predefined(backend): - id = identity(2, backend=backend) - id_mat = id.matrix() - tr = trace(2, backend=backend) + tr_ch = TraceOperation(2, backend=backend) + + id_ch = IdentityChannel(2, backend=backend) + id_mat = id_ch.matrix(backend=backend) backend.assert_allclose( id_mat, @@ -453,10 +454,10 @@ def test_predefined(backend): atol=1e-8, ) - traced = link_product("ij,j", id, tr, backend=backend) + traced = link_product("ij,j", id_ch, tr_ch, backend=backend) backend.assert_allclose( - tr.matrix(backend=backend), traced.matrix(backend=backend), atol=1e-8 + tr_ch.matrix(backend=backend), traced.matrix(backend=backend), atol=1e-8 ) @@ -464,21 +465,21 @@ def test_default_construction(backend): vec = np.random.rand(4).reshape([4, 1]) mat = np.random.rand(16).reshape([2, 2, 2, 2]) tensor = np.random.rand(16).reshape([4, 4]) - network = QuantumNetwork.from_nparray(vec, pure=True, backend=backend) + network = QuantumNetwork.from_operator(vec, pure=True, backend=backend) assert network.partition == (4, 1) assert network.system_input == (True, False) - comb1 = QuantumComb.from_nparray(vec, (4, 1), pure=True, backend=backend) + comb1 = QuantumComb.from_operator(vec, (4, 1), pure=True, backend=backend) assert comb1.system_input == (True, False) - comb2 = QuantumComb.from_nparray(vec, pure=True, backend=backend) + comb2 = QuantumComb.from_operator(vec, pure=True, backend=backend) assert comb2.partition == (4, 1) assert comb2.system_input == (True, False) - comb3 = QuantumComb.from_nparray(mat, pure=False, backend=backend) + comb3 = QuantumComb.from_operator(mat, pure=False, backend=backend) assert comb3.partition == (2, 2) assert comb3.system_input == (True, False) comb3 = QuantumComb(vec, system_input=(True, True), pure=True, backend=backend) assert comb3.partition == (4, 1) assert comb3.system_input == (True, False) - channel1 = QuantumChannel.from_nparray(vec, pure=True, backend=backend) + channel1 = QuantumChannel.from_operator(vec, pure=True, backend=backend) assert channel1.partition == (4, 1) assert channel1.system_input == (True, False) channel2 = QuantumChannel( From 4a2ee6469ff0ccf7c5190c4f3baa7416309d9a48 Mon Sep 17 00:00:00 2001 From: Canoming Date: Mon, 24 Jun 2024 04:34:09 +0800 Subject: [PATCH 40/67] complete docstring for `link_product` --- src/qibo/quantum_info/quantum_networks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 8fb3b1f7e1..49557fd579 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -947,6 +947,12 @@ def link_product( `numpy.einsum `_. 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 :class:`qibo.backends.GlobalBackend`. + 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 4f0e9da07b737c999eba667d67517f8bac066a92 Mon Sep 17 00:00:00 2001 From: Canoming Date: Mon, 24 Jun 2024 16:37:14 +0800 Subject: [PATCH 41/67] Set default dtype of predefined networks as `complex128` --- src/qibo/quantum_info/quantum_networks.py | 24 ++++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 49557fd579..b4b0825b80 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -714,12 +714,8 @@ def is_causal( dim_out = self.partition[-1] dim_in = self.partition[-2] - trace_out = backend.cast( - TraceOperation(dim_out, backend=backend).full(), dtype=self._tensor.dtype - ) - trace_in = backend.cast( - TraceOperation(dim_in, backend=backend).full(), dtype=self._tensor.dtype - ) + 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])) @@ -850,12 +846,8 @@ def is_unital( dim_out = self.partition[1] dim_in = self.partition[0] - trace_out = backend.cast( - TraceOperation(dim_out, backend=backend).full(), dtype=self._tensor.dtype - ) - trace_in = backend.cast( - TraceOperation(dim_in, backend=backend).full(), dtype=self._tensor.dtype - ) + 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])) @@ -1041,7 +1033,9 @@ class IdentityChannel(QuantumChannel): def __init__(self, dim: int, backend=None): - super().__init__(np.eye(dim), [dim, dim], pure=True, backend=backend) + super().__init__( + np.eye(dim, dtype=np.complex128), [dim, dim], pure=True, backend=backend + ) class TraceOperation(QuantumNetwork): @@ -1056,4 +1050,6 @@ class TraceOperation(QuantumNetwork): def __init__(self, dim: int, backend=None): - super().__init__(np.eye(dim), [dim], [True], pure=False, backend=backend) + super().__init__( + np.eye(dim, dtype=np.complex128), [dim], [True], pure=False, backend=backend + ) From 5384d232e872e3d6379bf9542c30f268ef7d70f1 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 27 Jun 2024 06:19:25 +0000 Subject: [PATCH 42/67] Update src/qibo/quantum_info/quantum_networks.py --- src/qibo/quantum_info/quantum_networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index b4b0825b80..8e27dfce07 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -959,7 +959,7 @@ def link_product( for i, operand in enumerate(operands): if not isinstance(operand, QuantumNetwork): - raise_error(TypeError, f"The {i}th operator is not a ``QuantumNetwork``.") + raise_error(TypeError, f"The {i}-th operator is not a ``QuantumNetwork``.") if backend is None: # pragma: no cover backend = operands[0]._backend From 8c227d1268eb89ccb289e51695e7b2f6d1fb0350 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 27 Jun 2024 10:39:42 +0400 Subject: [PATCH 43/67] lint improvements --- src/qibo/quantum_info/quantum_networks.py | 65 ++++++++++++++--------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 8e27dfce07..b6d7c0d1eb 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -100,9 +100,10 @@ def from_operator( pure: bool = False, backend=None, ): - """Construct a :class:`qibo.quantum_info.quantum_networks.QuantumNetwork` object from a numpy array. - This method converts a Choi operator to the internal representation of :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`. + """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: @@ -144,7 +145,8 @@ def from_operator( ): 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. " + "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 {arr.shape}. " + "If the input is pure, set `pure=True`.", ) @@ -220,7 +222,7 @@ def is_pure(self): 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}` of the network is Hermitian. + """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. @@ -230,7 +232,8 @@ def is_hermitian( parameter ``order`` for the ``tensorflow`` backend, please refer to `tensorflow.norm `_. For all other backends, please refer to - `numpy.linalg.norm `_. + `numpy.linalg.norm + `_. Args: order (str or int, optional): order of the norm. Defaults to ``None``. @@ -265,7 +268,7 @@ def is_hermitian( return float(norm) <= precision_tol def is_positive_semidefinite(self, precision_tol: float = 1e-8): - """Returns bool indicating if Choi operator :math:`\\mathcal{J}` of the network is positive-semidefinite. + """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 @@ -309,7 +312,8 @@ def link_product(self, subscripts: str, second_network): Args: subscripts (str, optional): Specifies the subscript for summation using the Einstein summation convention. For more details, please refer to - `numpy.einsum `_. + `numpy.einsum + `_. second_network (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum network to be applied to the original network. @@ -321,7 +325,7 @@ def link_product(self, subscripts: str, second_network): return link_product(subscripts, self, second_network, backend=self._backend) def copy(self): - """Returns a copy of the :class:`qibo.quantum_info.quantum_networks.QuantumNetwork` object.""" + """Returns a copy of the :class:`qibo.quantum_info.QuantumNetwork` object.""" return self.__class__( np.copy(self._tensor), partition=self.partition, @@ -600,7 +604,8 @@ def _set_parameters(self): raise_error( ValueError, "``partition`` does not match the shape of the input matrix. " - + f"Cannot reshape matrix of size {self._tensor.shape} to partition {self.partition}", + + f"Cannot reshape matrix of size {self._tensor.shape} " + + f"to partition {self.partition}", ) def full(self, backend=None, update=False): @@ -632,11 +637,14 @@ def full(self, backend=None, update=False): class QuantumComb(QuantumNetwork): - """Quantum comb is a quantum network such that the systems follows a sequential order. + """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. - Specifically, 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 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. @@ -685,11 +693,11 @@ def __init__( 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}` of the network satisfies the causal order condition. + """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. + 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``. @@ -751,13 +759,16 @@ def from_operator( class QuantumChannel(QuantumComb): - """Quantum channel is a special case of quantum comb, where there are only one input - and one output. - This class includes all quantum chnanels, including unitary operators, quantum states, etc. + """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`. + **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. @@ -810,9 +821,9 @@ def __init__( 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}` of the network is unital. - A map is unital if it preserves the identity operator. + """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). @@ -821,7 +832,8 @@ def is_unital( parameter ``order`` for the ``tensorflow`` backend, please refer to `tensorflow.norm `_. For all other backends, please refer to - `numpy.linalg.norm `_. + `numpy.linalg.norm + `_. Args: order (str or int, optional): order of the norm. Defaults to ``None``. @@ -868,7 +880,8 @@ def is_unital( elif len(self.partition) == 2: return True else: - # Unital is defined for quantum channels only. But we can extend it to quantum combs as follows: + # 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) From 66c4ee27b8ee9296c119da82574d37aced5786dd Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 27 Jun 2024 14:03:03 +0400 Subject: [PATCH 44/67] lint improvements --- src/qibo/quantum_info/quantum_networks.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index b6d7c0d1eb..550421b6fa 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -164,18 +164,19 @@ def from_operator( backend=backend, ) - def operator(self, backend=None, full=False): + 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``. - 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``. Returns: ndarray: Choi operator of the quantum network. @@ -608,7 +609,7 @@ def _set_parameters(self): + f"to partition {self.partition}", ) - def full(self, backend=None, update=False): + def full(self, update=False, backend=None): """Convert the internal representation to the full tensor of the network. Returns: From 2f1ded3961e6d135c5cb593b61718c4c3d8c985c Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 27 Jun 2024 14:12:43 +0400 Subject: [PATCH 45/67] improve docstring --- src/qibo/quantum_info/quantum_networks.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 550421b6fa..415efbc66b 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -772,11 +772,11 @@ class QuantumChannel(QuantumComb): 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]): 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. + 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``. From 99ce58e4b731376560c32f6dbd2362f03ced4f83 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Fri, 28 Jun 2024 08:50:14 +0400 Subject: [PATCH 46/67] reorder args --- src/qibo/quantum_info/quantum_networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 415efbc66b..c8e0fc16a6 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -214,7 +214,7 @@ def matrix(self, backend=None): Returns: ndarray: Choi operator of the quantum network. """ - return self.operator(backend, full=True).reshape((self.dims, self.dims)) + return self.operator(full=True, backend=backend).reshape((self.dims, self.dims)) def is_pure(self): """Returns bool indicading if the Choi operator of the network is pure.""" From a311ee8f02c2fd09b9543079ac3642c3118e7f29 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Fri, 28 Jun 2024 09:24:24 +0400 Subject: [PATCH 47/67] fix tests --- src/qibo/quantum_info/quantum_networks.py | 5 +--- tests/test_quantum_info_quantum_networks.py | 30 ++++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index c8e0fc16a6..0a615f169e 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -187,10 +187,7 @@ def operator(self, full: bool = False, backend=None): if self.is_pure() and not full: return backend.cast(self._tensor, dtype=self._tensor.dtype) - if self.is_pure(): - tensor = self.full(backend) - else: - tensor = self._tensor + tensor = self.full(backend) if self.is_pure() else self._tensor n = len(self.partition) order = self._order_tensor2operator(n) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 6ac96d1e38..8df14c9227 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -96,11 +96,11 @@ def test_errors(backend): # Multiplying QuantumNetwork with non-QuantumNetwork with pytest.raises(TypeError): - network @ network.operator(backend) + network @ network.operator(backend=backend) # Linking QuantumNetwork with non-QuantumNetwork with pytest.raises(TypeError): - network.link_product(network.operator(backend)) + network.link_product(network.operator(backend=backend)) with pytest.raises(TypeError): network.link_product(network, subscripts=True) @@ -175,36 +175,40 @@ def test_operational_logic(backend): # Sum with itself has to match multiplying by int backend.assert_allclose( - (network + network).operator(backend), (2 * network).operator(backend) + (network + network).operator(backend=backend), + (2 * network).operator(backend=backend), ) backend.assert_allclose( - (network_state_pure + network_state_pure).operator(backend), - (2 * network_state_pure).operator(backend, full=True), + (network_state_pure + network_state_pure).operator(backend=backend), + (2 * network_state_pure).operator(full=True, backend=backend), ) # Sum with itself has to match multiplying by float backend.assert_allclose( - (network + network).operator(backend), (2.0 * network).operator(backend) + (network + network).operator(backend=backend), + (2.0 * network).operator(backend=backend), ) backend.assert_allclose( - (network_state_pure + network_state_pure).operator(backend), - (2.0 * network_state_pure).operator(backend, full=True), + (network_state_pure + network_state_pure).operator(backend=backend), + (2.0 * network_state_pure).operator(full=True, backend=backend), ) # Multiplying and dividing by same scalar has to bring back to original network backend.assert_allclose( - ((2.0 * network) / 2).operator(backend), network.operator(backend) + ((2.0 * network) / 2).operator(backend=backend), + network.operator(backend=backend), ) unitary = random_unitary(dims, backend=backend) network_unitary = QuantumNetwork(unitary, (dims, dims), pure=True, backend=backend) backend.assert_allclose( - (network_unitary / 2).operator(backend), unitary / np.sqrt(2), atol=1e-5 + (network_unitary / 2).operator(backend=backend), unitary / np.sqrt(2), atol=1e-5 ) # Complex conjugate of a network has to match the complex conjugate of the operator backend.assert_allclose( - network.conj().operator(backend), backend.np.conj(network.operator(backend)) + network.conj().operator(backend=backend), + backend.np.conj(network.operator(backend=backend)), ) @@ -379,7 +383,9 @@ def test_with_comb(backend): channel, channel_partition, system_input=channel_sys_in, backend=backend ) - test = comb_choi.link_product(subscript, channel_choi).full(backend, update=True) + test = comb_choi.link_product(subscript, channel_choi).full( + update=True, backend=backend + ) channel_choi2 = comb_choi @ channel_choi backend.assert_allclose(test, channel_choi2.full(backend), atol=1e-5) From 604282ea945aa07ae7012434938960cae5094274 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Fri, 28 Jun 2024 09:54:21 +0400 Subject: [PATCH 48/67] docstring --- src/qibo/quantum_info/quantum_networks.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 0a615f169e..d09e627118 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -1033,13 +1033,13 @@ def link_product( class IdentityChannel(QuantumChannel): - """The identity channel with the given dimension. + """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 - to return the identity operator. If ``None``, defaults to the backend defined - when initializing the :class:`qibo.quant + dim (int): Dimension of the Identity operator. + 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, dim: int, backend=None): @@ -1050,10 +1050,10 @@ def __init__(self, dim: int, backend=None): class TraceOperation(QuantumNetwork): - """The trace operator with the given dimension. + """The Trace operator with the given dimension. Args: - dim (int): Dimension of the trace operator. + dim (int): Dimension of the Trace operator. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. From 93de8c2abcfcfd79a293854a407fbcdcb4b76653 Mon Sep 17 00:00:00 2001 From: Canoming Date: Thu, 4 Jul 2024 21:34:11 +0800 Subject: [PATCH 49/67] Resolve comments from @renatomello on Jun 28 --- src/qibo/quantum_info/quantum_networks.py | 129 ++++++++++++-------- tests/test_quantum_info_quantum_networks.py | 6 +- 2 files changed, 84 insertions(+), 51 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index d09e627118..8f90d42bce 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -51,40 +51,63 @@ def __init__( pure: bool = False, backend=None, ): - self._run_checks(partition, system_input, pure) - self._tensor = tensor - self.partition = partition + 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() - if len(self.partition) > 0: - self.dims = reduce(mul, self.partition) - else: - self.dims = 1 + self.dims = reduce(mul, self.partition) if len(self.partition) > 0 else 1 @staticmethod def _order_tensor2operator(n: int): + """ + Returns the order to reshape a tensor to an operator. + Given a tenosr of 2n leads, the order is [0, 2, 4, ..., 1, 3, 5, ...]. + """ order = list(range(0, n * 2, 2)) + list(range(1, n * 2, 2)) return order @staticmethod def _order_operator2tensor(n: int): + """ + Returns the order to reshape an operator to a tensor. + Given a operator of 2n systems, the order is [0, n, 1, n+1, 2, n+2, ...]. + """ order = list(sum(zip(list(range(0, n)), list(range(n, n * 2))), ())) return order @classmethod def _operator2tensor(cls, operator, partition: List[int]): + # check if tensorflow is installed + try: + from tensorflow import InvalidArgumentError + except ImportError: + InvalidArgumentError = ValueError + n = len(partition) order = cls._order_operator2tensor(n) try: tensor = np.transpose(operator.reshape(list(partition) * 2), order) return tensor.reshape([dim**2 for dim in partition]) - except: + except ValueError: + raise_error( + ValueError, + "``partition`` does not match the shape of the input matrix. " + + f"Cannot reshape matrix of size {operator.shape} to partition {partition}", + ) + except InvalidArgumentError: + raise_error( + ValueError, + "``partition`` does not match the shape of the input matrix. " + + f"Cannot reshape matrix of size {operator.shape} to partition {partition}", + ) + except RuntimeError: raise_error( ValueError, "``partition`` does not match the shape of the input matrix. " @@ -94,7 +117,7 @@ def _operator2tensor(cls, operator, partition: List[int]): @classmethod def from_operator( cls, - arr: np.ndarray, + operator, partition: Optional[Union[List[int], Tuple[int]]] = None, system_input: Optional[Union[List[bool], Tuple[bool]]] = None, pure: bool = False, @@ -126,35 +149,35 @@ def from_operator( if pure: if partition is None: - partition = arr.shape - tensor = arr + partition = tuple(operator.shape) + tensor = operator else: try: - tensor = arr.reshape(partition) + tensor = operator.reshape(partition) except: raise_error( ValueError, "``partition`` does not match the shape of the input matrix. " - + f"Cannot reshape matrix of size {arr.shape} to partition {partition}", + + f"Cannot reshape matrix of size {operator.shape} to partition {partition}", ) else: # check if arr is a valid choi operator - len_sys = len(arr.shape) + len_sys = len(operator.shape) if (len_sys % 2 != 0) or ( - arr.shape[: len_sys // 2] != arr.shape[len_sys // 2 :] + 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 {arr.shape}. " + + f"However, the shape of the input is {operator.shape}. " + "If the input is pure, set `pure=True`.", ) if partition is None: - partition = arr.shape[: len_sys // 2] + partition = operator.shape[: len_sys // 2] - tensor = cls._operator2tensor(arr, partition) + tensor = cls._operator2tensor(operator, partition) return cls( tensor, @@ -240,7 +263,9 @@ def is_hermitian( the norm given by ``order``. Defaults to :math:`10^{-8}`. Returns: - bool: Hermiticity condition. + 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( @@ -609,6 +634,12 @@ def _set_parameters(self): def full(self, update=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 :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. Returns: ndarray: The full reprentation of the quantum network. """ @@ -665,12 +696,11 @@ 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 == None: + if partition is None: if pure: partition = tensor.shape else: @@ -736,18 +766,19 @@ def is_causal( if float(norm) > precision_tol: return False - elif len(self.partition) == 2: + + if len(self.partition) == 2: return True - else: - return QuantumComb( - sub_comb, self.partition[:-2], pure=False, backend=self._backend - ).is_causal(order, precision_tol) + + return QuantumComb( + sub_comb, self.partition[:-2], pure=False, backend=self._backend + ).is_causal(order, precision_tol) @classmethod def from_operator( - cls, tensor, partition=None, pure=False, backend=None, inverse=False + cls, operator, partition=None, pure=False, backend=None, inverse=False ): - comb = super().from_operator(tensor, partition, None, pure, backend) + comb = super().from_operator(operator, partition, None, pure, backend) if ( inverse ): # Convert mathmetical convention of Choi operator to physical convention @@ -803,7 +834,7 @@ def __init__( + "For general quantum networks, one should use the ``QuantumNetwork`` class.", ) if len(partition) == 1: - if system_input == None: # Assume the input is a quantum state + if system_input is None: # Assume the input is a quantum state partition = (1, partition[0]) else: if isinstance(system_input, bool): @@ -875,14 +906,15 @@ def is_unital( norm = self._backend.calculate_norm((reduced - expected), order=order) if float(norm) > precision_tol: return False - elif len(self.partition) == 2: + + if len(self.partition) == 2: return True - else: - # 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) + + # 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) def is_channel( self, @@ -932,7 +964,7 @@ def apply(self, state): def link_product( - subscripts: str = "ij,jk -> ik", + subscripts: str, *operands: QuantumNetwork, backend=None, surpress_warning=False, @@ -1002,13 +1034,10 @@ def link_product( if index < 0: continue found += 1 - if found > 1: - if is_input != operands[inds[i]].system_input[index]: - pass - else: - warning( - f"Index {ind} connects two {'input' if is_input else 'output'} systems." - ) + 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( @@ -1044,9 +1073,9 @@ class IdentityChannel(QuantumChannel): def __init__(self, dim: int, backend=None): - super().__init__( - np.eye(dim, dtype=np.complex128), [dim, dim], pure=True, backend=backend - ) + 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): @@ -1061,6 +1090,6 @@ class TraceOperation(QuantumNetwork): def __init__(self, dim: int, backend=None): - super().__init__( - np.eye(dim, dtype=np.complex128), [dim], [True], pure=False, backend=backend - ) + identity = np.eye(dim, dtype=complex) + identity = backend.cast(identity, dtype=identity.dtype) + super().__init__(identity, [dim], [True], pure=False, backend=backend) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 8df14c9227..09113fdc7a 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -130,6 +130,7 @@ def test_errors(backend): QuantumNetwork.from_operator(matrix, (1, 2), pure=True, backend=backend) vec = np.random.rand(4) + vec = backend.cast(vec, dtype=vec.dtype) with pytest.raises(ValueError): QuantumNetwork.from_operator(vec, backend=backend) @@ -428,7 +429,7 @@ def test_non_hermitian_and_prints(backend): assert network.__str__() == "J[┍4┑, ┕4┙]" -def test_uility_func(): +def test_uility_function(): # _order_tensor2operator should convert # (a0,a1,b0,b1,...) to (a0,b0,..., a1,b1,...) old_shape = (0, 10, 1, 11, 2, 12, 3, 13) @@ -471,6 +472,9 @@ def test_default_construction(backend): vec = np.random.rand(4).reshape([4, 1]) mat = np.random.rand(16).reshape([2, 2, 2, 2]) tensor = np.random.rand(16).reshape([4, 4]) + vec = backend.cast(vec, dtype=vec.dtype) + mat = backend.cast(mat, dtype=mat.dtype) + tensor = backend.cast(tensor, dtype=tensor.dtype) network = QuantumNetwork.from_operator(vec, pure=True, backend=backend) assert network.partition == (4, 1) assert network.system_input == (True, False) From b9d56267b97707dc88eade00b52cc1c0789f9e23 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Fri, 5 Jul 2024 05:10:16 +0000 Subject: [PATCH 50/67] Update tests/test_quantum_info_quantum_networks.py --- tests/test_quantum_info_quantum_networks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 09113fdc7a..f981a8d9ef 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -131,6 +131,7 @@ def test_errors(backend): vec = np.random.rand(4) vec = backend.cast(vec, dtype=vec.dtype) + vec = backend.cast(vec, dtype=vec.dtype) with pytest.raises(ValueError): QuantumNetwork.from_operator(vec, backend=backend) From 6ccc0af36df50f5f2c6a5e716ee4e3c1399296f8 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Fri, 5 Jul 2024 05:11:21 +0000 Subject: [PATCH 51/67] Update tests/test_quantum_info_quantum_networks.py --- tests/test_quantum_info_quantum_networks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index f981a8d9ef..ad09fe88ec 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -476,6 +476,9 @@ def test_default_construction(backend): vec = backend.cast(vec, dtype=vec.dtype) mat = backend.cast(mat, dtype=mat.dtype) tensor = backend.cast(tensor, dtype=tensor.dtype) + vec = backend.cast(vec, dtype=vec.dtype) + mat = backend.cast(mat, dtype=mat.dtype) + tensor = backend.cast(tensor, dtype=tensor.dtype) network = QuantumNetwork.from_operator(vec, pure=True, backend=backend) assert network.partition == (4, 1) assert network.system_input == (True, False) From eb589514d2eceadaf7f0c5e4bf6f331145770c3b Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 17 Jul 2024 10:48:52 +0400 Subject: [PATCH 52/67] minor change --- src/qibo/quantum_info/quantum_networks.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 8f90d42bce..1a10541602 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -840,10 +840,9 @@ def __init__( if isinstance(system_input, bool): system_input = (system_input,) - if system_input[0]: - partition = (partition[0], 1) - else: - partition = (1, partition[0]) + partition = ( + (partition[0], 1) if system_input[0] else (1, partition[0]) + ) super().__init__(tensor, partition, pure=pure, backend=backend) From b2d74b4e290c7263576f55b73ca7c24bc7a10446 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 17 Jul 2024 10:53:49 +0400 Subject: [PATCH 53/67] required by `torch` --- src/qibo/quantum_info/quantum_networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 1a10541602..cf00ddb206 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -783,7 +783,7 @@ def from_operator( inverse ): # Convert mathmetical convention of Choi operator to physical convention comb.partition = comb.partition[::-1] - comb._tensor = np.transpose(comb._tensor) + comb._tensor = comb._tensor.T return comb From d8fea7e96e75c90b4ffc7fefa2bf43a56e9d5235 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 17 Jul 2024 11:03:28 +0400 Subject: [PATCH 54/67] minor docstring change --- src/qibo/quantum_info/quantum_networks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index cf00ddb206..a212e83e14 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -631,7 +631,7 @@ def _set_parameters(self): + f"to partition {self.partition}", ) - def full(self, update=False, backend=None): + def full(self, update: bool = False, backend=None): """Convert the internal representation to the full tensor of the network. Args: @@ -640,8 +640,9 @@ def full(self, update=False, backend=None): backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. + Returns: - ndarray: The full reprentation of the quantum network. + ndarray: full reprentation of the quantum network. """ if backend is None: # pragma: no cover backend = self._backend From c346601cfd41e6b2473e255d9a314946136ead48 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 17 Jul 2024 13:08:05 +0400 Subject: [PATCH 55/67] introduce `S` symbol --- src/qibo/symbols.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/qibo/symbols.py b/src/qibo/symbols.py index 9aa12a1dcc..8a76f1ec5b 100644 --- a/src/qibo/symbols.py +++ b/src/qibo/symbols.py @@ -161,3 +161,13 @@ class Z(PauliSymbol): """ pass + + +class S(PauliSymbol): + """Qibo symbol for the S operator. + + Args: + q (int): Target qubit id. + """ + + pass From da8c652f0b38e0764b40c097e513983c83d2bd01 Mon Sep 17 00:00:00 2001 From: Canoming <36161480+Canoming@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:01:38 +0800 Subject: [PATCH 56/67] Update src/qibo/quantum_info/quantum_networks.py Co-authored-by: Renato Mello --- src/qibo/quantum_info/quantum_networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index a212e83e14..cddea3cf91 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -82,7 +82,7 @@ def _order_operator2tensor(n: int): return order @classmethod - def _operator2tensor(cls, operator, partition: List[int]): + def _operator_to_tensor(cls, operator, partition: List[int]): # check if tensorflow is installed try: from tensorflow import InvalidArgumentError From afad5fa46551345c338fdb1ac42efd986fe0d0cf Mon Sep 17 00:00:00 2001 From: Canoming <36161480+Canoming@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:03:22 +0800 Subject: [PATCH 57/67] Update src/qibo/quantum_info/quantum_networks.py Co-authored-by: Renato Mello --- src/qibo/quantum_info/quantum_networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index cddea3cf91..d135d9d687 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -73,7 +73,7 @@ def _order_tensor2operator(n: int): return order @staticmethod - def _order_operator2tensor(n: int): + def _order_operator_to_tensor(n: int): """ Returns the order to reshape an operator to a tensor. Given a operator of 2n systems, the order is [0, n, 1, n+1, 2, n+2, ...]. From a09d0b1dd01b9e4e5b2854f60211f21dd52e75b9 Mon Sep 17 00:00:00 2001 From: Canoming <36161480+Canoming@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:04:04 +0800 Subject: [PATCH 58/67] Update src/qibo/quantum_info/quantum_networks.py Co-authored-by: Renato Mello --- src/qibo/quantum_info/quantum_networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index d135d9d687..2f5af063ce 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -64,7 +64,7 @@ def __init__( self.dims = reduce(mul, self.partition) if len(self.partition) > 0 else 1 @staticmethod - def _order_tensor2operator(n: int): + def _order_tensor_to_operator(n: int): """ Returns the order to reshape a tensor to an operator. Given a tenosr of 2n leads, the order is [0, 2, 4, ..., 1, 3, 5, ...]. From 3918df89d800e6873e0036312dc7005cfadbb58d Mon Sep 17 00:00:00 2001 From: Canoming Date: Wed, 17 Jul 2024 19:05:05 +0800 Subject: [PATCH 59/67] Resolve the remaining comments --- src/qibo/quantum_info/quantum_networks.py | 33 ++++++++------------- tests/test_quantum_info_quantum_networks.py | 6 ++-- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 2f5af063ce..8a597eb985 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -90,30 +90,21 @@ def _operator_to_tensor(cls, operator, partition: List[int]): InvalidArgumentError = ValueError n = len(partition) - order = cls._order_operator2tensor(n) + order = cls._order_operator_to_tensor(n) - try: - tensor = np.transpose(operator.reshape(list(partition) * 2), order) - return tensor.reshape([dim**2 for dim in partition]) - except ValueError: - raise_error( - ValueError, - "``partition`` does not match the shape of the input matrix. " - + f"Cannot reshape matrix of size {operator.shape} to partition {partition}", - ) - except InvalidArgumentError: - raise_error( - ValueError, - "``partition`` does not match the shape of the input matrix. " - + f"Cannot reshape matrix of size {operator.shape} to partition {partition}", - ) - except RuntimeError: + # 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}", ) + tensor = np.transpose(operator.reshape(list(partition) * 2), order) + return tensor.reshape([dim**2 for dim in partition]) + @classmethod def from_operator( cls, @@ -144,7 +135,7 @@ def from_operator( Defaults to ``None``. Returns: - :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: + :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: The quantum network constructed from the input Choi operator. """ if pure: @@ -177,7 +168,7 @@ def from_operator( if partition is None: partition = operator.shape[: len_sys // 2] - tensor = cls._operator2tensor(operator, partition) + tensor = cls._operator_to_tensor(operator, partition) return cls( tensor, @@ -213,7 +204,7 @@ def operator(self, full: bool = False, backend=None): tensor = self.full(backend) if self.is_pure() else self._tensor n = len(self.partition) - order = self._order_tensor2operator(n) + order = self._order_tensor_to_operator(n) operator = np.transpose( tensor.reshape(tuple(np.repeat(self.partition, 2))), order @@ -657,7 +648,7 @@ def full(self, update: bool = False, backend=None): tensor = self._tensordot(tensor, conj(tensor), dims=0) else: tensor = self._tensordot(tensor, conj(tensor), axes=0) - tensor = self._operator2tensor(tensor, self.partition) + tensor = self._operator_to_tensor(tensor, self.partition) if update: self._tensor = tensor diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index ad09fe88ec..adaff522fd 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -159,7 +159,7 @@ def test_errors(backend): def test_class_methods(backend): matrix = random_density_matrix(2**2, backend=backend) with pytest.raises(ValueError): - QuantumNetwork._operator2tensor(matrix, (3,)) + QuantumNetwork._operator_to_tensor(matrix, (3,)) def test_operational_logic(backend): @@ -437,8 +437,8 @@ def test_uility_function(): test_ls = np.ones(old_shape) n = len(test_ls.shape) // 2 - order2op = QuantumNetwork._order_tensor2operator(n) - order2tensor = QuantumNetwork._order_operator2tensor(n) + order2op = QuantumNetwork._order_tensor_to_operator(n) + order2tensor = QuantumNetwork._order_operator_to_tensor(n) new_shape = test_ls.transpose(order2op).shape for i in range(n): From 38451befdec81b8a6515fdb4f1d9495675bf91b6 Mon Sep 17 00:00:00 2001 From: Canoming Date: Sat, 20 Jul 2024 01:48:45 +0800 Subject: [PATCH 60/67] cover tests --- tests/test_quantum_info_quantum_networks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 3aeac001d2..daa8ab53f7 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -65,6 +65,9 @@ def test_errors(backend): with pytest.raises(TypeError): QuantumNetwork(channel.to_choi(backend=backend), partition=(1, 2), pure="True") + with pytest.raises(TypeError): + QuantumNetwork(channel.to_choi(backend=backend), partition=1, pure=True) + with pytest.raises(ValueError): network.is_hermitian(precision_tol=-1e-8) @@ -126,6 +129,9 @@ def test_errors(backend): with pytest.raises(ValueError): QuantumNetwork(matrix, (1, 2), backend=backend) + with pytest.raises(ValueError): + QuantumNetwork(matrix, (1, 1), pure=True, backend=backend) + with pytest.raises(ValueError): QuantumNetwork.from_operator(matrix, (1, 2), pure=True, backend=backend) From 1724570756675e6f83ca37dd71c56408bbaefebb Mon Sep 17 00:00:00 2001 From: Canoming Date: Sat, 20 Jul 2024 01:50:42 +0800 Subject: [PATCH 61/67] remove `Number` type dependency --- src/qibo/quantum_info/quantum_networks.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index bf08dd8a1e..eaebacd0cc 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -2,7 +2,6 @@ from functools import reduce from logging import warning -from numbers import Number from operator import mul from typing import List, Optional, Tuple, Union @@ -405,7 +404,7 @@ def __add__(self, second_network): backend=self._backend, ) - def __mul__(self, number: Union[float, int, Number]): + 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 @@ -420,7 +419,7 @@ def __mul__(self, number: Union[float, int, Number]): :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network with its Choi operator multiplied by ``number``. """ - if not isinstance(number, Number): + if not isinstance(number, Union[float, int]): raise_error( TypeError, "It is not possible to multiply a ``QuantumNetwork`` by a non-scalar.", @@ -445,11 +444,11 @@ def __mul__(self, number: Union[float, int, Number]): backend=self._backend, ) - def __rmul__(self, number: Union[float, int, Number]): + def __rmul__(self, number: Union[float, int]): """""" return self.__mul__(number) - def __truediv__(self, number: Union[float, int, 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 @@ -464,7 +463,7 @@ def __truediv__(self, number: Union[float, int, Number]): :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network with its Choi operator divided by ``number``. """ - if not isinstance(number, Number): + if not isinstance(number, Union[float, int]): raise_error( TypeError, "It is not possible to divide a ``QuantumNetwork`` by a non-scalar.", From 730ee5463c8ffdc237c00959bc75db52b59c0d66 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Sat, 20 Jul 2024 13:48:25 +0400 Subject: [PATCH 62/67] remove class --- src/qibo/symbols.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/qibo/symbols.py b/src/qibo/symbols.py index 8a76f1ec5b..9aa12a1dcc 100644 --- a/src/qibo/symbols.py +++ b/src/qibo/symbols.py @@ -161,13 +161,3 @@ class Z(PauliSymbol): """ pass - - -class S(PauliSymbol): - """Qibo symbol for the S operator. - - Args: - q (int): Target qubit id. - """ - - pass From 4871acff1650ea097e8b24e82fe8685b37c8544a Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Sat, 20 Jul 2024 14:04:20 +0400 Subject: [PATCH 63/67] remove `try / except` --- src/qibo/quantum_info/quantum_networks.py | 58 +++++++++++++++-------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index eaebacd0cc..dda4a50833 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -63,22 +63,36 @@ def __init__( self.dims = reduce(mul, self.partition) if len(self.partition) > 0 else 1 @staticmethod - def _order_tensor_to_operator(n: int): - """ - Returns the order to reshape a tensor to an operator. - Given a tenosr of 2n leads, the order is [0, 2, 4, ..., 1, 3, 5, ...]. + 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. """ - order = list(range(0, n * 2, 2)) + list(range(1, n * 2, 2)) - return order + return list(range(0, 2 * dims, 2)) + list(range(1, 2 * dims, 2)) @staticmethod - def _order_operator_to_tensor(n: int): - """ - Returns the order to reshape an operator to a tensor. - Given a operator of 2n systems, the order is [0, n, 1, n+1, 2, n+2, ...]. + 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. """ - order = list(sum(zip(list(range(0, n)), list(range(n, n * 2))), ())) - return order + return list( + sum(zip(list(range(0, nsystems)), list(range(nsystems, nsystems * 2))), ()) + ) @classmethod def _operator_to_tensor(cls, operator, partition: List[int]): @@ -88,7 +102,7 @@ def _operator_to_tensor(cls, operator, partition: List[int]): # 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]) + tuple(dim**2 for dim in partition) ): raise_error( ValueError, @@ -134,7 +148,8 @@ def from_operator( Defaults to ``None``. Returns: - :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: The quantum network constructed from the input Choi operator. + :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: + quantum network constructed from the input Choi operator. """ if pure: @@ -142,14 +157,15 @@ def from_operator( partition = tuple(operator.shape) tensor = operator else: - try: - tensor = operator.reshape(partition) - except: + 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}", + + 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) @@ -619,7 +635,8 @@ def _set_parameters(self): raise_error( ValueError, "``partition`` does not match the shape of the input matrix. " - + f"Cannot reshape matrix of size {self._tensor.shape} to partition {self.partition}", + + 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: @@ -629,7 +646,8 @@ def _set_parameters(self): raise_error( ValueError, "``partition`` does not match the shape of the input matrix. " - + f"Cannot reshape matrix of size {self._tensor.shape} to partition {self.partition}", + + 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) From d71649f17525a349084b92432b244e1dfa498649 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 20 Jul 2024 10:04:49 +0000 Subject: [PATCH 64/67] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/qibo/quantum_info/quantum_networks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index dda4a50833..61a09f356e 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -148,7 +148,7 @@ def from_operator( Defaults to ``None``. Returns: - :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: + :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: quantum network constructed from the input Choi operator. """ @@ -157,7 +157,9 @@ def from_operator( partition = tuple(operator.shape) tensor = operator else: - if np.prod(tuple(operator.shape)) != np.prod(tuple(dim**2 for dim in partition)): + 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. " From ca00dd6a6862d97c6adff4c25b11e3dc9de25e58 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Sat, 20 Jul 2024 14:13:01 +0400 Subject: [PATCH 65/67] modifications --- src/qibo/quantum_info/quantum_networks.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 61a09f356e..7a2ededa9a 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -556,7 +556,7 @@ def __matmul__(self, second_network): + "Use `link_product` method to specify the subscript.", ) - return self.link_product(subscripts, second_network) + 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.""" @@ -643,7 +643,7 @@ def _set_parameters(self): 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]) + tuple(dim**2 for dim in self.partition) ): raise_error( ValueError, @@ -674,7 +674,7 @@ def full(self, update: bool = False, backend=None): conj = backend.np.conj if self.is_pure(): - """Reshapes input matrix based on purity.""" + # Reshapes input matrix based on purity. tensor.reshape(self.dims) if self._backend.__class__.__name__ == "PyTorchBackend": tensor = self._tensordot(tensor, conj(tensor), dims=0) @@ -800,14 +800,14 @@ def is_causal( @classmethod def from_operator( - cls, operator, partition=None, pure=False, backend=None, inverse=False - ): + 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 + comb._tensor = comb._tensor.T # pylint: disable=W0212 return comb @@ -1028,13 +1028,13 @@ def link_product( raise_error(TypeError, f"The {i}-th operator is not a ``QuantumNetwork``.") if backend is None: # pragma: no cover - backend = operands[0]._backend + backend = operands[0]._backend # pylint: disable=W0212 tensors = [ ( backend.to_numpy(operand.full()) if operand.is_pure() - else backend.to_numpy(operand._tensor) + else backend.to_numpy(operand._tensor) # pylint: disable=W0212 ) for operand in operands ] From c19162faecc6f23c330b2dc661b133e3d87539fa Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Sat, 20 Jul 2024 14:56:45 +0400 Subject: [PATCH 66/67] fix bug --- src/qibo/quantum_info/quantum_networks.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 7a2ededa9a..223ab7c27d 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -157,9 +157,10 @@ def from_operator( partition = tuple(operator.shape) tensor = operator else: - if np.prod(tuple(operator.shape)) != np.prod( - tuple(dim**2 for dim in partition) - ): + 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. " @@ -803,9 +804,8 @@ 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 + 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 @@ -854,7 +854,7 @@ def __init__( if len(partition) > 2: raise_error( ValueError, - "A quantum channel should only contain one input system and one output system. " + "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: From ee4f624ecd9c3ddc76b5ea6c88ea43fb0aa70f1a Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Sat, 20 Jul 2024 15:26:35 +0400 Subject: [PATCH 67/67] fix type check --- src/qibo/quantum_info/quantum_networks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 223ab7c27d..691f40a515 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -438,7 +438,7 @@ def __mul__(self, number: Union[float, int]): :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network with its Choi operator multiplied by ``number``. """ - if not isinstance(number, Union[float, int]): + if not isinstance(number, (float, int)): raise_error( TypeError, "It is not possible to multiply a ``QuantumNetwork`` by a non-scalar.", @@ -482,7 +482,7 @@ def __truediv__(self, number: Union[float, int]): :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network with its Choi operator divided by ``number``. """ - if not isinstance(number, Union[float, int]): + if not isinstance(number, (float, int)): raise_error( TypeError, "It is not possible to divide a ``QuantumNetwork`` by a non-scalar.",