Aller au contenu principal
Ce projet est en phase bêta. Tu as trouvé un bug ? Ouvrir un ticket GitHub. Une question ou une idée ? Pose ta question dans Discussions — tes retours sont les bienvenus !
Quelle est la qualité de la traduction sur cette page ?

Comment ajuster les options de configuration

Le calcul des bornes est exact en principe mais approximatif en pratique : l'évolution d'un terme d'erreur de Pauli à travers un circuit produit un opérateur comportant un nombre exponentiel de termes, et trouver la norme spectrale du commutateur obtenu est lui-même coûteux. Les options ci-dessous contrôlent ce coût. Elles se répartissent en trois groupes :

  • Les options d'exactitude modifient la valeur numérique des bornes, en échangeant précision contre coût.

  • La portée détermine quelles bornes sont calculées.

  • Les options d'exécution modifient la manière dont le calcul s'exécute (et non ce qu'il calcule).

OptionValeur par défautContrôleEffet sur le résultat
Exactitude
evolution_max_terms1_000_000Combien de termes d'opérateur survivent à l'évolutionPlus bas : moins coûteux, bornes plus lâches
atol_simplify1e-8Quels termes du commutateur sont écartés comme négligeablesPlus haut : moins coûteux, bornes plus lâches
eigval_max_qubits14Quelle largeur peut avoir un commutateur pour que sa norme spectrale soit calculéePlus bas : moins coûteux, bornes plus lâches
atol_eigenvalue1e-10Quand le solveur propre itératif est considéré comme convergéPlus haut : moins coûteux, bornes moins exactes
Portée
max_num_boxesNonePour combien de boîtes les bornes sont calculéesLes boîtes au-delà de la limite gardent la borne triviale de 2.0
Exécution
timeoutNoneUn budget de temps réel pour l'ensemble du calculTout travail restant garde la borne triviale de 2.0
num_processes1Combien de processus de travail calculent les bornes en parallèleAucun en soi

Une borne n'est utile que si elle est valide : elle ne doit jamais sous-estimer l'effet d'un terme d'erreur sur l'observable. Relâcher l'une de ces options, hormis atol_eigenvalue, ne peut qu'agrandir les bornes (les rendre plus conservatrices), jamais les réduire. Ainsi, le coût d'un réglage lâche est un surcoût d'échantillonnage plus important, et non une valeur moyenne erronée - atol_eigenvalue fait exception.

Définition du circuit​

Les expériences suivantes utilisent le même circuit d'Ising trotterisé et la même observable que le guide de démarrage rapide. À cette taille, chaque option a un effet visible tandis que chaque calcul prend encore moins d'une seconde.

# 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

Une petite fonction utilitaire résume chaque ensemble de bornes en une ligne, de sorte que l'effet d'une option apparaisse sous la forme de quelques nombres plutôt que d'un mur de tableaux.

Chaque borne se situe entre 0 et la borne triviale de 2.0. Le résumé indique la borne moyenne et son écart type, puis la répartition des termes dans ces groupes :

  • Termes bornés par exactement 0 parce qu'ils commutent avec l'observable évoluée
  • Termes dont la borne calculée est comprise entre 0 et 2.0
  • Termes laissés à la borne triviale de 2.0

Note que une moyenne plus basse est plus serrée, et plus serrée signifie un surcoût d'échantillonnage plus faible par la suite.

Un balayage sur une option marque la valeur par défaut de l'option avec * :

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

Exactitude​

evolution_max_terms​

Faire évoluer un terme d'erreur de Pauli à travers des Gates non Clifford le transforme en une somme de Paulis, et cette somme croît avec la profondeur du circuit. evolution_max_terms plafonne le nombre de ces termes conservés. La 1-norme de ce qui est écarté est ajoutée à la borne, de sorte que rien n'est perdu silencieusement.

Le balayage va d'une valeur plus serrée que la valeur par défaut jusqu'à une coupe extrême. Abaisser le plafond rend le calcul moins coûteux et les bornes plus lâches.

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

Relever le plafond au-delà de sa valeur par défaut ne change rien ici. Pour un circuit de cette taille, la valeur par défaut conserve déjà tous les termes, donc les bornes sont aussi serrées que cette option peut les rendre. C'est la situation typique. Resserrer une option d'exactitude ne vaut la peine que lorsque sa valeur par défaut est réellement limitante, et le moyen de le savoir est l'expérience ci-dessus. Relance-la avec une valeur plus serrée et regarde si les bornes bougent.

Avertissement : La routine de propagation sous-jacente traite evolution_max_terms comme une capacité à pré-allouer, et non comme une limite supérieure à vérifier. Passer une valeur nominalement illimitée, comme numpy.iinfo(numpy.uint).max, demande à l'allocateur des téraoctets de mémoire et interrompt le processus. Garde un nombre réaliste. La valeur par défaut d'un million est déjà généreuse.

atol_simplify​

Une fois le commutateur formé, les termes dont le coefficient est inférieur à atol_simplify sont supprimés. Comme pour le plafond d'évolution, la 1-norme perdue de cette façon est ajoutée à la borne.

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​

La borne la plus serrée disponible est la norme spectrale du commutateur, qui nécessite un calcul de valeurs propres. Celui-ci devient coûteux à mesure que le commutateur s'élargit, donc eigval_max_qubits fixe la largeur au-delà de laquelle la tentative est abandonnée au profit de l'inégalité triangulaire. Cette borne, 2∑i∣ci∣2 \sum_i |c_i| sur les coefficients cic_i du commutateur, est valide mais plus lâche.

La fixer à zéro force le repli partout, ce qui montre le bénéfice du solveur propre en un seul nombre.

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

Même avec le solveur propre désactivé, la moyenne reste bien en dessous de la borne triviale de 2.0. La proportion de termes bornés par 0 ne change pas : un terme qui commute exactement avec l'observable évoluée a un commutateur nul, ce qui est reconnu avant même d'envisager un calcul de valeurs propres. Parmi les autres, la borne par inégalité triangulaire atteint 2.0 pour certains et y est plafonnée, mais pour beaucoup elle est encore plus petite, si bien qu'ils gardent une borne calculée inférieure à 2.0.

atol_eigenvalue​

Lorsque la norme spectrale est calculée, atol_eigenvalue est la tolérance de convergence du solveur propre itératif. La plupart des commutateurs n'atteignent cependant jamais ce solveur :

  • Un commutateur agissant sur au plus quatre qubits est diagonalisé exactement, directement.

  • Un commutateur plus large est d'abord réduit au plus petit nombre de qubits dont ses termes de Pauli ont besoin, et si cette forme réduite agit sur au plus huit qubits, il est diagonalisé exactement.

Seul un commutateur qui nécessite encore plus de huit qubits après cette réduction est confié au solveur itératif, ce qui est le seul endroit où cette tolérance s'applique. Les circuits de la taille utilisée dans ce guide n'en arrivent jamais là, donc un balayage sur cette option montrerait des bornes identiques pour chaque valeur. Elle compte pour des cônes de lumière larges sur de grands appareils.

Contrairement aux autres options d'exactitude, relâcher atol_eigenvalue n'est pas sûr dans un sens. Un solveur propre itératif approche la valeur propre extrême de l'intérieur du spectre, donc une estimation qui n'a pas totalement convergé sous-estime la norme spectrale, et donc la borne. La valeur par défaut est délibérément serrée pour cette raison. Si le solveur ne converge pas, le repli par inégalité triangulaire prend le relais, ce qui est de nouveau valide.

Portée​

max_num_boxes​

Parfois, la limite utile n'est pas l'exactitude mais la profondeur. max_num_boxes cesse de lancer des calculs de bornes au-delà d'un nombre donné de boîtes, et chaque couche au-delà reçoit à la place la borne triviale de 2.0.

La limite compte les instructions BoxOp bruitées (celles qui portent une annotation InjectNoise) plutôt que, par exemple, une profondeur en Gates. Cela la lie à la structure visible par l'utilisateur du circuit et évite de dépendre des références d'annotation générées automatiquement, qui peuvent changer d'une exécution de transpilation à l'autre.

C'est le choix naturel pour un circuit dont les deux moitiés sont traitées respectivement par des bornes avant et arrière : ne calcule chacune que jusqu'à la profondeur où elle sera utilisée.

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

Exécution​

timeout​

timeout est un budget de temps réel en secondes. Lorsqu'il expire, les bornes déjà calculées sont conservées et tout ce qui est en attente garde la borne triviale de 2.0, de sorte que le résultat est toujours utilisable, simplement plus lâche plus tu l'interromps tôt.

Ce n'est pas une échéance stricte : les tâches déjà en cours peuvent se terminer, donc l'appel peut dépasser le budget de la durée d'une tâche.

num_processes​

Les bornes de différents termes d'erreur sont indépendantes, donc num_processes les répartit sur des processus de travail. À lui seul, il n'affecte que la durée du calcul, jamais son résultat.

Note : Combiner timeout et num_processes rend le résultat dépendant du temps : quelles tâches se terminent avant l'expiration du budget n'est pas déterministe. Les bornes restent valides, mais elles ne sont plus reproductibles d'une exécution à l'autre.

Par où commencer​

Les valeurs par défaut sont choisies pour donner des bornes serrées sur des circuits d'une taille où cela reste abordable. Lorsqu'un calcul est trop lent, essaie d'appliquer les options à peu près dans cet ordre :

  1. num_processes : une accélération gratuite, sans coût en exactitude.

  2. max_num_boxes : si tu sais à l'avance jusqu'à quelle profondeur du circuit tu t'intéresses.

  3. eigval_max_qubits : saute l'étape la plus coûteuse pour les commutateurs les plus larges, qui sont généralement une petite minorité des termes.

  4. evolution_max_terms et atol_simplify : ils influent sur l'exactitude partout, donc ajuste-les en dernier, en vérifiant avec un balayage comme ceux ci-dessus de combien les bornes bougent.

  5. timeout : un arrêt brutal plutôt qu'une aide au réglage, pour le cas où un calcul doit se terminer avant une échéance, quelle que soit la qualité.

Plus les bornes sont lâches, plus le surcoût d'échantillonnage que compute_local_scales rapportera est grand pour une tolérance de biais donnée. Ce surcoût est la quantité à surveiller pendant le réglage : il t'indique ce que coûtent réellement les approximations.