Travailler avec des circuits fermioniques
Les concepts de ce guide sont actuellement disponibles uniquement dans l'API Python. Une fonctionnalité équivalente sera disponible dans l'API C dans une prochaine version.
Ce guide explique comment utiliser FermionicCircuit pour implémenter des algorithmes quantiques directement dans l'espace fermionique, au lieu de passer d'abord par un mapping vers des qubits.
Pourquoi utiliser des circuits fermioniques
Considère le workflow traditionnel pour implémenter l'évolution temporelle d'un hamiltonien fermionique sur une architecture à base de qubits :
-
Définis le hamiltonien fermionique (par exemple, ).
-
Mappe-le vers un opérateur de qubits (par exemple, avec le mapping de Jordan-Wigner).
-
Implémente l'évolution temporelle dans l'espace des qubits (avec la trotterisation ou d'autres méthodes).
Cette approche a une limite fondamentale : le mapping précoce vers des qubits fait perdre les connaissances propres au problème sur la structure fermionique. Les passes d'optimisation standard ne peuvent pas exploiter les symétries fermioniques ni les réordonnancements de termes autorisés, ce qui produit des circuits sous-optimaux avec des profondeurs et des nombres de portes inutilement élevés.
FermionicCircuit permet un meilleur workflow :
-
Définis le hamiltonien fermionique (par exemple, ).
-
Implémente l'évolution temporelle directement dans l'espace fermionique à l'aide de portes fermioniques.
-
Effectue le mapping vers l'espace des qubits dans le cadre du processus de transpilation.
Cette approche lève la limite en conservant les connaissances propres au problème dans l'espace fermionique, où les passes d'optimisation peuvent exploiter la structure fermionique et les relations de commutation pour réduire la profondeur du circuit. Le circuit fermionique décrit quel calcul effectuer, tandis que la transpilation gère comment le mapper vers des qubits, ce qui offre une séparation des responsabilités plus claire.
Indexation générique des modes
Le module operators comme FermionicCircuit utilisent une indexation générique par modes, qui ne fait aucune hypothèse sur la nature des modes. Un mode est simplement un indice abstrait qui désigne un degré de liberté fermionique, sans sémantique propre. Ce choix de conception reprend celui du module operators et garantit une flexibilité maximale. Par exemple, tu peux implémenter l'évolution temporelle de n'importe quel opérateur qui implémente le protocole OperatorTrait, quelle que soit sa représentation mathématique.
Construire un circuit fermionique
L'exemple ci-dessous implémente l'évolution temporelle décrite dans la section précédente. Il construit un FermionicCircuit en indiquant le nombre de modes fermioniques, puis ajoute des portes fermioniques issues de qiskit_fermions.circuit.library pour implémenter l'évolution temporelle.
L'exemple montre aussi comment intégrer des connaissances du domaine dans un opérateur grâce au regroupement des termes de l'opérateur. En attribuant des indices de groupe aux termes du hamiltonien, tu conserves une information structurelle que les passes d'optimisation peuvent exploiter dans toute la pile de transpilation.
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Create a circuit with 4 fermionic modes
>>> circuit = FermionicCircuit(4)
>>>
>>> # Define a simple fermionic Hamiltonian
>>> hamiltonian = FermionOperator.from_terms([
... ([cre(0), ann(2)], 0.5),
... ([cre(2), ann(0)], 0.5),
... ([cre(1), ann(3)], 0.5),
... ([cre(3), ann(1)], 0.5),
... ])
>>> # Add some problem structure by grouping our Hamiltonian terms
>>> hamiltonian.groups = [0, 0, 1, 1]
>>>
>>> # Add an evolution gate to implement exp(-i * t * H)
>>> evolution = Evolution(4, hamiltonian, time=1.0)
>>> circuit.append(evolution, circuit.register)
Remarque que le regroupement des termes de l'opérateur est préservé même lors d'opérations simples comme la décomposition. Cela montre comment l'information structurelle circule dans la pile de circuits. Pour comprendre la transpilation complète vers des qubits, consulte le guide Transpiler des circuits fermioniques.
Séparer des portes avec une barrière
Les passes d'optimisation fusionnent les portes voisines lorsque c'est possible. Par exemple, MergeOrbitalRotations combine une suite de portes OrbitalRotation consécutives en une seule, et MergeSlaterDeterminantPreparation intègre une porte InitializeModes dans la rotation qui la suit.
Utilise FermionicCircuit.barrier() quand tu veux l'empêcher. Une barrière n'a aucun effet unitaire, donc elle ne change pas l'état que prépare ton circuit ; elle marque seulement un point que les passes ne fusionnent pas. Appelle-la sans argument pour couvrir tous les modes, ou passe les modes auxquels la restreindre :
>>> from qiskit_fermions.circuit.library import OrbitalRotation
>>> import numpy as np
>>>
>>> rotations = FermionicCircuit(4)
>>> rotations.append(OrbitalRotation(np.eye(4, dtype=complex)), rotations.register)
>>> rotations.barrier()
>>> rotations.append(OrbitalRotation(np.eye(4, dtype=complex)), rotations.register)
Les barrières survivent à la transpilation fermion-vers-qubit, donc elles contraignent aussi l'optimisation au niveau des qubits qui suit. Le guide Transpiler des circuits fermioniques couvre cette étape.
Répéter un circuit
De nombreux algorithmes appliquent un même bloc de portes de nombreuses fois. L'évolution temporelle en est le cas courant : un pas de Trotter est construit une fois, puis répété pour atteindre le temps cible. Utilise repeat() pour construire le circuit répété, et compose() pour l'attacher à un préfixe de préparation d'état.
L'exemple ci-dessous construit un pas de Trotter d'ordre deux pour un modèle de Fermi-Hubbard. Le pas est symétrique : il sépare le terme de saut en deux portes OrbitalRotation de demi-durée qui encadrent l'interaction sur site.
>>> import scipy.linalg
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>>
>>> norb, reps, time = 2, 3, 1.0
>>> num_modes = 2 * norb
>>> dt = time / reps
>>>
>>> hopping = np.zeros((norb, norb))
>>> hopping[0, 1] = hopping[1, 0] = -1.0
>>> interaction = FermionOperator.from_dict(
... {(cre(p), ann(p), cre(p + norb), ann(p + norb)): 2.0 for p in range(norb)}
... )
>>>
>>> def rotation(duration):
... propagator = scipy.linalg.expm(-1j * duration * hopping)
... # the same rotation acts on the alpha and beta halves of the register
... return scipy.linalg.block_diag(propagator, propagator)
>>>
>>> trotter_step = FermionicCircuit(num_modes)
>>> trotter_step.append(OrbitalRotation(rotation(dt / 2)), trotter_step.modes)
>>> trotter_step.append(
... Evolution(num_modes, interaction, dt, atomic=True), trotter_step.modes
... )
>>> trotter_step.append(OrbitalRotation(rotation(dt / 2)), trotter_step.modes)
Répéter le pas et le composer sur une référence de Hartree-Fock donne l'évolution complète :
>>> evolution = FermionicCircuit(num_modes)
>>> evolution.append(InitializeModes([True, False, True, False]), evolution.modes)
>>> evolution.compose(trotter_step.repeat(reps), inplace=True)
>>> evolution.count_ops()["OrbitalRotation"]
6
Les répétitions sont aplaties dans le circuit retourné au lieu d'être enveloppées dans une seule porte, donc count_ops() compte deux rotations par pas. Cet aplatissement rend visible pour le transpileur la redondance aux frontières des pas : la rotation de demi-durée finale d'un pas et la rotation initiale du suivant sont adjacentes, donc MergeOrbitalRotations les combine en une seule rotation.
>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import MergeOrbitalRotations
>>>
>>> merged = FermionicPassManager([MergeOrbitalRotations()]).run(evolution)
>>> merged.count_ops()["OrbitalRotation"]
4
Le résultat coûte une rotation par pas plus une, ce qui correspond à ce que tu obtiens en fusionnant ces demi-pas à la main. Écrire le pas sous sa forme symétrique naturelle et le répéter produit le même circuit, donc la fusion est laissée à l'étape d'optimisation.
Quand tu as besoin de garder les répétitions séparées, passe insert_barriers=True pour placer une barrier() entre elles. Les passes d'optimisation ne fusionnent pas les portes de part et d'autre d'une barrière, donc chaque rotation subsiste :
>>> separated = trotter_step.repeat(reps, insert_barriers=True)
>>> FermionicPassManager([MergeOrbitalRotations()]).run(separated).count_ops()[
... "OrbitalRotation"
... ]
6
L'argument reps de FermionicSuzukiTrotter est un concept différent. Il subdivise une seule porte Evolution en facteurs plus nombreux et plus courts, alors que repeat() duplique des circuits entiers. Répéter un circuit qui contient une seule Evolution ne fait que multiplier son temps d'évolution total.
Mesurer des modes fermioniques
measure_all() lit chaque mode dans un bit classique distinct, en associant le mode i au bit i. Utilise measure() avec ton propre ClassicalRegister pour n'en lire que certains.
>>> measured = trotter_step.measure_all(inplace=False)
>>> measured.count_ops()["measure"]
4
Passer inplace=False retourne une copie mesurée et laisse l'original intact, ce qui compte car une mesure n'est pas unitaire : un circuit qui en contient une ne peut plus être appliqué à un vecteur d'état, donc garde un circuit non mesuré si tu veux aussi le simuler.
Mesurer ici, plutôt que sur le QuantumCircuit transpilé, permet de garder les chaînes de bits échantillonnées faciles à lire. Une mesure fermionique nomme le mode qu'elle lit, donc chaque étape qui déplace des modes (RelabelModes au niveau fermionique, placement et routage au niveau des qubits) emporte la mesure avec elle, et la chaîne de bits reste indexée par le mode que tu as demandé.
Contrairement à measure_all(), aucune barrier() n'est insérée avant les mesures. Appelle d'abord barrier() toi-même si tu en veux une à cet endroit.
Que le bit mesuré signifie ce que tu attends dépend de l'encodage fermion-vers-qubit, qui est choisi pendant la transpilation et non ici. Avec un encodage en base d'occupation comme Jordan-Wigner, chaque mode est porté par un qubit et le bit est l'occupation de ce mode ; consulte FermionicMeasure.
Transpiler des circuits fermioniques
Pour implémenter l'algorithme quantique représenté par ton FermionicCircuit, il doit être transpilé en un QuantumCircuit. Tu peux apprendre comment faire dans le guide Transpiler des circuits fermioniques.
Étapes suivantes
-
Découvre les portes fermioniques disponibles dans la documentation de
qiskit_fermions.circuit.library. -
Explore le guide d'explication des opérateurs pour comprendre comment construire des hamiltoniens fermioniques que tu peux utiliser avec des circuits fermioniques.
-
Consulte le guide de prise en main SQDRIFT pour un exemple pratique de bout en bout.
-
Lis le guide sur la relation avec ffsim pour comprendre comment un
FermionicCircuitpeut être simulé directement, et comment ces indices de modes génériques ne prennent une sémantique de spin qu'au moment de la simulation.

