owned this note
owned this note
Published
Linked with GitHub
# Bloqade & Amazon Braket
<center>
<img src="https://hackmd.io/_uploads/rkpuzHZ4T.png" alt="Basic Model of rydberg driving" width="600"/>
<br>
In Aquila the qubits are encoded in a shell electron transition between its ground state and a highly excited state. This excitation is called a Rydberg excitation.
</center>
\
\
**Analog Hamiltonian Simulation (AHS)** has proven itself to be a powerful method of quantum computing -- one that's well-suited for solving optimization problems and performing computationally difficult simulations of other quantum systems.
Unlike its counterpart digital/gate-based quantum computing, where you think of your programs in terms of unitary operations akin to classical gates, AHS switches things up where you program in terms of the **geometry of your qubits** (individual atoms!) and the waveforms of the lasers that are applied to them. With the laser control you can drive the atoms into excited states which allows them to interact and entangle via the Rydberg Blockade.
<center>
<img src="https://hackmd.io/_uploads/HkjeGHbVT.png", width="600">
<br>
Visual represenation of an <b>Analog Hamiltonian Simulation</b> . Instead of using descrete gates the program is implemented via a continuous time evolution of a time dependent hamiltonian
</center>
\
\
We believe AHS is a useful step on the path universal to fault tolerant quantum computation, but one that requires **a different set of tools** than we're used to in gate-based QC.
That's why we're psyched to announce the release of the **Bloqade for Python**! We got some super valuable feedback from the community and built that into a set of tools for programming AHS devices.
We figure, hey, as the first ever provider of **publicly cloud-accessible neutral atom hardware** (available on Amazon Braket), we're in a great position to build tools that put the power of AHS hardware at your fingertips.
![image210](https://hackmd.io/_uploads/H1wlbLzNp.png)
## Cool Things You Can Do with Bloqade
Some of Bloqade's main features (Smart Documentation, Parametrized Programs, and Integrated Visualization tools) have already been summarized in its [initial launch blog post](https://bloqade.quera.com/latest/blog/2023/posts/bloqade-release/). You can also find some great examples of Bloqade programs on its **[tutorials page](https://queracomputing.github.io/bloqade-python-examples/latest/) for inspiration**.
Here we'll take the opportunity to highlight for you some of its other powerful features as well.
### A Prototypical Example
Let's pair our neat features with a neat example:
```python!
from bloqade import start, cast
# Define the times for our waveform as variables we can assign values to later
durations = cast(["ramp_time", "run_time", "ramp_time"])
rabi_oscillations_program = (
start.add_position((0,0))
.rydberg.rabi.amplitude.uniform.piecewise_linear(
durations=durations, values=[0, "rabi_ampl", "rabi_ampl", 0]
)
.detuning.uniform.constant(duration=sum(durations), value="detuning_value")
)
```
In this example we apply the right waveforms to get [Rabi oscillations](https://en.wikipedia.org/wiki/Rabi_cycle) from a single atom. We start by defining the **position** of our atom with `add_position` and then specifying the **Rabi frequency** (`.rabi`) and **Detuning** (`.detuning`) waveforms. You might notice the mix of strings and numbers, what's up with that?
One of Bloqade's flagship features is that you can ***parametrize* your programs** by defining variables that can be assigned values later. The next section will dive into why exactly these are so neat to have.
### Parametrized Programs
Many near-term applications for QC as well as AHS require some notion of parameterized programs. The key idea is that you either:
* Want to explore how your program behaves across a **range of values** for a certain parameter (what happens if I put the atoms further/closer apart? What happens if I gradually increase the amplitude of a waveform?)
* Want to figure out the **optimal values for your program** to achieve some objective (e.g. be able to try a new value on the fly and tweak that value based on prior results)
To do the above, you need a program that has parameters you can tweak either during your program definition (the first point) or on the fly in a hybrid quantum-classical algorithm (our second point, and one which you'll see is quite easy to do with Amazon Braket Hybrid Jobs).
Revisiting our original Rabi Oscillation program we can assign values like so:
```python
import numpy as np
program_with_assignments = (
rabi_oscillations_program.assign(ramp_time=0.06, run_time=3.0)
.batch_assign(detuning_value=np.linspace(0, 10, 15))
.args(["rabi_ampl"])
)
```
You can see there are three options for variable assignment:
1. You **assign a single value** to a single variable with `.assign()`. In this case we set our waveform ramp time to be 0.06 microseconds and the run time (where we hold the Rabi frequency constant) for 3.0 microseconds.
2. You **assign multiple values** to a single variable with `.batch_assign()`.
3. You **defer assignment until runtime** of a variable with `.args()`. This means that you wait until execution of your program to pass in a value versus statically defining it.
The second option might seem a bit odd, but this is where Bloqade really shines: given multiple values for a single variable, Bloqade can **automatically generate multiple quantum tasks** that can either run on Amazon Braket or our eye-wateringly fast emulator you'll soon learn about.
Furthermore, Bloqade **automatically handles compiling the results** for you so no more keeping track of individual task IDs or forgetting which task belongs to which parameter value. This is great for experimenting with different values and seeing how the behavior of your program changes (a "parameter sweep" in official parlance).
The third option harkens back to the idea of **optimizing your program for a certain objective**. In the previously mentioned hybrid algorithm you have a quantum algorithm whose results are passed to a classical computer that, after some number crunching, tweaks the parameters of your quantum algorithm, working towards some optimal behavior. In this case we want to keep the same AHS program structure but just pass in our values later.
But what's the benefit of all these variables and parameters if you don't have a place to test them all out? Fret not, Bloqade also has you covered on that front.
### Targeting Multiple Backends
All Bloqade programs can be targeted to **multiple emulation and hardware backends** very easily using its dot-based chaining syntax. To select `braket` as your service simply select the `braket` attribute of your program. At this stage there will be two methods available for you, `aquila()` and `local_emulator()`.
Each backend has different restrictions in terms of the types of AHS programs that can be run, e.g. emulators will have a lot more flexibility than what hardware will accept.
Depending on the backend, there are also either one or two methods for executing your program. For **cloud devices**, Bloqade has an API for both **asynchronous** (`run_async()`) **and** **synchronous** (`run()`) method for executing the job. Local emulator backends only support the `run()` API.
Now let's revisit the meaning of `args()` assignment. Every execution method has an `args()` argument, this is where you can **specify the values of the parameters** defined in `args()` when defining your program. The order of the arguments in the `args()` tuple is the order of the variables specified in the `args()` method.
Continuing our Rabi Oscillation example program we can see all this come together nicely:
```python!
results = program_with_assignments.braket.local_emulator().run(shots=100, args=(4,))
```
Alternatively, you can also do the following:
```python!
executable_program = program_with_assignments.braket.local_emulator()
results = executable_program(4, shots=100)
```
Throughout this example we've relied on the Braket local emulator. However, Bloqade has another trick up its sleeve for emulation: its own blazing fast emulator!
### Built-In Bloqade Emulator
While Bloqade.jl holds the crown in terms of performance, the Python version of the state vector simulator has been carefully optimized to **push the boundaries of what Python can do**. The emulator supports both two- and three-level atom configurations, along with global and local driving and support for the blockade subspace for our neutral atom experts.
The blockade subspace and matrix calculations are **nearly optimal in both memory and time** -- we wrote them in pure NumPy and SciPy. We also have basic Numba JIT compiled sparse operations that further optimize memory when solving the time-dependent Schrödinger equation.
We hope our Python emulator will allow you to explore a wide variety of applications for neutral atoms and prototype some neat new algorithms with AHS.
## Adaptive Jobs
Remember those parameterized algorithms we mentioned earlier? Well it's now easier than ever to develop and execute those algorithms thanks to Braket Hybrid Jobs, where **all you need to do is add the `@hybrid_job` decorator** to your Python code. This submits the code to Amazon Braket which handles both which QPU to target as well as the classical resources with minimal additional code.
Even better, you can combine the `@hybrid_job` decorator with Bloqade's parameterized programs to **develop hybrid algorithms for neutral atom hardware** faster than you can say "Neutral atom hybrid algorithms are awesome!"
Okay maybe not that fast, but you'll be on your feet in no time!
We'll prove this claim with an example where we want to find an optimal detuning waveform on top of a fixed Rabi frequency that solve the **Maximum Independent Set (MIS) problem** on our neutral atom quantum computer *Aquila*. MIS is a Combinatorial Optimization problem where given a graph you want to find the largest set of nodes such that no two nodes share an edge. MIS is NP-Hard and has a number of applications in scheduling and resource allocation.
Additionally, **MIS a very natural problem for the AHS architecture** (check out this great [independent blog post](https://medium.com/the-modern-scientist/testing-maximal-independent-set-mis-with-queras-aquila-345ff32f26bf) about it!), as the Rydberg blockade effect causes the ground state of a collection of atoms within close enough proximity of each other to map to the problem on geometric graphs.
The majority of the source code for this example as well as a detailed explanation can be [found here](https://github.com/QuEraComputing/QuEra-braket-examples/blob/main/HybridJob/hybrid-job.ipynb), but we want to highlight just how easy it is use `@hybrid_job` with a snippet from the example:
```python!
from braket.jobs import (
InstanceConfig,
hybrid_job,
)
@hybrid_job(
# select the quantum hardware your hybrid algorithm will target
device = Devices.QuEra.Aquila,
# Let Amazon Braket know what dependencies we need
dependencies="requirements.txt",
# select an Amazon EC2 instance for classical computation
instance_config=InstanceConfig("ml.m5.large"),
)
# accept a Bloqade program where the "args" method was used to declare that
# a set of variables (in our case the parameterized detuning waveform) should have
# on the fly assignment.
def run_algo(assigned_program, n_calls=10, n_shots=10, detuning_bound=100):
# Use scikit-optimize's bayesian optimization by gaussian processes function
from skopt import gp_minimize
# Keep track of Hybrid Job progress and cost with a custom cost function
program_with_backend = assigned_program.braket.aquila()
wrapped_cost_func = CostFuncWrapper(program_with_backend, shots=n_shots)
# establish boundaries for the optimizer on how high/low a detuning value
# each variable can take by first finding the number of parameters in
# our program and then repeating the constraint that number of times
n_params = len(program_with_backend.params.args_list)
bounds = n_params * [(-detuning_bound, detuning_bound)]
# The optimizer will call the cost function which in turn,
# calls the actual program to execute on Aquila
result = gp_minimize(
wrapped_cost_func,
bounds,
callback=wrapped_cost_func.callback,
n_calls=n_calls,
)
# Associate with each detuning variable declared earlier a result from the optimizer,
# so we can pass the values back to our AHS program
detuning_values = {
var.name: val for var, val in zip(detuning_vars, result.x)
}
return detuning_values
```
For the AHS program we've parameterized the detuning waveform so at each time step there is an associated variable which defines what values the detuning can take. This program is passed into a function `run_algo()` function with the `@hybrid_job` decorator. Note that in the decorator we specify **which quantum device to target** along with letting Amazon Braket know **which dependencies we need** and an Amazon EC2 instance.
The actual thing that calls our program to run on *Aquila* is contained in a cost function instantiated by `CostFuncWrapper` that is later passed into scikit-optimize's `gp_minimize` function to help us **find the optimal detuning values** per each variable. The cost function we've defined also takes advantage of functionality from `braket.jobs` to log the progress of our program.
Using the Bloqade Emulator with `@hybrid_job` (you can do this by setting the device ARN in the `@hybrid_job` decorator to `None` and changing the backend our AHS program uses to the Bloqade Python emulator) we obtain the following results after using the optimized detuning waveform on the Bloqade Emulator:
![](https://hackmd.io/_uploads/HyjpfOTzp.png)
The leftmost panel shows the probabilities for each measurement while the center most panel shows what the actual geometric configuration looks like for a selected measurement. We've sleected the measurement with the highest probability and we see that its **geometric configuration is indeed the MIS of the graph** (the black nodes the atoms are in the Rydberg state and vice versa). The black nodes share no edges (first criteria for MIS) and it's the largest number of nodes possible that don't share an edge (second criteria for MIS).
All in all, we like to think this nice pairing of Bloqade with `@hybrid_job` is one of those few instances in life where you can have your cake and eat it too.