Skip to content
Pasqal Documentation

Results are limited to the current section : pulser

Results and Observables

What you will learn:

  • how to configure an Observable to measure quantities of interest in an emulation;

  • what observables are available by default;

  • how to retrieve the measured observables from a Results instance.

As showcased in the page on backend execution, the Observable mechanism provides an efficient and uniform way of calculating and storing different quantities of interest throughout an emulation.

It is a shared mechanism between the different emulator backends that can generally be used interchangeably with minimal to no modifications.

The pulser.backend module gives access to the following observables by default

pulser.backend.BitStrings(*[, …])

Stores bitstrings sampled from the state at the evaluation times.

pulser.backend.CorrelationMatrix(*[, …])

Stores the correlation matrix for the current state.

pulser.backend.Energy(*[, evaluation_times, …])

Stores the energy of the system at the evaluation times.

pulser.backend.EnergySecondMoment(*[, …])

Stores the expectation value of H(t)^2 at the evaluation times.

pulser.backend.EnergyVariance(*[, …])

Stores the variance of the Hamiltonian at the evaluation times.

pulser.backend.Expectation(operator, *[, …])

Stores the expectation of the given operator on the current state.

pulser.backend.Fidelity(state, *[, …])

Stores the fidelity with a pure state at the evaluation times.

pulser.backend.Occupation(*[, …])

Stores the occupation number of an eigenstate on each qudit.

pulser.backend.StateResult(*[, …])

Stores the quantum state at the evaluation times.

After choosing one or more observables, you must then configure them and provide them to the EmulationConfig (or the chosen backend’s specific EmulationConfig subclass).

To do so,

  • Follow the observable’s docstring to instantiate it with the required arguments (if any).

  • Optionally, you may also specify custom evaluation_times for a given observable - when not given, the emulator will simply use EmulationConfig.default_evaluation_times instead.

As a simple example, imagine that by default you only care about an observable’s value at the end of the sequence, but you are interested in knowing the energy of the system at the beginning and halfway points of the execution too. In this case, you could define your observables as follows:

from pulser.backend import BitStrings, Energy, EmulationConfig
config = EmulationConfig(
default_evaluation_times=[
1.0
], # By default, compute an observable only at the end
observables=[
BitStrings(), # No custom evaluation times -> Will be computed only at the end
Energy(
evaluation_times=[0.0, 0.5, 1.0]
), # Will be computed at the beginning, middle and end
],
)

While most observables can be defined without any arguments, there are two notable exceptions:

  • Fidelity requires a State instance to know against which state it should compute the fidelity;

  • Expectation requires an Operator instance to know which operator’s expectation value to compute.

Furthermore, both the State and Operator subclasses used must be compatible with the chosen backend. To make sure this is the case, follow these steps:

  1. Pick your target EmulatorBackend

  2. Take the preferred EmulationConfig class from your chosen backend via EmulatorBackend.config_type

  3. Take the preferred State or Operator from the config class via EmulationConfig.state_type or EmulationConfig.operator_type, respectively.

import pulser_simulation
from pulser.backend import Expectation, Fidelity
# Pick a backend, here we chose QutipBackendV2
emu_backend_class = pulser_simulation.QutipBackendV2
config_class = emu_backend_class.config_type # In this case, `QutipConfig`
state_class = config_class.state_type # In this case, `QutipState`
operator_class = config_class.operator_type # In this case, `QutipOperator`
# Arbitrarily chosen fidelity state |rr>
r_state = state_class.from_state_amplitudes(
eigenstates=("r", "g"),
amplitudes={"rr": 1.0},
)
# Use `tag_suffix` to better identify the observable in the Results
fidelity = Fidelity(r_state, tag_suffix="rr")
# Arbitrarily chosen operator XX (where X = |r><g|+|g><r|)
pauli_x = operator_class.from_operator_repr(
eigenstates=("r", "g"),
n_qudits=2,
operations=[(1.0, [({"rg": 1.0, "gr": 1.0}, {0, 1})])],
)
# Here we ask for the expectation value at multiple evaluation times
expectation = Expectation(
pauli_x,
evaluation_times=[0.0, 0.25, 0.5, 0.75, 1.0],
# Use `tag_suffix` to better identify the observable in the Results
tag_suffix="XX",
)
# Creating a new config with the defined observables
config = config_class(
observables=[fidelity, expectation],
)

On every Pulser backend, a call to Backend.run() will return an instance of Results (or in the case of a RemoteBackend, a sequence of Results).

Let us start by obtaining a Results instance for some arbitrary Pulser sequence, using the observables defined in the section above.

import numpy as np

import pulser
import pulser_simulation
from pulser.backend import (
    BitStrings,
    Energy,
    Expectation,
    Fidelity,
    StateResult,
)

# STEP 0: Make an arbitrary Pulser Sequence
reg = pulser.Register({"q0": (-5, 0), "q1": (5, 0)})

seq = pulser.Sequence(reg, pulser.AnalogDevice)
seq.declare_channel("rydberg_global", "rydberg_global")

t = 2000  # ns
amp_wf = pulser.BlackmanWaveform(duration=t, area=np.pi)
det_wf = pulser.RampWaveform(duration=t, start=-5, stop=5)
seq.add(pulser.Pulse(amp_wf, det_wf, 0), "rydberg_global")


# STEP 1: Pick the backend and extract the needed classes
emu_backend_class = pulser_simulation.QutipBackendV2

config_class = emu_backend_class.config_type  # In this case, `QutipConfig`
state_class = config_class.state_type  # In this case, `QutipState`
operator_class = config_class.operator_type  # In this case, `QutipOperator`


# STEP 2: Define the desired observables

# Takes `config.default_num_shots` at the `config.default_evaluation_times`
bitstrings = BitStrings()

# Records the state of the systems at the `config.default_evaluation_times`
state_obs = StateResult()

# Records the energy of the system at the beginning, middle and end
energy = Energy(evaluation_times=[0.0, 0.5, 1.0])

# Records fidelity with |rr> at the `config.default_evaluation_times`
r_state = state_class.from_state_amplitudes(
    eigenstates=("r", "g"),
    amplitudes={"rr": 1.0},
)
fidelity = Fidelity(r_state, tag_suffix="rr")

# Records expectation value of XX at custom evaluation times
pauli_x = operator_class.from_operator_repr(
    eigenstates=("r", "g"),
    n_qudits=2,
    operations=[(1.0, [({"rg": 1.0, "gr": 1.0}, {0, 1})])],
)
expectation = Expectation(
    pauli_x, evaluation_times=[0.0, 0.25, 0.5, 0.75, 1.0], tag_suffix="XX"
)

# STEP 3: Creating a new config with the defined observables

config = config_class(
    observables=[bitstrings, state_obs, energy, fidelity, expectation],
    default_evaluation_times=[
        1.0
    ],  # By default, compute an observable only at the end
)

# STEP 4: Run the emulation to get the `Results`

emu_backend = emu_backend_class(seq, config=config)
results = emu_backend.run()

Printing to get an overview of the Results

Section titled “Printing to get an overview of the Results”

Given a Results instance, the first thing we can do is print it to get an overview of what it contains

print(results)
Results
-------
Stored results: ['energy', 'expectation_XX', 'bitstrings', 'state', 'fidelity_rr']
Evaluation times per result: {'energy': [0.0, 0.5, 1.0], 'expectation_XX': [0.0, 0.25, 0.5, 0.75, 1.0], 'bitstrings': [1.0], 'state': [1.0], 'fidelity_rr': [1.0]}
Atom order in states and bitstrings: ('q0', 'q1')
Total sequence duration: 2000 ns

As expected, it contains results for each observable we defined, at the requested evaluation times. Now, there are multiple ways we can access these results.

Getting a list of results for each observable

Section titled “Getting a list of results for each observable”

Results.get_tagged_results() returns a dictionary with a list of values for each observable, containing one value per evaluation time.

results.get_tagged_results()
{'energy': [0.0, -1.2472756556445144, 0.8227610649668835],
 'expectation_XX': [0.0,
  0.004384697755591441,
  0.18339679420520738,
  0.16227680612962456,
  -0.06498566947522634],
 'bitstrings': [Counter({'11': 954, '01': 22, '10': 16, '00': 8})],
 'state': [QutipState
  ----------
  Eigenstates: ('r', 'g')
  Quantum object: dims=[[2, 2], [1]], shape=(4, 1), type='ket', dtype=Dense
  Qobj data =
  [[ 0.62156266+0.75102219j]
   [-0.03358354+0.14524474j]
   [-0.03358354+0.14524474j]
   [-0.00110516-0.07194168j]]],
 'fidelity_rr': [0.9503744800119598]}

To get the list of values for a particular observable, we can either access the dictionary directly or use a shortcut:

# These are equivalent
results.get_tagged_results()["fidelity_rr"]
results.fidelity_rr
[0.9503744800119598]

Getting the evaluation times of an observable

Section titled “Getting the evaluation times of an observable”
# These are all equivalent
obs = expectation
results.get_result_times(obs)
results.get_result_times(obs.tag)
results.get_result_times("expectation_XX")
[0.0, 0.25, 0.5, 0.75, 1.0]

Getting the result of an observable at a specific evaluation time

Section titled “Getting the result of an observable at a specific evaluation time”
results.get_result("energy", 0.5)
-1.2472756556445144

Since backend runs typically include the BitStrings observable at the end of the sequence, there is a dedicated shortcut to access it.

# These are equivalent
results.get_result("bitstrings", 1.0)
results.final_bitstrings
Counter({'11': 954, '01': 22, '10': 16, '00': 8})

The same goes for the quantum state at the end of the emulation

# These are equivalent
results.get_result("state", 1.0)
results.final_state
QutipState
----------
Eigenstates: ('r', 'g')
Quantum object: dims=[[2, 2], [1]], shape=(4, 1), type='ket', dtype=Dense
Qobj data =
[[ 0.62156266+0.75102219j]
 [-0.03358354+0.14524474j]
 [-0.03358354+0.14524474j]
 [-0.00110516-0.07194168j]]