Interop (Qiskit / PennyLane)¶
interop ¶
from_qiskit ¶
Convert a Qiskit QuantumCircuit into a QASMCircuit via OpenQASM 2.0 (qiskit.qasm2.dumps), reusing the existing QASMParser rather than a bespoke gate-by-gate translator.
Source code in dense_evolution/interop.py
from_pennylane ¶
Convert a PennyLane QNode or QuantumTape/QuantumScript into a QASMCircuit via OpenQASM 2.0, reusing the existing QASMParser.
PennyLane's own serialization API for a bare tape has changed across versions in an incompatible way (verified directly against both): - >=~0.43 (Python 3.11+ only): qml.to_openqasm(tape) returns the QASM string directly; QuantumTape/QuantumScript no longer has a to_openqasm() method at all. - <=0.42.x (still installed on Python 3.10, where newer PennyLane isn't available): qml.to_openqasm(tape) does NOT special-case a bare tape — it returns a QNode-oriented wrapper that crashes with AttributeError ('QuantumTape' object has no attribute 'func') if called on one. The tape's own tape.to_openqasm() method is what works there instead. So: a bare tape/QuantumScript uses its own to_openqasm() method when present (old API), otherwise falls through to the top-level qml.to_openqasm() (new API). A QNode (not a QuantumScript instance) always uses the top-level function, which returns a wrapper that must be called with the QNode's own arguments — consistent across both versions, this path was never the one that broke.
WIRE ORDER: by default, both PennyLane APIs number the exported QASM
qubits in the order wires are FIRST TOUCHED in the circuit, not by
their actual wire index — e.g. qml.PauliX(wires=2) followed by
qml.CNOT(wires=[2, 1]) becomes x q[0]; cx q[0],q[1]; in the
default export, silently renumbering wire 2 -> q[0] and wire 1 -> q[1].
Verified directly: this produced a topologically different circuit
from the one PennyLane itself executes whenever wires aren't touched
in ascending order (a QASMParser-based bridge has no way to recover
the true mapping after the fact — the touch-order renumbering has
already happened by the time QASM text exists). Both APIs accept an
explicit wires= argument that forces the true wire order into the
export instead — used here for both the QNode path (the device's own
declared wire order) and the tape path (the tape's own wires, sorted
ascending, since a bare tape has no device to ask).
Source code in dense_evolution/interop.py
run_qiskit_circuit ¶
run_qiskit_circuit(
circuit,
use_float32: bool = True,
sim: Optional[DenseSVSimulator] = None,
) -> Tuple[DenseSVSimulator, np.ndarray]
Run a Qiskit QuantumCircuit on DenseSVSimulator. Returns (sim, probabilities) with probabilities reordered into Qiskit's own little-endian bit convention, so they compare directly against Statevector(circuit).probabilities() — see _to_qiskit_bit_order.
Source code in dense_evolution/interop.py
run_pennylane_circuit ¶
run_pennylane_circuit(
circuit,
*args,
use_float32: bool = True,
sim: Optional[DenseSVSimulator] = None,
**kwargs,
) -> Tuple[DenseSVSimulator, np.ndarray]
Run a PennyLane QNode/tape on DenseSVSimulator. Returns (sim, probabilities) in Dense-Evolution's native ordering, WITHOUT any bit-reversal — unlike run_qiskit_circuit, because PennyLane's own wire convention (wire 0 = most significant) already matches Dense-Evolution's MSB-first convention. Do not "symmetrize" this with the Qiskit version; that would silently misorder circuits that are asymmetric under qubit reversal (verified directly: no permutation needed here, one is required for Qiskit — the two frameworks are genuinely different).
NOT DIFFERENTIABLE: from_pennylane() bakes every gate parameter into a plain Python float inside the QASM text, so it leaves the JAX trace. jax.grad through this function does not raise — it silently returns 0.0 (verified), which reads as "converged" rather than "not wired up". For a real gradient through a Dense-Evolution circuit, use the dashboard_core._vqe_energy_fn pattern instead (jax.value_and_grad over a jax.lax.scan template with sentinel-injected parameters).