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

QASM parser #1207

Merged
merged 43 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
015d308
feat: started imlementing QASM parser
BrunoLiegiBastonLiegi Feb 9, 2024
2902558
feat: updated gate definition and addition
BrunoLiegiBastonLiegi Feb 12, 2024
43dc6be
feat: created DefinedGate object
BrunoLiegiBastonLiegi Feb 12, 2024
c25bfd2
feat: fixed gate definition and qubits registers
BrunoLiegiBastonLiegi Feb 13, 2024
0a280ac
fix: improvements in error handling
BrunoLiegiBastonLiegi Feb 13, 2024
5107d6e
feat: added openqasm dependency + updated tests
BrunoLiegiBastonLiegi Feb 13, 2024
4b75bf8
fix: added parser extra dependency
BrunoLiegiBastonLiegi Feb 13, 2024
a12619b
fix: registers reordering
BrunoLiegiBastonLiegi Feb 14, 2024
ef18432
cov: added docstrings + improved coverage
BrunoLiegiBastonLiegi Feb 14, 2024
d4204e8
cov: improved coverage
BrunoLiegiBastonLiegi Feb 14, 2024
24314fc
Merge branch 'master' into qasm3
renatomello Feb 14, 2024
d9dc16b
Merge branch 'master' into qasm3
renatomello Feb 14, 2024
01a306d
Merge branch 'master' into qasm3
renatomello Feb 16, 2024
2f4b8df
Merge branch 'master' into qasm3
renatomello Feb 16, 2024
0518bf0
Merge branch 'master' into qasm3
renatomello Feb 16, 2024
d97fc27
update `poetry.lock`
renatomello Feb 16, 2024
01fe98e
Merge branch 'master' into qasm3
renatomello Feb 20, 2024
ee515a5
refactor: renamed parser.py
BrunoLiegiBastonLiegi Feb 21, 2024
5e3f025
refactor: removed args argument from raise_error
BrunoLiegiBastonLiegi Feb 21, 2024
63f348f
Update src/qibo/_openqasm.py
BrunoLiegiBastonLiegi Feb 23, 2024
c436cc1
Update src/qibo/_openqasm.py
BrunoLiegiBastonLiegi Feb 23, 2024
288d0c8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 23, 2024
f04b6bf
Update src/qibo/_openqasm.py
BrunoLiegiBastonLiegi Feb 23, 2024
ae3bf2c
Update src/qibo/_openqasm.py
renatomello Feb 24, 2024
d2992ac
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 24, 2024
b2616ed
suggestions
renatomello Feb 24, 2024
0927613
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 24, 2024
689b683
Merge branch 'master' into qasm3
renatomello Feb 26, 2024
bb0da07
fix: fixing suggestions
BrunoLiegiBastonLiegi Feb 26, 2024
7f3728b
refactor: made _get_gate and _unroll_expr more readable
BrunoLiegiBastonLiegi Feb 26, 2024
78e6bd5
refactor: adapted DefinedGate to build a unique FusedGate
BrunoLiegiBastonLiegi Feb 26, 2024
6485715
cov: adding coverage to some defined gates errors
BrunoLiegiBastonLiegi Feb 26, 2024
c25ad35
feat: added wire names to parser
BrunoLiegiBastonLiegi Feb 26, 2024
20b74f2
fix: changed excpetion error type in _get_gate
BrunoLiegiBastonLiegi Feb 26, 2024
4585ed1
Update src/qibo/_openqasm.py
BrunoLiegiBastonLiegi Feb 27, 2024
42f7fb7
refactor: renamed DefinedGate and moved _openqasm to models/
BrunoLiegiBastonLiegi Feb 27, 2024
79b844b
Delete src/qibo/_openqasm.py
BrunoLiegiBastonLiegi Feb 27, 2024
0407704
Merge branch 'master' into qasm3
renatomello Feb 27, 2024
26f1895
Merge branch 'qasm3' of github.com:qiboteam/qibo into qasm3
renatomello Feb 27, 2024
0177fbf
Merge branch 'master' into qasm3
BrunoLiegiBastonLiegi Mar 1, 2024
bf1f9fe
build: regenerated lock
BrunoLiegiBastonLiegi Mar 1, 2024
34eb5de
Merge branch 'master' into qasm3
BrunoLiegiBastonLiegi Mar 1, 2024
0912a3a
build: regenerated lock
BrunoLiegiBastonLiegi Mar 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 43 additions & 103 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ cma = "^3.3.0"
joblib = "^1.2.0"
hyperopt = "^0.2.7"
tabulate = "^0.9.0"
openqasm3 = {version = ">=0.5.0", extras = ["parser"]}
numpy = "^1.26.4"
networkx = "^3.2.1"
cvxpy = {version = "^1.4.2", optional = true}
Expand Down
287 changes: 287 additions & 0 deletions src/qibo/_openqasm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
"""Qibo wrapper for QASM 3.0 parser."""

from itertools import repeat
from typing import Union

import numpy as np
import openqasm3

import qibo
from qibo.config import raise_error
from qibo.gates import FusedGate


class DefinedGate:
BrunoLiegiBastonLiegi marked this conversation as resolved.
Show resolved Hide resolved
BrunoLiegiBastonLiegi marked this conversation as resolved.
Show resolved Hide resolved
"""Object that handles the definition of custom gates in QASM via the `gate` command.

Args:
gates (list): List of gates composing the defined gate.
qubits (list or tuple): Qubits identifiers (e.g. (q0, q1, q2, ...)).
args (list or tuple): Arguments identifiers (e.g. (theta, alpha, gamma, ...)).
"""

def __init__(
self,
name: str,
gates: list,
qubits: Union[list, tuple],
args: Union[list, tuple],
):
self.name = name
self.gates = gates
self.qubits = qubits
self.args = args

def get_gate(self, qubits: Union[list, tuple], args: Union[list, tuple]):
"""Returns the gates composing the defined gate applied on the
specified qubits with the specified ``args`` as a unique :class:`qibo.gates.special.FusedGate`.

Args:
qubits (list or tuple): Qubits where to apply the gates.
args (list or tuple): Arguments to evaluate the gates on.

Returns:
:class:`qibo.gates.special.FusedGate`: the composed gate evaluated on the input qubits with the input arguments.
"""
if len(self.args) != len(args):
raise_error(
ValueError,
f"Invalid `args` argument passed to the user-defined gate `{self.name}` upon construction. {args} was passed but something of the form {self.args} is expected.",
)
elif len(self.qubits) != len(qubits):
raise_error(
ValueError,
f"Invalid `qubits` argument passed to the user-defined gate `{self.name}` upon construction. {qubits} was passed but something of the form {self.qubits} is expected.",
)
qubit_map = dict(zip(self.qubits, qubits))
args_map = dict(zip(self.args, args))
return self._construct_fused_gate(self.gates, qubits, qubit_map, args_map)

def _construct_fused_gate(self, gates, qubits, qubit_map, args_map):
"""Constructs a :class:`qibo.gates.special.FusedGate` out of the provided list of gates on the specified qubits.

Args:
gates (list(:class:`qibo.gates.Gate`)): List of gates to build the fused gate from.
qubits (list(int)): List of qubits to construct the gate on.
qubit_map (dict): Mapping between the placeholders for the qubits contained in `gates` and the actual qubits indices to apply them on.
args_map (dict): Mapping between the placeholders for the kwargs contained in `gates` and the actual kwargs values.

Returns:
(qibo.gates.special.FusedGate): The resulting fused gate.
"""
fused_gate = FusedGate(*qubits)
for gate in gates:
if not isinstance(gate, FusedGate):
new_qubits, new_args = self._compile_gate_qubits_and_args(
gate, qubit_map, args_map
)
fused_gate.append(gate.__class__(*new_qubits, *new_args))
else:
qubits = [qubit_map[q] for q in gate.qubits]
fused_gate.append(
self._construct_fused_gate(gate.gates, qubits, qubit_map, args_map)
)
return fused_gate

def _compile_gate_qubits_and_args(self, gate, qubit_map, args_map):
"""Compile the qubits and arguments placeholders contained in the input gate with their actual values.

Args:
gate (:class:`qibo.gates.Gate`): The input gate containing the qubits and arguments placeholders.
qubit_map (dict): Mapping between the placeholders for the qubits contained in `gate` and the actual qubits indices to apply them on.
args_map (dict): Mapping between the placeholders for the kwargs contained in `gate` and the actual kwargs values.

Returns:
tuple(list, list): The compiled qubits and arguments.
"""
new_qubits = [qubit_map[q] for q in gate.qubits]
new_args = [args_map.get(arg, arg) for arg in gate.init_kwargs.values()]
return new_qubits, new_args


def _qibo_gate_name(gate):
if gate == "cx":
return "CNOT"

if gate == "id":
return "I"

if gate == "ccx":
return "TOFFOLI"

return gate.upper()


class QASMParser:
"""Wrapper around the :class:`openqasm3.parser` for QASM 3.0."""

def __init__(
self,
):
self.parser = openqasm3.parser
self.defined_gates = {}
self.q_registers = {}
self.c_registers = set()

def to_circuit(
self, qasm_string: str, accelerators: dict = None, density_matrix: bool = False
):
"""Converts a QASM program into a :class:`qibo.models.Circuit`.

Args:
qasm_string (str): QASM program.
accelerators (dict, optional): Maps device names to the number of times each
device will be used. Defaults to ``None``.
density_matrix (bool, optional): If ``True``, the constructed circuit would
evolve density matrices.

Returns:
:class:`qibo.models.Circuit`: circuit constructed from QASM string.
"""
parsed = self.parser.parse(qasm_string)
gates = []
self.defined_gates, self.q_registers, self.c_registers = {}, {}, {}

nqubits = 0
for statement in parsed.statements:
if isinstance(statement, openqasm3.ast.QuantumGate):
gates.append(self._get_gate(statement))
elif isinstance(statement, openqasm3.ast.QuantumMeasurementStatement):
gates.append(self._get_measurement(statement))
elif isinstance(statement, openqasm3.ast.QubitDeclaration):
q_name, q_size = self._get_qubit(statement)
self.q_registers.update(
{q_name: list(range(nqubits, nqubits + q_size))}
)
nqubits += q_size
elif isinstance(statement, openqasm3.ast.QuantumGateDefinition):
self._def_gate(statement)
elif isinstance(statement, openqasm3.ast.Include):
continue
elif isinstance(statement, openqasm3.ast.ClassicalDeclaration):
name = statement.identifier.name
size = statement.type.size.value
self.c_registers.update({name: list(range(size))})
else:
raise_error(RuntimeError, f"Unsupported {type(statement)} statement.")
circ = qibo.Circuit(
nqubits,
accelerators,
density_matrix,
wire_names=self._construct_wire_names(),
)
for gate in gates:
circ.add(gate)
self._reorder_registers(circ.measurements)
return circ

def _get_measurement(self, measurement):
"""Converts a :class:`openqasm3.ast.QuantumMeasurementStatement` statement
into :class:`qibo.gates.measurements.M`."""
qubit = self._get_qubit(measurement.measure.qubit)
register = measurement.target.name.name
if register not in self.c_registers:
raise_error(ValueError, f"Undefined measurement register `{register}`.")
ind = measurement.target.indices[0][0].value
if ind >= len(self.c_registers[register]):
raise_error(
IndexError, f"Index `{ind}` is out of bounds of register `{register}`."
)
self.c_registers[register][ind] = qubit
return getattr(qibo.gates, "M")(qubit, register_name=register)

def _get_qubit(self, qubit):
"""Extracts the qubit from a :class:`openqasm3.ast.QubitDeclaration` statement."""
if isinstance(qubit, openqasm3.ast.QubitDeclaration):
return qubit.qubit.name, qubit.size.value

if not isinstance(qubit, openqasm3.ast.Identifier):
return self.q_registers[qubit.name.name][qubit.indices[0][0].value]

return qubit.name

def _get_gate(self, gate):
"""Converts a :class:`openqasm3.ast.QuantumGate` statement
into :class:`qibo.gates.Gate`."""
qubits = [self._get_qubit(q) for q in gate.qubits]
init_args = []
for arg in gate.arguments:
arg = self._unroll_expression(arg)
try:
arg = eval(arg.replace("pi", "np.pi"))
except:
pass
init_args.append(arg)
# check whether the gate exists in qibo.gates already
if _qibo_gate_name(gate.name.name) in dir(qibo.gates):
try:
gate = getattr(qibo.gates, _qibo_gate_name(gate.name.name))(
*qubits, *init_args
)
# the gate exists in qibo.gates but invalid construction
except TypeError:
raise_error(
ValueError, f"Invalid gate declaration at span: {gate.span}"
)
# check whether the gate was defined by the user
elif gate.name.name in self.defined_gates:
try:
gate = self.defined_gates.get(gate.name.name).get_gate(
qubits, init_args
)
# the gate exists in self.defined_gates but invalid construction
except ValueError:
raise_error(
ValueError, f"Invalid gate declaration at span: {gate.span}"
)
# undefined gate
else:
raise_error(ValueError, f"Undefined gate at span: {gate.span}")
return gate

def _unroll_expression(self, expr):
"""Unrolls an argument definition expression to retrieve the
complete argument as a string."""
# check whether the expression is a simple string, e.g. `pi` or `theta`
if "name" in dir(expr):
return expr.name
# check whether the expression is a single value, e.g. `0.1234`
elif "value" in dir(expr):
return expr.value
# the expression is composite, e.g. `2*pi` or `3*theta/2`
else:
expr_dict = {}
for attr in ("lhs", "op", "expression", "rhs"):
expr_dict[attr] = ""
if attr in dir(expr):
val = self._unroll_expression(getattr(expr, attr))
else:
continue
expr_dict[attr] += str(val)
return "".join(list(expr_dict.values()))
BrunoLiegiBastonLiegi marked this conversation as resolved.
Show resolved Hide resolved

def _def_gate(self, definition):
"""Converts a :class:`openqasm3.ast.QuantumGateDefinition` statement
into :class:`qibo.parser.DefinedGate` object."""
name = definition.name.name
qubits = [self._get_qubit(q) for q in definition.qubits]
args = [self._unroll_expression(expr) for expr in definition.arguments]
gates = [self._get_gate(gate) for gate in definition.body]
self.defined_gates.update({name: DefinedGate(name, gates, qubits, args)})

def _reorder_registers(self, measurements):
"""Reorders the registers of the provided :class:`qibo.gates.measurements.M`
gates according to the classical registers order defined in the QASM program."""
for meas in measurements:
meas.target_qubits = [self.c_registers[meas.register_name].pop(0)]

def _construct_wire_names(self):
"""Builds the wires names from the declared quantum registers."""
wire_names = []
for reg_name, reg_qubits in self.q_registers.items():
wires = sorted(
zip(repeat(reg_name, len(reg_qubits)), reg_qubits), key=lambda x: x[1]
)
for wire in wires:
wire_names.append(f"{wire[0]}{wire[1]}")
return wire_names
7 changes: 2 additions & 5 deletions src/qibo/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,15 @@
MAX_ITERATIONS = 50


def raise_error(exception, message=None, args=None):
def raise_error(exception, message=None):
"""Raise exception with logging error.

Args:
exception (Exception): python exception.
message (str): the error message.
"""
log.error(message)
if args:
raise exception(message, args)
else:
raise exception(message)
raise exception(message)


def get_batch_size():
Expand Down
Loading
Loading