diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index 09c88a3fcf..64d385a6ab 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -1952,11 +1952,22 @@ 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 + + Random Ensembles ^^^^^^^^^^^^^^^^ @@ -2377,6 +2388,90 @@ Parameterized quantum circuit integral .. autofunction:: qibo.quantum_info.pqc_integral +.. _GST: + + + +Tomography +---------- + +Functions used to classically simulate tomography protocols. + + +Gate Set Tomography +^^^^^^^^^^^^^^^^^^^ + +Gate Set Tomography (GST) is a powerful technique employed in quantum information processing +to characterize the behavior of quantum gates on quantum hardware [1, 2, 3]. +The primary objective of GST is to provide a robust framework for obtaining a representation +of quantum gates within a predefined gate set when subjected to noise inherent to the +quantum hardware. + +By characterizing the impact of noise on quantum gates, GST enables the identification and +quantification of errors, laying the groundwork for subsequent error mitigation strategies. +The insights gained from GST are instrumental, for instance, in setting up the necessary +parameters for Probabilistic Error Cancellation (PEC). + +In practice, given a set of operators (or gates), :math:`\mathcal{O}=\{O_0, O_1, \dots, O_n\}`, +a set of initial states :math:`\{\rho_k\}`, and a set of measurement bases :math:`\{M_j\}`, +one performs GST on the :math:`l`-th operator by choosing an initial state :math:`\rho_k`, +applying the gate :math:`O_l \in \mathcal{O}`, measuring in the :math:`M_j` basis in order to +obtain the following matrix: + +.. math:: + \{\tilde{O}_l\}_{jk} = \text{tr}(M_j\,O_l\,\rho_k) \, , + +which provides an estimated representation of the operator :math:`O_l` in the specific system. + +This implementation makes use, in particular, of +:math:`\rho_k \in \{ \ketbra{0}{0}, \ketbra{1}{1}, \ketbra{+}{+}, \ketbra{y+}{y+} \}^{\otimes n}` and +:math:`M_j \in \{ I, X, Y, Z\}^{\otimes n}` [4], with :math:`n\in\{1,2\}` +being the number of qubits. However, :math:`\{\tilde{O}_l\}_{jk}` is not yet given in +the Pauli-Liouville representation (also known as *Pauli Transfer Matrix*). +To obtain the Pauli-Liouville representation, one needs the two matrices, described below. +The matrix :math:`\tilde{g}` has its elements :math:`\tilde{g}_{jk}` defined as + +.. math:: + \tilde{g}_{jk} = \text{tr}(M_j\,\rho_k) \, , + +which is obtained by measuring the initial states :math:`\{\rho_k\}` in each basis element :math:`\{M_j\}` +without any gates' application. +The *gauge matrix* :math:`T` is given by + +.. math:: + T = \begin{pmatrix} + 1 & 1 & 1 & 1 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & 1 \\ + 1 & -1 & 0 & 0 \\ + \end{pmatrix} \, . + +This is the matrix, in a common gauge, implementing a change of basis. +Therefore, the Pauli-Liouville representation can be recovered as + +.. math:: + O_l^{PL} = T\,g^{-1}\,\tilde{O_l}\,T^{-1} \, . + +References: + 1. R. Blume-Kohout *et al*. + *Robust, self-consistent, closed-form tomography of quantum logic gates on a trapped ion qubit* + (2013), `arXiv:1310.4492 `_. + + 2. D. Greenbaum, *Introduction to quantum gate set tomography* (2015), + `arXiv:1509.02921 `_. + + 3. E. Nielsen *et al.*, *Gate set tomography* (2021), + `Quantum 5, 557 `_. + + 4. S. Endo, S. C. Benjamin, and Y. Li, + *Practical quantum error mitigation for near-future applications* (2018), + `Physical Review X 8.3: 031027 `_. + + +.. autofunction:: qibo.tomography.gate_set_tomography.GST + + + .. _Parallel: Parallelism @@ -2518,12 +2613,10 @@ Alternatively, a Clifford circuit can also be executed starting from the :class: circuit = random_clifford(nqubits) result = Clifford.from_circuit(circuit) - .. autoclass:: qibo.backends.clifford.CliffordBackend :members: :member-order: bysource - Cloud Backends ^^^^^^^^^^^^^^ diff --git a/doc/source/code-examples/advancedexamples.rst b/doc/source/code-examples/advancedexamples.rst index de2388dcd3..50ebdef539 100644 --- a/doc/source/code-examples/advancedexamples.rst +++ b/doc/source/code-examples/advancedexamples.rst @@ -2112,3 +2112,64 @@ In this case circuits will first be transpiled to respect the 5-qubit star conne Then all gates will be converted to native. The :class:`qibo.transpiler.unroller.Unroller` transpiler used in this example assumes Z, RZ, GPI2 or U3 as the single-qubit native gates, and supports CZ and iSWAP as two-qubit natives. In this case we restricted the two-qubit gate set to CZ only. The final_layout contains the final logical-physical qubit mapping. + +.. _gst_example: + +How to perform Gate Set Tomography? +----------------------------------- + +In order to obtain an estimated representation of a set of quantum gates in a particular noisy environment, qibo provides a GST routine in its tomography module. + +Let's first define the set of gates we want to estimate: + +.. testcode:: + + from qibo import gates + + gate_set = {gates.X, gates.H, gates.CZ} + +For simulation purposes we can define a noise model. Naturally this is not needed when running on real quantum hardware, which is intrinsically noisy. For example, we can suppose that the three gates we want to estimate are going to be noisy: + +.. testcode:: + + from qibo.noise import NoiseModel, DepolarizingError + + noise_model = NoiseModel() + noise_model.add(DepolarizingError(1e-3), gates.X) + noise_model.add(DepolarizingError(1e-2), gates.H) + noise_model.add(DepolarizingError(3e-2), gates.CZ) + +Then the estimated representation of the gates in this noisy environment can be extracted by running the GST: + +.. testcode:: + + from qibo.tomography import GST + + estimated_gates = GST( + gate_set = gate_set, + nshots = 10000, + noise_model = noise_model + ) + +In some cases the empty circuit matrix :math:`E` can also be useful, and can be returned by setting the ``include_empty`` argument to ``True``: + +.. testcode:: + + empty_1q, empty_2q, *estimated_gates = GST( + gate_set = gate_set, + nshots = 10000, + noise_model = noise_model, + include_empty = True, + ) + +where ``empty_1q`` and ``empty_2q`` correspond to the single and two qubits empty matrices respectively. +Similarly, the Pauli-Liouville representation of the gates can be directly returned as well: + +.. testcode:: + + estimated_gates = GST( + gate_set = gate_set, + nshots = 10000, + noise_model = noise_model, + pauli_liouville = True, + ) 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/poetry.lock b/poetry.lock index eb75a1c294..81ca1c7ddc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -200,13 +200,13 @@ css = ["tinycss2 (>=1.1.0,<1.3)"] [[package]] name = "cachetools" -version = "5.3.3" +version = "5.4.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, - {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, + {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, + {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, ] [[package]] @@ -1181,13 +1181,13 @@ dev-env = ["black (==22.3.0)", "isort (==5.7.*)", "mypy (==0.931.*)", "pylint (= [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -1578,61 +1578,61 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] [[package]] name = "grpcio" -version = "1.64.1" +version = "1.65.1" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.8" files = [ - {file = "grpcio-1.64.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:55697ecec192bc3f2f3cc13a295ab670f51de29884ca9ae6cd6247df55df2502"}, - {file = "grpcio-1.64.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:3b64ae304c175671efdaa7ec9ae2cc36996b681eb63ca39c464958396697daff"}, - {file = "grpcio-1.64.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:bac71b4b28bc9af61efcdc7630b166440bbfbaa80940c9a697271b5e1dabbc61"}, - {file = "grpcio-1.64.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c024ffc22d6dc59000faf8ad781696d81e8e38f4078cb0f2630b4a3cf231a90"}, - {file = "grpcio-1.64.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7cd5c1325f6808b8ae31657d281aadb2a51ac11ab081ae335f4f7fc44c1721d"}, - {file = "grpcio-1.64.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0a2813093ddb27418a4c99f9b1c223fab0b053157176a64cc9db0f4557b69bd9"}, - {file = "grpcio-1.64.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2981c7365a9353f9b5c864595c510c983251b1ab403e05b1ccc70a3d9541a73b"}, - {file = "grpcio-1.64.1-cp310-cp310-win32.whl", hash = "sha256:1262402af5a511c245c3ae918167eca57342c72320dffae5d9b51840c4b2f86d"}, - {file = "grpcio-1.64.1-cp310-cp310-win_amd64.whl", hash = "sha256:19264fc964576ddb065368cae953f8d0514ecc6cb3da8903766d9fb9d4554c33"}, - {file = "grpcio-1.64.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:58b1041e7c870bb30ee41d3090cbd6f0851f30ae4eb68228955d973d3efa2e61"}, - {file = "grpcio-1.64.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bbc5b1d78a7822b0a84c6f8917faa986c1a744e65d762ef6d8be9d75677af2ca"}, - {file = "grpcio-1.64.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:5841dd1f284bd1b3d8a6eca3a7f062b06f1eec09b184397e1d1d43447e89a7ae"}, - {file = "grpcio-1.64.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8caee47e970b92b3dd948371230fcceb80d3f2277b3bf7fbd7c0564e7d39068e"}, - {file = "grpcio-1.64.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73819689c169417a4f978e562d24f2def2be75739c4bed1992435d007819da1b"}, - {file = "grpcio-1.64.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6503b64c8b2dfad299749cad1b595c650c91e5b2c8a1b775380fcf8d2cbba1e9"}, - {file = "grpcio-1.64.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1de403fc1305fd96cfa75e83be3dee8538f2413a6b1685b8452301c7ba33c294"}, - {file = "grpcio-1.64.1-cp311-cp311-win32.whl", hash = "sha256:d4d29cc612e1332237877dfa7fe687157973aab1d63bd0f84cf06692f04c0367"}, - {file = "grpcio-1.64.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e56462b05a6f860b72f0fa50dca06d5b26543a4e88d0396259a07dc30f4e5aa"}, - {file = "grpcio-1.64.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:4657d24c8063e6095f850b68f2d1ba3b39f2b287a38242dcabc166453e950c59"}, - {file = "grpcio-1.64.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:62b4e6eb7bf901719fce0ca83e3ed474ae5022bb3827b0a501e056458c51c0a1"}, - {file = "grpcio-1.64.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:ee73a2f5ca4ba44fa33b4d7d2c71e2c8a9e9f78d53f6507ad68e7d2ad5f64a22"}, - {file = "grpcio-1.64.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:198908f9b22e2672a998870355e226a725aeab327ac4e6ff3a1399792ece4762"}, - {file = "grpcio-1.64.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39b9d0acaa8d835a6566c640f48b50054f422d03e77e49716d4c4e8e279665a1"}, - {file = "grpcio-1.64.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5e42634a989c3aa6049f132266faf6b949ec2a6f7d302dbb5c15395b77d757eb"}, - {file = "grpcio-1.64.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1a82e0b9b3022799c336e1fc0f6210adc019ae84efb7321d668129d28ee1efb"}, - {file = "grpcio-1.64.1-cp312-cp312-win32.whl", hash = "sha256:55260032b95c49bee69a423c2f5365baa9369d2f7d233e933564d8a47b893027"}, - {file = "grpcio-1.64.1-cp312-cp312-win_amd64.whl", hash = "sha256:c1a786ac592b47573a5bb7e35665c08064a5d77ab88a076eec11f8ae86b3e3f6"}, - {file = "grpcio-1.64.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:a011ac6c03cfe162ff2b727bcb530567826cec85eb8d4ad2bfb4bd023287a52d"}, - {file = "grpcio-1.64.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4d6dab6124225496010bd22690f2d9bd35c7cbb267b3f14e7a3eb05c911325d4"}, - {file = "grpcio-1.64.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:a5e771d0252e871ce194d0fdcafd13971f1aae0ddacc5f25615030d5df55c3a2"}, - {file = "grpcio-1.64.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3c1b90ab93fed424e454e93c0ed0b9d552bdf1b0929712b094f5ecfe7a23ad"}, - {file = "grpcio-1.64.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20405cb8b13fd779135df23fabadc53b86522d0f1cba8cca0e87968587f50650"}, - {file = "grpcio-1.64.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0cc79c982ccb2feec8aad0e8fb0d168bcbca85bc77b080d0d3c5f2f15c24ea8f"}, - {file = "grpcio-1.64.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a3a035c37ce7565b8f4f35ff683a4db34d24e53dc487e47438e434eb3f701b2a"}, - {file = "grpcio-1.64.1-cp38-cp38-win32.whl", hash = "sha256:1257b76748612aca0f89beec7fa0615727fd6f2a1ad580a9638816a4b2eb18fd"}, - {file = "grpcio-1.64.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a12ddb1678ebc6a84ec6b0487feac020ee2b1659cbe69b80f06dbffdb249122"}, - {file = "grpcio-1.64.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:75dbbf415026d2862192fe1b28d71f209e2fd87079d98470db90bebe57b33179"}, - {file = "grpcio-1.64.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e3d9f8d1221baa0ced7ec7322a981e28deb23749c76eeeb3d33e18b72935ab62"}, - {file = "grpcio-1.64.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:5f8b75f64d5d324c565b263c67dbe4f0af595635bbdd93bb1a88189fc62ed2e5"}, - {file = "grpcio-1.64.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c84ad903d0d94311a2b7eea608da163dace97c5fe9412ea311e72c3684925602"}, - {file = "grpcio-1.64.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:940e3ec884520155f68a3b712d045e077d61c520a195d1a5932c531f11883489"}, - {file = "grpcio-1.64.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f10193c69fc9d3d726e83bbf0f3d316f1847c3071c8c93d8090cf5f326b14309"}, - {file = "grpcio-1.64.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac15b6c2c80a4d1338b04d42a02d376a53395ddf0ec9ab157cbaf44191f3ffdd"}, - {file = "grpcio-1.64.1-cp39-cp39-win32.whl", hash = "sha256:03b43d0ccf99c557ec671c7dede64f023c7da9bb632ac65dbc57f166e4970040"}, - {file = "grpcio-1.64.1-cp39-cp39-win_amd64.whl", hash = "sha256:ed6091fa0adcc7e4ff944090cf203a52da35c37a130efa564ded02b7aff63bcd"}, - {file = "grpcio-1.64.1.tar.gz", hash = "sha256:8d51dd1c59d5fa0f34266b80a3805ec29a1f26425c2a54736133f6d87fc4968a"}, + {file = "grpcio-1.65.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:3dc5f928815b8972fb83b78d8db5039559f39e004ec93ebac316403fe031a062"}, + {file = "grpcio-1.65.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:8333ca46053c35484c9f2f7e8d8ec98c1383a8675a449163cea31a2076d93de8"}, + {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:7af64838b6e615fff0ec711960ed9b6ee83086edfa8c32670eafb736f169d719"}, + {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb64b4166362d9326f7efbf75b1c72106c1aa87f13a8c8b56a1224fac152f5c"}, + {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8422dc13ad93ec8caa2612b5032a2b9cd6421c13ed87f54db4a3a2c93afaf77"}, + {file = "grpcio-1.65.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4effc0562b6c65d4add6a873ca132e46ba5e5a46f07c93502c37a9ae7f043857"}, + {file = "grpcio-1.65.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a6c71575a2fedf259724981fd73a18906513d2f306169c46262a5bae956e6364"}, + {file = "grpcio-1.65.1-cp310-cp310-win32.whl", hash = "sha256:34966cf526ef0ea616e008d40d989463e3db157abb213b2f20c6ce0ae7928875"}, + {file = "grpcio-1.65.1-cp310-cp310-win_amd64.whl", hash = "sha256:ca931de5dd6d9eb94ff19a2c9434b23923bce6f767179fef04dfa991f282eaad"}, + {file = "grpcio-1.65.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:bbb46330cc643ecf10bd9bd4ca8e7419a14b6b9dedd05f671c90fb2c813c6037"}, + {file = "grpcio-1.65.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d827a6fb9215b961eb73459ad7977edb9e748b23e3407d21c845d1d8ef6597e5"}, + {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:6e71aed8835f8d9fbcb84babc93a9da95955d1685021cceb7089f4f1e717d719"}, + {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a1c84560b3b2d34695c9ba53ab0264e2802721c530678a8f0a227951f453462"}, + {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27adee2338d697e71143ed147fe286c05810965d5d30ec14dd09c22479bfe48a"}, + {file = "grpcio-1.65.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f62652ddcadc75d0e7aa629e96bb61658f85a993e748333715b4ab667192e4e8"}, + {file = "grpcio-1.65.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:71a05fd814700dd9cb7d9a507f2f6a1ef85866733ccaf557eedacec32d65e4c2"}, + {file = "grpcio-1.65.1-cp311-cp311-win32.whl", hash = "sha256:b590f1ad056294dfaeac0b7e1b71d3d5ace638d8dd1f1147ce4bd13458783ba8"}, + {file = "grpcio-1.65.1-cp311-cp311-win_amd64.whl", hash = "sha256:12e9bdf3b5fd48e5fbe5b3da382ad8f97c08b47969f3cca81dd9b36b86ed39e2"}, + {file = "grpcio-1.65.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:54cb822e177374b318b233e54b6856c692c24cdbd5a3ba5335f18a47396bac8f"}, + {file = "grpcio-1.65.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:aaf3c54419a28d45bd1681372029f40e5bfb58e5265e3882eaf21e4a5f81a119"}, + {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:557de35bdfbe8bafea0a003dbd0f4da6d89223ac6c4c7549d78e20f92ead95d9"}, + {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8bfd95ef3b097f0cc86ade54eafefa1c8ed623aa01a26fbbdcd1a3650494dd11"}, + {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e6a8f3d6c41e6b642870afe6cafbaf7b61c57317f9ec66d0efdaf19db992b90"}, + {file = "grpcio-1.65.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1faaf7355ceed07ceaef0b9dcefa4c98daf1dd8840ed75c2de128c3f4a4d859d"}, + {file = "grpcio-1.65.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:60f1f38eed830488ad2a1b11579ef0f345ff16fffdad1d24d9fbc97ba31804ff"}, + {file = "grpcio-1.65.1-cp312-cp312-win32.whl", hash = "sha256:e75acfa52daf5ea0712e8aa82f0003bba964de7ae22c26d208cbd7bc08500177"}, + {file = "grpcio-1.65.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff5a84907e51924973aa05ed8759210d8cdae7ffcf9e44fd17646cf4a902df59"}, + {file = "grpcio-1.65.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:1fbd6331f18c3acd7e09d17fd840c096f56eaf0ef830fbd50af45ae9dc8dfd83"}, + {file = "grpcio-1.65.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:de5b6be29116e094c5ef9d9e4252e7eb143e3d5f6bd6d50a78075553ab4930b0"}, + {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:e4a3cdba62b2d6aeae6027ae65f350de6dc082b72e6215eccf82628e79efe9ba"}, + {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:941c4869aa229d88706b78187d60d66aca77fe5c32518b79e3c3e03fc26109a2"}, + {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f40cebe5edb518d78b8131e87cb83b3ee688984de38a232024b9b44e74ee53d3"}, + {file = "grpcio-1.65.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2ca684ba331fb249d8a1ce88db5394e70dbcd96e58d8c4b7e0d7b141a453dce9"}, + {file = "grpcio-1.65.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8558f0083ddaf5de64a59c790bffd7568e353914c0c551eae2955f54ee4b857f"}, + {file = "grpcio-1.65.1-cp38-cp38-win32.whl", hash = "sha256:8d8143a3e3966f85dce6c5cc45387ec36552174ba5712c5dc6fcc0898fb324c0"}, + {file = "grpcio-1.65.1-cp38-cp38-win_amd64.whl", hash = "sha256:76e81a86424d6ca1ce7c16b15bdd6a964a42b40544bf796a48da241fdaf61153"}, + {file = "grpcio-1.65.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:cb5175f45c980ff418998723ea1b3869cce3766d2ab4e4916fbd3cedbc9d0ed3"}, + {file = "grpcio-1.65.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b12c1aa7b95abe73b3e04e052c8b362655b41c7798da69f1eaf8d186c7d204df"}, + {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:3019fb50128b21a5e018d89569ffaaaa361680e1346c2f261bb84a91082eb3d3"}, + {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ae15275ed98ea267f64ee9ddedf8ecd5306a5b5bb87972a48bfe24af24153e8"}, + {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f096ffb881f37e8d4f958b63c74bfc400c7cebd7a944b027357cd2fb8d91a57"}, + {file = "grpcio-1.65.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2f56b5a68fdcf17a0a1d524bf177218c3c69b3947cb239ea222c6f1867c3ab68"}, + {file = "grpcio-1.65.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:941596d419b9736ab548aa0feb5bbba922f98872668847bf0720b42d1d227b9e"}, + {file = "grpcio-1.65.1-cp39-cp39-win32.whl", hash = "sha256:5fd7337a823b890215f07d429f4f193d24b80d62a5485cf88ee06648591a0c57"}, + {file = "grpcio-1.65.1-cp39-cp39-win_amd64.whl", hash = "sha256:1bceeec568372cbebf554eae1b436b06c2ff24cfaf04afade729fb9035408c6c"}, + {file = "grpcio-1.65.1.tar.gz", hash = "sha256:3c492301988cd720cd145d84e17318d45af342e29ef93141228f9cd73222368b"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.64.1)"] +protobuf = ["grpcio-tools (>=1.65.1)"] [[package]] name = "grpcio-status" @@ -1650,6 +1650,22 @@ googleapis-common-protos = ">=1.5.5" grpcio = ">=1.62.2" protobuf = ">=4.21.6" +[[package]] +name = "grpcio-status" +version = "1.65.1" +description = "Status proto mapping for gRPC" +optional = false +python-versions = ">=3.8" +files = [ + {file = "grpcio_status-1.65.1-py3-none-any.whl", hash = "sha256:0ec2070f7dbcc2fe78a7b34233a2a00f8ced727d2f1dec1af422d628cf86b92c"}, + {file = "grpcio_status-1.65.1.tar.gz", hash = "sha256:740d68d4a1824e59063f394df05171886262d5367b82256d54aac8aa7c5c79bf"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.5.5" +grpcio = ">=1.65.1" +protobuf = ">=5.26.1,<6.0dev" + [[package]] name = "h11" version = "0.14.0" @@ -2650,7 +2666,7 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, + {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, ] [[package]] @@ -2999,6 +3015,7 @@ description = "Nvidia JIT LTO Library" optional = false python-versions = ">=3" files = [ + {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-manylinux2014_aarch64.whl", hash = "sha256:98103729cc5226e13ca319a10bbf9433bbbd44ef64fe72f45f067cacc14b8d27"}, {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f9b37bc5c8cf7509665cb6ada5aaa0ce65618f2332b7d3e78e9790511f111212"}, {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-win_amd64.whl", hash = "sha256:e782564d705ff0bf61ac3e1bf730166da66dd2fe9012f111ede5fc49b64ae697"}, ] @@ -3475,6 +3492,26 @@ files = [ {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, ] +[[package]] +name = "protobuf" +version = "5.27.2" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-5.27.2-cp310-abi3-win32.whl", hash = "sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38"}, + {file = "protobuf-5.27.2-cp310-abi3-win_amd64.whl", hash = "sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505"}, + {file = "protobuf-5.27.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5"}, + {file = "protobuf-5.27.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b"}, + {file = "protobuf-5.27.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e"}, + {file = "protobuf-5.27.2-cp38-cp38-win32.whl", hash = "sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863"}, + {file = "protobuf-5.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6"}, + {file = "protobuf-5.27.2-cp39-cp39-win32.whl", hash = "sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca"}, + {file = "protobuf-5.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce"}, + {file = "protobuf-5.27.2-py3-none-any.whl", hash = "sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470"}, + {file = "protobuf-5.27.2.tar.gz", hash = "sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714"}, +] + [[package]] name = "psutil" version = "5.9.8" @@ -4014,7 +4051,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -4022,16 +4058,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -4048,7 +4076,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -4056,7 +4083,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -4612,18 +4638,18 @@ stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"] [[package]] name = "setuptools" -version = "70.0.0" +version = "70.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, - {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, + {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, + {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -4940,13 +4966,13 @@ numpy = "*" [[package]] name = "sympy" -version = "1.13.0" +version = "1.13.1" description = "Computer algebra system (CAS) in Python" optional = false python-versions = ">=3.8" files = [ - {file = "sympy-1.13.0-py3-none-any.whl", hash = "sha256:6b0b32a4673fb91bd3cac3b55406c8e01d53ae22780be467301cc452f6680c92"}, - {file = "sympy-1.13.0.tar.gz", hash = "sha256:3b6af8f4d008b9a1a6a4268b335b984b23835f26d1d60b0526ebc71d48a25f57"}, + {file = "sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8"}, + {file = "sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f"}, ] [package.dependencies] @@ -5591,4 +5617,4 @@ torch = ["torch"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "3a8d9f8c41d336ebf859edd0852cab251193e6c9fd98935233e2f5aa8218dc8a" +content-hash = "aae6addee9eeafec56d8349fee21e5c67826a0195f6fe233cc38283f5825f493" diff --git a/pyproject.toml b/pyproject.toml index 46182882c7..3db62429f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ ipython = "^8.10.0" qulacs = { version = "^0.6.4", markers="(sys_platform == 'darwin' and python_version > '3.9') or sys_platform != 'darwin'"} seaborn = "^0.13.2" ipykernel = "^6.29.4" +qibojit = { git = "https://github.com/qiboteam/qibojit.git" } [tool.poetry.group.tests] optional = true diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py index 48b34a0ff2..691f40a515 100644 --- a/src/qibo/quantum_info/quantum_networks.py +++ b/src/qibo/quantum_info/quantum_networks.py @@ -1,7 +1,7 @@ """Module defining the `QuantumNetwork` class and adjacent functions.""" -import re from functools import reduce +from logging import warning from operator import mul from typing import List, Optional, Tuple, Union @@ -12,30 +12,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``. - system_output (List[bool] or Tuple[bool], optional): mask on the output system of the + 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_output)=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`. @@ -44,162 +44,237 @@ 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._matrix = matrix - self.partition = partition - self.system_output = system_output + self._tensor = tensor + self.partition = tuple(partition) + self.system_input = system_input self._pure = pure self._backend = backend - self.dims = reduce(mul, self.partition) - self._set_tensor_and_parameters() + self._run_checks(self.partition, self.system_input, self._pure) - def matrix(self, backend=None): - """Returns the Choi operator of the quantum network in matrix form. + self._set_parameters() - 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``. + self.dims = reduce(mul, self.partition) if len(self.partition) > 0 else 1 - Returns: - ndarray: Choi matrix of the quantum network. - """ - if backend is None: # pragma: no cover - backend = self._backend + @staticmethod + def _order_tensor_to_operator(dims: int): + """Returns the order to reshape a tensor into an operator. - return backend.cast(self._matrix, dtype=self._matrix.dtype) + Given a tenosr of ``2 * dims`` leads, the order is + :math:`[0, 2, 4, ..., 1, 3, 5, ...]`. - def is_pure(self): - """Returns bool indicading if the Choi operator of the network is pure.""" - return self._pure + Args: + dims (int): dimension. - 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: + list: order to reshape tensor into an operator. + """ + return list(range(0, 2 * dims, 2)) + list(range(1, 2 * dims, 2)) - Hermicity is calculated as distance between :math:`\\mathcal{E}` and - :math:`\\mathcal{E}^{\\dagger}` with respect to a given norm. - Default is the ``Hilbert-Schmidt`` norm (also known as ``Frobenius`` norm). + @staticmethod + def _order_operator_to_tensor(nsystems: int): + """Returns the order to reshape an operator to a tensor. - 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 `_. + Given a operator of :math:`2n` systems, the order is + :math:`[0, n, 1, n+1, 2, n+2, ...]`. Args: - order (str or int, optional): order of the norm. Defaults to ``None``. - precision_tol (float, optional): threshold :math:`\\epsilon` that defines if - Choi operator of the network is :math:`\\epsilon`-close to Hermicity in - the norm given by ``order``. Defaults to :math:`10^{-8}`. + nsystems (int): number of systems. Returns: - bool: Hermiticity condition. + list: order to reshape operator into tensor. """ - if precision_tol < 0.0: + return list( + sum(zip(list(range(0, nsystems)), list(range(nsystems, nsystems * 2))), ()) + ) + + @classmethod + def _operator_to_tensor(cls, operator, partition: List[int]): + + n = len(partition) + order = cls._order_operator_to_tensor(n) + + # Check if the `partition` matches the shape of the input matrix + if np.prod(tuple(operator.shape)) != np.prod( + tuple(dim**2 for dim in partition) + ): raise_error( ValueError, - f"``precision_tol`` must be non-negative float, but it is {precision_tol}", + "``partition`` does not match the shape of the input matrix. " + + f"Cannot reshape matrix of size {operator.shape} to partition {partition}", ) - if order is None and self._backend.__class__.__name__ == "TensorflowBackend": - order = "euclidean" + # Check if `operator` is a pytourch tensor + tensor = operator.reshape(list(partition) * 2) + if operator.__class__.__name__ == "Tensor": + tensor = tensor.permute(order) + else: + tensor = tensor.transpose(order) + return tensor.reshape([dim**2 for dim in partition]) + + @classmethod + def from_operator( + cls, + operator, + partition: Optional[Union[List[int], Tuple[int]]] = None, + system_input: Optional[Union[List[bool], Tuple[bool]]] = None, + pure: bool = False, + backend=None, + ): + """Construct a :class:`qibo.quantum_info.QuantumNetwork` object from a ndarray. - self._matrix = self._full() - self._pure = False + 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. - reshaped = self._backend.cast( - self._backend.np.reshape(self._matrix, (self.dims, self.dims)), - dtype=self._matrix.dtype, - ) - reshaped = self._backend.cast( - self._backend.np.conj(reshaped).T - reshaped, dtype=reshaped.dtype - ) - norm = self._backend.calculate_norm_density_matrix(reshaped, order=order) + 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``. - return float(norm) <= precision_tol + Returns: + :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: + quantum network constructed from the input Choi operator. + """ - 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. + if pure: + if partition is None: + partition = tuple(operator.shape) + tensor = operator + else: + if tuple(partition) not in [ + tuple(operator.shape), + tuple(int(np.sqrt(dim)) for dim in operator.shape) * 2, + ]: + raise_error( + ValueError, + "``partition`` does not match the shape of the input matrix. " + + f"Cannot reshape matrix of size {operator.shape} " + + f"to partition {partition}", + ) + + tensor = operator.reshape(partition) + else: + # check if arr is a valid choi operator + len_sys = len(operator.shape) + if (len_sys % 2 != 0) or ( + operator.shape[: len_sys // 2] != operator.shape[len_sys // 2 :] + ): + raise_error( + ValueError, + "The opertor must be a square operator where the first half of the shape " + + "is the same as the second half of the shape. " + + f"However, the shape of the input is {operator.shape}. " + + "If the input is pure, set `pure=True`.", + ) - 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). + if partition is None: + partition = operator.shape[: len_sys // 2] - 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 `_. + tensor = cls._operator_to_tensor(operator, partition) + + return cls( + tensor, + partition=partition, + system_input=system_input, + pure=pure, + backend=backend, + ) + + 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: - 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}`. + full (bool, optional): If this is ``False``, and the network is pure, the method + will only return the eigenvector (unique when the network is pure). + If ``True``, returns the full tensor of the quantum network. Defaults to ``False``. + backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used + to return the Choi operator. If ``None``, defaults to the backend defined + when initializing the :class:`qibo.quantum_info.quantum_networks.QuantumNetwork` + object. Defaults to ``None``. Returns: - bool: Unitality condition. + ndarray: Choi operator of the quantum network. """ - if precision_tol < 0.0: - raise_error( - ValueError, - f"``precision_tol`` must be non-negative float, but it is {precision_tol}", - ) + if backend is None: # pragma: no cover + backend = self._backend - if order is None and self._backend.__class__.__name__ == "TensorflowBackend": - order = "euclidean" + if self.is_pure() and not full: + return backend.cast(self._tensor, dtype=self._tensor.dtype) - self._matrix = self._full() - self._pure = False + tensor = self.full(backend) if self.is_pure() else self._tensor - partial_trace = self._einsum("jkjl -> kl", self._matrix) - identity = self._backend.cast( - np.eye(partial_trace.shape[0]), dtype=partial_trace.dtype - ) + n = len(self.partition) + order = self._order_tensor_to_operator(n) - norm = self._backend.calculate_norm_density_matrix( - partial_trace - identity, - order=order, + operator = self._backend.np.transpose( + tensor.reshape(tuple(np.repeat(self.partition, 2))), order ) - return float(norm) <= precision_tol + return backend.cast(operator, dtype=self._tensor.dtype) - def is_causal( + def matrix(self, backend=None): + """Returns the Choi operator of the quantum network in the matrix form. + The shape of the returned operator is :math:`(self.dims, self.dims)`. + + Args: + backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used + to return the Choi operator. If ``None``, defaults to the backend defined + when initializing the :class:`qibo.quantum_info.quantum_networks.QuantumNetwork` + object. Defaults to ``None``. + + Returns: + ndarray: Choi operator of the quantum network. + """ + return self.operator(full=True, backend=backend).reshape((self.dims, self.dims)) + + def is_pure(self): + """Returns bool indicading if the Choi operator of the network is pure.""" + return self._pure + + 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 satisfies the causal order condition. + """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` is Hermitian. - Causality is calculated as distance between partial trace of :math:`\\mathcal{E}` - and the Identity operator :math:`I`, 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 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``. - 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}`. + precision_tol (float, optional): threshold :math:`\\epsilon` that defines if + Choi operator of the network is :math:`\\epsilon`-close to Hermicity in + the norm given by ``order``. Defaults to :math:`10^{-8}`. Returns: - bool: Causal order 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( @@ -210,101 +285,61 @@ def is_causal( if order is None and self._backend.__class__.__name__ == "TensorflowBackend": order = "euclidean" - self._matrix = self._full() - self._pure = False + if self.is_pure(): # if the input is pure, it is always hermitian + return True - partial_trace = self._einsum("jklk -> jl", self._matrix) - identity = self._backend.cast( - np.eye(partial_trace.shape[0]), dtype=partial_trace.dtype + reshaped = self._backend.cast( + self.matrix(), + dtype=self._tensor.dtype, ) + if self._backend.__class__.__name__ == "PyTorchBackend": + adjoint = self._backend.np.transpose(reshaped, (1, 0)) + else: + adjoint = self._backend.np.transpose(reshaped) - norm = self._backend.calculate_norm_density_matrix( - partial_trace - identity, - order=order, - ) + mat_diff = self._backend.np.conj(adjoint) - reshaped + norm = self._backend.calculate_norm_density_matrix(mat_diff, order=order) 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}` 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`. Returns: bool: Positive-semidefinite condition. """ - self._matrix = self._full() - self._pure = False + if precision_tol < 0.0: + raise_error( + ValueError, + f"``precision_tol`` must be non-negative float, but it is {precision_tol}", + ) + + if self.is_pure(): # if the input is pure, it is always positive semidefinite + return True + + reshaped = self._backend.cast( + self.matrix(), + dtype=self._tensor.dtype, + ) - reshaped = self._backend.np.reshape(self._matrix, (self.dims, self.dims)) if self.is_hermitian(): eigenvalues = self._backend.calculate_eigenvalues(reshaped) else: - if self._backend.__class__.__name__ in [ - "CupyBackend", - "CuQuantumBackend", - ]: # pragma: no cover - reshaped = np.array(reshaped.tolist(), dtype=reshaped.dtype) - eigenvalues = self._backend.calculate_eigenvalues(reshaped, hermitian=False) + return False return all( self._backend.np.real(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 = self._backend.cast(self._matrix, copy=True) - - if self.is_pure(): - return self._einsum( - "kj,ml,jl -> km", matrix, self._backend.np.conj(matrix), state - ) - - return self._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 @@ -313,97 +348,40 @@ def link_product(self, second_network, subscripts: str = "ij,jk -> ik"): 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 `_. + `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 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.", - ) + return link_product(subscripts, self, second_network, backend=self._backend) - first_matrix = self._full() - second_matrix = second_network._full() # pylint: disable=W0212 - - if super_subscripts: - cexpr = "jklmnopq,klop->jmnq" - return QuantumNetwork( - self._einsum(cexpr, first_matrix, second_matrix), - [self.partition[0] + self.partition[-1]], - backend=self._backend, - ) - - cexpr = "jkab,klbc->jlac" - - if inv_subscripts: - return QuantumNetwork( - self._einsum(cexpr, second_matrix, first_matrix), - [second_network.partition[0], self.partition[1]], - backend=self._backend, - ) - - return QuantumNetwork( - self._einsum(cexpr, first_matrix, second_matrix), - [self.partition[0], second_network.partition[1]], + def copy(self): + """Returns a copy of the :class:`qibo.quantum_info.QuantumNetwork` object.""" + return self.__class__( + self._backend.np.copy(self._tensor), + partition=self.partition, + system_input=self.system_input, + pure=self._pure, backend=self._backend, ) - def copy(self): - """Returns a copy of the :class:`qibo.quantum_info.quantum_networks.QuantumNetwork` object.""" + def conj(self): + """Returns the conjugate of the quantum network.""" return self.__class__( - self._backend.np.copy(self._matrix), + self._backend.np.conj(self._tensor), partition=self.partition, - system_output=self.system_output, + system_input=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. @@ -424,23 +402,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, ) @@ -468,19 +446,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._tensor, 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, ) @@ -513,9 +491,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._tensor / number, partition=self.partition, - system_output=self.system_output, + system_input=self.system_input, pure=self.is_pure(), backend=self._backend, ) @@ -579,29 +557,21 @@ 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) # pylint: disable=E0606 def __str__(self): """Method to define how to print relevant information of the quantum network.""" - string_in = ", ".join( - [ - str(self.partition[k]) - for k in range(len(self.partition)) - if not self.system_output[k] - ] - ) + systems = [] - string_out = ", ".join( - [ - str(self.partition[k]) - for k in range(len(self.partition)) - if self.system_output[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_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( @@ -624,11 +594,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): @@ -637,55 +607,512 @@ def _run_checks(self, partition, system_output, pure): f"``pure`` must be type ``bool``, but it is type ``{type(pure)}``.", ) - def _set_tensor_and_parameters(self): - """Sets tensor based on inputs.""" + @staticmethod + def _check_system_input(system_input, partition) -> Tuple[bool]: + """ + If `system_input` not defined, assume the network follows the order of a quantum Comb. + """ + + if system_input is None: + system_input = [ + False, + ] * len(partition) + for k in range(len(partition) // 2): + system_input[k * 2] = True + return tuple(system_input) + + def _set_parameters(self): + """Standarize the parameters.""" self._backend = _check_backend(self._backend) + self.partition = tuple(self.partition) + + self.system_input = self._check_system_input(self.system_input, self.partition) + self._einsum = self._backend.np.einsum + self._tensordot = self._backend.np.tensordot + self._tensor = self._backend.cast(self._tensor, dtype=self._tensor.dtype) + + if self._pure: + if np.prod(tuple(self._tensor.shape)) != np.prod(tuple(self.partition)): + raise_error( + ValueError, + "``partition`` does not match the shape of the input matrix. " + + f"Cannot reshape matrix of size {self._tensor.shape} " + + f"to partition {self.partition}.", + ) + self._tensor = self._backend.np.reshape(self._tensor, self.partition) + else: + if np.prod(tuple(self._tensor.shape)) != np.prod( + tuple(dim**2 for dim in self.partition) + ): + raise_error( + ValueError, + "``partition`` does not match the shape of the input matrix. " + + f"Cannot reshape matrix of size {self._tensor.shape} " + + f"to partition {self.partition}.", + ) + matrix_partition = [dim**2 for dim in self.partition] + self._tensor = self._backend.np.reshape(self._tensor, matrix_partition) + + def full(self, update: bool = False, backend=None): + """Convert the internal representation to the full tensor of the network. + + Args: + update (bool, optional): If ``True``, updates the internal representation of the + network to the full tensor. Defaults to ``False``. + backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in + calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. + + Returns: + ndarray: full reprentation of the quantum network. + """ + if backend is None: # pragma: no cover + backend = self._backend + tensor = self._backend.np.copy(self._tensor) + tensor = backend.cast(tensor, dtype=self._tensor.dtype) + conj = backend.np.conj + + if self.is_pure(): + # Reshapes input matrix based on purity. + tensor.reshape(self.dims) + if self._backend.__class__.__name__ == "PyTorchBackend": + tensor = self._tensordot(tensor, conj(tensor), dims=0) + else: + tensor = self._tensordot(tensor, conj(tensor), axes=0) + tensor = self._operator_to_tensor(tensor, self.partition) + + if update: + self._tensor = tensor + self._pure = False + + return tensor + - if isinstance(self.partition, list): - self.partition = tuple(self.partition) +class QuantumComb(QuantumNetwork): + """Stores a Quantum comb, which is a network in which the systems follows a sequential order. - try: - if self._pure: - self._matrix = self._backend.np.reshape(self._matrix, self.partition) + It is also called the *non-Markovian quantum process* in many literatures. + A quantum comb is a quantum network of the form :math:`J[┍i1┑,┕o1┙,┍i2┑,┕o2┙, ...]`, + where the process first take an input state from system :math:`i1`, + then output a state to system :math:`o1`, and so on. + This is a non-Markovian process as the output of the system :math:`o2` may depend on + what happened in systems :math:`i1`, and :math:`o1`. + + A quantum channel is a special case of quantum comb, where there are only one input + system and one output system. + + Args: + tensor (ndarray): the tensor representations of the quantum Comb. + partition (List[int] or Tuple[int]): partition of ``matrix``. + system_input (List[bool] or Tuple[bool], optional): mask on the input system of the + Choi operator. If ``None``, defaults to + ``(True,False,True,False,...)``, where ``len(system_input)=len(partition)``. + Defaults to ``None``. + pure (bool, optional): ``True`` when ``tensor`` is a "pure" representation (e.g. a pure + state, a unitary operator, etc.), ``False`` otherwise. Defaults to ``False``. + backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in + calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. + """ + + def __init__( + self, + tensor, + partition: Optional[Union[List[int], Tuple[int]]] = None, + system_input: Optional[Union[List[bool], Tuple[bool]]] = None, + pure: bool = False, + backend=None, + ): + if partition is None: + if pure: + partition = tensor.shape else: - matrix_partition = self.partition * 2 - self._matrix = self._backend.np.reshape(self._matrix, matrix_partition) - except: + partition = tuple(int(np.sqrt(d)) for d in tensor.shape) + if len(partition) % 2 != 0: 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}", + "A quantum comb should only contain equal number of input and output systems. " + + "For general quantum networks, one should use the ``QuantumNetwork`` class.", + ) + if system_input is not None: + warning("system_input is ignored for QuantumComb") + + super().__init__( + tensor, partition, [True, False] * (len(partition) // 2), pure, backend + ) + + def is_causal( + self, order: Optional[Union[int, str]] = None, precision_tol: float = 1e-8 + ): + """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` satisfies causal order + + Causality is calculated based on a recursive constrains. + This method reduce a n-comb to a (n-1)-comb at each step, + and checks if the reduced comb is independent on the last output system. + + Args: + order (str or int, optional): order of the norm. Defaults to ``None``. + precision_tol (float, optional): threshold :math:`\\epsilon` that defines + if Choi operator of the network is :math:`\\epsilon`-close to causality + in the norm given by ``order``. Defaults to :math:`10^{-8}`. + + Returns: + bool: Causal order condition. + """ + if precision_tol < 0.0: + raise_error( + ValueError, + f"``precision_tol`` must be non-negative float, but it is {precision_tol}", ) - if 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) + if order is None and self._backend.__class__.__name__ == "TensorflowBackend": + order = "euclidean" + + backend = self._backend + + dim_out = self.partition[-1] + dim_in = self.partition[-2] + + trace_out = TraceOperation(dim_out, backend=backend).full() + trace_in = TraceOperation(dim_in, backend=backend).full() + + if self._backend.__class__.__name__ == "PyTorchBackend": + reduced = self._tensordot(self.full(), trace_out, dims=([-1], [0])) + sub_comb = self._tensordot(reduced, trace_in, dims=([-1], [0])) + expected = self._tensordot(sub_comb, trace_in / dim_in, dims=0) else: - self.system_output = tuple(self.system_output) + 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) - def _full(self): - """Reshapes input matrix based on purity.""" - matrix = self._backend.cast(self._matrix, copy=True) + norm = self._backend.calculate_norm(reduced - expected, order=order) - if self.is_pure(): - matrix = self._einsum( - "jk,lm -> kjml", matrix, self._backend.np.conj(matrix) + if float(norm) > precision_tol: + return False + + if len(self.partition) == 2: + return True + + return QuantumComb( + sub_comb, self.partition[:-2], pure=False, backend=self._backend + ).is_causal(order, precision_tol) + + @classmethod + def from_operator( + cls, operator, partition=None, inverse=False, pure=False, backend=None + ): # pylint: disable=W0237 + comb = super().from_operator(operator, partition, None, pure, backend) + if inverse: + # Convert mathmetical convention of Choi operator to physical convention + comb.partition = comb.partition[::-1] + comb._tensor = comb._tensor.T # pylint: disable=W0212 + return comb + + +class QuantumChannel(QuantumComb): + """Stores a Quantum channel, which is a special case of quantum comb. + + A quantum channel is a quantum comb with only one input and one output. + This class includes all quantum channels, unitary operators, and quantum states. + + To construct a `QuantumChannel` object, one can use the `QuantumNetwork.from_nparray` method. + **Note**: if one try to construct a quantum network from a unitary operator or Choi operator, + the first system will be the output. + However, here we assume the first system is the input system. + It is important to specify `inverse=True` when constructing by `QuantumNetwork.from_nparray`. + + Args: + tensor (ndarray): the tensor representations of the quantum comb. + partition (List[int] or Tuple[int], optional): partition of ``matrix``. + If not provided and `system_input` is `None`, assume the input is a quantum state, + whose input is a trivial system. If `system_input` is set to `True`, + assume the input is an observable, whose output is a trivial system. + system_input (List[bool] or Tuple[bool], optional): mask on the input system of the + Choi operator. If ``None`` the default is ``(True,False)``. + Defaults to ``None``. + pure (bool, optional): ``True`` when ``tensor`` is a "pure" representation (e.g. a pure + state, a unitary operator, etc.), ``False`` otherwise. Defaults to ``False``. + backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in + calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. + """ + + def __init__( + self, + tensor, + partition: Optional[Union[List[int], Tuple[int]]] = None, + system_input: Optional[Union[List[bool], Tuple[bool]]] = None, + pure: bool = False, + backend=None, + ): + if isinstance(partition, int): + partition = (partition,) + + if partition is not None: + if len(partition) > 2: + raise_error( + ValueError, + "A quantum channel should only contain one input system and one output system." + + "For general quantum networks, one should use the ``QuantumNetwork`` class.", + ) + if len(partition) == 1: + if system_input is None: # Assume the input is a quantum state + partition = (1, partition[0]) + else: + if isinstance(system_input, bool): + system_input = (system_input,) + + partition = ( + (partition[0], 1) if system_input[0] else (1, partition[0]) + ) + + super().__init__(tensor, partition, pure=pure, backend=backend) + + def is_unital( + self, order: Optional[Union[int, str]] = None, precision_tol: float = 1e-8 + ): + """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` is unital. + + A map is unital if it preserves the identity operator. + Unitality is calculated as distance between the partial trace of :math:`\\mathcal{J}` + and the Identity operator :math:`I`, with respect to a given norm. + Default is the ``Hilbert-Schmidt`` norm (also known as ``Frobenius`` norm). + + For specifications on the other possible values of the + parameter ``order`` for the ``tensorflow`` backend, please refer to + `tensorflow.norm `_. + 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" + + backend = self._backend + + dim_out = self.partition[1] + dim_in = self.partition[0] + + trace_out = TraceOperation(dim_out, backend=backend).full() + trace_in = TraceOperation(dim_in, backend=backend).full() + + if self._backend.__class__.__name__ == "PyTorchBackend": + reduced = self._tensordot(self.full(), trace_in, dims=([0], [0])) + sub_comb = self._tensordot( + reduced, + trace_out, + dims=([0], [0]), ) + expected = self._tensordot(trace_out / dim_out, sub_comb, dims=0) + else: + reduced = self._tensordot(self.full(), trace_in, axes=(0, 0)) + sub_comb = self._tensordot(reduced, trace_out, axes=(0, 0)) + expected = self._tensordot(trace_out / dim_out, sub_comb, axes=0) + + norm = self._backend.calculate_norm((reduced - expected), order=order) + if float(norm) > precision_tol: + return False + + if len(self.partition) == 2: + return True - return matrix + # 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 _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) + 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. - return bool(re.match(pattern_two, subscripts)), bool( - re.match(pattern_four, subscripts) + 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)`. + """ + operator = self.copy().operator() + conj = self._backend.np.conj + + if self.is_pure(): + return self._einsum("ij,lk,il", operator, conj(operator), state) + + return self._einsum("ijkl, jl", operator, state) + + +def link_product( + subscripts: str, + *operands: QuantumNetwork, + backend=None, + surpress_warning=False, +): + """Link product between two quantum networks. + + The link product is not commutative. Here, we assume that + :math:`A.\\textup{link_product}(B)` means "applying :math:`B` to :math:`A`". + However, the ``link_product`` is associative, so we override the `@` operation + in order to simplify notation. + + Args: + subscripts (str, optional): Specifies the subscript for summation using + the Einstein summation convention. For more details, please refer to + `numpy.einsum `_. + 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 the link product between two quantum networks. + """ + + if not isinstance(subscripts, str): + raise_error( + TypeError, + f"subscripts must be type str, but it is type {type(subscripts)}.", ) + + for i, operand in enumerate(operands): + if not isinstance(operand, QuantumNetwork): + raise_error(TypeError, f"The {i}-th operator is not a ``QuantumNetwork``.") + + if backend is None: # pragma: no cover + backend = operands[0]._backend # pylint: disable=W0212 + + tensors = [ + ( + backend.to_numpy(operand.full()) + if operand.is_pure() + else backend.to_numpy(operand._tensor) # pylint: disable=W0212 + ) + for operand in operands + ] + + # keep track of the `partition` and `system_input` of the network + _, contracrtion_list = np.einsum_path( + subscripts, *tensors, optimize=False, einsum_call=True + ) + + inds, idx_rm, einsum_str, _, _ = contracrtion_list[0] + input_str, results_index = einsum_str.split("->") + inputs = input_str.split(",") + + # Warning if the same index connects two input or two output systems + if not surpress_warning: + for ind in idx_rm: + found = 0 + for i, script in enumerate(inputs): + index = script.find(ind) + if index < 0: + continue + found += 1 + if found > 1 and is_input == operands[inds[i]].system_input[index]: + warning( + f"Index {ind} connects two {'input' if is_input else 'output'} systems." + ) + is_input = operands[inds[i]].system_input[index] + if found > 2: + warning( + f"Index {ind} appears multiple times in the input subscripts {input_str}." + ) + + # set correct order of the `partition` and `system_input` + partition = [] + system_input = [] + for ind in results_index: + for i, script in enumerate(inputs): + index = script.find(ind) + if index < 0: + continue + + partition.append(operands[inds[i]].partition[index]) + system_input.append(operands[inds[i]].system_input[index]) + + new_tensor = np.einsum(subscripts, *tensors) + + return QuantumNetwork(new_tensor, partition, system_input, backend=backend) + + +class IdentityChannel(QuantumChannel): + """The Identity channel with the given dimension. + + Args: + dim (int): Dimension of the Identity operator. + backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in + calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. + """ + + def __init__(self, dim: int, backend=None): + + identity = np.eye(dim, dtype=complex) + identity = backend.cast(identity, dtype=identity.dtype) + super().__init__(identity, [dim, dim], pure=True, backend=backend) + + +class TraceOperation(QuantumNetwork): + """The Trace operator with the given dimension. + + Args: + dim (int): Dimension of the Trace operator. + backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in + calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. + """ + + def __init__(self, dim: int, backend=None): + + identity = np.eye(dim, dtype=complex) + identity = backend.cast(identity, dtype=identity.dtype) + super().__init__(identity, [dim], [True], pure=False, backend=backend) diff --git a/src/qibo/quantum_info/superoperator_transformations.py b/src/qibo/quantum_info/superoperator_transformations.py index de1b76f2ec..73bce85f97 100644 --- a/src/qibo/quantum_info/superoperator_transformations.py +++ b/src/qibo/quantum_info/superoperator_transformations.py @@ -170,6 +170,8 @@ def to_choi(channel, order: str = "row", backend=None): Returns: ndarray: quantum channel in its Choi representation. """ + backend = _check_backend(backend) + channel = vectorization(channel, order=order, backend=backend) channel = backend.np.outer(channel, backend.np.conj(channel)) @@ -194,6 +196,8 @@ def to_liouville(channel, order: str = "row", backend=None): Returns: ndarray: quantum channel in its Liouville representation. """ + backend = _check_backend(backend) + channel = to_choi(channel, order=order, backend=backend) channel = _reshuffling(channel, order=order, backend=backend) @@ -229,7 +233,9 @@ def to_pauli_liouville( Returns: ndarray: quantum channel in its Pauli-Liouville representation. """ - from qibo.quantum_info.basis import comp_basis_to_pauli + from qibo.quantum_info.basis import comp_basis_to_pauli # pylint: disable=C0415 + + backend = _check_backend(backend) nqubits = int(np.log2(channel.shape[0])) @@ -779,7 +785,7 @@ def kraus_to_chi( Returns: ndarray: Chi-matrix representation of the Kraus channel. """ - from qibo.quantum_info.basis import comp_basis_to_pauli + from qibo.quantum_info.basis import comp_basis_to_pauli # pylint: disable=C0415 backend = _check_backend(backend) @@ -1159,7 +1165,7 @@ def pauli_to_liouville( Returns: ndarray: superoperator in the Liouville representation. """ - from qibo.quantum_info.basis import pauli_to_comp_basis + from qibo.quantum_info.basis import pauli_to_comp_basis # pylint: disable=C0415 backend = _check_backend(backend) diff --git a/src/qibo/tomography/__init__.py b/src/qibo/tomography/__init__.py new file mode 100644 index 0000000000..c6a0c18621 --- /dev/null +++ b/src/qibo/tomography/__init__.py @@ -0,0 +1 @@ +from qibo.tomography.gate_set_tomography import * diff --git a/src/qibo/tomography/gate_set_tomography.py b/src/qibo/tomography/gate_set_tomography.py new file mode 100644 index 0000000000..127350953e --- /dev/null +++ b/src/qibo/tomography/gate_set_tomography.py @@ -0,0 +1,338 @@ +from functools import cache +from inspect import signature +from itertools import product +from random import Random +from typing import List, Union + +import numpy as np +from sympy import S + +from qibo import Circuit, gates, symbols +from qibo.backends import _check_backend +from qibo.config import raise_error +from qibo.hamiltonians import SymbolicHamiltonian +from qibo.transpiler.optimizer import Preprocessing +from qibo.transpiler.pipeline import Passes +from qibo.transpiler.placer import Random +from qibo.transpiler.router import Sabre +from qibo.transpiler.unroller import NativeGates, Unroller + +SUPPORTED_NQUBITS = [1, 2] +"""Supported nqubits for GST.""" + + +def _check_nqubits(nqubits): + if nqubits not in SUPPORTED_NQUBITS: + raise_error( + ValueError, + f"nqubits given as {nqubits}. nqubits needs to be either 1 or 2.", + ) + + +@cache +def _gates(nqubits) -> List: + """Gates implementing all the GST state preparations. + + Args: + nqubits (int): Number of qubits for the circuit. + Returns: + List(:class:`qibo.gates.Gate`): gates used to prepare the possible states. + """ + + return list( + product( + [(gates.I,), (gates.X,), (gates.H,), (gates.H, gates.S)], repeat=nqubits + ) + ) + + +@cache +def _measurements(nqubits: int) -> List: + """Measurement gates implementing all the GST measurement bases. + + Args: + nqubits (int): Number of qubits for the circuit. + Returns: + List(:class:`qibo.gates.Gate`): gates implementing the possible measurement bases. + """ + + return list(product([gates.Z, gates.X, gates.Y, gates.Z], repeat=nqubits)) + + +@cache +def _observables(nqubits: int) -> List: + """All the observables measured in the GST protocol. + + Args: + nqubits (int): number of qubits for the circuit. + + Returns: + List[:class:`qibo.symbols.Symbol`]: all possible observables to be measured. + """ + + return list(product([symbols.I, symbols.Z, symbols.Z, symbols.Z], repeat=nqubits)) + + +@cache +def _get_observable(j: int, nqubits: int): + """Returns the :math:`j`-th observable. The :math:`j`-th observable is expressed as a base-4 indexing and is given by + + .. math:: + j \\in \\{0, 1, 2, 3\\}^{\\otimes n} \\equiv \\{ I, X, Y, Z\\}^{\\otimes n}. + + Args: + j (int): index of the measurement basis (in base-4) + nqubits (int): number of qubits. + + Returns: + List[:class:`qibo.hamiltonians.SymbolicHamiltonian`]: observables represented by symbolic Hamiltonians. + """ + + if j == 0: + _check_nqubits(nqubits) + observables = _observables(nqubits)[j] + observable = S(1) + for q, obs in enumerate(observables): + if obs is not symbols.I: + observable *= obs(q) + return SymbolicHamiltonian(observable, nqubits=nqubits) + + +@cache +def _prepare_state(k, nqubits): + """Prepares the :math:`k`-th state for an :math:`n`-qubits (`nqubits`) circuit. + Using base-4 indexing for :math:`k`, + + .. math:: + k \\in \\{0, 1, 2, 3\\}^{\\otimes n} \\equiv \\{ 0\\rangle\\langle0|, |1\\rangle\\langle1|, + |+\\rangle\\langle +|, |y+\\rangle\\langle y+|\\}^{\\otimes n}. + + Args: + k (int): index of the state to be prepared. + nqubits (int): Number of qubits. + + Returns: + list(:class:`qibo.gates.Gate`): gates that prepare the :math:`k`-th state. + """ + + _check_nqubits(nqubits) + gates = _gates(nqubits)[k] + return [gate(q) for q in range(len(gates)) for gate in gates[q]] + + +@cache +def _measurement_basis(j, nqubits): + """Constructs the :math:`j`-th measurement basis element for an :math:`n`-qubits (`nqubits`) circuit. + Base-4 indexing is used for the :math:`j`-th measurement basis and is given by + + .. math:: + j \\in \\{0, 1, 2, 3\\}^{\\otimes n} \\equiv \\{ I, X, Y, Z\\}^{\\otimes n}. + + Args: + j (int): index of the measurement basis element. + nqubits (int): number of qubits. + + Returns: + List[:class:`qibo.gates.Gate`]: gates forming the :math:`j`-th element + of the Pauli measurement basis. + """ + + _check_nqubits(nqubits) + measurements = _measurements(nqubits)[j] + return [gates.M(q, basis=measurements[q]) for q in range(len(measurements))] + + +def _gate_tomography( + nqubits: int, + gate: gates.Gate = None, + nshots: int = int(1e4), + noise_model=None, + backend=None, + transpiler=None, +): + """Runs gate tomography for a 1 or 2 qubit gate. + + It obtains a :math:`4^{n} \\times 4^{n}` matrix, where :math:`n` is the number of qubits. + This matrix needs to be post-processed to get the Pauli-Liouville representation of the gate. + The matrix has elements :math:`\\text{tr}(M_{j} \\, \\rho_{k})` or + :math:`\\text{tr}(M_{j} \\, O_{l} \\rho_{k})`, depending on whether the gate + :math:`O_{l}` is present or not. + + Args: + nqubits (int): number of qubits of the gate. + gate (:class:`qibo.gates.Gate`, optional): gate to perform gate tomography on. + If ``None``, then gate tomography will be performed for an empty circuit. + Defaults to ``None``. + nshots (int, optional): number of shots used. + noise_model (:class:`qibo.noise.NoiseModel`, optional): noise model applied to simulate + noisy computations. + backend (:class:`qibo.backends.abstract.Backend`, optional): backend + to be used in the execution. If ``None``, it uses + :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. + + Returns: + ndarray: matrix approximating the input gate. + """ + + # Check if gate is 1 or 2 qubit gate. + _check_nqubits(nqubits) + + backend = _check_backend(backend) + + if gate is not None: + if nqubits != len(gate.qubits): + raise_error( + ValueError, + f"Mismatched inputs: nqubits given as {nqubits}. {gate} is a {len(gate.qubits)}-qubit gate.", + ) + gate = gate.__class__(*gate.qubits, **gate.init_kwargs) + + # GST for empty circuit or with gates + matrix_jk = np.zeros((4**nqubits, 4**nqubits)) + for k in range(4**nqubits): + circ = Circuit(nqubits, density_matrix=True) + circ.add(_prepare_state(k, nqubits)) + + if gate is not None: + circ.add(gate) + + for j in range(4**nqubits): + if j == 0: + exp_val = 1.0 + else: + new_circ = circ.copy() + measurements = _measurement_basis(j, nqubits) + new_circ.add(measurements) + observable = _get_observable(j, nqubits) + if noise_model is not None and backend.name != "qibolab": + new_circ = noise_model.apply(new_circ) + if transpiler is not None: + new_circ, _ = transpiler(new_circ) + exp_val = observable.expectation_from_samples( + backend.execute_circuit(new_circ, nshots=nshots).frequencies() + ) + matrix_jk[j, k] = exp_val + return matrix_jk + + +def GST( + gate_set: Union[tuple, set, list], + nshots=int(1e4), + noise_model=None, + include_empty=False, + pauli_liouville=False, + gauge_matrix=None, + backend=None, + transpiler=None, +): + """Runs Gate Set Tomography on the input ``gate_set``. + + Args: + gate_set (tuple or set or list): set of :class:`qibo.gates.Gate` to run GST on. + nshots (int, optional): number of shots used in Gate Set Tomography per gate. + Defaults to :math:`10^{4}`. + noise_model (:class:`qibo.noise.NoiseModel`, optional): noise model applied to simulate + noisy computations. + include_empty (bool, optional): if ``True``, additionally performs gate set tomography + for :math:`1`- and :math:`2`-qubit empty circuits, returning the corresponding empty + matrices in the first and second position of the ouput list. + pauli_liouville (bool, optional): if ``True``, returns the matrices in the + Pauli-Liouville representation. Defaults to ``False``. + gauge_matrix (ndarray, optional): gauge matrix transformation to the Pauli-Liouville + representation. Defaults to + + .. math:: + \\begin{pmatrix} + 1 & 1 & 1 & 1 \\\\ + 0 & 0 & 1 & 0 \\\\ + 0 & 0 & 0 & 1 \\\\ + 1 & -1 & 0 & 0 \\\\ + \\end{pmatrix} + + backend (:class:`qibo.backends.abstract.Backend`, optional): backend + to be used in the execution. If ``None``, it uses + :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. + + + Returns: + List(ndarray): input ``gate_set`` represented by matrices estimaded via GST. + """ + + backend = _check_backend(backend) + + if backend.name == "qibolab" and transpiler is None: # pragma: no cover + transpiler = Passes( + connectivity=backend.platform.topology, + passes=[ + Preprocessing(backend.platform.topology), + Random(backend.platform.topology), + Sabre(backend.platform.topology), + Unroller(NativeGates.default()), + ], + ) + + matrices = [] + empty_matrices = [] + if include_empty or pauli_liouville: + for nqubits in SUPPORTED_NQUBITS: + empty_matrix = _gate_tomography( + nqubits=nqubits, + gate=None, + nshots=nshots, + noise_model=noise_model, + backend=backend, + transpiler=transpiler, + ) + empty_matrices.append(empty_matrix) + + for gate in gate_set: + if gate is not None: + init_args = signature(gate).parameters + if "q" in init_args: + nqubits = 1 + elif "q0" in init_args and "q1" in init_args and "q2" not in init_args: + nqubits = 2 + else: + raise_error( + RuntimeError, + f"Gate {gate} is not supported for `GST`, only 1- and 2-qubits gates are supported.", + ) + gate = gate(*range(nqubits)) + + matrices.append( + _gate_tomography( + nqubits=nqubits, + gate=gate, + nshots=nshots, + noise_model=noise_model, + backend=backend, + transpiler=transpiler, + ) + ) + + if pauli_liouville: + if gauge_matrix is not None: + if np.linalg.det(gauge_matrix) == 0: + raise_error(ValueError, "Matrix is not invertible") + else: + gauge_matrix = np.array( + [[1, 1, 1, 1], [0, 0, 1, 0], [0, 0, 0, 1], [1, -1, 0, 0]] + ) + PL_matrices = [] + gauge_matrix_1q = gauge_matrix + gauge_matrix_2q = np.kron(gauge_matrix, gauge_matrix) + for matrix in matrices: + gauge_matrix = gauge_matrix_1q if matrix.shape[0] == 4 else gauge_matrix_2q + empty = empty_matrices[0] if matrix.shape[0] == 4 else empty_matrices[1] + PL_matrices.append( + gauge_matrix + @ np.linalg.inv(empty) + @ matrix + @ np.linalg.inv(gauge_matrix) + ) + matrices = PL_matrices + + if include_empty: + matrices = empty_matrices + matrices + + return matrices diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py index 5ebbbea3d3..daa8ab53f7 100644 --- a/tests/test_quantum_info_quantum_networks.py +++ b/tests/test_quantum_info_quantum_networks.py @@ -4,7 +4,14 @@ import pytest from qibo import gates -from qibo.quantum_info.quantum_networks import QuantumNetwork +from qibo.quantum_info.quantum_networks import ( + IdentityChannel, + QuantumChannel, + QuantumComb, + QuantumNetwork, + TraceOperation, + link_product, +) from qibo.quantum_info.random_ensembles import ( random_density_matrix, random_gaussian_matrix, @@ -18,7 +25,13 @@ def test_errors(backend): nqubits = len(channel.target_qubits) dims = 2**nqubits partition = (dims, dims) - network = QuantumNetwork( + network = QuantumNetwork.from_operator( + channel.to_choi(backend=backend), partition, backend=backend + ) + quantum_comb = QuantumComb.from_operator( + channel.to_choi(backend=backend), partition, backend=backend + ) + quantum_channel = QuantumChannel.from_operator( channel.to_choi(backend=backend), partition, backend=backend ) @@ -32,7 +45,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,20 +59,26 @@ 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): 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) with pytest.raises(ValueError): - network.is_unital(precision_tol=-1e-8) + network.is_positive_semidefinite(precision_tol=-1e-8) + + with pytest.raises(ValueError): + quantum_comb.is_causal(precision_tol=-1e-8) with pytest.raises(ValueError): - network.is_causal(precision_tol=-1e-8) + quantum_channel.is_unital(precision_tol=-1e-8) with pytest.raises(TypeError): network + 1 @@ -75,23 +94,20 @@ 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=backend) # Linking QuantumNetwork with non-QuantumNetwork with pytest.raises(TypeError): - network.link_product(network.matrix(backend)) + network.link_product(network.operator(backend=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 @@ -113,6 +129,44 @@ 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) + + 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) + + with pytest.raises(ValueError): + QuantumComb.from_operator(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, backend=backend) + + with pytest.raises(TypeError): + link_product("ij, i", quantum_comb, matrix, backend=backend) + + # raise warning + 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): + matrix = random_density_matrix(2**2, backend=backend) + with pytest.raises(ValueError): + QuantumNetwork._operator_to_tensor(matrix, (3,)) + def test_operational_logic(backend): lamb = float(np.random.rand()) @@ -120,7 +174,7 @@ def test_operational_logic(backend): nqubits = len(channel.target_qubits) dims = 2**nqubits partition = (dims, dims) - network = QuantumNetwork( + network = QuantumNetwork.from_operator( channel.to_choi(backend=backend), partition, backend=backend ) @@ -129,31 +183,40 @@ 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=backend), + (2 * network).operator(backend=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=backend), + (2 * network_state_pure).operator(full=True, backend=backend), ) # 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=backend), + (2.0 * network).operator(backend=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=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).matrix(backend), network.matrix(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).matrix(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), + backend.np.conj(network.operator(backend=backend)), ) @@ -165,20 +228,33 @@ def test_parameters(backend): dims = 2**nqubits partition = (dims, dims) - network = QuantumNetwork( - channel.to_choi(backend=backend), partition, backend=backend + choi = channel.to_choi(backend=backend) + + 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_operator( + rand, partition, backend=backend, inverse=True ) - 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, (True, False)) - assert network.is_causal() - assert network.is_unital() assert network.is_hermitian() assert network.is_positive_semidefinite() - assert network.is_channel() + 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): @@ -186,24 +262,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_operator(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_operator( + 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,jk -> ik", network_channel) backend.assert_allclose(state_output_network, state_output) - backend.assert_allclose( - state_output_link.matrix(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,51 +289,120 @@ 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_operator( + unitary_1, (dims, dims), pure=True, backend=backend, inverse=True ) - network_4 = QuantumNetwork( - unitary_1 @ unitary_2, (dims, dims), pure=True, backend=backend + network_2 = QuantumComb.from_operator( + unitary_2, (dims, dims), pure=True, backend=backend, inverse=True + ) + network_3 = QuantumComb.from_operator( + unitary_2 @ unitary_1, (dims, dims), pure=True, backend=backend, inverse=True + ) + network_4 = QuantumComb.from_operator( + unitary_1 @ unitary_2, (dims, dims), pure=True, backend=backend, inverse=True ) - test = network_1.link_product(network_2, subscript).to_full(backend=backend) + test = network_1.link_product(subscript, network_2).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(backend=backend, update=True), 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, update=True), 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)) + + # 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" 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) + unitary_2 = random_unitary(4, backend=backend) + non_channel = QuantumNetwork.from_operator( + rand_choi, + (2, 2, 2, 2), + system_input=(True, True, False, False), + backend=backend, + ) + 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_operator( + 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, backend=backend + ) + non_comb = QuantumComb( + non_comb.full(backend=backend), + (2, 2, 2, 2), + system_input=(True, False, True, False), + backend=backend, + ) + two_comb = link_product( + "ij kl, km on, i, o", + unitary_channel, + unitary_channel2, + TraceOperation(2, backend=backend), + TraceOperation(2, backend=backend), + backend=backend, + ) + two_comb = QuantumComb( + two_comb.full(backend=backend), + (2, 2, 2, 2), + system_input=(True, False, True, False), + backend=backend, + ) comb = random_density_matrix(2**4, backend=backend) channel = random_density_matrix(2**2, backend=backend) - comb_choi = QuantumNetwork( - comb, comb_partition, system_output=comb_sys_out, backend=backend + comb_choi = QuantumNetwork.from_operator( + comb, comb_partition, system_input=comb_sys_in, backend=backend ) - channel_choi = QuantumNetwork( - channel, channel_partition, system_output=channel_sys_out, backend=backend + channel_choi = QuantumNetwork.from_operator( + channel, channel_partition, system_input=channel_sys_in, backend=backend ) - test = comb_choi.link_product(channel_choi, subscript).to_full(backend) + 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.to_full(backend), atol=1e-5) + backend.assert_allclose(test, channel_choi2.full(backend), atol=1e-5) + + 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): @@ -270,7 +411,9 @@ 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.from_operator( + unitary, (dims, dims), pure=True, backend=backend, inverse=True + ) applied = network.apply(state) target = unitary @ state @ backend.np.conj(unitary).T @@ -283,11 +426,95 @@ 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_operator( + matrix, (dims, dims), pure=False, backend=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]" + assert network.__str__() == "J[┍4┑, ┕4┙]" + + +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) + test_ls = np.ones(old_shape) + n = len(test_ls.shape) // 2 + + 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): + assert (new_shape[i] - new_shape[i + n]) == -10 + + assert tuple(test_ls.transpose(order2op).transpose(order2tensor).shape) == old_shape + + +def test_predefined(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, + 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, + ) + + traced = link_product("ij,j", id_ch, tr_ch, backend=backend) + + backend.assert_allclose( + tr_ch.matrix(backend=backend), traced.matrix(backend=backend), atol=1e-8 + ) + + +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) + 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) + comb1 = QuantumComb.from_operator(vec, (4, 1), pure=True, backend=backend) + assert comb1.system_input == (True, False) + comb2 = QuantumComb.from_operator(vec, pure=True, backend=backend) + assert comb2.partition == (4, 1) + assert comb2.system_input == (True, False) + 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_operator(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 == (1, 4) + 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) + channel5 = QuantumChannel(tensor, pure=False, backend=backend) + assert channel5.partition == (2, 2) + assert channel5.system_input == (True, False) diff --git a/tests/test_tomography_gate_set_tomography.py b/tests/test_tomography_gate_set_tomography.py new file mode 100644 index 0000000000..bbed8cff06 --- /dev/null +++ b/tests/test_tomography_gate_set_tomography.py @@ -0,0 +1,305 @@ +from functools import reduce +from itertools import repeat + +import numpy as np +import pytest +from sympy import S + +from qibo import gates, symbols +from qibo.hamiltonians import SymbolicHamiltonian +from qibo.noise import DepolarizingError, NoiseModel +from qibo.quantum_info.superoperator_transformations import to_pauli_liouville +from qibo.tomography.gate_set_tomography import ( + GST, + _gate_tomography, + _get_observable, + _measurement_basis, + _prepare_state, +) +from qibo.transpiler.optimizer import Preprocessing +from qibo.transpiler.pipeline import Passes +from qibo.transpiler.placer import Random +from qibo.transpiler.router import Sabre +from qibo.transpiler.unroller import NativeGates, Unroller + + +def _compare_gates(g1, g2): + assert g1.__class__.__name__ == g2.__class__.__name__ + assert g1.qubits == g2.qubits + + +INDEX_NQUBITS = ( + list(zip(range(4), repeat(1, 4))) + + list(zip(range(16), repeat(2, 16))) + + [(0, 3), (17, 1)] +) + + +@pytest.mark.parametrize( + "k,nqubits", + INDEX_NQUBITS, +) +def test__prepare_state(k, nqubits): + correct_gates = { + 1: [[gates.I(0)], [gates.X(0)], [gates.H(0)], [gates.H(0), gates.S(0)]], + 2: [ + [gates.I(0), gates.I(1)], + [gates.I(0), gates.X(1)], + [gates.I(0), gates.H(1)], + [gates.I(0), gates.H(1), gates.S(1)], + [gates.X(0), gates.I(1)], + [gates.X(0), gates.X(1)], + [gates.X(0), gates.H(1)], + [gates.X(0), gates.H(1), gates.S(1)], + [gates.H(0), gates.I(1)], + [gates.H(0), gates.X(1)], + [gates.H(0), gates.H(1)], + [gates.H(0), gates.H(1), gates.S(1)], + [gates.H(0), gates.S(0), gates.I(1)], + [gates.H(0), gates.S(0), gates.X(1)], + [gates.H(0), gates.S(0), gates.H(1)], + [gates.H(0), gates.S(0), gates.H(1), gates.S(1)], + ], + } + errors = {(0, 3): ValueError, (17, 1): IndexError} + if (k, nqubits) in [(0, 3), (17, 1)]: + with pytest.raises(errors[(k, nqubits)]): + prepared_states = _prepare_state(k, nqubits) + else: + prepared_states = _prepare_state(k, nqubits) + for groundtruth, gate in zip(correct_gates[nqubits][k], prepared_states): + _compare_gates(groundtruth, gate) + + +@pytest.mark.parametrize( + "j,nqubits", + INDEX_NQUBITS, +) +def test__measurement_basis(j, nqubits): + correct_gates = { + 1: [ + [gates.M(0)], + [gates.M(0, basis=gates.X)], + [gates.M(0, basis=gates.Y)], + [gates.M(0, basis=gates.Z)], + ], + 2: [ + [gates.M(0), gates.M(1)], + [gates.M(0), gates.M(1, basis=gates.X)], + [gates.M(0), gates.M(1, basis=gates.Y)], + [gates.M(0), gates.M(1)], + [gates.M(0, basis=gates.X), gates.M(1)], + [gates.M(0, basis=gates.X), gates.M(1, basis=gates.X)], + [gates.M(0, basis=gates.X), gates.M(1, basis=gates.Y)], + [gates.M(0, basis=gates.X), gates.M(1)], + [gates.M(0, basis=gates.Y), gates.M(1)], + [gates.M(0, basis=gates.Y), gates.M(1, basis=gates.X)], + [gates.M(0, basis=gates.Y), gates.M(1, basis=gates.Y)], + [gates.M(0, basis=gates.Y), gates.M(1)], + [gates.M(0), gates.M(1)], + [gates.M(0), gates.M(1, basis=gates.X)], + [gates.M(0), gates.M(1, basis=gates.Y)], + [gates.M(0), gates.M(1)], + ], + } + errors = {(0, 3): ValueError, (17, 1): IndexError} + if (j, nqubits) in [(0, 3), (17, 1)]: + with pytest.raises(errors[(j, nqubits)]): + prepared_gates = _measurement_basis(j, nqubits) + else: + prepared_gates = _measurement_basis(j, nqubits) + for groundtruth, gate in zip(correct_gates[nqubits][j], prepared_gates): + _compare_gates(groundtruth, gate) + for g1, g2 in zip(groundtruth.basis, gate.basis): + _compare_gates(g1, g2) + + +@pytest.mark.parametrize( + "j,nqubits", + INDEX_NQUBITS, +) +def test__get_observable(j, nqubits): + correct_observables = { + 1: [ + (S(1),), + (symbols.Z(0),), + (symbols.Z(0),), + (symbols.Z(0),), + ], + 2: [ + (S(1), S(1)), + (S(1), symbols.Z(1)), + (S(1), symbols.Z(1)), + (S(1), symbols.Z(1)), + (symbols.Z(0), S(1)), + (symbols.Z(0), symbols.Z(1)), + (symbols.Z(0), symbols.Z(1)), + (symbols.Z(0), symbols.Z(1)), + (symbols.Z(0), S(1)), + (symbols.Z(0), symbols.Z(1)), + (symbols.Z(0), symbols.Z(1)), + (symbols.Z(0), symbols.Z(1)), + (symbols.Z(0), S(1)), + (symbols.Z(0), symbols.Z(1)), + (symbols.Z(0), symbols.Z(1)), + (symbols.Z(0), symbols.Z(1)), + ], + } + correct_observables[1] = [ + SymbolicHamiltonian(h[0]).form for h in correct_observables[1] + ] + correct_observables[2] = [ + SymbolicHamiltonian(reduce(lambda x, y: x * y, h)).form + for h in correct_observables[2] + ] + errors = {(0, 3): ValueError, (17, 1): IndexError} + if (j, nqubits) in [(0, 3), (17, 1)]: + with pytest.raises(errors[(j, nqubits)]): + prepared_observable = _get_observable(j, nqubits) + else: + prepared_observable = _get_observable(j, nqubits).form + groundtruth = correct_observables[nqubits][j] + assert groundtruth == prepared_observable + + +@pytest.mark.parametrize( + "nqubits, gate", + [ + (1, gates.CNOT(0, 1)), + (3, gates.TOFFOLI(0, 1, 2)), + ], +) +def test_gate_tomography_value_error(backend, nqubits, gate): + with pytest.raises(ValueError): + matrix_jk = _gate_tomography( + nqubits=nqubits, + gate=gate, + nshots=int(1e4), + noise_model=None, + backend=backend, + ) + + +def test_gate_tomography_noise_model(backend): + nqubits = 1 + gate = gates.H(0) + lam = 1.0 + noise_model = NoiseModel() + noise_model.add(DepolarizingError(lam)) + # return noise_model + target = _gate_tomography( + nqubits=nqubits, + gate=gate, + nshots=int(1e4), + noise_model=noise_model, + backend=backend, + ) + exact_matrix = np.array([[1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]) + backend.assert_allclose( + target, + exact_matrix, + atol=1e-1, + ) + + +@pytest.mark.parametrize( + "target_gates", + [[gates.SX(0), gates.Z(0), gates.CY(0, 1)], [gates.TOFFOLI(0, 1, 2)]], +) +@pytest.mark.parametrize("pauli_liouville", [False, True]) +def test_GST(backend, target_gates, pauli_liouville): + T = np.array([[1, 1, 1, 1], [0, 0, 1, 0], [0, 0, 0, 1], [1, -1, 0, 0]]) + T = backend.cast(T, dtype=T.dtype) + target_matrices = [g.matrix(backend=backend) for g in target_gates] + # superoperator representation of the target gates in the Pauli basis + target_matrices = [ + to_pauli_liouville(m, normalize=True, backend=backend) for m in target_matrices + ] + gate_set = [g.__class__ for g in target_gates] + + if len(target_gates) == 3: + empty_1q, empty_2q, *approx_gates = GST( + gate_set=gate_set, + nshots=int(1e4), + include_empty=True, + pauli_liouville=pauli_liouville, + backend=backend, + ) + print(type(empty_1q), type(empty_2q)) + T_2q = np.kron(T, T) + for target, estimate in zip(target_matrices, approx_gates): + if not pauli_liouville: + G = empty_1q if estimate.shape[0] == 4 else empty_2q + G_inv = np.linalg.inv(G) + T_matrix = T if estimate.shape[0] == 4 else T_2q + estimate = T_matrix @ G_inv @ estimate @ G_inv + backend.assert_allclose( + target, + estimate, + atol=1e-1, + ) + else: + with pytest.raises(RuntimeError): + empty_1q, empty_2q, *approx_gates = GST( + gate_set=[g.__class__ for g in target_gates], + nshots=int(1e4), + include_empty=True, + pauli_liouville=pauli_liouville, + backend=backend, + ) + + +def test_GST_invertible_matrix(): + T = np.array([[1, 1, 1, 1], [0, 0, 1, 0], [0, 0, 0, 1], [1, -1, 0, 0]]) + matrices = GST(gate_set=[], pauli_liouville=True, gauge_matrix=T) + assert True + + +def test_GST_non_invertible_matrix(): + T = np.array([[1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], [1, -1, 0, 0]]) + with pytest.raises(ValueError): + matrices = GST(gate_set=[], pauli_liouville=True, gauge_matrix=T) + + +def test_GST_with_transpiler(backend): + import networkx as nx + + target_gates = [gates.SX(0), gates.Z(0), gates.CNOT(0, 1)] + gate_set = [g.__class__ for g in target_gates] + # standard not transpiled GST + empty_1q, empty_2q, *approx_gates = GST( + gate_set=gate_set, + nshots=int(1e4), + include_empty=True, + pauli_liouville=False, + backend=backend, + transpiler=None, + ) + # define transpiler + connectivity = nx.Graph() + # star connectivity + connectivity.add_edges_from([(0, 2), (1, 2), (2, 3), (2, 4)]) + transpiler = Passes( + connectivity=connectivity, + passes=[ + Preprocessing(connectivity), + Random(connectivity), + Sabre(connectivity), + Unroller(NativeGates.default()), + ], + int_qubit_names=True, + ) + # transpiled GST + T_empty_1q, T_empty_2q, *T_approx_gates = GST( + gate_set=gate_set, + nshots=int(1e4), + include_empty=True, + pauli_liouville=False, + backend=backend, + transpiler=transpiler, + ) + + backend.assert_allclose(empty_1q, T_empty_1q, atol=1e-1) + backend.assert_allclose(empty_2q, T_empty_2q, atol=1e-1) + for standard, transpiled in zip(approx_gates, T_approx_gates): + backend.assert_allclose(standard, transpiled, atol=1e-1)