<jupyter_start><jupyter_text>Pulse Sequence Creation<jupyter_code>import numpy as np
import pulser
from pprint import pprint
from pulser import Pulse, Sequence, Register<jupyter_output><empty_output><jupyter_text>1. Creating the `Register` The `Register` defines the positions of the atoms and their names of each one. There are multiple ways of defining creating a `Register`, the most customizable one being to create a dictionary that associates a name (the key) to a cooordinate (the value).<jupyter_code>L = 4
square = np.array([[i, j] for i in range(L) for j in range(L)], dtype=float)
square -= np.mean(square, axis=0)
square *= 5

qubits = dict(enumerate(square))
reg = Register(qubits)<jupyter_output><empty_output><jupyter_text>The `Register` class provides some useful features, like the ability to visualise the array and to rotate it.<jupyter_code>reg1 = Register(qubits)  # Copy of 'reg' to keep the original intact
print("The original array:")
reg1.draw()
reg1.rotate(45)  # Rotate by 45 degrees
print("The rotated array:")
reg1.draw()<jupyter_output><empty_output><jupyter_text>If one doesn't particularly care about the name given to the qubits, one can also create a `Register` just from a list of coordinates (using the `Register.from_coordinates` class method). In this case, the qubit ID's are just numbered, starting from 0, in the order they are provided in, with the option of adding a common prefix before each number. Also, it automatically centers the entire array around the origin, an option that can be disabled if desired.<jupyter_code>reg2 = Register.from_coordinates(
    square, prefix="q"
)  # All qubit IDs will start with 'q'
reg2.draw()<jupyter_output><empty_output><jupyter_text>Furthermore, there are also built-in class methods from creation of common array patterns, namely:- Square lattices in rectangular or square shapes- Triangular latticesWe could, thus, create the same square array as before by doing:<jupyter_code>reg3 = Register.square(4, spacing=5)  # 4x4 array with atoms 5 um apart
reg3.draw()<jupyter_output><empty_output><jupyter_text>2. Initializing the Sequence To create a `Sequence`, one has to provide it with the `Register` instance and the device in which the sequence will be executed. The chosen device will dicate whether the register is valid or not.We import the device (in this case, `Chadoq2`) from `pulser.devices` and initialize our sequence with the freshly created register:<jupyter_code>from pulser.devices import Chadoq2

seq = Sequence(reg, Chadoq2)<jupyter_output><empty_output><jupyter_text>3. Declaring the channels that will be used Inspecting what channels are available on this device:<jupyter_code>seq.available_channels<jupyter_output><empty_output><jupyter_text>We're going to choose the `'rydberg_local'` and `'raman_local'` channels. Note how a declared channel is no longer reported as available.<jupyter_code>seq.declare_channel("ch0", "raman_local")
print("Available channels after declaring 'ch0':")
pprint(seq.available_channels)

seq.declare_channel("ch1", "rydberg_local", initial_target=4)
print("\nAvailable channels after declaring 'ch1':")
pprint(seq.available_channels)<jupyter_output><empty_output><jupyter_text>At any time, we can also consult which channels were declared, their specifications and the name they were given by calling:<jupyter_code>seq.declared_channels<jupyter_output><empty_output><jupyter_text>4. Composing the Sequence Every channel needs to start with a target. For `Global` channels this is predefined to be all qubits in the device, but for `Local` channels this has to be defined. This initial target can be set through at channel declaration (see how `'ch1'` was set to target qubit `4`), or it can be done through the standard `target` instruction.<jupyter_code>seq.target(1, "ch0")<jupyter_output><empty_output><jupyter_text>Now both channels have an initial target, so we can start building the sequence. Let's start by creating a simple pulse with a constant Rabi frequency of 2 rad/µs and a constant detuning of -10 rad/µs that lasts 200 ns.<jupyter_code>simple_pulse = Pulse.ConstantPulse(200, 2, -10, 0)<jupyter_output><empty_output><jupyter_text>Let's add this pulse to `'ch0'`:<jupyter_code>seq.add(simple_pulse, "ch0")<jupyter_output><empty_output><jupyter_text>Now, say we want to idle `'ch1'` for 100 ns while `'ch0'` is doing its pulse. We do that by calling:<jupyter_code>seq.delay(100, "ch1")<jupyter_output><empty_output><jupyter_text>Next, we want to create a more complex pulse to add to `'ch1'`, where the amplitude and the detuning are not constant. To do that, we use `Waveform`s:<jupyter_code>from pulser.waveforms import RampWaveform, BlackmanWaveform

duration = 1000
amp_wf = BlackmanWaveform(duration, np.pi / 2)  # Duration: 1000 ns, Area: pi/2
detuning_wf = RampWaveform(
    duration, -20, 20
)  # Duration: 1000ns, linear sweep from -20 to 20 rad/µs<jupyter_output><empty_output><jupyter_text>We can visualize a waveform by calling:<jupyter_code>amp_wf.draw()<jupyter_output><empty_output><jupyter_text>Also, it is often convenient to find the integral of a waveform, which can be obtain by calling:<jupyter_code>amp_wf.integral  # dimensionless<jupyter_output><empty_output><jupyter_text>We then create the pulse with the waveforms instead of fixed values and we can also visualize it:<jupyter_code>complex_pulse = Pulse(amp_wf, detuning_wf, phase=0)
complex_pulse.draw()<jupyter_output><empty_output><jupyter_text>When we're satisfied, we can then add it to a channel:<jupyter_code>seq.add(complex_pulse, "ch1")<jupyter_output><empty_output><jupyter_text>Let's check the schedule to see how this is looking:<jupyter_code>print(seq)<jupyter_output><empty_output><jupyter_text>We can also draw the sequence, for a more visual representation:<jupyter_code>seq.draw()<jupyter_output><empty_output><jupyter_text>Now, let's see how the Sequence builder handles conflicts (i.e. two channels acting on the same qubit at once). We're going to add a `complex_pulse`to `'ch0'`, but now we want to target it to qubit `4`, which is the same target of `'ch1'`.<jupyter_code>seq.target(4, "ch0")
seq.add(complex_pulse, "ch0")

print("Current Schedule:")
print(seq)
seq.draw()<jupyter_output><empty_output><jupyter_text>By looking at the current schedule, we can see that `'ch0'` was delayed from `ti=220` to `tf=1100`, and only then was the `complex_pulse` added. The reason for this is simple: it had to wait for the pulse on `ch1`, also targeted to qubit `4`, to finish before it could apply its own. It behaved this way because, in `Sequence.add` there is a default argument `protocol='min-delay`. Another protocol for pulse addition is `wait-for-all`, which makes the new pulse wait even if there is no conflict. Let's remove the conflict and add two `simple_pulse`s to `ch1` with the different protocols to see how the compare.<jupyter_code>seq.target(0, "ch1")
seq.add(simple_pulse, "ch1", protocol="min-delay")
seq.add(simple_pulse, "ch1", protocol="wait-for-all")

print("Current Schedule:")
print(seq)
seq.draw()<jupyter_output><empty_output><jupyter_text>Because we removed the conflict by changing the target of `ch1` to qubit `0`, we see that the first pulse was added without delay from `ti=1100` to `tf=1300` (i.e. while `complex_pulse` is still running in `ch0`). However, once we changed to `protocol='wait-for-all'`, there is now a delay (from `ti=1300` to `tf=2100`) that idles `ch1` until `ch0`is finished with its pulse, even though there was no conflict. The third protocol is called `'no-delay'` which, as the name implies, never delays the channel where the pulse is being added, even if that means introducing a conflict.<jupyter_code>seq.target(0, "ch0")
seq.add(complex_pulse, "ch0", protocol="no-delay")

print("Current Schedule:")
print(seq)
seq.draw()<jupyter_output><empty_output><jupyter_text>With this protocol, it is possible (though not advised), to create an overlap where multiple channels can be acting on the same qubit at the same time. Here, we can see that both act on qubit `0` from `ti=2100` to `tf=2300`.  5. Measurement To finish a sequence, we measure it. A measurement signals the end of a sequence, so after it no more changes are possible. We can measure a sequence by calling:<jupyter_code>seq.measure(basis="ground-rydberg")<jupyter_output><empty_output><jupyter_text>When measuring, one has to select the desired measurement basis. The availabe options depend on the device and can be consulted by calling:<jupyter_code>Chadoq2.supported_bases<jupyter_output><empty_output><jupyter_text>And so, we've obtained the final sequence!<jupyter_code>seq.draw()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09320276497695852, 'fraction_numerical': 0.016359447004608296, 'mean_word_length': 6.4133219470538005, 'pattern_counts': {'":': 0, '<': 105, '<?xml version=': 0, '>': 105, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '26', 'dir': 'jupyter-structured-clean-dedup', 'id': '27966', 'license': 'Apache-2.0', 'n_tokens_mistral': 2818, 'n_tokens_neox': 2651, 'n_words': 1014, 'path': 'tutorials/creating_sequences.ipynb', 'repo_name': 'lvignoli/Pulser', 'size': '8680'}