Estimación de fase cuántica con las funciones de Qiskit de Q-CTRL
Estimación de uso: 40 segundos en un procesador Heron r2. (NOTA: Esto es solo una estimación. Su tiempo de ejecución puede variar.)
Contexto
La estimación de fase cuántica (QPE, por sus siglas en inglés) es un algoritmo fundamental en la computación cuántica que constituye la base de muchas aplicaciones importantes como el algoritmo de Shor, la estimaci ón de la energía del estado fundamental en química cuántica y los problemas de valores propios. La QPE estima la fase asociada con un estado propio de un operador unitario, codificada en la relación
y la determina con una precisión de utilizando qubits de conteo [1]. Al preparar estos qubits en superposición, aplicar potencias controladas de y luego utilizar la transformada de Fourier cuántica inversa (QFT) para extraer la fase en resultados de medición codificados en binario, la QPE produce una distribución de probabilidad con picos en las cadenas de bits cuyas fracciones binarias aproximan . En el caso ideal, el resultado de medición más probable corresponde directamente a la expansión binaria de la fase, mientras que la probabilidad de otros resultados disminuye rápidamente con el número de qubits de conteo. Sin embargo, ejecutar circuitos profundos de QPE en hardware presenta desafíos: el gran número de qubits y operaciones de entrelazamiento hacen que el algoritmo sea altamente sensible a la decoherencia y los errores de compuerta. Esto resulta en distribuciones de cadenas de bits ampliadas y desplazadas, enmascarando la fase propia verdadera. Como consecuencia, la cadena de bits con la mayor probabilidad puede ya no corresponder a la expansión binaria correcta de .
En este tutorial, presentamos una implementación del algoritmo QPE utilizando las herramientas de supresión de errores y gestión de rendimiento Fire Opal de Q-CTRL, ofrecidas como una función de Qiskit (consulta la documentación de Fire Opal). Fire Opal aplica automáticamente optimizaciones avanzadas, incluyendo desacoplamiento dinámico, mejoras en la disposición de qubits y técnicas de supresión de errores, lo que resulta en resultados de mayor fidelidad. Estas mejoras acercan las distribuciones de cadenas de bits del hardware a las obtenidas en simulaciones sin ruido, de modo que puedas identificar de manera confiable la fase propia correcta incluso bajo los efectos del ruido.
Requisitos
Antes de comenzar este tutorial, asegúrate de tener instalado lo siguiente:
- Qiskit SDK v1.4 o posterior, con soporte de visualización
- Qiskit Runtime v0.40 o posterior (
pip install qiskit-ibm-runtime) - Qiskit Functions Catalog v0.9.0 (
pip install qiskit-ibm-catalog) - Fire Opal SDK v9.0.2 o posterior (
pip install fire-opal) - Q-CTRL Visualizer v8.0.2 o posterior (
pip install qctrl-visualizer)
Configuración
Primero, autentíquese utilizando su clave de API de IBM Quantum. Luego, seleccione la función de Qiskit de la siguiente manera. (Este código asume que ya ha guardado su cuenta en su entorno local.)
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qctrlvisualizer qiskit qiskit-aer qiskit-ibm-catalog qiskit-ibm-runtime
from qiskit import QuantumCircuit
import numpy as np
import matplotlib.pyplot as plt
import qiskit
from qiskit import qasm2
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
import qctrlvisualizer as qv
from qiskit_ibm_catalog import QiskitFunctionsCatalog
plt.style.use(qv.get_qctrl_style())
catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")
# Access Function
perf_mgmt = catalog.load("q-ctrl/performance-management")
Paso 1: Mapear las entradas clásicas a un problema cuántico
En este tutorial, ilustramos la QPE para recuperar la fase propia de una unitaria de un solo qubit conocida. La unitaria cuya fase queremos estimar es la compuerta de fase de un solo qubit aplicada al qubit objetivo:
Preparamos su estado propio . Dado que es un vector propio de con valor propio , la fase propia a estimar es:
Establecemos , por lo que la fase verdadera es . El circuito QPE implementa las potencias controladas aplicando rotaciones de fase controladas con ángulos , luego aplica la QFT inversa al registro de conteo y lo mide. Las cadenas de bits resultantes se concentran alrededor de la representación binaria de .
El circuito utiliza qubits de conteo (para establecer la precisión de la estimación) más un qubit objetivo. Comenzamos definiendo los bloques de construcción necesarios para implementar la QPE: la transformada de Fourier cuántica (QFT) y su inversa, funciones utilitarias para mapear entre fracciones decimales y binarias de la fase propia, y funciones auxiliares para normalizar los conteos brutos en probabilidades para comparar los resultados de simulación y hardware.
def inverse_quantum_fourier_transform(quantum_circuit, number_of_qubits):
"""
Apply an inverse Quantum Fourier Transform the first `number_of_qubits` qubits in the
`quantum_circuit`.
"""
for qubit in range(number_of_qubits // 2):
quantum_circuit.swap(qubit, number_of_qubits - qubit - 1)
for j in range(number_of_qubits):
for m in range(j):
quantum_circuit.cp(-np.pi / float(2 ** (j - m)), m, j)
quantum_circuit.h(j)
return quantum_circuit
def bitstring_count_to_probabilities(data, shot_count):
"""
This function turns an unsorted dictionary of bitstring counts into a sorted dictionary
of probabilities.
"""
# Turn the bitstring counts into probabilities.
probabilities = {
bitstring: bitstring_count / shot_count
for bitstring, bitstring_count in data.items()
}
sorted_probabilities = dict(
sorted(probabilities.items(), key=lambda x: x[1], reverse=True)
)
return sorted_probabilities
Paso 2: Optimizar el problema para la ejecución en hardware cuántico
Construimos el circuito QPE preparando los qubits de conteo en superposición, aplicando rotaciones de fase controladas para codificar la fase propia objetivo y finalizando con una QFT inversa antes de la medición.
def quantum_phase_estimation_benchmark_circuit(
number_of_counting_qubits, phase
):
"""
Create the circuit for quantum phase estimation.
Parameters
----------
number_of_counting_qubits : The number of qubits in the circuit.
phase : The desired phase.
Returns
-------
QuantumCircuit
The quantum phase estimation circuit for `number_of_counting_qubits` qubits.
"""
qc = QuantumCircuit(
number_of_counting_qubits + 1, number_of_counting_qubits
)
target = number_of_counting_qubits
# |1> eigenstate for the single-qubit phase gate
qc.x(target)
# Hadamards on counting register
for q in range(number_of_counting_qubits):
qc.h(q)
# ONE controlled phase per counting qubit: cp(phase * 2**k)
for k in range(number_of_counting_qubits):
qc.cp(phase * (1 << k), k, target)
qc.barrier()
# Inverse QFT on counting register
inverse_quantum_fourier_transform(qc, number_of_counting_qubits)
qc.barrier()
for q in range(number_of_counting_qubits):
qc.measure(q, q)
return qc
Paso 3: Ejecutar utilizando primitivas de Qiskit
Establecemos el número de disparos y qubits para el experimento, y codificamos la fase objetivo utilizando dígitos binarios. Con estos parámetros, construimos el circuito QPE que se ejecutará en simulación, hardware predeterminado y backends mejorados con Fire Opal.
shot_count = 10000
num_qubits = 35
phase = (1 / 6) * 2 * np.pi
circuits_quantum_phase_estimation = (
quantum_phase_estimation_benchmark_circuit(
number_of_counting_qubits=num_qubits, phase=phase
)
)
Ejecutar simulación MPS
Primero, generamos una distribución de referencia utilizando el simulador matrix_product_state y convertimos los conteos en probabilidades normalizadas para una comparación posterior con los resultados de hardware.
# Run the algorithm on the IBM Aer simulator.
aer_simulator = AerSimulator(method="matrix_product_state")
# Transpile the circuits for the simulator.
transpiled_circuits = qiskit.transpile(
circuits_quantum_phase_estimation, aer_simulator
)
simulated_result = (
aer_simulator.run(transpiled_circuits, shots=shot_count)
.result()
.get_counts()
)
simulated_result_probabilities = []
simulated_result_probabilities.append(
bitstring_count_to_probabilities(
simulated_result,
shot_count=shot_count,
)
)
Ejecutar en hardware
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
isa_circuits = pm.run(circuits_quantum_phase_estimation)
# Run the algorithm with IBM default.
sampler = Sampler(backend)
# Run all circuits using Qiskit Runtime.
ibm_default_job = sampler.run([isa_circuits], shots=shot_count)
Ejecutar en hardware con Fire Opal
# Run the circuit using the sampler
fire_opal_job = perf_mgmt.run(
primitive="sampler",
pubs=[qasm2.dumps(circuits_quantum_phase_estimation)],
backend_name=backend.name,
options={"default_shots": shot_count},
)
Paso 4: Post-procesar y devolver el resultado en el formato clásico deseado
# Retrieve results.
ibm_default_result = ibm_default_job.result()
ibm_default_probabilities = []
for idx, pub_result in enumerate(ibm_default_result):
ibm_default_probabilities.append(
bitstring_count_to_probabilities(
pub_result.data.c0.get_counts(),
shot_count=shot_count,
)
)
fire_opal_result = fire_opal_job.result()
fire_opal_probabilities = []
for idx, pub_result in enumerate(fire_opal_result):
fire_opal_probabilities.append(
bitstring_count_to_probabilities(
pub_result.data.c0.get_counts(),
shot_count=shot_count,
)
)
data = {
"simulation": simulated_result_probabilities,
"default": ibm_default_probabilities,
"fire_opal": fire_opal_probabilities,
}
def plot_distributions(
data,
number_of_counting_qubits,
top_k=None,
by="prob",
shot_count=None,
):
def nrm(d):
s = sum(d.values())
return {k: (v / s if s else 0.0) for k, v in d.items()}
def as_float(d):
return {k: float(v) for k, v in d.items()}
def to_space(d):
if by == "prob":
return nrm(as_float(d))
else:
if shot_count and 0.99 <= sum(d.values()) <= 1.01:
return {
k: v * float(shot_count) for k, v in as_float(d).items()
}
else:
return as_float(d)
def topk(d, k):
items = sorted(d.items(), key=lambda kv: kv[1], reverse=True)
return items[: (k or len(d))]
phase = "1/6"
sim = to_space(data["simulation"])
dft = to_space(data["default"])
qct = to_space(data["fire_opal"])
correct = max(sim, key=sim.get) if sim else None
print("Correct result:", correct)
sim_items = topk(sim, top_k)
dft_items = topk(dft, top_k)
qct_items = topk(qct, top_k)
sim_keys, y_sim = zip(*sim_items) if sim_items else ([], [])
dft_keys, y_dft = zip(*dft_items) if dft_items else ([], [])
qct_keys, y_qct = zip(*qct_items) if qct_items else ([], [])
fig, axes = plt.subplots(3, 1, layout="constrained")
ylab = "Probabilities"
def panel(ax, keys, ys, title, color):
x = np.arange(len(keys))
bars = ax.bar(x, ys, color=color)
ax.set_title(title)
ax.set_ylabel(ylab)
ax.set_xticks(x)
ax.set_xticklabels(keys, rotation=90)
ax.set_xlabel("Bitstrings")
if correct in keys:
i = keys.index(correct)
bars[i].set_edgecolor("black")
bars[i].set_linewidth(2)
return max(ys, default=0.0)
c_sim, c_dft, c_qct = (
qv.QCTRL_STYLE_COLORS[5],
qv.QCTRL_STYLE_COLORS[1],
qv.QCTRL_STYLE_COLORS[0],
)
m1 = panel(axes[0], list(sim_keys), list(y_sim), "Simulation", c_sim)
m2 = panel(axes[1], list(dft_keys), list(y_dft), "Default", c_dft)
m3 = panel(axes[2], list(qct_keys), list(y_qct), "Q-CTRL", c_qct)
for ax, m in zip(axes, (m1, m2, m3)):
ax.set_ylim(0, 1.05 * (m or 1.0))
for ax in axes:
ax.label_outer()
fig.suptitle(
rf"{number_of_counting_qubits} counting qubits, $2\pi\varphi$={phase}"
)
fig.set_size_inches(20, 10)
plt.show()
experiment_index = 0
phase_index = 0
distributions = {
"simulation": data["simulation"][phase_index],
"default": data["default"][phase_index],
"fire_opal": data["fire_opal"][phase_index],
}
plot_distributions(
distributions, num_qubits, top_k=100, by="prob", shot_count=shot_count
)
Correct result: 00101010101010101010101010101010101