Vai al contenuto principale
Questo progetto è in fase beta. Hai trovato un bug? Apri una issue su GitHub. Hai una domanda o un'idea? Chiedi su GitHub Discussions — apprezziamo il tuo feedback!
Com'è la qualità della traduzione su questa pagina?

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 con debug=True, ogni nodo del samplex porta un oggetto TraceInfo che 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")

../_images/f6b6fe7b60a39ba897814008443fa79b66213005462ab09f49ffc89aab1f3668.png

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=value prefissato da @ e separato da &, ricavato dalle annotazioni del box. La chiave tag proviene da un'annotazione Tag e inject_noise proviene da un'annotazione InjectNoise.

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)

../_images/e533d70a29504005a636aa27306d3ce3b1d2488358ac6a53f0674f2934698ab8.png

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:

FormaColoreTipoRuolo
StellaRossoSamplingNodeIstanzia nuovi registri virtuali da una distribuzione o da un input
CerchioVerdeEvaluationNodeTrasforma, combina o propaga registri virtuali
FarfallaBlu / ViolaCollectionNodeLegge i registri e scrive negli output di sample()

L'esecuzione procede in tre fasi:

  1. Prima vengono eseguiti tutti i nodi di campionamento (in parallelo).

  2. Poi vengono eseguiti i nodi di valutazione, in ordine topologico (in parallelo all'interno di ogni generazione).

  3. 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": assegna ref sequenziali (t0, t1 e così via) ai box nell'ordine del circuito. Ogni box riceve un ref distinto, indipendentemente dalla sua struttura.

  • "unique_box": calcola un hash strutturale del contenuto di ciascun box e assegna lo stesso ref a tutti i box strutturalmente equivalenti. È utile per raggruppare i box per tipo anziché per posizione.

  • "noise_ref": copia il ref dall'annotazione InjectNoise di 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