Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor output of OpenQASM 3 exporter to use fewer aliases #10249

Merged
merged 5 commits into from
Jul 18, 2023

Conversation

jakelishman
Copy link
Member

Summary

This removes spurious _loose_bit "registers" from the OpenQASM 3 output, and instead emits loose bits with individual bit and qubit declarations. Non-overlapping registers are emitted using regular bit[n] and qubit[n] definitions when possible, and we only resort to aliasing if we must to describe the structure.

This avoids introducing structure to the definitions that does not exist in the original program, making round-trips and interactions with other OQ3 consumers more straightforwards. It's better not to use advanced features that don't map to hardware particularly well when it's not necessary.

On the technical side, all bits are now properly tracked in the symbol table. Previously, there was a lot of code duplication, internal state tracking and magic inferences that attempted to "guess" how a qubit/clbit should be referred to. Instead, we just properly add them as variables to the symbol table, which also drastically reduces the number of objects that effectively reserve names that the user may not use.

Details and comments

As an example, given this code:

from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, qasm3
from qiskit.circuit import Qubit, Clbit

qc = QuantumCircuit(
    QuantumRegister(3, "qmem"),
    [Qubit(), Qubit(), Qubit(), Clbit(), Clbit(), Clbit()],
    ClassicalRegister(3, "a"),
)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
for i in range(3):
    qc.cx(i, i + 3)
qc.measure(range(6), range(6))

print(qasm3.dumps(qc))

the output before this PR was:

OPENQASM 3;
include "stdgates.inc";
bit[3] _loose_clbits;
bit[3] a;
qubit[6] _all_qubits;
let qmem = _all_qubits[0:2];
h qmem[0];
cx qmem[0], qmem[1];
cx qmem[0], qmem[2];
cx qmem[0], _all_qubits[3];
cx qmem[1], _all_qubits[4];
cx qmem[2], _all_qubits[5];
_loose_clbits[0] = measure qmem[0];
_loose_clbits[1] = measure qmem[1];
_loose_clbits[2] = measure qmem[2];
a[0] = measure _all_qubits[3];
a[1] = measure _all_qubits[4];
a[2] = measure _all_qubits[5];

and now it is the more natural

OPENQASM 3;
include "stdgates.inc";
bit _bit0;
bit _bit1;
bit _bit2;
bit[3] a;
qubit _qubit3;
qubit _qubit4;
qubit _qubit5;
qubit[3] qmem;
h qmem[0];
cx qmem[0], qmem[1];
cx qmem[0], qmem[2];
cx qmem[0], _qubit3;
cx qmem[1], _qubit4;
cx qmem[2], _qubit5;
_bit0 = measure qmem[0];
_bit1 = measure qmem[1];
_bit2 = measure qmem[2];
a[0] = measure _qubit3;
a[1] = measure _qubit4;
a[2] = measure _qubit5;
``

This removes spurious `_loose_bit` "registers" from the OpenQASM 3
output, and instead emits loose bits with individual `bit` and
`qubit` declarations.  Non-overlapping registers are emitted using
regular `bit[n]` and `qubit[n]` definitions when possible, and we only
resort to aliasing if we must to describe the structure.

This avoids introducing structure to the definitions that does not exist
in the original program, making round-trips and interactions with other
OQ3 consumers more straightforwards.  It's better not to use advanced
features that don't map to hardware particularly well when it's not
necessary.

On the technical side, all bits are now properly tracked in the symbol
table.  Previously, there was a lot of code duplication, internal state
tracking and magic inferences that attempted to "guess" how a
qubit/clbit should be referred to.  Instead, we just properly add them
as variables to the symbol table, which also drastically reduces the
number of objects that effectively reserve names that the user may not
use.
@jakelishman jakelishman added Changelog: New Feature Include in the "Added" section of the changelog Changelog: API Change Include in the "Changed" section of the changelog mod: qasm2 Relating to OpenQASM 2 import or export labels Jun 9, 2023
@jakelishman jakelishman added this to the 0.25.0 milestone Jun 9, 2023
@jakelishman jakelishman requested a review from ihincks June 9, 2023 14:20
@jakelishman jakelishman requested a review from a team as a code owner June 9, 2023 14:20
@qiskit-bot
Copy link
Collaborator

One or more of the the following people are requested to review this:

  • @Qiskit/terra-core

@coveralls
Copy link

coveralls commented Jun 9, 2023

Pull Request Test Coverage Report for Build 5245029921

  • 58 of 59 (98.31%) changed or added relevant lines in 3 files are covered.
  • 8 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.03%) to 85.945%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/qasm3/exporter.py 49 50 98.0%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/expr.rs 1 93.76%
crates/qasm2/src/lex.rs 3 90.63%
crates/accelerate/src/sabre_swap/layer.rs 4 97.32%
Totals Coverage Status
Change from base Build 5244976406: 0.03%
Covered Lines: 71382
Relevant Lines: 83055

💛 - Coveralls

qiskit/qasm3/ast.py Show resolved Hide resolved
qiskit/qasm3/exporter.py Outdated Show resolved Hide resolved
Comment on lines +134 to +135
alias_classical_registers: bool = None,
allow_aliasing: bool = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
alias_classical_registers: bool = None,
allow_aliasing: bool = None,
alias_classical_registers: Optional[bool, None] = None,
allow_aliasing: Optional[bool, None] = None,

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm never super sure about this type of annotation. I mean, technically yes Optional[bool] (or Union[bool, None]) is correct for what the values are considered, but logically a caller should never pass None. It's just an implementation detail we use to track whether the option was manually specified during the switchover period. It'll stop accepting None once that period's over and just have the default False.

I can go either way on this - I don't know what type-hints users typically prefer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please proceed with your preference

test/python/qasm3/test_export.py Show resolved Hide resolved
Copy link
Contributor

@ihincks ihincks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jakelishman , LGTM

@jakelishman jakelishman added mod: qasm3 Related to OpenQASM 3 import or export and removed mod: qasm2 Relating to OpenQASM 2 import or export labels Jun 15, 2023
jakelishman added a commit to jakelishman/qiskit-terra that referenced this pull request Jul 7, 2023
@jlapeyre
Copy link
Contributor

A minor point. Why is this line
https://github.com/Qiskit/qiskit-terra/blob/c365df87c62c58c378a242f57e4d563da42cea09/test/python/qasm3/test_export.py#L141
not written qubits = [Qubit() for _ in range(10)] ? Is it because the dummy variable _ is always set to None, suggesting that its value is irrelevant?

@jakelishman
Copy link
Member Author

It's just a bit of an idiosyncrasy - there's no meaning. It so happens that [None]*n is one of the fastest iterators in CPython, despite the allocation. It's technically beaten by itertools.repeat(None, n), but for some reason I just tend to default to [None]*n.

@jlapeyre
Copy link
Contributor

jlapeyre commented Jul 18, 2023

I'm having difficulty understanding the main point(s) of this PR. What is the problem with aliasing and "trying to guess how a qubit/clbit should be referred to" ?

Putting the summary at the top: I don't see that aliasing is essential if you want to collect loose bits as the existing code does (but I may be missing something). I do see that the PR may allow a more faithful round trip.

The example given in the opening comment doesn't make it clear (to me at least) that the change is better.
In this code

from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, qasm3
from qiskit.circuit import Qubit, Clbit

n_qubits = 3
n_clbits = 3

use_register = False
if use_register:
    qubits = QuantumRegister(n_qubits, "qmem")
    clbits = ClassicalRegister(n_clbits, "a")
else:
    qubits = [Qubit() for _ in range(n_qubits)]
    clbits = [Clbit() for _ in range(n_clbits)]

qc = QuantumCircuit(qubits, clbits)

qc.h(0)
for i in range(1, n_qubits):
    qc.cx(0, i)

qc.measure(range(n_qubits), range(n_qubits)) # assume enough clbits
print(qasm3.dumps(qc))

what is the semantic difference when use_register is True vs False? Should it be reflected in the OQ3?
To me it looks cleaner in some cases, such as this example, to collect the bits in registers.

Is the following an instance of the unwanted aliasing?

qubit[3] _all_qubits;
let qmem = _all_qubits[0:2];

Can you avoid aliasing by outputting something like this ?

qubit[3] user_supplied_register_name_1;
qubit[3] _loose_qubits_1;
qubit[3] user_supplied_register_name_2;

One possible advantage that I can see of this PR is the following. If the OC3 representation doesn't collect "scalar" qubits into a register, then the round-trip representation in qiskit can be more faithful to the original. Maybe that's worth preserving.

EDIT: The following may be an example of the guess work or arbitrariness in referring to bits. If I have use a single list of qubits [Qubit() for _ in range(10)] then you could argue that it's natural to make this a register. But what about
[Qubit() for _ in range(10)], [special_qubit_1, another_qubit_for_a_different_purpose], QuantumRegister(3)] ?
I would still lose the names in going to OC3 and back, but at least I would not put unrelated qubits in a register if I did not do this in qiskit.

@jakelishman
Copy link
Member Author

It's mostly about the OQ3 reflecting the structure of the Qiskit program more closely, which allows better round-tripping. That's originally why Ian had requested I do this; qiskit.qasm3.loads(qiskit.qasm2.dumps(qc)) == qc would typically be False if the circuit had loose bits, because the OQ3 dump would insert spurious structure.

The other problem is that a bit can be in more than one register. The original code always used aliasing so that there only needed to be a single path, but that quickly needed modifying when it became clear that hardware was never going to support the concept of aliasing classical registers, so the alias_classical_registers option was born. This PR simplifies a bunch of the magic that was added along with that option, moving the decision about "how should this bit be referenced?" into a single place, with the same logic working for both classical and quantum bits - previously, every place that referenced a bit did some magic guesswork to determine what OQ3 node should be output for a given bit.

To speak to the edit: whatever names you give your bits in Python space are irrelevant, because they're not stored in the QuantumCircuit. There's no possibility of round-tripping those in the current way Qiskit works. This is mostly just about making the Qiskit <-> OQ3 translation higher fidelity where there are simple ways to do that, and as a side-effect, it's a long overdue refactor to remove a lot of the magic names and tightly coupled logic between "definition" and "use" in the OQ3 exporter.

"let first_four = {_qubit0, _qubit1, _qubit2, _qubit3};",
"let last_five = {_qubit5, _qubit6, _qubit7, _qubit8, _qubit9};",
"let alternate = {first_four[0], first_four[2], _qubit4, last_five[1], last_five[3]};",
"let sporadic = {alternate[2], alternate[1], last_five[4]};",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like a bit is referred to by its most recently created alias. Is this an implementation detail? Is it just an artifact of the implementation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jlapeyre
Copy link
Contributor

whatever names you give your bits in Python space are irrelevant, because they're not stored in the QuantumCircuit

I tried to say the same thing. The identifiers used in Python are not preserved as identifiers in OC3 (I don't know how to do this.) But whether the bit is in a Qiskit register or not can be preserved. I think this part of the structure you are talking about.

@jlapeyre
Copy link
Contributor

An answer to my question above:

Is the following an instance of the unwanted aliasing?

qubit[3] _all_qubits;
let qmem = _all_qubits[0:2];

In fact you have simplified this for input QuantumRegister(3, "qmem"). I somehow missed this in my tests.

Copy link
Contributor

@jlapeyre jlapeyre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM as well.

@jlapeyre jlapeyre added this pull request to the merge queue Jul 18, 2023
Merged via the queue into Qiskit:main with commit 2194f55 Jul 18, 2023
@jakelishman jakelishman deleted the qasm3/fewer-qubit-aliases branch July 18, 2023 23:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: API Change Include in the "Changed" section of the changelog Changelog: New Feature Include in the "Added" section of the changelog mod: qasm3 Related to OpenQASM 3 import or export
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants