Native programming

In order to execute a program on a quantum computer, each qubit in the program must be mapped to a physical qubit on the device, and each operation must be mapped to one or more “native gates”, that is, gates that are natively implemented by the hardware. While this can be handled automatically by a compiler, a quantum software developer or researcher may want to be able to control these mappings explicitly. We refer to this low-level programming as “native programming”.

This notebook provides a demonstration of the native programming features of AutoQASM by targeting a simple two-qubit circuit to physical qubits and native gates of an IonQ quantum computer, which is available through Amazon Braket.

[1]:
# general imports
import IPython

# AutoQASM imports
import autoqasm as aq

# AWS imports: Import Braket SDK modules
from braket.devices import Devices

The circuit we will use for this demonstration is a program which creates and measures a Bell state on two qubits. Here, we write this program at the typical level of abstraction, which is hardware-agnostic. We use integers 0 and 1 to specify qubit indices, and we use the built-in h and cnot instructions from the AutoQASM instructions module.

[2]:
from autoqasm.instructions import cnot, h, measure


@aq.main
def bell_state():
    h(0)
    cnot(0, 1)
    return measure([0, 1])


print(bell_state.build().to_ir())
OPENQASM 3.0;
output bit[2] return_value;
qubit[2] __qubits__;
h __qubits__[0];
cnot __qubits__[0], __qubits__[1];
bit[2] __bit_0__ = "00";
__bit_0__[0] = measure __qubits__[0];
__bit_0__[1] = measure __qubits__[1];
return_value = __bit_0__;

As seen in the generated OpenQASM program, this produces a program that uses a two-qubit register __qubits__ and the built-in h and cnot gates. At runtime, the compiler will automatically map this to two physical qubits, and will compile the h and cnot instructions to an equivalent sequence of gates which are native to the target device.

In the native programming scenario, however, the developer wants full control over the physical qubit mappings and conversion to native gates. We can take advantage of two features of AutoQASM to enable this. First, we replace the integers 0 and 1, which specify virtual qubit indices, with the strings "$0" and "$1", which specify physical qubits. Second, we wrap the gates inside a verbatim block (using the aq.verbatim() context), which instructs the compiler to avoid modifying anything inside the block.

[3]:
@aq.main
def bell_state():
    with aq.verbatim():
        h("$0")
        cnot("$0", "$1")
    return measure(["$0", "$1"])


print(bell_state.build().to_ir())
OPENQASM 3.0;
output bit[2] return_value;
pragma braket verbatim
box {
    h $0;
    cnot $0, $1;
}
bit[2] __bit_0__ = "00";
__bit_0__[0] = measure $0;
__bit_0__[1] = measure $1;
return_value = __bit_0__;

This program now targets physical qubits, and the gates will not be modified by the compiler.

Device-specific validation

Bypassing the mapping and compilation is only the first step of native programming. Because native programming is intended for targeting a program to a specific device, we need to specify the target device in the AutoQASM program. We can accomplish this by adding a device argument to the build() call, passing the ARN of the Amazon Braket device (or, optionally, a braket.devices.Device object) that we want to target.

Here we target the Devices.IonQ.ForteEnterprise1 device. When building this program, AutoQASM will validate that (among other things) the contents of any verbatim blocks respect the native gate set and connectivity of the target device.

[4]:
@aq.main
def bell_state():
    with aq.verbatim():
        h("$0")
        cnot("$0", "$1")
    return measure(["$0", "$1"])


try:
    bell_state.build(device=Devices.IonQ.ForteEnterprise1)
except Exception as e:
    print("ERROR:", e)
ERROR: The gate "h" is not a native gate of the target device "Forte Enterprise 1". Only native gates may be used inside a verbatim block. The native gates of the device are: ['gpi', 'gpi2', 'zz']

The validation error indicates that we cannot use h and cnot inside a verbatim block for this device. Instead, we must express our program in terms of the native gates of the device: gpi, gpi2, and zz.

Custom gate definitions using @aq.gate

In order to do this, we can use the @aq.gate decorator in AutoQASM to define custom gates, which we implement in terms of this native gate set. In the Python script ionq_gates.py, we define custom implementations of the h and cnot gates which are built on top of the gpi, gpi2, and zz gates.

[5]:
IPython.display.Code(filename="ionq_gates.py")
[5]:
import numpy as np

import autoqasm as aq
from autoqasm.instructions import gpi, gpi2, zz


@aq.gate
def h(q: aq.Qubit):
    gpi2(q, np.pi / 2)
    gpi(q, 0)


@aq.gate
def u(q: aq.Qubit, a: float, b: float, c: float):
    gpi2(q, a)
    gpi(q, b)
    gpi2(q, c)


@aq.gate
def rx(q: aq.Qubit, theta: float):
    u(q, np.pi / 2, theta / 2 + np.pi / 2, np.pi / 2)


@aq.gate
def ry(q: aq.Qubit, theta: float):
    u(q, np.pi, theta / 2 + np.pi, np.pi)


@aq.gate
def xx(q0: aq.Qubit, q1: aq.Qubit, theta: float):
    h(q0)
    h(q1)
    zz(q0, q1, theta)
    h(q0)
    h(q1)


@aq.gate
def cnot(q0: aq.Qubit, q1: aq.Qubit):
    ry(q0, np.pi / 2)
    xx(q0, q1, np.pi / 2)
    rx(q0, -np.pi / 2)
    rx(q1, -np.pi / 2)
    ry(q0, -np.pi / 2)

We can now use these definitions of the h and cnot gates in our device-targeted program.

[6]:
from ionq_gates import cnot, h  # noqa: F811


@aq.main
def bell_state():
    with aq.verbatim():
        h("$0")
        cnot("$0", "$1")
    return measure(["$0", "$1"])


print(bell_state.build(device=Devices.IonQ.ForteEnterprise1).to_ir())
OPENQASM 3.0;
gate h q {
    gpi2(1.5707963267948966) q;
    gpi(0) q;
}
gate u(a, b, c) q {
    gpi2(a) q;
    gpi(b) q;
    gpi2(c) q;
}
gate ry(theta) q {
    u(3.141592653589793, theta / 2 + 3.141592653589793, 3.141592653589793) q;
}
gate xx(theta) q0, q1 {
    h q0;
    h q1;
    zz(theta) q0, q1;
    h q0;
    h q1;
}
gate rx(theta) q {
    u(1.5707963267948966, theta / 2 + 1.5707963267948966, 1.5707963267948966) q;
}
gate cnot q0, q1 {
    ry(1.5707963267948966) q0;
    xx(1.5707963267948966) q0, q1;
    rx(-1.5707963267948966) q0;
    rx(-1.5707963267948966) q1;
    ry(-1.5707963267948966) q0;
}
output bit[2] return_value;
pragma braket verbatim
box {
    h $0;
    cnot $0, $1;
}
bit[2] __bit_0__ = "00";
__bit_0__[0] = measure $0;
__bit_0__[1] = measure $1;
return_value = __bit_0__;

The device-specific validation now passes, and the program is successfully built. We can see that the generated OpenQASM program contains gate definitions for h, u, ry, rx, xx, and cnot, which correspond to the @aq.gate definitions in ionq_gates.py.

Summary

In this notebook, we demonstrated several aspects of native programming using a two-qubit example program. We showed how to modify a program to use physical qubits instead of virtual qubits. We introduced the usage of verbatim blocks via the aq.verbatim() context, and we demonstrated the device-specific targeting functionality provided by AutoQASM. Finally, we demonstrated the definition of custom gates using the @aq.gate decorator, and we used these gate definitions to implement our example program purely in terms of the native gates of the target device.