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