| 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 |
|
|