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

Merged
merged 30 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a6e09f6
Remove check for observer return type
bdg221 Sep 24, 2024
1a42969
Add error for more than one measurement per qubit
bdg221 Sep 25, 2024
aa2597d
Remove commented line
bdg221 Sep 25, 2024
a5ec9d0
Add test for multiple measurements on a qubit
bdg221 Sep 25, 2024
a08a87f
Update executor and observable documentation
bdg221 Sep 25, 2024
edcf03d
Handle correct case without measurements
bdg221 Sep 26, 2024
4225339
Add back line that broke density matrices
bdg221 Sep 26, 2024
6a6fa13
Remove test for condition that was removed
bdg221 Sep 26, 2024
1d447cf
Remove no longer required test parameters
bdg221 Sep 26, 2024
163b045
executor_observable compatability with typehint and tests
bdg221 Sep 27, 2024
2def914
Add logic to check executor observable compat and tests
bdg221 Sep 27, 2024
7c76316
retry failed attempt with measurement and check returned type
bdg221 Oct 4, 2024
72c0168
add test to compare typed vs nontyped
bdg221 Oct 4, 2024
dc32389
Add numpy float64 to FloatLike
bdg221 Oct 4, 2024
1333ebf
remove uncessary assert
bdg221 Oct 29, 2024
99c7374
update f-string formatting
bdg221 Oct 29, 2024
bbd1298
Add back executor call in float test
bdg221 Oct 30, 2024
02bd3f5
Update finding and checking existing qubit with measurements
bdg221 Oct 30, 2024
3c7b027
Parse results using manual return type
bdg221 Oct 30, 2024
7c4a60f
Check type inside Sequence and Iterators
bdg221 Oct 30, 2024
a0e4cda
Add a second qubit to multi measurement test
bdg221 Oct 30, 2024
8a68dfe
Update Sequence check and add tests
bdg221 Nov 4, 2024
72fcc27
Merge branch 'main' into 2449-REM-typehinting
bdg221 Dec 6, 2024
71a4464
Merge branch 'main' into 2449-REM-typehinting
natestemen Dec 10, 2024
e3d8e2a
correct spelling
bdg221 Dec 12, 2024
802e743
remove manual return type check and error clear error message
bdg221 Dec 13, 2024
b78e824
Merge branch '2449-REM-typehinting' of https://github.com/bdg221/miti…
bdg221 Dec 13, 2024
19934a9
Implemented suggestions with formatting
bdg221 Dec 17, 2024
97c8d27
Update tests with new wording
bdg221 Dec 17, 2024
f8120b3
Add test for unexpected typehint
bdg221 Dec 17, 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
44 changes: 32 additions & 12 deletions mitiq/executor/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,36 @@ def evaluate(
"Expected observable to be hermitian. Continue with caution."
)

# Check executor and observable compatability with type hinting
# If FloatLike is specified as a return and observable is used
if self._executor_return_type in FloatLike and observable is not None:
# Type hinted as FloatLike and observable passed
if self._executor_return_type is not None:
raise ValueError(
"When using an executor which returns a float-like "
"result, measurements should be added before the circuit "
"is executed instead of with an observable."
)
else:
# Using an observable but no type hinting
raise ValueError(
"When using an observable, the return type of the "
"executor must be specified using typehinting."
)
elif observable is None:
# Type hinted as DensityMatrixLike but no observable is set
if self._executor_return_type in DensityMatrixLike:
raise ValueError(
"When using a density matrix 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."
)

# Get all required circuits to run.
if (
observable is not None
Expand All @@ -158,16 +188,6 @@ def evaluate(
for circuit_with_measurements in observable.measure_in(circuit)
]
result_step = observable.ngroups
elif (
observable is not None
and self._executor_return_type not in MeasurementResultLike
and self._executor_return_type not in DensityMatrixLike
):
raise ValueError(
"""Executor and observable are not compatible. Executors
returning expectation values as float must be used with
observable=None"""
)
else:
all_circuits = circuits
result_step = 1
Expand Down Expand Up @@ -201,8 +221,8 @@ def evaluate(

else:
raise ValueError(
f"Could not parse executed results from executor with type"
f" {self._executor_return_type}."
f"Could not parse executed results from executor with type "
f"{self._executor_return_type}."
)

return results
Expand Down
160 changes: 112 additions & 48 deletions mitiq/executor/tests/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import numpy as np
import pyquil
import pytest
from qiskit import QuantumCircuit

from mitiq import MeasurementResult
from mitiq.executor.executor import Executor
Expand All @@ -37,7 +38,7 @@ def executor_batched_unique(circuits) -> List[float]:
return [executor_serial_unique(circuit) for circuit in circuits]


def executor_serial_unique(circuit):
def executor_serial_unique(circuit) -> float:
return float(len(circuit))


Expand All @@ -58,21 +59,33 @@ def executor_pyquil_batched(programs) -> List[float]:


# Serial / batched executors which return measurements.
def executor_measurements(circuit) -> MeasurementResult:
def executor_measurements(circuit):
return sample_bitstrings(circuit, noise_level=(0,))


def executor_measurements_typed(circuit) -> MeasurementResult:
return sample_bitstrings(circuit, noise_level=(0,))


def executor_measurements_batched(circuits) -> List[MeasurementResult]:
return [executor_measurements(circuit) for circuit in circuits]
return [executor_measurements_typed(circuit) for circuit in circuits]


# Serial / batched executors which return density matrices.
def executor_density_matrix(circuit) -> np.ndarray:
def executor_density_matrix(circuit):
return compute_density_matrix(circuit, noise_level=(0,))


def executor_density_matrix_typed(circuit) -> np.ndarray:
return compute_density_matrix(circuit, noise_level=(0,))


def executor_density_matrix_batched(circuits) -> List[np.ndarray]:
return [executor_density_matrix(circuit) for circuit in circuits]
return [executor_density_matrix_typed(circuit) for circuit in circuits]


def executor_typed_npfloat32(circuit) -> np.float32:
return np.float32(3.14)


def test_executor_simple():
Expand All @@ -86,7 +99,7 @@ def test_executor_is_batched_executor():
assert Executor.is_batched_executor(executor_batched)
assert not Executor.is_batched_executor(executor_serial_typed)
assert not Executor.is_batched_executor(executor_serial)
assert not Executor.is_batched_executor(executor_measurements)
assert not Executor.is_batched_executor(executor_measurements_typed)
assert Executor.is_batched_executor(executor_measurements_batched)


Expand All @@ -96,7 +109,7 @@ def test_executor_non_hermitian_observable():
q = cirq.LineQubit(0)
circuits = [cirq.Circuit(cirq.I.on(q)), cirq.Circuit(cirq.X.on(q))]

executor = Executor(executor_measurements)
executor = Executor(executor_measurements_typed)

with pytest.warns(UserWarning, match="hermitian"):
executor.evaluate(circuits, obs)
Expand Down Expand Up @@ -199,53 +212,26 @@ def test_run_executor_preserves_order(s, b):
)
def test_executor_evaluate_float(execute):
q = cirq.LineQubit(0)
circuits = [cirq.Circuit(cirq.X(q)), cirq.Circuit(cirq.H(q), cirq.Z(q))]
circuits = [
cirq.Circuit(cirq.X(q), cirq.M(q)),
cirq.Circuit(cirq.H(q), cirq.Z(q), cirq.M(q)),
]

executor = Executor(execute)

results = executor.evaluate(circuits)
assert np.allclose(results, [1, 2])
executor.evaluate(circuits)

if execute is executor_serial_unique:
assert executor.calls_to_executor == 2
else:
assert executor.calls_to_executor == 1

assert executor.executed_circuits == circuits
assert executor.quantum_results == [1, 2]


@pytest.mark.parametrize(
"execute",
[
executor_batched,
executor_batched_unique,
executor_serial_unique,
executor_serial_typed,
executor_serial,
executor_pyquil_batched,
],
)
@pytest.mark.parametrize(
"obs",
[
PauliString("X"),
PauliString("XZ"),
PauliString("Z"),
],
)
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)
bdg221 marked this conversation as resolved.
Show resolved Hide resolved
assert executor.quantum_results == [2, 3]


@pytest.mark.parametrize(
"execute", [executor_measurements, executor_measurements_batched]
"execute", [executor_measurements_typed, executor_measurements_batched]
)
def test_executor_evaluate_measurements(execute):
obs = Observable(PauliString("Z"))
Expand All @@ -258,24 +244,24 @@ def test_executor_evaluate_measurements(execute):
results = executor.evaluate(circuits, obs)
assert np.allclose(results, [1, -1])

if execute is executor_measurements:
if execute is executor_measurements_typed:
assert executor.calls_to_executor == 2
else:
assert executor.calls_to_executor == 1

assert executor.executed_circuits[0] == circuits[0] + cirq.measure(q)
assert executor.executed_circuits[1] == circuits[1] + cirq.measure(q)
assert executor.quantum_results[0] == executor_measurements(
assert executor.quantum_results[0] == executor_measurements_typed(
circuits[0] + cirq.measure(q)
)
assert executor.quantum_results[1] == executor_measurements(
assert executor.quantum_results[1] == executor_measurements_typed(
circuits[1] + cirq.measure(q)
)
assert len(executor.quantum_results) == len(circuits)


@pytest.mark.parametrize(
"execute", [executor_density_matrix, executor_density_matrix_batched]
"execute", [executor_density_matrix_typed, executor_density_matrix_batched]
)
def test_executor_evaluate_density_matrix(execute):
obs = Observable(PauliString("Z"))
Expand All @@ -288,16 +274,94 @@ def test_executor_evaluate_density_matrix(execute):
results = executor.evaluate(circuits, obs)
assert np.allclose(results, [1, -1])

if execute is executor_density_matrix:
if execute is executor_density_matrix_typed:
assert executor.calls_to_executor == 2
else:
assert executor.calls_to_executor == 1

assert executor.executed_circuits == circuits
assert np.allclose(
executor.quantum_results[0], executor_density_matrix(circuits[0])
executor.quantum_results[0], executor_density_matrix_typed(circuits[0])
)
assert np.allclose(
executor.quantum_results[1], executor_density_matrix(circuits[1])
executor.quantum_results[1], executor_density_matrix_typed(circuits[1])
)
assert len(executor.quantum_results) == len(circuits)


def test_executor_float_with_observable_typed():
obs = Observable(PauliString("Z"))
q = cirq.LineQubit(0)
circuit = cirq.Circuit(cirq.X.on(q))
executor = Executor(executor_serial_typed)
with pytest.raises(
ValueError,
match="When using an executor which returns a float-like ",
):
executor.evaluate(circuit, obs)


def test_executor_measurements_without_observable_typed():
q = cirq.LineQubit(0)
circuit = cirq.Circuit(cirq.X.on(q))
executor = Executor(executor_measurements_typed)
with pytest.raises(
ValueError,
match="When using a measurement, or bitstring, like result",
):
executor.evaluate(circuit)


def test_executor_density_matrix_without_observable_typed():
q = cirq.LineQubit(0)
circuit = cirq.Circuit(cirq.X.on(q))
executor = Executor(executor_density_matrix_typed)
with pytest.raises(
ValueError,
match="When using a density matrix result",
):
executor.evaluate(circuit)


def test_executor_float_not_typed():
executor = Executor(executor_serial)
executor_typed = Executor(executor_serial_typed)
qcirc = QuantumCircuit(1)
qcirc.h(0)
assert executor.evaluate(qcirc) == executor_typed.evaluate(qcirc)


def test_executor_density_matrix_not_typed():
obs = Observable(PauliString("Z"))
executor = Executor(executor_density_matrix)
q = cirq.LineQubit(0)
circuit = cirq.Circuit(cirq.X.on(q))
with pytest.raises(
ValueError,
match="When using an observable",
):
executor.evaluate(circuit, obs)


def test_executor_measurements_not_typed():
obs = Observable(PauliString("Z"))
executor = Executor(executor_measurements)
q = cirq.LineQubit(0)
circuit = cirq.Circuit(cirq.X.on(q))
with pytest.raises(
ValueError,
match="When using an observable",
):
executor.evaluate(circuit, obs)


def test_executor_unknown_type():
obs = Observable(PauliString("Z"))
executor = Executor(executor_typed_npfloat32)
q = cirq.LineQubit(0)
circuit = cirq.Circuit(cirq.X.on(q))
with pytest.raises(
ValueError,
match="Could not parse executed results",
):
executor.evaluate(circuit, obs)
Loading