swap.ctrl loses its control when translating to OpenQASM 2
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 1.1k
- Forks
- 455
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 165
Description
Describe the bug
Translating a kernel that uses swap.ctrl to OpenQASM 2 drops the control. The emitted circuit is an unconditional swap, so it implements a different operator than the one CUDA-Q itself simulates, with no error and no warning.
The decomposition is SwapToCX, in lib/Optimizer/Transforms/DecompositionPatterns.cpp. It reads op.getTarget(0) and op.getTarget(1) and emits three CNOTs between them. It never reads op.getControls(), so any controls on the SwapOp are discarded.
Steps to reproduce the bug
import cudaq
@cudaq.kernel
def k():
q = cudaq.qvector(3)
x(q[1])
swap.ctrl(q[0], q[1], q[2]) # control is |0>, so this should do nothing
print(cudaq.get_state(k))
print(cudaq.translate(k, format="openqasm2"))
The state CUDA-Q computes is |010>, which is right: the control is |0>, so the swap does not happen.
The OpenQASM 2 it emits is
qreg var0[3];
x var0[1];
cx var0[2], var0[1];
cx var0[1], var0[2];
cx var0[2], var0[1];
var0[0] does not appear at all. Those three CNOTs are an unconditional swap of var0[1] and var0[2], so running the emitted circuit gives |001>.
I also checked it with MQT QCEC, comparing
x q[1]; cswap q[0], q[1], q[2];
against the emitted version. It returns not_equivalent.
One note on reproducing: the control has to be |0>. With the control in |1> the controlled swap and the unconditional swap agree, so that case passes either way.
Expected behavior
The emitted OpenQASM 2 should implement the same operator as the kernel. For a controlled swap that means a Fredkin, for example cx b,a; ccx c,a,b; cx b,a.
Alternatively the pattern could decline to fire when the op has controls, which is what the other patterns in that file do. HToPhasedRx has
if (!op.getControls().empty())
return failure();
and the file already has a helper for it, checkNumControls(op, n), used by CHToCX among others. SwapToCX calling checkNumControls(op, 0) would make it bail out and leave the controlled swap to another pattern.
I looked at the other 31 patterns with that signature in the file. SwapToCX is the only one that both ignores controls and is reachable with them from the Python API. ExpPauliDecomposition also never mentions controls, but exp_pauli has no .ctrl in the Python DSL, so I could not produce a case for it and am not claiming one.
Is this a regression?
I do not know. I only tested 0.15.1.
Environment
CUDA-Q 0.15.1, commit aca5853a76d499ecc3d5f97c2e06163ae99d9c75, installed with pip install cudaq. Python 3.11.16, Linux x86_64.
QIR output is not affected: there the controlled swap survives as __quantum__qis__swap__ctl, so this looks specific to paths where the swap has to be decomposed.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in lib/Optimizer/Transforms/DecompositionPatterns.cpp at the SwapToCX pattern, then compare its control handling with HToPhasedRx and CHToCX and the checkNumControls helper. Run the supplied controlled-swap Python example and inspect the OpenQASM 2 output. Done means controlled swaps preserve their control during translation, or the pattern declines to fire when controls are present.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, python
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100