Dilation POVMs and how to deal with ancilla qubits¶
This document shows you how to use dilation measurements. More specifically, it showcases how we deal with required measurement ancilla qubits.
[1]:
%load_ext autoreload
%autoreload 2
First example of a dilation POVM¶
[2]:
import numpy as np
from povm_toolbox.library import DilationMeasurements
# parameters that define a SIC-POVM (SIC stands for "symmetric and informationally-complete")
sic_parameters = np.array(
[0.75, 0.30408673, 0.375, 0.40678524, 0.32509973, 0.25000035, 0.49999321, 0.83333313],
)
povm = DilationMeasurements(num_qubits=1, parameters=sic_parameters, insert_barriers=True)
povm.definition().draw_bloch()
[2]:
IC dilation measurements are defined on a space spanning (at least) four states: e.g. \(\{|0\rangle,|1\rangle,|2\rangle,|3\rangle\}\). To achieve such a measurement using qubits, every qubit gets paired with an ancilla qubit. Then, the dilation measurement can be constructed via some two-qubit unitary followed by measurements in the computational basis. The binary outcomes \(\{|00\rangle,|01\rangle,|10\rangle,|11\rangle\}\) can then be mapped to the four states above.
[3]:
from qiskit.circuit import QuantumCircuit
qc = QuantumCircuit(1)
qc.h(0)
povm.compose_circuits(qc).draw("mpl")
[3]:
Adding necessary ancilla qubits¶
The user doesn’t have to deal with adding ancilla qubits, the method DilationMeasurements.compose_circuit
takes care of adding ancilla qubits when necessary. It will check if the input circuit has idling qubits that can be used. If there are not enough idling qubits in the input circuit, the method will add additional ancilla qubits to the input circuit.
1. Quantum Circuit without ancilla qubits¶
Example of a quantum circuit without idling qubits.
[4]:
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.draw("mpl")
[4]:
[5]:
povm = DilationMeasurements(num_qubits=2, parameters=sic_parameters)
# The input circuit does not have any idling qubits. The method `compose_circuit` will add two ancilla qubits.
povm.compose_circuits(qc).draw("mpl", wire_order=[0, 2, 1, 3])
[5]:
2. Quantum Circuit with a few idling qubits¶
Example of a quantum circuit with a few idling qubits, but not enough.
[6]:
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.draw("mpl")
[6]:
[7]:
povm.measurement_layout = [0, 1]
# The input circuit has one idling qubit. The method `compose_circuit` will use it and also add one ancilla qubit.
povm.compose_circuits(qc).draw("mpl", wire_order=[0, 2, 1, 3])
[7]:
3. Quantum Circuit with enough idling qubits¶
Example of a quantum circuit with enough idling qubits.
[8]:
qc = QuantumCircuit(4)
qc.h(1)
qc.cx(1, 3)
qc.draw("mpl")
[8]:
[9]:
povm.measurement_layout = [1, 3]
# The input circuit has two idling qubit. The method `compose_circuit` will use both of them and does not need to add any additional ancilla qubits.
povm.compose_circuits(qc).draw("mpl", wire_order=[1, 0, 3, 2])
[9]:
4. Quantum Circuit with many idling qubits¶
Example of a quantum circuit with more than enough idling qubits.
[10]:
qc = QuantumCircuit(6)
qc.h(1)
qc.cx(1, 3)
qc.draw("mpl")
[10]:
[11]:
povm.measurement_layout = [1, 3]
# The input circuit has four idling qubit. The method `compose_circuit` will use two of them (and does not need to add any additional ancilla qubits).
povm.compose_circuits(qc).draw("mpl", wire_order=[1, 0, 3, 2, 4, 5])
[11]:
5. Quantum circuit with ancilla qubits that are not idling¶
Example of a quantum circuit that has an AncillaRegister
with non-idle qubits. These cannot be used and, thus, we need additional ancilla qubits to ensure all dilation measurements can be performed.
[12]:
from qiskit.circuit import AncillaRegister, QuantumRegister
qc = QuantumCircuit(QuantumRegister(2), AncillaRegister(2))
qc.h(0)
qc.cx(0, 1)
qc.sx(2)
qc.cx(2, 1)
qc.draw("mpl")
[12]:
[13]:
# We only want to measure qubits 0 and 1 (qubit 2 is an ancilla qubit)
povm.measurement_layout = [0, 1]
# The input circuit has one idling qubit. The method `compose_circuit` will use it and also add one ancilla qubit.
povm.compose_circuits(qc).draw("mpl", wire_order=[0, 3, 1, 4, 2])
[13]:
Transpiled Quantum Circuits¶
Examples with transpiled quantum circuits.
Generate a generic backend¶
[14]:
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Generate a 6-qubit simulated backend
backend = GenericBackendV2(
num_qubits=6, coupling_map=[[0, 2], [1, 2], [2, 3], [4, 3], [5, 3]], seed=0
)
backend.set_options(seed_simulator=25)
pm = generate_preset_pass_manager(optimization_level=2, backend=backend, seed_transpiler=0)
coupling_map = backend.coupling_map
# coupling_map.draw()
Define quantum circuit¶
[15]:
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.draw("mpl")
[15]:
Transpile circuit¶
[16]:
# 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 backend.
qc_isa = pm.run(qc)
qc_isa.draw("mpl")
[16]:
Define measurement procedure¶
There is no need to set a particular measurement_layout
, because the POVMSampler
will analyze the transpiled circuit and apply the TranspileLayout
to the measurement circuit.
[17]:
measurement = DilationMeasurements(
num_qubits=2,
parameters=sic_parameters,
insert_barriers=True,
)
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:
[18]:
print(qc_isa.layout.final_index_layout(filter_ancillas=True))
[2, 1]
Run the job¶
Initialize Sampler
and POVMSampler
. Then run the job. A pass manager has to be provided to transform the final composed circuit into an ISA circuit.
[19]:
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(mode=backend)
# Then define the POVM sampler, which takes the sampler 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.
# A pass manager has to be provided to make the measurement circuit ISA.
job = povm_sampler.run([qc_isa], shots=4096, povm=measurement, pass_manager=pm)
Look at the final composed circuit to check that the measurement was performed on the correct qubits. It is important to note that the two-qubit unitary which is part of the dilation measurement can cause a significant SWAP overhead, depending on the qubit connectivity between the measured qubit and its associated ancilla.
[20]:
pub_result = job.result()[0]
pub_result.metadata.composed_circuit.draw("mpl", fold=-1)
[20]:
Define observable¶
[21]:
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.
[22]:
from povm_toolbox.post_processor import POVMPostProcessor
post_processor = POVMPostProcessor(pub_result)
exp_value, std = post_processor.get_expectation_value(observable)
print(exp_value)
2.0032563568087483
For reference, we can compare our estimated expectation value to the exact value.
[23]:
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.9999999999999987
Estimated value: 2.0032563568087483
Transpiled Quantum Circuit with ancilla qubits¶
Example where the quantum circuit had ancilla qubits before transpilation.
Define quantum circuit¶
The circuit can contain some ancilla qubits that you might not want to measure.
[24]:
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")
[24]:
Transpile circuit with ancilla qubits¶
[25]:
# 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")
[25]:
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:
[26]:
# 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 = DilationMeasurements(
2,
measurement_layout=final_msmt_layout,
parameters=sic_parameters,
insert_barriers=True,
)
Final measurement layout: [2, 1]
Run the job¶
Initialize Sampler
and POVMSampler
. Then run the job.
[27]:
# First define a standard sampler (that will be used under the hood).
runtime_sampler = RuntimeSampler(mode=backend)
# Then define the POVM sampler, which takes the sampler 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, pass_manager=pm)
pub_result = job.result()[0]
You can check that measurement was performed on the correct physical qubits by looking at the final composed circuit.
[28]:
pub_result.metadata.composed_circuit.draw("mpl", fold=-1)
[28]:
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.
[29]:
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.9999999999999987
Estimated value: 2.0032563568087483