Tatopenn's picture
Sync package with GitHub Dense-Evolution v8.1.21
4173e5f
Raw
History Blame Contribute Delete
7.49 kB
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