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

QASM3 exporter #6565

Merged
merged 4 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions qiskit/circuit/library/standard_gates/x.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,31 @@ def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str,
"cx", 2, [], num_ctrl_qubits=1, label=label, ctrl_state=ctrl_state, base_gate=XGate()
)

def _define_qasm3(self):
from qiskit.qasm3.ast import (
Constant,
Identifier,
Integer,
QuantumBlock,
QuantumGateModifier,
QuantumGateModifierName,
QuantumGateSignature,
QuantumGateDefinition,
QuantumGateCall,
)

control, target = Identifier("c"), Identifier("t")
call = QuantumGateCall(
Identifier("U"),
[control, target],
parameters=[Constant.pi, Integer(0), Constant.pi],
modifiers=[QuantumGateModifier(QuantumGateModifierName.ctrl)],
)
return QuantumGateDefinition(
QuantumGateSignature(Identifier("cx"), [control, target]),
QuantumBlock([call]),
)
Comment on lines +192 to +215
Copy link
Member

Choose a reason for hiding this comment

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

This is pretty temporary: just like the QASM 2 exporter, we probably will want a way for gates to define their QASM 3 representation, but with QASM 3 being far more complex, the structure of exporting now goes "Terra -> AST -> string". So gates have to return something that can be made into an AST object. For the time being, while we're vendoring our own AST, we return that. In the future, once we've shifted to using the reference AST, which crucially will include a "string -> AST" parser, we'll be able to allow gates to return their QASM 3 definition as a string, and in those cases we'll be able to use the parser to raise it to a full AST node.

Having the definitions in proper AST representation is nice because we get a separation of concerns between AST generation and text output, and we can optionally do much more compiler-like things, like verifying that the instruction is valid, and that the signature is consistent with (say) another definition given in an include file.

The CX gate here is mostly just an example - we may want to move this to a different gate, or just define a new test gate in a unit test, since in reality cx should be mapped to builtin names most of the time.

Copy link
Member Author

Choose a reason for hiding this comment

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

The CX gate here is mostly just an example

Not sure. The problem with cx is that has no definition in Qiskit. Not having a _define_qasm3 means that cx is opaque when the standard include is missing.

Copy link
Member

Choose a reason for hiding this comment

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

The QASM 3 exporter should be able to do something sensible for it without a _define_qasm3 method - it's a simple control gate, and we probably should have some way of quickly working out that it can be defined by ctrl @ x q0, q1 by inspecting the Terra definition. It'll be a nuisance for us (we have many controlled gates) and users (Terra can't use information they've already given) if we don't handle it automatically.

Copy link
Member

@jakelishman jakelishman Oct 6, 2021

Choose a reason for hiding this comment

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

That said, I'd be happy if the way we do it is to define _define_qasm3() on ControlledGate, and have the inherited to all members, but that starts to get slightly more complicated, because then ControlledGate._define_qasm3 will need to have a way to query the exporter for how the inner gate is defined. That's an example of what you were worried about when we called - there needs to be a two-way flow of information in that case. One way to do that might be by a sort of co-routine approach - we could allow _define_qasm3 to yield arbitrary Terra objects, and have the caller feed their AST representations back in with generator.send.

Something like:

class ControlledGate:
    def _define_qasm3(self):
        base_ast = yield (self.base_gate,)
        # ... modify the AST into the form we want ...
        return out

and the caller does something along the lines of (forgive me - I never get the flow right the first time with co-routines):

def handle_object(self, obj, context):
    output = obj._define_qasm3()
    if isinstance(output, ASTNode):
        self.update_context(context, object, output)
        return output
    if not isinstance(output, generator):
        raise QiskitError(...)
    to_parse = next(output)
    while True:
        parsed = []
        for inner in to_parse:
            parsed.append(self.handle_object(inner, context))
            self.update_context(context, inner, parsed[-1])
        try:
            to_parse = output.send(parsed)
        except StopIteration as result:
            break
    self.update_context(context, obj, result.value)
    return result

This allows _define_qasm3 to yield an arbitrary number of collections of objects back to the exporter, which will parse them into AST, then pass them back in parsed form. It then just loops round doing that til the original _define_qasm3 returns its final value. This would have problems with object cycles, but I'm not sure if we're allowing QASM 3 to be recursive anyway, but if so, we can do a similar thing to deepcopy.

Copy link
Member

Choose a reason for hiding this comment

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

My longer term question here is about the interface we move to. I get that this is temporary given the current state of the upstream qasm ast and the lack of a qiskit parser. But I feel longer term the place we want to be in a __qasm3__ attribute that just returns a string of the qasm representation of the gate.

The thing I want to avoid is the pattern of having _qasm() and _assemble() on every standard gate. For things in qiskit's standard lib we should have enough context to know how to deal with them without having to hard code a bunch of stuff in the classes. But, having something that's opt-in for objects where we don't have that context or in other situations makes sense.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I don't think we should need a _qasm() method on any standard gate - we should be able to set up the types enough that this is never necessary. I didn't originally think we were going to merge with this AST still attached to CX - I thought it was mostly just for testing purposes, and we were planning to move it into a custom test gate.

We definitely do need something opt-in, that isn't just static, because there are problems when the exporter needs to munge the output names to avoid conflicts in custom gates. It's ok if the custom gate only relies on standard gates with invariant names (it's easy enough for us to regex-capture the name the gate has returned), but if it relies on other custom Terra instructions, it needs to know how to call them in QASM in order to output the correct call. If not, we might mistake it for a different instruction.


def control(
self,
num_ctrl_qubits: int = 1,
Expand Down
75 changes: 75 additions & 0 deletions qiskit/qasm/libs/stdgates.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// OpenQASM 3 standard gate library
Copy link
Member

Choose a reason for hiding this comment

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

Is this dependent on the answer to #6492 (comment) ?

Copy link
Member Author

@1ucian0 1ucian0 Sep 21, 2021

Choose a reason for hiding this comment

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

good point. I will add that also as a dependency. This is not strictly needed, i added as a way to test the possibility to add different inc files. If there is no access to stdgates.inc, the gate declaration will be added.


// phase gate
gate p(lambda) a { ctrl @ gphase(lambda) a; }

// Pauli gate: bit-flip or NOT gate
gate x a { U(pi, 0, pi) a; }
// Pauli gate: bit and phase flip
gate y a { U(pi, pi/2, pi/2) a; }
// Pauli gate: phase flip
gate z a { p(pi) a; }

// Clifford gate: Hadamard
gate h a { U(pi/2, 0, pi) a; }
// Clifford gate: sqrt(Z) or S gate
gate s a { pow(1/2) @ z a; }
// Clifford gate: inverse of sqrt(Z)
gate sdg a { inv @ pow(1/2) @ z a; }

// sqrt(S) or T gate
gate t a { pow(1/2) @ s a; }
// inverse of sqrt(S)
gate tdg a { inv @ pow(1/2) @ s a; }

// sqrt(NOT) gate
gate sx a { pow(1/2) @ x a; }

// Rotation around X-axis
gate rx(theta) a { U(theta, -pi/2, pi/2) a; }
// rotation around Y-axis
gate ry(theta) a { U(theta, 0, 0) a; }
// rotation around Z axis
gate rz(lambda) a { gphase(-lambda/2); U(0, 0, lambda) a; }

// controlled-NOT
gate cx c, t { ctrl @ x c, t; }
// controlled-Y
gate cy a, b { ctrl @ y a, b; }
// controlled-Z
gate cz a, b { ctrl @ z a, b; }
// controlled-phase
gate cp(lambda) a, b { ctrl @ p(lambda) a, b; }
// controlled-rx
gate crx(theta) a, b { ctrl @ rx(theta) a, b; }
// controlled-ry
gate cry(theta) a, b { ctrl @ ry(theta) a, b; }
// controlled-rz
gate crz(theta) a, b { ctrl @ rz(theta) a, b; }
// controlled-H
gate ch a, b { ctrl @ h a, b; }

// swap
gate swap a, b { cx a, b; cx b, a; cx a, b; }

// Toffoli
gate ccx a, b, c { ctrl @ ctrl @ x a, b, c; }
// controlled-swap
gate cswap a, b, c { ctrl @ swap a, b, c; }

// four parameter controlled-U gate with relative phase
gate cu(theta, phi, lambda, gamma) c, t { p(gamma) c; ctrl @ U(theta, phi, lambda) c, t; }

// Gates for OpenQASM 2 backwards compatibility
// CNOT
gate CX c, t { ctrl @ U(pi, 0, pi) c, t; }
// phase gate
gate phase(lambda) q { U(0, 0, lambda) q; }
// controlled-phase
gate cphase(lambda) a, b { ctrl @ phase(lambda) a, b; }
// identity or idle gate
gate id a { U(0, 0, 0) a; }
// IBM Quantum experience gates
gate u1(lambda) q { U(0, 0, lambda) q; }
gate u2(phi, lambda) q { gphase(-(phi+lambda)/2); U(pi/2, phi, lambda) q; }
gate u3(theta, phi, lambda) q { gphase(-(phi+lambda)/2); U(theta, phi, lambda) q; }
68 changes: 68 additions & 0 deletions qiskit/qasm3/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
==========================
Qasm (:mod:`qiskit.qasm3`)
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be OpenQASM3 to differentiate it from the qiskit.qasm module

Suggested change
Qasm (:mod:`qiskit.qasm3`)
OpenQASM 3 (:mod:`qiskit.qasm3`)

==========================

.. currentmodule:: qiskit.qasm3

.. autosummary::
:toctree: ../stubs/

Exporter
dumps
dump
"""

from .exporter import Exporter


def dumps(circuit, **kwargs) -> str:
"""Serialize a :class:`~qiskit.circuit.QuantumCircuit` object in an OpenQASM3 string.

.. note::

This is a quick interface to the main :obj:`.Exporter` interface. All keyword arguments to
this function are inherited from the constructor of that class, and if you have multiple
circuits to export, it will be faster to create an :obj:`.Exporter` instance, and use its
:obj:`.Exporter.dumps` method.

Args:
circuit (QuantumCircuit): Circuit to serialize.
**kwargs: Arguments for the :obj:`.Exporter` constructor.

Returns:
str: The OpenQASM3 serialization
"""
return Exporter(**kwargs).dumps(circuit)


def dump(circuit, stream, **kwargs) -> None:
"""Serialize a :class:`~qiskit.circuit.QuantumCircuit` object as a OpenQASM3 stream to file-like
object.

.. note::

This is a quick interface to the main :obj:`.Exporter` interface. All keyword arguments to
this function are inherited from the constructor of that class, and if you have multiple
circuits to export, it will be faster to create an :obj:`.Exporter` instance, and use its
:obj:`.Exporter.dump` method.

Args:
circuit (QuantumCircuit): Circuit to serialize.
stream (TextIOBase): stream-like object to dump the OpenQASM3 serialization
**kwargs: Arguments for the :obj:`.Exporter` constructor.

"""
Exporter(**kwargs).dump(circuit, stream)
Loading