Zum Hauptinhalt springen

Simulation eines gekickten Ising-Modells mit der TEM-Funktion

Die Tensor-Network Error Mitigation (TEM) von Algorithmiq ist ein hybrider quanten-klassischer Algorithmus, der darauf ausgelegt ist, Rauschminderung vollständig in der klassischen Nachverarbeitungsphase durchzuführen. Mit TEM kannst du Erwartungswerte von Observablen berechnen und dabei die unvermeidlichen rauschbedingten Fehler, die auf Quantenhardware auftreten, mit erhöhter Genauigkeit und Kosteneffizienz kompensieren – was es zu einer äußerst attraktiven Option für Quantenforschende und Fachleute aus der Industrie macht.

Dieses Tutorial zeigt, wie TEM sinnvolle Ergebnisse für die Dynamik eines Quantensystems erzielen kann, die ohne Fehlerminderung nicht zugänglich wären und die bei anderen Fehlerminderungsmethoden wie PEC und ZNE deutlich mehr Quantenressourcen erfordern würden.

Nutzungsschätzung: Dieses Notebook verwendet ungefähr 10 QPU-Minuten auf Heron-r3-Geräten. Die Laufzeit kann je nach gewähltem Gerät erheblich variieren. Nutzungsschätzungen pro Abschnitt findest du weiter unten.

Fehlergeminderte Experimente mit dem gekickten Ising-Modell und der TEM-Funktion

Dieses Tutorial basiert auf folgender Referenz: L. E. Fischer et al., Nat. Phys. (2026). Diese Referenz beschreibt eine reale Simulation auf Quantenhardware mit bis zu 91 Qubits. In diesem Tutorial stellen wir eine ähnliche Simulation mit einer kleineren Schaltkreisgröße nach.

Das gekickte Ising-Modell entspricht dem üblichen Ising-Modell:

H^I=Jn=0N2Z^nZ^n+1+hn=0N1Z^n\hat{H}_{\text{I}} = J \sum_{n=0}^{N-2} \hat{Z}_n \hat{Z}_{n+1} + h \sum_{n=0}^{N-1} \hat{Z}_n

auf das ein transversaler Kick angewendet wird:

H^K=bn=0N1X^n\hat{H}_{K} = b \sum_{n=0}^{N-1} \hat{X}_n

Das Ziel ist es, die Dynamik eines Zustands unter dem transversal gekickten Ising-Hamiltonian zu simulieren, dessen Zeitentwicklung durch einen Floquet-Unitären U^KI=eiH^KeiH^I\hat{U}_{\text{KI}} = e^{-i \hat{H}_K} e^{-i \hat{H}_I} implementiert werden kann. Der zu entwickelnde Anfangszustand ist derjenige, bei dem sich das erste Qubit im Zustand +|+\rangle befindet, während die übrigen paarweise in den Bell-Zustand (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2} versetzt sind.

Die Größe, die wir beobachten möchten, ist die Korrelationsfunktion. Das Referenzpapier erläutert, wie diese Größe als X^\hat{X}-Pauli-Operator auf dem ntenn^{ten} Qubit umgeschrieben werden kann. Nach einer Anzahl von physikalischen Zeitschritten tt berechnen wir den Wert des Pauli-Operators X^n=t\hat{X}_{n=t}. Abhängig von den Parametern des Systems ist der Wert dieser Observablen gleich einem Wert, der exakt berechnet werden kann, oder nur durch Näherungsmethoden simulierbar ist. Insbesondere gilt für J=b=π/4|J|=|b|=\pi/4, dass er gleich [cos(2h)]t[\cos(2h)]^t ist – dies ist der Wert, den wir als Referenz für die Ergebnisse dieses Tutorials verwenden werden. Darüber hinaus gilt bei einem gegebenen Zeitschritt tt, dass X^nt\langle\hat{X}_{n\neq t}\rangle null ist. Einzelheiten zur Herleitung dieser Werte sowie Vergleiche mit approximativen klassischen Simulationsergebnissen außerhalb dieser Parameter findest du in L. E. Fischer et al., Nat. Phys. (2026).

TEM funktioniert, indem zunächst das Rauschen für jede einzigartige Schicht von Zwei-Qubit-Gates im Schaltkreis sowie der Auslesefehler charakterisiert werden. Anschließend wird der Schaltkreis auf dem Quantenrechner ausgeführt. Schließlich wird die Tensor-Netzwerk-Fehlerminderung auf klassischen Ressourcen in IBM Cloud® durchgeführt und der geminderte Wert zurückgegeben. In diesem Beispiel hat der Schaltkreis zwei einzigartige Schichten, die charakterisiert werden müssen.

Setup

Stelle als Voraussetzung sicher, dass die notwendigen Abhängigkeiten installiert sind.

%pip install numpy matplotlib qiskit qiskit-ibm-catalog qiskit-ibm-runtime pylatexenc qiskit_qasm3_import
import os
from matplotlib import pyplot as plt
import numpy as np

from qiskit.quantum_info import SparsePauliOp
from qiskit.qasm3 import load

from qiskit_ibm_catalog import QiskitFunctionsCatalog

Fehlerminderung mit TEM

Wir stellen hier einen Schaltkreis bereit, der das oben beschriebene gekickte Ising-Modell implementiert. Der Schaltkreis wird wie folgt vorbereitet. Zunächst gibt es eine Zustandsvorbereitungsphase, in der sich das erste Qubit im Zustand +|+\rangle befindet, während die übrigen in Bell-Paaren (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2} sind. Darauf folgt die Ziegelmauerstruktur, die die unitäre Zeitentwicklung U^KI\hat{U}_{\text{KI}} implementiert. Die Anzahl der physikalischen Zeitschritte entspricht t/2t/2 Schaltkreisschichten. Der folgende Code lädt die zwei QASM-Dateien herunter, die für dieses Tutorial benötigt werden.

# Download required QASM files
import urllib

urllib.request.urlretrieve(
"https://ibm.box.com/shared/static/swy5jtq309b0xpzluzlmsmj908yphes8.qasm",
"ki_30q.qasm",
)
urllib.request.urlretrieve(
"https://ibm.box.com/shared/static/et3gkodonw6gsp2trs43lzaozrdtiu7s.qasm",
"ki_12q.qasm",
)

Wir können eine kleine Version des Schaltkreises mit 12 Qubits und sechs Zeitschritten visualisieren:

# Parameters of the kicked Ising model
h = 0.0
num_qubits = 12
t_steps = 6

# Load the circuit for the kicked Ising model
small_circuit = load("ki_12q.qasm")

# Draw the circuit
small_circuit.draw("mpl", scale=0.25, fold=-1)

Output of the previous code cell

Als nächstes erstellen wir die Observable X^n=t\hat{X}_{n=t}. Sie wird als einfacher Pauli-String konstruiert, dessen Reihenfolge der von Qiskit verwendeten entspricht:

def xt_observable(n_qubits, t_steps):
pauli_str = "".join(["I" * t_steps, "X", "I" * (n_qubits - t_steps - 1)])
pauli_str = pauli_str[::-1] # Reverse the string to match qiskit order
return SparsePauliOp(data=pauli_str, coeffs=1.0)

In unserem kleinen 12-Qubit-Beispiel sieht die Observable so aus:

# Build the observable for the kicked Ising model
small_observable = xt_observable(n_qubits=12, t_steps=6)
print(small_observable)
SparsePauliOp(['IIIIIXIIIIII'],
coeffs=[1.+0.j])

Qiskit-Funktionen verwenden PUBs als Weg, die Eingaben zu erfassen. In unserem Fall betrachten wir einen einzelnen Schaltkreis und eine Observable als unser PUB:

# Collect the input PUBs, in this case composed of a
# single circuit and observable
pubs = [(small_circuit, [small_observable])]

Als nächstes erhalten wir Zugang zur TEM-Funktion. Wir richten zunächst die erforderliche Authentifizierung für IBM Cloud ein und wählen ein Backend aus den verfügbaren Geräten aus. Das Token, die verfügbaren Backends und die entsprechenden Cloud-Ressourcennamen (CRN) können durch Anmeldung bei deinem Konto auf dem IBM Quantum Platform-Dashboard abgerufen werden.

# Set IBM Quantum credentials and backend configuration
personal_token = os.environ.get(
"QISKIT_IBM_TOKEN", "<API-KEY>"
) # Replace with your personal token or set the environment variable
channel = "ibm_quantum_platform"
crn = "your_crn" # Replace with the Cloud Resource Name (CRN)

# Select the QPU backend
backend_name = "ibm_qpu_name" # Replace with your desired backend's name

Lade die TEM-Funktion aus dem Qiskit Functions Catalog:

# Load the TEM function from the Qiskit Functions Catalog
catalog = QiskitFunctionsCatalog(
channel=channel,
token=personal_token,
instance=crn,
)
tem = catalog.load("algorithmiq/tem")

Wir können jetzt ein Experiment auf dem gekickten Ising-Schaltkreis mit durch TEM bereitgestellter Fehlerminderung ausführen. Mit den Standardeinstellungen kann TEM auf einfache Weise ausgeführt werden, mit einer erwarteten QPU-Laufzeit von etwa 2,5 Minuten, abhängig vom QPU:

tem_job = tem.run(pubs=pubs, backend_name=backend_name)

Mit den Standardoptionen führt die TEM-Funktion drei Jobs auf dem Quantencomputer aus: Rauschlernen, Ausleseminderung und Schaltkreisabtastung. Die Anzahl der Shots, die von jedem dieser Jobs verwendet werden, kann in den an die Funktion übergebenen Optionen geändert werden. Standardmäßig sind diese Parameter so eingestellt, dass eine Genauigkeit von 0,05 in den geminderten Erwartungswerten erzielt wird. Du kannst den Status deines Jobs auf dem IBM Quantum Platform-Dashboard oder wie folgt überprüfen:

print(tem_job.status())
QUEUED

Wenn der Status DONE ist, können wir die unbearbeiteten und geminderten Ergebnisse überprüfen. Die unten definierten tem_evs sind die Erwartungswerte der angeforderten Observablen – in diesem Fall nur eine Observable, X^n=t\langle \hat X_{n=t}\rangle – und tem_std sind die entsprechenden Standardabweichungen.

# Get the results of the TEM job
tem_results = tem_job.result()[
0
] # Get the first and only result from the job
tem_evs = tem_results.data.evs[0]
tem_std = tem_results.data.stds[0]

print(f"TEM Result: {tem_evs:.3f} ± {tem_std:.3f}")
TEM Result: 1.031 ± 0.046

Wir können auch überprüfen, wie viel Quantenlaufzeit für jeden Aufruf auf der IBM Quantum Platform verwendet wurde, oder indem wir die Ergebnis-Metadaten aus dem Python-Code untersuchen.

# Get the TEM job runtime
tem_runtime = tem_job.result().metadata["resource_usage"][
"RUNNING: EXECUTING_QPU"
]["QPU_TIME"]

print(f"TEM Runtime: {tem_runtime} seconds")
TEM Runtime: 155.0 seconds

TEM-Parameter anpassen und erweiterte Optionen

Die TEM-Funktion bietet mehrere erweiterte Optionen, mit denen du deinen Fehlerminderungs-Workflow anpassen kannst. Diese Optionen erlauben es dir, die Präzision, die Anzahl der Shots, Rauschlernerstrategien und weitere Parameter zu steuern, um die Anforderungen deines Experiments und die verfügbaren Quantenressourcen besser zu berücksichtigen.

Häufig genutzte erweiterte Optionen sind:

  • precision: Gibt die Zielpräzision für die geminderten Erwartungswerte an.
  • default_shots: Anstelle von precision kannst du die Anzahl der Shots angeben, die der Messprozess verwendet.
  • tem_max_bond_dimension: Die maximale Bond-Dimension, die im Tensornetzwerk verwendet wird.
  • tem_compression_cutoff: Der Schwellenwert, der für das Tensornetzwerk verwendet wird.
  • Rauschlernen-Optionen: Konfiguriere, wie das Rauschen charakterisiert wird, z. B. die Anzahl der Wiederholungen oder spezifische Kalibrierungsschaltkreise.
  • private: Stelle sicher, dass Circuits und Experimentergebnisse nur für dich sichtbar sind, und deaktiviere mehrfache Downloads von Job-Ergebnissen.

Die vollständige Liste der unterstützten Optionen und deren Beschreibungen findest du in der TEM-Dokumentation oder im Qiskit Functions Catalog. Du kannst diese Parameter anpassen, um das richtige Gleichgewicht zwischen Laufzeit, Ressourcenverbrauch und Ergebnisgenauigkeit zu finden. Diese Optionen kannst du als Dictionary über das Argument options übergeben, wenn du die TEM-Funktion ausführst:

options = {
"default_shots": 10_000,
"tem_max_bond_dimension": 512,
"tem_compression_cutoff": 1e-16,
# This option helps optimizing the measurement
# stage since the observable is strongly biased
# toward the X operator for all the qubits.
"compute_shadows_bias_from_observable": True,
# set to True to keep experiment results private,
# recommended for confidential circuits
"private": False,
}

Es können auch benutzerdefinierte Optionen für den Rausch-Lerner übergeben werden. Diese folgen den Definitionen, die in den Qiskit Runtime NoiseLearnerOptions verwendet werden:

nl_options = {
"num_randomizations": 32,
"max_layers_to_learn": 2,
"shots_per_randomization": 128,
"layer_pair_depths": [0, 1, 2, 4, 16, 32],
}

# add noise learning options to the overall options
options |= nl_options

Führe das Experiment erneut mit diesen auf unseren Circuit abgestimmten benutzerdefinierten Optionen aus. Die erwartete Laufzeit beträgt ungefähr vier QPU-Minuten.

tem_job_custom = tem.run(
pubs=pubs, backend_name=backend_name, options=options
)

Wenn der Job nicht als privat eingestellt ist, kann das Ergebnis zu einem späteren Zeitpunkt abgerufen werden. Speichere dazu die hier ausgegebene Job-ID und verwende tem_job_custom = catalog.get_job_by_id("your-job-id").

job_id = tem_job_custom.job_id
print(f"Job ID: {job_id}")
Job ID: 1ba10094-a541-457a-9287-dbd49306d12d
results_custom = tem_job_custom.result()
tem_evs = results_custom[0].data.evs[0]
tem_std = results_custom[0].data.stds[0]

print(f"TEM Result: {tem_evs:.3f} ± {tem_std:.3f}")
TEM Result: 0.956 ± 0.018

Nun können wir die Ergebnisse und die Metadaten untersuchen, um Einblicke in das Experiment zu gewinnen:

metadata_custom = results_custom[0].metadata

unmitigated_evs = metadata_custom["evs_non_mitigated"][0]
unmitigated_stds = metadata_custom["stds_non_mitigated"][0]
print(f"Unmitigated Result: {unmitigated_evs:.3f} ± {unmitigated_stds:.3f}")

# Exact result for the kicked Ising model from the reference paper
exact_evs = np.cos(2 * h) ** t_steps
print("Exact Result:", exact_evs)
Unmitigated Result: 0.894 ± 0.015
Exact Result: 1.0
# Plot comparing the different expectation values
plt.bar(
["Unmitigated", "TEM"],
[unmitigated_evs, tem_evs],
yerr=[unmitigated_stds, tem_std],
color=["grey", "c"],
)
plt.hlines(y=exact_evs, xmin=-0.5, xmax=1.5, colors="r", linestyles="dashed")
plt.ylabel("Expectation Value")
plt.ylim(0, 1.1)
plt.show()

Output of the previous code cell

Abschließend können wir die Auswirkungen der benutzerdefinierten Optionen auf die QPU- und klassische Laufzeit überprüfen:

# Get the metadata of the TEM job
job_metadata = results_custom.metadata

# Get the runtime of the TEM job
qpu_runtime = job_metadata["resource_usage"]["RUNNING: EXECUTING_QPU"][
"QPU_TIME"
]
classical_runtime = (
job_metadata["resource_usage"]["RUNNING: OPTIMIZING_FOR_HARDWARE"][
"CPU_TIME"
]
+ job_metadata["resource_usage"]["RUNNING: POST_PROCESSING"]["CPU_TIME"]
)

print(f"QPU Runtime: {qpu_runtime} seconds")
print(f"Classical Runtime: {classical_runtime} seconds")
QPU Runtime: 342.0 seconds
Classical Runtime: 107.632604 seconds

TEM auf große Circuits skalieren

Große Circuits können grundsätzlich mit der TEM-Funktion ausgeführt werden. Es ist jedoch wichtig, sich der Einschränkungen der klassischen Ressourcen bewusst zu sein, da TEM auf IBM Cloud-Rechnern ausgeführt wird, was zu sehr langen Laufzeiten führen kann. Für extrem große Circuits wende dich an das TEM-Support-Team unter qiskit_ibm@algorithmiq.fi.

Hier führen wir ein Beispiel mit einem größeren, nutzungsskaligen 30-Qubit-Circuit aus und optimieren die TEM-Parameter dabei auf Geschwindigkeit statt auf Genauigkeit.

# Kicked Ising model parameters
n_qubits = 30
t_steps = 15
h = 0.0

# Load the circuit for the kicked Ising model
circuit = load("ki_30q.qasm")

# Build the observable for the kicked Ising model
observable = xt_observable(n_qubits=n_qubits, t_steps=t_steps)

# Collect the input PUBs, in this case composed of a
# single circuit and observable
pubs = [(circuit, [observable])]

Definieren wir einige leistungsorientierte Optionen:

options = {
"num_randomizations": 32,
"max_layers_to_learn": 2,
"shots_per_randomization": 128,
"layer_pair_depths": [0, 1, 2, 4, 16, 32, 64],
"default_shots": 5_000,
"tem_max_bond_dimension": 128,
"tem_compression_cutoff": 1e-10,
"compute_shadows_bias_from_observable": True,
"private": False,
}

Abschließend führen wir das Experiment aus, holen das Ergebnis ab und visualisieren es. Dies wird ungefähr 3,5 QPU-Minuten dauern.

tem_job_large = tem.run(pubs=pubs, backend_name=backend_name, options=options)
job_id = tem_job_large.job_id
print(f"Job ID: {job_id}")
Job ID: 9f3f190f-f4b0-4dcb-bb83-5f71f37d0d77
results_large = tem_job_large.result()
tem_evs = results_large[0].data.evs[0]
tem_std = results_large[0].data.stds[0]

print(f"TEM Result: {tem_evs:.3f} ± {tem_std:.3f}")

# Get the metadata of the TEM job
job_metadata = tem_job_large.result().metadata

# Get the runtime of the TEM job
qpu_runtime = job_metadata["resource_usage"]["RUNNING: EXECUTING_QPU"][
"QPU_TIME"
]
classical_runtime = (
job_metadata["resource_usage"]["RUNNING: OPTIMIZING_FOR_HARDWARE"][
"CPU_TIME"
]
+ job_metadata["resource_usage"]["RUNNING: POST_PROCESSING"]["CPU_TIME"]
)

print(f"QPU Runtime: {qpu_runtime} seconds")
print(f"Classical Runtime: {classical_runtime} seconds")
TEM Result: 0.794 ± 0.026
QPU Runtime: 203.0 seconds
Classical Runtime: 251.71805499999996 seconds
# Plot comparing the different expectation values
metadata_large = results_large[0].metadata
unmitigated_evs = metadata_large["evs_non_mitigated"][0]
unmitigated_stds = metadata_large["stds_non_mitigated"][0]

exact_evs = np.cos(2 * h) ** t_steps

plt.bar(
["Unmitigated", "TEM"],
[unmitigated_evs, tem_evs],
yerr=[unmitigated_stds, tem_std],
color=["grey", "c"],
)
plt.hlines(y=exact_evs, xmin=-0.5, xmax=1.5, colors="r", linestyles="dashed")
plt.ylabel("Expectation Value")
plt.ylim(0, 1.1)
plt.show()

Output of the previous code cell