Results and Observables
What you will learn:
how to configure an
Observableto measure quantities of interest in an emulation;what observables are available by default;
how to retrieve the measured observables from a
Resultsinstance.
The Observable mechanism
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.
Available Observables
Info
The observables below are typical quantities of interest that can generally be given to any emulator backend.
Nonetheless, specific emulators may choose to define additional observables or even modify existing ones, so please make sure to consult your chosen emulator’s specific documentation.
The pulser.backend module gives access to the following observables by default
|
Stores bitstrings sampled from the state at the evaluation times. |
|
Stores the correlation matrix for the current state. |
|
Stores the energy of the system at the evaluation times. |
Stores the expectation value of |
|
|
Stores the variance of the Hamiltonian at the evaluation times. |
|
Stores the expectation of the given operator on the current state. |
|
Stores the fidelity with a pure state at the evaluation times. |
|
Stores the occupation number of an eigenstate on each qudit. |
|
Stores the quantum state at the evaluation times. |
Configuring an Observable
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_timesfor a given observable - when not given, the emulator will simply useEmulationConfig.default_evaluation_timesinstead.
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:
[1]:
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
],
)
Note
evaluation_times are given as fractions of a sequence’s total duration, so 0.0 corresponds to the beginning, 0.5 to the halfway point and 1.0 to the end of the sequence.
State- or Operator-dependent observables
While most observables can be defined without any arguments, there are two notable exceptions:
Fidelityrequires aStateinstance to know against which state it should compute the fidelity;Expectationrequires anOperatorinstance 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:
Pick your target
EmulatorBackendTake the preferred
EmulationConfigclass from your chosen backend viaEmulatorBackend.config_typeTake the preferred
StateorOperatorfrom the config class viaEmulationConfig.state_typeorEmulationConfig.operator_type, respectively.
Tip
The example shown below is purposefully agnostic of the emulator backend, all it takes to change backend is to change the emu_backend_class variable.
[2]:
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],
)
Accessing Results
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()
Info
The toggled “Details” above hides a self-contained script replicating many of the steps outlined in the previous section or in the backend execution page.
Feel free to skip ahead if you wish to jump straight into how to access Results.
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
[4]:
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
Results.get_tagged_results() returns a dictionary with a list of values for each observable, containing one value per evaluation time.
[5]:
results.get_tagged_results()
[5]:
{'energy': [0.0, -1.2472756556445144, 0.8227610649668835],
'expectation_XX': [0.0,
0.004384697755591441,
0.18339679420520738,
0.16227680612962456,
-0.06498566947522634],
'bitstrings': [Counter({'11': 956, '10': 23, '01': 19, '00': 2})],
'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:
[6]:
# These are equivalent
results.get_tagged_results()["fidelity_rr"]
results.fidelity_rr
[6]:
[0.9503744800119598]
Getting the evaluation times of an observable
[7]:
# These are all equivalent
obs = expectation
results.get_result_times(obs)
results.get_result_times(obs.tag)
results.get_result_times("expectation_XX")
[7]:
[0.0, 0.25, 0.5, 0.75, 1.0]
Getting the result of an observable at a specific evaluation time
[8]:
results.get_result("energy", 0.5)
[8]:
-1.2472756556445144
Shortcuts for final bitstrings and state
Since backend runs typically include the BitStrings observable at the end of the sequence, there is a dedicated shortcut to access it.
[9]:
# These are equivalent
results.get_result("bitstrings", 1.0)
results.final_bitstrings
[9]:
Counter({'11': 956, '10': 23, '01': 19, '00': 2})
The same goes for the quantum state at the end of the emulation
[10]:
# These are equivalent
results.get_result("state", 1.0)
results.final_state
[10]:
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]]