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

Address executor and observable incompatibility #2514

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

bdg221
Copy link
Collaborator

@bdg221 bdg221 commented Sep 25, 2024

Description

Fixes #2449

This PR address code changes in the Executor and Pauli classes to account for when an Observable is passed to an executor along with a circuit that already has a measurement on a qubit effected by the Observable.

Also, I removed a check in the Executor class, with the assumption the user is passing a valid executor if an observable is used.

Furthermore, the docs for the Executor and Observable have been updated with the annotation/typehinting requirement.

Finally, additional unit testing has been added to ensure that incompatible Executors and Observables are caught and an appropriate message is given to the user.


License

  • I license this contribution under the terms of the GNU GPL, version 3 and grant Unitary Fund the right to provide additional permissions as described in section 7 of the GNU GPL, version 3.

Before opening the PR, please ensure you have completed the following where appropriate.

@bdg221 bdg221 self-assigned this Sep 25, 2024
@bdg221
Copy link
Collaborator Author

bdg221 commented Sep 25, 2024

I believe there are additional unit tests that need to be created to ensure that we cover as many scenarios as possible. This is especially true when we assume the user has defined a compatible executor when passing an observable.

I plan to discuss this in more detail with @natestemen to ensure appropriate test coverage.

Copy link

codecov bot commented Sep 26, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.73%. Comparing base (6f912f6) to head (8a68dfe).
Report is 15 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2514      +/-   ##
==========================================
+ Coverage   98.71%   98.73%   +0.01%     
==========================================
  Files          89       92       +3     
  Lines        4131     4185      +54     
==========================================
+ Hits         4078     4132      +54     
  Misses         53       53              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@bdg221
Copy link
Collaborator Author

bdg221 commented Sep 27, 2024

@natestemen @cosenal I ran into a bit of an issue and I was curious how you think I should proceed. I added logic into the executor that will check the scenario where the executor does not have a return type specified and an observable is not passed. In this scenario we assume that the return type is float, therefore I am checking the circuits to make sure that there is a measurement included, if not I check if the return type was passed in (error messaging telling the user to include measurements in their circuit since they meant for it to return a float) or not (error message saying to use type hinting and if meant to return a float then add measurements.)

I believe this is what we want, however I found that we have over 200 tests that fail when the code is implemented. The executors in the tests that fail do not use type hinting and also do not include measurement gates (since typically a simple float is return like the length of the circuit or a random number.)

I can certainly go through and clean those up, however I am wondering if I should break that out to a new issue/PR because it is a bit involved.

We could also let the error come from the backend, however those messages may vary. We also cannot mention using type hinting with this option. Examples of some error messages from cirq and qiskit:
Circuit has no measurements to sample.
No counts for experiment

@bdg221 bdg221 requested a review from cosenal September 27, 2024 22:22
mitiq/executor/executor.py Outdated Show resolved Hide resolved
@bdg221
Copy link
Collaborator Author

bdg221 commented Oct 4, 2024

@natestemen @cosenal With this latest update, I am only checking now for obvious incompatibility. This means that if a user does use type hinting and does/does not include an observable it will throw errors if the following scenarios are not met:

FloatLike should NOT have an observable
DensityMatrixLike MUST have an observable
MeasurementResultLike MUST have an observable

When the return type is MeasurementResultLike, we use the observable to add measurement gates to the circuit before call self.run(). Previously, if the return type was not specified this would result in an error from the backend because the circuit would not have measurements. However, now I added a test if self.run() fails to see if there is an observable and if so to add the measurement(s) to the circuit(s) and try self.run() again.

After calling self.run() successfully, we normally parse the results. FloatLike grabs np.real_if_close(), DensityMatrixLike uses the observable._expectation_from_density_matrix(), and MeasurementResultLike uses the observable._expectation_from_measurements(). While previously the code was looking self._executor_return_type, I am also now using the return type of the first object in the sequence returned by self.run().

I added tests that check the obviously bad scenarios when type hinting is used. Also, I compare the results of typed versus non-typed to make that if we assume the user does things correctly without type hinting that they get the correct answer.

Copy link
Member

@natestemen natestemen left a comment

Choose a reason for hiding this comment

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

Thanks for getting started with this Brian!

Apologies on the long delay for the review. In general since the Executor is so core to a lot of things in Mitiq, I'm hesitant to make changes when we don't understand the side effects thoroughly. Hopefully we can mitigate some of those worries through good tests and some back and forth :)

Comment on lines +43 to +44
np.float32,
np.float64,
Copy link
Member

Choose a reason for hiding this comment

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

Were these needed when adding further tests?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I found the need to add these when running make test. Here is the result without those in FloatLike:
image
And with those 2 lines, I don't see the errors:
image

@natestemen do you see this behavior too?

Copy link
Collaborator Author

@bdg221 bdg221 Oct 30, 2024

Choose a reason for hiding this comment

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

I remembered why these two are in FloatLike. Since we are trying to assume things if a return type is not specified, we check the type of what is returned from self.run(). If we do not include them, then the ifs during Parse the results. will fail, especially if we no longer look at self._executor_return_type as mentioned #2514 (comment)

mitiq/observable/pauli.py Outdated Show resolved Hide resolved
mitiq/observable/pauli.py Outdated Show resolved Hide resolved
mitiq/executor/tests/test_executor.py Outdated Show resolved Hide resolved
Comment on lines -237 to -244
def test_executor_observable_compatibility_check(execute, obs):
q = cirq.LineQubit(0)
circuits = [cirq.Circuit(cirq.X(q)), cirq.Circuit(cirq.H(q), cirq.Z(q))]

executor = Executor(execute)

with pytest.raises(ValueError, match="are not compatible"):
executor.evaluate(circuits, obs)
Copy link
Member

Choose a reason for hiding this comment

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

Does this test need to be removed? I think I'm missing something because I don't quite see why these are not compatible.

Copy link
Collaborator Author

@bdg221 bdg221 Oct 30, 2024

Choose a reason for hiding this comment

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

All of those executors return floats or list[floats]. We do not allow you to have an observables with an executor that is returning FloatLike. However, this was an old test from years ago. This is now covered in def test_executor_float_with_observable_typed().

Comment on lines +163 to +175
elif observable is None:
# Type hinted as DensityMatrixLik but no observable is set
if self._executor_return_type in DensityMatrixLike:
raise ValueError(
"When using a density matrix like result, an observable "
"is required."
)
# Type hinted as MeasurementResulteLike but no observable is set
elif self._executor_return_type in MeasurementResultLike:
raise ValueError(
"When using a measurement, or bitstring, like result, an "
"observable is required."
)
Copy link
Member

Choose a reason for hiding this comment

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

These checks make it impossible to use an Executor to run and return density matrices and counts. I don't think we want to limit users in that way, do we?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It does make it impossible to use an Executor to run and return density matrices and counts if an observable is not provided. This limitation actually comes from parsing the results where observable is used by density matrices _expectation_from_density_matrix() and counts _expectation_from_measurements().

all_results = self.run(all_circuits, force_run_all, **kwargs)
try:
all_results = self.run(all_circuits, force_run_all, **kwargs)
except Exception:
Copy link
Member

Choose a reason for hiding this comment

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

Catch specific exceptions when possible. What fails in the above?

Copy link
Collaborator Author

@bdg221 bdg221 Oct 29, 2024

Choose a reason for hiding this comment

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

This exception is coming from the backend which makes it tough to be more specific. This is being used to capture the scenario of a MeasurementResultLike expected return type, but without one being specified. In that situation, an observable should be passed in but we didn't go through the if at line 178 to add the measurements to the circuit(s). Currently, the different backends would throw an error like:

Qiskit: QiskitError: 'No counts for experiment "0"'
Cirq: ValueError: Circuit has no measurements to sample.

The cost of re-running an experiment may not be worth it for this scenario. Of all the times the executors fail, how frequently do we with think this is the scenario and how costly is it to re-run in those other scenarios?

]
all_results = self.run(all_circuits, force_run_all, **kwargs)
else:
raise
Copy link
Member

Choose a reason for hiding this comment

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

What happens here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is checking if the observable is not none and trying to see if the result type was meant to be MeasurementResultLike. If the return type was specified then the measurements would have been added to the circuit(s).

Comment on lines 209 to 211
manual_return_type = None
if len(all_results) > 0:
manual_return_type = type(all_results[0])
Copy link
Member

Choose a reason for hiding this comment

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

Should we even care about the _executor_return_type at this point?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If an executor returns something different than what is specified there will be other errors thrown. You are right, at this point we can stick with the manual_return_type.

Copy link
Collaborator Author

@bdg221 bdg221 Oct 30, 2024

Choose a reason for hiding this comment

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

Fixed in 3c7b027

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just another note on this. The type call will only return the high level object type like list without the element types. FloatLike, MeasurementResultLike, and DensityMatrixLike are expecting List[float] for example since this was previously coming from the _executor_return_type.

I handle this with 7c4a60f by checking for if the result falls under a Sequence or Iterable and if so, I save the first element type to the manual_return_type.

Copy link
Collaborator Author

@bdg221 bdg221 Oct 30, 2024

Choose a reason for hiding this comment

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

Should I add more unit testing since I just introduced this if branching?

EDIT: Too late. I add test and updated the check to see if the Sequence is empty in 8a68dfe

Comment on lines 192 to +206
# Run all required circuits.
all_results = self.run(all_circuits, force_run_all, **kwargs)
try:
all_results = self.run(all_circuits, force_run_all, **kwargs)
except Exception:
if observable is not None and self._executor_return_type is None:
all_circuits = [
circuit_with_measurements
for circuit in circuits
for circuit_with_measurements in observable.measure_in(
circuit
)
]
all_results = self.run(all_circuits, force_run_all, **kwargs)
else:
raise
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the idea of just executing one of the circuits and checking the return type. Does this mean we can move all the compatibility checks in lines 156-175 after this manual checking of return type? This would remove the need of _executor_return_type variable and no typehinting is needed anymore. Maybe I am missing something.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A couple of things.

  1. This is NOT checking the return type of a single circuit, but instead this is running all circuits with the executor.

  2. The checks in lines 156-175 are specifically checking the following cases based off limitations already in the execute function.

  • FloatLike - expected the circuits to already include measurements and there should NOT be an observable. The executor does not handle observables for FloatLike results.
  • DensityMatrixLike and MeasurementLike - MeasurementResultLike handles observables in lines 182-187 and both respectively use OBSERVABLE._expectation_from_density_matrix() and OBSERVABLE._expectation_from_measurements()

Since these are requirements, I would think it would be better to check and break out in bad scenarios, instead of trying to run the circuits and then error out.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yes, you are right that it is running all circuits! I guess I was looking at lines 209-216 where you use the manual return type (of the first circuit?) to parse results. I was thinking to just run the first circuit to get the manual return type and do all the checks on observable compatibility afterwards. This is a completely different approach than using _executor_return_type, so understandable if we don't want to that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The main use of _executor_return_type (outside of what I have done in this PR) is to check if it is MeasurementResultLike and there is an observable so that observable.measure_in() can be called to add measurements into the circuit. Without doing this, the backends complain. I can't think of a way to do this without the _executor_return_type since calling run() to try to get back a manual return type throws errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Executors fail to recognize observable when function is not typehint
3 participants