Rétroaction classique et flux de contrôle (circuits dynamiques)
Package versions
Le code de cette page a été développé avec les dépendances suivantes. Nous recommandons d'utiliser ces versions ou des versions plus récentes.
qiskit[all]~=2.4.0
Les circuits dynamiques sont des outils puissants qui permettent de mesurer des qubits au milieu de l'exécution d'un circuit quantique, puis d'effectuer des opérations logiques classiques au sein du circuit en fonction du résultat de ces mesures intermédiaires. Ce processus est également appelé rétroaction classique. Bien que nous en soyons encore aux premières étapes de la compréhension de la meilleure façon de tirer parti des circuits dynamiques, la communauté de recherche quantique a déjà identifié plusieurs cas d'usage, notamment les suivants :
- Préparation efficace d'états quantiques, comme l'état GHZ, l'état W (pour en savoir plus sur l'état W, voir aussi « State preparation by shallow circuits using feed forward »), et une large classe d'états produits matriciels
- Intrication longue portée efficace entre qubits d'une même puce en utilisant des circuits peu profonds
- Échantillonnage efficace de circuits de type IQP
Qiskit prend en charge quatre constructions de flux de contrôle pour la rétroaction classique, chacune implémentée en tant que méthode sur QuantumCircuit. Les constructions et leurs méthodes correspondantes sont :
- Instruction if -
QuantumCircuit.if_test - Instruction switch -
QuantumCircuit.switch - Boucle for -
QuantumCircuit.for_loop - Boucle while -
QuantumCircuit.while_loop
Chacune de ces méthodes retourne un gestionnaire de contexte et est généralement utilisée dans une instruction with. La suite de ce guide explique chacune de ces constructions et comment les utiliser.
Certaines limitations des opérations de rétroaction classique et de flux de contrôle sur le matériel quantique peuvent impacter ton programme. Pour en savoir plus, voir Exécuter des circuits dynamiques.
Instruction if
L'instruction if permet d'effectuer des opérations de manière conditionnelle en fonction de la valeur d'un bit ou d'un registre classique.
Dans l'exemple ci-dessous, on applique une porte Hadamard à un qubit et on le mesure. Si le résultat est 1, on applique une porte X sur le qubit, ce qui a pour effet de le faire revenir à l'état 0. On mesure ensuite le qubit à nouveau. Le résultat de la mesure devrait être 0 avec une probabilité de 100 %.
# Added by doQumentation — required packages for this notebook
!pip install -q qiskit
from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
qubits = QuantumRegister(1)
clbits = ClassicalRegister(1)
circuit = QuantumCircuit(qubits, clbits)
(q0,) = qubits
(c0,) = clbits
circuit.h(q0)
circuit.measure(q0, c0)
with circuit.if_test((c0, 1)):
circuit.x(q0)
circuit.measure(q0, c0)
circuit.draw("mpl")
# example output counts: {'0': 1024}
L'instruction with peut recevoir une cible d'affectation qui est elle-même un gestionnaire de contexte pouvant être sauvegardé et utilisé ultérieurement pour créer un bloc else, exécuté à chaque fois que le contenu du bloc if n'est pas exécuté.
Dans l'exemple ci-dessous, on initialise des registres avec deux qubits et deux bits classiques. On applique une porte Hadamard au premier qubit et on le mesure. Si le résultat est 1, on applique une porte Hadamard sur le second qubit ; sinon, on lui applique une porte X. Enfin, on mesure également le second qubit.
qubits = QuantumRegister(2)
clbits = ClassicalRegister(2)
circuit = QuantumCircuit(qubits, clbits)
(q0, q1) = qubits
(c0, c1) = clbits
circuit.h(q0)
circuit.measure(q0, c0)
with circuit.if_test((c0, 1)) as else_:
circuit.h(q1)
with else_:
circuit.x(q1)
circuit.measure(q1, c1)
circuit.draw("mpl")
# example output counts: {'01': 260, '11': 272, '10': 492}
En plus de conditionner sur un seul bit classique, il est également possible de conditionner sur la valeur d'un registre classique composé de plusieurs bits.
Dans l'exemple ci-dessous, on applique des portes Hadamard à deux qubits et on les mesure. Si le résultat est 01, c'est-à-dire que le premier qubit est 1 et le second est 0, on applique une porte X à un troisième qubit. Enfin, on mesure le troisième qubit. À noter que par souci de clarté, nous avons choisi de préciser l'état du troisième bit classique, qui est 0, dans la condition if. Dans le dessin du circuit, la condition est indiquée par des cercles sur les bits classiques testés. Un cercle plein indique un conditionnement sur 1, tandis qu'un cercle vide indique un conditionnement sur 0.
qubits = QuantumRegister(3)
clbits = ClassicalRegister(3)
circuit = QuantumCircuit(qubits, clbits)
(q0, q1, q2) = qubits
(c0, c1, c2) = clbits
circuit.h([q0, q1])
circuit.measure(q0, c0)
circuit.measure(q1, c1)
with circuit.if_test((clbits, 0b001)):
circuit.x(q2)
circuit.measure(q2, c2)
circuit.draw("mpl")
# example output counts: {'101': 269, '011': 260, '000': 252, '010': 243}
Instruction switch
L'instruction switch permet de sélectionner des actions en fonction de la valeur d'un bit ou d'un registre classique. Elle est similaire à une instruction if, mais tu peux spécifier davantage de cas pour la logique de branchement. L'exemple ci-dessous applique une porte Hadamard à un qubit et le mesure. Si le résultat est 0, on applique une porte X sur le qubit, et si le résultat est 1, on applique une porte Z. Le résultat de la mesure devrait être 1 avec une probabilité de 100 %.
qubits = QuantumRegister(1)
clbits = ClassicalRegister(1)
circuit = QuantumCircuit(qubits, clbits)
(q0,) = qubits
(c0,) = clbits
circuit.h(q0)
circuit.measure(q0, c0)
with circuit.switch(c0) as case:
with case(0):
circuit.x(q0)
with case(1):
circuit.z(q0)
circuit.measure(q0, c0)
circuit.draw("mpl")
# example output counts: {'1': 1024}
Puisque l'exemple ci-dessus utilisait un seul bit classique, il n'y avait que deux cas possibles, ce qui aurait pu être obtenu avec une instruction if-else. L'instruction switch est surtout utile pour brancher sur la valeur d'un registre classique composé de plusieurs bits. L'exemple suivant montre comment construire un cas par défaut, exécuté si aucun des cas précédents ne correspond. À noter que dans une instruction switch, un seul bloc est exécuté à la fois. Il n'y a pas de fallthrough.
L'exemple ci-dessous applique des portes Hadamard à deux qubits et les mesure. Si le résultat est 00 ou 11, on applique une porte Z au troisième qubit. Si le résultat est 01, on applique une porte Y. Si aucun des cas précédents ne correspond, on applique une porte X. Enfin, on mesure le troisième qubit.
qubits = QuantumRegister(3)
clbits = ClassicalRegister(3)
circuit = QuantumCircuit(qubits, clbits)
(q0, q1, q2) = qubits
(c0, c1, c2) = clbits
circuit.h([q0, q1])
circuit.measure(q0, c0)
circuit.measure(q1, c1)
with circuit.switch(clbits) as case:
with case(0b000, 0b011):
circuit.z(q2)
with case(0b001):
circuit.y(q2)
with case(case.DEFAULT):
circuit.x(q2)
circuit.measure(q2, c2)
circuit.draw("mpl")
# example output counts: {'101': 267, '110': 249, '011': 265, '000': 243}
Boucle for
Une boucle for permet d'itérer sur une séquence de valeurs classiques et d'effectuer des opérations à chaque itération.
L'exemple suivant utilise une boucle for pour appliquer 5 portes X à un qubit, puis le mesure. Comme il effectue un nombre impair de portes X, l'effet global est de faire passer le qubit de l'état 0 à l'état 1.
qubits = QuantumRegister(1)
clbits = ClassicalRegister(1)
circuit = QuantumCircuit(qubits, clbits)
(q0,) = qubits
(c0,) = clbits
with circuit.for_loop(range(5)) as _:
circuit.x(q0)
circuit.measure(q0, c0)
circuit.draw("mpl")
# example output counts: {'1': 1024}
Boucle while
Une boucle while permet de répéter des instructions tant qu'une condition est satisfaite.
L'exemple ci-dessous applique des portes Hadamard à deux qubits et les mesure. Il crée ensuite une boucle while qui répète cette procédure tant que le résultat de la mesure est 11. Ainsi, la mesure finale ne devrait jamais être 11, les possibilités restantes apparaissant avec une fréquence approximativement égale.
qubits = QuantumRegister(2)
clbits = ClassicalRegister(2)
circuit = QuantumCircuit(qubits, clbits)
q0, q1 = qubits
c0, c1 = clbits
circuit.h([q0, q1])
circuit.measure(q0, c0)
circuit.measure(q1, c1)
with circuit.while_loop((clbits, 0b11)):
circuit.h([q0, q1])
circuit.measure(q0, c0)
circuit.measure(q1, c1)
circuit.draw("mpl")
# example output counts: {'01': 334, '10': 368, '00': 322}
Expressions classiques
Le module d'expressions classiques de Qiskit qiskit.circuit.classical contient une représentation expérimentale des opérations sur les valeurs classiques lors de l'exécution du circuit.
L'exemple suivant montre comment utiliser le calcul de parité pour créer un état GHZ à n qubits avec des circuits dynamiques. On génère d'abord paires de Bell sur des qubits adjacents. Ensuite, on relie ces paires entre elles avec une couche de portes CNOT. On mesure ensuite le qubit cible de toutes les portes CNOT précédentes et on remet chaque qubit mesuré à l'état . On applique à chaque site non mesuré pour lequel la parité de tous les bits précédents est impaire. Enfin, des portes CNOT sont appliquées aux qubits mesurés pour rétablir l'intrication perdue lors de la mesure.
Dans le calcul de parité, le premier élément de l'expression construite consiste à élever l'objet Python mr[0] vers un nœud Value (lift sert à convertir des objets arbitraires en expressions classiques). Cette étape n'est pas nécessaire pour mr[1] et les éventuels registres classiques suivants, car ils sont des entrées de expr.bit_xor, et toute élévation nécessaire est effectuée automatiquement dans ces cas. De telles expressions peuvent être construites dans des boucles et d'autres structures.
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.classical import expr
num_qubits = 8
if num_qubits % 2 or num_qubits < 4:
raise ValueError("num_qubits must be an even integer ≥ 4")
meas_qubits = list(range(2, num_qubits, 2)) # qubits to measure and reset
qr = QuantumRegister(num_qubits, "qr")
mr = ClassicalRegister(len(meas_qubits), "m")
qc = QuantumCircuit(qr, mr)
# Create local Bell pairs
qc.reset(qr)
qc.h(qr[::2])
for ctrl in range(0, num_qubits, 2):
qc.cx(qr[ctrl], qr[ctrl + 1])
# Glue neighboring pairs
for ctrl in range(1, num_qubits - 1, 2):
qc.cx(qr[ctrl], qr[ctrl + 1])
# Measure boundary qubits between pairs,reset to 0
for k, q in enumerate(meas_qubits):
qc.measure(qr[q], mr[k])
qc.reset(qr[q])
# Parity-conditioned X corrections
# Each non-measured qubit gets flipped iff the parity (XOR) of all
# preceding measurement bits is 1
for tgt in range(num_qubits):
if tgt in meas_qubits: # skip measured qubits
continue
# all measurement registers whose physical qubit index < tgt
left_bits = [k for k, q in enumerate(meas_qubits) if q < tgt]
if not left_bits: # skip if list empty
continue
# build XOR-parity expression
parity = expr.lift(
mr[left_bits[0]]
) # lift the first bit to Value so it will be treated like a boolean.
for k in left_bits[1:]:
parity = expr.bit_xor(
mr[k], parity
) # calculate parity with all other bits
with qc.if_test(parity): # Add X if parity is 1
qc.x(qr[tgt])
# Re-entangle measured qubits
for ctrl in range(1, num_qubits - 1, 2):
qc.cx(qr[ctrl], qr[ctrl + 1])
qc.draw(output="mpl", style="iqp", idle_wires=False, fold=-1)
Store
Tu peux utiliser l'instruction store pour enregistrer le résultat d'une expression classique, si cette expression est utilisée de manière répétée. Les opérations sont automatiquement parallélisées, ce qui rend ton code nettement plus efficace à l'exécution.
Par exemple, il est plus naturel et plus efficace à l'exécution d'écrire , où , que . La première forme calcule la négation en une seule étape parallèle avant la chaîne XOR, au lieu d'évaluer chaque négation séquentiellement dans l'expression.
Exemple complet :
from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.classical import expr
qregs = QuantumRegister(4, "q")
creg = ClassicalRegister(3, "c")
# temp is a plain ClassicalRegister used as the store target
temp = ClassicalRegister(3, "temp")
qc = QuantumCircuit(qregs, creg, temp)
qc.h([0, 1, 2])
qc.measure([0, 1, 2], creg)
# Store bit-NOT of the full 3-bit register into temp
qc.store(temp, expr.bit_not(creg))
# Compute parity of temp using bit-indexed XOR
parity = expr.bit_xor(
expr.bit_xor(expr.index(temp, 0), expr.index(temp, 1)),
expr.index(temp, 2),
)
# Flip q3 if parity of ~creg is 1
with qc.if_test(parity):
qc.x(3)
qc.measure([0, 1, 2], creg)
qc.draw("mpl")
Prochaines étapes
- Apprends à implémenter un découplage dynamique précis en utilisant stretch.
- Utilise la visualisation du calendrier du circuit pour déboguer et optimiser tes circuits dynamiques.
- Exécute des circuits dynamiques.