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 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
4 changes: 3 additions & 1 deletion docs/source/guide/executors.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ To instantiate an `Executor`, provide a function which either:
1. Inputs a `mitiq.QPROGRAM` and outputs a `mitiq.QuantumResult`.
2. Inputs a sequence of `mitiq.QPROGRAM`s and outputs a sequence of `mitiq.QuantumResult`s.

**The function must be [annotated](https://peps.python.org/pep-3107/) to tell Mitiq which type of `QuantumResult` it returns. Functions with no annotations are assumed to return `float`s.**
```{warning}
To avoid confusion and invalid results, the executor function must be [annotated](https://peps.python.org/pep-3107/) to tell Mitiq which type of `QuantumResult` it returns. Functions without annotations are assumed to return `float`s.
```

A `QPROGRAM` is "something which a quantum computer inputs" and a `QuantumResult` is "something which a quantum computer outputs." The latter is canonically a bitstring for real quantum hardware, but can be other objects for testing, e.g. a density matrix.

Expand Down
4 changes: 2 additions & 2 deletions docs/source/guide/observables.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ obs.expectation(circuit, execute=mitiq_cirq.sample_bitstrings)

In error mitigation techniques, you can provide an observable to specify the expectation value to mitigate.

```{admonition} Note:
When specifying an `Observable`, you must ensure that the return type of the executor function is `MeasurementResultLike` or `DensityMatrixLike`.
```{warning}
As note in the [executor documentation](./executors.md#the-input-function), the executor must be annotated with the appropriate type hinting for the return type. Additionally, when specifying an `Observable`, you must ensure that the return type of the executor function is `MeasurementResultLike` or `DensityMatrixLike`.
```

```{code-cell} ipython3
Expand Down
64 changes: 54 additions & 10 deletions mitiq/executor/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import numpy as np
import numpy.typing as npt
from cirq import MeasurementGate

from mitiq import QPROGRAM, MeasurementResult, QuantumResult
from mitiq.interface import convert_from_mitiq, convert_to_mitiq
Expand Down Expand Up @@ -149,6 +150,59 @@ def evaluate(
"Expected observable to be hermitian. Continue with caution."
)

# Check executor and observable compatability with type hinting
if self._executor_return_type in FloatLike and observable is not None:
if self._executor_return_type is None:
raise ValueError(
"Please use type hinting with the executor. Without a "
"return type defined, it is assumed a float is returned. "
"When returning a float, an observable should not be used."
)
else:
raise ValueError(
"When using a float like result, measurements should be "
"included in the circuit and an observable should not be "
"used."
)
elif observable is None:
if self._executor_return_type in DensityMatrixLike:
raise ValueError(
"When using a density matrix like result, an observable "
"is required."
)
elif self._executor_return_type in MeasurementResultLike:
raise ValueError(
"When using a measurement, or bitstring, like result, an "
"observable is required."
)
else:
for circuit in circuits:
if (
len(
list(
convert_to_mitiq(circuit)[
0
].findall_operations_with_gate_type(
MeasurementGate
)
)
)
== 0
):
if self._executor_return_type is None:
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 is line 192 to 204 that is causing the tests to fail.

raise ValueError(
"Please use type hinting with the executor. "
"Without a return type defined, it is assumed "
"a float is returned. When returning a float, "
"the circuit is expected to include "
"measurements."
)
else:
raise ValueError(
"When using a float like result, measurements "
"should be included in the circuit."
)

# Get all required circuits to run.
if (
observable is not None
Expand All @@ -160,16 +214,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
146 changes: 99 additions & 47 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,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():
Expand All @@ -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)


Expand All @@ -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)
Expand Down Expand Up @@ -199,53 +208,27 @@ 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])
assert np.allclose(results, [2, 3])

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)
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 +241,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 +271,85 @@ 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_no_typehint():
obs = Observable(PauliString("Z"))
q = cirq.LineQubit(0)
circuit = cirq.Circuit(cirq.X.on(q))
executor = Executor(executor_serial)

with pytest.raises(
ValueError,
match="Please use type hinting with",
):
executor.evaluate(circuit, obs)

with pytest.raises(
ValueError,
match="Please use type hinting with the executor",
):
executor.evaluate(circuit)


@pytest.mark.parametrize(
"execute",
[executor_density_matrix, executor_measurements],
)
def test_executor_non_float_no_typehint(execute):
executor = Executor(execute)

qcirc = QuantumCircuit(1)
qcirc.h(0)

with pytest.raises(
ValueError, match="Please use type hinting with the executor"
):
executor.evaluate(qcirc)
19 changes: 19 additions & 0 deletions mitiq/observable/pauli.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,27 @@ def _measure_in(

basis_rotations = set()
support = set()

# Find any existing measurement gates in the circuit
existing_measurements = []
measurement_tuples = list(
circuit.findall_operations_with_gate_type(cirq.MeasurementGate)
)
if measurement_tuples:
existing_measurements = [
measurement_tuple[1].qubits[0]
for measurement_tuple in measurement_tuples
]

for pauli in paulis.elements:
basis_rotations.update(pauli._basis_rotations())
for qubit in pauli._qubits_to_measure():
if qubit in existing_measurements:
raise ValueError(
f"""More than one measaurement found for qubit: """
f"""{qubit}. Only a single measurement is allowed """
f"""per qubit."""
)
support.update(pauli._qubits_to_measure())
measured = circuit + basis_rotations + cirq.measure(*sorted(support))

Expand Down
10 changes: 10 additions & 0 deletions mitiq/observable/tests/test_pauli.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ def test_pauli_measure_in_bad_qubits_error():
pauli.measure_in(circuit)


def test_pauli_measure_in_multi_measurements_per_qubit():
n = 4
pauli = PauliString(spec="Z" * n)
circuit = cirq.Circuit(cirq.H.on_each(cirq.LineQubit.range(n)))
# add a measurement to qubit 0
circuit = circuit + cirq.measure(cirq.LineQubit(0))
with pytest.raises(ValueError, match="More than one measaurement"):
pauli.measure_in(circuit)


def test_can_be_measured_with_single_qubit():
pauli = PauliString(spec="Z")

Expand Down
Loading