From 12c9931b4af4a33c7e69600dfe209b7bc785fc9a Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 10 Nov 2021 10:13:03 +0100 Subject: [PATCH 01/24] pvqd dradt --- qiskit/algorithms/__init__.py | 2 + qiskit/algorithms/pvqd.py | 220 ++++++++++++++++++++++++++++ test/python/algorithms/test_pvqd.py | 53 +++++++ 3 files changed, 275 insertions(+) create mode 100644 qiskit/algorithms/pvqd.py create mode 100644 test/python/algorithms/test_pvqd.py diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index a1f5db97f955..e2166e9b6034 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -212,6 +212,7 @@ PhaseEstimationResult, IterativePhaseEstimation, ) +from .pvqd import PVQD from .exceptions import AlgorithmError __all__ = [ @@ -252,6 +253,7 @@ "PhaseEstimationScale", "PhaseEstimation", "PhaseEstimationResult", + "PVQD", "IterativePhaseEstimation", "AlgorithmError", ] diff --git a/qiskit/algorithms/pvqd.py b/qiskit/algorithms/pvqd.py new file mode 100644 index 000000000000..ff1482b101db --- /dev/null +++ b/qiskit/algorithms/pvqd.py @@ -0,0 +1,220 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The projected Variational Quantum Dynamics Algorithm.""" + +from typing import Optional, Union, List, Tuple, Callable + +import numpy as np + +from qiskit.algorithms.optimizers import Optimizer +from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library import PauliEvolutionGate +from qiskit.providers import Backend +from qiskit.opflow import OperatorBase, CircuitSampler, ExpectationBase, ListOp, StateFn +from qiskit.synthesis import EvolutionSynthesis, LieTrotter +from qiskit.utils import QuantumInstance + +from .algorithm_result import AlgorithmResult + + +class PVQDResult(AlgorithmResult): + """The result object for the pVQD algorithm.""" + + times = None + parameters = None + fidelities = None + estimated_error = None + observables = None + + +class PVQD: + """The projected Variational Quantum Dynamics Algorithm.""" + + def __init__( + self, + ansatz: QuantumCircuit, + initial_parameters: np.ndarray, + optimizer: Optimizer, + quantum_instance: Union[Backend, QuantumInstance], + expectation: ExpectationBase, + evolution: Optional[EvolutionSynthesis] = None, + ) -> None: + """ + Args: + ansatz: A parameterized circuit preparing the variational ansatz to model the + time evolved quantum state. + initial_parameters: The initial parameters for the ansatz. + optimizer: The classical optimizers used to minimize the overlap between + Trotterization and ansatz. + quantum_instance: The backend of quantum instance used to evaluate the circuits. + expectation: The expectation converter to evaluate expectation values. + evolution: The evolution synthesis to use for the construction of the Trotter step. + Defaults to first-order Lie-Trotter decomposition. + """ + if evolution is None: + evolution = LieTrotter() + + self.ansatz = ansatz + self.initial_parameters = initial_parameters + self.optimizer = optimizer + self.expectation = expectation + self.evolution = evolution + + self._sampler = CircuitSampler(quantum_instance) + + def step( + self, hamiltonian: OperatorBase, theta: np.ndarray, dt: float + ) -> Tuple[np.ndarray, float]: + """Perform a single time step. + + Args: + hamiltonian: The Hamiltonian under which to evolve. + theta: The current parameters. + dt: The time step. + + Returns: + A tuple consisting of the next parameters and the fidelity of the optimization. + """ + # construct cost function + overlap = self.get_overlap(hamiltonian, dt, theta) + + # call optimizer + optimizer_result = self.optimizer.minimize(lambda x: -overlap(x), self.initial_parameters) + + return optimizer_result.x, -optimizer_result.fun + + def get_overlap( + self, hamiltonian: OperatorBase, dt: float, current_parameters: np.ndarray + ) -> Callable[[np.ndarray], float]: + """Get a function to evaluate the overlap between Trotter step and ansatz. + + Args: + hamiltonian: The Hamiltonian under which to evolve. + dt: The time step. + current_parameters: The current parameters. + + Returns: + A callable to evaluate the overlap. + """ + # use Trotterization to evolve the current state + trotterized = self.ansatz.bind_parameters(current_parameters) + trotterized.append( + PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution), self.ansatz.qubits + ) + + # define the overlap of the Trotterized state and the ansatz + overlap = StateFn(self.ansatz).adjoint() @ StateFn(trotterized) + + # apply the expectation converter + converted = self.expectation.convert(overlap) + + ansatz_parameters = self.ansatz.parameters + + def evaluate_overlap(theta: np.ndarray) -> float: + """Evaluate the overlap of the ansatz with the Trotterized evolution. + + Args: + theta: The parameters for the ansatz. + + Returns: + The fidelity of the ansatz with parameters ``theta`` and the Trotterized evolution. + """ + # evaluate with the circuit sampler + value_dict = dict(zip(ansatz_parameters, theta)) + sampled = self._sampler.convert(converted, params=value_dict) + return np.abs(sampled.eval()) ** 2 + + return evaluate_overlap + + def _get_observable_evaluator(self, observables): + if isinstance(observables, list): + observables = ListOp(observables) + + expectation_value = StateFn(observables, is_measurement=True) @ StateFn(self.ansatz) + converted = self.expectation.convert(expectation_value) + + ansatz_parameters = self.ansatz.parameters + + def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]: + """Evaluate the observables for the ansatz parameters ``theta``. + + Args: + theta: The ansatz parameters. + + Returns: + The observables evaluated at the ansatz parameters. + """ + value_dict = dict(zip(ansatz_parameters, theta)) + sampled = self._sampler.convert(converted, params=value_dict) + return sampled.eval() + + return evaluate_observables + + def evolve( + self, + hamiltonian: OperatorBase, + time: float, + dt: float, + observables: Optional[Union[OperatorBase, List[OperatorBase]]] = None, + ) -> PVQDResult: + """ + Args: + hamiltonian: The Hamiltonian under which to evolve. + time: The total evolution time. + dt: The time step. + observables: The observables to evaluate at each time step. + + Returns: + A result object containing the evolution information and evaluated observables. + + Raises: + ValueError: If the evolution time is not positive or the timestep is too small. + """ + if time <= 0: + raise ValueError("The evolution time must be larger than 0.") + + if not 0 < dt <= time: + raise ValueError( + "The time step must be larger than 0 and smaller equal the evolution time." + ) + + # get the function to evaluate the observables for a given set of ansatz parameters + evaluate_observables = self._get_observable_evaluator(observables) + + observable_values = [evaluate_observables(self.initial_parameters)] + fidelities = [1] + times = [0] + parameters = [self.initial_parameters] + + current_time = 0 + while current_time < time: + # perform VQE to find the next parameters + next_parameters, fidelity = self.step(hamiltonian, parameters[-1], dt) + + # store parameters + parameters.append(next_parameters) + fidelities.append(fidelity) + observable_values.append(evaluate_observables(next_parameters)) + + # increase time + current_time += dt + times.append(current_time) + + result = PVQDResult() + result.times = times + result.parameters = parameters + result.fidelities = fidelities + result.estimated_error = np.prod(result.fidelities) + result.observables = np.asarray(observable_values) + + return result diff --git a/test/python/algorithms/test_pvqd.py b/test/python/algorithms/test_pvqd.py new file mode 100644 index 000000000000..46d007ac5864 --- /dev/null +++ b/test/python/algorithms/test_pvqd.py @@ -0,0 +1,53 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +from qiskit.test import QiskitTestCase + +import numpy as np + +from qiskit import Aer +from qiskit.algorithms import PVQD +from qiskit.algorithms.optimizers import SPSA +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import X, Z, MatrixExpectation + +import matplotlib.pyplot as plt + + +class TestPVQD(QiskitTestCase): + """Tests for the pVQD algorithm.""" + + def test_pvqd(self): + """Test a simple evolution.""" + + backend = Aer.get_backend("statevector_simulator") + expectation = MatrixExpectation() + hamiltonian = X ^ X + observable = Z ^ Z + + time = 0.1 + dt = 0.01 + + ansatz = EfficientSU2(2, reps=3) + optimizer = SPSA(maxiter=100, learning_rate=0.01, perturbation=0.01) + initial_parameters = np.zeros(ansatz.num_parameters) + initial_parameters[-2] = np.pi / 2 + initial_parameters[-4] = np.pi / 2 + + pvqd = PVQD(ansatz, initial_parameters, optimizer, backend, expectation) + result = pvqd.evolve(hamiltonian, time, dt, observables=[hamiltonian, observable]) + + fig, (ax1, ax2) = plt.subplots(2, 1) + ax1.plot(result.times, result.observables[:, 0]) + ax2.plot(result.times, result.observables[:, 1]) + plt.show() + print(result) From f1dc8ec49a4c10db476155dd7a550a3b658995d0 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 23 Nov 2021 18:45:02 +0100 Subject: [PATCH 02/24] update as theta + difference --- qiskit/algorithms/pvqd.py | 47 +++++++++++++++++------ test/python/algorithms/test_pvqd.py | 58 ++++++++++++++++++++++++----- 2 files changed, 83 insertions(+), 22 deletions(-) diff --git a/qiskit/algorithms/pvqd.py b/qiskit/algorithms/pvqd.py index ff1482b101db..9276452bec73 100644 --- a/qiskit/algorithms/pvqd.py +++ b/qiskit/algorithms/pvqd.py @@ -17,10 +17,12 @@ import numpy as np from qiskit.algorithms.optimizers import Optimizer -from qiskit.circuit import QuantumCircuit +from qiskit.circuit import QuantumCircuit, ParameterVector from qiskit.circuit.library import PauliEvolutionGate from qiskit.providers import Backend -from qiskit.opflow import OperatorBase, CircuitSampler, ExpectationBase, ListOp, StateFn +from qiskit.opflow import ( + OperatorBase, CircuitSampler, ExpectationBase, ListOp, StateFn, GradientBase +) from qiskit.synthesis import EvolutionSynthesis, LieTrotter from qiskit.utils import QuantumInstance @@ -47,6 +49,7 @@ def __init__( optimizer: Optimizer, quantum_instance: Union[Backend, QuantumInstance], expectation: ExpectationBase, + gradient: Optional[GradientBase] = None, evolution: Optional[EvolutionSynthesis] = None, ) -> None: """ @@ -67,13 +70,14 @@ def __init__( self.ansatz = ansatz self.initial_parameters = initial_parameters self.optimizer = optimizer + self.gradient = gradient self.expectation = expectation self.evolution = evolution self._sampler = CircuitSampler(quantum_instance) def step( - self, hamiltonian: OperatorBase, theta: np.ndarray, dt: float + self, hamiltonian: OperatorBase, theta: np.ndarray, dt: float, initial_guess: np.ndarray ) -> Tuple[np.ndarray, float]: """Perform a single time step. @@ -86,12 +90,12 @@ def step( A tuple consisting of the next parameters and the fidelity of the optimization. """ # construct cost function - overlap = self.get_overlap(hamiltonian, dt, theta) + overlap, gradient = self.get_overlap(hamiltonian, dt, theta) # call optimizer - optimizer_result = self.optimizer.minimize(lambda x: -overlap(x), self.initial_parameters) + optimizer_result = self.optimizer.minimize(lambda x: -overlap(x), initial_guess, gradient) - return optimizer_result.x, -optimizer_result.fun + return theta + optimizer_result.x, -optimizer_result.fun def get_overlap( self, hamiltonian: OperatorBase, dt: float, current_parameters: np.ndarray @@ -113,28 +117,42 @@ def get_overlap( ) # define the overlap of the Trotterized state and the ansatz - overlap = StateFn(self.ansatz).adjoint() @ StateFn(trotterized) + x = ParameterVector("w", self.ansatz.num_parameters) + shifted = self.ansatz.assign_parameters(current_parameters + x) + overlap = StateFn(trotterized).adjoint() @ StateFn(shifted) # apply the expectation converter converted = self.expectation.convert(overlap) ansatz_parameters = self.ansatz.parameters - def evaluate_overlap(theta: np.ndarray) -> float: + def evaluate_overlap(displacement: np.ndarray) -> float: """Evaluate the overlap of the ansatz with the Trotterized evolution. Args: - theta: The parameters for the ansatz. + displacement: The parameters for the ansatz. Returns: The fidelity of the ansatz with parameters ``theta`` and the Trotterized evolution. """ # evaluate with the circuit sampler - value_dict = dict(zip(ansatz_parameters, theta)) + value_dict = dict(zip(x, displacement)) sampled = self._sampler.convert(converted, params=value_dict) return np.abs(sampled.eval()) ** 2 - return evaluate_overlap + if self.gradient is not None: + gradient = self.gradient.convert(overlap) + + def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: + """Evaluate the gradient.""" + # evaluate with the circuit sampler + value_dict = dict(zip(ansatz_parameters, current_parameters + displacement)) + sampled = self._sampler.convert(gradient, params=value_dict) + return 2 * sampled.eval() + + return evaluate_overlap, evaluate_gradient + + return evaluate_overlap, None def _get_observable_evaluator(self, observables): if isinstance(observables, list): @@ -197,9 +215,14 @@ def evolve( parameters = [self.initial_parameters] current_time = 0 + initial_guess = np.random.random(self.initial_parameters.size) * 0.01 + # initial_guess = np.zeros(self.initial_parameters.size) while current_time < time: # perform VQE to find the next parameters - next_parameters, fidelity = self.step(hamiltonian, parameters[-1], dt) + next_parameters, fidelity = self.step(hamiltonian, parameters[-1], dt, initial_guess) + + # set initial guess to last parameter update + initial_guess = next_parameters - parameters[-1] # store parameters parameters.append(next_parameters) diff --git a/test/python/algorithms/test_pvqd.py b/test/python/algorithms/test_pvqd.py index 46d007ac5864..b592df9de001 100644 --- a/test/python/algorithms/test_pvqd.py +++ b/test/python/algorithms/test_pvqd.py @@ -13,12 +13,14 @@ from qiskit.test import QiskitTestCase import numpy as np +import scipy as sc from qiskit import Aer from qiskit.algorithms import PVQD -from qiskit.algorithms.optimizers import SPSA +from qiskit.algorithms.optimizers import SPSA, L_BFGS_B, GradientDescent, COBYLA from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import X, Z, MatrixExpectation +from qiskit.opflow import X, Z, I, MatrixExpectation, Gradient +from qiskit.quantum_info import Statevector import matplotlib.pyplot as plt @@ -26,28 +28,64 @@ class TestPVQD(QiskitTestCase): """Tests for the pVQD algorithm.""" + def exact(self, final_time, dt, hamiltonian, observable, initial_state): + """Get the exact values for energy and the observable.""" + energies = [] # list of energies evaluated at timesteps dt + obs = [] # list of observables + ts = [] # list of timepoints at which energy/obs are evaluated + t = 0 + while t <= final_time: + # get exact state at time t + exact_state = initial_state.evolve(sc.linalg.expm(-1j * t * hamiltonian.to_matrix())) + + # store observables and time + ts.append(t) + energies.append(exact_state.expectation_value(hamiltonian.to_matrix())) + obs.append(exact_state.expectation_value(observable.to_matrix())) + + # next timestep + t += dt + + return ts, energies, obs + def test_pvqd(self): """Test a simple evolution.""" backend = Aer.get_backend("statevector_simulator") expectation = MatrixExpectation() - hamiltonian = X ^ X + hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) observable = Z ^ Z - time = 0.1 + time = 1 dt = 0.01 - ansatz = EfficientSU2(2, reps=3) - optimizer = SPSA(maxiter=100, learning_rate=0.01, perturbation=0.01) + ansatz = EfficientSU2(2, reps=2) + optimizer = SPSA(maxiter=300, learning_rate=0.1, perturbation=0.01) + # optimizer = SPSA() + # optimizer = COBYLA() + # optimizer = GradientDescent(learning_rate=0.01) + # optimizer = L_BFGS_B() initial_parameters = np.zeros(ansatz.num_parameters) initial_parameters[-2] = np.pi / 2 initial_parameters[-4] = np.pi / 2 - pvqd = PVQD(ansatz, initial_parameters, optimizer, backend, expectation) + # run pVQD keeping track of the energy and the magnetization + pvqd = PVQD(ansatz, initial_parameters, optimizer, + quantum_instance=backend, expectation=expectation) result = pvqd.evolve(hamiltonian, time, dt, observables=[hamiltonian, observable]) - fig, (ax1, ax2) = plt.subplots(2, 1) - ax1.plot(result.times, result.observables[:, 0]) - ax2.plot(result.times, result.observables[:, 1]) + # get reference results + initial_state = Statevector(ansatz.bind_parameters(initial_parameters)) + ref_t, ref_energy, ref_magn = self.exact(time, dt, hamiltonian, observable, initial_state) + + _, (ax1, ax2) = plt.subplots(2, 1, sharex=True) + ax1.set_title("Energy") + ax1.plot(result.times, result.observables[:, 0], label="pVQD") + ax1.plot(ref_t, ref_energy, label="exact") + ax2.set_title("Magnetization") + ax2.plot(result.times, result.observables[:, 1], label="pVQD") + ax2.plot(ref_t, ref_magn, label="exact") + ax2.set_xlabel(r"time $t$") + plt.tight_layout() plt.show() print(result) From 504aea97635b847aa9491a63b47f52a0a47e9bcb Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 10 Dec 2021 22:05:47 +0100 Subject: [PATCH 03/24] black --- qiskit/algorithms/pvqd.py | 7 ++++++- test/python/algorithms/test_pvqd.py | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/qiskit/algorithms/pvqd.py b/qiskit/algorithms/pvqd.py index 9276452bec73..37f25f087be0 100644 --- a/qiskit/algorithms/pvqd.py +++ b/qiskit/algorithms/pvqd.py @@ -21,7 +21,12 @@ from qiskit.circuit.library import PauliEvolutionGate from qiskit.providers import Backend from qiskit.opflow import ( - OperatorBase, CircuitSampler, ExpectationBase, ListOp, StateFn, GradientBase + OperatorBase, + CircuitSampler, + ExpectationBase, + ListOp, + StateFn, + GradientBase, ) from qiskit.synthesis import EvolutionSynthesis, LieTrotter from qiskit.utils import QuantumInstance diff --git a/test/python/algorithms/test_pvqd.py b/test/python/algorithms/test_pvqd.py index b592df9de001..af8ef3c473e5 100644 --- a/test/python/algorithms/test_pvqd.py +++ b/test/python/algorithms/test_pvqd.py @@ -70,8 +70,9 @@ def test_pvqd(self): initial_parameters[-4] = np.pi / 2 # run pVQD keeping track of the energy and the magnetization - pvqd = PVQD(ansatz, initial_parameters, optimizer, - quantum_instance=backend, expectation=expectation) + pvqd = PVQD( + ansatz, initial_parameters, optimizer, quantum_instance=backend, expectation=expectation + ) result = pvqd.evolve(hamiltonian, time, dt, observables=[hamiltonian, observable]) # get reference results From 2f3f7c296d1952abf8c968793455d3e789a99042 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 14 Dec 2021 18:28:38 +0100 Subject: [PATCH 04/24] refactor test --- qiskit/algorithms/pvqd.py | 1 + test/python/algorithms/test_pvqd.py | 68 ++++++++++++++++------------- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/qiskit/algorithms/pvqd.py b/qiskit/algorithms/pvqd.py index 37f25f087be0..aafc3e12e17d 100644 --- a/qiskit/algorithms/pvqd.py +++ b/qiskit/algorithms/pvqd.py @@ -90,6 +90,7 @@ def step( hamiltonian: The Hamiltonian under which to evolve. theta: The current parameters. dt: The time step. + initial_guess: The initial guess for the update to minimize the fidelity. Returns: A tuple consisting of the next parameters and the fidelity of the optimization. diff --git a/test/python/algorithms/test_pvqd.py b/test/python/algorithms/test_pvqd.py index af8ef3c473e5..42ef6f16b482 100644 --- a/test/python/algorithms/test_pvqd.py +++ b/test/python/algorithms/test_pvqd.py @@ -28,6 +28,17 @@ class TestPVQD(QiskitTestCase): """Tests for the pVQD algorithm.""" + def setUp(self): + super().setUp() + self.backend = Aer.get_backend("statevector_simulator") + self.expectation = MatrixExpectation() + self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + self.observable = Z ^ Z + self.ansatz = EfficientSU2(2, reps=2) + self.initial_parameters = np.zeros(self.ansatz.num_parameters) + self.initial_parameters[-2] = np.pi / 2 + self.initial_parameters[-4] = np.pi / 2 + def exact(self, final_time, dt, hamiltonian, observable, initial_state): """Get the exact values for energy and the observable.""" energies = [] # list of energies evaluated at timesteps dt @@ -50,43 +61,38 @@ def exact(self, final_time, dt, hamiltonian, observable, initial_state): def test_pvqd(self): """Test a simple evolution.""" + time = 0.02 + dt = 0.01 - backend = Aer.get_backend("statevector_simulator") - expectation = MatrixExpectation() - hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) - observable = Z ^ Z + optimizer = L_BFGS_B() - time = 1 + # run pVQD keeping track of the energy and the magnetization + pvqd = PVQD( + self.ansatz, self.initial_parameters, optimizer, quantum_instance=self.backend, + expectation=self.expectation + ) + result = pvqd.evolve(self.hamiltonian, time, dt, observables=[self.hamiltonian, self.observable]) + + self.assertTrue(len(result.fidelities) == 3) + self.assertTrue(np.all(result.times == np.array([0.0, 0.01, 0.02]))) + self.assertTrue(result.observables.shape == (3, 2)) + num_parameters = self.ansatz.num_parameters + self.assertTrue(len(result.parameters) == 3 and np.all( + [len(params) == num_parameters for params in result.parameters])) + + def test_gradients(self): + """Test the calculation of gradients with the gradient framework.""" + time = 0.01 dt = 0.01 - ansatz = EfficientSU2(2, reps=2) - optimizer = SPSA(maxiter=300, learning_rate=0.1, perturbation=0.01) - # optimizer = SPSA() - # optimizer = COBYLA() - # optimizer = GradientDescent(learning_rate=0.01) - # optimizer = L_BFGS_B() - initial_parameters = np.zeros(ansatz.num_parameters) - initial_parameters[-2] = np.pi / 2 - initial_parameters[-4] = np.pi / 2 + optimizer = GradientDescent(learning_rate=0.01) + gradient = Gradient() # run pVQD keeping track of the energy and the magnetization pvqd = PVQD( - ansatz, initial_parameters, optimizer, quantum_instance=backend, expectation=expectation + self.ansatz, self.initial_parameters, optimizer, gradient=gradient, + quantum_instance=self.backend, expectation=self.expectation ) - result = pvqd.evolve(hamiltonian, time, dt, observables=[hamiltonian, observable]) - - # get reference results - initial_state = Statevector(ansatz.bind_parameters(initial_parameters)) - ref_t, ref_energy, ref_magn = self.exact(time, dt, hamiltonian, observable, initial_state) - - _, (ax1, ax2) = plt.subplots(2, 1, sharex=True) - ax1.set_title("Energy") - ax1.plot(result.times, result.observables[:, 0], label="pVQD") - ax1.plot(ref_t, ref_energy, label="exact") - ax2.set_title("Magnetization") - ax2.plot(result.times, result.observables[:, 1], label="pVQD") - ax2.plot(ref_t, ref_magn, label="exact") - ax2.set_xlabel(r"time $t$") - plt.tight_layout() - plt.show() + result = pvqd.evolve(self.hamiltonian, time, dt) + print(result) From cd3e45911d66be3d0ce8cac4113a872178000d6f Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 4 Jul 2022 17:32:38 +0200 Subject: [PATCH 05/24] update to new time evo framework --- .../algorithms/evolvers/evolution_problem.py | 6 +- qiskit/algorithms/evolvers/pvqd.py | 264 ++++++++++++++++++ test/python/algorithms/test_pvqd.py | 58 ++-- 3 files changed, 304 insertions(+), 24 deletions(-) create mode 100644 qiskit/algorithms/evolvers/pvqd.py diff --git a/qiskit/algorithms/evolvers/evolution_problem.py b/qiskit/algorithms/evolvers/evolution_problem.py index b069effe1747..e0f9fe3063c6 100644 --- a/qiskit/algorithms/evolvers/evolution_problem.py +++ b/qiskit/algorithms/evolvers/evolution_problem.py @@ -31,7 +31,7 @@ def __init__( self, hamiltonian: OperatorBase, time: float, - initial_state: Union[StateFn, QuantumCircuit], + initial_state: Optional[Union[StateFn, QuantumCircuit]] = None, aux_operators: Optional[ListOrDict[OperatorBase]] = None, truncation_threshold: float = 1e-12, t_param: Optional[Parameter] = None, @@ -41,7 +41,9 @@ def __init__( Args: hamiltonian: The Hamiltonian under which to evolve the system. time: Total time of evolution. - initial_state: Quantum state to be evolved. + initial_state: The quantum state to be evolved for methods like Trotterization. + For variational time evolutions, where the evolution happens in an ansatz, + this argument is not required. aux_operators: Optional list of auxiliary operators to be evaluated with the evolved ``initial_state`` and their expectation values returned. truncation_threshold: Defines a threshold under which values can be assumed to be 0. diff --git a/qiskit/algorithms/evolvers/pvqd.py b/qiskit/algorithms/evolvers/pvqd.py new file mode 100644 index 000000000000..dc95db17854c --- /dev/null +++ b/qiskit/algorithms/evolvers/pvqd.py @@ -0,0 +1,264 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The projected Variational Quantum Dynamics Algorithm.""" + +from typing import Optional, Union, List, Tuple, Callable + +import numpy as np + +from qiskit.algorithms.optimizers import Optimizer +from qiskit.circuit import QuantumCircuit, ParameterVector +from qiskit.circuit.library import PauliEvolutionGate +from qiskit.providers import Backend +from qiskit.opflow import ( + OperatorBase, + CircuitSampler, + ExpectationBase, + ListOp, + StateFn, + GradientBase, +) +from qiskit.synthesis import EvolutionSynthesis, LieTrotter +from qiskit.utils import QuantumInstance + +from .evolution_problem import EvolutionProblem +from .evolution_result import EvolutionResult +from .real_evolver import RealEvolver + + +class PVQDResult(EvolutionResult): + """The result object for the pVQD algorithm.""" + + times = None + parameters = None + fidelities = None + estimated_error = None + observables = None + + +class PVQD(RealEvolver): + """The projected Variational Quantum Dynamics Algorithm.""" + + def __init__( + self, + ansatz: QuantumCircuit, + initial_parameters: np.ndarray, + timestep: float, + optimizer: Optimizer, + quantum_instance: Union[Backend, QuantumInstance], + expectation: ExpectationBase, + initial_guess: Optional[np.ndarray] = None, + gradient: Optional[GradientBase] = None, + evolution: Optional[EvolutionSynthesis] = None, + ) -> None: + """ + Args: + ansatz: A parameterized circuit preparing the variational ansatz to model the + time evolved quantum state. + initial_parameters: The initial parameters for the ansatz. + timestep: The time step. + optimizer: The classical optimizers used to minimize the overlap between + Trotterization and ansatz. + quantum_instance: The backend of quantum instance used to evaluate the circuits. + expectation: The expectation converter to evaluate expectation values. + initial_guess: The initial guess for the first VQE optimization. Afterwards the + previous iteration result is used as initial guess. + gradient: A gradient converter to use for gradient-based optimizers. + evolution: The evolution synthesis to use for the construction of the Trotter step. + Defaults to first-order Lie-Trotter decomposition. + """ + if evolution is None: + evolution = LieTrotter() + + self.ansatz = ansatz + self.initial_parameters = initial_parameters + self.timestep = timestep + self.optimizer = optimizer + self.gradient = gradient + self.initial_guess = initial_guess + self.expectation = expectation + self.evolution = evolution + + self._sampler = CircuitSampler(quantum_instance) + + def step( + self, hamiltonian: OperatorBase, theta: np.ndarray, dt: float, initial_guess: np.ndarray + ) -> Tuple[np.ndarray, float]: + """Perform a single time step. + + Args: + hamiltonian: The Hamiltonian under which to evolve. + theta: The current parameters. + dt: The time step. + initial_guess: The initial guess for the update to minimize the fidelity. + + Returns: + A tuple consisting of the next parameters and the fidelity of the optimization. + """ + # construct cost function + overlap, gradient = self.get_overlap(hamiltonian, dt, theta) + + # call optimizer + optimizer_result = self.optimizer.minimize(lambda x: -overlap(x), initial_guess, gradient) + + return theta + optimizer_result.x, -optimizer_result.fun + + def get_overlap( + self, hamiltonian: OperatorBase, dt: float, current_parameters: np.ndarray + ) -> Callable[[np.ndarray], float]: + """Get a function to evaluate the overlap between Trotter step and ansatz. + + Args: + hamiltonian: The Hamiltonian under which to evolve. + dt: The time step. + current_parameters: The current parameters. + + Returns: + A callable to evaluate the overlap. + """ + # use Trotterization to evolve the current state + trotterized = self.ansatz.bind_parameters(current_parameters) + trotterized.append( + PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution), self.ansatz.qubits + ) + + # define the overlap of the Trotterized state and the ansatz + x = ParameterVector("w", self.ansatz.num_parameters) + shifted = self.ansatz.assign_parameters(current_parameters + x) + overlap = StateFn(trotterized).adjoint() @ StateFn(shifted) + + # apply the expectation converter + converted = self.expectation.convert(overlap) + + ansatz_parameters = self.ansatz.parameters + + def evaluate_overlap(displacement: np.ndarray) -> float: + """Evaluate the overlap of the ansatz with the Trotterized evolution. + + Args: + displacement: The parameters for the ansatz. + + Returns: + The fidelity of the ansatz with parameters ``theta`` and the Trotterized evolution. + """ + # evaluate with the circuit sampler + value_dict = dict(zip(x, displacement)) + sampled = self._sampler.convert(converted, params=value_dict) + return np.abs(sampled.eval()) ** 2 + + if self.gradient is not None: + gradient = self.gradient.convert(overlap) + + def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: + """Evaluate the gradient.""" + # evaluate with the circuit sampler + value_dict = dict(zip(ansatz_parameters, current_parameters + displacement)) + sampled = self._sampler.convert(gradient, params=value_dict) + return 2 * sampled.eval() + + return evaluate_overlap, evaluate_gradient + + return evaluate_overlap, None + + def _get_observable_evaluator(self, observables): + if isinstance(observables, list): + observables = ListOp(observables) + + expectation_value = StateFn(observables, is_measurement=True) @ StateFn(self.ansatz) + converted = self.expectation.convert(expectation_value) + + ansatz_parameters = self.ansatz.parameters + + def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]: + """Evaluate the observables for the ansatz parameters ``theta``. + + Args: + theta: The ansatz parameters. + + Returns: + The observables evaluated at the ansatz parameters. + """ + value_dict = dict(zip(ansatz_parameters, theta)) + sampled = self._sampler.convert(converted, params=value_dict) + return sampled.eval() + + return evaluate_observables + + def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: + """ + Args: + evolution_problem: The evolution problem containing the hamiltonian, total evolution + time and observables to evaluate. + + Returns: + A result object containing the evolution information and evaluated observables. + + Raises: + ValueError: If the evolution time is not positive or the timestep is too small. + """ + time = evolution_problem.time + observables = evolution_problem.aux_operators + hamiltonian = evolution_problem.hamiltonian + + if time <= 0: + raise ValueError(f"The evolution time must be larger than 0, but is {time}.") + + if not 0 < self.timestep <= time: + raise ValueError( + f"The time step ({self.timestep}) must be larger than 0 and smaller equal " + f"the evolution time ({time})." + ) + + # get the function to evaluate the observables for a given set of ansatz parameters + evaluate_observables = self._get_observable_evaluator(observables) + + observable_values = [evaluate_observables(self.initial_parameters)] + fidelities = [1] + times = [0] + parameters = [self.initial_parameters] + + current_time = 0 + + if self.initial_guess is None: + initial_guess = np.random.random(self.initial_parameters.size) * 0.01 + else: + initial_guess = self.initial_guess + + while current_time < time: + # perform VQE to find the next parameters + next_parameters, fidelity = self.step( + hamiltonian, parameters[-1], self.timestep, initial_guess + ) + + # set initial guess to last parameter update + initial_guess = next_parameters - parameters[-1] + + # store parameters + parameters.append(next_parameters) + fidelities.append(fidelity) + observable_values.append(evaluate_observables(next_parameters)) + + # increase time + current_time += self.timestep + times.append(current_time) + + evolved_state = self.ansatz.bind_parameters(parameters[-1]) + + result = PVQDResult(evolved_state=evolved_state, aux_ops_evaluated=observable_values[-1]) + result.times = times + result.parameters = parameters + result.fidelities = fidelities + result.estimated_error = np.prod(result.fidelities) + result.observables = np.asarray(observable_values) + + return result diff --git a/test/python/algorithms/test_pvqd.py b/test/python/algorithms/test_pvqd.py index 42ef6f16b482..15df251fccd7 100644 --- a/test/python/algorithms/test_pvqd.py +++ b/test/python/algorithms/test_pvqd.py @@ -15,8 +15,9 @@ import numpy as np import scipy as sc -from qiskit import Aer -from qiskit.algorithms import PVQD +from qiskit import BasicAer as Aer +from qiskit.algorithms.evolvers import EvolutionProblem +from qiskit.algorithms.evolvers.pvqd import PVQD from qiskit.algorithms.optimizers import SPSA, L_BFGS_B, GradientDescent, COBYLA from qiskit.circuit.library import EfficientSU2 from qiskit.opflow import X, Z, I, MatrixExpectation, Gradient @@ -68,31 +69,44 @@ def test_pvqd(self): # run pVQD keeping track of the energy and the magnetization pvqd = PVQD( - self.ansatz, self.initial_parameters, optimizer, quantum_instance=self.backend, - expectation=self.expectation + self.ansatz, + self.initial_parameters, + dt, + optimizer, + quantum_instance=self.backend, + expectation=self.expectation, ) - result = pvqd.evolve(self.hamiltonian, time, dt, observables=[self.hamiltonian, self.observable]) + problem = EvolutionProblem( + self.hamiltonian, time, aux_operators=[self.hamiltonian, self.observable] + ) + result = pvqd.evolve(problem) self.assertTrue(len(result.fidelities) == 3) self.assertTrue(np.all(result.times == np.array([0.0, 0.01, 0.02]))) self.assertTrue(result.observables.shape == (3, 2)) num_parameters = self.ansatz.num_parameters - self.assertTrue(len(result.parameters) == 3 and np.all( - [len(params) == num_parameters for params in result.parameters])) - - def test_gradients(self): - """Test the calculation of gradients with the gradient framework.""" - time = 0.01 - dt = 0.01 - - optimizer = GradientDescent(learning_rate=0.01) - gradient = Gradient() - - # run pVQD keeping track of the energy and the magnetization - pvqd = PVQD( - self.ansatz, self.initial_parameters, optimizer, gradient=gradient, - quantum_instance=self.backend, expectation=self.expectation + self.assertTrue( + len(result.parameters) == 3 + and np.all([len(params) == num_parameters for params in result.parameters]) ) - result = pvqd.evolve(self.hamiltonian, time, dt) - print(result) + # def test_gradients(self): + # """Test the calculation of gradients with the gradient framework.""" + # time = 0.01 + # dt = 0.01 + + # optimizer = GradientDescent(learning_rate=0.01) + # gradient = Gradient() + + # # run pVQD keeping track of the energy and the magnetization + # pvqd = PVQD( + # self.ansatz, + # self.initial_parameters, + # optimizer, + # gradient=gradient, + # quantum_instance=self.backend, + # expectation=self.expectation, + # ) + # result = pvqd.evolve(self.hamiltonian, time, dt) + + # print(result) From 32fd38743fb9bf06d73d77652a0249af232f78c3 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 6 Jul 2022 16:05:25 +0200 Subject: [PATCH 06/24] add gradients --- qiskit/algorithms/evolvers/pvqd.py | 105 +++++++++++----- .../algorithms/{ => evolvers}/test_pvqd.py | 113 +++++++++--------- 2 files changed, 132 insertions(+), 86 deletions(-) rename test/python/algorithms/{ => evolvers}/test_pvqd.py (50%) diff --git a/qiskit/algorithms/evolvers/pvqd.py b/qiskit/algorithms/evolvers/pvqd.py index dc95db17854c..6a45d3dbf63d 100644 --- a/qiskit/algorithms/evolvers/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd.py @@ -16,8 +16,10 @@ import numpy as np +from qiskit import transpile +from qiskit.exceptions import QiskitError from qiskit.algorithms.optimizers import Optimizer -from qiskit.circuit import QuantumCircuit, ParameterVector +from qiskit.circuit import QuantumCircuit, ParameterVector, ParameterExpression, Parameter from qiskit.circuit.library import PauliEvolutionGate from qiskit.providers import Backend from qiskit.opflow import ( @@ -26,8 +28,8 @@ ExpectationBase, ListOp, StateFn, - GradientBase, ) +from qiskit.opflow.gradients.circuit_gradients import ParamShift from qiskit.synthesis import EvolutionSynthesis, LieTrotter from qiskit.utils import QuantumInstance @@ -58,7 +60,6 @@ def __init__( quantum_instance: Union[Backend, QuantumInstance], expectation: ExpectationBase, initial_guess: Optional[np.ndarray] = None, - gradient: Optional[GradientBase] = None, evolution: Optional[EvolutionSynthesis] = None, ) -> None: """ @@ -73,7 +74,6 @@ def __init__( expectation: The expectation converter to evaluate expectation values. initial_guess: The initial guess for the first VQE optimization. Afterwards the previous iteration result is used as initial guess. - gradient: A gradient converter to use for gradient-based optimizers. evolution: The evolution synthesis to use for the construction of the Trotter step. Defaults to first-order Lie-Trotter decomposition. """ @@ -84,7 +84,6 @@ def __init__( self.initial_parameters = initial_parameters self.timestep = timestep self.optimizer = optimizer - self.gradient = gradient self.initial_guess = initial_guess self.expectation = expectation self.evolution = evolution @@ -106,17 +105,18 @@ def step( A tuple consisting of the next parameters and the fidelity of the optimization. """ # construct cost function - overlap, gradient = self.get_overlap(hamiltonian, dt, theta) + loss, gradient = self.get_loss(hamiltonian, dt, theta) # call optimizer - optimizer_result = self.optimizer.minimize(lambda x: -overlap(x), initial_guess, gradient) + optimizer_result = self.optimizer.minimize(loss, initial_guess, gradient) + fidelity = 1 - optimizer_result.fun - return theta + optimizer_result.x, -optimizer_result.fun + return theta + optimizer_result.x, fidelity - def get_overlap( + def get_loss( self, hamiltonian: OperatorBase, dt: float, current_parameters: np.ndarray ) -> Callable[[np.ndarray], float]: - """Get a function to evaluate the overlap between Trotter step and ansatz. + """Get a function to evaluate the infidelity between Trotter step and ansatz. Args: hamiltonian: The Hamiltonian under which to evolve. @@ -124,7 +124,7 @@ def get_overlap( current_parameters: The current parameters. Returns: - A callable to evaluate the overlap. + A callable to evaluate the infidelity. """ # use Trotterization to evolve the current state trotterized = self.ansatz.bind_parameters(current_parameters) @@ -140,9 +140,9 @@ def get_overlap( # apply the expectation converter converted = self.expectation.convert(overlap) - ansatz_parameters = self.ansatz.parameters - - def evaluate_overlap(displacement: np.ndarray) -> float: + def evaluate_loss( + displacement: Union[np.ndarray, List[np.ndarray]] + ) -> Union[float, List[float]]: """Evaluate the overlap of the ansatz with the Trotterized evolution. Args: @@ -151,24 +151,72 @@ def evaluate_overlap(displacement: np.ndarray) -> float: Returns: The fidelity of the ansatz with parameters ``theta`` and the Trotterized evolution. """ - # evaluate with the circuit sampler - value_dict = dict(zip(x, displacement)) + if isinstance(displacement, list): + displacement = np.asarray(displacement) + value_dict = {x_i: displacement[:, i].tolist() for i, x_i in enumerate(x)} + else: + value_dict = dict(zip(x, displacement)) + sampled = self._sampler.convert(converted, params=value_dict) - return np.abs(sampled.eval()) ** 2 + return 1 - np.abs(sampled.eval()) ** 2 + + def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: + """Evaluate the gradient with the parameter-shift rule. + + This is hardcoded here since the gradient framework does not support computing + gradients for overlaps. + + Args: + displacement: The parameters for the ansatz. - if self.gradient is not None: - gradient = self.gradient.convert(overlap) + Returns: + The gradient. + """ + # construct lists where each element is shifted by plus (or minus) pi/2 + dim = displacement.size + plus_shifts = (displacement + np.pi / 2 * np.identity(dim)).tolist() + minus_shifts = (displacement - np.pi / 2 * np.identity(dim)).tolist() + + evaluated = evaluate_loss(plus_shifts + minus_shifts) + + gradient = (evaluated[:dim] - evaluated[dim:]) / 2 - def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: - """Evaluate the gradient.""" - # evaluate with the circuit sampler - value_dict = dict(zip(ansatz_parameters, current_parameters + displacement)) - sampled = self._sampler.convert(gradient, params=value_dict) - return 2 * sampled.eval() + return gradient - return evaluate_overlap, evaluate_gradient + return evaluate_loss, evaluate_gradient - return evaluate_overlap, None + def _check_gradient_supported(self) -> QuantumCircuit: + """Check whether we can apply a simple parameter shift rule to obtain gradients.""" + + # check whether the circuit can be unrolled to supported gates + try: + unrolled = transpile( + self.ansatz, basis_gates=ParamShift.SUPPORTED_GATES, optimization_level=0 + ) + except Exception as exc: + raise QiskitError( + "Failed to compute the gradients as we could not apply the " + "parameter shift rule to the ansatz." + ) from exc + + # check whether all parameters are unique and we do not need to apply the chain rule + # (since it's not implemented yet) + all_parameters = [] + for circuit_instruction in unrolled.data: + for param in circuit_instruction.operation.params: + if isinstance(param, ParameterExpression): + if isinstance(param, Parameter): + all_parameters.append(param) + else: + raise QiskitError( + "Circuit is only allowed to have plain parameters, as " + "the chain rule is not yet implemented." + ) + + if len(all_parameters) != self.ansatz.num_parameters: + raise QiskitError( + "Circuit parameters must be unique, the product rule is not yet " "implemented." + ) def _get_observable_evaluator(self, observables): if isinstance(observables, list): @@ -210,9 +258,6 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: observables = evolution_problem.aux_operators hamiltonian = evolution_problem.hamiltonian - if time <= 0: - raise ValueError(f"The evolution time must be larger than 0, but is {time}.") - if not 0 < self.timestep <= time: raise ValueError( f"The time step ({self.timestep}) must be larger than 0 and smaller equal " diff --git a/test/python/algorithms/test_pvqd.py b/test/python/algorithms/evolvers/test_pvqd.py similarity index 50% rename from test/python/algorithms/test_pvqd.py rename to test/python/algorithms/evolvers/test_pvqd.py index 15df251fccd7..dc379b4a2abc 100644 --- a/test/python/algorithms/test_pvqd.py +++ b/test/python/algorithms/evolvers/test_pvqd.py @@ -10,22 +10,22 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -from qiskit.test import QiskitTestCase +"""Tests for PVQD.""" +from ddt import ddt, data import numpy as np -import scipy as sc + +from qiskit.test import QiskitTestCase from qiskit import BasicAer as Aer from qiskit.algorithms.evolvers import EvolutionProblem from qiskit.algorithms.evolvers.pvqd import PVQD -from qiskit.algorithms.optimizers import SPSA, L_BFGS_B, GradientDescent, COBYLA +from qiskit.algorithms.optimizers import L_BFGS_B, GradientDescent, SPSA from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import X, Z, I, MatrixExpectation, Gradient -from qiskit.quantum_info import Statevector - -import matplotlib.pyplot as plt +from qiskit.opflow import X, Z, I, MatrixExpectation +@ddt class TestPVQD(QiskitTestCase): """Tests for the pVQD algorithm.""" @@ -35,44 +35,25 @@ def setUp(self): self.expectation = MatrixExpectation() self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) self.observable = Z ^ Z - self.ansatz = EfficientSU2(2, reps=2) + self.ansatz = EfficientSU2(2, reps=1) self.initial_parameters = np.zeros(self.ansatz.num_parameters) - self.initial_parameters[-2] = np.pi / 2 - self.initial_parameters[-4] = np.pi / 2 - - def exact(self, final_time, dt, hamiltonian, observable, initial_state): - """Get the exact values for energy and the observable.""" - energies = [] # list of energies evaluated at timesteps dt - obs = [] # list of observables - ts = [] # list of timepoints at which energy/obs are evaluated - t = 0 - while t <= final_time: - # get exact state at time t - exact_state = initial_state.evolve(sc.linalg.expm(-1j * t * hamiltonian.to_matrix())) - - # store observables and time - ts.append(t) - energies.append(exact_state.expectation_value(hamiltonian.to_matrix())) - obs.append(exact_state.expectation_value(observable.to_matrix())) - - # next timestep - t += dt - - return ts, energies, obs - - def test_pvqd(self): + + @data(True, False) + def test_pvqd(self, gradient): """Test a simple evolution.""" time = 0.02 - dt = 0.01 - optimizer = L_BFGS_B() + if gradient: + optimizer = GradientDescent(maxiter=1) + else: + optimizer = L_BFGS_B(maxiter=1) # run pVQD keeping track of the energy and the magnetization pvqd = PVQD( self.ansatz, self.initial_parameters, - dt, - optimizer, + timestep=0.01, + optimizer=optimizer, quantum_instance=self.backend, expectation=self.expectation, ) @@ -90,23 +71,43 @@ def test_pvqd(self): and np.all([len(params) == num_parameters for params in result.parameters]) ) - # def test_gradients(self): - # """Test the calculation of gradients with the gradient framework.""" - # time = 0.01 - # dt = 0.01 - - # optimizer = GradientDescent(learning_rate=0.01) - # gradient = Gradient() - - # # run pVQD keeping track of the energy and the magnetization - # pvqd = PVQD( - # self.ansatz, - # self.initial_parameters, - # optimizer, - # gradient=gradient, - # quantum_instance=self.backend, - # expectation=self.expectation, - # ) - # result = pvqd.evolve(self.hamiltonian, time, dt) - - # print(result) + def test_invalid_timestep(self): + """Test raises if the timestep is larger than the evolution time.""" + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + timestep=1, + optimizer=L_BFGS_B(), + quantum_instance=self.backend, + expectation=self.expectation, + ) + problem = EvolutionProblem( + self.hamiltonian, time=0.01, aux_operators=[self.hamiltonian, self.observable] + ) + + with self.assertRaises(ValueError): + _ = pvqd.evolve(problem) + + def test_initial_guess_and_observables(self): + """Test doing no optimizations stays at initial guess.""" + initial_guess = np.zeros(self.ansatz.num_parameters) + + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + timestep=0.01, + optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), + initial_guess=initial_guess, + quantum_instance=self.backend, + expectation=self.expectation, + ) + problem = EvolutionProblem( + self.hamiltonian, time=0.1, aux_operators=[self.hamiltonian, self.observable] + ) + + result = pvqd.evolve(problem) + + observables = result.aux_ops_evaluated + print(result.evolved_state.draw()) + self.assertEqual(observables[0], 0.1) # expected energy + self.assertEqual(observables[1], 1) # expected magnetization From c5b9c12e530f5c8d12987549ba8a771049c965b6 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 8 Jul 2022 11:44:25 +0200 Subject: [PATCH 07/24] use gradient only if supported --- qiskit/algorithms/evolvers/pvqd.py | 149 +++++++++++++------ test/python/algorithms/evolvers/test_pvqd.py | 80 +++++++++- 2 files changed, 178 insertions(+), 51 deletions(-) diff --git a/qiskit/algorithms/evolvers/pvqd.py b/qiskit/algorithms/evolvers/pvqd.py index 6a45d3dbf63d..e83840c5f771 100644 --- a/qiskit/algorithms/evolvers/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd.py @@ -14,11 +14,11 @@ from typing import Optional, Union, List, Tuple, Callable +import logging import numpy as np -from qiskit import transpile -from qiskit.exceptions import QiskitError -from qiskit.algorithms.optimizers import Optimizer +from qiskit import transpile, QiskitError +from qiskit.algorithms.optimizers import Optimizer, Minimizer from qiskit.circuit import QuantumCircuit, ParameterVector, ParameterExpression, Parameter from qiskit.circuit.library import PauliEvolutionGate from qiskit.providers import Backend @@ -37,15 +37,41 @@ from .evolution_result import EvolutionResult from .real_evolver import RealEvolver +logger = logging.getLogger(__name__) + class PVQDResult(EvolutionResult): """The result object for the pVQD algorithm.""" - times = None - parameters = None - fidelities = None - estimated_error = None - observables = None + def __init__( + self, + evolved_state: Union[StateFn, QuantumCircuit, OperatorBase], + # TODO: aux_ops_evaluated: Optional[ListOrDict[Tuple[complex, complex]]] = None, + aux_ops_evaluated: Optional[List[Tuple[complex, complex]]] = None, + times: Optional[List[float]] = None, + parameters: Optional[List[np.ndarray]] = None, + fidelities: Optional[List[float]] = None, + estimated_error: Optional[float] = None, + observables: Optional[List[List[float]]] = None, + ): + """ + Args: + evolved_state: An evolved quantum state. + aux_ops_evaluated: Optional list of observables for which expected values on an evolved + state are calculated. These values are in fact tuples formatted as (mean, standard + deviation). + times: The times evaluated during the time integration. + parameters: The parameter values at each evaluation time. + fidelities: The fidelity of the Trotter step and variational update at each iteration. + estimated_error: The overall estimated error evaluated as product of all fidelities. + observables: The value of the observables evaluated at each iteration. + """ + super().__init__(evolved_state, aux_ops_evaluated) + self.times = times + self.parameters = parameters + self.fidelities = fidelities + self.estimated_error = estimated_error + self.observables = observables class PVQD(RealEvolver): @@ -56,11 +82,12 @@ def __init__( ansatz: QuantumCircuit, initial_parameters: np.ndarray, timestep: float, - optimizer: Optimizer, + optimizer: Union[Optimizer, Minimizer], quantum_instance: Union[Backend, QuantumInstance], expectation: ExpectationBase, initial_guess: Optional[np.ndarray] = None, evolution: Optional[EvolutionSynthesis] = None, + gradients: bool = True, ) -> None: """ Args: @@ -69,13 +96,16 @@ def __init__( initial_parameters: The initial parameters for the ansatz. timestep: The time step. optimizer: The classical optimizers used to minimize the overlap between - Trotterization and ansatz. + Trotterization and ansatz. Can be either a :class:`.Optimizer` or a callable + using the :class:`.Minimizer` protocol. quantum_instance: The backend of quantum instance used to evaluate the circuits. expectation: The expectation converter to evaluate expectation values. initial_guess: The initial guess for the first VQE optimization. Afterwards the previous iteration result is used as initial guess. evolution: The evolution synthesis to use for the construction of the Trotter step. Defaults to first-order Lie-Trotter decomposition. + gradients: If True, use the parameter shift rule to compute gradients. If False, + the optimizer will not be passed a gradient callable. """ if evolution is None: evolution = LieTrotter() @@ -87,6 +117,7 @@ def __init__( self.initial_guess = initial_guess self.expectation = expectation self.evolution = evolution + self.gradients = gradients self._sampler = CircuitSampler(quantum_instance) @@ -108,7 +139,11 @@ def step( loss, gradient = self.get_loss(hamiltonian, dt, theta) # call optimizer - optimizer_result = self.optimizer.minimize(loss, initial_guess, gradient) + if isinstance(self.optimizer, Optimizer): + optimizer_result = self.optimizer.minimize(loss, initial_guess, gradient) + else: + optimizer_result = self.optimizer(loss, initial_guess, gradient) + fidelity = 1 - optimizer_result.fun return theta + optimizer_result.x, fidelity @@ -160,32 +195,37 @@ def evaluate_loss( sampled = self._sampler.convert(converted, params=value_dict) return 1 - np.abs(sampled.eval()) ** 2 - def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: - """Evaluate the gradient with the parameter-shift rule. + if self._check_gradient_supported() and self.gradients: - This is hardcoded here since the gradient framework does not support computing - gradients for overlaps. + def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: + """Evaluate the gradient with the parameter-shift rule. - Args: - displacement: The parameters for the ansatz. + This is hardcoded here since the gradient framework does not support computing + gradients for overlaps. - Returns: - The gradient. - """ - # construct lists where each element is shifted by plus (or minus) pi/2 - dim = displacement.size - plus_shifts = (displacement + np.pi / 2 * np.identity(dim)).tolist() - minus_shifts = (displacement - np.pi / 2 * np.identity(dim)).tolist() + Args: + displacement: The parameters for the ansatz. - evaluated = evaluate_loss(plus_shifts + minus_shifts) + Returns: + The gradient. + """ + # construct lists where each element is shifted by plus (or minus) pi/2 + dim = displacement.size + plus_shifts = (displacement + np.pi / 2 * np.identity(dim)).tolist() + minus_shifts = (displacement - np.pi / 2 * np.identity(dim)).tolist() - gradient = (evaluated[:dim] - evaluated[dim:]) / 2 + evaluated = evaluate_loss(plus_shifts + minus_shifts) - return gradient + gradient = (evaluated[:dim] - evaluated[dim:]) / 2 + + return gradient + + else: + evaluate_gradient = None return evaluate_loss, evaluate_gradient - def _check_gradient_supported(self) -> QuantumCircuit: + def _check_gradient_supported(self) -> bool: """Check whether we can apply a simple parameter shift rule to obtain gradients.""" # check whether the circuit can be unrolled to supported gates @@ -193,11 +233,13 @@ def _check_gradient_supported(self) -> QuantumCircuit: unrolled = transpile( self.ansatz, basis_gates=ParamShift.SUPPORTED_GATES, optimization_level=0 ) - except Exception as exc: - raise QiskitError( - "Failed to compute the gradients as we could not apply the " - "parameter shift rule to the ansatz." - ) from exc + except QiskitError: + # failed to map to supported basis + logger.log( + logging.INFO, + "No gradient support: Failed to unroll to gates supported by parameter-shift.", + ) + return False # check whether all parameters are unique and we do not need to apply the chain rule # (since it's not implemented yet) @@ -208,15 +250,22 @@ def _check_gradient_supported(self) -> QuantumCircuit: if isinstance(param, Parameter): all_parameters.append(param) else: - raise QiskitError( - "Circuit is only allowed to have plain parameters, as " - "the chain rule is not yet implemented." + logger.log( + logging.INFO, + "No gradient support: Circuit is only allowed to have plain parameters, " + "as the chain rule is not yet implemented.", ) + return False if len(all_parameters) != self.ansatz.num_parameters: - raise QiskitError( - "Circuit parameters must be unique, the product rule is not yet " "implemented." + logger.log( + logging.INFO, + "No gradient support: Circuit is only allowed to have unique parameters, " + "as the product rule is not yet implemented.", ) + return False + + return True def _get_observable_evaluator(self, observables): if isinstance(observables, list): @@ -265,9 +314,10 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: ) # get the function to evaluate the observables for a given set of ansatz parameters - evaluate_observables = self._get_observable_evaluator(observables) + if observables is not None: + evaluate_observables = self._get_observable_evaluator(observables) + observable_values = [evaluate_observables(self.initial_parameters)] - observable_values = [evaluate_observables(self.initial_parameters)] fidelities = [1] times = [0] parameters = [self.initial_parameters] @@ -291,7 +341,8 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: # store parameters parameters.append(next_parameters) fidelities.append(fidelity) - observable_values.append(evaluate_observables(next_parameters)) + if observables is not None: + observable_values.append(evaluate_observables(next_parameters)) # increase time current_time += self.timestep @@ -299,11 +350,15 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: evolved_state = self.ansatz.bind_parameters(parameters[-1]) - result = PVQDResult(evolved_state=evolved_state, aux_ops_evaluated=observable_values[-1]) - result.times = times - result.parameters = parameters - result.fidelities = fidelities - result.estimated_error = np.prod(result.fidelities) - result.observables = np.asarray(observable_values) + result = PVQDResult( + evolved_state=evolved_state, + times=times, + parameters=parameters, + fidelities=fidelities, + estimated_error=np.prod(fidelities), + ) + if observables is not None: + result.observables = observable_values + result.aux_ops_evaluated = observable_values[-1] return result diff --git a/test/python/algorithms/evolvers/test_pvqd.py b/test/python/algorithms/evolvers/test_pvqd.py index dc379b4a2abc..322bef22ec0a 100644 --- a/test/python/algorithms/evolvers/test_pvqd.py +++ b/test/python/algorithms/evolvers/test_pvqd.py @@ -12,19 +12,42 @@ """Tests for PVQD.""" +from functools import partial from ddt import ddt, data import numpy as np from qiskit.test import QiskitTestCase from qiskit import BasicAer as Aer +from qiskit.circuit import QuantumCircuit, Parameter, Gate from qiskit.algorithms.evolvers import EvolutionProblem from qiskit.algorithms.evolvers.pvqd import PVQD -from qiskit.algorithms.optimizers import L_BFGS_B, GradientDescent, SPSA +from qiskit.algorithms.optimizers import L_BFGS_B, GradientDescent, SPSA, OptimizerResult from qiskit.circuit.library import EfficientSU2 from qiskit.opflow import X, Z, I, MatrixExpectation +# pylint: disable=unused-argument, invalid-name +def gradient_supplied(fun, x0, jac, info): + """A mock optimizer that checks whether the gradient is supported or not.""" + result = OptimizerResult() + result.x = x0 + result.fun = 0 + info["has_gradient"] = jac is not None + + return result + + +class WhatAmI(Gate): + """An custom opaque gate that can be inverted but not decomposed.""" + + def __init__(self, angle): + super().__init__(name="whatami", num_qubits=2, params=[angle]) + + def inverse(self): + return WhatAmI(-self.params[0]) + + @ddt class TestPVQD(QiskitTestCase): """Tests for the pVQD algorithm.""" @@ -63,8 +86,8 @@ def test_pvqd(self, gradient): result = pvqd.evolve(problem) self.assertTrue(len(result.fidelities) == 3) - self.assertTrue(np.all(result.times == np.array([0.0, 0.01, 0.02]))) - self.assertTrue(result.observables.shape == (3, 2)) + self.assertTrue(np.all(result.times == [0.0, 0.01, 0.02])) + self.assertTrue(np.asarray(result.observables).shape == (3, 2)) num_parameters = self.ansatz.num_parameters self.assertTrue( len(result.parameters) == 3 @@ -108,6 +131,55 @@ def test_initial_guess_and_observables(self): result = pvqd.evolve(problem) observables = result.aux_ops_evaluated - print(result.evolved_state.draw()) self.assertEqual(observables[0], 0.1) # expected energy self.assertEqual(observables[1], 1) # expected magnetization + + def test_gradient_supported(self): + """Test the gradient support is correctly determined.""" + # gradient supported here + empty = QuantumCircuit(2) + wrapped = EfficientSU2(2) # a circuit wrapped into a big instruction + plain = wrapped.decompose() # a plain circuit with already supported instructions + + # gradients not supported on the following circuits + x = Parameter("x") + duplicated = QuantumCircuit(2) + duplicated.rx(x, 0) + duplicated.rx(x, 1) + + needs_chainrule = QuantumCircuit(2) + needs_chainrule.rx(2 * x, 0) + + custom_gate = WhatAmI(x) + unsupported = QuantumCircuit(2) + unsupported.append(custom_gate, [0, 1]) + + tests = [ + (empty, True), # tuple: (circuit, gradient support) + (wrapped, True), + (plain, True), + (duplicated, False), + (needs_chainrule, False), + (unsupported, False), + ] + + # used to store the info if a gradient callable is passed into the + # optimizer of not + info = {"has_gradient": None} + optimizer = partial(gradient_supplied, info=info) + + pvqd = PVQD( + ansatz=empty, + initial_parameters=np.array([]), + timestep=0.01, + optimizer=optimizer, + quantum_instance=self.backend, + expectation=self.expectation, + ) + problem = EvolutionProblem(self.hamiltonian, time=0.01) + for circuit, expected_support in tests: + with self.subTest(circuit=circuit, expected_support=expected_support): + pvqd.ansatz = circuit + pvqd.initial_parameters = np.zeros(circuit.num_parameters) + _ = pvqd.evolve(problem) + self.assertEqual(info["has_gradient"], expected_support) From a361656a5f6372e48dca0b7de212cc8ff9408bde Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 8 Jul 2022 16:20:40 +0200 Subject: [PATCH 08/24] polishing! - reno - remove old pvqd file - allow attributes to be None - more tests --- qiskit/algorithms/evolvers/pvqd.py | 134 ++++++++-- qiskit/algorithms/pvqd.py | 249 ------------------ .../project-dynamics-2f848a5f89655429.yaml | 51 ++++ test/python/algorithms/evolvers/test_pvqd.py | 60 ++++- 4 files changed, 215 insertions(+), 279 deletions(-) delete mode 100644 qiskit/algorithms/pvqd.py create mode 100644 releasenotes/notes/project-dynamics-2f848a5f89655429.yaml diff --git a/qiskit/algorithms/evolvers/pvqd.py b/qiskit/algorithms/evolvers/pvqd.py index e83840c5f771..4169a9c82a98 100644 --- a/qiskit/algorithms/evolvers/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2021. +# (C) Copyright IBM 2019, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -75,19 +75,69 @@ def __init__( class PVQD(RealEvolver): - """The projected Variational Quantum Dynamics Algorithm.""" + """The projected Variational Quantum Dynamics Algorithm. + + In each timestep this algorithm computes the next state with a Trotter formula + (specified by the ``evolution`` argument) and projects it onto a variational form (``ansatz``). + The projection is determined by maximizing the fidelity of the Trotter-evolved state + and the ansatz, using a classical optimization routine. See Ref. [1] for details. + + Example: + + .. code-block:: python + + import numpy as np + + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import X, Z, I, MatrixExpectation + + backend = BasicAer.get_backend("statevector_simulator") + expectation = MatrixExpectation() + hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + observable = Z ^ Z + ansatz = EfficientSU2(2, reps=1) + initial_parameters = np.zeros(ansatz.num_parameters) + + time = 0.02 + optimizer = L_BFGS_B() + + # setup the algorithm + pvqd = PVQD( + ansatz, + initial_parameters, + timestep=0.01, + optimizer=optimizer, + quantum_instance=backend, + expectation=expectation + ) + + # specify the evolution problem + problem = EvolutionProblem( + hamiltonian, time, aux_operators=[hamiltonian, observable] + ) + + # and evolve! + result = pvqd.evolve(problem) + + References: + + [1] Stefano Barison, Filippo Vicentini, and Giuseppe Carleo (2021), An efficient + quantum algorithm for the time evolution of parameterized circuits, + `Quantum 5, 512 `_. + """ def __init__( self, - ansatz: QuantumCircuit, - initial_parameters: np.ndarray, - timestep: float, - optimizer: Union[Optimizer, Minimizer], - quantum_instance: Union[Backend, QuantumInstance], - expectation: ExpectationBase, + ansatz: Optional[QuantumCircuit] = None, + initial_parameters: Optional[np.ndarray] = None, + timestep: Optional[float] = None, + optimizer: Optional[Union[Optimizer, Minimizer]] = None, + expectation: Optional[ExpectationBase] = None, initial_guess: Optional[np.ndarray] = None, evolution: Optional[EvolutionSynthesis] = None, gradients: bool = True, + quantum_instance: Optional[Union[Backend, QuantumInstance]] = None, ) -> None: """ Args: @@ -98,14 +148,15 @@ def __init__( optimizer: The classical optimizers used to minimize the overlap between Trotterization and ansatz. Can be either a :class:`.Optimizer` or a callable using the :class:`.Minimizer` protocol. - quantum_instance: The backend of quantum instance used to evaluate the circuits. expectation: The expectation converter to evaluate expectation values. initial_guess: The initial guess for the first VQE optimization. Afterwards the - previous iteration result is used as initial guess. + previous iteration result is used as initial guess. If None, this is set to + a random vector with elements in the interval :math:`[-0.01, 0.01]`. evolution: The evolution synthesis to use for the construction of the Trotter step. Defaults to first-order Lie-Trotter decomposition. gradients: If True, use the parameter shift rule to compute gradients. If False, the optimizer will not be passed a gradient callable. + quantum_instance: The backend of quantum instance used to evaluate the circuits. """ if evolution is None: evolution = LieTrotter() @@ -119,7 +170,23 @@ def __init__( self.evolution = evolution self.gradients = gradients - self._sampler = CircuitSampler(quantum_instance) + self._sampler = None + self.quantum_instance = quantum_instance + + @property + def quantum_instance(self) -> Optional[QuantumInstance]: + """Return the current quantum instance.""" + return self._quantum_instance + + @quantum_instance.setter + def quantum_instance(self, quantum_instance: Optional[Union[Backend, QuantumInstance]]) -> None: + """Set the quantum instance and circuit sampler.""" + if quantum_instance is not None: + if not isinstance(quantum_instance, QuantumInstance): + quantum_instance = QuantumInstance(quantum_instance) + self._sampler = CircuitSampler(quantum_instance) + + self._quantum_instance = quantum_instance def step( self, hamiltonian: OperatorBase, theta: np.ndarray, dt: float, initial_guess: np.ndarray @@ -138,6 +205,9 @@ def step( # construct cost function loss, gradient = self.get_loss(hamiltonian, dt, theta) + if initial_guess is None: + initial_guess = np.random.random(self.initial_parameters.size) * 0.01 + # call optimizer if isinstance(self.optimizer, Optimizer): optimizer_result = self.optimizer.minimize(loss, initial_guess, gradient) @@ -149,7 +219,10 @@ def step( return theta + optimizer_result.x, fidelity def get_loss( - self, hamiltonian: OperatorBase, dt: float, current_parameters: np.ndarray + self, + hamiltonian: OperatorBase, + dt: float, + current_parameters: np.ndarray, ) -> Callable[[np.ndarray], float]: """Get a function to evaluate the infidelity between Trotter step and ansatz. @@ -161,6 +234,8 @@ def get_loss( Returns: A callable to evaluate the infidelity. """ + self._validate_setup() + # use Trotterization to evolve the current state trotterized = self.ansatz.bind_parameters(current_parameters) trotterized.append( @@ -225,6 +300,27 @@ def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: return evaluate_loss, evaluate_gradient + def _validate_setup(self): + """Validate the current setup and raise an error if something misses to run.""" + + required_args = { + "ansatz", + "initial_parameters", + "timestep", + "optimizer", + "quantum_instance", + "expectation", + } + for arg in required_args: + if getattr(self, arg) is None: + raise ValueError(f"The {arg} attribute cannot be None.") + + if len(self.initial_parameters) != self.ansatz.num_parameters: + raise QiskitError( + f"Mismatching number of parameters in the ansatz ({self.ansatz.num_parameters}) " + f"and the initial parameters ({len(self.initial_parameters)})." + ) + def _check_gradient_supported(self) -> bool: """Check whether we can apply a simple parameter shift rule to obtain gradients.""" @@ -243,12 +339,12 @@ def _check_gradient_supported(self) -> bool: # check whether all parameters are unique and we do not need to apply the chain rule # (since it's not implemented yet) - all_parameters = [] + total_num_parameters = 0 for circuit_instruction in unrolled.data: for param in circuit_instruction.operation.params: if isinstance(param, ParameterExpression): if isinstance(param, Parameter): - all_parameters.append(param) + total_num_parameters += 1 else: logger.log( logging.INFO, @@ -257,7 +353,7 @@ def _check_gradient_supported(self) -> bool: ) return False - if len(all_parameters) != self.ansatz.num_parameters: + if total_num_parameters != self.ansatz.num_parameters: logger.log( logging.INFO, "No gradient support: Circuit is only allowed to have unique parameters, " @@ -303,6 +399,8 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: Raises: ValueError: If the evolution time is not positive or the timestep is too small. """ + self._validate_setup() + time = evolution_problem.time observables = evolution_problem.aux_operators hamiltonian = evolution_problem.hamiltonian @@ -323,11 +421,7 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: parameters = [self.initial_parameters] current_time = 0 - - if self.initial_guess is None: - initial_guess = np.random.random(self.initial_parameters.size) * 0.01 - else: - initial_guess = self.initial_guess + initial_guess = self.initial_guess while current_time < time: # perform VQE to find the next parameters diff --git a/qiskit/algorithms/pvqd.py b/qiskit/algorithms/pvqd.py deleted file mode 100644 index aafc3e12e17d..000000000000 --- a/qiskit/algorithms/pvqd.py +++ /dev/null @@ -1,249 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The projected Variational Quantum Dynamics Algorithm.""" - -from typing import Optional, Union, List, Tuple, Callable - -import numpy as np - -from qiskit.algorithms.optimizers import Optimizer -from qiskit.circuit import QuantumCircuit, ParameterVector -from qiskit.circuit.library import PauliEvolutionGate -from qiskit.providers import Backend -from qiskit.opflow import ( - OperatorBase, - CircuitSampler, - ExpectationBase, - ListOp, - StateFn, - GradientBase, -) -from qiskit.synthesis import EvolutionSynthesis, LieTrotter -from qiskit.utils import QuantumInstance - -from .algorithm_result import AlgorithmResult - - -class PVQDResult(AlgorithmResult): - """The result object for the pVQD algorithm.""" - - times = None - parameters = None - fidelities = None - estimated_error = None - observables = None - - -class PVQD: - """The projected Variational Quantum Dynamics Algorithm.""" - - def __init__( - self, - ansatz: QuantumCircuit, - initial_parameters: np.ndarray, - optimizer: Optimizer, - quantum_instance: Union[Backend, QuantumInstance], - expectation: ExpectationBase, - gradient: Optional[GradientBase] = None, - evolution: Optional[EvolutionSynthesis] = None, - ) -> None: - """ - Args: - ansatz: A parameterized circuit preparing the variational ansatz to model the - time evolved quantum state. - initial_parameters: The initial parameters for the ansatz. - optimizer: The classical optimizers used to minimize the overlap between - Trotterization and ansatz. - quantum_instance: The backend of quantum instance used to evaluate the circuits. - expectation: The expectation converter to evaluate expectation values. - evolution: The evolution synthesis to use for the construction of the Trotter step. - Defaults to first-order Lie-Trotter decomposition. - """ - if evolution is None: - evolution = LieTrotter() - - self.ansatz = ansatz - self.initial_parameters = initial_parameters - self.optimizer = optimizer - self.gradient = gradient - self.expectation = expectation - self.evolution = evolution - - self._sampler = CircuitSampler(quantum_instance) - - def step( - self, hamiltonian: OperatorBase, theta: np.ndarray, dt: float, initial_guess: np.ndarray - ) -> Tuple[np.ndarray, float]: - """Perform a single time step. - - Args: - hamiltonian: The Hamiltonian under which to evolve. - theta: The current parameters. - dt: The time step. - initial_guess: The initial guess for the update to minimize the fidelity. - - Returns: - A tuple consisting of the next parameters and the fidelity of the optimization. - """ - # construct cost function - overlap, gradient = self.get_overlap(hamiltonian, dt, theta) - - # call optimizer - optimizer_result = self.optimizer.minimize(lambda x: -overlap(x), initial_guess, gradient) - - return theta + optimizer_result.x, -optimizer_result.fun - - def get_overlap( - self, hamiltonian: OperatorBase, dt: float, current_parameters: np.ndarray - ) -> Callable[[np.ndarray], float]: - """Get a function to evaluate the overlap between Trotter step and ansatz. - - Args: - hamiltonian: The Hamiltonian under which to evolve. - dt: The time step. - current_parameters: The current parameters. - - Returns: - A callable to evaluate the overlap. - """ - # use Trotterization to evolve the current state - trotterized = self.ansatz.bind_parameters(current_parameters) - trotterized.append( - PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution), self.ansatz.qubits - ) - - # define the overlap of the Trotterized state and the ansatz - x = ParameterVector("w", self.ansatz.num_parameters) - shifted = self.ansatz.assign_parameters(current_parameters + x) - overlap = StateFn(trotterized).adjoint() @ StateFn(shifted) - - # apply the expectation converter - converted = self.expectation.convert(overlap) - - ansatz_parameters = self.ansatz.parameters - - def evaluate_overlap(displacement: np.ndarray) -> float: - """Evaluate the overlap of the ansatz with the Trotterized evolution. - - Args: - displacement: The parameters for the ansatz. - - Returns: - The fidelity of the ansatz with parameters ``theta`` and the Trotterized evolution. - """ - # evaluate with the circuit sampler - value_dict = dict(zip(x, displacement)) - sampled = self._sampler.convert(converted, params=value_dict) - return np.abs(sampled.eval()) ** 2 - - if self.gradient is not None: - gradient = self.gradient.convert(overlap) - - def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: - """Evaluate the gradient.""" - # evaluate with the circuit sampler - value_dict = dict(zip(ansatz_parameters, current_parameters + displacement)) - sampled = self._sampler.convert(gradient, params=value_dict) - return 2 * sampled.eval() - - return evaluate_overlap, evaluate_gradient - - return evaluate_overlap, None - - def _get_observable_evaluator(self, observables): - if isinstance(observables, list): - observables = ListOp(observables) - - expectation_value = StateFn(observables, is_measurement=True) @ StateFn(self.ansatz) - converted = self.expectation.convert(expectation_value) - - ansatz_parameters = self.ansatz.parameters - - def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]: - """Evaluate the observables for the ansatz parameters ``theta``. - - Args: - theta: The ansatz parameters. - - Returns: - The observables evaluated at the ansatz parameters. - """ - value_dict = dict(zip(ansatz_parameters, theta)) - sampled = self._sampler.convert(converted, params=value_dict) - return sampled.eval() - - return evaluate_observables - - def evolve( - self, - hamiltonian: OperatorBase, - time: float, - dt: float, - observables: Optional[Union[OperatorBase, List[OperatorBase]]] = None, - ) -> PVQDResult: - """ - Args: - hamiltonian: The Hamiltonian under which to evolve. - time: The total evolution time. - dt: The time step. - observables: The observables to evaluate at each time step. - - Returns: - A result object containing the evolution information and evaluated observables. - - Raises: - ValueError: If the evolution time is not positive or the timestep is too small. - """ - if time <= 0: - raise ValueError("The evolution time must be larger than 0.") - - if not 0 < dt <= time: - raise ValueError( - "The time step must be larger than 0 and smaller equal the evolution time." - ) - - # get the function to evaluate the observables for a given set of ansatz parameters - evaluate_observables = self._get_observable_evaluator(observables) - - observable_values = [evaluate_observables(self.initial_parameters)] - fidelities = [1] - times = [0] - parameters = [self.initial_parameters] - - current_time = 0 - initial_guess = np.random.random(self.initial_parameters.size) * 0.01 - # initial_guess = np.zeros(self.initial_parameters.size) - while current_time < time: - # perform VQE to find the next parameters - next_parameters, fidelity = self.step(hamiltonian, parameters[-1], dt, initial_guess) - - # set initial guess to last parameter update - initial_guess = next_parameters - parameters[-1] - - # store parameters - parameters.append(next_parameters) - fidelities.append(fidelity) - observable_values.append(evaluate_observables(next_parameters)) - - # increase time - current_time += dt - times.append(current_time) - - result = PVQDResult() - result.times = times - result.parameters = parameters - result.fidelities = fidelities - result.estimated_error = np.prod(result.fidelities) - result.observables = np.asarray(observable_values) - - return result diff --git a/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml b/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml new file mode 100644 index 000000000000..a8319d355e9f --- /dev/null +++ b/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml @@ -0,0 +1,51 @@ +features: + - | + Added the the projected Variational Quantum Dynamics (p-VQD) algorithm, see + also Barison et al. [1]. + + In each timestep this algorithm computes the next state with a Trotter formula + (specified by the ``evolution`` argument) and projects it onto a variational form (``ansatz``). + The projection is determined by maximizing the fidelity of the Trotter-evolved state + and the ansatz, using a classical optimization routine. + + .. code-block:: python + + import numpy as np + + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import X, Z, I, MatrixExpectation + + backend = BasicAer.get_backend("statevector_simulator") + expectation = MatrixExpectation() + hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + observable = Z ^ Z + ansatz = EfficientSU2(2, reps=1) + initial_parameters = np.zeros(ansatz.num_parameters) + + time = 0.02 + optimizer = L_BFGS_B() + + # setup the algorithm + pvqd = PVQD( + ansatz, + initial_parameters, + timestep=0.01, + optimizer=optimizer, + quantum_instance=backend, + expectation=expectation + ) + + # specify the evolution problem + problem = EvolutionProblem( + hamiltonian, time, aux_operators=[hamiltonian, observable] + ) + + # and evolve! + result = pvqd.evolve(problem) + + References: + + [1] Stefano Barison, Filippo Vicentini, and Giuseppe Carleo (2021), An efficient + quantum algorithm for the time evolution of parameterized circuits, + `Quantum 5, 512 `_. \ No newline at end of file diff --git a/test/python/algorithms/evolvers/test_pvqd.py b/test/python/algorithms/evolvers/test_pvqd.py index 322bef22ec0a..6d9cc14c40ef 100644 --- a/test/python/algorithms/evolvers/test_pvqd.py +++ b/test/python/algorithms/evolvers/test_pvqd.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -13,12 +13,12 @@ """Tests for PVQD.""" from functools import partial -from ddt import ddt, data +from ddt import ddt, data, unpack import numpy as np from qiskit.test import QiskitTestCase -from qiskit import BasicAer as Aer +from qiskit import BasicAer from qiskit.circuit import QuantumCircuit, Parameter, Gate from qiskit.algorithms.evolvers import EvolutionProblem from qiskit.algorithms.evolvers.pvqd import PVQD @@ -54,15 +54,17 @@ class TestPVQD(QiskitTestCase): def setUp(self): super().setUp() - self.backend = Aer.get_backend("statevector_simulator") + self.sv_backend = BasicAer.get_backend("statevector_simulator") + self.qasm_backend = BasicAer.get_backend("qasm_simulator") self.expectation = MatrixExpectation() self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) self.observable = Z ^ Z self.ansatz = EfficientSU2(2, reps=1) self.initial_parameters = np.zeros(self.ansatz.num_parameters) - @data(True, False) - def test_pvqd(self, gradient): + @data((True, "sv"), (False, "qasm")) + @unpack + def test_pvqd(self, gradient, backend_type): """Test a simple evolution.""" time = 0.02 @@ -71,13 +73,15 @@ def test_pvqd(self, gradient): else: optimizer = L_BFGS_B(maxiter=1) + backend = self.sv_backend if backend_type == "sv" else self.qasm_backend + # run pVQD keeping track of the energy and the magnetization pvqd = PVQD( self.ansatz, self.initial_parameters, timestep=0.01, optimizer=optimizer, - quantum_instance=self.backend, + quantum_instance=backend, expectation=self.expectation, ) problem = EvolutionProblem( @@ -101,7 +105,7 @@ def test_invalid_timestep(self): self.initial_parameters, timestep=1, optimizer=L_BFGS_B(), - quantum_instance=self.backend, + quantum_instance=self.sv_backend, expectation=self.expectation, ) problem = EvolutionProblem( @@ -121,7 +125,7 @@ def test_initial_guess_and_observables(self): timestep=0.01, optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), initial_guess=initial_guess, - quantum_instance=self.backend, + quantum_instance=self.sv_backend, expectation=self.expectation, ) problem = EvolutionProblem( @@ -173,7 +177,7 @@ def test_gradient_supported(self): initial_parameters=np.array([]), timestep=0.01, optimizer=optimizer, - quantum_instance=self.backend, + quantum_instance=self.sv_backend, expectation=self.expectation, ) problem = EvolutionProblem(self.hamiltonian, time=0.01) @@ -183,3 +187,39 @@ def test_gradient_supported(self): pvqd.initial_parameters = np.zeros(circuit.num_parameters) _ = pvqd.evolve(problem) self.assertEqual(info["has_gradient"], expected_support) + + def test_invalid_setup(self): + """Test appropriate error is raised if attributes are missing or incompatible.""" + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + timestep=0.01, + optimizer=L_BFGS_B(maxiter=1), + quantum_instance=self.qasm_backend, + expectation=self.expectation, + ) + problem = EvolutionProblem(self.hamiltonian, time=0.01) + + args_to_test = [ + ("ansatz", self.ansatz), + ("initial_parameters", self.initial_parameters), + ("timestep", 0.01), + ("optimizer", L_BFGS_B(maxiter=1)), + ("quantum_instance", self.qasm_backend), + ("expectation", self.expectation), + ] + + for attr, value in args_to_test: + with self.subTest(msg=f"missing: {attr}"): + # set attribute to None to invalidate the setup + setattr(pvqd, attr, None) + + with self.assertRaises(ValueError): + _ = pvqd.evolve(problem) + + # set the correct value again + setattr(pvqd, attr, value) + + # check PVQD is running now that all arguments are set + result = pvqd.evolve(problem) + self.assertIsNotNone(result.evolved_state) From 35c7b289f03dfbe0ac00a66165b68b4ec76f94bd Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 8 Jul 2022 16:24:37 +0200 Subject: [PATCH 09/24] fix algorithms import --- qiskit/algorithms/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index feeb73001fc4..a1f4bc1f1fee 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -243,10 +243,10 @@ PhaseEstimationResult, IterativePhaseEstimation, ) -from .pvqd import PVQD from .exceptions import AlgorithmError from .aux_ops_evaluator import eval_observables from .evolvers.trotterization import TrotterQRTE +from .evolvers.pvqd import PVQD __all__ = [ "AlgorithmResult", From 9c0f9ae19380d97bd332970378662f5f349390ec Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 15 Jul 2022 13:43:30 +0200 Subject: [PATCH 10/24] changes from code review --- qiskit/algorithms/evolvers/pvqd.py | 195 +++++++++++++++-------------- 1 file changed, 104 insertions(+), 91 deletions(-) diff --git a/qiskit/algorithms/evolvers/pvqd.py b/qiskit/algorithms/evolvers/pvqd.py index 4169a9c82a98..c571e1845077 100644 --- a/qiskit/algorithms/evolvers/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd.py @@ -197,7 +197,10 @@ def step( hamiltonian: The Hamiltonian under which to evolve. theta: The current parameters. dt: The time step. - initial_guess: The initial guess for the update to minimize the fidelity. + initial_guess: The initial guess for the classical optimization of the + fidelity between the next variational state and the Trotter-evolved last state. + If None, this is set to a random vector with elements in the interval + :math:`[-0.01, 0.01]`. Returns: A tuple consisting of the next parameters and the fidelity of the optimization. @@ -214,7 +217,8 @@ def step( else: optimizer_result = self.optimizer(loss, initial_guess, gradient) - fidelity = 1 - optimizer_result.fun + # clip the fidelity to [0, 1] + fidelity = np.clip(1 - optimizer_result.fun, 0, 1) return theta + optimizer_result.x, fidelity @@ -270,7 +274,7 @@ def evaluate_loss( sampled = self._sampler.convert(converted, params=value_dict) return 1 - np.abs(sampled.eval()) ** 2 - if self._check_gradient_supported() and self.gradients: + if _is_gradient_supported(self.ansatz) and self.gradients: def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: """Evaluate the gradient with the parameter-shift rule. @@ -300,93 +304,6 @@ def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: return evaluate_loss, evaluate_gradient - def _validate_setup(self): - """Validate the current setup and raise an error if something misses to run.""" - - required_args = { - "ansatz", - "initial_parameters", - "timestep", - "optimizer", - "quantum_instance", - "expectation", - } - for arg in required_args: - if getattr(self, arg) is None: - raise ValueError(f"The {arg} attribute cannot be None.") - - if len(self.initial_parameters) != self.ansatz.num_parameters: - raise QiskitError( - f"Mismatching number of parameters in the ansatz ({self.ansatz.num_parameters}) " - f"and the initial parameters ({len(self.initial_parameters)})." - ) - - def _check_gradient_supported(self) -> bool: - """Check whether we can apply a simple parameter shift rule to obtain gradients.""" - - # check whether the circuit can be unrolled to supported gates - try: - unrolled = transpile( - self.ansatz, basis_gates=ParamShift.SUPPORTED_GATES, optimization_level=0 - ) - except QiskitError: - # failed to map to supported basis - logger.log( - logging.INFO, - "No gradient support: Failed to unroll to gates supported by parameter-shift.", - ) - return False - - # check whether all parameters are unique and we do not need to apply the chain rule - # (since it's not implemented yet) - total_num_parameters = 0 - for circuit_instruction in unrolled.data: - for param in circuit_instruction.operation.params: - if isinstance(param, ParameterExpression): - if isinstance(param, Parameter): - total_num_parameters += 1 - else: - logger.log( - logging.INFO, - "No gradient support: Circuit is only allowed to have plain parameters, " - "as the chain rule is not yet implemented.", - ) - return False - - if total_num_parameters != self.ansatz.num_parameters: - logger.log( - logging.INFO, - "No gradient support: Circuit is only allowed to have unique parameters, " - "as the product rule is not yet implemented.", - ) - return False - - return True - - def _get_observable_evaluator(self, observables): - if isinstance(observables, list): - observables = ListOp(observables) - - expectation_value = StateFn(observables, is_measurement=True) @ StateFn(self.ansatz) - converted = self.expectation.convert(expectation_value) - - ansatz_parameters = self.ansatz.parameters - - def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]: - """Evaluate the observables for the ansatz parameters ``theta``. - - Args: - theta: The ansatz parameters. - - Returns: - The observables evaluated at the ansatz parameters. - """ - value_dict = dict(zip(ansatz_parameters, theta)) - sampled = self._sampler.convert(converted, params=value_dict) - return sampled.eval() - - return evaluate_observables - def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: """ Args: @@ -413,7 +330,9 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: # get the function to evaluate the observables for a given set of ansatz parameters if observables is not None: - evaluate_observables = self._get_observable_evaluator(observables) + evaluate_observables = _get_observable_evaluator( + self.ansatz, observables, self.expectation, self._sampler + ) observable_values = [evaluate_observables(self.initial_parameters)] fidelities = [1] @@ -456,3 +375,97 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: result.aux_ops_evaluated = observable_values[-1] return result + + def _validate_setup(self): + """Validate the current setup and raise an error if something misses to run.""" + + required_args = { + "ansatz", + "initial_parameters", + "timestep", + "optimizer", + "quantum_instance", + "expectation", + } + for arg in required_args: + if getattr(self, arg) is None: + raise ValueError(f"The {arg} attribute cannot be None.") + + if len(self.initial_parameters) != self.ansatz.num_parameters: + raise QiskitError( + f"Mismatching number of parameters in the ansatz ({self.ansatz.num_parameters}) " + f"and the initial parameters ({len(self.initial_parameters)})." + ) + + +def _is_gradient_supported(ansatz: QuantumCircuit) -> bool: + """Check whether we can apply a simple parameter shift rule to obtain gradients.""" + + # check whether the circuit can be unrolled to supported gates + try: + unrolled = transpile(ansatz, basis_gates=ParamShift.SUPPORTED_GATES, optimization_level=0) + except QiskitError: + # failed to map to supported basis + logger.log( + logging.INFO, + "No gradient support: Failed to unroll to gates supported by parameter-shift.", + ) + return False + + # check whether all parameters are unique and we do not need to apply the chain rule + # (since it's not implemented yet) + total_num_parameters = 0 + for circuit_instruction in unrolled.data: + for param in circuit_instruction.operation.params: + if isinstance(param, ParameterExpression): + if isinstance(param, Parameter): + total_num_parameters += 1 + else: + logger.log( + logging.INFO, + "No gradient support: Circuit is only allowed to have plain parameters, " + "as the chain rule is not yet implemented.", + ) + return False + + if total_num_parameters != ansatz.num_parameters: + logger.log( + logging.INFO, + "No gradient support: Circuit is only allowed to have unique parameters, " + "as the product rule is not yet implemented.", + ) + return False + + return True + + +def _get_observable_evaluator( + ansatz: QuantumCircuit, + observables: Union[OperatorBase, List[OperatorBase]], + expectation: ExpectationBase, + sampler: CircuitSampler, +) -> Callable[[np.ndarray], Union[float, List[float]]]: + """Get a callable to evaluate a (list of) observable(s) for given circuit parameters.""" + + if isinstance(observables, list): + observables = ListOp(observables) + + expectation_value = StateFn(observables, is_measurement=True) @ StateFn(ansatz) + converted = expectation.convert(expectation_value) + + ansatz_parameters = ansatz.parameters + + def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]: + """Evaluate the observables for the ansatz parameters ``theta``. + + Args: + theta: The ansatz parameters. + + Returns: + The observables evaluated at the ansatz parameters. + """ + value_dict = dict(zip(ansatz_parameters, theta)) + sampled = sampler.convert(converted, params=value_dict) + return sampled.eval() + + return evaluate_observables From 3662e16996ebecc7f0cb53294b80488a18f007e7 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 15 Jul 2022 15:49:41 +0200 Subject: [PATCH 11/24] add more tests for different ops --- qiskit/algorithms/evolvers/pvqd.py | 3 +- test/python/algorithms/evolvers/test_pvqd.py | 43 ++++++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/qiskit/algorithms/evolvers/pvqd.py b/qiskit/algorithms/evolvers/pvqd.py index c571e1845077..22d5a1ce5b92 100644 --- a/qiskit/algorithms/evolvers/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd.py @@ -308,7 +308,8 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: """ Args: evolution_problem: The evolution problem containing the hamiltonian, total evolution - time and observables to evaluate. + time and observables to evaluate. Note that :class:`~.PVQD` currently does not support + hamiltonians of type :class:`~.MatrixOp`. Returns: A result object containing the evolution information and evaluated observables. diff --git a/test/python/algorithms/evolvers/test_pvqd.py b/test/python/algorithms/evolvers/test_pvqd.py index 6d9cc14c40ef..ef9cc8f8a554 100644 --- a/test/python/algorithms/evolvers/test_pvqd.py +++ b/test/python/algorithms/evolvers/test_pvqd.py @@ -24,7 +24,7 @@ from qiskit.algorithms.evolvers.pvqd import PVQD from qiskit.algorithms.optimizers import L_BFGS_B, GradientDescent, SPSA, OptimizerResult from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import X, Z, I, MatrixExpectation +from qiskit.opflow import X, Z, I, MatrixExpectation, PauliExpectation # pylint: disable=unused-argument, invalid-name @@ -62,18 +62,29 @@ def setUp(self): self.ansatz = EfficientSU2(2, reps=1) self.initial_parameters = np.zeros(self.ansatz.num_parameters) - @data((True, "sv"), (False, "qasm")) + @data( + ("ising", MatrixExpectation, True, "sv"), + ("ising", PauliExpectation, True, "qasm"), + ("pauli", PauliExpectation, False, "qasm"), + ) @unpack - def test_pvqd(self, gradient, backend_type): + def test_pvqd(self, hamiltonian_type, expectation_cls, gradient, backend_type): """Test a simple evolution.""" time = 0.02 + if hamiltonian_type == "ising": + hamiltonian = self.hamiltonian + else: # hamiltonian_type == "pauli": + hamiltonian = X ^ X + + # parse input arguments if gradient: optimizer = GradientDescent(maxiter=1) else: optimizer = L_BFGS_B(maxiter=1) backend = self.sv_backend if backend_type == "sv" else self.qasm_backend + expectation = expectation_cls() # run pVQD keeping track of the energy and the magnetization pvqd = PVQD( @@ -82,11 +93,9 @@ def test_pvqd(self, gradient, backend_type): timestep=0.01, optimizer=optimizer, quantum_instance=backend, - expectation=self.expectation, - ) - problem = EvolutionProblem( - self.hamiltonian, time, aux_operators=[self.hamiltonian, self.observable] + expectation=expectation, ) + problem = EvolutionProblem(hamiltonian, time, aux_operators=[hamiltonian, self.observable]) result = pvqd.evolve(problem) self.assertTrue(len(result.fidelities) == 3) @@ -98,6 +107,26 @@ def test_pvqd(self, gradient, backend_type): and np.all([len(params) == num_parameters for params in result.parameters]) ) + def test_matrix_op_raises(self): + """Since the PauliEvolutionGate does not support MatrixOp as input, neither does the PVQD.""" + + hamiltonian = self.hamiltonian.to_matrix_op() + optimizer = L_BFGS_B(maxiter=1) + + # run pVQD keeping track of the energy and the magnetization + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + timestep=0.01, + optimizer=optimizer, + quantum_instance=self.sv_backend, + expectation=MatrixExpectation(), + ) + problem = EvolutionProblem(hamiltonian, time=0.02) + + with self.assertRaises(ValueError): + _ = pvqd.evolve(problem) + def test_invalid_timestep(self): """Test raises if the timestep is larger than the evolution time.""" pvqd = PVQD( From a0846601066555b5b7e6065d7c2d7b40935a8482 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 18 Jul 2022 16:11:06 +0200 Subject: [PATCH 12/24] refactor PVQD to multiple files --- qiskit/algorithms/evolvers/pvqd/__init__.py | 18 +++ qiskit/algorithms/evolvers/{ => pvqd}/pvqd.py | 133 ++---------------- qiskit/algorithms/evolvers/pvqd/result.py | 55 ++++++++ qiskit/algorithms/evolvers/pvqd/utils.py | 100 +++++++++++++ 4 files changed, 185 insertions(+), 121 deletions(-) create mode 100644 qiskit/algorithms/evolvers/pvqd/__init__.py rename qiskit/algorithms/evolvers/{ => pvqd}/pvqd.py (73%) create mode 100644 qiskit/algorithms/evolvers/pvqd/result.py create mode 100644 qiskit/algorithms/evolvers/pvqd/utils.py diff --git a/qiskit/algorithms/evolvers/pvqd/__init__.py b/qiskit/algorithms/evolvers/pvqd/__init__.py new file mode 100644 index 000000000000..4338e24fa52f --- /dev/null +++ b/qiskit/algorithms/evolvers/pvqd/__init__.py @@ -0,0 +1,18 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The projected Variational Quantum Dynamic (p-VQD) module.""" + +from .result import PVQDResult +from .pvqd import PVQD + +__all__ = ["PVQD", "PVQDResult"] diff --git a/qiskit/algorithms/evolvers/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py similarity index 73% rename from qiskit/algorithms/evolvers/pvqd.py rename to qiskit/algorithms/evolvers/pvqd/pvqd.py index 22d5a1ce5b92..973c846dab27 100644 --- a/qiskit/algorithms/evolvers/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -17,61 +17,28 @@ import logging import numpy as np -from qiskit import transpile, QiskitError +from qiskit import QiskitError from qiskit.algorithms.optimizers import Optimizer, Minimizer -from qiskit.circuit import QuantumCircuit, ParameterVector, ParameterExpression, Parameter +from qiskit.circuit import QuantumCircuit, ParameterVector from qiskit.circuit.library import PauliEvolutionGate from qiskit.providers import Backend from qiskit.opflow import ( OperatorBase, CircuitSampler, ExpectationBase, - ListOp, StateFn, ) -from qiskit.opflow.gradients.circuit_gradients import ParamShift from qiskit.synthesis import EvolutionSynthesis, LieTrotter from qiskit.utils import QuantumInstance -from .evolution_problem import EvolutionProblem -from .evolution_result import EvolutionResult -from .real_evolver import RealEvolver - -logger = logging.getLogger(__name__) +from .result import PVQDResult +from .utils import _get_observable_evaluator, _is_gradient_supported +from ..evolution_problem import EvolutionProblem +from ..evolution_result import EvolutionResult +from ..real_evolver import RealEvolver -class PVQDResult(EvolutionResult): - """The result object for the pVQD algorithm.""" - - def __init__( - self, - evolved_state: Union[StateFn, QuantumCircuit, OperatorBase], - # TODO: aux_ops_evaluated: Optional[ListOrDict[Tuple[complex, complex]]] = None, - aux_ops_evaluated: Optional[List[Tuple[complex, complex]]] = None, - times: Optional[List[float]] = None, - parameters: Optional[List[np.ndarray]] = None, - fidelities: Optional[List[float]] = None, - estimated_error: Optional[float] = None, - observables: Optional[List[List[float]]] = None, - ): - """ - Args: - evolved_state: An evolved quantum state. - aux_ops_evaluated: Optional list of observables for which expected values on an evolved - state are calculated. These values are in fact tuples formatted as (mean, standard - deviation). - times: The times evaluated during the time integration. - parameters: The parameter values at each evaluation time. - fidelities: The fidelity of the Trotter step and variational update at each iteration. - estimated_error: The overall estimated error evaluated as product of all fidelities. - observables: The value of the observables evaluated at each iteration. - """ - super().__init__(evolved_state, aux_ops_evaluated) - self.times = times - self.parameters = parameters - self.fidelities = fidelities - self.estimated_error = estimated_error - self.observables = observables +logger = logging.getLogger(__name__) class PVQD(RealEvolver): @@ -205,13 +172,11 @@ def step( Returns: A tuple consisting of the next parameters and the fidelity of the optimization. """ - # construct cost function loss, gradient = self.get_loss(hamiltonian, dt, theta) if initial_guess is None: initial_guess = np.random.random(self.initial_parameters.size) * 0.01 - # call optimizer if isinstance(self.optimizer, Optimizer): optimizer_result = self.optimizer.minimize(loss, initial_guess, gradient) else: @@ -227,7 +192,8 @@ def get_loss( hamiltonian: OperatorBase, dt: float, current_parameters: np.ndarray, - ) -> Callable[[np.ndarray], float]: + ) -> Tuple[Callable[[np.ndarray], float], Optional[Callable[[np.ndarray], np.ndarray]]]: + """Get a function to evaluate the infidelity between Trotter step and ansatz. Args: @@ -236,7 +202,8 @@ def get_loss( current_parameters: The current parameters. Returns: - A callable to evaluate the infidelity. + A callable to evaluate the infidelity and, if gradients are supported and required, + a second callable to evaluate the gradient of the infidelity. """ self._validate_setup() @@ -251,7 +218,6 @@ def get_loss( shifted = self.ansatz.assign_parameters(current_parameters + x) overlap = StateFn(trotterized).adjoint() @ StateFn(shifted) - # apply the expectation converter converted = self.expectation.convert(overlap) def evaluate_loss( @@ -352,13 +318,11 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: # set initial guess to last parameter update initial_guess = next_parameters - parameters[-1] - # store parameters parameters.append(next_parameters) fidelities.append(fidelity) if observables is not None: observable_values.append(evaluate_observables(next_parameters)) - # increase time current_time += self.timestep times.append(current_time) @@ -397,76 +361,3 @@ def _validate_setup(self): f"Mismatching number of parameters in the ansatz ({self.ansatz.num_parameters}) " f"and the initial parameters ({len(self.initial_parameters)})." ) - - -def _is_gradient_supported(ansatz: QuantumCircuit) -> bool: - """Check whether we can apply a simple parameter shift rule to obtain gradients.""" - - # check whether the circuit can be unrolled to supported gates - try: - unrolled = transpile(ansatz, basis_gates=ParamShift.SUPPORTED_GATES, optimization_level=0) - except QiskitError: - # failed to map to supported basis - logger.log( - logging.INFO, - "No gradient support: Failed to unroll to gates supported by parameter-shift.", - ) - return False - - # check whether all parameters are unique and we do not need to apply the chain rule - # (since it's not implemented yet) - total_num_parameters = 0 - for circuit_instruction in unrolled.data: - for param in circuit_instruction.operation.params: - if isinstance(param, ParameterExpression): - if isinstance(param, Parameter): - total_num_parameters += 1 - else: - logger.log( - logging.INFO, - "No gradient support: Circuit is only allowed to have plain parameters, " - "as the chain rule is not yet implemented.", - ) - return False - - if total_num_parameters != ansatz.num_parameters: - logger.log( - logging.INFO, - "No gradient support: Circuit is only allowed to have unique parameters, " - "as the product rule is not yet implemented.", - ) - return False - - return True - - -def _get_observable_evaluator( - ansatz: QuantumCircuit, - observables: Union[OperatorBase, List[OperatorBase]], - expectation: ExpectationBase, - sampler: CircuitSampler, -) -> Callable[[np.ndarray], Union[float, List[float]]]: - """Get a callable to evaluate a (list of) observable(s) for given circuit parameters.""" - - if isinstance(observables, list): - observables = ListOp(observables) - - expectation_value = StateFn(observables, is_measurement=True) @ StateFn(ansatz) - converted = expectation.convert(expectation_value) - - ansatz_parameters = ansatz.parameters - - def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]: - """Evaluate the observables for the ansatz parameters ``theta``. - - Args: - theta: The ansatz parameters. - - Returns: - The observables evaluated at the ansatz parameters. - """ - value_dict = dict(zip(ansatz_parameters, theta)) - sampled = sampler.convert(converted, params=value_dict) - return sampled.eval() - - return evaluate_observables diff --git a/qiskit/algorithms/evolvers/pvqd/result.py b/qiskit/algorithms/evolvers/pvqd/result.py new file mode 100644 index 000000000000..7aa920062fb2 --- /dev/null +++ b/qiskit/algorithms/evolvers/pvqd/result.py @@ -0,0 +1,55 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Result object for p-VQD.""" + +from typing import Union, Optional, List, Tuple +import numpy as np + +from qiskit.circuit import QuantumCircuit +from qiskit.opflow import StateFn, OperatorBase + +from ..evolution_result import EvolutionResult + + +class PVQDResult(EvolutionResult): + """The result object for the pVQD algorithm.""" + + def __init__( + self, + evolved_state: Union[StateFn, QuantumCircuit, OperatorBase], + # TODO: aux_ops_evaluated: Optional[ListOrDict[Tuple[complex, complex]]] = None, + aux_ops_evaluated: Optional[List[Tuple[complex, complex]]] = None, + times: Optional[List[float]] = None, + parameters: Optional[List[np.ndarray]] = None, + fidelities: Optional[List[float]] = None, + estimated_error: Optional[float] = None, + observables: Optional[List[List[float]]] = None, + ): + """ + Args: + evolved_state: An evolved quantum state. + aux_ops_evaluated: Optional list of observables for which expected values on an evolved + state are calculated. These values are in fact tuples formatted as (mean, standard + deviation). + times: The times evaluated during the time integration. + parameters: The parameter values at each evaluation time. + fidelities: The fidelity of the Trotter step and variational update at each iteration. + estimated_error: The overall estimated error evaluated as product of all fidelities. + observables: The value of the observables evaluated at each iteration. + """ + super().__init__(evolved_state, aux_ops_evaluated) + self.times = times + self.parameters = parameters + self.fidelities = fidelities + self.estimated_error = estimated_error + self.observables = observables diff --git a/qiskit/algorithms/evolvers/pvqd/utils.py b/qiskit/algorithms/evolvers/pvqd/utils.py new file mode 100644 index 000000000000..589f12005de8 --- /dev/null +++ b/qiskit/algorithms/evolvers/pvqd/utils.py @@ -0,0 +1,100 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + +"""Utilities for p-VQD.""" + +from typing import Union, List, Callable +import logging + +import numpy as np + +from qiskit.circuit import QuantumCircuit, Parameter, ParameterExpression +from qiskit.compiler import transpile +from qiskit.exceptions import QiskitError +from qiskit.opflow import ListOp, CircuitSampler, ExpectationBase, StateFn, OperatorBase +from qiskit.opflow.gradients.circuit_gradients import ParamShift + +logger = logging.getLogger(__name__) + + +def _is_gradient_supported(ansatz: QuantumCircuit) -> bool: + """Check whether we can apply a simple parameter shift rule to obtain gradients.""" + + # check whether the circuit can be unrolled to supported gates + try: + unrolled = transpile(ansatz, basis_gates=ParamShift.SUPPORTED_GATES, optimization_level=0) + except QiskitError: + # failed to map to supported basis + logger.log( + logging.INFO, + "No gradient support: Failed to unroll to gates supported by parameter-shift.", + ) + return False + + # check whether all parameters are unique and we do not need to apply the chain rule + # (since it's not implemented yet) + total_num_parameters = 0 + for circuit_instruction in unrolled.data: + for param in circuit_instruction.operation.params: + if isinstance(param, ParameterExpression): + if isinstance(param, Parameter): + total_num_parameters += 1 + else: + logger.log( + logging.INFO, + "No gradient support: Circuit is only allowed to have plain parameters, " + "as the chain rule is not yet implemented.", + ) + return False + + if total_num_parameters != ansatz.num_parameters: + logger.log( + logging.INFO, + "No gradient support: Circuit is only allowed to have unique parameters, " + "as the product rule is not yet implemented.", + ) + return False + + return True + + +def _get_observable_evaluator( + ansatz: QuantumCircuit, + observables: Union[OperatorBase, List[OperatorBase]], + expectation: ExpectationBase, + sampler: CircuitSampler, +) -> Callable[[np.ndarray], Union[float, List[float]]]: + """Get a callable to evaluate a (list of) observable(s) for given circuit parameters.""" + + if isinstance(observables, list): + observables = ListOp(observables) + + expectation_value = StateFn(observables, is_measurement=True) @ StateFn(ansatz) + converted = expectation.convert(expectation_value) + + ansatz_parameters = ansatz.parameters + + def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]: + """Evaluate the observables for the ansatz parameters ``theta``. + + Args: + theta: The ansatz parameters. + + Returns: + The observables evaluated at the ansatz parameters. + """ + value_dict = dict(zip(ansatz_parameters, theta)) + sampled = sampler.convert(converted, params=value_dict) + return sampled.eval() + + return evaluate_observables From 8ecd9baba88a2ffd2d5753912b938b2205ece9f5 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 18 Jul 2022 17:22:04 +0200 Subject: [PATCH 13/24] remove todo --- qiskit/algorithms/evolvers/pvqd/result.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/algorithms/evolvers/pvqd/result.py b/qiskit/algorithms/evolvers/pvqd/result.py index 7aa920062fb2..1d401f619471 100644 --- a/qiskit/algorithms/evolvers/pvqd/result.py +++ b/qiskit/algorithms/evolvers/pvqd/result.py @@ -27,7 +27,6 @@ class PVQDResult(EvolutionResult): def __init__( self, evolved_state: Union[StateFn, QuantumCircuit, OperatorBase], - # TODO: aux_ops_evaluated: Optional[ListOrDict[Tuple[complex, complex]]] = None, aux_ops_evaluated: Optional[List[Tuple[complex, complex]]] = None, times: Optional[List[float]] = None, parameters: Optional[List[np.ndarray]] = None, From c7b74aa78164f3d5bcdb506dff08362338182ab8 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 19 Jul 2022 08:46:52 +0200 Subject: [PATCH 14/24] comments from review --- qiskit/algorithms/evolvers/pvqd/__init__.py | 2 +- qiskit/algorithms/evolvers/pvqd/pvqd.py | 11 +-- .../pvqd/{result.py => pvqd_result.py} | 0 test/python/algorithms/evolvers/test_pvqd.py | 83 +++++++++++-------- 4 files changed, 54 insertions(+), 42 deletions(-) rename qiskit/algorithms/evolvers/pvqd/{result.py => pvqd_result.py} (100%) diff --git a/qiskit/algorithms/evolvers/pvqd/__init__.py b/qiskit/algorithms/evolvers/pvqd/__init__.py index 4338e24fa52f..9377ce631b4e 100644 --- a/qiskit/algorithms/evolvers/pvqd/__init__.py +++ b/qiskit/algorithms/evolvers/pvqd/__init__.py @@ -12,7 +12,7 @@ """The projected Variational Quantum Dynamic (p-VQD) module.""" -from .result import PVQDResult +from .pvqd_result import PVQDResult from .pvqd import PVQD __all__ = ["PVQD", "PVQDResult"] diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py index 973c846dab27..7f2dfeb96e7c 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -31,7 +31,7 @@ from qiskit.synthesis import EvolutionSynthesis, LieTrotter from qiskit.utils import QuantumInstance -from .result import PVQDResult +from .pvqd_result import PVQDResult from .utils import _get_observable_evaluator, _is_gradient_supported from ..evolution_problem import EvolutionProblem @@ -103,7 +103,7 @@ def __init__( expectation: Optional[ExpectationBase] = None, initial_guess: Optional[np.ndarray] = None, evolution: Optional[EvolutionSynthesis] = None, - gradients: bool = True, + use_parameter_shift: bool = True, quantum_instance: Optional[Union[Backend, QuantumInstance]] = None, ) -> None: """ @@ -121,8 +121,9 @@ def __init__( a random vector with elements in the interval :math:`[-0.01, 0.01]`. evolution: The evolution synthesis to use for the construction of the Trotter step. Defaults to first-order Lie-Trotter decomposition. - gradients: If True, use the parameter shift rule to compute gradients. If False, - the optimizer will not be passed a gradient callable. + use_parameter_shift: If True, use the parameter shift rule to compute gradients. + If False, the optimizer will not be passed a gradient callable. In that case, + Qiskit optimizers will use a finite difference rule to approximate the gradients. quantum_instance: The backend of quantum instance used to evaluate the circuits. """ if evolution is None: @@ -135,7 +136,7 @@ def __init__( self.initial_guess = initial_guess self.expectation = expectation self.evolution = evolution - self.gradients = gradients + self.gradients = use_parameter_shift self._sampler = None self.quantum_instance = quantum_instance diff --git a/qiskit/algorithms/evolvers/pvqd/result.py b/qiskit/algorithms/evolvers/pvqd/pvqd_result.py similarity index 100% rename from qiskit/algorithms/evolvers/pvqd/result.py rename to qiskit/algorithms/evolvers/pvqd/pvqd_result.py diff --git a/test/python/algorithms/evolvers/test_pvqd.py b/test/python/algorithms/evolvers/test_pvqd.py index ef9cc8f8a554..b9635d9e03f6 100644 --- a/test/python/algorithms/evolvers/test_pvqd.py +++ b/test/python/algorithms/evolvers/test_pvqd.py @@ -167,6 +167,53 @@ def test_initial_guess_and_observables(self): self.assertEqual(observables[0], 0.1) # expected energy self.assertEqual(observables[1], 1) # expected magnetization + def test_invalid_setup(self): + """Test appropriate error is raised if attributes are missing or incompatible.""" + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + timestep=0.01, + optimizer=L_BFGS_B(maxiter=1), + quantum_instance=self.qasm_backend, + expectation=self.expectation, + ) + problem = EvolutionProblem(self.hamiltonian, time=0.01) + + args_to_test = [ + ("ansatz", self.ansatz), + ("initial_parameters", self.initial_parameters), + ("timestep", 0.01), + ("optimizer", L_BFGS_B(maxiter=1)), + ("quantum_instance", self.qasm_backend), + ("expectation", self.expectation), + ] + + for attr, value in args_to_test: + with self.subTest(msg=f"missing: {attr}"): + # set attribute to None to invalidate the setup + setattr(pvqd, attr, None) + + with self.assertRaises(ValueError): + _ = pvqd.evolve(problem) + + # set the correct value again + setattr(pvqd, attr, value) + + # check PVQD is running now that all arguments are set + result = pvqd.evolve(problem) + self.assertIsNotNone(result.evolved_state) + + +class TestPVQDUtils(QiskitTestCase): + """Test some utility functions for PVQD.""" + + def setUp(self): + super().setUp() + self.sv_backend = BasicAer.get_backend("statevector_simulator") + self.expectation = MatrixExpectation() + self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + self.ansatz = EfficientSU2(2, reps=1) + def test_gradient_supported(self): """Test the gradient support is correctly determined.""" # gradient supported here @@ -216,39 +263,3 @@ def test_gradient_supported(self): pvqd.initial_parameters = np.zeros(circuit.num_parameters) _ = pvqd.evolve(problem) self.assertEqual(info["has_gradient"], expected_support) - - def test_invalid_setup(self): - """Test appropriate error is raised if attributes are missing or incompatible.""" - pvqd = PVQD( - self.ansatz, - self.initial_parameters, - timestep=0.01, - optimizer=L_BFGS_B(maxiter=1), - quantum_instance=self.qasm_backend, - expectation=self.expectation, - ) - problem = EvolutionProblem(self.hamiltonian, time=0.01) - - args_to_test = [ - ("ansatz", self.ansatz), - ("initial_parameters", self.initial_parameters), - ("timestep", 0.01), - ("optimizer", L_BFGS_B(maxiter=1)), - ("quantum_instance", self.qasm_backend), - ("expectation", self.expectation), - ] - - for attr, value in args_to_test: - with self.subTest(msg=f"missing: {attr}"): - # set attribute to None to invalidate the setup - setattr(pvqd, attr, None) - - with self.assertRaises(ValueError): - _ = pvqd.evolve(problem) - - # set the correct value again - setattr(pvqd, attr, value) - - # check PVQD is running now that all arguments are set - result = pvqd.evolve(problem) - self.assertIsNotNone(result.evolved_state) From 8d3bcf5459b02eb4f7ca4f1f2490b8085e72e2bc Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 19 Jul 2022 08:48:00 +0200 Subject: [PATCH 15/24] rm OrderedDict from unitary no idea why this unused import existed, that should be caught before this PR? --- qiskit/extensions/unitary.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/extensions/unitary.py b/qiskit/extensions/unitary.py index 5d9364805ded..a1bd2d2551aa 100644 --- a/qiskit/extensions/unitary.py +++ b/qiskit/extensions/unitary.py @@ -14,7 +14,6 @@ Arbitrary unitary circuit instruction. """ -from collections import OrderedDict import numpy from qiskit.circuit import Gate, ControlledGate From 712184c3f622601024a5b5510b275a9efee7dc55 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 26 Jul 2022 10:41:14 +0200 Subject: [PATCH 16/24] changes from code review --- qiskit/algorithms/evolvers/pvqd/pvqd.py | 71 +++++++++++++++---- .../algorithms/evolvers/pvqd/pvqd_result.py | 5 +- test/python/algorithms/evolvers/test_pvqd.py | 47 ++++++++++-- 3 files changed, 101 insertions(+), 22 deletions(-) diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py index 7f2dfeb96e7c..21dd6ee93946 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -42,12 +42,12 @@ class PVQD(RealEvolver): - """The projected Variational Quantum Dynamics Algorithm. + """The projected Variational Quantum Dynamics (p-VQD) Algorithm. - In each timestep this algorithm computes the next state with a Trotter formula - (specified by the ``evolution`` argument) and projects it onto a variational form (``ansatz``). - The projection is determined by maximizing the fidelity of the Trotter-evolved state - and the ansatz, using a classical optimization routine. See Ref. [1] for details. + In each timestep, this algorithm computes the next state with a Trotter formula + (specified by the ``evolution`` argument) and projects the timestep onto a variational form + (``ansatz``). The projection is determined by maximizing the fidelity of the Trotter-evolved + state and the ansatz, using a classical optimization routine. See Ref. [1] for details. Example: @@ -110,7 +110,8 @@ def __init__( Args: ansatz: A parameterized circuit preparing the variational ansatz to model the time evolved quantum state. - initial_parameters: The initial parameters for the ansatz. + initial_parameters: The initial parameters for the ansatz. Together with the ansatz, + these define the initial state of the time evolution. timestep: The time step. optimizer: The classical optimizers used to minimize the overlap between Trotterization and ansatz. Can be either a :class:`.Optimizer` or a callable @@ -120,7 +121,8 @@ def __init__( previous iteration result is used as initial guess. If None, this is set to a random vector with elements in the interval :math:`[-0.01, 0.01]`. evolution: The evolution synthesis to use for the construction of the Trotter step. - Defaults to first-order Lie-Trotter decomposition. + Defaults to first-order Lie-Trotter decomposition, see also + :mod:`~qiskit.synthesis.evolution` for different options. use_parameter_shift: If True, use the parameter shift rule to compute gradients. If False, the optimizer will not be passed a gradient callable. In that case, Qiskit optimizers will use a finite difference rule to approximate the gradients. @@ -157,12 +159,19 @@ def quantum_instance(self, quantum_instance: Optional[Union[Backend, QuantumInst self._quantum_instance = quantum_instance def step( - self, hamiltonian: OperatorBase, theta: np.ndarray, dt: float, initial_guess: np.ndarray + self, + hamiltonian: OperatorBase, + ansatz: QuantumCircuit, + theta: np.ndarray, + dt: float, + initial_guess: np.ndarray, ) -> Tuple[np.ndarray, float]: """Perform a single time step. Args: hamiltonian: The Hamiltonian under which to evolve. + ansatz: The parameterized quantum circuit which attempts to approximate the + time-evolved state. theta: The current parameters. dt: The time step. initial_guess: The initial guess for the classical optimization of the @@ -173,7 +182,7 @@ def step( Returns: A tuple consisting of the next parameters and the fidelity of the optimization. """ - loss, gradient = self.get_loss(hamiltonian, dt, theta) + loss, gradient = self.get_loss(hamiltonian, ansatz, dt, theta) if initial_guess is None: initial_guess = np.random.random(self.initial_parameters.size) * 0.01 @@ -191,6 +200,7 @@ def step( def get_loss( self, hamiltonian: OperatorBase, + ansatz: QuantumCircuit, dt: float, current_parameters: np.ndarray, ) -> Tuple[Callable[[np.ndarray], float], Optional[Callable[[np.ndarray], np.ndarray]]]: @@ -199,6 +209,8 @@ def get_loss( Args: hamiltonian: The Hamiltonian under which to evolve. + ansatz: The parameterized quantum circuit which attempts to approximate the + time-evolved state. dt: The time step. current_parameters: The current parameters. @@ -209,14 +221,14 @@ def get_loss( self._validate_setup() # use Trotterization to evolve the current state - trotterized = self.ansatz.bind_parameters(current_parameters) + trotterized = ansatz.bind_parameters(current_parameters) trotterized.append( - PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution), self.ansatz.qubits + PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution), ansatz.qubits ) # define the overlap of the Trotterized state and the ansatz x = ParameterVector("w", self.ansatz.num_parameters) - shifted = self.ansatz.assign_parameters(current_parameters + x) + shifted = ansatz.assign_parameters(current_parameters + x) overlap = StateFn(trotterized).adjoint() @ StateFn(shifted) converted = self.expectation.convert(overlap) @@ -239,9 +251,12 @@ def evaluate_loss( value_dict = dict(zip(x, displacement)) sampled = self._sampler.convert(converted, params=value_dict) + + # in principle we could add different loss functions here, but we're currently + # not aware of a use-case for a different one than in the paper return 1 - np.abs(sampled.eval()) ** 2 - if _is_gradient_supported(self.ansatz) and self.gradients: + if _is_gradient_supported(ansatz) and self.gradients: def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: """Evaluate the gradient with the parameter-shift rule. @@ -283,6 +298,7 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: Raises: ValueError: If the evolution time is not positive or the timestep is too small. + NotImplementedError: If the evolution problem contains an initial state. """ self._validate_setup() @@ -296,6 +312,11 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: f"the evolution time ({time})." ) + if evolution_problem.initial_state is not None: + raise NotImplementedError( + "Setting an initial state for the evolution is not yet supported for PVQD." + ) + # get the function to evaluate the observables for a given set of ansatz parameters if observables is not None: evaluate_observables = _get_observable_evaluator( @@ -313,7 +334,7 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: while current_time < time: # perform VQE to find the next parameters next_parameters, fidelity = self.step( - hamiltonian, parameters[-1], self.timestep, initial_guess + hamiltonian, self.ansatz, parameters[-1], self.timestep, initial_guess ) # set initial guess to last parameter update @@ -334,7 +355,7 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: times=times, parameters=parameters, fidelities=fidelities, - estimated_error=np.prod(fidelities), + estimated_error=1 - np.prod(fidelities), ) if observables is not None: result.observables = observable_values @@ -357,8 +378,28 @@ def _validate_setup(self): if getattr(self, arg) is None: raise ValueError(f"The {arg} attribute cannot be None.") + if self.ansatz.num_parameters == 0: + raise QiskitError( + "The ansatz cannot have 0 parameters, otherwise it cannot be trained." + ) + if len(self.initial_parameters) != self.ansatz.num_parameters: raise QiskitError( f"Mismatching number of parameters in the ansatz ({self.ansatz.num_parameters}) " f"and the initial parameters ({len(self.initial_parameters)})." ) + + def _attach_initial_state(self, initial_state): + """Prepend the initial state to the circuit and validate the size matches.""" + if isinstance(initial_state, StateFn): + initial_circuit = initial_state.to_circuit_op().primitive + else: + initial_circuit = initial_state + + if initial_circuit.num_qubits != self.ansatz.num_qubits: + raise ValueError( + f"Mismatching number of qubits in the initial state ({initial_circuit.num_qubits}) " + f"and the ansatz ({self.ansatz.num_qubits})." + ) + + return initial_circuit.compose(self.ansatz) diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd_result.py b/qiskit/algorithms/evolvers/pvqd/pvqd_result.py index 1d401f619471..e14e7a0c9db5 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd_result.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd_result.py @@ -22,7 +22,7 @@ class PVQDResult(EvolutionResult): - """The result object for the pVQD algorithm.""" + """The result object for the p-VQD algorithm.""" def __init__( self, @@ -43,7 +43,8 @@ def __init__( times: The times evaluated during the time integration. parameters: The parameter values at each evaluation time. fidelities: The fidelity of the Trotter step and variational update at each iteration. - estimated_error: The overall estimated error evaluated as product of all fidelities. + estimated_error: The overall estimated error evaluated as one minus the + product of all fidelities. observables: The value of the observables evaluated at each iteration. """ super().__init__(evolved_state, aux_ops_evaluated) diff --git a/test/python/algorithms/evolvers/test_pvqd.py b/test/python/algorithms/evolvers/test_pvqd.py index b9635d9e03f6..b3b4adf3ba43 100644 --- a/test/python/algorithms/evolvers/test_pvqd.py +++ b/test/python/algorithms/evolvers/test_pvqd.py @@ -18,7 +18,7 @@ from qiskit.test import QiskitTestCase -from qiskit import BasicAer +from qiskit import BasicAer, QiskitError from qiskit.circuit import QuantumCircuit, Parameter, Gate from qiskit.algorithms.evolvers import EvolutionProblem from qiskit.algorithms.evolvers.pvqd import PVQD @@ -203,6 +203,45 @@ def test_invalid_setup(self): result = pvqd.evolve(problem) self.assertIsNotNone(result.evolved_state) + def test_zero_parameters(self): + """Test passing an ansatz with zero parameters raises an error.""" + problem = EvolutionProblem(self.hamiltonian, time=0.02) + + pvqd = PVQD( + QuantumCircuit(2), + np.array([]), + timestep=0.01, + optimizer=SPSA(maxiter=10, learning_rate=0.1, perturbation=0.01), + quantum_instance=self.sv_backend, + expectation=self.expectation, + ) + + with self.assertRaises(QiskitError): + _ = pvqd.evolve(problem) + + def test_initial_state_raises(self): + """Test passing an initial state raises an error for now.""" + initial_state = QuantumCircuit(2) + initial_state.x(0) + + problem = EvolutionProblem( + self.hamiltonian, + time=0.02, + initial_state=initial_state, + ) + + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + timestep=0.01, + optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), + quantum_instance=self.sv_backend, + expectation=self.expectation, + ) + + with self.assertRaises(NotImplementedError): + _ = pvqd.evolve(problem) + class TestPVQDUtils(QiskitTestCase): """Test some utility functions for PVQD.""" @@ -217,7 +256,6 @@ def setUp(self): def test_gradient_supported(self): """Test the gradient support is correctly determined.""" # gradient supported here - empty = QuantumCircuit(2) wrapped = EfficientSU2(2) # a circuit wrapped into a big instruction plain = wrapped.decompose() # a plain circuit with already supported instructions @@ -235,8 +273,7 @@ def test_gradient_supported(self): unsupported.append(custom_gate, [0, 1]) tests = [ - (empty, True), # tuple: (circuit, gradient support) - (wrapped, True), + (wrapped, True), # tuple: (circuit, gradient support) (plain, True), (duplicated, False), (needs_chainrule, False), @@ -249,7 +286,7 @@ def test_gradient_supported(self): optimizer = partial(gradient_supplied, info=info) pvqd = PVQD( - ansatz=empty, + ansatz=None, initial_parameters=np.array([]), timestep=0.01, optimizer=optimizer, From c8bab5943dacc4150716ec5c854730526afc7ea7 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 26 Jul 2022 11:01:41 +0200 Subject: [PATCH 17/24] remove function to attach intial states --- qiskit/algorithms/evolvers/pvqd/pvqd.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py index 21dd6ee93946..4ba777ee9a4d 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -388,18 +388,3 @@ def _validate_setup(self): f"Mismatching number of parameters in the ansatz ({self.ansatz.num_parameters}) " f"and the initial parameters ({len(self.initial_parameters)})." ) - - def _attach_initial_state(self, initial_state): - """Prepend the initial state to the circuit and validate the size matches.""" - if isinstance(initial_state, StateFn): - initial_circuit = initial_state.to_circuit_op().primitive - else: - initial_circuit = initial_state - - if initial_circuit.num_qubits != self.ansatz.num_qubits: - raise ValueError( - f"Mismatching number of qubits in the initial state ({initial_circuit.num_qubits}) " - f"and the ansatz ({self.ansatz.num_qubits})." - ) - - return initial_circuit.compose(self.ansatz) From ba088e4d497b39a24d9a455b0cfec4dfaaab5249 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 28 Jul 2022 14:16:00 +0200 Subject: [PATCH 18/24] include comments from review - support MatrixOp - default for timestep - update reno with refs - test step and get_loss --- qiskit/algorithms/__init__.py | 5 +- qiskit/algorithms/evolvers/pvqd/pvqd.py | 39 +++++----- .../project-dynamics-2f848a5f89655429.yaml | 18 ++--- test/python/algorithms/evolvers/test_pvqd.py | 73 ++++++++++++++++--- 4 files changed, 94 insertions(+), 41 deletions(-) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index a1f4bc1f1fee..63e44798a710 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -108,6 +108,8 @@ RealEvolver ImaginaryEvolver TrotterQRTE + PVQD + PVQDResult EvolutionResult EvolutionProblem @@ -246,7 +248,7 @@ from .exceptions import AlgorithmError from .aux_ops_evaluator import eval_observables from .evolvers.trotterization import TrotterQRTE -from .evolvers.pvqd import PVQD +from .evolvers.pvqd import PVQD, PVQDResult __all__ = [ "AlgorithmResult", @@ -294,6 +296,7 @@ "PhaseEstimation", "PhaseEstimationResult", "PVQD", + "PVQDResult", "IterativePhaseEstimation", "AlgorithmError", "eval_observables", diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py index 4ba777ee9a4d..f4d25e1b4574 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -21,13 +21,9 @@ from qiskit.algorithms.optimizers import Optimizer, Minimizer from qiskit.circuit import QuantumCircuit, ParameterVector from qiskit.circuit.library import PauliEvolutionGate +from qiskit.extensions import HamiltonianGate from qiskit.providers import Backend -from qiskit.opflow import ( - OperatorBase, - CircuitSampler, - ExpectationBase, - StateFn, -) +from qiskit.opflow import OperatorBase, CircuitSampler, ExpectationBase, StateFn, MatrixOp from qiskit.synthesis import EvolutionSynthesis, LieTrotter from qiskit.utils import QuantumInstance @@ -51,6 +47,9 @@ class PVQD(RealEvolver): Example: + This snippet computes the real time evolution of a quantum Ising model on two + neighboring sites and keeps track of the magnetization. + .. code-block:: python import numpy as np @@ -98,7 +97,7 @@ def __init__( self, ansatz: Optional[QuantumCircuit] = None, initial_parameters: Optional[np.ndarray] = None, - timestep: Optional[float] = None, + timestep: float = 0.01, optimizer: Optional[Union[Optimizer, Minimizer]] = None, expectation: Optional[ExpectationBase] = None, initial_guess: Optional[np.ndarray] = None, @@ -218,13 +217,17 @@ def get_loss( A callable to evaluate the infidelity and, if gradients are supported and required, a second callable to evaluate the gradient of the infidelity. """ - self._validate_setup() + self._validate_setup(exclude={"optimizer"}) # use Trotterization to evolve the current state trotterized = ansatz.bind_parameters(current_parameters) - trotterized.append( - PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution), ansatz.qubits - ) + + if isinstance(hamiltonian, MatrixOp): + evolution_gate = HamiltonianGate(hamiltonian.primitive, time=dt) + else: + evolution_gate = PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution) + + trotterized.append(evolution_gate, ansatz.qubits) # define the overlap of the Trotterized state and the ansatz x = ParameterVector("w", self.ansatz.num_parameters) @@ -290,8 +293,7 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: """ Args: evolution_problem: The evolution problem containing the hamiltonian, total evolution - time and observables to evaluate. Note that :class:`~.PVQD` currently does not support - hamiltonians of type :class:`~.MatrixOp`. + time and observables to evaluate. Returns: A result object containing the evolution information and evaluated observables. @@ -308,7 +310,7 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: if not 0 < self.timestep <= time: raise ValueError( - f"The time step ({self.timestep}) must be larger than 0 and smaller equal " + f"The time step ({self.timestep}) must be greater than 0 and less than or equal to " f"the evolution time ({time})." ) @@ -363,17 +365,20 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: return result - def _validate_setup(self): + def _validate_setup(self, exclude=None): """Validate the current setup and raise an error if something misses to run.""" + if exclude is None: # arguments not to check for + exclude = {} + required_args = { "ansatz", "initial_parameters", - "timestep", "optimizer", "quantum_instance", "expectation", - } + }.difference(exclude) + for arg in required_args: if getattr(self, arg) is None: raise ValueError(f"The {arg} attribute cannot be None.") diff --git a/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml b/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml index a8319d355e9f..c7b8666b3e72 100644 --- a/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml +++ b/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml @@ -1,12 +1,12 @@ features: - | - Added the the projected Variational Quantum Dynamics (p-VQD) algorithm, see - also Barison et al. [1]. + Added the :class:`PVQD` class to the time evolution framework. This class implements the + projected Variational Quantum Dynamics (p-VQD) algorithm as :class:`.PVQD` of + `Barison et al. `_. - In each timestep this algorithm computes the next state with a Trotter formula - (specified by the ``evolution`` argument) and projects it onto a variational form (``ansatz``). - The projection is determined by maximizing the fidelity of the Trotter-evolved state - and the ansatz, using a classical optimization routine. + In each timestep this algorithm computes the next state with a Trotter formula and projects it + onto a variational form. The projection is determined by maximizing the fidelity of the + Trotter-evolved state and the ansatz, using a classical optimization routine. .. code-block:: python @@ -43,9 +43,3 @@ features: # and evolve! result = pvqd.evolve(problem) - - References: - - [1] Stefano Barison, Filippo Vicentini, and Giuseppe Carleo (2021), An efficient - quantum algorithm for the time evolution of parameterized circuits, - `Quantum 5, 512 `_. \ No newline at end of file diff --git a/test/python/algorithms/evolvers/test_pvqd.py b/test/python/algorithms/evolvers/test_pvqd.py index b3b4adf3ba43..01c870420245 100644 --- a/test/python/algorithms/evolvers/test_pvqd.py +++ b/test/python/algorithms/evolvers/test_pvqd.py @@ -64,6 +64,7 @@ def setUp(self): @data( ("ising", MatrixExpectation, True, "sv"), + ("ising_matrix", MatrixExpectation, True, "sv"), ("ising", PauliExpectation, True, "qasm"), ("pauli", PauliExpectation, False, "qasm"), ) @@ -74,6 +75,8 @@ def test_pvqd(self, hamiltonian_type, expectation_cls, gradient, backend_type): if hamiltonian_type == "ising": hamiltonian = self.hamiltonian + elif hamiltonian_type == "ising_matrix": + hamiltonian = self.hamiltonian.to_matrix_op() else: # hamiltonian_type == "pauli": hamiltonian = X ^ X @@ -107,25 +110,74 @@ def test_pvqd(self, hamiltonian_type, expectation_cls, gradient, backend_type): and np.all([len(params) == num_parameters for params in result.parameters]) ) - def test_matrix_op_raises(self): - """Since the PauliEvolutionGate does not support MatrixOp as input, neither does the PVQD.""" + # def test_matrix_op_raises(self): + # """Since the PauliEvolutionGate does not support MatrixOp as input, neither does the PVQD.""" - hamiltonian = self.hamiltonian.to_matrix_op() - optimizer = L_BFGS_B(maxiter=1) + # hamiltonian = self.hamiltonian.to_matrix_op() + # optimizer = L_BFGS_B(maxiter=1) + + # # run pVQD keeping track of the energy and the magnetization + # pvqd = PVQD( + # self.ansatz, + # self.initial_parameters, + # timestep=0.01, + # optimizer=optimizer, + # quantum_instance=self.sv_backend, + # expectation=MatrixExpectation(), + # ) + # problem = EvolutionProblem(hamiltonian, time=0.02) + + # with self.assertRaises(ValueError): + # _ = pvqd.evolve(problem) + + def test_step(self): + """Test calling the step method directly.""" - # run pVQD keeping track of the energy and the magnetization pvqd = PVQD( self.ansatz, self.initial_parameters, - timestep=0.01, - optimizer=optimizer, + optimizer=L_BFGS_B(maxiter=100), quantum_instance=self.sv_backend, expectation=MatrixExpectation(), ) - problem = EvolutionProblem(hamiltonian, time=0.02) - with self.assertRaises(ValueError): - _ = pvqd.evolve(problem) + # perform optimization for a timestep of 0, then the optimal parameters are the current + # ones and the fidelity is 1 + theta_next, fidelity = pvqd.step( + self.hamiltonian.to_matrix_op(), + self.ansatz, + self.initial_parameters, + dt=0.0, + initial_guess=np.zeros_like(self.initial_parameters), + ) + + self.assertTrue(np.allclose(theta_next, self.initial_parameters)) + self.assertAlmostEqual(fidelity, 1) + + def test_get_loss(self): + """Test getting the loss function directly.""" + + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + quantum_instance=self.sv_backend, + expectation=MatrixExpectation(), + use_parameter_shift=False, + ) + + theta = np.ones(self.ansatz.num_parameters) + loss, gradient = pvqd.get_loss( + self.hamiltonian, self.ansatz, dt=0.0, current_parameters=theta + ) + + displacement = np.arange(self.ansatz.num_parameters) + + with self.subTest(msg="check gradient is None"): + self.assertIsNone(gradient) + + with self.subTest(msg="check loss works"): + self.assertGreater(loss(displacement), 0) + self.assertAlmostEqual(loss(np.zeros_like(theta)), 0) def test_invalid_timestep(self): """Test raises if the timestep is larger than the evolution time.""" @@ -182,7 +234,6 @@ def test_invalid_setup(self): args_to_test = [ ("ansatz", self.ansatz), ("initial_parameters", self.initial_parameters), - ("timestep", 0.01), ("optimizer", L_BFGS_B(maxiter=1)), ("quantum_instance", self.qasm_backend), ("expectation", self.expectation), From 2d24ab1bd1c0db0781f140cbf4bfb453a19ef10e Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 28 Jul 2022 18:25:03 +0200 Subject: [PATCH 19/24] make only quantum instance and optimizer optional, and use num_timesteps --- qiskit/algorithms/evolvers/pvqd/pvqd.py | 74 ++++++++++---------- test/python/algorithms/evolvers/test_pvqd.py | 62 +++++----------- 2 files changed, 53 insertions(+), 83 deletions(-) diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py index f4d25e1b4574..412aeae85c03 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -95,14 +95,14 @@ class PVQD(RealEvolver): def __init__( self, - ansatz: Optional[QuantumCircuit] = None, - initial_parameters: Optional[np.ndarray] = None, - timestep: float = 0.01, + ansatz: QuantumCircuit, + initial_parameters: np.ndarray, + expectation: ExpectationBase, optimizer: Optional[Union[Optimizer, Minimizer]] = None, - expectation: Optional[ExpectationBase] = None, - initial_guess: Optional[np.ndarray] = None, + num_timesteps: Optional[int] = None, evolution: Optional[EvolutionSynthesis] = None, use_parameter_shift: bool = True, + initial_guess: Optional[np.ndarray] = None, quantum_instance: Optional[Union[Backend, QuantumInstance]] = None, ) -> None: """ @@ -111,20 +111,21 @@ def __init__( time evolved quantum state. initial_parameters: The initial parameters for the ansatz. Together with the ansatz, these define the initial state of the time evolution. - timestep: The time step. + expectation: The expectation converter to evaluate expectation values. optimizer: The classical optimizers used to minimize the overlap between Trotterization and ansatz. Can be either a :class:`.Optimizer` or a callable using the :class:`.Minimizer` protocol. - expectation: The expectation converter to evaluate expectation values. - initial_guess: The initial guess for the first VQE optimization. Afterwards the - previous iteration result is used as initial guess. If None, this is set to - a random vector with elements in the interval :math:`[-0.01, 0.01]`. + num_timestep: The number of time steps. If ``None`` it will be set such that the timestep + is close to 0.01. evolution: The evolution synthesis to use for the construction of the Trotter step. Defaults to first-order Lie-Trotter decomposition, see also :mod:`~qiskit.synthesis.evolution` for different options. use_parameter_shift: If True, use the parameter shift rule to compute gradients. If False, the optimizer will not be passed a gradient callable. In that case, Qiskit optimizers will use a finite difference rule to approximate the gradients. + initial_guess: The initial guess for the first VQE optimization. Afterwards the + previous iteration result is used as initial guess. If None, this is set to + a random vector with elements in the interval :math:`[-0.01, 0.01]`. quantum_instance: The backend of quantum instance used to evaluate the circuits. """ if evolution is None: @@ -132,7 +133,7 @@ def __init__( self.ansatz = ansatz self.initial_parameters = initial_parameters - self.timestep = timestep + self.num_timesteps = num_timesteps self.optimizer = optimizer self.initial_guess = initial_guess self.expectation = expectation @@ -181,6 +182,8 @@ def step( Returns: A tuple consisting of the next parameters and the fidelity of the optimization. """ + self._validate_setup() + loss, gradient = self.get_loss(hamiltonian, ansatz, dt, theta) if initial_guess is None: @@ -217,7 +220,7 @@ def get_loss( A callable to evaluate the infidelity and, if gradients are supported and required, a second callable to evaluate the gradient of the infidelity. """ - self._validate_setup(exclude={"optimizer"}) + self._validate_setup(skip={"optimizer"}) # use Trotterization to evolve the current state trotterized = ansatz.bind_parameters(current_parameters) @@ -230,7 +233,7 @@ def get_loss( trotterized.append(evolution_gate, ansatz.qubits) # define the overlap of the Trotterized state and the ansatz - x = ParameterVector("w", self.ansatz.num_parameters) + x = ParameterVector("w", ansatz.num_parameters) shifted = ansatz.assign_parameters(current_parameters + x) overlap = StateFn(trotterized).adjoint() @ StateFn(shifted) @@ -308,11 +311,11 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: observables = evolution_problem.aux_operators hamiltonian = evolution_problem.hamiltonian - if not 0 < self.timestep <= time: - raise ValueError( - f"The time step ({self.timestep}) must be greater than 0 and less than or equal to " - f"the evolution time ({time})." - ) + # determine the number of timesteps and set the timestep + num_timesteps = ( + int(np.ceil(time / 0.01)) if self.num_timesteps is None else self.num_timesteps + ) + timestep = time / num_timesteps if evolution_problem.initial_state is not None: raise NotImplementedError( @@ -327,16 +330,15 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: observable_values = [evaluate_observables(self.initial_parameters)] fidelities = [1] - times = [0] parameters = [self.initial_parameters] + times = np.linspace(0, time, num_timesteps + 1).tolist() # +1 to include initial time 0 - current_time = 0 initial_guess = self.initial_guess - while current_time < time: + for _ in range(num_timesteps): # perform VQE to find the next parameters next_parameters, fidelity = self.step( - hamiltonian, self.ansatz, parameters[-1], self.timestep, initial_guess + hamiltonian, self.ansatz, parameters[-1], timestep, initial_guess ) # set initial guess to last parameter update @@ -347,9 +349,6 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: if observables is not None: observable_values.append(evaluate_observables(next_parameters)) - current_time += self.timestep - times.append(current_time) - evolved_state = self.ansatz.bind_parameters(parameters[-1]) result = PVQDResult( @@ -365,23 +364,22 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: return result - def _validate_setup(self, exclude=None): + def _validate_setup(self, skip=None): """Validate the current setup and raise an error if something misses to run.""" - if exclude is None: # arguments not to check for - exclude = {} + if skip is None: + skip = {} + + required_attributes = {"quantum_instance", "optimizer"}.difference(skip) - required_args = { - "ansatz", - "initial_parameters", - "optimizer", - "quantum_instance", - "expectation", - }.difference(exclude) + for attr in required_attributes: + if getattr(self, attr, None) is None: + raise ValueError(f"The {attr} cannot be None.") - for arg in required_args: - if getattr(self, arg) is None: - raise ValueError(f"The {arg} attribute cannot be None.") + if self.num_timesteps is not None and self.num_timesteps <= 0: + raise ValueError( + f"The number of timesteps must be positive but is {self.num_timesteps}." + ) if self.ansatz.num_parameters == 0: raise QiskitError( diff --git a/test/python/algorithms/evolvers/test_pvqd.py b/test/python/algorithms/evolvers/test_pvqd.py index 01c870420245..5c450fa24822 100644 --- a/test/python/algorithms/evolvers/test_pvqd.py +++ b/test/python/algorithms/evolvers/test_pvqd.py @@ -63,13 +63,13 @@ def setUp(self): self.initial_parameters = np.zeros(self.ansatz.num_parameters) @data( - ("ising", MatrixExpectation, True, "sv"), - ("ising_matrix", MatrixExpectation, True, "sv"), - ("ising", PauliExpectation, True, "qasm"), - ("pauli", PauliExpectation, False, "qasm"), + ("ising", MatrixExpectation, True, "sv", 2), + ("ising_matrix", MatrixExpectation, True, "sv", None), + ("ising", PauliExpectation, True, "qasm", 2), + ("pauli", PauliExpectation, False, "qasm", None), ) @unpack - def test_pvqd(self, hamiltonian_type, expectation_cls, gradient, backend_type): + def test_pvqd(self, hamiltonian_type, expectation_cls, gradient, backend_type, num_timesteps): """Test a simple evolution.""" time = 0.02 @@ -93,7 +93,7 @@ def test_pvqd(self, hamiltonian_type, expectation_cls, gradient, backend_type): pvqd = PVQD( self.ansatz, self.initial_parameters, - timestep=0.01, + num_timesteps=num_timesteps, optimizer=optimizer, quantum_instance=backend, expectation=expectation, @@ -110,26 +110,6 @@ def test_pvqd(self, hamiltonian_type, expectation_cls, gradient, backend_type): and np.all([len(params) == num_parameters for params in result.parameters]) ) - # def test_matrix_op_raises(self): - # """Since the PauliEvolutionGate does not support MatrixOp as input, neither does the PVQD.""" - - # hamiltonian = self.hamiltonian.to_matrix_op() - # optimizer = L_BFGS_B(maxiter=1) - - # # run pVQD keeping track of the energy and the magnetization - # pvqd = PVQD( - # self.ansatz, - # self.initial_parameters, - # timestep=0.01, - # optimizer=optimizer, - # quantum_instance=self.sv_backend, - # expectation=MatrixExpectation(), - # ) - # problem = EvolutionProblem(hamiltonian, time=0.02) - - # with self.assertRaises(ValueError): - # _ = pvqd.evolve(problem) - def test_step(self): """Test calling the step method directly.""" @@ -179,12 +159,12 @@ def test_get_loss(self): self.assertGreater(loss(displacement), 0) self.assertAlmostEqual(loss(np.zeros_like(theta)), 0) - def test_invalid_timestep(self): - """Test raises if the timestep is larger than the evolution time.""" + def test_invalid_num_timestep(self): + """Test raises if the num_timestep is not positive.""" pvqd = PVQD( self.ansatz, self.initial_parameters, - timestep=1, + num_timesteps=0, optimizer=L_BFGS_B(), quantum_instance=self.sv_backend, expectation=self.expectation, @@ -203,7 +183,7 @@ def test_initial_guess_and_observables(self): pvqd = PVQD( self.ansatz, self.initial_parameters, - timestep=0.01, + num_timesteps=10, optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), initial_guess=initial_guess, quantum_instance=self.sv_backend, @@ -219,27 +199,22 @@ def test_initial_guess_and_observables(self): self.assertEqual(observables[0], 0.1) # expected energy self.assertEqual(observables[1], 1) # expected magnetization - def test_invalid_setup(self): - """Test appropriate error is raised if attributes are missing or incompatible.""" + def test_missing_attributesquantum_instance(self): + """Test appropriate error is raised if the quantum instance is missing.""" pvqd = PVQD( self.ansatz, self.initial_parameters, - timestep=0.01, optimizer=L_BFGS_B(maxiter=1), - quantum_instance=self.qasm_backend, expectation=self.expectation, ) problem = EvolutionProblem(self.hamiltonian, time=0.01) - args_to_test = [ - ("ansatz", self.ansatz), - ("initial_parameters", self.initial_parameters), + attrs_to_test = [ ("optimizer", L_BFGS_B(maxiter=1)), ("quantum_instance", self.qasm_backend), - ("expectation", self.expectation), ] - for attr, value in args_to_test: + for attr, value in attrs_to_test: with self.subTest(msg=f"missing: {attr}"): # set attribute to None to invalidate the setup setattr(pvqd, attr, None) @@ -250,9 +225,9 @@ def test_invalid_setup(self): # set the correct value again setattr(pvqd, attr, value) - # check PVQD is running now that all arguments are set - result = pvqd.evolve(problem) - self.assertIsNotNone(result.evolved_state) + with self.subTest(msg="all set again"): + result = pvqd.evolve(problem) + self.assertIsNotNone(result.evolved_state) def test_zero_parameters(self): """Test passing an ansatz with zero parameters raises an error.""" @@ -261,7 +236,6 @@ def test_zero_parameters(self): pvqd = PVQD( QuantumCircuit(2), np.array([]), - timestep=0.01, optimizer=SPSA(maxiter=10, learning_rate=0.1, perturbation=0.01), quantum_instance=self.sv_backend, expectation=self.expectation, @@ -284,7 +258,6 @@ def test_initial_state_raises(self): pvqd = PVQD( self.ansatz, self.initial_parameters, - timestep=0.01, optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), quantum_instance=self.sv_backend, expectation=self.expectation, @@ -339,7 +312,6 @@ def test_gradient_supported(self): pvqd = PVQD( ansatz=None, initial_parameters=np.array([]), - timestep=0.01, optimizer=optimizer, quantum_instance=self.sv_backend, expectation=self.expectation, From 167c1004ddbc9076e4123fae76b0364007ee3525 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 28 Jul 2022 18:34:56 +0200 Subject: [PATCH 20/24] fix docs --- qiskit/algorithms/evolvers/pvqd/pvqd.py | 4 ++-- releasenotes/notes/project-dynamics-2f848a5f89655429.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py index 412aeae85c03..002abd5cfcb7 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -65,14 +65,14 @@ class PVQD(RealEvolver): ansatz = EfficientSU2(2, reps=1) initial_parameters = np.zeros(ansatz.num_parameters) - time = 0.02 + time = 1 optimizer = L_BFGS_B() # setup the algorithm pvqd = PVQD( ansatz, initial_parameters, - timestep=0.01, + num_timesteps=100, optimizer=optimizer, quantum_instance=backend, expectation=expectation diff --git a/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml b/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml index c7b8666b3e72..a33fdfefed28 100644 --- a/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml +++ b/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml @@ -23,14 +23,14 @@ features: ansatz = EfficientSU2(2, reps=1) initial_parameters = np.zeros(ansatz.num_parameters) - time = 0.02 + time = 1 optimizer = L_BFGS_B() # setup the algorithm pvqd = PVQD( ansatz, initial_parameters, - timestep=0.01, + num_timesteps=100, optimizer=optimizer, quantum_instance=backend, expectation=expectation From 0717168a54faf7f627f8d49badf6726644bfcb6b Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 29 Jul 2022 11:49:03 +0200 Subject: [PATCH 21/24] add comment why Optimizer is optional --- qiskit/algorithms/evolvers/pvqd/pvqd.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py index 002abd5cfcb7..56222361c9de 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -114,7 +114,9 @@ def __init__( expectation: The expectation converter to evaluate expectation values. optimizer: The classical optimizers used to minimize the overlap between Trotterization and ansatz. Can be either a :class:`.Optimizer` or a callable - using the :class:`.Minimizer` protocol. + using the :class:`.Minimizer` protocol. This argument is optional since it is + not required for :meth:`get_loss`, but it has to be set before :meth:`evolve` + is called. num_timestep: The number of time steps. If ``None`` it will be set such that the timestep is close to 0.01. evolution: The evolution synthesis to use for the construction of the Trotter step. From 3be5d5435da2bb0c57e62fff57e2273e8d645071 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 29 Jul 2022 16:38:48 +0200 Subject: [PATCH 22/24] use class attributes to document mutable attrs --- qiskit/algorithms/evolvers/pvqd/pvqd.py | 27 ++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py index 56222361c9de..59545c6245ad 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -45,6 +45,27 @@ class PVQD(RealEvolver): (``ansatz``). The projection is determined by maximizing the fidelity of the Trotter-evolved state and the ansatz, using a classical optimization routine. See Ref. [1] for details. + The following attributes can be set via the initializer but can also be changed once the + PVQD object has been constructed. + + Attributes: + + ansatz (QuantumCircuit): The parameterized circuit representing the time-evolved state. + initial_parameters (np.ndarray): The parameters of the ansatz at time 0. + expectation (ExpectationBase): The method to compute expectation values. + optimizer: (Optional[Union[Optimizer, Minimizer]]): The classical optimization routine + used to maximize the fidelity of the Trotter step and ansatz. + num_timesteps (Optional[int]): The number of timesteps to take. If None, it is automatically + selected to achieve a timestep of approximately 0.01. + evolution: (Optional[EvolutionSynthesis]): The method to perform the Trotter step. + Defaults to first-order Lie-Trotter evolution. + use_parameter_shift (bool): If True, use the parameter shift rule for loss function + gradients (if the ansatz supports). + initial_guess (Optional[np.ndarray]): The starting point for the first classical optimization + run, at time 0. Defaults to random values in :math:`[-0.01, 0.01]`. + quantum_instance (Optional[Union[Backend, QuantumInstance]]): The backend or quantum + instance used to evaluate the circuits. + Example: This snippet computes the real time evolution of a quantum Ising model on two @@ -128,7 +149,7 @@ def __init__( initial_guess: The initial guess for the first VQE optimization. Afterwards the previous iteration result is used as initial guess. If None, this is set to a random vector with elements in the interval :math:`[-0.01, 0.01]`. - quantum_instance: The backend of quantum instance used to evaluate the circuits. + quantum_instance: The backend or quantum instance used to evaluate the circuits. """ if evolution is None: evolution = LieTrotter() @@ -140,7 +161,7 @@ def __init__( self.initial_guess = initial_guess self.expectation = expectation self.evolution = evolution - self.gradients = use_parameter_shift + self.use_parameter_shift = use_parameter_shift self._sampler = None self.quantum_instance = quantum_instance @@ -264,7 +285,7 @@ def evaluate_loss( # not aware of a use-case for a different one than in the paper return 1 - np.abs(sampled.eval()) ** 2 - if _is_gradient_supported(ansatz) and self.gradients: + if _is_gradient_supported(ansatz) and self.use_parameter_shift: def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: """Evaluate the gradient with the parameter-shift rule. From 51e743f516104cd6668044aff48e009a8e894ba7 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 2 Aug 2022 09:51:00 +0200 Subject: [PATCH 23/24] rm duplicate quantum_instance doc --- qiskit/algorithms/evolvers/pvqd/pvqd.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py index 59545c6245ad..e2344dc42d86 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -63,8 +63,6 @@ class PVQD(RealEvolver): gradients (if the ansatz supports). initial_guess (Optional[np.ndarray]): The starting point for the first classical optimization run, at time 0. Defaults to random values in :math:`[-0.01, 0.01]`. - quantum_instance (Optional[Union[Backend, QuantumInstance]]): The backend or quantum - instance used to evaluate the circuits. Example: From 86b6ced563457b9d4bdf57c7ec5d3e3a37d60de2 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 3 Aug 2022 21:34:42 +0200 Subject: [PATCH 24/24] fix attributes docs --- qiskit/algorithms/evolvers/pvqd/pvqd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py index e2344dc42d86..e4a1d5893bb5 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -45,19 +45,19 @@ class PVQD(RealEvolver): (``ansatz``). The projection is determined by maximizing the fidelity of the Trotter-evolved state and the ansatz, using a classical optimization routine. See Ref. [1] for details. - The following attributes can be set via the initializer but can also be changed once the - PVQD object has been constructed. + The following attributes can be set via the initializer but can also be read and + updated once the PVQD object has been constructed. Attributes: ansatz (QuantumCircuit): The parameterized circuit representing the time-evolved state. initial_parameters (np.ndarray): The parameters of the ansatz at time 0. expectation (ExpectationBase): The method to compute expectation values. - optimizer: (Optional[Union[Optimizer, Minimizer]]): The classical optimization routine + optimizer (Optional[Union[Optimizer, Minimizer]]): The classical optimization routine used to maximize the fidelity of the Trotter step and ansatz. num_timesteps (Optional[int]): The number of timesteps to take. If None, it is automatically selected to achieve a timestep of approximately 0.01. - evolution: (Optional[EvolutionSynthesis]): The method to perform the Trotter step. + evolution (Optional[EvolutionSynthesis]): The method to perform the Trotter step. Defaults to first-order Lie-Trotter evolution. use_parameter_shift (bool): If True, use the parameter shift rule for loss function gradients (if the ansatz supports).