diff --git a/docs/source/Primitives.ipynb b/docs/source/Primitives.ipynb index d5aeb72e..f5f6b3cf 100644 --- a/docs/source/Primitives.ipynb +++ b/docs/source/Primitives.ipynb @@ -13,11 +13,7 @@ "\n", "The two currently available primitives are the `Sampler` and the `Estimator`. The first one computes quasi-probability distributions from circuit measurements, while the second one calculates and interprets expectation values of quantum operators that are required for many near-term quantum algorithms.\n", "\n", - "DDSIM provides its own version of these Qiskit Primitives:\n", - "\n", - "- `Sampler` leverages the default circuit simulator based on decision diagrams, while preserving the methods and functionality of the original Qiskit's sampler.\n", - "\n", - "- `Estimator` is currently in development and will be available soon.\n" + "DDSIM provides its own version of these Qiskit Primitives that leverage the default circuit simulator based on decision diagrams, while preserving the methods and functionality of the original Qiskit primitives." ] }, { @@ -262,11 +258,225 @@ "dist = sampler.run(qc, shots=int(1e4)).result().quasi_dists[0]\n", "plot_distribution(dist.binary_probabilities())" ] + }, + { + "cell_type": "markdown", + "id": "9e706eec", + "metadata": {}, + "source": [ + "## Estimator" + ] + }, + { + "cell_type": "markdown", + "id": "42304e06", + "metadata": {}, + "source": [ + "The `Estimator` calculates the expectation value of an observable with respect to a certain quantum state (described by a quantum circuit). In contrast to Qiskit's estimator, the DDSIM version exactly computes the expectation value using its simulator based on decision diagrams instead of sampling.\n", + "\n", + "The `Estimator` also handles parameter binding when dealing with parametrized circuits.\n", + "\n", + "Here we show an example on how to use it:" + ] + }, + { + "cell_type": "markdown", + "id": "7068a1b9", + "metadata": {}, + "source": [ + "First, we build the observable and the quantum state. The observable is given as a `SparsePauliOp` object, while the quantum state is described by a `QuantumCircuit`.\n", + "\n", + "In this example, our observable is the Pauli matrix $\\sigma_{x}$ and the quantum state is $\\frac{1}{\\sqrt{2}}(|0\\rangle + |1\\rangle)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce1f92d8", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from qiskit import QuantumCircuit\n", + "from qiskit.circuit import Parameter\n", + "from qiskit.quantum_info import Pauli\n", + "\n", + "from mqt.ddsim.primitives.estimator import Estimator\n", + "\n", + "# Build quantum state\n", + "circ = QuantumCircuit(1)\n", + "circ.ry(np.pi / 2, 0)\n", + "circ.measure_all()\n", + "\n", + "# Build observable\n", + "pauli_x = Pauli(\"X\")\n", + "\n", + "# Show circuit\n", + "circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6a0403a", + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize estimator\n", + "\n", + "estimator = Estimator()" + ] + }, + { + "cell_type": "markdown", + "id": "4681e409", + "metadata": {}, + "source": [ + "The next step involves running the estimation using the `run()` method. This method requires three arguments: a sequence of `QuantumCircuit` objects representing quantum states, a sequence of `SparsePauliOp` objects representing observables, and optionally, a parameter sequence if we are dealing with parametrized circuits.\n", + "\n", + "The user has to ensure that the number of circuits matches the number of observables, as the estimator pairs corresponding elements from both lists sequentially." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a8a27a6", + "metadata": {}, + "outputs": [], + "source": [ + "# Enter observable and circuit as a sequence\n", + "\n", + "job = estimator.run([circ], [pauli_x])\n", + "result = job.result()" + ] + }, + { + "cell_type": "markdown", + "id": "38f41050", + "metadata": {}, + "source": [ + "The `result()` method of the job returns a `EstimatorResult` object, which contains the computed expectation values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88316971", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\">>> {result}\")\n", + "print(f\" > Expectation values: {result.values}\")" + ] + }, + { + "cell_type": "markdown", + "id": "c5b267cb", + "metadata": {}, + "source": [ + "Now we explore how the `Estimator` works with multiple circuits and observables. For this example, we will calculate the expectation value of $\\sigma_{x}$ and $\\sigma_{y}$ with respect to the quantum state $|1\\rangle$. Since our observable list has a length of 2, we need to enter two copies of the `QuantumCircuit` object representing $|1\\rangle$ as a sequence:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d732634", + "metadata": {}, + "outputs": [], + "source": [ + "# Build quantum state\n", + "circ = QuantumCircuit(1)\n", + "circ.ry(np.pi, 0)\n", + "circ.measure_all()\n", + "\n", + "# Build observables\n", + "pauli_x = Pauli(\"X\")\n", + "pauli_y = Pauli(\"Y\")\n", + "\n", + "# Construct input arguments\n", + "observables = [pauli_x, pauli_y]\n", + "quantum_states = [circ, circ]\n", + "\n", + "# Run estimator\n", + "job = estimator.run(quantum_states, observables)\n", + "result = job.result()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5cc6e1dc", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\">>> {result}\")\n", + "print(f\" > Expectation values: {result.values}\")" + ] + }, + { + "cell_type": "markdown", + "id": "9d4caecc", + "metadata": {}, + "source": [ + "The first and second entries of the list of values are the expectation value of $\\sigma_{x}$ and $\\sigma_{y}$ respectively." + ] + }, + { + "cell_type": "markdown", + "id": "251487b1", + "metadata": {}, + "source": [ + "Let's now calculate the expectation values of $\\sigma_{x}$ with respect to $\\frac{1}{\\sqrt{2}}(|0\\rangle + |1\\rangle)$ and $\\frac{1}{\\sqrt{2}}(|0\\rangle - |1\\rangle)$. For this example we will use parametrized circuits to build the quantum state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee3f93bb", + "metadata": {}, + "outputs": [], + "source": [ + "theta = Parameter(\"theta\")\n", + "circ_2 = QuantumCircuit(1)\n", + "circ_2.ry(theta, 0)\n", + "circ_2.measure_all()\n", + "\n", + "# Show circuit\n", + "circ_2.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5610c6fa", + "metadata": {}, + "outputs": [], + "source": [ + "# Construct input arguments\n", + "observables = [pauli_x, pauli_x]\n", + "quantum_states = [circ_2, circ_2]\n", + "parameters = [[np.pi / 2], [-np.pi / 2]]\n", + "\n", + "# Run estimator\n", + "job = estimator.run(quantum_states, observables, parameters)\n", + "result = job.result()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9262c988", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\">>> {result}\")\n", + "print(f\" > Expectation values: {result.values}\")" + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "My Virtual Environment", "language": "python", "name": "venv" }, diff --git a/src/mqt/ddsim/primitives/estimator.py b/src/mqt/ddsim/primitives/estimator.py new file mode 100644 index 00000000..5fa40914 --- /dev/null +++ b/src/mqt/ddsim/primitives/estimator.py @@ -0,0 +1,268 @@ +"""Estimator implementation using DDSIM CircuitSimulator""" + +from __future__ import annotations + +from itertools import accumulate +from typing import TYPE_CHECKING, Any, Mapping, Sequence, Union + +import numpy as np +from qiskit.circuit import QuantumCircuit +from qiskit.primitives.base import BaseEstimator, EstimatorResult +from qiskit.primitives.primitive_job import PrimitiveJob +from qiskit.primitives.utils import _circuit_key, _observable_key, init_observable +from qiskit.quantum_info import Pauli, PauliList, SparsePauliOp + +from mqt.ddsim.pyddsim import CircuitSimulator +from mqt.ddsim.qasmsimulator import QasmSimulatorBackend + +if TYPE_CHECKING: + from qiskit.circuit import Parameter + from qiskit.circuit.parameterexpression import ParameterValueType + from qiskit.quantum_info.operators.base_operator import BaseOperator + + Parameters = Union[Mapping[Parameter, ParameterValueType], Sequence[ParameterValueType]] + + +class Estimator(BaseEstimator): + """DDSIM implementation of qiskit's sampler. + Code adapted from Qiskit's BackendEstimator class. + """ + + def __init__( + self, + options: dict | None = None, + abelian_grouping: bool = False, + ) -> None: + """Initialize a new Estimator instance + + Args: + options: Default options. + abelian_grouping: Whether the observable should be grouped into + commuting + """ + super().__init__(options=options) + + self._abelian_grouping = abelian_grouping + + self._preprocessed_circuits: tuple[list[QuantumCircuit], list[list[QuantumCircuit]]] | None = None + + self._grouping = list(zip(range(len(self._circuits)), range(len(self._observables)))) + + self._circuit_ids: dict[tuple, int] = {} + self._observable_ids: dict[tuple, int] = {} + + @property + def preprocessed_circuits( + self, + ) -> tuple[list[QuantumCircuit], list[list[QuantumCircuit]]]: + """ + Generate quantum circuits for states and observables produced by preprocessing. + + Returns: + Tuple: A tuple containing two entries: + - List: Quantum circuits list entered in run() method. + - List: Quantum circuit representations of the observables. + """ + self._preprocessed_circuits = self._preprocessing() + return self._preprocessed_circuits + + def _preprocessing(self) -> tuple[list[QuantumCircuit], list[list[QuantumCircuit]]]: + """ + Perform preprocessing for circuit arranging and packaging. + + Returns: + Tuple: + - List: Quantum circuits list entered in run() method. + - List: Quantum circuit representations of the observables. + + This method performs preprocessing for circuit arranging and packaging. It processes quantum circuits and observables + based on the specified grouping and abelian grouping conditions. + """ + state_circuits = [] + observable_circuits = [] + for group in self._grouping: + circuit = self._circuits[group[0]] + observable = self._observables[group[1]] + diff_circuits: list[QuantumCircuit] = [] + if self._abelian_grouping: + for obs in observable.group_commuting(qubit_wise=True): + basis = Pauli((np.logical_or.reduce(obs.paulis.z), np.logical_or.reduce(obs.paulis.x))) + obs_circuit, indices = self._observable_circuit(circuit.num_qubits, basis) + paulis = PauliList.from_symplectic( + obs.paulis.z[:, indices], + obs.paulis.x[:, indices], + obs.paulis.phase, + ) + obs_circuit.metadata = { + "paulis": paulis, + "coeffs": np.real_if_close(obs.coeffs), + } + diff_circuits.append(obs_circuit) + else: + for basis, obs in zip(observable.paulis, observable): + obs_circuit, indices = self._observable_circuit(circuit.num_qubits, basis) + paulis = PauliList.from_symplectic( + obs.paulis.z[:, indices], + obs.paulis.x[:, indices], + obs.paulis.phase, + ) + obs_circuit.metadata = { + "paulis": paulis, + "coeffs": np.real_if_close(obs.coeffs), + } + diff_circuits.append(obs_circuit) + + state_circuits.append(circuit.copy()) + observable_circuits.append(diff_circuits) + return state_circuits, observable_circuits + + @staticmethod + def _observable_circuit(num_qubits: int, pauli: Pauli) -> tuple[QuantumCircuit, list[int]]: + """ + Creates the quantum circuit representation of an observable given as a Pauli string. + + Parameters: + - num_qubits: Number of qubits of the observable. + - pauli_string (str): The Pauli string representing the observable. + + Returns: + Tuple: A tuple containing two entries: + - QuantumCircuit: The quantum circuit representation of the observable. + - List: A list of the qubits involved in the observables operators + """ + qubit_indices = np.arange(pauli.num_qubits)[pauli.z | pauli.x] + if not np.any(qubit_indices): + qubit_indices = [0] + obs_circuit = QuantumCircuit(num_qubits, len(qubit_indices)) + for i in qubit_indices: + if pauli.x[i]: + if pauli.z[i]: + obs_circuit.y(i) + else: + obs_circuit.x(i) + elif pauli.z[i]: + obs_circuit.z(i) + + return obs_circuit, qubit_indices + + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | SparsePauliOp], + parameter_values: Sequence[Parameters], + **run_options: dict[str, Any], + ) -> PrimitiveJob: + circuit_indices = [] + for circuit in circuits: + key_circ = _circuit_key(circuit) + index = self._circuit_ids.get(key_circ) + if index is not None: + circuit_indices.append(index) + else: + num_circuits = len(self._circuits) + circuit_indices.append(num_circuits) + self._circuit_ids[key_circ] = num_circuits + self._circuits.append(circuit) + self._parameters.append(circuit.parameters) + observable_indices = [] + for observable in observables: + observable_copy = init_observable(observable) + key_obs = _observable_key(observable_copy) + index = self._observable_ids.get(key_obs) + if index is not None: + observable_indices.append(index) + else: + num_observables = len(self._observables) + observable_indices.append(num_observables) + self._observable_ids[key_obs] = num_observables + self._observables.append(observable_copy) + job = PrimitiveJob(self._call, circuit_indices, observable_indices, parameter_values, **run_options) + job.submit() + return job + + def _call( + self, + circuits: Sequence[int], + observables: Sequence[int], + parameter_values: Sequence[Parameters], + **run_options: dict[str, Any], + ) -> EstimatorResult: + # Organize circuits + self._grouping = list(zip(circuits, observables)) + state_circuits, observable_circuits = self.preprocessed_circuits + num_observables = [len(obs_circ_list) for obs_circ_list in observable_circuits] + accum = [0, *list(accumulate(num_observables))] + + # Extract metadata from circuits + metadata = [circ.metadata for obs_circ_list in observable_circuits for circ in obs_circ_list] + for obs_circ_list in observable_circuits: + for circ in obs_circ_list: + circ.metadata = {} + + # Bind parameters + bound_circuits = QasmSimulatorBackend.assign_parameters(state_circuits, parameter_values) + + # Run and bind parameters + result_list = [ + exp + for circ, obs_circ_list in zip(bound_circuits, observable_circuits) + for exp in self._run_experiment(circ, obs_circ_list, **run_options) + ] + + return self._postprocessing(result_list, accum, metadata) + + @staticmethod + def _run_experiment( + circ: QuantumCircuit, obs_circ_list: list[QuantumCircuit], **options: dict[str, Any] + ) -> list[float]: + approximation_step_fidelity = options.get("approximation_step_fidelity", 1.0) + approximation_steps = options.get("approximation_steps", 1) + approximation_strategy = options.get("approximation_strategy", "fidelity") + seed = options.get("seed_simulator", -1) + + sim = CircuitSimulator( + circ, + approximation_step_fidelity=approximation_step_fidelity, + approximation_steps=approximation_steps, + approximation_strategy=approximation_strategy, + seed=seed, + ) + + return [sim.expectation_value(observable=obs) for obs in obs_circ_list] + + @staticmethod + def _postprocessing(result_list: list[float], accum: list[int], metadata: list[dict]) -> EstimatorResult: + """ + Perform postprocessing for the evaluation of expectation values. + + Parameters: + - result_list (list[float]): A list of measurement results. + - accum (list[int]): A list representing accumulated indices for grouping measurement results. + - metadata (list[dict]): A list of dictionaries containing metadata associated with each measurement. + + Returns: + EstimatorResult: An instance of the EstimatorResult class containing the processed expectation values and metadata. + + This method calculates the expectation values by combining measurement results according to the provided accumulation indices. + The resulting expectation values are then packaged into an EstimatorResult object along with metadata. + + The input metadata is overwritten, setting "variance" and "shots" to 0 for each expectation value. + """ + expval_list = [] + + for i, j in zip(accum, accum[1:]): + combined_expval = 0.0 + + for k in range(i, j): + expval = [result_list[k]] + meta = metadata[k] + coeffs = meta["coeffs"] + + # Accumulate + combined_expval += np.dot(expval, coeffs) + + expval_list.append(combined_expval) + + metadata = [{"variance": 0, "shots": 0} for _ in expval_list] + + return EstimatorResult(np.real_if_close(expval_list), metadata) diff --git a/src/mqt/ddsim/qasmsimulator.py b/src/mqt/ddsim/qasmsimulator.py index 65f7ceea..0c9bb1d3 100644 --- a/src/mqt/ddsim/qasmsimulator.py +++ b/src/mqt/ddsim/qasmsimulator.py @@ -85,7 +85,7 @@ def max_circuits(self) -> int | None: return None @staticmethod - def _assign_parameters( + def assign_parameters( quantum_circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Parameters] | None, ) -> list[QuantumCircuit]: @@ -137,7 +137,7 @@ def _run_job( self._validate(quantum_circuits) start = time.time() - bound_circuits = self._assign_parameters(quantum_circuits, parameter_values) + bound_circuits = self.assign_parameters(quantum_circuits, parameter_values) result_list = [self._run_experiment(q_circ, **options) for q_circ in bound_circuits] end = time.time() diff --git a/test/python/primitives/test_estimator.py b/test/python/primitives/test_estimator.py new file mode 100644 index 00000000..8be42bf8 --- /dev/null +++ b/test/python/primitives/test_estimator.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +import numpy as np +import pytest +from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library import RealAmplitudes +from qiskit.primitives import EstimatorResult +from qiskit.quantum_info import Operator, Pauli, SparsePauliOp + +from mqt.ddsim.primitives.estimator import Estimator + + +@pytest.fixture() +def estimator() -> Estimator: + """The estimator fixture for the tests in this file.""" + return Estimator() + + +@pytest.fixture() +def circuits() -> list[QuantumCircuit]: + """The circuit fixture for the tests in this file.""" + qc_z = QuantumCircuit(1) + qc_z.ry(np.pi / 2, 0) + qc_z.rz(np.pi, 0) + qc_z.ry(np.pi / 2, 0) + + qc_y = QuantumCircuit(1) + qc_y.rx(-np.pi / 2, 0) + + qc_x = QuantumCircuit(1) + qc_x.ry(np.pi / 2, 0) + + ansatz = RealAmplitudes(num_qubits=2, reps=2) + param_qc_1 = RealAmplitudes(num_qubits=2, reps=2) + param_qc_2 = RealAmplitudes(num_qubits=2, reps=3) + + return [ansatz, [param_qc_1, param_qc_2], [qc_x, qc_y, qc_z]] + + +@pytest.fixture() +def observables() -> list[SparsePauliOp]: + """The observable fixture for the tests in this file.""" + observable = SparsePauliOp.from_list([ + ("II", -1.052373245772859), + ("IZ", 0.39793742484318045), + ("ZI", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ]) + + pauli_x = Pauli("X") + pauli_y = Pauli("Y") + pauli_z = Pauli("Z") + + hamiltonian_1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + hamiltonian_2 = SparsePauliOp.from_list([("IZ", 1)]) + hamiltonian_3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]) + + return [observable, [hamiltonian_1, hamiltonian_2, hamiltonian_3], [pauli_x, pauli_y, pauli_z]] + + +def test_estimator_run_single_circuit__observable_no_params( + circuits: list[QuantumCircuit], observables: list[SparsePauliOp], estimator: Estimator +) -> None: + """test for estimator with a single circuit/observable and no parameters""" + circuit = circuits[0].assign_parameters([0, 1, 1, 2, 3, 5]) + observable = observables[0] + + # Pass circuit and observable as a sequence + result = estimator.run([circuit], [observable]).result() + + assert isinstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1.284366511861733], rtol=1e-7, atol=1e-7) + + # Pass circuit and observable as an object + result = estimator.run(circuit, observable).result() + + assert isinstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1.284366511861733], rtol=1e-7, atol=1e-7) + + +def test_run_with_operator(circuits: list[QuantumCircuit], estimator: Estimator) -> None: + """test for run with Operator as an observable""" + circuit = circuits[0].assign_parameters([0, 1, 1, 2, 3, 5]) + matrix = Operator([ + [-1.06365335, 0.0, 0.0, 0.1809312], + [0.0, -1.83696799, 0.1809312, 0.0], + [0.0, 0.1809312, -0.24521829, 0.0], + [0.1809312, 0.0, 0.0, -1.06365335], + ]) + result = estimator.run([circuit], [matrix]).result() + + assert isinstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1.284366511861733], rtol=1e-7, atol=1e-7) + + +def test_estimator_run_single_circuit__observable_with_params( + circuits: list[QuantumCircuit], observables: list[SparsePauliOp], estimator: Estimator +) -> None: + """test for estimator with a single circuit/observable and parameters""" + circuit = circuits[0] + observable = observables[0] + + # Pass circuit, observable and parameter values as a sequence + result = estimator.run([circuit], [observable], [[0, 1, 1, 2, 3, 5]]).result() + + assert isinstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1.284366511861733], rtol=1e-7, atol=1e-7) + + # Pass circuit, observable and parameter values as objects + result = estimator.run(circuit, observable, [0, 1, 1, 2, 3, 5]).result() + + assert isinstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1.284366511861733], rtol=1e-7, atol=1e-7) + + +def test_estimator_run_multiple_circuits_observables_no_params( + circuits: list[QuantumCircuit], observables: list[SparsePauliOp], estimator: Estimator +) -> None: + """test for estimator with multiple circuits/observables and no parameters""" + qc_x, qc_y, qc_z = circuits[2] + pauli_x, pauli_y, pauli_z = observables[2] + + result = estimator.run([qc_x, qc_y, qc_z], [pauli_x, pauli_y, pauli_z]).result() + + assert isinstance(result, EstimatorResult) + assert np.array_equal(result.values, [1, 1, 1]) + + +def test_estimator_run_multiple_circuits_observables_with_params( + circuits: list[QuantumCircuit], observables: list[SparsePauliOp], estimator: Estimator +) -> None: + """test for estimator with multiple circuits/observables with parameters""" + psi1, psi2 = circuits[1] + hamiltonian_1, hamiltonian_2, hamiltonian_3 = observables[1] + theta_1, theta_2, theta_3 = ([0, 1, 1, 2, 3, 5], [0, 1, 1, 2, 3, 5, 8, 13], [1, 2, 3, 4, 5, 6]) + + result = estimator.run( + [psi1, psi2, psi1], [hamiltonian_1, hamiltonian_2, hamiltonian_3], [theta_1, theta_2, theta_3] + ).result() + + assert isinstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1.55555728, 0.17849238, -1.08766318], rtol=1e-7, atol=1e-7) + + +def test_estimator_sequenctial_run( + circuits: list[QuantumCircuit], observables: list[SparsePauliOp], estimator: Estimator +) -> None: + """test for estimator's sequenctial run""" + psi1, psi2 = circuits[1] + hamiltonian_1, hamiltonian_2, hamiltonian_3 = observables[1] + theta_1, theta_2, theta_3 = ([0, 1, 1, 2, 3, 5], [0, 1, 1, 2, 3, 5, 8, 13], [1, 2, 3, 4, 5, 6]) + + # First run + result = estimator.run([psi1], [hamiltonian_1], [theta_1]).result() + + assert isinstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1.5555573817900956], rtol=1e-7, atol=1e-7) + + # Second run + result2 = estimator.run([psi2], [hamiltonian_1], [theta_2]).result() + + assert isinstance(result2, EstimatorResult) + np.testing.assert_allclose(result2.values, [2.97797666], rtol=1e-7, atol=1e-7) + + # Third run + result3 = estimator.run([psi1, psi1], [hamiltonian_2, hamiltonian_3], [theta_1] * 2).result() + + assert isinstance(result3, EstimatorResult) + np.testing.assert_allclose(result3.values, [-0.551653, 0.07535239], rtol=1e-7, atol=1e-7) + + # Last run + result4 = estimator.run( + [psi1, psi2, psi1], [hamiltonian_1, hamiltonian_2, hamiltonian_3], [theta_1, theta_2, theta_3] + ).result() + + assert isinstance(result4, EstimatorResult) + np.testing.assert_allclose(result4.values, [1.55555728, 0.17849238, -1.08766318], rtol=1e-7, atol=1e-7)