diff --git a/CHANGELOG.md b/CHANGELOG.md index eb7ce182f..44b98c17a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ### Improvements 🛠 +* Measurement operations are now added to the PennyLane template when a `QuantumCircuit` + is converted using `load`. Additionally, one can override any existing terminal + measurements by providing a list of PennyLane + `measurements `_ themselves. + [(#405)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/405) + ### Breaking changes 💔 ### Deprecations 👋 @@ -16,6 +22,8 @@ This release contains contributions from (in alphabetical order): +Utkarsh Azad + --- # Release 0.34.0 diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 2149ac2ed..5058b8278 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -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 @@ -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]: @@ -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 @@ -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." ) @@ -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. Args: quantum_circuit (qiskit.QuantumCircuit): the QuantumCircuit to be converted + measurements (list[pennylane.measurements.MeasurementProcess]): the list of PennyLane + `measurements `_ + 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 @@ -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__ @@ -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): + 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) + 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 diff --git a/tests/test_converter.py b/tests/test_converter.py index b568efc54..a62ed6573 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -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 @@ -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) @@ -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." ) @@ -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 == [] @@ -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 == [] @@ -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): + """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))