Przejdź do głównej treści
Ten projekt jest w wersji beta. Znalazłeś błąd? Otwórz zgłoszenie na GitHub. Masz pytanie lub pomysł? Zapytaj w Discussions — chętnie poznamy Twoją opinię!
Jak oceniasz jakość tłumaczenia tej strony?

Transpilacja ansatzu LUCJ

Ważne

Pojęcia z tego przewodnika są dostępne tylko w API Pythona.

Ansatz lokalnego unitarnego klastra Jastrowa (LUCJ) to zwarta, wydajna sprzętowo parametryzacja skorelowanej funkcji falowej elektronów. Należy do ogólniejszej rodziny unitarnego klastra Jastrowa (UCJ) i ma postać

∣Ψ⟩=(∏k=1LUk eiJk Uk†)∣Φ0⟩,\lvert \Psi \rangle = \left(\prod_{k=1}^{L} \mathcal{U}_k\, e^{i \mathcal{J}_k}\, \mathcal{U}_k^\dagger\right) \lvert \Phi_0 \rangle,

gdzie ∣Φ0⟩\lvert \Phi_0 \rangle jest stanem odniesienia (zwykle Hartree-Focka), każde Uk\mathcal{U}_k jest rotacją orbitali, a każde Jk\mathcal{J}_k jest diagonalnym operatorem Coulomba

J=12∑ij,στJijστ niσ njτ,\mathcal{J} = \frac12 \sum_{ij,\sigma\tau} \mathbf{J}^{\sigma\tau}_{ij}\, n_{i\sigma}\, n_{j\tau},

gdzie niσn_{i\sigma} jest operatorem liczby cząstek na orbitalu przestrzennym ii o spinie σ\sigma.

ffsim buduje operator ansatzu; ten pakiet zamienia go w obwód i sprowadza ten obwód do kubitów. Ten przewodnik dotyczy drugiej połowy tego zdania. Powodem, by odbyć tę drogę, jest samo sprowadzanie: własne bramki Qiskit ffsim są specyficzne dla transformacji Jordana-Wignera, podczas gdy FermionicCircuit nie zakłada niczego o kodowaniu fermion-kubit, więc ten sam ansatz można zsyntezować przez dowolne kodowanie odpowiednie dla urządzenia docelowego.

Operator ansatzu trafia więc tutaj gotowy, a wszystko poniżej opisuje, co dzieje się z nim potem: na co rozkłada się bramka, jak jest sprowadzana przez Jordana-Wignera, jak jest dopasowywana do mapy sprzężeń urządzenia i czego nadal potrzebowałoby sprowadzenie inne niż Jordana-Wignera. Aby zbudować, sparametryzować lub zasymulować ansatz UCJ, zobacz własne przewodniki ffsim.

Zobacz także

Przewodnik o relacji z ffsim opisuje, jak oba pakiety dzielą pracę, a przewodnik o transpilacji opisuje etapy potoku użyte poniżej.

1. Uzyskaj operator LUCJ z ffsim​

Ansatz jest inicjalizowany amplitudami z obliczenia metodą sprzężonych klastrów z pojedynczymi i podwójnymi wzbudzeniami (CCSD). Uruchom ograniczoną metodę Hartree-Focka, a następnie CCSD dla cząsteczki wodoru w bazie 6-31g, używając PySCF do obliczeń chemii kwantowej, a potem przekaż amplitudy do from_t_amplitudes() z ffsim. Wykonuje ona podwójną faktoryzację amplitud t2t_2, aby uzyskać diagonalne macierze Coulomba i rotacje orbitali dla każdej warstwy, oraz wyprowadza opcjonalną końcową rotację orbitali z amplitud t1t_1. Liczba powtórzeń LL jest tym, co daje ta faktoryzacja; n_reps ją obcina, wymieniając dokładność na płytszy obwód.

>>> 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)

Tutaj ansatz przestaje być sprawą ffsim. ffsim oferuje tu wybory, których ten pakiet nie widzi i nie musi znać, takie jak wariacyjnie optymalizowana („skompresowana”) faktoryzacja stojąca za optimize=True albo pakowanie wektora parametrów, którym steruje optymalizator wariacyjny. Wszystkie dają ten sam rodzaj operatora, a wszystko poniżej działa bez zmian na każdym z nich.

2. Zamień operator w obwód fermionowy​

Bramka UCJ opakowuje operator i wyraża go jako obwód na modach fermionowych. Bramka jest czystą operacją unitarną bez własnego odniesienia, więc dodaj na początku bramkę InitializeModes (zbudowaną za pomocą from_hartree_fock()), aby dostarczyć stan odniesienia Hartree-Focka, do którego stosowany jest 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)

Rozłożenie obwodu ujawnia jego anatomię i jest to reprezentacja, na której operuje reszta przewodnika. Bramka InitializeModes przygotowuje wyznacznik odniesienia, a każda warstwa ansatzu wnosi OrbitalRotation Uk†\mathcal{U}_k^\dagger, następnie eiJke^{i\mathcal{J}_k} (Evolution diagonalnego operatora Coulomba Jk\mathcal{J}_k), potem Uk\mathcal{U}_k, a na końcu jest końcowa OrbitalRotation. Rotacje orbitali działają osobno na każdy sektor spinowy, więc każda jest umieszczana niezależnie na modach alfa 0..norb i modach beta norb..2*norb.

>>> circuit.decompose().draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The gates that the UCJ ansatz decomposes into.

Każda bramka w tym rozkładzie jest nadal fermionowa: OrbitalRotation i Evolution są zdefiniowane na modach i nie pojawił się jeszcze żaden operator kubitowy ani Pauliego. To właśnie pozostawia kodowanie otwarte i w tym stanie ansatz pozostaje, dopóki etap syntezy go nie wybierze.

Uwaga

Każda warstwa kończy się Uk\mathcal{U}_k, a następna zaczyna się od Uk+1†\mathcal{U}_{k+1}^\dagger, więc sąsiednie bramki OrbitalRotation można by scalić w jedną rotację. Przebieg transpilacji wykonujący to scalanie jest planowanym przyszłym rozwiązaniem.

3. Wybierz kodowanie fermion-kubit​

Obwód jest nadal fermionowy i właśnie w tym miejscu ten pakiet zyskuje swoje miejsce w przepływie pracy. FermionicCircuit nie zakłada niczego o tym, jak mody stają się kubitami, więc kodowanie jest wyborem na etapie syntezy, a nie czymś wpisanym w ansatz. To właśnie ten pakiet dodaje w porównaniu z własnymi bramkami Qiskit ffsim, które są specyficzne dla transformacji Jordana-Wignera.

Dla samego Jordana-Wignera nie potrzebowałbyś tego objazdu: ffsim dostarcza UCJOpSpinBalancedJW i jego odpowiedniki, które samodzielnie zamieniają operator UCJ w obwód kubitowy. Powodem, by prowadzić ansatz przez obwód fermionowy, jest wszystko inne, co może dać kodowanie. Na przykład kodowania lokalne wydają dodatkowe kubity, aby ograniczyć wagę Pauliego każdego wyrazu, co przewodniki o zbiorach przepływu dla 1D i 2D wykorzystują, aby głębokość dwukubitowa kroku Trottera nie zależała od rozmiaru układu.

Konkretnie dla ansatzu UCJ etap syntezy musi obsłużyć dwa rodzaje bramek:

  • Diagonalna ewolucja Coulomba. Evolution jest już niezależna od kodowania: MapperFnEvolutionSynthesis przyjmuje funkcję mappera, więc warstwy eiJke^{i \mathcal{J}_k} są sprowadzane przez dowolne kodowanie, które da się wyrazić w ten sposób. Przewodniki o zbiorach przepływu pokazują, jak taką napisać.

  • Rotacje orbitali i przygotowanie wyznacznika Slatera. Ani OrbitalRotation, ani PrepareSlaterDeterminant, w które początkowe InitializeModes i rotacja się scalają (zobacz niżej), nie jest jako takie odwzorowywane na kubity. Przebieg F2FSynthesis najpierw rozkłada je w przestrzeni fermionowej na bramki Evolution na jednym lub dwóch modach każda (a w przypadku przygotowania także na InitializeModes stanu odniesienia), więc każde kodowanie, które potrafi odwzorować Evolution, sobie z nimi poradzi. W kodowaniu Jordana-Wignera czynniki między sąsiednimi modami stają się na etapie kubitowym obiektami XXPlusYYGate.

  • Inicjalizacja. Wtyczka dla InitializeModes nie jest parametryzowana kodowaniem: dokumentuje założenie kodowania w bazie obsadzeń z odwzorowaniem mod-kubit 1 do 1 i emituje bramki bezpośrednio w tej bazie. Inne kodowanie wymaga własnej wtyczki, a żadna jeszcze nie jest dostarczana.

To, jak transpilować obwody UCJ w kodowaniu innym niż Jordana-Wignera, jest otwartym pytaniem badawczym, a ten pakiet zapewnia ramy, w których można rozwijać taki potok. W pozostałej części przewodnika, jako przykład, budujemy potok transpilatora dla kodowania Jordana-Wignera, równoważny temu, który tworzy generate_preset_jw_pass_manager().

4. Sprowadź go przez Jordana-Wignera​

Sprowadzanie odbywa się w F2QSynthesis, który działa jak HighLevelSynthesis w Qiskit: przechodzi po instrukcjach fermionowych i przekazuje każdą wtyczce zarejestrowanej dla nazwy tej bramki. Jego atrybut F2QSynthesis.methods przechowuje jedną F2QSynthesisPlugin na nazwę bramki, więc zbudowanie etapu oznacza wybranie wtyczki dla każdej bramki fermionowej zawartej w obwodzie:

>>> 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()

Przypisywanie instancji w ten sposób jest też sposobem, w jaki napisana przez ciebie wtyczka trafia do potoku bez konieczności wcześniejszego rejestrowania jej gdziekolwiek.

Tutaj rozstrzyga się kodowanie, na dwa sposoby opisane w sekcji 3: jawnie, przez przekazanie jordan_wigner() do MapperFnEvolutionSynthesis, oraz niejawnie, przez drugą wtyczkę, która już je zakłada.

Ten etap następnie wchodzi do MultiStagePassManager, którego etapy działają w ustalonej kolejności:

>>> 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(),
... )

Etap optymalizacji działa przed syntezą, na obwodzie fermionowym, i właśnie to czyni go wartościowym: przepisuje bramki, dopóki są jeszcze fermionowe i niezależne od kodowania, więc oszczędności przenoszą się na dowolne następujące po nim kodowanie. MergeOrbitalRotations zwija każdy ciąg kolejnych bramek OrbitalRotation w jedną, co ma znaczenie dla ansatzu UCJ, ponieważ każda warstwa kończy się Uk\mathcal{U}_k, a następna zaczyna się od Uk+1†\mathcal{U}_{k+1}^\dagger. MergeSlaterDeterminantPreparation następnie scala początkowe InitializeModes i następującą po nim rotację w jedno PrepareSlaterDeterminant, które synteza sprowadza przez tańszy zredukowany rozkład Slatera zamiast pełnej rotacji kwadratowej. Kolejność ma znaczenie: najpierw scalenie rotacji odsłania pojedynczą rotację tuż po inicjalizacji, którą scalanie Slatera następnie w nią wchłania. F2FSynthesis jest ostatni, ponieważ rozbija każdą pozostałą rotację, a zrobienie tego wcześniej zostawiłoby oba przebiegi scalające bez niczego do scalenia.

Pozostałe etapy to infrastruktura. input i output konwertują między obwodami a DAG-ami, layout przypisuje mody fermionowe do kubitów (TrivialF2QLayout odwzorowuje mod i na kubit i), a qubit to zwykły menedżer przebiegów Qiskit, który przejmuje pracę, gdy nie zostaje już nic fermionowego.

Uruchomienie wymaga jednego kroku przygotowawczego: złożona bramka UCJ musi najpierw zostać rozłożona na bramki pierwotne, aby etap optymalizacji widział poszczególne rotacje i ewolucje.

>>> transpiled = pm.run(circuit.decompose())
>>> print(dict(sorted(transpiled.count_ops().items())))
{'rz': 14, 'rzz': 12, 'x': 2, 'xx_plus_yy': 28}

Bez urządzenia docelowego odwzorowuje to na 2 * norb kubitów przy założeniu łączności każdy z każdym; rotacje orbitali są syntezowane do obiektów XXPlusYYGate, a diagonalne ewolucje Coulomba do obiektów RZZGate:

>>> transpiled.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The Jordan-Wigner transpiled LUCJ circuit.

Ten potok jest dokładnie tym, co składa generate_preset_jw_pass_manager(), więc w praktyce używaj gotowego presetu, a etapy buduj ręcznie, gdy trzeba zmienić któryś z nich:

>>> 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. Dopasuj topologię urządzenia​

Prawdziwe urządzenie ma ograniczoną mapę sprzężeń kubitów, a ansatz LUCJ jest zaprojektowany tak, by do niej pasować. Oddziaływania między tymi samymi spinami (pairs_aa) tworzą dwa łańcuchy liniowe, a oddziaływania alfa-beta (pairs_ab) je łączą. generate_lucj_pass_manager() z ffsim buduje dla tej struktury potok kubitowy świadomy urządzenia i zwraca podzbiór pairs_ab, który sprzęt faktycznie może obsłużyć. Wstaw ten potok w etap qubit presetu, zachowując własną syntezę fermion-kubit tego pakietu:

>>> 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)]

Na tym polega sens takiej kompozycji: etap qubit jest świadomy urządzenia, podczas gdy etap syntezy fermion-kubit przed nim pozostaje własnością tego pakietu, a więc można go zastąpić. Zastąpienie etapu qubit usuwa to, co preset tam rejestruje, czyli HopEvolutionSynthesis, które zamienia czynniki Givensa rotacji orbitali w obiekty XXPlusYYGate. Przekaż je dalej przez hls_config, jak wyżej, albo baza urządzenia musi zostać osiągnięta bez niego.

Gdy pm.qubit jest już ustawiony na potok świadomy urządzenia, uruchomienie menedżera przebiegów rozmieszcza obwód na kubitach backendu i trasuje go do mapy sprzężeń. Dla nieograniczonego ansatzu z kroku 1, którego diagonalny operator Coulomba nadal zawiera wyrazy alfa-beta, do których sprzęt nie ma bezpośredniego dostępu, router musi wstawić wiele bramek SWAP, aby je połączyć:

>>> 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

Ogranicz ansatz do oddziaływań możliwych do zrealizowania sprzętowo

Rozwiązaniem jest przekazanie allowed_pairs_ab z powrotem do konstrukcji ansatzu przez argument interaction_pairs w from_t_amplitudes(), tak aby diagonalny operator Coulomba zawierał tylko wyrazy alfa-beta, które mapa sprzężeń może zrealizować bezpośrednio. Ansatz pasuje wtedy do topologii urządzenia i router prawie nie musi go zmieniać:

>>> 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

Rysowanie tylko aktywnych kubitów (idle_wires=False) pokazuje obwód ograniczony do dwóch łańcuchów spinowych i mostu alfa-beta, wyrażony w bramkach bazowych urządzenia. Rozmieszczenie i trasowanie rozpraszają logiczne mody po fizycznych kubitach urządzenia, więc przekaż wire_order pobrany z końcowego rozmieszczenia obwodu (final_index_layout() wymienia fizyczny kubit, na którym zakończył każdy kubit wejściowy, w kolejności kubitów wejściowych), aby narysować przewody z powrotem w pierwotnej kolejności modów:

>>> 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>

The hardware-restricted LUCJ circuit routed onto the heavy-hex device coupling map.

Uwaga

Dokładna liczba bramek i głębokość po rozmieszczeniu zależą od przebiegów trasowania/optymalizacji i wybranego urządzenia, więc nie podajemy ich tutaj. Kluczowa jest wspólna optymalizacja projektu: wyrażenie ansatzu za pomocą łańcucha pairs_aa między najbliższymi sąsiadami i mostów pairs_ab przefiltrowanych pod kątem sprzętu utrzymuje zsyntezowany obwód blisko topologii urządzenia, minimalizując narzut trasowania (wstawiane bramki SWAP). Zobacz FermionicSlaterGivensDecomposition dla pokrewnego wyboru syntezy (minimize_2q_gate_count), który wymienia liczbę bramek dwukubitowych na głębokość po trasowaniu.

Następne kroki​