Skip to content

Commit

Permalink
Add support for physical qubits
Browse files Browse the repository at this point in the history
  • Loading branch information
jlapeyre committed Jan 27, 2023
1 parent 7efa394 commit 3fe2d3b
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 32 deletions.
3 changes: 3 additions & 0 deletions releasenotes/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
encoding: utf8
default_branch: main
10 changes: 10 additions & 0 deletions releasenotes/notes/support-physical-qubits-27a1bc5a659ee438.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
features:
- |
Added the ability to handle references to physical qubits. A layout is
included in the output circuit that maps a quantum register to the
non-negative integers identifying the physical qubits in the order that they
are encountered in the OpenQASM 3 code.
Currently mixing references to physical qubits with declarations of virtual
qubits in an OpenQASM 3 program is not allowed and raises an error.
37 changes: 30 additions & 7 deletions src/qiskit_qasm3_import/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@
)
from qiskit.circuit.parametertable import ParameterReferences
from qiskit.circuit.library import standard_gates as _std
from qiskit.transpiler import Layout
from qiskit.transpiler.layout import TranspileLayout

from . import types
from .data import Scope, Symbol
from .data import Scope, Symbol, AddressingMode
from .exceptions import ConversionError, raise_from_node
from .expression import ValueResolver, resolve_condition
from .expression import (
ValueResolver,
resolve_condition,
is_physical,
physical_qubit_identifiers_to_ints,
)


_STDGATES = {
Expand Down Expand Up @@ -89,14 +96,15 @@


class State:
__slots__ = ("scope", "source", "circuit", "symbol_table", "_unique")
__slots__ = ("scope", "source", "circuit", "symbol_table", "_unique", "addressing_mode")

def __init__(self, scope: Scope, source: Optional[str] = None):
self.scope = scope
self.source = source
self.circuit = QuantumCircuit()
self.symbol_table = _BUILTINS.copy()
self._unique = (f"_{x}" for x in itertools.count())
self.addressing_mode = AddressingMode()

def gate_scope(self):
"""Get a new state for entry to a "gate" scope."""
Expand Down Expand Up @@ -183,13 +191,23 @@ class ConvertVisitor(QASMVisitor[State]):
# In some places, such as symbol definitions, we do some simple checks to help everyone's
# sanity, as the reference package doesn't yet do this.

# pylint: disable=missing-function-docstring,no-self-use,unused-argument
# pylint: disable=missing-function-docstring,no-self-use,unused-argument,protected-access

def convert(self, node: ast.Program, *, source: Optional[str] = None) -> QuantumCircuit:
"""Convert a program node into a :class:`~qiskit.circuit.QuantumCircuit`. If given,
`source` is a string containing the OpenQASM 3 source code that was parsed into `node`.
This is used to generated improved error messages."""
return self.visit(node, State(Scope.GLOBAL, source)).circuit

state = self.visit(node, State(Scope.GLOBAL, source))
symbols = state.symbol_table
if any(is_physical(name) for name in symbols.keys()):
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))
state.circuit._layout = TranspileLayout(initial_layout, input_qubit_mapping)
return state.circuit

def _raise_previously_defined(self, new: Symbol, old: Symbol, node: ast.QASMNode) -> NoReturn:
message = f"'{new.name}' is already defined."
Expand Down Expand Up @@ -265,7 +283,7 @@ def _add_circuit_parameter(self, parameter: Parameter, context: State):
context.circuit._parameter_table[parameter] = ParameterReferences(())

def _resolve_generic(self, node: ast.Expression, context: State) -> Tuple[Any, types.Type]:
return ValueResolver(context.symbol_table).resolve(node)
return ValueResolver(context.symbol_table).resolve(node, context)

def _resolve_constant_int(self, node: ast.Expression, context: State) -> int:
value, type = self._resolve_generic(node, context)
Expand Down Expand Up @@ -312,7 +330,7 @@ def _resolve_qarg(
def _resolve_condition(
self, node: ast.Expression, context: State
) -> Union[Tuple[ClassicalRegister, int], Tuple[Clbit, bool]]:
lhs, rhs = resolve_condition(node, context.symbol_table)
lhs, rhs = resolve_condition(node, context)
if not isinstance(lhs, (Clbit, ClassicalRegister)):
name = context.unique_name()
lhs = ClassicalRegister(name=_escape_qasm2(name), bits=lhs)
Expand All @@ -339,6 +357,11 @@ def visit_Include(self, node: ast.Include, context: State) -> State:
return context

def visit_QubitDeclaration(self, node: ast.QubitDeclaration, context: State) -> State:
if not context.addressing_mode.set_virtual_mode():
raise_from_node(
node,
"Virtual qubit declared in physical addressing mode. Mixing modes not currently supported.",
)
name = node.qubit.name
if node.size is None:
bit = Qubit()
Expand Down
29 changes: 29 additions & 0 deletions src/qiskit_qasm3_import/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,35 @@ class Scope(enum.Enum):
NONE = enum.auto()


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


class Symbol:
"""An internal symbol used during parsing."""

Expand Down
87 changes: 62 additions & 25 deletions src/qiskit_qasm3_import/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,33 @@

__all__ = ["ValueResolver", "resolve_condition"]

import re
from typing import Any, Iterable, Mapping, Tuple, Union

from openqasm3 import ast
from openqasm3.visitor import QASMVisitor
from qiskit.circuit import Clbit
from qiskit.circuit import Clbit, Qubit

from . import types
from .exceptions import raise_from_node
from .data import Symbol
from .data import Symbol, Scope


_IntegerT = Union[types.Never, types.Int, types.Uint]

_PHYSICAL_QUBIT_RE = re.compile(r"\$\d+")


def is_physical(name: str):
"Return true if name is a valid identifier for a physical qubit."
return re.match(_PHYSICAL_QUBIT_RE, name) is not None


# Qiskit represents physical qubits in a layout by integers.
def physical_qubit_identifiers_to_ints(names):
"""Convert an iterable of identifiers of physical qubits to a list of corresponding `int`s."""
return [int(name[1:]) for name in names]


def join_integer_types(left: _IntegerT, right: _IntegerT) -> _IntegerT:
if isinstance(left, types.Never):
Expand Down Expand Up @@ -62,43 +76,60 @@ class ValueResolver(QASMVisitor):
def __init__(self, symbols: Mapping[str, Symbol]):
self.symbols = symbols

def resolve(self, node: ast.Expression) -> Tuple[Any, types.Type]:
def resolve(self, node: ast.Expression, context=None) -> Tuple[Any, types.Type]:
"""The entry point to the resolver, resolving the AST node into a 2-tuple of a relevant
Qiskit type, and the :class:`.Type` that it is an instance of."""
return self.visit(node)
if context is None:
return self.visit(node)
return self.visit(node, context)

def visit(self, node: ast.QASMNode, context: None = None) -> Tuple[Any, types.Type]:
value, type = super().visit(node, context)
if context is None:
value, type = super().visit(node)
else:
value, type = super().visit(node, context)
if isinstance(type, types.Error):
raise_from_node(node, "type error")
return value, type

def generic_visit(self, node: ast.QASMNode, context: None = None):
raise_from_node(node, f"'{node.__class__.__name__}' cannot be resolved into a Qiskit value")

def visit_Identifier(self, node: ast.Identifier):
def visit_Identifier(self, node: ast.Identifier, context=None):
name = node.name
if name not in self.symbols:
raise_from_node(node, f"name '{name}' is not defined in this scope")
symbol = self.symbols[name]
if is_physical(name): # Physical qubits are not declared.
if not context.addressing_mode.set_physical_mode():
raise_from_node(
node,
"Physical qubit referenced in virtual addressing mode. Mixing modes not currently supported.",
)
bit = Qubit()
symbol = Symbol(name, bit, types.Qubit(), Scope.GLOBAL, node)
context.circuit.add_bits([bit])
self.symbols[name] = symbol
else:
raise_from_node(node, f"name '{name}' is not defined in this scope")
else:
symbol = self.symbols[name]
return symbol.data, symbol.type

def visit_IntegerLiteral(self, node: ast.IntegerLiteral):
def visit_IntegerLiteral(self, node: ast.IntegerLiteral, _context=None):
return node.value, types.Int(const=True)

def visit_FloatLiteral(self, node: ast.FloatLiteral):
def visit_FloatLiteral(self, node: ast.FloatLiteral, _context=None):
return node.value, types.Float(const=True)

def visit_BooleanLiteral(self, node: ast.BooleanLiteral):
def visit_BooleanLiteral(self, node: ast.BooleanLiteral, _context=None):
return node.value, types.Bool(const=True)

def visit_BitstringLiteral(self, node: ast.BitstringLiteral):
def visit_BitstringLiteral(self, node: ast.BitstringLiteral, _context=None):
return node.value, types.Uint(const=True, size=node.width)

def visit_DurationLiteral(self, node: ast.DurationLiteral):
def visit_DurationLiteral(self, node: ast.DurationLiteral, _context=None):
return (node.value, node.unit.name), types.Duration(const=True)

def visit_DiscreteSet(self, node: ast.DiscreteSet):
def visit_DiscreteSet(self, node: ast.DiscreteSet, _context=None):
if not node.values:
return (), types.Sequence(types.Never())
set_type: _IntegerT = types.Never()
Expand All @@ -113,7 +144,7 @@ def visit_DiscreteSet(self, node: ast.DiscreteSet):
values.append(expr_value)
return tuple(values), types.Sequence(set_type)

def visit_RangeDefinition(self, node: ast.RangeDefinition):
def visit_RangeDefinition(self, node: ast.RangeDefinition, _context=None):
start, start_type = (None, types.Never()) if node.start is None else self.visit(node.start)
end, end_type = (None, types.Never()) if node.end is None else self.visit(node.end)
step, step_type = (None, types.Never()) if node.step is None else self.visit(node.step)
Expand All @@ -129,7 +160,7 @@ def visit_RangeDefinition(self, node: ast.RangeDefinition):
end = end + 1 if positive else end - 1
return slice(start, end, step), types.Range(range_type)

def visit_Concatenation(self, node: ast.Concatenation):
def visit_Concatenation(self, node: ast.Concatenation, _context=None):
lhs_value, lhs_type = self.visit(node.lhs)
rhs_value, rhs_type = self.visit(node.rhs)
if not (
Expand All @@ -142,7 +173,7 @@ def visit_Concatenation(self, node: ast.Concatenation):
out_value = tuple(lhs_value) + tuple(rhs_value)
return out_value, type(lhs_type)(len(out_value))

def visit_UnaryExpression(self, node: ast.UnaryExpression):
def visit_UnaryExpression(self, node: ast.UnaryExpression, _context=None):
# In all this, we're only supporting things that we can actually output; `~` for example is
# supported on `Bit` and `BitArray`, but Qiskit doesn't have any representation of the
# literals for those or the actual operation on `Clbit` / `ClassicalRegister`, so we can
Expand All @@ -157,7 +188,7 @@ def visit_UnaryExpression(self, node: ast.UnaryExpression):
return (-value), type
raise_from_node(node, f"unhandled unary operator '{node.op.name}'")

def visit_BinaryExpression(self, node: ast.BinaryExpression):
def visit_BinaryExpression(self, node: ast.BinaryExpression, _context=None):
if node.op.name not in ("+", "-", "*", "/"):
raise_from_node(node, f"unsupported binary operation '{node.op.name}'")
lhs_value, lhs_type = self.visit(node.lhs)
Expand Down Expand Up @@ -273,10 +304,10 @@ def _index_collection(
return value, (types.Bit() if isinstance(value, Clbit) else types.Qubit())
raise_from_node(base, f"unsupported index type: '{index_type.pretty()}'")

def visit_IndexExpression(self, node: ast.IndexExpression):
def visit_IndexExpression(self, node: ast.IndexExpression, _context=None):
return self._index_collection(*self.visit(node.collection), node.index, node)

def visit_IndexedIdentifier(self, node: ast.IndexedIdentifier):
def visit_IndexedIdentifier(self, node: ast.IndexedIdentifier, _context=None):
collection, collection_type = self.visit(node.name)
for index in node.indices:
collection, collection_type = self._index_collection(
Expand All @@ -286,21 +317,27 @@ def visit_IndexedIdentifier(self, node: ast.IndexedIdentifier):


def resolve_condition(
node: ast.Expression, symbols: Mapping[str, Symbol]
node: ast.Expression, context=None # symbols: Mapping[str, Symbol]
) -> Union[Tuple[Clbit, bool], Tuple[Iterable[Clbit], int]]:
"""A resolver for conditions that can be converted into Qiskit's very basic equality form
of either ``Clbit == bool`` or ``ClassicalRegister == int``.
This effectively just handles very special outer cases, then delegates the rest of the work to a
:class:`.ValueResolver`."""

if isinstance(context, dict):
symbols = context
context = None
else:
symbols = context.symbol_table

value_resolver = ValueResolver(symbols)

if isinstance(node, ast.BinaryExpression):
if node.op not in (ast.BinaryOperator["=="], ast.BinaryOperator["!="]):
raise_from_node(node, f"unhandled binary operator '{node.op.name}'")
lhs_value, lhs_type = value_resolver.visit(node.lhs)
rhs_value, rhs_type = value_resolver.visit(node.rhs)
lhs_value, lhs_type = value_resolver.visit(node.lhs, context)
rhs_value, rhs_type = value_resolver.visit(node.rhs, context)
bad_type_message = (
"conditions must be 'bit == const bool' or 'bitarray == const int',"
f" not '{lhs_type.pretty()} {node.op.name} {rhs_type.pretty()}'"
Expand All @@ -327,11 +364,11 @@ def resolve_condition(
if isinstance(node, ast.UnaryExpression):
if node.op is not ast.UnaryOperator["~"]:
raise_from_node(node, f"unhandled unary operator '{node.op.name}'")
value, type = value_resolver.visit(node.expression)
value, type = value_resolver.visit(node.expression, context)
if isinstance(type, types.Bit):
return (value, False)
else:
value, type = value_resolver.visit(node)
value, type = value_resolver.visit(node, context)
if isinstance(type, types.Bit):
return (value, True)
raise_from_node(
Expand Down
Loading

0 comments on commit 3fe2d3b

Please sign in to comment.