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

Support for measurements operations #405

Merged
merged 12 commits into from
Feb 9, 2024
55 changes: 44 additions & 11 deletions pennylane_qiskit/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@

import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter, ParameterExpression
from qiskit.circuit import Parameter, ParameterExpression, Measure, Barrier
from qiskit.circuit.library import GlobalPhaseGate
from qiskit.exceptions import QiskitError
from sympy import lambdify

Expand All @@ -45,7 +46,7 @@ def _check_parameter_bound(param: Parameter, var_ref_map: Dict[Parameter, Any]):
a dictionary mapping qiskit parameters to trainable parameter values
"""
if isinstance(param, Parameter) and param not in var_ref_map:
raise ValueError("The parameter {} was not bound correctly.".format(param))
raise ValueError(f"The parameter {param} was not bound correctly.")


def _extract_variable_refs(params: Dict[Parameter, Any]) -> Dict[Parameter, Any]:
Expand Down Expand Up @@ -87,9 +88,7 @@ def _check_circuit_and_bind_parameters(
QuantumCircuit: quantum circuit with bound parameters
"""
if not isinstance(quantum_circuit, QuantumCircuit):
raise ValueError(
"The circuit {} is not a valid Qiskit QuantumCircuit.".format(quantum_circuit)
)
raise ValueError(f"The circuit {quantum_circuit} is not a valid Qiskit QuantumCircuit.")

if params is None:
return quantum_circuit
Expand Down Expand Up @@ -120,8 +119,8 @@ def map_wires(qc_wires: list, wires: list) -> dict:
return dict(zip(qc_wires, wires))

raise qml.QuantumFunctionError(
"The specified number of wires - {} - does not match "
"the number of wires the loaded quantum circuit acts on.".format(len(wires))
f"The specified number of wires - {len(wires)} - does not match "
"the number of wires the loaded quantum circuit acts on."
)


Expand All @@ -143,18 +142,22 @@ def execute_supported_operation(operation_name: str, parameters: list, wires: li
operation(*parameters, wires=wires)


def load(quantum_circuit: QuantumCircuit):
def load(quantum_circuit: QuantumCircuit, measurements=None):
"""Loads a PennyLane template from a Qiskit QuantumCircuit.
Warnings are created for each of the QuantumCircuit instructions that were
not incorporated in the PennyLane template.
obliviateandsurrender marked this conversation as resolved.
Show resolved Hide resolved

Args:
quantum_circuit (qiskit.QuantumCircuit): the QuantumCircuit to be converted
measurements (list[pennylane.measurements.MeasurementProcess]): the list of PennyLane
`measurements <https://docs.pennylane.ai/en/stable/introduction/measurements.html>`_
that overrides the terminal measurements that may be present in the input circuit.

Returns:
function: the resulting PennyLane template
"""

# pylint:disable=fixme, too-many-branches
def _function(params: dict = None, wires: list = None):
"""Returns a PennyLane template created based on the input QuantumCircuit.
Warnings are created for each of the QuantumCircuit instructions that were
Expand All @@ -177,8 +180,11 @@ def _function(params: dict = None, wires: list = None):

wire_map = map_wires(qc_wires, wires)

# Stores the measurements encountered in the circuit
mid_circ_meas, terminal_meas = [], []

# Processing the dictionary of parameters passed
for op, qargs, cargs in qc.data:
for idx, (op, qargs, _) in enumerate(qc.data):
# the new Singleton classes have different names than the objects they represent, but base_class.__name__ still matches
instruction_name = getattr(op, "base_class", op.__class__).__name__

Expand Down Expand Up @@ -224,16 +230,43 @@ def _function(params: dict = None, wires: list = None):
gate = dagger_map[instruction_name]
qml.adjoint(gate)(wires=operation_wires)

elif isinstance(op, Measure):
# Store the current operation wires
op_wires = set(operation_wires)
# Look-ahead for more gate(s) on its wire(s)
meas_terminal = True
for next_op, next_qargs, __ in qc.data[idx + 1 :]:
# Check if the subsequent whether next_op is measurement interfering
if not isinstance(next_op, (Barrier, GlobalPhaseGate)):
next_op_wires = set(wire_map[hash(qubit)] for qubit in next_qargs)
# Check if there's any overlapping wires
if next_op_wires.intersection(op_wires):
obliviateandsurrender marked this conversation as resolved.
Show resolved Hide resolved
meas_terminal = False
break

# Allows for queing the mid-circuit measurements
if not meas_terminal:
mid_circ_meas.append(qml.measure(wires=operation_wires))
else:
terminal_meas.extend(operation_wires)

obliviateandsurrender marked this conversation as resolved.
Show resolved Hide resolved
else:
try:
operation_matrix = op.to_matrix()
pennylane_ops.QubitUnitary(operation_matrix, wires=operation_wires)
except (AttributeError, QiskitError):
warnings.warn(
__name__ + ": The {} instruction is not supported by PennyLane,"
" and has not been added to the template.".format(instruction_name),
f"{__name__}: The {instruction_name} instruction is not supported by PennyLane,"
" and has not been added to the template.",
UserWarning,
)
# Use the user-provided measurements
if measurements:
if qml.queuing.QueuingManager.active_context():
return [qml.apply(meas) for meas in measurements]
return measurements

return tuple(mid_circ_meas + list(map(qml.measure, terminal_meas))) or None

return _function

Expand Down
93 changes: 81 additions & 12 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
from qiskit import extensions as ex
from qiskit.circuit import Parameter
from qiskit.circuit.library import EfficientSU2
from qiskit.exceptions import QiskitError
from qiskit.quantum_info.operators import Operator

Expand Down Expand Up @@ -771,10 +772,9 @@ def test_map_wires_exception_mismatch_in_number_of_wires(self, recorder):
class TestConverterWarnings:
"""Tests that the converter.load function emits warnings."""

def test_barrier_not_supported(self, recorder):
def test_template_not_supported(self, recorder):
"""Tests that a warning is raised if an unsupported instruction was reached."""
qc = QuantumCircuit(3, 1)
qc.barrier()
qc = EfficientSU2(3, reps=1)

quantum_circuit = load(qc)

Expand All @@ -785,7 +785,7 @@ def test_barrier_not_supported(self, recorder):
# check that the message matches
assert (
record[-1].message.args[0]
== "pennylane_qiskit.converter: The Barrier instruction is not supported by"
== "pennylane_qiskit.converter: The Gate instruction is not supported by"
" PennyLane, and has not been added to the template."
)

Expand Down Expand Up @@ -818,11 +818,10 @@ def test_qasm_from_file(self, tmpdir, recorder):

quantum_circuit = load_qasm_from_file(qft_qasm)

with pytest.warns(UserWarning) as record:
with recorder:
quantum_circuit()
with recorder:
quantum_circuit()

assert len(recorder.queue) == 6
assert len(recorder.queue) == 10

assert recorder.queue[0].name == "PauliX"
assert recorder.queue[0].parameters == []
Expand Down Expand Up @@ -868,11 +867,10 @@ def test_qasm_(self, recorder):

quantum_circuit = load_qasm(qasm_string)

with pytest.warns(UserWarning) as record:
with recorder:
quantum_circuit(params={})
with recorder:
quantum_circuit(params={})

assert len(recorder.queue) == 2
assert len(recorder.queue) == 6

assert recorder.queue[0].name == "PauliX"
assert recorder.queue[0].parameters == []
Expand Down Expand Up @@ -1110,3 +1108,74 @@ def cost(x, y):
]

assert np.allclose(jac, jac_expected)

def test_meas_circuit_in_qnode(self, qubit_device_2_wires):
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
"""Tests loading a converted template in a QNode with measurements."""

angle = 0.543

qc = QuantumCircuit(2, 2)
qc.h(0)
qc.measure(0, 0)
qc.rz(angle, [0])
qc.cx(0, 1)
qc.measure_all()

measurements = [qml.expval(qml.PauliZ(0)), qml.vn_entropy([1])]
quantum_circuit = load(qc, measurements=measurements)

@qml.qnode(qubit_device_2_wires)
def circuit_loaded_qiskit_circuit():
return quantum_circuit()

@qml.qnode(qubit_device_2_wires)
def circuit_native_pennylane():
qml.Hadamard(0)
qml.measure(0)
qml.RZ(angle, wires=0)
qml.CNOT([0, 1])
return [qml.expval(qml.PauliZ(0)), qml.vn_entropy([1])]

assert circuit_loaded_qiskit_circuit() == circuit_native_pennylane()

quantum_circuit = load(qc, measurements=None)

@qml.qnode(qubit_device_2_wires)
def circuit_loaded_qiskit_circuit2():
meas = quantum_circuit()
return [qml.expval(m) for m in meas]

@qml.qnode(qubit_device_2_wires)
def circuit_native_pennylane2():
qml.Hadamard(0)
m0 = qml.measure(0)
qml.RZ(angle, wires=0)
qml.CNOT([0, 1])
return [qml.expval(m) for m in [m0, qml.measure(0), qml.measure(1)]]

assert circuit_loaded_qiskit_circuit2() == circuit_native_pennylane2()

def test_diff_meas_circuit(self):
"""Tests mid-measurements are recognized and returned correctly."""

angle = 0.543

qc = QuantumCircuit(3, 3)
qc.h(0)
qc.measure(0, 0)
qc.rx(angle, [0])
qc.cx(0, 1)
qc.measure(1, 1)

qc1 = QuantumCircuit(3, 3)
qc1.h(0)
qc1.measure(2, 2)
qc1.rx(angle, [0])
qc1.cx(0, 1)
qc1.measure(1, 1)

qtemp, qtemp1 = load(qc), load(qc1)
assert qtemp()[0] == qml.measure(0) and qtemp1()[0] == qml.measure(2)

qtemp2 = load(qc, measurements=[qml.expval(qml.PauliZ(0))])
assert qtemp()[0] != qtemp2()[0] and qtemp2()[0] == qml.expval(qml.PauliZ(0))
Loading