diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index 40d3cc91d8..38aa1e11e5 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -1781,6 +1781,13 @@ Frame Potential Quantum Networks ^^^^^^^^^^^^^^^^ +Quantum network is an object that unifies the representation of quantum states, channels, +observables, and higher-order quantum operators. + +For more details, see G. Chiribella *et al.*, *Theoretical framework for quantum networks*, +`Physical Review A 80.2 (2009): 022339 +`_. + .. autoclass:: qibo.quantum_info.quantum_networks.QuantumNetwork :members: :member-order: bysource diff --git a/doc/source/code-examples/tutorials/quantum_networks/README.md b/doc/source/code-examples/tutorials/quantum_networks/README.md new file mode 100644 index 0000000000..e0b074640c --- /dev/null +++ b/doc/source/code-examples/tutorials/quantum_networks/README.md @@ -0,0 +1,122 @@ +# 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 b9d5b01f96..8992169ae5 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -1,4 +1,4 @@ -"""Module defining the QuantumNetwork class and adjacent functions.""" +"""Module defining the `QuantumNetwork` class and adjacent functions.""" import re from functools import reduce @@ -12,7 +12,21 @@ class QuantumNetwork: - """Quantum network object that holds a Choi operator as a tensor. + """This class stores the Choi operator of the quantum network as a tensor, + which is an unique representation of the quantum network. + + A minimum quantum network is a quantum channel, which is a quantum network of the form + :math:`J[n \\to m]`, where :math:`n` is the dimension of the input system , + and :math:`m` is the dimension of the output system. + A quantum state is a quantum network of the form :math:`J[1 \\to n]`, + such that the input system is trivial. + An observable is a quantum network of the form :math:`J[n \\to 1]`, + such that the output system is trivial. + + A quantum network may contain multiple input and output systems. + For example, a "quantum comb" is a quantum network of the form :math:`J[n', n \\to m, m']`, + which convert a quantum channel of the form :math:`J[n \\to m]` + to a quantum channel of the form :math:`J[n' \\to m']`. Args: matrix (ndarray): input Choi operator. @@ -21,8 +35,8 @@ class QuantumNetwork: Choi operator. If ``None``, defaults to ``(False,True,False,True,...)``, where ``len(system_output)=len(partition)``. Defaults to ``None``. - pure (bool, optional): ``True`` when ``matrix`` is a rank-:math:`1` operator, - ``False`` otherwise. Defaults to ``False``. + pure (bool, optional): ``True`` when ``matrix`` is a "pure" representation (e.g. a pure + state, a unitary operator, etc.), ``False`` otherwise. Defaults to ``False``. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. @@ -64,11 +78,11 @@ def matrix(self, backend=None): return backend.cast(self._matrix, dtype=self._matrix.dtype) - def pure(self): + def is_pure(self): """Returns bool indicading if the Choi operator of the network is pure.""" return self._pure - def hermitian( + 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. @@ -114,7 +128,7 @@ def hermitian( return float(norm) <= precision_tol - def unital( + 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. @@ -162,7 +176,7 @@ def unital( return float(norm) <= precision_tol - def causal( + 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. @@ -210,8 +224,8 @@ def causal( return float(norm) <= precision_tol - def positive_semidefinite(self, precision_tol: float = 1e-8): - """Returns bool indicating if Choi operator :math:`\\mathcal{E}` of the networn is positive-semidefinite. + def is_positive_semidefinite(self, precision_tol: float = 1e-8): + """Returns bool indicating if Choi operator :math:`\\mathcal{E}` of the network is positive-semidefinite. Args: precision_tol (float, optional): threshold value used to check if eigenvalues of @@ -228,7 +242,7 @@ def positive_semidefinite(self, precision_tol: float = 1e-8): reshaped = np.reshape(self._matrix, (self.dims, self.dims)) - if self.hermitian(): + if self.is_hermitian(): eigenvalues = np.linalg.eigvalsh(reshaped) else: if self._backend.__class__.__name__ in [ @@ -240,7 +254,7 @@ def positive_semidefinite(self, precision_tol: float = 1e-8): return all(eigenvalue >= -precision_tol for eigenvalue in eigenvalues) - def channel( + def is_channel( self, order: Optional[Union[int, str]] = None, precision_tol_causal: float = 1e-8, @@ -263,9 +277,9 @@ def channel( Returns: bool: Channel condition. """ - return self.causal(order, precision_tol_causal) and self.positive_semidefinite( - precision_tol_psd - ) + 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`. @@ -280,7 +294,7 @@ def apply(self, state): """ matrix = np.copy(self._matrix) - if self.pure(): + if self.is_pure(): return np.einsum("kj,ml,jl -> km", matrix, np.conj(matrix), state) return np.einsum("jklm,km -> jl", matrix, state) @@ -323,8 +337,8 @@ def link_product(self, second_network, subscripts: str = "ij,jk -> ik"): inv_subscripts = pattern_two and subscripts[0] == subscripts[4] super_subscripts = ( pattern_four - and subscripts[2] == subscripts[5] - and subscripts[3] == subscripts[6] + and subscripts[1] == subscripts[5] + and subscripts[2] == subscripts[6] ) if not channel_subscripts and not inv_subscripts and not super_subscripts: @@ -337,10 +351,10 @@ def link_product(self, second_network, subscripts: str = "ij,jk -> ik"): second_matrix = second_network._full() # pylint: disable=W0212 if super_subscripts: - cexpr = "jklmnopq,nopqrstu->jklmrstu" + cexpr = "jklmnopq,klop->jmnq" return QuantumNetwork( np.einsum(cexpr, first_matrix, second_matrix), - self.partition[:2] + second_network.partition[2:], + [self.partition[0] + self.partition[-1]], ) cexpr = "jkab,klbc->jlac" @@ -376,7 +390,7 @@ def to_full(self, backend=None): if backend is None: # pragma: no cover backend = self._backend - if self.pure(): + if self.is_pure(): self._matrix = self._full() self._pure = False @@ -426,6 +440,11 @@ def __add__(self, second_network): def __mul__(self, number: Union[float, int]): """Returns quantum network with its Choi operator multiplied by a scalar. + If the quantum network is pure and ``number > 0.0``, the method returns a pure quantum + network with its Choi operator multiplied by the square root of ``number``. + This is equivalent to multiplying `self.to_full()` by the ``number``. + Otherwise, this method will return a full quantum network. + Args: number (float or int): scalar to multiply the Choi operator of the network with. @@ -439,7 +458,7 @@ def __mul__(self, number: Union[float, int]): "It is not possible to multiply a ``QuantumNetwork`` by a non-scalar.", ) - if self.pure() and number > 0.0: + if self.is_pure() and number > 0.0: return QuantumNetwork( np.sqrt(number) * self.matrix(backend=self._backend), partition=self.partition, @@ -465,6 +484,11 @@ def __rmul__(self, number: Union[float, int]): def __truediv__(self, number: Union[float, int]): """Returns quantum network with its Choi operator divided by a scalar. + If the quantum network is pure and ``number > 0.0``, the method returns a pure quantum + network with its Choi operator divided by the square root of ``number``. + This is equivalent to dividing `self.to_full()` by the ``number``. + Otherwise, this method will return a full quantum network. + Args: number (float or int): scalar to divide the Choi operator of the network with. @@ -478,20 +502,13 @@ def __truediv__(self, number: Union[float, int]): "It is not possible to divide a ``QuantumNetwork`` by a non-scalar.", ) - if self.pure() and number > 0.0: - return QuantumNetwork( - self.matrix(backend=self._backend) / np.sqrt(number), - partition=self.partition, - system_output=self.system_output, - pure=True, - backend=self._backend, - ) + number = np.sqrt(number) if self.is_pure() and number > 0.0 else number return QuantumNetwork( self.matrix(backend=self._backend) / number, partition=self.partition, system_output=self.system_output, - pure=False, + pure=self.is_pure(), backend=self._backend, ) @@ -515,17 +532,38 @@ def __matmul__(self, second_network): + "``QuantumNetwork`` by a non-``QuantumNetwork``.", ) - if self.partition != second_network.partition: - raise_error( - ValueError, - "partitions of the networks do not match: " - + f"{self.partition} != {second_network.partition}.", - ) + if len(self.partition) == 2: # `self` is a channel + if len(second_network.partition) != 2: + raise_error( + ValueError, + f"`QuantumNetwork {second_network} is assumed to be a channel, but it is not. " + + "Use `link_product` method to specify the subscript.", + ) + if self.partition[1] != second_network.partition[0]: + raise_error( + ValueError, + "partitions of the networks do not match: " + + f"{self.partition[1]} != {second_network.partition[0]}.", + ) - if len(self.partition) == 2: subscripts = "jk,kl -> jl" - elif len(self.partition) == 4: - subscripts = "jklm,lmno -> jkno" + + elif len(self.partition) == 4: # `self` is a super-channel + if len(second_network.partition) != 2: + raise_error( + ValueError, + f"`QuantumNetwork {second_network} is assumed to be a channel, but it is not. " + + "Use `link_product` method to specify the subscript.", + ) + if self.partition[1] != second_network.partition[0]: + raise_error( + ValueError, + "Systems of the channel do not match the super-channel: " + + f"{self.partition[1], self.partition[2]} != " + + f"{second_network.partition[0],second_network.partition[1]}.", + ) + + subscripts = "jklm,kl -> jm" else: raise_error( NotImplementedError, @@ -624,7 +662,7 @@ def _set_tensor_and_parameters(self): def _full(self): """Reshapes input matrix based on purity.""" matrix = np.copy(self._matrix) - if self.pure(): + if self.is_pure(): matrix = np.einsum("jk,lm -> kjml", matrix, np.conj(matrix)) return matrix @@ -635,7 +673,7 @@ 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 * 4 + "->" + braket * 4) + pattern_four = re.compile(braket * 4 + "," + braket * 2 + "->" + braket * 2) return bool(re.match(pattern_two, subscripts)), bool( re.match(pattern_four, subscripts) diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index a84103ece5..9345ee3ede 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -46,13 +46,13 @@ def test_errors(backend): QuantumNetwork(channel.to_choi(backend=backend), partition=(1, 2), pure="True") with pytest.raises(ValueError): - network.hermitian(precision_tol=-1e-8) + network.is_hermitian(precision_tol=-1e-8) with pytest.raises(ValueError): - network.unital(precision_tol=-1e-8) + network.is_unital(precision_tol=-1e-8) with pytest.raises(ValueError): - network.causal(precision_tol=-1e-8) + network.is_causal(precision_tol=-1e-8) with pytest.raises(TypeError): network + 1 @@ -88,7 +88,7 @@ def test_errors(backend): with pytest.raises(NotImplementedError): net @ net - with pytest.raises(ValueError): + with pytest.raises(NotImplementedError): net @ network with pytest.raises(ValueError): @@ -155,11 +155,11 @@ def test_parameters(backend): backend.assert_allclose(network.partition, partition) backend.assert_allclose(network.system_output, (False, True)) - assert network.causal() - assert network.unital() - assert network.hermitian() - assert network.positive_semidefinite() - assert network.channel() + assert network.is_causal() + assert network.is_unital() + assert network.is_hermitian() + assert network.is_positive_semidefinite() + assert network.is_channel() def test_with_states(backend): @@ -186,8 +186,8 @@ def test_with_states(backend): state_output_link.matrix(backend=backend).reshape((dims, dims)), state_output ) - assert network_state.hermitian() - assert network_state.positive_semidefinite() + assert network_state.is_hermitian() + assert network_state.is_positive_semidefinite() @pytest.mark.parametrize("subscript", ["jk,kl->jl", "jk,lj->lk"]) @@ -223,25 +223,26 @@ def test_with_unitaries(backend, subscript): def test_with_comb(backend): - subscript = "jklm,lmno->jkno" - partition = (2,) * 4 - sys_out = (False, True) * 2 + subscript = "jklm,kl->jm" + comb_partition = (2,) * 4 + channel_partition = (2,) * 2 + comb_sys_out = (False, True) * 2 + channel_sys_out = (False, True) comb = random_density_matrix(2**4, backend=backend) - comb_2 = random_density_matrix(2**4, backend=backend) + channel = random_density_matrix(2**2, backend=backend) - comb_choi = QuantumNetwork(comb, partition, system_output=sys_out, backend=backend) - comb_choi_2 = QuantumNetwork( - comb_2, partition, system_output=sys_out, backend=backend + comb_choi = QuantumNetwork( + comb, comb_partition, system_output=comb_sys_out, backend=backend + ) + channel_choi = QuantumNetwork( + channel, channel_partition, system_output=channel_sys_out, backend=backend ) - comb_choi_3 = QuantumNetwork( - comb @ comb_2, partition, system_output=sys_out, backend=backend - ).to_full(backend) - test = comb_choi.link_product(comb_choi_2, subscript).to_full(backend) + test = comb_choi.link_product(channel_choi, subscript).to_full(backend) + channel_choi2 = comb_choi @ channel_choi - backend.assert_allclose(test, comb_choi_3, atol=1e-5) - backend.assert_allclose(test, (comb_choi @ comb_choi_2).to_full(backend), atol=1e-5) + backend.assert_allclose(test, channel_choi2.to_full(backend), atol=1e-5) def test_apply(backend): @@ -265,9 +266,9 @@ def test_non_hermitian_and_prints(backend): matrix = random_gaussian_matrix(dims**2, backend=backend) network = QuantumNetwork(matrix, (dims, dims), pure=False, backend=backend) - assert not network.hermitian() - assert not network.causal() - assert not network.positive_semidefinite() - assert not network.channel() + 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]"