Saltar al contenido principal
Este proyecto está en fase beta. ¿Encontraste un error? Abre un issue en GitHub. ¿Tienes una pregunta o una idea? Pregunta en Discussions — ¡nos encantaría tu opinión!
¿Cómo es la calidad de la traducción en esta página?

Depuración y trazado

La función build() transforma un circuito anotado en un par formado por un circuito plantilla y un samplex. En un circuito complejo puede ser difícil saber qué caja del circuito original corresponde a qué barreras de la plantilla, o a qué nodos del DAG del samplex.

Samplomatic ofrece dos herramientas de trazado complementarias:

  • Etiquetas de barrera: Cada barrera del circuito plantilla lleva una etiqueta que codifica su origen. Siempre están presentes, sin importar si se establece debug=True.

  • Información de trazado en los nodos del samplex: Cuando se llama a build() con debug=True, cada nodo del samplex lleva un objeto TraceInfo que lo vincula con la caja o las cajas que lo produjeron.

Ambas herramientas son más útiles cuando las cajas llevan una anotación Tag, que adjunta una cadena ref a la caja. Alternativamente, también se adjuntan las referencias de InjectNoise.

Etiqueta las cajas​

Una anotación Tag adjunta una cadena ref a una caja. La ref aparece entonces en las etiquetas de barrera y en la información de trazado de los nodos del samplex.

El siguiente circuito de ejemplo tiene tres cajas: dos cajas CX etiquetadas sobre pares de qubits disjuntos y una caja de medición sin etiquetar con vestido a la derecha. La segunda caja CX también lleva una anotación InjectNoise: tanto su referencia tag como su ref de ruido aparecerán en las etiquetas de las barreras. Las dos cajas CX cubren qubits distintos, lo que permite que el optimizador de samplex fusione sus nodos de propagación en nodos compartidos. Esto se muestra en la sección de información de la traza.

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

Etiquetas de barrera en el circuito plantilla​

Al llamar a build() sobre el circuito anterior se produce una plantilla cuyas barreras llevan etiquetas identificativas. Cada etiqueta tiene la forma {side}{scope}@{key=value&...}:

  • Side (lado): L = límite de vestido izquierdo, M = límite del contenido interno de la caja, R = límite de vestido derecho.

  • Scope (ámbito): Un índice entero (o una lista separada por guiones bajos para cajas anidadas) que distingue varias cajas del mismo nivel de anidamiento.

  • Anotaciones: Una lista de pares key=value precedida por @ y separada por &, derivada de las anotaciones de la caja. La clave tag proviene de una anotación Tag e inject_noise proviene de una anotación InjectNoise.

Las barreras de cajas sin etiquetar llevan solo el lado y el ámbito (por ejemplo, L2), sin sufijo @.

template, samplex = build(circuit)
template.draw("mpl", fold=100)

../_images/e533d70a29504005a636aa27306d3ce3b1d2488358ac6a53f0674f2934698ab8.png

Las etiquetas de barrera también se pueden extraer de forma programática:

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']

Información de trazado en los nodos del samplex​

Pasar debug=True a build() adjunta información de trazado a cada nodo del samplex. El atributo trace_info de cada nodo es un objeto TraceInfo cuyo diccionario trace_refs asigna claves de anotación (por ejemplo, "tag" e "inject_noise") a conjuntos de cadenas ref. Los nodos sin una anotación de caja correspondiente tienen trace_info=None.

Cuando se llama a draw() sobre un samplex construido en modo de depuración, al pasar el cursor sobre cualquier nodo del grafo interactivo se muestran sus trace_refs en la información emergente.

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

La información de trazado también se puede inspeccionar de forma programática. Observa que algunos nodos llevan referencias tanto de 'cx_ab' como de 'cx_cd'. El optimizador del samplex fusionó sus nodos de propagación paralelos porque las dos cajas cubren qubits disjuntos y comparten un predecesor común de la emisión de la caja de medición vestida a la derecha.

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

Para encontrar todos los nodos que provienen de una caja específica, filtra por la clave "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

El DAG del samplex​

El samplex devuelto por build() es un grafo acíclico dirigido (DAG) en el que las aristas denotan dependencia de registros. Es decir, una arista del nodo A al nodo B significa que B debe esperar a que A haya actuado sobre los registros virtuales compartidos antes de que B pueda actuar. Esto no es un orden temporal como el DAG de un circuito cuántico. En cambio, el grafo fluye desde los nodos responsables de generar las aleatorizaciones hasta los nodos responsables de sintetizarlas como salidas.

Hay tres tipos de nodo, cada uno con un estilo visual distinto en el gráfico interactivo:

FormaColorTipoFunción
EstrellaRojoSamplingNodeInstancia nuevos registros virtuales a partir de una distribución o una entrada
CírculoVerdeEvaluationNodeTransforma, combina o propaga registros virtuales
PajaritaAzul / MoradoCollectionNodeLee registros y escribe en las salidas de sample()

La ejecución avanza en tres fases:

  1. Primero se ejecutan todos los nodos de muestreo (en paralelo).

  2. A continuación se ejecutan los nodos de evaluación, en orden topológico (en paralelo dentro de cada generación).

  3. Por último se ejecutan los nodos de colección (en paralelo).

El diseño de graphviz lo refleja. Los nodos de muestreo aparecen arriba y los nodos de colección abajo.

Información emergente al pasar el cursor​

Cada nodo de la visualización interactiva muestra una información emergente al pasar el cursor sobre él. La información emergente contiene lo siguiente:

  • Nombre de la clase del nodo y su índice entero en el grafo.

  • Manifiestos de registros, que indican qué registros instancia, lee, escribe y elimina el nodo. Estos te dicen cómo fluyen los datos entre nodos.

  • Detalles específicos del nodo, incluido el tipo de distribución de los nodos de muestreo, el operando de los nodos de multiplicación, los índices de parámetros de la plantilla de los nodos de colección, etcétera.

  • Trace refs (solo cuando debug=True), que son las claves de anotación y las cadenas ref que vinculan el nodo con las cajas de las que proviene.

Hacer clic en el gráfico y luego pasar el cursor sobre nodos individuales es la forma más rápida de entender qué hace un nodo determinado sin leer el código fuente.

Resumen del samplex​

Antes de examinar la visualización, print(samplex) ofrece un resumen de texto rápido del número de nodos, las entradas requeridas y las salidas prometidas.

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.

Inspecciona los registros​

Pasar keep_registers=True a sample() conserva los objetos VirtualRegister intermedios que siguen vivos al final del muestreo y los almacena en outputs.metadata["registers"]. Cada registro es un arreglo 2D (de forma (num_subsystems, num_randomizations), con posibles dimensiones finales de forma de puerta) de elementos de grupo virtuales.

Esto es útil para verificar que las puertas virtuales se combinaron correctamente, o para inspeccionar las muestras brutas de Pauli o unitarias antes de que se sinteticen en ángulos de rotación.

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)

Puedes ver el contenido del estado final de un registro concreto. En el siguiente ejemplo, el registro es de tipo PauliRegister. Consulta la API documentation(/apidocs/index)__ para ver los detalles sobre el formato de almacenamiento.

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)

Cómo se asignan los nodos del samplex a los parámetros de la plantilla​

El arreglo outputs["parameter_values"] devuelto por sample() tiene forma (num_randomizations, N), donde N coincide con len(template.parameters). Cada columna corresponde a un parámetro del circuito plantilla: la i-ésima columna rellena template.parameters[i].

Los nodos de colección CollectTemplateValues son el vínculo entre los registros virtuales y los parámetros de la plantilla. Cada uno de estos nodos guarda información de índices que registra en qué columnas del arreglo de salida escribe. Esto permite rastrear qué subsistemas de puertas virtuales controlan qué parámetros de la plantilla.

Consulta la guía {Samplex inputs and outputs}samplex-io para aprender a enlazar los valores de parámetros muestreados al circuito plantilla y a ejecutar experimentos.

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]]

Etiquetado automático​

Al usar generate_boxing_pass_manager(), el parámetro add_tags añade automáticamente anotaciones Tag a todas las cajas. Hay tres modos disponibles:

  • "unique_instance": Asigna ref secuenciales (t0, t1, y así sucesivamente) a los boxes en el orden del circuito. Cada box recibe un ref distinto, independientemente de su estructura.

  • "unique_box": Calcula un hash estructural del contenido de cada box y asigna el mismo ref a todos los boxes estructuralmente equivalentes. Esto resulta útil para agrupar los boxes por tipo en lugar de por posición.

  • "noise_ref": Copia el ref de la anotación InjectNoise de cada box y solo etiqueta los boxes que tienen una. Esto resulta útil cuando ya existen noise refs significativos.

El siguiente ejemplo aplica "unique_instance" y "unique_box" a un circuito cuyos dos boxes CX son estructuralmente equivalentes. Con "unique_instance", cada uno recibe un ref distinto, mientras que con "unique_box" comparten 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