From 5b8a34494ec90961a0834257567c017040e81dc9 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Wed, 27 Sep 2023 16:16:14 -0400 Subject: [PATCH 1/3] Add documentation about decorators Co-authored-by: Tim Chen Co-authored-by: Ryan Shaffer <3620100+rmshaffer@users.noreply.github.com> --- src/braket/experimental/autoqasm/README.md | 2 + .../experimental/autoqasm/doc/decorators.md | 123 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 src/braket/experimental/autoqasm/doc/decorators.md diff --git a/src/braket/experimental/autoqasm/README.md b/src/braket/experimental/autoqasm/README.md index 2584ba884..1bc1af135 100644 --- a/src/braket/experimental/autoqasm/README.md +++ b/src/braket/experimental/autoqasm/README.md @@ -98,6 +98,8 @@ task = device.run(my_bell_program, shots=100) result = task.result() ``` +Read more about AutoQASM decorators like `@aq.main` [here](doc/decorators.md). + For more example usage of AutoQASM, visit the [example notebooks](../../../../examples/autoqasm). ## Architecture diff --git a/src/braket/experimental/autoqasm/doc/decorators.md b/src/braket/experimental/autoqasm/doc/decorators.md new file mode 100644 index 000000000..ab75b7907 --- /dev/null +++ b/src/braket/experimental/autoqasm/doc/decorators.md @@ -0,0 +1,123 @@ +# AutoQASM decorators + +AutoQASM function decorators are special wrapper objects that allow us to override the normal behavior of the wrapped code. This is how we are able to hook into normal python control flow statements add them to the quantum program within our wrapped functions, for instance. + +There are a handful of decorators available through AutoQASM. Each one may attach its own special behaviors to the function it wraps. If you are new to AutoQASM, you can just use `@aq.main`! The other decorators unlock further capabilities, when you need it. + +## `@aq.main` + +This decorator marks the entry point to a quantum program. + +You can include gates and pulse control, classical control and subroutine calls. When you call the function wrapped by `@aq.main`, you will get a `Program` object. The `Program` object can execute on Braket devices. The code snippet below creates a quantum program with `@aq.main` and runs it on the `AwsDevice` instantiated as `device`. + +``` +@aq.main(num_qubits=5) +def ghz_state(max_qubits): + """Create a GHZ state from a variable number of qubits.""" + h(0) + for i in aq.range(max_qubits): + cnot(0, i) + measure(list(range(max_qubits))) + +ghz_state_program = ghz_state(max_qubits=5) + +device.run(ghz_state_program) +``` + +When you run your quantum program, the Amazon Braket SDK automatically serializes the program to OpenQASM before sending it to the Amazon Braket service. In AutoQASM, you can optionally view the OpenQASM script of your quantum program before submitting to a device. + +``` +print(ghz_state_program.to_ir()) +``` + +## `@aq.subroutine` + +This decorator declares a function to be a quantum program subroutine. + +Like any subroutine, `@aq.subroutine` is often used to simplify repeated code and increase the readability of a program, and it must be called at least once to have an effect. + +AutoQASM must support typed serialization formats, and so you must provide type hints for the inputs of your subroutine definitions. Qubits are like global registers to our quantum computation, so virtual qubits used in the body of a subroutine definition must be passed as input arguments. + +Our example below uses a subroutine to make two bell states. +``` +@aq.subroutine +def bell(q0: int, q1: int) -> None: + h(q0) + cnot(q0, q1) + + +@aq.main(num_qubits=4) +def two_bell() -> None: + bell(0, 1) + bell(2, 3) + +two_bell_program = two_bell() +``` + +Let's take a look at the serialized output from `two_bell_program.to_ir()`, which shows that the modularity of the subroutine is preserved. + +``` +OPENQASM 3.0; +def bell(int[32] q0, int[32] q1) { + h __qubits__[q0]; + cnot __qubits__[q0], __qubits__[q1]; +} +qubit[4] __qubits__; +bell(0, 1); +bell(2, 3); +``` + +## `@aq.gate` + +Represents a gate definition. + +Gate definitions define higher-level gates with support gates, and are often used to decompose a gate into the native gates of a device. + +The body of a gate definition can only contain gates. Qubits used in the body of a gate definition must be passed as input arguments, with the type hint `aq.Qubit`. Like subroutines, a gate must be called by a main quantum program to have an effect. + +``` +@aq.gate +def ch(q0: aq.Qubit, q1: aq.Qubit): + """Define a controlled-Hadamard gate.""" + ry(q1, -math.pi / 4) + cz(q0, q1) + ry(q1, math.pi / 4) + +@aq.main(num_qubits=2) +def main(): + h(0) + ch(0, 1) + +main_program = main() +``` + + +## `@aq.gate_calibration` + +This decorator allows you to register a calibration for a gate. A gate calibration is a device-specific, low-level, pulse implementation for a logical gate operation. + +At the pulse level, qubits are no longer interchangable. Each one has unique properties. Thus, a gate calibration is usually defined for a concrete set of qubits and parameters, but you can use input arguments to your function as well. + +The body of a gate calibration must only contain pulse operations. This decorator requires one input arguments to specify the `Gate` that the calibration will be registered to. Concrete values for the qubits and parameters are supplied as keyword arguments to the decorator. +The union of the arguments of the decorator and the decorated function must match the arguments of the gate to be implemented. + +For example, the gate `rx` takes two arguments, target and angle. Each arguments must be either set in the decorator or declared as an input parameter to the decorated function. To add the gate calibration to your program, use the `with_calibrations` method of the main program. + +``` +# This calibration only applies to physical qubit zero, so we +# mark that in the decorator call +@aq.gate_calibration(implements=rx, target="$0") +def cal_1(angle: float): + # The calibration is applicable for any rotation angle, + # so we accept it as an input argument + pulse.barrier("$0") + pulse.shift_frequency(q0_rf_frame, -321047.14178613486) + pulse.play(q0_rf_frame, waveform(angle)) + pulse.shift_frequency(q0_rf_frame, 321047.14178613486) + pulse.barrier("$0") + +@aq.main +def my_program(): + rx(0, 0.123) + measure(0) +``` From 272b83e9a2660dac2c344b8c2aecbb4c09fc43b5 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 28 Sep 2023 13:39:35 -0400 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Ryan Shaffer <3620100+rmshaffer@users.noreply.github.com> --- .../experimental/autoqasm/doc/decorators.md | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/braket/experimental/autoqasm/doc/decorators.md b/src/braket/experimental/autoqasm/doc/decorators.md index ab75b7907..4aaf70e72 100644 --- a/src/braket/experimental/autoqasm/doc/decorators.md +++ b/src/braket/experimental/autoqasm/doc/decorators.md @@ -1,14 +1,14 @@ # AutoQASM decorators -AutoQASM function decorators are special wrapper objects that allow us to override the normal behavior of the wrapped code. This is how we are able to hook into normal python control flow statements add them to the quantum program within our wrapped functions, for instance. +AutoQASM function decorators allow us to override the normal behavior of the decorated code. This is how we are able to hook into normal Python control flow statements and add them to the quantum program within our wrapped functions, for instance. -There are a handful of decorators available through AutoQASM. Each one may attach its own special behaviors to the function it wraps. If you are new to AutoQASM, you can just use `@aq.main`! The other decorators unlock further capabilities, when you need it. +There are a handful of decorators available through AutoQASM. Each one attaches its own special behaviors to the function it wraps. If you are new to AutoQASM, you can just use `@aq.main`! The other decorators unlock further capabilities, when you need it. ## `@aq.main` This decorator marks the entry point to a quantum program. -You can include gates and pulse control, classical control and subroutine calls. When you call the function wrapped by `@aq.main`, you will get a `Program` object. The `Program` object can execute on Braket devices. The code snippet below creates a quantum program with `@aq.main` and runs it on the `AwsDevice` instantiated as `device`. +You can include gates, pulse control, classical control and subroutine calls. When you call the function wrapped by `@aq.main`, you will get a `Program` object. The `Program` object can be executed on [devices available through Amazon Braket](https://docs.aws.amazon.com/braket/latest/developerguide/braket-devices.html), including local simulators. The code snippet below creates a quantum program with `@aq.main` and runs it on the `Device` instantiated as `device`. ``` @aq.main(num_qubits=5) @@ -24,7 +24,7 @@ ghz_state_program = ghz_state(max_qubits=5) device.run(ghz_state_program) ``` -When you run your quantum program, the Amazon Braket SDK automatically serializes the program to OpenQASM before sending it to the Amazon Braket service. In AutoQASM, you can optionally view the OpenQASM script of your quantum program before submitting to a device. +When you run your quantum program, the Amazon Braket SDK automatically serializes the program to OpenQASM before sending it to the local simulator or the Amazon Braket service. In AutoQASM, you can optionally view the OpenQASM script of your quantum program before submitting to a device by calling `to_ir()` on the `Program` object. ``` print(ghz_state_program.to_ir()) @@ -34,11 +34,11 @@ print(ghz_state_program.to_ir()) This decorator declares a function to be a quantum program subroutine. -Like any subroutine, `@aq.subroutine` is often used to simplify repeated code and increase the readability of a program, and it must be called at least once to have an effect. +Like any subroutine, `@aq.subroutine` is often used to simplify repeated code and increase the readability of a program. A subroutine must be called at least once from within an `@aq.main` function or another `@aq.subroutine` function in order to be included in a program. -AutoQASM must support typed serialization formats, and so you must provide type hints for the inputs of your subroutine definitions. Qubits are like global registers to our quantum computation, so virtual qubits used in the body of a subroutine definition must be passed as input arguments. +Because AutoQASM supports strongly-typed serialization formats such as OpenQASM, you must provide type hints for the inputs of your subroutine definitions. -Our example below uses a subroutine to make two bell states. +Our example below uses a subroutine to make Bell states on two pairs of qubits. ``` @aq.subroutine def bell(q0: int, q1: int) -> None: @@ -71,9 +71,9 @@ bell(2, 3); Represents a gate definition. -Gate definitions define higher-level gates with support gates, and are often used to decompose a gate into the native gates of a device. +Gate definitions define higher-level gates in terms of other gates, and are often used to decompose a gate into the native gates of a device. -The body of a gate definition can only contain gates. Qubits used in the body of a gate definition must be passed as input arguments, with the type hint `aq.Qubit`. Like subroutines, a gate must be called by a main quantum program to have an effect. +The body of a gate definition can only contain gates. Qubits used in the body of a gate definition must be passed as input arguments, with the type hint `aq.Qubit`. Like subroutines, a gate definition must be called from within the context of a main quantum program or subroutine in order to be included in the program. ``` @aq.gate @@ -98,8 +98,10 @@ This decorator allows you to register a calibration for a gate. A gate calibrati At the pulse level, qubits are no longer interchangable. Each one has unique properties. Thus, a gate calibration is usually defined for a concrete set of qubits and parameters, but you can use input arguments to your function as well. -The body of a gate calibration must only contain pulse operations. This decorator requires one input arguments to specify the `Gate` that the calibration will be registered to. Concrete values for the qubits and parameters are supplied as keyword arguments to the decorator. -The union of the arguments of the decorator and the decorated function must match the arguments of the gate to be implemented. +The body of a function decorated with `@aq.gate_calibration` must only contain pulse operations. + +The first argument to the `@aq.gate_calibration` decorator must be the gate function that the calibration will be registered to. Concrete values for the qubits and parameters are supplied as keyword arguments to the decorator. +Every qubit and angle parameter of the gate being implemented must appear either as an argument to the `@aq.gate_calibration` decorator, or as a parameter of the decorated function. For example, the gate `rx` takes two arguments, target and angle. Each arguments must be either set in the decorator or declared as an input parameter to the decorated function. To add the gate calibration to your program, use the `with_calibrations` method of the main program. @@ -118,6 +120,6 @@ def cal_1(angle: float): @aq.main def my_program(): - rx(0, 0.123) - measure(0) + rx("$0", 0.123) + measure("$0") ``` From 0fc286ce485d307dd5e19c39fbea22301ffd2152 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 28 Sep 2023 14:14:05 -0400 Subject: [PATCH 3/3] Respond to CR --- src/braket/experimental/autoqasm/doc/decorators.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/braket/experimental/autoqasm/doc/decorators.md b/src/braket/experimental/autoqasm/doc/decorators.md index 4aaf70e72..494cb90c1 100644 --- a/src/braket/experimental/autoqasm/doc/decorators.md +++ b/src/braket/experimental/autoqasm/doc/decorators.md @@ -103,7 +103,7 @@ The body of a function decorated with `@aq.gate_calibration` must only contain p The first argument to the `@aq.gate_calibration` decorator must be the gate function that the calibration will be registered to. Concrete values for the qubits and parameters are supplied as keyword arguments to the decorator. Every qubit and angle parameter of the gate being implemented must appear either as an argument to the `@aq.gate_calibration` decorator, or as a parameter of the decorated function. -For example, the gate `rx` takes two arguments, target and angle. Each arguments must be either set in the decorator or declared as an input parameter to the decorated function. To add the gate calibration to your program, use the `with_calibrations` method of the main program. +For example, the gate `rx` takes two arguments, target and angle. Each arguments must be either set in the decorator or declared as an input parameter to the decorated function. ``` # This calibration only applies to physical qubit zero, so we @@ -117,9 +117,15 @@ def cal_1(angle: float): pulse.play(q0_rf_frame, waveform(angle)) pulse.shift_frequency(q0_rf_frame, 321047.14178613486) pulse.barrier("$0") - +``` + +To add the gate calibration to your program, use the `with_calibrations` method of the main program. + +``` @aq.main def my_program(): rx("$0", 0.123) measure("$0") + +my_program().with_calibrations([cal_1]) ```