Specifying a measurement layout

This document shows you how to deal with circuit layout in different cases.

[1]:
%load_ext autoreload
%autoreload 2

1. Transpiled Quantum Circuit

Example of a quantum circuit without ancilla qubits before transpilation.

Define quantum circuit

[2]:
from qiskit import QuantumCircuit

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.draw("mpl", style="iqp")
[2]:
../_images/how_tos_layout_4_0.png

Transpile circuit

[3]:
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime.fake_provider import FakeManilaV2

backend = FakeManilaV2()
backend.set_options(seed_simulator=25)
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)

# Transpile the circuit to an "Instruction Set Architecture" (ISA) circuit.
# Note: the transpiler automatically adds "ancilla" qubits to make the transpiled
# circuit match the size of the FakeManilaV2 backend.
qc_isa = pm.run(qc)
qc_isa.draw("mpl", style="iqp")
[3]:
../_images/how_tos_layout_6_0.png

Define measurement procedure

No need to set a particular measurement_layout, because the POVMSampler will analyze the transpiled circuit and apply the TranspileLayout to the measurement circuit.

[4]:
from povm_toolbox.library import ClassicalShadows

measurement = ClassicalShadows(2, measurement_layout=None, seed=13)

More precisely, the measurement layout that will be automatically applied by the POVMSampler is extracted as :

transpile_layout = transpiled_circuit.layout
measurement_layout = transpile_layout.final_index_layout(filter_ancillas=True)

For the above circuit, we have:

[5]:
print(qc_isa.layout.final_index_layout(filter_ancillas=True))
[4, 3]

Run the job

Initialize Sampler and POVMSampler. Then run the job.

[6]:
from povm_toolbox.sampler import POVMSampler
from qiskit_ibm_runtime import SamplerV2 as RuntimeSampler

# First define a standard sampler (that will be used under the hood).
runtime_sampler = RuntimeSampler(backend=backend)
# Then define the POVM sampler, which takes BaseSampler as an argument.
povm_sampler = POVMSampler(runtime_sampler)
# Submit the job by specifying which POVM to use, which circuit(s) to measure and the shot budget.
job = povm_sampler.run([qc_isa], shots=4096, povm=measurement)

Look at the final composed circuit to check that the measurement was performed on the correct qubits.

[7]:
pub_result = job.result()[0]
pub_result.metadata.composed_circuit.draw("mpl", style="iqp")
[7]:
../_images/how_tos_layout_14_0.png

Define observable

[8]:
from qiskit.quantum_info import SparsePauliOp

observable = SparsePauliOp(["XI", "XX", "YY", "ZX"], coeffs=[1, 1, -1, 1])

Get the expected value

The observable has to be specified in terms of virtual qubits. Therefore, there is no need to apply the layout that was used by the POVM on the physical qubits.

[9]:
from povm_toolbox.post_processor import POVMPostProcessor

post_processor = POVMPostProcessor(pub_result)
exp_value, std = post_processor.get_expectation_value(observable)
print(exp_value)
1.8845214843749996

For reference, we can compare our estimated expectation value to the exact value.

[10]:
import numpy as np
from qiskit.quantum_info import Statevector

isa_observable = observable.apply_layout(layout=qc_isa.layout, num_qubits=qc_isa.num_qubits)

exact_expectation_value = np.real_if_close(Statevector(qc_isa).expectation_value(isa_observable))
print(f"Exact value:     {exact_expectation_value}")
print(f"Estimated value: {exp_value}")
Exact value:     1.9999999999999996
Estimated value: 1.8845214843749996

2. Quantum Circuit with ancilla qubits

Example of an un-transpiled quantum circuit with ancilla qubits

Define quantum circuit

The circuit can contain some ancilla qubits that you might not want to measure.

[11]:
qc_with_ancilla = QuantumCircuit(4)

# Qubits (1,3):
qc_with_ancilla.h(1)
qc_with_ancilla.cx(1, 3)

# Ancilla qubits (0,2):
qc_with_ancilla.z(0)
qc_with_ancilla.z(2)

qc_with_ancilla.draw("mpl", style="iqp")
[11]:
../_images/how_tos_layout_23_0.png

Define measurement procedure

The measurement_layout argument specifies which qubits to measure. It can be seen as a map from virtual to physical qubits indices. In this example, we only want to measure qubits 1 and 3.

[12]:
# The first local POVM will act on physical qubit 1 and the second local POVM will act on physical qubit 3.
measurement = ClassicalShadows(num_qubits=2, measurement_layout=[1, 3], seed=14)

Run the job

Initialize Sampler and POVMSampler. Then run the job.

[13]:
from numpy.random import default_rng
from qiskit.primitives import StatevectorSampler

rng = default_rng(26)

sampler = StatevectorSampler(seed=rng)
povm_sampler = POVMSampler(sampler=sampler)

job = povm_sampler.run([qc_with_ancilla], shots=4096, povm=measurement)
pub_result = job.result()[0]

Look at the final composed circuit to check that the measurement was performed on the correct qubits.

[14]:
pub_result.metadata.composed_circuit.draw("mpl")
[14]:
../_images/how_tos_layout_29_0.png

Get the expected value

The observable has to be specified in terms of virtual qubits. Therefore, there is no need to apply the layout that was used by the POVM on the full register of qubits.

[15]:
post_processor = POVMPostProcessor(pub_result)

print(f"Exact value:     {exact_expectation_value}")

exp_value, std = post_processor.get_expectation_value(observable)
print(f"Estimated value: {exp_value}")
Exact value:     1.9999999999999996
Estimated value: 2.0104980468749996

3. Transpiled Quantum Circuit with ancilla qubits

Example where the quantum circuit had ancilla qubits before transpilation.

Transpile circuit with ancilla qubits

[16]:
# Transpile the circuit to an "Instruction Set Architecture" (ISA) circuit
qc_with_ancilla_isa = pm.run(qc_with_ancilla)
qc_with_ancilla_isa.draw("mpl", style="iqp")
[16]:
../_images/how_tos_layout_34_0.png

Define measurement procedure

The measurement_layout argument specifies which qubits to measure. This argument overrides the automatic detection of a possible TranspileLayout attribute of the supplied circuit. I.e., it specifies directly the (final) physical qubits on which the POVM will act. In this example, we want to measure physical qubits 4 and 3.

If you want to specify the virtual qubits instead (here virtual qubits 1 and 3), you have to do the composition with the TranspileLayout manually as follows:

[17]:
# Measuring virtual qubits 1 and 3
virtual_msmt_layout = [1, 3]
# Get the transpilation layout
transpile_layout = qc_with_ancilla_isa.layout.final_index_layout()
# Compute the final measurement layout
final_msmt_layout = [transpile_layout[idx] for idx in virtual_msmt_layout]
print("Final measurement layout:", final_msmt_layout)

measurement = ClassicalShadows(2, measurement_layout=final_msmt_layout, seed=15)
Final measurement layout: [4, 3]

Run the job

Initialize Sampler and POVMSampler. Then run the job.

[18]:
# First define a standard sampler (that will be used under the hood).
runtime_sampler = RuntimeSampler(backend=backend)
# Then define the POVM sampler, which takes BaseSampler as an argument.
povm_sampler = POVMSampler(runtime_sampler)
# Submit the job by specifying which POVM to use, which circuit(s) to measure and the shot budget.
job = povm_sampler.run([qc_with_ancilla_isa], shots=4096, povm=measurement)
pub_result = job.result()[0]

You can check that measurement was performed on the correct physical qubits by looking at the final composed circuit.

[19]:
pub_result.metadata.composed_circuit.draw("mpl", style="iqp")
[19]:
../_images/how_tos_layout_40_0.png

Get the expected value

The observable has to be specified in terms of virtual qubits. Therefore, there is no need to apply the layout that was used by the POVM on the physical qubits.

[20]:
post_processor = POVMPostProcessor(pub_result)

print(f"Exact value:     {exact_expectation_value}")

exp_value, std = post_processor.get_expectation_value(observable)
print(f"Estimated value: {exp_value}")
Exact value:     1.9999999999999996
Estimated value: 1.8706054687499996