Advanced Usage
Geometry Concepts¶
In Getting Started, we discussed the parameterization of waveforms inside a Rydberg/hyperfine drive. We have also provided infrastructure that allows you to parameterize the position of the atoms/sites in your atom array. There are a few methods to do this:
Scaling Your Lattice¶
Every AtomArrangement
object has an option to scale
, which applies a multiplicative factor to all site coordinates. To parameterize this scaling, you can either pass a literal value, a string, or a scalar expression:
new_geometry_1 = my_geometry.scale(1.0)
new_geometry_2 = my_geometry.scale('scale_factor')
scale_expr = var("param") / 2 + var("starting_scale")
new_geometry_3 = my_geometry.scale(scalar_expr)
Parameterize the Position of Individual Sites¶
Suppose you are constructing a set of sites using the start.add_position(...)
method. In that case, you can pass a literal value, a string, or a scalar expression to the position
argument:
x_div_2 = var("x") / 2
y_dic_2 = var("y") / 2
start.add_position([(0, 1), ("x", "y"), (x_div_2, y_div_2)])
Parallelize Over Space¶
For AHS programs with a few sites/atoms, you can (and should) make full use of the user area allowed by QuEra's AHS devices. While the Rydberg interaction between the atoms decays as a power-law, it decays fast enough for you to separate small clusters of atom sites cleanly and be assured they will not interact and entangle. This fact allows you to parallelize your program over space. We have added this functionality into Bloqade by simply calling the .parallelize(spacing)
method, where spacing
is a real value, the spacing between the bounding boxes of the clusters. For example, calling .parallelize(20.0)
would ensure that the atoms in other clusters are at least 20 um away from each other.
When fetching the results and generating the report, the shot results will be organized by the cluster to which they belong. The methods of the Report
object will always merge all the cluster shots so you can analyze them as if you're analyzing a single cluster.
Programming A Local Drive¶
In the Getting Started section, you might have noticed that we used ...uniform...
when defining the various drives applied to the atoms. This syntax refers to one of three ways of expressing the spatial modulation of the waveform. What do we mean by spatial modulation? In this case, you can think of a drive having a temporal component, e.g., the waveform, and a spatial part, which we call spatial modulation. The spatial modulation is a scale factor applied to the waveform when acting on a particular site. For example, if we have two atoms and we have a spatial modulation for site-0
which is 0.1
and for site-1
which is 0.2
, then the waveform will be scaled by 0.1
when acting on-site-0
and 0.2
when applied to site-1
.
Intuitively, uniform
spatial modulation simply means that regardless of the atom, the waveform value is always the same. The other two options for defining spatial modulations are: scale
and location
. They have two distinct use cases:
scale
Method¶
When calling the scale
method, you can pass either a list of real values or a string. The list of values defines the scaling for every atom in the system, so the number of elements must equal the number of sites in your geometry. The string is a placeholder that allows you to define that mask as a list of values by passing them in through assign
, batch_assign
, or args
. For assign
and batch_assign
, you can pass a list or a list of lists, respectively. On the other hand, args
, the list of values, must be inserted into the args
tuple when calling run
or run_async
, for example, let's take a two-atom program:
from bloqade import start
program = (
start.add_position([(0, 0), (0, 5)])
.rydberg.detuning.scale("local_detuning")
.piecewise_linear([0.1, 1.0, 0.1], [0, "detuning", "detuning", 0])
.amplitude.uniform.piecewise_linear([0.1, 1.0, 0.1], [0, 15, 15, 0])
.args(["local_detuning", "detuning"])
.bloqade.python()
)
To call the run
method properly, you must unpack the spatial modulation list into the tuple, e.g.
location
Method¶
The location
method takes two arguments; the second is optional. The first argument is an integer or a list of integers corresponding to the site(s) to which you want to apply the spatial modulation. The second argument is the value you wish to apply to the site(s). If you pass a list of integers, you must pass a list of values for the second argument, while if you pass a single integer, you must pass a single value. The interpretation is that for any sites not specified in the first argument, the spatial modulation is 0.0
. Note that the values you pass in can be a real value, a string (for a variable), or a scalar expression. Below is an example using location
:
program = (
start.add_position([(0, 0), (0, 5)])
.rydberg.detuning.location([0, 1], ["detuning_0", "detuning_1"])
.piecewise_linear([0.1, 1.0, 0.1], [0, "detuning", "detuning", 0])
.amplitude.location(0)
.piecewise_linear([0.1, 1.0, 0.1], [0, 15, 15, 0])
.location(0, "rabi_scale")
.piecewise_linear([0.1, 1.0], [0, 15, 0])
)
Slicing Waveforms¶
Sometimes, you want to run a program at intermediate time points as a parameter scan. To facilitate this, we have added the slice
method that can be called during the waveform construction. The slice
method takes two arguments: start
and stop
. The start
argument determines the new starting point for the sliced waveform. At the same time, stop
specifies the new stopping time for the waveform. A valid slice must have start <= stop
, start >= 0
, and stop <= duration
, where duration
is the duration of the waveform. Both arguments can be Scalar expressions as well as concrete values.
record
is another useful feature we have implemented to be used with slice
. For example, if you slice a waveform, you may or may not know the value of the waveform at the end of the resulting waveform from the slice. On the other hand, you need to be able to use that slice as a base for a new waveform that is continuous, which requires knowledge of the value of the sliced waveform at the end of the slice. A common use case for this is to take a Rabi drive and ensure it is 0
at the end of the protocol to make it compatible with hardware. A great example of this exact use case is Quantum Scar Dynamics Tutorial, which goes through exactly how to use slice
and record
together.
Python Functions as Waveforms¶
In Bloqade, we provide a set of commonly used waveform shapes for hardware applications. However, we also allow users to pass various Python functions as Waveforms! There are two ways to go about using Python functions as waveforms. The first involves passing the function into the fn()
method when building your program. The second involves the bloqade.waveform
decorator on your function, turning it into a waveform
object.
These are super convenient for emulation. However, these waveforms can't be executed on quantum hardware. As such, we provide the sample
method that allows you to easily discretize your waveform into a form compatible with the hardware. This method replaces your waveform with a piecewise linear or piecewise constant form. The Rabi amplitude and detuning both expect piecewise linear while the phase expects piecewise constant for the waveforms. If you are using the builder method to build your program, you do not need to specify the kind of interpolation explicitly; Bloqade can deduce from the build what type of interpolation to do, but if you are using the waveform
decoration, you need to specify the kind of interpolation, either 'linear'
or 'constant'
.
An example on Floquet Dynamics is an excellent place to start with these particular features.