File size: 7,490 Bytes
4173e5f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | from typing import Optional, Tuple
import numpy as np
from .parser import QASMParser, QASMCircuit
from .simulator import DenseSVSimulator
try:
import qiskit.qasm2 as _qasm2
HAS_QISKIT = True
except ImportError:
HAS_QISKIT = False
try:
import pennylane as qml
HAS_PENNYLANE = True
except ImportError:
HAS_PENNYLANE = False
def _require_qiskit():
if not HAS_QISKIT:
raise ImportError(
"Qiskit interop requires the 'qiskit' package. "
"Install it with: pip install dense-evolution[qiskit]")
def _require_pennylane():
if not HAS_PENNYLANE:
raise ImportError(
"PennyLane interop requires the 'pennylane' package. "
"Install it with: pip install dense-evolution[pennylane]")
def _to_qiskit_bit_order(probs: np.ndarray, n_qubits: int) -> np.ndarray:
"""
Reindex a probability array from Dense-Evolution's native MSB-first
convention (qubit 0 = most significant bit of the index, the same
convention as apply_gate_1q/apply_gate_2q/measure/beast-mode) to
Qiskit's little-endian convention (qubit 0 = least significant bit),
so the result is directly comparable to Statevector(...).probabilities().
A plain bit-reversal permutation of the index β verified against
Statevector.from_instruction(...).probabilities() on an asymmetric
circuit (exact match only after this reversal, not before).
"""
perm = [int(format(i, f'0{n_qubits}b')[::-1], 2) for i in range(2 ** n_qubits)]
return probs[perm]
def from_qiskit(circuit) -> QASMCircuit:
"""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."""
_require_qiskit()
qasm_str = _qasm2.dumps(circuit)
return QASMParser().parse(qasm_str)
def _sorted_wires(wires):
"""Best-effort ascending order for a wire sequence. Falls back to the
original order if the labels aren't mutually comparable (e.g. mixed
str/int wire names) β better to fall back to the old touch-order
behavior than to crash on an exotic device's wire labels."""
try:
return sorted(wires)
except TypeError:
return list(wires)
def from_pennylane(circuit, *args, **kwargs) -> QASMCircuit:
"""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).
"""
_require_pennylane()
if isinstance(circuit, qml.tape.QuantumScript):
wires = _sorted_wires(circuit.wires)
if hasattr(circuit, 'to_openqasm'):
qasm_str = circuit.to_openqasm(wires=wires, measure_all=False)
else:
qasm_str = qml.to_openqasm(circuit, wires=wires, measure_all=False)
else:
device = getattr(circuit, 'device', None)
wires = device.wires if device is not None else None
result = qml.to_openqasm(circuit, wires=wires, measure_all=False)
qasm_str = result if isinstance(result, str) else result(*args, **kwargs)
return QASMParser().parse(qasm_str)
def 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."""
circ = from_qiskit(circuit)
if sim is None:
sim = DenseSVSimulator(n_qubits=circ.n_qubits, use_float32=use_float32)
sim.run_circuit(circ.to_tuples())
probs = np.asarray(sim.get_probabilities())
return sim, _to_qiskit_bit_order(probs, circ.n_qubits)
def 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)."""
circ = from_pennylane(circuit, *args, **kwargs)
if sim is None:
sim = DenseSVSimulator(n_qubits=circ.n_qubits, use_float32=use_float32)
sim.run_circuit(circ.to_tuples())
probs = np.asarray(sim.get_probabilities())
return sim, probs
|