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 ?

Débogage et traçage

La fonction build() transforme un circuit annoté en une paire circuit modèle et samplex. Dans un circuit complexe, il peut être difficile de savoir quelle boîte du circuit d'origine correspond à quelles barrières du modèle, ou à quels nœuds du DAG du samplex.

Samplomatic fournit deux outils de traçage complémentaires :

  • Étiquettes de barrière : chaque barrière du circuit modèle porte une étiquette qui encode son origine. Elles sont toujours présentes, que debug=True soit défini ou non.

  • Informations de trace sur les nœuds du samplex : quand build() est appelé avec debug=True, chaque nœud du samplex porte un objet TraceInfo qui le relie à la ou aux boîtes qui l'ont produit.

Les deux outils sont particulièrement utiles quand les boîtes portent une annotation Tag, qui attache une chaîne ref à la boîte. Les références InjectNoise sont également attachées.

Étiqueter les boîtes​

Une annotation Tag attache une chaîne ref à une boîte. Le ref apparaît ensuite dans les étiquettes de barrière et dans les informations de trace des nœuds du samplex.

Le circuit d'exemple suivant comporte trois boîtes : deux boîtes CX étiquetées sur des paires de qubits disjointes, et une boîte de mesure à habillage droit sans étiquette. La seconde boîte CX porte aussi une annotation InjectNoise — son tag ref comme son ref de bruit apparaîtront dans les étiquettes de barrière. Les deux boîtes CX couvrent des qubits différents, ce qui permet à l'optimiseur de samplex de fusionner leurs nœuds de propagation en nœuds partagés. Cela est montré dans la section sur les 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​

Appeler build() sur le circuit ci-dessus produit un modèle dont les barrières portent des étiquettes d'identification. Chaque étiquette a la forme {side}{scope}@{key=value&...} :

  • Côté : L = frontière d'habillage gauche, M = frontière du contenu interne de la boîte, R = frontière d'habillage droit.

  • Portée : un indice entier (ou une liste séparée par des tirets bas pour les boîtes imbriquées) qui distingue plusieurs boîtes au même niveau d'imbrication.

  • Annotations : une liste préfixée par @ et séparée par & de paires key=value dérivées des annotations de la boîte. La clé tag provient d'une annotation Tag et inject_noise provient d'une annotation InjectNoise.

Les barrières issues de boîtes sans étiquette ne portent 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 aussi ê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 du samplex​

Passer debug=True à build() attache des informations de trace à chaque nœud du samplex. L'attribut trace_info de chaque nœud est un objet TraceInfo dont le dictionnaire trace_refs associe des clés d'annotation (par exemple, "tag" et "inject_noise") à des ensembles de chaînes ref. Les nœuds sans annotation de boîte correspondante ont trace_info=None.

Quand draw() est appelé sur un samplex construit en mode debug, survoler n'importe quel nœud du graphe interactif révèle ses trace_refs dans l'infobulle.

template, samplex = build(circuit, debug=True)
samplex.draw()

Les informations de trace peuvent aussi être inspectées par programmation. Remarque que certains nœuds portent des références à la fois de 'cx_ab' et de 'cx_cd'. L'optimiseur de samplex a fusionné leurs nœuds de propagation parallèles parce que 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 à habillage droit.

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 issus d'une boîte précise, filtre par 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 du samplex​

Le samplex renvoyé par build() est un graphe orienté acyclique (DAG) où les arêtes dénotent une dépendance de registre. Autrement dit, une arête du nœud A vers le nœud B signifie que B doit attendre que A ait agi sur les registres virtuels partagés avant de pouvoir agir. Ce n'est pas un ordre temporel comme le DAG d'un circuit quantique. Le graphe va plutôt des nœuds chargés de générer les randomisations vers les nœuds chargés de les synthétiser en sorties.

Il existe trois types de nœuds, chacun avec un style visuel distinct dans le tracé interactif :

FormeCouleurTypeRôle
ÉtoileRougeSamplingNodeInstancie de nouveaux registres virtuels à partir d'une distribution ou d'une entrée
CercleVertEvaluationNodeTransforme, combine ou propage des registres virtuels
Nœud papillonBleu / VioletCollectionNodeLit des registres et écrit dans les sorties de sample()

L'exécution 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 disposition graphviz reflète cela. 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 infobulle quand tu le survoles. L'infobulle contient les informations suivantes :

  • Nom de la classe du nœud et son indice entier dans le graphe.

  • Manifestes de registres, c'est-à-dire les registres que le nœud instancie, lit, écrit et supprime. Ils t'indiquent comment les données circulent entre les nœuds.

  • Détails propres au nœud, y compris le type de distribution pour les nœuds d'échantillonnage, l'opérande pour les nœuds de multiplication, les indices de paramètres du modèle pour les nœuds de collecte, etc.

  • Trace refs (uniquement avec debug=True), qui sont les clés d'annotation et les chaînes ref reliant le nœud aux boîtes dont il provient.

Cliquer sur le tracé puis survoler les nœuds un par un est le moyen le plus rapide de comprendre ce que fait un nœud donné sans lire le code source.

Résumé du samplex​

Avant d'examiner la visualisation, print(samplex) donne un résumé textuel rapide du nombre de nœuds, des entrées requises et des sorties promises.

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.

Inspecter les registres​

Passer keep_registers=True à sample() conserve les objets VirtualRegister intermédiaires qui sont actifs à la fin de l'échantillonnage, et les stocke dans outputs.metadata["registers"]. Chaque registre est un tableau 2D (de forme (num_subsystems, num_randomizations), avec d'éventuelles dimensions finales de forme de porte) d'éléments de groupe virtuels.

C'est utile pour vérifier que les portes virtuelles ont été combinées correctement, ou pour inspecter les échantillons bruts de Pauli ou unitaires avant qu'ils 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)

Tu peux afficher le contenu de l'état final d'un registre particulier. Dans l'exemple suivant, le registre est de type PauliRegister. Consulte la API documentation(/apidocs/index)__ pour des 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 du samplex correspondent aux paramètres du modèle​

Le tableau outputs["parameter_values"] renvoyé par sample() a la forme (num_randomizations, N), où N correspond à len(template.parameters). Chaque colonne correspond à un paramètre du circuit modèle — la i-ème colonne renseigne template.parameters[i].

Les nœuds de collecte CollectTemplateValues font le lien entre les registres virtuels et les paramètres du modèle. Chacun de ces nœuds contient des informations d'indice qui enregistrent dans quelles colonnes du tableau de sortie il écrit. Cela permet de retrouver quels sous-systèmes de portes virtuelles pilotent quels paramètres du modèle.

Consulte le guide {Samplex inputs and outputs}samplex-io pour apprendre à lier les valeurs de paramètres échantillonnées au circuit modèle et à exécuter 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]]

Étiquetage automatique​

Quand tu utilises generate_boxing_pass_manager(), le paramètre add_tags ajoute automatiquement des annotations Tag à toutes les boîtes. Trois modes sont disponibles :

  • "unique_instance" : attribue des ref séquentiels (t0, t1, etc.) aux boxes dans l'ordre du circuit. Chaque box reçoit un ref distinct, quelle que soit sa structure.

  • "unique_box" : calcule un hachage structurel du contenu de chaque box et attribue le même ref à toutes les boxes structurellement équivalentes. C'est utile pour regrouper les boxes par type plutôt que par position.

  • "noise_ref" : copie le ref de l'annotation InjectNoise de chaque box et ne marque que les boxes qui en ont une. C'est utile lorsque des noise refs pertinents existent déjà.

L'exemple suivant applique "unique_instance" et "unique_box" à un circuit dont les deux boxes CX sont structurellement équivalentes. Avec "unique_instance", chacune reçoit un ref distinct, alors qu'avec "unique_box", elles en partagent un seul.

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