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