Skip to content

Compiler

QuantumTranspiler rewrites a circuit's non-native gates into ones the simulator's fast path actually knows — run_circuit calls it automatically (see Gates's Details for which gate names have no GATE_IDS entry at all: swap, ccx, ecr, iswap, u2, u3, exactly the gates this page decomposes).

Step 1. Transpile a circuit by hand

import dense_evolution as de
from dense_evolution.circuits.compiler import QuantumTranspiler

qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[3]; h q[0]; swap q[0],q[1]; ccx q[0],q[1],q[2];'
circuit = de.QASMParser().parse(qasm)
expanded = QuantumTranspiler.transpile(circuit.to_tuples())
len(expanded), expanded[:4]
(19, [('h', 0), ('cx', 0, 1), ('cx', 1, 0), ('cx', 0, 1)])

3 gates went in, 19 came out: h passed through unchanged, swap became 3 cx gates, ccx became 15 more. run_circuit_jit runs exactly this step first, automatically, on every call — every step below is one piece of what just happened.

Step 2. Toffoli (CCX) → 15 native gates

QuantumTranspiler.decompose_toffoli(0, 1, 2)
[('h', 2), ('cx', 1, 2), ('tdg', 2), ('cx', 0, 2), ('t', 2), ('cx', 1, 2), ('tdg', 2),
 ('cx', 0, 2), ('t', 1), ('t', 2), ('cx', 0, 1), ('h', 2), ('t', 0), ('tdg', 1), ('cx', 0, 1)]

The standard Barenco T/Tdg/CX construction: 6 cx plus 7 single-qubit gates (h, t, tdg), 15 total, for one 3-qubit Toffoli — the two controls are c1, c2, the target is t.

Step 3. SWAP → 3 CX

QuantumTranspiler.decompose_swap(0, 1)
[('cx', 0, 1), ('cx', 1, 0), ('cx', 0, 1)]

The textbook identity SWAP = CX(a,b)·CX(b,a)·CX(a,b) — no ancilla, no extra qubits, just three CX gates in alternating direction.

Step 4. iSWAP → {S, H, CX}, verified exact

import numpy as np

qasm2 = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; h q[0]; rx(0.7) q[1]; cx q[0],q[1];'
circuit2 = de.QASMParser().parse(qasm2)

sim_direct = de.DenseSVSimulator(2)
sim_direct.run_circuit_jit(circuit2.to_tuples())
sim_direct.apply_gate_2q(de.GATES['iswap'], 0, 1)

sim_decomp = de.DenseSVSimulator(2)
sim_decomp.run_circuit_jit(circuit2.to_tuples())
sim_decomp.run_circuit_jit(QuantumTranspiler.decompose_iswap(0, 1))

float(np.max(np.abs(sim_direct.get_statevector() - sim_decomp.get_statevector())))
1.1102230246251565e-16

decompose_iswap(q1, q2) returns [('s', q1), ('h', q1), ('s', q2), ('cx', q1, q2), ('cx', q2, q1), ('h', q2)] — applying those 6 gates gives the exact same statevector as applying GATES['iswap'] directly, down to floating-point noise, not merely a physically-equivalent state up to global phase.

Step 5. ECR →

QuantumTranspiler.decompose_ecr(0, 1)
[('s', 0), ('sx', 1), ('cx', 0, 1), ('x', 0), ('gphase', 0, -0.7853981633974483)]

The {s, sx, x, cx} part alone reproduces ECR only up to a constant e^{i pi/4} global phase; the trailing gphase gate (-pi/4 here) supplies exactly the correction needed to match GATES['ecr'] exactly, verified the same way Step 4 verifies iswap — direct gate versus decomposed circuit, same statevector to machine precision.

Step 6. U3/U2 →

QuantumTranspiler.decompose_u3(0, 0.3, 0.5, 0.7)
[('rz', 0, 0.7), ('ry', 0, 0.3), ('rz', 0, 0.5), ('gphase', 0, 0.6)]

U3(theta, phi, lam) = e^{i(phi+lam)/2} * Rz(phi)*Ry(theta)*Rz(lam) — a standard ZYZ decomposition, applied rightmost-first (rz(lam) then ry(theta) then rz(phi)), with gphase supplying the leading e^{i(phi+lam)/2} scalar so the result matches PARAMETRIC_GATES['u3'](theta, phi, lam) exactly rather than up to an unobservable phase. decompose_u2(q, phi, lam) is the same function called with theta fixed to pi/2U2(phi, lam) = U3(pi/2, phi, lam).


Details

What gphase actually does

gphase(q, alpha) multiplies the entire n-qubit statevector by the scalar e^{i alpha}, not just qubit q's local |1> amplitude the way p/u1 (phase, GATE_IDS entry 12) does — applying e^{i alpha} * I inside any single qubit's 2x2 subspace is mathematically identical to scaling the whole state by e^{i alpha}, since it commutes with every other qubit's identity. It only ever appears as output from decompose_u3/decompose_u2 — nothing in this library emits it any other way.

transpile leaves everything else untouched

Only ccx, swap, iswap, ecr, and parametric u2/u3 get expanded; every other gate name in the input list is passed straight through unchanged, in place, in order.

A structural-only transpile pass (no real angles yet)

Some callers — dense_evolution.solvers.autodiff's VQE template builder — run transpile on tuples that carry qubit indices but no parameter values yet (angles get substituted in later, from a traced theta during optimization). decompose_u2/decompose_u3 can't run without real theta/phi/lam values (needed to compute the gphase angle), so a u2/u3 tuple that's too short to actually contain them is passed through unchanged instead of raising a confusing TypeError here — a caller that genuinely can't support u2/u3 templates raises its own clearer error further down its own pipeline.

The actual compiled kernel this all feeds

_apply_gate_fast_step (one gate) and _run_circuit_scan_core (a whole circuit via jax.lax.scan) are this module's private JAX-jitted engine — what DenseSVSimulator.run_circuit_jit actually calls, dispatching on the same numeric gate IDs Gates documents (GATE_IDS) rather than string names, via jax.lax.switch/cond so the whole circuit compiles to one XLA call. Two public-ish wrappers exist: _compile_and_run_circuit_jit (safe to call repeatedly with the same input buffer) and _compile_and_run_circuit_jit_donated (marks its statevector argument donated — faster, but only safe where the caller immediately rebinds its own reference and never reads the old buffer again, which is exactly run_circuit_jit's self.sv = ... pattern).

Bug history worth knowing if you're reading this kernel's source

The 1-qubit and 2-qubit fast-path kernels originally indexed qubits LSB-first (1 << q) while every other part of the simulator (MSB-first) — silently wrong on any multi-qubit circuit not symmetric under qubit reversal, fixed by computing the same n_qubits - 1 - qubit physical position everywhere else does. CP and CRZ (gate IDs 22 and 25) used to be silently merged under one ID even though they're different gates (CP phases only the |11> component; CRZ phases the target conditioned on its own bit) — CRZ wasn't reachable at all before this was split apart.

compiler

QuantumTranspiler

Gate-level transpiler: decomposes non-native gates into the native {H, T, Tdg, CX, CZ} basis and performs basic circuit optimisations.

decompose_toffoli staticmethod

decompose_toffoli(c1: int, c2: int, t: int) -> List[Tuple]

Decompose CCX (Toffoli) into 15 native gates using the standard T/Tdg/CX Barenco decomposition.

Gate count: 6 CX + 7 single-qubit (H, T, Tdg) = 15 total.

Source code in dense_evolution/circuits/compiler.py
@staticmethod
def decompose_toffoli(c1: int, c2: int, t: int) -> List[Tuple]:
    """
    Decompose CCX (Toffoli) into 15 native gates using the
    standard T/Tdg/CX Barenco decomposition.

    Gate count: 6 CX + 7 single-qubit (H, T, Tdg) = 15 total.
    """
    return [
        ('h',   t),
        ('cx',  c2, t),  ('tdg', t),
        ('cx',  c1, t),  ('t',   t),
        ('cx',  c2, t),  ('tdg', t),
        ('cx',  c1, t),
        ('t',   c2),     ('t',   t),
        ('cx',  c1, c2), ('h',   t),
        ('t',   c1),     ('tdg', c2),
        ('cx',  c1, c2),
    ]

decompose_swap staticmethod

decompose_swap(q1: int, q2: int) -> List[Tuple]

Decompose SWAP into 3 CX gates.

Source code in dense_evolution/circuits/compiler.py
@staticmethod
def decompose_swap(q1: int, q2: int) -> List[Tuple]:
    """Decompose SWAP into 3 CX gates."""
    return [('cx', q1, q2), ('cx', q2, q1), ('cx', q1, q2)]

decompose_iswap staticmethod

decompose_iswap(q1: int, q2: int) -> List[Tuple]

Decompose iSWAP into {S, H, CX}, EXACT (verified numerically against dense_evolution.circuits.gates.GATES['iswap'] on random 2-qubit states, atol=1e-9 -- no global-phase correction needed, unlike decompose_ecr below). Circuit structure cross-checked against Qiskit's own iSwapGate transpiled to a {cx,h,s,...} basis (qiskit==2.5.0), not derived from memory.

Source code in dense_evolution/circuits/compiler.py
@staticmethod
def decompose_iswap(q1: int, q2: int) -> List[Tuple]:
    """Decompose iSWAP into {S, H, CX}, EXACT (verified numerically
    against dense_evolution.circuits.gates.GATES['iswap'] on random
    2-qubit states, atol=1e-9 -- no global-phase correction needed,
    unlike decompose_ecr below). Circuit structure cross-checked
    against Qiskit's own iSwapGate transpiled to a {cx,h,s,...} basis
    (qiskit==2.5.0), not derived from memory."""
    return [('s', q1), ('h', q1), ('s', q2), ('cx', q1, q2), ('cx', q2, q1), ('h', q2)]

decompose_ecr staticmethod

decompose_ecr(q1: int, q2: int) -> List[Tuple]

Decompose ECR into {S, SX, X, CX, GPhase}, EXACT: matches dense_evolution.circuits.gates.GATES['ecr'] to atol=1e-9 on random 2-qubit states once the GPhase(-pi/4) correction is included (the {S,SX,X,CX} part alone reproduces ECR only up to a constant e^{i*pi/4} global phase -- verified numerically, not assumed). Circuit structure cross-checked against Qiskit's own ECRGate transpiled to a {cx,h,s,sx,x,...} basis (qiskit==2.5.0).

Source code in dense_evolution/circuits/compiler.py
@staticmethod
def decompose_ecr(q1: int, q2: int) -> List[Tuple]:
    """Decompose ECR into {S, SX, X, CX, GPhase}, EXACT: matches
    dense_evolution.circuits.gates.GATES['ecr'] to atol=1e-9 on random
    2-qubit states once the GPhase(-pi/4) correction is included (the
    {S,SX,X,CX} part alone reproduces ECR only up to a constant
    e^{i*pi/4} global phase -- verified numerically, not assumed).
    Circuit structure cross-checked against Qiskit's own ECRGate
    transpiled to a {cx,h,s,sx,x,...} basis (qiskit==2.5.0)."""
    return [('s', q1), ('sx', q2), ('cx', q1, q2), ('x', q1), ('gphase', q1, -np.pi / 4.0)]

decompose_u3 staticmethod

decompose_u3(q, theta, phi, lam) -> List[Tuple]

Decompose U3(θ,φ,λ) into {Rz, Ry, Rz, GPhase}.

U3(θ,φ,λ) = e^{i(φ+λ)/2} · Rz(φ)·Ry(θ)·Rz(λ) -- a standard ZYZ decomposition, EXACT (not just up to global phase): the GPhase(α) gate (see GATE_IDS/_apply_gate_fast_step) supplies the leading scalar e^{i(φ+λ)/2} so the decomposed circuit reproduces dense_evolution.circuits.gates.PARAMETRIC_GATES'u3' exactly, not merely a physically-equivalent state differing by an unobservable phase -- verified numerically (random θ,φ,λ, atol=1e-10) before this was wired in here.

Gate sequence is matrix-product-order reversed (rightmost matrix applied first): Rz(λ) then Ry(θ) then Rz(φ), then the phase.

Source code in dense_evolution/circuits/compiler.py
@staticmethod
def decompose_u3(q, theta, phi, lam) -> List[Tuple]:
    """Decompose U3(θ,φ,λ) into {Rz, Ry, Rz, GPhase}.

    U3(θ,φ,λ) = e^{i(φ+λ)/2} · Rz(φ)·Ry(θ)·Rz(λ) -- a standard ZYZ
    decomposition, EXACT (not just up to global phase): the GPhase(α)
    gate (see GATE_IDS/_apply_gate_fast_step) supplies the leading
    scalar e^{i(φ+λ)/2} so the decomposed circuit reproduces
    dense_evolution.circuits.gates.PARAMETRIC_GATES['u3'](theta, phi,
    lam) exactly, not merely a physically-equivalent state differing
    by an unobservable phase -- verified numerically (random θ,φ,λ,
    atol=1e-10) before this was wired in here.

    Gate sequence is matrix-product-order reversed (rightmost matrix
    applied first): Rz(λ) then Ry(θ) then Rz(φ), then the phase.
    """
    return [
        ('rz', q, lam),
        ('ry', q, theta),
        ('rz', q, phi),
        ('gphase', q, (phi + lam) / 2.0),
    ]

decompose_u2 staticmethod

decompose_u2(q, phi, lam) -> List[Tuple]

U2(φ,λ) = U3(π/2, φ, λ) -- see decompose_u3.

Source code in dense_evolution/circuits/compiler.py
@staticmethod
def decompose_u2(q, phi, lam) -> List[Tuple]:
    """U2(φ,λ) = U3(π/2, φ, λ) -- see decompose_u3."""
    return QuantumTranspiler.decompose_u3(q, np.pi / 2.0, phi, lam)

transpile classmethod

transpile(circuit: List[Tuple]) -> List[Tuple]

Expand CCX → 15 native gates, SWAP → 3 CX, iSWAP → {S,H,CX}, ECR → {S,SX,X,CX,GPhase}, and U2/U3 → {Rz,Ry,Rz,GPhase} -- all EXACT (see each decompose_* method's own docstring for the numerical verification). All other gates are passed through unchanged.

Parameters:

Name Type Description Default
circuit list of tuples (gate_name, qubit, ...)
required

Returns:

Type Description
Expanded circuit as a list of tuples.
Source code in dense_evolution/circuits/compiler.py
@classmethod
def transpile(cls, circuit: List[Tuple]) -> List[Tuple]:
    """
    Expand CCX → 15 native gates, SWAP → 3 CX, iSWAP → {S,H,CX},
    ECR → {S,SX,X,CX,GPhase}, and U2/U3 → {Rz,Ry,Rz,GPhase} -- all
    EXACT (see each decompose_* method's own docstring for the
    numerical verification). All other gates are passed through
    unchanged.

    Parameters
    ----------
    circuit : list of tuples (gate_name, qubit, ...)

    Returns
    -------
    Expanded circuit as a list of tuples.
    """
    out: List[Tuple] = []
    for cmd in circuit:
        name = cmd[0].lower()            # BUG FIX: was cmd.lower() on a tuple
        if name == 'ccx':
            out.extend(cls.decompose_toffoli(*cmd[1:4]))
        elif name == 'swap':
            out.extend(cls.decompose_swap(*cmd[1:3]))
        elif name == 'iswap':
            out.extend(cls.decompose_iswap(*cmd[1:3]))
        elif name == 'ecr':
            out.extend(cls.decompose_ecr(*cmd[1:3]))
        # len(cmd) guards below: some callers (e.g.
        # dense_evolution.solvers.autodiff._build_template) run a
        # STRUCTURAL-ONLY pass through transpile -- (name, *qubits),
        # deliberately no parameter values yet (those get injected
        # later via a traced theta, for gates whose template row
        # carries a single parameter slot). decompose_u2/u3 need the
        # real theta/phi/lam values to compute the GPhase angle and
        # can't run on a qubit-only tuple -- pass those through
        # unchanged instead of crashing on missing arguments, so a
        # caller's own downstream check (e.g. _build_template's clear
        # "u2/u3 aren't supported here" ValueError) still fires
        # exactly as before, rather than a confusing TypeError here.
        elif name == 'u3' and len(cmd) >= 5:
            out.extend(cls.decompose_u3(*cmd[1:5]))
        elif name == 'u2' and len(cmd) >= 4:
            out.extend(cls.decompose_u2(*cmd[1:4]))
        else:
            out.append(cmd)
    return out

See Also

  • DenseSVSimulatorrun_circuit_jit, the method that calls transpile and the compiled kernel automatically.
  • GatesGATE_IDS's exact coverage, and the GATES/PARAMETRIC_GATES matrices every decomposition here is verified against.
  • QASMParser — produces the tuple lists transpile and every method on this page operate on.