# Parametrized Sequences

```
[1]:
```

```
import numpy as np
import pulser
from pulser import Pulse, Sequence, Register
from pulser.waveforms import RampWaveform, BlackmanWaveform, CompositeWaveform
from pulser.devices import DigitalAnalogDevice
```

From simple sweeps to variational quantum algorithms, it is often the case that one wants to try out multiple pulse sequences that vary only in a few parameters. For this effect, the ability to make a `Sequence`

**parametrized** was developed.

A parametrized `Sequence`

can be used just like a “regular” `Sequence`

, with a few key differences. Initialization and channel declaration, for example, don’t change at all:

```
[2]:
```

```
reg = Register.square(2, prefix="q", spacing=6)
seq = Sequence(reg, DigitalAnalogDevice)
seq.declare_channel("rydberg", "rydberg_global")
seq.declare_channel("raman", "raman_local")
```

## Variables and Parametrized Objects

The defining characteristic of a parametrized `Sequence`

is its use of **variables**. These variables are declared within a `Sequence`

, by calling:

```
[3]:
```

```
Omega_max = seq.declare_variable("Omega_max")
ts = seq.declare_variable("ts", size=2, dtype=int)
last_target = seq.declare_variable("last_target", dtype=int)
```

The returned `Omega_max`

, `ts`

and `last_target`

objects are of type `Variable`

, and are defined by their name, size and data type. In this case, `Omega_max`

is a single variable with `dtype=float`

(the default), `ts`

is an array of two `int`

values and `last_target`

is an `int`

.

These returned `Variable`

objects support simple arithmetic operations (when applicable) and, when of `size > 1`

, even item indexing. Take the following examples:

```
[4]:
```

```
t_rise, t_fall = ts # Unpacking is possible too
U = Omega_max / 2.3
delta_0 = -6 * U
delta_f = 2 * U
t_sweep = (delta_f - delta_0) / (2 * np.pi * 10) * 1000
```

Both the original `Variables`

and the results of these operations serve as valid inputs for `Waveforms`

, `Pulses`

or `Sequence`

-building instructions. We can take `Omega_max`

as an argument for a waveform:

```
[5]:
```

```
pi_wf = BlackmanWaveform.from_max_val(Omega_max, np.pi)
```

or use derived quantities, like `t_rise`

, `t_fall`

, `delta_0`

and `delta_f`

:

```
[6]:
```

```
rise_wf = RampWaveform(t_rise, delta_0, delta_f)
fall_wf = RampWaveform(t_fall, delta_f, delta_0)
rise_fall_wf = CompositeWaveform(rise_wf, fall_wf)
```

These waveforms are *parametrized* objects, so usual attributes like `duration`

or `samples`

are not available, as they depend on the values of the underlying variables. Nonetheless, they can be used as regular waveforms when creating `Pulses`

, which will consequently be *parametrized* too.

```
[7]:
```

```
pi_pulse = Pulse.ConstantDetuning(pi_wf, 0, 0)
rise_fall = Pulse.ConstantAmplitude(Omega_max, rise_fall_wf, 0)
```

## Constructing the Sequence

Upon initialization, a `Sequence`

is, by default, not parametrized. We can check this by calling:

```
[8]:
```

```
seq.is_parametrized()
```

```
[8]:
```

```
False
```

While it is not parametrized, it is just a normal sequence. We can do the usual stuff, like targeting a local channel, adding regular pulses, or plotting the sequence:

```
[9]:
```

```
generic_pulse = Pulse.ConstantPulse(100, 2 * np.pi, 2, 0.0)
seq.add(generic_pulse, "rydberg")
seq.target("q0", "raman")
seq.add(generic_pulse, "raman")
seq.draw()
```

The `Sequence`

becomes parametrized at the moment a parametrized object or variable is given to a sequence-building instruction. For example:

```
[10]:
```

```
seq.target_index(last_target, "raman")
seq.is_parametrized()
```

```
[10]:
```

```
True
```

From this point onward, functionalities like drawing are no longer available, because the instructions start being stored instead of executed on the fly. We can still check the current state of a parametrized sequence by printing it:

```
[11]:
```

```
print(seq)
```

```
Prelude
-------
Channel: rydberg
t: 0 | Initial targets: q0, q1, q2, q3 | Phase Reference: 0.0
t: 0->100 | Pulse(Amp=6.28 rad/µs, Detuning=2 rad/µs, Phase=0) | Targets: q0, q1, q2, q3
Channel: raman
t: 0 | Initial targets: q0 | Phase Reference: 0.0
t: 0->100 | Delay
t: 100->200 | Pulse(Amp=6.28 rad/µs, Detuning=2 rad/µs, Phase=0) | Targets: q0
Stored calls
------------
1. target_index(last_target[0], raman)
```

Naturally, we can also add the parametrized pulses we previously created:

```
[12]:
```

```
seq.add(rise_fall, "rydberg")
seq.add(pi_pulse, "raman")
```

## Building

Once we’re happy with our parametrized sequence, the last step is to build it into a regular sequence. For that, we call the `Sequence.build()`

method, in which we **must attribute values for all the declared variables**:

```
[13]:
```

```
built_seq = seq.build(
Omega_max=2.3 * 2 * np.pi,
ts=[200, 500],
last_target=reg.find_indices(["q3"])[0],
)
built_seq.draw()
```

```
/home/docs/checkouts/readthedocs.org/user_builds/pulser/envs/latest/lib/python3.9/site-packages/pulser/sequence/sequence.py:1319: UserWarning: A duration of 519 ns is not a multiple of the channel's clock period (4 ns). It was rounded up to 520 ns.
self._add(pulse, channel, protocol)
```

And here we have a regular sequence, built from our parametrized sequence. To create a new one with different parameters, we can simply build it again with new values:

```
[14]:
```

```
alt_seq = seq.build(
Omega_max=2 * np.pi, ts=[400, 100], last_target=reg.find_indices(["q2"])[0]
)
alt_seq.draw()
```