diff --git a/mitiq/executor/executor.py b/mitiq/executor/executor.py index 3dcef180ef..c34f04c910 100644 --- a/mitiq/executor/executor.py +++ b/mitiq/executor/executor.py @@ -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 a float like result, measurements should be " + "included manually and an observable should not be " + "used." + ) + else: + # Using an observable but no type hinting + raise ValueError( + "When using an observable, the return type must be " + "specified on the user defined executor." + ) + 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 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." + ) + # Get all required circuits to run. if ( observable is not None @@ -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 @@ -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 diff --git a/mitiq/executor/tests/test_executor.py b/mitiq/executor/tests/test_executor.py index f38006d03e..effc75bf51 100644 --- a/mitiq/executor/tests/test_executor.py +++ b/mitiq/executor/tests/test_executor.py @@ -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 @@ -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)) @@ -58,21 +59,29 @@ 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 test_executor_simple(): @@ -86,7 +95,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) @@ -96,7 +105,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) @@ -199,12 +208,14 @@ 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 @@ -212,40 +223,11 @@ def test_executor_evaluate_float(execute): assert executor.calls_to_executor == 1 assert executor.executed_circuits == circuits - assert executor.quantum_results == [1, 2] + assert executor.quantum_results == [2, 3] @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) - - -@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")) @@ -258,24 +240,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")) @@ -288,16 +270,82 @@ 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 a float like result", + ): + 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 like 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)