Debug e tracciamento
La funzione build() trasforma un circuito annotato in un circuito modello e in una coppia samplex. In un circuito complesso può risultare difficile stabilire quale casella del circuito originale corrisponda a quali barriere nel modello, oppure a quali nodi nel DAG di Samplex.
Samplomatic mette a disposizione due strumenti di tracciamento complementari:
- Etichette delle barriere : ogni barriera nel circuito del modello è contrassegnata da un'etichetta che ne indica l'origine. Questi sono sempre presenti, indipendentemente dal fatto che
debug=Truesia impostato o meno. - Informazioni di tracciamento sui nodi samplex : quando
build()viene chiamato condebug=True, ogni nodo samplex contiene un oggettoTraceInfoche lo ricollega alla casella o alle caselle che lo hanno generato.
Entrambi gli strumenti risultano particolarmente utili quando i riquadri contengono un’annotazione Tag , che associa una stringa ref al riquadro. In alternativa, sono allegati anche i riferimenti InjectNoise .
Caselle dei tag
Un'annotazione Tag associa una stringa ref a un riquadro. Il simbolo compare ref quindi nelle etichette delle barriere e nelle informazioni di tracciamento sui nodi samplex.
Il seguente circuito di esempio presenta tre caselle: due caselle CX etichettate su coppie di qubit disgiunte e una casella di misurazione "right-dressed" non etichettata. Anche la seconda casella CX presenta un’annotazione InjectNoise : sia il suo tag ref che il suo rumore ref appariranno nelle etichette delle barriere. Le due caselle CX coprono qubit diversi, il che consente all’ottimizzatore Samplex di unire i loro nodi di propagazione in nodi condivisi. Ciò è indicato nella sezione delle informazioni di tracciamento.
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 di barriera nel circuito del modello
La chiamata build() al circuito sopra riportato genera un modello le cui barriere recano etichette identificative. Ogni etichetta ha la forma {side}{scope}@{key=value&...}:
- Lato :
L= bordo con allineamento a sinistra,M= bordo del contenuto del riquadro interno,R= bordo con allineamento a destra. - Ambito : un indice intero (o un elenco separato da trattini bassi nel caso di riquadri annidati) che distingue più riquadri allo stesso livello di annidamento.
- Annotazioni : un elenco di coppie
key=value,@precedute dal prefisso "-" e&separate da "-", ricavate dalle annotazioni della casella. La chiavetagproviene da un'annotazioneTageinject_noiseproviene da un'annotazioneInjectNoise.
Le barriere provenienti da caselle senza etichetta riportano solo il lato e l'estensione (ad esempio, L2), senza alcun suffisso @ .
template, samplex = build(circuit)
template.draw("mpl", fold=100)
Le etichette delle barriere possono anche essere estratte a livello di programmazione:
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 sulle tracce nei nodi Samplex
Il passaggio debug=True a build() aggiunge informazioni di tracciamento a ogni nodo samplex. L'attributo trace_info di ciascun nodo è un TraceInfo oggetto il cui dizionario trace_refs associa le chiavi delle annotazioni (ad esempio, "tag" e "inject_noise") a insiemi di stringhe di riferimento. I nodi privi di una corrispondente annotazione in un riquadro hanno trace_info=None.
Quando draw() si chiama questa funzione su un samplex compilato in modalità debug, passando il mouse su un nodo qualsiasi del grafico interattivo, ne viene visualizzata la descrizione all’interno trace_refs del tooltip che appare al passaggio del mouse.
template, samplex = build(circuit, debug=True)
samplex.draw()È inoltre possibile esaminare le informazioni di tracciamento a livello di programmazione. Si noti che alcuni nodi contengono riferimenti sia da 'cx_ab' che da 'cx_cd'. L'ottimizzatore Samplex ha unito i nodi di propagazione paralleli poiché i due riquadri coprono qubit disgiunti e condividono un predecessore comune proveniente dall'emissione del riquadro di misurazione "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 individuare tutti i nodi provenienti da una casella specifica, applicare un filtro in base alla 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 di Samplex
Il samplex restituito da build() è un grafo aciclico orientato (DAG) in cui gli archi indicano le dipendenze tra i registri. In altre parole, un arco dal nodo A al nodo B implica che B debba attendere che A abbia effettuato le operazioni sui registri virtuali condivisi prima che B possa a sua volta agire. Non si tratta di un ordinamento temporale come il DAG di un circuito quantistico. Il grafico, invece, si snoda dai nodi incaricati di generare le randomizzazioni ai nodi incaricati di sintetizzarle come output.
Esistono tre tipi di nodi, ciascuno con uno stile visivo distinto nel grafico interattivo:
Forma | Colore | Tipo | Ruolo |
|---|---|---|---|
| Stella | Rosso | SamplingNode | Crea nuovi registri virtuali a partire da una distribuzione o da un input |
| Cerchio | Verde | EvaluationNode | Trasforma, combina o propaga i registri virtuali |
| Papillon | Blu / Viola | CollectionNode | Legge i registri e scrive sulle uscite sample() |
L'esecuzione si svolge in tre fasi:
- Tutti i nodi di campionamento vengono eseguiti per primi (in parallelo).
- Successivamente vengono eseguiti i nodi di valutazione, in ordine topologico (in parallelo all’interno di ciascuna generazione).
- I nodi di raccolta vengono eseguiti per ultimi (in parallelo).
Il layout di Graphviz riflette questa situazione. I nodi di campionamento compaiono in alto, quelli di raccolta in basso.
Suggerimenti al passaggio del mouse
Ogni nodo della visualizzazione interattiva mostra un'informativa quando ci si passa sopra con il mouse. Il testo di aiuto contiene le seguenti informazioni:
- Nome della classe del nodo e relativo indice intero nel grafo.
- Manifesti di registrazione, che registrano i nodi che l'istanza crea, da cui legge, su cui scrive e che rimuove. Questi schemi illustrano come avvengono i flussi di dati tra i nodi.
- Dettagli specifici per ciascun nodo, tra cui il tipo di distribuzione per i nodi di campionamento, l'operando per i nodi di moltiplicazione, gli indici dei parametri del modello per i nodi di raccolta e così via.
- Riferimenti di tracciamento (solo quando
debug=True), ovvero le chiavi di annotazione e le stringhe di riferimento che collegano il nodo alle caselle da cui proviene.
Cliccare sul grafico e poi passare il mouse sui singoli nodi è il modo più veloce per capire a cosa serve un determinato nodo senza leggere il codice sorgente.
Sintesi di Samplex
Prima di esaminare la visualizzazione, print(samplex) fornisce un breve riassunto testuale relativo al numero di nodi, agli input richiesti e agli output previsti.
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.
Controllare i registri
Il passaggio keep_registers=True a sample() conserva gli oggetti VirtualRegister intermedi che sono ancora attivi al termine del campionamento e li memorizza in outputs.metadata["registers"]. Ogni registro è un array di tipo " 2D " (di forma (num_subsystems, num_randomizations), con eventuali dimensioni aggiuntive relative alla forma del gate) composto da elementi di gruppi virtuali.
Ciò è utile per verificare che le porte virtuali siano state combinate correttamente, oppure per esaminare 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)
È possibile visualizzare il contenuto dello stato finale di un determinato registro. Nell'esempio seguente, il registro è di tipo PauliRegister. Per ulteriori dettagli sul formato di archiviazione, consultare la sezione API documentation(/apidocs/index)__.
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)
Come i nodi di Samplex vengono associati ai parametri dei modelli
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 modello — la i-esima colonna inserisce template.parameters[i].
I nodi di raccolta CollectTemplateValues costituiscono il collegamento tra i registri virtuali e i parametri dei modelli. Ciascuno di questi nodi contiene informazioni sull'indice che indicano in quali colonne dell'array di output vengono scritti i dati. Ciò consente di individuare quali sottosistemi di gate virtuali controllano quali parametri dei modelli.
Consulta la guida “ {Samplex inputs and outputs}samplex-io ” per scoprire come associare i valori dei parametri campionati al circuito del modello 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]]
Etichettatura automatica
Quando si utilizza generate_boxing_pass_manager(), il parametro add_tags aggiunge automaticamente le annotazioni Tag a tutte le caselle. Sono disponibili tre modalità:
"unique_instance": Assegna numeri sequenziali (ref``t0,t1, e così via) alle caselle nell'ordine del circuito. A ogni casella viene assegnato un identificativo univoco, indipendentementerefdalla sua struttura."unique_box": Calcola un hash strutturale del contenuto di ogni casella e assegna lo stesso hash areftutte le caselle strutturalmente equivalenti. Questo è utile per raggruppare i riquadri in base al tipo anziché alla posizione."noise_ref": Copia l'annotazioneInjectNoisedarefciascuna casella e contrassegna solo le caselle che ne contengono una. Ciò risulta utile quando esistono già riferimenti significativi al rumore.
L'esempio seguente applica "unique_instance" e "unique_box" a un circuito le cui due caselle CX sono strutturalmente equivalenti. Con "unique_instance", ciascuno ottiene un ref, 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