Transpilador
La función generate_boxing_pass_manager() es una herramienta flexible y práctica para construir un qiskit.transpiler.PassManager que puede agrupar las instrucciones del circuito en boxes anotados. Esto ofrece una alternativa sencilla a agrupar y anotar manualmente los circuitos, y resulta útil para automatizar un flujo de trabajo, probar distintas estrategias de agrupación de circuitos en boxes, etc.
Esta guía ilustra cómo usar generate_boxing_pass_manager() y sus argumentos. Para destacar los efectos de cada uno de los argumentos de la función, las secciones siguientes se refieren principalmente al circuito que aparece a continuación.
from qiskit.circuit import Parameter, QuantumCircuit
circuit = QuantumCircuit(4, 7)
circuit.h(1)
circuit.h(2)
circuit.cz(1, 2)
circuit.h(1)
circuit.cx(1, 0)
circuit.cx(2, 3)
circuit.measure(range(1, 4), range(3))
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
for qubit in range(4):
circuit.rz(Parameter(f"th_{qubit}"), qubit)
circuit.rx(Parameter(f"phi_{qubit}"), qubit)
circuit.rz(Parameter(f"lam_{qubit}"), qubit)
circuit.measure(range(4), range(3, 7))
circuit.draw("mpl", scale=0.8)
Agrupar operaciones en boxes
El argumento enable_gates puede establecerse en True o False para especificar si las puertas de dos qubits deben agruparse en cajas. De manera similar, enable_measures especifica si las mediciones deben agruparse o no. El siguiente fragmento muestra un ejemplo en el que se agrupan tanto las puertas como las mediciones.
from samplomatic.transpiler import generate_boxing_pass_manager
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)

Como se puede ver en la figura, el pass manager crea boxes de dos tipos: los que contienen una única capa de puertas de dos qubits y los que contienen una única capa de mediciones. Esta separación refleja las prácticas habituales en los protocolos de aprendizaje y mitigación de ruido, que normalmente se dirigen a capas de operaciones homogéneas. Las puertas de dos qubits y las mediciones se colocan en el box situado más a la izquierda que pueda acogerlas, y cada puerta de un solo qubit se coloca en el mismo box que la puerta de dos qubits o la medición a la que precede.
El siguiente código muestra un ejemplo en el que enable_gates se establece en False. Como se puede ver, las puertas de dos qubits no se agrupan en boxes, ni tampoco las puertas de un solo qubit que las preceden.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=False,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)
Elegir cómo anotar los boxes
Todas las puertas de dos qubits y los boxes de medición del circuito devuelto poseen anotaciones con dressing a la izquierda. En particular, todos los boxes que contienen puertas de dos qubits se anotan con un Twirl. Para los boxes de medición, los usuarios pueden elegir entre Twirl, BasisTranform (con mode establecido en "measure") o ambos. El siguiente código genera un circuito en el que se aplica twirling a todos los boxes y los boxes de medición se anotan además con BasisTranform.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
measure_annotations="all",
)
transpiled_circuit = boxing_pass_manager.run(circuit)
Preparar el circuito para la inyección de ruido
inject_noise_targets te permite especificar qué boxes deben recibir una anotación InjectNoise. Por ejemplo, el siguiente código genera un circuito en el que los boxes de puertas de dos qubits poseen una anotación InjectNoise, pero los boxes de medición no.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
inject_noise_targets="gates",
)
transpiled_circuit = boxing_pass_manager.run(circuit)
Si un circuito contiene dos o más boxes que son equivalentes salvo por las puertas de un solo qubit, todos ellos se anotan con una anotación InjectNoise con el mismo ref. Por lo tanto, el número de refs únicos en el circuito devuelto es igual al número de boxes únicos.
El valor de inject_noise_strategy afecta a las anotaciones InjectNoise. Son posibles los siguientes valores:
-
modifier_ref='': Recomendado cuando no necesitas modificar los mapas de ruido antes de muestrear de ellos. -
modifier_ref=ref: Recomendado cuando todos los mapas de ruido deben escalarse de manera uniforme por el mismo factor. -
Un valor único de
modifier_ref: Recomendado cuando cada mapa de ruido debe escalarse por un factor distinto.
El siguiente código genera un circuito en el que los boxes de puertas de dos qubits poseen una anotación InjectNoise con valores únicos de modifier_ref.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
inject_noise_targets="gates",
inject_noise_strategy="individual_modification",
)
transpiled_circuit = boxing_pass_manager.run(circuit)
Seleccionar una estrategia de twirling
El boxing pass manager comienza eligiendo qué puertas entrelazantes e instrucciones de medición deben aparecer en los mismos boxes. Una vez hecho esto, los boxes, en general, no tendrán el ancho completo: probablemente incluirán solo un subconjunto de los qubits que están activos en el circuito o presentes en el target del backend. En las configuraciones típicas del boxing pass manager, dado que se aplica twirling a todos los qubits sobre los que actúa una instrucción box (incluso aquellos sobre los que no actúa ninguna instrucción dentro del box), tener boxes de ancho parcial afecta a los efectos de adaptación del ruido del twirling. A los qubits inactivos no se les aplica twirling y pueden, por ejemplo, acumular errores coherentes mientras se aplica el box. twirling_strategy ofrece opciones para extender los qubits que posee cada box, modificando así el número de qubits inactivos:
-
"active"(predeterminado) no extiende los boxes a ningún qubit inactivo. -
"active_accum"extiende todos los boxes a aquellos qubits sobre los que ya ha actuado una instrucción previa del circuito. Esto es útil si quieres dejar los qubits al inicio del circuito en su estado fundamental durante el mayor tiempo posible, sin aplicarles twirling hasta que se vuelvan activos. -
"active_circuit"extiende todos los boxes a aquellos qubits sobre los que actúa cualquier instrucción del circuito. Esto es útil si tienes un circuito estructurado con capas entrelazantes repetidas y quieres minimizar el número de boxes únicos, porque hay que aprender el ruido de cada uno. -
"all"extiende todos los boxes a todos los qubits de todos los registros cuánticos del circuito. Esto es útil como extensión de"active_circuit"cuando quieres que a todos los qubits de la QPU se les aplique siempre twirling.
En el siguiente ejemplo, cada una de las estrategias se aplica al circuito base y luego se dibujan los resultados.
circuit = QuantumCircuit(5, 2)
circuit.h(range(4))
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
circuit.barrier()
circuit.h(range(4))
circuit.measure([1, 2], [0, 1])
circuit.draw("mpl", scale=0.6)
import matplotlib.pyplot as plt
plt.figure(figsize=(9, 6))
for idx, strategy in enumerate(["active", "active_accum", "active_circuit", "all"]):
ax = plt.subplot(2, 2, idx + 1)
pm = generate_boxing_pass_manager(twirling_strategy=strategy, remove_barriers="never")
transpiled_circuit = pm.run(circuit)
fig = transpiled_circuit.draw("mpl", scale=0.8, ax=ax)
ax.set_title(f"twirling_strategy='{strategy}'")
plt.tight_layout()

Especificar cómo tratar las barreras
De forma predeterminada, las barreras se eliminan del circuito después de que todas las puertas entrelazantes e instrucciones de medición se hayan agrupado en boxes según lo indicado, incluidas sus extensiones debidas a la selección de twirling_strategy, pero antes de que se añadan las puertas de un solo qubit al box situado inmediatamente a su derecha, si es posible.
Por ejemplo, el siguiente circuito contiene una barrera:
circuit_with_barrier = QuantumCircuit(4)
circuit_with_barrier.h(range(4))
circuit_with_barrier.cz(0, 1)
circuit_with_barrier.barrier()
circuit_with_barrier.cz(2, 3)
circuit_with_barrier.measure_all()
circuit_with_barrier.draw("mpl", scale=0.8)
El comportamiento predeterminado impide que las dos puertas entrelazantes acaben en el mismo box, aunque permite que las puertas de un solo qubit sean absorbidas por el siguiente box disponible a pesar de la barrera.
boxing_pass_manager = generate_boxing_pass_manager()
transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
Establecer remove_barriers en 'never' conserva las barreras, de modo que las puertas de un solo qubit quedan restringidas.
boxing_pass_manager = generate_boxing_pass_manager(remove_barriers="never")
transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
Por último, si las barreras se eliminan de inmediato, los entrelazadores ya no están obligados a acabar en boxes separados.
boxing_pass_manager = generate_boxing_pass_manager(remove_barriers="immediately")
transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
Cómo funciona la estratificación
El boxing pass manager ejecuta dos pasadas clave en secuencia:
-
GroupGatesIntoBoxes— Estratifica las puertas entrelazantes en boxes. Las puertas de un solo qubit se ignoran en esta etapa; solo se agrupan las puertas de dos qubits. -
AbsorbSingleQubitGates— Absorbe cada puerta de un solo qubit en el box elegible más cercano a su derecha.
Durante la estratificación, GroupGatesIntoBoxes recorre el DAG del circuito en orden topológico y coloca cada puerta de dos qubits en el primer grupo cuyos qubits aún no están ocupados. Tres tipos de instrucciones actúan como delimitadores que vacían el grupo actual para sus qubits, forzando que las puertas posteriores sobre esos qubits pasen a un nuevo grupo:
-
Barreras
-
Instrucciones box existentes
-
Measurements
Las puertas de un solo qubit no son delimitadores: no tienen efecto en la agrupación y se dejan para que las gestione AbsorbSingleQubitGates. Actualmente no se realiza ningún análisis de conmutatividad; la agrupación respeta el orden topológico del circuito tal cual.
Por qué remove_barriers="after_stratification" es el valor predeterminado recomendado
El modo predeterminado es "after_stratification" porque combina dos ventajas:
-
Las barreras restringen cómo se agrupan las puertas entrelazantes (actúan como delimitadores durante la estratificación).
-
Las barreras se eliminan antes de que se ejecute
AbsorbSingleQubitGates, de modo que las puertas de un solo qubit aún pueden fluir a través de las posiciones de las antiguas barreras y ser absorbidas por el box más cercano.
En cambio:
-
"never"mantiene las barreras durante todo el proceso, lo que atrapa las puertas de un solo qubit entre una barrera y un box, impide su absorción y deja rotaciones sin twirling en el circuito. -
"immediately"elimina las barreras antes de la estratificación, por lo que no tienen efecto en la agrupación. -
"finally"es útil cuando quieres barreras en el circuito de salida (por ejemplo, para visualización), pero las puertas de un solo qubit entre una barrera y un box no se absorben.
Agrupar en cajas los circuitos ISA con barreras
Los circuitos con una estructura repetida (como las simulaciones hamiltonianas con Trotter, los ansatze variacionales y las secuencias de amplificación de errores) suelen construirse de forma programática y luego transpilarse a la forma ISA. Añadir cajas manualmente a un circuito transpilado es engorroso, por lo que el flujo de trabajo natural es el siguiente:
-
Construye el circuito lógico e inserta barreras en los límites de capa que quieras que se conviertan en límites de caja.
-
Transpila a circuitos ISA. El transpilador de Qiskit conserva las barreras durante el enrutamiento y la optimización.
-
Ejecuta
generate_boxing_pass_manager()sobre el circuito ISA.
Las barreras son necesarias cuando el final de una capa no cubre todos los qubits. De lo contrario, el algoritmo voraz colocaría en el mismo grupo las puertas de la capa siguiente que actúan sobre esos qubits «libres». Como ejemplo concreto, considera una simulación de Heisenberg ZZ de dos pasos en un anillo de 5 qubits. Cada paso de Trotter tiene tres capas de enlaces: los enlaces pares (0,1), (2,3), los enlaces impares (1,2), (3,4) y el enlace de cierre (4,0). Después de la capa de enlaces impares, los qubits 0 y 4 quedan libres. Sin una barrera, el enlace de cierre (4,0) se agrupa de forma voraz con los enlaces del siguiente paso de Trotter que usan esos qubits libres, mezclando en la misma caja puertas de pasos distintos.
from qiskit.circuit import Parameter, QuantumCircuit
n = 5 # 5-qubit ring: bonds (0,1), (1,2), (2,3), (3,4), (4,0)
dt = Parameter("dt")
heisenberg = QuantumCircuit(n)
for step in range(2):
# Layer 1: even bonds (0,1) and (2,3)
for idx in range(0, n - 1, 2):
heisenberg.rzz(dt, idx, idx + 1)
heisenberg.barrier()
# Layer 2: odd bonds (1,2) and (3,4) -- qubits 0 and 4 are now free
for idx in range(1, n - 1, 2):
heisenberg.rzz(dt, idx, idx + 1)
heisenberg.barrier()
# Layer 3: wrap bond (4,0) -- both qubits were free after layer 2
heisenberg.rzz(dt, n - 1, 0)
if step < 1:
heisenberg.barrier() # separates the two Trotter steps
heisenberg.measure_all()
heisenberg.draw("mpl", scale=0.7)
El siguiente código transpila el circuito a la forma ISA, suponiendo que el dispositivo subyacente tiene CX como entrelazador nativo. Ten en cuenta que las barreras sobreviven a la transpilación y siguen siendo visibles en el circuito de salida.
from qiskit.transpiler import generate_preset_pass_manager
preset_pm = generate_preset_pass_manager(
basis_gates=["rz", "sx", "cx"],
coupling_map=[[0, 1], [1, 2], [2, 3], [3, 4], [4, 0]],
optimization_level=1,
)
isa_circuit = preset_pm.run(heisenberg)
isa_circuit.draw("mpl", scale=0.5, fold=200)
A continuación, aplica el gestor de pases de encajonado al circuito ISA. Cada una de las tres capas de enlaces se convierte en una caja independiente en cada paso de Trotter.
boxing_pm = generate_boxing_pass_manager()
boxed_isa = boxing_pm.run(isa_circuit)
boxed_isa.draw("mpl", scale=0.5, fold=100)
Incorporar el gestor de pases de Samplomatic a los gestores de pases preestablecidos de Qiskit
El código siguiente muestra cómo incorporar generate_boxing_pass_manager() a un gestor de pases preestablecido de Qiskit. El circuito transpilado es ISA y contiene cajas.
from qiskit.transpiler import generate_preset_pass_manager
preset_pass_manager = generate_preset_pass_manager(
basis_gates=["rz", "sx", "cx"],
coupling_map=[[0, 1], [1, 2]],
optimization_level=0,
)
boxing_pass_manager = generate_boxing_pass_manager()
# Run the boxing pass manager after the scheduling stage
preset_pass_manager.post_scheduling = boxing_pass_manager
circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 1)
circuit.cx(0, 2)
circuit.measure_all()
transpiled_circuit = preset_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)
Este patrón también se combina de forma natural con el flujo de trabajo de encajonado ISA basado en barreras. El gestor de pases de encajonado se ejecuta como un paso posterior a la planificación, después de que las barreras del circuito lógico original hayan sobrevivido a la transpilación, de modo que guían la estratificación como se espera.
preset_pm = generate_preset_pass_manager(
basis_gates=["rz", "sx", "cx"],
coupling_map=[[0, 1], [1, 2], [2, 3], [3, 4], [4, 0]],
optimization_level=1,
)
preset_pm.post_scheduling = generate_boxing_pass_manager()
boxed_heisenberg = preset_pm.run(heisenberg)
boxed_heisenberg.draw("mpl", scale=0.5, fold=100)
Inspeccionar y modificar anotaciones
Después del encajonado automático, puede que quieras inspeccionar las anotaciones de cajas concretas o ajustarlas. Por ejemplo, para añadir inyección de ruido, eliminar anotaciones o cambiar un grupo de twirling. Samplomatic ofrece para ello un conjunto de funciones de utilidad en samplomatic.utils. Todas devuelven un circuito nuevo sin modificar el original.
extend_annotations() añade una o más anotaciones a cada caja del circuito.
from samplomatic import InjectNoise, Twirl
from samplomatic.utils import extend_annotations
boxed = generate_boxing_pass_manager().run(circuit_with_barrier)
# Add InjectNoise to every box
with_noise = extend_annotations(boxed, InjectNoise("my_noise_ref"))
with_noise.draw("mpl", scale=0.8)
filter_annotations() conserva solo las anotaciones de los tipos especificados y elimina el resto. replace_annotations() aplica un invocable a cada anotación; el invocable devuelve una lista de anotaciones que se sustituyen en su lugar (devuelve [] para eliminar, una lista de un solo elemento para reemplazar o una lista de varios elementos para expandir).
from samplomatic.utils import filter_annotations, replace_annotations
# Keep only Twirl annotations, drop everything else
twirl_only = filter_annotations(with_noise, Twirl)
# Replace the twirling group on every Twirl annotation
updated = replace_annotations(
twirl_only, lambda a: [Twirl(group="local_c1")] if isinstance(a, Twirl) else [a]
)
La función de propósito general map_annotations() acepta cualquier invocable que transforme una lista de anotaciones en una lista nueva, lo que te da control total sobre las transformaciones por caja. strip_annotations() es un contenedor de conveniencia que elimina todas las anotaciones de cada caja.
Para cambios masivos, como añadir InjectNoise a todas las cajas de puertas, el parámetro inject_noise_targets de generate_boxing_pass_manager() es la opción más concisa. Estas utilidades son útiles para ediciones selectivas por caja que los parámetros del gestor de pases no cubren.
Construir un circuito
Todo gestor de pases producido por generate_boxing_pass_manager() devuelve circuitos que build() puede convertir correctamente en un par plantilla/samplex. Por ejemplo, el siguiente código llama a la función build() sobre un circuito producido por un gestor de pases de encajonado.
from samplomatic import build
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)
template, samplex = build(transpiled_circuit)
Para garantizar que cualquier circuito transpilado se pueda construir correctamente, los gestores de pases saben incluir cajas adicionales cuando hacen falta. Por ejemplo, considera el siguiente circuito, que termina con un qubit 0 sin medir.
circuit_with_unmeasured_qubit = QuantumCircuit(4, 3)
circuit_with_unmeasured_qubit.cz(0, 1)
circuit_with_unmeasured_qubit.cz(2, 3)
for qubit in range(4):
circuit_with_unmeasured_qubit.rz(Parameter(f"th_{qubit}"), qubit)
circuit_with_unmeasured_qubit.rx(Parameter(f"phi_{qubit}"), qubit)
circuit_with_unmeasured_qubit.rz(Parameter(f"lam_{qubit}"), qubit)
circuit_with_unmeasured_qubit.measure(range(1, 4), range(3))
circuit_with_unmeasured_qubit.draw("mpl", scale=0.8)
Dibujar cajas con vestido a la izquierda alrededor de las puertas y las mediciones daría como resultado un circuito con puertas virtuales sin recoger en el qubit 0, y llamar a build() sobre este circuito produciría un error. Para evitarlo, los gestores de pases devueltos por generate_boxing_pass_manager() pueden añadir cajas con vestido a la derecha que actúen como recolectoras. Por ejemplo, en el siguiente código, el qubit 0 termina en una caja con vestido a la derecha que recoge la puerta virtual sin recoger. Las puertas de un solo qubit que actúan sobre el qubit 0 también se colocan dentro de la caja para minimizar la profundidad del circuito resultante.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit_with_unmeasured_qubit)
transpiled_circuit.draw("mpl", scale=0.8)
En otro ejemplo, se añade una caja con vestido a la derecha para recoger las puertas virtuales que de otro modo quedarían sin recoger debido a las mediciones sin encajonar.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=False,
)
transpiled_circuit = boxing_pass_manager.run(circuit_with_unmeasured_qubit)
transpiled_circuit.draw("mpl", scale=0.8)