Lavora con circuiti fermionici
I concetti di questa guida sono attualmente disponibili solo nell'API Python. Una funzionalità equivalente sarà resa disponibile nell'API C in una versione futura.
Questa guida spiega come usare il FermionicCircuit per implementare algoritmi quantistici direttamente nello spazio fermionico, invece di mappare prima sui qubit.
Perché usare i circuiti fermionici
Considera il flusso di lavoro tradizionale per implementare l'evoluzione temporale di un Hamiltoniano fermionico su un'architettura basata su qubit:
-
Definisci l'Hamiltoniano fermionico (ad esempio, ).
-
Mappalo su un operatore di qubit (ad esempio, usando la mappatura di Jordan-Wigner).
-
Implementa l'evoluzione temporale nello spazio dei qubit (usando la trotterizzazione o altri metodi).
Questo approccio ha un limite fondamentale: la mappatura anticipata sui qubit scarta la conoscenza del problema sulla struttura fermionica. I passi di ottimizzazione standard non possono sfruttare le simmetrie fermioniche né i riordinamenti consentiti dei termini, e producono circuiti subottimali con profondità e numero di gate inutilmente elevati.
Il FermionicCircuit consente un flusso di lavoro migliore:
-
Definisci l'Hamiltoniano fermionico (ad esempio, ).
-
Implementa l'evoluzione temporale direttamente nello spazio fermionico usando gate fermionici.
-
Esegui la mappatura sullo spazio dei qubit come parte del processo di transpilazione.
Questo approccio supera il limite mantenendo la conoscenza del problema nello spazio fermionico, dove i passi di ottimizzazione possono sfruttare la struttura fermionica e le relazioni di commutazione per ridurre la profondità del circuito. Il circuito fermionico descrive quale calcolo eseguire, mentre la transpilazione si occupa di come mapparlo sui qubit, offrendo una separazione più pulita delle responsabilità.
Indicizzazione generica dei modi
Sia il modulo operators sia il FermionicCircuit usano un'indicizzazione generica basata sui modi, che non fa alcuna ipotesi sulla natura dei modi. Un modo è semplicemente un indice astratto che etichetta un grado di libertà fermionico, senza alcuna semantica intrinseca. Questa scelta di progettazione rispecchia il modulo operators e garantisce la massima flessibilità. Ad esempio, puoi implementare l'evoluzione temporale per qualsiasi operatore che implementi il protocollo OperatorTrait, indipendentemente dalla sua rappresentazione matematica.
Costruire un circuito fermionico
L'esempio seguente implementa l'evoluzione temporale descritta nella sezione precedente. Costruisce un FermionicCircuit specificando il numero di modi fermionici, poi aggiunge i gate fermionici di qiskit_fermions.circuit.library per implementare l'evoluzione temporale.
L'esempio mostra anche come incorporare la conoscenza del dominio in un operatore usando il raggruppamento dei termini dell'operatore. Assegnando indici di gruppo ai termini dell'Hamiltoniano, preservi informazioni strutturali che i passi di ottimizzazione possono sfruttare lungo tutto lo stack di transpilazione.
>>> 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)
Nota come il raggruppamento dei termini dell'operatore venga preservato anche in operazioni semplici come la scomposizione. Questo mostra come l'informazione strutturale fluisca attraverso lo stack del circuito. Per comprendere la transpilazione completa sui qubit, consulta la guida Transpilazione dei circuiti fermionici.
Tenere separati i gate con una barriera
I passi di ottimizzazione fondono i gate adiacenti quando possibile. Ad esempio, MergeOrbitalRotations combina una sequenza di gate OrbitalRotation consecutivi in uno solo, e MergeSlaterDeterminantPreparation incorpora un InitializeModes nella rotazione che lo segue.
Usa FermionicCircuit.barrier() quando vuoi impedirlo. Una barriera non ha alcun effetto unitario, quindi non modifica lo stato preparato dal tuo circuito; segna soltanto un punto oltre il quale i passi non fondono i gate. Chiamala senza argomenti per estenderla a tutti i modi, oppure passa i modi a cui limitarla:
>>> 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)
Le barriere sopravvivono alla transpilazione da fermioni a qubit, quindi vincolano anche l'ottimizzazione a livello di qubit che segue. La guida Transpilazione dei circuiti fermionici tratta quella fase.
Ripetere un circuito
Molti algoritmi applicano molte volte lo stesso blocco di gate. L'evoluzione temporale è il caso più comune: un passo di Trotter viene costruito una volta e poi ripetuto per raggiungere il tempo desiderato. Usa repeat() per costruire il circuito ripetuto e compose() per unirlo a un prefisso di preparazione dello stato.
L'esempio seguente costruisce un passo di Trotter del secondo ordine per un modello di Fermi-Hubbard. Il passo è simmetrico: divide il termine di hopping in due gate OrbitalRotation di mezza durata che racchiudono l'interazione on-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)
Ripetere il passo e comporlo su un riferimento di Hartree-Fock restituisce l'evoluzione completa:
>>> 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
Le ripetizioni vengono appiattite nel circuito restituito invece di essere racchiuse in un unico gate, quindi count_ops() conta due rotazioni per passo. Questo appiattimento rende visibile al Transpiler la ridondanza ai confini tra i passi: la rotazione finale di mezza durata di un passo e quella iniziale del successivo sono adiacenti, quindi MergeOrbitalRotations le combina in una sola rotazione.
>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import MergeOrbitalRotations
>>>
>>> merged = FermionicPassManager([MergeOrbitalRotations()]).run(evolution)
>>> merged.count_ops()["OrbitalRotation"]
4
Il risultato costa una rotazione per passo più una, ed è ciò che si ottiene fondendo a mano quei mezzi passi. Scrivere il passo nella sua forma simmetrica naturale e ripeterlo produce lo stesso circuito, quindi la fusione è lasciata alla fase di ottimizzazione.
Quando ti serve mantenere separate le ripetizioni, passa insert_barriers=True per inserire una barrier() tra di esse. I passi di ottimizzazione non fondono i gate attraverso una barriera, quindi ogni rotazione sopravvive:
>>> separated = trotter_step.repeat(reps, insert_barriers=True)
>>> FermionicPassManager([MergeOrbitalRotations()]).run(separated).count_ops()[
... "OrbitalRotation"
... ]
6
L'argomento reps di FermionicSuzukiTrotter è un concetto diverso. Suddivide un singolo gate Evolution in fattori più numerosi e più brevi, mentre repeat() duplica interi circuiti. Ripetere un circuito che contiene un solo Evolution ne moltiplica soltanto il tempo totale di evoluzione.
Misurare i modi fermionici
measure_all() legge ogni modo in un bit classico dedicato, associando il modo i al bit i. Usa measure() con il tuo ClassicalRegister per leggerne solo alcuni.
>>> measured = trotter_step.measure_all(inplace=False)
>>> measured.count_ops()["measure"]
4
Passare inplace=False restituisce una copia misurata e lascia intatto l'originale. Questo conta perché una misura non è unitaria: un circuito che ne contiene una non può più essere applicato a un vettore di stato, quindi conserva un circuito non misurato se vuoi anche simularlo.
Misurare qui, anziché sul QuantumCircuit transpilato, è ciò che mantiene facilmente leggibili le bitstring campionate. Una misura fermionica indica il modo che legge, quindi ogni fase che sposta i modi (RelabelModes a livello fermionico, layout e routing a livello di qubit) porta con sé la misura, e la bitstring resta indicizzata dal modo che hai richiesto.
A differenza di measure_all(), non viene inserita alcuna barrier() prima delle misure. Chiama prima tu stesso barrier() se ne vuoi una in quel punto.
Se un bit misurato significhi ciò che ti aspetti dipende dalla codifica da fermioni a qubit, che viene scelta durante la transpilazione e non qui. In una codifica nella base di occupazione come Jordan-Wigner ogni modo è portato da un qubit e il bit è l'occupazione di quel modo; vedi FermionicMeasure.
Transpilare i circuiti fermionici
Per implementare l'algoritmo quantistico rappresentato dal tuo FermionicCircuit devi transpilarlo in un QuantumCircuit. Puoi scoprire come farlo nella guida Transpilazione dei circuiti fermionici.
Passaggi successivi
-
Scopri i gate fermionici disponibili nella documentazione di
qiskit_fermions.circuit.library. -
Esplora la guida esplicativa sugli operatori per capire come costruire Hamiltoniani fermionici da usare con i circuiti fermionici.
-
Consulta la guida introduttiva a SQDRIFT per un esempio pratico end-to-end.
-
Leggi la guida sul rapporto con ffsim per capire come un
FermionicCircuitpossa essere simulato direttamente, e come questi indici di modo generici assumano una semantica di spin solo al momento della simulazione.

