SQD do estymacji energii hamiltonianu chemicznego
W tej lekcji zastosujemy SQD do oszacowania energii stanu podstawowego cząsteczki.
W szczególności omówimy następujące tematy, korzystając z -etapowego podejścia wzorca Qiskit:
- Krok 1: Odwzorowanie problemu na obwody i operatory kwantowe
- Skonfigurowanie molecular Hamiltonianu dla .
- Wyjaśnienie inspirowanego chemią i przyjaznego sprzętowi lokalnego unitarnego klastra Jastrowa (LUCJ) [1]
- Krok 2: Optymalizacja pod kątem docelowego sprzętu
- Optymalizacja liczby bramek i układu ansatzu pod kątem wykonania na sprzęcie
- Krok 3: Wykonanie na docelowym sprzęcie
- Uruchomienie zoptymalizowanego obwodu na rzeczywistym QPU w celu wygenerowania próbek podprzestrzeni.
- Krok 4: Przetwarzanie końcowe wyników
- Wprowadzenie samouzgodnionej pętli odzyskiwania konfiguracji [2]
- Przetwarzanie pełnego zestawu próbek ciągów bitów, przy użyciu wcześniejszej wiedzy o liczbie cząstek i średniej obsadzie orbitalnej obliczonej w ostatniej iteracji.
- Probabilistyczne tworzenie partii podpróbek z odzyskanych ciągów bitów.
- Rzutowanie i diagonalizowanie molecular Hamiltonianu w każdej próbkowanej podprzestrzeni.
- Zapisanie minimalnej znalezionej energii stanu podstawowego we wszystkich partiach i aktualizacja średniej obsady orbitalnej.
- Wprowadzenie samouzgodnionej pętli odzyskiwania konfiguracji [2]
W trakcie lekcji użyjemy kilku pakietów oprogramowania.
PySCFdo zdefiniowania cząsteczki i skonfigurowania Hamiltonianu.- pakiet
ffsimdo skonstruowania ansatzu LUCJ. Qiskitdo transpilacji ansatzu pod kątem wykonania na sprzęcie.Qiskit IBM Runtimedo wykonania obwodu na QPU i zbierania próbek.Qiskit addon SQDdo odzyskiwania konfiguracji oraz estymacji energii stanu podstawowego przy użyciu rzutowania na podprzestrzeń i diagonalizacji macierzy.
1. Odwzorowanie problemu na obwody i operatory kwantowe
Molecular Hamiltonian
Molecular Hamiltonian ma ogólną postać:
/ to fermionowe operatory kreacji/anihilacji powiązane z -tym elementem zbioru bazowego i spinem . i to jedno- i dwuciałowe całki elektronowe. Korzystając z pySCF, zdefiniujemy cząsteczkę i obliczymy jedno- i dwuciałowe całki Hamiltonianu dla zbioru bazowego 6-31g.
# Added by doQumentation — required packages for this notebook
!pip install -q ffsim matplotlib numpy pyscf qiskit qiskit-addon-sqd qiskit-ibm-runtime
import warnings
import pyscf
import pyscf.cc
import pyscf.mcscf
warnings.filterwarnings("ignore")
# Specify molecule properties
open_shell = False
spin_sq = 0
# Build N2 molecule
mol = pyscf.gto.Mole()
mol.build(
atom=[["N", (0, 0, 0)], ["N", (1.0, 0, 0)]], # Two N atoms 1 angstrom apart
basis="6-31g",
symmetry="Dooh",
)
# Define active space
n_frozen = 2
active_space = range(n_frozen, mol.nao_nr())
# Get molecular integrals
scf = pyscf.scf.RHF(mol).run()
num_orbitals = len(active_space)
n_electrons = int(sum(scf.mo_occ[active_space]))
num_elec_a = (n_electrons + mol.spin) // 2
num_elec_b = (n_electrons - mol.spin) // 2
cas = pyscf.mcscf.CASCI(scf, num_orbitals, (num_elec_a, num_elec_b))
mo = cas.sort_mo(active_space, base=0)
hcore, nuclear_repulsion_energy = cas.get_h1cas(mo) # hcore: one-body integrals
eri = pyscf.ao2mo.restore(1, cas.get_h2cas(mo), num_orbitals) # eri: two-body integrals
# Compute exact energy for comparison
exact_energy = cas.run().e_tot
converged SCF energy = -108.835236570774
CASCI E = -109.046671778080 E(CI) = -32.8155692383188 S^2 = 0.0000000
W tej lekcji użyjemy transformacji Jordan-Wigner (JW), aby odwzorować funkcję falową fermionową na funkcję falową kubitową, tak aby można ją było przygotować przy użyciu obwodu kwantowego. Transformacja JW odwzorowuje przestrzeń Focka fermionów w M spatial orbitals na przestrzeń Hilberta 2M kubitów, czyli spatial orbital jest dzielony na dwa spin orbitals, jeden związany z elektronem o spinie w górę (), a drugi o spinie w dół (). Spin orbital może być obsadzony lub nieobsadzony. Zwykle, gdy mówimy o liczbie orbitali, mamy na myśli liczbę orbitali przestrzennych (spatial). Liczba spin orbitals będzie dwa razy większa. W obwodach kwantowych każdy spin orbital będziemy reprezentować jednym kubitem. W ten sposób jeden zbiór kubitów będzie reprezentować orbitale o spinie w górę lub , a inny zbiór będzie reprezentować orbitale o spinie w dół lub . Na przykład cząsteczka dla zestawu bazowego 6-31g ma spatial orbitals (czyli + = spin orbitals). Dlatego będziemy potrzebować obwodu kwantowego z kubitami (możemy potrzebować dodatkowych kubitów pomocniczych, jak omówiono później). Kubity są mierzone w bazie obliczeniowej, aby wygenerować ciągi bitów, które reprezentują konfiguracje elektronowe lub wyznaczniki (Slatera). W całej tej lekcji będziemy używać pojęć ciągów bitów, konfiguracji i wyznaczników zamiennie. Ciągi bitów mówią nam o obsadzeniu elektronów w spin orbitals: na pozycji bitu oznacza, że odpowiadający spin orbital jest obsadzony, podczas gdy oznacza, że spin orbital jest pusty. Ponieważ problemy struktury elektronowej zachowują liczbę cząstek, tylko stała liczba spin orbitals musi być obsadzona. Cząsteczka ma elektronów o spinie w górę () i elektronów o spinie w dół (). Dlatego każdy ciąg bitów reprezentujący orbitale i musi mieć po pięć dla cząsteczki .
1.1 Obwód kwantowy do generowania próbek: ansatz LUCJ
W tej lekcji użyjemy ansatz local unitary coupled cluster Jastrow (LUCJ) \[1\] do przygotowania stanu kwantowego i późniejszego próbkowania. Najpierw wyjaśnimy różne bloki budulcowe pełnego ansatz UCJ oraz przybliżenia wprowadzone w jego wersji lokalnej. Następnie, używając pakietu ffsim, skonstruujemy ansatz LUCJ i zoptymalizujemy go za pomocą transpilera qiskit do wykonania sprzętowego.
Ansatz UCJ ma następującą postać (dla iloczynu warstw lub powtórzeń operatora UCJ).
gdzie jest stanem referencyjnym, zazwyczaj przyjmowanym jako stan Hartree-Fock (HF). Ponieważ stan Hartree-Fock jest zdefiniowany jako mający obsadzone orbitale o najniższych numerach, przygotowanie stanu HF będzie obejmować zastosowanie bramek X do ustawienia kubitów odpowiadających obsadzonym orbitalom na jeden. Na przykład blok przygotowania stanu HF dla 4 spatial orbitals oraz 2 elektronów o spinie w górę i 2 elektronów o spinie w dół może wyglądać następująco:
Pojedyncze powtórzenie operatora UCJ składa się z ewolucji diagonalnej Coulomba () otoczonej rotacjami orbital ( oraz ).
Bloki rotacji orbital działają na pojedynczym rodzaju spinu ( (spin w górę)/ (spin w dół)). Dla każdego rodzaju elektronu rotacja orbital składa się z warstwy jednokubitowych bramek , po których następuje sekwencja dwukubitowych bramek rotacji Givensa (bramki ).
Bramki dwukubitowe działają na sąsiednich spin-orbitals (najbliższych sąsiadujących kubitach) i dlatego są realizowalne na jednostkach QPU IBM® bez potrzeby stosowania bramek SWAP.
, znany również jako operator diagonalny Coulomba, składa się z trzech bloków. Dwa z nich działają na sektory o tym samym spinie ( oraz ), a jeden działa pomiędzy dwoma sektorami spinowymi ().
Wszystkie bloki w składają się z bramek liczba-liczba [1]. Bramka może być dalej rozłożona na bramkę , po której następują dwie jednokubitowe bramki działające na dwóch oddzielnych kubitach.
Komponenty o tym samym spinie ( i ) mają bramki pomiędzy wszystkimi możliwymi parami kubitów. Jednakże, ponieważ nadprzewodzące jednostki QPU mają ograniczoną łączność, kubity muszą być zamieniane, aby zrealizować bramki pomiędzy niesąsiadującymi kubitami.
Rozważmy na przykład następujący blok (lub ) dla spatial orbitals. Przy liniowej łączności kubitów ostatnie trzy bramki nie są bezpośrednio realizowalne, ponieważ działają pomiędzy niesąsiadującymi kubitami (na przykład Q0 i Q2 nie są bezpośrednio połączone). Dlatego potrzebujemy bramek SWAP, aby je zbliżyć (następny rysunek pokazuje przykład z bramkami SWAP).
Następnie, realizuje bramki pomiędzy orbitalami o tym samym indeksie z różnych sektorów spinowych (na przykład pomiędzy i ). Podobnie, jeśli kubity nie są fizycznie sąsiednie na QPU, te bramki również będą wymagały SWAP-ów.
Z powyższej dyskusji wynika, że ansatz UCJ napotyka pewne przeszkody przy wykonaniu sprzętowym, ponieważ wymaga bramek SWAP z powodu interakcji niesąsiadujących kubitów. Lokalny wariant ansatz UCJ, LUCJ, rozwiązuje to wyzwanie, usuwając niektóre z operatora diagonalnego Coulomba.
W blokach tych samych rodzajów elektronów, i , zachowujemy tylko bramki kompatybilne z łącznością najbliższych sąsiadów i usuwamy bramki pomiędzy niesąsiadującymi kubitami w wersji LUCJ. Następujący rysunek pokazuje blok LUCJ po usunięciu bramek niesąsiadujących.
Następnie, wersja LUCJ bloku , który działa pomiędzy różnymi rodzajami elektronów, może przybierać różne kształty w zależności od topologii urządzenia.
Również tutaj wersja LUCJ pozbywa się niekompatybilnych bramek. Poniższy rysunek pokazuje warianty bloku dla różnych topologii kubitów, w tym siatki, heksagonalnej, heavy-hex i liniowej.
- Kwadratowa: możemy mieć bramki pomiędzy wszystkimi orbitalami i bez żadnych SWAP-ów, a zatem nie musimy usuwać żadnych bramek .
- Heavy-hex: Interakcje - są zachowywane pomiędzy co -tym indeksowanym (takim jak 0-ty, 4-ty i 8-my) spin orbitals i są mediowane przez ancilla, czyli potrzebujemy kubitów pomocniczych (ancilla) pomiędzy łańcuchami liniowymi reprezentującymi orbitale i . Ten układ wymaga ograniczonej liczby SWAP-ów.
- Heksagonalna: Co drugi orbital, taki jak 0-ty, 2-gi i 4-ty indeksowany orbital, staje się najbliższymi sąsiadami, gdy i są rozłożone w dwóch sąsiadujących łańcuchach liniowych.
- Liniowa: Tylko jeden orbital i jeden orbital są połączone, co oznacza, że blok będzie miał tylko jedną bramkę.
Chociaż usunięcie bramek z ansatz UCJ w celu skonstruowania wersji LUCJ czyni go bardziej kompatybilnym ze sprzętem, ansatz traci nieco ekspresywności. Dlatego przy użyciu ansatz LUCJ może być potrzebnych więcej powtórzeń () zmodyfikowanego operatora UCJ.
1.2 Inicjalizacja ansatz LUCJ
LUCJ jest sparametryzowanym ansatz i musimy zainicjalizować parametry przed wykonaniem sprzętowym. Jednym ze sposobów inicjalizacji ansatz jest użycie amplitud t1 i t2 z klasycznej metody coupled cluster singles and doubles (CCSD), gdzie amplitudy t1 są współczynnikami operatorów pojedynczych wzbudzeń, a t2 amplitudes są dla operatorów podwójnych wzbudzeń.
Zauważ, że chociaż inicjalizacja ansatz LUCJ amplitudami t1 i t2 daje przyzwoite wyniki, parametry ansatz mogą wymagać dalszej optymalizacji.
# Get CCSD t2 amplitudes for initializing the ansatz
ccsd = pyscf.cc.CCSD(
scf, frozen=[i for i in range(mol.nao_nr()) if i not in active_space]
)
ccsd.run()
t1 = ccsd.t1
t2 = ccsd.t2
E(CCSD) = -109.0398256929733 E_corr = -0.20458912219883
1.3 Konstruowanie ansatz LUCJ przy użyciu ffsim
Użyjemy pakietu ffsim do utworzenia i zainicjalizowania ansatz amplitudami t1 i t2 obliczonymi powyżej. Ponieważ nasza cząsteczka ma stan Hartree-Fock z zamkniętą powłoką, użyjemy wariantu ansatz UCJ zbalansowanego pod względem spinu, UCJOpSpinBalanced.
Ponieważ sprzęt IBM ma topologię heavy-hex, przyjmiemy wzorzec zig-zag użyty w [1] i wyjaśniony powyżej dla interakcji kubitów. W tym wzorcu orbitale (kubity) o tym samym spinie są połączone topologią liniową (czerwone i niebieskie kółka). Ze względu na topologię heavy-hex orbitale dla różnych spinów mają połączenia pomiędzy co 4-tym orbital, czyli 0-tym, 4-tym, 8-mym itd. (fioletowe kółka).

import ffsim
from qiskit import QuantumCircuit, QuantumRegister
n_reps = 2
alpha_alpha_indices = [(p, p + 1) for p in range(num_orbitals - 1)]
alpha_beta_indices = [(p, p) for p in range(0, num_orbitals, 4)]
ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(
t2=t2,
t1=t1,
n_reps=n_reps,
interaction_pairs=(alpha_alpha_indices, alpha_beta_indices),
)
nelec = (num_elec_a, num_elec_b)
# create an empty quantum circuit
qubits = QuantumRegister(2 * num_orbitals, name="q")
circuit = QuantumCircuit(qubits)
# prepare Hartree-Fock state as the reference state and append it to the quantum circuit
circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits)
# apply the UCJ operator to the reference state
circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)
circuit.measure_all()
# circuit.decompose().draw("mpl", scale=0.5, fold=-1)
Ansatz LUCJ z powtórzonymi warstwami można zoptymalizować, scalając niektóre sąsiednie bloki. Rozważ przypadek dla n_reps=2. Dwa bloki rotacji orbital w środku mogą być scalone w pojedynczy blok rotacji orbital. Pakiet ffsim ma pass manager o nazwie ffsim.qiskit.PRE_INIT do optymalizacji obwodu poprzez scalanie takich sąsiednich bloków.

2. Optymalizacja pod docelowy sprzęt
Najpierw pobieramy wybrany backend. Zoptymalizujemy nasz obwód pod ten backend, a następnie uruchomimy zoptymalizowany obwód na tym samym backendzie, aby wygenerować próbki dla podprzestrzeni.
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
# Use the least-busy backend or specify a quantum computer using the syntax commented out below.
backend = service.least_busy(operational=True, simulator=False)
# backend = service.backend("ibm_brisbane")
Następnie zalecamy poniższe kroki w celu optymalizacji ansatzu i uczynienia go zgodnym ze sprzętem.
- Wybierz fizyczne kubity (
initial_layout) z docelowego sprzętu, które są zgodne ze wzorcem zygzaka (dwa liniowe łańcuchy z kubitem pomocniczym pomiędzy nimi) opisanym powyżej. Ułożenie kubitów w tym wzorcu prowadzi do wydajnego, zgodnego ze sprzętem obwodu z mniejszą liczbą bramek. - Wygeneruj etapowy pass manager za pomocą funkcji
generate_preset_pass_managerz Qiskit z wybranymbackendiinitial_layout. - Ustaw etap
pre_initswojego etapowego pass managera naffsim.qiskit.PRE_INIT.ffsim.qiskit.PRE_INITzawiera przebiegi transpilera Qiskit, które rozkładają bramki na rotacje orbitalne, a następnie łączą te rotacje orbitalne, co skutkuje mniejszą liczbą bramek w końcowym obwodzie. - Uruchom pass manager na swoim obwodzie.
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
spin_a_layout = [0, 14, 18, 19, 20, 33, 39, 40, 41, 53, 60, 61, 62, 72, 81, 82]
spin_b_layout = [2, 3, 4, 15, 22, 23, 24, 34, 43, 44, 45, 54, 64, 65, 66, 73]
initial_layout = spin_a_layout + spin_b_layout
pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, initial_layout=initial_layout
)
# without PRE_INIT passes
isa_circuit = pass_manager.run(circuit)
print(f"Gate counts (w/o pre-init passes): {isa_circuit.count_ops()}")
# with PRE_INIT passes
# We will use the circuit generated by this pass manager for hardware execution
pass_manager.pre_init = ffsim.qiskit.PRE_INIT
isa_circuit = pass_manager.run(circuit)
print(f"Gate counts (w/ pre-init passes): {isa_circuit.count_ops()}")
Gate counts (w/o pre-init passes): OrderedDict({'rz': 7579, 'sx': 6106, 'ecr': 2316, 'x': 336, 'measure': 32, 'barrier': 1})
Gate counts (w/ pre-init passes): OrderedDict({'rz': 4088, 'sx': 3125, 'ecr': 1262, 'x': 201, 'measure': 32, 'barrier': 1})
3. Wykonanie na docelowym sprzęcie
Po zoptymalizowaniu obwodu pod kątem wykonania sprzętowego jesteśmy gotowi uruchomić go na docelowym sprzęcie i zebrać próbki do oszacowania energii stanu podstawowego. Ponieważ mamy tylko jeden obwód, użyjemy trybu wykonania Job z Qiskit Runtime i wykonamy nasz obwód.
from qiskit_ibm_runtime import SamplerV2 as Sampler
sampler = Sampler(mode=backend)
sampler.options.dynamical_decoupling.enable = True
job = sampler.run([isa_circuit], shots=10_000) # Takes approximately 5sec of QPU time
# Run cell after IQX job completion
primitive_result = job.result()
pub_result = primitive_result[0]
counts = pub_result.data.meas.get_counts()
4. Przetwarzanie końcowe wyników
Część post-processing przepływu pracy SQD można podsumować za pomocą następującego diagramu.
Próbkowanie układu LUCJ w bazie obliczeniowej generuje pulę zaszumionych konfiguracji , które są używane w procedurze post-processing. Obejmuje ona metodę zwaną (szczegóły omówione później) odzyskiwaniem konfiguracji (ang. configuration recovery), która probabilistycznie koryguje konfiguracje z błędnymi liczbami elektronów. Konfiguracje tylko z poprawnymi liczbami elektronów są następnie podpróbkowywane i dystrybuowane do wielu partii (batches) w oparciu o częstość występowania każdej unikalnej konfiguracji. Każda partia próbek definiuje podprzestrzeń (). Następnie hamiltonian molekularny, , jest rzutowany na podprzestrzenie: