# Bloqade SDK for Python ## Introduction **Analog Hamiltonian Simulation (AHS)** has proven itself to be a powerful method of quantum computing -- one that's is 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 to program in terms of the **geometry of your qubits** (individual atoms!) and the waveforms of the lasers that are applied to them. 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 SDK 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. ### What About the Other SDK? For those of you already familiar with QuEra's work you might be wondering "Wait, don't you guys already have an SDK?" That's correct! We have another SDK written in the Julia programming language known as [Bloqade.jl](https://queracomputing.github.io/Bloqade.jl/dev/). Bloqade.jl is focused primarily on high-performance emulation of AHS whereas the **Python version** is focused on **making it as easy as possible to create AHS programs** for hardware. Rest assured, Bloqade.jl will not go anywhere and will in fact be connected to the Python version in the future, giving you both **blazing-fast emulation** and **easy-as-pie hardware-compatible program creation** capabilities. ![](https://hackmd.io/_uploads/By1PLI_Mp.png) *Your reaction to learning QuEra has TWO SDKs* P.S. *From now on, the Bloqade SDK for Python will just be known as "Bloqade" :)* ## Installation Bloqade is a pure Python library, so **installation is as easy as `pip install bloqade`**. Once you've got it installed, you have a variety of options for building your AHS program. We've made these options as friendly and flexible as possible to make your journey into AHS a fruitful one. Without further ado, let's dive into some of the features Bloqade has on offer. ## 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/0.9.0/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. ### Job Management Alright, enough about emulation, how about **submitting to actual hardware**? Does Bloqade make your life any easier on that front? The answer is a resounding yes from us thanks to its built-in quantum job management features. Remember the fact that Bloqade allows for a `.batch_assign()` method which lets you plug in a bunch of different values as well as automatically handle the results from all the tasks? With such a feature, we know you'll probably have to wait a bit to get your results back and that **you'd rather do other things than stay glued to your computer monitor**. We also know that you'll want to be able to reproduce the data you get from such parameter sweeps quickly and easily. As a result Bloqade has the following functionality continuing off our Rabi Oscillation program: ```python! from bloqade import save, load # submit the program to our hardware on the Amazon Braket cloud, saving the submission data # so that Bloqade can reload them whenever you want batch_task = program_with_assignments.braket.aquila().run_async(shots = 100, args=(4, )) save(batch_task, “aquila_submission.json”) # reload the submission data and use `.fetch()` to fetch the results from Amazon Braket loaded_batch_task = load(“aquila_submission.json”) loaded_batch_task.fetch() # Now that you have the results you can easily save them back to file form for later use save(loaded_batch_task, "aquila_results.json") ``` Here you can see we use the `.run_async` API, which **lets the program continue running after submitting** tasks to our hardware on the Amazon Braket cloud. Then we save our submission data into a standard JSON file with `save`. At any point in the future you can `load` the JSON file and then **get your results from Amazon Braket** with `.fetch()`, which gets the task statuses and stores completed task data for you. Alternatively you can use: * `.pull()` which blocks until all tasks are complete on Amazon Braket * `.get_tasks(*status_codes)` which returns a new `Batch` of tasks object with the status code you provide * `.remove_tasks(*status_codes)` which returns a new `Batch` of tasks object without the status code you provide Refer [here](https://bloqade.quera.com/latest/reference/bloqade/task/batch/#bloqade.task.batch.RemoteBatch) for more details on what the various status codes are and what they mean. ## 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. And 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 (modifed to use Bloqade's *emulator* instead of targeting quantum hardware to keep from incurring too much cost): ```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): # 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_max, detuning_max)] # 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: ![](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.