Transpilează un ansatz LUCJ
Conceptele din acest ghid sunt disponibile doar în API-ul Python.
Ansatzul local unitary cluster Jastrow (LUCJ) este o parametrizare compactă, eficientă hardware, a unei funcții de undă electronice corelate. Este un membru al familiei mai generale unitary cluster Jastrow (UCJ) și are forma
unde este o stare de referință (de obicei Hartree-Fock), fiecare este o rotație orbitală, iar fiecare este un operator Coulomb diagonal
cu operatorul de număr pe orbitalul spațial cu spinul .
ffsim construiește operatorul ansatz; acest pachet îl transformă într-un circuit și coboară acel circuit pe qubiți. Acest ghid tratează a doua jumătate a acestei propoziții. Motivul pentru care merită să faci acest drum este chiar coborârea: porțile Qiskit proprii ale ffsim sunt specifice transformării Jordan-Wigner, în timp ce un FermionicCircuit nu presupune nimic despre codificarea fermion-qubit, deci același ansatz poate fi sintetizat prin orice codificare potrivită dispozitivului țintă.
Prin urmare, operatorul ansatz ajunge aici gata făcut, iar tot ce urmează este ce se întâmplă cu el după aceea: în ce se descompune poarta, cum coboară prin Jordan-Wigner, cum este potrivit pe o hartă de cuplare a dispozitivului și ce ar mai fi necesar pentru o coborâre non-Jordan-Wigner. Pentru construirea, parametrizarea sau simularea unui ansatz UCJ de la bun început, vezi ghidurile proprii ale ffsim.
Ghidul relației cu ffsim pentru modul în care cele două pachete își împart munca și ghidul de transpilare pentru etapele pipeline-ului folosite mai jos.
1. Obține un operator LUCJ din ffsim
Ansatzul este inițializat din amplitudinile unui calcul coupled-cluster cu simple și duble excitații (CCSD). Rulează Hartree-Fock restricționat urmat de CCSD pentru o moleculă de hidrogen în baza 6-31g, folosind PySCF pentru chimia cuantică, apoi dă amplitudinile funcției from_t_amplitudes() din ffsim. Aceasta realizează o factorizare dublă a amplitudinilor pentru a obține matricile Coulomb diagonale și rotațiile orbitale pentru fiecare strat și deduce o rotație orbitală finală opțională din amplitudinile . Numărul de repetări este cel pe care îl produce acea factorizare; n_reps îl trunchiază, schimbând acuratețea pe un circuit mai puțin adânc.
>>> 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)
Aici ansatzul încetează să mai fie o preocupare a ffsim. ffsim oferă aici opțiuni pe care acest pachet nici nu le vede, nici nu trebuie să le cunoască, cum ar fi factorizarea („comprimată”) optimizată variațional din spatele lui optimize=True sau împachetarea vectorului de parametri pe care îl manipulează un optimizator variațional. Toate produc același tip de operator, iar tot ce urmează funcționează neschimbat pe oricare dintre ele.
2. Transformă operatorul într-un circuit fermionic
Poarta UCJ înfășoară operatorul și îl exprimă ca un circuit peste moduri fermionice. Poarta este un unitar pur, fără nicio referință proprie, așa că adaugă la început o poartă InitializeModes (construită cu from_hartree_fock()) pentru a furniza referința Hartree-Fock căreia i se aplică ansatzul.
>>> 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)
Descompunerea circuitului îi dezvăluie anatomia și aceasta este reprezentarea pe care operează restul ghidului. Poarta InitializeModes pregătește determinantul de referință, iar fiecare strat al ansatzului contribuie cu un OrbitalRotation , apoi (un Evolution al operatorului Coulomb diagonal ), apoi , cu un OrbitalRotation final la sfârșit. Rotațiile orbitale acționează pe fiecare sector de spin, deci fiecare este plasată independent pe modurile alfa 0..norb și pe modurile beta norb..2*norb.
>>> circuit.decompose().draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

Fiecare poartă din acea descompunere este încă fermionică: OrbitalRotation și Evolution sunt definite pe moduri și încă nu a apărut niciun operator de qubit sau Pauli. Aceasta lasă codificarea deschisă și este starea în care rămâne ansatzul până când etapa de sinteză alege una.
Fiecare strat se termină cu , iar următorul începe cu , deci porțile OrbitalRotation adiacente ar putea fi îmbinate într-o singură rotație. Un pas de transpilare care realizează această fuziune este o dezvoltare viitoare planificată.
3. Alege o codificare fermion-qubit
Circuitul este încă fermionic și tocmai aici pachetul acesta își câștigă locul în fluxul de lucru. Un FermionicCircuit nu presupune nimic despre cum devin modurile qubiți, deci codificarea este o alegere din etapa de sinteză, nu ceva încorporat în ansatz. Aceasta este ceea ce adaugă acest pachet față de porțile Qiskit proprii ale ffsim, care sunt specifice transformării Jordan-Wigner.
Doar pentru Jordan-Wigner nu ai avea nevoie de acest ocol: ffsim include UCJOpSpinBalancedJW și variantele sale, care transformă singure un operator UCJ într-un circuit de qubiți. Motivul pentru care treci ansatzul printr-un circuit fermionic este tot ce altceva îți poate oferi o codificare. Codificările locale, de exemplu, consumă qubiți suplimentari pentru a limita greutatea Pauli a fiecărui termen, lucru pe care ghidurile seturilor de flux 1D și 2D îl folosesc pentru a face adâncimea cu două qubiți a unui pas Trotter independentă de dimensiunea sistemului.
Pentru un ansatz UCJ în mod specific, etapa de sinteză trebuie să acopere două tipuri de porți:
-
Evoluția Coulomb diagonală.
Evolutioneste deja agnostică față de codificare:MapperFnEvolutionSynthesisprimește o funcție de mapare, deci straturile coboară prin orice codificare pe care o poți exprima ca atare. Ghidurile seturilor de flux arată cum se scrie una. -
Rotațiile orbitale și pregătirea determinantului Slater. Nici
OrbitalRotation, niciPrepareSlaterDeterminantîn care fuzioneazăInitializeModesde la început și rotația (vezi mai jos) nu este mapată ca atare pe qubiți. PasulF2FSynthesisle descompune mai întâi, în spațiul fermionic, în porțiEvolutionpe unul sau două moduri fiecare (și, pentru pregătire, unInitializeModesal referinței), așa că orice codificare care poate mapa unEvolutionle poate gestiona. Sub Jordan-Wigner, factorii dintre modurile adiacente devin obiecteXXPlusYYGateîn etapa de qubiți. -
Inițializarea. Pluginul pentru
InitializeModesnu este parametrizat de o codificare: documentează presupunerea unei codificări în baza de ocupare cu o mapare 1-la-1 între moduri și qubiți și emite porți direct în acea bază. O codificare diferită are nevoie de propriul plugin pentru aceasta și încă nu există niciunul.
Cum se transpilează circuitele UCJ sub o codificare non-Jordan-Wigner este o problemă de cercetare deschisă, iar acest pachet oferă cadrul în care se poate dezvolta un astfel de pipeline. Restul acestui ghid construiește, ca exemplu, pipeline-ul de transpilare pentru codificarea Jordan-Wigner, echivalent cu cel produs de generate_preset_jw_pass_manager().
4. Coboar-o prin Jordan-Wigner
Coborârea are loc în F2QSynthesis, care funcționează ca HighLevelSynthesis din Qiskit: parcurge instrucțiunile fermionice și o predă pe fiecare unui plugin înregistrat pentru numele acelei porți. Atributul său F2QSynthesis.methods conține un F2QSynthesisPlugin pentru fiecare nume de poartă, deci construirea etapei înseamnă alegerea unui plugin pentru fiecare poartă fermionică pe care o conține circuitul:
>>> 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()
Atribuirea de instanțe în acest mod este și felul în care un plugin scris de tine intră în pipeline, fără a fi nevoie să îl înregistrezi mai întâi undeva.
Aici se stabilește codificarea, în cele două moduri descrise în secțiunea 3: explicit, dând jordan_wigner() lui MapperFnEvolutionSynthesis, și implicit, prin celălalt plugin care o presupune deja.
Această etapă se integrează apoi într-un MultiStagePassManager ale cărui etape rulează într-o ordine fixă:
>>> 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(),
... )
Etapa de optimizare rulează înainte de sinteză, pe circuitul fermionic, ceea ce o face utilă: rescrie porțile cât timp sunt încă fermionice și agnostice față de codificare, astfel încât economiile se transferă oricărei codificări care urmează. MergeOrbitalRotations restrânge fiecare șir de porți OrbitalRotation consecutive într-una singură, lucru important pentru un ansatz UCJ deoarece fiecare strat se termină cu , iar următorul începe cu . MergeSlaterDeterminantPreparation fuzionează apoi InitializeModes de la început și rotația care îl urmează într-un singur PrepareSlaterDeterminant, pe care sinteza îl coboară prin descompunerea Slater redusă, mai ieftină, în loc de o rotație pătrată completă. Ordinea contează: îmbinarea mai întâi a rotațiilor expune rotația unică imediat după inițializare, pe care fuziunea Slater o contractă apoi în ea. F2FSynthesis vine ultima, deoarece descompune orice rotație rămasă, iar dacă ar rula mai devreme, cele două pase de îmbinare nu ar mai avea nimic de fuzionat.
Etapele rămase sunt de legătură. input și output convertesc între circuite și DAG-uri, layout atribuie moduri fermionice qubiților (TrivialF2QLayout mapează modul i pe qubitul i), iar qubit este un pass manager Qiskit obișnuit care preia controlul când nu mai rămâne nimic fermionic.
Rularea lui necesită un pas pregătitor: poarta compusă UCJ trebuie mai întâi descompusă în porțile sale primitive, astfel încât etapa de optimizare să poată vedea rotațiile și evoluțiile individuale.
>>> transpiled = pm.run(circuit.decompose())
>>> print(dict(sorted(transpiled.count_ops().items())))
{'rz': 14, 'rzz': 12, 'x': 2, 'xx_plus_yy': 28}
Fără un dispozitiv țintă, aceasta se mapează pe 2 * norb qubiți cu conectivitate de tip toți-cu-toți presupusă; rotațiile orbitale se sintetizează în obiecte XXPlusYYGateiar evoluțiile Coulomb diagonale în obiecte RZZGate:
>>> transpiled.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Acest pipeline este exact ceea ce asamblează generate_preset_jw_pass_manager(), deci în practică folosește presetul, iar etapele construiește-le manual când trebuie să modifici una dintre ele:
>>> 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. Potrivește topologia dispozitivului
Un dispozitiv real are o hartă de cuplare a qubiților restricționată, iar ansatzul LUCJ este proiectat să i se potrivească. Interacțiunile cu același spin (pairs_aa) formează două lanțuri liniare, iar interacțiunile alfa-beta (pairs_ab) le leagă. generate_lucj_pass_manager() din ffsim construiește un pipeline de qubiți conștient de dispozitiv pentru această structură și returnează submulțimea lui pairs_ab pe care hardware-ul o poate efectiv accepta. Introdu acel pipeline în etapa qubit a presetului, păstrând sinteza fermion-qubit proprie a pachetului:
>>> 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)]
Această compunere este esența: etapa qubit este conștientă de dispozitiv, în timp ce etapa de sinteză fermion-qubit dinaintea ei rămâne cea proprie a acestui pachet și deci rămâne înlocuibilă. Înlocuirea etapei qubit elimină ceea ce presetul înregistrează acolo, adică HopEvolutionSynthesis care transformă factorii Givens ai unei rotații orbitale în obiecte XXPlusYYGateTransmite-l mai departe prin hls_config ca mai sus, altfel baza dispozitivului trebuie atinsă fără el.
Cu pm.qubit setat acum la pipeline-ul conștient de dispozitiv, rularea pass manager-ului așază circuitul pe qubiții backend-ului și îl rutează pe harta de cuplare. Pentru ansatzul nerestricționat de la pasul 1, al cărui operator Coulomb diagonal conține încă termeni alfa-beta pe care hardware-ul nu îi poate atinge direct, router-ul trebuie să insereze multe porți SWAP pentru a-i lega:
>>> 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
Restricționează ansatzul la interacțiunile implementabile pe hardware
Soluția este să transmiți allowed_pairs_ab înapoi în construcția ansatzului, prin argumentul interaction_pairs al lui from_t_amplitudes(), astfel încât operatorul Coulomb diagonal să conțină doar termeni alfa-beta pe care harta de cuplare îi poate implementa direct. Ansatzul se potrivește atunci cu topologia dispozitivului, iar router-ul abia trebuie să îl modifice:
>>> 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
Desenarea doar a qubiților activi (idle_wires=False) arată circuitul restricționat la cele două lanțuri de spin și puntea alfa-beta, exprimat în porțile de bază ale dispozitivului. Layout-ul și rutarea împrăștie modurile logice pe qubiții fizici ai dispozitivului, așa că transmite un wire_order luat din layout-ul final al circuitului (final_index_layout() listează qubitul fizic pe care a ajuns fiecare qubit de intrare, în ordinea qubiților de intrare) pentru a desena firele înapoi în ordinea inițială a modurilor:
>>> 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>

Numărul exact de porți și adâncimea după layout depind de pașii de rutare/optimizare și de dispozitivul ales, deci nu sunt reproduse aici. Ideea cheie este co-proiectarea; exprimarea ansatzului cu un lanț pairs_aa între vecini apropiați și punți pairs_ab filtrate de hardware menține circuitul sintetizat aproape de topologia dispozitivului, minimizând costul rutării (porți SWAP inserate). Vezi FermionicSlaterGivensDecomposition pentru o alegere de sinteză înrudită (minimize_2q_gate_count), care schimbă numărul de porți cu doi qubiți pe adâncimea rutată.
Pașii următori
-
Află cum funcționează porțile individuale în documentația
qiskit_fermions.circuit.libraryși în ghidul circuitelor fermionice. -
Explorează ghidul explicativ despre operatori pentru a înțelege cum se construiesc direct operatori fermionici și Hamiltoniene.
-
Vezi cum este mapat un circuit fermionic pe qubiți în ghidul de transpilare.
-
Citește documentația
F2QSynthesispentru cealaltă modalitate de a configura etapa de sinteză: un dicționarconfigcare selectează plugin-uri după numele scurte sub care sunt înregistrate în punctele de intrareqiskit_fermions.transpiler.synthesis, în loc să le selecteze după instanță. -
Citește ghidul relației cu ffsim pentru a înțelege cum își împart cele două pachete munca și ce protocoale permit ffsim să simuleze circuitele acestui pachet.
