From 134b3c6ef1b758724ac588751701fb937262d4a8 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Thu, 6 Jun 2024 20:45:15 +0900 Subject: [PATCH 1/6] support parameter expression in qiskit2tq --- torchquantum/layer/layers/module_from_ops.py | 14 +- torchquantum/plugin/qiskit/qiskit_plugin.py | 148 ++++++++++++++----- 2 files changed, 118 insertions(+), 44 deletions(-) diff --git a/torchquantum/layer/layers/module_from_ops.py b/torchquantum/layer/layers/module_from_ops.py index f5aea5e0..b0a14541 100644 --- a/torchquantum/layer/layers/module_from_ops.py +++ b/torchquantum/layer/layers/module_from_ops.py @@ -22,16 +22,16 @@ SOFTWARE. """ +from typing import Iterable + +import numpy as np import torch import torch.nn as nn +from torchpack.utils.logging import logger + import torchquantum as tq import torchquantum.functional as tqf -import numpy as np - - -from typing import Iterable from torchquantum.plugin.qiskit import QISKIT_INCOMPATIBLE_FUNC_NAMES -from torchpack.utils.logging import logger __all__ = [ "QuantumModuleFromOps", @@ -61,6 +61,6 @@ def forward(self, q_device: tq.QuantumDevice): None """ - self.q_device = q_device + q_device.reset_states(1) for op in self.ops: - op(q_device) + op(q_device, wires=op.wires) diff --git a/torchquantum/plugin/qiskit/qiskit_plugin.py b/torchquantum/plugin/qiskit/qiskit_plugin.py index bca3a7d2..846cfd6f 100644 --- a/torchquantum/plugin/qiskit/qiskit_plugin.py +++ b/torchquantum/plugin/qiskit/qiskit_plugin.py @@ -22,24 +22,28 @@ SOFTWARE. """ +from __future__ import annotations + +from typing import Iterable, List + +import numpy as np +import qiskit.circuit.library.standard_gates as qiskit_gate +import symengine +import sympy import torch +from qiskit import Aer, ClassicalRegister, QuantumCircuit, execute +from qiskit.circuit import CircuitInstruction, Parameter, ParameterExpression +from qiskit.circuit.parametervector import ParameterVectorElement +from torchpack.utils.logging import logger + import torchquantum as tq import torchquantum.functional as tqf -import qiskit.circuit.library.standard_gates as qiskit_gate -import numpy as np - -from qiskit import QuantumCircuit, ClassicalRegister -from qiskit import Aer, execute -from qiskit.circuit import Parameter -from torchpack.utils.logging import logger +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", @@ -85,7 +89,9 @@ def qiskit2tq_op_history(circ): 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", @@ -104,12 +110,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", @@ -138,12 +144,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: @@ -206,7 +213,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( @@ -297,6 +307,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": @@ -535,7 +546,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,11 +684,9 @@ 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"] - ) - + + append_fixed_gate(circ, op["name"], param, op["wires"], op["inverse"]) + circs_all.append(circ) return circs_all @@ -677,7 +694,7 @@ def op_history2qiskit_expand_params(n_wires, op_history, bsz): # construct a tq QuantumModule object according to the qiskit QuantumCircuit # object -def qiskit2tq_Operator(circ: QuantumCircuit): +def qiskit2tq_Operator(circ: QuantumCircuit, initial_parameters=None): if getattr(circ, "_layout", None) is not None: try: p2v_orig = circ._layout.final_layout.get_physical_bits().copy() @@ -697,14 +714,26 @@ def qiskit2tq_Operator(circ: QuantumCircuit): for p in range(circ.num_qubits): p2v[p] = p + if initial_parameters is None: + initial_parameters = torch.nn.init.uniform_( + torch.ones(len(circ.parameters)), -np.pi, np.pi + ) + + param_to_index = {} + for i, param in enumerate(circ.parameters): + param_to_index[param] = i + ops = [] for gate in circ.data: op_name = gate[0].name wires = list(map(lambda x: x.index, gate[1])) 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 + # init_params = ( + # list(map(float, gate[0].params)) if len(gate[0].params) > 0 else None + # ) + init_params = qiskit2tq_translate_qiskit_params( + gate, initial_parameters, param_to_index ) if op_name in [ @@ -762,12 +791,57 @@ def qiskit2tq_Operator(circ: QuantumCircuit): raise NotImplementedError( f"{op_name} conversion to tq is currently not supported." ) - + return ops -def qiskit2tq(circ: QuantumCircuit): - ops = qiskit2tq_Operator(circ) +def qiskit2tq_translate_qiskit_params( + circuit_instruction: CircuitInstruction, initial_parameters, param_to_index +): + parameters = [] + for p in circuit_instruction.operation.params: + if isinstance(p, Parameter) or isinstance(p, ParameterVectorElement): + parameters.append(initial_parameters[param_to_index[p]]) + elif isinstance(p, ParameterExpression): + if len(p.parameters) == 0: + parameters.append(float(p)) + continue + + expr = p.sympify().simplify() + if isinstance(expr, symengine.Expr): # qiskit uses symengine if available + expr = expr._sympy_() # sympy.Expr + + for free_symbol in expr.free_symbols: + # replace names: theta[0] -> theta_0 + # ParameterVector creates symbols with brackets like theta[0] + # but sympy.lambdify does not allow brackets in symbol names + free_symbol.name = free_symbol.name.replace("[", "_").replace("]", "") + + parameter_list = list(p.parameters) + sympy_symbols = [param._symbol_expr for param in parameter_list] + # replace names again: theta[0] -> theta_0 + sympy_symbols = [ + sympy.Symbol(str(symbol).replace("[", "_").replace("]", "")) + for symbol in sympy_symbols + ] + lam_f = sympy.lambdify(sympy_symbols, expr, modules="math") + parameters.append( + lam_f( + *[ + initial_parameters[param_to_index[param]] + for param in parameter_list + ] + ) + ) + else: # non-parameterized gate + parameters.append(p) + return parameters + + +def qiskit2tq( + circ: QuantumCircuit, initial_parameters: list[torch.nn.Parameter] | None = None +): + ops = qiskit2tq_Operator(circ, initial_parameters) return tq.QuantumModuleFromOps(ops) From c4ab1833b22d3bd25781d5a9b3998fcf26ad2cb1 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Fri, 7 Jun 2024 10:50:22 +0900 Subject: [PATCH 2/6] test: add test for qiskit2tq --- test/plugin/test_qiskit2tq.py | 174 ++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 test/plugin/test_qiskit2tq.py diff --git a/test/plugin/test_qiskit2tq.py b/test/plugin/test_qiskit2tq.py new file mode 100644 index 00000000..9894da9d --- /dev/null +++ b/test/plugin/test_qiskit2tq.py @@ -0,0 +1,174 @@ +""" +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 random + +import numpy as np +import pytest +import torch +import torch.optim as optim +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter, ParameterVector +from torch.optim.lr_scheduler import CosineAnnealingLR + +import torchquantum as tq +from torchquantum.plugin import qiskit2tq + +seed = 42 +random.seed(seed) +np.random.seed(seed) +torch.manual_seed(seed) + + +class TQModel(tq.QuantumModule): + def __init__(self, init_params=None): + super().__init__() + self.n_wires = 2 + self.rx = tq.RX(has_params=True, trainable=True, init_params=[init_params[0]]) + self.u3_0 = tq.U3(has_params=True, trainable=True, init_params=init_params[1:4]) + self.u3_1 = tq.U3( + has_params=True, + trainable=True, + init_params=torch.tensor( + [ + init_params[4] + init_params[2], + init_params[5] * init_params[3], + init_params[6] * init_params[1], + ] + ), + ) + self.cu3_0 = tq.CU3( + has_params=True, + trainable=True, + init_params=torch.tensor( + [ + torch.sin(init_params[7]), + torch.abs(torch.sin(init_params[8])), + torch.abs(torch.sin(init_params[9])) + * torch.exp(init_params[2] + init_params[3]), + ] + ), + ) + + def forward(self, q_device: tq.QuantumDevice): + q_device.reset_states(1) + self.rx(q_device, wires=0) + self.u3_0(q_device, wires=0) + self.u3_1(q_device, wires=1) + self.cu3_0(q_device, wires=[0, 1]) + + +def get_qiskit_ansatz(): + ansatz = QuantumCircuit(2) + ansatz_param = Parameter("Θ") # parameter + ansatz.rx(ansatz_param, 0) + ansatz_param_vector = ParameterVector("φ", 9) # parameter vector + ansatz.u(ansatz_param_vector[0], ansatz_param_vector[1], ansatz_param_vector[2], 0) + ansatz.u( + ansatz_param_vector[3] + ansatz_param_vector[1], # parameter expression + ansatz_param_vector[4] * ansatz_param_vector[2], + ansatz_param_vector[5] / ansatz_param_vector[0], + 1, + ) + ansatz.cu( + np.sin(ansatz_param_vector[6]), # numpy functions + np.abs(np.sin(ansatz_param_vector[7])), # nested numpy functions + # complex expression + np.abs(np.sin(ansatz_param_vector[8])) + * np.exp(ansatz_param_vector[1] + ansatz_param_vector[2]), + 0.0, + 0, + 1, + ) + return ansatz + + +def train_step(target_state, device, model, optimizer): + model(device) + result_state = device.get_states_1d()[0] + + # compute the state infidelity + loss = 1 - torch.dot(result_state, target_state).abs() ** 2 + + optimizer.zero_grad() + loss.backward() + optimizer.step() + + infidelity = loss.item() + target_state_vector = target_state.detach().cpu().numpy() + result_state_vector = result_state.detach().cpu().numpy() + print( + f"infidelity (loss): {infidelity}, \n target state : " + f"{target_state_vector}, \n " + f"result state : {result_state_vector}\n" + ) + return infidelity, target_state_vector, result_state_vector + + +def train(init_params, backend): + device = torch.device("cpu") + + if backend == "qiskit": + ansatz = get_qiskit_ansatz() + model = qiskit2tq(ansatz, initial_parameters=init_params).to(device) + elif backend == "torchquantum": + model = TQModel(init_params).to(device) + + print(model) + + n_epochs = 10 + optimizer = optim.Adam(model.parameters(), lr=1e-2, weight_decay=0) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + q_device = tq.QuantumDevice(n_wires=2) + target_state = torch.tensor([0, 1, 0, 0], dtype=torch.complex64) + + result_list = [] + for epoch in range(1, n_epochs + 1): + # print(f"Epoch {epoch}, LR: {optimizer.param_groups[0]['lr']}") + result_list.append(train_step(target_state, q_device, model, optimizer)) + scheduler.step() + + return result_list + + +@pytest.mark.parametrize( + "init_params", + [ + torch.nn.init.uniform_(torch.ones(10), -np.pi, np.pi), + torch.nn.init.uniform_(torch.ones(10), -np.pi, np.pi), + torch.nn.init.uniform_(torch.ones(10), -np.pi, np.pi), + ], +) +def test_qiskit2tq(init_params): + qiskit_result = train(init_params, "qiskit") + tq_result = train(init_params, "torchquantum") + for qi_tensor, tq_tensor in zip(qiskit_result, tq_result): + torch.testing.assert_close(qi_tensor[0], tq_tensor[0]) + torch.testing.assert_close(qi_tensor[1], tq_tensor[1]) + torch.testing.assert_close(qi_tensor[2], tq_tensor[2]) + + +if __name__ == "__main__": + test_qiskit2tq(torch.nn.init.uniform_(torch.ones(10), -np.pi, np.pi)) From ee6834a8b5f37264b72ba2242d647cf1bb2e8a45 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Fri, 7 Jun 2024 10:58:03 +0900 Subject: [PATCH 3/6] change: print --- test/plugin/test_qiskit2tq.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/plugin/test_qiskit2tq.py b/test/plugin/test_qiskit2tq.py index 9894da9d..23427423 100644 --- a/test/plugin/test_qiskit2tq.py +++ b/test/plugin/test_qiskit2tq.py @@ -135,7 +135,7 @@ def train(init_params, backend): elif backend == "torchquantum": model = TQModel(init_params).to(device) - print(model) + print(f"{backend} model:", model) n_epochs = 10 optimizer = optim.Adam(model.parameters(), lr=1e-2, weight_decay=0) @@ -146,7 +146,7 @@ def train(init_params, backend): result_list = [] for epoch in range(1, n_epochs + 1): - # print(f"Epoch {epoch}, LR: {optimizer.param_groups[0]['lr']}") + print(f"Epoch {epoch}, LR: {optimizer.param_groups[0]['lr']}") result_list.append(train_step(target_state, q_device, model, optimizer)) scheduler.step() From 120fc2ae64df7a04b6ee3ad728414ed143a4a517 Mon Sep 17 00:00:00 2001 From: king-p3nguin Date: Tue, 11 Jun 2024 04:23:26 +0900 Subject: [PATCH 4/6] change: remove comments --- torchquantum/plugin/qiskit/qiskit_plugin.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/torchquantum/plugin/qiskit/qiskit_plugin.py b/torchquantum/plugin/qiskit/qiskit_plugin.py index 846cfd6f..1011fa8b 100644 --- a/torchquantum/plugin/qiskit/qiskit_plugin.py +++ b/torchquantum/plugin/qiskit/qiskit_plugin.py @@ -728,10 +728,7 @@ def qiskit2tq_Operator(circ: QuantumCircuit, initial_parameters=None): op_name = gate[0].name wires = list(map(lambda x: x.index, gate[1])) 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 - # ) + init_params = qiskit2tq_translate_qiskit_params( gate, initial_parameters, param_to_index ) From 64919c2490e063fd593f4a4ceb9d9171540528b4 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Wed, 12 Jun 2024 23:55:21 +0900 Subject: [PATCH 5/6] fix: fix test --- torchquantum/plugin/qiskit/qiskit_plugin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/torchquantum/plugin/qiskit/qiskit_plugin.py b/torchquantum/plugin/qiskit/qiskit_plugin.py index 9177d5bd..b018f2ec 100644 --- a/torchquantum/plugin/qiskit/qiskit_plugin.py +++ b/torchquantum/plugin/qiskit/qiskit_plugin.py @@ -22,15 +22,18 @@ SOFTWARE. """ - from typing import Iterable import numpy as np import qiskit import qiskit.circuit.library.standard_gates as qiskit_gate +import symengine +import sympy import torch from qiskit import ClassicalRegister, QuantumCircuit, transpile -from qiskit.circuit import Parameter +from qiskit.circuit import CircuitInstruction, Parameter +from qiskit.circuit.parameter import ParameterExpression +from qiskit.circuit.parametervector import ParameterVectorElement from qiskit_aer import AerSimulator from torchpack.utils.logging import logger From cd2f3d4f01513e365276c452139571f09fb75d37 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Thu, 13 Jun 2024 00:00:40 +0900 Subject: [PATCH 6/6] fix: fix type annotations --- torchquantum/plugin/qiskit/qiskit_plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/torchquantum/plugin/qiskit/qiskit_plugin.py b/torchquantum/plugin/qiskit/qiskit_plugin.py index b018f2ec..97b7943b 100644 --- a/torchquantum/plugin/qiskit/qiskit_plugin.py +++ b/torchquantum/plugin/qiskit/qiskit_plugin.py @@ -21,8 +21,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations -from typing import Iterable +from typing import Iterable, Optional import numpy as np import qiskit @@ -837,7 +838,7 @@ def qiskit2tq_translate_qiskit_params( def qiskit2tq( - circ: QuantumCircuit, initial_parameters: list[torch.nn.Parameter] | None = None + circ: QuantumCircuit, initial_parameters: Optional[list[torch.nn.Parameter]] = None ): ops = qiskit2tq_Operator(circ, initial_parameters) return tq.QuantumModuleFromOps(ops)