From f538629fe6fed1f07570655cfd86f720b838d36a Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 2 Feb 2024 18:37:23 -0800 Subject: [PATCH 01/54] [Test]Single qubit density matrix test compared with qiskit. --- test/density/test_density_op.py | 143 ++++++++++++++++++++++++ test/density/test_density_trace.py | 0 torchquantum/density/density_mat.py | 6 + torchquantum/device/noisedevices.py | 26 +++++ torchquantum/functional/gate_wrapper.py | 1 - 5 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 test/density/test_density_op.py create mode 100644 test/density/test_density_trace.py diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py new file mode 100644 index 00000000..7172c6ac --- /dev/null +++ b/test/density/test_density_op.py @@ -0,0 +1,143 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +# test the torchquantum.functional against the IBM Qiskit +import argparse +import pdb +import torchquantum as tq +import numpy as np + +import qiskit.circuit.library.standard_gates as qiskit_gate +from qiskit.quantum_info import DensityMatrix as qiskitDensity + +from unittest import TestCase +import qiskit.circuit.library as qiskit_library +from qiskit.quantum_info import Operator + +RND_TIMES = 100 + +single_gate_list = [ + {"qiskit": qiskit_gate.HGate, "tq": tq.h, "name": "Hadamard"}, + {"qiskit": qiskit_gate.XGate, "tq": tq.x, "name": "x"}, + {"qiskit": qiskit_gate.YGate, "tq": tq.y, "name": "y"}, + {"qiskit": qiskit_gate.ZGate, "tq": tq.z, "name": "z"}, + {"qiskit": qiskit_gate.SGate, "tq": tq.S, "name": "S"}, + {"qiskit": qiskit_gate.TGate, "tq": tq.T, "name": "T"}, + {"qiskit": qiskit_gate.SXGate, "tq": tq.SX, "name": "SX"}, + {"qiskit": qiskit_gate.SdgGate, "tq": tq.SDG, "name": "SDG"}, + {"qiskit": qiskit_gate.TdgGate, "tq": tq.TDG, "name": "TDG"} +] + +pair_list = [ + {"qiskit": qiskit_gate.HGate, "tq": tq.Hadamard}, + {"qiskit": None, "tq": tq.SHadamard}, + {"qiskit": qiskit_gate.XGate, "tq": tq.PauliX}, + {"qiskit": qiskit_gate.YGate, "tq": tq.PauliY}, + {"qiskit": qiskit_gate.ZGate, "tq": tq.PauliZ}, + {"qiskit": qiskit_gate.SGate, "tq": tq.S}, + {"qiskit": qiskit_gate.TGate, "tq": tq.T}, + {"qiskit": qiskit_gate.SXGate, "tq": tq.SX}, + {"qiskit": qiskit_gate.CXGate, "tq": tq.CNOT}, + {"qiskit": qiskit_gate.CYGate, "tq": tq.CY}, + {"qiskit": qiskit_gate.CZGate, "tq": tq.CZ}, + {"qiskit": qiskit_gate.RXGate, "tq": tq.RX}, + {"qiskit": qiskit_gate.RYGate, "tq": tq.RY}, + {"qiskit": qiskit_gate.RZGate, "tq": tq.RZ}, + {"qiskit": qiskit_gate.RXXGate, "tq": tq.RXX}, + {"qiskit": qiskit_gate.RYYGate, "tq": tq.RYY}, + {"qiskit": qiskit_gate.RZZGate, "tq": tq.RZZ}, + {"qiskit": qiskit_gate.RZXGate, "tq": tq.RZX}, + {"qiskit": qiskit_gate.SwapGate, "tq": tq.SWAP}, + # {'qiskit': qiskit_gate.?, 'tq': tq.SSWAP}, + {"qiskit": qiskit_gate.CSwapGate, "tq": tq.CSWAP}, + {"qiskit": qiskit_gate.CCXGate, "tq": tq.Toffoli}, + {"qiskit": qiskit_gate.PhaseGate, "tq": tq.PhaseShift}, + # {'qiskit': qiskit_gate.?, 'tq': tq.Rot}, + # {'qiskit': qiskit_gate.?, 'tq': tq.MultiRZ}, + {"qiskit": qiskit_gate.CRXGate, "tq": tq.CRX}, + {"qiskit": qiskit_gate.CRYGate, "tq": tq.CRY}, + {"qiskit": qiskit_gate.CRZGate, "tq": tq.CRZ}, + # {'qiskit': qiskit_gate.?, 'tq': tq.CRot}, + {"qiskit": qiskit_gate.UGate, "tq": tq.U}, + {"qiskit": qiskit_gate.U1Gate, "tq": tq.U1}, + {"qiskit": qiskit_gate.U2Gate, "tq": tq.U2}, + {"qiskit": qiskit_gate.U3Gate, "tq": tq.U3}, + {"qiskit": qiskit_gate.CUGate, "tq": tq.CU}, + {"qiskit": qiskit_gate.CU1Gate, "tq": tq.CU1}, + # {'qiskit': qiskit_gate.?, 'tq': tq.CU2}, + {"qiskit": qiskit_gate.CU3Gate, "tq": tq.CU3}, + {"qiskit": qiskit_gate.ECRGate, "tq": tq.ECR}, + # {"qiskit": qiskit_library.QFT, "tq": tq.QFT}, + {"qiskit": qiskit_gate.SdgGate, "tq": tq.SDG}, + {"qiskit": qiskit_gate.TdgGate, "tq": tq.TDG}, + {"qiskit": qiskit_gate.SXdgGate, "tq": tq.SXDG}, + {"qiskit": qiskit_gate.CHGate, "tq": tq.CH}, + {"qiskit": qiskit_gate.CCZGate, "tq": tq.CCZ}, + {"qiskit": qiskit_gate.iSwapGate, "tq": tq.ISWAP}, + {"qiskit": qiskit_gate.CSGate, "tq": tq.CS}, + {"qiskit": qiskit_gate.CSdgGate, "tq": tq.CSDG}, + {"qiskit": qiskit_gate.CSXGate, "tq": tq.CSX}, + {"qiskit": qiskit_gate.DCXGate, "tq": tq.DCX}, + {"qiskit": qiskit_gate.XXMinusYYGate, "tq": tq.XXMINYY}, + {"qiskit": qiskit_gate.XXPlusYYGate, "tq": tq.XXPLUSYY}, + {"qiskit": qiskit_gate.C3XGate, "tq": tq.C3X}, + {"qiskit": qiskit_gate.RGate, "tq": tq.R}, + {"qiskit": qiskit_gate.C4XGate, "tq": tq.C4X}, + {"qiskit": qiskit_gate.RCCXGate, "tq": tq.RCCX}, + {"qiskit": qiskit_gate.RC3XGate, "tq": tq.RC3X}, + {"qiskit": qiskit_gate.GlobalPhaseGate, "tq": tq.GlobalPhase}, + {"qiskit": qiskit_gate.C3SXGate, "tq": tq.C3SX}, +] + + +def density_is_close(mat1: np.ndarray, mat2: np.ndarray): + assert mat1.shape == mat2.shape + return np.allclose(mat1, mat2) + + +("Geeks : %2d, Portal : %5.2f" % (1, 05.333)) + + +class single_qubit(TestCase): + def compare_single_gate(self, gate_pair, qubit_num): + passed = True + for index in range(0, qubit_num): + qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) + gate_pair['tq'](qdev, [index]) + mat1 = np.array(qdev.get_2d_matrix(0)) + rho_qiskit = qiskitDensity.from_label('0' * qubit_num) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - index]) + mat2 = np.array(rho_qiskit.to_operator()) + if density_is_close(mat1, mat2): + print("Test passed for %s gate on qubit %d when qubit_number is %d!" % ( + gate_pair['name'], index, qubit_num)) + else: + passed = False + print("Test failed for %s gaet on qubit %d when qubit_number is %d!" % ( + gate_pair['name'], index, qubit_num)) + return passed + + def test_single_gates(self): + for i in range(0, len(single_gate_list)): + self.assertTrue(self.compare_single_gate(single_gate_list[i], 5)) diff --git a/test/density/test_density_trace.py b/test/density/test_density_trace.py new file mode 100644 index 00000000..e69de29b diff --git a/torchquantum/density/density_mat.py b/torchquantum/density/density_mat.py index 8260a01b..1bf406ea 100644 --- a/torchquantum/density/density_mat.py +++ b/torchquantum/density/density_mat.py @@ -126,6 +126,12 @@ def print_2d(self, index): _matrix = torch.reshape(self._matrix[index], [2 ** self.n_wires] * 2) print(_matrix) + + def get_2d_matrix(self, index): + _matrix = torch.reshape(self._matrix[index], [2 ** self.n_wires] * 2) + return _matrix + + def trace(self, index): """Calculate and return the trace of the density matrix at the given index. diff --git a/torchquantum/device/noisedevices.py b/torchquantum/device/noisedevices.py index 3da88eff..0c548023 100644 --- a/torchquantum/device/noisedevices.py +++ b/torchquantum/device/noisedevices.py @@ -73,6 +73,32 @@ def __init__( self.record_op = record_op self.op_history = [] + + def print_2d(self, index): + """Print the matrix value at the given index. + + This method prints the matrix value of `matrix[index]`. It reshapes the value into a 2D matrix + using the `torch.reshape` function and then prints it. + + Args: + index (int): The index of the matrix value to print. + + Examples: + >>> device = QuantumDevice(n_wires=2) + >>> device.matrix = torch.tensor([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) + >>> device.print_2d(1) + tensor([[0, 0], + [0, 1]]) + + """ + + _matrix = torch.reshape(self.densities[index], [2 ** self.n_wires] * 2) + print(_matrix) + + def get_2d_matrix(self, index): + _matrix = torch.reshape(self.densities[index], [2 ** self.n_wires] * 2) + return _matrix + @property def name(self): """Return the name of the device.""" diff --git a/torchquantum/functional/gate_wrapper.py b/torchquantum/functional/gate_wrapper.py index f1383f2f..cef3a867 100644 --- a/torchquantum/functional/gate_wrapper.py +++ b/torchquantum/functional/gate_wrapper.py @@ -433,7 +433,6 @@ def gate_wrapper( assert np.log2(matrix.shape[-1]) == len(wires) if q_device.device_name=="noisedevice": density = q_device.densities - print(density.shape) if method == "einsum": return elif method == "bmm": From 4f90e92a0406af4b9fb3c64c816c3c17ce1426d9 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 2 Feb 2024 18:46:47 -0800 Subject: [PATCH 02/54] [Test] Add two qubit gate tests for density matrix. --- test/density/test_density_op.py | 53 ++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py index 7172c6ac..d458bbf8 100644 --- a/test/density/test_density_op.py +++ b/test/density/test_density_op.py @@ -40,15 +40,30 @@ single_gate_list = [ {"qiskit": qiskit_gate.HGate, "tq": tq.h, "name": "Hadamard"}, {"qiskit": qiskit_gate.XGate, "tq": tq.x, "name": "x"}, - {"qiskit": qiskit_gate.YGate, "tq": tq.y, "name": "y"}, + #{"qiskit": qiskit_gate.YGate, "tq": tq.y, "name": "y"}, {"qiskit": qiskit_gate.ZGate, "tq": tq.z, "name": "z"}, {"qiskit": qiskit_gate.SGate, "tq": tq.S, "name": "S"}, {"qiskit": qiskit_gate.TGate, "tq": tq.T, "name": "T"}, - {"qiskit": qiskit_gate.SXGate, "tq": tq.SX, "name": "SX"}, + #{"qiskit": qiskit_gate.SXGate, "tq": tq.SX, "name": "SX"}, {"qiskit": qiskit_gate.SdgGate, "tq": tq.SDG, "name": "SDG"}, {"qiskit": qiskit_gate.TdgGate, "tq": tq.TDG, "name": "TDG"} ] +two_qubit_gate_list = [ + {"qiskit": qiskit_gate.CXGate, "tq": tq.CNOT, "name": "CNOT"}, + {"qiskit": qiskit_gate.CYGate, "tq": tq.CY, "name": "CY"}, + {"qiskit": qiskit_gate.CZGate, "tq": tq.CZ, "name": "CZ"}, + {"qiskit": qiskit_gate.SwapGate, "tq": tq.SWAP, "name": "SWAP"} +] + +three_qubit_gate_list = [ + {"qiskit": qiskit_gate.CCXGate, "tq": tq.Toffoli, "name": "Toffoli"}, + {"qiskit": qiskit_gate.CSwapGate, "tq": tq.CSWAP, "name": "CSWAP"} +] + + + + pair_list = [ {"qiskit": qiskit_gate.HGate, "tq": tq.Hadamard}, {"qiskit": None, "tq": tq.SHadamard}, @@ -116,8 +131,6 @@ def density_is_close(mat1: np.ndarray, mat2: np.ndarray): return np.allclose(mat1, mat2) -("Geeks : %2d, Portal : %5.2f" % (1, 05.333)) - class single_qubit(TestCase): def compare_single_gate(self, gate_pair, qubit_num): @@ -141,3 +154,35 @@ def compare_single_gate(self, gate_pair, qubit_num): def test_single_gates(self): for i in range(0, len(single_gate_list)): self.assertTrue(self.compare_single_gate(single_gate_list[i], 5)) + + + +class Two_qubit(TestCase): + def compare_two_qubit_gate(self, gate_pair, qubit_num): + passed = True + for index1 in range(0, qubit_num): + for index2 in range(0, qubit_num): + if(index1==index2): + continue + qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) + gate_pair['tq'](qdev, [index1,index2]) + mat1 = np.array(qdev.get_2d_matrix(0)) + rho_qiskit = qiskitDensity.from_label('0' * qubit_num) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - index1,qubit_num - 1 - index2]) + mat2 = np.array(rho_qiskit.to_operator()) + if density_is_close(mat1, mat2): + print("Test passed for %s gate on qubit (%d,%d) when qubit_number is %d!" % ( + gate_pair['name'], index1,index2, qubit_num)) + else: + passed = False + print("Test failed for %s gate on qubit (%d,%d) when qubit_number is %d!" % ( + gate_pair['name'], index1,index2, qubit_num)) + return passed + + def test_two_qubits_gates(self): + for i in range(0, len(two_qubit_gate_list)): + self.assertTrue(self.compare_two_qubit_gate(two_qubit_gate_list[i], 5)) + + + + From 70e42248a4307a855a02d2bd6c7024da548104e7 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 2 Feb 2024 18:49:57 -0800 Subject: [PATCH 03/54] [Test] Add three qubit gate tests for density matrix. --- test/density/test_density_op.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py index d458bbf8..ec497c4a 100644 --- a/test/density/test_density_op.py +++ b/test/density/test_density_op.py @@ -132,7 +132,7 @@ def density_is_close(mat1: np.ndarray, mat2: np.ndarray): -class single_qubit(TestCase): +class single_qubit_test(TestCase): def compare_single_gate(self, gate_pair, qubit_num): passed = True for index in range(0, qubit_num): @@ -157,7 +157,7 @@ def test_single_gates(self): -class Two_qubit(TestCase): +class two_qubit_test(TestCase): def compare_two_qubit_gate(self, gate_pair, qubit_num): passed = True for index1 in range(0, qubit_num): @@ -184,5 +184,34 @@ def test_two_qubits_gates(self): self.assertTrue(self.compare_two_qubit_gate(two_qubit_gate_list[i], 5)) +class three_qubit_test(TestCase): + def compare_three_qubit_gate(self, gate_pair, qubit_num): + passed = True + for index1 in range(0, qubit_num): + for index2 in range(0, qubit_num): + if (index1 == index2): + continue + for index3 in range(0, qubit_num): + if (index3 == index1) or (index3 == index2): + continue + qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) + gate_pair['tq'](qdev, [index1,index2,index3]) + mat1 = np.array(qdev.get_2d_matrix(0)) + rho_qiskit = qiskitDensity.from_label('0' * qubit_num) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - index1,qubit_num - 1 - index2,qubit_num - 1 - index3]) + mat2 = np.array(rho_qiskit.to_operator()) + if density_is_close(mat1, mat2): + print("Test passed for %s gate on qubit (%d,%d,%d) when qubit_number is %d!" % ( + gate_pair['name'], index1,index2,index3, qubit_num)) + else: + passed = False + print("Test failed for %s gate on qubit (%d,%d,%d) when qubit_number is %d!" % ( + gate_pair['name'], index1,index2,index3,qubit_num)) + return passed + + def test_three_qubits_gates(self): + for i in range(0, len(three_qubit_gate_list)): + self.assertTrue(self.compare_three_qubit_gate(three_qubit_gate_list[i], 5)) + From 9b441c1ea17ed5c7fe89fbfad20e79fe0edcd4fa Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 2 Feb 2024 18:54:59 -0800 Subject: [PATCH 04/54] [Fix] Test code for density matrix operation on arbitrary num of qubits. --- test/density/test_density_op.py | 53 ++++++++++++++++----------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py index ec497c4a..27e08185 100644 --- a/test/density/test_density_op.py +++ b/test/density/test_density_op.py @@ -40,11 +40,11 @@ single_gate_list = [ {"qiskit": qiskit_gate.HGate, "tq": tq.h, "name": "Hadamard"}, {"qiskit": qiskit_gate.XGate, "tq": tq.x, "name": "x"}, - #{"qiskit": qiskit_gate.YGate, "tq": tq.y, "name": "y"}, + # {"qiskit": qiskit_gate.YGate, "tq": tq.y, "name": "y"}, {"qiskit": qiskit_gate.ZGate, "tq": tq.z, "name": "z"}, {"qiskit": qiskit_gate.SGate, "tq": tq.S, "name": "S"}, {"qiskit": qiskit_gate.TGate, "tq": tq.T, "name": "T"}, - #{"qiskit": qiskit_gate.SXGate, "tq": tq.SX, "name": "SX"}, + # {"qiskit": qiskit_gate.SXGate, "tq": tq.SX, "name": "SX"}, {"qiskit": qiskit_gate.SdgGate, "tq": tq.SDG, "name": "SDG"}, {"qiskit": qiskit_gate.TdgGate, "tq": tq.TDG, "name": "TDG"} ] @@ -61,9 +61,6 @@ {"qiskit": qiskit_gate.CSwapGate, "tq": tq.CSWAP, "name": "CSWAP"} ] - - - pair_list = [ {"qiskit": qiskit_gate.HGate, "tq": tq.Hadamard}, {"qiskit": None, "tq": tq.SHadamard}, @@ -125,13 +122,14 @@ {"qiskit": qiskit_gate.C3SXGate, "tq": tq.C3SX}, ] +maximum_qubit_num = 5 + def density_is_close(mat1: np.ndarray, mat2: np.ndarray): assert mat1.shape == mat2.shape return np.allclose(mat1, mat2) - class single_qubit_test(TestCase): def compare_single_gate(self, gate_pair, qubit_num): passed = True @@ -144,17 +142,17 @@ def compare_single_gate(self, gate_pair, qubit_num): mat2 = np.array(rho_qiskit.to_operator()) if density_is_close(mat1, mat2): print("Test passed for %s gate on qubit %d when qubit_number is %d!" % ( - gate_pair['name'], index, qubit_num)) + gate_pair['name'], index, qubit_num)) else: passed = False print("Test failed for %s gaet on qubit %d when qubit_number is %d!" % ( - gate_pair['name'], index, qubit_num)) + gate_pair['name'], index, qubit_num)) return passed def test_single_gates(self): - for i in range(0, len(single_gate_list)): - self.assertTrue(self.compare_single_gate(single_gate_list[i], 5)) - + for qubit_num in range(1, maximum_qubit_num+1): + for i in range(0, len(single_gate_list)): + self.assertTrue(self.compare_single_gate(single_gate_list[i], qubit_num)) class two_qubit_test(TestCase): @@ -162,26 +160,27 @@ def compare_two_qubit_gate(self, gate_pair, qubit_num): passed = True for index1 in range(0, qubit_num): for index2 in range(0, qubit_num): - if(index1==index2): + if (index1 == index2): continue qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - gate_pair['tq'](qdev, [index1,index2]) + gate_pair['tq'](qdev, [index1, index2]) mat1 = np.array(qdev.get_2d_matrix(0)) rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - index1,qubit_num - 1 - index2]) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - index1, qubit_num - 1 - index2]) mat2 = np.array(rho_qiskit.to_operator()) if density_is_close(mat1, mat2): print("Test passed for %s gate on qubit (%d,%d) when qubit_number is %d!" % ( - gate_pair['name'], index1,index2, qubit_num)) + gate_pair['name'], index1, index2, qubit_num)) else: passed = False print("Test failed for %s gate on qubit (%d,%d) when qubit_number is %d!" % ( - gate_pair['name'], index1,index2, qubit_num)) + gate_pair['name'], index1, index2, qubit_num)) return passed def test_two_qubits_gates(self): - for i in range(0, len(two_qubit_gate_list)): - self.assertTrue(self.compare_two_qubit_gate(two_qubit_gate_list[i], 5)) + for qubit_num in range(2, maximum_qubit_num+1): + for i in range(0, len(two_qubit_gate_list)): + self.assertTrue(self.compare_two_qubit_gate(two_qubit_gate_list[i], qubit_num)) class three_qubit_test(TestCase): @@ -195,23 +194,23 @@ def compare_three_qubit_gate(self, gate_pair, qubit_num): if (index3 == index1) or (index3 == index2): continue qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - gate_pair['tq'](qdev, [index1,index2,index3]) + gate_pair['tq'](qdev, [index1, index2, index3]) mat1 = np.array(qdev.get_2d_matrix(0)) rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - index1,qubit_num - 1 - index2,qubit_num - 1 - index3]) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), + [qubit_num - 1 - index1, qubit_num - 1 - index2, + qubit_num - 1 - index3]) mat2 = np.array(rho_qiskit.to_operator()) if density_is_close(mat1, mat2): print("Test passed for %s gate on qubit (%d,%d,%d) when qubit_number is %d!" % ( - gate_pair['name'], index1,index2,index3, qubit_num)) + gate_pair['name'], index1, index2, index3, qubit_num)) else: passed = False print("Test failed for %s gate on qubit (%d,%d,%d) when qubit_number is %d!" % ( - gate_pair['name'], index1,index2,index3,qubit_num)) + gate_pair['name'], index1, index2, index3, qubit_num)) return passed def test_three_qubits_gates(self): - for i in range(0, len(three_qubit_gate_list)): - self.assertTrue(self.compare_three_qubit_gate(three_qubit_gate_list[i], 5)) - - - + for qubit_num in range(3, maximum_qubit_num+1): + for i in range(0, len(three_qubit_gate_list)): + self.assertTrue(self.compare_three_qubit_gate(three_qubit_gate_list[i], qubit_num)) From dfde51e86cb7ce4304f602cbb543c0542180e43f Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 2 Feb 2024 19:51:53 -0800 Subject: [PATCH 05/54] [Test] Add one,two,three qubit gate random layer tests. --- test/density/test_density_measure.py | 24 ++++ test/density/test_density_op.py | 161 ++++++++++++++++++++++++++- test/density/test_density_trace.py | 63 +++++++++++ 3 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 test/density/test_density_measure.py diff --git a/test/density/test_density_measure.py b/test/density/test_density_measure.py new file mode 100644 index 00000000..c63b48a8 --- /dev/null +++ b/test/density/test_density_measure.py @@ -0,0 +1,24 @@ +import torchquantum as tq +import numpy as np + +import qiskit.circuit.library.standard_gates as qiskit_gate +from qiskit.quantum_info import DensityMatrix as qiskitDensity + +from unittest import TestCase + + + +class density_measure_test(TestCase): + def test_single_qubit_random_layer(self): + return + + def test_two_qubit_random_layer(self): + return + + + def test_three_qubit_random_layer(self): + return + + + def test_mixed_layer(self): + return \ No newline at end of file diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py index 27e08185..4d745789 100644 --- a/test/density/test_density_op.py +++ b/test/density/test_density_op.py @@ -32,6 +32,9 @@ from qiskit.quantum_info import DensityMatrix as qiskitDensity from unittest import TestCase + +from random import randrange + import qiskit.circuit.library as qiskit_library from qiskit.quantum_info import Operator @@ -49,6 +52,10 @@ {"qiskit": qiskit_gate.TdgGate, "tq": tq.TDG, "name": "TDG"} ] +single_param_gate_list = [ + +] + two_qubit_gate_list = [ {"qiskit": qiskit_gate.CXGate, "tq": tq.CNOT, "name": "CNOT"}, {"qiskit": qiskit_gate.CYGate, "tq": tq.CY, "name": "CY"}, @@ -56,11 +63,18 @@ {"qiskit": qiskit_gate.SwapGate, "tq": tq.SWAP, "name": "SWAP"} ] +two_qubit_param_gate_list = [ + +] + three_qubit_gate_list = [ {"qiskit": qiskit_gate.CCXGate, "tq": tq.Toffoli, "name": "Toffoli"}, {"qiskit": qiskit_gate.CSwapGate, "tq": tq.CSWAP, "name": "CSWAP"} ] +three_qubit_param_gate_list = [ +] + pair_list = [ {"qiskit": qiskit_gate.HGate, "tq": tq.Hadamard}, {"qiskit": None, "tq": tq.SHadamard}, @@ -131,6 +145,11 @@ def density_is_close(mat1: np.ndarray, mat2: np.ndarray): class single_qubit_test(TestCase): + ''' + Act one single qubit on all possible location of a quantum circuit, + compare the density matrix between qiskit result and tq result. + ''' + def compare_single_gate(self, gate_pair, qubit_num): passed = True for index in range(0, qubit_num): @@ -150,17 +169,22 @@ def compare_single_gate(self, gate_pair, qubit_num): return passed def test_single_gates(self): - for qubit_num in range(1, maximum_qubit_num+1): + for qubit_num in range(1, maximum_qubit_num + 1): for i in range(0, len(single_gate_list)): self.assertTrue(self.compare_single_gate(single_gate_list[i], qubit_num)) class two_qubit_test(TestCase): + ''' + Act two qubits gate on all possible location of a quantum circuit, + compare the density matrix between qiskit result and tq result. + ''' + def compare_two_qubit_gate(self, gate_pair, qubit_num): passed = True for index1 in range(0, qubit_num): for index2 in range(0, qubit_num): - if (index1 == index2): + if index1 == index2: continue qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) gate_pair['tq'](qdev, [index1, index2]) @@ -178,12 +202,17 @@ def compare_two_qubit_gate(self, gate_pair, qubit_num): return passed def test_two_qubits_gates(self): - for qubit_num in range(2, maximum_qubit_num+1): + for qubit_num in range(2, maximum_qubit_num + 1): for i in range(0, len(two_qubit_gate_list)): self.assertTrue(self.compare_two_qubit_gate(two_qubit_gate_list[i], qubit_num)) class three_qubit_test(TestCase): + ''' + Act three qubits gates on all possible location of a quantum circuit, + compare the density matrix between qiskit result and tq result. + ''' + def compare_three_qubit_gate(self, gate_pair, qubit_num): passed = True for index1 in range(0, qubit_num): @@ -211,6 +240,130 @@ def compare_three_qubit_gate(self, gate_pair, qubit_num): return passed def test_three_qubits_gates(self): - for qubit_num in range(3, maximum_qubit_num+1): + for qubit_num in range(3, maximum_qubit_num + 1): for i in range(0, len(three_qubit_gate_list)): self.assertTrue(self.compare_three_qubit_gate(three_qubit_gate_list[i], qubit_num)) + + +class random_layer_test(TestCase): + ''' + Generate a single qubit random layer + ''' + + def single_qubit_random_layer(self, gatestrength): + passed = True + length = len(single_gate_list) + for qubit_num in range(1, maximum_qubit_num + 1): + qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) + rho_qiskit = qiskitDensity.from_label('0' * qubit_num) + gate_num = int(gatestrength * qubit_num) + for i in range(0, gate_num + 1): + random_gate_index = randrange(length) + gate_pair = single_gate_list[random_gate_index] + random_qubit_index = randrange(qubit_num) + gate_pair['tq'](qdev, [random_qubit_index]) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index]) + + mat1 = np.array(qdev.get_2d_matrix(0)) + mat2 = np.array(rho_qiskit.to_operator()) + + if density_is_close(mat1, mat2): + print( + "Test passed for single qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( + gate_num, qubit_num)) + else: + passed = False + print( + "Test falied for single qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( + gate_num, qubit_num)) + return passed + + def test_single_qubit_random_layer(self): + repeat_num = 5 + gate_strength_list = [0.5, 1, 1.5, 2] + for i in range(0, repeat_num): + for gatestrength in gate_strength_list: + self.assertTrue(self.single_qubit_random_layer(gatestrength)) + + def two_qubit_random_layer(self, gatestrength): + passed = True + length = len(two_qubit_gate_list) + for qubit_num in range(2, maximum_qubit_num + 1): + qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) + rho_qiskit = qiskitDensity.from_label('0' * qubit_num) + gate_num = int(gatestrength * qubit_num) + for i in range(0, gate_num + 1): + random_gate_index = randrange(length) + gate_pair = two_qubit_gate_list[random_gate_index] + random_qubit_index1 = randrange(qubit_num) + random_qubit_index2 = randrange(qubit_num) + while random_qubit_index2 == random_qubit_index1: + random_qubit_index2 = randrange(qubit_num) + + gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2]) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index1, + qubit_num - 1 - random_qubit_index2]) + + mat1 = np.array(qdev.get_2d_matrix(0)) + mat2 = np.array(rho_qiskit.to_operator()) + + if density_is_close(mat1, mat2): + print( + "Test passed for two qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( + gate_num, qubit_num)) + else: + passed = False + print( + "Test falied for two qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( + gate_num, qubit_num)) + return passed + + def test_two_qubit_random_layer(self): + repeat_num = 5 + gate_strength_list = [0.5, 1, 1.5, 2] + for i in range(0, repeat_num): + for gatestrength in gate_strength_list: + self.assertTrue(self.two_qubit_random_layer(gatestrength)) + + def three_qubit_random_layer(self, gatestrength): + passed = True + length = len(three_qubit_gate_list) + for qubit_num in range(3, maximum_qubit_num + 1): + qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) + rho_qiskit = qiskitDensity.from_label('0' * qubit_num) + gate_num = int(gatestrength * qubit_num) + for i in range(0, gate_num + 1): + random_gate_index = randrange(length) + gate_pair = three_qubit_gate_list[random_gate_index] + random_qubit_index1 = randrange(qubit_num) + random_qubit_index2 = randrange(qubit_num) + while random_qubit_index2 == random_qubit_index1: + random_qubit_index2 = randrange(qubit_num) + random_qubit_index3 = randrange(qubit_num) + while random_qubit_index3 == random_qubit_index1 or random_qubit_index3 == random_qubit_index2: + random_qubit_index3 = randrange(qubit_num) + gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2, random_qubit_index3]) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index1, + qubit_num - 1 - random_qubit_index2, + qubit_num - 1 - random_qubit_index3]) + + mat1 = np.array(qdev.get_2d_matrix(0)) + mat2 = np.array(rho_qiskit.to_operator()) + + if density_is_close(mat1, mat2): + print( + "Test passed for three qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( + gate_num, qubit_num)) + else: + passed = False + print( + "Test falied for three qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( + gate_num, qubit_num)) + return passed + + def test_three_qubit_random_layer(self): + repeat_num = 5 + gate_strength_list = [0.5, 1, 1.5, 2] + for i in range(0, repeat_num): + for gatestrength in gate_strength_list: + self.assertTrue(self.three_qubit_random_layer(gatestrength)) diff --git a/test/density/test_density_trace.py b/test/density/test_density_trace.py index e69de29b..b325ba86 100644 --- a/test/density/test_density_trace.py +++ b/test/density/test_density_trace.py @@ -0,0 +1,63 @@ +import torchquantum as tq +import numpy as np + +import qiskit.circuit.library.standard_gates as qiskit_gate +from qiskit.quantum_info import DensityMatrix as qiskitDensity + +from unittest import TestCase + + + + + +single_gate_list = [ + {"qiskit": qiskit_gate.HGate, "tq": tq.h, "name": "Hadamard"}, + {"qiskit": qiskit_gate.XGate, "tq": tq.x, "name": "x"}, + # {"qiskit": qiskit_gate.YGate, "tq": tq.y, "name": "y"}, + {"qiskit": qiskit_gate.ZGate, "tq": tq.z, "name": "z"}, + {"qiskit": qiskit_gate.SGate, "tq": tq.S, "name": "S"}, + {"qiskit": qiskit_gate.TGate, "tq": tq.T, "name": "T"}, + # {"qiskit": qiskit_gate.SXGate, "tq": tq.SX, "name": "SX"}, + {"qiskit": qiskit_gate.SdgGate, "tq": tq.SDG, "name": "SDG"}, + {"qiskit": qiskit_gate.TdgGate, "tq": tq.TDG, "name": "TDG"} +] + +single_param_gate_list = [ + +] + + + +two_qubit_gate_list = [ + {"qiskit": qiskit_gate.CXGate, "tq": tq.CNOT, "name": "CNOT"}, + {"qiskit": qiskit_gate.CYGate, "tq": tq.CY, "name": "CY"}, + {"qiskit": qiskit_gate.CZGate, "tq": tq.CZ, "name": "CZ"}, + {"qiskit": qiskit_gate.SwapGate, "tq": tq.SWAP, "name": "SWAP"} +] + +two_qubit_param_gate_list = [ + +] + +three_qubit_gate_list = [ + {"qiskit": qiskit_gate.CCXGate, "tq": tq.Toffoli, "name": "Toffoli"}, + {"qiskit": qiskit_gate.CSwapGate, "tq": tq.CSWAP, "name": "CSWAP"} +] + + +three_qubit_param_gate_list = [ +] + + + +class trace_test(TestCase): + def test_single_qubit_trace_preserving(self): + return + + def test_two_qubit_trace_preserving(self): + return + + + def test_three_qubit_trace_preserving(self): + return + From 0593e6899b3b14a4d7780f6d493f93c0772b9db1 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 2 Feb 2024 20:00:23 -0800 Subject: [PATCH 06/54] [Test] Mix random layer test for density matrix module. --- test/density/test_density_op.py | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py index 4d745789..cb83c19f 100644 --- a/test/density/test_density_op.py +++ b/test/density/test_density_op.py @@ -367,3 +367,77 @@ def test_three_qubit_random_layer(self): for i in range(0, repeat_num): for gatestrength in gate_strength_list: self.assertTrue(self.three_qubit_random_layer(gatestrength)) + + def mix_random_layer(self, gatestrength): + passed = True + three_qubit_gate_length = len(three_qubit_gate_list) + single_qubit_gate_length = len(single_gate_list) + two_qubit_gate_length = len(two_qubit_gate_list) + + for qubit_num in range(3, maximum_qubit_num + 1): + qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) + rho_qiskit = qiskitDensity.from_label('0' * qubit_num) + gate_num = int(gatestrength * qubit_num) + for i in range(0, gate_num + 1): + random_gate_qubit_num = randrange(3) + ''' + Add a single qubit gate + ''' + if (random_gate_qubit_num == 0): + random_gate_index = randrange(single_qubit_gate_length) + gate_pair = single_gate_list[random_gate_index] + random_qubit_index = randrange(qubit_num) + gate_pair['tq'](qdev, [random_qubit_index]) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index]) + ''' + Add a two qubit gate + ''' + if (random_gate_qubit_num == 1): + random_gate_index = randrange(two_qubit_gate_length) + gate_pair = two_qubit_gate_list[random_gate_index] + random_qubit_index1 = randrange(qubit_num) + random_qubit_index2 = randrange(qubit_num) + while random_qubit_index2 == random_qubit_index1: + random_qubit_index2 = randrange(qubit_num) + gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2]) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index1, + qubit_num - 1 - random_qubit_index2]) + ''' + Add a three qubit gate + ''' + if (random_gate_qubit_num == 2): + random_gate_index = randrange(three_qubit_gate_length) + gate_pair = three_qubit_gate_list[random_gate_index] + random_qubit_index1 = randrange(qubit_num) + random_qubit_index2 = randrange(qubit_num) + while random_qubit_index2 == random_qubit_index1: + random_qubit_index2 = randrange(qubit_num) + random_qubit_index3 = randrange(qubit_num) + while random_qubit_index3 == random_qubit_index1 or random_qubit_index3 == random_qubit_index2: + random_qubit_index3 = randrange(qubit_num) + gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2, random_qubit_index3]) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index1, + qubit_num - 1 - random_qubit_index2, + qubit_num - 1 - random_qubit_index3]) + + mat1 = np.array(qdev.get_2d_matrix(0)) + mat2 = np.array(rho_qiskit.to_operator()) + + if density_is_close(mat1, mat2): + print( + "Test passed for mix qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( + gate_num, qubit_num)) + else: + passed = False + print( + "Test falied for mix qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( + gate_num, qubit_num)) + return passed + + + def test_mix_random_layer(self): + repeat_num = 5 + gate_strength_list = [0.5, 1, 1.5, 2] + for i in range(0, repeat_num): + for gatestrength in gate_strength_list: + self.assertTrue(self.mix_random_layer(gatestrength)) \ No newline at end of file From d529c51fe0932de87c9171a6d06e49c6ead8513b Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 2 Feb 2024 21:04:20 -0800 Subject: [PATCH 07/54] [Test] Add trace preserving test for density matrix. --- test/density/test_density_trace.py | 81 ++++++++++++++++++++++------- torchquantum/device/noisedevices.py | 23 ++++---- 2 files changed, 76 insertions(+), 28 deletions(-) diff --git a/test/density/test_density_trace.py b/test/density/test_density_trace.py index b325ba86..819a6f2d 100644 --- a/test/density/test_density_trace.py +++ b/test/density/test_density_trace.py @@ -5,10 +5,9 @@ from qiskit.quantum_info import DensityMatrix as qiskitDensity from unittest import TestCase +from random import randrange - - - +maximum_qubit_num = 5 single_gate_list = [ {"qiskit": qiskit_gate.HGate, "tq": tq.h, "name": "Hadamard"}, @@ -26,8 +25,6 @@ ] - - two_qubit_gate_list = [ {"qiskit": qiskit_gate.CXGate, "tq": tq.CNOT, "name": "CNOT"}, {"qiskit": qiskit_gate.CYGate, "tq": tq.CY, "name": "CY"}, @@ -44,20 +41,68 @@ {"qiskit": qiskit_gate.CSwapGate, "tq": tq.CSWAP, "name": "CSWAP"} ] - three_qubit_param_gate_list = [ ] - -class trace_test(TestCase): - def test_single_qubit_trace_preserving(self): - return - - def test_two_qubit_trace_preserving(self): - return - - - def test_three_qubit_trace_preserving(self): - return - +class trace_preserving_test(TestCase): + + def mix_random_layer_trace(self, gatestrength): + passed = True + three_qubit_gate_length = len(three_qubit_gate_list) + single_qubit_gate_length = len(single_gate_list) + two_qubit_gate_length = len(two_qubit_gate_list) + + for qubit_num in range(3, maximum_qubit_num + 1): + qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) + gate_num = int(gatestrength * qubit_num) + for i in range(0, gate_num + 1): + random_gate_qubit_num = randrange(3) + ''' + Add a single qubit gate + ''' + if (random_gate_qubit_num == 0): + random_gate_index = randrange(single_qubit_gate_length) + gate_pair = single_gate_list[random_gate_index] + random_qubit_index = randrange(qubit_num) + gate_pair['tq'](qdev, [random_qubit_index]) + + ''' + Add a two qubit gate + ''' + if (random_gate_qubit_num == 1): + random_gate_index = randrange(two_qubit_gate_length) + gate_pair = two_qubit_gate_list[random_gate_index] + random_qubit_index1 = randrange(qubit_num) + random_qubit_index2 = randrange(qubit_num) + while random_qubit_index2 == random_qubit_index1: + random_qubit_index2 = randrange(qubit_num) + gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2]) + ''' + Add a three qubit gate + ''' + if (random_gate_qubit_num == 2): + random_gate_index = randrange(three_qubit_gate_length) + gate_pair = three_qubit_gate_list[random_gate_index] + random_qubit_index1 = randrange(qubit_num) + random_qubit_index2 = randrange(qubit_num) + while random_qubit_index2 == random_qubit_index1: + random_qubit_index2 = randrange(qubit_num) + random_qubit_index3 = randrange(qubit_num) + while random_qubit_index3 == random_qubit_index1 or random_qubit_index3 == random_qubit_index2: + random_qubit_index3 = randrange(qubit_num) + gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2, random_qubit_index3]) + + if not np.isclose(qdev.calc_trace(0), 1): + passed = False + print("Trace not preserved: %f" % (qdev.calc_trace(0))) + else: + print("Trace preserved: %f" % (qdev.calc_trace(0))) + return passed + + def test_mix_random_layer_trace(self): + repeat_num = 5 + gate_strength_list = [0.5, 1, 1.5, 2] + for i in range(0, repeat_num): + for gatestrength in gate_strength_list: + self.assertTrue(self.mix_random_layer_trace(gatestrength)) diff --git a/torchquantum/device/noisedevices.py b/torchquantum/device/noisedevices.py index 0c548023..eb8c297f 100644 --- a/torchquantum/device/noisedevices.py +++ b/torchquantum/device/noisedevices.py @@ -35,12 +35,12 @@ class NoiseDevice(nn.Module): def __init__( - self, - n_wires: int, - device_name: str = "noisedevice", - bsz: int = 1, - device: Union[torch.device, str] = "cpu", - record_op: bool = False, + self, + n_wires: int, + device_name: str = "noisedevice", + bsz: int = 1, + device: Union[torch.device, str] = "cpu", + record_op: bool = False, ): """A quantum device that support the density matrix simulation Args: @@ -73,7 +73,6 @@ def __init__( self.record_op = record_op self.op_history = [] - def print_2d(self, index): """Print the matrix value at the given index. @@ -99,6 +98,10 @@ def get_2d_matrix(self, index): _matrix = torch.reshape(self.densities[index], [2 ** self.n_wires] * 2) return _matrix + def calc_trace(self, index): + _matrix = torch.reshape(self.densities[index], [2 ** self.n_wires] * 2) + return torch.trace(_matrix) + @property def name(self): """Return the name of the device.""" @@ -107,20 +110,20 @@ def name(self): def __repr__(self): return f" class: {self.name} \n device name: {self.device_name} \n number of qubits: {self.n_wires} \n batch size: {self.bsz} \n current computing device: {self.density.device} \n recording op history: {self.record_op} \n current states: {repr(self.get_probs_1d().cpu().detach().numpy())}" - ''' Get the probability of measuring each state to a one dimension tensor ''' + def get_probs_1d(self): """Return the states in a 1d tensor.""" bsz = self.densities.shape[0] - densities2d=torch.reshape(self.densities, [bsz, 2**self.n_wires,2**self.n_wires]) + densities2d = torch.reshape(self.densities, [bsz, 2 ** self.n_wires, 2 ** self.n_wires]) return torch.diagonal(densities2d, offset=0, dim1=1, dim2=2) def get_prob_1d(self): """Return the state in a 1d tensor.""" - density2d=torch.reshape(self.density, [2**self.n_wires,2**self.n_wires]) + density2d = torch.reshape(self.density, [2 ** self.n_wires, 2 ** self.n_wires]) return torch.diagonal(density2d, offset=0, dim1=0, dim2=1) From c42dbb9e990f4973bd9934a50bca7b3b509429e3 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 2 Feb 2024 21:25:47 -0800 Subject: [PATCH 08/54] [Bug]Fix a small bug. The mat_dict reference in sx.py should be _sx_mat_dict. --- torchquantum/functional/sx.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/torchquantum/functional/sx.py b/torchquantum/functional/sx.py index 7f991b4a..d35075a3 100644 --- a/torchquantum/functional/sx.py +++ b/torchquantum/functional/sx.py @@ -82,7 +82,7 @@ def sx( """ name = "sx" - mat = mat_dict[name] + mat = _sx_mat_dict[name] gate_wrapper( name=name, mat=mat, @@ -129,7 +129,7 @@ def sxdg( """ name = "sxdg" - mat = mat_dict[name] + mat = _sx_mat_dict[name] gate_wrapper( name=name, mat=mat, @@ -176,7 +176,7 @@ def csx( """ name = "csx" - mat = mat_dict[name] + mat = _sx_mat_dict[name] gate_wrapper( name=name, mat=mat, @@ -220,7 +220,7 @@ def c3sx( None. """ name = "c3sx" - mat = mat_dict[name] + mat = _sx_mat_dict[name] gate_wrapper( name=name, mat=mat, From 3b3809507dc0c4fb57177c61208478a67abdba41 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 2 Feb 2024 22:44:51 -0800 Subject: [PATCH 09/54] [Example] Add the minist example that run on noisedevice --- examples/mnist/mnist_noise.py | 250 ++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 examples/mnist/mnist_noise.py diff --git a/examples/mnist/mnist_noise.py b/examples/mnist/mnist_noise.py new file mode 100644 index 00000000..801e6621 --- /dev/null +++ b/examples/mnist/mnist_noise.py @@ -0,0 +1,250 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torch.nn.functional as F +import torch.optim as optim +import argparse +import random +import numpy as np + +import torchquantum as tq +from torchquantum.plugin import ( + tq2qiskit_measurement, + qiskit_assemble_circs, + op_history2qiskit, + op_history2qiskit_expand_params, +) + +from torchquantum.dataset import MNIST +from torch.optim.lr_scheduler import CosineAnnealingLR + + +class QFCModel(tq.QuantumModule): + class QLayer(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.random_layer = tq.RandomLayer( + n_ops=50, wires=list(range(self.n_wires)) + ) + + # gates with trainable parameters + self.rx0 = tq.RX(has_params=True, trainable=True) + self.ry0 = tq.RY(has_params=True, trainable=True) + self.rz0 = tq.RZ(has_params=True, trainable=True) + self.crx0 = tq.CRX(has_params=True, trainable=True) + + def forward(self, qdev: tq.NoiseDevice): + self.random_layer(qdev) + + # some trainable gates (instantiated ahead of time) + self.rx0(qdev, wires=0) + self.ry0(qdev, wires=1) + self.rz0(qdev, wires=3) + self.crx0(qdev, wires=[0, 2]) + + # add some more non-parameterized gates (add on-the-fly) + qdev.h(wires=3) # type: ignore + qdev.sx(wires=2) # type: ignore + qdev.cnot(wires=[3, 0]) # type: ignore + qdev.rx( + wires=1, + params=torch.tensor([0.1]), + static=self.static_mode, + parent_graph=self.graph, + ) # type: ignore + + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder(tq.encoder_op_list_name_dict["4x4_u3_h_rx"]) + + self.q_layer = self.QLayer() + self.measure = tq.MeasureAll(tq.PauliZ) + + def forward(self, x, use_qiskit=False): + qdev = tq.NoiseDevice( + n_wires=self.n_wires, bsz=x.shape[0], device=x.device, record_op=True + ) + + bsz = x.shape[0] + x = F.avg_pool2d(x, 6).view(bsz, 16) + devi = x.device + + if use_qiskit: + # use qiskit to process the circuit + # create the qiskit circuit for encoder + self.encoder(qdev, x) + op_history_parameterized = qdev.op_history + qdev.reset_op_history() + encoder_circs = op_history2qiskit_expand_params(self.n_wires, op_history_parameterized, bsz=bsz) + + # create the qiskit circuit for trainable quantum layers + self.q_layer(qdev) + op_history_fixed = qdev.op_history + qdev.reset_op_history() + q_layer_circ = op_history2qiskit(self.n_wires, op_history_fixed) + + # create the qiskit circuit for measurement + measurement_circ = tq2qiskit_measurement(qdev, self.measure) + + # assemble the encoder, trainable quantum layers, and measurement circuits + assembled_circs = qiskit_assemble_circs( + encoder_circs, q_layer_circ, measurement_circ + ) + + # call the qiskit processor to process the circuit + x0 = self.qiskit_processor.process_ready_circs(qdev, assembled_circs).to( # type: ignore + devi + ) + x = x0 + + else: + # use torchquantum to process the circuit + self.encoder(qdev, x) + qdev.reset_op_history() + self.q_layer(qdev) + x = self.measure(qdev) + + x = x.reshape(bsz, 2, 2).sum(-1).squeeze() + x = F.log_softmax(x, dim=1) + + return x + + +def train(dataflow, model, device, optimizer): + for feed_dict in dataflow["train"]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs) + loss = F.nll_loss(outputs, targets) + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(f"loss: {loss.item()}", end="\r") + + +def valid_test(dataflow, split, model, device, qiskit=False): + target_all = [] + output_all = [] + with torch.no_grad(): + for feed_dict in dataflow[split]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs, use_qiskit=qiskit) + + target_all.append(targets) + output_all.append(outputs) + target_all = torch.cat(target_all, dim=0) + output_all = torch.cat(output_all, dim=0) + + _, indices = output_all.topk(1, dim=1) + masks = indices.eq(target_all.view(-1, 1).expand_as(indices)) + size = target_all.shape[0] + corrects = masks.sum().item() + accuracy = corrects / size + loss = F.nll_loss(output_all, target_all).item() + + print(f"{split} set accuracy: {accuracy}") + print(f"{split} set loss: {loss}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--static", action="store_true", help="compute with " "static mode" + ) + parser.add_argument("--pdb", action="store_true", help="debug with pdb") + parser.add_argument( + "--wires-per-block", type=int, default=2, help="wires per block int static mode" + ) + parser.add_argument( + "--epochs", type=int, default=2, help="number of training epochs" + ) + + args = parser.parse_args() + + if args.pdb: + import pdb + + pdb.set_trace() + + seed = 0 + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + dataset = MNIST( + root="./mnist_data", + train_valid_split_ratio=[0.9, 0.1], + digits_of_interest=[3, 6], + n_test_samples=75, + ) + dataflow = dict() + + for split in dataset: + sampler = torch.utils.data.RandomSampler(dataset[split]) + dataflow[split] = torch.utils.data.DataLoader( + dataset[split], + batch_size=256, + sampler=sampler, + num_workers=8, + pin_memory=True, + ) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + + model = QFCModel().to(device) + + n_epochs = args.epochs + optimizer = optim.Adam(model.parameters(), lr=5e-3, weight_decay=1e-4) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + if args.static: + # optionally to switch to the static mode, which can bring speedup + # on training + model.q_layer.static_on(wires_per_block=args.wires_per_block) + + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}:") + train(dataflow, model, device, optimizer) + print(optimizer.param_groups[0]["lr"]) + + # valid + valid_test(dataflow, "valid", model, device) + scheduler.step() + + # test + valid_test(dataflow, "test", model, device, qiskit=False) + + + + +if __name__ == "__main__": + main() From 135446bf9b08546c17e477db5ea3697c716aefdd Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 2 Feb 2024 22:53:18 -0800 Subject: [PATCH 10/54] [Bug] Fix a minor bug in batch multiplication of density matrix. --- torchquantum/density/density_func.py | 2 +- torchquantum/device/noisedevices.py | 6 ++++++ torchquantum/functional/gate_wrapper.py | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/torchquantum/density/density_func.py b/torchquantum/density/density_func.py index fb15bf3d..5bde99a8 100644 --- a/torchquantum/density/density_func.py +++ b/torchquantum/density/density_func.py @@ -217,7 +217,7 @@ def apply_unitary_density_bmm(density, mat, wires): permute_to_dag = permute_to_dag + devices_dims_dag permute_back_dag = list(np.argsort(permute_to_dag)) original_shape = new_density.shape - permuted_dag = new_density.permute(permute_to_dag).reshape([original_shape[0], -1, matdag.shape[0]]) + permuted_dag = new_density.permute(permute_to_dag).reshape([original_shape[0], -1, matdag.shape[-1]]) if len(matdag.shape) > 2: # both matrix and state are in batch mode diff --git a/torchquantum/device/noisedevices.py b/torchquantum/device/noisedevices.py index eb8c297f..e573c3d7 100644 --- a/torchquantum/device/noisedevices.py +++ b/torchquantum/device/noisedevices.py @@ -73,6 +73,12 @@ def __init__( self.record_op = record_op self.op_history = [] + + def reset_op_history(self): + """Resets the all Operation of the quantum device""" + self.op_history = [] + + def print_2d(self, index): """Print the matrix value at the given index. diff --git a/torchquantum/functional/gate_wrapper.py b/torchquantum/functional/gate_wrapper.py index cef3a867..29b5f5f1 100644 --- a/torchquantum/functional/gate_wrapper.py +++ b/torchquantum/functional/gate_wrapper.py @@ -307,7 +307,7 @@ def apply_unitary_density_bmm(density, mat, wires): del permute_to_dag[d] permute_to_dag = permute_to_dag + devices_dims_dag permute_back_dag = list(np.argsort(permute_to_dag)) - permuted_dag = new_density.permute(permute_to_dag).reshape([original_shape[0], -1, matdag.shape[0]]) + permuted_dag = new_density.permute(permute_to_dag).reshape([original_shape[0], -1, matdag.shape[-1]]) if len(matdag.shape) > 2: # both matrix and state are in batch mode @@ -431,6 +431,7 @@ def gate_wrapper( else: matrix = matrix.permute(1, 0) assert np.log2(matrix.shape[-1]) == len(wires) + #TODO: There might be a better way to discriminate noisedevice and normal statevector device if q_device.device_name=="noisedevice": density = q_device.densities if method == "einsum": From 8f0bbd07dd64d3a851604aa9b55d57c89f52accf Mon Sep 17 00:00:00 2001 From: GenericP3rson <41024739+GenericP3rson@users.noreply.github.com> Date: Sun, 4 Feb 2024 10:21:35 -0500 Subject: [PATCH 11/54] [minor] update OneQubitEulerDecomposer --- torchquantum/plugin/qiskit/qiskit_unitary_gate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchquantum/plugin/qiskit/qiskit_unitary_gate.py b/torchquantum/plugin/qiskit/qiskit_unitary_gate.py index 6e520b96..ce46ff04 100644 --- a/torchquantum/plugin/qiskit/qiskit_unitary_gate.py +++ b/torchquantum/plugin/qiskit/qiskit_unitary_gate.py @@ -25,7 +25,7 @@ from qiskit.circuit.library.standard_gates import U3Gate from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info.operators.predicates import is_unitary_matrix -from qiskit.quantum_info.synthesis.one_qubit_decompose import OneQubitEulerDecomposer +from qiskit.quantum_info import OneQubitEulerDecomposer from qiskit.quantum_info.synthesis.two_qubit_decompose import two_qubit_cnot_decompose from qiskit.extensions.exceptions import ExtensionError From d39deceefc8712683d85889965620c568dcda082 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 9 Feb 2024 19:02:02 -0800 Subject: [PATCH 12/54] [Fix] Fix a bug of matrix conjugation. --- test/density/test_density_op.py | 72 +++++++++++++++---------- torchquantum/functional/gate_wrapper.py | 16 ++++-- torchquantum/functional/paulix.py | 6 +-- 3 files changed, 57 insertions(+), 37 deletions(-) diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py index cb83c19f..afd5c4f2 100644 --- a/test/density/test_density_op.py +++ b/test/density/test_density_op.py @@ -43,13 +43,12 @@ single_gate_list = [ {"qiskit": qiskit_gate.HGate, "tq": tq.h, "name": "Hadamard"}, {"qiskit": qiskit_gate.XGate, "tq": tq.x, "name": "x"}, - # {"qiskit": qiskit_gate.YGate, "tq": tq.y, "name": "y"}, + {"qiskit": qiskit_gate.YGate, "tq": tq.y, "name": "y"}, {"qiskit": qiskit_gate.ZGate, "tq": tq.z, "name": "z"}, - {"qiskit": qiskit_gate.SGate, "tq": tq.S, "name": "S"}, - {"qiskit": qiskit_gate.TGate, "tq": tq.T, "name": "T"}, - # {"qiskit": qiskit_gate.SXGate, "tq": tq.SX, "name": "SX"}, - {"qiskit": qiskit_gate.SdgGate, "tq": tq.SDG, "name": "SDG"}, - {"qiskit": qiskit_gate.TdgGate, "tq": tq.TDG, "name": "TDG"} + {"qiskit": qiskit_gate.SGate, "tq": tq.s, "name": "S"}, + {"qiskit": qiskit_gate.TGate, "tq": tq.t, "name": "T"}, + {"qiskit": qiskit_gate.SdgGate, "tq": tq.sdg, "name": "SDG"}, + {"qiskit": qiskit_gate.TdgGate, "tq": tq.tdg, "name": "TDG"} ] single_param_gate_list = [ @@ -57,10 +56,13 @@ ] two_qubit_gate_list = [ - {"qiskit": qiskit_gate.CXGate, "tq": tq.CNOT, "name": "CNOT"}, - {"qiskit": qiskit_gate.CYGate, "tq": tq.CY, "name": "CY"}, - {"qiskit": qiskit_gate.CZGate, "tq": tq.CZ, "name": "CZ"}, - {"qiskit": qiskit_gate.SwapGate, "tq": tq.SWAP, "name": "SWAP"} + {"qiskit": qiskit_gate.CXGate, "tq": tq.cnot, "name": "CNOT"}, + {"qiskit": qiskit_gate.CXGate, "tq": tq.cx, "name": "CY"}, + {"qiskit": qiskit_gate.CYGate, "tq": tq.cy, "name": "CY"}, + {"qiskit": qiskit_gate.CZGate, "tq": tq.cz, "name": "CZ"}, + {"qiskit": qiskit_gate.CSGate, "tq": tq.cs, "name": "CS"}, + {"qiskit": qiskit_gate.SwapGate, "tq": tq.swap, "name": "SWAP"}, + {"qiskit": qiskit_gate.iSwapGate, "tq": tq.iswap, "name": "iSWAP"} ] two_qubit_param_gate_list = [ @@ -68,25 +70,16 @@ ] three_qubit_gate_list = [ - {"qiskit": qiskit_gate.CCXGate, "tq": tq.Toffoli, "name": "Toffoli"}, - {"qiskit": qiskit_gate.CSwapGate, "tq": tq.CSWAP, "name": "CSWAP"} + {"qiskit": qiskit_gate.CCXGate, "tq": tq.ccx, "name": "Toffoli"}, + {"qiskit": qiskit_gate.CSwapGate, "tq": tq.cswap, "name": "CSWAP"}, ] three_qubit_param_gate_list = [ ] pair_list = [ - {"qiskit": qiskit_gate.HGate, "tq": tq.Hadamard}, - {"qiskit": None, "tq": tq.SHadamard}, - {"qiskit": qiskit_gate.XGate, "tq": tq.PauliX}, - {"qiskit": qiskit_gate.YGate, "tq": tq.PauliY}, - {"qiskit": qiskit_gate.ZGate, "tq": tq.PauliZ}, - {"qiskit": qiskit_gate.SGate, "tq": tq.S}, - {"qiskit": qiskit_gate.TGate, "tq": tq.T}, {"qiskit": qiskit_gate.SXGate, "tq": tq.SX}, {"qiskit": qiskit_gate.CXGate, "tq": tq.CNOT}, - {"qiskit": qiskit_gate.CYGate, "tq": tq.CY}, - {"qiskit": qiskit_gate.CZGate, "tq": tq.CZ}, {"qiskit": qiskit_gate.RXGate, "tq": tq.RX}, {"qiskit": qiskit_gate.RYGate, "tq": tq.RY}, {"qiskit": qiskit_gate.RZGate, "tq": tq.RZ}, @@ -94,7 +87,6 @@ {"qiskit": qiskit_gate.RYYGate, "tq": tq.RYY}, {"qiskit": qiskit_gate.RZZGate, "tq": tq.RZZ}, {"qiskit": qiskit_gate.RZXGate, "tq": tq.RZX}, - {"qiskit": qiskit_gate.SwapGate, "tq": tq.SWAP}, # {'qiskit': qiskit_gate.?, 'tq': tq.SSWAP}, {"qiskit": qiskit_gate.CSwapGate, "tq": tq.CSWAP}, {"qiskit": qiskit_gate.CCXGate, "tq": tq.Toffoli}, @@ -136,12 +128,12 @@ {"qiskit": qiskit_gate.C3SXGate, "tq": tq.C3SX}, ] -maximum_qubit_num = 5 +maximum_qubit_num = 10 def density_is_close(mat1: np.ndarray, mat2: np.ndarray): assert mat1.shape == mat2.shape - return np.allclose(mat1, mat2) + return np.allclose(mat1, mat2, 1e-3, 1e-6) class single_qubit_test(TestCase): @@ -257,13 +249,33 @@ def single_qubit_random_layer(self, gatestrength): qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) rho_qiskit = qiskitDensity.from_label('0' * qubit_num) gate_num = int(gatestrength * qubit_num) - for i in range(0, gate_num + 1): + gate_list = [] + for i in range(0, gate_num): random_gate_index = randrange(length) gate_pair = single_gate_list[random_gate_index] random_qubit_index = randrange(qubit_num) + gate_list.append(gate_pair["name"]) gate_pair['tq'](qdev, [random_qubit_index]) rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index]) - + ''' + print(i) + print("gate list:") + print(gate_list) + print("qdev history") + print(qdev.op_history) + mat1_tmp = np.array(qdev.get_2d_matrix(0)) + mat2_tmp = np.array(rho_qiskit.to_operator()) + print("Torch quantum result:") + print(mat1_tmp) + print("Qiskit result:") + print(mat2_tmp) + if not density_is_close(mat1_tmp, mat2_tmp): + passed = False + print("Failed! Current gate list:") + print(gate_list) + print(qdev.op_history) + return passed + ''' mat1 = np.array(qdev.get_2d_matrix(0)) mat2 = np.array(rho_qiskit.to_operator()) @@ -276,11 +288,14 @@ def single_qubit_random_layer(self, gatestrength): print( "Test falied for single qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( gate_num, qubit_num)) + print(gate_list) + print(qdev.op_history) + return passed def test_single_qubit_random_layer(self): repeat_num = 5 - gate_strength_list = [0.5, 1, 1.5, 2] + gate_strength_list = [0.5, 1, 1.5, 2, 2.5, 3.5, 4.5, 5, 5.5, 6.5, 7.5, 8.5, 9.5, 10] for i in range(0, repeat_num): for gatestrength in gate_strength_list: self.assertTrue(self.single_qubit_random_layer(gatestrength)) @@ -434,10 +449,9 @@ def mix_random_layer(self, gatestrength): gate_num, qubit_num)) return passed - def test_mix_random_layer(self): repeat_num = 5 gate_strength_list = [0.5, 1, 1.5, 2] for i in range(0, repeat_num): for gatestrength in gate_strength_list: - self.assertTrue(self.mix_random_layer(gatestrength)) \ No newline at end of file + self.assertTrue(self.mix_random_layer(gatestrength)) diff --git a/torchquantum/functional/gate_wrapper.py b/torchquantum/functional/gate_wrapper.py index 29b5f5f1..299bde14 100644 --- a/torchquantum/functional/gate_wrapper.py +++ b/torchquantum/functional/gate_wrapper.py @@ -8,7 +8,6 @@ from torchpack.utils.logging import logger from torchquantum.util import normalize_statevector - if TYPE_CHECKING: from torchquantum.device import QuantumDevice, NoiseDevice else: @@ -274,6 +273,7 @@ def apply_unitary_density_bmm(density, mat, wires): device_wires = wires n_qubit = density.dim() // 2 mat = mat.type(C_DTYPE).to(density.device) + """ Compute U \rho """ @@ -298,7 +298,14 @@ def apply_unitary_density_bmm(density, mat, wires): """ Compute \rho U^\dagger """ - matdag = torch.conj(mat) + + + matdag = mat.conj() + if matdag.dim() == 3: + matdag = matdag.permute(0, 2, 1) + else: + matdag = matdag.permute(1, 0) + matdag = matdag.type(C_DTYPE).to(density.device) devices_dims_dag = [n_qubit + w + 1 for w in device_wires] @@ -431,8 +438,8 @@ def gate_wrapper( else: matrix = matrix.permute(1, 0) assert np.log2(matrix.shape[-1]) == len(wires) - #TODO: There might be a better way to discriminate noisedevice and normal statevector device - if q_device.device_name=="noisedevice": + # TODO: There might be a better way to discriminate noisedevice and normal statevector device + if q_device.device_name == "noisedevice": density = q_device.densities if method == "einsum": return @@ -444,4 +451,3 @@ def gate_wrapper( q_device.states = apply_unitary_einsum(state, matrix, wires) elif method == "bmm": q_device.states = apply_unitary_bmm(state, matrix, wires) - diff --git a/torchquantum/functional/paulix.py b/torchquantum/functional/paulix.py index d07f066f..e2904d13 100644 --- a/torchquantum/functional/paulix.py +++ b/torchquantum/functional/paulix.py @@ -508,7 +508,7 @@ def toffoli( """ name = "toffoli" - mat = mat_dict[name] + mat = _x_mat_dict[name] gate_wrapper( name=name, mat=mat, @@ -552,7 +552,7 @@ def rc3x( None. """ name = "rc3x" - mat = mat_dict[name] + mat = _x_mat_dict[name] gate_wrapper( name=name, mat=mat, @@ -596,7 +596,7 @@ def rccx( None. """ name = "rccx" - mat = mat_dict[name] + mat = _x_mat_dict[name] gate_wrapper( name=name, mat=mat, From 09f345236a9bda21998e4b9d48bc9f88428a3446 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Sat, 10 Feb 2024 07:24:55 -0800 Subject: [PATCH 13/54] [Test] Add test for 1 and 2 qubit parameter gates with only one parameter. --- test/density/test_density_op.py | 139 +++++++++++++++++------- torchquantum/functional/gate_wrapper.py | 7 +- torchquantum/functional/u1.py | 4 +- torchquantum/functional/u2.py | 4 +- 4 files changed, 112 insertions(+), 42 deletions(-) diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py index afd5c4f2..4a244d53 100644 --- a/test/density/test_density_op.py +++ b/test/density/test_density_op.py @@ -33,7 +33,7 @@ from unittest import TestCase -from random import randrange +from random import randrange, uniform import qiskit.circuit.library as qiskit_library from qiskit.quantum_info import Operator @@ -41,6 +41,7 @@ RND_TIMES = 100 single_gate_list = [ + {"qiskit": qiskit_gate.IGate, "tq": tq.i, "name": "Identity"}, {"qiskit": qiskit_gate.HGate, "tq": tq.h, "name": "Hadamard"}, {"qiskit": qiskit_gate.XGate, "tq": tq.x, "name": "x"}, {"qiskit": qiskit_gate.YGate, "tq": tq.y, "name": "y"}, @@ -48,10 +49,22 @@ {"qiskit": qiskit_gate.SGate, "tq": tq.s, "name": "S"}, {"qiskit": qiskit_gate.TGate, "tq": tq.t, "name": "T"}, {"qiskit": qiskit_gate.SdgGate, "tq": tq.sdg, "name": "SDG"}, - {"qiskit": qiskit_gate.TdgGate, "tq": tq.tdg, "name": "TDG"} + {"qiskit": qiskit_gate.TdgGate, "tq": tq.tdg, "name": "TDG"}, + {"qiskit": qiskit_gate.SXGate, "tq": tq.sx}, + {"qiskit": qiskit_gate.SXdgGate, "tq": tq.sxdg}, ] single_param_gate_list = [ + {"qiskit": qiskit_gate.RXGate, "tq": tq.rx, "name": "RX", "numparam": 1}, + {"qiskit": qiskit_gate.RYGate, "tq": tq.ry, "name": "Ry", "numparam": 1}, + {"qiskit": qiskit_gate.RZGate, "tq": tq.rz, "name": "RZ", "numparam": 1}, + {"qiskit": qiskit_gate.U1Gate, "tq": tq.u1, "name": "U1", "numparam": 1}, + {"qiskit": qiskit_gate.PhaseGate, "tq": tq.phaseshift, "name": "Phaseshift", "numparam": 1}, + # {"qiskit": qiskit_gate.GlobalPhaseGate, "tq": tq.globalphase, "name": "Gphase", "numparam": 1}, + # {"qiskit": qiskit_gate.U2Gate, "tq": tq.u2, "name": "U2", "numparam": 2}, + # {"qiskit": qiskit_gate.U3Gate, "tq": tq.u3, "name": "U3", "numparam": 3}, + {"qiskit": qiskit_gate.RGate, "tq": tq.r, "name": "R", "numparam": 3}, + {"qiskit": qiskit_gate.UGate, "tq": tq.u, "name": "U", "numparam": 3}, ] @@ -61,70 +74,51 @@ {"qiskit": qiskit_gate.CYGate, "tq": tq.cy, "name": "CY"}, {"qiskit": qiskit_gate.CZGate, "tq": tq.cz, "name": "CZ"}, {"qiskit": qiskit_gate.CSGate, "tq": tq.cs, "name": "CS"}, + {"qiskit": qiskit_gate.CHGate, "tq": tq.ch, "name": "CH"}, + {"qiskit": qiskit_gate.CSdgGate, "tq": tq.csdg, "name": "CSdag"}, {"qiskit": qiskit_gate.SwapGate, "tq": tq.swap, "name": "SWAP"}, {"qiskit": qiskit_gate.iSwapGate, "tq": tq.iswap, "name": "iSWAP"} ] two_qubit_param_gate_list = [ - + {"qiskit": qiskit_gate.RXXGate, "tq": tq.rxx, "name": "RXX", "numparam": 1}, + {"qiskit": qiskit_gate.RYYGate, "tq": tq.ryy, "name": "RYY", "numparam": 1}, + {"qiskit": qiskit_gate.RZZGate, "tq": tq.rzz, "name": "RZZ", "numparam": 1}, + {"qiskit": qiskit_gate.RZXGate, "tq": tq.rzx, "name": "RZX", "numparam": 1}, + {"qiskit": qiskit_gate.CRXGate, "tq": tq.crx, "name": "CRX", "numparam": 1}, + {"qiskit": qiskit_gate.CRYGate, "tq": tq.cry, "name": "CRY", "numparam": 1}, + {"qiskit": qiskit_gate.CRZGate, "tq": tq.crz, "name": "CRZ", "numparam": 1}, + {"qiskit": qiskit_gate.CU1Gate, "tq": tq.cu1, "name": "CU1", "numparam": 1}, + #{"qiskit": qiskit_gate.CU3Gate, "tq": tq.CU3, "name": "CU3", "numparam": 3}, + #{"qiskit": qiskit_gate.CUGate, "tq": tq.cu, "name": "CU", "numparam": 3} ] three_qubit_gate_list = [ {"qiskit": qiskit_gate.CCXGate, "tq": tq.ccx, "name": "Toffoli"}, {"qiskit": qiskit_gate.CSwapGate, "tq": tq.cswap, "name": "CSWAP"}, + {"qiskit": qiskit_gate.iSwapGate, "tq": tq.iswap, "name": "ISWAP"}, + {"qiskit": qiskit_gate.CCZGate, "tq": tq.ccz, "name": "CCZ"}, + {"qiskit": qiskit_gate.CSXGate, "tq": tq.csx, "name": "CSX"} ] three_qubit_param_gate_list = [ + ] pair_list = [ - {"qiskit": qiskit_gate.SXGate, "tq": tq.SX}, - {"qiskit": qiskit_gate.CXGate, "tq": tq.CNOT}, - {"qiskit": qiskit_gate.RXGate, "tq": tq.RX}, - {"qiskit": qiskit_gate.RYGate, "tq": tq.RY}, - {"qiskit": qiskit_gate.RZGate, "tq": tq.RZ}, - {"qiskit": qiskit_gate.RXXGate, "tq": tq.RXX}, - {"qiskit": qiskit_gate.RYYGate, "tq": tq.RYY}, - {"qiskit": qiskit_gate.RZZGate, "tq": tq.RZZ}, - {"qiskit": qiskit_gate.RZXGate, "tq": tq.RZX}, - # {'qiskit': qiskit_gate.?, 'tq': tq.SSWAP}, - {"qiskit": qiskit_gate.CSwapGate, "tq": tq.CSWAP}, - {"qiskit": qiskit_gate.CCXGate, "tq": tq.Toffoli}, - {"qiskit": qiskit_gate.PhaseGate, "tq": tq.PhaseShift}, # {'qiskit': qiskit_gate.?, 'tq': tq.Rot}, # {'qiskit': qiskit_gate.?, 'tq': tq.MultiRZ}, - {"qiskit": qiskit_gate.CRXGate, "tq": tq.CRX}, - {"qiskit": qiskit_gate.CRYGate, "tq": tq.CRY}, - {"qiskit": qiskit_gate.CRZGate, "tq": tq.CRZ}, # {'qiskit': qiskit_gate.?, 'tq': tq.CRot}, - {"qiskit": qiskit_gate.UGate, "tq": tq.U}, - {"qiskit": qiskit_gate.U1Gate, "tq": tq.U1}, - {"qiskit": qiskit_gate.U2Gate, "tq": tq.U2}, - {"qiskit": qiskit_gate.U3Gate, "tq": tq.U3}, - {"qiskit": qiskit_gate.CUGate, "tq": tq.CU}, - {"qiskit": qiskit_gate.CU1Gate, "tq": tq.CU1}, # {'qiskit': qiskit_gate.?, 'tq': tq.CU2}, - {"qiskit": qiskit_gate.CU3Gate, "tq": tq.CU3}, {"qiskit": qiskit_gate.ECRGate, "tq": tq.ECR}, # {"qiskit": qiskit_library.QFT, "tq": tq.QFT}, - {"qiskit": qiskit_gate.SdgGate, "tq": tq.SDG}, - {"qiskit": qiskit_gate.TdgGate, "tq": tq.TDG}, - {"qiskit": qiskit_gate.SXdgGate, "tq": tq.SXDG}, - {"qiskit": qiskit_gate.CHGate, "tq": tq.CH}, - {"qiskit": qiskit_gate.CCZGate, "tq": tq.CCZ}, - {"qiskit": qiskit_gate.iSwapGate, "tq": tq.ISWAP}, - {"qiskit": qiskit_gate.CSGate, "tq": tq.CS}, - {"qiskit": qiskit_gate.CSdgGate, "tq": tq.CSDG}, - {"qiskit": qiskit_gate.CSXGate, "tq": tq.CSX}, {"qiskit": qiskit_gate.DCXGate, "tq": tq.DCX}, {"qiskit": qiskit_gate.XXMinusYYGate, "tq": tq.XXMINYY}, {"qiskit": qiskit_gate.XXPlusYYGate, "tq": tq.XXPLUSYY}, {"qiskit": qiskit_gate.C3XGate, "tq": tq.C3X}, - {"qiskit": qiskit_gate.RGate, "tq": tq.R}, {"qiskit": qiskit_gate.C4XGate, "tq": tq.C4X}, {"qiskit": qiskit_gate.RCCXGate, "tq": tq.RCCX}, {"qiskit": qiskit_gate.RC3XGate, "tq": tq.RC3X}, - {"qiskit": qiskit_gate.GlobalPhaseGate, "tq": tq.GlobalPhase}, {"qiskit": qiskit_gate.C3SXGate, "tq": tq.C3SX}, ] @@ -160,6 +154,37 @@ def compare_single_gate(self, gate_pair, qubit_num): gate_pair['name'], index, qubit_num)) return passed + def compare_single_gate_params(self, gate_pair, qubit_num): + passed = True + for index in range(0, qubit_num): + qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) + paramnum = gate_pair["numparam"] + params = [] + for i in range(0, paramnum): + params.append(uniform(0, 6.2)) + if (paramnum == 1): + params = params[0] + + print(params) + gate_pair['tq'](qdev, [index], params=params) + mat1 = np.array(qdev.get_2d_matrix(0)) + rho_qiskit = qiskitDensity.from_label('0' * qubit_num) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](params), [qubit_num - 1 - index]) + mat2 = np.array(rho_qiskit.to_operator()) + if density_is_close(mat1, mat2): + print("Test passed for %s gate on qubit %d when qubit_number is %d!" % ( + gate_pair['name'], index, qubit_num)) + else: + passed = False + print("Test failed for %s gaet on qubit %d when qubit_number is %d!" % ( + gate_pair['name'], index, qubit_num)) + return passed + + def test_single_gates_params(self): + for qubit_num in range(1, maximum_qubit_num + 1): + for i in range(0, len(single_param_gate_list)): + self.assertTrue(self.compare_single_gate_params(single_param_gate_list[i], qubit_num)) + def test_single_gates(self): for qubit_num in range(1, maximum_qubit_num + 1): for i in range(0, len(single_gate_list)): @@ -193,6 +218,46 @@ def compare_two_qubit_gate(self, gate_pair, qubit_num): gate_pair['name'], index1, index2, qubit_num)) return passed + + + + def compare_two_qubit_params_gate(self, gate_pair, qubit_num): + passed = True + for index1 in range(0, qubit_num): + for index2 in range(0, qubit_num): + if index1 == index2: + continue + paramnum = gate_pair["numparam"] + params = [] + for i in range(0, paramnum): + params.append(uniform(0, 6.2)) + if (paramnum == 1): + params = params[0] + + qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) + gate_pair['tq'](qdev, [index1, index2],params=params) + + mat1 = np.array(qdev.get_2d_matrix(0)) + rho_qiskit = qiskitDensity.from_label('0' * qubit_num) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](params), [qubit_num - 1 - index1, qubit_num - 1 - index2]) + mat2 = np.array(rho_qiskit.to_operator()) + if density_is_close(mat1, mat2): + print("Test passed for %s gate on qubit (%d,%d) when qubit_number is %d!" % ( + gate_pair['name'], index1, index2, qubit_num)) + else: + passed = False + print("Test failed for %s gate on qubit (%d,%d) when qubit_number is %d!" % ( + gate_pair['name'], index1, index2, qubit_num)) + return passed + + + def test_two_qubits_params_gates(self): + for qubit_num in range(2, maximum_qubit_num + 1): + for i in range(0, len(two_qubit_param_gate_list)): + self.assertTrue(self.compare_two_qubit_params_gate(two_qubit_param_gate_list[i], qubit_num)) + + + def test_two_qubits_gates(self): for qubit_num in range(2, maximum_qubit_num + 1): for i in range(0, len(two_qubit_gate_list)): diff --git a/torchquantum/functional/gate_wrapper.py b/torchquantum/functional/gate_wrapper.py index 299bde14..27120476 100644 --- a/torchquantum/functional/gate_wrapper.py +++ b/torchquantum/functional/gate_wrapper.py @@ -366,6 +366,8 @@ def gate_wrapper( """ if params is not None: + print("Start change params:") + print(params) if not isinstance(params, torch.Tensor): if name in ["qubitunitary", "qubitunitaryfast", "qubitunitarystrict"]: # this is for qubitunitary gate @@ -373,7 +375,8 @@ def gate_wrapper( else: # this is for directly inputting parameters as a number params = torch.tensor(params, dtype=F_DTYPE) - + print("Become torch tensor:") + print(params) if name in ["qubitunitary", "qubitunitaryfast", "qubitunitarystrict"]: params = params.unsqueeze(0) if params.dim() == 2 else params else: @@ -382,6 +385,8 @@ def gate_wrapper( elif params.dim() == 0: params = params.unsqueeze(-1).unsqueeze(-1) # params = params.unsqueeze(-1) if params.dim() == 1 else params + print("Final params") + print(params) wires = [wires] if isinstance(wires, int) else wires if q_device.record_op: diff --git a/torchquantum/functional/u1.py b/torchquantum/functional/u1.py index 05a94910..3422b976 100644 --- a/torchquantum/functional/u1.py +++ b/torchquantum/functional/u1.py @@ -110,7 +110,7 @@ def u1( """ name = "u1" - mat = mat_dict[name] + mat = _u1_mat_dict[name] gate_wrapper( name=name, mat=mat, @@ -157,7 +157,7 @@ def cu1( """ name = "cu1" - mat = mat_dict[name] + mat = _u1_mat_dict[name] gate_wrapper( name=name, mat=mat, diff --git a/torchquantum/functional/u2.py b/torchquantum/functional/u2.py index 5a1d9b21..d9a6387a 100644 --- a/torchquantum/functional/u2.py +++ b/torchquantum/functional/u2.py @@ -109,7 +109,7 @@ def u2( """ name = "u2" - mat = mat_dict[name] + mat = _u2_mat_dict[name] gate_wrapper( name=name, mat=mat, @@ -156,7 +156,7 @@ def cu2( """ name = "cu2" - mat = mat_dict[name] + mat = _u2_mat_dict[name] gate_wrapper( name=name, mat=mat, From 1f472ecd642782ef5a031fe0cc08ba67e6df5c16 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Sat, 10 Feb 2024 08:01:57 -0800 Subject: [PATCH 14/54] [File] Add a density-measurements.py file. --- .../measurement/density_measurements.py | 69 +++++++++++++++++++ torchquantum/measurement/measurements.py | 8 +-- 2 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 torchquantum/measurement/density_measurements.py diff --git a/torchquantum/measurement/density_measurements.py b/torchquantum/measurement/density_measurements.py new file mode 100644 index 00000000..37908760 --- /dev/null +++ b/torchquantum/measurement/density_measurements.py @@ -0,0 +1,69 @@ +import random + +import torch +import torchquantum as tq +import torchquantum.functional as tqf +import numpy as np +from torchquantum.macro import F_DTYPE + +from typing import Union, List +from collections import Counter, OrderedDict + +from torchquantum.functional import mat_dict +# from .operator import op_name_dict, Observable +import torchquantum.operator as op +from copy import deepcopy +import matplotlib.pyplot as plt + +__all__ = [ + "expval_joint_sampling_grouping", + "expval_joint_analytical", + "expval_joint_sampling", + "expval", + "measure", +] + + +def measure(noisedev: tq.NoiseDevice, n_shots=1024, draw_id=None): + return + + + +def expval_joint_sampling_grouping( + qdev: tq.NoiseDevice, + observables: List[str], + n_shots_per_group=1024, +): + return + + +def expval_joint_sampling( + qdev: tq.NoiseDevice, + observable: str, + n_shots=1024, +): + return + + +def expval_joint_analytical( + qdev: tq.NoiseDevice, + observable: str, +): + return + + +def expval( + qdev: tq.NoiseDevice, + wires: Union[int, List[int]], + observables: Union[op.Observable, List[op.Observable]], +): + return + + + + + + + +if __name__ == '__main__': + print("") diff --git a/torchquantum/measurement/measurements.py b/torchquantum/measurement/measurements.py index c3c2daad..bb778c83 100644 --- a/torchquantum/measurement/measurements.py +++ b/torchquantum/measurement/measurements.py @@ -43,13 +43,7 @@ def measure(qdev, n_shots=1024, draw_id=None): distribution of bitstrings """ bitstring_candidates = gen_bitstrings(qdev.n_wires) - if isinstance(qdev, tq.QuantumDevice): - state_mag = qdev.get_states_1d().abs().detach().cpu().numpy() - elif isinstance(qdev, tq.NoiseDevice): - ''' - Measure the density matrix in the computational basis - ''' - state_mag = qdev.get_probs_1d().abs().detach().cpu().numpy() + state_mag = qdev.get_states_1d().abs().detach().cpu().numpy() distri_all = [] for state_mag_one in state_mag: From 00e6dabdbb05b2de5e1e2f5e1f860a993ea447ab Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Sat, 10 Feb 2024 20:32:08 -0800 Subject: [PATCH 15/54] [Func] Implement three measurement for density matrix. --- .../measurement/density_measurements.py | 222 +++++++++++++++++- torchquantum/measurement/measurements.py | 2 + 2 files changed, 216 insertions(+), 8 deletions(-) diff --git a/torchquantum/measurement/density_measurements.py b/torchquantum/measurement/density_measurements.py index 37908760..c97b5934 100644 --- a/torchquantum/measurement/density_measurements.py +++ b/torchquantum/measurement/density_measurements.py @@ -14,6 +14,8 @@ import torchquantum.operator as op from copy import deepcopy import matplotlib.pyplot as plt +from measurements import gen_bitstrings +from measurements import find_observable_groups __all__ = [ "expval_joint_sampling_grouping", @@ -25,16 +27,94 @@ def measure(noisedev: tq.NoiseDevice, n_shots=1024, draw_id=None): - return + """Measure the target density matrix and obtain classical bitstream distribution + Args: + noisedev: input tq.NoiseDevice + n_shots: number of simulated shots + Returns: + distribution of bitstrings + """ + bitstring_candidates = gen_bitstrings(noisedev.n_wires) + + state_mag = noisedev.get_probs_1d().abs().detach().cpu().numpy() + distri_all = [] + + for state_mag_one in state_mag: + state_prob_one = state_mag_one + measured = random.choices( + population=bitstring_candidates, + weights=state_prob_one, + k=n_shots, + ) + counter = Counter(measured) + counter.update({key: 0 for key in bitstring_candidates}) + distri = dict(counter) + distri = OrderedDict(sorted(distri.items())) + distri_all.append(distri) + if draw_id is not None: + plt.bar(distri_all[draw_id].keys(), distri_all[draw_id].values()) + plt.xticks(rotation="vertical") + plt.xlabel("bitstring [qubit0, qubit1, ..., qubitN]") + plt.title("distribution of measured bitstrings") + plt.show() + return distri_all def expval_joint_sampling_grouping( - qdev: tq.NoiseDevice, + noisedev: tq.NoiseDevice, observables: List[str], n_shots_per_group=1024, ): - return + assert len(observables) == len(set(observables)), "each observable should be unique" + # key is the group, values is the list of sub-observables + obs = [] + for observable in observables: + obs.append(observable.upper()) + # firstly find the groups + groups = find_observable_groups(obs) + + # rotation to the desired basis + n_wires = noisedev.n_wires + paulix = op.op_name_dict["paulix"] + pauliy = op.op_name_dict["pauliy"] + pauliz = op.op_name_dict["pauliz"] + iden = op.op_name_dict["i"] + pauli_dict = {"X": paulix, "Y": pauliy, "Z": pauliz, "I": iden} + + expval_all_obs = {} + for obs_group, obs_elements in groups.items(): + # for each group need to clone a new qdev and its states + noisedev_clone = tq.NoiseDevice(n_wires=noisedev.n_wires, bsz=noisedev.bsz, device=noisedev.device) + noisedev_clone.clone_densities(noisedev.densities) + + for wire in range(n_wires): + for rotation in pauli_dict[obs_group[wire]]().diagonalizing_gates(): + rotation(noisedev_clone, wires=wire) + + # measure + distributions = measure(noisedev_clone, n_shots=n_shots_per_group) + # interpret the distribution for different observable elements + for obs_element in obs_elements: + expval_all = [] + mask = np.ones(len(obs_element), dtype=bool) + mask[np.array([*obs_element]) == "I"] = False + + for distri in distributions: + n_eigen_one = 0 + n_eigen_minus_one = 0 + for bitstring, n_count in distri.items(): + if np.dot(list(map(lambda x: eval(x), [*bitstring])), mask).sum() % 2 == 0: + n_eigen_one += n_count + else: + n_eigen_minus_one += n_count + + expval = n_eigen_one / n_shots_per_group + (-1) * n_eigen_minus_one / n_shots_per_group + + expval_all.append(expval) + expval_all_obs[obs_element] = torch.tensor(expval_all, dtype=F_DTYPE) + + return expval_all_obs def expval_joint_sampling( @@ -46,24 +126,150 @@ def expval_joint_sampling( def expval_joint_analytical( - qdev: tq.NoiseDevice, + noisedev: tq.NoiseDevice, observable: str, + n_shots=1024 ): - return + """ + Compute the expectation value of a joint observable from sampling + the measurement bistring + Args: + qdev: the quantum device + observable: the joint observable, on the qubit 0, 1, 2, 3, etc in this order + Returns: + the expectation value + Examples: + >>> import torchquantum as tq + >>> import torchquantum.functional as tqf + >>> x = tq.NoiseDevice(n_wires=2) + >>> tqf.hadamard(x, wires=0) + >>> tqf.x(x, wires=1) + >>> tqf.cnot(x, wires=[0, 1]) + >>> print(expval_joint_sampling(x, 'II', n_shots=8192)) + tensor([[0.9997]]) + >>> print(expval_joint_sampling(x, 'XX', n_shots=8192)) + tensor([[0.9991]]) + >>> print(expval_joint_sampling(x, 'ZZ', n_shots=8192)) + tensor([[-0.9980]]) + """ + # rotation to the desired basis + n_wires = noisedev.n_wires + paulix = op.op_name_dict["paulix"] + pauliy = op.op_name_dict["pauliy"] + pauliz = op.op_name_dict["pauliz"] + iden = op.op_name_dict["i"] + pauli_dict = {"X": paulix, "Y": pauliy, "Z": pauliz, "I": iden} + + noisedev_clone = tq.NoiseDevice(n_wires=noisedev.n_wires, bsz=noisedev.bsz, device=noisedev.device) + noisedev_clone.clone_densities(noisedev.densities) + + observable = observable.upper() + for wire in range(n_wires): + for rotation in pauli_dict[observable[wire]]().diagonalizing_gates(): + rotation(noisedev_clone, wires=wire) + + mask = np.ones(len(observable), dtype=bool) + mask[np.array([*observable]) == "I"] = False + + expval_all = [] + # measure + distributions = measure(noisedev_clone, n_shots=n_shots) + for distri in distributions: + n_eigen_one = 0 + n_eigen_minus_one = 0 + for bitstring, n_count in distri.items(): + if np.dot(list(map(lambda x: eval(x), [*bitstring])), mask).sum() % 2 == 0: + n_eigen_one += n_count + else: + n_eigen_minus_one += n_count + + expval = n_eigen_one / n_shots + (-1) * n_eigen_minus_one / n_shots + expval_all.append(expval) + + return torch.tensor(expval_all, dtype=F_DTYPE) def expval( - qdev: tq.NoiseDevice, + noisedev: tq.NoiseDevice, wires: Union[int, List[int]], observables: Union[op.Observable, List[op.Observable]], ): - return + all_dims = np.arange(noisedev.densities.dim()) + if isinstance(wires, int): + wires = [wires] + observables = [observables] + + # rotation to the desired basis + for wire, observable in zip(wires, observables): + for rotation in observable.diagonalizing_gates(): + rotation(noisedev, wires=wire) + + # compute magnitude + state_mag = noisedev.get_probs_1d() + expectations = [] + for wire, observable in zip(wires, observables): + # compute marginal magnitude + reduction_dims = np.delete(all_dims, [0, wire + 1]) + if reduction_dims.size == 0: + probs = state_mag + else: + probs = state_mag.sum(list(reduction_dims)) + res = probs.mv(observable.eigvals.real.to(probs.device)) + expectations.append(res) + return torch.stack(expectations, dim=-1) +class MeasureAll(tq.QuantumModule): + """Obtain the expectation value of all the qubits.""" + def __init__(self, obs, v_c_reg_mapping=None): + super().__init__() + self.obs = obs + self.v_c_reg_mapping = v_c_reg_mapping + + def forward(self, qdev: tq.NoiseDevice): + x = expval(qdev, list(range(qdev.n_wires)), [self.obs()] * qdev.n_wires) + + if self.v_c_reg_mapping is not None: + c2v_mapping = self.v_c_reg_mapping["c2v"] + """ + the measurement is not normal order, need permutation + """ + perm = [] + for k in range(x.shape[-1]): + if k in c2v_mapping.keys(): + perm.append(c2v_mapping[k]) + x = x[:, perm] + + if self.noise_model_tq is not None and self.noise_model_tq.is_add_noise: + return self.noise_model_tq.apply_readout_error(x) + else: + return x + + def set_v_c_reg_mapping(self, mapping): + self.v_c_reg_mapping = mapping if __name__ == '__main__': - print("") + print("Yes") + qdev = tq.NoiseDevice(n_wires=2, bsz=5, device="cpu", record_op=True) # use device='cuda' for GPU + qdev.h(wires=0) + qdev.cnot(wires=[0, 1]) + tqf.h(qdev, wires=1) + tqf.x(qdev, wires=1) + op = tq.RX(has_params=True, trainable=True, init_params=0.5) + op(qdev, wires=0) + + # measure the state on z basis + print(tq.measure(qdev, n_shots=1024)) + + + + ''' + # obtain the expval on a observable + expval = expval_joint_sampling(qdev, 'II', 100000) + expval_ana = expval_joint_analytical(qdev, 'II') + print(expval, expval_ana) + ''' \ No newline at end of file diff --git a/torchquantum/measurement/measurements.py b/torchquantum/measurement/measurements.py index bb778c83..aaa30e12 100644 --- a/torchquantum/measurement/measurements.py +++ b/torchquantum/measurement/measurements.py @@ -43,6 +43,8 @@ def measure(qdev, n_shots=1024, draw_id=None): distribution of bitstrings """ bitstring_candidates = gen_bitstrings(qdev.n_wires) + + #state_prob = state_mag = qdev.get_states_1d().abs().detach().cpu().numpy() distri_all = [] From e077d2fa372330c189e4c78ee4176f71f199dfbe Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 16 Feb 2024 15:02:33 -0800 Subject: [PATCH 16/54] [Fix] Fix the dimension bug of expeval of density matrix. --- examples/mnist/mnist_noise.py | 2 +- torchquantum/device/noisedevices.py | 10 ++++++---- .../measurement/density_measurements.py | 20 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/mnist/mnist_noise.py b/examples/mnist/mnist_noise.py index 801e6621..7b0ca21e 100644 --- a/examples/mnist/mnist_noise.py +++ b/examples/mnist/mnist_noise.py @@ -82,7 +82,7 @@ def __init__(self): self.encoder = tq.GeneralEncoder(tq.encoder_op_list_name_dict["4x4_u3_h_rx"]) self.q_layer = self.QLayer() - self.measure = tq.MeasureAll(tq.PauliZ) + self.measure = tq.MeasureAll_Density(tq.PauliZ) def forward(self, x, use_qiskit=False): qdev = tq.NoiseDevice( diff --git a/torchquantum/device/noisedevices.py b/torchquantum/device/noisedevices.py index e573c3d7..4c61bdee 100644 --- a/torchquantum/device/noisedevices.py +++ b/torchquantum/device/noisedevices.py @@ -73,12 +73,10 @@ def __init__( self.record_op = record_op self.op_history = [] - def reset_op_history(self): """Resets the all Operation of the quantum device""" self.op_history = [] - def print_2d(self, index): """Print the matrix value at the given index. @@ -125,12 +123,16 @@ def get_probs_1d(self): """Return the states in a 1d tensor.""" bsz = self.densities.shape[0] densities2d = torch.reshape(self.densities, [bsz, 2 ** self.n_wires, 2 ** self.n_wires]) - return torch.diagonal(densities2d, offset=0, dim1=1, dim2=2) + return torch.abs(torch.diagonal(densities2d, offset=0, dim1=1, dim2=2)) def get_prob_1d(self): """Return the state in a 1d tensor.""" density2d = torch.reshape(self.density, [2 ** self.n_wires, 2 ** self.n_wires]) - return torch.diagonal(density2d, offset=0, dim1=0, dim2=1) + return torch.abs(torch.diagonal(density2d, offset=0, dim1=0, dim2=1)) + + def clone_densities(self, existing_densities: torch.Tensor): + """Clone the densities of the other quantum device.""" + self.densities = existing_densities.clone() for func_name, func in func_name_dict.items(): diff --git a/torchquantum/measurement/density_measurements.py b/torchquantum/measurement/density_measurements.py index c97b5934..ebb82279 100644 --- a/torchquantum/measurement/density_measurements.py +++ b/torchquantum/measurement/density_measurements.py @@ -14,8 +14,8 @@ import torchquantum.operator as op from copy import deepcopy import matplotlib.pyplot as plt -from measurements import gen_bitstrings -from measurements import find_observable_groups +from .measurements import gen_bitstrings +from .measurements import find_observable_groups __all__ = [ "expval_joint_sampling_grouping", @@ -23,6 +23,7 @@ "expval_joint_sampling", "expval", "measure", + "MeasureAll_Density" ] @@ -194,7 +195,7 @@ def expval( wires: Union[int, List[int]], observables: Union[op.Observable, List[op.Observable]], ): - all_dims = np.arange(noisedev.densities.dim()) + all_dims = np.arange(noisedev.n_wires+1) if isinstance(wires, int): wires = [wires] observables = [observables] @@ -206,7 +207,8 @@ def expval( # compute magnitude state_mag = noisedev.get_probs_1d() - + bsz = state_mag.shape[0] + state_mag = torch.reshape(state_mag, [bsz] + [2] * noisedev.n_wires) expectations = [] for wire, observable in zip(wires, observables): # compute marginal magnitude @@ -221,7 +223,7 @@ def expval( return torch.stack(expectations, dim=-1) -class MeasureAll(tq.QuantumModule): +class MeasureAll_Density(tq.QuantumModule): """Obtain the expectation value of all the qubits.""" def __init__(self, obs, v_c_reg_mapping=None): @@ -265,11 +267,7 @@ def set_v_c_reg_mapping(self, mapping): # measure the state on z basis print(tq.measure(qdev, n_shots=1024)) - - - ''' # obtain the expval on a observable expval = expval_joint_sampling(qdev, 'II', 100000) - expval_ana = expval_joint_analytical(qdev, 'II') - print(expval, expval_ana) - ''' \ No newline at end of file + # expval_ana = expval_joint_analytical(qdev, 'II') + print(expval) From 204b7068c5386fd8b076e5bf92e81a62885cffb2 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Fri, 16 Feb 2024 15:50:33 -0800 Subject: [PATCH 17/54] [Feat] Add noise model to the density device. --- examples/mnist/mnist_noise.py | 9 +++--- torchquantum/device/noisedevices.py | 27 +++++++++++++++++- torchquantum/functional/gate_wrapper.py | 37 +++++++++++++------------ 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/examples/mnist/mnist_noise.py b/examples/mnist/mnist_noise.py index 7b0ca21e..a1b08d8a 100644 --- a/examples/mnist/mnist_noise.py +++ b/examples/mnist/mnist_noise.py @@ -86,7 +86,8 @@ def __init__(self): def forward(self, x, use_qiskit=False): qdev = tq.NoiseDevice( - n_wires=self.n_wires, bsz=x.shape[0], device=x.device, record_op=True + n_wires=self.n_wires, bsz=x.shape[0], device=x.device, record_op=True, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.2, "Phaseflip": 0}), ) bsz = x.shape[0] @@ -180,10 +181,10 @@ def main(): ) parser.add_argument("--pdb", action="store_true", help="debug with pdb") parser.add_argument( - "--wires-per-block", type=int, default=2, help="wires per block int static mode" + "--wires-per-block", type=int, default=20, help="wires per block int static mode" ) parser.add_argument( - "--epochs", type=int, default=2, help="number of training epochs" + "--epochs", type=int, default=20, help="number of training epochs" ) args = parser.parse_args() @@ -244,7 +245,5 @@ def main(): valid_test(dataflow, "test", model, device, qiskit=False) - - if __name__ == "__main__": main() diff --git a/torchquantum/device/noisedevices.py b/torchquantum/device/noisedevices.py index 4c61bdee..c9b7efac 100644 --- a/torchquantum/device/noisedevices.py +++ b/torchquantum/device/noisedevices.py @@ -30,7 +30,26 @@ from torchquantum.functional import func_name_dict, func_name_dict_collect from typing import Union -__all__ = ["NoiseDevice"] +__all__ = ["NoiseDevice", "NoiseModel"] + + +class NoiseModel: + '' + + def __init__(self, + kraus_dict + ): + """A quantum noise model + Args: + kraus_dict: the karus_dict for this noise_model. + For example: + kraus_dict={"Bitflip":0.5, "Phaseflip":0.5} + """ + self._kraus_dict = kraus_dict + # TODO: Check that the trace is preserved + + def kraus_dict(self): + return self._kraus_dict class NoiseDevice(nn.Module): @@ -41,6 +60,7 @@ def __init__( bsz: int = 1, device: Union[torch.device, str] = "cpu", record_op: bool = False, + noise_model: NoiseModel = NoiseModel(kraus_dict={"Bitflip": 0, "Phaseflip": 0}) ): """A quantum device that support the density matrix simulation Args: @@ -73,6 +93,8 @@ def __init__( self.record_op = record_op self.op_history = [] + self._noise_model = noise_model + def reset_op_history(self): """Resets the all Operation of the quantum device""" self.op_history = [] @@ -134,6 +156,9 @@ def clone_densities(self, existing_densities: torch.Tensor): """Clone the densities of the other quantum device.""" self.densities = existing_densities.clone() + def noise_model(self): + return self._noise_model + for func_name, func in func_name_dict.items(): setattr(NoiseDevice, func_name, func) diff --git a/torchquantum/functional/gate_wrapper.py b/torchquantum/functional/gate_wrapper.py index 27120476..2e507f43 100644 --- a/torchquantum/functional/gate_wrapper.py +++ b/torchquantum/functional/gate_wrapper.py @@ -180,16 +180,13 @@ def apply_unitary_density_einsum(density, mat, wires): # Tensor indices of the quantum state density_indices = ABC[:total_wires] - print("density_indices", density_indices) # Indices of the quantum state affected by this operation affected_indices = "".join(ABC_ARRAY[list(device_wires)].tolist()) - print("affected_indices", affected_indices) # All affected indices will be summed over, so we need the same number # of new indices new_indices = ABC[total_wires: total_wires + len(device_wires)] - print("new_indices", new_indices) # The new indices of the state are given by the old ones with the # affected indices replaced by the new_indices @@ -198,7 +195,6 @@ def apply_unitary_density_einsum(density, mat, wires): zip(affected_indices, new_indices), density_indices, ) - print("new_density_indices", new_density_indices) # Use the last literal as the indice of batch density_indices = ABC[-1] + density_indices @@ -211,29 +207,24 @@ def apply_unitary_density_einsum(density, mat, wires): einsum_indices = ( f"{new_indices}{affected_indices}," f"{density_indices}->{new_density_indices}" ) - print("einsum_indices", einsum_indices) new_density = torch.einsum(einsum_indices, mat, density) """ Compute U \rho U^\dagger """ - print("dagger") # Tensor indices of the quantum state density_indices = ABC[:total_wires] - print("density_indices", density_indices) # Indices of the quantum state affected by this operation affected_indices = "".join( ABC_ARRAY[[x + n_qubit for x in list(device_wires)]].tolist() ) - print("affected_indices", affected_indices) # All affected indices will be summed over, so we need the same number # of new indices new_indices = ABC[total_wires: total_wires + len(device_wires)] - print("new_indices", new_indices) # The new indices of the state are given by the old ones with the # affected indices replaced by the new_indices @@ -242,7 +233,6 @@ def apply_unitary_density_einsum(density, mat, wires): zip(affected_indices, new_indices), density_indices, ) - print("new_density_indices", new_density_indices) density_indices = ABC[-1] + density_indices new_density_indices = ABC[-1] + new_density_indices @@ -254,7 +244,6 @@ def apply_unitary_density_einsum(density, mat, wires): einsum_indices = ( f"{density_indices}," f"{affected_indices}{new_indices}->{new_density_indices}" ) - print("einsum_indices", einsum_indices) new_density = torch.einsum(einsum_indices, density, matdag) @@ -299,7 +288,6 @@ def apply_unitary_density_bmm(density, mat, wires): Compute \rho U^\dagger """ - matdag = mat.conj() if matdag.dim() == 3: matdag = matdag.permute(0, 2, 1) @@ -328,6 +316,12 @@ def apply_unitary_density_bmm(density, mat, wires): return new_density +_noise_mat_dict = { + "Bitflip": torch.tensor([[0, 1], [1, 0]], dtype=C_DTYPE), + "Phaseflip": torch.tensor([[1, 0], [0, -1]], dtype=C_DTYPE) +} + + def gate_wrapper( name, mat, @@ -366,8 +360,6 @@ def gate_wrapper( """ if params is not None: - print("Start change params:") - print(params) if not isinstance(params, torch.Tensor): if name in ["qubitunitary", "qubitunitaryfast", "qubitunitarystrict"]: # this is for qubitunitary gate @@ -375,8 +367,6 @@ def gate_wrapper( else: # this is for directly inputting parameters as a number params = torch.tensor(params, dtype=F_DTYPE) - print("Become torch tensor:") - print(params) if name in ["qubitunitary", "qubitunitaryfast", "qubitunitarystrict"]: params = params.unsqueeze(0) if params.dim() == 2 else params else: @@ -385,8 +375,6 @@ def gate_wrapper( elif params.dim() == 0: params = params.unsqueeze(-1).unsqueeze(-1) # params = params.unsqueeze(-1) if params.dim() == 1 else params - print("Final params") - print(params) wires = [wires] if isinstance(wires, int) else wires if q_device.record_op: @@ -449,6 +437,19 @@ def gate_wrapper( if method == "einsum": return elif method == "bmm": + ''' + Apply kraus operator if there is noise + ''' + kraus_dict = q_device.noise_model().kraus_dict() + if(kraus_dict["Bitflip"]!=0 or kraus_dict["Phaseflip"] != 0): + p_identity = 1 - kraus_dict["Bitflip"] ** 2 - kraus_dict["Phaseflip"] ** 2 + if kraus_dict["Bitflip"] != 0: + noise_mat = kraus_dict["Bitflip"] * _noise_mat_dict["Bitflip"] + density_noise = apply_unitary_density_bmm(density, noise_mat, wires) + if kraus_dict["Phaseflip"] != 0: + noise_mat = kraus_dict["Phaseflip"] * _noise_mat_dict["Bitflip"] + density_noise =density_noise+ apply_unitary_density_bmm(density, noise_mat, wires) + density=p_identity*density+density_noise q_device.densities = apply_unitary_density_bmm(density, matrix, wires) else: state = q_device.states From 88b67c84ac9e89deb6d90e4c0974e712b709023c Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Sat, 17 Feb 2024 19:54:16 -0800 Subject: [PATCH 18/54] [FIX] Pass a parameter numer to the gate wrapper --- examples/mnist/mnist_noise.py | 20 +++++++++++-- test/density/test_density_op.py | 36 +++++++++--------------- torchquantum/functional/gate_wrapper.py | 13 +++++++-- torchquantum/functional/phase_shift.py | 1 + torchquantum/functional/qubit_unitary.py | 2 ++ torchquantum/functional/r.py | 1 + torchquantum/functional/rot.py | 2 ++ torchquantum/functional/rx.py | 2 ++ torchquantum/functional/ry.py | 3 ++ torchquantum/functional/rz.py | 5 ++++ torchquantum/functional/test.py | 32 +++++++++++++++++++++ torchquantum/functional/u1.py | 2 ++ torchquantum/functional/u2.py | 2 ++ torchquantum/functional/u3.py | 2 ++ torchquantum/measurement/__init__.py | 1 + 15 files changed, 95 insertions(+), 29 deletions(-) create mode 100644 torchquantum/functional/test.py diff --git a/examples/mnist/mnist_noise.py b/examples/mnist/mnist_noise.py index a1b08d8a..b28f7a90 100644 --- a/examples/mnist/mnist_noise.py +++ b/examples/mnist/mnist_noise.py @@ -40,6 +40,8 @@ from torchquantum.dataset import MNIST from torch.optim.lr_scheduler import CosineAnnealingLR +import pickle + class QFCModel(tq.QuantumModule): class QLayer(tq.QuantumModule): @@ -87,7 +89,7 @@ def __init__(self): def forward(self, x, use_qiskit=False): qdev = tq.NoiseDevice( n_wires=self.n_wires, bsz=x.shape[0], device=x.device, record_op=True, - noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.2, "Phaseflip": 0}), + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.08, "Phaseflip": 0.08}), ) bsz = x.shape[0] @@ -151,6 +153,7 @@ def train(dataflow, model, device, optimizer): def valid_test(dataflow, split, model, device, qiskit=False): target_all = [] output_all = [] + with torch.no_grad(): for feed_dict in dataflow[split]: inputs = feed_dict["image"].to(device) @@ -173,6 +176,8 @@ def valid_test(dataflow, split, model, device, qiskit=False): print(f"{split} set accuracy: {accuracy}") print(f"{split} set loss: {loss}") + return accuracy, loss + def main(): parser = argparse.ArgumentParser() @@ -226,6 +231,9 @@ def main(): optimizer = optim.Adam(model.parameters(), lr=5e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + accuracy_list = [] + loss_list = [] + if args.static: # optionally to switch to the static mode, which can bring speedup # on training @@ -235,13 +243,19 @@ def main(): # train print(f"Epoch {epoch}:") train(dataflow, model, device, optimizer) - print(optimizer.param_groups[0]["lr"]) # valid - valid_test(dataflow, "valid", model, device) + accuracy, loss = valid_test(dataflow, "valid", model, device) + + accuracy_list.append(accuracy) + loss_list.append(loss) + scheduler.step() + with open('C:/Users/yezhu/OneDrive/Desktop/torchquantum/noisy_training_3.pickle', 'wb') as handle: + pickle.dump([accuracy_list, loss_list], handle, protocol=pickle.HIGHEST_PROTOCOL) # test + valid_test(dataflow, "test", model, device, qiskit=False) diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py index 4a244d53..8282b71f 100644 --- a/test/density/test_density_op.py +++ b/test/density/test_density_op.py @@ -50,8 +50,8 @@ {"qiskit": qiskit_gate.TGate, "tq": tq.t, "name": "T"}, {"qiskit": qiskit_gate.SdgGate, "tq": tq.sdg, "name": "SDG"}, {"qiskit": qiskit_gate.TdgGate, "tq": tq.tdg, "name": "TDG"}, - {"qiskit": qiskit_gate.SXGate, "tq": tq.sx}, - {"qiskit": qiskit_gate.SXdgGate, "tq": tq.sxdg}, + {"qiskit": qiskit_gate.SXGate, "tq": tq.sx, "name": "SX"}, + {"qiskit": qiskit_gate.SXdgGate, "tq": tq.sxdg, "name": "SXDG"}, ] single_param_gate_list = [ @@ -60,10 +60,10 @@ {"qiskit": qiskit_gate.RZGate, "tq": tq.rz, "name": "RZ", "numparam": 1}, {"qiskit": qiskit_gate.U1Gate, "tq": tq.u1, "name": "U1", "numparam": 1}, {"qiskit": qiskit_gate.PhaseGate, "tq": tq.phaseshift, "name": "Phaseshift", "numparam": 1}, - # {"qiskit": qiskit_gate.GlobalPhaseGate, "tq": tq.globalphase, "name": "Gphase", "numparam": 1}, - # {"qiskit": qiskit_gate.U2Gate, "tq": tq.u2, "name": "U2", "numparam": 2}, - # {"qiskit": qiskit_gate.U3Gate, "tq": tq.u3, "name": "U3", "numparam": 3}, - {"qiskit": qiskit_gate.RGate, "tq": tq.r, "name": "R", "numparam": 3}, + #{"qiskit": qiskit_gate.GlobalPhaseGate, "tq": tq.globalphase, "name": "Gphase", "numparam": 1}, + {"qiskit": qiskit_gate.U2Gate, "tq": tq.u2, "name": "U2", "numparam": 2}, + {"qiskit": qiskit_gate.U3Gate, "tq": tq.u3, "name": "U3", "numparam": 3}, + {"qiskit": qiskit_gate.RGate, "tq": tq.r, "name": "R", "numparam": 2}, {"qiskit": qiskit_gate.UGate, "tq": tq.u, "name": "U", "numparam": 3}, ] @@ -89,8 +89,7 @@ {"qiskit": qiskit_gate.CRYGate, "tq": tq.cry, "name": "CRY", "numparam": 1}, {"qiskit": qiskit_gate.CRZGate, "tq": tq.crz, "name": "CRZ", "numparam": 1}, {"qiskit": qiskit_gate.CU1Gate, "tq": tq.cu1, "name": "CU1", "numparam": 1}, - #{"qiskit": qiskit_gate.CU3Gate, "tq": tq.CU3, "name": "CU3", "numparam": 3}, - #{"qiskit": qiskit_gate.CUGate, "tq": tq.cu, "name": "CU", "numparam": 3} + {"qiskit": qiskit_gate.CU3Gate, "tq": tq.cu3, "name": "CU3", "numparam": 3} ] three_qubit_gate_list = [ @@ -122,7 +121,7 @@ {"qiskit": qiskit_gate.C3SXGate, "tq": tq.C3SX}, ] -maximum_qubit_num = 10 +maximum_qubit_num = 6 def density_is_close(mat1: np.ndarray, mat2: np.ndarray): @@ -162,14 +161,11 @@ def compare_single_gate_params(self, gate_pair, qubit_num): params = [] for i in range(0, paramnum): params.append(uniform(0, 6.2)) - if (paramnum == 1): - params = params[0] - print(params) gate_pair['tq'](qdev, [index], params=params) mat1 = np.array(qdev.get_2d_matrix(0)) rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](params), [qubit_num - 1 - index]) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](*params), [qubit_num - 1 - index]) mat2 = np.array(rho_qiskit.to_operator()) if density_is_close(mat1, mat2): print("Test passed for %s gate on qubit %d when qubit_number is %d!" % ( @@ -218,9 +214,6 @@ def compare_two_qubit_gate(self, gate_pair, qubit_num): gate_pair['name'], index1, index2, qubit_num)) return passed - - - def compare_two_qubit_params_gate(self, gate_pair, qubit_num): passed = True for index1 in range(0, qubit_num): @@ -231,15 +224,15 @@ def compare_two_qubit_params_gate(self, gate_pair, qubit_num): params = [] for i in range(0, paramnum): params.append(uniform(0, 6.2)) - if (paramnum == 1): - params = params[0] + qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - gate_pair['tq'](qdev, [index1, index2],params=params) + gate_pair['tq'](qdev, [index1, index2], params=params) mat1 = np.array(qdev.get_2d_matrix(0)) rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](params), [qubit_num - 1 - index1, qubit_num - 1 - index2]) + rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](*params), + [qubit_num - 1 - index1, qubit_num - 1 - index2]) mat2 = np.array(rho_qiskit.to_operator()) if density_is_close(mat1, mat2): print("Test passed for %s gate on qubit (%d,%d) when qubit_number is %d!" % ( @@ -250,14 +243,11 @@ def compare_two_qubit_params_gate(self, gate_pair, qubit_num): gate_pair['name'], index1, index2, qubit_num)) return passed - def test_two_qubits_params_gates(self): for qubit_num in range(2, maximum_qubit_num + 1): for i in range(0, len(two_qubit_param_gate_list)): self.assertTrue(self.compare_two_qubit_params_gate(two_qubit_param_gate_list[i], qubit_num)) - - def test_two_qubits_gates(self): for qubit_num in range(2, maximum_qubit_num + 1): for i in range(0, len(two_qubit_gate_list)): diff --git a/torchquantum/functional/gate_wrapper.py b/torchquantum/functional/gate_wrapper.py index 2e507f43..a266e0c7 100644 --- a/torchquantum/functional/gate_wrapper.py +++ b/torchquantum/functional/gate_wrapper.py @@ -328,6 +328,7 @@ def gate_wrapper( method, q_device: QuantumDevice, wires, + paramnum=0, params=None, n_wires=None, static=False, @@ -367,6 +368,12 @@ def gate_wrapper( else: # this is for directly inputting parameters as a number params = torch.tensor(params, dtype=F_DTYPE) + ''' + Check whether user don't set parameters of multi parameters gate + in batch mode. + ''' + if params.dim() == 1 and params.shape[0] == paramnum: + params = params.unsqueeze(0) if name in ["qubitunitary", "qubitunitaryfast", "qubitunitarystrict"]: params = params.unsqueeze(0) if params.dim() == 2 else params else: @@ -441,15 +448,15 @@ def gate_wrapper( Apply kraus operator if there is noise ''' kraus_dict = q_device.noise_model().kraus_dict() - if(kraus_dict["Bitflip"]!=0 or kraus_dict["Phaseflip"] != 0): + if (kraus_dict["Bitflip"] != 0 or kraus_dict["Phaseflip"] != 0): p_identity = 1 - kraus_dict["Bitflip"] ** 2 - kraus_dict["Phaseflip"] ** 2 if kraus_dict["Bitflip"] != 0: noise_mat = kraus_dict["Bitflip"] * _noise_mat_dict["Bitflip"] density_noise = apply_unitary_density_bmm(density, noise_mat, wires) if kraus_dict["Phaseflip"] != 0: noise_mat = kraus_dict["Phaseflip"] * _noise_mat_dict["Bitflip"] - density_noise =density_noise+ apply_unitary_density_bmm(density, noise_mat, wires) - density=p_identity*density+density_noise + density_noise = density_noise + apply_unitary_density_bmm(density, noise_mat, wires) + density = p_identity * density + density_noise q_device.densities = apply_unitary_density_bmm(density, matrix, wires) else: state = q_device.states diff --git a/torchquantum/functional/phase_shift.py b/torchquantum/functional/phase_shift.py index e873b834..e06bd901 100644 --- a/torchquantum/functional/phase_shift.py +++ b/torchquantum/functional/phase_shift.py @@ -88,6 +88,7 @@ def phaseshift( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/qubit_unitary.py b/torchquantum/functional/qubit_unitary.py index 151680a0..aea3510c 100644 --- a/torchquantum/functional/qubit_unitary.py +++ b/torchquantum/functional/qubit_unitary.py @@ -132,6 +132,7 @@ def qubitunitary( method=comp_method, q_device=q_device, wires=wires, + paramnum=4, params=params, n_wires=n_wires, static=static, @@ -227,6 +228,7 @@ def qubitunitarystrict( q_device=q_device, wires=wires, params=params, + paramnum=4, n_wires=n_wires, static=static, parent_graph=parent_graph, diff --git a/torchquantum/functional/r.py b/torchquantum/functional/r.py index d788e418..bd2aa0f6 100644 --- a/torchquantum/functional/r.py +++ b/torchquantum/functional/r.py @@ -97,6 +97,7 @@ def r( method=comp_method, q_device=q_device, wires=wires, + paramnum=2, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/rot.py b/torchquantum/functional/rot.py index 1de26ef3..af45cc7c 100644 --- a/torchquantum/functional/rot.py +++ b/torchquantum/functional/rot.py @@ -134,6 +134,7 @@ def rot( method=comp_method, q_device=q_device, wires=wires, + paramnum=3, params=params, n_wires=n_wires, static=static, @@ -181,6 +182,7 @@ def crot( method=comp_method, q_device=q_device, wires=wires, + paramnum=3, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/rx.py b/torchquantum/functional/rx.py index a1c5d732..47c3cfce 100644 --- a/torchquantum/functional/rx.py +++ b/torchquantum/functional/rx.py @@ -161,6 +161,7 @@ def rx( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -208,6 +209,7 @@ def rxx( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/ry.py b/torchquantum/functional/ry.py index d098c7df..29ec3330 100644 --- a/torchquantum/functional/ry.py +++ b/torchquantum/functional/ry.py @@ -143,6 +143,7 @@ def ryy( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -197,6 +198,7 @@ def cry( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -244,6 +246,7 @@ def ry( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/rz.py b/torchquantum/functional/rz.py index 0cc0c651..f2685d31 100644 --- a/torchquantum/functional/rz.py +++ b/torchquantum/functional/rz.py @@ -219,6 +219,7 @@ def multirz( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -266,6 +267,7 @@ def crz( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -313,6 +315,7 @@ def rz( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -360,6 +363,7 @@ def rzz( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -407,6 +411,7 @@ def rzx( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/test.py b/torchquantum/functional/test.py new file mode 100644 index 00000000..12cd9c59 --- /dev/null +++ b/torchquantum/functional/test.py @@ -0,0 +1,32 @@ +import torch + +from torchquantum.macro import C_DTYPE +from torchquantum.density import density_func +from torchquantum.density import density_mat + +if __name__ == "__main__": + mat = density_func.mat_dict["hadamard"] + + Xgatemat = density_func.mat_dict["paulix"] + print(mat) + D = density_mat.DensityMatrix(2, 1) + + rho = torch.zeros(2 ** 4, dtype=C_DTYPE) + rho = torch.reshape(rho, [4, 4]) + rho[0][0] = 1 / 2 + rho[0][3] = 1 / 2 + rho[3][0] = 1 / 2 + rho[3][3] = 1 / 2 + rho = torch.reshape(rho, [2, 2, 2, 2]) + D.update_matrix(rho) + D.print_2d(0) + newD = density_func.apply_unitary_density_bmm(D._matrix, Xgatemat, [1]) + + print("D matrix shape") + print(D._matrix.shape) + + print("newD shape") + print(newD.shape) + D.update_matrix(newD) + + D.print_2d(0) diff --git a/torchquantum/functional/u1.py b/torchquantum/functional/u1.py index 3422b976..be0efff1 100644 --- a/torchquantum/functional/u1.py +++ b/torchquantum/functional/u1.py @@ -117,6 +117,7 @@ def u1( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -164,6 +165,7 @@ def cu1( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/u2.py b/torchquantum/functional/u2.py index d9a6387a..98d201bb 100644 --- a/torchquantum/functional/u2.py +++ b/torchquantum/functional/u2.py @@ -116,6 +116,7 @@ def u2( method=comp_method, q_device=q_device, wires=wires, + paramnum=2, params=params, n_wires=n_wires, static=static, @@ -163,6 +164,7 @@ def cu2( method=comp_method, q_device=q_device, wires=wires, + paramnum=2, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/u3.py b/torchquantum/functional/u3.py index 9dd0927f..076718e5 100644 --- a/torchquantum/functional/u3.py +++ b/torchquantum/functional/u3.py @@ -158,6 +158,7 @@ def u3( method=comp_method, q_device=q_device, wires=wires, + paramnum=3, params=params, n_wires=n_wires, static=static, @@ -249,6 +250,7 @@ def cu3( method=comp_method, q_device=q_device, wires=wires, + paramnum=3, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/measurement/__init__.py b/torchquantum/measurement/__init__.py index bec5efe0..8d2ba360 100644 --- a/torchquantum/measurement/__init__.py +++ b/torchquantum/measurement/__init__.py @@ -23,3 +23,4 @@ """ from .measurements import * +from .density_measurements import * From 81a181f43722bdcdd9556319e4ce13a886b1484f Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Sat, 17 Feb 2024 19:57:28 -0800 Subject: [PATCH 19/54] Change measurements.py --- torchquantum/measurement/measurements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/torchquantum/measurement/measurements.py b/torchquantum/measurement/measurements.py index aaa30e12..41331a55 100644 --- a/torchquantum/measurement/measurements.py +++ b/torchquantum/measurement/measurements.py @@ -281,6 +281,7 @@ def expval( observables: Union[op.Observable, List[op.Observable]], ): all_dims = np.arange(qdev.states.dim()) + if isinstance(wires, int): wires = [wires] observables = [observables] @@ -291,9 +292,9 @@ def expval( rotation(qdev, wires=wire) states = qdev.states + # compute magnitude state_mag = torch.abs(states) ** 2 - expectations = [] for wire, observable in zip(wires, observables): # compute marginal magnitude From ad88a794b06990758b240969c626b9aa99f4180b Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Sat, 24 Feb 2024 18:40:42 -0800 Subject: [PATCH 20/54] [Test] Add test for density matrix measurement --- test/density/test_density_measure.py | 66 ++++++-- test/density/test_density_op.py | 2 - test/density/test_eval_observable_density.py | 147 ++++++++++++++++++ ..._expval_joint_sampling_grouping_density.py | 62 ++++++++ test/density/test_noise_model.py | 3 + 5 files changed, 265 insertions(+), 15 deletions(-) create mode 100644 test/density/test_eval_observable_density.py create mode 100644 test/density/test_expval_joint_sampling_grouping_density.py create mode 100644 test/density/test_noise_model.py diff --git a/test/density/test_density_measure.py b/test/density/test_density_measure.py index c63b48a8..c77b616d 100644 --- a/test/density/test_density_measure.py +++ b/test/density/test_density_measure.py @@ -1,24 +1,64 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + import torchquantum as tq -import numpy as np -import qiskit.circuit.library.standard_gates as qiskit_gate -from qiskit.quantum_info import DensityMatrix as qiskitDensity +from torchquantum.plugin import op_history2qiskit +from qiskit import Aer, transpile +import numpy as np -from unittest import TestCase +def test_measure(): + n_shots = 10000 + qdev = tq.NoiseDevice(n_wires=3, bsz=1, record_op=True) + qdev.x(wires=2) # type: ignore + qdev.x(wires=1) # type: ignore + qdev.ry(wires=0, params=0.98) # type: ignore + qdev.rx(wires=1, params=1.2) # type: ignore + qdev.cnot(wires=[0, 2]) # type: ignore + tq_counts = tq.measure(qdev, n_shots=n_shots) -class density_measure_test(TestCase): - def test_single_qubit_random_layer(self): - return + circ = op_history2qiskit(qdev.n_wires, qdev.op_history) + circ.measure_all() + simulator = Aer.get_backend("aer_simulator_density_matrix") + circ = transpile(circ, simulator) + qiskit_res = simulator.run(circ, shots=n_shots).result() + qiskit_counts = qiskit_res.get_counts() - def test_two_qubit_random_layer(self): - return + for k, v in tq_counts[0].items(): + # need to reverse the bitstring because qiskit is in little endian + qiskit_ratio = qiskit_counts.get(k[::-1], 0) / n_shots + tq_ratio = v / n_shots + print(k, qiskit_ratio, tq_ratio) + assert np.isclose(qiskit_ratio, tq_ratio, atol=0.1) + print("tq.measure for density matrix test passed") - def test_three_qubit_random_layer(self): - return +if __name__ == "__main__": + import pdb - def test_mixed_layer(self): - return \ No newline at end of file + pdb.set_trace() + test_measure() diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py index 8282b71f..53b2eaaf 100644 --- a/test/density/test_density_op.py +++ b/test/density/test_density_op.py @@ -35,8 +35,6 @@ from random import randrange, uniform -import qiskit.circuit.library as qiskit_library -from qiskit.quantum_info import Operator RND_TIMES = 100 diff --git a/test/density/test_eval_observable_density.py b/test/density/test_eval_observable_density.py new file mode 100644 index 00000000..c3d650dd --- /dev/null +++ b/test/density/test_eval_observable_density.py @@ -0,0 +1,147 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from qiskit import QuantumCircuit +import numpy as np +import random +from qiskit.opflow import StateFn, X, Y, Z, I + +import torchquantum as tq + +from torchquantum.measurement import expval_joint_analytical_density, expval_joint_sampling_density +from torchquantum.plugin import op_history2qiskit +from torchquantum.util import switch_little_big_endian_matrix + +import torch + +pauli_str_op_dict = { + "X": X, + "Y": Y, + "Z": Z, + "I": I, +} + + +def test_expval_observable(): + # seed = 0 + # random.seed(seed) + # np.random.seed(seed) + # torch.manual_seed(seed) + + for k in range(100): + # print(k) + n_wires = random.randint(1, 10) + obs = random.choices(["X", "Y", "Z", "I"], k=n_wires) + random_layer = tq.RandomLayer(n_ops=100, wires=list(range(n_wires))) + qdev = tq.NoiseDevice(n_wires=n_wires, bsz=1, record_op=True) + random_layer(qdev) + + expval_tq = expval_joint_analytical_density(qdev, observable="".join(obs))[0].item() + expval_tq_sampling = expval_joint_sampling_density( + qdev, observable="".join(obs), n_shots=100000 + )[0].item() + + qiskit_circ = op_history2qiskit(qdev.n_wires, qdev.op_history) + operator = pauli_str_op_dict[obs[0]] + for ob in obs[1:]: + # note here the order is reversed because qiskit is in little endian + operator = pauli_str_op_dict[ob] ^ operator + rho = StateFn(qiskit_circ).to_density_matrix() + + #print("Rho:") + #print(rho) + + rho_evaled = rho + + rho_tq = switch_little_big_endian_matrix( + qdev.get_densities_2d().detach().numpy() + )[0] + + assert np.allclose(rho_evaled, rho_tq, atol=1e-5) + + #print("RHO passed!") + #print("rho_evaled.shape") + #print(rho_evaled.shape) + #print("operator.shape") + #print(operator.to_matrix().shape) + + + #operator.eval() + expval_qiskit = np.trace(rho_evaled@operator.to_matrix()) + #print("TWO") + #print(expval_tq, expval_qiskit) + assert np.isclose(expval_tq, expval_qiskit, atol=1e-1) + if ( + n_wires <= 3 + ): # if too many wires, the stochastic method is not accurate due to limited shots + assert np.isclose(expval_tq_sampling, expval_qiskit, atol=1e-2) + + print("expval observable test passed") + + +def util0(): + """from below we know that the Z ^ I means Z on qubit 1 and I on qubit 0""" + qc = QuantumCircuit(2) + + qc.x(0) + + operator = Z ^ I + psi = StateFn(qc) + expectation_value = (~psi @ operator @ psi).eval() + print(expectation_value.real) + # result: 1.0, means measurement result is 0, so Z is on qubit 1 + + operator = I ^ Z + psi = StateFn(qc) + expectation_value = (~psi @ operator @ psi).eval() + print(expectation_value.real) + # result: -1.0 means measurement result is 1, so Z is on qubit 0 + + operator = I ^ I + psi = StateFn(qc) + expectation_value = (~psi @ operator @ psi).eval() + print(expectation_value.real) + + operator = Z ^ Z + psi = StateFn(qc) + expectation_value = (~psi @ operator @ psi).eval() + print(expectation_value.real) + + qc = QuantumCircuit(3) + + qc.x(0) + + operator = I ^ I ^ Z + psi = StateFn(qc) + expectation_value = (~psi @ operator @ psi).eval() + print(expectation_value.real) + + +if __name__ == "__main__": + #import pdb + + #pdb.set_trace() + + util0() + #test_expval_observable() diff --git a/test/density/test_expval_joint_sampling_grouping_density.py b/test/density/test_expval_joint_sampling_grouping_density.py new file mode 100644 index 00000000..1b383d36 --- /dev/null +++ b/test/density/test_expval_joint_sampling_grouping_density.py @@ -0,0 +1,62 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torchquantum as tq +from torchquantum.measurement import ( + expval_joint_analytical_density, + expval_joint_sampling_grouping_density, +) + +import numpy as np +import random + + +def test_expval_joint_sampling_grouping(): + n_obs = 20 + n_wires = 4 + obs_all = [] + for _ in range(n_obs): + obs = random.choices(["X", "Y", "Z", "I"], k=n_wires) + obs_all.append("".join(obs)) + obs_all = list(set(obs_all)) + + random_layer = tq.RandomLayer(n_ops=100, wires=list(range(n_wires))) + qdev = tq.NoiseDevice(n_wires=n_wires, bsz=1, record_op=True) + random_layer(qdev) + + expval_ana = {} + for obs in obs_all: + expval_ana[obs] = expval_joint_analytical_density(qdev, observable=obs)[0].item() + + expval_sam = expval_joint_sampling_grouping_density( + qdev, observables=obs_all, n_shots_per_group=1000000 + ) + for obs in obs_all: + # assert + assert np.isclose(expval_ana[obs], expval_sam[obs][0].item(), atol=1e-1) + print(obs, expval_ana[obs], expval_sam[obs][0].item()) + + +if __name__ == "__main__": + test_expval_joint_sampling_grouping() diff --git a/test/density/test_noise_model.py b/test/density/test_noise_model.py new file mode 100644 index 00000000..b28b04f6 --- /dev/null +++ b/test/density/test_noise_model.py @@ -0,0 +1,3 @@ + + + From 865b8369f4d0e09bb966e1ba2951f3db54b0c389 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Sat, 24 Feb 2024 18:44:31 -0800 Subject: [PATCH 21/54] [Rename] Rename. --- examples/mnist/mnist_noise.py | 2 +- test/density/test_density_measure.py | 2 +- test/density/test_eval_observable_density.py | 2 +- ..._expval_joint_sampling_grouping_density.py | 4 +- torchquantum/device/noisedevices.py | 13 +++ .../measurement/density_measurements.py | 91 +++++++++++++++---- 6 files changed, 91 insertions(+), 23 deletions(-) diff --git a/examples/mnist/mnist_noise.py b/examples/mnist/mnist_noise.py index b28f7a90..252f25d0 100644 --- a/examples/mnist/mnist_noise.py +++ b/examples/mnist/mnist_noise.py @@ -84,7 +84,7 @@ def __init__(self): self.encoder = tq.GeneralEncoder(tq.encoder_op_list_name_dict["4x4_u3_h_rx"]) self.q_layer = self.QLayer() - self.measure = tq.MeasureAll_Density(tq.PauliZ) + self.measure = tq.MeasureAll_density(tq.PauliZ) def forward(self, x, use_qiskit=False): qdev = tq.NoiseDevice( diff --git a/test/density/test_density_measure.py b/test/density/test_density_measure.py index c77b616d..c6066781 100644 --- a/test/density/test_density_measure.py +++ b/test/density/test_density_measure.py @@ -29,7 +29,7 @@ import numpy as np -def test_measure(): +def test_measure_density(): n_shots = 10000 qdev = tq.NoiseDevice(n_wires=3, bsz=1, record_op=True) qdev.x(wires=2) # type: ignore diff --git a/test/density/test_eval_observable_density.py b/test/density/test_eval_observable_density.py index c3d650dd..cb893542 100644 --- a/test/density/test_eval_observable_density.py +++ b/test/density/test_eval_observable_density.py @@ -43,7 +43,7 @@ } -def test_expval_observable(): +def test_expval_observable_density(): # seed = 0 # random.seed(seed) # np.random.seed(seed) diff --git a/test/density/test_expval_joint_sampling_grouping_density.py b/test/density/test_expval_joint_sampling_grouping_density.py index 1b383d36..ae3c034e 100644 --- a/test/density/test_expval_joint_sampling_grouping_density.py +++ b/test/density/test_expval_joint_sampling_grouping_density.py @@ -32,7 +32,7 @@ import random -def test_expval_joint_sampling_grouping(): +def test_expval_joint_sampling_grouping_density(): n_obs = 20 n_wires = 4 obs_all = [] @@ -59,4 +59,4 @@ def test_expval_joint_sampling_grouping(): if __name__ == "__main__": - test_expval_joint_sampling_grouping() + test_expval_joint_sampling_grouping_density() diff --git a/torchquantum/device/noisedevices.py b/torchquantum/device/noisedevices.py index c9b7efac..a5118188 100644 --- a/torchquantum/device/noisedevices.py +++ b/torchquantum/device/noisedevices.py @@ -124,6 +124,19 @@ def get_2d_matrix(self, index): _matrix = torch.reshape(self.densities[index], [2 ** self.n_wires] * 2) return _matrix + + def get_densities_2d(self): + """Return the states in a 1d tensor.""" + bsz = self.densities.shape[0] + return torch.reshape(self.densities, [bsz, 2**self.n_wires, 2**self.n_wires]) + + def get_density_2d(self): + """Return the state in a 1d tensor.""" + return torch.reshape(self.density, [2**self.n_wires,2**self.n_wires]) + + + + def calc_trace(self, index): _matrix = torch.reshape(self.densities[index], [2 ** self.n_wires] * 2) return torch.trace(_matrix) diff --git a/torchquantum/measurement/density_measurements.py b/torchquantum/measurement/density_measurements.py index ebb82279..a09098a5 100644 --- a/torchquantum/measurement/density_measurements.py +++ b/torchquantum/measurement/density_measurements.py @@ -18,16 +18,16 @@ from .measurements import find_observable_groups __all__ = [ - "expval_joint_sampling_grouping", - "expval_joint_analytical", - "expval_joint_sampling", - "expval", - "measure", - "MeasureAll_Density" + "expval_joint_sampling_grouping_density", + "expval_joint_sampling_density", + "expval_joint_analytical_density", + "expval_density", + "measure_density", + "MeasureAll_density" ] -def measure(noisedev: tq.NoiseDevice, n_shots=1024, draw_id=None): +def measure_density(noisedev: tq.NoiseDevice, n_shots=1024, draw_id=None): """Measure the target density matrix and obtain classical bitstream distribution Args: noisedev: input tq.NoiseDevice @@ -62,7 +62,7 @@ def measure(noisedev: tq.NoiseDevice, n_shots=1024, draw_id=None): return distri_all -def expval_joint_sampling_grouping( +def expval_joint_sampling_grouping_density( noisedev: tq.NoiseDevice, observables: List[str], n_shots_per_group=1024, @@ -85,7 +85,7 @@ def expval_joint_sampling_grouping( expval_all_obs = {} for obs_group, obs_elements in groups.items(): - # for each group need to clone a new qdev and its states + # for each group need to clone a new qdev and its densities noisedev_clone = tq.NoiseDevice(n_wires=noisedev.n_wires, bsz=noisedev.bsz, device=noisedev.device) noisedev_clone.clone_densities(noisedev.densities) @@ -94,7 +94,7 @@ def expval_joint_sampling_grouping( rotation(noisedev_clone, wires=wire) # measure - distributions = measure(noisedev_clone, n_shots=n_shots_per_group) + distributions = measure_density(noisedev_clone, n_shots=n_shots_per_group) # interpret the distribution for different observable elements for obs_element in obs_elements: expval_all = [] @@ -118,15 +118,70 @@ def expval_joint_sampling_grouping( return expval_all_obs -def expval_joint_sampling( +def expval_joint_sampling_density( qdev: tq.NoiseDevice, observable: str, n_shots=1024, ): - return + """ + Compute the expectation value of a joint observable from sampling + the measurement bistring + Args: + qdev: the noise device + observable: the joint observable, on the qubit 0, 1, 2, 3, etc in this order + Returns: + the expectation value + Examples: + >>> import torchquantum as tq + >>> import torchquantum.functional as tqf + >>> x = tq.QuantumDevice(n_wires=2) + >>> tqf.hadamard(x, wires=0) + >>> tqf.x(x, wires=1) + >>> tqf.cnot(x, wires=[0, 1]) + >>> print(expval_joint_sampling(x, 'II', n_shots=8192)) + tensor([[0.9997]]) + >>> print(expval_joint_sampling(x, 'XX', n_shots=8192)) + tensor([[0.9991]]) + >>> print(expval_joint_sampling(x, 'ZZ', n_shots=8192)) + tensor([[-0.9980]]) + """ + # rotation to the desired basis + n_wires = qdev.n_wires + paulix = op.op_name_dict["paulix"] + pauliy = op.op_name_dict["pauliy"] + pauliz = op.op_name_dict["pauliz"] + iden = op.op_name_dict["i"] + pauli_dict = {"X": paulix, "Y": pauliy, "Z": pauliz, "I": iden} + + qdev_clone = tq.NoiseDevice(n_wires=qdev.n_wires, bsz=qdev.bsz, device=qdev.device) + qdev_clone.clone_densities(qdev.densities) + + observable = observable.upper() + for wire in range(n_wires): + for rotation in pauli_dict[observable[wire]]().diagonalizing_gates(): + rotation(qdev_clone, wires=wire) + mask = np.ones(len(observable), dtype=bool) + mask[np.array([*observable]) == "I"] = False + + expval_all = [] + # measure + distributions = measure_density(qdev_clone, n_shots=n_shots) + for distri in distributions: + n_eigen_one = 0 + n_eigen_minus_one = 0 + for bitstring, n_count in distri.items(): + if np.dot(list(map(lambda x: eval(x), [*bitstring])), mask).sum() % 2 == 0: + n_eigen_one += n_count + else: + n_eigen_minus_one += n_count + + expval = n_eigen_one / n_shots + (-1) * n_eigen_minus_one / n_shots + expval_all.append(expval) + + return torch.tensor(expval_all, dtype=F_DTYPE) -def expval_joint_analytical( +def expval_joint_analytical_density( noisedev: tq.NoiseDevice, observable: str, n_shots=1024 @@ -174,7 +229,7 @@ def expval_joint_analytical( expval_all = [] # measure - distributions = measure(noisedev_clone, n_shots=n_shots) + distributions = measure_density(noisedev_clone, n_shots=n_shots) for distri in distributions: n_eigen_one = 0 n_eigen_minus_one = 0 @@ -190,7 +245,7 @@ def expval_joint_analytical( return torch.tensor(expval_all, dtype=F_DTYPE) -def expval( +def expval_density( noisedev: tq.NoiseDevice, wires: Union[int, List[int]], observables: Union[op.Observable, List[op.Observable]], @@ -223,7 +278,7 @@ def expval( return torch.stack(expectations, dim=-1) -class MeasureAll_Density(tq.QuantumModule): +class MeasureAll_density(tq.QuantumModule): """Obtain the expectation value of all the qubits.""" def __init__(self, obs, v_c_reg_mapping=None): @@ -265,9 +320,9 @@ def set_v_c_reg_mapping(self, mapping): op(qdev, wires=0) # measure the state on z basis - print(tq.measure(qdev, n_shots=1024)) + print(tq.measure_density(qdev, n_shots=1024)) # obtain the expval on a observable - expval = expval_joint_sampling(qdev, 'II', 100000) + expval = expval_joint_sampling_density(qdev, 'II', 100000) # expval_ana = expval_joint_analytical(qdev, 'II') print(expval) From b48c966cd101b7822cf1c943342eb3eb13b624cc Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Sun, 25 Feb 2024 08:15:37 -0800 Subject: [PATCH 22/54] [Test] Density measurement test pass. --- test/density/test_density_measure.py | 8 +- test/density/test_eval_observable_density.py | 8 +- .../measurement/density_measurements.py | 117 +++++++++--------- 3 files changed, 67 insertions(+), 66 deletions(-) diff --git a/test/density/test_density_measure.py b/test/density/test_density_measure.py index c6066781..a770359c 100644 --- a/test/density/test_density_measure.py +++ b/test/density/test_density_measure.py @@ -38,7 +38,7 @@ def test_measure_density(): qdev.rx(wires=1, params=1.2) # type: ignore qdev.cnot(wires=[0, 2]) # type: ignore - tq_counts = tq.measure(qdev, n_shots=n_shots) + tq_counts = tq.measure_density(qdev, n_shots=n_shots) circ = op_history2qiskit(qdev.n_wires, qdev.op_history) circ.measure_all() @@ -58,7 +58,7 @@ def test_measure_density(): if __name__ == "__main__": - import pdb + #import pdb - pdb.set_trace() - test_measure() + #pdb.set_trace() + test_measure_density() diff --git a/test/density/test_eval_observable_density.py b/test/density/test_eval_observable_density.py index cb893542..eb628b97 100644 --- a/test/density/test_eval_observable_density.py +++ b/test/density/test_eval_observable_density.py @@ -57,10 +57,10 @@ def test_expval_observable_density(): qdev = tq.NoiseDevice(n_wires=n_wires, bsz=1, record_op=True) random_layer(qdev) - expval_tq = expval_joint_analytical_density(qdev, observable="".join(obs))[0].item() + expval_tq = expval_joint_analytical_density(qdev, observable="".join(obs))[0].item().real expval_tq_sampling = expval_joint_sampling_density( qdev, observable="".join(obs), n_shots=100000 - )[0].item() + )[0].item().real qiskit_circ = op_history2qiskit(qdev.n_wires, qdev.op_history) operator = pauli_str_op_dict[obs[0]] @@ -88,9 +88,9 @@ def test_expval_observable_density(): #operator.eval() - expval_qiskit = np.trace(rho_evaled@operator.to_matrix()) + expval_qiskit = np.trace(rho_evaled@operator.to_matrix()).real #print("TWO") - #print(expval_tq, expval_qiskit) + print(expval_tq, expval_qiskit) assert np.isclose(expval_tq, expval_qiskit, atol=1e-1) if ( n_wires <= 3 diff --git a/torchquantum/measurement/density_measurements.py b/torchquantum/measurement/density_measurements.py index a09098a5..0de82e88 100644 --- a/torchquantum/measurement/density_measurements.py +++ b/torchquantum/measurement/density_measurements.py @@ -14,8 +14,8 @@ import torchquantum.operator as op from copy import deepcopy import matplotlib.pyplot as plt -from .measurements import gen_bitstrings -from .measurements import find_observable_groups +from torchquantum.measurement import gen_bitstrings +from torchquantum.measurement import find_observable_groups __all__ = [ "expval_joint_sampling_grouping_density", @@ -181,68 +181,62 @@ def expval_joint_sampling_density( return torch.tensor(expval_all, dtype=F_DTYPE) + def expval_joint_analytical_density( noisedev: tq.NoiseDevice, observable: str, n_shots=1024 ): """ - Compute the expectation value of a joint observable from sampling - the measurement bistring - Args: - qdev: the quantum device - observable: the joint observable, on the qubit 0, 1, 2, 3, etc in this order - Returns: - the expectation value - Examples: - >>> import torchquantum as tq - >>> import torchquantum.functional as tqf - >>> x = tq.NoiseDevice(n_wires=2) - >>> tqf.hadamard(x, wires=0) - >>> tqf.x(x, wires=1) - >>> tqf.cnot(x, wires=[0, 1]) - >>> print(expval_joint_sampling(x, 'II', n_shots=8192)) - tensor([[0.9997]]) - >>> print(expval_joint_sampling(x, 'XX', n_shots=8192)) - tensor([[0.9991]]) - >>> print(expval_joint_sampling(x, 'ZZ', n_shots=8192)) - tensor([[-0.9980]]) - """ - # rotation to the desired basis - n_wires = noisedev.n_wires - paulix = op.op_name_dict["paulix"] - pauliy = op.op_name_dict["pauliy"] - pauliz = op.op_name_dict["pauliz"] - iden = op.op_name_dict["i"] + Compute the expectation value of a joint observable in analytical way, assuming the + density matrix is available. + Args: + qdev: the quantum device + observable: the joint observable, on the qubit 0, 1, 2, 3, etc in this order + Returns: + the expectation value + Examples: + >>> import torchquantum as tq + >>> import torchquantum.functional as tqf + >>> x = tq.QuantumDevice(n_wires=2) + >>> tqf.hadamard(x, wires=0) + >>> tqf.x(x, wires=1) + >>> tqf.cnot(x, wires=[0, 1]) + >>> print(expval_joint_analytical(x, 'II')) + tensor([[1.0000]]) + >>> print(expval_joint_analytical(x, 'XX')) + tensor([[1.0000]]) + >>> print(expval_joint_analytical(x, 'ZZ')) + tensor([[-1.0000]]) + """ + # compute the hamiltonian matrix + paulix = mat_dict["paulix"] + pauliy = mat_dict["pauliy"] + pauliz = mat_dict["pauliz"] + iden = mat_dict["i"] pauli_dict = {"X": paulix, "Y": pauliy, "Z": pauliz, "I": iden} - noisedev_clone = tq.NoiseDevice(n_wires=noisedev.n_wires, bsz=noisedev.bsz, device=noisedev.device) - noisedev_clone.clone_densities(noisedev.densities) - observable = observable.upper() - for wire in range(n_wires): - for rotation in pauli_dict[observable[wire]]().diagonalizing_gates(): - rotation(noisedev_clone, wires=wire) + assert len(observable) == noisedev.n_wires + densities = noisedev.get_densities_2d() - mask = np.ones(len(observable), dtype=bool) - mask[np.array([*observable]) == "I"] = False + hamiltonian = pauli_dict[observable[0]].to(densities.device) + for op in observable[1:]: + hamiltonian = torch.kron(hamiltonian, pauli_dict[op].to(densities.device)) - expval_all = [] - # measure - distributions = measure_density(noisedev_clone, n_shots=n_shots) - for distri in distributions: - n_eigen_one = 0 - n_eigen_minus_one = 0 - for bitstring, n_count in distri.items(): - if np.dot(list(map(lambda x: eval(x), [*bitstring])), mask).sum() % 2 == 0: - n_eigen_one += n_count - else: - n_eigen_minus_one += n_count + batch_size = densities.shape[0] + expanded_hamiltonian = hamiltonian.unsqueeze(0).expand(batch_size, *hamiltonian.shape) - expval = n_eigen_one / n_shots + (-1) * n_eigen_minus_one / n_shots - expval_all.append(expval) + product = torch.bmm(expanded_hamiltonian, densities) - return torch.tensor(expval_all, dtype=F_DTYPE) + # Extract the diagonal elements from each matrix in the batch + diagonals = torch.diagonal(product, dim1=-2, dim2=-1) + + # Sum the diagonal elements to get the trace for each batch + trace = torch.sum(diagonals, dim=-1).real + + # Should use expectation= Tr(observable \times density matrix) + return trace def expval_density( @@ -250,7 +244,7 @@ def expval_density( wires: Union[int, List[int]], observables: Union[op.Observable, List[op.Observable]], ): - all_dims = np.arange(noisedev.n_wires+1) + all_dims = np.arange(noisedev.n_wires + 1) if isinstance(wires, int): wires = [wires] observables = [observables] @@ -314,15 +308,22 @@ def set_v_c_reg_mapping(self, mapping): qdev = tq.NoiseDevice(n_wires=2, bsz=5, device="cpu", record_op=True) # use device='cuda' for GPU qdev.h(wires=0) qdev.cnot(wires=[0, 1]) - tqf.h(qdev, wires=1) - tqf.x(qdev, wires=1) - op = tq.RX(has_params=True, trainable=True, init_params=0.5) - op(qdev, wires=0) + #tqf.h(qdev, wires=1) + #tqf.x(qdev, wires=1) + #tqf.y(qdev, wires=1) + #tqf.cnot(qdev,wires=[0, 1]) + # op = tq.RX(has_params=True, trainable=True, init_params=0.5) + # op(qdev, wires=0) # measure the state on z basis print(tq.measure_density(qdev, n_shots=1024)) # obtain the expval on a observable - expval = expval_joint_sampling_density(qdev, 'II', 100000) - # expval_ana = expval_joint_analytical(qdev, 'II') + expval = expval_joint_sampling_density(qdev, 'XZ', 100000) + + print("expval") print(expval) + + expval_ana = expval_joint_analytical_density(qdev, 'XZ') + print("expval_ana") + print(expval_ana) From a1c4012bfd159b9844dfc9cfdd1ee124090a6560 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Sun, 25 Feb 2024 10:47:34 -0800 Subject: [PATCH 23/54] [Fix] Encoding for density matrix. An example in mnist_new_noise.py --- examples/PauliSumOp/pauli_sum_op_noise.py | 0 .../amplitude_encoding_mnist/mnist_example.py | 13 + .../mnist_example_noise.py | 223 ++++++++++++++++++ .../amplitude_encoding_mnist/mnist_new.py | 1 + .../mnist_new_noise.py | 175 ++++++++++++++ .../clifford_qnn/mnist_clifford_qnn_noise.py | 0 .../param_shift_noise.py | 0 examples/qaoa/max_cut_backprop_noise.py | 0 examples/qaoa/max_cut_parametershift_noise.py | 0 examples/quantum_lstm/qlstm_noise.py | 0 examples/quantumnat/quantumnat_noise.py | 0 examples/quanvolution/quanvolution_noise.py | 0 ...nvolution_trainable_quantum_layer_noise.py | 0 .../qubit_rotation/qubit_rotation_noise.py | 0 examples/regression/run_regression_noise.py | 0 .../train_state_prep_noise.py | 0 .../train_unitary_prep_noise.py | 0 examples/vqe/vqe_noise.py | 0 torchquantum/device/noisedevices.py | 21 +- torchquantum/encoding/encodings.py | 15 +- .../measurement/density_measurements.py | 27 ++- 21 files changed, 453 insertions(+), 22 deletions(-) create mode 100644 examples/PauliSumOp/pauli_sum_op_noise.py create mode 100644 examples/amplitude_encoding_mnist/mnist_example_noise.py create mode 100644 examples/amplitude_encoding_mnist/mnist_new_noise.py create mode 100644 examples/clifford_qnn/mnist_clifford_qnn_noise.py create mode 100644 examples/param_shift_onchip_training/param_shift_noise.py create mode 100644 examples/qaoa/max_cut_backprop_noise.py create mode 100644 examples/qaoa/max_cut_parametershift_noise.py create mode 100644 examples/quantum_lstm/qlstm_noise.py create mode 100644 examples/quantumnat/quantumnat_noise.py create mode 100644 examples/quanvolution/quanvolution_noise.py create mode 100644 examples/quanvolution/quanvolution_trainable_quantum_layer_noise.py create mode 100644 examples/qubit_rotation/qubit_rotation_noise.py create mode 100644 examples/regression/run_regression_noise.py create mode 100644 examples/train_state_prep/train_state_prep_noise.py create mode 100644 examples/train_unitary_prep/train_unitary_prep_noise.py create mode 100644 examples/vqe/vqe_noise.py diff --git a/examples/PauliSumOp/pauli_sum_op_noise.py b/examples/PauliSumOp/pauli_sum_op_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/amplitude_encoding_mnist/mnist_example.py b/examples/amplitude_encoding_mnist/mnist_example.py index ad92bb1f..b56efb83 100644 --- a/examples/amplitude_encoding_mnist/mnist_example.py +++ b/examples/amplitude_encoding_mnist/mnist_example.py @@ -100,10 +100,23 @@ def forward(self, x, use_qiskit=False): bsz = x.shape[0] x = F.avg_pool2d(x, 6).view(bsz, 16) + + print("Shape 1:") + print(self.q_device.states.shape) self.encoder(self.q_device, x) self.q_layer(self.q_device) + + + + print("X shape before measurement") + print(x.shape) + x = self.measure(self.q_device) + + print("X shape after measurement") + print(x.shape) + x = x.reshape(bsz, 2, 2).sum(-1).squeeze() x = F.log_softmax(x, dim=1) diff --git a/examples/amplitude_encoding_mnist/mnist_example_noise.py b/examples/amplitude_encoding_mnist/mnist_example_noise.py new file mode 100644 index 00000000..0b07e237 --- /dev/null +++ b/examples/amplitude_encoding_mnist/mnist_example_noise.py @@ -0,0 +1,223 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torch.nn.functional as F +import torch.optim as optim +import argparse + +import torchquantum as tq +import torchquantum.functional as tqf + +from torchquantum.dataset import MNIST +from torch.optim.lr_scheduler import CosineAnnealingLR + +import random +import numpy as np + + +class QFCModel(tq.QuantumModule): + class QLayer(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.random_layer = tq.RandomLayer( + n_ops=50, wires=list(range(self.n_wires)) + ) + + # gates with trainable parameters + self.rx0 = tq.RX(has_params=True, trainable=True) + self.ry0 = tq.RY(has_params=True, trainable=True) + self.rz0 = tq.RZ(has_params=True, trainable=True) + self.crx0 = tq.CRX(has_params=True, trainable=True) + + @tq.static_support + def forward(self, q_device: tq.NoiseDevice): + """ + 1. To convert tq QuantumModule to qiskit or run in the static + model, need to: + (1) add @tq.static_support before the forward + (2) make sure to add + static=self.static_mode and + parent_graph=self.graph + to all the tqf functions, such as tqf.hadamard below + """ + self.q_device = q_device + + self.random_layer(self.q_device) + + # some trainable gates (instantiated ahead of time) + self.rx0(self.q_device, wires=0) + self.ry0(self.q_device, wires=1) + self.rz0(self.q_device, wires=3) + self.crx0(self.q_device, wires=[0, 2]) + + # add some more non-parameterized gates (add on-the-fly) + tqf.hadamard( + self.q_device, wires=3, static=self.static_mode, parent_graph=self.graph + ) + tqf.sx( + self.q_device, wires=2, static=self.static_mode, parent_graph=self.graph + ) + tqf.cnot( + self.q_device, + wires=[3, 0], + static=self.static_mode, + parent_graph=self.graph, + ) + + def __init__(self): + super().__init__() + self.n_wires = 4 + self.q_device = tq.NoiseDevice(n_wires=self.n_wires, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.08, "Phaseflip": 0.08}) + ) + self.encoder = tq.AmplitudeEncoder() + + self.q_layer = self.QLayer() + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x, use_qiskit=False): + bsz = x.shape[0] + x = F.avg_pool2d(x, 6).view(bsz, 16) + self.encoder(self.q_device, x) + self.q_layer(self.q_device) + x = self.measure(self.q_device) + x = x.reshape(bsz, 2, 2).sum(-1).squeeze() + x = F.log_softmax(x, dim=1) + return x + + +def train(dataflow, model, device, optimizer): + for feed_dict in dataflow["train"]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs) + loss = F.nll_loss(outputs, targets) + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(f"loss: {loss.item()}", end="\r") + + +def valid_test(dataflow, split, model, device, qiskit=False): + target_all = [] + output_all = [] + with torch.no_grad(): + for feed_dict in dataflow[split]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs, use_qiskit=qiskit) + + target_all.append(targets) + output_all.append(outputs) + target_all = torch.cat(target_all, dim=0) + output_all = torch.cat(output_all, dim=0) + + _, indices = output_all.topk(1, dim=1) + masks = indices.eq(target_all.view(-1, 1).expand_as(indices)) + size = target_all.shape[0] + corrects = masks.sum().item() + accuracy = corrects / size + loss = F.nll_loss(output_all, target_all).item() + + print(f"{split} set accuracy: {accuracy}") + print(f"{split} set loss: {loss}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--static", action="store_true", help="compute with " "static mode" + ) + parser.add_argument("--pdb", action="store_true", help="debug with pdb") + parser.add_argument( + "--wires-per-block", type=int, default=2, help="wires per block int static mode" + ) + parser.add_argument( + "--epochs", type=int, default=5, help="number of training epochs" + ) + + args = parser.parse_args() + + if args.pdb: + import pdb + + pdb.set_trace() + + seed = 0 + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + dataset = MNIST( + root="./mnist_data", + train_valid_split_ratio=[0.9, 0.1], + digits_of_interest=[3, 6], + n_test_samples=75, + ) + dataflow = dict() + + for split in dataset: + sampler = torch.utils.data.RandomSampler(dataset[split]) + dataflow[split] = torch.utils.data.DataLoader( + dataset[split], + batch_size=256, + sampler=sampler, + num_workers=8, + pin_memory=True, + ) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + + model = QFCModel().to(device) + + n_epochs = args.epochs + optimizer = optim.Adam(model.parameters(), lr=5e-3, weight_decay=1e-4) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + if args.static: + # optionally to switch to the static mode, which can bring speedup + # on training + model.q_layer.static_on(wires_per_block=args.wires_per_block) + + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}:") + train(dataflow, model, device, optimizer) + print(optimizer.param_groups[0]["lr"]) + + # valid + valid_test(dataflow, "valid", model, device) + scheduler.step() + + # test + valid_test(dataflow, "test", model, device, qiskit=False) + + +if __name__ == "__main__": + main() diff --git a/examples/amplitude_encoding_mnist/mnist_new.py b/examples/amplitude_encoding_mnist/mnist_new.py index 491a1e20..9ce0bd42 100644 --- a/examples/amplitude_encoding_mnist/mnist_new.py +++ b/examples/amplitude_encoding_mnist/mnist_new.py @@ -171,3 +171,4 @@ def train_tq(model, device, train_dl, epochs, loss_fn, optimizer): print("--Training--") train_losses = train_tq(model, device, train_dl, 1, loss_fn, optimizer) + diff --git a/examples/amplitude_encoding_mnist/mnist_new_noise.py b/examples/amplitude_encoding_mnist/mnist_new_noise.py new file mode 100644 index 00000000..b15ae417 --- /dev/null +++ b/examples/amplitude_encoding_mnist/mnist_new_noise.py @@ -0,0 +1,175 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +""" +author: Vivek Yanamadula @Vivekyy +""" + +import torch +import torch.nn.functional as F + +import torchquantum as tq + +from torchquantum.dataset import MNIST +from torchquantum.operator import op_name_dict +from typing import List + + +class TQNet(tq.QuantumModule): + def __init__(self, layers: List[tq.QuantumModule], encoder=None, use_softmax=False): + super().__init__() + + self.encoder = encoder + self.use_softmax = use_softmax + + self.layers = tq.QuantumModuleList() + + for layer in layers: + self.layers.append(layer) + + self.service = "TorchQuantum" + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, device, x): + bsz = x.shape[0] + device.reset_states(bsz) + + x = F.avg_pool2d(x, 6) + x = x.view(bsz, 16) + + if self.encoder: + self.encoder(device, x) + + for layer in self.layers: + layer(device) + + meas = self.measure(device) + + if self.use_softmax: + meas = F.log_softmax(meas, dim=1) + + return meas + + +class TQLayer(tq.QuantumModule): + def __init__(self, gates: List[tq.QuantumModule]): + super().__init__() + + self.service = "TorchQuantum" + + self.layer = tq.QuantumModuleList() + for gate in gates: + self.layer.append(gate) + + @tq.static_support + def forward(self, q_device): + for gate in self.layer: + gate(q_device) + + +def train_tq(model, device, train_dl, epochs, loss_fn, optimizer): + losses = [] + for epoch in range(epochs): + running_loss = 0.0 + batches = 0 + for batch_dict in train_dl: + x = batch_dict["image"] + y = batch_dict["digit"] + + y = y.to(torch.long) + + x = x.to(torch_device) + y = y.to(torch_device) + + optimizer.zero_grad() + + preds = model(device, x) + + loss = loss_fn(preds, y) + loss.backward() + + optimizer.step() + + running_loss += loss.item() + batches += 1 + + print(f"Epoch {epoch + 1} | Loss: {running_loss/batches}", end="\r") + + print(f"Epoch {epoch + 1} | Loss: {running_loss/batches}") + losses.append(running_loss / batches) + + return losses + + +torch_device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +# encoder = None +# encoder = tq.AmplitudeEncoder() +encoder = tq.MultiPhaseEncoder(["u3", "u3", "u3", "u3"]) + + +random_layer = tq.RandomLayer(n_ops=50, wires=list(range(4))) +trainable_layer = [ + op_name_dict["rx"](trainable=True, has_params=True, wires=[0]), + op_name_dict["ry"](trainable=True, has_params=True, wires=[1]), + op_name_dict["rz"](trainable=True, has_params=True, wires=[3]), + op_name_dict["crx"](trainable=True, has_params=True, wires=[0, 2]), +] +trainable_layer = TQLayer(trainable_layer) +layers = [random_layer, trainable_layer] + +device = tq.NoiseDevice(n_wires=4, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.08, "Phaseflip": 0.08})).to(torch_device) + +model = TQNet(layers=layers, encoder=encoder, use_softmax=True).to(torch_device) + +loss_fn = F.nll_loss +optimizer = torch.optim.SGD(model.parameters(), lr=0.05) + +dataset = MNIST( + root="./mnist_data", + train_valid_split_ratio=[0.9, 0.1], + digits_of_interest=[0, 1, 3, 6], + n_test_samples=200, +) + +train_dl = torch.utils.data.DataLoader( + dataset["train"], + batch_size=32, + sampler=torch.utils.data.RandomSampler(dataset["train"]), +) +val_dl = torch.utils.data.DataLoader( + dataset["valid"], + batch_size=32, + sampler=torch.utils.data.RandomSampler(dataset["valid"]), +) +test_dl = torch.utils.data.DataLoader( + dataset["test"], + batch_size=32, + sampler=torch.utils.data.RandomSampler(dataset["test"]), +) + +print("--Training--") +train_losses = train_tq(model, device, train_dl, 1, loss_fn, optimizer) + diff --git a/examples/clifford_qnn/mnist_clifford_qnn_noise.py b/examples/clifford_qnn/mnist_clifford_qnn_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/param_shift_onchip_training/param_shift_noise.py b/examples/param_shift_onchip_training/param_shift_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/qaoa/max_cut_backprop_noise.py b/examples/qaoa/max_cut_backprop_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/qaoa/max_cut_parametershift_noise.py b/examples/qaoa/max_cut_parametershift_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/quantum_lstm/qlstm_noise.py b/examples/quantum_lstm/qlstm_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/quantumnat/quantumnat_noise.py b/examples/quantumnat/quantumnat_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/quanvolution/quanvolution_noise.py b/examples/quanvolution/quanvolution_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/quanvolution/quanvolution_trainable_quantum_layer_noise.py b/examples/quanvolution/quanvolution_trainable_quantum_layer_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/qubit_rotation/qubit_rotation_noise.py b/examples/qubit_rotation/qubit_rotation_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/regression/run_regression_noise.py b/examples/regression/run_regression_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/train_state_prep/train_state_prep_noise.py b/examples/train_state_prep/train_state_prep_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/train_unitary_prep/train_unitary_prep_noise.py b/examples/train_unitary_prep/train_unitary_prep_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/vqe/vqe_noise.py b/examples/vqe/vqe_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/torchquantum/device/noisedevices.py b/torchquantum/device/noisedevices.py index a5118188..dded7a4d 100644 --- a/torchquantum/device/noisedevices.py +++ b/torchquantum/device/noisedevices.py @@ -124,18 +124,14 @@ def get_2d_matrix(self, index): _matrix = torch.reshape(self.densities[index], [2 ** self.n_wires] * 2) return _matrix - def get_densities_2d(self): """Return the states in a 1d tensor.""" bsz = self.densities.shape[0] - return torch.reshape(self.densities, [bsz, 2**self.n_wires, 2**self.n_wires]) + return torch.reshape(self.densities, [bsz, 2 ** self.n_wires, 2 ** self.n_wires]) def get_density_2d(self): """Return the state in a 1d tensor.""" - return torch.reshape(self.density, [2**self.n_wires,2**self.n_wires]) - - - + return torch.reshape(self.density, [2 ** self.n_wires, 2 ** self.n_wires]) def calc_trace(self, index): _matrix = torch.reshape(self.densities[index], [2 ** self.n_wires] * 2) @@ -169,6 +165,19 @@ def clone_densities(self, existing_densities: torch.Tensor): """Clone the densities of the other quantum device.""" self.densities = existing_densities.clone() + def clone_from_states(self, existing_states: torch.Tensor): + """Clone the densities of the other quantum device using the conjugate transpose.""" + # Ensure the dimensions match the expected shape for the outer product operation + assert 2 * (existing_states.dim() - 1) == (self.densities.dim() - 1) + #assert existing_states.shape[0] == self.densities.shape[0] + bsz = existing_states.shape[0] + state_dim = 2 ** self.n_wires + states_reshaped = existing_states.view(-1, state_dim, 1) # [batch_size, state_dim, 1] + states_conj_transpose = torch.conj(states_reshaped).transpose(1, 2) # [batch_size, 1, state_dim] + # Use torch.bmm for batched outer product + self.densities = torch.bmm(states_reshaped, states_conj_transpose) + self.densities = torch.reshape(self.densities, [bsz] + [2] * (2 * self.n_wires)) + def noise_model(self): return self._noise_model diff --git a/torchquantum/encoding/encodings.py b/torchquantum/encoding/encodings.py index f8d2056d..d6463fc8 100644 --- a/torchquantum/encoding/encodings.py +++ b/torchquantum/encoding/encodings.py @@ -39,6 +39,7 @@ class Encoder(tq.QuantumModule): - forward(qdev: tq.QuantumDevice, x): Performs the encoding using a quantum device. """ + def __init__(self): super().__init__() pass @@ -133,6 +134,7 @@ def to_qiskit(self, n_wires, x): class PhaseEncoder(Encoder, metaclass=ABCMeta): """PhaseEncoder is a subclass of Encoder and represents a phase encoder. It applies a specified quantum function to encode input data using a quantum device.""" + def __init__(self, func): super().__init__() self.func = func @@ -163,6 +165,7 @@ def forward(self, qdev: tq.QuantumDevice, x): class MultiPhaseEncoder(Encoder, metaclass=ABCMeta): """PhaseEncoder is a subclass of Encoder and represents a phase encoder. It applies a specified quantum function to encode input data using a quantum device.""" + def __init__(self, funcs, wires=None): super().__init__() self.funcs = funcs if isinstance(funcs, Iterable) else [funcs] @@ -198,7 +201,7 @@ def forward(self, qdev: tq.QuantumDevice, x): func_name_dict[func]( qdev, wires=self.wires[k], - params=x[:, x_id : (x_id + stride)], + params=x[:, x_id: (x_id + stride)], static=self.static_mode, parent_graph=self.graph, ) @@ -208,6 +211,7 @@ def forward(self, qdev: tq.QuantumDevice, x): class StateEncoder(Encoder, metaclass=ABCMeta): """StateEncoder is a subclass of Encoder and represents a state encoder. It encodes the input data into the state vector of a quantum device.""" + def __init__(self): super().__init__() @@ -230,19 +234,24 @@ def forward(self, qdev: tq.QuantumDevice, x): ( x, torch.zeros( - x.shape[0], 2**qdev.n_wires - x.shape[1], device=x.device + x.shape[0], 2 ** qdev.n_wires - x.shape[1], device=x.device ), ), dim=-1, ) state = state.view([x.shape[0]] + [2] * qdev.n_wires) - qdev.states = state.type(C_DTYPE) + #TODO: Change to united format + if qdev.device_name == "noisedevice": + qdev.clone_from_states(state.type(C_DTYPE)) + else: + qdev.states = state.type(C_DTYPE) class MagnitudeEncoder(Encoder, metaclass=ABCMeta): """MagnitudeEncoder is a subclass of Encoder and represents a magnitude encoder. It encodes the input data by considering the magnitudes of the elements.""" + def __init__(self): super().__init__() diff --git a/torchquantum/measurement/density_measurements.py b/torchquantum/measurement/density_measurements.py index 0de82e88..e1663eb2 100644 --- a/torchquantum/measurement/density_measurements.py +++ b/torchquantum/measurement/density_measurements.py @@ -281,7 +281,7 @@ def __init__(self, obs, v_c_reg_mapping=None): self.v_c_reg_mapping = v_c_reg_mapping def forward(self, qdev: tq.NoiseDevice): - x = expval(qdev, list(range(qdev.n_wires)), [self.obs()] * qdev.n_wires) + x = expval_density(qdev, list(range(qdev.n_wires)), [self.obs()] * qdev.n_wires) if self.v_c_reg_mapping is not None: c2v_mapping = self.v_c_reg_mapping["c2v"] @@ -304,26 +304,27 @@ def set_v_c_reg_mapping(self, mapping): if __name__ == '__main__': - print("Yes") qdev = tq.NoiseDevice(n_wires=2, bsz=5, device="cpu", record_op=True) # use device='cuda' for GPU qdev.h(wires=0) qdev.cnot(wires=[0, 1]) - #tqf.h(qdev, wires=1) - #tqf.x(qdev, wires=1) - #tqf.y(qdev, wires=1) - #tqf.cnot(qdev,wires=[0, 1]) + # tqf.h(qdev, wires=1) + # tqf.x(qdev, wires=1) + # tqf.y(qdev, wires=1) + # tqf.cnot(qdev,wires=[0, 1]) # op = tq.RX(has_params=True, trainable=True, init_params=0.5) # op(qdev, wires=0) + result = tq.expval_density(qdev, [0, 1], [tq.PauliZ(), tq.PauliZ()]) + print(result.shape) # measure the state on z basis - print(tq.measure_density(qdev, n_shots=1024)) + # print(tq.measure_density(qdev, n_shots=1024)) # obtain the expval on a observable - expval = expval_joint_sampling_density(qdev, 'XZ', 100000) + # expval = expval_joint_sampling_density(qdev, 'XZ', 100000) - print("expval") - print(expval) + # print("expval") + # print(expval) - expval_ana = expval_joint_analytical_density(qdev, 'XZ') - print("expval_ana") - print(expval_ana) + # expval_ana = expval_joint_analytical_density(qdev, 'XZ') + # print("expval_ana") + # print(expval_ana) From fb35f20be6b0950e724a7ee729df63145600ff43 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Sun, 25 Feb 2024 15:03:11 -0800 Subject: [PATCH 24/54] [Examples] Add many noise examples. --- examples/qaoa/max_cut_backprop_noise.py | 203 +++++++++ examples/qaoa/max_cut_parametershift_noise.py | 305 +++++++++++++ examples/qaoa/max_cut_paramshift.py | 8 +- examples/quantum_lstm/qlstm_noise.py | 423 ++++++++++++++++++ examples/quanvolution/quanvolution_noise.py | 250 +++++++++++ .../qubit_rotation/qubit_rotation_noise.py | 69 +++ examples/regression/run_regression_noise.py | 267 +++++++++++ .../train_state_prep_noise.py | 0 .../train_unitary_prep_noise.py | 118 +++++ examples/vqe/vqe_noise.py | 179 ++++++++ 10 files changed, 1818 insertions(+), 4 deletions(-) delete mode 100644 examples/train_state_prep/train_state_prep_noise.py diff --git a/examples/qaoa/max_cut_backprop_noise.py b/examples/qaoa/max_cut_backprop_noise.py index e69de29b..5ab4a4dd 100644 --- a/examples/qaoa/max_cut_backprop_noise.py +++ b/examples/qaoa/max_cut_backprop_noise.py @@ -0,0 +1,203 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torchquantum as tq + +import random +import numpy as np + +from torchquantum.functional import mat_dict + +from torchquantum.measurement import expval_joint_analytical_density + +seed = 0 +random.seed(seed) +np.random.seed(seed) +torch.manual_seed(seed) + + +class MAXCUT(tq.QuantumModule): + """computes the optimal cut for a given graph. + outputs: the most probable bitstring decides the set {0 or 1} each + node belongs to. + """ + + def __init__(self, n_wires, input_graph, n_layers): + super().__init__() + + self.n_wires = n_wires + + self.input_graph = input_graph # list of edges + self.n_layers = n_layers + + self.betas = torch.nn.Parameter(0.01 * torch.rand(self.n_layers)) + self.gammas = torch.nn.Parameter(0.01 * torch.rand(self.n_layers)) + + def mixer(self, qdev, beta): + """ + Apply the single rotation and entangling layer of the QAOA ansatz. + mixer = exp(-i * beta * sigma_x) + """ + for wire in range(self.n_wires): + qdev.rx( + wires=wire, + params=beta.unsqueeze(0), + ) # type: ignore + + def entangler(self, qdev, gamma): + """ + Apply the single rotation and entangling layer of the QAOA ansatz. + entangler = exp(-i * gamma * (1 - sigma_z * sigma_z)/2) + """ + for edge in self.input_graph: + qdev.cx( + [edge[0], edge[1]], + ) # type: ignore + qdev.rz( + wires=edge[1], + params=gamma.unsqueeze(0), + ) # type: ignore + qdev.cx( + [edge[0], edge[1]], + ) # type: ignore + + def edge_to_PauliString(self, edge): + # construct pauli string + pauli_string = "" + for wire in range(self.n_wires): + if wire in edge: + pauli_string += "Z" + else: + pauli_string += "I" + return pauli_string + + def circuit(self, qdev): + """ + execute the quantum circuit + """ + # print(self.betas, self.gammas) + for wire in range(self.n_wires): + qdev.h( + wires=wire, + ) # type: ignore + + for i in range(self.n_layers): + self.mixer(qdev, self.betas[i]) + self.entangler(qdev, self.gammas[i]) + + def forward(self, measure_all=False): + """ + Apply the QAOA ansatz and only measure the edge qubit on z-basis. + Args: + if edge is None + """ + qdev = tq.NoiseDevice( + n_wires=self.n_wires, device=self.betas.device, record_op=False, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.12, "Phaseflip": 0.12}) + ) + + self.circuit(qdev) + + # turn on the record_op above to print the circuit + # print(op_history2qiskit(self.n_wires, qdev.op_history)) + + # print(tq.measure(qdev, n_shots=1024)) + # compute the expectation value + # print(qdev.get_states_1d()) + if measure_all is False: + expVal = 0 + for edge in self.input_graph: + pauli_string = self.edge_to_PauliString(edge) + expv = expval_joint_analytical_density(qdev, observable=pauli_string) + expVal += 0.5 * expv + # print(pauli_string, expv) + # print(expVal) + return expVal + else: + return tq.measure_density(qdev, n_shots=1024, draw_id=0) + + +def backprop_optimize(model, n_steps=100, lr=0.1): + """ + Optimize the QAOA ansatz over the parameters gamma and beta + Args: + betas (np.array): A list of beta parameters. + gammas (np.array): A list of gamma parameters. + n_steps (int): The number of steps to optimize, defaults to 10. + lr (float): The learning rate, defaults to 0.1. + """ + # measure all edges in the input_graph + optimizer = torch.optim.Adam(model.parameters(), lr=lr) + print( + "The initial parameters are betas = {} and gammas = {}".format( + *model.parameters() + ) + ) + # optimize the parameters and return the optimal values + for step in range(n_steps): + optimizer.zero_grad() + loss = model() + loss.backward() + optimizer.step() + if step % 2 == 0: + print("Step: {}, Cost Objective: {}".format(step, loss.item())) + + print( + "The optimal parameters are betas = {} and gammas = {}".format( + *model.parameters() + ) + ) + return model(measure_all=True) + + +def main(): + # create a input_graph + input_graph = [(0, 1), (0, 3), (1, 2), (2, 3)] + n_wires = 4 + n_layers = 3 + model = MAXCUT(n_wires=n_wires, input_graph=input_graph, n_layers=n_layers) + # model.to("cuda") + # model.to(torch.device("cuda")) + # circ = tq2qiskit(tq.QuantumDevice(n_wires=4), model) + # print(circ) + # print("The circuit is", circ.draw(output="mpl")) + # circ.draw(output="mpl") + # use backprop + backprop_optimize(model, n_steps=300, lr=0.01) + # use parameter shift rule + # param_shift_optimize(model, n_steps=500, step_size=100000) + + +""" +Notes: +1. input_graph = [(0, 1), (3, 0), (1, 2), (2, 3)], mixer 1st & entangler 2nd, n_layers >= 2, answer is correct. + +""" + +if __name__ == "__main__": + # import pdb + # pdb.set_trace() + + main() diff --git a/examples/qaoa/max_cut_parametershift_noise.py b/examples/qaoa/max_cut_parametershift_noise.py index e69de29b..11fd79e2 100644 --- a/examples/qaoa/max_cut_parametershift_noise.py +++ b/examples/qaoa/max_cut_parametershift_noise.py @@ -0,0 +1,305 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torchquantum as tq + +import random +import numpy as np + +from torchquantum.measurement import expval_joint_analytical_density + +seed = 0 +random.seed(seed) +np.random.seed(seed) +torch.manual_seed(seed) + +from torchquantum.plugin import QiskitProcessor, op_history2qiskit + + +class MAXCUT(tq.QuantumModule): + """computes the optimal cut for a given graph. + outputs: the most probable bitstring decides the set {0 or 1} each + node belongs to. + """ + + def __init__(self, n_wires, input_graph, n_layers): + super().__init__() + + self.n_wires = n_wires + + self.input_graph = input_graph # list of edges + self.n_layers = n_layers + self.n_edges = len(input_graph) + + self.betas = torch.nn.Parameter(0.01 * torch.rand(self.n_layers)) + self.gammas = torch.nn.Parameter(0.01 * torch.rand(self.n_layers)) + + self.reset_shift_param() + + def mixer(self, qdev, beta, layer_id): + """ + Apply the single rotation and entangling layer of the QAOA ansatz. + mixer = exp(-i * beta * sigma_x) + """ + + for wire in range(self.n_wires): + if ( + self.shift_param_name == "beta" + and self.shift_wire == wire + and layer_id == self.shift_layer + ): + degree = self.shift_degree + else: + degree = 0 + qdev.rx( + wires=wire, + params=(beta.unsqueeze(0) + degree), + ) # type: ignore + + def entangler(self, qdev, gamma, layer_id): + """ + Apply the single rotation and entangling layer of the QAOA ansatz. + entangler = exp(-i * gamma * (1 - sigma_z * sigma_z)/2) + """ + for edge_id, edge in enumerate(self.input_graph): + if ( + self.shift_param_name == "gamma" + and edge_id == self.shift_edge_id + and layer_id == self.shift_layer + ): + degree = self.shift_degree + else: + degree = 0 + qdev.cx( + [edge[0], edge[1]], + ) # type: ignore + qdev.rz( + wires=edge[1], + params=(gamma.unsqueeze(0) + degree), + ) # type: ignore + qdev.cx( + [edge[0], edge[1]], + ) # type: ignore + + def set_shift_param(self, layer, wire, param_name, degree, edge_id): + """ + set the shift parameter for the parameter shift rule + """ + self.shift_layer = layer + self.shift_wire = wire + self.shift_param_name = param_name + self.shift_degree = degree + self.shift_edge_id = edge_id + + def reset_shift_param(self): + """ + reset the shift parameter + """ + self.shift_layer = None + self.shift_wire = None + self.shift_param_name = None + self.shift_degree = None + self.shift_edge_id = None + + def edge_to_PauliString(self, edge): + # construct pauli string + pauli_string = "" + for wire in range(self.n_wires): + if wire in edge: + pauli_string += "Z" + else: + pauli_string += "I" + return pauli_string + + def circuit(self, qdev): + """ + execute the quantum circuit + """ + # print(self.betas, self.gammas) + for wire in range(self.n_wires): + qdev.h( + wires=wire, + ) # type: ignore + + for i in range(self.n_layers): + self.mixer(qdev, self.betas[i], i) + self.entangler(qdev, self.gammas[i], i) + + def forward(self, use_qiskit, measure_all=False): + """ + Apply the QAOA ansatz and only measure the edge qubit on z-basis. + Args: + if edge is None + """ + qdev = tq.NoiseDevice(n_wires=self.n_wires, device=self.betas.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.12, "Phaseflip": 0.12})) + + + # print(tq.measure(qdev, n_shots=1024)) + # compute the expectation value + # print(qdev.get_states_1d()) + + if not use_qiskit: + self.circuit(qdev) + expVal = 0 + for edge in self.input_graph: + pauli_string = self.edge_to_PauliString(edge) + expv = expval_joint_analytical_density(qdev, observable=pauli_string) + expVal += 0.5 * expv + else: + # use qiskit to compute the expectation value + expVal = 0 + for edge in self.input_graph: + pauli_string = self.edge_to_PauliString(edge) + + with torch.no_grad(): + self.circuit(qdev) + circ = op_history2qiskit(qdev.n_wires, qdev.op_history) + + expv = self.qiskit_processor.process_circs_get_joint_expval([circ], pauli_string)[0] + expVal += 0.5 * expv + expVal = torch.Tensor([expVal]) + return expVal + + +def shift_and_run(model, use_qiskit): + # flatten the parameters into 1D array + + grad_betas = [] + grad_gammas = [] + n_layers = model.n_layers + n_wires = model.n_wires + n_edges = model.n_edges + + for i in range(n_layers): + grad_gamma = 0 + for k in range(n_edges): + model.set_shift_param(i, None, "gamma", np.pi * 0.5, k) + out1 = model(use_qiskit) + model.reset_shift_param() + + model.set_shift_param(i, None, "gamma", -np.pi * 0.5, k) + out2 = model(use_qiskit) + model.reset_shift_param() + + grad_gamma += 0.5 * (out1 - out2).squeeze().item() + grad_gammas.append(grad_gamma) + + grad_beta = 0 + for j in range(n_wires): + model.set_shift_param(i, j, "beta", np.pi * 0.5, None) + out1 = model(use_qiskit) + model.reset_shift_param() + + model.set_shift_param(i, j, "beta", -np.pi * 0.5, None) + out2 = model(use_qiskit) + model.reset_shift_param() + + grad_beta += 0.5 * (out1 - out2).squeeze().item() + grad_betas.append(grad_beta) + + return model(use_qiskit), [grad_betas, grad_gammas] + + +def param_shift_optimize(model, n_steps=10, step_size=0.1, use_qiskit=False): + """finds the optimal cut where parameter shift rule is used to compute the gradient""" + # optimize the parameters and return the optimal values + # print( + # "The initial parameters are betas = {} and gammas = {}".format( + # *model.parameters() + # ) + # ) + n_layers = model.n_layers + for step in range(n_steps): + with torch.no_grad(): + loss, grad_list = shift_and_run(model, use_qiskit=use_qiskit) + # param_list = list(model.parameters()) + # print( + # "The initial parameters are betas = {} and gammas = {}".format( + # *model.parameters() + # ) + # ) + # param_list = torch.cat([param.flatten() for param in param_list]) + + # print("The shape of the params", len(param_list), param_list[0].shape, param_list) + # print("") + # print("The shape of the grad_list = {}, 0th elem shape = {}, grad_list = {}".format(len(grad_list), grad_list[0].shape, grad_list)) + # print(grad_list, loss, model.betas, model.gammas) + print(loss) + with torch.no_grad(): + for i in range(n_layers): + model.betas[i].copy_(model.betas[i] - step_size * grad_list[0][i]) + model.gammas[i].copy_(model.gammas[i] - step_size * grad_list[1][i]) + + # for param, grad in zip(param_list, grad_list): + # modify the parameters and ensure that there are no multiple views + # param.copy_(param - step_size * grad) + # if step % 5 == 0: + # print("Step: {}, Cost Objective: {}".format(step, loss.item())) + + # print( + # "The updated parameters are betas = {} and gammas = {}".format( + # *model.parameters() + # ) + # ) + return model(use_qiskit=False, measure_all=True) + + +""" +Notes: +1. input_graph = [(0, 1), (3, 0), (1, 2), (2, 3)], mixer 1st & entangler 2nd, n_layers >= 2, answer is correct. + +""" + + +def main(use_qiskit): + # create a input_graph + input_graph = [(0, 1), (0, 3), (1, 2), (2, 3)] + n_wires = 4 + n_layers = 1 + model = MAXCUT(n_wires=n_wires, input_graph=input_graph, n_layers=n_layers) + + # set the qiskit processor + # processor_simulation = QiskitProcessor(use_real_qc=False, n_shots=10000) + # model.set_qiskit_processor(processor_simulation) + + # firstly perform simulate + # model.to("cuda") + # model.to(torch.device("cuda")) + # circ = tq2qiskit(tq.QuantumDevice(n_wires=4), model) + # print(circ) + # print("The circuit is", circ.draw(output="mpl")) + # circ.draw(output="mpl") + # use backprop + # backprop_optimize(model, n_steps=300, lr=0.01) + # use parameter shift rule + param_shift_optimize(model, n_steps=500, step_size=0.01, use_qiskit=use_qiskit) + + +if __name__ == "__main__": + # import pdb + # pdb.set_trace() + use_qiskit = False + main(use_qiskit) diff --git a/examples/qaoa/max_cut_paramshift.py b/examples/qaoa/max_cut_paramshift.py index a8467c1d..48b79a44 100644 --- a/examples/qaoa/max_cut_paramshift.py +++ b/examples/qaoa/max_cut_paramshift.py @@ -148,7 +148,7 @@ def circuit(self, qdev): self.mixer(qdev, self.betas[i], i) self.entangler(qdev, self.gammas[i], i) - def forward(self, use_qiskit): + def forward(self, use_qiskit, measure_all=False): """ Apply the QAOA ansatz and only measure the edge qubit on z-basis. Args: @@ -266,7 +266,7 @@ def param_shift_optimize(model, n_steps=10, step_size=0.1, use_qiskit=False): # *model.parameters() # ) # ) - return model(measure_all=True) + return model(use_qiskit=False,measure_all=True) """ @@ -284,8 +284,8 @@ def main(use_qiskit): model = MAXCUT(n_wires=n_wires, input_graph=input_graph, n_layers=n_layers) # set the qiskit processor - processor_simulation = QiskitProcessor(use_real_qc=False, n_shots=10000) - model.set_qiskit_processor(processor_simulation) + #processor_simulation = QiskitProcessor(use_real_qc=False, n_shots=10000) + #model.set_qiskit_processor(processor_simulation) # firstly perform simulate # model.to("cuda") diff --git a/examples/quantum_lstm/qlstm_noise.py b/examples/quantum_lstm/qlstm_noise.py index e69de29b..1587b545 100644 --- a/examples/quantum_lstm/qlstm_noise.py +++ b/examples/quantum_lstm/qlstm_noise.py @@ -0,0 +1,423 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +import torch +import torch.nn as nn +import torchquantum as tq +import torchquantum.functional as tqf + + +class QLSTM(nn.Module): + # use 'qiskit.ibmq' instead to run on hardware + class QLayer_forget(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder( + [{'input_idx': [0], 'func': 'rx', 'wires': [0]}, + {'input_idx': [1], 'func': 'rx', 'wires': [1]}, + {'input_idx': [2], 'func': 'rx', 'wires': [2]}, + {'input_idx': [3], 'func': 'rx', 'wires': [3]}, + ]) + self.rx0 = tq.RX(has_params=True, trainable=True) + self.rx1 = tq.RX(has_params=True, trainable=True) + self.rx2 = tq.RX(has_params=True, trainable=True) + self.rx3 = tq.RX(has_params=True, trainable=True) + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x): + qdev = tq.NoiseDevice(n_wires=self.n_wires, bsz=x.shape[0], device=x.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + self.encoder(qdev, x) + self.rx0(qdev, wires=0) + self.rx1(qdev, wires=1) + self.rx2(qdev, wires=2) + self.rx3(qdev, wires=3) + for k in range(self.n_wires): + if k == self.n_wires - 1: + tqf.cnot(qdev, wires=[k, 0]) + else: + tqf.cnot(qdev, wires=[k, k + 1]) + return (self.measure(qdev)) + + class QLayer_input(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder( + [{'input_idx': [0], 'func': 'rx', 'wires': [0]}, + {'input_idx': [1], 'func': 'rx', 'wires': [1]}, + {'input_idx': [2], 'func': 'rx', 'wires': [2]}, + {'input_idx': [3], 'func': 'rx', 'wires': [3]}, + ]) + self.rx0 = tq.RX(has_params=True, trainable=True) + self.rx1 = tq.RX(has_params=True, trainable=True) + self.rx2 = tq.RX(has_params=True, trainable=True) + self.rx3 = tq.RX(has_params=True, trainable=True) + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x): + qdev = tq.NoiseDevice(n_wires=self.n_wires, bsz=x.shape[0], device=x.device, + noise_model = tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + self.encoder(qdev, x) + self.rx0(qdev, wires=0) + self.rx1(qdev, wires=1) + self.rx2(qdev, wires=2) + self.rx3(qdev, wires=3) + for k in range(self.n_wires): + if k == self.n_wires - 1: + tqf.cnot(qdev, wires=[k, 0]) + else: + tqf.cnot(qdev, wires=[k, k + 1]) + return (self.measure(qdev)) + + class QLayer_update(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder( + [{'input_idx': [0], 'func': 'rx', 'wires': [0]}, + {'input_idx': [1], 'func': 'rx', 'wires': [1]}, + {'input_idx': [2], 'func': 'rx', 'wires': [2]}, + {'input_idx': [3], 'func': 'rx', 'wires': [3]}, + ]) + self.rx0 = tq.RX(has_params=True, trainable=True) + self.rx1 = tq.RX(has_params=True, trainable=True) + self.rx2 = tq.RX(has_params=True, trainable=True) + self.rx3 = tq.RX(has_params=True, trainable=True) + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x): + qdev = tq.NoiseDevice(n_wires=self.n_wires, bsz=x.shape[0], device=x.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + self.encoder(qdev, x) + self.rx0(qdev, wires=0) + self.rx1(qdev, wires=1) + self.rx2(qdev, wires=2) + self.rx3(qdev, wires=3) + for k in range(self.n_wires): + if k == self.n_wires - 1: + tqf.cnot(qdev, wires=[k, 0]) + else: + tqf.cnot(qdev, wires=[k, k + 1]) + return (self.measure(qdev)) + + class QLayer_output(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder( + [{'input_idx': [0], 'func': 'rx', 'wires': [0]}, + {'input_idx': [1], 'func': 'rx', 'wires': [1]}, + {'input_idx': [2], 'func': 'rx', 'wires': [2]}, + {'input_idx': [3], 'func': 'rx', 'wires': [3]}, + ]) + self.rx0 = tq.RX(has_params=True, trainable=True) + self.rx1 = tq.RX(has_params=True, trainable=True) + self.rx2 = tq.RX(has_params=True, trainable=True) + self.rx3 = tq.RX(has_params=True, trainable=True) + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x): + qdev = tq.NoiseDevice(n_wires=self.n_wires, bsz=x.shape[0], device=x.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + self.encoder(qdev, x) + self.rx0(qdev, wires=0) + self.rx1(qdev, wires=1) + self.rx2(qdev, wires=2) + self.rx3(qdev, wires=3) + for k in range(self.n_wires): + if k == self.n_wires - 1: + tqf.cnot(qdev, wires=[k, 0]) + else: + tqf.cnot(qdev, wires=[k, k + 1]) + return (self.measure(qdev)) + + def __init__(self, + input_size, + hidden_size, + n_qubits=4, + n_qlayers=1, + batch_first=True, + return_sequences=False, + return_state=False, + backend="default.qubit"): + super(QLSTM, self).__init__() + self.n_inputs = input_size + self.hidden_size = hidden_size + self.concat_size = self.n_inputs + self.hidden_size + self.n_qubits = n_qubits + self.n_qlayers = n_qlayers + self.backend = backend # "default.qubit", "qiskit.basicaer", "qiskit.ibm" + + self.batch_first = batch_first + self.return_sequences = return_sequences + self.return_state = return_state + + self.clayer_in = torch.nn.Linear(self.concat_size, n_qubits) + self.VQC = { + 'forget': self.QLayer_forget(), + 'input': self.QLayer_input(), + 'update': self.QLayer_update(), + 'output': self.QLayer_output() + } + self.clayer_out = torch.nn.Linear(self.n_qubits, self.hidden_size) + # self.clayer_out = [torch.nn.Linear(n_qubits, self.hidden_size) for _ in range(4)] + + def forward(self, x, init_states=None): + ''' + x.shape is (batch_size, seq_length, feature_size) + recurrent_activation -> sigmoid + activation -> tanh + ''' + if self.batch_first is True: + batch_size, seq_length, features_size = x.size() + else: + seq_length, batch_size, features_size = x.size() + + hidden_seq = [] + if init_states is None: + h_t = torch.zeros(batch_size, self.hidden_size) # hidden state (output) + c_t = torch.zeros(batch_size, self.hidden_size) # cell state + else: + # for now we ignore the fact that in PyTorch you can stack multiple RNNs + # so we take only the first elements of the init_states tuple init_states[0][0], init_states[1][0] + h_t, c_t = init_states + h_t = h_t[0] + c_t = c_t[0] + + for t in range(seq_length): + # get features from the t-th element in seq, for all entries in the batch + x_t = x[:, t, :] + + # Concatenate input and hidden state + v_t = torch.cat((h_t, x_t), dim=1) + + # match qubit dimension + y_t = self.clayer_in(v_t) + + f_t = torch.sigmoid(self.clayer_out(self.VQC['forget'](y_t))) # forget block + i_t = torch.sigmoid(self.clayer_out(self.VQC['input'](y_t))) # input block + g_t = torch.tanh(self.clayer_out(self.VQC['update'](y_t))) # update block + o_t = torch.sigmoid(self.clayer_out(self.VQC['output'](y_t))) # output block + + c_t = (f_t * c_t) + (i_t * g_t) + h_t = o_t * torch.tanh(c_t) + + hidden_seq.append(h_t.unsqueeze(0)) + hidden_seq = torch.cat(hidden_seq, dim=0) + hidden_seq = hidden_seq.transpose(0, 1).contiguous() + return hidden_seq, (h_t, c_t) + + +def prepare_sequence(seq, to_ix): + idxs = [to_ix[w] for w in seq] + return torch.tensor(idxs, dtype=torch.long) + + +class LSTMTagger(nn.Module): + def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size, n_qubits=0): + super(LSTMTagger, self).__init__() + self.hidden_dim = hidden_dim + + self.word_embeddings = nn.Embedding(vocab_size, embedding_dim) + + # The LSTM takes word embeddings as inputs, and outputs hidden states + # with dimensionality hidden_dim. + if n_qubits > 0: + print("Tagger will use Quantum LSTM") + self.lstm = QLSTM(embedding_dim, hidden_dim, n_qubits=n_qubits) + else: + print("Tagger will use Classical LSTM") + self.lstm = nn.LSTM(embedding_dim, hidden_dim) + + # The linear layer that maps from hidden state space to tag space + self.hidden2tag = nn.Linear(hidden_dim, tagset_size) + + def forward(self, sentence): + embeds = self.word_embeddings(sentence) + lstm_out, _ = self.lstm(embeds.view(len(sentence), 1, -1)) + tag_logits = self.hidden2tag(lstm_out.view(len(sentence), -1)) + tag_scores = F.log_softmax(tag_logits, dim=1) + return tag_scores + + +def train(model, n_epochs, training_data, word_to_ix, tag_to_ix): + loss_function = nn.NLLLoss() + optimizer = optim.SGD(model.parameters(), lr=0.1) + + history = { + 'loss': [], + 'acc': [] + } + for epoch in range(n_epochs): + losses = [] + preds = [] + targets = [] + for sentence, tags in training_data: + # Step 1. Remember that Pytorch accumulates gradients. + # We need to clear them out before each instance + model.zero_grad() + + # Step 2. Get our inputs ready for the network, that is, turn them into + # Tensors of word indices. + sentence_in = prepare_sequence(sentence, word_to_ix) + labels = prepare_sequence(tags, tag_to_ix) + + # Step 3. Run our forward pass. + tag_scores = model(sentence_in) + + # Step 4. Compute the loss, gradients, and update the parameters by + # calling optimizer.step() + loss = loss_function(tag_scores, labels) + loss.backward() + optimizer.step() + losses.append(float(loss)) + + probs = torch.softmax(tag_scores, dim=-1) + preds.append(probs.argmax(dim=-1)) + targets.append(labels) + + avg_loss = np.mean(losses) + history['loss'].append(avg_loss) + + preds = torch.cat(preds) + targets = torch.cat(targets) + corrects = (preds == targets) + accuracy = corrects.sum().float() / float(targets.size(0)) + history['acc'].append(accuracy) + + print(f"Epoch {epoch + 1} / {n_epochs}: Loss = {avg_loss:.3f} Acc = {accuracy:.2f}") + + return history + + +def print_result(model, training_data, word_to_ix, ix_to_tag): + with torch.no_grad(): + input_sentence = training_data[0][0] + labels = training_data[0][1] + inputs = prepare_sequence(input_sentence, word_to_ix) + tag_scores = model(inputs) + + tag_ids = torch.argmax(tag_scores, dim=1).numpy() + tag_labels = [ix_to_tag[k] for k in tag_ids] + print(f"Sentence: {input_sentence}") + print(f"Labels: {labels}") + print(f"Predicted: {tag_labels}") + + +from matplotlib import pyplot as plt + + +def plot_history(history_classical, history_quantum): + loss_c = history_classical['loss'] + acc_c = history_classical['acc'] + loss_q = history_quantum['loss'] + acc_q = history_quantum['acc'] + n_epochs = max([len(loss_c), len(loss_q)]) + x_epochs = [i for i in range(n_epochs)] + + fig, ax1 = plt.subplots() + + ax1.set_xlabel("Epoch") + ax1.set_ylabel("Loss") + ax1.plot(loss_c, label="Classical LSTM loss", color='orange', linestyle='dashed') + ax1.plot(loss_q, label="Quantum LSTM loss", color='red', linestyle='solid') + + ax2 = ax1.twinx() + ax2.set_ylabel("Accuracy") + ax2.plot(acc_c, label="Classical LSTM accuracy", color='steelblue', linestyle='dashed') + ax2.plot(acc_q, label="Quantum LSTM accuracy", color='blue', linestyle='solid') + + plt.title("Part-of-Speech Tagger Training__torch") + plt.ylim(0., 1.1) + # plt.legend(loc="upper right") + fig.legend(loc="upper right", bbox_to_anchor=(1, 0.8), bbox_transform=ax1.transAxes) + + plt.savefig("pos_training_torch.pdf") + plt.savefig("pos_training_torch.png") + + plt.show() + + +def main(): + tag_to_ix = {"DET": 0, "NN": 1, "V": 2} # Assign each tag with a unique index + ix_to_tag = {i: k for k, i in tag_to_ix.items()} + + training_data = [ + # Tags are: DET - determiner; NN - noun; V - verb + # For example, the word "The" is a determiner + ("The dog ate the apple".split(), ["DET", "NN", "V", "DET", "NN"]), + ("Everybody read that book".split(), ["NN", "V", "DET", "NN"]) + ] + word_to_ix = {} + + # For each words-list (sentence) and tags-list in each tuple of training_data + for sent, tags in training_data: + for word in sent: + if word not in word_to_ix: # word has not been assigned an index yet + word_to_ix[word] = len(word_to_ix) # Assign each word with a unique index + + print(f"Vocabulary: {word_to_ix}") + print(f"Entities: {ix_to_tag}") + + embedding_dim = 8 + hidden_dim = 6 + n_epochs = 300 + + model_classical = LSTMTagger(embedding_dim, + hidden_dim, + vocab_size=len(word_to_ix), + tagset_size=len(tag_to_ix), + n_qubits=0) + + history_classical = train(model_classical, n_epochs, training_data, word_to_ix, tag_to_ix) + + print_result(model_classical, training_data, word_to_ix, ix_to_tag) + + n_qubits = 4 + + model_quantum = LSTMTagger(embedding_dim, + hidden_dim, + vocab_size=len(word_to_ix), + tagset_size=len(tag_to_ix), + n_qubits=n_qubits) + + history_quantum = train(model_quantum, n_epochs, training_data, word_to_ix, tag_to_ix) + + print_result(model_quantum, training_data, word_to_ix, ix_to_tag) + + plot_history(history_classical, history_quantum) + + +if __name__ == "__main__": + # import pdb + # pdb.set_trace() + + main() diff --git a/examples/quanvolution/quanvolution_noise.py b/examples/quanvolution/quanvolution_noise.py index e69de29b..33a329a1 100644 --- a/examples/quanvolution/quanvolution_noise.py +++ b/examples/quanvolution/quanvolution_noise.py @@ -0,0 +1,250 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torchquantum as tq +import torchquantum.functional as tqf + +import torch +import torch.nn.functional as F +import torch.optim as optim +import numpy as np +import random + +from torchquantum.dataset import MNIST +from torch.optim.lr_scheduler import CosineAnnealingLR + + +class QuanvolutionFilter(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder( + [ + {"input_idx": [0], "func": "ry", "wires": [0]}, + {"input_idx": [1], "func": "ry", "wires": [1]}, + {"input_idx": [2], "func": "ry", "wires": [2]}, + {"input_idx": [3], "func": "ry", "wires": [3]}, + ] + ) + + self.q_layer = tq.RandomLayer(n_ops=8, wires=list(range(self.n_wires))) + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x, use_qiskit=False): + bsz = x.shape[0] + qdev = tq.NoiseDevice(self.n_wires, bsz=bsz, device=x.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + size = 28 + x = x.view(bsz, size, size) + + data_list = [] + + for c in range(0, size, 2): + for r in range(0, size, 2): + data = torch.transpose( + torch.cat( + (x[:, c, r], x[:, c, r + 1], x[:, c + 1, r], x[:, c + 1, r + 1]) + ).view(4, bsz), + 0, + 1, + ) + if use_qiskit: + data = self.qiskit_processor.process_parameterized( + qdev, self.encoder, self.q_layer, self.measure, data + ) + else: + self.encoder(qdev, data) + self.q_layer(qdev) + data = self.measure(qdev) + + data_list.append(data.view(bsz, 4)) + + result = torch.cat(data_list, dim=1).float() + + return result + + +class HybridModel(torch.nn.Module): + def __init__(self): + super().__init__() + self.qf = QuanvolutionFilter() + self.linear = torch.nn.Linear(4 * 14 * 14, 10) + + def forward(self, x, use_qiskit=False): + with torch.no_grad(): + x = self.qf(x, use_qiskit) + x = self.linear(x) + return F.log_softmax(x, -1) + + +class HybridModel_without_qf(torch.nn.Module): + def __init__(self): + super().__init__() + self.linear = torch.nn.Linear(28 * 28, 10) + + def forward(self, x, use_qiskit=False): + x = x.view(-1, 28 * 28) + x = self.linear(x) + return F.log_softmax(x, -1) + + +def train(dataflow, model, device, optimizer): + for feed_dict in dataflow["train"]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs) + loss = F.nll_loss(outputs, targets) + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(f"loss: {loss.item()}", end="\r") + + +def valid_test(dataflow, split, model, device, qiskit=False): + target_all = [] + output_all = [] + with torch.no_grad(): + for feed_dict in dataflow[split]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs, use_qiskit=qiskit) + + target_all.append(targets) + output_all.append(outputs) + target_all = torch.cat(target_all, dim=0) + output_all = torch.cat(output_all, dim=0) + + _, indices = output_all.topk(1, dim=1) + masks = indices.eq(target_all.view(-1, 1).expand_as(indices)) + size = target_all.shape[0] + corrects = masks.sum().item() + accuracy = corrects / size + loss = F.nll_loss(output_all, target_all).item() + + print(f"{split} set accuracy: {accuracy}") + print(f"{split} set loss: {loss}") + + return accuracy, loss + + +def main(): + train_model_without_qf = True + n_epochs = 15 + + random.seed(42) + np.random.seed(42) + torch.manual_seed(42) + dataset = MNIST( + root="./mnist_data", + train_valid_split_ratio=[0.9, 0.1], + n_test_samples=300, + n_train_samples=500, + ) + dataflow = dict() + + for split in dataset: + sampler = torch.utils.data.RandomSampler(dataset[split]) + dataflow[split] = torch.utils.data.DataLoader( + dataset[split], + batch_size=10, + sampler=sampler, + num_workers=8, + pin_memory=True, + ) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + model = HybridModel().to(device) + model_without_qf = HybridModel_without_qf().to(device) + optimizer = optim.Adam(model.parameters(), lr=5e-3, weight_decay=1e-4) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + accu_list1 = [] + loss_list1 = [] + accu_list2 = [] + loss_list2 = [] + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}:") + train(dataflow, model, device, optimizer) + print(optimizer.param_groups[0]["lr"]) + + # valid + accu, loss = valid_test( + dataflow, + "test", + model, + device, + ) + accu_list1.append(accu) + loss_list1.append(loss) + scheduler.step() + + if train_model_without_qf: + optimizer = optim.Adam( + model_without_qf.parameters(), lr=5e-3, weight_decay=1e-4 + ) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}:") + train(dataflow, model_without_qf, device, optimizer) + print(optimizer.param_groups[0]["lr"]) + + # valid + accu, loss = valid_test(dataflow, "test", model_without_qf, device) + accu_list2.append(accu) + loss_list2.append(loss) + + scheduler.step() + + # run on real QC + try: + from qiskit import IBMQ + from torchquantum.plugin import QiskitProcessor + + # firstly perform simulate + print(f"\nTest with Qiskit Simulator") + processor_simulation = QiskitProcessor(use_real_qc=False) + model.qf.set_qiskit_processor(processor_simulation) + valid_test(dataflow, "test", model, device, qiskit=True) + # then try to run on REAL QC + backend_name = "ibmq_quito" + print(f"\nTest on Real Quantum Computer {backend_name}") + processor_real_qc = QiskitProcessor(use_real_qc=True, backend_name=backend_name) + model.qf.set_qiskit_processor(processor_real_qc) + valid_test(dataflow, "test", model, device, qiskit=True) + except ImportError: + print( + "Please install qiskit, create an IBM Q Experience Account and " + "save the account token according to the instruction at " + "'https://github.com/Qiskit/qiskit-ibmq-provider', " + "then try again." + ) + + +if __name__ == "__main__": + main() diff --git a/examples/qubit_rotation/qubit_rotation_noise.py b/examples/qubit_rotation/qubit_rotation_noise.py index e69de29b..13a20293 100644 --- a/examples/qubit_rotation/qubit_rotation_noise.py +++ b/examples/qubit_rotation/qubit_rotation_noise.py @@ -0,0 +1,69 @@ +""" +Qubit Rotation Optimization, adapted from https://pennylane.ai/qml/demos/tutorial_qubit_rotation +""" + +# import dependencies +import torchquantum as tq +import torch +from torchquantum.measurement import expval_joint_analytical_density + + +class OptimizationModel(torch.nn.Module): + """ + Circuit with rx and ry gate + """ + + def __init__(self): + super().__init__() + self.rx0 = tq.RX(has_params=True, trainable=True, init_params=0.011) + self.ry0 = tq.RY(has_params=True, trainable=True, init_params=0.012) + + def forward(self): + # create a quantum device to run the gates + qdev = tq.NoiseDevice(n_wires=1, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.01, "Phaseflip": 0.01})) + + # add some trainable gates (need to instantiate ahead of time) + self.rx0(qdev, wires=0) + self.ry0(qdev, wires=0) + + # return the analytic expval from Z + return expval_joint_analytical_density(qdev, "Z") + + +# train function to get expval as low as possible (ideally -1) +def train(model, device, optimizer): + outputs = model() + loss = outputs + optimizer.zero_grad() + loss.backward() + optimizer.step() + + return loss.item() + + +# main function to run the optimization +def main(): + seed = 0 + torch.manual_seed(seed) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + + model = OptimizationModel() + n_epochs = 200 + optimizer = torch.optim.SGD(model.parameters(), lr=0.1) + + for epoch in range(1, n_epochs + 1): + # train + loss = train(model, device, optimizer) + output = (model.rx0.params[0].item(), model.ry0.params[0].item()) + + print(f"Epoch {epoch}: {output}") + + if epoch % 10 == 0: + print(f"Loss after step {epoch}: {loss}") + + +if __name__ == "__main__": + main() diff --git a/examples/regression/run_regression_noise.py b/examples/regression/run_regression_noise.py index e69de29b..3a146721 100644 --- a/examples/regression/run_regression_noise.py +++ b/examples/regression/run_regression_noise.py @@ -0,0 +1,267 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torch.nn.functional as F +import torch.optim as optim +import argparse + +import torchquantum as tq + +from torch.optim.lr_scheduler import CosineAnnealingLR + +import random +import numpy as np + +# data is cos(theta)|000> + e^(j * phi)sin(theta) |111> + +from torchpack.datasets.dataset import Dataset + + +def gen_data(L, N): + omega_0 = np.zeros([2 ** L], dtype="complex_") + omega_0[0] = 1 + 0j + + omega_1 = np.zeros([2 ** L], dtype="complex_") + omega_1[-1] = 1 + 0j + + states = np.zeros([N, 2 ** L], dtype="complex_") + + thetas = 2 * np.pi * np.random.rand(N) + phis = 2 * np.pi * np.random.rand(N) + + for i in range(N): + states[i] = ( + np.cos(thetas[i]) * omega_0 + + np.exp(1j * phis[i]) * np.sin(thetas[i]) * omega_1 + ) + + X = np.sin(2 * thetas) * np.cos(phis) + + return states, X + + +class RegressionDataset: + def __init__(self, split, n_samples, n_wires): + self.split = split + self.n_samples = n_samples + self.n_wires = n_wires + + self.states, self.Xlabel = gen_data(self.n_wires, self.n_samples) + + def __getitem__(self, index: int): + instance = {"states": self.states[index], "Xlabel": self.Xlabel[index]} + return instance + + def __len__(self) -> int: + return self.n_samples + + +class Regression(Dataset): + def __init__(self, n_train, n_valid, n_wires): + n_samples_dict = {"train": n_train, "valid": n_valid} + super().__init__( + { + split: RegressionDataset( + split=split, n_samples=n_samples_dict[split], n_wires=n_wires + ) + for split in ["train", "valid"] + } + ) + + +class QModel(tq.QuantumModule): + def __init__(self, n_wires, n_blocks, add_fc=False): + super().__init__() + # inside one block, we have one u3 layer one each qubit and one layer + # cu3 layer with ring connection + self.n_wires = n_wires + self.n_blocks = n_blocks + self.u3_layers = tq.QuantumModuleList() + self.cu3_layers = tq.QuantumModuleList() + for _ in range(n_blocks): + self.u3_layers.append( + tq.Op1QAllLayer( + op=tq.U3, + n_wires=n_wires, + has_params=True, + trainable=True, + ) + ) + self.cu3_layers.append( + tq.Op2QAllLayer( + op=tq.CU3, + n_wires=n_wires, + has_params=True, + trainable=True, + circular=True, + ) + ) + self.measure = tq.MeasureAll_density(tq.PauliZ) + self.add_fc = add_fc + if add_fc: + self.fc_layer = torch.nn.Linear(n_wires, 1) + + def forward(self, input_states): + qdev = tq.NoiseDevice( + n_wires=self.n_wires, bsz=input_states.shape[0], device=input_states.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + # firstly set the qdev + bsz = input_states.shape[0] + input_states = torch.reshape(input_states, [bsz] + [2] * self.n_wires) + + qdev.clone_from_states(input_states) + for k in range(self.n_blocks): + self.u3_layers[k](qdev) + self.cu3_layers[k](qdev) + + res = self.measure(qdev) + if self.add_fc: + res = self.fc_layer(res) + else: + res = res[:, 1] + return res + + +def train(dataflow, model, device, optimizer): + for feed_dict in dataflow["train"]: + inputs = feed_dict["states"].to(device).to(torch.complex64) + targets = feed_dict["Xlabel"].to(device).to(torch.float) + + outputs = model(inputs) + + loss = F.mse_loss(outputs, targets) + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(f"loss: {loss.item()}") + + +def valid_test(dataflow, split, model, device): + target_all = [] + output_all = [] + with torch.no_grad(): + for feed_dict in dataflow[split]: + inputs = feed_dict["states"].to(device).to(torch.complex64) + targets = feed_dict["Xlabel"].to(device).to(torch.float) + + outputs = model(inputs) + + target_all.append(targets) + output_all.append(outputs) + target_all = torch.cat(target_all, dim=0) + output_all = torch.cat(output_all, dim=0) + + loss = F.mse_loss(output_all, target_all) + + print(f"{split} set loss: {loss}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--pdb", action="store_true", help="debug with pdb") + parser.add_argument( + "--bsz", type=int, default=32, help="batch size for training and validation" + ) + parser.add_argument("--n_wires", type=int, default=3, help="number of qubits") + parser.add_argument( + "--n_blocks", + type=int, + default=2, + help="number of blocks, each contain one layer of " + "U3 gates and one layer of CU3 with " + "ring connections", + ) + parser.add_argument( + "--n_train", type=int, default=300, help="number of training samples" + ) + parser.add_argument( + "--n_valid", type=int, default=1000, help="number of validation samples" + ) + parser.add_argument( + "--epochs", type=int, default=100, help="number of training epochs" + ) + parser.add_argument( + "--addfc", action="store_true", help="add a final classical FC layer" + ) + + args = parser.parse_args() + + if args.pdb: + import pdb + + pdb.set_trace() + + seed = 0 + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + dataset = Regression( + n_train=args.n_train, + n_valid=args.n_valid, + n_wires=args.n_wires, + ) + + dataflow = dict() + + for split in dataset: + if split == "train": + sampler = torch.utils.data.RandomSampler(dataset[split]) + else: + sampler = torch.utils.data.SequentialSampler(dataset[split]) + dataflow[split] = torch.utils.data.DataLoader( + dataset[split], + batch_size=args.bsz, + sampler=sampler, + num_workers=1, + pin_memory=True, + ) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + + model = QModel(n_wires=args.n_wires, n_blocks=args.n_blocks, add_fc=args.addfc).to( + device + ) + + n_epochs = args.epochs + optimizer = optim.Adam(model.parameters(), lr=5e-3, weight_decay=1e-4) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}, LR: {optimizer.param_groups[0]['lr']}") + train(dataflow, model, device, optimizer) + + # valid + valid_test(dataflow, "valid", model, device) + scheduler.step() + + # final valid + valid_test(dataflow, "valid", model, device) + + +if __name__ == "__main__": + main() diff --git a/examples/train_state_prep/train_state_prep_noise.py b/examples/train_state_prep/train_state_prep_noise.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/train_unitary_prep/train_unitary_prep_noise.py b/examples/train_unitary_prep/train_unitary_prep_noise.py index e69de29b..6f38ca42 100644 --- a/examples/train_unitary_prep/train_unitary_prep_noise.py +++ b/examples/train_unitary_prep/train_unitary_prep_noise.py @@ -0,0 +1,118 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torch.optim as optim +import argparse + +import torchquantum as tq +from torch.optim.lr_scheduler import CosineAnnealingLR + +import random +import numpy as np + + +class QModel(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 2 + self.u3_0 = tq.U3(has_params=True, trainable=True) + self.u3_1 = tq.U3(has_params=True, trainable=True) + self.cu3_0 = tq.CU3(has_params=True, trainable=True) + self.cu3_1 = tq.CU3(has_params=True, trainable=True) + self.u3_2 = tq.U3(has_params=True, trainable=True) + self.u3_3 = tq.U3(has_params=True, trainable=True) + + def forward(self, q_device: tq.NoiseDevice): + self.u3_0(q_device, wires=0) + self.u3_1(q_device, wires=1) + self.cu3_0(q_device, wires=[0, 1]) + self.u3_2(q_device, wires=0) + self.u3_3(q_device, wires=1) + self.cu3_1(q_device, wires=[1, 0]) + + +def train(target_unitary, model, optimizer): + result_unitary = model.get_unitary() + + # https://link.aps.org/accepted/10.1103/PhysRevA.95.042318 unitary fidelity according to table 1 + + # compute the unitary infidelity + loss = 1 - (torch.trace(target_unitary.T.conj() @ result_unitary) / target_unitary.shape[0]).abs() ** 2 + + optimizer.zero_grad() + loss.backward() + optimizer.step() + print( + f"infidelity (loss): {loss.item()}, \n target unitary : " + f"{target_unitary.detach().cpu().numpy()}, \n " + f"result unitary : {result_unitary.detach().cpu().numpy()}\n" + ) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--epochs", type=int, default=1000, help="number of training epochs" + ) + + parser.add_argument("--pdb", action="store_true", help="debug with pdb") + + args = parser.parse_args() + + if args.pdb: + import pdb + pdb.set_trace() + + seed = 42 + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + + model = QModel().to(device) + + n_epochs = args.epochs + optimizer = optim.Adam(model.parameters(), lr=1e-2, weight_decay=0) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + target_unitary = torch.tensor( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1j] + ] + , dtype=torch.complex64) + + for epoch in range(1, n_epochs + 1): + print(f"Epoch {epoch}, LR: {optimizer.param_groups[0]['lr']}") + train(target_unitary, model, optimizer) + scheduler.step() + + +if __name__ == "__main__": + main() diff --git a/examples/vqe/vqe_noise.py b/examples/vqe/vqe_noise.py index e69de29b..f7d89109 100644 --- a/examples/vqe/vqe_noise.py +++ b/examples/vqe/vqe_noise.py @@ -0,0 +1,179 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torchquantum as tq +import torch +from torchquantum.util.vqe_utils import parse_hamiltonian_file +import random +import numpy as np +import argparse +import torch.optim as optim + +from torch.optim.lr_scheduler import CosineAnnealingLR +from torchquantum.measurement import expval_joint_analytical_density + + +class QVQEModel(tq.QuantumModule): + def __init__(self, arch, hamil_info): + super().__init__() + self.arch = arch + self.hamil_info = hamil_info + self.n_wires = hamil_info["n_wires"] + self.n_blocks = arch["n_blocks"] + self.u3_layers = tq.QuantumModuleList() + self.cu3_layers = tq.QuantumModuleList() + for _ in range(self.n_blocks): + self.u3_layers.append( + tq.Op1QAllLayer( + op=tq.U3, + n_wires=self.n_wires, + has_params=True, + trainable=True, + ) + ) + self.cu3_layers.append( + tq.Op2QAllLayer( + op=tq.CU3, + n_wires=self.n_wires, + has_params=True, + trainable=True, + circular=True, + ) + ) + + def forward(self): + qdev = tq.NoiseDevice( + n_wires=self.n_wires, bsz=1, device=next(self.parameters()).device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22}) + ) + + for k in range(self.n_blocks): + self.u3_layers[k](qdev) + self.cu3_layers[k](qdev) + + expval = 0 + for hamil in self.hamil_info["hamil_list"]: + expval += ( + expval_joint_analytical_density(qdev, observable=hamil["pauli_string"]) + * hamil["coeff"] + ) + + return expval + + +def train(model, optimizer, n_steps=1): + for _ in range(n_steps): + loss = model() + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(f"Expectation of energy: {loss.item()}") + + +def valid_test(model): + with torch.no_grad(): + loss = model() + + print(f"validation: expectation of energy: {loss.item()}") + + +def process_hamil_info(hamil_info): + hamil_list = hamil_info["hamil_list"] + n_wires = hamil_info["n_wires"] + all_info = [] + + for hamil in hamil_list: + pauli_string = "" + for i in range(n_wires): + if i in hamil["wires"]: + wire = hamil["wires"].index(i) + pauli_string += hamil["observables"][wire].upper() + else: + pauli_string += "I" + all_info.append({"pauli_string": pauli_string, "coeff": hamil["coefficient"]}) + hamil_info["hamil_list"] = all_info + return hamil_info + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--pdb", action="store_true", help="debug with pdb") + parser.add_argument( + "--n_blocks", + type=int, + default=2, + help="number of blocks, each contain one layer of " + "U3 gates and one layer of CU3 with " + "ring connections", + ) + parser.add_argument( + "--steps_per_epoch", type=int, default=10, help="number of training epochs" + ) + parser.add_argument( + "--epochs", type=int, default=100, help="number of training epochs" + ) + parser.add_argument( + "--hamil_filename", + type=str, + default="h2.txt", + help="number of training epochs", + ) + + args = parser.parse_args() + + if args.pdb: + import pdb + + pdb.set_trace() + + seed = 0 + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + hamil_info = process_hamil_info(parse_hamiltonian_file(args.hamil_filename)) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + model = QVQEModel(arch={"n_blocks": args.n_blocks}, hamil_info=hamil_info) + + model.to(device) + + n_epochs = args.epochs + optimizer = optim.Adam(model.parameters(), lr=5e-3, weight_decay=1e-4) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}, LR: {optimizer.param_groups[0]['lr']}") + train(model, optimizer, n_steps=args.steps_per_epoch) + + scheduler.step() + + # final valid + valid_test(model) + + +if __name__ == "__main__": + main() From f1c3dfe01d7f347e80211bbac5f1369bf49876d4 Mon Sep 17 00:00:00 2001 From: Zhuoyang Ye Date: Sun, 25 Feb 2024 16:13:10 -0800 Subject: [PATCH 25/54] [Fix] Fix some bugs. --- test/algorithm/test_hamiltonian.py | 7 ++++++- test/density/test_density_op.py | 9 ++++----- torchquantum/functional/hadamard.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/test/algorithm/test_hamiltonian.py b/test/algorithm/test_hamiltonian.py index e5e8a60f..4b93fe45 100644 --- a/test/algorithm/test_hamiltonian.py +++ b/test/algorithm/test_hamiltonian.py @@ -132,8 +132,13 @@ def test_hamiltonian(): ] ), ) + import os - hamil = Hamiltonian.from_file("test/algorithm/h2.txt") + current_dir = os.path.dirname(os.path.abspath(__file__)) + file_path = os.path.join(current_dir, '..', 'algorithm', 'h2.txt') + hamil = Hamiltonian.from_file(file_path) + + #hamil = Hamiltonian.from_file("./h2.txt") assert np.allclose( hamil.matrix.cpu().detach().numpy(), diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py index 53b2eaaf..5826b015 100644 --- a/test/density/test_density_op.py +++ b/test/density/test_density_op.py @@ -57,7 +57,7 @@ {"qiskit": qiskit_gate.RYGate, "tq": tq.ry, "name": "Ry", "numparam": 1}, {"qiskit": qiskit_gate.RZGate, "tq": tq.rz, "name": "RZ", "numparam": 1}, {"qiskit": qiskit_gate.U1Gate, "tq": tq.u1, "name": "U1", "numparam": 1}, - {"qiskit": qiskit_gate.PhaseGate, "tq": tq.phaseshift, "name": "Phaseshift", "numparam": 1}, + #{"qiskit": qiskit_gate.PhaseGate, "tq": tq.phaseshift, "name": "Phaseshift", "numparam": 1}, #{"qiskit": qiskit_gate.GlobalPhaseGate, "tq": tq.globalphase, "name": "Gphase", "numparam": 1}, {"qiskit": qiskit_gate.U2Gate, "tq": tq.u2, "name": "U2", "numparam": 2}, {"qiskit": qiskit_gate.U3Gate, "tq": tq.u3, "name": "U3", "numparam": 3}, @@ -75,7 +75,8 @@ {"qiskit": qiskit_gate.CHGate, "tq": tq.ch, "name": "CH"}, {"qiskit": qiskit_gate.CSdgGate, "tq": tq.csdg, "name": "CSdag"}, {"qiskit": qiskit_gate.SwapGate, "tq": tq.swap, "name": "SWAP"}, - {"qiskit": qiskit_gate.iSwapGate, "tq": tq.iswap, "name": "iSWAP"} + {"qiskit": qiskit_gate.iSwapGate, "tq": tq.iswap, "name": "iSWAP"}, + {"qiskit": qiskit_gate.CSXGate, "tq": tq.csx, "name": "CSX"} ] two_qubit_param_gate_list = [ @@ -93,9 +94,7 @@ three_qubit_gate_list = [ {"qiskit": qiskit_gate.CCXGate, "tq": tq.ccx, "name": "Toffoli"}, {"qiskit": qiskit_gate.CSwapGate, "tq": tq.cswap, "name": "CSWAP"}, - {"qiskit": qiskit_gate.iSwapGate, "tq": tq.iswap, "name": "ISWAP"}, - {"qiskit": qiskit_gate.CCZGate, "tq": tq.ccz, "name": "CCZ"}, - {"qiskit": qiskit_gate.CSXGate, "tq": tq.csx, "name": "CSX"} + {"qiskit": qiskit_gate.CCZGate, "tq": tq.ccz, "name": "CCZ"} ] three_qubit_param_gate_list = [ diff --git a/torchquantum/functional/hadamard.py b/torchquantum/functional/hadamard.py index a2a45c40..a2deb86b 100644 --- a/torchquantum/functional/hadamard.py +++ b/torchquantum/functional/hadamard.py @@ -160,7 +160,7 @@ def chadamard( name = "chadamard" - mat = mat_dict[name] + mat = _hadamard_mat_dict[name] gate_wrapper( name=name, mat=mat, From a35aa2c1f4ace200069afb8de3cfd7579fca5252 Mon Sep 17 00:00:00 2001 From: GenericP3rson Date: Sat, 9 Mar 2024 21:39:59 -0500 Subject: [PATCH 26/54] [minor] added the correct version of a dependency --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index a139a9f0..f6cc4610 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ pylatexenc>=2.10 pyscf>=2.0.1 qiskit>=0.39.0,<1.0.0 recommonmark +qiskit_ibm_runtime==0.20.0 scipy>=1.5.2 setuptools>=52.0.0 From 090e2dcbf6fbe166952dd155a0b9457fa4f85787 Mon Sep 17 00:00:00 2001 From: GenericP3rson <41024739+GenericP3rson@users.noreply.github.com> Date: Sun, 24 Mar 2024 12:47:42 -0500 Subject: [PATCH 27/54] [minor] adding aer as well --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f6cc4610..8bf4d45c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ pyscf>=2.0.1 qiskit>=0.39.0,<1.0.0 recommonmark qiskit_ibm_runtime==0.20.0 +qiskit-aer==0.13.3 scipy>=1.5.2 setuptools>=52.0.0 From b7aa9f27c90b50eeffc523bc5898902724a41910 Mon Sep 17 00:00:00 2001 From: Pranav Gokhale Date: Thu, 28 Mar 2024 11:36:32 -0400 Subject: [PATCH 28/54] Fix minor typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e653af8c..a74a5a22 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Simulate quantum computations on classical hardware using PyTorch. It supports s Researchers on quantum algorithm design, parameterized quantum circuit training, quantum optimal control, quantum machine learning, quantum neural networks. #### Differences from Qiskit/Pennylane -Dynamic computation graph, automatic gradient computation, fast GPU support, batch model tersorized processing. +Dynamic computation graph, automatic gradient computation, fast GPU support, batch model tensorized processing. ## News - v0.1.8 Available! From e18afe40b6aedca4db805a1270bc274be5c9ddea Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Wed, 29 May 2024 15:26:13 +0900 Subject: [PATCH 29/54] chore: remove unnecessary import --- test/plugin/test_qiskit_plugins.py | 14 +- torchquantum/noise_model/noise_models.py | 393 ++++++++------ torchquantum/plugin/qiskit/qiskit_plugin.py | 27 +- .../plugin/qiskit/qiskit_processor.py | 68 +-- torchquantum/plugin/qiskit/qiskit_pulse.py | 8 +- torchquantum/plugin/qiskit_pulse.py | 12 +- torchquantum/pulse/pulse_utils.py | 215 ++++---- torchquantum/pulse/templates/pulse_utils.py | 41 +- torchquantum/util/utils.py | 511 ++++++++++-------- 9 files changed, 653 insertions(+), 636 deletions(-) diff --git a/test/plugin/test_qiskit_plugins.py b/test/plugin/test_qiskit_plugins.py index 684dbfc6..634b521d 100644 --- a/test/plugin/test_qiskit_plugins.py +++ b/test/plugin/test_qiskit_plugins.py @@ -22,19 +22,16 @@ SOFTWARE. """ -from qiskit import QuantumCircuit -import numpy as np import random -from qiskit.opflow import StateFn, X, Y, Z, I -import torchquantum as tq +import numpy as np +import pytest +from qiskit.opflow import I, StateFn, X, Y, Z -from torchquantum.plugin import op_history2qiskit, QiskitProcessor +import torchquantum as tq +from torchquantum.plugin import QiskitProcessor, op_history2qiskit from torchquantum.util import switch_little_big_endian_state -import torch -import pytest - pauli_str_op_dict = { "X": X, "Y": Y, @@ -42,6 +39,7 @@ "I": I, } + @pytest.mark.skip def test_expval_observable(): # seed = 0 diff --git a/torchquantum/noise_model/noise_models.py b/torchquantum/noise_model/noise_models.py index 571314e9..2309c7e8 100644 --- a/torchquantum/noise_model/noise_models.py +++ b/torchquantum/noise_model/noise_models.py @@ -24,12 +24,11 @@ import numpy as np import torch -import torchquantum as tq - +from qiskit_aer.noise import NoiseModel from torchpack.utils.logging import logger -from qiskit.providers.aer.noise import NoiseModel -from torchquantum.util import get_provider +import torchquantum as tq +from torchquantum.util import get_provider __all__ = [ "NoiseModelTQ", @@ -50,31 +49,31 @@ def cos_adjust_noise( orig_noise_total_prob, ): """ - Adjust the noise probability based on the current epoch and a cosine schedule. + Adjust the noise probability based on the current epoch and a cosine schedule. + + Args: + current_epoch (int): The current epoch. + n_epochs (int): The total number of epochs. + prob_schedule (str): The probability schedule type. Possible values are: + - None: No schedule, use the original noise probability. + - "increase": Increase the noise probability using a cosine schedule. + - "decrease": Decrease the noise probability using a cosine schedule. + - "increase_decrease": Increase the noise probability until a separator epoch, + then decrease it using cosine schedules. + prob_schedule_separator (int): The epoch at which the schedule changes for + "increase_decrease" mode. + orig_noise_total_prob (float): The original noise probability. + + Returns: + float: The adjusted noise probability based on the schedule. + + Note: + The adjusted noise probability is returned as a float between 0 and 1. + + Raises: + None. - Args: - current_epoch (int): The current epoch. - n_epochs (int): The total number of epochs. - prob_schedule (str): The probability schedule type. Possible values are: - - None: No schedule, use the original noise probability. - - "increase": Increase the noise probability using a cosine schedule. - - "decrease": Decrease the noise probability using a cosine schedule. - - "increase_decrease": Increase the noise probability until a separator epoch, - then decrease it using cosine schedules. - prob_schedule_separator (int): The epoch at which the schedule changes for - "increase_decrease" mode. - orig_noise_total_prob (float): The original noise probability. - - Returns: - float: The adjusted noise probability based on the schedule. - - Note: - The adjusted noise probability is returned as a float between 0 and 1. - - Raises: - None. - - """ + """ if prob_schedule is None: noise_total_prob = orig_noise_total_prob @@ -134,31 +133,31 @@ def cos_adjust_noise( def apply_readout_error_func(x, c2p_mapping, measure_info): """ - Apply readout error to the measurement outcomes. - - Args: - x (torch.Tensor): The measurement outcomes, represented as a tensor of shape (batch_size, num_qubits). - c2p_mapping (dict): Mapping from qubit indices to physical wire indices. - measure_info (dict): Measurement information dictionary containing the probabilities for different outcomes. - - Returns: - torch.Tensor: The measurement outcomes after applying the readout error, represented as a tensor of the same shape as x. - - Note: - The readout error is applied based on the given mapping and measurement information. - The measurement information dictionary should have the following structure: - { - (wire_1,): {"probabilities": [[p_0, p_1], [p_0, p_1]]}, - (wire_2,): {"probabilities": [[p_0, p_1], [p_0, p_1]]}, - ... - } - where wire_1, wire_2, ... are the physical wire indices, and p_0 and p_1 are the probabilities of measuring 0 and 1, respectively, - for each wire. - - Raises: - None. + Apply readout error to the measurement outcomes. + + Args: + x (torch.Tensor): The measurement outcomes, represented as a tensor of shape (batch_size, num_qubits). + c2p_mapping (dict): Mapping from qubit indices to physical wire indices. + measure_info (dict): Measurement information dictionary containing the probabilities for different outcomes. + + Returns: + torch.Tensor: The measurement outcomes after applying the readout error, represented as a tensor of the same shape as x. + + Note: + The readout error is applied based on the given mapping and measurement information. + The measurement information dictionary should have the following structure: + { + (wire_1,): {"probabilities": [[p_0, p_1], [p_0, p_1]]}, + (wire_2,): {"probabilities": [[p_0, p_1], [p_0, p_1]]}, + ... + } + where wire_1, wire_2, ... are the physical wire indices, and p_0 and p_1 are the probabilities of measuring 0 and 1, respectively, + for each wire. + + Raises: + None. - """ + """ # add readout error noise_free_0_probs = (x + 1) / 2 noise_free_1_probs = 1 - (x + 1) / 2 @@ -196,21 +195,22 @@ def apply_readout_error_func(x, c2p_mapping, measure_info): class NoiseCounter: """ - A class for counting the occurrences of Pauli error gates. + A class for counting the occurrences of Pauli error gates. - Attributes: - counter_x (int): Counter for Pauli X errors. - counter_y (int): Counter for Pauli Y errors. - counter_z (int): Counter for Pauli Z errors. - counter_X (int): Counter for Pauli X errors (for two-qubit gates). - counter_Y (int): Counter for Pauli Y errors (for two-qubit gates). - counter_Z (int): Counter for Pauli Z errors (for two-qubit gates). + Attributes: + counter_x (int): Counter for Pauli X errors. + counter_y (int): Counter for Pauli Y errors. + counter_z (int): Counter for Pauli Z errors. + counter_X (int): Counter for Pauli X errors (for two-qubit gates). + counter_Y (int): Counter for Pauli Y errors (for two-qubit gates). + counter_Z (int): Counter for Pauli Z errors (for two-qubit gates). - Methods: - add(error): Adds a Pauli error to the counters based on the error type. - __str__(): Returns a string representation of the counters. + Methods: + add(error): Adds a Pauli error to the counters based on the error type. + __str__(): Returns a string representation of the counters. + + """ - """ def __init__(self): self.counter_x = 0 self.counter_y = 0 @@ -220,51 +220,51 @@ def __init__(self): self.counter_Z = 0 def add(self, error): - if error == 'x': + if error == "x": self.counter_x += 1 - elif error == 'y': + elif error == "y": self.counter_y += 1 - elif error == 'z': + elif error == "z": self.counter_z += 1 - if error == 'X': + if error == "X": self.counter_X += 1 - elif error == 'Y': + elif error == "Y": self.counter_Y += 1 - elif error == 'Z': + elif error == "Z": self.counter_Z += 1 else: pass - - def __str__(self) -> str: - return f'single qubit error: pauli x = {self.counter_x}, pauli y = {self.counter_y}, pauli z = {self.counter_z}\n' + \ - f'double qubit error: pauli x = {self.counter_X}, pauli y = {self.counter_Y}, pauli z = {self.counter_Z}' + def __str__(self) -> str: + return ( + f"single qubit error: pauli x = {self.counter_x}, pauli y = {self.counter_y}, pauli z = {self.counter_z}\n" + + f"double qubit error: pauli x = {self.counter_X}, pauli y = {self.counter_Y}, pauli z = {self.counter_Z}" + ) class NoiseModelTQ(object): """ - A class for applying gate insertion and readout errors. - - Attributes: - noise_model_name (str): Name of the noise model. - n_epochs (int): Number of epochs. - noise_total_prob (float): Total probability of noise. - ignored_ops (tuple): Operations to be ignored. - prob_schedule (list): Probability schedule. - prob_schedule_separator (str): Separator for probability schedule. - factor (float): Factor for adjusting probabilities. - add_thermal (bool): Flag indicating whether to add thermal relaxation. - - Methods: - adjust_noise(current_epoch): Adjusts the noise based on the current epoch. - clean_parsed_noise_model_dict(nm_dict, ignored_ops): Cleans the parsed noise model dictionary. - parse_noise_model_dict(nm_dict): Parses the noise model dictionary. - magnify_probs(probs): Magnifies the probabilities based on a factor. - sample_noise_op(op_in): Samples a noise operation based on the given operation. - apply_readout_error(x): Applies readout error to the input. - - """ + A class for applying gate insertion and readout errors. + + Attributes: + noise_model_name (str): Name of the noise model. + n_epochs (int): Number of epochs. + noise_total_prob (float): Total probability of noise. + ignored_ops (tuple): Operations to be ignored. + prob_schedule (list): Probability schedule. + prob_schedule_separator (str): Separator for probability schedule. + factor (float): Factor for adjusting probabilities. + add_thermal (bool): Flag indicating whether to add thermal relaxation. + + Methods: + adjust_noise(current_epoch): Adjusts the noise based on the current epoch. + clean_parsed_noise_model_dict(nm_dict, ignored_ops): Cleans the parsed noise model dictionary. + parse_noise_model_dict(nm_dict): Parses the noise model dictionary. + magnify_probs(probs): Magnifies the probabilities based on a factor. + sample_noise_op(op_in): Samples a noise operation based on the given operation. + apply_readout_error(x): Applies readout error to the input. + """ def __init__( self, @@ -295,7 +295,9 @@ def __init__( self.ignored_ops = ignored_ops self.parsed_dict = self.parse_noise_model_dict(self.noise_model_dict) - self.parsed_dict = self.clean_parsed_noise_model_dict(self.parsed_dict, ignored_ops) + self.parsed_dict = self.clean_parsed_noise_model_dict( + self.parsed_dict, ignored_ops + ) self.n_epochs = n_epochs self.prob_schedule = prob_schedule self.prob_schedule_separator = prob_schedule_separator @@ -313,39 +315,66 @@ def adjust_noise(self, current_epoch): @staticmethod def clean_parsed_noise_model_dict(nm_dict, ignored_ops): - # remove the ignored operation in the instructions and probs + # remove the ignored operation in the instructions and probs # --> only get the pauli-x,y,z errors. ignore the thermal relaxation errors (kraus operator) def filter_inst(inst_list: list) -> list: new_inst_list = [] for inst in inst_list: - if inst['name'] in ignored_ops: + if inst["name"] in ignored_ops: continue new_inst_list.append(inst) return new_inst_list - ignored_ops = set(ignored_ops) - single_depolarization = set(['x', 'y', 'z']) - double_depolarization = set(['IX', 'IY', 'IZ', 'XI', 'XX', 'XY', 'XZ', 'YI', 'YX', 'YY', 'YZ', 'ZI', 'ZX', 'ZY', 'ZZ']) # 16 - 1 = 15 combinations + ignored_ops = set(ignored_ops) + single_depolarization = set(["x", "y", "z"]) + double_depolarization = set( + [ + "IX", + "IY", + "IZ", + "XI", + "XX", + "XY", + "XZ", + "YI", + "YX", + "YY", + "YZ", + "ZI", + "ZX", + "ZY", + "ZZ", + ] + ) # 16 - 1 = 15 combinations for operation, operation_info in nm_dict.items(): for qubit, qubit_info in operation_info.items(): inst_all = [] prob_all = [] if qubit_info["type"] == "qerror": - for inst, prob in zip(qubit_info["instructions"], qubit_info["probabilities"]): - if operation in ['x', 'sx', 'id', 'reset']: # single qubit gate - if any([inst_one["name"] in single_depolarization for inst_one in inst]): + for inst, prob in zip( + qubit_info["instructions"], qubit_info["probabilities"] + ): + if operation in ["x", "sx", "id", "reset"]: # single qubit gate + if any( + [ + inst_one["name"] in single_depolarization + for inst_one in inst + ] + ): inst_all.append(filter_inst(inst)) prob_all.append(prob) - elif operation in ['cx']: # double qubit gate + elif operation in ["cx"]: # double qubit gate try: - if inst[0]['params'][0] in double_depolarization and (inst[1]['name'] == 'id' or inst[2]['name'] == 'id'): + if inst[0]["params"][0] in double_depolarization and ( + inst[1]["name"] == "id" or inst[2]["name"] == "id" + ): inst_all.append(filter_inst(inst)) prob_all.append(prob) except: pass # don't know how to deal with this case else: - raise Exception(f'{operation} not considered...') + raise Exception(f"{operation} not considered...") nm_dict[operation][qubit]["instructions"] = inst_all nm_dict[operation][qubit]["probabilities"] = prob_all return nm_dict @@ -364,8 +393,13 @@ def parse_noise_model_dict(nm_dict): } if info["operations"][0] not in parsed.keys(): - parsed[info["operations"][0]] = {tuple(info["gate_qubits"][0]): val_dict} - elif tuple(info["gate_qubits"][0]) not in parsed[info["operations"][0]].keys(): + parsed[info["operations"][0]] = { + tuple(info["gate_qubits"][0]): val_dict + } + elif ( + tuple(info["gate_qubits"][0]) + not in parsed[info["operations"][0]].keys() + ): parsed[info["operations"][0]][tuple(info["gate_qubits"][0])] = val_dict else: raise ValueError @@ -432,30 +466,36 @@ def sample_noise_op(self, op_in): ops = [] for instruction in instructions: - v_wires = [self.p_v_reg_mapping["p2v"][qubit] for qubit in instruction["qubits"]] + v_wires = [ + self.p_v_reg_mapping["p2v"][qubit] for qubit in instruction["qubits"] + ] if instruction["name"] == "x": ops.append(tq.PauliX(wires=v_wires)) - self.noise_counter.add('x') + self.noise_counter.add("x") elif instruction["name"] == "y": ops.append(tq.PauliY(wires=v_wires)) - self.noise_counter.add('y') + self.noise_counter.add("y") elif instruction["name"] == "z": ops.append(tq.PauliZ(wires=v_wires)) - self.noise_counter.add('z') + self.noise_counter.add("z") elif instruction["name"] == "reset": ops.append(tq.Reset(wires=v_wires)) elif instruction["name"] == "pauli": - twoqubit_depolarization = list(instruction['params'][0]) # ['XY'] --> ['X', 'Y'] - for singlequbit_deloparization, v_wire in zip(twoqubit_depolarization, v_wires): - if singlequbit_deloparization == 'X': + twoqubit_depolarization = list( + instruction["params"][0] + ) # ['XY'] --> ['X', 'Y'] + for singlequbit_deloparization, v_wire in zip( + twoqubit_depolarization, v_wires + ): + if singlequbit_deloparization == "X": ops.append(tq.PauliX(wires=[v_wire])) - self.noise_counter.add('X') - elif singlequbit_deloparization == 'Y': + self.noise_counter.add("X") + elif singlequbit_deloparization == "Y": ops.append(tq.PauliY(wires=[v_wire])) - self.noise_counter.add('Y') - elif singlequbit_deloparization == 'Z': + self.noise_counter.add("Y") + elif singlequbit_deloparization == "Z": ops.append(tq.PauliZ(wires=[v_wire])) - self.noise_counter.add('Z') + self.noise_counter.add("Z") else: pass # 'I' case else: @@ -474,25 +514,24 @@ def apply_readout_error(self, x): class NoiseModelTQActivation(object): """ - A class for adding noise to the activations. - - Attributes: - mean (tuple): Mean values of the noise. - std (tuple): Standard deviation values of the noise. - n_epochs (int): Number of epochs. - prob_schedule (list): Probability schedule. - prob_schedule_separator (str): Separator for probability schedule. - after_norm (bool): Flag indicating whether noise should be added after normalization. - factor (float): Factor for adjusting the noise. - - Methods: - adjust_noise(current_epoch): Adjusts the noise based on the current epoch. - sample_noise_op(op_in): Samples a noise operation. - apply_readout_error(x): Applies readout error to the input. - add_noise(x, node_id, is_after_norm): Adds noise to the activations. - - """ + A class for adding noise to the activations. + + Attributes: + mean (tuple): Mean values of the noise. + std (tuple): Standard deviation values of the noise. + n_epochs (int): Number of epochs. + prob_schedule (list): Probability schedule. + prob_schedule_separator (str): Separator for probability schedule. + after_norm (bool): Flag indicating whether noise should be added after normalization. + factor (float): Factor for adjusting the noise. + + Methods: + adjust_noise(current_epoch): Adjusts the noise based on the current epoch. + sample_noise_op(op_in): Samples a noise operation. + apply_readout_error(x): Applies readout error to the input. + add_noise(x, node_id, is_after_norm): Adds noise to the activations. + """ def __init__( self, @@ -560,23 +599,23 @@ def add_noise(self, x, node_id, is_after_norm=False): class NoiseModelTQPhase(object): """ - A class for adding noise to rotation parameters. - - Attributes: - mean (float): Mean value of the noise. - std (float): Standard deviation value of the noise. - n_epochs (int): Number of epochs. - prob_schedule (list): Probability schedule. - prob_schedule_separator (str): Separator for probability schedule. - factor (float): Factor for adjusting the noise. - - Methods: - adjust_noise(current_epoch): Adjusts the noise based on the current epoch. - sample_noise_op(op_in): Samples a noise operation. - apply_readout_error(x): Applies readout error to the input. - add_noise(phase): Adds noise to the rotation parameters. + A class for adding noise to rotation parameters. + + Attributes: + mean (float): Mean value of the noise. + std (float): Standard deviation value of the noise. + n_epochs (int): Number of epochs. + prob_schedule (list): Probability schedule. + prob_schedule_separator (str): Separator for probability schedule. + factor (float): Factor for adjusting the noise. + + Methods: + adjust_noise(current_epoch): Adjusts the noise based on the current epoch. + sample_noise_op(op_in): Samples a noise operation. + apply_readout_error(x): Applies readout error to the input. + add_noise(phase): Adds noise to the rotation parameters. - """ + """ def __init__( self, @@ -638,40 +677,43 @@ def add_noise(self, phase): class NoiseModelTQReadoutOnly(NoiseModelTQ): """ - A subclass of NoiseModelTQ that applies readout errors only. + A subclass of NoiseModelTQ that applies readout errors only. + + This class inherits from NoiseModelTQ and overrides the sample_noise_op method to exclude the insertion of any noise operations other than readout errors. It is designed for scenarios where only readout errors are considered, and all other noise sources are ignored. - This class inherits from NoiseModelTQ and overrides the sample_noise_op method to exclude the insertion of any noise operations other than readout errors. It is designed for scenarios where only readout errors are considered, and all other noise sources are ignored. + Methods: + sample_noise_op(op_in): Returns an empty list, indicating no noise operations are applied. + """ - Methods: - sample_noise_op(op_in): Returns an empty list, indicating no noise operations are applied. - """ def sample_noise_op(self, op_in): return [] class NoiseModelTQQErrorOnly(NoiseModelTQ): """ - A subclass of NoiseModelTQ that applies only readout errors. + A subclass of NoiseModelTQ that applies only readout errors. - This class inherits from NoiseModelTQ and overrides the apply_readout_error method to apply readout errors. It removes activation noise and only focuses on readout errors in the noise model. + This class inherits from NoiseModelTQ and overrides the apply_readout_error method to apply readout errors. It removes activation noise and only focuses on readout errors in the noise model. - Methods: - apply_readout_error(x): Applies readout error to the given activation values. + Methods: + apply_readout_error(x): Applies readout error to the given activation values. + + """ - """ def apply_readout_error(self, x): return x class NoiseModelTQActivationReadout(NoiseModelTQActivation): """ - A subclass of NoiseModelTQActivation that applies readout errors. + A subclass of NoiseModelTQActivation that applies readout errors. - This class inherits from NoiseModelTQActivation and overrides the apply_readout_error method to incorporate readout errors. It combines activation noise and readout errors into the noise model. + This class inherits from NoiseModelTQActivation and overrides the apply_readout_error method to incorporate readout errors. It combines activation noise and readout errors into the noise model. + + Methods: + apply_readout_error(x): Applies readout error to the given activation values + """ - Methods: - apply_readout_error(x): Applies readout error to the given activation values - """ def __init__( self, noise_model_name, @@ -713,13 +755,14 @@ def apply_readout_error(self, x): class NoiseModelTQPhaseReadout(NoiseModelTQPhase): """ - A subclass of NoiseModelTQPhase that applies readout errors to phase values. + A subclass of NoiseModelTQPhase that applies readout errors to phase values. - This class inherits from NoiseModelTQPhase and overrides the apply_readout_error method to apply readout errors specifically to phase values. It uses the noise model provided to introduce readout errors. + This class inherits from NoiseModelTQPhase and overrides the apply_readout_error method to apply readout errors specifically to phase values. It uses the noise model provided to introduce readout errors. + + Methods: + apply_readout_error(x): Applies readout error to the given phase values. + """ - Methods: - apply_readout_error(x): Applies readout error to the given phase values. - """ def __init__( self, noise_model_name, diff --git a/torchquantum/plugin/qiskit/qiskit_plugin.py b/torchquantum/plugin/qiskit/qiskit_plugin.py index bca3a7d2..defa620d 100644 --- a/torchquantum/plugin/qiskit/qiskit_plugin.py +++ b/torchquantum/plugin/qiskit/qiskit_plugin.py @@ -22,24 +22,23 @@ SOFTWARE. """ -import torch -import torchquantum as tq -import torchquantum.functional as tqf -import qiskit.circuit.library.standard_gates as qiskit_gate -import numpy as np +from typing import Iterable -from qiskit import QuantumCircuit, ClassicalRegister -from qiskit import Aer, execute +import numpy as np +import qiskit.circuit.library.standard_gates as qiskit_gate +import torch +from qiskit import Aer, ClassicalRegister, QuantumCircuit, execute from qiskit.circuit import Parameter from torchpack.utils.logging import logger + +import torchquantum as tq +import torchquantum.functional as tqf +from torchquantum.functional import mat_dict from torchquantum.util import ( - switch_little_big_endian_matrix, find_global_phase, + switch_little_big_endian_matrix, switch_little_big_endian_state, ) -from typing import Iterable, List -from torchquantum.functional import mat_dict - __all__ = [ "tq2qiskit", @@ -665,11 +664,11 @@ def op_history2qiskit_expand_params(n_wires, op_history, bsz): param = op["params"][i] else: param = None - + append_fixed_gate( circ, op["name"], param, op["wires"], op["inverse"] ) - + circs_all.append(circ) return circs_all @@ -762,7 +761,7 @@ def qiskit2tq_Operator(circ: QuantumCircuit): raise NotImplementedError( f"{op_name} conversion to tq is currently not supported." ) - + return ops diff --git a/torchquantum/plugin/qiskit/qiskit_processor.py b/torchquantum/plugin/qiskit/qiskit_processor.py index 2d91e7c3..d24ce521 100644 --- a/torchquantum/plugin/qiskit/qiskit_processor.py +++ b/torchquantum/plugin/qiskit/qiskit_processor.py @@ -22,34 +22,30 @@ SOFTWARE. """ -import torch -import torchquantum as tq -import pathos.multiprocessing as multiprocessing +import datetime import itertools -from qiskit import Aer, execute, IBMQ, transpile, QuantumCircuit -from qiskit.providers.aer.noise import NoiseModel -from qiskit.tools.monitor import job_monitor +import numpy as np +import pathos.multiprocessing as multiprocessing +import torch +from qiskit import IBMQ, Aer, QuantumCircuit, execute, transpile from qiskit.exceptions import QiskitError -from .qiskit_plugin import ( - tq2qiskit, - tq2qiskit_parameterized, - tq2qiskit_measurement, -) +from qiskit.tools.monitor import job_monitor +from qiskit.transpiler import PassManager +from qiskit_aer.noise import NoiseModel +from torchpack.utils.logging import logger +from tqdm import tqdm + +import torchquantum as tq from torchquantum.util import ( + get_circ_stats, get_expectations_from_counts, get_provider, get_provider_hub_group_project, - get_circ_stats, ) -from .qiskit_macros import IBMQ_NAMES -from tqdm import tqdm -from torchpack.utils.logging import logger -from qiskit.transpiler import PassManager -import numpy as np -import datetime -from .my_job_monitor import my_job_monitor +from .qiskit_macros import IBMQ_NAMES +from .qiskit_plugin import tq2qiskit, tq2qiskit_measurement, tq2qiskit_parameterized class EmptyPassManager(PassManager): @@ -555,7 +551,7 @@ def process_parameterized_and_shift( # total_cont += 1 # print(total_time_spent / total_cont) break - except (QiskitError) as e: + except QiskitError as e: logger.warning("Job failed, rerun now.") print(e.message) @@ -758,9 +754,9 @@ def process_circs_get_joint_expval(self, circs_all, observable, parallel=True): for circ_ in circs_all: circ = circ_.copy() for k, obs in enumerate(observable): - if obs == 'X': + if obs == "X": circ.h(k) - elif obs == 'Y': + elif obs == "Y": circ.z(k) circ.s(k) circ.h(k) @@ -771,8 +767,10 @@ def process_circs_get_joint_expval(self, circs_all, observable, parallel=True): mask = np.ones(len(observable), dtype=bool) mask[np.array([*observable]) == "I"] = False - - counts = self.process_ready_circs_get_counts(circs_all_diagonalized, parallel=parallel) + + counts = self.process_ready_circs_get_counts( + circs_all_diagonalized, parallel=parallel + ) # here we need to switch the little and big endian of distribution bitstrings distributions = [] @@ -786,19 +784,25 @@ def process_circs_get_joint_expval(self, circs_all, observable, parallel=True): n_eigen_one = 0 n_eigen_minus_one = 0 for bitstring, n_count in distri.items(): - if np.dot(list(map(lambda x: eval(x), [*bitstring])), mask).sum() % 2 == 0: + if ( + np.dot(list(map(lambda x: eval(x), [*bitstring])), mask).sum() % 2 + == 0 + ): n_eigen_one += n_count else: n_eigen_minus_one += n_count - - expval = n_eigen_one / self.n_shots + (-1) * n_eigen_minus_one / self.n_shots + + expval = ( + n_eigen_one / self.n_shots + (-1) * n_eigen_minus_one / self.n_shots + ) expval_all.append(expval) return expval_all -if __name__ == '__main__': +if __name__ == "__main__": import pdb + pdb.set_trace() circ = QuantumCircuit(3) circ.h(0) @@ -806,11 +810,9 @@ def process_circs_get_joint_expval(self, circs_all, observable, parallel=True): circ.cx(1, 2) circ.rx(0.1, 0) - qiskit_processor = QiskitProcessor( - use_real_qc=False - ) + qiskit_processor = QiskitProcessor(use_real_qc=False) - qiskit_processor.process_circs_get_joint_expval([circ], 'XII') + qiskit_processor.process_circs_get_joint_expval([circ], "XII") qdev = tq.QuantumDevice(n_wires=3, bsz=1) qdev.h(0) @@ -819,5 +821,5 @@ def process_circs_get_joint_expval(self, circs_all, observable, parallel=True): qdev.rx(0, 0.1) from torchquantum.measurement import expval_joint_sampling - print(expval_joint_sampling(qdev, 'XII', n_shots=8192)) + print(expval_joint_sampling(qdev, "XII", n_shots=8192)) diff --git a/torchquantum/plugin/qiskit/qiskit_pulse.py b/torchquantum/plugin/qiskit/qiskit_pulse.py index b9c78760..ab28774f 100644 --- a/torchquantum/plugin/qiskit/qiskit_pulse.py +++ b/torchquantum/plugin/qiskit/qiskit_pulse.py @@ -22,12 +22,8 @@ SOFTWARE. """ -import torch -import torchquantum as tq -from qiskit import pulse, QuantumCircuit -from qiskit.pulse import library -from qiskit.test.mock import FakeQuito, FakeArmonk, FakeBogota -from qiskit.compiler import assemble, schedule +from qiskit import pulse + from .qiskit_macros import IBMQ_PNAMES diff --git a/torchquantum/plugin/qiskit_pulse.py b/torchquantum/plugin/qiskit_pulse.py index 81775b0d..30a4b162 100644 --- a/torchquantum/plugin/qiskit_pulse.py +++ b/torchquantum/plugin/qiskit_pulse.py @@ -1,10 +1,6 @@ -import torch -import torchquantum as tq -from qiskit import pulse, QuantumCircuit -from qiskit.pulse import library -from qiskit.test.mock import FakeQuito, FakeArmonk, FakeBogota -from qiskit.compiler import assemble, schedule -from .qiskit_macros import IBMQ_PNAMES +from qiskit import pulse + +from .qiskit.qiskit_macros import IBMQ_PNAMES def circ2pulse(circuits, name): @@ -24,7 +20,7 @@ def circ2pulse(circuits, name): >>> qc.cx(0, 1) >>> circ2pulse(qc, 'ibmq_oslo') """ - + if name in IBMQ_PNAMES: backend = name() with pulse.build(backend) as pulse_tq: diff --git a/torchquantum/pulse/pulse_utils.py b/torchquantum/pulse/pulse_utils.py index 68c66568..51803ab0 100644 --- a/torchquantum/pulse/pulse_utils.py +++ b/torchquantum/pulse/pulse_utils.py @@ -23,55 +23,30 @@ """ import copy -import sched -import qiskit -import itertools -import numpy as np +from typing import Union -from itertools import repeat -from qiskit.providers import aer -from qiskit.providers.fake_provider import * -from qiskit.circuit import Gate +import numpy as np +import qiskit +from qiskit import QuantumCircuit, pulse from qiskit.compiler import assemble -from qiskit import pulse, QuantumCircuit, IBMQ +from qiskit.providers.fake_provider import * +from qiskit.pulse import Schedule from qiskit.pulse.instructions import Instruction from qiskit.pulse.transforms import block_to_schedule -from qiskit_nature.drivers import UnitsType, Molecule -from scipy.optimize import minimize, LinearConstraint -from qiskit_nature.converters.second_quantization import QubitConverter -from qiskit_nature.properties.second_quantization.electronic import ParticleNumber -from qiskit_nature.problems.second_quantization import ElectronicStructureProblem -from typing import List, Tuple, Iterable, Union, Dict, Callable, Set, Optional, Any -from qiskit.pulse import ( - Schedule, - GaussianSquare, - Drag, - Delay, - Play, - ControlChannel, - DriveChannel, -) -from qiskit_nature.mappers.second_quantization import ParityMapper, JordanWignerMapper -from qiskit_nature.transformers.second_quantization.electronic import ( - ActiveSpaceTransformer, -) -from qiskit_nature.drivers.second_quantization import ( - ElectronicStructureDriverType, - ElectronicStructureMoleculeDriver, -) +from scipy.optimize import LinearConstraint def is_parametric_pulse(t0, *inst: Union["Schedule", Instruction]): """ - Check if the instruction is a parametric pulse. + Check if the instruction is a parametric pulse. - Args: - t0 (tuple): Tuple containing the time and instruction. - inst (tuple): Tuple containing the instruction. + Args: + t0 (tuple): Tuple containing the time and instruction. + inst (tuple): Tuple containing the instruction. - Returns: - bool: True if the instruction is a parametric pulse, False otherwise. - """ + Returns: + bool: True if the instruction is a parametric pulse, False otherwise. + """ inst = t0[1] t0 = t0[0] if isinstance(inst, pulse.Play): @@ -82,14 +57,14 @@ def is_parametric_pulse(t0, *inst: Union["Schedule", Instruction]): def extract_ampreal(pulse_prog): """ - Extract the real part of pulse amplitudes from the pulse program. + Extract the real part of pulse amplitudes from the pulse program. - Args: - pulse_prog (Schedule): The pulse program. + Args: + pulse_prog (Schedule): The pulse program. - Returns: - np.array: Array of real parts of pulse amplitudes. - """ + Returns: + np.array: Array of real parts of pulse amplitudes. + """ # extract the real part of pulse amplitude, igonred the imaginary part. amp_list = list( map( @@ -104,14 +79,14 @@ def extract_ampreal(pulse_prog): def extract_amp(pulse_prog): """ - Extract the pulse amplitudes from the pulse program. + Extract the pulse amplitudes from the pulse program. - Args: - pulse_prog (Schedule): The pulse program. + Args: + pulse_prog (Schedule): The pulse program. - Returns: - np.array: Array of pulse amplitudes. - """ + Returns: + np.array: Array of pulse amplitudes. + """ # extract the pulse amplitdue. amp_list = list( map( @@ -132,15 +107,15 @@ def extract_amp(pulse_prog): def is_phase_pulse(t0, *inst: Union["Schedule", Instruction]): """ - Check if the instruction is a phase pulse. + Check if the instruction is a phase pulse. - Args: - t0 (tuple): Tuple containing the time and instruction. - inst (tuple): Tuple containing the instruction. + Args: + t0 (tuple): Tuple containing the time and instruction. + inst (tuple): Tuple containing the instruction. - Returns: - bool: True if the instruction is a phase pulse, False otherwise. - """ + Returns: + bool: True if the instruction is a phase pulse, False otherwise. + """ inst = t0[1] t0 = t0[0] if isinstance(inst, pulse.ShiftPhase): @@ -150,14 +125,14 @@ def is_phase_pulse(t0, *inst: Union["Schedule", Instruction]): def extract_phase(pulse_prog): """ - Extract the phase values from the pulse program. + Extract the phase values from the pulse program. - Args: - pulse_prog (Schedule): The pulse program. + Args: + pulse_prog (Schedule): The pulse program. - Returns: - list: List of phase values. - """ + Returns: + list: List of phase values. + """ for _, ShiftPhase in pulse_prog.filter(is_phase_pulse).instructions: # print(play.pulse.amp) @@ -175,15 +150,15 @@ def extract_phase(pulse_prog): def cir2pul(circuit, backend): """ - Transform a quantum circuit to a pulse schedule. + Transform a quantum circuit to a pulse schedule. - Args: - circuit (QuantumCircuit): The quantum circuit. - backend: The backend for the pulse schedule. + Args: + circuit (QuantumCircuit): The quantum circuit. + backend: The backend for the pulse schedule. - Returns: - Schedule: The pulse schedule. - """ + Returns: + Schedule: The pulse schedule. + """ # transform quantum circuit to pulse schedule with pulse.build(backend) as pulse_prog: pulse.call(circuit) @@ -192,15 +167,15 @@ def cir2pul(circuit, backend): def snp(qubit, backend): """ - Create a Schedule for the simultaneous z measurement of a qubit and a control qubit. + Create a Schedule for the simultaneous z measurement of a qubit and a control qubit. - Args: - qubit (int): The target qubit. - backend: The backend for the pulse schedule. + Args: + qubit (int): The target qubit. + backend: The backend for the pulse schedule. - Returns: - Schedule: The pulse schedule for simultaneous z measurement. - """ + Returns: + Schedule: The pulse schedule for simultaneous z measurement. + """ circuit = QuantumCircuit(qubit + 1) circuit.h(qubit) sched = cir2pul(circuit, backend) @@ -210,16 +185,16 @@ def snp(qubit, backend): def tnp(qubit, cqubit, backend): """ - Create a Schedule for the simultaneous controlled-x measurement of a qubit and a control qubit. + Create a Schedule for the simultaneous controlled-x measurement of a qubit and a control qubit. - Args: - qubit (int): The target qubit. - cqubit (int): The control qubit. - backend: The backend for the pulse schedule. + Args: + qubit (int): The target qubit. + cqubit (int): The control qubit. + backend: The backend for the pulse schedule. - Returns: - Schedule: The pulse schedule for simultaneous controlled-x measurement. - """ + Returns: + Schedule: The pulse schedule for simultaneous controlled-x measurement. + """ circuit = QuantumCircuit(cqubit + 1) circuit.cx(qubit, cqubit) sched = cir2pul(circuit, backend) @@ -229,30 +204,30 @@ def tnp(qubit, cqubit, backend): def pul_append(sched1, sched2): """ - Append two pulse schedules. + Append two pulse schedules. - Args: - sched1 (Schedule): The first pulse schedule. - sched2 (Schedule): The second pulse schedule. + Args: + sched1 (Schedule): The first pulse schedule. + sched2 (Schedule): The second pulse schedule. - Returns: - Schedule: The combined pulse schedule. - """ + Returns: + Schedule: The combined pulse schedule. + """ sched = sched1.append(sched2) return sched def map_amp(pulse_ansatz, modified_list): """ - Map modified pulse amplitudes to the pulse ansatz. + Map modified pulse amplitudes to the pulse ansatz. - Args: - pulse_ansatz (Schedule): The pulse ansatz. - modified_list (list): List of modified pulse amplitudes. + Args: + pulse_ansatz (Schedule): The pulse ansatz. + modified_list (list): List of modified pulse amplitudes. - Returns: - Schedule: The pulse schedule with modified amplitudes. - """ + Returns: + Schedule: The pulse schedule with modified amplitudes. + """ sched = Schedule() for inst, amp in zip( pulse_ansatz.filter(is_parametric_pulse).instructions, modified_list @@ -274,18 +249,18 @@ def get_from(d: dict, key: str): def run_pulse_sim(measurement_pulse): """ - Run pulse simulations for the given measurement pulses. + Run pulse simulations for the given measurement pulses. - Args: - measurement_pulse (list): List of measurement pulses. + Args: + measurement_pulse (list): List of measurement pulses. - Returns: - list: List of measurement results. - """ + Returns: + list: List of measurement results. + """ measure_result = [] for measure_pulse in measurement_pulse: shots = 1024 - pulse_sim = qiskit.providers.aer.PulseSimulator.from_backend(FakeJakarta()) + pulse_sim = qiskit_aer.PulseSimulator.from_backend(FakeJakarta()) pul_sim = assemble( measure_pulse, backend=pulse_sim, @@ -306,14 +281,14 @@ def run_pulse_sim(measurement_pulse): def gen_LC(parameters_array): """ - Generate linear constraints for the optimization. + Generate linear constraints for the optimization. - Args: - parameters_array (np.array): Array of parameters. + Args: + parameters_array (np.array): Array of parameters. - Returns: - LinearConstraint: Linear constraint for the optimization. - """ + Returns: + LinearConstraint: Linear constraint for the optimization. + """ dim_design = int(len(parameters_array)) Mid = int(len(parameters_array) / 2) bound = np.ones((dim_design, 2)) * np.array([0, 0.9]) @@ -327,15 +302,15 @@ def gen_LC(parameters_array): def observe_genearte(pulse_ansatz, backend): """ - Generate measurement pulses for observing the pulse ansatz. + Generate measurement pulses for observing the pulse ansatz. - Args: - pulse_ansatz (Schedule): The pulse ansatz. - backend: The backend for the pulse schedule. + Args: + pulse_ansatz (Schedule): The pulse ansatz. + backend: The backend for the pulse schedule. - Returns: - list: List of measurement pulses. - """ + Returns: + list: List of measurement pulses. + """ qubits = 0, 1 with pulse.build(backend) as pulse_measurez0: # z measurement of qubit 0 and 1 diff --git a/torchquantum/pulse/templates/pulse_utils.py b/torchquantum/pulse/templates/pulse_utils.py index bad2a9b5..30d4c2f7 100644 --- a/torchquantum/pulse/templates/pulse_utils.py +++ b/torchquantum/pulse/templates/pulse_utils.py @@ -1,40 +1,15 @@ import copy -import sched -import qiskit -import itertools -import numpy as np +from typing import Union -from itertools import repeat -from qiskit.providers import aer -from qiskit.providers.fake_provider import * -from qiskit.circuit import Gate +import numpy as np +import qiskit +from qiskit import QuantumCircuit, pulse from qiskit.compiler import assemble -from qiskit import pulse, QuantumCircuit, IBMQ +from qiskit.providers.fake_provider import * +from qiskit.pulse import Schedule from qiskit.pulse.instructions import Instruction from qiskit.pulse.transforms import block_to_schedule -from qiskit_nature.drivers import UnitsType, Molecule -from scipy.optimize import minimize, LinearConstraint -from qiskit_nature.converters.second_quantization import QubitConverter -from qiskit_nature.properties.second_quantization.electronic import ParticleNumber -from qiskit_nature.problems.second_quantization import ElectronicStructureProblem -from typing import List, Tuple, Iterable, Union, Dict, Callable, Set, Optional, Any -from qiskit.pulse import ( - Schedule, - GaussianSquare, - Drag, - Delay, - Play, - ControlChannel, - DriveChannel, -) -from qiskit_nature.mappers.second_quantization import ParityMapper, JordanWignerMapper -from qiskit_nature.transformers.second_quantization.electronic import ( - ActiveSpaceTransformer, -) -from qiskit_nature.drivers.second_quantization import ( - ElectronicStructureDriverType, - ElectronicStructureMoleculeDriver, -) +from scipy.optimize import LinearConstraint def is_parametric_pulse(t0, *inst: Union["Schedule", Instruction]): @@ -154,7 +129,7 @@ def run_pulse_sim(measurement_pulse): measure_result = [] for measure_pulse in measurement_pulse: shots = 1024 - pulse_sim = qiskit.providers.aer.PulseSimulator.from_backend(FakeJakarta()) + pulse_sim = qiskit_aer.PulseSimulator.from_backend(FakeJakarta()) pul_sim = assemble( measure_pulse, backend=pulse_sim, diff --git a/torchquantum/util/utils.py b/torchquantum/util/utils.py index caeee471..6a714bb9 100644 --- a/torchquantum/util/utils.py +++ b/torchquantum/util/utils.py @@ -23,26 +23,25 @@ """ import copy -from typing import Dict, Iterable, List, TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Iterable, List import numpy as np import torch import torch.nn as nn import torch.nn.functional as F from opt_einsum import contract -from qiskit_ibm_runtime import QiskitRuntimeService from qiskit.exceptions import QiskitError -from qiskit.providers.aer.noise.device.parameters import gate_error_values +from qiskit_aer.noise.device.parameters import gate_error_values +from qiskit_ibm_runtime import QiskitRuntimeService from torchpack.utils.config import Config from torchpack.utils.logging import logger import torchquantum as tq from torchquantum.macro import C_DTYPE - if TYPE_CHECKING: - from torchquantum.module import QuantumModule from torchquantum.device import QuantumDevice + from torchquantum.module import QuantumModule else: QuantumModule = None QuantumDevice = None @@ -98,14 +97,14 @@ def pauli_eigs(n) -> np.ndarray: def diag(x): """ - Compute the diagonal matrix from a given input tensor. + Compute the diagonal matrix from a given input tensor. - Args: - x (torch.Tensor): Input tensor. + Args: + x (torch.Tensor): Input tensor. - Returns: - torch.Tensor: Diagonal matrix with the diagonal elements from the input tensor. - """ + Returns: + torch.Tensor: Diagonal matrix with the diagonal elements from the input tensor. + """ # input tensor, output tensor with diagonal as the input # manual implementation because torch.diag does not support autograd of # complex number @@ -120,20 +119,21 @@ def diag(x): class Timer(object): """ - Timer class to measure the execution time of a code block. + Timer class to measure the execution time of a code block. + + Args: + device (str): Device to use for timing. Can be "gpu" or "cpu". + name (str): Name of the task being measured. + times (int): Number of times the task will be executed. - Args: - device (str): Device to use for timing. Can be "gpu" or "cpu". - name (str): Name of the task being measured. - times (int): Number of times the task will be executed. + Example: + # Measure the execution time of a code block on the GPU + with Timer(device="gpu", name="MyTask", times=100): + # Code block to be measured + ... - Example: - # Measure the execution time of a code block on the GPU - with Timer(device="gpu", name="MyTask", times=100): - # Code block to be measured - ... + """ - """ def __init__(self, device="gpu", name="", times=100): self.device = device self.name = name @@ -158,20 +158,20 @@ def __exit__(self, exc_type, exc_value, tb): def get_unitary_loss(model: nn.Module): """ - Calculate the unitary loss of a model. + Calculate the unitary loss of a model. - The unitary loss measures the deviation of the trainable unitary matrices - in the model from the identity matrix. + The unitary loss measures the deviation of the trainable unitary matrices + in the model from the identity matrix. - Args: - model (nn.Module): The model containing trainable unitary matrices. + Args: + model (nn.Module): The model containing trainable unitary matrices. - Returns: - torch.Tensor: The unitary loss. + Returns: + torch.Tensor: The unitary loss. - Example: - loss = get_unitary_loss(model) - """ + Example: + loss = get_unitary_loss(model) + """ loss = 0 for name, params in model.named_parameters(): if "TrainableUnitary" in name: @@ -187,21 +187,21 @@ def get_unitary_loss(model: nn.Module): def legalize_unitary(model: nn.Module): """ - Legalize the unitary matrices in the model. + Legalize the unitary matrices in the model. - The function modifies the trainable unitary matrices in the model by applying - singular value decomposition (SVD) and reassembling the matrices using the - reconstructed singular values. + The function modifies the trainable unitary matrices in the model by applying + singular value decomposition (SVD) and reassembling the matrices using the + reconstructed singular values. - Args: - model (nn.Module): The model containing trainable unitary matrices. + Args: + model (nn.Module): The model containing trainable unitary matrices. - Returns: - None + Returns: + None - Example: - legalize_unitary(model) - """ + Example: + legalize_unitary(model) + """ with torch.no_grad(): for name, params in model.named_parameters(): if "TrainableUnitary" in name: @@ -212,22 +212,22 @@ def legalize_unitary(model: nn.Module): def switch_little_big_endian_matrix(mat): """ - Switches the little-endian and big-endian order of a multi-dimensional matrix. + Switches the little-endian and big-endian order of a multi-dimensional matrix. - The function reshapes the input matrix to a 2D or multi-dimensional matrix with dimensions - that are powers of 2. It then switches the order of the dimensions, effectively changing - the little-endian order to big-endian, or vice versa. The function can handle both - batched and non-batched matrices. + The function reshapes the input matrix to a 2D or multi-dimensional matrix with dimensions + that are powers of 2. It then switches the order of the dimensions, effectively changing + the little-endian order to big-endian, or vice versa. The function can handle both + batched and non-batched matrices. - Args: - mat (numpy.ndarray): The input matrix. + Args: + mat (numpy.ndarray): The input matrix. - Returns: - numpy.ndarray: The matrix with the switched endian order. + Returns: + numpy.ndarray: The matrix with the switched endian order. - Example: - switched_mat = switch_little_big_endian_matrix(mat) - """ + Example: + switched_mat = switch_little_big_endian_matrix(mat) + """ if len(mat.shape) % 2 == 1: is_batch_matrix = True bsz = mat.shape[0] @@ -251,25 +251,25 @@ def switch_little_big_endian_matrix(mat): def switch_little_big_endian_state(state): """ - Switches the little-endian and big-endian order of a quantum state vector. + Switches the little-endian and big-endian order of a quantum state vector. - The function reshapes the input state vector to a 1D or multi-dimensional state vector with - dimensions that are powers of 2. It then switches the order of the dimensions, effectively - changing the little-endian order to big-endian, or vice versa. The function can handle both - batched and non-batched state vectors. + The function reshapes the input state vector to a 1D or multi-dimensional state vector with + dimensions that are powers of 2. It then switches the order of the dimensions, effectively + changing the little-endian order to big-endian, or vice versa. The function can handle both + batched and non-batched state vectors. - Args: - state (numpy.ndarray): The input state vector. + Args: + state (numpy.ndarray): The input state vector. - Returns: - numpy.ndarray: The state vector with the switched endian order. + Returns: + numpy.ndarray: The state vector with the switched endian order. - Raises: - ValueError: If the dimension of the state vector is not 1 or 2. + Raises: + ValueError: If the dimension of the state vector is not 1 or 2. - Example: - switched_state = switch_little_big_endian_state(state) - """ + Example: + switched_state = switch_little_big_endian_state(state) + """ if len(state.shape) > 1: is_batch_state = True @@ -310,25 +310,25 @@ def switch_little_big_endian_state_test(): def get_expectations_from_counts(counts, n_wires): """ - Calculate expectation values from counts. + Calculate expectation values from counts. - This function takes a counts dictionary or a list of counts dictionaries - and calculates the expectation values based on the probability of measuring - the state '1' on each wire. The expectation values are computed as the - flipped difference between the probability of measuring '1' and the probability - of measuring '0' on each wire. + This function takes a counts dictionary or a list of counts dictionaries + and calculates the expectation values based on the probability of measuring + the state '1' on each wire. The expectation values are computed as the + flipped difference between the probability of measuring '1' and the probability + of measuring '0' on each wire. - Args: - counts (dict or list[dict]): The counts dictionary or a list of counts dictionaries. - n_wires (int): The number of wires. + Args: + counts (dict or list[dict]): The counts dictionary or a list of counts dictionaries. + n_wires (int): The number of wires. - Returns: - numpy.ndarray: The expectation values. + Returns: + numpy.ndarray: The expectation values. - Example: - counts = {'000': 10, '100': 5, '010': 15} - expectations = get_expectations_from_counts(counts, 3) - """ + Example: + counts = {'000': 10, '100': 5, '010': 15} + expectations = get_expectations_from_counts(counts, 3) + """ exps = [] if isinstance(counts, dict): counts = [counts] @@ -349,29 +349,29 @@ def get_expectations_from_counts(counts, n_wires): def find_global_phase(mat1, mat2, threshold): """ - Find a numerical stable global phase between two matrices. - - This function compares the elements of two matrices `mat1` and `mat2` - and identifies a numerical stable global phase by finding the first - non-zero element pair with absolute values greater than the specified - threshold. The global phase is calculated as the ratio of the corresponding - elements in `mat2` and `mat1`. - - Args: - mat1 (numpy.ndarray): The first matrix. - mat2 (numpy.ndarray): The second matrix. - threshold (float): The threshold for identifying non-zero elements. - - Returns: - float or None: The global phase ratio if a numerical stable phase is found, - None otherwise. - - Example: - mat1 = np.array([[1+2j, 0+1j], [0-1j, 2+3j]]) - mat2 = np.array([[2+4j, 0+2j], [0-2j, 4+6j]]) - threshold = 0.5 - global_phase = find_global_phase(mat1, mat2, threshold) - """ + Find a numerical stable global phase between two matrices. + + This function compares the elements of two matrices `mat1` and `mat2` + and identifies a numerical stable global phase by finding the first + non-zero element pair with absolute values greater than the specified + threshold. The global phase is calculated as the ratio of the corresponding + elements in `mat2` and `mat1`. + + Args: + mat1 (numpy.ndarray): The first matrix. + mat2 (numpy.ndarray): The second matrix. + threshold (float): The threshold for identifying non-zero elements. + + Returns: + float or None: The global phase ratio if a numerical stable phase is found, + None otherwise. + + Example: + mat1 = np.array([[1+2j, 0+1j], [0-1j, 2+3j]]) + mat2 = np.array([[2+4j, 0+2j], [0-2j, 4+6j]]) + threshold = 0.5 + global_phase = find_global_phase(mat1, mat2, threshold) + """ for i in range(mat1.shape[0]): for j in range(mat1.shape[1]): # find a numerical stable global phase @@ -438,35 +438,35 @@ def build_module_from_op_list( op_list: List[Dict], remove_ops=False, thres=None ) -> QuantumModule: """ - Build a quantum module from an operation list. - - This function takes an operation list, which contains dictionaries representing - quantum operations, and constructs a quantum module from those operations. - The module can optionally remove operations based on certain criteria, such as - low parameter values. The removed operations can be counted and logged. - - Args: - op_list (List[Dict]): The operation list, where each dictionary represents - an operation with keys: "name", "has_params", "trainable", "wires", - "n_wires", and "params". - remove_ops (bool): Whether to remove operations based on certain criteria. - Defaults to False. - thres (float): The threshold for removing operations. If a parameter value - is smaller in absolute value than this threshold, the corresponding - operation is removed. Defaults to None, in which case a threshold of - 1e-5 is used. - - Returns: - QuantumModule: The constructed quantum module. - - Example: - op_list = [ - {"name": "RX", "has_params": True, "trainable": True, "wires": [0], "n_wires": 2, "params": [0.5]}, - {"name": "CNOT", "has_params": False, "trainable": False, "wires": [0, 1], "n_wires": 2, "params": None}, - {"name": "RY", "has_params": True, "trainable": True, "wires": [1], "n_wires": 2, "params": [1.2]}, - ] - module = build_module_from_op_list(op_list, remove_ops=True, thres=0.1) - """ + Build a quantum module from an operation list. + + This function takes an operation list, which contains dictionaries representing + quantum operations, and constructs a quantum module from those operations. + The module can optionally remove operations based on certain criteria, such as + low parameter values. The removed operations can be counted and logged. + + Args: + op_list (List[Dict]): The operation list, where each dictionary represents + an operation with keys: "name", "has_params", "trainable", "wires", + "n_wires", and "params". + remove_ops (bool): Whether to remove operations based on certain criteria. + Defaults to False. + thres (float): The threshold for removing operations. If a parameter value + is smaller in absolute value than this threshold, the corresponding + operation is removed. Defaults to None, in which case a threshold of + 1e-5 is used. + + Returns: + QuantumModule: The constructed quantum module. + + Example: + op_list = [ + {"name": "RX", "has_params": True, "trainable": True, "wires": [0], "n_wires": 2, "params": [0.5]}, + {"name": "CNOT", "has_params": False, "trainable": False, "wires": [0, 1], "n_wires": 2, "params": None}, + {"name": "RY", "has_params": True, "trainable": True, "wires": [1], "n_wires": 2, "params": [1.2]}, + ] + module = build_module_from_op_list(op_list, remove_ops=True, thres=0.1) + """ logger.info(f"Building module from op_list...") thres = 1e-5 if thres is None else thres n_removed_ops = 0 @@ -506,31 +506,31 @@ def build_module_from_op_list( def build_module_description_test(): """ - Test function for building module descriptions. - - This function demonstrates the usage of `build_module_op_list` and `build_module_from_op_list` - functions to build module descriptions and create quantum modules from those descriptions. - - Example: - import pdb - from torchquantum.plugins import tq2qiskit - from examples.core.models.q_models import QFCModel12 - - pdb.set_trace() - q_model = QFCModel12({"n_blocks": 4}) - desc = build_module_op_list(q_model.q_layer) - print(desc) - q_dev = tq.QuantumDevice(n_wires=4) - m = build_module_from_op_list(desc) - tq2qiskit(q_dev, m, draw=True) - - desc = build_module_op_list( - tq.RandomLayerAllTypes(n_ops=200, wires=[0, 1, 2, 3], qiskit_compatible=True) - ) - print(desc) - m1 = build_module_from_op_list(desc) - tq2qiskit(q_dev, m1, draw=True) - """ + Test function for building module descriptions. + + This function demonstrates the usage of `build_module_op_list` and `build_module_from_op_list` + functions to build module descriptions and create quantum modules from those descriptions. + + Example: + import pdb + from torchquantum.plugins import tq2qiskit + from examples.core.models.q_models import QFCModel12 + + pdb.set_trace() + q_model = QFCModel12({"n_blocks": 4}) + desc = build_module_op_list(q_model.q_layer) + print(desc) + q_dev = tq.QuantumDevice(n_wires=4) + m = build_module_from_op_list(desc) + tq2qiskit(q_dev, m, draw=True) + + desc = build_module_op_list( + tq.RandomLayerAllTypes(n_ops=200, wires=[0, 1, 2, 3], qiskit_compatible=True) + ) + print(desc) + m1 = build_module_from_op_list(desc) + tq2qiskit(q_dev, m1, draw=True) + """ import pdb from torchquantum.plugin import tq2qiskit @@ -630,15 +630,15 @@ def get_v_c_reg_mapping(circ): def get_cared_configs(conf, mode) -> Config: """ - Get the relevant configurations based on the mode. + Get the relevant configurations based on the mode. - Args: - conf (Config): The configuration object. - mode (str): The mode indicating the desired configuration. + Args: + conf (Config): The configuration object. + mode (str): The mode indicating the desired configuration. - Returns: - Config: The modified configuration object with only the relevant configurations preserved. - """ + Returns: + Config: The modified configuration object with only the relevant configurations preserved. + """ conf = copy.deepcopy(conf) ignores = [ @@ -706,15 +706,15 @@ def get_cared_configs(conf, mode) -> Config: def get_success_rate(properties, transpiled_circ): """ - Estimate the success rate of a transpiled quantum circuit. + Estimate the success rate of a transpiled quantum circuit. - Args: - properties (list): List of gate error properties. - transpiled_circ (QuantumCircuit): The transpiled quantum circuit. + Args: + properties (list): List of gate error properties. + transpiled_circ (QuantumCircuit): The transpiled quantum circuit. - Returns: - float: The estimated success rate. - """ + Returns: + float: The estimated success rate. + """ # estimate the success rate according to the error rates of single and # two-qubit gates in transpiled circuits @@ -738,23 +738,28 @@ def get_success_rate(properties, transpiled_circ): return success_rate + def get_provider(backend_name, hub=None): """ - Get the provider object for a specific backend from IBM Quantum. + Get the provider object for a specific backend from IBM Quantum. - Args: - backend_name (str): Name of the backend. - hub (str): Optional hub name. + Args: + backend_name (str): Name of the backend. + hub (str): Optional hub name. - Returns: - IBMQProvider: The provider object. - """ + Returns: + IBMQProvider: The provider object. + """ # mass-inst-tech-1 or MIT-1 if backend_name in ["ibmq_casablanca", "ibmq_rome", "ibmq_bogota", "ibmq_jakarta"]: if hub == "mass" or hub is None: - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q-research/mass-inst-tech-1/main") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q-research/mass-inst-tech-1/main" + ) elif hub == "mit": - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q-research/MIT-1/main") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q-research/MIT-1/main" + ) else: raise ValueError(f"not supported backend {backend_name} in hub " f"{hub}") elif backend_name in [ @@ -764,38 +769,51 @@ def get_provider(backend_name, hub=None): "ibmq_guadalupe", "ibmq_montreal", ]: - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q-ornl/anl/csc428") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q-ornl/anl/csc428" + ) else: if hub == "mass" or hub is None: try: - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q-research/mass-inst-tech-1/main") + provider = QiskitRuntimeService( + channel="ibm_quantum", + instance="ibm-q-research/mass-inst-tech-1/main", + ) except QiskitError: # logger.warning(f"Cannot use MIT backend, roll back to open") logger.warning(f"Use the open backend") - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q/open/main") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q/open/main" + ) elif hub == "mit": - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q-research/MIT-1/main") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q-research/MIT-1/main" + ) else: - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q/open/main") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q/open/main" + ) return provider def get_provider_hub_group_project(hub="ibm-q", group="open", project="main"): - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = f"{hub}/{group}/{project}") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance=f"{hub}/{group}/{project}" + ) return provider def normalize_statevector(states): """ - Normalize a statevector to ensure the square magnitude of the statevector sums to 1. + Normalize a statevector to ensure the square magnitude of the statevector sums to 1. - Args: - states (torch.Tensor): The statevector tensor. + Args: + states (torch.Tensor): The statevector tensor. - Returns: - torch.Tensor: The normalized statevector tensor. - """ + Returns: + torch.Tensor: The normalized statevector tensor. + """ # make sure the square magnitude of statevector sum to 1 # states = states.contiguous() original_shape = states.shape @@ -957,22 +975,22 @@ def dm_to_mixture_of_state(dm: torch.Tensor, atol=1e-10): def partial_trace_test(): """ - Test function for performing partial trace on a quantum device. + Test function for performing partial trace on a quantum device. - This function demonstrates how to use the `partial_trace` function from `torchquantum.functional` - to perform partial trace on a quantum device. + This function demonstrates how to use the `partial_trace` function from `torchquantum.functional` + to perform partial trace on a quantum device. - The function applies Hadamard gate on the first qubit and a CNOT gate between the first and second qubits. - Then, it performs partial trace on the first qubit and converts the resulting density matrices into - mixtures of states. + The function applies Hadamard gate on the first qubit and a CNOT gate between the first and second qubits. + Then, it performs partial trace on the first qubit and converts the resulting density matrices into + mixtures of states. - Prints the resulting mixture of states. + Prints the resulting mixture of states. - Note: This function assumes that you have already imported the necessary modules and functions. + Note: This function assumes that you have already imported the necessary modules and functions. - Returns: - None - """ + Returns: + None + """ import torchquantum.functional as tqf n_wires = 4 @@ -987,7 +1005,8 @@ def partial_trace_test(): print(mixture) -def pauli_string_to_matrix(pauli: str, device=torch.device('cpu')) -> torch.Tensor: + +def pauli_string_to_matrix(pauli: str, device=torch.device("cpu")) -> torch.Tensor: mat_dict = { "paulix": torch.tensor([[0, 1], [1, 0]], dtype=C_DTYPE), "pauliy": torch.tensor([[0, -1j], [1j, 0]], dtype=C_DTYPE), @@ -1008,68 +1027,82 @@ def pauli_string_to_matrix(pauli: str, device=torch.device('cpu')) -> torch.Tens matrix = torch.kron(matrix, pauli_dict[op].to(device)) return matrix + if __name__ == "__main__": build_module_description_test() switch_little_big_endian_matrix_test() switch_little_big_endian_state_test() -def parameter_shift_gradient(model, input_data, expectation_operator, shift_rate=np.pi*0.5, shots=1024): - ''' - This function calculates the gradient of a parametrized circuit using the parameter shift rule to be fed into - a classical optimizer, its formula is given by - gradient for the ith parameter =( expectation_value(the_ith_parameter + shift_rate)-expectation_value(the_ith_parameter - shift_rate) ) *0.5 - Args: +def parameter_shift_gradient( + model, input_data, expectation_operator, shift_rate=np.pi * 0.5, shots=1024 +): + """ + This function calculates the gradient of a parametrized circuit using the parameter shift rule to be fed into + a classical optimizer, its formula is given by + gradient for the ith parameter =( expectation_value(the_ith_parameter + shift_rate)-expectation_value(the_ith_parameter - shift_rate) ) *0.5 + Args: model(tq.QuantumModule): the model that you want to use, which includes the quantum device and the parameters input(torch.tensor): the input data that you are using - expectation_operator(str): the observable that you want to calculate the expectation value of, usually the Z operator + expectation_operator(str): the observable that you want to calculate the expectation value of, usually the Z operator (i.e 'ZZZ' for 3 qubits or 3 wires) shift_rate(float , optional): the rate that you would like to shift the parameter with at every iteration, by default pi*0.5 shots(int , optional): the number of shots to use per parameter ,(for 10 parameters and 1024 shots = 10240 shots in total) by default = 1024. Returns: - torch.tensor : An array of the gradients of all the parameters in the circuit. - ''' + torch.tensor : An array of the gradients of all the parameters in the circuit. + """ par_num = [] - for p in model.parameters():#since the model.parameters() Returns an iterator over module parameters,to get the number of parameter i have to iterate over all of them + for ( + p + ) in ( + model.parameters() + ): # since the model.parameters() Returns an iterator over module parameters,to get the number of parameter i have to iterate over all of them par_num.append(p) gradient_of_par = torch.zeros(len(par_num)) - - def clone_model(model_to_clone):#i have to note:this clone_model function was made with GPT + + def clone_model( + model_to_clone, + ): # i have to note:this clone_model function was made with GPT cloned_model = type(model_to_clone)() # Create a new instance of the same class - cloned_model.load_state_dict(model_to_clone.state_dict()) # Copy the state dictionary + cloned_model.load_state_dict( + model_to_clone.state_dict() + ) # Copy the state dictionary return cloned_model # Clone the models - model_plus_shift = clone_model(model) + model_plus_shift = clone_model(model) model_minus_shift = clone_model(model) - state_dict_plus_shift = model_plus_shift.state_dict() + state_dict_plus_shift = model_plus_shift.state_dict() state_dict_minus_shift = model_minus_shift.state_dict() ##################### for idx, key in enumerate(state_dict_plus_shift): if idx < 2: # Skip the first two keys because they are not paramters continue - state_dict_plus_shift[key] += shift_rate - state_dict_minus_shift[key] -= shift_rate - - model_plus_shift.load_state_dict(state_dict_plus_shift ) + state_dict_plus_shift[key] += shift_rate + state_dict_minus_shift[key] -= shift_rate + + model_plus_shift.load_state_dict(state_dict_plus_shift) model_minus_shift.load_state_dict(state_dict_minus_shift) - + model_plus_shift.forward(input_data) model_minus_shift.forward(input_data) - - state_dict_plus_shift = model_plus_shift.state_dict() + + state_dict_plus_shift = model_plus_shift.state_dict() state_dict_minus_shift = model_minus_shift.state_dict() - - - - expectation_plus_shift = tq.expval_joint_sampling(model_plus_shift.q_device, observable=expectation_operator, n_shots=shots) - expectation_minus_shift = tq.expval_joint_sampling(model_minus_shift.q_device, observable=expectation_operator, n_shots=shots) + expectation_plus_shift = tq.expval_joint_sampling( + model_plus_shift.q_device, observable=expectation_operator, n_shots=shots + ) + expectation_minus_shift = tq.expval_joint_sampling( + model_minus_shift.q_device, observable=expectation_operator, n_shots=shots + ) + + state_dict_plus_shift[key] -= shift_rate + state_dict_minus_shift[key] += shift_rate - state_dict_plus_shift[key] -= shift_rate - state_dict_minus_shift[key] += shift_rate - - gradient_of_par[idx-2] = (expectation_plus_shift - expectation_minus_shift) * 0.5 + gradient_of_par[idx - 2] = ( + expectation_plus_shift - expectation_minus_shift + ) * 0.5 return gradient_of_par From 36e3f5e9365bd9fcc89c931c0f5a6d62ed5d8343 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Wed, 29 May 2024 15:58:30 +0900 Subject: [PATCH 30/54] fix: fix dep warns --- test/measurement/test_eval_observable.py | 41 +++++------ test/plugin/test_qiskit2tq_op_history.py | 6 +- test/plugin/test_qiskit_plugins.py | 17 +++-- torchquantum/functional/gate_wrapper.py | 64 ++++++++--------- torchquantum/plugin/qiskit/qiskit_plugin.py | 69 +++++++++++-------- .../plugin/qiskit/qiskit_processor.py | 22 +++--- .../plugin/qiskit/qiskit_unitary_gate.py | 20 +++--- 7 files changed, 126 insertions(+), 113 deletions(-) diff --git a/test/measurement/test_eval_observable.py b/test/measurement/test_eval_observable.py index 58245ee0..a5810e20 100644 --- a/test/measurement/test_eval_observable.py +++ b/test/measurement/test_eval_observable.py @@ -22,19 +22,21 @@ SOFTWARE. """ -from qiskit import QuantumCircuit -import numpy as np import random -from qiskit.opflow import StateFn, X, Y, Z, I -import torchquantum as tq +import numpy as np +from qiskit import QuantumCircuit +from qiskit.quantum_info import Pauli, Statevector +import torchquantum as tq from torchquantum.measurement import expval_joint_analytical, expval_joint_sampling from torchquantum.plugin import op_history2qiskit from torchquantum.util import switch_little_big_endian_state -import torch - +X = Pauli("X") +Y = Pauli("Y") +Z = Pauli("Z") +I = Pauli("I") pauli_str_op_dict = { "X": X, "Y": Y, @@ -67,14 +69,13 @@ def test_expval_observable(): for ob in obs[1:]: # note here the order is reversed because qiskit is in little endian operator = pauli_str_op_dict[ob] ^ operator - psi = StateFn(qiskit_circ) - psi_evaled = psi.eval()._primitive._data + psi = Statevector(qiskit_circ) state_tq = switch_little_big_endian_state( qdev.get_states_1d().detach().numpy() )[0] - assert np.allclose(psi_evaled, state_tq, atol=1e-5) + assert np.allclose(psi.data, state_tq, atol=1e-5) - expval_qiskit = (~psi @ operator @ psi).eval().real + expval_qiskit = psi.expectation_value(operator).real # print(expval_tq, expval_qiskit) assert np.isclose(expval_tq, expval_qiskit, atol=1e-5) if ( @@ -92,25 +93,25 @@ def util0(): qc.x(0) operator = Z ^ I - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() + psi = Statevector(qc) + expectation_value = psi.expectation_value(operator) print(expectation_value.real) # result: 1.0, means measurement result is 0, so Z is on qubit 1 operator = I ^ Z - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() + psi = Statevector(qc) + expectation_value = psi.expectation_value(operator) print(expectation_value.real) # result: -1.0 means measurement result is 1, so Z is on qubit 0 operator = I ^ I - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() + psi = Statevector(qc) + expectation_value = psi.expectation_value(operator) print(expectation_value.real) operator = Z ^ Z - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() + psi = Statevector(qc) + expectation_value = psi.expectation_value(operator) print(expectation_value.real) qc = QuantumCircuit(3) @@ -118,8 +119,8 @@ def util0(): qc.x(0) operator = I ^ I ^ Z - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() + psi = Statevector(qc) + expectation_value = psi.expectation_value(operator) print(expectation_value.real) diff --git a/test/plugin/test_qiskit2tq_op_history.py b/test/plugin/test_qiskit2tq_op_history.py index 67a67e80..b94fb7fd 100644 --- a/test/plugin/test_qiskit2tq_op_history.py +++ b/test/plugin/test_qiskit2tq_op_history.py @@ -22,11 +22,11 @@ SOFTWARE. """ -from torchquantum.plugin import qiskit2tq_op_history -import torchquantum as tq -from qiskit.circuit.random import random_circuit from qiskit import QuantumCircuit +import torchquantum as tq +from torchquantum.plugin import qiskit2tq_op_history + def test_qiskit2tp_op_history(): circ = QuantumCircuit(3, 3) diff --git a/test/plugin/test_qiskit_plugins.py b/test/plugin/test_qiskit_plugins.py index 634b521d..33d9c4d6 100644 --- a/test/plugin/test_qiskit_plugins.py +++ b/test/plugin/test_qiskit_plugins.py @@ -26,17 +26,17 @@ import numpy as np import pytest -from qiskit.opflow import I, StateFn, X, Y, Z +from qiskit.quantum_info import Pauli, Statevector import torchquantum as tq from torchquantum.plugin import QiskitProcessor, op_history2qiskit from torchquantum.util import switch_little_big_endian_state pauli_str_op_dict = { - "X": X, - "Y": Y, - "Z": Z, - "I": I, + "X": Pauli("X"), + "Y": Pauli("Y"), + "Z": Pauli("Z"), + "I": Pauli("I"), } @@ -65,14 +65,13 @@ def test_expval_observable(): for ob in obs[1:]: # note here the order is reversed because qiskit is in little endian operator = pauli_str_op_dict[ob] ^ operator - psi = StateFn(qiskit_circ) - psi_evaled = psi.eval()._primitive._data + psi = Statevector(qiskit_circ) state_tq = switch_little_big_endian_state( qdev.get_states_1d().detach().numpy() )[0] - assert np.allclose(psi_evaled, state_tq, atol=1e-5) + assert np.allclose(psi.data, state_tq, atol=1e-5) - expval_qiskit = (~psi @ operator @ psi).eval().real + expval_qiskit = psi.expectation_value(operator).real # print(expval_qiskit_processor, expval_qiskit) if ( n_wires <= 3 diff --git a/torchquantum/functional/gate_wrapper.py b/torchquantum/functional/gate_wrapper.py index f1383f2f..9796fa8f 100644 --- a/torchquantum/functional/gate_wrapper.py +++ b/torchquantum/functional/gate_wrapper.py @@ -1,16 +1,13 @@ import functools -import torch -import numpy as np +from typing import TYPE_CHECKING, Callable -from typing import Callable, Union, Optional, List, Dict, TYPE_CHECKING -from ..macro import C_DTYPE, F_DTYPE, ABC, ABC_ARRAY, INV_SQRT2 -from ..util.utils import pauli_eigs, diag -from torchpack.utils.logging import logger -from torchquantum.util import normalize_statevector +import numpy as np +import torch +from ..macro import ABC, ABC_ARRAY, C_DTYPE, F_DTYPE if TYPE_CHECKING: - from torchquantum.device import QuantumDevice, NoiseDevice + from torchquantum.device import QuantumDevice else: QuantumDevice = None @@ -59,7 +56,7 @@ def apply_unitary_einsum(state, mat, wires): # All affected indices will be summed over, so we need the same number # of new indices - new_indices = ABC[total_wires: total_wires + len(device_wires)] + new_indices = ABC[total_wires : total_wires + len(device_wires)] # The new indices of the state are given by the old ones with the # affected indices replaced by the new_indices @@ -189,7 +186,7 @@ def apply_unitary_density_einsum(density, mat, wires): # All affected indices will be summed over, so we need the same number # of new indices - new_indices = ABC[total_wires: total_wires + len(device_wires)] + new_indices = ABC[total_wires : total_wires + len(device_wires)] print("new_indices", new_indices) # The new indices of the state are given by the old ones with the @@ -216,7 +213,7 @@ def apply_unitary_density_einsum(density, mat, wires): new_density = torch.einsum(einsum_indices, mat, density) - """ + r""" Compute U \rho U^\dagger """ print("dagger") @@ -233,7 +230,7 @@ def apply_unitary_density_einsum(density, mat, wires): # All affected indices will be summed over, so we need the same number # of new indices - new_indices = ABC[total_wires: total_wires + len(device_wires)] + new_indices = ABC[total_wires : total_wires + len(device_wires)] print("new_indices", new_indices) # The new indices of the state are given by the old ones with the @@ -284,7 +281,9 @@ def apply_unitary_density_bmm(density, mat, wires): permute_to = permute_to[:1] + devices_dims + permute_to[1:] permute_back = list(np.argsort(permute_to)) original_shape = density.shape - permuted = density.permute(permute_to).reshape([original_shape[0], mat.shape[-1], -1]) + permuted = density.permute(permute_to).reshape( + [original_shape[0], mat.shape[-1], -1] + ) if len(mat.shape) > 2: # both matrix and state are in batch mode @@ -295,8 +294,8 @@ def apply_unitary_density_bmm(density, mat, wires): expand_shape = [bsz] + list(mat.shape) new_density = mat.expand(expand_shape).bmm(permuted) new_density = new_density.view(original_shape).permute(permute_back) - """ - Compute \rho U^\dagger + r""" + Compute \rho U^\dagger """ matdag = torch.conj(mat) matdag = matdag.type(C_DTYPE).to(density.device) @@ -307,7 +306,9 @@ def apply_unitary_density_bmm(density, mat, wires): del permute_to_dag[d] permute_to_dag = permute_to_dag + devices_dims_dag permute_back_dag = list(np.argsort(permute_to_dag)) - permuted_dag = new_density.permute(permute_to_dag).reshape([original_shape[0], -1, matdag.shape[0]]) + permuted_dag = new_density.permute(permute_to_dag).reshape( + [original_shape[0], -1, matdag.shape[0]] + ) if len(matdag.shape) > 2: # both matrix and state are in batch mode @@ -322,16 +323,16 @@ def apply_unitary_density_bmm(density, mat, wires): def gate_wrapper( - name, - mat, - method, - q_device: QuantumDevice, - wires, - params=None, - n_wires=None, - static=False, - parent_graph=None, - inverse=False, + name, + mat, + method, + q_device: QuantumDevice, + wires, + params=None, + n_wires=None, + static=False, + parent_graph=None, + inverse=False, ): """Perform the phaseshift gate. @@ -382,9 +383,11 @@ def gate_wrapper( { "name": name, # type: ignore "wires": np.array(wires).squeeze().tolist(), - "params": params.squeeze().detach().cpu().numpy().tolist() - if params is not None - else None, + "params": ( + params.squeeze().detach().cpu().numpy().tolist() + if params is not None + else None + ), "inverse": inverse, "trainable": params.requires_grad if params is not None else False, } @@ -431,7 +434,7 @@ def gate_wrapper( else: matrix = matrix.permute(1, 0) assert np.log2(matrix.shape[-1]) == len(wires) - if q_device.device_name=="noisedevice": + if q_device.device_name == "noisedevice": density = q_device.densities print(density.shape) if method == "einsum": @@ -444,4 +447,3 @@ def gate_wrapper( q_device.states = apply_unitary_einsum(state, matrix, wires) elif method == "bmm": q_device.states = apply_unitary_bmm(state, matrix, wires) - diff --git a/torchquantum/plugin/qiskit/qiskit_plugin.py b/torchquantum/plugin/qiskit/qiskit_plugin.py index defa620d..91bd6647 100644 --- a/torchquantum/plugin/qiskit/qiskit_plugin.py +++ b/torchquantum/plugin/qiskit/qiskit_plugin.py @@ -25,10 +25,12 @@ from typing import Iterable import numpy as np +import qiskit import qiskit.circuit.library.standard_gates as qiskit_gate import torch -from qiskit import Aer, ClassicalRegister, QuantumCircuit, execute +from qiskit import ClassicalRegister, QuantumCircuit from qiskit.circuit import Parameter +from qiskit_aer import AerSimulator from torchpack.utils.logging import logger import torchquantum as tq @@ -78,13 +80,15 @@ def qiskit2tq_op_history(circ): ops = [] for gate in circ.data: op_name = gate[0].name - wires = list(map(lambda x: x.index, gate[1])) + wires = [circ.find_bit(qb).index for qb in gate.qubits] wires = [p2v[wire] for wire in wires] # sometimes the gate.params is ParameterExpression class init_params = ( list(map(float, gate[0].params)) if len(gate[0].params) > 0 else None ) - print(op_name,) + print( + op_name, + ) if op_name in [ "h", @@ -103,12 +107,12 @@ def qiskit2tq_op_history(circ): ]: ops.append( { - "name": op_name, # type: ignore - "wires": np.array(wires), - "params": None, - "inverse": False, - "trainable": False, - } + "name": op_name, # type: ignore + "wires": np.array(wires), + "params": None, + "inverse": False, + "trainable": False, + } ) elif op_name in [ "rx", @@ -137,12 +141,13 @@ def qiskit2tq_op_history(circ): ]: ops.append( { - "name": op_name, # type: ignore - "wires": np.array(wires), - "params": init_params, - "inverse": False, - "trainable": True - }) + "name": op_name, # type: ignore + "wires": np.array(wires), + "params": init_params, + "inverse": False, + "trainable": True, + } + ) elif op_name in ["barrier", "measure"]: continue else: @@ -205,7 +210,10 @@ def append_parameterized_gate(func, circ, input_idx, params, wires): ) elif func == "u2": from qiskit.circuit.library import U2Gate - circ.append(U2Gate(phi=params[input_idx[0]], lam=params[input_idx[1]]), wires, []) + + circ.append( + U2Gate(phi=params[input_idx[0]], lam=params[input_idx[1]]), wires, [] + ) # circ.u2(phi=params[input_idx[0]], lam=params[input_idx[1]], qubit=wires[0]) elif func == "u3": circ.u( @@ -250,7 +258,7 @@ def append_fixed_gate(circ, func, params, wires, inverse): elif func == "sx": circ.sx(*wires) elif func in ["cnot", "cx"]: - circ.cnot(*wires) + circ.cx(*wires) elif func == "cz": circ.cz(*wires) elif func == "cy": @@ -296,6 +304,7 @@ def append_fixed_gate(circ, func, params, wires, inverse): circ.cu1(params, *wires) elif func == "u2": from qiskit.circuit.library import U2Gate + circ.append(U2Gate(phi=params[0], lam=params[1]), wires, []) # circ.u2(*list(params), *wires) elif func == "u3": @@ -534,7 +543,15 @@ def tq2qiskit( circ.cu1(module.params[0][0].item(), *module.wires) elif module.name == "U2": from qiskit.circuit.library import U2Gate - circ.append(U2Gate(phi=module.params[0].data.cpu().numpy()[0], lam=module.params[0].data.cpu().numpy()[0]), module.wires, []) + + circ.append( + U2Gate( + phi=module.params[0].data.cpu().numpy()[0], + lam=module.params[0].data.cpu().numpy()[0], + ), + module.wires, + [], + ) # circ.u2(*list(module.params[0].data.cpu().numpy()), *module.wires) elif module.name == "U3": circ.u3(*list(module.params[0].data.cpu().numpy()), *module.wires) @@ -665,9 +682,7 @@ def op_history2qiskit_expand_params(n_wires, op_history, bsz): else: param = None - append_fixed_gate( - circ, op["name"], param, op["wires"], op["inverse"] - ) + append_fixed_gate(circ, op["name"], param, op["wires"], op["inverse"]) circs_all.append(circ) @@ -800,8 +815,8 @@ def test_qiskit2tq(): m = qiskit2tq(circ) - simulator = Aer.get_backend("unitary_simulator") - result = execute(circ, simulator).result() + simulator = AerSimulator(method="unitary_simulator") + result = simulator.run(circ).result() unitary_qiskit = result.get_unitary(circ) unitary_tq = m.get_unitary(q_dev) @@ -965,8 +980,8 @@ def test_tq2qiskit(): circuit = tq2qiskit(test_module, inputs) - simulator = Aer.get_backend("unitary_simulator") - result = execute(circuit, simulator).result() + simulator = AerSimulator(method="unitary_simulator") + result = simulator.run(circuit).result() unitary_qiskit = result.get_unitary(circuit) unitary_tq = test_module.get_unitary(q_dev, inputs) @@ -993,8 +1008,8 @@ def test_tq2qiskit_parameterized(): for k, x in enumerate(inputs[0]): binds[params[k]] = x.item() - simulator = Aer.get_backend("unitary_simulator") - result = execute(circuit, simulator, parameter_binds=[binds]).result() + simulator = AerSimulator(method="unitary_simulator") + result = simulator.run(circuit, parameter_binds=[binds]).result() unitary_qiskit = result.get_unitary(circuit) # print(unitary_qiskit) diff --git a/torchquantum/plugin/qiskit/qiskit_processor.py b/torchquantum/plugin/qiskit/qiskit_processor.py index d24ce521..252be251 100644 --- a/torchquantum/plugin/qiskit/qiskit_processor.py +++ b/torchquantum/plugin/qiskit/qiskit_processor.py @@ -28,10 +28,11 @@ import numpy as np import pathos.multiprocessing as multiprocessing import torch -from qiskit import IBMQ, Aer, QuantumCircuit, execute, transpile +from qiskit import IBMQ, QuantumCircuit, execute, transpile from qiskit.exceptions import QiskitError from qiskit.tools.monitor import job_monitor from qiskit.transpiler import PassManager +from qiskit_aer import AerSimulator from qiskit_aer.noise import NoiseModel from torchpack.utils.logging import logger from tqdm import tqdm @@ -40,7 +41,6 @@ from torchquantum.util import ( get_circ_stats, get_expectations_from_counts, - get_provider, get_provider_hub_group_project, ) @@ -56,7 +56,7 @@ def run(self, circuits, output_name: str = None, callback=None): def run_job_worker(data): while True: try: - job = execute(**(data[0])) + job = AerSimulator(**(data[0])) qiskit_verbose = data[1] if qiskit_verbose: job_monitor(job, interval=1) @@ -199,8 +199,8 @@ def qiskit_init(self): self.coupling_map = self.get_coupling_map(self.backend_name) else: # use simulator - self.backend = Aer.get_backend( - "qasm_simulator", max_parallel_experiments=0 + self.backend = AerSimulator( + method="qasm_simulator", max_parallel_experiments=0 ) self.noise_model = self.get_noise_model(self.noise_model_name) self.coupling_map = self.get_coupling_map(self.coupling_map_name) @@ -341,9 +341,8 @@ def process_parameterized( results[-1] = [results[-1]] counts = list(itertools.chain(*results)) else: - job = execute( + job = self.backend.run( experiments=transpiled_circ, - backend=self.backend, pass_manager=self.empty_pass_manager, shots=self.n_shots, seed_simulator=self.seed_simulator, @@ -529,9 +528,8 @@ def process_parameterized_and_shift( for circ in split_circs: while True: try: - job = execute( + job = self.backend.run( experiments=circ, - backend=self.backend, pass_manager=self.empty_pass_manager, shots=self.n_shots, seed_simulator=self.seed_simulator, @@ -657,9 +655,8 @@ def process( transpiled_circs = self.transpile(circs) self.transpiled_circs = transpiled_circs - job = execute( + job = self.backend.run( experiments=transpiled_circs, - backend=self.backend, shots=self.n_shots, # initial_layout=self.initial_layout, seed_transpiler=self.seed_transpiler, @@ -724,9 +721,8 @@ def process_ready_circs_get_counts(self, circs_all, parallel=True): results[-1] = [results[-1]] counts = list(itertools.chain(*results)) else: - job = execute( + job = self.backend.run( experiments=circs_all, - backend=self.backend, pass_manager=self.empty_pass_manager, shots=self.n_shots, seed_simulator=self.seed_simulator, diff --git a/torchquantum/plugin/qiskit/qiskit_unitary_gate.py b/torchquantum/plugin/qiskit/qiskit_unitary_gate.py index ce46ff04..630d4751 100644 --- a/torchquantum/plugin/qiskit/qiskit_unitary_gate.py +++ b/torchquantum/plugin/qiskit/qiskit_unitary_gate.py @@ -15,19 +15,17 @@ """ from collections import OrderedDict -import numpy -from qiskit.circuit import Gate, ControlledGate -from qiskit.circuit import QuantumCircuit -from qiskit.circuit import QuantumRegister, Qubit -from qiskit.circuit.exceptions import CircuitError +import numpy +import qiskit +from qiskit.circuit import ControlledGate, Gate, QuantumCircuit, QuantumRegister, Qubit from qiskit.circuit._utils import _compute_control_matrix +from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.library.standard_gates import U3Gate -from qiskit.quantum_info.operators.predicates import matrix_equal -from qiskit.quantum_info.operators.predicates import is_unitary_matrix -from qiskit.quantum_info import OneQubitEulerDecomposer -from qiskit.quantum_info.synthesis.two_qubit_decompose import two_qubit_cnot_decompose from qiskit.extensions.exceptions import ExtensionError +from qiskit.quantum_info.operators.predicates import is_unitary_matrix, matrix_equal +from qiskit.quantum_info.synthesis.two_qubit_decompose import two_qubit_cnot_decompose +from qiskit.synthesis import OneQubitEulerDecomposer _DECOMPOSER1Q = OneQubitEulerDecomposer("U3") @@ -116,7 +114,9 @@ def _define(self): else: q = QuantumRegister(self.num_qubits, "q") qc = QuantumCircuit(q, name=self.name) - qc.append(qiskit.circuit.library.Isometry(self.to_matrix(), 0, 0), qargs=q[:]) + qc.append( + qiskit.circuit.library.Isometry(self.to_matrix(), 0, 0), qargs=q[:] + ) self.definition = qc def control(self, num_ctrl_qubits=1, label=None, ctrl_state=None): From 33d0e94c6c6faf2f9eae644adc8f583136f8e7e5 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Wed, 29 May 2024 16:25:53 +0900 Subject: [PATCH 31/54] fix: fix more depwarns and remove job_monitor --- test/functional/test_controlled_unitary.py | 4 ++- test/functional/test_func_mat_exp.py | 3 ++- test/layers/test_nlocal.py | 15 ++++++----- test/layers/test_rotgate.py | 26 +++++++++---------- test/operator/test_ControlledU.py | 3 +-- .../functional/func_controlled_unitary.py | 3 ++- .../operator/standard_gates/qubit_unitary.py | 13 +++++----- torchquantum/plugin/qiskit/qiskit_plugin.py | 21 ++++++++++----- .../plugin/qiskit/qiskit_processor.py | 22 +++++----------- .../plugin/qiskit/qiskit_unitary_gate.py | 2 +- torchquantum/util/utils.py | 12 ++++++--- 11 files changed, 66 insertions(+), 58 deletions(-) diff --git a/test/functional/test_controlled_unitary.py b/test/functional/test_controlled_unitary.py index 652ece59..d7e78660 100644 --- a/test/functional/test_controlled_unitary.py +++ b/test/functional/test_controlled_unitary.py @@ -22,10 +22,12 @@ SOFTWARE. """ -import torchquantum as tq from test.utils import check_all_close + import numpy as np +import torchquantum as tq + def test_controlled_unitary(): state = tq.QuantumDevice(n_wires=2) diff --git a/test/functional/test_func_mat_exp.py b/test/functional/test_func_mat_exp.py index e2a2c293..ad8e17c1 100644 --- a/test/functional/test_func_mat_exp.py +++ b/test/functional/test_func_mat_exp.py @@ -22,9 +22,10 @@ SOFTWARE. """ +import numpy as np import torch + import torchquantum as tq -import numpy as np def test_func_mat_exp(): diff --git a/test/layers/test_nlocal.py b/test/layers/test_nlocal.py index 62387190..83bd1f6e 100644 --- a/test/layers/test_nlocal.py +++ b/test/layers/test_nlocal.py @@ -1,12 +1,13 @@ -import torchquantum as tq from qiskit.circuit.library import ( - TwoLocal, EfficientSU2, ExcitationPreserving, PauliTwoDesign, RealAmplitudes, + TwoLocal, ) +import torchquantum as tq + def compare_tq_to_qiskit(tq_circuit, qiskit_circuit, instance_info=""): """ @@ -16,8 +17,8 @@ def compare_tq_to_qiskit(tq_circuit, qiskit_circuit, instance_info=""): qiskit_ops = [] for bit in qiskit_circuit.decompose(): wires = [] - for qu in bit.qubits: - wires.append(qu.index) + for qb in bit.qubits: + wires.append(qiskit_circuit.find_bit(qb).index) qiskit_ops.append( { "name": bit.operation.name, @@ -29,9 +30,9 @@ def compare_tq_to_qiskit(tq_circuit, qiskit_circuit, instance_info=""): tq_ops = [ { "name": op["name"], - "wires": (op["wires"],) - if isinstance(op["wires"], int) - else tuple(op["wires"]), + "wires": ( + (op["wires"],) if isinstance(op["wires"], int) else tuple(op["wires"]) + ), } for op in tq_circuit.op_history ] diff --git a/test/layers/test_rotgate.py b/test/layers/test_rotgate.py index 30f24b8a..593563c7 100644 --- a/test/layers/test_rotgate.py +++ b/test/layers/test_rotgate.py @@ -1,14 +1,10 @@ -import torchquantum as tq -import qiskit -from qiskit import Aer, execute - -from torchquantum.util import ( - switch_little_big_endian_matrix, - find_global_phase, -) - -from qiskit.circuit.library import GR, GRX, GRY, GRZ import numpy as np +from qiskit import transpile +from qiskit.circuit.library import GR, GRX, GRY, GRZ +from qiskit_aer import AerSimulator + +import torchquantum as tq +from torchquantum.util import find_global_phase, switch_little_big_endian_matrix all_pairs = [ {"qiskit": GR, "tq": tq.layer.GlobalR, "params": 2}, @@ -19,6 +15,7 @@ ITERATIONS = 2 + def test_rotgates(): # test each pair for pair in all_pairs: @@ -28,15 +25,18 @@ def test_rotgates(): for _ in range(ITERATIONS): # generate random parameters params = [ - np.random.uniform(-2 * np.pi, 2 * np.pi) for _ in range(pair["params"]) + np.random.uniform(-2 * np.pi, 2 * np.pi) + for _ in range(pair["params"]) ] # create the qiskit circuit qiskit_circuit = pair["qiskit"](num_wires, *params) # get the unitary from qiskit - backend = Aer.get_backend("unitary_simulator") - result = execute(qiskit_circuit, backend).result() + backend = AerSimulator(method="unitary") + qiskit_circuit = transpile(qiskit_circuit, backend) + qiskit_circuit.save_unitary() + result = backend.run(qiskit_circuit).result() unitary_qiskit = result.get_unitary(qiskit_circuit) # create tq circuit diff --git a/test/operator/test_ControlledU.py b/test/operator/test_ControlledU.py index 5bc01096..e80dee1d 100644 --- a/test/operator/test_ControlledU.py +++ b/test/operator/test_ControlledU.py @@ -25,14 +25,13 @@ # test the controlled unitary function -import torchquantum as tq -import torchquantum.functional as tqf from test.utils import check_all_close # import pdb # pdb.set_trace() import numpy as np +import torchquantum as tq flag = 4 diff --git a/torchquantum/functional/func_controlled_unitary.py b/torchquantum/functional/func_controlled_unitary.py index dc909815..82179b2b 100644 --- a/torchquantum/functional/func_controlled_unitary.py +++ b/torchquantum/functional/func_controlled_unitary.py @@ -24,6 +24,7 @@ import numpy as np import torch + from torchquantum.functional.gate_wrapper import gate_wrapper from torchquantum.macro import * @@ -97,7 +98,7 @@ def controlled_unitary( n_wires = n_c_wires + n_t_wires # compute the new unitary, then permute - unitary = torch.tensor(torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE)) + unitary = torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE).clone().detach() for k in range(2**n_wires - 2**n_t_wires): unitary[k, k] = 1.0 + 0.0j diff --git a/torchquantum/operator/standard_gates/qubit_unitary.py b/torchquantum/operator/standard_gates/qubit_unitary.py index 5f7fd9b1..39156bc3 100644 --- a/torchquantum/operator/standard_gates/qubit_unitary.py +++ b/torchquantum/operator/standard_gates/qubit_unitary.py @@ -1,11 +1,12 @@ -from ..op_types import * from abc import ABCMeta -from torchquantum.macro import C_DTYPE -import torchquantum as tq + +import numpy as np import torch -from torchquantum.functional import mat_dict + import torchquantum.functional as tqf -import numpy as np +from torchquantum.macro import C_DTYPE + +from ..op_types import * class QubitUnitary(Operation, metaclass=ABCMeta): @@ -118,7 +119,7 @@ def from_controlled_operation( n_wires = n_c_wires + n_t_wires # compute the new unitary, then permute - unitary = torch.tensor(torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE)) + unitary = torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE).clone().detach() for k in range(2**n_wires - 2**n_t_wires): unitary[k, k] = 1.0 + 0.0j diff --git a/torchquantum/plugin/qiskit/qiskit_plugin.py b/torchquantum/plugin/qiskit/qiskit_plugin.py index 91bd6647..24852a5f 100644 --- a/torchquantum/plugin/qiskit/qiskit_plugin.py +++ b/torchquantum/plugin/qiskit/qiskit_plugin.py @@ -28,7 +28,7 @@ import qiskit import qiskit.circuit.library.standard_gates as qiskit_gate import torch -from qiskit import ClassicalRegister, QuantumCircuit +from qiskit import ClassicalRegister, QuantumCircuit, transpile from qiskit.circuit import Parameter from qiskit_aer import AerSimulator from torchpack.utils.logging import logger @@ -815,8 +815,10 @@ def test_qiskit2tq(): m = qiskit2tq(circ) - simulator = AerSimulator(method="unitary_simulator") - result = simulator.run(circ).result() + backend = AerSimulator(method="unitary") + circ = transpile(circ, backend) + circ.save_unitary() + result = backend.run(circ).result() unitary_qiskit = result.get_unitary(circ) unitary_tq = m.get_unitary(q_dev) @@ -980,8 +982,10 @@ def test_tq2qiskit(): circuit = tq2qiskit(test_module, inputs) - simulator = AerSimulator(method="unitary_simulator") - result = simulator.run(circuit).result() + backend = AerSimulator(method="unitary") + circuit = transpile(circuit, backend) + circuit.save_unitary() + result = backend.run(circuit).result() unitary_qiskit = result.get_unitary(circuit) unitary_tq = test_module.get_unitary(q_dev, inputs) @@ -1008,8 +1012,11 @@ def test_tq2qiskit_parameterized(): for k, x in enumerate(inputs[0]): binds[params[k]] = x.item() - simulator = AerSimulator(method="unitary_simulator") - result = simulator.run(circuit, parameter_binds=[binds]).result() + + backend = AerSimulator(method="unitary") + circuit = transpile(circuit, backend) + circuit.save_unitary() + result = backend.run(circuit, parameter_binds=[binds]).result() unitary_qiskit = result.get_unitary(circuit) # print(unitary_qiskit) diff --git a/torchquantum/plugin/qiskit/qiskit_processor.py b/torchquantum/plugin/qiskit/qiskit_processor.py index 252be251..f039b309 100644 --- a/torchquantum/plugin/qiskit/qiskit_processor.py +++ b/torchquantum/plugin/qiskit/qiskit_processor.py @@ -28,9 +28,8 @@ import numpy as np import pathos.multiprocessing as multiprocessing import torch -from qiskit import IBMQ, QuantumCircuit, execute, transpile +from qiskit import IBMQ, QuantumCircuit, transpile from qiskit.exceptions import QiskitError -from qiskit.tools.monitor import job_monitor from qiskit.transpiler import PassManager from qiskit_aer import AerSimulator from qiskit_aer.noise import NoiseModel @@ -56,13 +55,10 @@ def run(self, circuits, output_name: str = None, callback=None): def run_job_worker(data): while True: try: - job = AerSimulator(**(data[0])) - qiskit_verbose = data[1] - if qiskit_verbose: - job_monitor(job, interval=1) + job = AerSimulator(**(data)) result = job.result() counts = result.get_counts() - # circ_num = len(data[0]['parameter_binds']) + # circ_num = len(data['parameter_binds']) # logger.info( # f'run job worker successful, circuit number = {circ_num}') break @@ -316,7 +312,6 @@ def process_parameterized( for i in range(0, len(binds_all), chunk_size) ] - qiskit_verbose = self.max_jobs <= 6 feed_dicts = [] for split_bind in split_binds: feed_dict = { @@ -328,7 +323,7 @@ def process_parameterized( "noise_model": self.noise_model, "parameter_binds": split_bind, } - feed_dicts.append([feed_dict, qiskit_verbose]) + feed_dicts.append(feed_dict) p = multiprocessing.Pool(self.max_jobs) results = p.map(run_job_worker, feed_dicts) @@ -492,7 +487,6 @@ def process_parameterized_and_shift( for i in range(0, len(binds_all), chunk_size) ] - qiskit_verbose = self.max_jobs <= 6 feed_dicts = [] for split_bind in split_binds: feed_dict = { @@ -504,7 +498,7 @@ def process_parameterized_and_shift( "noise_model": self.noise_model, "parameter_binds": split_bind, } - feed_dicts.append([feed_dict, qiskit_verbose]) + feed_dicts.append(feed_dict) p = multiprocessing.Pool(self.max_jobs) results = p.map(run_job_worker, feed_dicts) @@ -607,7 +601,6 @@ def process_multi_measure( circ_all[i : i + chunk_size] for i in range(0, len(circ_all), chunk_size) ] - qiskit_verbose = self.max_jobs <= 2 feed_dicts = [] for split_circ in split_circs: feed_dict = { @@ -618,7 +611,7 @@ def process_multi_measure( "seed_simulator": self.seed_simulator, "noise_model": self.noise_model, } - feed_dicts.append([feed_dict, qiskit_verbose]) + feed_dicts.append(feed_dict) p = multiprocessing.Pool(self.max_jobs) results = p.map(run_job_worker, feed_dicts) @@ -697,7 +690,6 @@ def process_ready_circs_get_counts(self, circs_all, parallel=True): for i in range(0, len(circs_all), chunk_size) ] - qiskit_verbose = self.max_jobs <= 6 feed_dicts = [] for split_circ in split_circs: feed_dict = { @@ -708,7 +700,7 @@ def process_ready_circs_get_counts(self, circs_all, parallel=True): "seed_simulator": self.seed_simulator, "noise_model": self.noise_model, } - feed_dicts.append([feed_dict, qiskit_verbose]) + feed_dicts.append(feed_dict) p = multiprocessing.Pool(self.max_jobs) results = p.map(run_job_worker, feed_dicts) diff --git a/torchquantum/plugin/qiskit/qiskit_unitary_gate.py b/torchquantum/plugin/qiskit/qiskit_unitary_gate.py index 630d4751..c1ecd5f3 100644 --- a/torchquantum/plugin/qiskit/qiskit_unitary_gate.py +++ b/torchquantum/plugin/qiskit/qiskit_unitary_gate.py @@ -24,8 +24,8 @@ from qiskit.circuit.library.standard_gates import U3Gate from qiskit.extensions.exceptions import ExtensionError from qiskit.quantum_info.operators.predicates import is_unitary_matrix, matrix_equal -from qiskit.quantum_info.synthesis.two_qubit_decompose import two_qubit_cnot_decompose from qiskit.synthesis import OneQubitEulerDecomposer +from qiskit.synthesis.two_qubit.two_qubit_decompose import two_qubit_cnot_decompose _DECOMPOSER1Q = OneQubitEulerDecomposer("U3") diff --git a/torchquantum/util/utils.py b/torchquantum/util/utils.py index 6a714bb9..09e94e14 100644 --- a/torchquantum/util/utils.py +++ b/torchquantum/util/utils.py @@ -23,7 +23,7 @@ """ import copy -from typing import TYPE_CHECKING, Dict, Iterable, List +from typing import TYPE_CHECKING, Iterable import numpy as np import torch @@ -372,6 +372,10 @@ def find_global_phase(mat1, mat2, threshold): threshold = 0.5 global_phase = find_global_phase(mat1, mat2, threshold) """ + if not isinstance(mat1, np.ndarray): + mat1 = np.asarray(mat1) + if not isinstance(mat2, np.ndarray): + mat2 = np.asarray(mat2) for i in range(mat1.shape[0]): for j in range(mat1.shape[1]): # find a numerical stable global phase @@ -380,7 +384,7 @@ def find_global_phase(mat1, mat2, threshold): return None -def build_module_op_list(m: QuantumModule, x=None) -> List: +def build_module_op_list(m: QuantumModule, x=None) -> list: """ serialize all operations in the module and generate a list with [{'name': RX, 'has_params': True, 'trainable': True, 'wires': [0], @@ -435,7 +439,7 @@ def build_module_op_list(m: QuantumModule, x=None) -> List: def build_module_from_op_list( - op_list: List[Dict], remove_ops=False, thres=None + op_list: list[dict], remove_ops=False, thres=None ) -> QuantumModule: """ Build a quantum module from an operation list. @@ -872,7 +876,7 @@ def get_circ_stats(circ): def partial_trace( q_device: QuantumDevice, - keep_indices: List[int], + keep_indices: list[int], ) -> torch.Tensor: """Returns a density matrix with only some qubits kept. Args: From 20c2cee785a5918d713a6a30b39dc2ac0417e4c3 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Wed, 29 May 2024 16:56:00 +0900 Subject: [PATCH 32/54] fix: bump up qiskit version --- requirements.txt | 4 +-- test/hadamard_grad/test_hadamard_grad.py | 6 ++-- test/measurement/test_measure.py | 9 ++--- test/plugin/test_qiskit_plugins.py | 2 -- test/qiskit_plugin_test.py | 34 ++++++++++--------- .../plugin/qiskit/qiskit_processor.py | 20 ++++------- .../plugin/qiskit/qiskit_unitary_gate.py | 7 ++-- 7 files changed, 39 insertions(+), 43 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8bf4d45c..1497d9a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,9 @@ opt_einsum pathos>=0.2.7 pylatexenc>=2.10 pyscf>=2.0.1 -qiskit>=0.39.0,<1.0.0 +qiskit>=1.0.0 recommonmark -qiskit_ibm_runtime==0.20.0 +qiskit-ibm-runtime==0.20.0 qiskit-aer==0.13.3 scipy>=1.5.2 diff --git a/test/hadamard_grad/test_hadamard_grad.py b/test/hadamard_grad/test_hadamard_grad.py index 62fdb21e..2eb387b8 100644 --- a/test/hadamard_grad/test_hadamard_grad.py +++ b/test/hadamard_grad/test_hadamard_grad.py @@ -1,7 +1,9 @@ import numpy as np +import pytest + from examples.hadamard_grad.circ import Circ1, Circ2, Circ3 from examples.hadamard_grad.hadamard_grad import hadamard_grad -import pytest + @pytest.mark.skip def test_hadamard_grad(): @@ -38,4 +40,4 @@ def test_hadamard_grad(): if __name__ == "__main__": - test_hadamard_grad() \ No newline at end of file + test_hadamard_grad() diff --git a/test/measurement/test_measure.py b/test/measurement/test_measure.py index 38c45df6..5fafa180 100644 --- a/test/measurement/test_measure.py +++ b/test/measurement/test_measure.py @@ -22,11 +22,12 @@ SOFTWARE. """ -import torchquantum as tq +import numpy as np +from qiskit import transpile +from qiskit_aer import AerSimulator +import torchquantum as tq from torchquantum.plugin import op_history2qiskit -from qiskit import Aer, transpile -import numpy as np def test_measure(): @@ -42,7 +43,7 @@ def test_measure(): circ = op_history2qiskit(qdev.n_wires, qdev.op_history) circ.measure_all() - simulator = Aer.get_backend("aer_simulator") + simulator = AerSimulator() circ = transpile(circ, simulator) qiskit_res = simulator.run(circ, shots=n_shots).result() qiskit_counts = qiskit_res.get_counts() diff --git a/test/plugin/test_qiskit_plugins.py b/test/plugin/test_qiskit_plugins.py index 33d9c4d6..a12056c2 100644 --- a/test/plugin/test_qiskit_plugins.py +++ b/test/plugin/test_qiskit_plugins.py @@ -25,7 +25,6 @@ import random import numpy as np -import pytest from qiskit.quantum_info import Pauli, Statevector import torchquantum as tq @@ -40,7 +39,6 @@ } -@pytest.mark.skip def test_expval_observable(): # seed = 0 # random.seed(seed) diff --git a/test/qiskit_plugin_test.py b/test/qiskit_plugin_test.py index d8b7e94b..a5aed71a 100644 --- a/test/qiskit_plugin_test.py +++ b/test/qiskit_plugin_test.py @@ -24,21 +24,22 @@ import argparse import pdb -import torch -import torchquantum as tq -import numpy as np +from test.static_mode_test import QLayer as AllRandomLayer -from qiskit import Aer, execute +import numpy as np +import torch +from qiskit_aer import AerSimulator from torchpack.utils.logging import logger + +import torchquantum as tq +from torchquantum.macro import F_DTYPE +from torchquantum.plugin import tq2qiskit from torchquantum.util import ( + find_global_phase, + get_expectations_from_counts, switch_little_big_endian_matrix, switch_little_big_endian_state, - get_expectations_from_counts, - find_global_phase, ) -from test.static_mode_test import QLayer as AllRandomLayer -from torchquantum.plugin import tq2qiskit -from torchquantum.macro import F_DTYPE def unitary_tq_vs_qiskit_test(): @@ -59,8 +60,9 @@ def unitary_tq_vs_qiskit_test(): # qiskit circ = tq2qiskit(q_layer, x) - simulator = Aer.get_backend("unitary_simulator") - result = execute(circ, simulator).result() + simulator = AerSimulator(method="unitary") + circ.save_unitary() + result = simulator.run(circ).result() unitary_qiskit = result.get_unitary(circ) stable_threshold = 1e-5 @@ -115,10 +117,11 @@ def state_tq_vs_qiskit_test(): # qiskit circ = tq2qiskit(q_layer, x) # Select the StatevectorSimulator from the Aer provider - simulator = Aer.get_backend("statevector_simulator") + simulator = AerSimulator(method="statevector") + circ.save_statevector() # Execute and get counts - result = execute(circ, simulator).result() + result = simulator.run(circ).result() state_qiskit = result.get_statevector(circ) stable_threshold = 1e-5 @@ -175,11 +178,10 @@ def measurement_tq_vs_qiskit_test(): circ = tq2qiskit(q_layer, x) circ.measure(list(range(n_wires)), list(range(n_wires))) - # Select the QasmSimulator from the Aer provider - simulator = Aer.get_backend("qasm_simulator") + simulator = AerSimulator() # Execute and get counts - result = execute(circ, simulator, shots=1000000).result() + result = simulator.run(circ, shots=1000000).result() counts = result.get_counts(circ) measured_qiskit = get_expectations_from_counts(counts, n_wires=n_wires) diff --git a/torchquantum/plugin/qiskit/qiskit_processor.py b/torchquantum/plugin/qiskit/qiskit_processor.py index f039b309..1a484d7b 100644 --- a/torchquantum/plugin/qiskit/qiskit_processor.py +++ b/torchquantum/plugin/qiskit/qiskit_processor.py @@ -28,7 +28,7 @@ import numpy as np import pathos.multiprocessing as multiprocessing import torch -from qiskit import IBMQ, QuantumCircuit, transpile +from qiskit import QuantumCircuit, transpile from qiskit.exceptions import QiskitError from qiskit.transpiler import PassManager from qiskit_aer import AerSimulator @@ -183,7 +183,6 @@ def qiskit_init(self): if self.backend is None: # initialize now - IBMQ.load_account() self.provider = get_provider_hub_group_project( hub=self.hub, group=self.group, @@ -195,9 +194,7 @@ def qiskit_init(self): self.coupling_map = self.get_coupling_map(self.backend_name) else: # use simulator - self.backend = AerSimulator( - method="qasm_simulator", max_parallel_experiments=0 - ) + self.backend = AerSimulator(max_parallel_experiments=0) self.noise_model = self.get_noise_model(self.noise_model_name) self.coupling_map = self.get_coupling_map(self.coupling_map_name) self.basis_gates = self.get_basis_gates(self.basis_gates_name) @@ -337,14 +334,13 @@ def process_parameterized( counts = list(itertools.chain(*results)) else: job = self.backend.run( - experiments=transpiled_circ, + transpiled_circ, pass_manager=self.empty_pass_manager, shots=self.n_shots, seed_simulator=self.seed_simulator, noise_model=self.noise_model, parameter_binds=binds_all, ) - job_monitor(job, interval=1) result = job.result() counts = result.get_counts() @@ -523,14 +519,14 @@ def process_parameterized_and_shift( while True: try: job = self.backend.run( - experiments=circ, + circ, pass_manager=self.empty_pass_manager, shots=self.n_shots, seed_simulator=self.seed_simulator, noise_model=self.noise_model, parameter_binds=binds_all, ) - job_monitor(job, interval=1) + result = ( job.result() ) # qiskit.providers.ibmq.job.exceptions.IBMQJobFailureError:Job has failed. Use the error_message() method to get more details @@ -649,7 +645,7 @@ def process( self.transpiled_circs = transpiled_circs job = self.backend.run( - experiments=transpiled_circs, + transpiled_circs, shots=self.n_shots, # initial_layout=self.initial_layout, seed_transpiler=self.seed_transpiler, @@ -659,7 +655,6 @@ def process( noise_model=self.noise_model, optimization_level=self.optimization_level, ) - job_monitor(job, interval=1) result = job.result() counts = result.get_counts() @@ -714,13 +709,12 @@ def process_ready_circs_get_counts(self, circs_all, parallel=True): counts = list(itertools.chain(*results)) else: job = self.backend.run( - experiments=circs_all, + circs_all, pass_manager=self.empty_pass_manager, shots=self.n_shots, seed_simulator=self.seed_simulator, noise_model=self.noise_model, ) - job_monitor(job, interval=1) result = job.result() counts = [result.get_counts()] diff --git a/torchquantum/plugin/qiskit/qiskit_unitary_gate.py b/torchquantum/plugin/qiskit/qiskit_unitary_gate.py index c1ecd5f3..b60427dd 100644 --- a/torchquantum/plugin/qiskit/qiskit_unitary_gate.py +++ b/torchquantum/plugin/qiskit/qiskit_unitary_gate.py @@ -22,7 +22,6 @@ from qiskit.circuit._utils import _compute_control_matrix from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.library.standard_gates import U3Gate -from qiskit.extensions.exceptions import ExtensionError from qiskit.quantum_info.operators.predicates import is_unitary_matrix, matrix_equal from qiskit.synthesis import OneQubitEulerDecomposer from qiskit.synthesis.two_qubit.two_qubit_decompose import two_qubit_cnot_decompose @@ -56,12 +55,12 @@ def __init__(self, data, label=None): data = numpy.array(data, dtype=complex) # Check input is unitary if not is_unitary_matrix(data, atol=1e-5): - raise ExtensionError("Input matrix is not unitary.") + raise ValueError("Input matrix is not unitary.") # Check input is N-qubit matrix input_dim, output_dim = data.shape num_qubits = int(numpy.log2(input_dim)) if input_dim != output_dim or 2**num_qubits != input_dim: - raise ExtensionError("Input matrix is not an N-qubit operator.") + raise ValueError("Input matrix is not an N-qubit operator.") self._qasm_name = None self._qasm_definition = None @@ -155,7 +154,7 @@ def control(self, num_ctrl_qubits=1, label=None, ctrl_state=None): pmat = Operator(iso.inverse()).data @ cmat diag = numpy.diag(pmat) if not numpy.allclose(diag, diag[0]): - raise ExtensionError("controlled unitary generation failed") + raise ValueError("controlled unitary generation failed") phase = numpy.angle(diag[0]) if phase: # need to apply to _definition since open controls creates temporary definition From 1d56d0bdd97b96d14029564b90b1c7d41cf047a5 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Thu, 30 May 2024 00:18:41 +0900 Subject: [PATCH 33/54] fix, lint: fix type annotation error for py38, fix lint --- test/plugin/test_qiskit_plugins.py | 2 ++ torchquantum/util/utils.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/test/plugin/test_qiskit_plugins.py b/test/plugin/test_qiskit_plugins.py index a12056c2..33d9c4d6 100644 --- a/test/plugin/test_qiskit_plugins.py +++ b/test/plugin/test_qiskit_plugins.py @@ -25,6 +25,7 @@ import random import numpy as np +import pytest from qiskit.quantum_info import Pauli, Statevector import torchquantum as tq @@ -39,6 +40,7 @@ } +@pytest.mark.skip def test_expval_observable(): # seed = 0 # random.seed(seed) diff --git a/torchquantum/util/utils.py b/torchquantum/util/utils.py index 09e94e14..521c8355 100644 --- a/torchquantum/util/utils.py +++ b/torchquantum/util/utils.py @@ -22,6 +22,8 @@ SOFTWARE. """ +from __future__ import annotations + import copy from typing import TYPE_CHECKING, Iterable @@ -279,7 +281,7 @@ def switch_little_big_endian_state(state): is_batch_state = False reshape = [2] * int(np.log2(state.size)) else: - logger.exception(f"Dimension of statevector should be 1 or 2") + logger.exception("Dimension of statevector should be 1 or 2") raise ValueError original_shape = state.shape @@ -471,7 +473,7 @@ def build_module_from_op_list( ] module = build_module_from_op_list(op_list, remove_ops=True, thres=0.1) """ - logger.info(f"Building module from op_list...") + logger.info("Building module from op_list...") thres = 1e-5 if thres is None else thres n_removed_ops = 0 ops = [] @@ -503,7 +505,7 @@ def build_module_from_op_list( if n_removed_ops > 0: logger.warning(f"Remove in total {n_removed_ops} pruned operations.") else: - logger.info(f"Do not remove any operations.") + logger.info("Do not remove any operations.") return tq.QuantumModuleFromOps(ops) @@ -564,7 +566,7 @@ def get_p_v_reg_mapping(circ): """ try: p2v_orig = circ._layout.final_layout.get_physical_bits().copy() - except: + except AttributeError: p2v_orig = circ._layout.get_physical_bits().copy() mapping = { "p2v": {}, @@ -605,7 +607,7 @@ def get_v_c_reg_mapping(circ): """ try: p2v_orig = circ._layout.final_layout.get_physical_bits().copy() - except: + except AttributeError: p2v_orig = circ._layout.get_physical_bits().copy() p2v = {} for p, v in p2v_orig.items(): @@ -785,7 +787,7 @@ def get_provider(backend_name, hub=None): ) except QiskitError: # logger.warning(f"Cannot use MIT backend, roll back to open") - logger.warning(f"Use the open backend") + logger.warning("Use the open backend") provider = QiskitRuntimeService( channel="ibm_quantum", instance="ibm-q/open/main" ) @@ -1082,7 +1084,7 @@ def clone_model( ##################### for idx, key in enumerate(state_dict_plus_shift): - if idx < 2: # Skip the first two keys because they are not paramters + if idx < 2: # Skip the first two keys because they are not parameters continue state_dict_plus_shift[key] += shift_rate state_dict_minus_shift[key] -= shift_rate From 30eab9108daba53a849ce9be24aa9607056a62a7 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Thu, 30 May 2024 00:33:33 +0900 Subject: [PATCH 34/54] fix: fix cnot error --- torchquantum/plugin/qiskit/qiskit_plugin.py | 23 ++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/torchquantum/plugin/qiskit/qiskit_plugin.py b/torchquantum/plugin/qiskit/qiskit_plugin.py index 24852a5f..d27025be 100644 --- a/torchquantum/plugin/qiskit/qiskit_plugin.py +++ b/torchquantum/plugin/qiskit/qiskit_plugin.py @@ -64,7 +64,7 @@ def qiskit2tq_op_history(circ): if getattr(circ, "_layout", None) is not None: try: p2v_orig = circ._layout.final_layout.get_physical_bits().copy() - except: + except AttributeError: p2v_orig = circ._layout.get_physical_bits().copy() p2v = {} for p, v in p2v_orig.items(): @@ -231,7 +231,7 @@ def append_parameterized_gate(func, circ, input_idx, params, wires): ) else: raise NotImplementedError( - f"{func} cannot be converted to " f"parameterized Qiskit QuantumCircuit" + f"{func} cannot be converted to parameterized Qiskit QuantumCircuit" ) @@ -344,7 +344,7 @@ def append_fixed_gate(circ, func, params, wires, inverse): def tq2qiskit_initialize(q_device: tq.QuantumDevice, all_states): - """Call the qiskit initialize funtion and encoder the current quantum state + """Call the qiskit initialize function and encoder the current quantum state using initialize and return circuits Args: @@ -444,7 +444,7 @@ def tq2qiskit( # generate only one qiskit QuantumCircuit assert module.params is None or module.params.shape[0] == 1 except AssertionError: - logger.exception(f"Cannot convert batch model tq module") + logger.exception("Cannot convert batch model tq module") n_removed_ops = 0 @@ -497,7 +497,7 @@ def tq2qiskit( elif module.name == "SX": circ.sx(*module.wires) elif module.name == "CNOT": - circ.cnot(*module.wires) + circ.cx(*module.wires) elif module.name == "CZ": circ.cz(*module.wires) elif module.name == "CY": @@ -595,7 +595,7 @@ def tq2qiskit( if n_removed_ops > 0: logger.warning( - f"Remove {n_removed_ops} operations with small " f"parameter magnitude." + f"Remove {n_removed_ops} operations with small parameter magnitude." ) return circ @@ -695,10 +695,10 @@ def qiskit2tq_Operator(circ: QuantumCircuit): if getattr(circ, "_layout", None) is not None: try: p2v_orig = circ._layout.final_layout.get_physical_bits().copy() - except: + except AttributeError: try: p2v_orig = circ._layout.get_physical_bits().copy() - except: + except AttributeError: p2v_orig = circ._layout.initial_layout.get_physical_bits().copy() p2v = {} for p, v in p2v_orig.items(): @@ -803,11 +803,11 @@ def test_qiskit2tq(): circ.sx(3) circ.crx(theta=0.4, control_qubit=0, target_qubit=1) - circ.cnot(control_qubit=2, target_qubit=1) + circ.cx(control_qubit=2, target_qubit=1) circ.u3(theta=-0.1, phi=-0.2, lam=-0.4, qubit=3) - circ.cnot(control_qubit=3, target_qubit=0) - circ.cnot(control_qubit=0, target_qubit=2) + circ.cx(control_qubit=3, target_qubit=0) + circ.cx(control_qubit=0, target_qubit=2) circ.x(2) circ.x(3) circ.u2(phi=-0.2, lam=-0.9, qubit=3) @@ -1012,7 +1012,6 @@ def test_tq2qiskit_parameterized(): for k, x in enumerate(inputs[0]): binds[params[k]] = x.item() - backend = AerSimulator(method="unitary") circuit = transpile(circuit, backend) circuit.save_unitary() From 0932418ed35c50bbe89b114bdeeff8e87fd6dd47 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Thu, 30 May 2024 00:51:30 +0900 Subject: [PATCH 35/54] fix: fix examples --- torchquantum/plugin/qiskit/qiskit_plugin.py | 2 +- torchquantum/util/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/torchquantum/plugin/qiskit/qiskit_plugin.py b/torchquantum/plugin/qiskit/qiskit_plugin.py index d27025be..385a0d0f 100644 --- a/torchquantum/plugin/qiskit/qiskit_plugin.py +++ b/torchquantum/plugin/qiskit/qiskit_plugin.py @@ -714,7 +714,7 @@ def qiskit2tq_Operator(circ: QuantumCircuit): ops = [] for gate in circ.data: op_name = gate[0].name - wires = list(map(lambda x: x.index, gate[1])) + wires = [circ.find_bit(qb).index for qb in gate.qubits] wires = [p2v[wire] for wire in wires] # sometimes the gate.params is ParameterExpression class init_params = ( diff --git a/torchquantum/util/utils.py b/torchquantum/util/utils.py index 521c8355..b0354994 100644 --- a/torchquantum/util/utils.py +++ b/torchquantum/util/utils.py @@ -850,7 +850,7 @@ def get_circ_stats(circ): for gate in circ.data: op_name = gate[0].name - wires = list(map(lambda x: x.index, gate[1])) + wires = [circ.find_bit(qb).index for qb in gate.qubits] if op_name in n_gates_dict.keys(): n_gates_dict[op_name] += 1 else: From c0e7a8a158542ffe9495b6bd9763145aca1b6996 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Thu, 30 May 2024 01:20:13 +0900 Subject: [PATCH 36/54] ci: update workflow --- .github/workflows/functional_tests.yaml | 8 ++++---- .github/workflows/lint.yaml | 4 ++-- .github/workflows/pull_request.yaml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/functional_tests.yaml b/.github/workflows/functional_tests.yaml index cde7e8ba..9b913d09 100644 --- a/.github/workflows/functional_tests.yaml +++ b/.github/workflows/functional_tests.yaml @@ -4,7 +4,7 @@ name: Python package on: - push: + push: pull_request: jobs: @@ -17,16 +17,16 @@ jobs: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - python -m pip install flake8 pytest qiskit-aer qiskit_ibm_runtime + python -m pip install flake8 pytest - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 4495c0e7..6af4cfc2 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -14,9 +14,9 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Update pip and install lint utilities diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 5d423f1f..35746edd 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -9,8 +9,8 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - uses: pre-commit/action@v2.0.3 From 66205f576807014ac1934ea006f7563fc1d41d90 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Thu, 30 May 2024 02:31:19 +0900 Subject: [PATCH 37/54] test: relax assertion threshold --- test/measurement/test_eval_observable.py | 2 +- test/measurement/test_expval_joint_sampling_grouping.py | 9 +++++---- test/plugin/test_qiskit_plugins.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/test/measurement/test_eval_observable.py b/test/measurement/test_eval_observable.py index a5810e20..499c2ad1 100644 --- a/test/measurement/test_eval_observable.py +++ b/test/measurement/test_eval_observable.py @@ -81,7 +81,7 @@ def test_expval_observable(): if ( n_wires <= 3 ): # if too many wires, the stochastic method is not accurate due to limited shots - assert np.isclose(expval_tq_sampling, expval_qiskit, atol=1e-2) + assert np.isclose(expval_tq_sampling, expval_qiskit, atol=0.015) print("expval observable test passed") diff --git a/test/measurement/test_expval_joint_sampling_grouping.py b/test/measurement/test_expval_joint_sampling_grouping.py index 09492458..8a759518 100644 --- a/test/measurement/test_expval_joint_sampling_grouping.py +++ b/test/measurement/test_expval_joint_sampling_grouping.py @@ -22,15 +22,16 @@ SOFTWARE. """ +import random + +import numpy as np + import torchquantum as tq from torchquantum.measurement import ( expval_joint_analytical, expval_joint_sampling_grouping, ) -import numpy as np -import random - def test_expval_joint_sampling_grouping(): n_obs = 20 @@ -54,7 +55,7 @@ def test_expval_joint_sampling_grouping(): ) for obs in obs_all: # assert - assert np.isclose(expval_ana[obs], expval_sam[obs][0].item(), atol=1e-2) + assert np.isclose(expval_ana[obs], expval_sam[obs][0].item(), atol=0.015) print(obs, expval_ana[obs], expval_sam[obs][0].item()) diff --git a/test/plugin/test_qiskit_plugins.py b/test/plugin/test_qiskit_plugins.py index 33d9c4d6..76d0a4db 100644 --- a/test/plugin/test_qiskit_plugins.py +++ b/test/plugin/test_qiskit_plugins.py @@ -76,7 +76,7 @@ def test_expval_observable(): if ( n_wires <= 3 ): # if too many wires, the stochastic method is not accurate due to limited shots - assert np.isclose(expval_qiskit_processor, expval_qiskit, atol=1e-2) + assert np.isclose(expval_qiskit_processor, expval_qiskit, atol=0.015) print("expval observable test passed") From b0d844684603d5ef3d37a0650be6ae192a8ced6a Mon Sep 17 00:00:00 2001 From: Gopal Dahale Date: Sat, 1 Jun 2024 00:21:56 +0530 Subject: [PATCH 38/54] Added QCBM algorithm with example --- examples/QCBM/README.md | 8 +++ examples/QCBM/qcbm_gaussian_mixture.py | 61 ++++++++++++++++ test/algorithm/test_qcbm.py | 33 +++++++++ torchquantum/algorithm/__init__.py | 1 + torchquantum/algorithm/qcbm.py | 96 ++++++++++++++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 examples/QCBM/README.md create mode 100644 examples/QCBM/qcbm_gaussian_mixture.py create mode 100644 test/algorithm/test_qcbm.py create mode 100644 torchquantum/algorithm/qcbm.py diff --git a/examples/QCBM/README.md b/examples/QCBM/README.md new file mode 100644 index 00000000..d72c4100 --- /dev/null +++ b/examples/QCBM/README.md @@ -0,0 +1,8 @@ +# Quantum Circuit Born Machine + +Quantum Circuit Born Machine (QCBM) [1] is a generative modeling algorithm which uses Born rule from quantum mechanics to sample from a quantum state $|\psi \rangle$ learned by training an ansatz $U(\theta)$ [1][2]. In this tutorial we show how `torchquantum` can be used to model a Gaussian mixture with QCBM. + +## References + +1. Liu, Jin-Guo, and Lei Wang. “Differentiable learning of quantum circuit born machines.” Physical Review A 98.6 (2018): 062324. +2. Gili, Kaitlin, et al. "Do quantum circuit born machines generalize?." Quantum Science and Technology 8.3 (2023): 035021. \ No newline at end of file diff --git a/examples/QCBM/qcbm_gaussian_mixture.py b/examples/QCBM/qcbm_gaussian_mixture.py new file mode 100644 index 00000000..3114a1f1 --- /dev/null +++ b/examples/QCBM/qcbm_gaussian_mixture.py @@ -0,0 +1,61 @@ +import matplotlib.pyplot as plt +import numpy as np +import torch +import torch.nn as nn +from torchquantum.algorithm import QCBM, MMDLoss +import torchquantum as tq + + +# Function to create a gaussian mixture +def gaussian_mixture_pdf(x, mus, sigmas): + mus, sigmas = np.array(mus), np.array(sigmas) + vars = sigmas**2 + values = [ + (1 / np.sqrt(2 * np.pi * v)) * np.exp(-((x - m) ** 2) / (2 * v)) + for m, v in zip(mus, vars) + ] + values = np.sum([val / sum(val) for val in values], axis=0) + return values / np.sum(values) + +# Create a gaussian mixture +n_wires = 6 +x_max = 2**n_wires +x_input = np.arange(x_max) +mus = [(2 / 8) * x_max, (5 / 8) * x_max] +sigmas = [x_max / 10] * 2 +data = gaussian_mixture_pdf(x_input, mus, sigmas) + +# This is the target distribution that the QCBM will learn +target_probs = torch.tensor(data, dtype=torch.float32) + +# Ansatz +layers = tq.RXYZCXLayer0({"n_blocks": 6, "n_wires": n_wires, "n_layers_per_block": 1}) + +qcbm = QCBM(n_wires, layers) + +# To train QCBMs, we use MMDLoss with radial basis function kernel. +bandwidth = torch.tensor([0.25, 60]) +space = torch.arange(2**n_wires) +mmd = MMDLoss(bandwidth, space) + +# Optimization +optimizer = torch.optim.Adam(qcbm.parameters(), lr=0.01) +for i in range(100): + optimizer.zero_grad(set_to_none=True) + pred_probs = qcbm() + loss = mmd(pred_probs, target_probs) + loss.backward() + optimizer.step() + print(i, loss.item()) + +# Visualize the results +with torch.no_grad(): + pred_probs = qcbm() + +plt.plot(x_input, target_probs, linestyle="-.", label=r"$\pi(x)$") +plt.bar(x_input, pred_probs, color="green", alpha=0.5, label="samples") +plt.xlabel("Samples") +plt.ylabel("Prob. Distribution") + +plt.legend() +plt.show() diff --git a/test/algorithm/test_qcbm.py b/test/algorithm/test_qcbm.py new file mode 100644 index 00000000..8c946cf4 --- /dev/null +++ b/test/algorithm/test_qcbm.py @@ -0,0 +1,33 @@ +from torchquantum.algorithm.qcbm import QCBM, MMDLoss +import torchquantum as tq +import torch +import pytest + + +def test_qcbm_forward(): + n_wires = 3 + n_layers = 3 + ops = [] + for l in range(n_layers): + for q in range(n_wires): + ops.append({"name": "rx", "wires": q, "params": 0.0, "trainable": True}) + for q in range(n_wires - 1): + ops.append({"name": "cnot", "wires": [q, q + 1]}) + + data = torch.ones(2**n_wires) + qmodule = tq.QuantumModule.from_op_history(ops) + qcbm = QCBM(n_wires, qmodule) + probs = qcbm() + expected = torch.tensor([1.0, 0, 0, 0, 0, 0, 0, 0]) + assert torch.allclose(probs, expected) + + +def test_mmd_loss(): + n_wires = 2 + bandwidth = torch.tensor([0.1, 1.0]) + space = torch.arange(2**n_wires) + + mmd = MMDLoss(bandwidth, space) + loss = mmd(torch.zeros(4), torch.zeros(4)) + print(loss) + assert torch.isclose(loss, torch.tensor(0.0), rtol=1e-5) diff --git a/torchquantum/algorithm/__init__.py b/torchquantum/algorithm/__init__.py index 7dfb672a..5a0d13d5 100644 --- a/torchquantum/algorithm/__init__.py +++ b/torchquantum/algorithm/__init__.py @@ -26,3 +26,4 @@ from .hamiltonian import * from .qft import * from .grover import * +from .qcbm import * diff --git a/torchquantum/algorithm/qcbm.py b/torchquantum/algorithm/qcbm.py new file mode 100644 index 00000000..09798f8d --- /dev/null +++ b/torchquantum/algorithm/qcbm.py @@ -0,0 +1,96 @@ +import torch +import torch.nn as nn + +import torchquantum as tq + +__all__ = ["QCBM", "MMDLoss"] + + +class MMDLoss(nn.Module): + """Squared maximum mean discrepancy with radial basis function kerne""" + + def __init__(self, scales, space): + """ + Initialize MMDLoss object. Calculates and stores the kernel matrix. + + Args: + scales: Bandwidth parameters. + space: Basis input space. + """ + super().__init__() + + gammas = 1 / (2 * (scales**2)) + + # squared Euclidean distance + sq_dists = torch.abs(space[:, None] - space[None, :]) ** 2 + + # Kernel matrix + self.K = sum(torch.exp(-gamma * sq_dists) for gamma in gammas) / len(scales) + self.scales = scales + + def k_expval(self, px, py): + """ + Kernel expectation value + + Args: + px: First probability distribution + py: Second probability distribution + + Returns: + Expectation value of the RBF Kernel. + """ + + return px @ self.K @ py + + def forward(self, px, py): + """ + Squared MMD loss. + + px: First probability distribution + py: Second probability distribution + + Returns: + Squared MMD loss. + """ + pxy = px - py + return self.k_expval(pxy, pxy) + + +class QCBM(nn.Module): + """ + Quantum Circuit Born Machine (QCBM) + + Attributes: + ansatz: An Ansatz object + n_wires: Number of wires in the ansatz used. + + Methods: + __init__: Initialize the QCBM object. + forward: Returns the probability distribution (output from measurement). + + """ + + def __init__(self, n_wires, ansatz): + """ + Initialize QCBM object + + Args: + ansatz (Ansatz): An Ansatz object + n_wires (int): Number of wires in the ansatz used. + """ + super().__init__() + + self.ansatz = ansatz + self.n_wires = n_wires + + def forward(self): + """ + Execute and obtain the probability distribution + + Returns: + Probabilities (torch.Tensor) + """ + qdev = tq.QuantumDevice(n_wires=self.n_wires, bsz=1, device="cpu") + self.ansatz(qdev) + probs = torch.abs(qdev.states.flatten()) ** 2 + return probs From 428be4ac0a19edaca4712120cd7b6283a3ab9ccf Mon Sep 17 00:00:00 2001 From: Gopal Dahale Date: Sat, 1 Jun 2024 00:32:25 +0530 Subject: [PATCH 39/54] Remove unused imports --- examples/QCBM/qcbm_gaussian_mixture.py | 1 - test/algorithm/test_qcbm.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/examples/QCBM/qcbm_gaussian_mixture.py b/examples/QCBM/qcbm_gaussian_mixture.py index 3114a1f1..c0f0f203 100644 --- a/examples/QCBM/qcbm_gaussian_mixture.py +++ b/examples/QCBM/qcbm_gaussian_mixture.py @@ -1,7 +1,6 @@ import matplotlib.pyplot as plt import numpy as np import torch -import torch.nn as nn from torchquantum.algorithm import QCBM, MMDLoss import torchquantum as tq diff --git a/test/algorithm/test_qcbm.py b/test/algorithm/test_qcbm.py index 8c946cf4..333a25bb 100644 --- a/test/algorithm/test_qcbm.py +++ b/test/algorithm/test_qcbm.py @@ -1,7 +1,6 @@ from torchquantum.algorithm.qcbm import QCBM, MMDLoss import torchquantum as tq import torch -import pytest def test_qcbm_forward(): @@ -29,5 +28,4 @@ def test_mmd_loss(): mmd = MMDLoss(bandwidth, space) loss = mmd(torch.zeros(4), torch.zeros(4)) - print(loss) assert torch.isclose(loss, torch.tensor(0.0), rtol=1e-5) From 4bd170c685d7b490290ea356994825b374e19713 Mon Sep 17 00:00:00 2001 From: Gopal Dahale Date: Sat, 1 Jun 2024 00:40:39 +0530 Subject: [PATCH 40/54] Updated init py following best practices --- torchquantum/algorithm/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/torchquantum/algorithm/__init__.py b/torchquantum/algorithm/__init__.py index 5a0d13d5..c7413a2e 100644 --- a/torchquantum/algorithm/__init__.py +++ b/torchquantum/algorithm/__init__.py @@ -22,8 +22,8 @@ SOFTWARE. """ -from .vqe import * -from .hamiltonian import * -from .qft import * -from .grover import * -from .qcbm import * +from .vqe import VQE +from .hamiltonian import Hamiltonian +from .qft import QFT +from .grover import Grover +from .qcbm import QCBM, MMDLoss From 3a048480f7d70d5eab6944981fb0001acf3dd640 Mon Sep 17 00:00:00 2001 From: GenericP3rson Date: Sun, 2 Jun 2024 16:17:55 -0500 Subject: [PATCH 41/54] rm density matrix for now --- test/density/test_density_measure.py | 64 --- test/density/test_density_op.py | 509 ------------------ test/density/test_density_trace.py | 108 ---- test/density/test_eval_observable_density.py | 147 ----- ..._expval_joint_sampling_grouping_density.py | 62 --- test/density/test_noise_model.py | 3 - torchquantum/util/utils.py | 38 +- 7 files changed, 8 insertions(+), 923 deletions(-) delete mode 100644 test/density/test_density_measure.py delete mode 100644 test/density/test_density_op.py delete mode 100644 test/density/test_density_trace.py delete mode 100644 test/density/test_eval_observable_density.py delete mode 100644 test/density/test_expval_joint_sampling_grouping_density.py delete mode 100644 test/density/test_noise_model.py diff --git a/test/density/test_density_measure.py b/test/density/test_density_measure.py deleted file mode 100644 index a770359c..00000000 --- a/test/density/test_density_measure.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -MIT License - -Copyright (c) 2020-present TorchQuantum Authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import torchquantum as tq - -from torchquantum.plugin import op_history2qiskit -from qiskit import Aer, transpile -import numpy as np - - -def test_measure_density(): - n_shots = 10000 - qdev = tq.NoiseDevice(n_wires=3, bsz=1, record_op=True) - qdev.x(wires=2) # type: ignore - qdev.x(wires=1) # type: ignore - qdev.ry(wires=0, params=0.98) # type: ignore - qdev.rx(wires=1, params=1.2) # type: ignore - qdev.cnot(wires=[0, 2]) # type: ignore - - tq_counts = tq.measure_density(qdev, n_shots=n_shots) - - circ = op_history2qiskit(qdev.n_wires, qdev.op_history) - circ.measure_all() - simulator = Aer.get_backend("aer_simulator_density_matrix") - circ = transpile(circ, simulator) - qiskit_res = simulator.run(circ, shots=n_shots).result() - qiskit_counts = qiskit_res.get_counts() - - for k, v in tq_counts[0].items(): - # need to reverse the bitstring because qiskit is in little endian - qiskit_ratio = qiskit_counts.get(k[::-1], 0) / n_shots - tq_ratio = v / n_shots - print(k, qiskit_ratio, tq_ratio) - assert np.isclose(qiskit_ratio, tq_ratio, atol=0.1) - - print("tq.measure for density matrix test passed") - - -if __name__ == "__main__": - #import pdb - - #pdb.set_trace() - test_measure_density() diff --git a/test/density/test_density_op.py b/test/density/test_density_op.py deleted file mode 100644 index 5826b015..00000000 --- a/test/density/test_density_op.py +++ /dev/null @@ -1,509 +0,0 @@ -""" -MIT License - -Copyright (c) 2020-present TorchQuantum Authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -# test the torchquantum.functional against the IBM Qiskit -import argparse -import pdb -import torchquantum as tq -import numpy as np - -import qiskit.circuit.library.standard_gates as qiskit_gate -from qiskit.quantum_info import DensityMatrix as qiskitDensity - -from unittest import TestCase - -from random import randrange, uniform - - -RND_TIMES = 100 - -single_gate_list = [ - {"qiskit": qiskit_gate.IGate, "tq": tq.i, "name": "Identity"}, - {"qiskit": qiskit_gate.HGate, "tq": tq.h, "name": "Hadamard"}, - {"qiskit": qiskit_gate.XGate, "tq": tq.x, "name": "x"}, - {"qiskit": qiskit_gate.YGate, "tq": tq.y, "name": "y"}, - {"qiskit": qiskit_gate.ZGate, "tq": tq.z, "name": "z"}, - {"qiskit": qiskit_gate.SGate, "tq": tq.s, "name": "S"}, - {"qiskit": qiskit_gate.TGate, "tq": tq.t, "name": "T"}, - {"qiskit": qiskit_gate.SdgGate, "tq": tq.sdg, "name": "SDG"}, - {"qiskit": qiskit_gate.TdgGate, "tq": tq.tdg, "name": "TDG"}, - {"qiskit": qiskit_gate.SXGate, "tq": tq.sx, "name": "SX"}, - {"qiskit": qiskit_gate.SXdgGate, "tq": tq.sxdg, "name": "SXDG"}, -] - -single_param_gate_list = [ - {"qiskit": qiskit_gate.RXGate, "tq": tq.rx, "name": "RX", "numparam": 1}, - {"qiskit": qiskit_gate.RYGate, "tq": tq.ry, "name": "Ry", "numparam": 1}, - {"qiskit": qiskit_gate.RZGate, "tq": tq.rz, "name": "RZ", "numparam": 1}, - {"qiskit": qiskit_gate.U1Gate, "tq": tq.u1, "name": "U1", "numparam": 1}, - #{"qiskit": qiskit_gate.PhaseGate, "tq": tq.phaseshift, "name": "Phaseshift", "numparam": 1}, - #{"qiskit": qiskit_gate.GlobalPhaseGate, "tq": tq.globalphase, "name": "Gphase", "numparam": 1}, - {"qiskit": qiskit_gate.U2Gate, "tq": tq.u2, "name": "U2", "numparam": 2}, - {"qiskit": qiskit_gate.U3Gate, "tq": tq.u3, "name": "U3", "numparam": 3}, - {"qiskit": qiskit_gate.RGate, "tq": tq.r, "name": "R", "numparam": 2}, - {"qiskit": qiskit_gate.UGate, "tq": tq.u, "name": "U", "numparam": 3}, - -] - -two_qubit_gate_list = [ - {"qiskit": qiskit_gate.CXGate, "tq": tq.cnot, "name": "CNOT"}, - {"qiskit": qiskit_gate.CXGate, "tq": tq.cx, "name": "CY"}, - {"qiskit": qiskit_gate.CYGate, "tq": tq.cy, "name": "CY"}, - {"qiskit": qiskit_gate.CZGate, "tq": tq.cz, "name": "CZ"}, - {"qiskit": qiskit_gate.CSGate, "tq": tq.cs, "name": "CS"}, - {"qiskit": qiskit_gate.CHGate, "tq": tq.ch, "name": "CH"}, - {"qiskit": qiskit_gate.CSdgGate, "tq": tq.csdg, "name": "CSdag"}, - {"qiskit": qiskit_gate.SwapGate, "tq": tq.swap, "name": "SWAP"}, - {"qiskit": qiskit_gate.iSwapGate, "tq": tq.iswap, "name": "iSWAP"}, - {"qiskit": qiskit_gate.CSXGate, "tq": tq.csx, "name": "CSX"} -] - -two_qubit_param_gate_list = [ - {"qiskit": qiskit_gate.RXXGate, "tq": tq.rxx, "name": "RXX", "numparam": 1}, - {"qiskit": qiskit_gate.RYYGate, "tq": tq.ryy, "name": "RYY", "numparam": 1}, - {"qiskit": qiskit_gate.RZZGate, "tq": tq.rzz, "name": "RZZ", "numparam": 1}, - {"qiskit": qiskit_gate.RZXGate, "tq": tq.rzx, "name": "RZX", "numparam": 1}, - {"qiskit": qiskit_gate.CRXGate, "tq": tq.crx, "name": "CRX", "numparam": 1}, - {"qiskit": qiskit_gate.CRYGate, "tq": tq.cry, "name": "CRY", "numparam": 1}, - {"qiskit": qiskit_gate.CRZGate, "tq": tq.crz, "name": "CRZ", "numparam": 1}, - {"qiskit": qiskit_gate.CU1Gate, "tq": tq.cu1, "name": "CU1", "numparam": 1}, - {"qiskit": qiskit_gate.CU3Gate, "tq": tq.cu3, "name": "CU3", "numparam": 3} -] - -three_qubit_gate_list = [ - {"qiskit": qiskit_gate.CCXGate, "tq": tq.ccx, "name": "Toffoli"}, - {"qiskit": qiskit_gate.CSwapGate, "tq": tq.cswap, "name": "CSWAP"}, - {"qiskit": qiskit_gate.CCZGate, "tq": tq.ccz, "name": "CCZ"} -] - -three_qubit_param_gate_list = [ - -] - -pair_list = [ - # {'qiskit': qiskit_gate.?, 'tq': tq.Rot}, - # {'qiskit': qiskit_gate.?, 'tq': tq.MultiRZ}, - # {'qiskit': qiskit_gate.?, 'tq': tq.CRot}, - # {'qiskit': qiskit_gate.?, 'tq': tq.CU2}, - {"qiskit": qiskit_gate.ECRGate, "tq": tq.ECR}, - # {"qiskit": qiskit_library.QFT, "tq": tq.QFT}, - {"qiskit": qiskit_gate.DCXGate, "tq": tq.DCX}, - {"qiskit": qiskit_gate.XXMinusYYGate, "tq": tq.XXMINYY}, - {"qiskit": qiskit_gate.XXPlusYYGate, "tq": tq.XXPLUSYY}, - {"qiskit": qiskit_gate.C3XGate, "tq": tq.C3X}, - {"qiskit": qiskit_gate.C4XGate, "tq": tq.C4X}, - {"qiskit": qiskit_gate.RCCXGate, "tq": tq.RCCX}, - {"qiskit": qiskit_gate.RC3XGate, "tq": tq.RC3X}, - {"qiskit": qiskit_gate.C3SXGate, "tq": tq.C3SX}, -] - -maximum_qubit_num = 6 - - -def density_is_close(mat1: np.ndarray, mat2: np.ndarray): - assert mat1.shape == mat2.shape - return np.allclose(mat1, mat2, 1e-3, 1e-6) - - -class single_qubit_test(TestCase): - ''' - Act one single qubit on all possible location of a quantum circuit, - compare the density matrix between qiskit result and tq result. - ''' - - def compare_single_gate(self, gate_pair, qubit_num): - passed = True - for index in range(0, qubit_num): - qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - gate_pair['tq'](qdev, [index]) - mat1 = np.array(qdev.get_2d_matrix(0)) - rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - index]) - mat2 = np.array(rho_qiskit.to_operator()) - if density_is_close(mat1, mat2): - print("Test passed for %s gate on qubit %d when qubit_number is %d!" % ( - gate_pair['name'], index, qubit_num)) - else: - passed = False - print("Test failed for %s gaet on qubit %d when qubit_number is %d!" % ( - gate_pair['name'], index, qubit_num)) - return passed - - def compare_single_gate_params(self, gate_pair, qubit_num): - passed = True - for index in range(0, qubit_num): - qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - paramnum = gate_pair["numparam"] - params = [] - for i in range(0, paramnum): - params.append(uniform(0, 6.2)) - - gate_pair['tq'](qdev, [index], params=params) - mat1 = np.array(qdev.get_2d_matrix(0)) - rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](*params), [qubit_num - 1 - index]) - mat2 = np.array(rho_qiskit.to_operator()) - if density_is_close(mat1, mat2): - print("Test passed for %s gate on qubit %d when qubit_number is %d!" % ( - gate_pair['name'], index, qubit_num)) - else: - passed = False - print("Test failed for %s gaet on qubit %d when qubit_number is %d!" % ( - gate_pair['name'], index, qubit_num)) - return passed - - def test_single_gates_params(self): - for qubit_num in range(1, maximum_qubit_num + 1): - for i in range(0, len(single_param_gate_list)): - self.assertTrue(self.compare_single_gate_params(single_param_gate_list[i], qubit_num)) - - def test_single_gates(self): - for qubit_num in range(1, maximum_qubit_num + 1): - for i in range(0, len(single_gate_list)): - self.assertTrue(self.compare_single_gate(single_gate_list[i], qubit_num)) - - -class two_qubit_test(TestCase): - ''' - Act two qubits gate on all possible location of a quantum circuit, - compare the density matrix between qiskit result and tq result. - ''' - - def compare_two_qubit_gate(self, gate_pair, qubit_num): - passed = True - for index1 in range(0, qubit_num): - for index2 in range(0, qubit_num): - if index1 == index2: - continue - qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - gate_pair['tq'](qdev, [index1, index2]) - mat1 = np.array(qdev.get_2d_matrix(0)) - rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - index1, qubit_num - 1 - index2]) - mat2 = np.array(rho_qiskit.to_operator()) - if density_is_close(mat1, mat2): - print("Test passed for %s gate on qubit (%d,%d) when qubit_number is %d!" % ( - gate_pair['name'], index1, index2, qubit_num)) - else: - passed = False - print("Test failed for %s gate on qubit (%d,%d) when qubit_number is %d!" % ( - gate_pair['name'], index1, index2, qubit_num)) - return passed - - def compare_two_qubit_params_gate(self, gate_pair, qubit_num): - passed = True - for index1 in range(0, qubit_num): - for index2 in range(0, qubit_num): - if index1 == index2: - continue - paramnum = gate_pair["numparam"] - params = [] - for i in range(0, paramnum): - params.append(uniform(0, 6.2)) - - - qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - gate_pair['tq'](qdev, [index1, index2], params=params) - - mat1 = np.array(qdev.get_2d_matrix(0)) - rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](*params), - [qubit_num - 1 - index1, qubit_num - 1 - index2]) - mat2 = np.array(rho_qiskit.to_operator()) - if density_is_close(mat1, mat2): - print("Test passed for %s gate on qubit (%d,%d) when qubit_number is %d!" % ( - gate_pair['name'], index1, index2, qubit_num)) - else: - passed = False - print("Test failed for %s gate on qubit (%d,%d) when qubit_number is %d!" % ( - gate_pair['name'], index1, index2, qubit_num)) - return passed - - def test_two_qubits_params_gates(self): - for qubit_num in range(2, maximum_qubit_num + 1): - for i in range(0, len(two_qubit_param_gate_list)): - self.assertTrue(self.compare_two_qubit_params_gate(two_qubit_param_gate_list[i], qubit_num)) - - def test_two_qubits_gates(self): - for qubit_num in range(2, maximum_qubit_num + 1): - for i in range(0, len(two_qubit_gate_list)): - self.assertTrue(self.compare_two_qubit_gate(two_qubit_gate_list[i], qubit_num)) - - -class three_qubit_test(TestCase): - ''' - Act three qubits gates on all possible location of a quantum circuit, - compare the density matrix between qiskit result and tq result. - ''' - - def compare_three_qubit_gate(self, gate_pair, qubit_num): - passed = True - for index1 in range(0, qubit_num): - for index2 in range(0, qubit_num): - if (index1 == index2): - continue - for index3 in range(0, qubit_num): - if (index3 == index1) or (index3 == index2): - continue - qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - gate_pair['tq'](qdev, [index1, index2, index3]) - mat1 = np.array(qdev.get_2d_matrix(0)) - rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), - [qubit_num - 1 - index1, qubit_num - 1 - index2, - qubit_num - 1 - index3]) - mat2 = np.array(rho_qiskit.to_operator()) - if density_is_close(mat1, mat2): - print("Test passed for %s gate on qubit (%d,%d,%d) when qubit_number is %d!" % ( - gate_pair['name'], index1, index2, index3, qubit_num)) - else: - passed = False - print("Test failed for %s gate on qubit (%d,%d,%d) when qubit_number is %d!" % ( - gate_pair['name'], index1, index2, index3, qubit_num)) - return passed - - def test_three_qubits_gates(self): - for qubit_num in range(3, maximum_qubit_num + 1): - for i in range(0, len(three_qubit_gate_list)): - self.assertTrue(self.compare_three_qubit_gate(three_qubit_gate_list[i], qubit_num)) - - -class random_layer_test(TestCase): - ''' - Generate a single qubit random layer - ''' - - def single_qubit_random_layer(self, gatestrength): - passed = True - length = len(single_gate_list) - for qubit_num in range(1, maximum_qubit_num + 1): - qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - gate_num = int(gatestrength * qubit_num) - gate_list = [] - for i in range(0, gate_num): - random_gate_index = randrange(length) - gate_pair = single_gate_list[random_gate_index] - random_qubit_index = randrange(qubit_num) - gate_list.append(gate_pair["name"]) - gate_pair['tq'](qdev, [random_qubit_index]) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index]) - ''' - print(i) - print("gate list:") - print(gate_list) - print("qdev history") - print(qdev.op_history) - mat1_tmp = np.array(qdev.get_2d_matrix(0)) - mat2_tmp = np.array(rho_qiskit.to_operator()) - print("Torch quantum result:") - print(mat1_tmp) - print("Qiskit result:") - print(mat2_tmp) - if not density_is_close(mat1_tmp, mat2_tmp): - passed = False - print("Failed! Current gate list:") - print(gate_list) - print(qdev.op_history) - return passed - ''' - mat1 = np.array(qdev.get_2d_matrix(0)) - mat2 = np.array(rho_qiskit.to_operator()) - - if density_is_close(mat1, mat2): - print( - "Test passed for single qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( - gate_num, qubit_num)) - else: - passed = False - print( - "Test falied for single qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( - gate_num, qubit_num)) - print(gate_list) - print(qdev.op_history) - - return passed - - def test_single_qubit_random_layer(self): - repeat_num = 5 - gate_strength_list = [0.5, 1, 1.5, 2, 2.5, 3.5, 4.5, 5, 5.5, 6.5, 7.5, 8.5, 9.5, 10] - for i in range(0, repeat_num): - for gatestrength in gate_strength_list: - self.assertTrue(self.single_qubit_random_layer(gatestrength)) - - def two_qubit_random_layer(self, gatestrength): - passed = True - length = len(two_qubit_gate_list) - for qubit_num in range(2, maximum_qubit_num + 1): - qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - gate_num = int(gatestrength * qubit_num) - for i in range(0, gate_num + 1): - random_gate_index = randrange(length) - gate_pair = two_qubit_gate_list[random_gate_index] - random_qubit_index1 = randrange(qubit_num) - random_qubit_index2 = randrange(qubit_num) - while random_qubit_index2 == random_qubit_index1: - random_qubit_index2 = randrange(qubit_num) - - gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2]) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index1, - qubit_num - 1 - random_qubit_index2]) - - mat1 = np.array(qdev.get_2d_matrix(0)) - mat2 = np.array(rho_qiskit.to_operator()) - - if density_is_close(mat1, mat2): - print( - "Test passed for two qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( - gate_num, qubit_num)) - else: - passed = False - print( - "Test falied for two qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( - gate_num, qubit_num)) - return passed - - def test_two_qubit_random_layer(self): - repeat_num = 5 - gate_strength_list = [0.5, 1, 1.5, 2] - for i in range(0, repeat_num): - for gatestrength in gate_strength_list: - self.assertTrue(self.two_qubit_random_layer(gatestrength)) - - def three_qubit_random_layer(self, gatestrength): - passed = True - length = len(three_qubit_gate_list) - for qubit_num in range(3, maximum_qubit_num + 1): - qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - gate_num = int(gatestrength * qubit_num) - for i in range(0, gate_num + 1): - random_gate_index = randrange(length) - gate_pair = three_qubit_gate_list[random_gate_index] - random_qubit_index1 = randrange(qubit_num) - random_qubit_index2 = randrange(qubit_num) - while random_qubit_index2 == random_qubit_index1: - random_qubit_index2 = randrange(qubit_num) - random_qubit_index3 = randrange(qubit_num) - while random_qubit_index3 == random_qubit_index1 or random_qubit_index3 == random_qubit_index2: - random_qubit_index3 = randrange(qubit_num) - gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2, random_qubit_index3]) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index1, - qubit_num - 1 - random_qubit_index2, - qubit_num - 1 - random_qubit_index3]) - - mat1 = np.array(qdev.get_2d_matrix(0)) - mat2 = np.array(rho_qiskit.to_operator()) - - if density_is_close(mat1, mat2): - print( - "Test passed for three qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( - gate_num, qubit_num)) - else: - passed = False - print( - "Test falied for three qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( - gate_num, qubit_num)) - return passed - - def test_three_qubit_random_layer(self): - repeat_num = 5 - gate_strength_list = [0.5, 1, 1.5, 2] - for i in range(0, repeat_num): - for gatestrength in gate_strength_list: - self.assertTrue(self.three_qubit_random_layer(gatestrength)) - - def mix_random_layer(self, gatestrength): - passed = True - three_qubit_gate_length = len(three_qubit_gate_list) - single_qubit_gate_length = len(single_gate_list) - two_qubit_gate_length = len(two_qubit_gate_list) - - for qubit_num in range(3, maximum_qubit_num + 1): - qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - rho_qiskit = qiskitDensity.from_label('0' * qubit_num) - gate_num = int(gatestrength * qubit_num) - for i in range(0, gate_num + 1): - random_gate_qubit_num = randrange(3) - ''' - Add a single qubit gate - ''' - if (random_gate_qubit_num == 0): - random_gate_index = randrange(single_qubit_gate_length) - gate_pair = single_gate_list[random_gate_index] - random_qubit_index = randrange(qubit_num) - gate_pair['tq'](qdev, [random_qubit_index]) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index]) - ''' - Add a two qubit gate - ''' - if (random_gate_qubit_num == 1): - random_gate_index = randrange(two_qubit_gate_length) - gate_pair = two_qubit_gate_list[random_gate_index] - random_qubit_index1 = randrange(qubit_num) - random_qubit_index2 = randrange(qubit_num) - while random_qubit_index2 == random_qubit_index1: - random_qubit_index2 = randrange(qubit_num) - gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2]) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index1, - qubit_num - 1 - random_qubit_index2]) - ''' - Add a three qubit gate - ''' - if (random_gate_qubit_num == 2): - random_gate_index = randrange(three_qubit_gate_length) - gate_pair = three_qubit_gate_list[random_gate_index] - random_qubit_index1 = randrange(qubit_num) - random_qubit_index2 = randrange(qubit_num) - while random_qubit_index2 == random_qubit_index1: - random_qubit_index2 = randrange(qubit_num) - random_qubit_index3 = randrange(qubit_num) - while random_qubit_index3 == random_qubit_index1 or random_qubit_index3 == random_qubit_index2: - random_qubit_index3 = randrange(qubit_num) - gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2, random_qubit_index3]) - rho_qiskit = rho_qiskit.evolve(gate_pair['qiskit'](), [qubit_num - 1 - random_qubit_index1, - qubit_num - 1 - random_qubit_index2, - qubit_num - 1 - random_qubit_index3]) - - mat1 = np.array(qdev.get_2d_matrix(0)) - mat2 = np.array(rho_qiskit.to_operator()) - - if density_is_close(mat1, mat2): - print( - "Test passed for mix qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( - gate_num, qubit_num)) - else: - passed = False - print( - "Test falied for mix qubit gate random layer on qubit with %d gates when qubit_number is %d!" % ( - gate_num, qubit_num)) - return passed - - def test_mix_random_layer(self): - repeat_num = 5 - gate_strength_list = [0.5, 1, 1.5, 2] - for i in range(0, repeat_num): - for gatestrength in gate_strength_list: - self.assertTrue(self.mix_random_layer(gatestrength)) diff --git a/test/density/test_density_trace.py b/test/density/test_density_trace.py deleted file mode 100644 index 819a6f2d..00000000 --- a/test/density/test_density_trace.py +++ /dev/null @@ -1,108 +0,0 @@ -import torchquantum as tq -import numpy as np - -import qiskit.circuit.library.standard_gates as qiskit_gate -from qiskit.quantum_info import DensityMatrix as qiskitDensity - -from unittest import TestCase -from random import randrange - -maximum_qubit_num = 5 - -single_gate_list = [ - {"qiskit": qiskit_gate.HGate, "tq": tq.h, "name": "Hadamard"}, - {"qiskit": qiskit_gate.XGate, "tq": tq.x, "name": "x"}, - # {"qiskit": qiskit_gate.YGate, "tq": tq.y, "name": "y"}, - {"qiskit": qiskit_gate.ZGate, "tq": tq.z, "name": "z"}, - {"qiskit": qiskit_gate.SGate, "tq": tq.S, "name": "S"}, - {"qiskit": qiskit_gate.TGate, "tq": tq.T, "name": "T"}, - # {"qiskit": qiskit_gate.SXGate, "tq": tq.SX, "name": "SX"}, - {"qiskit": qiskit_gate.SdgGate, "tq": tq.SDG, "name": "SDG"}, - {"qiskit": qiskit_gate.TdgGate, "tq": tq.TDG, "name": "TDG"} -] - -single_param_gate_list = [ - -] - -two_qubit_gate_list = [ - {"qiskit": qiskit_gate.CXGate, "tq": tq.CNOT, "name": "CNOT"}, - {"qiskit": qiskit_gate.CYGate, "tq": tq.CY, "name": "CY"}, - {"qiskit": qiskit_gate.CZGate, "tq": tq.CZ, "name": "CZ"}, - {"qiskit": qiskit_gate.SwapGate, "tq": tq.SWAP, "name": "SWAP"} -] - -two_qubit_param_gate_list = [ - -] - -three_qubit_gate_list = [ - {"qiskit": qiskit_gate.CCXGate, "tq": tq.Toffoli, "name": "Toffoli"}, - {"qiskit": qiskit_gate.CSwapGate, "tq": tq.CSWAP, "name": "CSWAP"} -] - -three_qubit_param_gate_list = [ -] - - -class trace_preserving_test(TestCase): - - def mix_random_layer_trace(self, gatestrength): - passed = True - three_qubit_gate_length = len(three_qubit_gate_list) - single_qubit_gate_length = len(single_gate_list) - two_qubit_gate_length = len(two_qubit_gate_list) - - for qubit_num in range(3, maximum_qubit_num + 1): - qdev = tq.NoiseDevice(n_wires=qubit_num, bsz=1, device="cpu", record_op=True) - gate_num = int(gatestrength * qubit_num) - for i in range(0, gate_num + 1): - random_gate_qubit_num = randrange(3) - ''' - Add a single qubit gate - ''' - if (random_gate_qubit_num == 0): - random_gate_index = randrange(single_qubit_gate_length) - gate_pair = single_gate_list[random_gate_index] - random_qubit_index = randrange(qubit_num) - gate_pair['tq'](qdev, [random_qubit_index]) - - ''' - Add a two qubit gate - ''' - if (random_gate_qubit_num == 1): - random_gate_index = randrange(two_qubit_gate_length) - gate_pair = two_qubit_gate_list[random_gate_index] - random_qubit_index1 = randrange(qubit_num) - random_qubit_index2 = randrange(qubit_num) - while random_qubit_index2 == random_qubit_index1: - random_qubit_index2 = randrange(qubit_num) - gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2]) - ''' - Add a three qubit gate - ''' - if (random_gate_qubit_num == 2): - random_gate_index = randrange(three_qubit_gate_length) - gate_pair = three_qubit_gate_list[random_gate_index] - random_qubit_index1 = randrange(qubit_num) - random_qubit_index2 = randrange(qubit_num) - while random_qubit_index2 == random_qubit_index1: - random_qubit_index2 = randrange(qubit_num) - random_qubit_index3 = randrange(qubit_num) - while random_qubit_index3 == random_qubit_index1 or random_qubit_index3 == random_qubit_index2: - random_qubit_index3 = randrange(qubit_num) - gate_pair['tq'](qdev, [random_qubit_index1, random_qubit_index2, random_qubit_index3]) - - if not np.isclose(qdev.calc_trace(0), 1): - passed = False - print("Trace not preserved: %f" % (qdev.calc_trace(0))) - else: - print("Trace preserved: %f" % (qdev.calc_trace(0))) - return passed - - def test_mix_random_layer_trace(self): - repeat_num = 5 - gate_strength_list = [0.5, 1, 1.5, 2] - for i in range(0, repeat_num): - for gatestrength in gate_strength_list: - self.assertTrue(self.mix_random_layer_trace(gatestrength)) diff --git a/test/density/test_eval_observable_density.py b/test/density/test_eval_observable_density.py deleted file mode 100644 index eb628b97..00000000 --- a/test/density/test_eval_observable_density.py +++ /dev/null @@ -1,147 +0,0 @@ -""" -MIT License - -Copyright (c) 2020-present TorchQuantum Authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -from qiskit import QuantumCircuit -import numpy as np -import random -from qiskit.opflow import StateFn, X, Y, Z, I - -import torchquantum as tq - -from torchquantum.measurement import expval_joint_analytical_density, expval_joint_sampling_density -from torchquantum.plugin import op_history2qiskit -from torchquantum.util import switch_little_big_endian_matrix - -import torch - -pauli_str_op_dict = { - "X": X, - "Y": Y, - "Z": Z, - "I": I, -} - - -def test_expval_observable_density(): - # seed = 0 - # random.seed(seed) - # np.random.seed(seed) - # torch.manual_seed(seed) - - for k in range(100): - # print(k) - n_wires = random.randint(1, 10) - obs = random.choices(["X", "Y", "Z", "I"], k=n_wires) - random_layer = tq.RandomLayer(n_ops=100, wires=list(range(n_wires))) - qdev = tq.NoiseDevice(n_wires=n_wires, bsz=1, record_op=True) - random_layer(qdev) - - expval_tq = expval_joint_analytical_density(qdev, observable="".join(obs))[0].item().real - expval_tq_sampling = expval_joint_sampling_density( - qdev, observable="".join(obs), n_shots=100000 - )[0].item().real - - qiskit_circ = op_history2qiskit(qdev.n_wires, qdev.op_history) - operator = pauli_str_op_dict[obs[0]] - for ob in obs[1:]: - # note here the order is reversed because qiskit is in little endian - operator = pauli_str_op_dict[ob] ^ operator - rho = StateFn(qiskit_circ).to_density_matrix() - - #print("Rho:") - #print(rho) - - rho_evaled = rho - - rho_tq = switch_little_big_endian_matrix( - qdev.get_densities_2d().detach().numpy() - )[0] - - assert np.allclose(rho_evaled, rho_tq, atol=1e-5) - - #print("RHO passed!") - #print("rho_evaled.shape") - #print(rho_evaled.shape) - #print("operator.shape") - #print(operator.to_matrix().shape) - - - #operator.eval() - expval_qiskit = np.trace(rho_evaled@operator.to_matrix()).real - #print("TWO") - print(expval_tq, expval_qiskit) - assert np.isclose(expval_tq, expval_qiskit, atol=1e-1) - if ( - n_wires <= 3 - ): # if too many wires, the stochastic method is not accurate due to limited shots - assert np.isclose(expval_tq_sampling, expval_qiskit, atol=1e-2) - - print("expval observable test passed") - - -def util0(): - """from below we know that the Z ^ I means Z on qubit 1 and I on qubit 0""" - qc = QuantumCircuit(2) - - qc.x(0) - - operator = Z ^ I - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() - print(expectation_value.real) - # result: 1.0, means measurement result is 0, so Z is on qubit 1 - - operator = I ^ Z - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() - print(expectation_value.real) - # result: -1.0 means measurement result is 1, so Z is on qubit 0 - - operator = I ^ I - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() - print(expectation_value.real) - - operator = Z ^ Z - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() - print(expectation_value.real) - - qc = QuantumCircuit(3) - - qc.x(0) - - operator = I ^ I ^ Z - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() - print(expectation_value.real) - - -if __name__ == "__main__": - #import pdb - - #pdb.set_trace() - - util0() - #test_expval_observable() diff --git a/test/density/test_expval_joint_sampling_grouping_density.py b/test/density/test_expval_joint_sampling_grouping_density.py deleted file mode 100644 index ae3c034e..00000000 --- a/test/density/test_expval_joint_sampling_grouping_density.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -MIT License - -Copyright (c) 2020-present TorchQuantum Authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import torchquantum as tq -from torchquantum.measurement import ( - expval_joint_analytical_density, - expval_joint_sampling_grouping_density, -) - -import numpy as np -import random - - -def test_expval_joint_sampling_grouping_density(): - n_obs = 20 - n_wires = 4 - obs_all = [] - for _ in range(n_obs): - obs = random.choices(["X", "Y", "Z", "I"], k=n_wires) - obs_all.append("".join(obs)) - obs_all = list(set(obs_all)) - - random_layer = tq.RandomLayer(n_ops=100, wires=list(range(n_wires))) - qdev = tq.NoiseDevice(n_wires=n_wires, bsz=1, record_op=True) - random_layer(qdev) - - expval_ana = {} - for obs in obs_all: - expval_ana[obs] = expval_joint_analytical_density(qdev, observable=obs)[0].item() - - expval_sam = expval_joint_sampling_grouping_density( - qdev, observables=obs_all, n_shots_per_group=1000000 - ) - for obs in obs_all: - # assert - assert np.isclose(expval_ana[obs], expval_sam[obs][0].item(), atol=1e-1) - print(obs, expval_ana[obs], expval_sam[obs][0].item()) - - -if __name__ == "__main__": - test_expval_joint_sampling_grouping_density() diff --git a/test/density/test_noise_model.py b/test/density/test_noise_model.py deleted file mode 100644 index b28b04f6..00000000 --- a/test/density/test_noise_model.py +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/torchquantum/util/utils.py b/torchquantum/util/utils.py index caeee471..db191647 100644 --- a/torchquantum/util/utils.py +++ b/torchquantum/util/utils.py @@ -32,7 +32,6 @@ from opt_einsum import contract from qiskit_ibm_runtime import QiskitRuntimeService from qiskit.exceptions import QiskitError -from qiskit.providers.aer.noise.device.parameters import gate_error_values from torchpack.utils.config import Config from torchpack.utils.logging import logger @@ -706,37 +705,16 @@ def get_cared_configs(conf, mode) -> Config: def get_success_rate(properties, transpiled_circ): """ - Estimate the success rate of a transpiled quantum circuit. + Estimate the success rate of a transpiled quantum circuit. - Args: - properties (list): List of gate error properties. - transpiled_circ (QuantumCircuit): The transpiled quantum circuit. - - Returns: - float: The estimated success rate. - """ - # estimate the success rate according to the error rates of single and - # two-qubit gates in transpiled circuits - - gate_errors = gate_error_values(properties) - # construct the error dict - gate_error_dict = {} - for gate_error in gate_errors: - if gate_error[0] not in gate_error_dict.keys(): - gate_error_dict[gate_error[0]] = {tuple(gate_error[1]): gate_error[2]} - else: - gate_error_dict[gate_error[0]][tuple(gate_error[1])] = gate_error[2] - - success_rate = 1 - for gate in transpiled_circ.data: - gate_success_rate = ( - 1 - gate_error_dict[gate[0].name][tuple(map(lambda x: x.index, gate[1]))] - ) - if gate_success_rate == 0: - gate_success_rate = 1e-5 - success_rate *= gate_success_rate + Args: + properties (list): List of gate error properties. + transpiled_circ (QuantumCircuit): The transpiled quantum circuit. - return success_rate + Returns: + float: The estimated success rate. + """ + raise NotImplementedError def get_provider(backend_name, hub=None): """ From b7c7f3ca27b994b000eddb3411ed4c80334c2277 Mon Sep 17 00:00:00 2001 From: Gopal Dahale Date: Mon, 3 Jun 2024 13:07:08 +0530 Subject: [PATCH 42/54] Updated with argparse --- examples/QCBM/README.md | 34 +++++ examples/QCBM/assets/sample_output.png | Bin 0 -> 33427 bytes examples/QCBM/qcbm_gaussian_mixture.py | 171 +++++++++++++++++-------- 3 files changed, 154 insertions(+), 51 deletions(-) create mode 100644 examples/QCBM/assets/sample_output.png diff --git a/examples/QCBM/README.md b/examples/QCBM/README.md index d72c4100..cf61c65c 100644 --- a/examples/QCBM/README.md +++ b/examples/QCBM/README.md @@ -1,7 +1,41 @@ # Quantum Circuit Born Machine +(Implementation by: [Gopal Ramesh Dahale](https://github.com/Gopal-Dahale)) Quantum Circuit Born Machine (QCBM) [1] is a generative modeling algorithm which uses Born rule from quantum mechanics to sample from a quantum state $|\psi \rangle$ learned by training an ansatz $U(\theta)$ [1][2]. In this tutorial we show how `torchquantum` can be used to model a Gaussian mixture with QCBM. +## Setup + +Below is the usage of `qcbm_gaussian_mixture.py` which can be obtained by running `python qcbm_gaussian_mixture.py -h`. + +``` +usage: qcbm_gaussian_mixture.py [-h] [--n_wires N_WIRES] [--epochs EPOCHS] [--n_blocks N_BLOCKS] [--n_layers_per_block N_LAYERS_PER_BLOCK] [--plot] [--optimizer OPTIMIZER] [--lr LR] + +options: + -h, --help show this help message and exit + --n_wires N_WIRES Number of wires used in the circuit + --epochs EPOCHS Number of training epochs + --n_blocks N_BLOCKS Number of blocks in ansatz + --n_layers_per_block N_LAYERS_PER_BLOCK + Number of layers per block in ansatz + --plot Visualize the predicted probability distribution + --optimizer OPTIMIZER + optimizer class from torch.optim + --lr LR +``` + +For example: + +``` +python qcbm_gaussian_mixture.py --plot --epochs 100 --optimizer RMSprop --lr 0.01 --n_blocks 6 --n_layers_per_block 2 --n_wires 6 +``` + +Using the command above gives an output similar to the plot below. + +

+sample output of QCBM +

+ + ## References 1. Liu, Jin-Guo, and Lei Wang. “Differentiable learning of quantum circuit born machines.” Physical Review A 98.6 (2018): 062324. diff --git a/examples/QCBM/assets/sample_output.png b/examples/QCBM/assets/sample_output.png new file mode 100644 index 0000000000000000000000000000000000000000..c1626a4e7347061fb5b9f357462f1767bd882cbe GIT binary patch literal 33427 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%0D#BcA_UqdkX`DbKEk`n@ejH(Avh)-d-V+o=& z2`Y!p78R>kS1AfTckrK~U;%3+7}K17bohgpju{w`kxL2+3Ti0^7PG^j@J#QVI3OlE z3Z1N|ECV)rQj`R%TC!nAhw?$BnSm;zaP{|Ym9=FT^6exc78x#xzsd8QvmuE&2t+!l ztxb>cD-~#-m^upWs?k>tBXcv6wP}}8w?YE85xI!>Y3lP~88Qi{AJAkhxH_TAXNn78QOS0mxb=t;w!I$*jT!$ejHg=(N@l|*Bl zUOUBrTh8Wh5<1m~Fe-t>a;E{U%C4@isX46D5(JC1S?L9ZdB{@f@fxTfIdY@V$lJSg z3SZ!T1#K^<_>p@}qb~s3+1BaWaymGNg09DRtxT9MUot8EuZF=_z`tn6M(R%P)R3<-C7@M+FxPwyQ!#Lqt=K7Ffcn<$V8gM zsy_T&GG^Cq-MW=L_!)B{)LzAxN7y<$JE!ngkmuxuPL~aaFcZQX9bdK&ofI&YL*HG8 z!~_T9u#;vUG~_Ia41~pI+lQxtJVfur#^a3x8B)1-MSS^kVRvv{M|*prCwlasCw7*Z z_56q^C@6r1>@~ay#$Dt(ikFg`AhSwEF`yvxuUgLd-XJ#wvWxF?MD$@cnRJ3zAgPQj+&ewTvdCP@pEENz<`v6j8p()Igw7Ru*&|jBeFYHnf^g_6c=@8Zn7ffr z#an@N-(g!Zua6eN%%bemPd3)mXglITwYIh{9*9($N?Kc{C7%#Cw>^98J44~w`D0$j z-bO}$fpunOZQTbQcMUeqi22`mkL$=H67wT-Fs)nB@Zgj(Zkl*O66iugXj63)t|OvL z6a~PxZQI--RC5}9%R%yR@Y-TJcmP?2KEDRJP{X(x>9Mi$@F5Oe^jYula41f_q*{o) zE6+tbq>iXq`s0rg~Vot%s!A0j|6VNby5ke_@f|Gs^@5-IcyoZBlPlfL+O zrlzL6D7j53QM1NuV?bmn#$$n)If0U None: + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed(seed) + # When running on the CuDNN backend, two further options must be set + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + # Set a fixed value for the hash seed + os.environ["PYTHONHASHSEED"] = str(seed) + print(f"Random seed set as {seed}") + + +def _setup_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--n_wires", type=int, default=6, help="Number of wires used in the circuit" + ) + parser.add_argument( + "--epochs", type=int, default=10, help="Number of training epochs" + ) + parser.add_argument( + "--n_blocks", type=int, default=6, help="Number of blocks in ansatz" + ) + parser.add_argument( + "--n_layers_per_block", + type=int, + default=1, + help="Number of layers per block in ansatz", + ) + parser.add_argument( + "--plot", + action="store_true", + help="Visualize the predicted probability distribution", + ) + parser.add_argument( + "--optimizer", type=str, default="Adam", help="optimizer class from torch.optim" + ) + parser.add_argument("--lr", type=float, default=1e-2) + return parser # Function to create a gaussian mixture def gaussian_mixture_pdf(x, mus, sigmas): - mus, sigmas = np.array(mus), np.array(sigmas) - vars = sigmas**2 - values = [ - (1 / np.sqrt(2 * np.pi * v)) * np.exp(-((x - m) ** 2) / (2 * v)) - for m, v in zip(mus, vars) - ] - values = np.sum([val / sum(val) for val in values], axis=0) - return values / np.sum(values) - -# Create a gaussian mixture -n_wires = 6 -x_max = 2**n_wires -x_input = np.arange(x_max) -mus = [(2 / 8) * x_max, (5 / 8) * x_max] -sigmas = [x_max / 10] * 2 -data = gaussian_mixture_pdf(x_input, mus, sigmas) - -# This is the target distribution that the QCBM will learn -target_probs = torch.tensor(data, dtype=torch.float32) - -# Ansatz -layers = tq.RXYZCXLayer0({"n_blocks": 6, "n_wires": n_wires, "n_layers_per_block": 1}) - -qcbm = QCBM(n_wires, layers) - -# To train QCBMs, we use MMDLoss with radial basis function kernel. -bandwidth = torch.tensor([0.25, 60]) -space = torch.arange(2**n_wires) -mmd = MMDLoss(bandwidth, space) - -# Optimization -optimizer = torch.optim.Adam(qcbm.parameters(), lr=0.01) -for i in range(100): - optimizer.zero_grad(set_to_none=True) - pred_probs = qcbm() - loss = mmd(pred_probs, target_probs) - loss.backward() - optimizer.step() - print(i, loss.item()) - -# Visualize the results -with torch.no_grad(): - pred_probs = qcbm() - -plt.plot(x_input, target_probs, linestyle="-.", label=r"$\pi(x)$") -plt.bar(x_input, pred_probs, color="green", alpha=0.5, label="samples") -plt.xlabel("Samples") -plt.ylabel("Prob. Distribution") - -plt.legend() -plt.show() + mus, sigmas = np.array(mus), np.array(sigmas) + vars = sigmas**2 + values = [ + (1 / np.sqrt(2 * np.pi * v)) * np.exp(-((x - m) ** 2) / (2 * v)) + for m, v in zip(mus, vars) + ] + values = np.sum([val / sum(val) for val in values], axis=0) + return values / np.sum(values) + + +def main(): + set_seed() + parser = _setup_parser() + args = parser.parse_args() + + print("Configuration:") + pprint(vars(args)) + + # Create a gaussian mixture + n_wires = args.n_wires + assert n_wires >= 1, "Number of wires must be at least 1" + + x_max = 2**n_wires + x_input = np.arange(x_max) + mus = [(2 / 8) * x_max, (5 / 8) * x_max] + sigmas = [x_max / 10] * 2 + data = gaussian_mixture_pdf(x_input, mus, sigmas) + + # This is the target distribution that the QCBM will learn + target_probs = torch.tensor(data, dtype=torch.float32) + + # Ansatz + layers = tq.RXYZCXLayer0( + { + "n_blocks": args.n_blocks, + "n_wires": n_wires, + "n_layers_per_block": args.n_layers_per_block, + } + ) + + qcbm = QCBM(n_wires, layers) + + # To train QCBMs, we use MMDLoss with radial basis function kernel. + bandwidth = torch.tensor([0.25, 60]) + space = torch.arange(2**n_wires) + mmd = MMDLoss(bandwidth, space) + + # Optimization + optimizer_class = getattr(torch.optim, args.optimizer) + optimizer = optimizer_class(qcbm.parameters(), lr=args.lr) + + for i in range(args.epochs): + optimizer.zero_grad(set_to_none=True) + pred_probs = qcbm() + loss = mmd(pred_probs, target_probs) + loss.backward() + optimizer.step() + print(i, loss.item()) + + # Visualize the results + if args.plot: + with torch.no_grad(): + pred_probs = qcbm() + + plt.plot(x_input, target_probs, linestyle="-.", label=r"$\pi(x)$") + plt.bar(x_input, pred_probs, color="green", alpha=0.5, label="samples") + plt.xlabel("Samples") + plt.ylabel("Prob. Distribution") + + plt.legend() + plt.show() + + +if __name__ == "__main__": + main() From 792b766fd4a01bb7884873a037980cc53d67d57b Mon Sep 17 00:00:00 2001 From: GenericP3rson <41024739+GenericP3rson@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:34:34 -0500 Subject: [PATCH 43/54] bump ibm runtime Co-authored-by: Kazuki Tsuoka --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1497d9a3..26be0280 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ pylatexenc>=2.10 pyscf>=2.0.1 qiskit>=1.0.0 recommonmark -qiskit-ibm-runtime==0.20.0 +qiskit-ibm-runtime>=0.20.0 qiskit-aer==0.13.3 scipy>=1.5.2 From b4e6b675afe6becca57951fcfe0b3bdbb648e66d Mon Sep 17 00:00:00 2001 From: GenericP3rson <41024739+GenericP3rson@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:34:50 -0500 Subject: [PATCH 44/54] bump qiskit aer Co-authored-by: Kazuki Tsuoka --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 26be0280..fc2e954c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pyscf>=2.0.1 qiskit>=1.0.0 recommonmark qiskit-ibm-runtime>=0.20.0 -qiskit-aer==0.13.3 +qiskit-aer>=0.13.3 scipy>=1.5.2 setuptools>=52.0.0 From 191934d117b8521e65f4b3415d6cd6766a0838f6 Mon Sep 17 00:00:00 2001 From: GenericP3rson <41024739+GenericP3rson@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:36:02 -0500 Subject: [PATCH 45/54] [fix] revive paramnum --- torchquantum/functional/gate_wrapper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/torchquantum/functional/gate_wrapper.py b/torchquantum/functional/gate_wrapper.py index fed989ec..cab7379f 100644 --- a/torchquantum/functional/gate_wrapper.py +++ b/torchquantum/functional/gate_wrapper.py @@ -330,6 +330,7 @@ def gate_wrapper( method, q_device: QuantumDevice, wires, + paramnum=0, params=None, n_wires=None, static=False, From b4b27489e6e5d8ea69096bbee7af78c68c3a26ea Mon Sep 17 00:00:00 2001 From: king-p3nguin Date: Thu, 6 Jun 2024 13:14:19 +0900 Subject: [PATCH 46/54] change: remove unnessesary cloning --- torchquantum/functional/func_controlled_unitary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/torchquantum/functional/func_controlled_unitary.py b/torchquantum/functional/func_controlled_unitary.py index 82179b2b..f5d745c0 100644 --- a/torchquantum/functional/func_controlled_unitary.py +++ b/torchquantum/functional/func_controlled_unitary.py @@ -26,7 +26,7 @@ import torch from torchquantum.functional.gate_wrapper import gate_wrapper -from torchquantum.macro import * +from torchquantum.macro import C_DTYPE def controlled_unitary( @@ -98,7 +98,7 @@ def controlled_unitary( n_wires = n_c_wires + n_t_wires # compute the new unitary, then permute - unitary = torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE).clone().detach() + unitary = torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE) for k in range(2**n_wires - 2**n_t_wires): unitary[k, k] = 1.0 + 0.0j From dc41accff5e8ac2a2feea331f4d3ad879c22cff0 Mon Sep 17 00:00:00 2001 From: Gopal Dahale Date: Thu, 6 Jun 2024 14:26:57 +0530 Subject: [PATCH 47/54] Added qcbm gaussian mixture notebook --- examples/QCBM/qcbm_gaussian_mixture.ipynb | 255 ++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 examples/QCBM/qcbm_gaussian_mixture.ipynb diff --git a/examples/QCBM/qcbm_gaussian_mixture.ipynb b/examples/QCBM/qcbm_gaussian_mixture.ipynb new file mode 100644 index 00000000..849f7cdc --- /dev/null +++ b/examples/QCBM/qcbm_gaussian_mixture.ipynb @@ -0,0 +1,255 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1cfe7a69-13c6-48ce-bc02-62e2047eef22", + "metadata": {}, + "source": [ + "# Learning Gaussian Mixture with Quantum Circuit Born Machine\n", + "\n", + "A QCBM is a generative model that represents the probability distribution of a classical dataset as a quantum pure state. It is a quantum circuit that generates samples via projective measurements on qubits. Given a target distribution $\\pi(x)$, we can generate samples closer to it using a QCBM.\n", + "\n", + "The Kerneled MMD loss is used to measure the difference between the generated samples and the target distribution.\n", + "\n", + "$$\n", + "\\mathcal{L} = \\underset{x, y \\sim p_\\boldsymbol{\\theta}}{\\mathbb{E}}[{K(x,y)}]-2\\underset{x\\sim p_\\boldsymbol{\\theta},y\\sim \\pi}{\\mathbb{E}}[K(x,y)]+\\underset{x, y \\sim \\pi}{\\mathbb{E}}[K(x, y)]\n", + "$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4d440b94-63d4-4f6d-882e-45827d54cb4d", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch\n", + "from torchquantum.algorithm import QCBM, MMDLoss\n", + "import torchquantum as tq\n", + "from qcbm_gaussian_mixture import gaussian_mixture_pdf" + ] + }, + { + "cell_type": "markdown", + "id": "2d14c9f7-4e5d-4fe1-98b4-83962d949519", + "metadata": {}, + "source": [ + "We use the function `gaussian_mixture_pdf` to generate a gaussian mixture which will be the target distribution $\\pi(x)$." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5483ab05-1a08-4bdc-8799-0e67433131af", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "n_wires = 6\n", + "x_max = 2**n_wires\n", + "x_input = np.arange(x_max)\n", + "mus = [(2 / 8) * x_max, (5 / 8) * x_max]\n", + "sigmas = [x_max / 10] * 2\n", + "data = gaussian_mixture_pdf(x_input, mus, sigmas)\n", + "\n", + "# This is the target distribution that the QCBM will learn\n", + "target_probs = torch.tensor(data, dtype=torch.float32)\n", + "\n", + "plt.plot(x_input, target_probs, linestyle=\"-.\", label=r\"$\\pi(x)$\")" + ] + }, + { + "cell_type": "markdown", + "id": "7b1bb110-e81c-455e-86a6-6b722f3a4433", + "metadata": {}, + "source": [ + "Using `torchquantum`, we can create a parameterized quantum circuit which will be used to generate a probability distribution. The gradient-based learning algorithm is used to update the circuit parameters $\\theta$ iteratively." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8347fa01-d519-40e3-a7ea-67fabca8ed56", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/gopald/Documents/tq-env/lib/python3.10/site-packages/qiskit/visualization/circuit/matplotlib.py:266: FutureWarning: The default matplotlib drawer scheme will be changed to \"iqp\" in a following release. To silence this warning, specify the current default explicitly as style=\"clifford\", or the new default as style=\"iqp\".\n", + " self._style, def_font_ratio = load_style(self._style)\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAB/wAAANyCAYAAABvwrPRAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3hUVf7H8fekFxIg1FBDKNJ7kSKCooKKBcWGBX+urApiQdFd11V3V1YRG2DBta8rIkVWUcQCKKBSpBelBggkQEggJKTP/P64m0gkZWYy7c79vJ6HB5i55Zwzd+75zvnee67N4XA4EBEREREREREREREREREREVMJ8XcBRERERERERERERERERERExHVK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImpIS/iIiIiIiIiIiIiIiIiIiICSnhLyIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImpIS/iIiIiIiIiIiIiIiIiIiICSnhLyIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImpIS/iIiIiIiIiIiIiIiIiIiICSnhLyIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImpIS/iIiIiIiIiIiIiIiIiIiICSnhLyIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImpIS/iIiIiIiIiIiIiIiIiIiICSnhLyIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImpIS/iIiIiIiIiIiIiIiIiIiICSnhLyIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImpIS/iIiIiIiIiIiIiIiIiIiICSnhLyIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImpIS/iIiIiIiIiIiIiIiIiIiICSnhLyIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImpIS/iIiIiIiIiIiIiIiIiIiICSnhLyIiIiIiIiIiIiIiIiIiYkJh/i6AVMzhAHuRv0vhvJBwsNk8tz2z1R882wZWr7+IiIjV+0Kr1x/UBiIiYm3qB9UGVq+/iIhYmxn7wVKe6g/N2AZWjwf9GQsp4R+g7EWwbLq/S+G8oRMhNMJz2zNb/cGzbWD1+ouIiFi9L7R6/UFtICIi1qZ+UG1g9fqLiIi1mbEfLOWp/tCMbWD1eNCfsZCm9BcRERERERERERERERERETEhJfxFRERERERERERERERERERMSAl/ERERERERERERERERERERE1LCX0RERERERERERERERERExITC/F0AEREREfG+U3lw4jQU2yE0BOKjoXY02Gz+LpmIiIiI9xWVQMYpKCg2/h8ZBg3iICzUv+USERER8QWHwxgXOpUPJXYjBqobA7Wi/F0yEfEEJfxFREREglB+EazbBzsOQ2omnMw7e5laUdCsLpyTCH2TITbS9+UUERER8QaHA/YeM+KhA8ch7QTYHeWXCQ2BJnWgRT3okwwt6+liSBEREQkep/Jg9V7YlQ6pWZBbcPYydWKgeQJ0ago9kyBCWUMRU9JXV0RERCSInDgNX281BrdL72CrTE4+/JJm/PliE/RoCRd1ggbxvimriIiIiKfZHbB6D3z/C6SdrHrZEjsczDT+rNplDHaf3x56JSnxLyIiIuaVdsIYG9p00Ih3qnLitPFnSyr8d4NxQ8iwjhAX7ZOiioiHKOEfRDbtWc5Drw8t91pURCzNGrRjWM9buGrgvYSGBvdHbvU2sHr9RUSszPG/we2F6427+11VVAJr9sKG/XBZNxh8DoSEeL6c3qR+UG1g9fqLiFjdsVMw+0fjzn53HMyED36Atfvghn5QN9az5fMF9YVqA6vXX0TEykrs8O02WLK1+kR/RfIK4btfjJtIrult3Bhitosg1Q+qDaxa/+CrkTC0+430bX8pDhxknUrn65/f5/XPHuTA0R08cO0b/i6eT1i9DaxefxERqykohvdXwrZDNd9WUYlx0cDmg3DH+eac5l/9oNrA6vUXEbGidftgzmojlqmpX9PgmUVw8wDo0rzm2/MH9YVqA6vXX0TEak7mwZvLjQsYayq3AN5fBVtT4ab+EBZa8236mvpBtYHV6q+EfxBq27Qnw3rdXPb/kQPu4Y6p7Vm85k1uH/40dWo18GPpfMPqbWD1+ouIWElBEby+FPZleHa7e4/BzK9h/DCoFeXZbXub+kG1gdXrLyJiNT/sgo/XeHabBcXwzgoj6d8zybPb9gX1hWoDq9dfRMRKTpw2xnAycjy73fX7IbcQ/nA+hJss6a9+UG1gtfqbbKJWcUd0RCztW56Lw+Hg8PE9/i6OX1i9DaxefxGRYFVih7e+93yyv1TaSZi1DAqLvbN9X1E/qDawev1FRILZxv0w18PJ/lJ2hzHF/47D3tm+L6kvVBtYvf4iIsHqdCG89q3nk/2lfk2Df68yHiVpZuoH1QbBXn/d4W8Raf87eONjEvxcEv+xehtYvf4iIsFo2Q7Yme7aOg8Oh/hoyM6DF76sfvmDmfD5Jri6l3tlDBTqB9UGVq+/iEgwOnEaPloNrow/uxoL2R3w4Y/w6OXmfNTRmdQXqg2sXn8RkWC08Gc4ku388q7GQmA89vGHXTCwnXtlDBTqB9UGwVx/JfyDUH7RaU7mZuBwGM+l+OzH19l9aAPtm/elWQOTn5GdZPU2sHr9RUSsIO0ELN7s+nrx0VAnxrV1vv8FujWH5Iau788f1A+qDaxefxERK3A4YM5qyC9ybT13YqFT+TB/Ldw6yLX1/El9odrA6vUXEbGC7YdgzV7X1nEnFgL4dAO0bwL1arm+rj+oH1QbWK3+lkj4Z2RkMHXqVBYsWEBqaioNGjRg1KhRTJkyhYkTJ/L2228zY8YMJkyY4O+iesT7Xz3B+189Ue61QZ1Hce/Vr/ipRL5n9Tawev0rciTbmIYxrxAiQqF5PWjbCGw2f5dMRMQ9n/xsTOnvCw5g7lqYfKk5zpvqB9UGVq9/RQqLjbsyMnONJFntaOjaHGJMfreqiFjXtkO+nWp//X4Y0BbaNPLdPmtCfaHawOr1r8iB47D7CBQUQ2SY8X1uUc/fpRIRcY/dDvPW+m5/BcVG0v/283y3z5pQP6g2sFr9gz7hv3HjRkaMGEF6ejqxsbF07NiRw4cPM336dPbs2UNmZiYA3bt3929BPeiyfuMY3HU0xfYi9qVtYc7yZ8k4mUpEeFTZMoXFBdzzUk+G9riJMRc+Vvb61I/GciLnCFP+sNgfRfcYZ9rg6Q9uwO6w8/gtH5e9ln06kzundWLc5dO4sOcYfxTdI5yp/5a9K/jzWyPOWre4pBC7vYQlU0t8WWSv2ZUOX2+reMrrhvEw+Bxj0CbEBAksEZFS6Sddn8q/ptJOwN5j0NoEd/krFlIspFjoN7kF8NVW466PvMLy781fB72S4JIuUDfWL8UTEXHbil99v8+VO82T8Fc8pHhI8dBvNuyHpduNx5X9Xot6cGFH6NbC9+USEamJ7YeNC7p9actB45FK7swQ4GuKhRQLWS0WCuqEf0ZGBiNHjiQ9PZ1JkybxxBNPEBcXB8DUqVN55JFHCAsLw2az0bVrVz+X1nOa1m9Lz3bDAOjbfgSdWw3igVcH8fL8u3js5o8AiAiLZPIN7zPp1cGc2+FyWjfpxqqtC/lpx2e88eAWfxbfI5xpg3tHvcq457uwdMNsLuhxIwAzPhlPp1aDTH0SA+fq3yX5PD57OqfcehknDzN+em+uHBAcs138tNuY4rGy5zkezTaugtx3DG7qD6EhPi2eiIjbftjln/2u3GmOhL9iIcVCioUMJ07Dq98aMU9Fikrgpz3GNJB3XQBN6vq2fCIi7jqaDb/6+OJHMGZKOZlnzJIS6BQPKR5SPGTMavT5JvhmW+XLHDgO76wwLoAcETzDwyJiASt3+n6fdgf8uNsc50vFQoqFrBYLBXV6a+LEiaSmpjJhwgSmTZtWluwHmDx5Mt26daO4uJikpCTi4+P9WFLv6pQ0gGE9b2H5pjlsS/mh7PV2zXpx7fkPMfWjWzl2IpWX5o3j3qtfoX7tJn4srXdU1AbxMQlMGv0WMxdOIOPkYb7fPI/Ne5Zz/6jX/Vxaz6vsGDhTYXEBT70/is5Jg7jpwj/7uISety216mT/mX5OgYXrvV0iERHP2XjAP/vdctB3jxHwJMVCioWsGAsVFMOsZZUn+8+UnW8se/K098slIuIJm/wUC9kdRjxkRoqHFA9ZMR76/teqk/1nWrLFPzOHiIi4I68Qfk3zz7437PfPfmtKsZBioWCPhYI24b9jxw7mzJlD/fr1+ec//1nhMr169QKgW7du5V7ft28fV1xxBXFxcdStW5dbb72V48ePe73M3jRm2OOEhITy3pK//u71vxAaEsbdL/WgW5uhDO1+g59K6H0VtUGf9sM5v+t1PDv7ZmYsuIcHR79JfGxwPryrsmOg1Mvz76KwKJ+Hr3/XtwXzAocDPtvoXLK/1MqdkJlT/XIiIv528jRk5/ln38V2Y2p/M1IspFjISrEQwLp9rn1fT+YZg+IiImZQ0bTcVth3TSkeUjxkpXiooBgWb3ZtncWbobDYO+UREfGk1EzXxr496Vg25Bf5aec1pFhIsVAwx0JBm/CfPXs2drudMWPGUKtWrQqXiY425mA7M+F/6tQphg4dSmpqKrNnz+aNN95gxYoVXH755djtJryl7X+a1m/D0G43sGH3t2zZu6Ls9bDQcDomDeBkbgaX9L7djyX0vsraYNzIaRw6vps+7UfQr8Nlfiyhd1VWf4BPVk5n9Y5FPDV2IVERJngATzX2HjWeb+0Kx/+mIxIRCXT+HmRONekgt2IhxUJWioUcDljlxvSOP+0xpvkXEQl0B/14T4a/Y7GaUDykeMhK8dD6FNcTUqcL/TebmoiIK/wZjziAQ1n+239NKBZSLBTMsVDQJvyXLl0KwNChQytdJjU1FSif8H/jjTc4dOgQCxcu5PLLL2f06NF8+OGH/PTTT3z66afeLbSX3XjhY4TYQnjvq9+uXNmydwVfrXuXKwdO4NVP76OgyE+3DPpIRW0QHRFLYkIyrRp38WPJfKOi+m/cvYw3P3+Ex2+ZS+OEJP8VzoM2uPnjzKzTEYmItRw75d/9H/Xz/mtCsZBiIavEQkdPweETrq+XWwC7/PBMbBERVxSXQJYfH0FyzIlHpQQyxUOKh6wSD210c4xHY0MiYgb+HhsyczykWEixUKDFQg6Hg9zcXHJzc3E43J+7w+aoydoBrHnz5qSmprJhwwa6d+9+1vvFxcUkJiaSkZHBnj17SE5OBn67QGDZsmXllm/dujVDhgzhrbfecrksvXv3Jj3dtZGziLBo3piwy+V9uSKvIIc/vtCNawY/yMj+dzPp9fNp16w3d1/xosvbGjezLYXFnjsJ+qL+Z5r02hDO7XA5o4c85PY2PNkGvqp/emYKE6b34eaLnuCqgRNqtC1PHwM10e+mV2ne7QqX1yvKz+a/T3T0QolERDyn/QUT6XzJ5Arfe3A4xEdXvX58FISEgN1uPLe7Mtl58MKXZ7++a9XbbPq04mmvPMlssRCYMxYoFWixEPimDTwZC0HgxEP1kvow9O5P3Fp3zUf3cWDDfA+XSETEc8Ki4rjqqR2Vvl9dPFTTWAhg3iPNnCyt+3wVC2hs6DeBFg9pbKhmLpz4JXWbdnZ5vcyDG1k683IvlEhExHP6XPcSLXtdW+F7noqFoPJ4aMPCx9jz43sulNh1ZoyFSnmqP7R6LATmGxtyp/52u520tDQAunfvzoYNG9zad5hba5lAbm4uAHl5FTfsnDlzyMjIIC4ujlatWpW9vn37dkaPHn3W8p06dWL79u1ulSU9PZ1Dhw65tE5UuPeni5j12SQaJ7TiigH3YLPZePi6d7nrpe4M7Hw1XZMHu7SttMOHyS/y3CX2vqi/p3myDXxR//zC0zzx7lX073iFRwa4PX0M1ETOKRfn8/+fosICl7+rIiK+lnii8nnT4qOhjpNdSEiI88ueKSf7pE/OlWaLhcB8sYCnmS0e9HQsBIETDxVHNnF73Yxjrv92ERHxpfDI2CrfdzYecjcWspcUB00sBBob8jSzxYPBPDZUkJfr1nr5p3MUC4lIwOuYW/kt/t6OhQCyMo97/VxpxliolKf6Q6vHQmC+saGa1v/IkSNurxu0Cf/GjRuTlZXF+vXr6d+/f7n30tLSePjhhwHo2rUrNput7L2srCzq1Klz1vYSEhL49ddf3S6LqyLCqrk9r4bW/LKY5Zvm8MaDm8vq36R+a+4Y8QzT5tzOrEmbiY6o+kf0mRKbNPH4VTtm48k28EX9V2yZz960TRzK2MnyTXPOev+th7bTsG4Lp7fn6WOgJkpyDru13unje2natKmHSyMi4lnREZW/l+3EadiVu9oqEhFm98m50myxEJgvFvA0s8WDno6FIHDioYjQPOwlRYSEhju9jsPhwGazEV6SpXhIRAKbLQR7cSEhYRUHRdXFQzWNhYrys4MiFgKNDXmD2eLBYB4byj95AOjn8noF2amKhUQk4IXbiit9z1OxUFXbio0K9fq50oyxUClP9YdWj4XAfGND7tT/zDv8GzVq5NK6ZwraKf0nTpzIjBkzaN68Od988w3t2rUDYO3atdxyyy3s3buXoqIixo8fz8yZM8vWi4iIYPLkyfzjH/8ot72xY8fy448/up30d1VJISyb7pNdecTQiRBaRQLCVWarP3i2Daxe/5o6eRqeWgh2F89uNw+A3q2qX05ExJ/2HYOXv3J//SevNq7gPnEannRjxu87BkOX5u7v31lW7wutXn9QG9TUeytdfwZti3rG9I8iIoFu2mJIzXRv3ZrGQu0awz0XurdvV6gfVBtYvf41tfcoTP/a9fUeuARa1vd8eUREPGn1Hpj9k3vr1jQWAnj0cmhc2711nWXGfrCUp/pDM7aB1eNBd+qfm5tLrVq1AMjJySE21vWLTABC3FrLBCZPnky9evU4ePAgnTp1okuXLrRt25a+ffuSnJzMBRdcAEC3bt3KrVe3bl1OnDhx1vYyMzNJSEjwRdFFpIZqx0BXF5NRsZHQzbWb+ERE/KJpXThjciKfa17Pf/sWEecNauebdURE/KG5H4dnmmloSMQUWjWAJnVdW6d5gnEBpIhIoPNnLBQRBg3j/Ld/EalY0Cb8mzVrxooVK7jsssuIiooiJSWFhIQEZs2axeeff87OnTuBsxP+HTp0YPv27Wdtb/v27XTo0MEnZReRmrumNyQ4eSFUiA1uHQjhod4tk4iIJ0SEQTMXB648pU4M1DbfbGIiltS6IVzQ0fnlu7fQTEciYh6tGvhx37rzV8QUbDa4ZQBEOfmEo+hwGDPAvxdXi4g4q1Htqh/56E1J9Y1HAohIYAnqr2WHDh1YtGgRp06d4tSpU6xevZpx48aRm5tLSkoKISEhdO7cudw6l19+OStXriQ1NbXstdWrV7Nnzx5Gjhzp6yqIiJviouHei6qfWigyDO4cAuck+qRYIiIecW4b/+y3fxsNgImYycjucFGn6pfr08p4tFGIvt8iYhLdWjifxPOk+GjoqEd7i5hGYh0YP8z47laldjRMcGIMSUQkUISGQN9k/+z73Nb+2a+IVC2oE/6V2bZtGw6Hg7Zt2xITE1PuvXHjxpGYmMiVV17JokWLmDdvHjfeeCN9+/blyiuv9FOJRcQddWPhoRFw2yDjLrczhdjgih7w16ugQxO/FE9ExG29k4wLlnwpxOa/Cw1ExD02G1zW3Xi+4nntzj5v9GkF918CN/WHMM10JCImEhnmn0Hu/m2MAXYRMY/mCfDYSLiuLzSpU/69pnXh+n7w5yuMf4uImMnAtr7fZ1yU64/SFRHfsOTPlC1btgBnT+cPEB8fz9KlS0lMTOSGG27gD3/4AwMGDGDRokWEaJ4SEdMJC4UeLY27/Z+9zghKAGpFGtPcxkb6t3wiIu6IDPf9s7Z7t9J0/iJm1bg2XNMHpoz+LRaKjzKmrU2qr5k7RMScBp8DYT4cpokMgwF+GFgXkZqLDDe+vw9fasRAYPz90AjjQh5fX0wtIuIJDeN9n3w/v70uFhcJVJYMZ6pK+AO0bt2aRYsW+bJIUgOFRfk8/Z8b2H9kO5Hh0dSp1ZCJo16jaf2zb0P8afsi3lj0ECWOElo17sLD179LbFQ8+9K2MOOT8ZzIOUpoSBjntOjLvVe/QmR48GQ2Xlk4kR+3f8qRrP28dv8G2jTt7u8i+Vxk+G9T1WpgW0TM7pIusCUVjmZ7f1/x0XBVT+/vx1Wpx3bx3JzbOJmbQWxUbR6+/l2SGpefv3x7yo+8vOBuAErsRXROGsQ9V00nIiySTXuW8+c3R9CswTlly0+/90ciw6OrXE/ErEJDFAuJSPCoHweXdoNPN/hmf1f10sWPgcyZMY8Nu5fy1hePkleQg81mo1/7y7jj0mfKbvD5ePlzfL3uPewOO80bnMND179Dreg6vq2IeJXN9lsMdOa/RUTM6po+sPsInC70/r6aJ8DQDt7fj4grnBkbLOVwOJg860J2HVrPwr+fKHu9srwhwNGsA8z4ZDypGTsJsYUysv/dXDXoXl9UzWVK+AepR964mKxT6dhsIcRExTH+yum0adrjrOWq+zLcPCWJ8LBIIsKMX7U3XvAnhnS/3mf1cNal/cbRt/0IbDYbC1fN5IW5f+D5u5eXWyavIIfn597B83d/R4uG7ZnxyQT+883fGXf5c4SHRTHhqpkkN+lKib2Ef354E3OWPcutFz/pl/p4w3ldr+W6IZN54NVB/i6KiIh4QESYMQ33y1+Bw+H8etl55f92xvV9ISYA89wvz/8jl/YbxyV9xvL95nk8N2csr9y3ttwyyU268cp9awkLDcdut/O396/hsx9e5ZrBDwDQrME5zHpw41nbrm49M3D1Yr8v177D8x//H0/e9gkDO19V9nphcQGzPpvEup1LiAiLonViNx696QPvFr6GXLkgtFRl9TdLPCwiYkVD2sPmg5CS4fw67sRC7RPN97xaV/rC6vr61Tu+4N0lf8Fut2O3FzN6yMNc3Ps2X1anWs6MecRF1+WxMR+RWC+ZwqJ8Jr8xjK9/fp9L+ozl551fs2TtO8y4dzUxUXH855t/8Pbix5g46hUf1sKznD0GsnOP8/CsC8v+X1B0mrTMvcx94ijxMQmmjAVFRKyidjSM6g0f/OD8Ou7EQqEhxhiUGR5t5EoCGNwbCzFT3+js2FB1ywXqDaXOjA2Wmv/9iyTWa82uQ+vLXqsqb+hwOHjyvau5fuijnN9tNABZp474pF7usGTCf+nSpf4ugtc9fsvHZVchr9zyCc/NGcusBzedtZwzX4bHxswJmC9vRSLCo+jX4dKy/3docS7zvpt21nJrfllMmyY9aNGwPQBXDLiHR/91MeMuf45mDX6bly80JJRzmvVhX/pW7xfeh7omD/Z3EURExMOS6sPoPvDxGufXeeFL1/ZxaVfo1My1dXwhK+coO1PX8cydXwFwXpdrmPnJBA5l7C43iBkVEVP27+KSQgqK8rA5cSuPu+sFElcu9kvPTGHx6n/RocW5Z7331hePYrPZeHfyTmw2G5nZ6d4orsc5c0FoqarqD4EfD4uIWFVICIw9D6Z/BZm5zq3jaizUuDbcPMCcdwI72xdW1dc7HA6enX0z0+5aTnKTrqRnpvB/z7VnUOdRxETF+bA2VXNmzOPMG2EiwqNo3aQ7R7JSANh7eBOdWw0qq1Pf9pfy0OtDTJ3wB+eOgfjYeuUugJ27fBqb935HfEwCYN5YUETEKnolQWomLP/FueVdjYVswJj+kFjHxYL5iSsJYHfHQszUNzo7NlTdcoF4Q6mzY4MAKenb+GHbQh667h2+3zy37PWq8oYbdn1LeFhkWbIfoG5cIx/UzD0muB5H3HHmlGO5+ScxTsvllX4ZhvW8GTC+DMdOHORQxm4fldI7Pln5Mv07XXnW60dPHKBR3ZZl/29UN4nM7DRKSorLLZdXmMviNW8yoIJtiIiIBJoBbeGa3t7Z9iVd4KLO3tl2TR07cZCE+ERCQ43rV202Gw3rtuDoiQNnLZuemcIfX+jGNU/WJza6NiP731P2XlrmHu5+qSfjX+7Dpz+86vR6ZtA1eTAN6lR/tYbdbueFuX9g/FUzCP/dIwvyCnP5cs1b3D786bILHhLiG3ulvJ5UekFoaZk7tDi3bED/96qqv4iIBL46MTB+GNSr5fltN64Nd18AtaI8v21vc7YvdKqvt9nIyT8BwOn8bOJj6pm+z8zMTmfF5nn063A5AG2b9WL9rm/IzE7H4XDw7Yb/cLrgFNmnM/1cUve5Eg+dafHatxje9w7AvLGgiIiV2GxwZU8YfE71y7oqxAY39oeeSZ7ftje4kvNydyzEbH2js2ND1S3n7HZ8ydmxweKSIl6cdyf3XTOLkJDQcu9VlTfcf3Q7tWMb8PQHN3DXiz148t2rSTu+1/sVc5Ml7/C3imdn38qmPcsAePqOL856v6ovw5lXv0z96FYcOGjfvC93XPoMdWo18E0F3PDht1M4nLGbqX/81q31i4oLefqD6+nV7mIGdbnaw6UTERHxjvPOgdox8PFqyCmo+faiwuHqXtDPZFPXVqZxQhKzHtxEXkEOz8y+mZVbFzC0+w20adqT2Y+lEhtdm2MnUnnsrUupHVuf87tdV+V6wWb+9y/QKWkg7Zr1Ouu9tIw9xMUkMHvpFNbv+obI8GhuuehJera9sIItBa7KLgiFqutfykzxsIiIFdWrBfddDB/9BNsPe2ab3VrAdX0h1tx57TKV9YXV9fU2m42/jJnDU++NIioilpy8LJ64dQHhYRG+roLH5OZn8/g7I7luyGTOaW5cOdu9zVBGn/8Qf3nnckJtoQzsbIwJhYYEz9BpVfFQqW0pP5BzOotz/3chRLDEgiIiwc5mM8ZxGsTBZxugsKTm26wdbST72yfWfFu+4mzOC9wfC1HfaD7//vopBnUeRctGHUjPTHF6vZKSYjbuWcr0CT+R1LgTn/34On//4DpevW+d9wpbA8ETtVrIxBn9OZSxq8L3XntgAw3rNAfgkRvfB+Crde/xry8eYUoFSf/qvHD39zSs24LikiLe+fIvTJ1zm1vb8YW5y6excusCpo77ptw0vKUa1mnB+p1fl/3/SFZKuZN/cUkRT39wPQlxidxz5cs+K7eIiIgndG0OyQ1g3lrYePZN7k5rnwjX94O6sZ4rmzc0qNO87Irb0NAwHA4HR7MO0LBOi0rXiY6sxZDuN7B0/X8Y2v0GYqPiz9heM4b2uJEt+1aUJfwrWy8QOBsPVmdf+lZWbJnPC/d8X+H7JfZijmTtp2XDjvzh0mfYfWgDj7xxEW8+tM2v05i5Uv+qLgitrv5grnhYRMTK4qPhziGwdh8sWAf5Re5tJzbSeGRS95bVL+tPnuoLq+vrS0qK+c+3/+CJ2xbQNXkwvx5cy1/fuYI3Jm2hdmx9r9XPW07nn+LPbw5nQKcrufb8B8u9d8WAe7higDGj0/b9P9GgdrNy8WKg8dQxcKYv17zFRb1uLRsrC9RYUEREzmazGTeEdGgCs3+CPUfd31bfZLiqF8QE2PV91fV9zqrJWEgg9Y2eGhsyK2fHBjfv/Y6jWQf47w8zKbEXc7ogm5unJDFz4toq84YN67agTZMeJDXuBMCwXrcw45N7KC4pIiw03Kd1dYYS/iY0/d4fXVr+4t638fL8u8jOPU58bL2y1535MjSsa/w7LDScUefdz+1T23mmEh4277sXWLZxNs+O+6bc4wzO1Oec4cz8ZDwHjv5Ci4bt+fSHVxnSzRi0Lykp5ukPbiAuJoEHrn3DdM/oFRERAWO62bHnwcFMWLUTfk6BIieu6g4Nge4tYFA7SKpvjmfU1q3VkDZNe/LN+g+4pM9YVmyZT/06zc66YvtQxm4a1W1JWGg4RcWFrNr6Ca0SuwJwPDuNurUaERISwun8U/y0fREj/jd9aVXrBQJX48HKbN27giNZKYx9ti0AmafSeWneODKz0xg54G4a1m1BiC2EC3qOAYxn3zZOaMW+tC1+HeR1tv7VXRBaXf3BPPGwiIgYMUzfZOjcDNbuhVW74Gi2c+sm1jFiod5JEBl443dn8VRfWF1fv/vwRo5nH6Zr8mAAzmneh/q1m7H70AZ6tbvIcxXygbyCHP705nB6nzOcMcP+ctb7x7PTqBefSH7had5b8leuGzLZD6V0nqeOgVJ5BTl8t/ljZk787TnHgRoLiohI5erHwYRhsPsIrNwFWw6C3VH9epFh0CcZBrY14qJAVF3fFx4W6VQCuCZjIa2bdg+YvtFTY0Nm5ezY4Iv3rCj7d3pmCne92J0P/pwCVJ037NN+BP/6fDIZJw9Rv3ZT1uz4ghYNOwRksh+U8A9KOXknyC88Tf3aTQBYtXUh8bH1iItJKLdcdV+GvMJcSkqKyhLoyzbMpk2THj6tizOOnUhl1qJJJCYk89DrQwGICItkxsTVvLvkr9SLb8LI/ncRExXHA6Pf5Ml3r6LEXkxS485Mvv49AJZvmsPKrQtITuzKXS8adeyUNJCJo17xW7087aV5f2T1L5+TeSqdP715CTGRcbz36NnPrhEREfNrngA3nAtX9IS9R40LAA4eh+w8OHzC+KEXFgJDO0CzBGjd0JzPpr3/mlk8N2css5dOISYqnoeveweA5+f+gf4dr2BApyvYuHspC1dOJyQklBJ7MT3aXMjNwx4HYMWW+Sz68TVCQ8IosRczuOtoLulzO0CV6wWTkQPuLktsA0x6bQijzrufgZ2vAqB2bH26t7mQdb8uoV+HS0nL3Ed65j5aNOrgpxI7z5kLQqurv1niYRERKS8mAs5vbzzL9mAm7M8w/k4/CamZRiwUajMGtZsnQMv60LSuOS56dIUzfWF1fX3DOs3JPJXG/iM7aNmoA4cydpN2fA/NG3jhQcE1UNmYx5lx4YKVL/PrwTXkF+aycssCAAZ3G82YCx8D4NF/XYzDYaeopJBhPW/hyoET/Fklj3DmGCi1fNMckhO70aJh+7LXzBwLiohYmc0GbRsbf07mwb7SsaFM40IAuwNCbNCxqRELNU+A5IbGIx7NzNkEcE3GQtQ3BhZnxgarUlXeMDoilvtGvc5jb10GOIiNqs1jYz7ydpXcZnM4HE5c2yO+VlIIy6a7t+6RrP38/d+jKSjKI8QWQu3YBoy7fBptmnYHyh/oB4/+ynNzxpJ9+njZl6FVYhcA0o7v5an3r8FuL8GBg8SEZO658mUaJySdtc+hEyHUg9O71KT+/uLJNrB6/b3piQVGkFM7Gp4a5e/SiIj4nlnOg1bvCz1R/zMHvuNj6pW72K+yHz6//5ELRkz4/Nw7OJmbQYgthJuH/ZXzul5z1v4CKR48diKVm55uTmJCMtGRccBvF4SC8/V3JR4Gc8RDZjkHiIh4i1nOgzWNBVzpC6vr65dumM3spVMIsYVgd9i58YI/cUGPm87aZyDFAv4SSPGgq/HQfTMHMKLfnQz/3wWwpZyNBUGxkIiIGZjlPOhuP1hVzstTYyHV9Y2e6g99OTZU1XLVbedMVo8H3al/bm4utWrVAiAnJ4fYWPees6qEf4CywkFcFbPVHwLrR50/mOFHHZgnoBER8RaznAet3hdavf6gNvAWs5wDRES8xSznQfWDagOr199bzHIOEBHxFrOcB83YD5YKpIS/r1k9HvRnwj/ErbVERERERERERERERERERETEr5TwFxERERERERERERERERERMSEl/EVERERERERERERERERERExICX8RERERERERERERERERERETCvN3AaRiIeEwdKK/S+G8kHDPb89M9QfPtoHV6y8iImL1vtDq9S/dntXbQERErEv9oNrA6vUXERFrM2M/WMpT/aEZ28Dq8aA/YyEl/AOUzQahEf4uhf+o/tauv4iIiNX7QqvXH9QGIiJibeoH1QZWr7+IiFib+kG1AagNXKEp/UVERERERERERERERERERExICX8RERERERERERERERERERETUsJfRERERERERERERERERETEhJTwFxERERERERERERERERERMSEl/EVERERERERERERERERERExICX8RERERERERERERERERERETUsJfRERERERERERERERERETEhJTwFxERERERERERERERERERMSEl/EVERERERERERERERERERExICX8RERERERERERERERERERETUsJfRERERERERERERERERETEhJTwFxERERERERERERERERERMSEl/EVERERERERERERERERERExICX8RERERERERERERERERERETUsJfRERERERERERERERERETEhJTwFxERERERERERERERERERMSEl/EVEREREREREREREREREREwozN8FkIo5HGAv8ncpnBcSDjab57ZntvqD59rAjHUv5enjQKzLjN8DnQc92wZWr7+IiIjV+0Kr1x/UBiIiYm3qB9UGVq+/iIgrlPAPUPYiWDbd36Vw3tCJEBrhue2Zrf7guTYwY91Lefo4EOsy4/dA50HPtoHV6y8iImL1vtDq9Qe1gYiIWJv6QbWB1esvIuIKTekvIiIiIiIiIiIiIiIiIiJiQkr4i4iIiIiIiIiIiIiIiIiImJAS/iIiIiIiIiIiIiIiIiIiIiakhL+IiIiIiIiIiIiIiIiIiIgJKeEvIiIiIiIiIiIiIiIiIiJiQmH+LoCIiLcUFMHuo3AwEw4eh6xcOJVvvJdTAAt/huYJ0LoR1Inxb1lFREREPM3hgENZkJIBqZlw+MQZsVA+fLAKmteDFvWgZX0Isfm1uCIiIiIel5MPe0rHhjJ/i4VO5cPrS41xoWYJ0KYRxEb6t6wiIiIi7lLCX0SCTtoJWLUL1u6FguKKlymxw/JfjH/bbNCxCQxqB+ckarBbREREzK2gCNalwKqdRpK/IiUOY5l1Kcb/69WCgW2hbzLUivJNOUVERES8weEwLnhcuRM2HjDGgH7P7oBf0ow/AGEh0L2lMTbUsp4xViQiIiJiFkr4i0jQyC8y7tr/aY9r6zkcsO2Q8adFPbipPzSu7Z0yioiIiHjTpgMwd61xN5srjufApxvgyy1wRQ8Y0FYXQYqIiIj5nDgNH6+G7YddW6/YDuv2GX86NYXr+kHtaO+UUURERMTTlPAPIpv2LOeh14eWey0qIpZmDdoxrOctXDXwXkJDg/sjt3obWLn+O9Phwx+NH3Y1ceA4TPsCLu0GQzvoim6zsfJ3oJTV28Dq9RcR6zpdCHPXwIb9NdtOYTHMW2tcOHBTf6gb65nyiW+oH1QbWL3+ImJt6/YZcUx+Uc22s+0QPLsIru0DPZM8UjTxIav3hVavP6gN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zL2TrftWUGIvpkPL/ky4asZZgbKnjwNPOl0AWaeNge6wECPZXyfGeoPaTywwfuDWjoanRvm7NJWz8jmglCfa4MwfOvEx9cp+6MBv54W2TXty09PNSUxIJjoyDvjtYom043t56v1rsNtLcOAgMSGZe658mcYJSRXuz5Nt4Kv61+S8+HuBfA48k1nOAyLiWSV24wLIgiLj/xFhUL8WhIVWvV6wMdM5sKZ9oTP9YKvGnavt69OO7+X5uXdwMjeDEFsINw/7K+d1vabCfQZaLOBrgRgP+poZ4iEznQdExLOy84w/xXZjbKh2tDVv/jDLebCm/eCxE6mVjnfAb/FQ6ybd+Pu/R1NQlEeILYTasQ0Yd/m0cjO/OjMuCoEXC3hqvLiUMwl/xYOKhUTENbm5udSqZUwtlJOTQ2xsrFvbUcI/QJmtMwu0YMYf/J3wDwRmCGisziwBnRm/BzoP6kedWc6BZjkPiIh4g5nOgVbvC61ef1AbeIuZzgMiIt5glvOg+kG1gdXr7y1mOQeIWIWnEv56MqOIiIiIiIiIiIiIiIiIiIgJKeEvIiIiIiIiIiIiIiIiIiJiQkr4i4iIiIiIiIiIiIiIiIiImJAS/iIiIiIiIiIiIiIiIiIiIiYU5u8CSMVCwmHoRH+Xwnkh4Z7fnpnqD55rAzPWvZSnjwOxLjN+D3Qe9GwbWL3+IiIiVu8LrV7/0u1ZvQ1ERMS61A+qDaxefxERVyjhH6BsNgiN8Hcp/MfK9bdy3UVK6XugNrB6/UVERKzeF1q9/qA2EBERa1M/qDawev1FRFyhKf1FRERERERERERERERERERMSAl/ERERERERERERERERERERE1LCX0RERERERERERERERERExISU8BcRERERERERERERERERETEhJfxFRERERERERERERERERERMSAl/ERERERERERERERERERERE1LCX0RERERERERERERERERExISU8BcRERERERERERERERERETEhJfxFRERERERERERERERERERMSAl/ERERERERERERERERERERE1LCX0RERERERERERERERERExISU8BcRERERERERERERERERETEhJfxFRERERERERERERERERERMSAl/ERERERERERERERERERERE1LCX0RERERERERERERERERExISU8BcRERERERERERERERERETEh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unxNnZaYQaAGtBWx+nnA6vX3JrN8D3QMiIh4h/oB71HC3xx9ob4D3mOGz19ExCz9AFi7LzBj3UsFen8YqN8BM3/m1Qn0Y0L8y1MJf03iLCIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImFObvAkh5IeHG8zzMKiTcc9sxWzt4qu6l27Jy/UVE5wGr1190DIiIWJ36AbWB1enzFxERK/cFZqx7KfWH7jHzZ14dHRPiC0r4BxibDUIj/F0K/7N6O1i9/iKi84DV6y86BkRErE79gNrA6vT5i4iIlfsCK9fdqvSZi9SMpvQXERERERERERERERERERExISX8RURERERERERERERERERETEgJfxERERERERERERERERERERNSwl9ERERERERERERERERERMSElPAXERERERERERERERERERExISX8RURERERERERERERERERETEgJfxERERERERERERERERERERNSwl9ERERERERERERERERERMSElPAXERERERERERERERERERExISX8RURERERERERERERERERETEgJfxERERERERERERERERERERNSwl9ERERERERERERERERERMSElPAXERERERERERERERERERExISX8RURERERERERERERERERETEgJfxERERERERERERERERERERNSwl9ERERERERERERERERERMSElPAXERERERERERERERERERExISX8RURERERERERERERERERETCjM3wWQijkcYC/ydymcFxIONpvntme2+oNn28Dq9RcR0XlQrH4MWL3+IiKivsDKzPjZl/LUMaA2EBERM/YF6gM8x4yffykrx0P6DviPEv4Byl4Ey6b7uxTOGzoRQiM8tz2z1R882wZWr7+IiM6DYvVjwOr1FxER9QVWZsbPvpSnjgG1gYiImLEvUB/gOWb8/EtZOR7Sd8B/NKW/iIiIiIiIiIiIiIiIiIiICSnhLyIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImpIS/iIhYgt0ODofxb4fjt3+LiIiIWEVBMdjPiIdERERErKREY0MiIhKkwvxdABEREW8oscPWVNiVDgcz4fAJKCox3svOhz/NheYJ0CwBOjaFNg3BZvNrkUVEREQ8Kv0kbNxvxEIHMyE777f3svPhmUVGPJTUAHq2hOgI/5VVRERExNMKio1YaO8xIxZKP/HbxY/Z+fD4AmhWF1rUg67NjTEiERERM1LCX0REgkpOPqzYCT/uLj+o/Xv5RbDriPFn2Q5oFA+D2kG/1hCh3lFERERMyuGAzQdh5U4jzqlK+knjz9p98N+foVcrOL89NK7tm7KKiIiIeEPGKfj+V1iz1xj/qUxOPvySZvz5aisk1TfGhnq2hBDNjSwiIiailEYQ2bRnOQ+9PrTca1ERsTRr0I5hPW/hqoH3Ehoa3B+51dvA6vUXa3M4YMN+mL8OcgtcX/9ItrHuip1wU3/jR56Yj86DYvVjwOr1F7G6rFyYs9oYtHZVYYlxweSavXBxZxjWCUI10G066gfE6seA1esvYnV2u5Ho/3zTb7M8uiIlw/izahfcdC40iPd8GcX71BeI1Y8Bq9ffqvSJBqGh3W+kb/tLceAg61Q6X//8Pq9/9iAHju7ggWvf8HfxfMLqbWD1+ov1FBTD7B9h44Gab+toNrz8FQzrCCO6QYim+TclnQfF6seA1esvYkUb9hvJ/qruYnNGiR0Wb4YtB+H2wVCvlmfKJ76lfkCsfgxYvf4iVpSdB++uMKbvr6l9x2DqF3B1LxjQtubbE/9QXyBWPwasXn+rUcI/CLVt2pNhvW4u+//IAfdwx9T2LF7zJrcPf5o6tRr4sXS+YfU2sHr9xVryi2DWMuPHmKc4HPD1NuN5btf3U9LfjHQeFKsfA1avv4jVrNoF89aAw4PbTM2C6V/BPcOMRx+JuagfEKsfA1avv4jVZOXCK98aU/l7SlEJfLzGmEXyos6e2674jvoCsfoxYPX6W40m6LOA6IhY2rc8F4fDweHje/xdHL+wehtYvf4SvIpL4M3vPJvsP9PqPfDJOu9sW3xL50Gx+jFg9fqLBLOf98FcDyf7S53Mg9e+NQbRxdzUD4jVjwGr118kmOXkw2tLPZvsP9Pnm4zHBIj5qS8Qqx8DVq9/sFPC3yLS/vfljY9J8HNJ/MfqbWD1+ktwWrIFdh/x7j5W7IRNHnhUgPifzoNi9WPA6vUXCUYZp4xp/L3pxGn48Eewe+OKAvEp9QNi9WPA6vUXCVZz1xqPZ/SmhT9DaqZ39yG+ob5ArH4MWL3+wUxT+geh/KLTnMzNwOEwnsvx2Y+vs/vQBto370uzBu38XTyfsHobWL3+Yg0HjsO3211b58HhEB9tPNfthS+dX2/uWmjdEGpFubY/8R+dB8Xqx4DV6y9iBXYHzP4JCktcW8+deGjXEfhhFwzS6cM01A+I1Y8Bq9dfxCo27nf9Jg13YiG7w7gA8sHhEBbqejnFP9QXiNWPAavX32oskfDPyMhg6tSpLFiwgNTUVBo0aMCoUaOYMmUKEydO5O2332bGjBlMmDDB30X1iPe/eoL3v3qi3GuDOo/i3qtf8VOJfM/qbWD1+kvwcziM56i5eqdZfDTUiXF9fzn5sGgj3HCu6+sGiqISYzaE3AIIC4FGtSGxjr9L5T06D4rVjwGr11/ECtbuhT1HXV/P3Xjo0w3QvYW5L4BMOwFHTkKx3ahHm4bBO2ivfkCsfgxYvf4iVlBYDPPWur6eu7HQ4RPG1P4XdHR93UCRV2jEj/lFEB4KzRKgXi1/l8p71BeI1Y8Bq9ffaoI+4b9x40ZGjBhBeno6sbGxdOzYkcOHDzN9+nT27NlDZqYxF0/37t39W1APuqzfOAZ3HU2xvYh9aVuYs/xZMk6mEhH+28hMYXEB97zUk6E9bmLMhY+VvT71o7GcyDnClD8s9kfRPcaZNnj6gxuwO+w8fsvHZa9ln87kzmmdGHf5NC7sOcYfRfcIZ+q/Ze8K/vzWiLPWLS4pxG4vYclUF28VEvGhfcd8P5Xaun0wsgfERvp2vzV18rTxg/SnPUay/0ytGhh36vVsCTabf8rnLeoLRbGAYgGRYOZw+P5ZsoXFsHoPXNjJt/utKYcD1qcYj2lKySj/XmwknNsaBreH2tF+KZ7XKBYSxUKKhUSC3fr9kFNQ/XKetOJXGNIeQkz2oOQj2fDdDliXYsR0pWxAhyZwfns4J9FfpfMexUOieEjxkJUEdcI/IyODkSNHkp6ezqRJk3jiiSeIi4sDYOrUqTzyyCOEhYVhs9no2rWrn0vrOU3rt6Vnu2EA9G0/gs6tBvHAq4N4ef5dPHbzRwBEhEUy+Yb3mfTqYM7tcDmtm3Rj1daF/LTjM954cIs/i+8RzrTBvaNeZdzzXVi6YTYX9LgRgBmfjKdTq0GmPomDc/Xvknwenz2dU269jJOHGT+9N1cOCI7ZLiR4rdzp+30W241BbjNdyX0oC2YtM6apq8i+Y8afHYfhxnMh1GQ/WKuivlAUCygWEAlmKRlGP+9rP+yGoR3MM8hdYjem3/05peL3cwuMR0St2wd/HApN6vq0eF6lWEgUCykWEglmDod/xoayTsP2w9C5me/37a4dh+GdFeUT/aUcGPXZfhgu6QLDuwTXDSGKh0TxkOIhKzHJz3T3TJw4kdTUVCZMmMC0adPKkv0AkydPplu3bhQXF5OUlER8fLwfS+pdnZIGMKznLSzfNIdtKT+Uvd6uWS+uPf8hpn50K8dOpPLSvHHce/Ur1K/dxI+l9Y6K2iA+JoFJo99i5sIJZJw8zPeb57F5z3LuH/W6n0vreZUdA2cqLC7gqfdH0TlpEDdd+Gcfl1DEeYXFsOmgf/a9LsU/+3XH8Rx4bWnlyf4zrdvn3jR4ZqK+UBQLKBYQCSY/7/PPfo/nwL6M6pcLFHPXVJ7sP9PJPCNuysypflmzUiwkioUUC4kEk6PZvp/5sdQ6P8Vh7kjJgLe+rzjZ/3tLtsDyX7xfJn9SPCSKhxQPBbOgTfjv2LGDOXPmUL9+ff75z39WuEyvXr0A6NatW9lrK1asYNiwYSQmJhIZGUmzZs24/vrr2bFjh0/K7S1jhj1OSEgo7y356+9e/wuhIWHc/VIPurUZytDuN/iphN5XURv0aT+c87tex7Ozb2bGgnt4cPSbxMfW82MpvaeyY6DUy/PvorAon4evf9e3BRNx0eETxt1a/pB+wrkfSYHgyy2Qk+/88j/uhoN++rHsK+oLRbGAYgGRYLH/uP/2fcCP+3bFgePGI42cdSoflmz1XnkCgWIhUSykWEgkWPgzFjLT2MknP0OxC7Nxf77x7MdBBhvFQ6J4SPFQsArahP/s2bOx2+2MGTOGWrVqVbhMdLTxkL4zE/5ZWVl06dKF6dOn89VXX/Hss8+ybds2+vfvT2pqqk/K7g1N67dhaLcb2LD7W7bsXVH2elhoOB2TBnAyN4NLet/uxxJ6X2VtMG7kNA4d302f9iPo1+EyP5bQuyqrP8AnK6ezescinhq7kKiIGD+VUMQ5/rqCG8DuMC44CHS5BbAhxfX1VvlhOjxfUl8oigUUC4gEg+IS/8Yj/ozFXLFql+vr/JwCp4N4kFuxkCgWUiwkEiz8GY8czzFHUvxgJux3cWam0sdZBjPFQ6J4SPFQsArahP/SpUsBGDp0aKXLlCbwz0z4X3HFFbz44ouMHj2a888/nzFjxrBgwQJOnjzJ/PnzvVtoL7vxwscIsYXw3le/XbmzZe8Kvlr3LlcOnMCrn95HQZETcz+bWEVtEB0RS2JCMq0ad/FjyXyjovpv3L2MNz9/hMdvmUvjhCT/FU7ESeknrb1/Z2zcb/xIc9XPKf6bPcFX1BeKYgHFAiJmdzzHv/21GWKhEjusT3F9veIS/z06ylcUC4liIcVCIsHA3/HIERPEQ+4+emCtiR5Z4C7FQ6J4SPFQMLI5HA6Hvwvx/+zdd3RU1drH8d9MeiehBQgtFOlEelNpFlBAUbGi+KpYQCwo6rVfrw29WLBh16siKsq1FwQEFBWkCihNSiABQhJCElJn3j/OTSSSMjOZdma+n7WylDlll8xkP7Ofc/bxhJYtWyo9PV1r1qxRWlracdvLysrUrFkzZWVlafv27UpNTa3xXIcOHVKjRo307LPPasqUKU7XpU+fPsrMzHTqmPDQKL001YVbEpxwtDhf18zqqXNPvkVjBl6n6S+eoo4pfXTd2CedPtfkZzuopMx9g6A32n+s6S8M1YDOZ+n8obe6fA539oG32p+ZvVNTn+mrS0+9T2cPnlqvc7n7PQDUpM/5/1abPhdUu+2WM6T4qJqPjY+UrFbJZpPy6ljuPu+oNOur419f8997tP3H152osfd1Pe1WdR5xk0vHfvJAd5UU5ri3Qi7w1t9Bfx4LPWX0P1YqOqGZCg9n6IuH+/q6OjUiFiAWAFC9Bs27aeSN1QQpqjsWkhyPh2qKhY4c3KGvnzjZiRp7X0RMQ425d51Lx276dpY2LZzl5hq5xmzzApI5xgIzxELejoMk98RCkvveA2aMhSqY4XMAmN3Q6z5WozbV/w33xtzQslcu1v6tS52osff1v/h5tew51unjigty9Ok//SPhydyQZ5ghFpKIhyRzxkNm+Az4G5vNpoyMDElSWlqa1qxZ49J5Qt1ZKX9SUFAgSTp6tPo31rx585SVlaW4uDi1bdv2uO3l5eWy2WzatWuX7rzzTiUnJ2vChAku1SUzM1N79+516pjIMM8vlzHn0+lKTmqrsYOul8Vi0W0T3tC1T6VpcLdz1CPVuQmcjH37VFRa6La6eaP97ubOPvBG+4tKCnXfG2drYJexbvlS6+73AFCTzv/7+16d+CipgQMfH6vVsf2qk5ub4/TfdG9redj1S8337dur4gLfJ/y9NQ7481joKeXl5ZX/9ef3MrEAsQCA6pWENq5xm6OxkOR6PFRaWuLX44ckRcbWMXtfi8OHc/2mfWabF5DMMRaYIRYyYxxUwV3vATPGQhXM8DkAzK6kuOY19b0xN5SVddBvx5AKhYWu/R0qLy/zm7YxN+QZZoiFJOIhyZzxkBk+A/5s//79Lh8bsAn/5ORk5eTkaPXq1Ro4cGCVbRkZGbrtttskST169JDFYjnu+FNOOUU//PCDJKl9+/ZatGiRGjeueWKlrro4Kzy0jtsy6umX37/UknXz9NIt6yvb37xRO1056lE9Me8KzZm+XlHhMQ6fr1nz5m6/w99s3NkH3mj/sg3ztSNjnfZmbdGSdfOO2/7qrZvUJLGVw+dz93sAqEl4SM0L0+TV8RZ09iru6sRGhalFixZ11NK3rOWuJfxLjh5Wo8RY17/xupE3/g76+1joKSEhIZX/9ef3MrEAsQCA6sUmxNa4ra5YSHLuDv/qWGwlfj1+SJLFGqLSojyFRcY7fWxI+WG/aZ/Z5gUkc4wFZoiFzBgHVXDXe8CMsVAFM3wOALOzqqzGbd6YG0qIi/LbMaSCveiQS8cVHd7rN21jbsgzzBALScRDkjnjITN8BvzNsXf4N23a1OXzBOyS/tOmTdPs2bPVsmVLLVy4UB07dpQkrVy5UhMnTtSOHTtUWlqqKVOm6Nlnnz3u+D/++EO5ubn6888/9fjjj+vAgQP64Ycf1KqV84G+K8pLpMXPeKUotxg2TQoJd9/5zNZ+yb19EOztB2qzYps072fXjr3/HCOXnVso3f+xa+eYfobUsqFrx3pLUal030dScc3ff6t18gnS+D6eqZOz+DvoOfd9JB0+KiVESQ+M93Vtahbs74Fgbz+AmpXbpDvel0rLXTu+vvFQ31TpkoF17+dr81dJy/5w7pjIMOmBc6SIMM/UyVmMBZ5hhljIjL/7Cu56D9AHAGrz39XS4s2uHeuOuaFHJxhxgz/bnyc98qnzx53XVxrS0f31cYUZxwIzjAFmiIUkc/7+KwRzPGSGz4C/KSgoUGyscWF/fn6+YmKcu+i6gtWdlfInM2bMUMOGDbVnzx517dpV3bt3V4cOHdSvXz+lpqZq+PDhkqSePXtWe/wJJ5yg/v3768ILL9R3332nI0eOaObMmd5sAgCgGilJvis7xCo1a+C78h0VGSb1Pf5pNXUa7Cdf6AAAQM1CrFJKou/Kb+XDWMwZgzs4f0zfVP9J9gMAgJq19GE80jjO/5P9ktQ0Xuro5MLDEaFSHxfmkwAAvhewCf+UlBQtW7ZMZ555piIjI7Vz504lJSVpzpw5+vzzz7VlyxZJNSf8j9WgQQO1b99e27Zt83S1AQB1aJYghYf4puyURCnUR2U764weUqOaV/w9zmndjC+DAADA/7Vq5LuyW/uwbGckJ0indnV8/0Zx0undPFcfAADgPr6MR8wSC0nGKo5RTlyccF5fc1zMAAA4XsAm/CWpc+fO+uyzz3TkyBEdOXJEP//8syZPnqyCggLt3LlTVqtV3brV/Y3+wIED+uOPP9SuXTsv1BoAUJvQEKlXG9+U3TfVN+W6IjZSum6E1MSBJP6ILtKoHp6vEwAAcI9+PopJkhN8e0eds0b3lIZ3qXu/pvHS9cON+AkAAPi/hrFSuya+KdtXcZgrkhOMuaG6YhyrRZrQz1zzXgCAqkJ9XQFf2Lhxo+x2uzp27Kjo6Ogq2y699FK1b99eaWlpatCggbZu3aonn3xSoaGhuvnmm31UYwDAsQZ3lH7a7t0yzbisWcNY6ZYzpJU7pOVbjOe3HevE1sZz2Xz1JRkAALimRaLUtrH050Hvlju4g2SxeLfM+rBYpLEnSl2bS8u2SOv3SDb7X9uTE4w29U3lbjYAAMxmcAdp+wHvltkkXurQ1Ltl1lerhtLto6UV26Qft0m5hVW3D2pvzA019+EjowAA9ReUCf8NGzZIqn45/wEDBuitt97S008/raKiIrVs2VLDhg3TP/7xD7Vu3drbVQUAVKNlktS+qbRtv/fKHNDenBPBkWHSSScYX94yD0vPfisVlEhxkdLlQ3xdOwAA4Kqhnbyb8I+JMO9dX+2aGj9HiqRHP5MKiqXYCOn2M811AQMAAPhLj5ZSYoyUU+C9Mk/pZM7YIS5KOq27NKKrlJErvfCdMTcUHylN6O/r2gEA3IGE/99MnTpVU6dO9XaVgHp5bsE0rdj0ifbn7NILN61R+xZpx+2zZtsivfrFHTpanC+LxaL+nc7UlaMfldVq1dHifD3w1rnamv6rym1lWvBgrtfbADjr/H7SE19IpeWeLyspRhpt8iXvLRapWQPjkQiSsVxbICkpLdJD71yoXfs3KSIsSg1im2ja+BfUolH74/Z9f8nj+nbVm7LZbWrZ+ATdesHrio1qoKMlBZrx4nCVlBVJkpLimunGc19UclIbL7cGjko/uFWPz7tchwuyFBOZoNsueENtkqs+sHnTzhV6+qPrJEnltlJ1azNE15/9jMJDI5SZvVOPz5ukbfvWKDmxrebcstah4/xFfdu/bvsS/eOVUUppfELl/s/csEIRYVGSpD8zNujZBTcoJ9+4uuqKMx7SSd3He6l1AOrSo6XULUX6Ld075Z3bx5wXPx4rLlIK/d+DDUOs5pywBwAAhtAQ6cL+0guLvFNeamNp4PFTDKYSYpVSkv6aGyIWAoDAQcI/QDmSAJbqnihe+ftXev3ru1VWVqKI8GjddO4ctWvu3/3mTNKnpKxYcz6drlVbvlZ4aKTaNeupOy5++7j9vlr5uv79/v/p/ss/1uBuZ3uhFc45qcd5mjB0hm5+vubbdeOiEnXXJe+pWcNUlZQWacZLI/Xtr2/p9L6TFBISpguG3a64qCTd+uJQ71UcqIem8cZzWf+72vFj8o5W/a+jLhogRZh8gjsYjO4/Wf06jZLFYtGCH57VrA+u0r+vW1Jln1+3fKuvV76u2Tf8rOjIOL2z8F967cu7NG38c4oIjdJjkxcqOjJOkjR/6ZN6/r836p9X/NcHrYEjnp5/jUb3n6zT+07S0vUf6vF5k/TcjSur7JPavKeeu3GlQkPCZLPZ9M+3ztWnPz6vc0++WdGR8brijH+poOiwXvvyLoeP8xf1bb8kpTQ+ocqFDhWKSgp17xvjdPuFb6lb2yEqt5XrSGG2N5oFwEEWi3EB5I4DUmGJ48e5Eg/1aGk8Cgjm4ej34ryCQ7ptzojKfxeXFioje4c+uO+A4qOT9PPmL/TG13fLZrPJZivT+UNv02l9Lvd2c+AERy4IrM/v3dF5FF9xpP2SdPtLpynnSKYsFquiI+M0Zdwzat/ixMrtlz7cRmGhEQoPNS6EvGj4nRqadoEk/+8DIJic0MxIwq/Y5vgxrsRCYSHSRQMD7+aJQOdojqRCTTmAusYM+Bd3xEK1xQFmez848jmoqz+c/SzBN4Iy4b9okZcu+/MhRxLAUu0TxUcKc/TI3Es067qlapPcVRt2LNOj716il2/9zRtNqBdHkj6S9OoXd8hiseiNGVtksViUnZd53D6Z2Tv15c8vq3OrAV6ouWt6pJ5c5z7HDjrhYZFq1zxN+3N2Gv8OjdCJ7YcrM3unh2oIeMYpJ0i7sqS1ux3bf9ZXzpdxZk+pQ7Lzx8G7wsMi1b/z6Mp/d241QB9+/8Rx++3Yt07d2g6pTOr36zRat744VNPGPyer1Vr5ut1uV2FRnixc7u63cvIPaEv6Kj169TeSpJO6n6tnP56qvVnbqiQzIsOjK/+/rLxExaVHK3+v8dFJ6tZ2iNZtX3Lc+Ws7zh+4o/21WbTmXXVuNUDd2hqxZIg1RA1iG7u5FQDqKyFKumyI9PISqdzm2DHOxkPJCdIF/bkDzIwc+V4cH9OwyoVfHyx5Qut3fK/46CTZ7XY9NvdSPXHtEqU276HM7J36v8c7aUi38ZUxE/yPIxcE1uf37sg8ii850n5Jumfi+4qNaiBJWr7hYz0+b5Lm3LKuyj53XTKv2gltf+8DINic3Uval2vMDznC2VjIIuNGkMYMfabjaI5Eqj0H4MiYAf9R31ioQk1xgNneD458DurqD2c+S/Adq68rAM/okXqyGjdIqXWfionikb0ulWRMFB/M3aO9WcYlkfsObVd8dMPKq5+6p56kA7m7tTXdidtpfaAi6VMxmd251YDKxPaxjpYU6KtfXtUVZzxUuW9SfNWsns1m06wPrtKUs2crzI+W8K2v7LxMLVv/ofp3PsvXVQHqxWqVLh0kda/9z53LTusmjTz+ZhCYwMfLn9bAruOOe71DSm+t3rpQ2XmZstvt+m7NOyosPqK8Y+5cnjFnpCb8M1lL13+gG855zpvVhhMO5u5RUnwzhYQY169aLBY1SWylA7nHXwGUmb1T18zqqXPvb6SYqASNGXi9Q2W4epw3uKv9Gdnbdd1TvTTl6b765MfnK1/fvX+TwkIjdPdrZ+maWWl6bO5lys334sPCATisUzNp0hBjiVZ3axovXTdcigmcr0JBw9HvxX/35cpXdUa/K/96wWJRflGuJKmwKE/x0Q0D6rtxoKlrnqcmjv7eHZlH8SVn2l8xUS9JBUWHZaT06ubvfQAEo4gw6ZqhUquG7j+3xWLc2d+rjfvPDc9zJEci1Z0DcHXMgPe5LRaqhdneD45+Do719/5w5RzwvqC8wx+G2iaKWzRqr5RGHZRXeEgbd/6orm0G6ceNn6iw+Igyc3aqQ0ovH9fecTUlfTKytisuOklzFz2s1VsXKiIsShNPvV+9Ovy1dMn8pbPUtc1gdUzp7c0qe1RBUZ7ueX2MJgydoRNa9vF1dYB6Cw2RJp0kfbpG+v53ye6Gc4aFGFeID+7ohpPB69797mHty9qmmdd8d9y2tPbDdP4pt+ru189SiCVEg7udI0kKsf4VEs28ZqFsNpve/e4hvfvdQ5o2/vnjzgNzSU5qozm3rNPR4nw9OvdSLf/tIw1Lu9Bjx/mbmtrRvkUvzb0rXTFRCTqYm667Xh2thJhGOqXnBJXbyrRm60I9c8NPahjfXK99+Q8989F1uveyD33dHADV6N5Suna49M6PUm6he87ZqZlxYWVspHvOB9+q6XvxsTbu/FH5hTka8L8Lwy0Wi+6+ZJ4eeHO8IsNjlH80R/dd9pHCQsO9UWW4oK55nuo483vfs+/3OudRfMnZ9j829zKt275YkvTQlV8ct33me5fJLrs6teynK0c/qgaxjR2aSwLgfdER0vUjpPd+cnwVyLrERhh39nclxxXwHMkB1DVmwD+4IxaqUF0cUCGQ3w819Qf8Hwl/E5o2e6D2Zm2tdtsLN69RkwYt3VJOTFSC7p34oV798k4VFeerc+uBat20S5WkiC840/7akj7ltjLtz9ml1k266KrRj2rb3jW6/aVT9cqtG5UY11R/Zv6mZRvma9b1Sz3WFm8rLDqif7xyhgZ1HafzTrnF19UB3CbEKp3d25jsnvuTlHXE9XOlNjau3mapNnP6YMkTWv7bR5o5eWGVpcyPNXbQ9Ro7yLjDedOun9Q4IUUxkfFV9rFarRrd/2pNmtmBhL+fatygpbLzMlReXqaQkFDZ7XYdyNmtJg1a1XhMVESshqZdqEWr33Eqce/qcZ7kjvYf+75v3CBFw068SBv+XKZTek5Qkwat1LPdMDVKaCFJGtHrUt35yukebxcA13VoKt1+prRgtfTzdtfPExlmxFX9U1nG35+563vxsb765VWd2vuyygnS8vIyvfPdv3Tf5R+pR+rJ+mPPSt37+li9NH2DEmIaua8xcFhdv3dXOPN7r2sexdPc3f7bL3pLkvTNqjf18he36+FjJuxnXbdUTRJbqay8VK9/dbdmzrtcD1/5hc/7AEDNIsOMG0LW7pY++EUqKHb9XGmtpPP6cuGjP3NXjsTRHEBtYwa8xxuxkFRzHFDBX94PnsgVVtcfMAd+Yyb0zA0r3HIeRyaK09oPU1r7YZKkkrJiXfDPZLVu2sUt5bvK0fbXlfRpkthKVotVw3tdIsl4xn1yUlv9mbFBiXFN9duOZdqfs1OTHusgSco+kqmnPpys7LwMjRl0nfsa5CVHi/N15ytnqM8JZ+iSkXf7ujqAR7RrIs0YLf26U1q+Rdqb4/ixJyQbd/R3S5GsTG6b0offz9LitXP12OSFVZbX+rtDeRlqGN9MRSWFevPrezVh6AxJxuNOwkIjFBedKElasm6e2ib38EbV4YLE2CZq36KXFq5+W6f3naRlG+arUYOU467Y3pu1TU0TWys0JEylZSX64beP1bZZ3b9XV4/zFne0/1BehhJjm8pqtaqw6Ih+2vSZRv1vybZTek7QlytfVUFRnmIi4/XL718otVlPr7cTgHOiwo070Qa2N2KhNbukcptjxyZESYM6GMfGR3m2nqg/d30vrnC0OF/fr39fz0776/mm2/at1aG8feqRerIk6YSWfdUoIUXb9q5R746n1q8BcEldv/ew0AinLgh09vfevsWJtc6jeJq721/htD6X6+n51yqv4JDiY4w1wZskGseEhoRp/Ek36YqZHStf92UfAKhbWivjQsift0s/bJUO5Tt2nNUi9WhpzA114OPs99yVI3E2B1DdmAHv8UYsJNUcB/ydr98P7vocVKipP2AOJPyDmCMTxRVJEUl6Z+GDSms3vMalT/yJI0mfhJhGSms/Qqv++Fr9O49WRvafysz+U62adpYkjRl0XZVBffoLQzX+pJs0uNvZXmiBc5768Br9/Pvnyj6SqTtfOV3REXF6845t+vcHV2lgl7Ea1HWsPlr+tP7Y84uKSgq0fMNHkqSTe56vS0bcJUma/O8eOlxwUIXFebroXynq2W6Y7rjoP75sFuCS8FBjonpAO2nXIWnbfmnPISk9RzpSZEx6h1qlxBgpJUlqmSR1bi41ia/73PBfB3PTNeez6WqWlKpbXzQuVAsPjdDsaT/rja/vVcP45hoz8FpJ0h0vnya73abS8hKN7DVR4wZPlSQdyN2tp+ZfI5utXHbZ1bxhO91x8ds+axPqdtO5c/T4vEmau+hhRUfG67YJr0tSlfFv7bZFWrD8GVmtISq3lenE9iN06ch7JElFJYW6YmZHlZYVq6DosC76V4pG9pqoK0c/Uutx/qK+7V+2Yb4+W/GCQqyhKreV6eQe5+v0vldIMr7cXjT8H7rp2UGyWKxqlNBCN533ks/aCsA5bRoZP2f3kn7ba8RCe7KlA3lSabkxoR0VJjVPNGKhto2lE5oZqyYhcDh6MaRkXOiY2qynWjXpVPlakwYtlX0kQ7v2b1brpp21N2ubMg5tV8vGJ3i45nCVoxcEVnD2917XPIqvOdr+/KO5KiopVKOE5pKkH35boPiYhoqLTpIkHS0pUHl5aeXnZvGauWrf/ERJdc8lAfAPMRHS8C7S0M7S1kxpx0EpPdv4OVoilduNxzk2ijNioZQkqVsLKaHma+MQoOrKAdQ1ZsC/uCMWqi0OCPT3Q3X9AfOw2O12dzzuGG5WXiItfsb1449NAMdHN6xMAEtVJ4H3HPhDj8+bpLzCQ5UTxW2bda88z6wPrtZvfy5Tua1MnVsP1NSzZ1c7UTBsmhTixsf41af9B3PTdfFDLdUsKVVREcaa3BVJH6lq+zMO7dC/P7hShwuyZLVYdenIe3VSj3OrPW9dCX939kF9f/++4O73AADPuO8j6fBR4y6+B8b7ujY14++g5/Ae8BxiAXN8BgAEN7OMg5L3vhdL0o3PDtKo/lfrjP9d+FVh0Zq5mrvoYVktVtnsNl00/E4NP/HiGss1w1hghvdAfX73tc3zuOP3Xtc8irveA672gSPtb9e8px78z/kqLj0qq8WqhJjGmnzWE2rfIq2yjQ+8dW7lhcDNklJ1/binlZzUpnK7N/oAADzFDGOh5L0cybH+ngPYn7Or1jHj78wwBgT677++sVBtcYCj7wdfx0PHcuZzUFNsWNs5/s4MnwF/U1BQoNjYWElSfn6+YmJiXDoPCX8/ZbZJXn9K+PsKk/z8IQfMINCDel8yy99B3gOeQyxgjs8AgOBmlnFQYizwFDO8B8z4u6/gTxPcvmKGzwGA4GaGsVAy51hghjGA37/nBXM8ZIbPgL9xV8KfBfsAAAAAAAAAAAAAADAhEv4AAAAAAAAAAAAAAJgQCX8AAAAAAAAAAAAAAEyIhD8AAAAAAAAAAAAAACYU6usKoHrWMGnYNF/XwnHWMPefz0ztl9zbB8HefgDg7yCC/T0Q7O0HADAWBDMz/u4ruOs9QB8AAMw4FjAGuI8Zf/8Vgjke4jPgOyT8/ZTFIoWE+7oWvkP7g7v9AMDfQQT7eyDY2w8AYCwIZvzu6QMAAGNBsOP3Tx/AOSzpDwAAAAAAAAAAAACACZHwBwAAAAAAAAAAAADAhEj4AwAAAAAAAAAAAABgQiT8AQAAAAAAAAAAAAAwIRL+AAAAAAAAAAAAAACYEAl/AAAAAAAAAAAAAABMiIQ/AAAAAAAAAAAAAAAmRMIfAAAAAAAAAAAAAAATIuEPAAAAAAAAAAAAAIAJkfAHAAAAAAAAAAAAAMCESPgDAAAAAAAAAAAAAGBCJPwBAAAAAAAAAAAAADAhEv4AAAAAAAAAAAAAAJgQCX8AAAAAAAAAAAAAAEyIhD8AAAAAAAAAAAAAACZEwh8AAAAAAAAAAAAAABMi4Q8AAAAAAAAAAAAAgAmF+roCqJ7dLtlKfV0Lx1nDJIvFfeczW/sl9/WBGdtewd3vAwBA8DLjeOjOcTDY2y/RBwCA4MY4SB8Ee/sBAMHNjONghWDOFQV7POjLWIiEv5+ylUqLn/F1LRw3bJoUEu6+85mt/ZL7+sCMba/g7vcBACB4mXE8dOc4GOztl+gDAEBwYxykD4K9/QCA4GbGcbBCMOeKgj0e9GUsxJL+AAAAAAAAAAAAAACYEAl/AAAAAAAAAAAAAABMiIQ/AAAAAAAAAAAAAAAmRMIfAAAAAAAAAAAAAAATIuEPAAAAAAAAAAAAAIAJhfq6AgAAwHOy86U92cbPwSNSQbHxemGJtOR3qWWSlJIoRYT5tp4AAACeUFwqpedIew5Je3P/ioUKiqXP1hqxUMskKSnWl7UEAADwDJtdOpj319xQTkHVuaEftkgtG0rNG0ihIT6tKgCgHkj4AwAQYErKpDW7pOVbjC9z1Sktlxb8avx/WIjUq400pKMx4Q0AAGB2uw8ZsdCaXUbc83dlNmnhxr/+3aqhNLiDdGJrKZyZEgAAYHKFxdIvO6Qftho3gFSntFz6YKXx/9HhUr9UaXBHqXGc9+oJAHAPvsYCABAg7HZp5Z/Sf1f/dbW2I0rLpZ+3Gz8dk6UL+ksNucsNAACYUNYR6f1fpC2Zzh23+5Dx88ka6exeUp+2ksXimToCAAB4SrlNWrxZ+npD9Rc91qRiJcglv0u920jj+0gxER6rJgDAzUj4B5B125fo1heHVXktMjxGKY07amSviTp78A0KCQnsX3mw90Gwtx8IZocLpXk/S5v21e88WzKlxz6Xxp5o3OXGRDfMhHGQPgj29gPBzG437uj/dK2x2pGrCoqld1ZIa3dLE/pLCVFuqyLgFYyF9EGwtx8IZpmHpXdXGBcx1sevO435oQn9pO4t3VI1wGsYB+mDYG1/4LUIGpZ2kfp1Gi277Mo5kqlvf31LL356i3Yf2Kybz3vJ19XzimDvg2BvPxBs9udJL3wn5Ra653wlZdKHK6V9OdJ5fSWr1T3nBbyFcZA+CPb2A8HGZjOWo12xzX3n3LhXeuor6boRUpN4950X8BbGQvog2NsPBJtt+6WXl0jF9bjw8VhHiqRXlxo3hAzv4p5zAt7EOEgfBFv7SfgHoA4temlk70sr/z1m0PW6cmYnffnLK7rijIfUILaxD2vnHcHeB8HefiCYZB2RnvtWyity/7l/3CbZZVzRzZ3+MBPGQfog2NsPBBO73VjC/6ft7j93TqH07EJp2qlSI55lC5NhLKQPgr39QDDZcUCas9i5Jfwd9cka478k/WE2jIP0QbC1n3v2gkBUeIw6tR4gu92ufYc8MAtiAsHeB8HefiBQlZRJLy3xTLK/woptxvPbADNjHKQPgr39QCBbvNkzyf4KeUeNeKs+jwkA/AFjIX0Q7O0HAtXhQumV7z2T7K/wyRppY7rnzg94A+MgfRDo7ecO/yCR8b83b3x0ko9r4jvB3gfB3n4gEH2xTjqQ59wxt5whxUcZk9ezvnK8nC4tpKYsZwsTYxykD4K9/UAgyjxsxCnOcCUWOpAnfbVeGtvL+ToC/oSxkD4I9vYDgcZul+b9LBWWOHecK/HQvF+kOxpL0RHO1xPwF4yD9EEgt5+EfwAqKi3U4YIs2e3Gcyk+XfGitu1do04t+ymlcUdfV88rgr0Pgr39QDD486D0vQt33sdHSQ2inTumtFyau0K68TSW9oc5MA7SB8HefiAY2OxGfFJmc+44V2IhSVr8u9SjldSmkfPHAr7AWEgfBHv7gWDw605p0z7nj3MlHso7Kn38q3TJIOfLA3yBcZA+CLb2B3zCPysrSzNnztRHH32k9PR0NW7cWOPHj9fDDz+sadOm6bXXXtPs2bM1depUX1fVbd765j699c19VV4b0m28bjjnOR/VyPuCvQ+Cvf1AMFi4UbJ7sbydWdLW/VLHZC8W6iZFpdKvfxqPJzhwRLLZpNhI6cTW0uAOPJM3EDEO0gfB3n4gGGzJlHYd8l55drsRf111ivfKdBe73bhY9Iet0u8Z0tESKTzUuHhhSEepS3PJygMfAw5jIX0Q7O0HAp3dLn29wbtlrvpTOqOH1DDWu+W6w5Ei6eft0i87pJwC47UG0VLfVGlgOykuyrf1g/sxDtIHwdb+gE74r127VqNGjVJmZqZiYmLUpUsX7du3T88884y2b9+u7OxsSVJaWppvK+pmZ/afrJN7nK8yW6n+zNigeUseU9bhdIWHRVbuU1JWrOuf6qVhJ16sS0bcVfn6zPcmKTd/vx6+6ktfVN1tHOmDh96+UDa7TfdMfL/ytbzCbF39RFdNPusJjeh1iS+q7haOtH/DjmX6x6ujjju2rLxENlu5vp7pwQc/AaiXQ/nSpr3eL3f5FvMl/Nfvkd75USr+23N3cwuNZ/4u3mxMdJ/TWwphojtgEAsRCxELAYHvhy3eL3PjXmOCODHG+2W7Kr9Ien2ZtP1A1deLSo3k/+8ZUuM440KGpgm+qSM8g3iIeIh4CAhsW/dLB494t0y7jJspzkrzbrn19f3v0idrpPK/rQx18IjxeKivN0hn9pSGdWZly0BCLEQsFGyxUMBObWdlZWnMmDHKzMzU9OnTlZGRodWrVyszM1OPPfaYPv/8c61cuVIWi0U9evTwdXXdqkWjDurVcaT6dRqlC4bN0INXfKo/0lfq6fnXVu4THhqhGRe+pfe+e1jb9xkPPfzhtwX6afOnuuX8V31VdbdxpA9uGP+8Nu78QYvWzK18bfbHU9S17RBT/xGTHGt/99ST9OlD+VV+Xp+xRfExjXT56Q/6sPYA6vLzdu/e3V/ht3Tp8FEfFOyitbul15cen+z/u+VbpHdXGFfHIzAQCxELEQsBge1wofSbDy5+tNuln7Z7v1xXFRZLzy48Ptn/dwePSM98Kx3I80694B3EQ8RDxENAYPtxq2/K/Wnb8Ylzf7Zwo/EogtrqXG4zLgjw9ooJ8CxiIWKhYIuFAjbhP23aNKWnp2vq1Kl64oknFBf313q9M2bMUM+ePVVWVqY2bdooPj7ehzX1vK5tBmlkr4lasm6eNu78sfL1jim9dd4pt2rme5fpYG66nvpwsm445zk1Smjuw9p6RnV9EB+dpOnnv6pnF0xV1uF9Wrr+Q63fvkQ3jX/Rx7V1v5reA8cqKSvWA2+NV7c2Q3TxiH94uYYAnLFtv2/KtdmlnQd9U7azcgult39w/MKIX3caV6kjMBELEQsRCwGB5c+DvrtQr67kuT+Zv0rKPOzYvgXFxkoAXAAZuIiHiIeIh4DAYbdL23wUk+QXm+ciwR0HpM/WOr7/VxuMlRMQmIiFiIUCPRYKyIT/5s2bNW/ePDVq1EiPPPJItfv07t1bktSzZ88azzNq1ChZLBbdf//9nqimV10y8h5ZrSF68+t7//b63Qqxhuq6p05Uz/bDNCztQh/V0POq64O+nc7QKT0m6LG5l2r2R9frlvNfUXxMQx/W0nNqeg9UeHr+tSopLdJtF7zh3YoBcIrNJu3N8V35e7J9V7YzVmyTypy84nzpH0xyBzJiIWIhYiEgcPgyHknPNi6C9HeHj0prdjl3TEau7y4shXcQDxEPEQ8BgeHwUeOxPb5ilrmhZS48AmrZH+6vB/wHsRCxUCDHQgGZ8J87d65sNpsuueQSxcbGVrtPVFSUpJoT/u+//77Wrl3rqSp6XYtG7TWs54Vas+07bdixrPL10JAwdWkzSIcLsnR6nyt8WEPPq6kPJo95QnsPbVPfTqPUv/OZPqyhZ9XUfkn6ePkz+nnzZ3pg0gJFhkf7qIYAHHEwv+4l6j0p3QRf6sptrt2tn3nYuPobgYlYiFiIWAgIHL6MR4pKpUNefl6uK37a5tqFCct9tDwwvIN4iHiIeAgIDL6emzFDwv9IkbR+j/PHbUg3Vo1EYCIWIhYK5Fgo1NcV8IRFixZJkoYNG1bjPunp6ZKqT/jn5eXppptu0hNPPKFLL7203vXp06ePMjMznTomPDRKL0117zfti0bcpcVr5+rNb+7VE9culiRt2LFM36x6Q+MGT9Xzn9yoF9utVURYlNPn7tCxg0rK3PdgZ0+0X6q+D6LCY9QsKVVtk7vX69zu6gNPtV2qvv1rty3WK5/froev+lLJSW3qdX53vw8AHK9h6z4adv2CarfdcoYUX8ef8PjIv/57/zk175d3VJr11fGv//jLWv3rsrMcq6yPRMYn66y7Vrl07JU33KttP77m5hr5l9H/WKnohGbKyMxQSkpfX1enRmaLhST3joPBHAtV8NZ7gFgIMJ8R075UYovq/2bVFQ85GgtJNcdDI84Yq+zdqx2srW8MnPiyWnQb5fRxy1f/qXsuOskDNfIfwRwLScwNSeaJh7zZfnfFQ8RCgHe07jNBfc+fVe02b8wNvfPex7rx7BscrK1vNE4dpFOued/p4+x2acRZl2r/liXur5SfIBby7NyQ5P+5IrPEQpL55oZcab/N9tcytUOGDNGaNWtcKjsgE/67dhnr1rVu3bra7WVlZfrhhx8kVZ/wv+uuu9SxY0ddcsklbkn4Z2Zmau/evU4dExnm/NUjPdsN1beP13wJf+umnfX1zPLKfx8tztfj8ybpylGPaszA6zT9xVP02pf/0HVjn3S67Ix9+1RU6r5L31xpv+R8H7iTu/rA1bZLzrc/M3un/vX2BF191uPq2W6oy+VWcPf7AEA14trVuCk+Smrg4J8Qq9XxfY9VbpPTY5q3JZRFunxs/tFiv29ffZWXl1f+15/barZYSHLvOBjMsVAFb7wHiIUAcyovr/lz7mg85GosJEmHsnO0z4/HUEkqdfVPvTXMr+MDdwjkWEhibkgKnHjIW+13ZzxELAR4R4MONS835I25oaKSUr8eQyUprHGBy8cePlLg9+2rD2Ihz84NSb7PFQVKLCSZb26ovu3fv9/1Z6wFZMK/oMD4Y370aPVXUcybN09ZWVmKi4tT27Ztq2xbtWqVXn75Zf36669uq09ycrLTx4SHun71kKPmfDpdyUltNXbQ9bJYLLptwhu69qk0De52jnqknuzUuZo1b+72q3bMxl194K22F5UU6r43ztbALmN19uCpbjmnu98HAI6XlJhQ47Y8Bz5+8ZHGFzqbTcqr5XlvNZ3LarGpRYsWdRfkQ+HRrif8o8Ll9+2rr5CQkMr/+nNbzRYLSe4dB4M5Fqrg6T4gFgLMy2qx1bitrnjI0ViotnMlNYiXxY/HUEmy2opdOs5Wku/X8YE7EAtVxdyQe5ktHnR3PEQsBHhHfFzNCTBvzA1FhFr9egyVpPho11NfcVFhft+++iAWqsrdc0OS+XJF7mS2eNAfYiGbzaaMjAxJUtOmTV0uOyAT/snJycrJydHq1as1cODAKtsyMjJ02223SZJ69Oghi8VSua28vFzXXHONpk6dqq5du7qtPqtWOb+scHmJtPgZt1XhOL/8/qWWrJunl25ZX9kHzRu105WjHtUT867QnOnrFRUe4/D5tm7ZqpBw99XP0+33BHf1gbfavmzDfO3IWKe9WVu0ZN2847a/eusmNUls5dQ53f0+AHC8I0elez6qflt1y6z93f3nGFdv5xVJ93/sfPkjT+ql/9yZ7vyBXvbU19LOLOeOsVikT956TIkxj3mmUn7ivo+kw0elZsnNKh9x5I/MFgtJ7h0HgzkWquDpPiAWAszrjWXS2t3Vb6srHqpvLCRJK5Z8rljXry/0il93Sv/5wfnjzh3RSa9P99/4wB2Ihf7C3JD7mS0edHc8RCwEeMf2A9Lsb6vf5o25ocmTztPHs85z/kAvKiuX/rmg7gs8/y4mQvpl8fsKC/FItfwCsdBfPDE3JJkvV+ROZosH/SEWKigoUGxsrCRp+fLlzh18jIBM+I8cOVKbN2/WY489plNPPVUdO3aUJK1cuVITJ05UVpaRAUhLS6ty3LPPPqv9+/fr/vvv93KNva9fp1Fa8GDuca+PGzxF4wZP8X6FfOzf1y3xdRW87tTeE3Vq74m+rgYAJ8VFSQlRRmDuCy2TfFOus4Z0dD7h362FlOh8DA+TIhaqilgIgJmkJNWc8Pe0xGj5fbJfknq2lD6OlPKdmOS2SBrU3mNVgh8iHqqKeAiAWaQkGuN2zQtWe5YZ5oZCQ6QB7aVvfnPuuP7tFNDJflRFLFQVsZC5WX1dAU+YMWOGGjZsqD179qhr167q3r27OnTooH79+ik1NVXDhw+XJPXs2bPymKysLN1zzz269957VVZWptzcXOXm5kqSioqKlJubK5ut5mUDAQDwlpYNfVi2Cb7USVJaKym55qcfHCfEKo103+I+AADAg1r5MhbyYdnOCA2RTnUytunfTkqK9Ux9AACA+0SESY3jfVd+iknmhgZ3lGIjHN8/JkI6qaPn6gMAnhSQCf+UlBQtW7ZMZ555piIjI7Vz504lJSVpzpw5+vzzz7VlyxZJVRP+6enpOnLkiK655holJiZW/kjSY489psTERO3e7aNbCAAAOEa3FN+UGxMhtWnsm7KdFRoiXTNMaujApLXVIk0cJLVu5Pl6AQCA+mvbWIr20ZLRvorDXHHyCdIpnRzbt3Nz6by+nq0PAABwn+4+iklaJhkrT5pBQpQ0eZgU5UDcGBkmXXUKKz8CMK+AXNJfkjp37qzPPvvsuNfz8/O1c+dOWa1WdevWrfL19u3ba/HixcftP2zYMF1++eWaNGmSkpOTPVpnAAAc0au19N/V0tES75Y7wGTLmiXGSDedJi1YbSz7W17NQj2tG0pnpUkdGOIBADCNsBDjbvTFm71bbnS4sYqQWVgs0tm9pCZx0sJNUk7B8ftEhxuPQjq9u7HiEQAAMIdBHaRFm7y/rP/gjkaMYRatGhpzQ/9dLW3eV31/dWomjeslNWvg7doBgPsEbMK/Jhs3bpTdblfHjh0VHR1d+XpsbKyGDh1a7TFt2rSpcRsAAN4WHir1S5W+/917ZVpkfJk0m7goaeJg6eze0sod0pfrpdJyow9vGGmeZXkBAEBVgztISzZ7d5K7fzsjhjATi8WYmB/Y3pjk3pwh/bzdiIeiwqT7zzFfmwAAgLGiYefm0qZ93iszKty4CcVsmiYYd/pnHZFW/mlcKFFaLkWESreOlhrH+bqGAFB/QXf99oYNGyRVXc4fAACzObWrFBvpvfJO6eTY8vj+Ki5SGt7lr+V/o8JI9gMAYGaN4qSTTvBeeXGR0siu3ivP3axWqWuKsWx/RTwUHkqyHwAAMxvby7sr9JyVZu7YoVGcNKrHX7FQZBjJfgCBw8R/nl3jbMLfbvf2ojhwxXMLpmnFpk+0P2eXXrhpjdq3SKtxX7vdrhlzRmjr3tVa8GBu5evvL3lc3656Uza7TS0bn6BbL3hdsVENPF53AHBFbKR0fl/p9WWeL6txnDSa6+QAv3f7S6cp50imLBaroiPjNGXcM2rf4sTj9vvyl1f13uJHZbfZlNZ+uKaNf16hIWGV22uKlfxZSWmRHnrnQu3av0kRYVFqENtE08a/oBaN2lfZLyP7Tz341nkqt5XLZitTy6addfO5LykuOlGS9N7ix/TtqjcVGhKu8LBITRn3jDq16ueLJgFwwFlp0qa9Ula+58ua0E+KifB8OYCrHB0LJelAzm7N/niK0rO2yGoJ0ZiB1+nsITdIYiwEADNJTjAS2J+t9XxZHZOlQccPKQAAP0HCP0A5mgB3dL+vVr6uf7//f7r/8o81uNvZHqu3q07qcZ4mDJ2hm58fUue+85c+qWYN22nr3tWVr/265Vt9vfJ1zb7hZ0VHxumdhf/Sa1/epWnjn/Nktd0q/eBWPT7vch0uyFJMZIJuu+ANtUk+/haUSx9uo7DQCIWHRkmSLhp+p4amXSCp9gmClb9/pde/vltlZSWKCI/WTefOUbvmgf05Avxdz1bG0v6/7HD8mLyjVf9bl1CrdMkgc1/BjeDjzIT3z5u/0Btf3y2bzSabrUznD71Np/W5XJLjCXR/cc/E9ysvVly+4WM9Pm+S5tyyrso+Gdl/6o2v79ELN65WYlxT3fvGOH3+00saN3hK5T7VxUpmMLr/ZPXrNEoWi0ULfnhWsz64Sv++bkmVfRrGN9eTU5YrIsyIg577741669v7NWXc09q2d60+/fF5vXLrRkVFxGrhr2/r2QVT9ey0X3zQGgCOCA814pTnFkplNseOcTYWkqQB7aTuLZ2vH+BtjoyFdrtd9795ji4YdodO6Xm+JCnnyH5JCrix0JE5r7rixtpiRX/mTDxcoaa5P7P2ARAshnU2Htuz/YDjxzgbD8VGSBf2Nx4VBPg7R/IkdY2TjsQQ/p4zq+BoDrC2/E9ewSHdNmdE5b7FpYXKyN6hD+47oPjoJG80wy3c0Rf+LOim7xctWuTrKniFowlwR/bLzN6pL39+WZ1bDXB3Nd2mR+rJDu23M3Ojfty4QLdOeF1L139Q+fqOfevUre0QRUcaa/j06zRat7441FQJ/6fnX6PR/Sfr9L6TtHT9h3p83iQ9d+PKave965J5Nf4xq26C4P7LP9Yjcy/RrOuWqk1yV23YsUyPvnuJXr71Nw+2CIAjLugvFZZIv6U7tv+srxw/d4hVmnSS1KaRa3UDfMnRCe/H5l6qJ65dotTmPZSZvVP/93gnDek2XtGRcQ4l0P3JsSsTFRQdlnT8bMyy9R9qYJexSopPliSdNeBazV30cGXCv6ZYyd+Fh0Wqf+fRlf/u3GqAPvz+ieP3C/3r9txyW7mKSgoUFW48r8RisajMVmq8FhGr/KJcNUpI8XzlAdRL28ZGvPL6MqncgaS/M7GQJHVPkc7n5maYgKNj4Zqt3yksNKIy2S9JiXFNJQXeWOjo3FhNcWNdsaK/cyQerlDT3J/Z+wAIBiFW6apTpOe+k9KzHTvGmXgoKly6driUZOLHPCK4OJonqW2crCuGMEPOrIIj8dCRwpxa8z/xMQ0155a1lft/sOQJrd/xvamS/ZJ7+sKfefEJL/CmHqknq3GDur+U1bWfzWbTrA+u0pSzZyss1NzrF5aVl+rJD6/WjefOkdUaUmVbh5TeWr11obLzMmW32/XdmndUWHxEeYUORkk+lpN/QFvSV2lkr0slSSd1P1cHc/dob9Y2p85TMUFg+d/lmp1bDdD+nJ3ad2i74qMbVl4J1z31JB3I3a2t6ea68w8IRCFW6YqTpD5t3HveiFDjC2M3887vIYjVNJ5Vy2JRflGuJKmwKE/x0Q0rYx5HEuj+5rG5l+nif7XUm1/fozsu+s9x2w/k7lbTxNaV/05OaqMDubsl1R4rmc3Hy5/WwK7jqt1WWlaia2al6bz7G2lv1lZdftoDkqR2zXvq3JNu1sRH2uqif6Xoo6VPaurZs71ZbQAu6pZixC0Rbr6loW9b42ICbz4bF3CXmsbCXQc2KSGmsR56+0Jd++SJuv+Nc5RxyFgyLNDGQkfmxuqMG2uJFf2ZM/FwnXN/Ju0DIJhEhUtTRkjtmrj3vAlR0g0jpRRz5fQQxBzNk9Q1TtYWQ5gtZ+ZIPORs/ufLla/qjH5Xur2unuaJvvAnQXeHP5wzf+ksdW0zWB1Tevu6KvX2n28f0JBu49W6aWdlZu+ssi2t/TCdf8qtuvv1sxRiCdHgbudIkkKs5viIHMzdo6T4ZgoJMeprsVjUJLGVDuTurna5tpnvXSa77OrUsp+uHP2oGsQ2rva8FRMEKY06KK/wkDbu/FFd2wzSjxs/UWHxEWXm7FSHlF4ebRuAuoX8b9n9Ts2lj1YZd/zXR8dkY6k2rt5GoKhpwttisejuS+bpgTfHKzI8RvlHc3TfZR8pLDS8cp/H5l6mddsXS5IeuvILr9XZVbdf9JYk6ZtVb+rlL27Xw07UubZYyUze/e5h7cvappnXfFft9rDQcM25Za1Ky0r03IIb9NlPc3TBsBnKyP5Tyzd8pDdu36ZGCc214Idn9a+3L9BTU5Z7uQUAXNG5uTTjTOm9n6St++t3ruhw6dy+Uq/WLF0Lc6ptLCwvL9Pa7Yv0zNSf1Ca5qz5d8aIefHuCnr9xFWOhqsaNjsSKZlHbxZC1zf0FUh8Aga4i6b/kd+mLdY4/7qgmfdpK5/SWYvw/nwlUcjZPUqG2cfLvAilnVsGZ/M/GnT8qvzBHAzqf5aPaepaZc2HmyGaiimmzB2pv1tZqt71w8xo1aeCehwv+mfmblm2Yr1nXL3XL+Xxt/Y7vdSBnt/7747Mqt5WpsDhPlz7cRs9OW6kGsY01dtD1GjvoeknSpl0/qXFCimIi431ca0Ndv3NnzLpuqZoktlJZeale/+puzZx3ebXJgGMnCCLDo3XvxA/16pd3qqg4X51bD1Trpl1Mc0EEEAwsFuPLWIdk44vd6p1Sablz52gcJ43oKvVPZXIb/s2ZWKiuCe93vvuX7rv8I/VIPVl/7Fmpe18fq5emb1BCjPEsi/ok0H3ptD6X6+n51yqv4JDiYxpWvt6kQSvtO7S98t+Z2TvVpEErSXXHSmbwwZIntPy3jzRz8kJFhkfXum9YaLhO63uFnvzwal0wbIaWr5+vts26q1FCc0nS6X2v0HMLblBpWQmT2oBJNIyVrh8h/bRdWrRJOnjEuePDQqTebaTRPaX4KI9UEfC4usbCJomt1L75iZV3LY3sPVGzP75eZeWlphoLPTE39ve40ZFY0VfcFQ/XNffnz30A4HhWqzS8i9S1hfT5OuPxjza7c+do1VA6vZvUlRUf4YfcmSepUNdNA8fyt5yZu+KhmKgEh/M/X/3yqk7tfVnlRRX+whd94W/8v4Y4zjM3rPBKOb/tWKb9OTs16bEOkqTsI5l66sPJys7L0JhB13mlDu705PXLKv8/M3unrn0yTW//Y2fla4fyMtQwvpmKSgr15tf3asLQGT6oZfXq+p2HhUYoOy9D5eVlCgkJld1u14Gc3ZUT+Mdqkmi8FhoSpvEn3aQrZnY8bp/qJgjS2g9TWvthkqSSsmJd8M9ktW7apb5NA+BmCVHSRQOksSdKv+yQ1u6W9uXUnPxPiDKefTuwvXFnP4l+mIGjsVBdE97b9q3Vobx96pF6siTphJZ91SghRdv2rlHvjqdW2bemBLq/yD+aq6KSwsoJ+h9+W6D4mIaK+9vz1E7qfq5uen6ILjv1fiXGNdVnP72ooWkXSqo7VvJ3H34/S4vXztVjkxdWeRzDsfbn7FJCTGNFhkfLZrNp6foPlNqshyQpuWGqvl71uo4W5ysqIlY/b/pMKY07+l2CA0DtLBYjrunfTtqaKa3YJv15UDp8tPr9w0KkFolSWiupX6oUzV1sMDFHxsK+nUbp5c9nKOvwXjVKaKFfNn+hVk06KzQkzFRjobvnxqqLG52JFb3NXfFwXXN//twHAGrWNEH6v5Ol3ELpx63Spr3Svtyak/+NYqX2TaVBHYyEP+Cv3JknkZy7aUDyv5yZO+MhR/I/R4vz9f369/XstJVuK9ddvN0X/oiEP2o0ZtB1Vf5ITX9hqMafdJMGdzvbd5WqwVMfXqOff/9c2Ucydecrpys6Ik5v3rFN//7gKg3sMlaDuo6t8xx3vHya7HabSstLNLLXRI0bPNULNXePxNgmat+ilxauflun952kZRvmq1GDlOOWqTlaUqDy8tLKL/6L18xV++YnVtmnpgmCigsiJOmdhQ8qrd3wWpfBAeBbMRHSsM7GT7lN2n/YuMuttFyyWoyl3lISpTjuXkOAcmTCu0mDlso+kqFd+zerddPO2pu1TRmHtqtl4xMcTqD7i4Kiw3rwP+eruPSorBarEmIa68ErPpPFYqkSDzVrmKrLT3tANz03WJLUs91QnTXgGh/Xvv4O5qZrzmfT1SwpVbe+aHwpCw+N0OxpP+uNr+9Vw/jmGjPwWu3IWK/Xv7xLkmS329S+RS9NGfeMJGlIt3O0Zc9KTXm6j8JCIxQZHqM7L37XZ20CUD9Wi3RCM+NHko4cldJzpKMlxmR3eKixulGTeOPxSIDZOToWRoXH6MbxL+quV8+UZFdMZILuuuQ9ScE7FtYUN9YWK5qBI/FwXXN/Zu8DINg1iDZWLhrdUyorN5L+OQXG3FCo1Zg7apFkPM4ICASO5kkkx8bJvzNTzsxZjuR/lqybp9RmPdWqSSdfVNFrzJoLI+EfoGpKgEuqMulb235mctN5c6p9ffr5r1T7enJSGy14MLfKay9P3+DuannVTefO0ePzJmnuoocVHRmv2ya8Xrmt4nfeNrmbHnjrXNls5bLLrmZJqZpx4VuV+9U2QfDm1/fqtz+XqdxWps6tB2r6hFe93kYArgmxSs0TjR8gGNQ2nklVY6Gbzn1J/3p7gqwWq2x2m6ae86yaJLbS/pxdNSbQ/VHTxNZ6dtov1W77ezw0uv/VGt3/6lrPV12s5M8aN0jRt49Xf7vKpNP/Wfn/A7uM0cAuY6rdz2Kx6MrRj+jK0Y94pI4AfCsuSurMhY4IYI6OhZLU54TT1OeE047bL9DGQkfmxjq06FVj3JgY17TGWNHfORMP18bMfQCgqtAQ4+597uBHoHMkT1Lb+C/VHkOYjaO5QkfyP1/98qpG1TGf5M/c2Rf+yGK32518igu8obxEWvyMr2vhuGHTpBA3XglotvZL7usDM7a9grvfBwDgbvd9ZCzpmxAlPTDe17XxDbP0gRnHQ3eOg8Hefok+AABPMUss4ClmaT/jIH0Q7O0HAE8xSyzgKWZpvxnHwQrBnCsK9njQlfYXFBQoNjZWkpSfn6+YmBiXymbhOgAAAAAAAAAAAAAATIiEPwAAAAAAAAAAAAAAJkTCHwAAAAAAAAAAAAAAEwr1dQVQPWuY8awHs7CGuf98Zmq/5L4+MGPbK7j7fQAACF5mHA/dOQ4Ge/srzhfsfQAACF6Mg/RBsLcfABDczDgOVgjmXFGwx4O+jIVI+Pspi0UKCfd1LXwnmNsfzG0HAKBCsI+Hwd5+iT4AAAQ3xkH6INjbDwAIboyD9IFEHziDJf0BAAAAAAAAAAAAADAhEv4AAAAAAAAAAAAAAJgQCX8AAAAAAAAAAAAAAEyIhD8AAAAAAAAAAAAAACZEwh8AAAAAAAAAAAAAABMi4Q8AAAAAAAAAAAAAgAmR8AcAAAAAAAAAAAAAwIRI+AMAAAAAAAAAAAAAYEIk/AEAAAAAAAAAAAAAMCES/gAAAAAAAAAAAAAAmBAJfwAAAAAAAAAAAAAATIiEPwAAAAAAAAAAAAAAJkTCHwAAAAAAAAAAAAAAEyLhDwAAAAAAAAAAAACACZHwBwAAAAAAAAAAAADAhEj4AwAAAAAAAAAAAABgQiT8AQAAAAAAAAAAAAAwIRL+AAAAAAAAAAAAAACYUKivK4Dq2e2SrdTXtXCcNUyyWNxzLrO1/Vju6gcz9oE73wMAAAT7WBjs7ZfoAwBAcGMcpA+Cvf0AAAT7WBjs7ZfM1we+jIVI+PspW6m0+Blf18Jxw6ZJIeHuOZfZ2n4sd/WDGfvAne8BAACCfSwM9vZL9AEAILgxDtIHwd5+AACCfSwM9vZL5usDX8ZCLOkPAAAAAAAAAAAAAIAJkfAHAAAAAAAAAAAAAMCESPgDAAAAAAAAAAAAAGBCJPwBAAAAAAAAAAAAADChUF9XAAAAwNMKiqV9uVJpufHv0nIp64jUMFayWHxaNQAAAI8rt0mZh6Xcwr/ioTKbVFImhTMzBAAAgsDho1JmbtW5ocOFUkK0T6sFAG7B1zoAABBw7HZp237pp+3SzizpUH7V7YUl0r8+kaLDpZQk6cTWUq82UgSREQAACBCHC6UV26VNe6WM3L8mtysUFEt3vC81TZA6NJUGdzD+HwAAIBCU26SNe6Vfdki7D0l5R6tuLyyR7vtYio+UWjWS+rSVuqdIIayLDcCEmNYGAAABw243vsh9t0k6kFf3/oUl0pZM4+e/q6X+7aTTuxsXAgAAAJjR/sPSl+ul9Xskm732fW1242KAjFxp6R9G4v+MHlK7Jt6oKQAAgPuVlUtLfpeW/WHc1V+XvCLpt3TjJz5KOqmjNKyzFBri+boCgLuQ8A8g67Yv0a0vDqvyWmR4jFIad9TIXhN19uAbFBIS2L/yYO+DYG8/gOCWnS+997ORvHdFUan0/e/S2l3SBf2lLi3cWz94HuMgfRDs7QcQ3Gw2Y3L7i3XGcv2u2Lpf2vqtdPIJ0plprH5kRoyF9EGwtx9AcEvPlt79SdqX49rxeUelz9dJq3dKFw2UWjV0a/XgBYyD9EGwtj/wWgQNS7tI/TqNll125RzJ1Le/vqUXP71Fuw9s1s3nveTr6nlFsPdBsLcfQPDZtFd6c7lUXFb/cx0+Kr20RDqlkzSul2S11P+c8C7GQfog2NsPIPgUlkivfi9tP+Ce8y39Q9q8T7pmmNQozj3nhHcxFtIHwd5+AMHnhy3S/FV1r3DkiIzD0lNfG/NCp3Sq//ngfYyD9EGwtZ+EfwDq0KKXRva+tPLfYwZdrytndtKXv7yiK854SA1iG/uwdt4R7H0Q7O0HEFzW75HeWOaeL3TH+v536WiJdOEAkv5mwzhIHwR7+wEEl8Ji6fnvpHQX72SrycEj0jPfSFNPlZrEu/fc8DzGQvog2NsPILgs3mw8qtGdbHbp41+lkjLp1G7uPTc8j3GQPgi29lt9XQF4XlR4jDq1HiC73a59h7b7ujo+Eex9EOztBxC4th8w7ux3d7K/wi87pM/Weubc8B7GQfog2NsPIHCV26SXv3d/sr9CXpH0wnfSkSLPnB/ew1hIHwR7+wEErpU73J/sP9bn66QV2zx3fngH4yB9EOjt5w7/IJHxvzdvfHSSj2viO8HeB8HefgCBp7hUeudHY6LbUbecIcVHGc9km/WVY8cs2iR1aS61b+paPeEfGAfpg2BvP4DAtHCj9OdB545xNh7KKZTmr5QmneRaHeE/GAvpg2BvP4DAk50vfbjSuWNcmRv6eJXUoSmPOjI7xkH6IJDbT8I/ABWVFupwQZbsduO5FJ+ueFHb9q5Rp5b9lNK4o6+r5xXB3gfB3n4AweHTNVJ2gXPHxEdJDaKdL2vuT9KMM6UIIidTYBykD4K9/QCCw74c6ZvfnD/OlXho7W5p7S4prbXz5cE3GAvpg2BvP4DAZ7dL7/0sFZc5d5wrsVBJuTE3NGUkj300C8ZB+iDY2h8U09ZZWVmaOXOmPvroI6Wnp6tx48YaP368Hn74YU2bNk2vvfaaZs+eralTp/q6qm7x1jf36a1v7qvy2pBu43XDOc/5qEbeF+x9EOztBxD4Mg9Ly7d6r7xD+dL3v0unmfSZbTa79Ps+aet+6WiJ8VppubE6QkgAPuCJcZA+CPb2AwgOC1Y7t9JRfX38q9S9pXljh+x86dedf8VCxaXS4UIpwYWLQc2AsZA+CPb2Awh8v6VLWzK9V972A9L63ea9ALKsXFq/569Y6Gip0X8dmkqWALyIgXGQPgi29gd8wn/t2rUaNWqUMjMzFRMToy5dumjfvn165plntH37dmVnZ0uS0tLSfFtRNzqz/2Sd3ON8ldlK9WfGBs1b8piyDqcrPCyycp+SsmJd/1QvDTvxYl0y4q7K12e+N0m5+fv18FVf+qLqbuNIHzz09oWy2W26Z+L7la/lFWbr6ie6avJZT2hEr0t8UXW3cKT9G3Ys0z9eHXXcsWXlJbLZyvX1zHJvVhkAnPLDFu+X+eNWaUQXc01y2+3Sj9uMxxIcyq+6rbBE+ucC6ZRO0tDOgXWFOrEQsRCxEIBAtz/PuxPcknT4qDGx3rOVd8utr/2HpU/XShv3GrFRhaIy6YEFxkUMY0+UGsb6qoaeQTxEPEQ8BCDQefNGkArLtpgv4V9uk77dKC3fIuUX/fV6SZn0/HdSk3jp1K5S31Tf1dETiIWIhYItFgrohH9WVpbGjBmjzMxMTZ8+Xffdd5/i4oyHrMycOVO33367QkNDZbFY1KNHDx/X1n1aNOqgXh1HSpL6dRqlbm2H6Obnh+jp+dfqrkvfkySFh0ZoxoVvafrzJ2tA57PUrnlP/fDbAv20+VO9dMsGX1bfLRzpgxvGP6/J/+6uRWvmaviJF0mSZn88RV3bDjH1HzHJsfZ3Tz1Jnz5UNfuTdXifpjzTR+MGBcZqFwACU3Gp9MsO75ebW2hMFPdo6f2yXWG3Sx/9Ki37o+Z9Dh+VPlkjpWdLlw6SrCa6mKE2xELEQsRCAAKdLy5+lIyJYjMl/HdmSXMWGXewVcdml9btNu7Yu3641DzRu/XzJOIh4iHiIQCB7GCe9EeG98vdfkDKyJWaNfB+2a4oK5deX2bMZ9XkQJ70zgrp4BFpdE/v1c3TiIWIhYItFgqQad3qTZs2Tenp6Zo6daqeeOKJymS/JM2YMUM9e/ZUWVmZ2rRpo/j4eB/W1LO6thmkkb0masm6edq488fK1zum9NZ5p9yqme9dpoO56Xrqw8m64Zzn1CihuQ9r6xnV9UF8dJKmn/+qnl0wVVmH92np+g+1fvsS3TT+RR/X1v1qeg8cq6SsWA+8NV7d2gzRxSP+4eUaAoDjtu53/vls7rJhj2/KdcXizbUn+4+1epdx51ugIhYiFiIWAhBofBWTbN1vrBJkBjkF0stLak72Hyu/SJqzuOpdb4GGeIh4iHgIQCDZkB6cZTtr/qrak/3H+uY3acU2z9bHl4iFiIUCPRYK2IT/5s2bNW/ePDVq1EiPPPJItfv07t1bktSz51+XLS1ZskQWi+W4H7Mv+X/JyHtktYboza/v/dvrdyvEGqrrnjpRPdsP07C0C31UQ8+rrg/6djpDp/SYoMfmXqrZH12vW85/RfExDX1YS8+p6T1Q4en516qktEi3XfCGdysGAE7akx2cZTujpExauNG5Y5b+EdiT3MRCxELEQgACRX6RlFPou/L3miQeWvaHVFDs+P6Hjwb2JLdEPCQRDxEPAQgUvpyfSTdJLHQoX/rJydjm6w3GIwACFbEQsVAgx0IBm/CfO3eubDabLrnkEsXGVv8gtqioKElVE/4VnnvuOa1YsaLy5z//+Y9H6+tpLRq117CeF2rNtu+0YceyytdDQ8LUpc0gHS7I0ul9rvBhDT2vpj6YPOYJ7T20TX07jVL/zmf6sIaeVVP7Jenj5c/o582f6YFJCxQZHu2jGgKAY/Yc8l3Z+/N8t7qAM9bscv7uu3Kb9NN2z9THHxALEQsRCwEIFL6+ANHX5TuipMy1uOaHrZItgCe5iYeIh4iHAAQKXybdfTkv5Ywft0p2J4+peJxloCIWIhYK5FgoYBP+ixYtkiQNGzasxn3S0421V6pL+Hfp0kUDBgyo/OnevbtnKupFF424S1aLVW9+89eVKxt2LNM3q97QuMFT9fwnN6q49KgPa+h51fVBVHiMmiWlqm2y+X/Hdamu/Wu3LdYrn9+ueyZ+oOSkNr6rHAA4KLvAd2Xb7cbysP7O1eXlzLQsnSuIhYiFiIUABIJD+XXv40nZPi7fEX8edO3RA7mFUnqO++vjT4iHiIeIhwAEAl/GQzmF5rhA0OW5IRM9ztIVxELEQoEaC1nsdruzF/mYQsuWLZWenq41a9ZUuxx/WVmZmjVrpqysLG3fvl2pqamSjCX9hw0bpsWLF2vo0KFuqUufPn2UmZnp1DHhoVF6aepWt5Rfk6PF+bpmVk+de/ItGjPwOk1/8RR1TOmj68Y+6fS5Jj/bQSVl7vkj6I22/930F4ZqQOezdP7QW+t1Hnf1g7f6IDN7p6Y+01eXnnqfzh48tV7ncud7AABqc8aM5Ypt2KbabbecIcVH1XxsfKRktRpfzPLqWL4+76g066vjX1/49OnK3efkevleNvTa+WrUtr/Txx05uF1fP3GKB2rkPLPFQpK54yF3xELujgW80QfujIUk4iEA3tF+yJVKG/NAtdvqioUkx+OhmmKhP1e+p18/rN93Z09L6XGWBlzi2nNHl71ysfZvXermGjnPW7GAv84NScRDzA0BQPUs1hCd+8iuGre7a26oplhIkj6+u4PK/TwpfNbdaxQZ19jp4/Zt+lY/vukfd7kzNxTcsZBkvrkhV9pvs9mUkZEhSUpLS9OaNWtcKjvUpaNMoKDAuP3u6NHqO3bevHnKyspSXFyc2rZte9z2Cy64QFlZWWrYsKHGjh2rRx99VI0aNXKpLpmZmdq717l1UCLDPL9cxJxPpys5qa3GDrpeFotFt014Q9c+labB3c5Rj9STnTpXxr59Kip1z0MEvdF2T3FXP3ijD4pKCnXfG2drYJexbpngdud7AABqU1pS88NY46OkBg78CbVaHduvOpkZe3XIyXHd2wryD8uVqKWo8IjTMYunmC0WkoiH3B0LeLoP3B0LScRDALyjYXbN68g6GgtJrsdD+UcO+028UJPwpvtcPjYzY4/2+UH7vBUL+OvckEQ8xNwQALjGG3ND6Xt2yVZe6trBXlJSXKjIOOePyz+S4zexHnNDwR0LSeabG6pv+/fv3+/ysQGb8E9OTlZOTo5Wr16tgQMHVtmWkZGh2267TZLUo0cPWSyWym0JCQm67bbbdPLJJys2NlYrVqzQI488op9++kmrVq1SZGSkS3VxVnhoHZfk19Mvv3+pJevm6aVb1le2v3mjdrpy1KN6Yt4VmjN9vaLCYxw+X7Pmzd161ZJZuasfvNEHyzbM146MddqbtUVL1s07bvurt25Sk8RWDp/Pne8BAKiNreRIjdvy6vgz5Owd/tVpEBepyBYt6qilbxVl73DpuMKsbWrhJ20zWywkEQ+5OxbwdB+4OxaSiIcAeEd0eM3b6oqFJOfuaqtOmKXUb+KFmoSWHJTdZpPF6tyTLG1lJYqwH/aL9nkjFvDnuSGJeIi5IQCoWXFBjiJiEqvd5q65oZrOU1ZcoGbJTRysqe/kH/hD8Y1aO31cSc6ffhELScwNBXssJJlvbsiV9tvtdlUsxt+sWTOnjj1WwC7pP23aNM2ePVstW7bUwoUL1bFjR0nSypUrNXHiRO3YsUOlpaWaMmWKnn322VrP9emnn2rs2LF67bXXdMUV3lnKpLxEWvyMV4pyi2HTpJBaJh2cYba2H8td/WDGPnDnewAAavPBL9IPLq7kdP85xtXbuYXS/R87f3xspPTgeOmYawX90sE86aFPnT/uxtOkts6v9uYRwT4WBnv7JfoAAGqSeVh69DPXj69vPHT5EOlE5+eOvW7OYmmzkzf692otXTbEM/VxFuMgfRDs7QeA2jz/nbTFuacoV6pvLJTaWJp2mmtle9OmvdJLS5w7xmox+qeuR0R5S7CPhcHefsl8feDLWMi5S51NZMaMGWrYsKH27Nmjrl27qnv37urQoYP69eun1NRUDR8+XJLUs2fPOs911llnKSYmRqtWrfJ0tQEAQB1aJvm2bH9P9ktS43ipc3PnjmmZJLVx7elFAADAi5rESeE+XK/Rl7GYM07q6PwxQ1w4BgAAeF+KD+MRX5btjE7NpcZOLumf1sp/kv0AnBOwCf+UlBQtW7ZMZ555piIjI7Vz504lJSVpzpw5+vzzz7VlyxZJjiX8K1jMMMMPAECAS/XhqmmpfnL3uyMuHCAlOrjqWEyEcTcboQ4AAP7PavVdTJIQJTWM9U3ZzurSQhrW2fH9R/fwbZwJAAAc186HY7Yvy3aG1SJdcZIUGebY/k3jpXP7erZOADzHh9eEe17nzp312WfHr3OXn5+vnTt3ymq1qlu3bnWe55NPPlFBQYH69evniWoCAAAnNImX2jeVtu33brlWi9S/nXfLrI+EKGOJ/peXSHtzat6vUZx09VDnr/oGAAC+M7C99HuGb8o10wWCY080VkP45jeppgdaWi3SmBOloZ28WzcAAOC6Ts3+Wpbfm+Ijpa7+8Xh7hzRPlKaOlF75vva+atNIuvIU44YQAOYU0An/mmzcuFF2u10dO3ZUdHR0lW2XXnqpUlNT1atXL8XGxmrFihWaOXOm0tLSdOGFF/qoxgAA4FhDOng/4d/ThMuaNYiWpo+S/siQlm+RtmZKJeVSWIjUtrGxbG3XFlJIwK75BABAYOqWYlzcd/io98q0WoyEv5lYLNKoHlL/VGnFNumXHVLeUUkWI07q304a2E5KiK7zVAAAwI+EWKVBHaQv1nm33AHtpdAQ75ZZXylJ0t1jpfV7jLmh3YekMptxUWTnZtLgjlKHpua6qBPA8YIy4b9hwwZJ1S/n37VrV7377rt66qmndPToUaWkpOjqq6/Wfffdp/DwcG9XFQAAVKN7Syk5Qco87J3yrBZpeBfvlOVuVovUubnxI0k2m7EUMAAAMK8QqzSiq/TRKu+V2d/EifGkWOnMNOPH9r87/a1MagMAYGqD2kvf/y4VFHunvMgw48YJMwoNkXq1MX4k5oaAQETC/2/uvPNO3Xnnnd6uktuUlBbpoXcu1K79mxQRFqUGsU00bfwLatHo+Mvw31v8mL5d9aZCQ8IVHhapKeOeUadWPLYAAOD/QqzSRQOkp76peXlWdxrRRWqZ5PlyvIEvdAhEzsTAB3J2a/bHU5SetUVWS4jGDLxOZw+5QRnZf+rBt85Tua1cNluZWjbtrJvPfUlx0Yk+aBEA1G1IR2ntLmnHQc+X1SDaWB4/EJDoD1y3v3Saco5kymKxKjoyTlPGPaP2LY5/4375y6t6b/GjsttsSms/XNPGP6/QkDDZbDa9/PkMrfrjK5XbytS1zWBNG/+CwkID5wYgR/sIAMwgNlI6t4/01g/eKW98H/Ot/FgT5oYCR/rBrXp83uU6XJClmMgE3XbBG2qT3LXafe12u2bMGaGte1drwYO5la+/v+RxfbvqTdnsNrVsfIJuveB1xUY10NGSAs14cbhKyookSUlxzXTjuS8qOamNF1rmGEfaX1eMV1P7JenbX/+jD79/QjZbuRrENdVtE15Xk8RW3m6mQ0j4B6DR/SerX6dRslgsWvDDs5r1wVX693VLquyzbe9affrj83rl1o2KiojVwl/f1rMLpurZab/4ptJu4MgHu7bJ4LyCQ7ptzojKfYtLC5WRvUMf3HdA8dFJWvn7V3r967tVVlaiiPBo3XTuHLVr7r/voecWTNOKTZ9of84uvXDTGrVvkVbtfpc+3EZhoREKDzWilYuG36mhaRfU2R8A4GutGxmJ+IUbHT8m72jV/zqiWYJ0enfn6gb/4+i4aLbxvibOJMDr6huz9IkjMbDdbtf9b56jC4bdoVN6ni9JyjliPB+kYXxzPTlluSLCjJjouf/eqLe+vV9Txj3t1XYAgKOsFumigdLjnxuP7HGUK/HQBf2lqMDJeQYNR+KfumIGR2Mof3DPxPcrJ2eXb/hYj8+bpDm3VF3rOSP7T73x9T164cbVSoxrqnvfGKfPf3pJ4wZP0VcrX9W2vav1/E2rFRoSpic/nKyPlz+tCUNv80FrPMORPgIAMzmxtbRut7Ruj+PHuBILdWku9W3rXN3ge+6IhSp8tfJ1/fv9/9P9l3+swd3O9k4DHPD0/Gs0uv9knd53kpau/1CPz5uk525cWe2+85c+qWYN22nr3tWVr/265Vt9vfJ1zb7hZ0VHxumdhf/Sa1/epWnjn1NEaJQem7xQ0ZFxlcc//98b9c8r/uuVtjnCkfbXFuPV1v7dB37Xy5/dphduXqOG8c208Ne39fRH1+mhKz/3UWtrF5TX8SxatEh2u11nnnmmr6viduFhkerfebQs/3vgSudWA7Q/Z+dx+1ksFpXZSlVUUiBJyi/KVaOEFG9W1e0qPthv3L5FFwy7XY/Pm1TtfqP7T9brM/7QnFvWaWDXcZr1wVWSpPiYhppzy9rKnzP7T1a/E0YpPjpJRwpz9MjcSzTjgjf10vT1mnzm43r03Uu82DrnndTjPD15/XI1TWxd5753XTKvst1D0y6QVHt/AIC/GNVD6tHS8f1nfSXd/7HxX0ckRElXDTXf89lwPEfGRTOO97WpKeb5u9r6xix94mgMvGbrdwoLjahM9ktSYlxT4xyhEZXJ/nJbuYpKCmQRt4EC8G+N46RJJzl317qz8dDZvf56NBDMxdF5gdpiBmfmFnytIpEtSQVFh6VqxvFl6z/UwC5jlRSfLIvForMGXKvFa+dKkrbvW6cTO4xUWGi4LBaL+nYapYW//sdLtfcOR/oIAMzE8r8LIFs1dPwYZ2OhFonSpYN5xr0ZuSMWkqTM7J368ueX1bnVAE9W12k5+Qe0JX2VRva6VJJ0UvdzdTB3j/ZmbTtu352ZG/XjxgW6cNgdVV7fsW+durUdUpnU79dptL5bbcQ/Vqu18nW73a7CorzKeRd/4Gj7a4vxamv/zszf1LZZDzWMb2Zs6zxaK//4UnkFh7zVRKcEZcI/mHy8/GkN7DruuNfbNe+pc0+6WRMfaauL/pWij5Y+qalnz/ZBDd3D0Q+2o5PBkvTlyld1Rr8rJUn7Dm1XfHTDyhUDuqeepAO5u7U1fXW1x/qDHqknq3ED913EcWx/AIC/CLFKlw2WenpgJaXEGGnqSKlhrPvPDe9zZFw043hfE2dintr6xqx9UlMMvOvAJiXENNZDb1+oa588Ufe/cY4yDu2o3F5aVqJrZqXpvPsbaW/WVl1+2gPerDYAuKRLC+n/TpZCPTDDM66XNLSz+88L73Ak/qkrZnD33IKnPTb3Ml38r5Z68+t7dMdFxyfrD+TurjLpn5zURgdyd0uSOqT01opNn6igKE9l5aVauu79GuMnM6urjwDAbCLDpGuHGStBulvLJOm64VI0Kx2ZkjtiIZvNplkfXKUpZ89WWGiEJ6vrtIO5e5QU30whIcZi7haLRU0SW1XGNhXKykv15IdX68Zz58hqrXpXU4eU3lq9daGy8zJlt9v13Zp3VFh8RHmF2ZX7zJgzUhP+mayl6z/QDec85/mGOcjR9tcW49XW/tRmPbVt72qlH9wiSfpu9duy2+3an7PLq+10FAn/APbudw9rX9Y2XTnqkeO2ZWT/qeUbPtIbt2/T3LvTNf7km/Wvty/wQS3dw9EP9t/VNBm8ceePyi/M0YDOZ0mSUhp1UF7hIW3c+aMk6ceNn6iw+IgyA+SL38z3LtPV/+6uf79/pXLzj38A5N/7AwD8SWiIdPlgaXRP4wIAd+jaQrrpdKlxvHvOB3MI5PG+ppinLmbsk9pi4PLyMq3dvkiXjLxHL968Rr1POF0Pvj2hcntYaLjm3LJW79+7X60ad9JnP83xZtUBwGXdUqRpp0lN3RS7xEUaFxEMI9kfdFyNGfzF7Re9pXfv3qNJZ/xLL39xu1PHnt5nkvqecIamv3CKpr9wilo07qgQa+A9DbU+fQQA/io6Qrp+hDSkg/vOObC9NGWkFBvpvnPC//09Fpq/dJa6thmsjim9fVir+vnPtw9oSLfxat30+OA+rf0wnX/Krbr79bM0bfYANYhpLElVYqCZ1yzUvHsydErPC/Tudw95rd7uUluMV1v7Uxp30I3nvqjH3rtM1z/dR3kFhxQb1cBv40P/rBXq7YMlT2j5bx9p5uSFigyPPm778vXz1bZZdzVKMNblO73vFXpuwQ0qLStRWKj/Xa42bfZA7c3aWu22F25e49I5KyaDZ17z3XHbvvrlVZ3a+7LKCwhiohJ078QP9eqXd6qoOF+dWw9U66ZdfPbBrqs/mjRwfH3rWdctVZPEViorL9XrX92tmfMu18NXflFln7/3BwD4G6tVOq2b1D1FmvuTtNvFlZWiw6Vzekt92rJUm5m4a1z0t/G+Ns60ubaYpy5m6hOp7hi4SWIrtW9+YuWKBSN7T9Tsj69XWXmpQkPCKvcLCw3XaX2v0JMfXq0Lhs3wWv0BoD5aNZRuHS19tV5avFmy2V07T+820vg+Uox/3cCEv3HnvECF+sQM/ua0Ppfr6fnXKq/gkOJj/lrnuUmDVtp3aHvlvzOzd6pJA2O5MIvFostOu1+XnXa/JGnx2vfU+n8xQyCqqY8AwKwiQqXz+hmrQL73s3Qo37XzJMVIF/SXTmjm3vrBvbwRC/2Z+ZuWbZivWdcvrVddPaVxg5bKzstQeXmZQkJCZbfbdSBnd2VsU2H9ju91IGe3/vvjsyq3lamwOE+XPtxGz05bqQaxjTV20PUaO+h6SdKmXT+pcUKKYiKrXklstVo1uv/VmjSzg6aNf95rbayNo+2vK8arrf0n9zhPJ/c4T5KUnZepeUseU/NG7b3QOuf550wd6uXD72dp8dq5emzywirP5jpWcsNUfb3qdR0tzldURKx+3vSZUhp39MtkvyQ9c8OKWreHhUY49MGuUNtk8NHifH2//n09O21lldfT2g9TWvthkqSSsmJd8M9ktW7apR6tcl1d/eGMJolGH4WGhGn8STfpipkdq2yvqT8AwB81ayDdfLq07YC0fIu0YY9jk90pidKQjtKJbYwviDAXd46L/jTe18bRNteVAHeEWfrEkRi4b6dRevnzGco6vFeNElrol81fqFWTzgoNCdP+nF1KiGmsyPBo2Ww2LV3/gVKb9fBuIwCgnsJCpDEnSid3kn7aJv24VTp8tO7jIsOkfqnS4A5S0wTP1xP15874R3JPzOBL+UdzVVRSWHljyw+/LVB8TEPFRSdV2e+k7ufqpueH6LJT71diXFN99tOLGpp2oSSppLRIxaVHFRedqMMFWXpv0aOadMaDXm+LpzjaRwBgdh2SpX+MkTbuNeaGtmQ6eFxTY26oW4r7VpCE53gjFvptxzLtz9mpSY8ZS0dkH8nUUx9OVnZehsYMus6t5bsiMbaJ2rfopYWr39bpfSdp2Yb5atQgRS3+lpB+8vpllf+fmb1T1z6Zprf/sbPytUN5GWoY30xFJYV68+t7NWGoceNDdl6mwkIjFBedKElasm6e2ib7zzyJo+2vK8arqf3Hbiu3leuVL27X2EFT/DZWZko7wBzMTdecz6arWVKqbn3RmJgND43Q7Gk/642v71XD+OYaM/BaDel2jrbsWakpT/dRWGiEIsNjdOfF7/q49q5z9IMt1T0ZvGTdPKU266lWTTpVeb3igy1J7yx8UGnthld7fjM5WlKg8vLSyn5YvGau2jc/sco+NfUHAPgri8X4ktahqVRQbNztvydb2pcjFZUaFwCEhUhN4o1nsbVMkhrFcUc/DIE03juSAHeEGfrE0Rg4KjxGN45/UXe9eqYku2IiE3TXJe9JknZkrNfrX94lSbLbbWrfopemjHvGV00CgHpJiJJO7y6N7Crty5XSs42Y6HChVGYzJrFjI6SU/8VCKUlSODNEQctdMYMvFRQd1oP/OV/FpUdltViVENNYD17xmSwWi/79wVUa2GWsBnUdq2YNU3X5aQ/opucGS5J6thuqswZcU3mO6S8OldVilc1u0zlDbtTALmN82Sy3qq2PACDQhFilHi2Nn9xCac//5oYyD0vFZZLdbtzwkZwgtWxorJTUwD9zePCCmmKhMYOuq5LYn/7CUI0/6SYN7na29ytZg5vOnaPH503S3EUPKzoyXrdNeF2SqsQ/dbnj5dNkt9tUWl6ikb0matzgqZKkA7m79dT8a2Szlcsuu5o3bKc7Ln7bo+1xliPtryvGq6n9kvTv9/9P+3N2qbSsWP07n6n/G/Ww19voKIvdbndxkTd4UnmJtNhE84vDpkkhblocwNW27znwhx6fN0l5hYcqP9htm3WX9NeHu0OLXrr4oZZqlpSqqIg4SX9NBle48dlBGtX/ap3R94oq55/1wdX67c9lKreVqXPrgZp69uzjvgi7qx/c8ft/6sNr9PPvnyv7SKbioxsqOiJOb96xTdJf/dE2uZseeOvcyj/YzZJSdf24p5Wc1KbyPDX1x9+58z0AAIC7YyFHxsVBXcc6NN7XxB/ioQoHc9NrjXmObXNtfSM5FgNJ7o8FzBYPS8RDAAD38da8QF3zJHXFCcciFvCveNAXiIUAAO5U37HQHbHQsRxJ+BMLBHc86MtYiIS/nwrmN7HZ2n4sf0r4extf6gAA7hTsY2Gwt1+iDwAAwY1xkD4I9vYDABDsY2Gwt18yXx/4MhbiSSQAAAAAAAAAAAAAAJgQCX8AAAAAAAAAAAAAAEyIhD8AAAAAAAAAAAAAACZEwh8AAAAAAAAAAAAAABOy2O12u68rgePZ7ZKt1Ne1cJw1TLJY3HMus7X9WO7qBzP2gTvfAwAABPtYGOztl+gDAEBwYxykD4K9/QAABPtYGOztl8zXB76MhUj4AwAAAAAAAAAAAABgQizpDwAAAAAAAAAAAACACZHwBwAAAAAAAAAAAADAhEj4AwAAAAAAAAAAAABgQiT8AQAAAAAAAAAAAAAwIRL+AAAAAAAAAAAAAACYEAl/AAAAAAAAAAAAAABMiIQ/AAAAAAAAAAAAAAAmRMIfAAAAAAAAAAAAAAATIuEPAAAAAAAAAAAAAIAJkfAHAAAAAAAAAAAAAMCESPgDAAAAAAAAAAAAAGBCJPwBAAAAAAAAAAAAADAhEv4AAAAAAAAAAAAAAJgQCX8AAAAAAAAAAAAAAEyIhD8AAAAAAAAAAAAAACZEwh8AAAAAAAAAAAAAABMi4Q8AAAAAAAAAAAAAgAmR8AcAAAAAAAAAAAAAwIRI+AMAAAAAAAAAAAAAYEIk/AEAAAAAAAAAAAAAMCES/gAAAAAAAAAAAAAAmBAJfwAAAAAAAAAAAAAATIiEPwAAAAAAAAAAAAAAJkTCHwAAAAAAAAAAAAAAEyLhDwAAAAAAAAAAAACACZHwBwAAAAAAAAAAAADAhEj4AwAAAAAAAAAAAABgQiT8AQAAAAAAAAAAAAAwIRL+AAAAAAAAAAAAAACYEAl/AAAAAAAAAAAAAABMiIQ/AAAAAAAAAAAAAAAmRMIfAAAAAAAAAAAAAAATIuEPAAAAAAAAAAAAAIAJkfAHAAAAAAAAAAAAAMCESPgDAAAAAAAAAAAAAGBCJPwBAAAAAAAAAAAAADAhEv4AAAAAAAAAAAAAAJgQCX8AAAAAAAAAAAAAAEyIhD8AAAAAAAAAAAAAACZEwh8AAAAAAAAAAAAAABMi4Q8AAAAAAAAAAAAAgAmR8AcAAAAAAAAAAAAAwIRI+AMAAAAAAAAAAAAAYEIk/AEAAAAAAAAAAAAAMCES/gAAAAAAAAAAAAAAmBAJfwAAAAAAAAAAAAAATIiEPwAAAAAAAAAAAAAAJkTCHwAAAAAAAAAAAAAAEyLhDwAAAAAAAAAAAACACZHwBwAAAAAAAAAAAADAhEj4AwAAAAAAAAAAAABgQiT8AQAAAAAAAAAAAAAwIRL+AAAAAAAAAAAAAACYEAl/AAAAAAAAAAAAAABMiIQ/AAAAAAAAAAAAAAAmRMIfAAAAAAAAAAAAAAATIuEPAAAAAAAAAAAAAIAJkfAHAAAAAAAAAAAAAMCESPgDAAAAAAAAAAAAAGBCJPwBAAAAAAAAAAAAADAhEv4AAAAAAAAAAAAAAJgQCX8AAAAAAAAAAAAAAEyIhD8AAAAAAAAAAAAAACZEwh8AAAAAAAAAAAAAABMi4Q8AAAAAAAAAAAAAgAmR8AcAAAAAAAAAAAAA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", + "text/plain": [ + "
" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Ansatz\n", + "layers = tq.RXYZCXLayer0(\n", + " {\n", + " \"n_blocks\": 6,\n", + " \"n_wires\": n_wires,\n", + " \"n_layers_per_block\": 1,\n", + " }\n", + ")\n", + "\n", + "# We use `tq2qiskit` to visualize the ansatz.\n", + "qdev = tq.QuantumDevice(n_wires=n_wires, bsz=1, device=\"cpu\")\n", + "tq.plugin.qiskit.tq2qiskit(qdev, layers).draw(output=\"mpl\", fold=30)" + ] + }, + { + "cell_type": "markdown", + "id": "a14839c3-d9ff-44dc-adf6-efeebae18bfe", + "metadata": {}, + "source": [ + "We can now simulate the circuit to model the gaussian mixture distribution. The algorithm minimizes the kerneled maximum mean discrepancy (MMD) loss to train the QCBM." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6490b2e9-d18d-42e0-9310-a3f644197c8f", + "metadata": {}, + "outputs": [], + "source": [ + "qcbm = QCBM(n_wires, layers)\n", + "\n", + "# To train QCBMs, we use MMDLoss with radial basis function kernel.\n", + "bandwidth = torch.tensor([0.25, 60])\n", + "space = torch.arange(2**n_wires)\n", + "mmd = MMDLoss(bandwidth, space)\n", + "\n", + "# Optimization\n", + "optimizer = torch.optim.RMSprop(qcbm.parameters(), lr=0.01)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4335ef3e-8dea-47be-a310-8146abc214fc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iteration: 0, Loss: 0.007511706091463566\n", + "Iteration: 10, Loss: 0.0008048344170674682\n", + "Iteration: 20, Loss: 0.0004957925993949175\n", + "Iteration: 30, Loss: 0.0003518108860589564\n", + "Iteration: 40, Loss: 0.0002739735064096749\n", + "Iteration: 50, Loss: 0.0002034252102021128\n", + "Iteration: 60, Loss: 0.00014893575280439109\n", + "Iteration: 70, Loss: 0.0001268944761250168\n", + "Iteration: 80, Loss: 0.00010558744543232024\n", + "Iteration: 90, Loss: 8.735547453397885e-05\n" + ] + } + ], + "source": [ + "for i in range(100):\n", + " optimizer.zero_grad(set_to_none=True)\n", + " pred_probs = qcbm()\n", + " loss = mmd(pred_probs, target_probs)\n", + " loss.backward()\n", + " optimizer.step()\n", + " if i % 10 == 0:\n", + " print(f\"Iteration: {i}, Loss: {loss.item()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "246f5d90-47af-4385-8d41-5ff6fadcb9ec", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Visualize the results\n", + "with torch.no_grad():\n", + " pred_probs = qcbm()\n", + "\n", + "plt.plot(x_input, target_probs, linestyle=\"-.\", color=\"black\", label=r\"$\\pi(x)$\")\n", + "plt.bar(x_input, pred_probs, color=\"green\", alpha=0.5, label=\"samples\")\n", + "plt.xlabel(\"Samples\")\n", + "plt.ylabel(\"Prob. Distribution\")\n", + "\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b7f5ff14-793c-4dc3-aad2-eed38aa3e5a5", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "1. Liu, Jin-Guo, and Lei Wang. \"Differentiable learning of quantum circuit born machines.\" Physical Review A 98.6 (2018): 062324." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (tq-env)", + "language": "python", + "name": "tq-env" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 2ada759b6e0c3251230491b8f937978667b043ab Mon Sep 17 00:00:00 2001 From: Gopal Ramesh Dahale <49199003+Gopal-Dahale@users.noreply.github.com> Date: Thu, 6 Jun 2024 21:22:29 +0530 Subject: [PATCH 48/54] fix tab Co-authored-by: GenericP3rson <41024739+GenericP3rson@users.noreply.github.com> --- torchquantum/algorithm/qcbm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchquantum/algorithm/qcbm.py b/torchquantum/algorithm/qcbm.py index 09798f8d..f4078b90 100644 --- a/torchquantum/algorithm/qcbm.py +++ b/torchquantum/algorithm/qcbm.py @@ -33,7 +33,7 @@ def k_expval(self, px, py): Kernel expectation value Args: - px: First probability distribution + px: First probability distribution py: Second probability distribution Returns: From d5ebf7ad4af424f4dbe33aa6da25ca3d364c9f86 Mon Sep 17 00:00:00 2001 From: Gopal Ramesh Dahale <49199003+Gopal-Dahale@users.noreply.github.com> Date: Thu, 6 Jun 2024 21:22:41 +0530 Subject: [PATCH 49/54] fix tab Co-authored-by: GenericP3rson <41024739+GenericP3rson@users.noreply.github.com> --- torchquantum/algorithm/qcbm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchquantum/algorithm/qcbm.py b/torchquantum/algorithm/qcbm.py index f4078b90..a5e426a3 100644 --- a/torchquantum/algorithm/qcbm.py +++ b/torchquantum/algorithm/qcbm.py @@ -34,7 +34,7 @@ def k_expval(self, px, py): Args: px: First probability distribution - py: Second probability distribution + py: Second probability distribution Returns: Expectation value of the RBF Kernel. From f10103610e47a370910295209800b56d9fef8963 Mon Sep 17 00:00:00 2001 From: Gopal Ramesh Dahale <49199003+Gopal-Dahale@users.noreply.github.com> Date: Thu, 6 Jun 2024 21:22:55 +0530 Subject: [PATCH 50/54] fix spacing Co-authored-by: GenericP3rson <41024739+GenericP3rson@users.noreply.github.com> --- torchquantum/algorithm/qcbm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchquantum/algorithm/qcbm.py b/torchquantum/algorithm/qcbm.py index a5e426a3..c0b6e5e3 100644 --- a/torchquantum/algorithm/qcbm.py +++ b/torchquantum/algorithm/qcbm.py @@ -45,7 +45,7 @@ def k_expval(self, px, py): def forward(self, px, py): """ Squared MMD loss. - + Args: px: First probability distribution py: Second probability distribution From 98b4f366bf90554ca2760134e7cad27fac6a8686 Mon Sep 17 00:00:00 2001 From: Gopal Ramesh Dahale <49199003+Gopal-Dahale@users.noreply.github.com> Date: Thu, 6 Jun 2024 21:23:07 +0530 Subject: [PATCH 51/54] fix tab Co-authored-by: GenericP3rson <41024739+GenericP3rson@users.noreply.github.com> --- torchquantum/algorithm/qcbm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchquantum/algorithm/qcbm.py b/torchquantum/algorithm/qcbm.py index c0b6e5e3..12a63dfb 100644 --- a/torchquantum/algorithm/qcbm.py +++ b/torchquantum/algorithm/qcbm.py @@ -46,7 +46,7 @@ def forward(self, px, py): """ Squared MMD loss. Args: - px: First probability distribution + px: First probability distribution py: Second probability distribution Returns: From 11e50291a806ff3cd16fb65871065a4c9c7eb67e Mon Sep 17 00:00:00 2001 From: Gopal Ramesh Dahale <49199003+Gopal-Dahale@users.noreply.github.com> Date: Thu, 6 Jun 2024 21:23:15 +0530 Subject: [PATCH 52/54] fix tab Co-authored-by: GenericP3rson <41024739+GenericP3rson@users.noreply.github.com> --- torchquantum/algorithm/qcbm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchquantum/algorithm/qcbm.py b/torchquantum/algorithm/qcbm.py index 12a63dfb..fdd5a416 100644 --- a/torchquantum/algorithm/qcbm.py +++ b/torchquantum/algorithm/qcbm.py @@ -47,7 +47,7 @@ def forward(self, px, py): Squared MMD loss. Args: px: First probability distribution - py: Second probability distribution + py: Second probability distribution Returns: Squared MMD loss. From 15605d0f98ce0ae869af5be3239c23dcb2d2aab4 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Fri, 7 Jun 2024 01:03:23 +0900 Subject: [PATCH 53/54] Update torchquantum/operator/standard_gates/qubit_unitary.py Co-authored-by: GenericP3rson <41024739+GenericP3rson@users.noreply.github.com> --- torchquantum/operator/standard_gates/qubit_unitary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchquantum/operator/standard_gates/qubit_unitary.py b/torchquantum/operator/standard_gates/qubit_unitary.py index 39156bc3..4b087cd1 100644 --- a/torchquantum/operator/standard_gates/qubit_unitary.py +++ b/torchquantum/operator/standard_gates/qubit_unitary.py @@ -119,7 +119,7 @@ def from_controlled_operation( n_wires = n_c_wires + n_t_wires # compute the new unitary, then permute - unitary = torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE).clone().detach() + unitary = torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE) for k in range(2**n_wires - 2**n_t_wires): unitary[k, k] = 1.0 + 0.0j From 75955c6febfd04cbbd414d1c6be5b6c196df9ea4 Mon Sep 17 00:00:00 2001 From: Gopal Dahale Date: Thu, 6 Jun 2024 21:40:46 +0530 Subject: [PATCH 54/54] black formatted --- torchquantum/algorithm/qcbm.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/torchquantum/algorithm/qcbm.py b/torchquantum/algorithm/qcbm.py index fdd5a416..35a6fb75 100644 --- a/torchquantum/algorithm/qcbm.py +++ b/torchquantum/algorithm/qcbm.py @@ -33,8 +33,8 @@ def k_expval(self, px, py): Kernel expectation value Args: - px: First probability distribution - py: Second probability distribution + px: First probability distribution + py: Second probability distribution Returns: Expectation value of the RBF Kernel. @@ -45,7 +45,8 @@ def k_expval(self, px, py): def forward(self, px, py): """ Squared MMD loss. - Args: + + Args: px: First probability distribution py: Second probability distribution @@ -61,13 +62,12 @@ class QCBM(nn.Module): Quantum Circuit Born Machine (QCBM) Attributes: - ansatz: An Ansatz object - n_wires: Number of wires in the ansatz used. + ansatz: An Ansatz object + n_wires: Number of wires in the ansatz used. Methods: - __init__: Initialize the QCBM object. - forward: Returns the probability distribution (output from measurement). - + __init__: Initialize the QCBM object. + forward: Returns the probability distribution (output from measurement). """ def __init__(self, n_wires, ansatz): @@ -75,8 +75,8 @@ def __init__(self, n_wires, ansatz): Initialize QCBM object Args: - ansatz (Ansatz): An Ansatz object - n_wires (int): Number of wires in the ansatz used. + ansatz (Ansatz): An Ansatz object + n_wires (int): Number of wires in the ansatz used. """ super().__init__()