Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reference implementation of EstimatorV2 #11227

Merged
merged 69 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
0e428a9
Add EstimatorV2
ikkoham Nov 6, 2023
5564603
Merge branch 'main' into primitives/estimator-v2
ikkoham Nov 22, 2023
14c3ff3
Merge branch 'main' into primitives/estimator-v2
ikkoham Nov 27, 2023
4698d74
Update qiskit/primitives/base/base_estimator.py
ikkoham Nov 27, 2023
a074c82
Merge branch 'main' into primitives/estimator-v2
ikkoham Nov 28, 2023
a3d7643
Use PositiveInt for shots
ikkoham Nov 28, 2023
c18b209
improve type hint
ikkoham Nov 28, 2023
cc27a64
Update qiskit/primitives/containers/data_bin.py
ikkoham Nov 28, 2023
9d59318
remove _run and make run abstractmethod
ikkoham Nov 29, 2023
556aec2
Apply suggestions from code review
ikkoham Nov 29, 2023
2325224
fix from review comments
ikkoham Nov 29, 2023
200ef36
move test/python/primitives/containers
ikkoham Nov 29, 2023
a679f17
Merge branch 'main' into primitives/estimator-v2
ikkoham Nov 29, 2023
55122f4
Apply suggestions from code review
ikkoham Nov 29, 2023
9ffda4e
update docs
ikkoham Nov 29, 2023
8cfc8ff
rename task to pubs
ikkoham Nov 30, 2023
18e7395
Pubs -> Pub
ikkoham Nov 30, 2023
cd5b70f
make pydantic optional
ikkoham Dec 1, 2023
f075b4e
Apply suggestions from code review
ikkoham Dec 1, 2023
90447be
Apply suggestions from code review
ikkoham Dec 1, 2023
18053bd
fix lint
ikkoham Dec 1, 2023
998547e
type hint
ikkoham Dec 1, 2023
c3fd89a
fix type hint for python 3.8
ikkoham Dec 1, 2023
2bc2655
improve BindingsArray
ikkoham Dec 1, 2023
dfcc996
fix docs warning
ikkoham Dec 1, 2023
ce01d8f
Remove `slots=True` in dataclass usage.
ihincks Dec 1, 2023
199aeaa
update from review comments
ikkoham Dec 2, 2023
388d2bf
Update qiskit/primitives/containers/estimator_pub.py
ikkoham Dec 2, 2023
7777dea
Update qiskit/primitives/base/base_estimator.py
ikkoham Dec 2, 2023
5d320e9
Merge branch 'main' into primitives/estimator-v2
ikkoham Dec 4, 2023
5307922
Apply suggestions from code review
ikkoham Dec 19, 2023
bcdf842
lint
ikkoham Dec 19, 2023
b6d9376
Merge branch 'main' into primitives/estimator-v2
ikkoham Dec 21, 2023
48bddad
Merge branch 'main' into primitives/estimator-v2
ikkoham Jan 4, 2024
3066fb1
Merge branch 'main' into primitives/estimator-v2
ikkoham Jan 9, 2024
4bbb67f
Merge branch 'main' into primitives/estimator-v2
ikkoham Jan 11, 2024
321cd14
Add BaseEstimatorV2 class
chriseclectic Jan 9, 2024
db9e22a
Fix removal of BasePub
chriseclectic Jan 9, 2024
63ed918
Remove precision from EstimatorPub
chriseclectic Jan 9, 2024
3db0063
Add BaseEstimator._make_data_bin() static method
ihincks Jan 9, 2024
e0101f4
Apply suggestions from code review
chriseclectic Jan 9, 2024
9046da8
Apply suggestions from code review
chriseclectic Jan 9, 2024
75ca51c
linting
chriseclectic Jan 9, 2024
dea7b3b
Move precision to EstimatorPub and Estimator.run
chriseclectic Jan 10, 2024
a5d1e55
Update EstimatorV2 run return type, fix some typos
chriseclectic Jan 16, 2024
b2b74e4
Fix some minor problems
ihincks Jan 17, 2024
3d694ac
add tests
ihincks Jan 17, 2024
4d917c6
Merge branch 'main' into primitives/estimator-v2
ikkoham Jan 17, 2024
c12bb3c
Merge branch 'primitives/estimator-v2-redux' into primitives/estimato…
ikkoham Jan 17, 2024
8387f27
update
ikkoham Jan 17, 2024
5bb3833
Merge branch 'main' into primitives/estimator-v2
ikkoham Jan 18, 2024
1d37ddb
Apply suggestions from code review
ikkoham Jan 18, 2024
a095154
Update qiskit/primitives/statevector_estimator.py
ikkoham Jan 18, 2024
bfaf4c0
revert BasePrimitiveV1
ikkoham Jan 18, 2024
105551d
update
ikkoham Jan 18, 2024
47b2302
rm base_pub.py
ikkoham Jan 18, 2024
71a814a
options do not have precision
ikkoham Jan 18, 2024
1aa8e4a
remove Options
ikkoham Jan 19, 2024
d37ce1b
Merge branch 'main' into primitives/estimator-v2
ikkoham Jan 19, 2024
c2c97dd
refactoring
ikkoham Jan 19, 2024
8e26f18
Update qiskit/primitives/statevector_estimator.py
ihincks Jan 19, 2024
49a1275
Merge branch 'main' into primitives/estimator-v2
ikkoham Jan 24, 2024
f30debb
add seed
ikkoham Jan 24, 2024
10648a7
Update qiskit/primitives/statevector_estimator.py
ikkoham Jan 24, 2024
00a7acf
clean
ikkoham Jan 24, 2024
98af76c
validate non hermitian
ikkoham Jan 25, 2024
a128546
Merge branch 'main' into primitives/estimator-v2
ikkoham Jan 26, 2024
dbf4363
Update qiskit/primitives/statevector_estimator.py
ikkoham Jan 30, 2024
5d2ae1b
black
ikkoham Jan 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion qiskit/primitives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@
BackendEstimator
EstimatorPub

EstimatorV2
===========

.. autosummary::
:toctree: ../stubs/

StatevectorEstimator

Sampler
=======

Expand Down Expand Up @@ -65,12 +73,13 @@
from .base.sampler_result import SamplerResult
from .containers import (
BindingsArray,
EstimatorPub,
ObservablesArray,
PrimitiveResult,
PubResult,
SamplerPub,
EstimatorPub,
)
from .estimator import Estimator
from .sampler import Sampler
from .statevector_estimator import StatevectorEstimator
from .statevector_sampler import StatevectorSampler
2 changes: 1 addition & 1 deletion qiskit/primitives/containers/estimator_pub.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

from __future__ import annotations

from typing import Tuple, Union
from numbers import Real
from typing import Tuple, Union

import numpy as np

Expand Down
65 changes: 65 additions & 0 deletions qiskit/primitives/statevector_estimator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2023, 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.
"""
Estimator class
"""

from __future__ import annotations

from collections.abc import Iterable

import numpy as np

from qiskit.quantum_info import SparsePauliOp, Statevector

from .base import BaseEstimatorV2
from .containers import EstimatorPub, EstimatorPubLike, PrimitiveResult, PubResult
from .primitive_job import PrimitiveJob
from .utils import bound_circuit_to_instruction


class StatevectorEstimator(BaseEstimatorV2):
"""
Simple implementation of :class:`BaseEstimatorV2` with Statevector.
ihincks marked this conversation as resolved.
Show resolved Hide resolved
"""

def run(
self, pubs: Iterable[EstimatorPubLike], *, precision: float | None = None
) -> PrimitiveJob[PrimitiveResult[PubResult]]:
if precision is not None and precision > 0:
raise ValueError("precision must be None or 0 for StatevectorEstimator.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if precision is not None and precision > 0:
raise ValueError("precision must be None or 0 for StatevectorEstimator.")
if precision is None:
precision = 0

We don't want to error if a user puts in precision > 0 value for precision (which is a target precision), since we will always exceed it since precision is guaranteed to be 0 by implementation. This would prevent actually being able to use this in any application / algorithm workflow which uses a sensible initial guess for precision which would always be > 0.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chriseclectic do you propose to:

  1. ignore the precision
  2. artificially add imprecision, as was previously implemented

The argument for (1) is that this is a statevector simulation, so it should always be perfect.
The argument for (2) is that some folks may want to use this implementation to test their ideas, which might include testing that their ideas are robust to finite noise.

Copy link
Member

@chriseclectic chriseclectic Jan 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1: in the base class we say that it is a target precision, which i implicitly assumed meant "I want you to return me a mean estimate with standard error <= this value". Maybe we can make wording more explicit.

To add to my lack of documentation comments, this classes doc string should explain how the simulation is done and say that it will always return standard error as zero because it returns exact expectation value means computed from the full state vector.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But (1) and (2) both satisfy "return me an estimate with standard error <= this value". Sure, (1) does a better estimation job, but 2 technically satisfies the requirements if the sampling is done right.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but neither raises an error and makes the estimator unusable in portable workflow like this does

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @ikkoham : the main point of the StatevectorEstimator is not really to compute expectation values for users, it is to provide a simple reference implementation, and a tool for testing. This is because there are other implementations that are strictly better at computing expectation values: for non-exponential (albeit nisqy) runtime you need to use a runtime backend, and for more performant classical simulations (including Clifford) you can use aer. I realize this is a reversal of some previous comments I made, but I've had a bit more time to think about it now.

So, I am in favour of (2) artificially add imprecision, as was previously implemented because some folks may want to use this implementation to test their ideas, which might include testing that their ideas are robust to finite noise. If some user is in the unlikely situation that they want to call some_application_funcion(estimator: BaseEstimatorV2) with noiseless estimator, and they know that some_application_function internally hard-codes specific precisions >0, they can subclass StatevectorEstimator:

class NoiselessEstimator(StatevectorEstimator):
    def run(self, pubs, precision = None):
        return super().run(pubs, 0.0)

result = some_application_function(NoiselessEstimator())

I do think that the default precision for StatevectorEstimator should be 0, however.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why a user should expect this estimator to be noisy unless we tell them that it is. I think we should explicitly state this is an ideal estimator and the precision argument is ignored since it always perfectly estimates the observable in the class documentation.

You need to consider how an estimator is used in computational workflows. If someone is writing an algorithm (say VQE for example) that takes an estimator to run on, it will choose some initial guess for a precision, and then perhaps depending on the variance of the result may modify this processing for subsequent evaluations. If you have some special test Estimator that only works with an argument of precision 0 this will error in these sort of applications. You don't loose anything by return a precision less than requested.

If you want to add noise to this estimator so precision effects the results artificially you can do so (or make a second subclass estimator that adds noise), but i will not approve this PR while it raises an error or warning under a canonical workflow.

Copy link
Contributor

@ihincks ihincks Jan 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Also, to be clear, and I think we all three agree on this point: the StatevectorEstimator shouldn't raise errors when given precision>0 in any case. Thanks @chriseclectic for flagging this, I shouldn't have missed that. That would make the thing non-portable, in addition to being annoying. So I'm in particular trying to get to the bottom of choosing between 1 and 2, outlined near the top of this thread)

Copy link
Member

@chriseclectic chriseclectic Jan 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want the class to add noise for precision > 0 I am fine with that, just dont add a shots option, all you shuold need is a seed field (like sampler) and initialize an rng in run and do something like rng.normal(evs, precision) (im not exactly sure correct syntax for normal distribution sampling in numpy)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, let's go with (2) and implement rng seeding in the same way as the StatevectorSampler. No need to mention shots anywhere.

coerced_pubs = [EstimatorPub.coerce(pub, precision) for pub in pubs]
job = PrimitiveJob(self._run, coerced_pubs)
job._submit()
return job

def _run(self, pubs: list[EstimatorPub]) -> PrimitiveResult[PubResult]:
return PrimitiveResult([self._run_pub(pub) for pub in pubs])

def _run_pub(self, pub: EstimatorPub) -> PubResult:
circuit = pub.circuit
observables = pub.observables
parameter_values = pub.parameter_values
bound_circuits = parameter_values.bind_all(circuit)
bc_circuits, bc_obs = np.broadcast_arrays(bound_circuits, observables)
evs = np.zeros_like(bc_circuits, dtype=np.float64)
stds = np.zeros_like(bc_circuits, dtype=np.float64)
for index in np.ndindex(*bc_circuits.shape):
bound_circuit = bc_circuits[index]
observable = bc_obs[index]
final_state = Statevector(bound_circuit_to_instruction(bound_circuit))
paulis, coeffs = zip(*observable.items())
obs = SparsePauliOp(paulis, coeffs) # TODO: support non Pauli operators
evs[index] = np.real_if_close(final_state.expectation_value(obs))
data_bin_cls = self._make_data_bin(pub)
data_bin = data_bin_cls(evs=evs, stds=stds)
return PubResult(data_bin, metadata={"precision": 0})
260 changes: 260 additions & 0 deletions test/python/primitives/test_estimatorv2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
# 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.

"""Tests for Estimator."""

import unittest

import numpy as np

from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.circuit.library import RealAmplitudes
from qiskit.primitives import BindingsArray, EstimatorPub, ObservablesArray, StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
from qiskit.test import QiskitTestCase


class TestEstimatorV2(QiskitTestCase):
"""Test Estimator"""

def setUp(self):
super().setUp()
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],
)

def test_estimator_run(self):
"""Test Estimator.run()"""
psi1, psi2 = self.psi
hamiltonian1, hamiltonian2, hamiltonian3 = self.hamiltonian
theta1, theta2, theta3 = self.theta
estimator = StatevectorEstimator()

# Specify the circuit and observable by indices.
# calculate [ <psi1(theta1)|H1|psi1(theta1)> ]
job = estimator.run([(psi1, hamiltonian1, [theta1])])
result = job.result()
np.testing.assert_allclose(result[0].data.evs, [1.5555572817900956])

# 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)|H2|psi2(theta2)> ]
result2 = estimator.run([(psi2, hamiltonian1, theta2)]).result()
np.testing.assert_allclose(result2[0].data.evs, [2.97797666])

# calculate [ <psi1(theta1)|H2|psi1(theta1)>, <psi1(theta1)|H3|psi1(theta1)> ]
result3 = estimator.run([(psi1, [hamiltonian2, hamiltonian3], theta1)]).result()
np.testing.assert_allclose(result3[0].data.evs, [-0.551653, 0.07535239])

# calculate [ [<psi1(theta1)|H1|psi1(theta1)>,
# <psi1(theta3)|H3|psi1(theta3)>],
# [<psi2(theta2)|H2|psi2(theta2)>] ]
result4 = estimator.run(
[(psi1, [hamiltonian1, hamiltonian3], [theta1, theta3]), (psi2, hamiltonian2, theta2)]
).result()
np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318])
np.testing.assert_allclose(result4[1].data.evs, [0.17849238])

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

obs1 = ObservablesArray.coerce([hamiltonian1, hamiltonian3])
bind1 = BindingsArray.coerce([theta1, theta3])
pub1 = EstimatorPub(psi1, obs1, bind1)
obs2 = ObservablesArray.coerce(hamiltonian2)
bind2 = BindingsArray.coerce(theta2)
pub2 = EstimatorPub(psi2, obs2, bind2)

estimator = StatevectorEstimator()
result4 = estimator.run([pub1, pub2]).result()
np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318])
np.testing.assert_allclose(result4[1].data.evs, [0.17849238])

def test_estimator_run_no_params(self):
"""test for estimator without parameters"""
circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5])
est = StatevectorEstimator()
result = est.run([(circuit, self.observable)]).result()
np.testing.assert_allclose(result[0].data.evs, [-1.284366511861733])

def test_run_single_circuit_observable(self):
"""Test for single circuit and single observable case."""
est = StatevectorEstimator()

with self.subTest("No parameter"):
qc = QuantumCircuit(1)
qc.x(0)
op = SparsePauliOp("Z")
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)
self.assertEqual(result[0].metadata["precision"], 0)

with self.subTest("One parameter"):
param = Parameter("x")
qc = QuantumCircuit(1)
qc.ry(param, 0)
op = SparsePauliOp("Z")
param_vals = [
[np.pi],
[[np.pi]],
np.array([np.pi]),
np.array([[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)
self.assertEqual(result[0].metadata["precision"], 0)

with self.subTest("More than one parameter"):
qc = self.psi[0]
op = self.hamiltonian[0]
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)
self.assertEqual(result[0].metadata["precision"], 0)

def test_run_1qubit(self):
"""Test for 1-qubit cases"""
qc = QuantumCircuit(1)
qc2 = QuantumCircuit(1)
qc2.x(0)

op = SparsePauliOp.from_list([("I", 1)])
op2 = SparsePauliOp.from_list([("Z", 1)])

est = StatevectorEstimator()
result = est.run([(qc, op)]).result()
np.testing.assert_allclose(result[0].data.evs, [1])

result = est.run([(qc, op2)]).result()
np.testing.assert_allclose(result[0].data.evs, [1])

result = est.run([(qc2, op)]).result()
np.testing.assert_allclose(result[0].data.evs, [1])

result = est.run([(qc2, op2)]).result()
np.testing.assert_allclose(result[0].data.evs, [-1])

def test_run_2qubits(self):
"""Test for 2-qubit cases (to check endian)"""
qc = QuantumCircuit(2)
qc2 = QuantumCircuit(2)
qc2.x(0)

op = SparsePauliOp.from_list([("II", 1)])
op2 = SparsePauliOp.from_list([("ZI", 1)])
op3 = SparsePauliOp.from_list([("IZ", 1)])

est = StatevectorEstimator()
result = est.run([(qc, op)]).result()
np.testing.assert_allclose(result[0].data.evs, [1])

result = est.run([(qc2, op)]).result()
np.testing.assert_allclose(result[0].data.evs, [1])

result = est.run([(qc, op2)]).result()
np.testing.assert_allclose(result[0].data.evs, [1])

result = est.run([(qc2, op2)]).result()
np.testing.assert_allclose(result[0].data.evs, [1])

result = est.run([(qc, op3)]).result()
np.testing.assert_allclose(result[0].data.evs, [1])

result = est.run([(qc2, op3)]).result()
np.testing.assert_allclose(result[0].data.evs, [-1])

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 = StatevectorEstimator()
# TODO: add validation
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()

def test_run_numpy_params(self):
"""Test for numpy array as parameter values"""
qc = RealAmplitudes(num_qubits=2, reps=2)
op = SparsePauliOp.from_list([("IZ", 1), ("XI", 2), ("ZY", -1)])
k = 5
params_array = np.random.rand(k, qc.num_parameters)
params_list = params_array.tolist()
params_list_array = list(params_array)
estimator = StatevectorEstimator()
target = estimator.run([(qc, op, params_list)]).result()

with self.subTest("ndarrary"):
result = estimator.run([(qc, op, params_array)]).result()
self.assertEqual(len(result[0].data.evs), k)
np.testing.assert_allclose(result[0].data.evs, target[0].data.evs)

with self.subTest("list of ndarray"):
result = estimator.run([(qc, op, params_list_array)]).result()
self.assertEqual(len(result[0].data.evs), k)
np.testing.assert_allclose(result[0].data.evs, target[0].data.evs)


if __name__ == "__main__":
unittest.main()
Loading