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
Section titled “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
Section titled “Available Observables”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
Section titled “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:
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 ],)State- or Operator-dependent observables
Section titled “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.
import pulser_simulationfrom pulser.backend import Expectation, Fidelity
# Pick a backend, here we chose QutipBackendV2emu_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 Resultsfidelity = 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 timesexpectation = 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
Section titled “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()
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 equivalentresults.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 equivalentobs = expectationresults.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
Shortcuts for final bitstrings and state
Section titled “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.
# These are equivalentresults.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 equivalentresults.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]]