diff --git a/quafu/algorithms/ansatz.py b/quafu/algorithms/ansatz.py index a402024..3da7297 100644 --- a/quafu/algorithms/ansatz.py +++ b/quafu/algorithms/ansatz.py @@ -17,6 +17,7 @@ import numpy as np from quafu.circuits.quantum_circuit import QuantumCircuit +from quafu.elements import Parameter from quafu.synthesis.evolution import ProductFormula from .hamiltonian import Hamiltonian @@ -52,8 +53,10 @@ def __init__(self, hamiltonian: Hamiltonian, num_qubits: int, num_layers: int = self._evol = ProductFormula() # Initialize parameters - self._beta = np.zeros(num_layers) - self._gamma = np.zeros(num_layers) + self._beta = np.array([Parameter(f"beta_{i}", 0.0) for i in range(num_layers)]) + self._gamma = np.array( + [Parameter(f"gamma_{i}", 0.0) for i in range(num_layers)] + ) # Build circuit structure super().__init__(num_qubits) @@ -122,7 +125,10 @@ def __init__(self, num_qubits: int, layer: int): layer: Number of layers. """ self._layer = layer - self._theta = np.zeros((layer + 1, num_qubits)) + self._theta = np.array( + [Parameter(f"theta_{i}", 0.0) for i in range((layer + 1) * num_qubits)] + ) + self._theta = np.reshape(self._theta, (layer + 1, num_qubits)) super().__init__(num_qubits) def _build(self): diff --git a/quafu/algorithms/estimator.py b/quafu/algorithms/estimator.py index b35e4fe..93ae8da 100644 --- a/quafu/algorithms/estimator.py +++ b/quafu/algorithms/estimator.py @@ -13,15 +13,16 @@ # limitations under the License. """Pre-build wrapper to calculate expectation value""" from typing import List, Optional + from ..circuits.quantum_circuit import QuantumCircuit +from ..simulators import simulate from ..tasks.tasks import Task from .hamiltonian import Hamiltonian -from ..simulators import simulate def execute_circuit(circ: QuantumCircuit, observables: Hamiltonian): """Execute circuit on quafu simulator""" - sim_res = simulate(circ, hamiltonian= observables) + sim_res = simulate(circ, hamiltonian=observables) expectations = sim_res["pauli_expects"] return sum(expectations) @@ -44,6 +45,7 @@ def __init__( task_options: options to config a task instance """ self._circ = circ + self._circ.get_parameter_grads() # parameter shift currently requires calling this for initialization self._backend = backend self._task = None if backend != "sim": @@ -85,7 +87,7 @@ def run(self, observables: Hamiltonian, params: List[float]): Expectation value """ if params is not None: - self._circ.update_params(params) + self._circ._update_params(params) if self._backend == "sim": return self._run_simulation(observables) diff --git a/quafu/algorithms/gradients/__init__.py b/quafu/algorithms/gradients/__init__.py index de792ee..1c3b61b 100644 --- a/quafu/algorithms/gradients/__init__.py +++ b/quafu/algorithms/gradients/__init__.py @@ -12,5 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. +from .gradiant import grad_adjoint, grad_finit_diff, grad_para_shift from .param_shift import ParamShift from .vjp import compute_vjp, jacobian, run_circ diff --git a/quafu/algorithms/gradient.py b/quafu/algorithms/gradients/gradiant.py similarity index 67% rename from quafu/algorithms/gradient.py rename to quafu/algorithms/gradients/gradiant.py index 6b5b266..c472a4d 100644 --- a/quafu/algorithms/gradient.py +++ b/quafu/algorithms/gradients/gradiant.py @@ -1,45 +1,51 @@ -from ..circuits.quantum_circuit import QuantumCircuit -from ..simulators.simulator import SVSimulator -from ..elements import Parameter, ParameterExpression import numpy as np -from ..exceptions import CircuitError -from ..elements.matrices import XMatrix, YMatrix, ZMatrix -from ..elements import QuantumGate, ControlledGate +from quafu.circuits.quantum_circuit import QuantumCircuit +from quafu.elements import ControlledGate, Parameter, ParameterExpression, QuantumGate +from quafu.elements.matrices import XMatrix, YMatrix, ZMatrix +from quafu.exceptions import CircuitError +from quafu.simulators.simulator import SVSimulator + def assemble_grads(para_grads, gate_grads): grads = [] for var in para_grads: grad_p = para_grads[var] - fullgrad = 0. + fullgrad = 0.0 for pos_g in grad_p: - pos, gp = pos_g + pos, gp = pos_g gg = gate_grads[pos[0]][pos[1]] fullgrad += gg * gp grads.append(fullgrad) - + return grads -def grad_para_shift(qc:QuantumCircuit, hamiltonian, backend=SVSimulator()): + +def grad_para_shift(qc: QuantumCircuit, hamiltonian, backend=SVSimulator()): """ Parameter shift gradients. Each gate must have one parameter """ para_grads = qc._calc_parameter_grads() - gate_grads= [[] for _ in qc.gates] + gate_grads = [[] for _ in qc.gates] for i, op in enumerate(qc.gates): if len(op.paras) > 0: - if isinstance(op.paras[0], Parameter) or isinstance(op.paras[0],ParameterExpression): + if isinstance(op.paras[0], Parameter) or isinstance( + op.paras[0], ParameterExpression + ): if op.name not in ["RX", "RY", "RZ"]: - raise CircuitError("It seems the circuit can not apply parameter-shift rule to calculate gradient.You may need compile the circuit first") - op.paras[0] = op.paras[0] + np.pi/2 + raise CircuitError( + "It seems the circuit can not apply parameter-shift rule to calculate gradient. You may need compile the circuit first" + ) + op.paras[0] = op.paras[0] + np.pi / 2 res1 = sum(backend.run(qc, hamiltonian=hamiltonian)["pauli_expects"]) op.paras[0] = op.paras[0] - np.pi res2 = sum(backend.run(qc, hamiltonian=hamiltonian)["pauli_expects"]) op.paras[0]._undo(2) - gate_grads[i].append((res1 - res2) / 2.) - + gate_grads[i].append((res1 - res2) / 2.0) + return assemble_grads(para_grads, gate_grads) + def grad_finit_diff(qc, hamiltonian, backend=SVSimulator()): variables = qc.variables grads = [] @@ -50,7 +56,7 @@ def grad_finit_diff(qc, hamiltonian, backend=SVSimulator()): res2 = sum(backend.run(qc, hamiltonian=hamiltonian)["pauli_expects"]) v.value += 1e-10 grads.append((res1 - res2) / (2 * 1e-10)) - + return grads @@ -60,37 +66,37 @@ def grad_gate(op): """ if isinstance(op, ControlledGate): if op._targ_name == "RX": - circ = QuantumCircuit(max(op.pos)+1) + circ = QuantumCircuit(max(op.pos) + 1) deriv_mat = -0.5j * XMatrix @ op._get_targ_matrix() circ << QuantumGate("dRX", op.targs, [], deriv_mat) cdim = 1 << (len(op.ctrls)) proj_mat = np.zeros((cdim, cdim)) - proj_mat[cdim-1, cdim-1] = 1. + proj_mat[cdim - 1, cdim - 1] = 1.0 circ << QuantumGate("projCtrl", op.ctrls, [], proj_mat) return circ.wrap() - + elif op._targ_name == "RY": - circ = QuantumCircuit(max(op.pos)+1) + circ = QuantumCircuit(max(op.pos) + 1) deriv_mat = -0.5j * YMatrix @ op._get_targ_matrix() circ << QuantumGate("dRY", op.targs, [], deriv_mat) cdim = 1 << (len(op.ctrls)) proj_mat = np.zeros((cdim, cdim)) - proj_mat[cdim-1, cdim-1] = 1. + proj_mat[cdim - 1, cdim - 1] = 1.0 circ << QuantumGate("projCtrl", op.ctrls, [], proj_mat) return circ.wrap() - + elif op._targ_name == "RZ": - circ = QuantumCircuit(max(op.pos)+1) + circ = QuantumCircuit(max(op.pos) + 1) deriv_mat = -0.5j * ZMatrix @ op._get_targ_matrix() circ << QuantumGate("dRZ", op.targs, [], deriv_mat) cdim = 1 << (len(op.ctrls)) proj_mat = np.zeros((cdim, cdim)) - proj_mat[cdim-1, cdim-1] = 1. + proj_mat[cdim - 1, cdim - 1] = 1.0 circ << QuantumGate("projCtrl", op.ctrls, [], proj_mat) return circ.wrap() else: raise NotImplementedError - + else: if op.name == "RX": deriv_mat = -0.5j * XMatrix @ op.matrix @@ -103,30 +109,37 @@ def grad_gate(op): return QuantumGate("dRZ", op.pos, [], deriv_mat) else: raise NotImplementedError - + + def grad_adjoint(qc, hamiltonian, psi_in=np.array([], dtype=complex)): """ Reverse mode gradient: arXiv:2009.02823 """ para_grads = qc._calc_parameter_grads() backend = SVSimulator() - lam = backend.run(qc, psi = psi_in)["statevector"] + lam = backend.run(qc, psi=psi_in)["statevector"] phi = np.copy(lam) lam = backend._apply_hamil(hamiltonian, lam) begin = 0 end = len(qc.gates) - gate_grads= [[] for _ in range(end)] + gate_grads = [[] for _ in range(end)] for i, op in enumerate(qc.gates): - if len(op.paras) > 0 and (isinstance(op.paras[0], Parameter) or isinstance(op.paras[0],ParameterExpression)): + if len(op.paras) > 0 and ( + isinstance(op.paras[0], Parameter) + or isinstance(op.paras[0], ParameterExpression) + ): begin = i break - + for i in range(begin, end)[::-1]: op = qc.gates[i] phi = backend._apply_op(op.dagger(), phi) - if len(op.paras) > 0 and (isinstance(op.paras[0], Parameter) or isinstance(op.paras[0],ParameterExpression)): - mu = np.copy(phi) - mu = backend._apply_op(grad_gate(op), mu) - gate_grads[i].append(np.real(2. * np.inner(lam.conj(), mu))) + if len(op.paras) > 0 and ( + isinstance(op.paras[0], Parameter) + or isinstance(op.paras[0], ParameterExpression) + ): + mu = np.copy(phi) + mu = backend._apply_op(grad_gate(op), mu) + gate_grads[i].append(np.real(2.0 * np.inner(lam.conj(), mu))) lam = backend._apply_op(op.dagger(), lam) - return assemble_grads(para_grads, gate_grads) \ No newline at end of file + return assemble_grads(para_grads, gate_grads) diff --git a/quafu/algorithms/gradients/param_shift.py b/quafu/algorithms/gradients/param_shift.py index c725188..32fd56f 100644 --- a/quafu/algorithms/gradients/param_shift.py +++ b/quafu/algorithms/gradients/param_shift.py @@ -19,6 +19,7 @@ from ..estimator import Estimator from ..hamiltonian import Hamiltonian +from .gradiant import grad_para_shift class ParamShift: @@ -34,7 +35,7 @@ def __call__(self, obs: Hamiltonian, params: List[float]): estimator (Estimator): estimator to calculate expectation values params (List[float]): params to optimize """ - return self.grad(obs, params) + return self.new_grad(obs, params) def _gen_param_shift_vals(self, params): """Given a param list with n values, replicate to 2*n param list""" @@ -45,6 +46,7 @@ def _gen_param_shift_vals(self, params): minus_params = params - offsets * np.pi / 2 return plus_params.tolist() + minus_params.tolist() + # TODO: delete after 0.4.1 def grad(self, obs: Hamiltonian, params: List[float]): """grad. @@ -61,3 +63,12 @@ def grad(self, obs: Hamiltonian, params: List[float]): num_shift_params = len(res) grads = (res[: num_shift_params // 2] - res[num_shift_params // 2 :]) / 2 return grads + + def new_grad(self, obs: Hamiltonian, params: List[float]): + """Calculate the gradients of given the circuit based on the parameter shift rule + Args: + obs (Hamiltonian): observables for measurement. + params (List[float]): parameters to apply to the circuit. + """ + self._est._circ._update_params(params) + return grad_para_shift(self._est._circ, obs) diff --git a/quafu/circuits/quantum_circuit.py b/quafu/circuits/quantum_circuit.py index 752a17d..16c3478 100644 --- a/quafu/circuits/quantum_circuit.py +++ b/quafu/circuits/quantum_circuit.py @@ -277,6 +277,7 @@ def _update_params(self, values, order=[]): val = values[order[i]] if order else values[i] self._variables[i].value = val + # TODO: delete after 0.4.1 def update_params(self, paras_list: List[Any]): """Update parameters of parameterized gates Args: diff --git a/tests/quafu/algorithms/merge_circuits_test.py b/tests/quafu/algorithms/construct_qlayers_test.py similarity index 92% rename from tests/quafu/algorithms/merge_circuits_test.py rename to tests/quafu/algorithms/construct_qlayers_test.py index 5af7f48..3f827be 100644 --- a/tests/quafu/algorithms/merge_circuits_test.py +++ b/tests/quafu/algorithms/construct_qlayers_test.py @@ -18,10 +18,10 @@ from quafu.circuits.quantum_circuit import QuantumCircuit -class TestMergeCircuits: - """Example of merging circuits""" +class TestConstructQLayers: + """Test stacking multiple different quantum layers""" - def test_merge_circuits(self): + def test_construct_qlayers(self): state = np.array([7, 2, 3, 4]) encoding_layer = AmplitudeEmbedding(state=state, num_qubits=2, normalize=True) diff --git a/tests/quafu/algorithms/gradient_test.py b/tests/quafu/algorithms/gradient_test.py index 429f25e..976461e 100644 --- a/tests/quafu/algorithms/gradient_test.py +++ b/tests/quafu/algorithms/gradient_test.py @@ -14,29 +14,47 @@ import sys +import numpy as np import pytest from quafu.algorithms.estimator import Estimator -from quafu.algorithms.gradients import ParamShift +from quafu.algorithms.gradients import ParamShift, grad_para_shift from quafu.algorithms.hamiltonian import Hamiltonian from quafu.circuits.quantum_circuit import QuantumCircuit +from quafu.elements import Parameter +# TODO: remove this test after releasing 0.4.1 as it is not necessary class TestParamShift: @pytest.mark.skipif( sys.platform == "darwin", reason="Avoid error on MacOS arm arch." ) def test_call(self): + """ + This test simply ensures that the legacy implementation of parameter shift produces + the same results with the new implementation + """ + theta_0 = Parameter("theta_0", 0.2) + theta_1 = Parameter("theta_1", 0.6) ham = Hamiltonian.from_pauli_list([("Z0 Z1", 1), ("X1", 1)]) - circ = QuantumCircuit(2) - # circ.h(0) - # circ.h(1) - circ.rx(0, 0.5) - circ.cnot(0, 1) - circ.ry(1, 0.5) + + circ_0 = QuantumCircuit(2) + circ_0.rx(0, theta_0) + circ_0.cnot(0, 1) + circ_0.ry(1, theta_1) params = [0.2, 0.6] - estimator = Estimator(circ) + estimator = Estimator(circ_0) grad = ParamShift(estimator) - grads = grad(ham, params) - print(grads) + grads_0 = grad(ham, params) + print(grads_0) + + circ_1 = QuantumCircuit(2) + circ_1.rx(0, theta_0) + circ_1.cnot(0, 1) + circ_1.ry(1, theta_1) + circ_1.get_parameter_grads() + grads_1 = grad_para_shift(circ_1, ham) + print(grads_1) + + assert np.allclose(grads_0, grads_1, atol=1e-6) diff --git a/tests/quafu/algorithms/qnn_test.py b/tests/quafu/algorithms/qnn_test.py index 0a84f53..b0bc07e 100644 --- a/tests/quafu/algorithms/qnn_test.py +++ b/tests/quafu/algorithms/qnn_test.py @@ -19,6 +19,7 @@ from quafu.algorithms.interface.torch import TorchTransformer from quafu.algorithms.templates.basic_entangle import BasicEntangleLayers from quafu.circuits.quantum_circuit import QuantumCircuit +from quafu.elements import Parameter from torch import nn from torch.utils.data import DataLoader, TensorDataset @@ -112,10 +113,11 @@ def forward(self, features): class TestLayers: circ = QuantumCircuit(2) + theta = [Parameter(f"theta_{i}", 0.1) for i in range(3)] circ.x(0) - circ.rx(0, 0.1) - circ.ry(1, 0.5) - circ.ry(0, 0.1) + circ.rx(0, theta[0]) + circ.ry(1, theta[1]) + circ.ry(0, theta[2]) def _model_grad(self, model, batch_size): """Test one forward pass and gradient calculation of a model""" diff --git a/tests/quafu/algorithms/varational_test.py b/tests/quafu/algorithms/varational_test.py index 7bcbae8..0376b57 100644 --- a/tests/quafu/algorithms/varational_test.py +++ b/tests/quafu/algorithms/varational_test.py @@ -2,7 +2,7 @@ import numpy as np import scipy.sparse as sp -from quafu.algorithms.gradient import grad_adjoint, grad_finit_diff, grad_para_shift +from quafu.algorithms.gradients import grad_adjoint, grad_finit_diff, grad_para_shift from quafu.algorithms.hamiltonian import Hamiltonian, PauliMats, PauliOp from quafu.elements import Parameter from quafu.elements.element_gates import CRYGate, CXGate, HGate, RXGate, RYGate, RZGate