Zum Hauptinhalt springen
Dieses Projekt befindet sich in der Beta-Phase. Einen Fehler gefunden? GitHub Issue erstellen. Frage oder Idee? In Discussions fragen — wir freuen uns über dein Feedback!
Wie ist die Übersetzungsqualität auf dieser Seite?

So stimmst du die Konfigurationsoptionen ab

Die Berechnung der Schranken ist im Prinzip exakt, in der Praxis aber näherungsweise: Die Zeitentwicklung eines Pauli-Fehlerterms durch einen Circuit erzeugt einen Operator mit exponentiell vielen Termen, und die Bestimmung der Spektralnorm des resultierenden Kommutators ist selbst aufwendig. Die folgenden Optionen steuern diesen Aufwand. Sie fallen in drei Gruppen:

  • Genauigkeits-Optionen verändern den numerischen Wert der Schranken und tauschen Schärfe gegen Aufwand.

  • Umfang bestimmt, welche Schranken berechnet werden.

  • Ausführungs-Optionen verändern, wie die Berechnung abläuft (nicht, was sie berechnet).

OptionStandardSteuertAuswirkung auf das Ergebnis
Genauigkeit
evolution_max_terms1_000_000Wie viele Operatorterme die Zeitentwicklung überlebenNiedriger: günstiger, lockerere Schranken
atol_simplify1e-8Welche Kommutatorterme als vernachlässigbar verworfen werdenHöher: günstiger, lockerere Schranken
eigval_max_qubits14Wie breit ein Kommutator sein darf, damit seine Spektralnorm berechnet wirdNiedriger: günstiger, lockerere Schranken
atol_eigenvalue1e-10Wann der iterative Eigenwertlöser als konvergiert giltHöher: günstiger, ungenauere Schranken
Umfang
max_num_boxesNoneFür wie viele Boxen Schranken berechnet werdenBoxen jenseits des Limits behalten die triviale Schranke 2.0
Ausführung
timeoutNoneEin Zeitbudget (Echtzeit) für die gesamte BerechnungJede verbleibende Arbeit behält die triviale Schranke 2.0
num_processes1Wie viele Worker-Prozesse Schranken parallel berechnenFür sich allein keine

Eine Schranke ist nur nützlich, wenn sie gültig ist: Sie darf die Wirkung eines Fehlerterms auf die Observable niemals unterschätzen. Das Lockern jeder dieser Optionen außer atol_eigenvalue kann die Schranken nur größer (konservativer) machen, niemals kleiner. Die Kosten einer lockeren Einstellung sind also ein größerer Sampling- Overhead, nicht ein falscher Erwartungswert - atol_eigenvalue ist die Ausnahme.

Circuit-Definition​

Die folgenden Experimente verwenden denselben Trotter-Ising-Circuit und dieselbe Observable wie der Schnellstart-Leitfaden. Bei dieser Größe hat jede Option einen sichtbaren Effekt, während jede Berechnung noch weniger als eine Sekunde dauert.

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_slc": "qiskit-addon-slc", "samplomatic": "samplomatic"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli
from qiskit_addon_slc.utils import generate_noise_model_paulis
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.utils import find_unique_box_instructions

def trotter_ising_circuit(num_qubits, num_steps, rx_angle, rzz_angle):
"""Trotterized transverse-field Ising evolution on a 1D chain."""
circuit = QuantumCircuit(num_qubits)
for _ in range(num_steps):
circuit.rx(rx_angle, range(num_qubits))
circuit.barrier()
for start in (0, 1): # even then odd bonds
for i in range(start, num_qubits - 1, 2):
circuit.rzz(rzz_angle, i, i + 1)
circuit.barrier()
return circuit

num_qubits = 10
circuit = trotter_ising_circuit(
num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3
)
observable = Pauli("I" * num_qubits).compose("Z", [num_qubits // 2])

boxing_pass = generate_boxing_pass_manager(
inject_noise_targets="all",
inject_noise_strategy="individual_modification",
inject_noise_site="after",
twirling_strategy="active",
remove_barriers="never",
)
boxed_circuit = boxing_pass.run(circuit)
noise_model_paulis = generate_noise_model_paulis(
find_unique_box_instructions(boxed_circuit)
)

print(f"noisy layers: {len(noise_model_paulis)}")
print(
f"Pauli error terms: {sum(len(p) for p in noise_model_paulis.values())}"
)
noisy layers: 2
Pauli error terms: 198

Eine kleine Hilfsfunktion fasst jeden Satz von Schranken in einer Zeile zusammen, sodass sich die Wirkung einer Option in wenigen Zahlen statt in einer Wand aus Arrays zeigt.

Jede Schranke liegt zwischen 0 und der trivialen Schranke 2.0. Die Zusammenfassung gibt die mittlere Schranke und ihre Standardabweichung an, gefolgt davon, wie sich die Terme auf diese Gruppen verteilen:

  • Terme mit der Schranke genau 0, weil sie mit der zeitentwickelten Observable kommutieren
  • Terme mit einer berechneten Schranke zwischen 0 und 2.0
  • Terme, die bei der trivialen Schranke 2.0 bleiben

Beachte: Ein niedrigerer Mittelwert ist schärfer, und schärfer bedeutet später einen kleineren Sampling-Overhead.

Ein Sweep über eine Option markiert den Standardwert der Option mit *:

import inspect
import time

from qiskit_addon_slc.bounds import compute_forward_bounds

def summarize(rates, label, elapsed):
"""Prints a one-line summary of a set of bounds."""
# The bounds are floating-point results, so a term counts as bounded by 0, or as left at the
# trivial 2.0, when it is within rounding error of that value.
exact = np.isclose(rates, 0.0, rtol=0.0, atol=1e-12)
trivial = np.isclose(rates, 2.0, rtol=0.0, atol=1e-12)
computed = ~exact & ~trivial
print(
f"{label:28} mean {rates.mean():.4f} ± {rates.std():.3f} │ "
f"at 0: {exact.mean():4.0%} computed: {computed.mean():4.0%} "
f"at 2.0: {trivial.mean():4.0%} │ "
f"{elapsed:4.2f}s"
)

def forward(label, **kwargs):
"""Computes the forward bounds, summarizes them and returns all their rates."""
start = time.perf_counter()
bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, **kwargs
)
elapsed = time.perf_counter() - start
rates = np.concatenate([bound.rates for bound in bounds.values()])
summarize(rates, label, elapsed)
return rates

def sweep(option, values):
"""Computes the forward bounds for each value of one option, marking the default with `*`."""
default = (
inspect.signature(compute_forward_bounds).parameters[option].default
)
for value in values:
marker = "*" if value == default else " "
forward(f"{marker} {option}={value:g}", **{option: value})

baseline = forward(" defaults")
defaults mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s

Genauigkeit​

evolution_max_terms​

Die Zeitentwicklung eines Pauli-Fehlerterms durch Nicht-Clifford-Gates macht daraus eine Summe von Paulis, und diese Summe wächst mit der Circuit-Tiefe. evolution_max_terms begrenzt, wie viele dieser Terme behalten werden. Die 1-Norm des Verworfenen wird zur Schranke addiert, sodass nichts stillschweigend verloren geht.

Der Sweep reicht von schärfer als der Standard bis hin zu einem extremen Abschneiden. Eine Absenkung der Obergrenze macht die Berechnung günstiger und die Schranken lockerer.

sweep("evolution_max_terms", (10_000_000, 1_000_000, 1000, 100, 10, 2))
evolution_max_terms=1e+07 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.38s
* evolution_max_terms=1e+06 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
evolution_max_terms=1000 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
evolution_max_terms=100 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s
evolution_max_terms=10 mean 0.3927 ± 0.711 │ at 0: 59% computed: 31% at 2.0: 9% │ 0.28s
evolution_max_terms=2 mean 0.5245 ± 0.792 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.21s

Eine Anhebung der Obergrenze über den Standardwert hinaus ändert hier nichts. Für einen Circuit dieser Größe behält der Standard bereits jeden Term, sodass die Schranken so scharf sind, wie diese Option sie machen kann. Das ist der typische Fall. Das Verschärfen einer Genauigkeitsoption lohnt sich erst, wenn ihr Standardwert tatsächlich einschränkt, und das findest du mit dem obigen Experiment heraus. Führe es mit einem schärferen Wert erneut aus und prüfe, ob sich die Schranken verschieben.

Warnung: Die zugrunde liegende Propagationsroutine behandelt evolution_max_terms als vorab zu reservierende Kapazität, nicht als Obergrenze, gegen die geprüft wird. Wenn du etwas nominell Unbegrenztes übergibst, etwa numpy.iinfo(numpy.uint).max, fordert der Allokator Terabytes an Speicher an und der Prozess bricht ab. Halte dich an eine realistische Zahl. Der Standardwert von einer Million ist bereits großzügig.

atol_simplify​

Nachdem der Kommutator gebildet wurde, werden Terme mit einem Koeffizienten unterhalb von atol_simplify verworfen. Wie bei der Entwicklungsobergrenze wird die dabei verlorene 1-Norm zur Schranke addiert.

sweep("atol_simplify", (0.0, 1e-8, 1e-4, 1e-2, 1e-1))
atol_simplify=0 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
* atol_simplify=1e-08 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
atol_simplify=0.0001 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
atol_simplify=0.01 mean 0.3656 ± 0.685 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.31s
atol_simplify=0.1 mean 0.3837 ± 0.697 │ at 0: 59% computed: 31% at 2.0: 10% │ 0.31s

eigval_max_qubits​

Die schärfste verfügbare Schranke ist die Spektralnorm des Kommutators, für die eine Eigenwertberechnung nötig ist. Diese wird teuer, wenn der Kommutator breiter wird, daher legt eigval_max_qubits die Breite fest, ab der der Versuch zugunsten der Dreiecksungleichung abgebrochen wird. Diese Schranke, 2∑i∣ci∣2 \sum_i |c_i| über die Koeffizienten cic_i des Kommutators, ist gültig, aber lockerer.

Setzt du den Wert auf null, wird überall der Rückfall erzwungen, was den Nutzen des Eigenwertlösers in einer einzigen Zahl zeigt.

sweep("eigval_max_qubits", (20, 14, 8, 4, 2, 0))
eigval_max_qubits=20 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
* eigval_max_qubits=14 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
eigval_max_qubits=8 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
eigval_max_qubits=4 mean 0.3742 ± 0.694 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.30s
eigval_max_qubits=2 mean 0.4983 ± 0.809 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.27s
eigval_max_qubits=0 mean 0.5081 ± 0.810 │ at 0: 59% computed: 21% at 2.0: 19% │ 0.27s

Selbst bei abgeschaltetem Eigenwertlöser bleibt der Mittelwert weit unter der trivialen 2.0. Der Anteil der Terme mit Schranke 0 ändert sich nicht: Ein Term, der exakt mit der zeitentwickelten Observable kommutiert, hat einen Null-Kommutator, was erkannt wird, bevor überhaupt eine Eigenwertberechnung in Betracht kommt. Bei den übrigen erreicht die Schranke der Dreiecksungleichung für einige 2.0 und wird dort gekappt, für viele ist sie aber noch kleiner, sodass sie eine berechnete Schranke unter 2.0 behalten.

atol_eigenvalue​

Wenn die Spektralnorm berechnet wird, ist atol_eigenvalue die Konvergenztoleranz des iterativen Eigenwertlösers. Die meisten Kommutatoren erreichen diesen Löser allerdings nie:

  • Ein Kommutator, der auf höchstens vier Qubits wirkt, wird direkt exakt diagonalisiert.

  • Ein breiterer wird zunächst auf die kleinste Anzahl von Qubits reduziert, die seine Pauli-Terme benötigen, und wenn diese reduzierte Form auf höchstens acht Qubits wirkt, wird er exakt diagonalisiert.

Nur ein Kommutator, der nach dieser Reduktion immer noch mehr als acht Qubits benötigt, geht an den iterativen Löser, und nur dort gilt diese Toleranz. Circuits in der Größe dieses Leitfadens kommen nie so weit, ein Sweep über diese Option würde also für jeden Wert identische Schranken zeigen. Relevant wird sie für breite Lichtkegel auf großen Geräten.

Anders als die anderen Genauigkeitsoptionen ist das Lockern von atol_eigenvalue in einer Richtung nicht sicher. Ein iterativer Eigenwertlöser nähert sich dem extremalen Eigenwert aus dem Inneren des Spektrums, sodass eine nicht vollständig konvergierte Schätzung die Spektralnorm und damit die Schranke unterschätzt. Der Standardwert ist aus diesem Grund bewusst streng. Konvergiert der Löser nicht, übernimmt der Rückfall auf die Dreiecksungleichung, der wieder gültig ist.

Umfang​

max_num_boxes​

Manchmal ist die nützliche Grenze nicht die Genauigkeit, sondern die Tiefe. max_num_boxes hört ab einer bestimmten Anzahl von Boxen auf, Schrankenberechnungen zu starten, und jeder darüber hinausgehenden Schicht wird stattdessen die triviale Schranke 2.0 zugewiesen.

Das Limit zählt die verrauschten BoxOp-Anweisungen (solche mit einer InjectNoise-Annotation) und nicht zum Beispiel eine Tiefe in Gates. So bleibt es an die für den Nutzer sichtbare Struktur des Circuits gebunden und vermeidet die Kopplung an die automatisch erzeugten Annotationsreferenzen, die sich zwischen Transpilationsläufen ändern können.

Das ist die naheliegende Wahl für einen Circuit, dessen zwei Hälften jeweils durch Vorwärts- und Rückwärtsschranken behandelt werden: Berechne jede nur so tief, wie sie gebraucht wird.

from qiskit.circuit import BoxOp
from samplomatic import InjectNoise

# The limit counts the noisy boxes encountered while iterating the circuit, not the unique ones:
# a repeated layer is one noise model but several boxes. Boxes without an `InjectNoise`
# annotation carry no error terms and do not count.
num_boxes = sum(
1
for inst in boxed_circuit.data
if isinstance(inst.operation, BoxOp)
and any(
isinstance(annotation, InjectNoise)
for annotation in inst.operation.annotations
)
)
print(
f"the circuit has {num_boxes} noisy boxes drawn from "
f"{len(noise_model_paulis)} distinct noise models\n"
)

for limit in (None, 6, 4, 2, 1):
marker = "*" if limit is None else " "
forward(f"{marker} max_num_boxes={limit}", max_num_boxes=limit)
the circuit has 9 noisy boxes drawn from 2 distinct noise models

* max_num_boxes=None mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
max_num_boxes=6 mean 0.7486 ± 0.935 │ at 0: 53% computed: 18% at 2.0: 30% │ 0.18s
max_num_boxes=4 mean 1.1506 ± 0.980 │ at 0: 40% computed: 5% at 2.0: 55% │ 0.11s
max_num_boxes=2 mean 1.5707 ± 0.821 │ at 0: 21% computed: 0% at 2.0: 79% │ 0.05s
max_num_boxes=1 mean 1.8157 ± 0.579 │ at 0: 9% computed: 0% at 2.0: 91% │ 0.03s

Ausführung​

timeout​

timeout ist ein Zeitbudget in Sekunden (Echtzeit). Läuft es ab, bleiben die bereits berechneten Schranken erhalten, und alles Ausstehende behält die triviale Schranke 2.0, sodass das Ergebnis immer nutzbar ist, nur umso lockerer, je früher du abbrichst.

Es ist keine harte Frist: Bereits laufende Aufgaben dürfen abgeschlossen werden, sodass der Aufruf das Budget um die Dauer einer Aufgabe überschreiten kann.

num_processes​

Schranken für verschiedene Fehlerterme sind unabhängig voneinander, daher verteilt num_processes sie auf Worker- Prozesse. Für sich genommen beeinflusst das nur, wie lange die Berechnung dauert, niemals ihr Ergebnis.

Hinweis: Ein timeout in Kombination mit num_processes macht das Ergebnis vom Timing abhängig: Welche Aufgaben vor Ablauf des Budgets fertig werden, ist nicht deterministisch. Die Schranken bleiben gültig, sind aber nicht mehr von Lauf zu Lauf reproduzierbar.

Wo du anfangen solltest​

Die Standardwerte sind so gewählt, dass sie bei Circuits einer Größe, bei der das bezahlbar ist, scharfe Schranken liefern. Wenn eine Berechnung zu langsam ist, wende die Optionen ungefähr in dieser Reihenfolge an:

  1. num_processes: Eine kostenlose Beschleunigung ohne Genauigkeitsverlust.

  2. max_num_boxes: Wenn du im Voraus weißt, wie tief in den Circuit du interessiert bist.

  3. eigval_max_qubits: Überspringt den teuersten Schritt für die breitesten Kommutatoren, die meist eine kleine Minderheit der Terme sind.

  4. evolution_max_terms und atol_simplify: Diese beeinflussen die Genauigkeit durchgängig, stimme sie daher zuletzt ab und prüfe mit einem Sweep wie den obigen, wie weit sich die Schranken verschieben.

  5. timeout: Ein harter Stopp statt einer Abstimmungshilfe, für den Fall, dass eine Berechnung bis zu einer Frist fertig sein muss, welche Qualität auch immer.

Je lockerer die Schranken, desto größer der Sampling-Overhead, den compute_local_scales für eine gegebene Bias-Toleranz meldet. Dieser Overhead ist die Größe, die beim Abstimmen im Blick zu behalten lohnt: Er zeigt dir, was die Näherungen tatsächlich kosten.