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

Add support for physical qubits #2

Merged
merged 71 commits into from
Apr 28, 2023
Merged

Conversation

jlapeyre
Copy link
Collaborator

@jlapeyre jlapeyre commented Jan 25, 2023

Example

The most basic example is

from qiskit_qasm3_import import parse

program = """
OPENQASM 3.0;

reset $2;
"""

circuit = parse(program)
circuit.draw()
print(circuit._layout)

which prints

0 ─|0>─       

TranspileLayout(initial_layout=Layout({
2: Qubit(QuantumRegister(1, 'qr'), 0)
}), input_qubit_mapping={Qubit(QuantumRegister(1, 'qr'), 0): 2})

Tasks

  • Refactor non-DRY parts
  • Tests
  • Raise some errors, for example when mixing types of qubits
  • Doc strings [No need for doc strings]
  • Release note

Additional Tasks

  • Rewrite AddressingMode
  • Split some tests into separate functions
  • Test (and implement) rejecting references to hardware qubits in function body.
  • Test hardware qubits in nested scopes.
  • [GJL] Test that layout is correctly generated for hardware qubits.
  • [GJL] Test hardware qubit first seen in local scope broken.
  • Remove commented out cruft in OQ code.
  • Second arg to resolve_condition should not be optional
  • Second arg to resolve_condition should be of one type (State)
  • Create and use a type for hardware qubits, eg type.HardwareQubit.
  • In ValueResolver.visit no need to handle missing context explicitly.
  • Check entry point resolve probably shouldn't accept None as an input to context here, since it's now required.
  • Either pass all state data in resolve or store in class, but don't mix. See Add support for physical qubits #2 (comment)
  • In ConvertVisitor.convert fix inefficiency in iterating through keys.
  • Make protected-access lint exception more locally scoped.
  • Disable too many lines lint check globally
  • AddressingMode: use enum for the mode number. Or switch to using just an enum
  • Add HardwareQubit to the docs/index.rst
  • Remove dead code, old comments, etc.
  • test that the AddressingMode is being set correctly and propagated all the way up to the global scope
  • add a test that we can use hardware addressing in the main program, but still have user-defined gates
  • fairly sure Qiskit says that backends are required to label their hardware qubits starting from 0 with no gaps
  • add an option to the top-level convert function to allow the user to specify the total size of the backend
  • Python Enum instances can (should) be compared by is checks
  • the triple-nested if/else now, can we refactor this a little to do early return
  • Rewrite SymbolTable with stack-like structure.
  • test that hardware qubits can be used outside the local scope they're first seen in
  • test that "hardware mode" being set in a local scope and then referencing virtual qubit after that scope is exited causes an error to be raised.
  • Remove test involving identifer $q.
  • Remove hardware_qubits method. use a different design.

Notes

EDIT: Regarding _layout and TranspileLayout below: PR Qiskit/qiskit#9486 now provides an interface and some documentation.

QuantumCircuit has an undocumented (including in comments) property _layout. It is initialized to None. It seems that sometimes it is set to a Layout and sometimes a TranspileLayout. I find that printing/drawing the circuit fails if _layout is a Layout, because the output methods reference _layout.initial_layout, which does not exist. So I use TranspileLayout, which does have _layout.initial_layout.

The hardware qubits are referred to by numbers. These are mapped Qubits in a QuantumRegister , "qr", in the order that they were seen in the OQ3 code; the first is mapped to qr[0] , and so on.

The references to hardware qubits are collected in ValueResolver.visit_Identifier. The layout is then created and attached to the circuit as the last step in the method ConvertVisitor.convert.

Here is the code that adds the layout

            names = filter(is_physical, symbols.keys())
            intlist = physical_qubit_identifiers_to_ints(names)
            qr = QuantumRegister(len(intlist), "qr")
            initial_layout = Layout.from_intlist(intlist, qr)
            input_qubit_mapping = dict(zip(qr, intlist))
            layout = TranspileLayout(initial_layout, input_qubit_mapping)
            state.circuit._layout = layout

Previously, the method ValueResolver.resolve takes only the node to be resolved and a symbol table. I added as a third argument the context (an instance of State) because it contains the circuit, and each physical qubit must be added to the circuit as are the virtual qubits in ConvertVisitor.visit_QubitDeclaration.

Closes #1

Closes the proxy issue Qiskit/qiskit#9435

@jlapeyre
Copy link
Collaborator Author

jlapeyre commented Jan 25, 2023

EDIT: Letting tox handle the environment is better. And already documented in this repo.

pylint -sn -rn src tests is failing in CI. This command failed to run locally under Python 3.11 because I had wrapt v1.12 installed in my virtual env. After upgrading to wrapt v1.14, the command runs correctly.

Perhaps this requirement should be added somewhere. Presumably not in this repo, because wrapt is an indirect dependency. If this has actually been fixed by a version requirement in the direct dependency that is using wrapt, we could make a requirement here. I have not tracked this down.

@jakelishman
Copy link
Member

It's transitive and already fixed - not our responsibility, and we'd just be polluting our repo if we did it.

@jakelishman
Copy link
Member

I provided a tox configuration, so you can run tox -e py311 to run the tests under Python 3.11, or tox -e lint to run the lint tests in an environment isolated from your current one, which should alleviate a lot of out-of-date environment issues.

@jlapeyre
Copy link
Collaborator Author

jlapeyre commented Jan 25, 2023

Running tox -e lint gives me the following

************* Module qiskit_qasm3_import.types
src/qiskit_qasm3_import/types.py:5:11: E1101: Module 'abc' has no 'ABC' member (no-member)
src/qiskit_qasm3_import/types.py:16:5: E1101: Module 'abc' has no 'abstractmethod' member (no-member)
src/qiskit_qasm3_import/types.py:58:4: W0231: __init__ method from base class 'Type' is not called (super-init-not-called)

Looking in .tox/lint/bin shows that this is using 3.11

These errors don't seem to be reported in CI. I haven't yet found how to use 3.10 to lint without modifying tox.ini.

EDIT: setting basepython = python3.10 in [testenv:lint] solves this problem, in the sense that the errors above are not reported.

EDIT: The following does what I want without modifying tox.ini:

tox -e lint -x testenv:lint.basepython=python3.10

@jakelishman
Copy link
Member

jakelishman commented Jan 25, 2023

That's pretty weird - looks like a bug in the pylint 2.14 series, because abc.abstractmethod and abc.ABC are defined as normal Python objects in the standard library. There isn't even any weird C-extension re-exports for those. I'll bump the pylint version to 2.15.x, since that has no errors with Python 3.11.

Fwiw, all the non-generated tox targets use the "system" python binary. It's not super ideal, but if you have a Python 3.10 environment that also has tox installed, you can do /path/to/python3.10 -m tox [tox options] (or /<py310 venv>/bin/tox [tox options]) to get it to do what you want here. (By "system", I just mean whatever's currently on the PATH as python, so virtual environments affect it.)

@jlapeyre
Copy link
Collaborator Author

jlapeyre commented Jan 26, 2023

This is ready for review.

In particular, the section "Notes" in the opening comment above describes the implementation.

This is nearly finished. You may want to take a look at this before I clean up rough edges it to see if we need structural changes to the implementation. In any case, I'll start polishing a bit.

@jlapeyre jlapeyre changed the title [WIP] Add support for physical qubits Add support for physical qubits Jan 26, 2023
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

Thanks for looking at this. I think this is a solid start. I think we'd be better leveraging the type system and pushing a lot of this work onto the type checker, rather than doing it ad-hoc throughout the higher-level components, but that's not too difficult a change. I also had a couple of data-representation points, but nothing super serious.

releasenotes/config.yaml Show resolved Hide resolved
Comment on lines 24 to 50
class AddressingMode:
"""Addressing mode for qubits in OpenQASM 3 programs.

This class is useful as long as we allow only physical or virtual addressing modes, but
not mixed. If the latter is supported in the future, this class will be modified or removed.
"""

# 0 == UNKNOWN
# 1 == PHYSICAL
# 2 == VIRTUAL

def __init__(self):
self._state = 0

def set_physical_mode(self):
"""Set the addressing mode to physical. On success return `True`, otherwise `False`."""
if self._state != 2:
self._state = 1
return True
return False

def set_virtual_mode(self):
"""Set the addressing mode to virtual. On success return `True`, otherwise `False`."""
if self._state != 1:
self._state = 2
return True
return False
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps a flat enum here with no interior mutability would be better. The interface added here still requires consumers to do the error checking, so I'm not sure it adds anything significant, and it ends up with behaviour of the importer being defined inside its data, rather than in the importer - the capabilities of the importer are being defined by methods on a separate piece of data in a separate file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The commits are squashed in this PR. But I did use Enum earlier: jlapeyre@a4491f4 . I didn't like that the details of how we switch modes and what is allowed, and how this is implemented, are spread across a few files and sections of code. I wanted it in one place and behind an abstraction. I noted in the commit message for the current code, jlapeyre@0834755, that the abstraction leaks. But if you compare the code you may agree that it is better this way than with Enum. I considered raising the error in AddressingMode, which would hide the abstraction at the cost of another call in the call stack. I think that would be a good trade.

I agree with your argument that this does not belong in data.py.

Copy link
Member

@jakelishman jakelishman Jan 27, 2023

Choose a reason for hiding this comment

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

Personally I think that Enum code looks much cleaner, and because it doesn't try to abstract very simple mutation away, it doesn't leak either. If you really wanted encapsulate the setter and erroring out, I'd use an enum and put the setter logic in a method on State, but really I'd prefer to just revert exactly to that commit you had.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Let me take another try at a non-leaky abstraction. If it doesn't look good we can revert to Enum. My thought is that with a good abstraction you have one instantiation of an AddressingMode (with no explicit initial data passed). And two calls in two files, one to set physical addressing, the other to set virtual. If there is a problem you get an error.

Compare to Enum here: jlapeyre@a4491f4 . First, AddressingMode should be, as you say, moved, to converter.py. But it must be initialized with a value UNKNOWN. Then in two different files there are two reads of the data and two mutations. If I have a good abstraction, then I change the implementation, or with luck even the semantics, without changing code at the call sites. But with Enum I have to find and understand all five uses of the data and the logic surrounding them.

src/qiskit_qasm3_import/converter.py Outdated Show resolved Hide resolved
src/qiskit_qasm3_import/converter.py Outdated Show resolved Hide resolved
src/qiskit_qasm3_import/converter.py Outdated Show resolved Hide resolved
src/qiskit_qasm3_import/expression.py Outdated Show resolved Hide resolved
src/qiskit_qasm3_import/expression.py Outdated Show resolved Hide resolved
src/qiskit_qasm3_import/expression.py Outdated Show resolved Hide resolved
tests/test_convert.py Show resolved Hide resolved
tests/test_convert.py Outdated Show resolved Hide resolved
@jlapeyre
Copy link
Collaborator Author

jlapeyre commented Feb 1, 2023

Because there were several items requested in the review, I added another checklist in the opening comment. All items except adding some more tests have been addressed.

Handling of the addressing mode is encapsulated in a class. This is slightly more complex structure-wise than using just an Enum jlapeyre@a4491f4, but not by much. It has the advantage that mutable details of the state of addressing mode are not spread across classes in two files. In general, it seems Qiskit prefers spreading mutation of state throughout the code. I suppose it is simpler in a sense and often is not problematic. In the present case in particular, reading and writing the Enum instead of using the present class is not really too complex; this code is not very big. I'm ok with using the Enum instead.

There are probably a few commented out print statements left to remove.

@jlapeyre
Copy link
Collaborator Author

jlapeyre commented Feb 2, 2023

Hardware qubits seen for the first time in local scope do not appear in the global symbol table. Local symbols are, correctly, discarded. In this case, the layout does not include these hardware qubits. I think this and other clumsy manipulations might be ameliorated by storing hardware qubits in a separate structure.

Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

I think if we could just make the slightly safer typing in the SymbolTable, we can merge this. There's the question of "number of qubits in the backend", but let's do that in a follow-up.

@jlapeyre
Copy link
Collaborator Author

jlapeyre commented Mar 7, 2023

There's the question of "number of qubits in the backend", but let's do that in a follow-up.

A good idea. This PR discussion is already complicated.

@jlapeyre
Copy link
Collaborator Author

jlapeyre commented Mar 9, 2023

I started to add some tests for SymbolTable (7a412ea, 8e02af4) in order to make clear what we want it to do. This turned up some incorrect behavior.

include 'stdgates.inc';
qubit p;
"""
with pytest.raises(ConversionError, match="already inserted in symbol table"):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This isn't really a good error message. If these were really errors, we would trap it and throw a more specific error. Because trying to insert a symbol twice may arise from different operations. But since we really want to handle the messages as diagnostics, not errors, we need another way to get the same effect.

Use a stack instead of a recursive structure for managing nested symbol tables.
@jlapeyre
Copy link
Collaborator Author

jlapeyre commented Mar 15, 2023

@jakelishman I redesigned the symbol table. It is a stack rather than a recursive structure. I think this is much more in line with what you were thinking about above. Furthermore the stack is managed by context managers.

EDIT:

Now the global symbol table is first on the stack. This is the natural way to implement the global symbols in this implementation. You still have to reference it in particular, knowing it holds the globals, in a couple of places.

There are still a couple of spots where the logic should be cleaned up. The biggest offenders have been fixed.

@jlapeyre
Copy link
Collaborator Author

jlapeyre commented Mar 15, 2023

Fixes for a couple of outstanding issues:

  • I refactored until PhysicalQubitInGate was called in only one place. Then I replaced it with a call to raise_from_node. I check the identifier name (a str) rather than checking for types.HardwareQubit because I need to catch an illegal use where the hardware qubit's first appearance is in the gate, in which case it has not yet been constructed.

  • I refactored _check_visible to remove redundant checks and also make the logic easier to read.

This change increases performance by a factor of 1.5 or 2.
Setting when already set takes about 65ns vs 120ns.
Probably not a bottleneck at the moment.
@jakelishman jakelishman merged commit b7f4d7b into Qiskit:main Apr 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for references to physical qubits
2 participants