Source code for qtest.backends.pennylane_backend

"""PennyLane-based simulator backend for qtest.

Import-safe without PennyLane: every ``import pennylane`` is deferred to a
method body, so constructing :class:`PennyLaneBackend` (and importing this
module) never requires PennyLane.

Native circuit type
--------------------
Unlike Qiskit/Cirq, PennyLane has no free-standing "circuit" object — programs
are expressed as quantum functions bound to a device. This backend therefore
operates on a :class:`pennylane.tape.QuantumScript` (a.k.a. ``QuantumTape``),
which is the closest device-independent container of operations. Build one as::

    import pennylane as qml
    tape = qml.tape.QuantumScript([qml.Hadamard(0), qml.CNOT([0, 1])])

Measurement bitstrings are ordered with the **lowest-index wire leftmost**.
Existing measurements on the tape are ignored; qtest supplies its own.

Noise
-----
qtest's :class:`~qtest.noise.NoiseModel` targets Qiskit Aer, so passing a
``noise_model`` here raises :class:`NotImplementedError`.
"""

from __future__ import annotations

from typing import Any

import numpy as np

from qtest.backends.base import Backend

_PENNYLANE_MISSING_MSG = (
    "PennyLaneBackend requires PennyLane. Install it with:\n  pip install 'qtest[pennylane]'"
)


def _require_pennylane() -> Any:
    try:
        import pennylane as qml
    except ImportError as exc:  # pragma: no cover - exercised only without pennylane
        raise ImportError(_PENNYLANE_MISSING_MSG) from exc
    return qml


def _extract(circuit: Any) -> tuple[list[Any], list[Any]]:
    """Return ``(operations, wire_order)`` from a QuantumScript / QuantumTape."""
    if circuit is None:
        raise ValueError("circuit must not be None")
    if not hasattr(circuit, "operations") or not hasattr(circuit, "wires"):
        raise ValueError(
            "PennyLaneBackend expects a pennylane.tape.QuantumScript/QuantumTape; "
            f"got {type(circuit).__name__}."
        )
    operations = list(circuit.operations)
    wire_order = sorted(circuit.wires.tolist())
    if not wire_order:
        raise ValueError("circuit has no wires")
    return operations, wire_order


[docs] class PennyLaneBackend(Backend): """PennyLane ``default.qubit`` simulator backend. Parameters ---------- shots Default shot count for :meth:`run_circuit`. """ def __init__(self, shots: int = 1024) -> None: if not isinstance(shots, int) or isinstance(shots, bool) or shots <= 0: raise ValueError(f"shots must be a positive integer, got {shots!r}") self._default_shots = shots @property def name(self) -> str: return "pennylane:default.qubit" @property def supports_statevector(self) -> bool: return True # ------------------------------------------------------------------ # # Execution # # ------------------------------------------------------------------ #
[docs] def run_circuit( self, circuit: Any, shots: int | None = None, seed: int | None = None, noise_model: Any | None = None, ) -> dict[str, int]: qml = _require_pennylane() if noise_model is not None: raise NotImplementedError( "PennyLaneBackend does not support qtest noise models (they " "target Qiskit Aer). Use the Qiskit backend for noisy simulation." ) shots_val = self._default_shots if shots is None else shots if not isinstance(shots_val, int) or isinstance(shots_val, bool) or shots_val <= 0: raise ValueError(f"shots must be a positive integer, got {shots_val!r}") if seed is not None and (not isinstance(seed, int) or isinstance(seed, bool)): raise ValueError(f"seed must be an integer or None, got {seed!r}") operations, wire_order = _extract(circuit) dev = qml.device("default.qubit", wires=wire_order, seed=seed) tape = qml.tape.QuantumScript(operations, [qml.counts(all_outcomes=True)], shots=shots_val) counts = qml.execute([tape], dev)[0] return {str(k): int(v) for k, v in counts.items() if int(v) > 0}
[docs] def get_statevector(self, circuit: Any) -> np.ndarray: qml = _require_pennylane() operations, wire_order = _extract(circuit) dev = qml.device("default.qubit", wires=wire_order) tape = qml.tape.QuantumScript(operations, [qml.state()]) state = qml.execute([tape], dev)[0] return np.asarray(state, dtype=complex)
[docs] def get_unitary(self, circuit: Any) -> np.ndarray: qml = _require_pennylane() operations, wire_order = _extract(circuit) matrix = qml.matrix(qml.tape.QuantumScript(operations, []), wire_order=wire_order) return np.asarray(matrix, dtype=complex)