# Get started with the Estimator primitive¶

In this tutorial we will show you how to set up the Qiskit Rumtime Estimator primitive, explore the different options you can use to configure it, and invoke the primitive efficiently inside a session.

## Primitives¶

Primitives are core functions that make it easier to build modular algorithms and applications.

Instead of simply returning counts, they return more immediately meaningful information.

Additionally, they provide a seamless way to access the latest advancements in IBM Quantum hardware and software.

The initial release of Qiskit Runtime includes two primitives:

Sampler: Generates quasi-probabilities from input circuits.

Estimator: Calculates expectation values from input circuits and observables.

In this tutorial we will focus on the Estimator primitive. There is a separate tutorial on Getting started with the Sampler primtive.

## Using the Estimator primitive¶

Similar to the Backend base class, there is an Estimator base class defined in Qiskit Terra that standardizes the way users interact with all Estimator implementations. This allows users to easily change their choice of simulator or device for performing expectation value calculations, even if the underlying implementation is different.

In this section we will be using the default implementation in Qiskit Terra, which uses a local statevector simulator.

### 1. Create a circuit¶

For a basic expectation value calculation you will need at least one quantum circuit to prepare our system in a precise quantum state for study. Our examples all have circuits in them, but you can use Qiskit to create your own. To learn how to create circuits by using Qiskit, see the Circuit basics tutorial.

[1]:

from qiskit.circuit.random import random_circuit

circuit = random_circuit(2, 2, seed=0).decompose(reps=1)
display(circuit.draw("mpl"))


### 2. Create an observable to measure¶

You will also need at least one observable to measure. Observables represent physical properties of a quantum system (e.g. energy, spin), and allow said properties to be measured (e.g. their expectation values) for a given state of our system. For simplicity, you can use the PauliSumOp class in Qiskit to define them, as illustrated in the example below.

[2]:

from qiskit.quantum_info import SparsePauliOp

observable = SparsePauliOp("XZ")
print(f">>> Observable: {observable.paulis}")

>>> Observable: ['XZ']


### 3. Initialize an Estimator class¶

The next step is to create an instance of an Estimator class. This can be any Estimator class that complies with the Estimator primitive specification. For simplicity, we will use Qiskit Terra’s qiskit.primitives.Estimator class, based on the Statevector construct (i.e. algebraic simulation).

[3]:

from qiskit.primitives import Estimator

estimator = Estimator()


### 4. Invoke the Estimator and get results¶

To calculate the expectation values, invoke the run() method of the Estimator instance you just created and pass in the circuit and observable as input parameters. This method call is asynchronous, and you will get a Job object back. You can use this object to query for information like job_id() and status().

[4]:

job = estimator.run(circuit, observable)
print(f">>> Job ID: {job.job_id()}")
print(f">>> Job Status: {job.status()}")

>>> Job ID: 85a54b84-7ce2-49f8-b614-cc39ef8eafcc
>>> Job Status: JobStatus.DONE


The result() method of the job will return the EstimatorResult, which includes both the expectation values and job metadata.

[5]:

result = job.result()
print(f">>> {result}")
print(f"  > Expectation value: {result.values[0]}")

>>> EstimatorResult(values=array([0.85347811]), metadata=[{}])
> Expectation value: 0.8534781134132173


You can keep invoking the run() method again with the different inputs:

[6]:

circuit = random_circuit(2, 2, seed=1).decompose(reps=1)
observable = SparsePauliOp("IY")

job = estimator.run(circuit, observable)
result = job.result()

display(circuit.draw("mpl"))
print(f">>> Observable: {observable.paulis}")
print(f">>> Expectation value: {result.values[0]}")

>>> Observable: ['IY']
>>> Expectation value: -1.582539029327245e-16


You can also provide compound inputs to the run() method:

[7]:

circuits = (
random_circuit(2, 2, seed=0).decompose(reps=1),
random_circuit(2, 2, seed=1).decompose(reps=1),
)
observables = (
SparsePauliOp("XZ"),
SparsePauliOp("IY"),
)

job = estimator.run(circuits, observables)
result = job.result()

[display(cir.draw("mpl")) for cir in circuits]
print(f">>> Observables: {[obs.paulis for obs in observables]}")
print(f">>> Expectation values: {result.values.tolist()}")

>>> Observables: [PauliList(['XZ']), PauliList(['IY'])]
>>> Expectation values: [0.8534781134132173, -1.582539029327245e-16]


Or use parameterized circuits:

[8]:

from qiskit.circuit.library import RealAmplitudes

circuit = RealAmplitudes(num_qubits=2, reps=2).decompose(reps=1)
observable = SparsePauliOp("ZI")
parameter_values = [0, 1, 2, 3, 4, 5]

job = estimator.run(circuit, observable, parameter_values)
result = job.result()

display(circuit.draw("mpl"))
print(f">>> Observable: {observable.paulis}")
print(f">>> Parameter values: {parameter_values}")
print(f">>> Expectation value: {result.values[0]}")

>>> Observable: ['ZI']
>>> Parameter values: [0, 1, 2, 3, 4, 5]
>>> Expectation value: -0.6485568434766461


## Using Qiskit Runtime Estimator¶

In this section we will go over how to use Qiskit Runtime’s implementation of the Estimator primitive.

### 1. Initialize the account¶

Since Qiskit Runtime Estimator is a managed service, you will first need to initialize your account. You can then select the simulator or real backend you want to use to calculate the expectation value.

Follow the steps in the getting started guide if you don’t already have an account set up.

[9]:

from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService(channel="ibm_quantum")
backend = service.backend("ibmq_qasm_simulator")


### 2. Create a circuit and an observable¶

Just like the section before, you will need at least one circuit and one observable as inputs to the Estimator primitive.

[10]:

from qiskit.circuit.random import random_circuit
from qiskit.quantum_info import SparsePauliOp

circuit = random_circuit(2, 2, seed=0).decompose(reps=1)
display(circuit.draw("mpl"))

observable = SparsePauliOp("XZ")
print(f">>> Observable: {observable.paulis}")

>>> Observable: ['XZ']


### 3. Initialize the Qiskit Runtime Estimator¶

Here we are initializing an instance of qiskit_ibm_runtime.Estimator instead of qiskit.primitives.Estimator to use Qiskit Runtime’s implementation of the Estimator.

When you initialize the Estimator, you’ll need to pass in the backend you previously selected as the target device (or simulator), using the session parameter. This automatically opens a session for that backend. We will talk more about session in a later section.

[11]:

from qiskit_ibm_runtime import Estimator

estimator = Estimator(session=backend)


### 4. Invoke the Estimator and get results¶

You can then invoke the run() method to calculate expectation values for the input circuit(s) and observable(s).

[12]:

job = estimator.run(circuit, observable)
print(f">>> Job ID: {job.job_id()}")
print(f">>> Job Status: {job.status()}")

>>> Job ID: cdkrlhian60ka16e78dg
>>> Job Status: JobStatus.RUNNING

[13]:

result = job.result()
print(f">>> {result}")
print(f"  > Expectation value: {result.values[0]}")

>>> EstimatorResult(values=array([0.859]), metadata=[{'variance': 0.262119, 'shots': 4000}])
> Expectation value: 0.859
> Metadata: {'variance': 0.262119, 'shots': 4000}


## Options¶

Primitives come with a number of options that are grouped into different categories. Commonly used options, such as resilience_level, are at the first level.

You can use the Options class to specify different options.

In the example below, we create an instance of the Options class. optimization_level is a first level option and can be passed as an input parameter. Options related to the execution environment are passed using the environment parameter.

[14]:

from qiskit_ibm_runtime import Options

options = Options(optimization_level=3, environment={"log_level": "INFO"})


Options supports auto-complete. Once you create an instance of the Options class, you can use auto-complete to see what options are available. If you choose one of the categories, you can use auto-complete again to see what options are available under that category.

[15]:

from qiskit_ibm_runtime import Options

options = Options()
options.resilience_level = 1
options.execution.shots = 2048


When creating an instance of the Estimator class, you can pass in the options you just created. Those options will then be applied when you use run() to perform the calculation.

[16]:

estimator = Estimator(session=backend, options=options)
result = estimator.run(circuit, observable).result()

>>> Metadata: {'variance': 0.2847862243652344, 'shots': 2048, 'readout_mitigation_num_twirled_circuits': 16, 'readout_mitigation_shots_calibration': 8192}


You can also pass in options via the run() method. This will overwrite the options you specified when creating the Estimator instance for that particular execution.

Since most users will only overwrite a handful of options at the job level, it is not necessary to specify the category the options are in. The code below, for example, specifies shots=1024 instead of execution={"shots": 1024} (which is also valid).

[17]:

estimator = Estimator(session=backend, options=options)
result = estimator.run(circuit, observable, shots=1024).result()

>>> Metadata: {'variance': 0.2814788818359375, 'shots': 1024, 'readout_mitigation_num_twirled_circuits': 16, 'readout_mitigation_shots_calibration': 8192}


### Error suppression and mitigation¶

optimization_level and resilience_level are used to configure error suppress and mitigation.

Estimator supports optimization_level 0-3 and resilience_level 0-3.

Advanced mitigation options can be specified under resilience.

[18]:

from qiskit_ibm_runtime import Options

options = Options(optimization_level=3, resilience_level=1)

[19]:

estimator = Estimator(session=backend, options=options)
result = estimator.run(circuit, observable).result()
print(f">>> Expectation value: {result.values[0]}")

>>> Expectation value: 0.8485


## Session¶

A Qiskit Runtime session allows you to group a collection of iterative calls to the quantum computer. A session is started when the first job within the session is started. As long as the session is active, subsequent jobs within the session are prioritized by the scheduler to minimize artificial delay within an iterative algorithm. Data used within a session, such as transpiled circuits, is also cached to avoid unnecessary overhead.

### Session timing¶

When a session is started, it is assigned a maximum session timeout value. You can set this value by using the max_time parameter.

If you don’t specify a timeout value, it is set to the initial job’s maximum execution time and is the smaller of these values:

• The system limit which can be found here.

• The max_execution_time defined by the program.

After this time limit is reached, the session is permanently closed.

A session also has an interactive timeout value. If there are no session jobs queued within that window, the session is temporarily deactivated and normal job selection resumes. This interactive timeout value is set by the system and cannot be overwritten.

### Invoking Estimator.run within a session¶

You can create a Qiskit Runtime session using the context manager (with ...:), which automatically opens and closes the session for you. You can invoke Estimator.run one or more times within a session:

[20]:

from qiskit_ibm_runtime import Session, Estimator

with Session(backend=backend, max_time="1h"):
estimator = Estimator()

result = estimator.run(circuit, observable).result()
print(f">>> Expectation value from the first run: {result.values[0]}")

result = estimator.run(circuit, observable).result()
print(f">>> Expectation value from the second run: {result.values[0]}")

>>> Expectation value from the first run: 0.858
>>> Expectation value from the second run: 0.85


### Invoke multiple primitives in a session¶

You are not restricted to a single primitive function within a session. In this section we will show you an example of using multiple primitives.

First we prepare a circuit for the sampler primitive.

[21]:

from qiskit.circuit.random import random_circuit

sampler_circuit = random_circuit(2, 2, seed=0).decompose(reps=1)
sampler_circuit.measure_all()
display(circuit.draw("mpl"))


The example below shows how you can create both an instance of the Sampler class and one of the Estimator class and invoke their run() methods within a session.

[22]:

from qiskit_ibm_runtime import Session, Sampler, Estimator

with Session(backend=backend):
sampler = Sampler()
estimator = Estimator()

result = sampler.run(sampler_circuit).result()
print(f">>> Quasi Distribution from the sampler job: {result.quasi_dists[0]}")

result = estimator.run(circuit, observable).result()
print(f">>> Expectation value from the estimator job: {result.values[0]}")

>>> Quasi Distribution from the sampler job: {2: 0.50125, 0: 0.49875}
>>> Expectation value from the estimator job: 0.8525


The calls can also be asynchronous. You don’t need to wait for the result of a previous job before submitting another one.

[23]:

from qiskit_ibm_runtime import Session, Sampler, Estimator

with Session(backend=backend):
sampler = Sampler()
estimator = Estimator()

sampler_job = sampler.run(sampler_circuit)
estimator_job = estimator.run(circuit, observable)

print(f">>> Quasi Distribution from the sampler job: {sampler_job.result().quasi_dists[0]}")
print(f">>> Expectation value from the estimator job: {estimator_job.result().values[0]}")

>>> Quasi Distribution from the sampler job: {2: 0.49025, 0: 0.50975}
>>> Expectation value from the estimator job: 0.8475


## Summary¶

Below is a quick recap of using Qiskit Runtime primitives, options, and session.

[24]:

from qiskit_ibm_runtime import QiskitRuntimeService, Session, Sampler, Estimator, Options

# 1. Initialize account
service = QiskitRuntimeService(channel="ibm_quantum")

# 2. Specify options, such as enabling error mitigation
options = Options(resilience_level=1)

# 3. Select a backend.
backend = service.backend("ibmq_qasm_simulator")

# 4. Create a session
with Session(backend=backend):

# 5. Create primitive instances
sampler = Sampler(options=options)
estimator = Estimator(options=options)

# 6. Submit jobs
sampler_job = sampler.run(sampler_circuit)
estimator_job = estimator.run(circuit, observable)

# 7. Get results
print(f">>> Quasi Distribution from the sampler job: {sampler_job.result().quasi_dists[0]}")
print(f">>> Expectation value from the estimator job: {estimator_job.result().values[0]}")

>>> Quasi Distribution from the sampler job: {0: 0.49575, 2: 0.50425}
>>> Expectation value from the estimator job: 0.8315


## Reference¶

You can find more details about the Estimator methods in the Estimator API reference.

And all the available options in the Options API reference.

[25]:

import qiskit_ibm_runtime
qiskit_ibm_runtime.version.get_version_info()

[25]:

'0.8.0'

[26]:

from qiskit.tools.jupyter import *

%qiskit_version_table


### Version Information

Qiskit SoftwareVersion
qiskit-terra0.22.2
qiskit-aer0.11.0
qiskit-ibmq-provider0.19.2
qiskit-nature0.5.0
System information
Python version3.8.1
Python compilerClang 11.0.3 (clang-1103.0.32.62)
Python builddefault, Jul 15 2020 18:48:27
OSDarwin
CPUs8
Memory (Gb)16.0
Mon Nov 07 21:11:54 2022 EST