qiskit-aer/test/terra/primitives/test_estimator_v2.py

478 lines
20 KiB
Python

# This code is part of Qiskit.
#
# (C) Copyright IBM 2024.
#
# 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.
"""Tests for Estimator V2."""
from __future__ import annotations
import unittest
from test.terra.common import QiskitAerTestCase
import numpy as np
from qiskit import transpile
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.circuit.library import RealAmplitudes
from qiskit.primitives import StatevectorEstimator
from qiskit.primitives.containers.bindings_array import BindingsArray
from qiskit.primitives.containers.estimator_pub import EstimatorPub
from qiskit.primitives.containers.observables_array import ObservablesArray
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit_aer import AerSimulator
from qiskit_aer.primitives import EstimatorV2
class TestEstimatorV2(QiskitAerTestCase):
"""Test Estimator V2"""
def setUp(self):
super().setUp()
self._precision = 5e-3
self._rtol = 3e-1
self._seed = 15
self._rng = np.random.default_rng(self._seed)
self._options = {
"run_options": {"seed_simulator": self._seed},
"default_precision": self._precision,
}
self.ansatz = RealAmplitudes(num_qubits=2, reps=2)
self.observable = SparsePauliOp.from_list(
[
("II", -1.052373245772859),
("IZ", 0.39793742484318045),
("ZI", -0.39793742484318045),
("ZZ", -0.01128010425623538),
("XX", 0.18093119978423156),
]
)
self.expvals = -1.0284380963435145, -1.284366511861733
self.psi = (RealAmplitudes(num_qubits=2, reps=2), RealAmplitudes(num_qubits=2, reps=3))
self.params = tuple(psi.parameters for psi in self.psi)
self.hamiltonian = (
SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]),
SparsePauliOp.from_list([("IZ", 1)]),
SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]),
)
self.theta = (
[0, 1, 1, 2, 3, 5],
[0, 1, 1, 2, 3, 5, 8, 13],
[1, 2, 3, 4, 5, 6],
)
self.backend = AerSimulator()
def test_estimator_run(self):
"""Test Estimator.run()"""
psi1, psi2 = self.psi
hamiltonian1, hamiltonian2, hamiltonian3 = self.hamiltonian
theta1, theta2, theta3 = self.theta
pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend)
psi1, psi2 = pm.run([psi1, psi2])
estimator = EstimatorV2(options=self._options)
# Specify the circuit and observable by indices.
# calculate [ <psi1(theta1)|H1|psi1(theta1)> ]
ham1 = hamiltonian1.apply_layout(psi1.layout)
job = estimator.run([(psi1, ham1, [theta1])])
result = job.result()
np.testing.assert_allclose(result[0].data.evs, [1.5555572817900956], rtol=self._rtol)
self.assertIn("simulator_metadata", result[0].metadata)
# Objects can be passed instead of indices.
# Note that passing objects has an overhead
# since the corresponding indices need to be searched.
# User can append a circuit and observable.
# calculate [ <psi2(theta2)|H1|psi2(theta2)> ]
ham1 = hamiltonian1.apply_layout(psi2.layout)
result2 = estimator.run([(psi2, ham1, theta2)]).result()
np.testing.assert_allclose(result2[0].data.evs, [2.97797666], rtol=self._rtol)
self.assertIn("simulator_metadata", result2[0].metadata)
# calculate [ <psi1(theta1)|H2|psi1(theta1)>, <psi1(theta1)|H3|psi1(theta1)> ]
ham2 = hamiltonian2.apply_layout(psi1.layout)
ham3 = hamiltonian3.apply_layout(psi1.layout)
result3 = estimator.run([(psi1, [ham2, ham3], theta1)]).result()
np.testing.assert_allclose(result3[0].data.evs, [-0.551653, 0.07535239], rtol=self._rtol)
self.assertIn("simulator_metadata", result3[0].metadata)
# calculate [ [<psi1(theta1)|H1|psi1(theta1)>,
# <psi1(theta3)|H3|psi1(theta3)>],
# [<psi2(theta2)|H2|psi2(theta2)>] ]
ham1 = hamiltonian1.apply_layout(psi1.layout)
ham3 = hamiltonian3.apply_layout(psi1.layout)
ham2 = hamiltonian2.apply_layout(psi2.layout)
result4 = estimator.run(
[
(psi1, [ham1, ham3], [theta1, theta3]),
(psi2, ham2, theta2),
]
).result()
np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318], rtol=self._rtol)
np.testing.assert_allclose(result4[1].data.evs, [0.17849238], rtol=self._rtol)
self.assertIn("simulator_metadata", result4[0].metadata)
self.assertIn("simulator_metadata", result4[1].metadata)
def test_estimator_with_pub(self):
"""Test estimator with explicit EstimatorPubs."""
psi1, psi2 = self.psi
hamiltonian1, hamiltonian2, hamiltonian3 = self.hamiltonian
theta1, theta2, theta3 = self.theta
pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend)
psi1, psi2 = pm.run([psi1, psi2])
ham1 = hamiltonian1.apply_layout(psi1.layout)
ham3 = hamiltonian3.apply_layout(psi1.layout)
obs1 = ObservablesArray.coerce([ham1, ham3])
bind1 = BindingsArray.coerce({tuple(psi1.parameters): [theta1, theta3]})
pub1 = EstimatorPub(psi1, obs1, bind1)
ham2 = hamiltonian2.apply_layout(psi2.layout)
obs2 = ObservablesArray.coerce(ham2)
bind2 = BindingsArray.coerce({tuple(psi2.parameters): theta2})
pub2 = EstimatorPub(psi2, obs2, bind2)
estimator = EstimatorV2(options=self._options)
result4 = estimator.run([pub1, pub2]).result()
np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318], rtol=self._rtol)
np.testing.assert_allclose(result4[1].data.evs, [0.17849238], rtol=self._rtol)
def test_estimator_run_no_params(self):
"""test for estimator without parameters"""
circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5])
pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend)
circuit = pm.run(circuit)
est = EstimatorV2(options=self._options)
observable = self.observable.apply_layout(circuit.layout)
result = est.run([(circuit, observable)]).result()
np.testing.assert_allclose(result[0].data.evs, [-1.284366511861733], rtol=self._rtol)
def test_run_single_circuit_observable(self):
"""Test for single circuit and single observable case."""
est = EstimatorV2(options=self._options)
pm = generate_preset_pass_manager(optimization_level=0, target=self.backend.target)
with self.subTest("No parameter"):
qc = QuantumCircuit(1)
qc.x(0)
qc = pm.run(qc)
op = SparsePauliOp("Z")
op = op.apply_layout(qc.layout)
param_vals = [None, [], [[]], np.array([]), np.array([[]]), [np.array([])]]
target = [-1]
for val in param_vals:
self.subTest(f"{val}")
result = est.run([(qc, op, val)]).result()
np.testing.assert_allclose(result[0].data.evs, target, rtol=self._rtol)
self.assertEqual(result[0].metadata["target_precision"], self._precision)
with self.subTest("One parameter"):
param = Parameter("x")
qc = QuantumCircuit(1)
qc.ry(param, 0)
qc = pm.run(qc)
op = SparsePauliOp("Z")
op = op.apply_layout(qc.layout)
param_vals = [
[np.pi],
np.array([np.pi]),
]
target = [-1]
for val in param_vals:
self.subTest(f"{val}")
result = est.run([(qc, op, val)]).result()
np.testing.assert_allclose(result[0].data.evs, target, rtol=self._rtol)
self.assertEqual(result[0].metadata["target_precision"], self._precision)
with self.subTest("More than one parameter"):
qc = self.psi[0]
qc = pm.run(qc)
op = self.hamiltonian[0]
op = op.apply_layout(qc.layout)
param_vals = [
self.theta[0],
[self.theta[0]],
np.array(self.theta[0]),
np.array([self.theta[0]]),
[np.array(self.theta[0])],
]
target = [1.5555572817900956]
for val in param_vals:
self.subTest(f"{val}")
result = est.run([(qc, op, val)]).result()
np.testing.assert_allclose(result[0].data.evs, target, rtol=self._rtol)
self.assertEqual(result[0].metadata["target_precision"], self._precision)
def test_run_1qubit(self):
"""Test for 1-qubit cases"""
qc = QuantumCircuit(1)
qc2 = QuantumCircuit(1)
qc2.x(0)
pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend)
qc, qc2 = pm.run([qc, qc2])
op = SparsePauliOp.from_list([("I", 1)])
op2 = SparsePauliOp.from_list([("Z", 1)])
est = EstimatorV2(options=self._options)
op_1 = op.apply_layout(qc.layout)
result = est.run([(qc, op_1)]).result()
np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol)
op_2 = op2.apply_layout(qc.layout)
result = est.run([(qc, op_2)]).result()
np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol)
op_3 = op.apply_layout(qc2.layout)
result = est.run([(qc2, op_3)]).result()
np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol)
op_4 = op2.apply_layout(qc2.layout)
result = est.run([(qc2, op_4)]).result()
np.testing.assert_allclose(result[0].data.evs, [-1], rtol=self._rtol)
def test_run_2qubits(self):
"""Test for 2-qubit cases (to check endian)"""
qc = QuantumCircuit(2)
qc2 = QuantumCircuit(2)
qc2.x(0)
pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend)
qc, qc2 = pm.run([qc, qc2])
op = SparsePauliOp.from_list([("II", 1)])
op2 = SparsePauliOp.from_list([("ZI", 1)])
op3 = SparsePauliOp.from_list([("IZ", 1)])
est = EstimatorV2(options=self._options)
op_1 = op.apply_layout(qc.layout)
result = est.run([(qc, op_1)]).result()
np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol)
op_2 = op.apply_layout(qc2.layout)
result = est.run([(qc2, op_2)]).result()
np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol)
op_3 = op2.apply_layout(qc.layout)
result = est.run([(qc, op_3)]).result()
np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol)
op_4 = op2.apply_layout(qc2.layout)
result = est.run([(qc2, op_4)]).result()
np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol)
op_5 = op3.apply_layout(qc.layout)
result = est.run([(qc, op_5)]).result()
np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol)
op_6 = op3.apply_layout(qc2.layout)
result = est.run([(qc2, op_6)]).result()
np.testing.assert_allclose(result[0].data.evs, [-1], rtol=self._rtol)
def test_run_errors(self):
"""Test for errors"""
qc = QuantumCircuit(1)
qc2 = QuantumCircuit(2)
op = SparsePauliOp.from_list([("I", 1)])
op2 = SparsePauliOp.from_list([("II", 1)])
est = EstimatorV2(options=self._options)
with self.assertRaises(ValueError):
est.run([(qc, op2)]).result()
with self.assertRaises(ValueError):
est.run([(qc, op, [[1e4]])]).result()
with self.assertRaises(ValueError):
est.run([(qc2, op2, [[1, 2]])]).result()
with self.assertRaises(ValueError):
est.run([(qc, [op, op2], [[1]])]).result()
with self.assertRaises(ValueError):
est.run([(qc, op)], precision=-1).result()
with self.assertRaises(ValueError):
est.run([(qc, 1j * op)], precision=0.1).result()
# precision < 0
with self.assertRaises(ValueError):
est.run([(qc, op, None, -1)]).result()
with self.assertRaises(ValueError):
est.run([(qc, op)], precision=-1).result()
with self.subTest("missing []"):
with self.assertRaisesRegex(ValueError, "An invalid Estimator pub-like was given"):
_ = est.run((qc, op)).result()
def test_run_numpy_params(self):
"""Test for numpy array as parameter values"""
qc = RealAmplitudes(num_qubits=2, reps=2)
pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend)
qc = pm.run(qc)
op = SparsePauliOp.from_list([("IZ", 1), ("XI", 2), ("ZY", -1)])
op = op.apply_layout(qc.layout)
k = 5
params_array = self._rng.random((k, qc.num_parameters))
params_list = params_array.tolist()
params_list_array = list(params_array)
statevector_estimator = StatevectorEstimator(seed=123)
target = statevector_estimator.run([(qc, op, params_list)]).result()
estimator = EstimatorV2(options=self._options)
with self.subTest("ndarrary"):
result = estimator.run([(qc, op, params_array)]).result()
self.assertEqual(result[0].data.evs.shape, (k,))
np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol)
with self.subTest("list of ndarray"):
result = estimator.run([(qc, op, params_list_array)]).result()
self.assertEqual(result[0].data.evs.shape, (k,))
np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol)
def test_precision(self):
"""Test for precision"""
estimator = EstimatorV2(options=self._options)
pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend)
psi1 = pm.run(self.psi[0])
hamiltonian1 = self.hamiltonian[0].apply_layout(psi1.layout)
theta1 = self.theta[0]
job = estimator.run([(psi1, hamiltonian1, [theta1])])
result = job.result()
np.testing.assert_allclose(result[0].data.evs, [1.901141473854881], rtol=self._rtol)
# The result of the second run is the same
job = estimator.run([(psi1, hamiltonian1, [theta1]), (psi1, hamiltonian1, [theta1])])
result = job.result()
np.testing.assert_allclose(result[0].data.evs, [1.901141473854881], rtol=self._rtol)
np.testing.assert_allclose(result[1].data.evs, [1.901141473854881], rtol=self._rtol)
# apply smaller precision value
job = estimator.run([(psi1, hamiltonian1, [theta1])], precision=self._precision * 0.5)
result = job.result()
np.testing.assert_allclose(result[0].data.evs, [1.5555572817900956], rtol=self._rtol)
def test_diff_precision(self):
"""Test for running different precisions at once"""
estimator = EstimatorV2(options=self._options)
pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend)
psi1 = pm.run(self.psi[0])
hamiltonian1 = self.hamiltonian[0].apply_layout(psi1.layout)
theta1 = self.theta[0]
job = estimator.run(
[(psi1, hamiltonian1, [theta1]), (psi1, hamiltonian1, [theta1], self._precision * 0.8)]
)
result = job.result()
np.testing.assert_allclose(result[0].data.evs, [1.901141473854881], rtol=self._rtol)
np.testing.assert_allclose(result[1].data.evs, [1.901141473854881], rtol=self._rtol)
def test_iter_pub(self):
"""test for an iterable of pubs"""
circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5])
pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend)
circuit = pm.run(circuit)
estimator = EstimatorV2(options=self._options)
observable = self.observable.apply_layout(circuit.layout)
result = estimator.run(iter([(circuit, observable), (circuit, observable)])).result()
np.testing.assert_allclose(result[0].data.evs, [-1.284366511861733], rtol=self._rtol)
np.testing.assert_allclose(result[1].data.evs, [-1.284366511861733], rtol=self._rtol)
def test_metadata(self):
"""Test for metadata"""
qc = QuantumCircuit(2)
qc2 = QuantumCircuit(2)
qc2.metadata = {"a": 1}
estimator = EstimatorV2(options=self._options)
pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend)
qc, qc2 = pm.run([qc, qc2])
op = SparsePauliOp("ZZ").apply_layout(qc.layout)
op2 = SparsePauliOp("ZZ").apply_layout(qc2.layout)
result = estimator.run([(qc, op), (qc2, op2)], precision=0.1).result()
self.assertEqual(len(result), 2)
self.assertEqual(result.metadata, {"version": 2})
metadata = result[0].metadata
self.assertIsInstance(metadata["simulator_metadata"], dict)
del metadata["simulator_metadata"]
self.assertEqual(
metadata,
{"target_precision": 0.1, "circuit_metadata": qc.metadata},
)
metadata = result[1].metadata
self.assertIsInstance(metadata["simulator_metadata"], dict)
del metadata["simulator_metadata"]
self.assertEqual(
result[1].metadata,
{"target_precision": 0.1, "circuit_metadata": qc2.metadata},
)
def test_truncate(self):
"""Test for truncation of save_expval"""
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.append(RealAmplitudes(num_qubits=2, reps=2), [0, 1])
backend_2 = GenericBackendV2(num_qubits=2)
backend_5 = GenericBackendV2(num_qubits=5)
qc_2 = transpile(qc, backend_2, optimization_level=0)
qc_5 = transpile(qc, backend_5, optimization_level=0)
estimator_2 = EstimatorV2.from_backend(backend_2, options=self._options)
estimator_5 = EstimatorV2.from_backend(backend_5, options=self._options)
H1 = self.observable
H1_2 = H1.apply_layout(qc_2.layout)
H1_5 = H1.apply_layout(qc_5.layout)
theta1 = [0, 1, 1, 2, 3, 5]
result_2 = estimator_2.run(
[
(qc_2, [H1_2], [theta1]),
],
precision=0.01,
).result()
result_5 = estimator_5.run(
[
(qc_5, [H1_5], [theta1]),
],
precision=0.01,
).result()
self.assertAlmostEqual(
result_5[0].data["evs"][0], result_2[0].data["evs"][0], delta=self._rtol
)
def test_truncate_large_backends(self):
"""Test truncation allows us to run few-qubit circuits on many-qubit backends"""
N = 12
qc = QuantumCircuit(N)
qc.x(range(N))
qc.h(range(N))
for kk in range(N // 2, 0, -1):
qc.ch(kk, kk - 1)
for kk in range(N // 2, N - 1):
qc.ch(kk, kk + 1)
op = SparsePauliOp("Z" * N)
backend_127 = GenericBackendV2(num_qubits=127)
pm = generate_preset_pass_manager(backend=backend_127, optimization_level=1)
isa_circuit = pm.run(qc)
mapped_observable = op.apply_layout(isa_circuit.layout)
ref_est = StatevectorEstimator()
ref_result = ref_est.run(pubs=[(qc, op)]).result()
est = EstimatorV2()
res = est.run(pubs=[(isa_circuit, mapped_observable)]).result()
self.assertAlmostEqual(ref_result[0].data.evs, res[0].data.evs)
if __name__ == "__main__":
unittest.main()