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.0The 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 hereControlled-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.0Earlier 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.
