Simulations with Noise and Errors

Introduction

This notebook is the first in a series of tutorials on the use of the pulser-simulation module to simulate pulse sequences with noise and errors. These are an unavoidable part of a real quantum system, and a good simulation of a pulse sequence should be able to reproduce them accurately. A detailed explanation of these phenomena can be found in the following reference.

Currently, pulser-simulation supports noises for the laser pulses and errors for the state preparation and measurement stages:

1. SPAM (State Preparation And Measurement) errors:

  • State Preparation errors: The initial state preparation can fail to trap all the atoms in the ground state \(\ket{g}\). We model this by the probability \(\eta\) of not having a given atom during processing.

  • Measurement errors: In addition, the probabilities \(\epsilon, \epsilon '\) are linked to detection errors. The first one is the probability of detecting an atom in the ground state \(\ket{g}\) as if it were in the excited state \(\ket{r}\) (“false positives”), and the second one is the probability of detecting an atom in the excited state \(\ket{r}\) as if it were in the ground state \(\ket{g}\) (“false negatives”).

2. Laser noises:

The fluctuations of the properties of the lasers affect their effectiveness in addressing the desired transitions between atomic levels:

  • Doppler damping: The atoms in the register are cooled to a temperature \(T \sim 50\mu K\), which is low but still non-zero. Therefore, the laser frequency they experience is Doppler-shifted due to thermal motion. This can be seen as a shift in the detuning frequency of the laser.

  • Waist of the laser: For global pulses, the laser amplitude has a Gaussian profile and atoms at the border of the waist feel a slightly lower amplitude than those at the focus.

  • Amplitude fluctuations: Fluctuations in the laser amplitude from pulse to pulse.

3. Effective noise channels:

More generally, we can include effective noise channels in our simulation. These can represent the aggregate of different noise sources, as well as an interaction with an environment. To include these in our simulations, we use the Lindblad master equation, an evolution equation for the density matrix \(\rho\) of the system. Some introductory lecture notes on the topic can be found here (see chapters 8.2 to 8.4). pulser-simulation includes the following noise channels:

  • Dephasing channel: Implements a decay in the coherence or interference terms (the off-diagonal terms in \(\rho\)). It applies random \(z\)-rotations on each qubit according to a given probability \(p\), the dephasing probability.

  • Depolarizing channel: A more general type of noise in which \(\rho\) loses information due to interactions with the environment. This is represented as an evolution towards the completely mixed state \(\rho \rightarrow \frac{\mathbb{I}}{2}\), effectively erasing the coherence in \(\rho\) with a probability \(p\).

  • Custom effective noise channel: we can also include custom noise channels by providing the Lindblad operators and their coefficients.

[1]:
import numpy as np
import matplotlib.pyplot as plt
import qutip

from pulser import Register, Pulse, Sequence
from pulser_simulation import SimConfig, QutipEmulator
from pulser.devices import AnalogDevice

Single atom noisy simulations

Sequence preparation

We prepare a single atom.

[2]:
reg = Register.from_coordinates([(0, 0)], prefix="q")

We act on this atom with a Constant Pulse, such that it oscillates between the ground state and excited Rydberg state (Rabi oscillations):

[3]:
seq = Sequence(reg, AnalogDevice)
seq.declare_channel("ch0", "rydberg_global")
duration = 2500
pulse = Pulse.ConstantPulse(duration, 2 * np.pi, 0.0, 0.0)
seq.add(pulse, "ch0")
seq.draw()
../_images/tutorials_noisy_sim_8_0.png

In order to characterize simulation results, we designed a dedicated class, the SimulationResults class. It has two child classes, the CoherentResults for when the final state can be represented as a state vector or density matrix and NoisyResults when it can only be provided as a probability distribution of final states.

In the following, we run a noiseless simulation to obtain a CoherentResults object in clean_res. This object allows to get useful information on the simulation parameters, the simulation results and more.

[4]:
sim = QutipEmulator.from_sequence(seq, sampling_rate=0.05)
clean_res = sim.run()

We can for example plot the expectation values of an observable across time

[5]:
# Define Rydberg state observable
obs = qutip.basis(2, 0).proj()
[6]:
plt.figure(figsize=(4, 3))
plt.plot(clean_res._sim_times, clean_res.expect([obs])[0])
plt.xlabel(r"Time ($\mu s$)")
plt.ylabel(r"Population in $|r\rangle$")
plt.show()
../_images/tutorials_noisy_sim_13_0.png

The SimConfig object

Each simulation has an associated SimConfig object, which encapsulates parameters such as noise types, the temperature of the register and so on. You can view it at any time using the show_config() method:

[7]:
sim.show_config()
Options:
----------
Number of runs:        15
Samples per run:       5

When creating a new SimConfig, you can choose several parameters such as the number of runs and the number of delivered samples per run through the fields 'runs' and 'samples_per_run' respectively. The number of samples per run has no physical interpretation, this is used simply to cut down the calculation time by acquiring more shots without resampling the noise parameters.

SPAM noise use case

We will use as a toy example the SPAM noise and add it to our simulation in order to describe the different objects used for noisy simulations.

To do so, we can create a new SimConfig object and assign it to the config field of sim via the Simulation.set_config setter. We pass noise types as a tuple of strings to the SimConfig object created.

You can list the different types of noises supported for each basis by calling Simconfig.supported_noises as follows:

[8]:
sim.config.supported_noises
[8]:
{'ising': {'SPAM',
  'amplitude',
  'dephasing',
  'depolarizing',
  'doppler',
  'eff_noise'},
 'XY': {'SPAM'}}

By default, all config classes are in the ising mode, allowing a wide range of noises. However, for some applications, we need to operate in the XY mode, where only the SPAM noise is available for the moment.

Let’s come back to our toy example. We add "SPAM" noise to our clean simulation, select 30 different realizations of the state taking each time 5 samples per run:

[9]:
config_spam = SimConfig(noise=("SPAM"), runs=30, samples_per_run=5)
sim.set_config(config_spam)

The configuration is updated, the current parameters of the SPAM errors should be included:

[10]:
sim.show_config()
Options:
----------
Number of runs:        30
Samples per run:       5
Noise types:           SPAM
SPAM dictionary:       {'eta': 0.005, 'epsilon': 0.01, 'epsilon_prime': 0.05}

The SimConfig.spam_dict is the spam parameters dictionary where eta is the probability of a badly prepared state, epsilon the false positive probability and epsilon_prime the false negative one.

When dealing with a SimConfig object with different noise parameters from the config in Simulation.config, you can “add” both configurations together, obtaining a single SimConfig with all noises from both configurations - on the other hand, the runs and samples_per_run will always be updated. This adds simulation parameters to noises that weren’t available in the former Simulation.config. Noises specified in both SimConfigs will keep the noise parameters in Simulation.config. Try it out with Simulation.add_config:

[11]:
cfg2 = SimConfig(
    noise=("SPAM", "dephasing", "doppler"),
    eta=0.8,
    temperature=1000,
    runs=50,
)
sim.add_config(cfg2)
sim.show_config()
Options:
----------
Number of runs:        50
Samples per run:       5
Noise types:           SPAM, doppler, dephasing
SPAM dictionary:       {'eta': 0.005, 'epsilon': 0.01, 'epsilon_prime': 0.05}
Temperature:           1000.0µK
Dephasing probability: 0.05

We can observe that the eta parameter wasn’t changed, since both SimConfig objects had 'SPAM' as a noise model already.

We created a noisy simulation using set_config and add_config methods of the the Simulation class. But it’s not the only way, you can directly create a noisy simulation when you create your simulation object.

[12]:
sim = QutipEmulator.from_sequence(
    seq,
    sampling_rate=0.05,
    config=SimConfig(
        noise=("SPAM", "dephasing", "doppler"),
        temperature=1000,
        runs=50,
    ),
)
sim.show_config()
Options:
----------
Number of runs:        50
Samples per run:       5
Noise types:           SPAM, dephasing, doppler
SPAM dictionary:       {'eta': 0.005, 'epsilon': 0.01, 'epsilon_prime': 0.05}
Temperature:           1000.0µK
Dephasing probability: 0.05

Setting evaluation times

As a Simulation field, evaluation_times refers to the times at which you sample during your simulation. You can assign it different types of values:

  • A float between 0 and 1: indicating the fraction of the full time array at which you want to sample.

  • The string 'Full' to evaluate every ns or 'Minimal' to evaluate only at the start and the end.

  • A time list: if there are specific periods of times at which you want to sample. Only, make sure the list you use is a subset of the hamiltonian sample time list.

[13]:
sim.set_evaluation_times(0.5)
[14]:
noisy_res = sim.run()

We get a NoisyResults object from our noisy simulation. This object represents the final result as a probability distribution over the sampled bitstrings, rather than a quantum state QObj in the CleanResults case.

Plotting noisy and clean results

The new res instance has similar methods to the usual SimResults object. For example, we can calculate expectation values.

Below, the comparison between the clean and noise simulations.

[15]:
plt.figure(figsize=(4, 3))
plt.plot(noisy_res._sim_times, noisy_res.expect([obs])[0], label="noisy")
plt.plot(clean_res._sim_times, clean_res.expect([obs])[0], label="clean")
plt.xlabel(r"Time ($\mu s$)")
plt.ylabel(r"Population in $|r\rangle$")
plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0)
plt.show()
../_images/tutorials_noisy_sim_41_0.png

You can also use the SimResults.plot(obs) method to plot expectation values of a given observable. Here we compute the sigma_z local operator expectation values. Error bars are activated by default for noise simulations but if you don’t want them you can set to False the field error_bars.

Be wary that computing the expectation value of non-diagonal operators will raise an error, as NoisyResults bitstrings are already projected on the \(Z\) basis.

[16]:
obs = qutip.sigmaz()
plt.figure(figsize=(4, 3))
noisy_res.plot(obs, fmt=".")
../_images/tutorials_noisy_sim_44_0.png

Now you have the basic knowledge to tackle noise simulations. In the following tutorials, we will see in more detail simulations with SPAM errors, laser noise and effective noise channels. Enjoy!