Zum Hauptinhalt springen

Quantenphasenschätzung mit den Qiskit Functions von Q-CTRL

Geschätzte Nutzungsdauer: 40 Sekunden auf einem Heron-r2-Prozessor. (HINWEIS: Dies ist nur eine Schätzung. Deine tatsächliche Laufzeit kann abweichen.)

Hintergrund

Quantenphasenschätzung (QPE) ist ein grundlegender Algorithmus im Quantencomputing, der die Basis vieler wichtiger Anwendungen bildet, wie zum Beispiel Shors Algorithmus, die Berechnung von Grundzustandsenergien in der Quantenchemie und Eigenwertprobleme. QPE schätzt die Phase φ\varphi, die mit einem Eigenzustand eines unitären Operators verknüpft ist, gemäß der Beziehung

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

und bestimmt sie mit einer Präzision von ϵ=O(1/2m)\epsilon = O(1/2^m) unter Verwendung von mm Zähl-Qubits [1]. Indem diese Qubits in Superposition gebracht werden, kontrollierte Potenzen von UU angewendet werden und anschließend die inverse Quantenfouriertransformation (QFT) die Phase in binär-kodierte Messergebnisse überführt, erzeugt QPE eine Wahrscheinlichkeitsverteilung mit einem Maximum bei Bitstrings, deren binäre Brüche φ\varphi approximieren. Im idealen Fall entspricht das wahrscheinlichste Messergebnis direkt der Binärentwicklung der Phase, während die Wahrscheinlichkeit anderer Ergebnisse mit der Anzahl der Zähl-Qubits schnell abnimmt. Das Ausführen tiefer QPE-Schaltkreise auf Hardware bringt jedoch Herausforderungen mit sich: Die große Anzahl an Qubits und verschränkenden Operationen macht den Algorithmus sehr anfällig für Dekohärenz und Gate-Fehler. Dies führt zu verbreiterten und verschobenen Bitstring-Verteilungen, die die wahre Eigenphase verdecken. Als Folge davon entspricht der Bitstring mit der höchsten Wahrscheinlichkeit möglicherweise nicht mehr der korrekten Binärentwicklung von φ\varphi.

In diesem Tutorial stellen wir eine Implementierung des QPE-Algorithmus mit den Fire-Opal-Fehlerunterdrückungs- und Performance-Management-Tools von Q-CTRL vor, die als Qiskit Function angeboten werden (siehe die Fire-Opal-Dokumentation). Fire Opal wendet automatisch fortgeschrittene Optimierungen an, darunter dynamisches Entkoppeln, verbesserte Qubit-Layouts und Fehlerunterdrückungstechniken, was zu höherwertigen Ergebnissen führt. Diese Verbesserungen bringen die Hardware-Bitstring-Verteilungen näher an jene aus rauschfreien Simulationen, sodass du die korrekte Eigenphase auch unter Rauscheinflüssen zuverlässig identifizieren kannst.

Voraussetzungen

Stelle vor Beginn dieses Tutorials sicher, dass Folgendes installiert ist:

  • Qiskit SDK v1.4 oder höher, mit Unterstützung für Visualisierung
  • Qiskit Runtime v0.40 oder höher (pip install qiskit-ibm-runtime)
  • Qiskit Functions Catalog v0.9.0 (pip install qiskit-ibm-catalog)
  • Fire Opal SDK v9.0.2 oder höher (pip install fire-opal)
  • Q-CTRL Visualizer v8.0.2 oder höher (pip install qctrl-visualizer)

Einrichtung

Authentifiziere dich zunächst mit deinem IBM Quantum API-Schlüssel. Wähle dann die Qiskit Function wie folgt aus. (Dieser Code setzt voraus, dass du dein Konto bereits lokal gespeichert hast.)

# 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")

Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden

In diesem Tutorial illustrieren wir QPE zur Wiederherstellung der Eigenphase eines bekannten Einzel-Qubit-Unitären. Das Unitäre, dessen Phase wir schätzen möchten, ist das auf das Ziel-Qubit angewendete Einzel-Qubit-Phasengate:

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|}.

Wir bereiten den Eigenzustand ψ=1|\psi\rangle=|1\rangle vor. Da 1|1\rangle ein Eigenvektor von U(θ)U(\theta) mit dem Eigenwert eiθe^{i\theta} ist, lautet die zu schätzende Eigenphase:

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

Wir setzen θ=162π\theta=\tfrac{1}{6}\cdot 2\pi, sodass die wahre Phase φ=1/6\varphi=1/6 beträgt. Der QPE-Schaltkreis implementiert die kontrollierten Potenzen U2kU^{2^k} durch Anwenden kontrollierter Phasenrotationen mit den Winkeln θ2k\theta\cdot2^k, wendet anschließend die inverse QFT auf das Zählregister an und misst dieses. Die resultierenden Bitstrings konzentrieren sich um die Binärdarstellung von 1/61/6.

Der Schaltkreis verwendet mm Zähl-Qubits (zur Festlegung der Schätzpräzision) plus ein Ziel-Qubit. Wir beginnen damit, die Bausteine für die QPE-Implementierung zu definieren: die Quantenfouriertransformation (QFT) und ihre Inverse, Hilfsfunktionen zur Umrechnung zwischen Dezimal- und Binärbrüchen der Eigenphase sowie Hilfsprogramme zur Normalisierung von Rohzählungen in Wahrscheinlichkeiten für den Vergleich von Simulations- und Hardware-Ergebnissen.

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

Schritt 2: Problem für die Quantenhardware-Ausführung optimieren

Wir erstellen den QPE-Schaltkreis, indem wir die Zähl-Qubits in Superposition versetzen, kontrollierte Phasenrotationen zur Kodierung der Ziel-Eigenphase anwenden und mit einer inversen QFT vor der Messung abschließen.

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

Schritt 3: Mit Qiskit Primitives ausführen

Wir legen die Anzahl der Shots und Qubits für das Experiment fest und kodieren die Zielphase φ=1/6\varphi = 1/6 mit mm Binärstellen. Mit diesen Parametern erstellen wir den QPE-Schaltkreis, der auf Simulation, Standard-Hardware und Fire-Opal-optimierten Backends ausgeführt wird.

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
)
)

MPS-Simulation ausführen

Zunächst erzeugen wir mit dem matrix_product_state-Simulator eine Referenzverteilung und konvertieren die Zählungen in normalisierte Wahrscheinlichkeiten für den späteren Vergleich mit Hardware-Ergebnissen.

# 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,
)
)

Auf Hardware ausführen

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)

Mit Fire Opal auf Hardware ausführen

# 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},
)

Schritt 4: Nachverarbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format

# 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

Output of the previous code cell

Die Simulation liefert den Ausgangspunkt für die korrekte Eigenphase. Standard-Hardware-Läufe zeigen Rauschen, das dieses Ergebnis verdeckt, da das Rauschen die Wahrscheinlichkeit auf viele falsche Bitstrings verteilt. Mit Q-CTRL Performance Management wird die Verteilung schärfer und das korrekte Ergebnis wird wiederhergestellt, was eine zuverlässige QPE in diesem Maßstab ermöglicht.

Referenzen

[1] Vorlesung 7: Phasenschätzung und Faktorisierung. IBM Quantum Learning - Grundlagen der Quantenalgorithmen. Abgerufen am 3. Oktober 2025.

Tutorial-Umfrage

Nimm dir bitte eine Minute Zeit, um Feedback zu diesem Tutorial zu geben. Deine Einblicke helfen uns, unser Inhaltsangebot und die Benutzererfahrung zu verbessern.

Link zur Umfrage