Skip to content

Commit

Permalink
Add documentation about decorators (#714)
Browse files Browse the repository at this point in the history
* Add documentation about decorators

Co-authored-by: Tim Chen <yitchen-tim>
Co-authored-by: Ryan Shaffer <[email protected]>

* Apply suggestions from code review

Co-authored-by: Ryan Shaffer <[email protected]>

* Respond to CR

---------

Co-authored-by: Ryan Shaffer <[email protected]>
  • Loading branch information
laurencap and rmshaffer authored Sep 28, 2023
1 parent 92f131d commit 9cf49c2
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/braket/experimental/autoqasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
131 changes: 131 additions & 0 deletions src/braket/experimental/autoqasm/doc/decorators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# AutoQASM decorators

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 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, 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)
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 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())
```

## `@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. 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.

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 Bell states on two pairs of qubits.
```
@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 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 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
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 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.

```
# 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")
```

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])
```

0 comments on commit 9cf49c2

Please sign in to comment.