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 BackendEstimatorV2 #11931

Merged
merged 15 commits into from Mar 15, 2024
Merged

Add BackendEstimatorV2 #11931

merged 15 commits into from Mar 15, 2024

Conversation

t-imamichi
Copy link
Member

@t-imamichi t-imamichi commented Mar 1, 2024

Summary

This PR adds BackendEstimatorV2 as a BaseEstimatorV2 implementation using a backend.
This is another approach to #11899.
This PR provides an implementation that directly relies on backend.run while #11899 provides a converter from EstimatorV1 to EstimatorV2.
This PR can do qubit-wise grouping taking into account broadcasting, e.g., estimator.run([(circuit, ["IX", "YI"])]) -> run 1 circuit.
I made tests based on those for StatevectorEstimator.

Notes (same as #11928)

  • I intentionally specified only BackendV2 as backend option though it still supports BackendV1 because I think we need to support only the latest version of backends. If requested, I can add BackendV1.
    • I added BackendV1 support as well as BackendV2
  • I didn't add transpile feature because Runtime Primitives V2 do not support transpile during execution. Users need to transform input circuits into ISA format beforehand.

Details and comments

@t-imamichi t-imamichi requested review from a team as code owners March 1, 2024 14:26
@qiskit-bot
Copy link
Collaborator

One or more of the the following people are requested to review this:

  • @Qiskit/terra-core
  • @ajavadia
  • @ikkoham
  • @levbishop
  • @t-imamichi

@t-imamichi t-imamichi added mod: primitives Related to the Primitives module Changelog: New Feature Include in the "Added" section of the changelog labels Mar 1, 2024
@coveralls
Copy link

coveralls commented Mar 1, 2024

Pull Request Test Coverage Report for Build 8293552364

Details

  • 139 of 142 (97.89%) changed or added relevant lines in 3 files are covered.
  • 18 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.003%) to 89.282%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/primitives/backend_estimator_v2.py 135 138 97.83%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/lex.rs 6 91.94%
crates/qasm2/src/parse.rs 12 97.15%
Totals Coverage Status
Change from base Build 8285398834: -0.003%
Covered Lines: 59709
Relevant Lines: 66877

💛 - Coveralls

Comment on lines +78 to 79
# calculate [ <psi2(theta2)|H1|psi2(theta2)> ]
result2 = estimator.run([psi2], [hamiltonian1], [theta2]).result()
Copy link
Member Author

@t-imamichi t-imamichi Mar 2, 2024

Choose a reason for hiding this comment

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

H2 should be H1 because the test uses hamiltonian1. Same for other estimator tests.

Comment on lines 80 to 82
basis = PassManagerConfig.from_backend(backend).basis_gates
self._passmanager = PassManager(
[Optimize1qGatesDecomposition(basis=basis, target=backend.target)]
Copy link
Member Author

@t-imamichi t-imamichi Mar 2, 2024

Choose a reason for hiding this comment

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

This is a workaround to support both BasicSimulator and AerSimulator when translating measurement circuits to their basis gates.

If I don't set basis, some tests for BasicSimulator fail as follows.

ERROR: test_run_numpy_params_1 (test.python.primitives.test_backend_estimator_v2.TestBackendEstimatorV2)
test.python.primitives.test_backend_estimator_v2.TestBackendEstimatorV2.test_run_numpy_params_1
...
qiskit.providers.basic_provider.exceptions.BasicProviderError: 'basic_simulator encountered unrecognized operation "sdg"'

On the other hand, if I use generate_preset_passmanager instead of Optimize1qGatesDecomposition, it causes an error with AerSimulator as follows.

ERROR: test_aer_1 (test.python.primitives.test_backend_estimator_v2.TestBackendEstimatorV2)
test.python.primitives.test_backend_estimator_v2.TestBackendEstimatorV2.test_aer_1
...
qiskit.transpiler.exceptions.TranspilerError: "Unable to translate the operations in the circuit: ['sdg', 'h', 'measure'] to the backend's (or manually specified) target basis: ['barrier', 'measure', 'h', 'snapshot']. This likely means the target basis is not universal or there are additional equivalence rules needed in the EquivalenceLibrary being used. For more details on this error see: https://docs.quantum.ibm.com/api/qiskit/transpiler_passes.BasisTranslator#translation-errors"

return PrimitiveResult([self._run_pub(pub) for pub in pubs])

def _run_pub(self, pub: EstimatorPub) -> PubResult:
shots = int(np.ceil(self._VARIANCE_UPPER_BOUND / pub.precision**2))
Copy link
Member Author

Choose a reason for hiding this comment

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

Since I'm not sure the details of precison, this shots estimation is quite rough one. Could you tell me how can we determine the number of shots depending on precision, @ihincks and @chriseclectic?

Copy link
Member

Choose a reason for hiding this comment

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

FWIW, I find confusing that the default precision parameter is 0 but precision should be larger than 0.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

I left a different comment about the default value before reading this. Please go ahead and set it to any reasonable >0 value you like.

Copy link
Contributor

Choose a reason for hiding this comment

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

how can we determine the number of shots

What you've written looks like a good first stab, and essentially the same as the one in the IBM runtime at the moment. For this reason I think it's okay as-is.

If we wanted to make a better guess we could do this: suppose a sparse Pauli sum observable A=\sum_i a_i P_i. Then Var[A]=\sum_i |a_i|^2 Var[P_i]. If we take the worst case that each P_i has expectation value 0, then each Var[P_i] will be about 1, so that Var[A]=\sum_i |a_i|^2. So a conservative guess would be to go through every observable in the array, find the one with the worst such variance, and use that to decide the shots.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you for the details. I copied the API doc of qiskit-ibm-runtime and simplified the code by removing _VARIANCE_UPPER_BOUND.

@1ucian0
Copy link
Member

1ucian0 commented Mar 11, 2024

The following example with Aer:

import numpy as np
from qiskit.circuit.library import IQP
from qiskit import transpile
from qiskit.quantum_info import SparsePauliOp, random_hermitian
from qiskit.primitives import BackendEstimatorV2
from qiskit_aer import Aer

qasm_simulator = Aer.get_backend('qasm_simulator')
qasm_simulator_estimator = BackendEstimatorV2(backend=qasm_simulator)

n_qubits = 12
 
mat = np.real(random_hermitian(n_qubits, seed=1234))
circuit = IQP(mat)
observable = SparsePauliOp("Z" * n_qubits)
 
isa_circuit = transpile(circuit, backend=qasm_simulator, optimization_level=1)
isa_observable = observable.apply_layout(isa_circuit.layout) 
 
job = qasm_simulator_estimator.run([(isa_circuit, isa_observable)], precision=0.01)
result = job.result()
 
print(f" > Expectation value: {result[0].data.evs}")
print(f" > Metadata: {result[0].metadata}")
 > Expectation value: -0.00385
 > Metadata: {'precision': 0.01}

@1ucian0
Copy link
Member

1ucian0 commented Mar 11, 2024

Here an example with FakeBackendV2:

import numpy as np
from qiskit.circuit.library import IQP
from qiskit import transpile
from qiskit.quantum_info import SparsePauliOp, random_hermitian
from qiskit.primitives import BackendEstimatorV2
from qiskit_ibm_runtime.fake_provider import FakeAlmadenV2 

fake_almaden = FakeAlmadenV2()
fake_almaden_estimator = BackendEstimatorV2(backend=fake_almaden)

n_qubits = 12
 
mat = np.real(random_hermitian(n_qubits, seed=1234))
circuit = IQP(mat)
observable = SparsePauliOp("Z" * n_qubits)
 
isa_circuit = transpile(circuit, backend=fake_almaden, optimization_level=1)
isa_observable = observable.apply_layout(isa_circuit.layout) 
 
job = fake_almaden_estimator.run([(isa_circuit, isa_observable)], precision=0.01)
result = job.result()
 
print(f" > Expectation value: {result[0].data.evs}")
print(f" > Metadata: {result[0].metadata}")
 > Expectation value: -0.00575
 > Metadata: {'precision': 0.01}

Copy link
Contributor

@ihincks ihincks left a comment

Choose a reason for hiding this comment

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

Thank-you @t-imamichi this looks very good! I don't have any major comments.

qiskit/primitives/__init__.py Outdated Show resolved Hide resolved
qiskit/primitives/backend_estimator_v2.py Outdated Show resolved Hide resolved
qiskit/primitives/backend_estimator_v2.py Outdated Show resolved Hide resolved
qiskit/primitives/backend_estimator_v2.py Outdated Show resolved Hide resolved
qiskit/primitives/backend_estimator_v2.py Outdated Show resolved Hide resolved
qiskit/primitives/backend_estimator_v2.py Outdated Show resolved Hide resolved
return PrimitiveResult([self._run_pub(pub) for pub in pubs])

def _run_pub(self, pub: EstimatorPub) -> PubResult:
shots = int(np.ceil(self._VARIANCE_UPPER_BOUND / pub.precision**2))
Copy link
Contributor

Choose a reason for hiding this comment

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

I left a different comment about the default value before reading this. Please go ahead and set it to any reasonable >0 value you like.

return PrimitiveResult([self._run_pub(pub) for pub in pubs])

def _run_pub(self, pub: EstimatorPub) -> PubResult:
shots = int(np.ceil(self._VARIANCE_UPPER_BOUND / pub.precision**2))
Copy link
Contributor

Choose a reason for hiding this comment

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

how can we determine the number of shots

What you've written looks like a good first stab, and essentially the same as the one in the IBM runtime at the moment. For this reason I think it's okay as-is.

If we wanted to make a better guess we could do this: suppose a sparse Pauli sum observable A=\sum_i a_i P_i. Then Var[A]=\sum_i |a_i|^2 Var[P_i]. If we take the worst case that each P_i has expectation value 0, then each Var[P_i] will be about 1, so that Var[A]=\sum_i |a_i|^2. So a conservative guess would be to go through every observable in the array, find the one with the worst such variance, and use that to decide the shots.

qiskit/primitives/backend_estimator_v2.py Outdated Show resolved Hide resolved
qiskit/primitives/backend_estimator_v2.py Outdated Show resolved Hide resolved
self,
*,
backend: BackendV1 | BackendV2,
default_precision: float = 0.015625,
Copy link
Member Author

Choose a reason for hiding this comment

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

Args:
backend: The backend to run the primitive on.
default_precision: The default precision to use if none are specified in :meth:`~run`.
Default: 0.015625 (1 / sqrt(4096)).
Copy link
Member Author

Choose a reason for hiding this comment

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

return PrimitiveResult([self._run_pub(pub) for pub in pubs])

def _run_pub(self, pub: EstimatorPub) -> PubResult:
shots = int(np.ceil(1.0 / pub.precision**2))
Copy link
Member Author

Choose a reason for hiding this comment

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

I removed _VARIANCE_UPPER_BOUND and simplified this part since the numerator is 1.

@t-imamichi
Copy link
Member Author

I added BackendV1 support and tests with Fake7QPulseV1.

from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.utils import optionals

BACKENDS = [BasicSimulator(), Fake7QPulseV1(), BackendV2Converter(Fake7QPulseV1())]
Copy link
Member Author

Choose a reason for hiding this comment

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

It would be great if we could set random seed number via constructor of these simulators. I currently set a conservative target precision value. But it may fail in the worst case.

@t-imamichi
Copy link
Member Author

t-imamichi commented Mar 13, 2024

I'm wondering how to add seed.

  1. add seed parameter to the constructor, i.e., BackendEstimator(self, *, backend, default_precision, seed).
  1. add seed parameter to run method, i.e., BackendEstimator.run(self, pubs, *, default_precision, seed).
    We will pass seed to a simulator via _run_circuit. Then, we can set a random seed number though some simulator behavior might be deterministic.

@t-imamichi
Copy link
Member Author

t-imamichi commented Mar 13, 2024

I made a PoC to add seed_simulator option torun method and constructor. I confirmed that it fixes deviation of expectation values of the unit tests.
If it looks good, I will merge the branch to this PR and make the same stuff for BackendSamplerV2.

run method
t-imamichi/qiskit-terra@backend-estimator-v2...t-imamichi:qiskit-terra:backend-estimator-v2-seed

constructor
t-imamichi/qiskit-terra@backend-estimator-v2...t-imamichi:qiskit-terra:backend-estimator-v2-seed2

@jyu00
Copy link
Contributor

jyu00 commented Mar 13, 2024

I made a PoC to add seed_simulator option torun method and constructor.

@t-imamichi We had said in the options RFC that we didn't want to pass options to run() (except those that are required, i.e. shots and precision) anymore. The primitive should instead have an options attribute, preferably a dataclass, e.g.

estimator = BackendEstimatorV2(...)
estimator.options.seed_simulator = 42

This would also be more consistent with what qiskit-ibm-runtime has.

@t-imamichi
Copy link
Member Author

Thank you for your suggestion. I made an option based on dataclass. Could you take a look at it?

@jyu00
Copy link
Contributor

jyu00 commented Mar 14, 2024

@t-imamichi thanks for the update! I'd prefer having default_precision in options as well, since unlike precision, default_precision is not defined in the base interface. I think it's nicer to have all options not defined in the base to live together in options.

# 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)
Copy link
Member

Choose a reason for hiding this comment

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

now you can seed the execution, do you still need assert_allclose?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. It is necessary because unit tests use both an exact simulator and noise simulators.

@1ucian0
Copy link
Member

1ucian0 commented Mar 14, 2024

Some non-blocking comments. It they can be addressed in this PR, great. Otherwise, in a follow up. The comment in #11931 (comment) seems to be the only blocking issue on this PR.

@t-imamichi
Copy link
Member Author

I moved default_precision to Options and update reno to include an example.

I have a question. I used dataclass to define Options instead of using qiskit.providers.Options to allow autocompletion and expect a validation of a wrong option name. If qiskit.providers.Options is preferable, I would update the options.

@1ucian0
Copy link
Member

1ucian0 commented Mar 15, 2024

On my side, LGTM and this can be merged.

I have a question. I used dataclass to define Options instead of using qiskit.providers.Options to allow autocompletion and expect a validation of a wrong option name. If qiskit.providers.Options is preferable, I would update the options.

I don't think it matters. @jyu00 ?

@jyu00
Copy link
Contributor

jyu00 commented Mar 15, 2024

I have a question. I used dataclass to define Options instead of using qiskit.providers.Options to allow autocompletion and expect a validation of a wrong option name. If qiskit.providers.Options is preferable, I would update the options.

Dataclass is actually more preferable than qiskit.providers.Options, so this looks good to me!

@1ucian0 1ucian0 added this pull request to the merge queue Mar 15, 2024
Merged via the queue into Qiskit:main with commit 7370ed0 Mar 15, 2024
12 checks passed
@t-imamichi t-imamichi deleted the backend-estimator-v2 branch March 15, 2024 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog mod: primitives Related to the Primitives module
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants