Trasformare un approccio LUCJ
I concetti descritti in questa guida sono disponibili solo nell’API di Python.
L'approccio del cluster unitario locale di Jastrow ( LUCJ ) è una parametrizzazione compatta ed efficiente dal punto di vista hardware di una funzione d'onda elettronica correlata. Appartiene alla famiglia più ampia degli aggregati unitari di Jastrow (UCJ) e assume la forma
dove è uno stato di riferimento (tipicamente Hartree-Fock), ogni rappresenta una rotazione orbitale e ogni è un operatore di Coulomb diagonale
con l'operatore numerico sull'orbitale spaziale con spin .
ffsim genera l'operatore ansatz; questo pacchetto lo trasforma in un circuito e lo implementa sui qubit. Questa guida riguarda la seconda parte di quella frase. Il motivo per cui vale la pena fare questo passaggio è proprio il lowering stesso: i gate Qiskit di ffsim sono specifici per la trasformazione di Jordan-Wigner, mentre a non prevede FermionicCircuit alcuna ipotesi sulla codifica da fermioni a qubit, quindi lo stesso ansatz può essere sintetizzato tramite qualsiasi codifica adatta al dispositivo di destinazione.
L'operatore di ansatz si presenta quindi qui già pronto, e tutto ciò che segue descrive ciò che accade in seguito: in cosa si scompone il gate, come viene abbassato tramite il metodo di Jordan-Wigner, come viene abbinato a una mappa di accoppiamento del dispositivo e cosa sarebbe ancora necessario in caso di un abbassamento non Jordan-Wigner. Per quanto riguarda la creazione, la parametrizzazione o la simulazione di un modello UCJ, si rimanda innanzitutto alle guide fornite da ffsim.
La guida alle relazioni ffsim, che illustra come i due pacchetti si ripartiscono il lavoro, e la guida alla transpilazione, che descrive le fasi della pipeline utilizzate di seguito.
1. Scarica un operatore LUCJ da ffsim
L'ansatz viene inizializzato a partire dalle ampiezze ottenute da un calcolo CCSD (Coupled-Cluster Singles and Doubles). Eseguire il metodo Hartree-Fock con restrizioni seguito dal metodo CCSD per una molecola di idrogeno nella base 6-31g , utilizzando PySCF per la chimica quantistica, quindi trasferire le ampiezze a ffsim from_t_amplitudes(). Esegue una doppia fattorizzazione delle ampiezze per ottenere le matrici di Coulomb diagonali per ogni strato e le rotazioni orbitali, e ricava una rotazione orbitale finale opzionale dalle ampiezze . Il numero di ripetizioni è dato dal risultato di tale fattorizzazione; lo n_reps tronca, sacrificando la precisione a favore di un circuito meno complesso.
>>> import ffsim
>>> import pyscf
>>> import pyscf.cc
>>>
>>> # build the molecule and run Hartree-Fock
>>> mol = pyscf.gto.Mole()
>>> mol.build(
... atom=[["H", (0, 0, 0)], ["H", (0, 0, 0.74)]],
... basis="6-31g",
... symmetry="Dooh",
... verbose=0,
... )
<pyscf.gto.mole.Mole object at ...>
>>> scf = pyscf.scf.RHF(mol).run()
>>>
>>> norb = scf.mo_coeff.shape[1]
>>> nelec = (mol.nelec[0], mol.nelec[1])
>>>
>>> # run CCSD for the t-amplitudes, then factorize them into an ansatz operator
>>> ccsd = pyscf.cc.CCSD(scf).run()
>>> t1, t2 = ccsd.t1, ccsd.t2
>>>
>>> ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(t2, t1=t1, n_reps=2)È qui che l’ansatz smette di essere una questione di interesse per ffsim. ffsim offre qui delle opzioni che questo pacchetto non rileva né ha bisogno di conoscere, come la fattorizzazione ottimizzata variazionalmente (“compressa”) alla base di optimize=True, o il raggruppamento dei vettori di parametri gestito da un ottimizzatore variazionale. Tutti generano lo stesso tipo di operatore e tutto ciò che segue funziona senza modifiche su ciascuno di essi.
2. Trasformare l'operatore in un circuito fermionico
Il gate UCJ avvolge l'operatore e lo rappresenta come un circuito su modi fermionici. Il gate è un'entità puramente unitaria che non porta con sé alcun riferimento proprio; pertanto, è necessario anteporre un InitializeModes gate (costruito con from_hartree_fock()) per fornire il riferimento Hartree-Fock a cui viene applicato l'ansatz.
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, UCJ
>>>
>>> ansatz = UCJ(ucj_op)
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(ansatz, circuit.modes)La scomposizione del circuito ne rivela l'anatomia, ed è proprio su questa rappresentazione che si basa il resto della guida. Il gate InitializeModes prepara il determinante di riferimento, e ogni strato di ansatz contribuisce con un OrbitalRotation , poi (un elemento della Evolution diagonale dell’operatore di Coulomb ), quindi , con un finale OrbitalRotation alla fine. Le rotazioni orbitali agiscono per settore di spin, quindi ciascuna viene applicata alle modalità alfa e 0..norb alle modalità beta in modo indipendente norb..2*norb .
>>> circuit.decompose().draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Ogni porta in quella scomposizione è ancora fermionica: OrbitalRotation e Evolution sono definiti sui modi, e non è ancora comparso alcun qubit né operatore di Pauli. È questo che lascia aperta la codifica, ed è lo stato in cui l’ansatz rimane fino a quando la fase di sintesi non ne sceglie uno.
Ogni livello termina con “ ” e quello successivo inizia con “ ”, quindi i gate OrbitalRotation adiacenti potrebbero essere uniti in un’unica rotazione. È previsto per il futuro un passaggio di transpilazione che effettui questa fusione.
3. Scegliere una codifica da fermioni a qubit
Il circuito è ancora fermionico, ed è proprio questo il punto in cui questo pacchetto si guadagna un posto nel flusso di lavoro. A non comporta FermionicCircuit alcuna ipotesi su come le modalità si trasformino in qubit, pertanto la codifica è una scelta che riguarda la fase di sintesi piuttosto che un elemento intrinseco all'ansatz. Questo è ciò che il presente pacchetto aggiunge rispetto ai gate Qiskit di ffsim, che sono specifici per la trasformazione di Jordan-Wigner.
Per il solo Jordan-Wigner non servirebbe questa deviazione: ffsim e UCJOpSpinBalancedJW i suoi derivati, che da soli trasformano un operatore UCJ in un circuito di qubit. Il motivo per cui si fa passare l’ansatz attraverso un circuito fermionico è tutto ciò che una codifica può offrire in più. Le codifiche locali, ad esempio, impiegano qubit aggiuntivi per vincolare il peso di Pauli di ciascun termine, cosa che le guide di flusso “ 1D ” e “ 2D ” utilizzano per rendere la profondità a due qubit di un passo di Trotter indipendente dalle dimensioni del sistema.
Per quanto riguarda nello specifico l'approccio UCJ, la fase di sintesi deve coprire due tipi di gate:
- L'evoluzione di Coulomb in diagonale.
Evolutionè già indipendente dalla codifica:MapperFnEvolutionSynthesisaccetta una funzione di mappatura, quindi i livelli si applicano a qualsiasi codifica che si possa esprimere come tale. Le guide del set di flusso spiegano come crearne uno. - Tutto il resto. I plugin per
OrbitalRotation,InitializeModese nonPrepareSlaterDeterminantsono parametrizzati in base a una codifica; ciascuno di essi specifica l’ipotesi di una codifica basata sulle occupazioni con una mappatura da modalità a qubit di tipo “ 1-to-1 ” ed emette porte direttamente su tale base. Per ciascuna di queste tre codifiche è necessario un plugin specifico, ma al momento non ne è ancora disponibile nessuno.
Come traspilare i circuiti UCJ con una codifica diversa da quella di Jordan-Wigner è una questione di ricerca ancora aperta e questo pacchetto fornisce il framework all’interno del quale sviluppare tale pipeline. Il resto di questa guida illustra come costruire la pipeline del transpiler per la codifica di Jordan-Wigner, equivalente a quella prodotta da generate_preset_jw_pass_manager(), a titolo di esempio.
4. Abbassarlo tramite Jordan-Wigner
Il processo di lowering avviene in F2QSynthesis, che funziona come in Qiskit HighLevelSynthesis: percorre le istruzioni fermioniche e le passa una per una a un plugin registrato per quel nome di gate. Il suo attributo F2QSynthesis.methods ne contiene uno per F2QSynthesisPlugin ogni nome di gate, quindi costruire lo stage significa scegliere un plugin per ogni gate fermionico contenuto nel circuito:
>>> from qiskit_fermions.mappers.library import jordan_wigner
>>> from qiskit_fermions.transpiler.passes import F2QSynthesis
>>> from qiskit_fermions.transpiler.passes.synthesis import (
... GivensDecompositionOrbitalRotationSynthesis,
... GivensDecompositionSlaterDeterminantSynthesis,
... MapperFnEvolutionSynthesis,
... TrivialOccupationInitializeModesSynthesis,
... )
>>>
>>> synthesis = F2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(jordan_wigner)
>>> synthesis.methods["InitializeModes"] = TrivialOccupationInitializeModesSynthesis()
>>> synthesis.methods["OrbitalRotation"] = GivensDecompositionOrbitalRotationSynthesis()
>>> synthesis.methods["PrepareSlaterDeterminant"] = (
... GivensDecompositionSlaterDeterminantSynthesis()
... )Assegnare istanze in questo modo permette anche a un plugin scritto da te di entrare nella pipeline, senza doverlo prima registrare da nessuna parte.
È qui che viene definita la codifica, nei due modi descritti nella sezione 3: in modo esplicito, passando jordan_wigner() a MapperFnEvolutionSynthesis, e in modo implicito, poiché gli altri tre plugin la presuppongono già.
Quella fase si inserisce quindi in un processo le cui MultiStagePassManager fasi si susseguono in un ordine prestabilito:
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.transpiler import generate_preset_pass_manager
>>>
>>> from qiskit_fermions.transpiler import (
... FermionicCircuitToDAG,
... FermionicPassManager,
... QuantumDAGToCircuit,
... )
>>> from qiskit_fermions.transpiler.passes import (
... MergeOrbitalRotations,
... MergeSlaterDeterminantPreparation,
... TrivialF2QLayout,
... )
>>>
>>> pm = MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... optimization=FermionicPassManager(
... [MergeOrbitalRotations(), MergeSlaterDeterminantPreparation()]
... ),
... layout=FermionicPassManager(TrivialF2QLayout()),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(),
... output=QuantumDAGToCircuit(),
... )La fase di ottimizzazione viene eseguita prima della sintesi, sul circuito fermionico, ed è proprio questo che la rende utile: riscrive le porte mentre sono ancora fermioniche e indipendenti dalla codifica, quindi i risparmi si trasferiscono a qualsiasi codifica successiva. MergeOrbitalRotations raggruppa ogni sequenza di porte OrbitalRotation consecutive in una sola, il che è rilevante per un ansatz UCJ poiché ogni strato termina con e quello successivo inizia con . Successivamente MergeSlaterDeterminantPreparation , fonde la porta iniziale e InitializeModes la rotazione che la segue in un’unica operazione PrepareSlaterDeterminant, che la sintesi riduce utilizzando la decomposizione di Slater ridotta, più economica, anziché una rotazione quadrata completa. L'ordine è importante: unendo prima le rotazioni, la singola rotazione risulta visibile immediatamente dopo l'inizializzazione, e la fusione di Slater la contrae poi al suo interno.
I lavori rimanenti riguardano l'impianto idraulico. input e effettua output la conversione tra circuiti e DAG, layout assegna le modalità fermioniche ai qubit (TrivialF2QLayout mappa una modalità i a un qubit i) ed è qubit un normale gestore di passaggi di Qiskit che subentra quando non rimangono più elementi fermionici.
Per eseguirlo è necessario un passaggio preparatorio: il gate UCJ composito deve essere prima scomposto nei suoi gate primitivi, in modo che la fase di ottimizzazione possa individuare le singole rotazioni ed evoluzioni.
>>> transpiled = pm.run(circuit.decompose())
>>> print(dict(sorted(transpiled.count_ops().items())))
{'p': 14, 'rzz': 12, 'x': 2, 'xx_plus_yy': 28}In assenza di un dispositivo di destinazione, ciò si traduce in 2 * norb qubit con connettività "all-to-all"; le rotazioni orbitali si sintetizzano in XXPlusYYGateoggetti e le evoluzioni di Coulomb diagonali in RZZGateoggetti:
>>> transpiled.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Questa pipeline è esattamente ciò che generate_preset_jw_pass_manager() assembla, quindi in pratica usa il preset e crea le fasi manualmente quando devi modificarne una:
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> preset = generate_preset_jw_pass_manager()
>>> preset.run(circuit.decompose()).count_ops() == transpiled.count_ops()
True5. Adattare la topologia del dispositivo
Un dispositivo reale presenta una mappa di accoppiamento dei qubit limitata, e l'ansatz LUCJ è progettato per adattarsi ad essa. Le interazioni di stesso spin (pairs_aa) formano due catene lineari, mentre le interazioni alfa-beta (pairs_ab) le collegano tra loro. ffsim crea generate_lucj_pass_manager() una pipeline di qubit adatta al dispositivo per questa struttura e restituisce il sottoinsieme che pairs_ab l'hardware è effettivamente in grado di supportare. Inserire quella pipeline nella qubit fase del preset, mantenendo la sintesi da fermioni a qubit propria del pacchetto:
>>> from ffsim.qiskit import generate_lucj_pass_manager
>>> from qiskit.providers.fake_provider import GenericBackendV2
>>> from qiskit.transpiler import CouplingMap
>>>
>>> # a heavy-hex device coupling map (any BackendV2 works, e.g. a real fake_provider backend)
>>> coupling_map = CouplingMap.from_heavy_hex(5)
>>> backend = GenericBackendV2(
... num_qubits=coupling_map.size(),
... basis_gates=["cp", "xx_plus_yy", "p", "x", "swap"],
... coupling_map=coupling_map,
... )
>>>
>>> # nearest-neighbor same-spin chain; let the pass manager choose the alpha-beta pairs
>>> pairs_aa = [(p, p + 1) for p in range(norb - 1)]
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> pm.qubit, allowed_pairs_ab = generate_lucj_pass_manager(
... backend, norb, "heavy-hex", (pairs_aa, None), optimization_level=3, seed_transpiler=0
... )
>>>
>>> # the alpha-beta interactions the heavy-hex connectivity can implement
>>> print(allowed_pairs_ab)
[(0, 0)]Il punto sta proprio in questa composizione: la fase qubit è compatibile con il dispositivo, mentre la fase di sintesi da fermioni a qubit che la precede rimane propria di questo pacchetto e, pertanto, sostituibile.
Una volta pm.qubit impostata la pipeline sensibile al dispositivo, l'esecuzione del pass manager dispone il circuito sui qubit del backend e lo instrada verso la mappa di accoppiamento. Per l'ansatz senza restrizioni del passaggio 1, il cui operatore di Coulomb diagonale contiene ancora termini alfa-beta che l'hardware non può raggiungere direttamente, il router deve inserire numerose porte SWAP logiche per colmare tale distanza:
>>> naive = pm.run(circuit.decompose())
>>> naive.num_qubits # laid out on the full heavy-hex device register
57
>>> naive_swaps = naive.count_ops()["swap"]
>>> naive_swaps # many SWAPs to bridge the unreachable alpha-beta interactions
33Limitare l'approccio alle interazioni implementabili a livello hardware
La soluzione consiste nel reintrodurre allowed_pairs_ab il tutto nella costruzione dell'ansatz, tramite l'argomento interaction_pairs di from_t_amplitudes(), in modo che l'operatore di Coulomb diagonale contenga solo termini alfa-beta che la mappa di accoppiamento possa implementare direttamente. L'ansatz si adatta quindi alla topologia del dispositivo e il router deve intervenire solo in misura minima:
>>> restricted = UCJ(
... ffsim.UCJOpSpinBalanced.from_t_amplitudes(
... t2, t1=t1, n_reps=2, interaction_pairs=(pairs_aa, allowed_pairs_ab)
... )
... )
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(restricted, circuit.modes)
>>>
>>> transpiled = pm.run(circuit.decompose())
>>> restricted_swaps = transpiled.count_ops()["swap"]
>>> restricted_swaps # far fewer routing SWAPs than the unrestricted ansatz
4Disegnando solo i qubit attivi (idle_wires=False) si ottiene il circuito limitato alle due catene di spin e al ponte alfa-beta, espresso in termini di porte nella base del dispositivo. Il layout e il routing distribuiscono le modalità logiche tra i qubit fisici del dispositivo; pertanto, passa un elemento tratto wire_order dal layout finale del circuito (final_index_layout() che elenca il qubit fisico su cui è terminato ciascun qubit di ingresso, nell'ordine dei qubit di ingresso) per ridisegnare i collegamenti nell'ordine originale delle modalità:
>>> wire_order = transpiled.layout.final_index_layout(filter_ancillas=False)
>>> transpiled.draw("mpl", idle_wires=False, fold=-1, wire_order=wire_order)
<Figure size ... with 1 Axes>
Il numero esatto di gate e la profondità post-layout dipendono dalle fasi di ottimizzazione del routing /opt e dal dispositivo scelto, pertanto non vengono riportati in questa sede. Il punto chiave è la progettazione congiunta; esprimere l'ansatz tramite una pairs_aa catena dei vicini più prossimi e pairs_ab ponti filtrati dall'hardware permette di mantenere il circuito sintetizzato vicino alla topologia del dispositivo, riducendo al minimo il sovraccarico di instradamento ( SWAP gate inseriti). Si veda GivensDecompositionSlaterDeterminantSynthesis per una scelta di sintesi correlata (minimize_2q_gate_count), che mette a confronto il numero di gate a due qubit con la profondità del percorso.
Passi successivi
- Scopri come funzionano i singoli gate nella documentazione
qiskit_fermions.circuit.librarye nella guida ai circuiti fermionici. - Consulta la guida esplicativa sugli operatori per capire come costruire direttamente gli operatori fermionici e gli hamiltoniani.
- Scopri come un circuito fermionico viene mappato sui qubit nella guida alla transpilazione.
- Leggi la documentazione
F2QSynthesisper scoprire l'altro metodo di configurazione della fase di sintesi: unconfigdizionario che seleziona i plugin in base ai nomi abbreviati registrati sotto i rispettivi punti di ingressoqiskit_fermions.transpiler.synthesis, anziché in base alle istanze. - Leggi la guida alle interazioni di ffsim per capire come i due pacchetti si suddividono il lavoro e quali protocolli consentono a ffsim di simulare i circuiti di questo pacchetto.