Saltar al contenido principal

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 φ\varphi asociada con un estado propio de un operador unitario, codificada en la relación

Uφ=e2πiφφ,U \lvert \varphi \rangle = e^{2\pi i \varphi} \lvert \varphi \rangle,

y la determina con una precisión de ϵ=O(1/2m)\epsilon = O(1/2^m) utilizando mm qubits de conteo [1]. Al preparar estos qubits en superposición, aplicar potencias controladas de UU 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 φ\varphi. 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 φ\varphi.

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:

U(θ)=(100eiθ)=eiθ1 ⁣1.U(\theta)= \begin{pmatrix} 1 & 0\\[2pt] 0 & e^{i\theta} \end{pmatrix} = e^{i\theta\,|1\rangle\!\langle 1|}.

Preparamos su estado propio ψ=1|\psi\rangle=|1\rangle. Dado que 1|1\rangle es un vector propio de U(θ)U(\theta) con valor propio eiθe^{i\theta}, la fase propia a estimar es:

φ=θ2π(mod1)\varphi = \frac{\theta}{2\pi} \pmod{1}

Establecemos θ=162π\theta=\tfrac{1}{6}\cdot 2\pi, por lo que la fase verdadera es φ=1/6\varphi=1/6. El circuito QPE implementa las potencias controladas U2kU^{2^k} aplicando rotaciones de fase controladas con ángulos θ2k\theta\cdot2^k, 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 1/61/6.

El circuito utiliza mm 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 φ=1/6\varphi = 1/6 utilizando mm 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

Salida de la celda de c�ódigo anterior

La simulación establece la línea base para la fase propia correcta. Las ejecuciones de hardware predeterminadas muestran ruido que oscurece este resultado, ya que el ruido distribuye la probabilidad entre muchas cadenas de bits incorrectas. Con la gestión de rendimiento de Q-CTRL, la distribución se vuelve más nítida y el resultado correcto se recupera, lo que permite una QPE confiable a esta escala.

Referencias

[1] Lección 7: Estimación de fase y factorización. IBM Quantum Learning - Fundamentos de algoritmos cuánticos. Consultado el 3 de octubre de 2025.

Encuesta del tutorial

Por favor, tómese un minuto para proporcionar comentarios sobre este tutorial. Sus opiniones nos ayudarán a mejorar nuestras ofertas de contenido y la experiencia del usuario.

Enlaza a la encuesta

Note: This survey is provided by IBM Quantum and relates to the original English content. To give feedback on doQumentation's website, translations, or code execution, please open a GitHub issue.

Source: IBM Quantum docs — updated 27 abr 2026
English version on doQumentation — updated 7 may 2026
This translation based on the English version of 9 abr 2026