Skip to main content
IBM Quantum Platform

Estimación de fase cuántica con el controlador de fase de Q-CTRL ( Qiskit Functions )

Estimación de uso: 40 segundos en un procesador Heron r2. (NOTA: Esto es sólo una estimación. Su tiempo de ejecución puede variar)


En segundo plano

La estimación cuántica de fase (QPE) es un algoritmo fundacional de 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 básico de la química cuántica y los problemas de valores propios. QPE estima la fase φ\varphi asociada a 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 lo determina con una precisión de ϵ=O(1/2m)\epsilon = O(1/2^m) utilizando mm qubits contadores [1]. Preparando estos qubits en superposición, aplicando potencias controladas de UU, y utilizando a continuación la transformada cuántica de Fourier inversa (QFT) para extraer la fase en resultados de medición codificados en binario, la QPE produce una distribución de probabilidad con un pico en las cadenas de bits cuyas fracciones binarias se aproximan a φ\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 recuento. Sin embargo, ejecutar circuitos QPE profundos en hardware presenta desafíos: el gran número de qubits y operaciones de enredo hacen que el algoritmo sea muy sensible a la decoherencia y a los errores de puerta. Esto da lugar a distribuciones ampliadas y desplazadas de las cadenas de bits, enmascarando la verdadera fase propia. Como consecuencia, es posible que la cadena de bits con mayor probabilidad ya no corresponda 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 Qiskit (consulte la documentación de Fire Opal ). Fire Opal aplica automáticamente optimizaciones avanzadas, como el desacoplamiento dinámico, mejoras en la disposición de los qubits y técnicas de supresión de errores, lo que se traduce en resultados de mayor fidelidad. Estas mejoras acercan las distribuciones de cadenas de bits de hardware a las obtenidas en simulaciones sin ruido, de modo que se puede identificar con fiabilidad la fase propia correcta incluso bajo los efectos del ruido.


Requisitos

Antes de empezar 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 API IBM Quantum. A continuación, selecciona la función de Qiskit de la siguiente manera. (Este código da por hecho que ya has guardado tu cuenta en tu entorno local.)

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: Asignar entradas clásicas a un problema cuántico

En este tutorial, ilustramos QPE para recuperar la eigenfase de un unitario single-qubit conocido. El unitario cuya fase queremos estimar es la puerta 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}

Fijamos θ=162π\theta=\tfrac{1}{6}\cdot 2\pi, de modo que la fase de verdad fundamental 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 recuento y lo mide. Las cadenas de bits resultantes se concentran en torno a la representación binaria de 1/61/6.

El circuito utiliza mm qubits contadores (para establecer la precisión de la estimación) más un qubit objetivo. Empezaremos definiendo los componentes necesarios para implementar la QPE: la transformada cuántica de Fourier (QFT) y su inversa, funciones de utilidad para mapear entre fracciones decimales y binarias de la fase propia, y ayudantes para normalizar los recuentos 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 contadores en superposición, aplicando rotaciones de fase controladas para codificar la fase propia objetivo y terminando 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 Qiskit primitives

Fijamos 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 por defecto y backends mejorados 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

En primer lugar, generamos una distribución de referencia utilizando el simulador matrix_product_state y convertimos los recuentos en probabilidades normalizadas para su posterior comparación con los resultados del 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 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: Procesamiento posterior y devolución del 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
)

Output:

Correct result: 00101010101010101010101010101010101
Output of the previous code cell

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


Referencias

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


Encuesta tutorial

Tómese un minuto para comentar este tutorial. Su opinión nos ayudará a mejorar nuestra oferta de contenidos y la experiencia de los usuarios.

Enlace a la encuesta

¿Le ha resultado útil esta página?
Informe de un error, de una errata o solicite contenido en GitHub.