Source code for qc_executor.pennylane.pennylane_circuit

"""PennyLane native circuit, compiled from the framework-independent circuit IR."""

from __future__ import annotations

from typing import Any, Callable, Optional, cast

import pennylane as qml
import sympy as sp

from ..base.circuit_base import QuantumCircuitBase
from ..base.circuit_ir import CircuitIR, Instruction
from ..base.expressions import compile_expression
from ..base.gate_set import GATE_DEFS, OPCODE_BY_NAME, OpCode
from ..parameters import sort_parameters
from ._sympy_interface import _get_sympy_interface
from .pennylane_gates import qiskit_pennylane_gate_dict

#: Opcodes PennyLane executes directly, derived from its gate table.  PennyLane
#: covers almost the whole gate set, so the shared lowering pass has little to
#: do; it replaced the ``qiskit.transpile(target=pennylane_target)`` call.
_SUPPORTED = frozenset(
    OPCODE_BY_NAME[name] for name in qiskit_pennylane_gate_dict if name in OPCODE_BY_NAME
) | {OpCode.BARRIER}

#: What the built callable may return besides applying the gates.
_VALID_MEASUREMENTS = (None, "probs", "state")


[docs] class PennyLaneCircuit(QuantumCircuitBase): """A quantum circuit that compiles to PennyLane. Built like any other circuit -- ``PennyLaneCircuit(2)`` then ``circuit.h(0)`` -- or converted from an existing one with :meth:`from_quantum_circuit`. Compilation into PennyLane operations is lazy and re-runs whenever the instruction store changes. Args: num_qubits: Number of qubits in the circuit. num_clbits: Number of classical bits, for mid-circuit measurement. measurement: What the built callable returns besides applying the gates. ``None`` (default) returns nothing, for a caller that attaches its own measurement -- as the executor does when it pairs the circuit with an observable. ``"probs"`` returns ``qml.probs`` over every qubit and ``"state"`` returns ``qml.state()``, which makes the callable a complete QNode body, e.g. for ``qml.metric_tensor``. Circuits derived through ``copy``, ``invert`` or ``assign_parameters`` start again from ``None``. _ir: Adopt this instruction store instead of starting empty. Raises: ValueError: If ``measurement`` is not ``None``, ``"probs"`` or ``"state"``. """
[docs] @classmethod def supported_opcodes(cls) -> frozenset: """Return the opcodes PennyLane executes directly. Without this the base default (the whole gate set) would apply and the lowering pass would silently stop rewriting anything. """ return _SUPPORTED
def __init__( self, num_qubits: int = 0, num_clbits: int = 0, *, measurement: Optional[str] = None, _ir: "CircuitIR | None" = None, ) -> None: if measurement not in _VALID_MEASUREMENTS: raise ValueError( f"Unknown measurement {measurement!r}; expected one of {_VALID_MEASUREMENTS}." ) super().__init__(num_qubits, num_clbits, _ir=_ir) self._measurement = measurement self._pennylane_gates = [] self._pennylane_gates_param_function = [] self._pennylane_gates_wires = [] self._pennylane_conditions = [] self._pennylane_circuit = None self._compiled_revision = -1
[docs] @classmethod def from_quantum_circuit( cls, circuit: QuantumCircuitBase, measurement: Optional[str] = None ) -> "PennyLaneCircuit": """Convert any circuit into a PennyLane circuit. Args: circuit: The circuit to convert. measurement: What the built callable returns; see the class docstring. Returns: ``circuit`` unchanged if it already is a PennyLane circuit with this measurement, else a new one holding a copy of its instructions. """ if isinstance(circuit, cls) and circuit.measurement == measurement: return circuit return cls( circuit.num_qubits, circuit.num_clbits, measurement=measurement, _ir=circuit.ir.copy(), )
# ------------------------------------------------------------------ # Compilation # ------------------------------------------------------------------ def _ensure_compiled(self) -> None: """Compile the instruction store into PennyLane operations, once per revision.""" if self._compiled_revision != self._ir.revision: self._build_circuit_instructions(self._lowered_ir()) self._compiled_revision = self._ir.revision self._pennylane_circuit = None def _build_native(self) -> Callable: """Compile the instruction store into the callable PennyLane circuit.""" return self.build_pennylane_circuit() @property def measurement(self) -> Optional[str]: """What the built callable returns: ``None``, ``"probs"`` or ``"state"``.""" return self._measurement @property def pennylane_circuit(self) -> Optional[Callable]: """PennyLane circuit that can be called with parameters""" self._ensure_compiled() if self._pennylane_circuit is None: self._pennylane_circuit = self.build_pennylane_circuit() return self._pennylane_circuit @property def parameter_names(self) -> list: """List of circuit parameter names""" return list(self.parameter_dimensions) @property def parameter_dimensions(self) -> dict: """Dictionary with the dimension of each circuit parameter. Derived from the instruction store rather than from the compiled operations, so it answers without forcing compilation. Lowering does not change which parameters appear, only which gates carry them. """ dimensions: dict = {} for parameter in self.parameters: dimensions[parameter.vector_name] = dimensions.get(parameter.vector_name, 0) + 1 return dimensions def __call__(self, *args, **kwargs): return self.pennylane_circuit(*args, **kwargs) @staticmethod def _get_gate_condition(instruction: Instruction): """Get the classical condition for an instruction, or None if unconditional. Reads the condition straight off the IR. The previous implementation looked for ``Instruction.condition``, which Qiskit removed in 2.0, so conditional gates had silently stopped being applied. Args: instruction: The instruction to inspect. Returns: ``(bit_indices, value)`` where ``bit_indices`` is an int for a single classical bit and a list otherwise, or ``None``. """ condition = instruction.condition if condition is None: return None bits = list(condition.clbits) return (bits[0] if len(bits) == 1 else bits, condition.value) @staticmethod def _get_gate_param_tuple(instruction: Instruction, symbol_tuple, printer, modules): """Build the parameter function tuple for a gate, or None if it has none. Angles arrive as numbers or SymPy expressions; symbolic ones are lambdified onto PennyLane's autograd-aware numpy. """ if not instruction.params: return None param_tuple: tuple = () for param in instruction.params: if isinstance(param, sp.Basic) and param.free_symbols: param_tuple += ( compile_expression(param, symbol_tuple, modules=modules, printer=printer), ) else: param_tuple += (float(param),) return param_tuple def _build_circuit_instructions(self, ir: CircuitIR) -> None: """Build the PennyLane instruction lists from the circuit IR. Walks the lowered instruction store directly; this used to iterate a transpiled Qiskit circuit and look gates up by their Qiskit name. Args: ir: The lowered instruction store. Raises: NotImplementedError: For gates PennyLane cannot express. """ self._pennylane_gates = [] self._pennylane_gates_param_function = [] self._pennylane_gates_wires = [] self._pennylane_conditions = [] # Same order parameter_dimensions counts in, so the executor's # flattened argument list lines up with the lambdified callables. symbol_tuple = tuple(sort_parameters(ir.free_parameters)) printer, modules = _get_sympy_interface() for instruction in ir: # Barriers are compiler directives with no effect on the statevector. if instruction.opcode is OpCode.BARRIER: continue self._pennylane_conditions.append(self._get_gate_condition(instruction)) self._pennylane_gates_param_function.append( self._get_gate_param_tuple(instruction, symbol_tuple, printer, modules) ) if instruction.opcode is OpCode.MEASURE: # Measurement results are stored in a classical-bit array. self._pennylane_gates.append(("measure", list(instruction.clbits))) self._pennylane_gates_wires.append(list(instruction.qubits)) continue name = GATE_DEFS[instruction.opcode].name if name not in qiskit_pennylane_gate_dict: raise NotImplementedError( f"Gate {name} is unfortunatly not supported " "in sQUlearn's PennyLane backend." ) self._pennylane_gates.append(qiskit_pennylane_gate_dict[name]) self._pennylane_gates_wires.append(list(instruction.qubits)) def _apply_conditional_gate( self, circuit_gate, evaluated_param, condition, measurements, wires ): """Apply a gate that has a classical condition.""" condition_idx, condition_target = condition if isinstance(condition_idx, list): val = sum(2**j * measurements[condition_idx[j]] for j in range(len(condition_idx))) else: val = measurements[condition_idx] if isinstance(val, int): if val == condition_target: if evaluated_param is not None: circuit_gate(*evaluated_param, wires=wires) else: circuit_gate(wires=wires) else: # Cast to Callable so the type checker knows it accepts abitrary arguments cond_fn = cast(Callable[..., Any], qml.cond(val == condition_target, circuit_gate)) if evaluated_param is not None: cond_fn(*evaluated_param, wires=wires) else: cond_fn(wires=wires)
[docs] def build_pennylane_circuit(self): """Return a callable PennyLane circuit built from the instruction store. Compiles the store first if it has changed since the last call. Returns: Callable PennyLane circuit """ self._ensure_compiled() def pennylane_circuit(*args): """PennyLane circuit that can be called with parameters""" measurements: list = [0] * self.num_clbits # Collects the args values connected to the circuit parameters circ_param_list = sum([list(args[i]) for i in range(len(self.parameter_names))], []) # Loop through all penny lane gates for i, circuit_gate in enumerate(self._pennylane_gates): if isinstance(circuit_gate, tuple): # Special case for measurement # add measurement to the circuit and store the result in the measurements array if circuit_gate[0] == "measure": for j, wire in enumerate(self._pennylane_gates_wires[i]): measurements[circuit_gate[1][j]] = qml.measure(wire) else: # Evaluate the (non-linear) parameter expression of the gate evaluated_param = None if self._pennylane_gates_param_function[i] is not None: evaluated_param = tuple( func(*circ_param_list) if callable(func) else func for func in self._pennylane_gates_param_function[i] ) wires = self._pennylane_gates_wires[i] condition = self._pennylane_conditions[i] # Treat c_if conditions of the gate (if present) if condition is not None: self._apply_conditional_gate( circuit_gate, evaluated_param, condition, measurements, wires ) elif evaluated_param is not None: circuit_gate(*evaluated_param, wires=wires) else: circuit_gate(wires=wires) if self._measurement == "probs": return qml.probs(wires=range(self.num_qubits)) if self._measurement == "state": return qml.state() return None return pennylane_circuit