Debug e tracciamento
La funzione build() trasforma un circuito annotato in una coppia formata da un circuito template e un samplex. In un circuito complesso può essere difficile capire quale box del circuito originale corrisponda a quali barriere nel template, o a quali nodi nel DAG del samplex.
Samplomatic offre due strumenti di tracciamento complementari:
-
Etichette delle barriere: ogni barriera nel circuito template porta un'etichetta che ne codifica l'origine. Sono sempre presenti, indipendentemente dal fatto che sia impostato
debug=True. -
Informazioni di traccia sui nodi del samplex: quando
build()viene chiamata condebug=True, ogni nodo del samplex porta un oggettoTraceInfoche lo collega al box o ai box che lo hanno prodotto.
Entrambi gli strumenti sono più utili quando i box portano un'annotazione Tag, che associa una stringa ref al box. In alternativa, vengono associati anche i riferimenti InjectNoise.
Etichettare i box con i tag
Un'annotazione Tag associa una stringa ref a un box. Il ref compare poi nelle etichette delle barriere e nelle informazioni di traccia sui nodi del samplex.
Il seguente circuito di esempio ha tre box: due box CX con tag su coppie di qubit disgiunte e un box di misura right-dressed senza tag. Il secondo box CX porta anche un'annotazione InjectNoise — sia il suo tag ref sia il suo ref del rumore compariranno nelle etichette delle barriere. I due box CX coprono qubit diversi, il che consente all'ottimizzatore del samplex di unire i loro nodi di propagazione in nodi condivisi. Questo è mostrato nella sezione sulle informazioni di traccia.
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")
Etichette delle barriere nel circuito template
Chiamare build() sul circuito qui sopra produce un template le cui barriere portano etichette identificative. Ogni etichetta ha la forma {side}{scope}@{key=value&...}:
-
Lato:
L= confine di dressing sinistro,M= confine del contenuto interno del box,R= confine di dressing destro. -
Ambito: un indice intero (o un elenco separato da underscore per i box annidati) che distingue più box allo stesso livello di annidamento.
-
Annotazioni: un elenco di coppie
key=valueprefissato da@e separato da&, ricavato dalle annotazioni del box. La chiavetagproviene da un'annotazioneTageinject_noiseproviene da un'annotazioneInjectNoise.
Le barriere dei box senza tag portano solo il lato e l'ambito (ad esempio L2), senza suffisso @.
template, samplex = build(circuit)
template.draw("mpl", fold=100)
Le etichette delle barriere possono anche essere estratte in modo programmatico:
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']
Informazioni di traccia sui nodi del samplex
Passare debug=True a build() associa le informazioni di traccia a ogni nodo del samplex. L'attributo trace_info di ciascun nodo è un oggetto TraceInfo il cui dizionario trace_refs associa le chiavi di annotazione (ad esempio "tag" e "inject_noise") a insiemi di stringhe ref. I nodi senza una corrispondente annotazione del box hanno trace_info=None.
Quando draw() viene chiamata su un samplex costruito in modalità debug, passando il mouse su qualsiasi nodo nel grafo interattivo si rivelano i suoi trace_refs nel tooltip.
template, samplex = build(circuit, debug=True)
samplex.draw()
Le informazioni di traccia possono anche essere ispezionate in modo programmatico. Nota che alcuni nodi portano riferimenti sia da 'cx_ab' sia da 'cx_cd'. L'ottimizzatore del samplex ha unito i loro nodi di propagazione paralleli perché i due box coprono qubit disgiunti e condividono un predecessore comune dall'emissione del box di misura 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
Per trovare tutti i nodi che hanno origine da un box specifico, filtra per la chiave "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
Il DAG del samplex
Il samplex restituito da build() è un grafo aciclico orientato (DAG) in cui gli archi denotano la dipendenza tra registri. Cioè, un arco dal nodo A al nodo B significa che B deve attendere che A abbia agito sui registri virtuali condivisi prima di poter agire. Questo non è un ordinamento temporale come il DAG di un circuito quantistico. Invece, il grafo scorre dai nodi responsabili della generazione delle randomizzazioni ai nodi responsabili della loro sintesi come output.
Ci sono tre tipi di nodo, ciascuno con uno stile visivo distinto nel grafico interattivo:
| Forma | Colore | Tipo | Ruolo |
|---|---|---|---|
| Stella | Rosso | SamplingNode | Istanzia nuovi registri virtuali da una distribuzione o da un input |
| Cerchio | Verde | EvaluationNode | Trasforma, combina o propaga registri virtuali |
| Farfalla | Blu / Viola | CollectionNode | Legge i registri e scrive negli output di sample() |
L'esecuzione procede in tre fasi:
-
Prima vengono eseguiti tutti i nodi di campionamento (in parallelo).
-
Poi vengono eseguiti i nodi di valutazione, in ordine topologico (in parallelo all'interno di ogni generazione).
-
Per ultimi vengono eseguiti i nodi di raccolta (in parallelo).
Il layout di graphviz lo riflette. I nodi di campionamento compaiono in alto, i nodi di raccolta in basso.
Tooltip al passaggio del mouse
Ogni nodo nella visualizzazione interattiva mostra un tooltip quando ci passi sopra con il mouse. Il tooltip contiene le seguenti informazioni:
-
Nome della classe del nodo e il suo indice intero nel grafo.
-
Manifesti dei registri, cioè quali registri il nodo istanzia, legge, scrive e rimuove. Indicano come fluiscono i dati tra i nodi.
-
Dettagli specifici del nodo, tra cui il tipo di distribuzione per i nodi di campionamento, l'operando per i nodi di moltiplicazione, gli indici dei parametri del template per i nodi di raccolta e così via.
-
Trace refs (solo con
debug=True), cioè le chiavi di annotazione e le stringhe ref che collegano il nodo ai box da cui ha origine.
Fare clic sul grafico e poi passare il mouse sui singoli nodi è il modo più rapido per capire cosa fa un dato nodo senza leggere il codice sorgente.
Riepilogo del samplex
Prima di esaminare la visualizzazione, print(samplex) fornisce un rapido riepilogo testuale del numero di nodi, degli input richiesti e degli output promessi.
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.
Ispezionare i registri
Passare keep_registers=True a sample() conserva gli oggetti intermedi VirtualRegister ancora attivi alla fine del campionamento e li memorizza in outputs.metadata["registers"]. Ogni registro è un array 2D (di forma (num_subsystems, num_randomizations), con possibili dimensioni finali per la forma dei gate) di elementi di gruppo virtuali.
Questo è utile per verificare che i gate virtuali siano stati combinati correttamente, o per ispezionare i campioni grezzi di Pauli o unitari prima che vengano sintetizzati in angoli di rotazione.
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)
Puoi visualizzare il contenuto dello stato finale di un particolare registro. Nell'esempio seguente, il registro è di tipo PauliRegister. Consulta la API documentation(/apidocs/index)__ per i dettagli sul formato di archiviazione.
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)
Come i nodi del samplex si associano ai parametri del template
L'array outputs["parameter_values"] restituito da sample() ha forma (num_randomizations, N), dove N corrisponde a len(template.parameters). Ogni colonna corrisponde a un parametro del circuito template — la i-esima colonna riempie template.parameters[i].
I nodi di raccolta CollectTemplateValues sono il collegamento tra i registri virtuali e i parametri del template. Ciascuno di questi nodi contiene informazioni sugli indici che registrano su quali colonne dell'array di output scrive. Questo rende possibile tracciare quali sottosistemi di gate virtuali guidano quali parametri del template.
Consulta la guida {Samplex inputs and outputs}samplex-io per imparare a associare i valori dei parametri campionati al circuito template ed eseguire esperimenti.
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]]
Tag automatici
Quando si usa generate_boxing_pass_manager(), il parametro add_tags aggiunge automaticamente annotazioni Tag a tutti i box. Sono disponibili tre modalità:
-
"unique_instance": assegnarefsequenziali (t0,t1e così via) ai box nell'ordine del circuito. Ogni box riceve unrefdistinto, indipendentemente dalla sua struttura. -
"unique_box": calcola un hash strutturale del contenuto di ciascun box e assegna lo stessorefa tutti i box strutturalmente equivalenti. È utile per raggruppare i box per tipo anziché per posizione. -
"noise_ref": copia ilrefdall'annotazioneInjectNoisedi ciascun box e contrassegna solo i box che ne hanno una. È utile quando esistono già noise ref significativi.
L'esempio seguente applica "unique_instance" e "unique_box" a un circuito i cui due box CX sono strutturalmente equivalenti. Con "unique_instance" ciascuno riceve un ref distinto, mentre con "unique_box" ne condividono uno.
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
