Transpileur
La fonction generate_boxing_pass_manager() est un outil flexible et pratique pour construire un qiskit.transpiler.PassManager capable de regrouper les instructions du circuit en boxes annotées. Cela offre une alternative simple au regroupement et à l'annotation manuels des circuits, et est utile pour automatiser un workflow, tester différentes stratégies de mise en boxes des circuits, etc.
Ce guide illustre comment utiliser generate_boxing_pass_manager() et ses arguments. Pour mettre en évidence les effets de chacun des arguments de la fonction, les sections suivantes se réfèrent principalement au circuit ci-dessous.
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)
Regrouper les opérations en boxes
L'argument enable_gates peut être défini à True ou False pour indiquer si les portes à deux qubits doivent être regroupées en boxes. De même, enable_measures indique si les mesures doivent être regroupées ou non. L'extrait suivant montre un exemple où les portes et les mesures sont toutes deux regroupées.
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)

Comme le montre la figure, le pass manager crée des boxes de deux types : celles qui contiennent une seule couche de portes à deux qubits, et celles qui contiennent une seule couche de mesures. Cette séparation reflète les pratiques courantes dans les protocoles d'apprentissage et d'atténuation du bruit, qui ciblent généralement des couches d'opérations homogènes. Les portes à deux qubits et les mesures sont placées dans la box la plus à gauche qui peut les accueillir, et chaque porte à un qubit est placée dans la même box que la porte à deux qubits ou la mesure qu'elle précède.
Le code suivant montre un exemple où enable_gates est défini à False. Comme on peut le voir, les portes à deux qubits ne sont pas regroupées en boxes, pas plus que les portes à un qubit qui les précèdent.
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)
Choisir comment annoter les boxes
Toutes les boxes de portes à deux qubits et de mesures du circuit renvoyé possèdent des annotations d'habillage à gauche. En particulier, toutes les boxes qui contiennent des portes à deux qubits sont annotées avec un Twirl. Pour les boxes de mesure, les utilisateurs peuvent choisir entre Twirl, BasisTranform (avec mode défini à "measure"), ou les deux. Le code suivant génère un circuit où toutes les boxes subissent un twirling, et où les boxes de mesure sont en plus annotées avec BasisTranform.
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
measure_annotations="all",
)
transpiled_circuit = boxing_pass_manager.run(circuit)
Préparer le circuit pour l'injection de bruit
inject_noise_targets te permet de spécifier quelles boxes doivent recevoir une annotation InjectNoise. Par exemple, le code suivant génère un circuit où les boxes de portes à deux qubits possèdent une annotation InjectNoise mais pas les boxes de mesure.
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 circuit contient deux boxes ou plus qui sont équivalentes à des portes à un qubit près, toutes sont annotées avec une annotation InjectNoise ayant le même ref. Ainsi, le nombre de ref uniques dans le circuit renvoyé est égal au nombre de boxes uniques.
La valeur de inject_noise_strategy influence les annotations InjectNoise. Les valeurs suivantes sont possibles :
-
modifier_ref='': recommandé lorsque tu n'as pas besoin de modifier les cartes de bruit avant d'y échantillonner. -
modifier_ref=ref: recommandé lorsque toutes les cartes de bruit doivent être mises à l'échelle uniformément par le même facteur. -
Une valeur unique de
modifier_ref: recommandé lorsque chaque carte de bruit doit être mise à l'échelle par un facteur différent.
Le code suivant génère un circuit où les boxes de portes à deux qubits possèdent une annotation InjectNoise avec des valeurs de modifier_ref uniques.
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)
Sélectionner une stratégie de twirling
Le boxing pass manager commence par choisir quelles portes d'intrication et quelles instructions de mesure doivent apparaître dans les mêmes boxes. Une fois cela fait, les boxes n'auront en général pas la pleine largeur : elles n'incluront probablement qu'un sous-ensemble des qubits actifs dans le circuit ou présents dans la cible du backend. Dans les configurations typiques du boxing pass manager, puisque tous les qubits sur lesquels agit une instruction box subissent un twirling (même ceux sur lesquels aucune instruction de la box n'agit), avoir des boxes de largeur partielle affecte les effets de façonnage du bruit du twirling. Les qubits inactifs ne subissent pas de twirling et peuvent, par exemple, accumuler des erreurs cohérentes pendant l'application de la box. twirling_strategy offre des options pour étendre les qubits possédés par chaque box, modifiant ainsi le nombre de qubits inactifs :
-
"active"(par défaut) n'étend les boxes à aucun qubit inactif. -
"active_accum"étend toutes les boxes aux qubits sur lesquels une instruction antérieure du circuit a déjà agi. C'est utile si tu veux laisser les qubits du début du circuit dans leur état fondamental le plus longtemps possible, en ne leur appliquant un twirling que lorsqu'ils deviennent actifs. -
"active_circuit"étend toutes les boxes aux qubits sur lesquels agit une instruction quelconque du circuit. C'est utile si tu as un circuit structuré avec des couches d'intrication répétées et que tu veux minimiser le nombre de boxes uniques, car le bruit de chacune doit être appris. -
"all"étend toutes les boxes à tous les qubits de tous les registres quantiques du circuit. C'est utile comme extension de"active_circuit"lorsque tu veux que tous les qubits du QPU subissent toujours un twirling.
Dans l'exemple suivant, chacune des stratégies est appliquée au circuit de base, puis les résultats sont dessinés.
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()

Spécifier comment traiter les barrières
Par défaut, les barrières sont supprimées du circuit une fois que toutes les portes d'intrication et instructions de mesure ont été placées dans des boxes comme demandé, y compris leurs extensions dues à la sélection de twirling_strategy, mais avant que les portes à un qubit ne soient ajoutées à la box située immédiatement à leur droite, si possible.
Par exemple, le circuit suivant contient une barrière :
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)
Le comportement par défaut empêche les deux portes d'intrication de se retrouver dans la même box, bien qu'il permette aux portes à un qubit d'être absorbées dans la prochaine box disponible malgré la barrière.
boxing_pass_manager = generate_boxing_pass_manager()
transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
Définir remove_barriers à 'never' préserve les barrières, de sorte que les portes à un qubit sont contraintes.
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)
Enfin, si les barrières sont supprimées immédiatement, les portes d'intrication ne sont plus contraintes de se retrouver dans des boxes séparées.
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)
Comment fonctionne la stratification
Le boxing pass manager exécute deux passes clés en séquence :
-
GroupGatesIntoBoxes— stratifie les portes d'intrication en boxes. Les portes à un qubit sont ignorées à ce stade ; seules les portes à deux qubits sont regroupées. -
AbsorbSingleQubitGates— absorbe chaque porte à un qubit dans la box éligible la plus proche à sa droite.
Pendant la stratification, GroupGatesIntoBoxes parcourt le DAG du circuit dans l'ordre topologique et place chaque porte à deux qubits dans le premier groupe dont les qubits ne sont pas encore occupés. Trois types d'instructions agissent comme des délimiteurs qui vident le groupe courant pour leurs qubits, forçant les portes suivantes sur ces qubits à aller dans un nouveau groupe :
-
Barrières
-
Instructions box existantes
-
Measurements
Les portes à un qubit ne sont pas des délimiteurs : elles n'ont aucun effet sur le regroupement et sont laissées à AbsorbSingleQubitGates. Aucune analyse de commutativité n'est actuellement effectuée ; le regroupement respecte l'ordre topologique du circuit tel quel.
Pourquoi remove_barriers="after_stratification" est la valeur par défaut recommandée
Le mode par défaut est "after_stratification" car il combine deux avantages :
-
Les barrières contraignent la façon dont les portes d'intrication sont regroupées (elles agissent comme des délimiteurs pendant la stratification).
-
Les barrières sont supprimées avant l'exécution de
AbsorbSingleQubitGates, de sorte que les portes à un qubit peuvent encore traverser les anciennes positions des barrières et être absorbées dans la box la plus proche.
En revanche :
-
"never"conserve les barrières tout du long, ce qui piège les portes à un qubit entre une barrière et une box, empêchant leur absorption et laissant des rotations sans twirling dans le circuit. -
"immediately"supprime les barrières avant la stratification, de sorte qu'elles n'ont aucun effet sur le regroupement. -
"finally"est utile lorsque tu veux des barrières dans le circuit de sortie (par exemple, pour la visualisation), mais les portes à un qubit situées entre une barrière et une box ne sont pas absorbées.
Encadrer des circuits ISA avec des barrières
Les circuits à structure répétée — comme les simulations hamiltoniennes trotterisées, les ansatz variationnels et les séquences d'amplification d'erreur — sont généralement construits par programmation puis transpilés en forme ISA. Ajouter manuellement des boîtes à un circuit transpilé est fastidieux ; le flux de travail naturel est donc le suivant :
-
Construis le circuit logique en insérant des barrières aux frontières de couches que tu veux transformer en frontières de boîtes.
-
Transpile en circuits ISA. Le transpileur de Qiskit préserve les barrières pendant le routage et l'optimisation.
-
Exécute
generate_boxing_pass_manager()sur le circuit ISA.
Les barrières sont nécessaires lorsque la fin d'une couche ne couvre pas tous les qubits. Sinon, l'algorithme glouton placerait dans le même groupe les portes de la couche suivante — sur ces qubits « libres ». Prenons comme exemple concret une simulation de Heisenberg ZZ en deux étapes sur un anneau de 5 qubits. Chaque étape de Trotter comporte trois couches de liaisons : les liaisons paires (0,1), (2,3), les liaisons impaires (1,2), (3,4) et la liaison de bouclage (4,0). Après la couche de liaisons impaires, les qubits 0 et 4 sont tous deux libres. Sans barrière, la liaison de bouclage (4,0) est regroupée de façon gloutonne avec les liaisons de l'étape de Trotter suivante qui utilisent ces qubits libres, ce qui mélange dans une même boîte des portes d'étapes différentes.
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)
Le code suivant transpile le circuit en forme ISA, en supposant que l'appareil sous-jacent a CX comme intricateur natif. Note que les barrières survivent à la transpilation et restent visibles dans le circuit de sortie.
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)
Ensuite, applique le pass manager d'encadrement au circuit ISA. Chacune des trois couches de liaisons devient une boîte distincte dans chaque étape de Trotter.
boxing_pm = generate_boxing_pass_manager()
boxed_isa = boxing_pm.run(isa_circuit)
boxed_isa.draw("mpl", scale=0.5, fold=100)
Intégrer le pass manager de Samplomatic aux pass managers prédéfinis de Qiskit
Le code ci-dessous montre comment intégrer generate_boxing_pass_manager() dans un pass manager prédéfini de Qiskit. Le circuit transpilé est ISA et contient des boîtes.
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)
Ce modèle se combine aussi naturellement avec le flux de travail d'encadrement ISA basé sur les barrières. Le pass manager d'encadrement s'exécute comme une étape postérieure à l'ordonnancement, après que les barrières du circuit logique d'origine ont survécu à la transpilation, de sorte qu'elles guident la stratification comme prévu.
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)
Inspecter et modifier les annotations
Après l'encadrement automatique, tu peux vouloir inspecter les annotations de certaines boîtes ou les ajuster. Par exemple, pour ajouter une injection de bruit, supprimer des annotations ou changer un groupe de twirling. Samplomatic fournit pour cela un ensemble de fonctions utilitaires dans samplomatic.utils. Toutes renvoient un nouveau circuit sans modifier l'original.
extend_annotations() ajoute une ou plusieurs annotations à chaque boîte du circuit.
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() ne conserve que les annotations des types spécifiés et supprime les autres. replace_annotations() applique un callable à chaque annotation ; le callable renvoie une liste d'annotations à substituer à la place (renvoie [] pour supprimer, une liste à un élément pour remplacer, ou une liste à plusieurs éléments pour développer).
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 fonction généraliste map_annotations() accepte n'importe quel callable qui associe une liste d'annotations à une nouvelle liste, ce qui te donne un contrôle total sur les transformations par boîte. strip_annotations() est un wrapper pratique qui supprime toutes les annotations de chaque boîte.
Pour des modifications en masse, comme ajouter InjectNoise à toutes les boîtes de portes, le paramètre inject_noise_targets de generate_boxing_pass_manager() est l'option la plus concise. Ces utilitaires sont utiles pour des modifications sélectives par boîte que les paramètres du pass manager ne couvrent pas.
Construire un circuit
Chaque pass manager produit par generate_boxing_pass_manager() renvoie des circuits qui peuvent être transformés avec succès en une paire modèle/samplex par build(). Par exemple, le code suivant appelle la fonction build() sur un circuit produit par un pass manager d'encadrement.
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)
Pour garantir que tout circuit transpilé puisse être construit avec succès, les pass managers savent inclure des boîtes supplémentaires lorsque c'est nécessaire. Par exemple, considère le circuit ci-dessous, qui se termine par un qubit 0 non mesuré.
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)
Dessiner des boîtes habillées à gauche autour des portes et des mesures donnerait un circuit comportant des portes virtuelles non collectées sur le qubit 0, et appeler build() sur ce circuit provoquerait une erreur. Pour l'éviter, les pass managers renvoyés par generate_boxing_pass_manager() peuvent ajouter des boîtes habillées à droite qui jouent le rôle de collecteurs. Par exemple, dans le code suivant, le qubit 0 est terminé par une boîte habillée à droite qui récupère la porte virtuelle non collectée. Les portes à un qubit agissant sur le qubit 0 sont aussi placées à l'intérieur de la boîte afin de minimiser la profondeur du circuit obtenu.
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)
Dans un autre exemple, une boîte habillée à droite est ajoutée pour collecter les portes virtuelles qui resteraient sinon non collectées à cause des mesures non encadrées.
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)