Skip to main content
IBM Quantum Platform

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=True soit défini ou non.
  • Informations de traçage sur les nœuds de samplex : lorsque build() est appelé avec debug=True, chaque nœud de samplex contient un objet TraceInfo qui 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")
../_images/f6b6fe7b60a39ba897814008443fa79b66213005462ab09f49ffc89aab1f3668.png

É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é tag provient d'une Tag annotation et inject_noise provient d'une annotation InjectNoise .

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)
../_images/e533d70a29504005a636aa27306d3ce3b1d2488358ac6a53f0674f2934698ab8.png

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
ÉtoileRougeSamplingNodeCrée de nouveaux registres virtuels à partir d'une distribution ou d'une entrée
CercleVertEvaluationNodeTransforme, combine ou propage des registres virtuels
Nœud papillonBleu / VioletCollectionNodeLit les registres et écrit dans les sorties sample()

La mise en œuvre se déroule en trois phases :

  1. Tous les nœuds d'échantillonnage s'exécutent en premier (en parallèle).
  2. Les nœuds d'évaluation s'exécutent ensuite, dans l'ordre topologique (en parallèle au sein de chaque génération).
  3. 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_gates
lhs_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équentiels ref (t0, t1, etc.) aux cases dans l'ordre du circuit. Chaque boîte se voit attribuer un identifiant unique, ref quelle que soit sa structure.
  • "unique_box": Calcule un hachage structurel du contenu de chaque boîte et attribue le même hachage à ref toutes les boîtes structurellement équivalentes. Cela permet de regrouper les encadrés par type plutôt que par position.
  • "noise_ref": Copie le contenu de ref l'annotation InjectNoise de 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
Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.