Débogage et traçage
Cette fonction build() transforme un circuit annoté en un circuit modèle et en une paire « samplex ». Dans un circuit complexe, il peut être difficile de déterminer quelle case du circuit d'origine correspond à quelles barrières du modèle, ou à quels nœuds du DAG « samplex ».
Samplomatic propose deux outils de traçage complémentaires :
- Étiquettes des barrières : chaque barrière du circuit modèle porte une étiquette qui indique son origine. Elles sont toujours présentes, que
debug=Truesoit défini ou non. - Informations de traçage sur les nœuds de samplex : lorsque
build()est appelé avecdebug=True, chaque nœud de samplex contient un objetTraceInfoqui le relie à la ou aux boîtes qui l'ont généré.
Ces deux outils sont particulièrement utiles lorsque les encadrés comportent une annotation Tag , qui associe une chaîne ref de caractères à l'encadré. Vous trouverez également ci-joint des références InjectNoise .
Boîtes de balises
Une annotation Tag associe une chaîne ref de caractères à une case. Le apparaît ref alors dans les étiquettes de barrière et dans les informations de trace sur les nœuds Samplex.
Le circuit d'exemple suivant comporte trois boîtes : deux boîtes CX marquées, associées à des paires de qubits disjointes, et une boîte de mesure non marquée, orientée vers la droite. La deuxième boîte CX comporte également une InjectNoise annotation : sa tag référence et son bruit ref apparaîtront tous deux dans les étiquettes de la barrière. Les deux boîtes CX couvrent différents qubits, ce qui permet à l'optimiseur Samplex de fusionner leurs nœuds de propagation en nœuds partagés. Cela apparaît dans la section « Informations de trace ».
from qiskit.circuit import QuantumCircuit
from samplomatic import InjectNoise, Tag, Twirl, build
circuit = QuantumCircuit(4, 4)
with circuit.box([Twirl(), Tag("cx_ab")]):
circuit.cx(0, 1)
with circuit.box([Twirl(), InjectNoise("cx_noise"), Tag("cx_cd")]):
circuit.cx(2, 3)
with circuit.box([Twirl(), Tag("meas_box")]):
circuit.measure(range(4), range(4))
circuit.draw("mpl")
Étiquettes de barrière dans le circuit modèle
L'appel build() du circuit ci-dessus génère un modèle dont les barrières portent des étiquettes d'identification. Chaque étiquette se présente sous la forme suivante {side}{scope}@{key=value&...}:
- Côté :
L= limite de justification à gauche,M= limite du contenu de la boîte intérieure,R= limite de justification à droite. - Portée : un index entier (ou une liste séparée par des traits de soulignement pour les encadrés imbriqués) qui permet de distinguer plusieurs encadrés situés au même niveau d'imbrication.
- Annotations : liste de paires
key=value@préfixées par « an » et&séparées par un tiret, issues des annotations de la boîte. La clétagprovient d'uneTagannotation etinject_noiseprovient d'une annotationInjectNoise.
Les barrières issues de cases non étiquetées ne comportent que le côté et la portée (par exemple, L2), sans suffixe @ .
template, samplex = build(circuit)
template.draw("mpl", fold=100)
Les étiquettes de barrière peuvent également être extraites par programmation :
barrier_labels = [instr.operation.label for instr in template if instr.operation.name == "barrier"]
barrier_labels['L0@tag=cx_ab',
'M0@tag=cx_ab',
'R0@tag=cx_ab',
'L1@inject_noise=cx_noise&tag=cx_cd',
'M1@inject_noise=cx_noise&tag=cx_cd',
'R1@inject_noise=cx_noise&tag=cx_cd',
'L2@tag=meas_box',
'M2@tag=meas_box',
'R2@tag=meas_box']
Informations de trace sur les nœuds Samplex
La fonction « passing » debug=True ajoute build() des informations de trace à chaque nœud « samplex ». L'attribut trace_info de chaque nœud est un TraceInfo objet dont le dictionnaire trace_refs associe des clés d'annotation (par exemple, "tag" et "inject_noise") à des ensembles de chaînes de référence. Les nœuds ne comportant pas d'annotation sous forme de cadre ont trace_info=None.
Lorsque draw() est appelé sur un samplex compilé en mode débogage, le fait de passer la souris sur n'importe quel nœud du graphe interactif permet d'afficher son trace_refs dans l'infobulle qui s'affiche.
template, samplex = build(circuit, debug=True)
samplex.draw()Les informations de trace peuvent également être consultées par programmation. Notez que certains nœuds contiennent des références provenant à la fois de 'cx_ab' et de 'cx_cd'. L'optimiseur Samplex a fusionné ses nœuds de propagation parallèles, car les deux boîtes couvrent des qubits disjoints et partagent un prédécesseur commun issu de l'émission de la boîte de mesure « right-dressed ».
for node in samplex.graph.nodes():
if node.trace_info is not None:
tags = node.trace_info.trace_refs.get("tag", set())
merged = " ← merged" if len(tags) > 1 else ""
print(f"{type(node).__name__:40s} tags={tags}{merged}")TwirlSamplingNode tags={'meas_box'}
InjectNoiseNode tags={'cx_cd'}
CollectZ2ToOutputNode tags={'cx_cd'}
TwirlSamplingNode tags={'cx_cd'}
CombineRegistersNode tags={'cx_cd'}
CollectTemplateValues tags={'cx_cd'}
TwirlSamplingNode tags={'cx_ab'}
CombineRegistersNode tags={'cx_ab', 'cx_cd'} ← merged
CollectTemplateValues tags={'cx_ab', 'cx_cd'} ← merged
Pour trouver tous les nœuds provenant d'une boîte spécifique, appliquez un filtre sur la clé "tag" :
tag_ref = "cx_ab"
matching_nodes = [
node
for node in samplex.graph.nodes()
if node.trace_info is not None and tag_ref in node.trace_info.trace_refs.get("tag", set())
]
print(f"Nodes originating from box '{tag_ref}':")
for node in matching_nodes:
print(f" {type(node).__name__}")Nodes originating from box 'cx_ab':
TwirlSamplingNode
CombineRegistersNode
CollectTemplateValues
Le DAG « samplex »
Le samplex renvoyé par build() est un graphe acyclique orienté (DAG) dans lequel les arêtes représentent des dépendances entre registres. En d'autres termes, une arête reliant le nœud A au nœud B signifie que B doit attendre qu'A ait effectué ses opérations sur les registres virtuels partagés avant que B ne soit autorisé à agir. Il ne s'agit pas d'un ordre temporel comme le DAG d'un circuit quantique. Au contraire, le graphe relie les nœuds chargés de générer des randomisations aux nœuds chargés de les synthétiser sous forme de sorties.
Il existe trois types de nœuds, chacun présentant un style visuel distinct dans le graphique interactif :
Forme | Couleur | Type | Rôle |
|---|---|---|---|
| Étoile | Rouge | SamplingNode | Crée de nouveaux registres virtuels à partir d'une distribution ou d'une entrée |
| Cercle | Vert | EvaluationNode | Transforme, combine ou propage des registres virtuels |
| Nœud papillon | Bleu / Violet | CollectionNode | Lit les registres et écrit dans les sorties sample() |
La mise en œuvre se déroule en trois phases :
- Tous les nœuds d'échantillonnage s'exécutent en premier (en parallèle).
- Les nœuds d'évaluation s'exécutent ensuite, dans l'ordre topologique (en parallèle au sein de chaque génération).
- Les nœuds de collecte s'exécutent en dernier (en parallèle).
La mise en page de Graphviz en tient compte. Les nœuds d'échantillonnage apparaissent en haut, les nœuds de collecte en bas.
Infobulles au survol
Chaque nœud de la visualisation interactive affiche une info-bulle lorsque vous passez la souris dessus. L'info-bulle contient les informations suivantes :
- Nom de la classe du nœud et son index entier dans le graphe.
- Enregistrer les manifestes, qui répertorient les nœuds que l'instance crée, lit, modifie et supprime. Celles-ci vous indiquent comment les données circulent entre les nœuds.
- Détails spécifiques aux nœuds, notamment le type de distribution pour les nœuds d'échantillonnage, l'opérande pour les nœuds de multiplication, les indices des paramètres de modèle pour les nœuds de collection, etc.
- Références de traçage (uniquement lorsque
debug=True), qui correspondent aux clés d’annotation et aux chaînes de référence reliant le nœud à ses encadrés d’origine.
Cliquer sur le graphique puis passer la souris sur chaque nœud est le moyen le plus rapide de comprendre le rôle d'un nœud donné sans avoir à lire le code source.
Résumé de Samplex
Avant d'examiner la visualisation, print(samplex) propose un bref résumé écrit du nombre de nœuds, des données d'entrée requises et des résultats attendus.
template, samplex = build(circuit)
print(samplex)Samplex(<17 nodes>)
Inputs:
- 'pauli_lindblad_maps.cx_noise' <PauliLindbladMap>: A PauliLindblad map acting on 2
qubits, with 'num_terms_cx_noise' terms.
Outputs:
* 'measurement_flips.c' <bool['num_randomizations', 1, 4]>: Bit-flip corrections for
measurement twirling.
* 'parameter_values' <float32['num_randomizations', 24]>: Parameter values valid for an
associated template circuit.
* 'pauli_signs' <bool['num_randomizations', 1]>: Signs from sampled Pauli Lindblad
maps, where boolean values represent the parity of the number of non-trivial factors in the
sampled error that arise from negative rates. In other words, in order to implement basic
PEC, the sign used to correct expectation values should be ``(-1)**bool_value``. The order
matches the iteration order of boxes in the original circuit with noise injection
annotations.
Vérifier les registres
La fonction passing keep_registers=True conserve sample() les objets VirtualRegister intermédiaires qui sont encore actifs à la fin de l'échantillonnage et les stocke dans outputs.metadata["registers"]. Chaque registre est un tableau de type « 2D » (de forme (num_subsystems, num_randomizations), pouvant comporter des dimensions supplémentaires correspondant à la forme de la porte) d'éléments de groupe virtuel.
Cela permet de vérifier que les portes virtuelles ont été combinées correctement, ou d'examiner les échantillons bruts de Pauli ou unitaires avant qu'ils ne soient synthétisés en angles de rotation.
from qiskit.quantum_info import PauliLindbladMap
outputs = samplex.sample(
{"pauli_lindblad_maps.cx_noise": PauliLindbladMap.identity(2)},
num_randomizations=3,
keep_registers=True,
)
for name, reg in outputs.metadata["registers"].items():
print(f"{name}: type={reg.TYPE.value}, shape={reg.virtual_gates.shape}")lhs_0: type=pauli, shape=(4, 3)
rhs_0: type=pauli, shape=(4, 3)
lhs_3: type=pauli, shape=(2, 3)
rhs_3: type=pauli, shape=(2, 3)
inject_noise_1: type=pauli, shape=(2, 3)
sign_1: type=z2, shape=(1, 3)
lhs_7: type=pauli, shape=(2, 3)
rhs_7: type=pauli, shape=(2, 3)
meas_prop_6: type=pauli, shape=(4, 3)
collect_10: type=u2, shape=(2, 3, 2, 2)
meas_prop_z2a_6: type=z2, shape=(4, 3)
collect_9: type=u2, shape=(4, 3, 2, 2)
collect_5: type=u2, shape=(2, 3, 2, 2)
Vous pouvez consulter le contenu de l'état final d'un registre donné. Dans l'exemple suivant, le registre est de type PauliRegister. Consultez la section API documentation(/apidocs/index)__ pour plus de détails sur le format de stockage.
print("lhs_0:", outputs.metadata["registers"]["lhs_0"])
outputs.metadata["registers"]["lhs_0"].virtual_gateslhs_0: PauliRegister(<4, 3>)
array([[0, 0, 1],
[3, 1, 1],
[3, 0, 0],
[1, 0, 0]], dtype=uint8)
Comment les nœuds Samplex sont mis en correspondance avec les paramètres de modèle
Le tableau outputs["parameter_values"] renvoyé par sample() a une dimension (num_randomizations, N), où correspond N à len(template.parameters). Chaque colonne correspond à un paramètre du circuit type — la i-ième colonne contient la valeur template.parameters[i].
Les nœuds de collection CollectTemplateValues constituent le lien entre les registres virtuels et les paramètres de modèle. Chacun de ces nœuds contient des informations d'index qui indiquent dans quelles colonnes du tableau de sortie il effectue l'écriture. Cela permet de déterminer quels sous-systèmes de portes virtuelles commandent quels paramètres de modèle.
Consultez le guide « {Samplex inputs and outputs}samplex-io » pour découvrir comment associer les valeurs des paramètres échantillonnés au circuit type et mener des expériences.
from samplomatic.samplex.nodes import CollectTemplateValues
template, samplex = build(circuit, debug=True)
for node in samplex.graph.nodes():
if isinstance(node, CollectTemplateValues):
tags = node.trace_info.trace_refs.get("tag", set()) if node.trace_info else set()
print(f"tags={tags} → template param indices: {node.template_idxs.tolist()}")tags={'cx_cd'} → template param indices: [[6, 7, 8], [9, 10, 11]]
tags={'cx_ab', 'cx_cd'} → template param indices: [[12, 13, 14], [15, 16, 17], [18, 19, 20], [21, 22, 23]]
tags=set() → template param indices: [[0, 1, 2], [3, 4, 5]]
Marquage automatique
Lorsque vous utilisez generate_boxing_pass_manager(), le paramètre add_tags ajoute automatiquement des annotations Tag à toutes les zones. Trois modes sont disponibles :
"unique_instance": Attribue des numéros séquentielsref(t0,t1, etc.) aux cases dans l'ordre du circuit. Chaque boîte se voit attribuer un identifiant unique,refquelle que soit sa structure."unique_box": Calcule un hachage structurel du contenu de chaque boîte et attribue le même hachage àreftoutes les boîtes structurellement équivalentes. Cela permet de regrouper les encadrés par type plutôt que par position."noise_ref": Copie le contenu derefl'annotationInjectNoisede chaque case et ne marque que les cases qui en possèdent une. Cela s'avère utile lorsqu'il existe déjà des références de bruit pertinentes.
L'exemple suivant applique "unique_instance" et "unique_box" à un circuit dont les deux boîtiers CX sont structurellement équivalents. Avec "unique_instance", chacun reçoit un distinct ref, tandis qu'avec "unique_box", ils en partagent un.
from samplomatic.transpiler import generate_boxing_pass_manager
base_circuit = QuantumCircuit(3)
base_circuit.cx(0, 1)
base_circuit.cx(1, 2)
base_circuit.measure_all()
# unique_instance: every box gets a distinct tag ref
pm = generate_boxing_pass_manager(add_tags="unique_instance")
boxed = pm.run(base_circuit)
template, _ = build(boxed)
print("unique_instance barrier labels:")
for instr in template:
if instr.operation.name == "barrier" and instr.operation.label:
print(f" {instr.operation.label}")unique_instance barrier labels:
L0@tag=t0
M0@tag=t0
R0@tag=t0
L1@tag=t1
M1@tag=t1
R1@tag=t1
L2@tag=t2
M2@tag=t2
R2@tag=t2