Transpilare un ansatz LUCJ
I concetti di questa guida sono disponibili solo nell'API Python.
L'ansatz local unitary cluster Jastrow (LUCJ) è una parametrizzazione compatta e hardware-efficient di una funzione d'onda elettronica correlata. È un membro della più generale famiglia unitary cluster Jastrow (UCJ) e ha la forma
dove è uno stato di riferimento (tipicamente Hartree-Fock), ogni è una rotazione orbitale e ogni è un operatore di Coulomb diagonale
con l'operatore numero sull'orbitale spaziale con spin .
ffsim costruisce l'operatore ansatz; questo pacchetto lo trasforma in un circuito e porta quel circuito sui qubit. Questa guida riguarda la seconda metà di quella frase. Il motivo per fare il viaggio è la discesa di livello in sé: i gate Qiskit di ffsim sono specifici della trasformazione di Jordan-Wigner, mentre un FermionicCircuit non fa alcuna assunzione sulla sua codifica fermione-qubit, quindi lo stesso ansatz può essere sintetizzato con qualunque codifica si adatti al dispositivo di destinazione.
L'operatore ansatz arriva quindi qui già pronto, e tutto ciò che segue è ciò che gli accade dopo: in cosa si decompone il gate, come viene portato a livello inferiore tramite Jordan-Wigner, come viene adattato alla mappa di accoppiamento di un dispositivo e di cosa avrebbe ancora bisogno una discesa di livello non Jordan-Wigner. Per costruire, parametrizzare o simulare un ansatz UCJ in primo luogo, consulta le guide di ffsim.
La guida alla relazione con ffsim per come i due pacchetti si dividono il lavoro, e la guida alla transpilazione per le fasi della pipeline usate di seguito.
1. Ottenere un operatore LUCJ da ffsim
L'ansatz viene inizializzato a partire dalle ampiezze di un calcolo coupled-cluster singles and doubles (CCSD). Esegui Hartree-Fock ristretto seguito da CCSD per una molecola di idrogeno nella base 6-31g, usando PySCF per la chimica quantistica, poi passa le ampiezze a from_t_amplitudes() di ffsim. Questo esegue una doppia fattorizzazione delle ampiezze per ottenere le matrici di Coulomb diagonali e le rotazioni orbitali per ogni strato, e ricava una rotazione orbitale finale facoltativa dalle ampiezze . Il numero di ripetizioni è quello che risulta da tale fattorizzazione; n_reps lo tronca, scambiando accuratezza con un circuito meno profondo.
>>> 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 l'ansatz smette di essere una questione di ffsim. ffsim offre qui scelte che questo pacchetto non vede e non ha bisogno di conoscere, come la fattorizzazione («compressa») ottimizzata in modo variazionale che sta dietro a optimize=True, o l'impacchettamento del vettore dei parametri su cui lavora un ottimizzatore variazionale. Tutte producono lo stesso tipo di operatore, e tutto ciò che segue funziona invariato su ognuna di esse.
2. Trasformare l'operatore in un circuito fermionico
Il gate UCJ racchiude l'operatore e lo esprime come un circuito sui modi fermionici. Il gate è un puro unitario senza alcun riferimento proprio, quindi anteponi un gate InitializeModes (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)
Decomporre il circuito ne rivela l'anatomia, ed è la rappresentazione su cui opera il resto della guida. Il gate InitializeModes prepara il determinante di riferimento, e ogni strato dell'ansatz contribuisce con una OrbitalRotation , poi (una Evolution dell'operatore di Coulomb diagonale ), poi , con una OrbitalRotation finale alla fine. Le rotazioni orbitali agiscono per settore di spin, quindi ciascuna viene posizionata indipendentemente sui modi alpha 0..norb e sui modi beta norb..2*norb.
>>> circuit.decompose().draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

Ogni gate in quella decomposizione è ancora fermionico: OrbitalRotation ed Evolution sono definiti sui modi, e non è ancora comparso alcun operatore di qubit o di Pauli. È questo che lascia aperta la codifica, ed è lo stato in cui l'ansatz resta finché la fase di sintesi non ne sceglie una.
Ogni strato termina con e il successivo inizia con , quindi i gate OrbitalRotation adiacenti potrebbero essere fusi in un'unica rotazione. Un pass di transpilazione che esegua questa fusione è uno sviluppo futuro pianificato.
3. Scegliere una codifica fermione-qubit
Il circuito è ancora fermionico, ed è il punto in cui questo pacchetto si guadagna il suo posto nel flusso di lavoro. Un FermionicCircuit non fa alcuna assunzione su come i modi diventino qubit, quindi la codifica è una scelta della fase di sintesi e non qualcosa incorporato nell'ansatz. Questo è ciò che questo pacchetto aggiunge ai gate Qiskit di ffsim, che sono specifici della trasformazione di Jordan-Wigner.
Per il solo Jordan-Wigner non avresti bisogno di questa deviazione: ffsim include UCJOpSpinBalancedJW e le sue varianti, che portano da sole un operatore UCJ a un circuito di qubit. Il motivo per instradare l'ansatz attraverso un circuito fermionico è tutto ciò che una codifica può offrirti oltre a questo. Le codifiche locali, per esempio, spendono qubit aggiuntivi per limitare il peso di Pauli di ciascun termine, cosa che le guide sui flow set 1D e 2D sfruttano per rendere la profondità a due qubit di un passo di Trotter indipendente dalla dimensione del sistema.
Per un ansatz UCJ in particolare, la fase di sintesi deve coprire due tipi di gate:
-
L'evoluzione di Coulomb diagonale.
Evolutionè già indipendente dalla codifica:MapperFnEvolutionSynthesisaccetta una funzione di mapper, quindi gli strati vengono portati a livello inferiore tramite qualsiasi codifica tu possa esprimere come tale. Le guide sui flow set mostrano come scriverne una. -
Le rotazioni orbitali e la preparazione del determinante di Slater. Né una
OrbitalRotationné laPrepareSlaterDeterminantin cui si fondono l'InitializeModesiniziale e la rotazione (vedi sotto) viene mappata sui qubit come tale. Il passF2FSynthesisle scompone prima, nello spazio fermionico, in gateEvolutionsu uno o due modi ciascuno (e, per la preparazione, unInitializeModesdel riferimento), quindi qualsiasi codifica in grado di mappare unaEvolutionle gestisce. Con Jordan-Wigner, i fattori tra modi adiacenti diventano oggettiXXPlusYYGatenella fase dei qubit. -
L'inizializzazione. Il plugin per
InitializeModesnon è parametrizzato da una codifica: documenta la sua assunzione di una codifica nella base di occupazione con una mappatura 1 a 1 tra modi e qubit, ed emette i gate direttamente su quella base. Una codifica diversa richiede un proprio plugin, e per ora non ne è disponibile nessuno.
Come transpilare i circuiti UCJ con una codifica diversa da Jordan-Wigner è una questione di ricerca aperta, e questo pacchetto fornisce il framework entro cui sviluppare una tale pipeline. Il resto di questa guida costruisce come esempio la pipeline del Transpiler per la codifica Jordan-Wigner, equivalente a quella prodotta da generate_preset_jw_pass_manager().
4. Portarlo a livello inferiore tramite Jordan-Wigner
La discesa di livello avviene in F2QSynthesis, che funziona come HighLevelSynthesis di Qiskit: scorre le istruzioni fermioniche e affida ciascuna a un plugin registrato per quel nome di gate. Il suo attributo F2QSynthesis.methods contiene un F2QSynthesisPlugin per ogni nome di gate, quindi costruire la fase 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 (
... MapperFnEvolutionSynthesis,
... TrivialOccupationInitializeModesSynthesis,
... )
>>>
>>> synthesis = F2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(jordan_wigner)
>>> synthesis.methods["InitializeModes"] = TrivialOccupationInitializeModesSynthesis()
Assegnare istanze in questo modo è anche il modo in cui un plugin scritto da te entra nella pipeline, senza doverlo prima registrare da nessuna parte.
Qui la codifica viene stabilita, nei due modi descritti nella sezione 3: esplicitamente, passando jordan_wigner() a MapperFnEvolutionSynthesis, e implicitamente, tramite l'altro plugin che già la assume.
Quella fase si inserisce poi in un MultiStagePassManager le cui fasi vengono eseguite in un ordine fisso:
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.transpiler import generate_preset_pass_manager
>>> from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig
>>>
>>> from qiskit_fermions.transpiler import (
... FermionicCircuitToDAG,
... FermionicPassManager,
... QuantumDAGToCircuit,
... )
>>> from qiskit_fermions.transpiler.passes import (
... F2FSynthesis,
... HopEvolutionSynthesis,
... MergeOrbitalRotations,
... MergeSlaterDeterminantPreparation,
... TrivialF2QLayout,
... )
>>>
>>> pm = MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... optimization=FermionicPassManager(
... [
... MergeOrbitalRotations(),
... MergeSlaterDeterminantPreparation(),
... F2FSynthesis(
... {"OrbitalRotation": "Givens", "PrepareSlaterDeterminant": "Givens"}
... ),
... ]
... ),
... layout=FermionicPassManager(TrivialF2QLayout()),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(
... hls_config=HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
... ),
... output=QuantumDAGToCircuit(),
... )
La fase di ottimizzazione viene eseguita prima della sintesi, sul circuito fermionico, ed è ciò che la rende utile: riscrive i gate mentre sono ancora fermionici e indipendenti dalla codifica, quindi i risparmi si trasferiscono a qualunque codifica segua. MergeOrbitalRotations riduce ogni sequenza di gate OrbitalRotation consecutivi a uno solo, il che è importante per un ansatz UCJ perché ogni strato termina con e il successivo inizia con . MergeSlaterDeterminantPreparation fonde poi l'InitializeModes iniziale e la rotazione che lo segue in un'unica PrepareSlaterDeterminant, che la sintesi porta a livello inferiore tramite la decomposizione di Slater ridotta, più economica, invece di una rotazione quadrata completa. L'ordine conta: fondere prima le rotazioni espone la singola rotazione subito dopo l'inizializzazione, che la fusione di Slater poi incorpora in essa. F2FSynthesis viene per ultimo, perché scompone qualsiasi rotazione rimasta, e farlo prima lascerebbe ai due pass di fusione nulla da fondere.
Le fasi rimanenti sono di semplice supporto. input e output convertono tra circuiti e DAG, layout assegna i modi fermionici ai qubit (TrivialF2QLayout mappa il modo i sul qubit i), e qubit è un normale pass manager di Qiskit che subentra quando non resta più nulla di fermionico.
Eseguirla richiede un passaggio preparatorio: il gate composito UCJ deve prima essere decomposto nei suoi gate primitivi, in modo che la fase di ottimizzazione possa vedere le singole rotazioni ed evoluzioni.
>>> transpiled = pm.run(circuit.decompose())
>>> print(dict(sorted(transpiled.count_ops().items())))
{'rz': 14, 'rzz': 12, 'x': 2, 'xx_plus_yy': 28}
Senza un dispositivo di destinazione, questo si mappa su 2 * norb qubit assumendo connettività all-to-all; le rotazioni orbitali si sintetizzano in oggetti XXPlusYYGatee le evoluzioni di Coulomb diagonali in oggetti RZZGate:
>>> transpiled.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Questa pipeline è esattamente ciò che assembla generate_preset_jw_pass_manager(), quindi nella pratica usa il preset e costruisci le fasi a mano 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()
True
5. Adattare la topologia del dispositivo
Un dispositivo reale ha una mappa di accoppiamento dei qubit limitata, e l'ansatz LUCJ è progettato per adattarvisi. Le interazioni con lo stesso spin (pairs_aa) formano due catene lineari e le interazioni alpha-beta (pairs_ab) le collegano. generate_lucj_pass_manager() di ffsim costruisce una pipeline di qubit consapevole del dispositivo per questa struttura, e restituisce il sottoinsieme di pairs_ab che l'hardware può effettivamente supportare. Inserisci quella pipeline nella fase qubit del preset mantenendo la sintesi fermione-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()
>>> hls_config = HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
>>> pm.qubit, allowed_pairs_ab = generate_lucj_pass_manager(
... backend,
... norb,
... "heavy-hex",
... (pairs_aa, None),
... optimization_level=3,
... seed_transpiler=0,
... hls_config=hls_config,
... )
>>>
>>> # the alpha-beta interactions the heavy-hex connectivity can implement
>>> print(allowed_pairs_ab)
[(0, 0)]
Questa composizione è il punto centrale: la fase qubit è consapevole del dispositivo, mentre la fase di sintesi fermione-qubit che la precede resta quella propria di questo pacchetto, e quindi resta sostituibile. Sostituire la fase qubit elimina ciò che il preset vi registra, cioè la HopEvolutionSynthesis che trasforma i fattori di Givens di una rotazione orbitale in oggetti XXPlusYYGate. Passala tramite hls_config come sopra, oppure la base del dispositivo deve essere raggiunta senza di essa.
Con pm.qubit ora impostato sulla pipeline consapevole del dispositivo, l'esecuzione del pass manager dispone il circuito sui qubit del Backend e lo instrada sulla mappa di accoppiamento. Per l'ansatz non ristretto del passo 1, il cui operatore di Coulomb diagonale contiene ancora termini alpha-beta che l'hardware non può raggiungere direttamente, il router deve inserire molti gate SWAP per colmarli:
>>> 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
33
Limitare l'ansatz alle interazioni implementabili dall'hardware
La soluzione è reinserire allowed_pairs_ab nella costruzione dell'ansatz, tramite l'argomento interaction_pairs di from_t_amplitudes(), in modo che l'operatore di Coulomb diagonale contenga solo termini alpha-beta che la mappa di accoppiamento può implementare direttamente. L'ansatz si adatta così alla topologia del dispositivo e il router deve toccarlo a malapena:
>>> 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
4
Disegnare solo i qubit attivi (idle_wires=False) mostra il circuito ristretto alle due catene di spin e al ponte alpha-beta, espresso nei gate di base del dispositivo. Layout e instradamento disperdono i modi logici sui qubit fisici del dispositivo, quindi passa un wire_order ricavato dal layout finale del circuito (final_index_layout() elenca il qubit fisico su cui è finito ogni qubit di input, nell'ordine dei qubit di input) per ridisegnare i fili nell'ordine originale dei modi:
>>> 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à dopo il layout dipendono dai pass di instradamento/ottimizzazione e dal dispositivo scelto, quindi non sono riportati qui. Il punto chiave è il co-design; esprimere l'ansatz con una catena pairs_aa tra primi vicini e ponti pairs_ab filtrati dall'hardware mantiene il circuito sintetizzato vicino alla topologia del dispositivo, minimizzando l'overhead di instradamento (gate SWAP inseriti). Vedi FermionicSlaterGivensDecomposition per una scelta di sintesi correlata (minimize_2q_gate_count), che bilancia il numero di gate a due qubit rispetto alla profondità dopo l'instradamento.
Prossimi passi
-
Scopri come funzionano i singoli gate nella documentazione di
qiskit_fermions.circuit.librarye nella guida al circuito fermionico. -
Esplora la guida esplicativa sugli operatori per capire come costruire direttamente operatori fermionici e Hamiltoniani.
-
Scopri come un circuito fermionico viene mappato sui qubit nella guida alla transpilazione.
-
Leggi la documentazione di
F2QSynthesisper l'altro modo di configurare la fase di sintesi: un dizionarioconfigche seleziona i plugin tramite i nomi brevi con cui si registrano nei loro entry pointqiskit_fermions.transpiler.synthesis, invece che tramite istanza. -
Leggi la guida alla relazione con ffsim per capire come i due pacchetti si dividono il lavoro e quali protocolli permettono a ffsim di simulare i circuiti di questo pacchetto.
