Skip to content

QASM export assigns qubit indices from a counter instead of the operands #2

Description

@cleitonaugusto

I was reading the OpenQASM backend and the exported circuit does not match the
IR. The qubit each gate lands on comes from a running counter, not from the
operands.

qasm_export.rs, lines 59 to 66, and 231:

let mut qubit_counter = 0usize;
for (_op_key, op) in &ctx.ops {
    let q0 = qubit_counter % num_qubits;
    let q1 = (qubit_counter + 1) % num_qubits;
    ...
    qubit_counter += 1;

op.inputs is never read, so the Nth gate is emitted on qubit N mod num_qubits
no matter what the IR says.

Smallest case I could get to. Three gates, all on the same qubit:

#dialect quantum
module @all_on_q2 {
    func @f(%q0: qubit, %q1: qubit, %q2: qubit) -> (qubit, qubit, qubit) {
        %a = "quantum.x"(%q2) : (qubit) -> qubit
        %b = "quantum.y"(%a)  : (qubit) -> qubit
        %c = "quantum.z"(%b)  : (qubit) -> qubit
        return %q0, %q1, %c
    }
}

lift-cli export t.lif --backend qasm

expected: x q[2];  y q[2];  z q[2];
got:      x q[0];  y q[1];  z q[2];

Three gates on one qubit come out on three different qubits. The file is valid
OpenQASM 3 and there is no error and no warning.

The same thing hits examples/quantum_bell.lif, which the README points at:

h q[0];
cx q[1], q[0];

The IR puts the control on the qubit that got the H, so control and target are
swapped here. I ran the two circuits through MQT QCEC and it returns
not_equivalent. In the exported version the control sits on the idle q1, so the
result is a product state rather than a Bell pair.

Two more things come out of the same loop. Putting them here since one fix
covers all three.

Gate order. The loop walks the ctx.ops slotmap instead of block.ops. slotmap
hands out the slot of a removed entry to the next insert, so once one pass
removes an op and a later pass creates one, the new gate is emitted earlier than
it runs. I saw a block order of [x, z, h] export as [x, h, z]. This one is only
reachable through the library API, since the optimise output cannot be read back
in (filed separately as #1).

Qubit count. num_qubits comes from lines 30 to 47. max_qubits counts qubit typed
values, and in SSA every gate result is a new one, so it counts gates rather than
qubits. qubit_args sums block args across all blocks of all functions. Two
2-qubit functions export as qubit[4] q; with the gates of both concatenated into
one circuit.

For what it is worth, the pieces to fix it look like they are already there.
llvm.rs and onnx.rs walk region.blocks and then block.ops, and
DefSite::BlockArg carries arg_index, so following an operand back to its
definition gives the qubit number.

Version: I built from 9ca5a92, which is tagged v0.4.4. The binary reports 0.3.0,
because lift-cli/src/main.rs:6 has the version hardcoded rather than using
env!("CARGO_PKG_VERSION"). Small thing, but it will make version reports
confusing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions