Superfermion logo
Guides

Controlled Gates: CU & CP

Native controlled-U3 (cu/cu3) and controlled-phase (cp) gates: exact simulation, compilation and JSON round-trips that preserve them.

cu, cu3 and cp are native IR gates in Superfermion — they simulate, differentiate and serialize exactly, without decomposition.

Controlled-U3: cu / cu3

circuit.cu(theta, phi, lam, control, target, gamma=0.0) (alias cu3) applies a controlled U3 rotation with Qiskit's optional gamma phase:

CU = diag(I, e^{iγ}·U3(θ, φ, λ))

— the same control-first layout as Qiskit's CUGate. gamma defaults to 0, which reproduces the original controlled-U3 (and PennyLane's qml.CU), so existing calls are unchanged:

import superfermion as sf

qc = sf.Circuit(2).cp(0.7, 0, 1).cu(1.2, 0.3, 0.4, 0, 1)
state = sf.simulate(qc)
print(state.numpy().round(4))
# [1.+0.j 0.+0.j 0.+0.j 0.+0.j]  (control in |0> -> CU leaves |00> alone)

With the control qubit in |0⟩ the CU acts as identity, so the state stays |00⟩. Flip the control to see the U3 branch:

qc2 = sf.Circuit(2).x(0).cu(1.2, 0.3, 0.4, 0, 1)
sv = sf.run(qc2, shots=0).statevector
# <Z> = P(0) - P(1) = -1.0 on the |1> control, so it stayed |1>
print(sf.expval(sv, sf.PauliString("ZI", coeffs=1.0)))  # -1.0

The gates accept symbolic parameters like any other rotation, work with gradients, and are preserved by compile() optimization passes.

Qiskit gamma phase

cu accepts Qiskit's fourth gamma parameter — a phase e^{iγ} on the control-1 subspace. from_qiskit/to_qiskit map it natively (the gate is no longer routed through a lossy matrix fallback):

from qiskit import QuantumCircuit
from superfermion.bridge import from_qiskit

qc = QuantumCircuit(2).cu(0.7, 0.3, 0.2, 0.9, 0, 1)   # theta, phi, lam, gamma
sf_qc = from_qiskit(qc)              # imports as a native CU gate
# equivalent, all-native Superfermion form:
sf_qc = sf.Circuit(2).cu(0.7, 0.3, 0.2, 0, 1, 0.9)   # gamma is last here

Controlled-phase: cp

circuit.cp(phi, control, target) applies the phase-controlled CP(phi) = diag(1, 1, 1, e^{i phi}) gate:

qc3 = sf.Circuit(2).x(0).x(1).cp(0.7, 0, 1)
sv = sf.run(qc3, shots=0).statevector
print(sv[3])   # ~ (0.764842+0.644218j) = e^(i*0.7) on |11>

Serialization round-trips

DAG serialization preserves every controlled gate, including CP and CU. Converting a circuit to a QuantumDAG, dumping it to JSON and loading it back yields an identical statevector:

dag = qc.to_ir()
dag2 = type(dag).from_json(dag.to_json())

sv1 = dag.simulate()
sv2 = dag2.simulate()
print(max(abs(a - b) for a, b in zip(sv1, sv2)))   # 0.0

Earlier builds of the Rust IR silently dropped CP gates on this round-trip (and cu was rejected with "Unknown gate: cu"); both now survive unchanged.

See also

  • Gate reference and parameterized gates: Circuits
  • Compiler passes and optimization: Compiler

On this page