Generování obvodů SqDRIFT
U vzorkové kvantové diagonalizace (SQD) musíš zvolit ansatz, ze kterého se vzorkují bitové řetězce. Varianta SqDRIFT místo toho používá soubor obvodů časového vývoje sestavených přímo z cílového hamiltoniánu. Dosahuje se toho vzorkováním menších operátorů časového vývoje z hamiltoniánu podle jeho koeficientů, což je známo jako trotterizační metoda qDRIFT.
Tento úvodní návod ukazuje, jak vygenerovat soubor takových randomizovaných obvodů.
1. Nastavení hamiltoniánu
Pro účely tohoto návodu načti hamiltonián elektronové struktury N2 ze souboru FCIDUMP. Existují i jiné způsoby, jak sestavit FermionOperator. Určitě se podívej do jeho dokumentace i do qiskit_fermions.operators.library.
- PYTHON
- C
>>> from qiskit_fermions.operators.library import FCIDump
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> fcidump = FCIDump.from_file("docs/guides/n2.fcidump")
>>> num_modes = 2 * fcidump.norb
>>> hamil = FermionOperator.from_fcidump(fcidump)
#include <qiskit_fermions.h>
QfFCIDump* fcidump = NULL;
qf_fcidump_from_file("docs/guides/n2.fcidump", &fcidump);
QfFermionOperator* hamil = qf_ferm_op_from_fcidump(fcidump);
uint32_t num_modes = 2 * qf_fcidump_norb(fcidump);
2. Seskupení členů hamiltoniánu
Využij četné symetrie přítomné v hamiltoniánu elektronové struktury tím, že seskupíš související členy se shodnými koeficienty. Tato akce mění rozdělení koeficientů operátoru, z něhož protokol qDRIFT vzorkuje, ale neovlivňuje jeho záruky konvergence. Seskupení členů souvisejících symetrií především vede k výhodnému vyrušení Pauliho členů, což má za následek celkově menší hloubku obvodu při časovém vývoji stavu pod jejich působením.
Modul qiskit_fermions.operators.terms.grouping poskytuje pomocné funkce pro seskupování členů operátoru. Podrobněji je to vysvětleno v tomto návodu.
Implementace group_terms_by_electronic_structure() předpokládá, že členy hamiltoniánu jsou v normálním uspořádání!
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.grouping import group_terms_by_electronic_structure
>>> from qiskit_fermions.operators.terms.ordering import canonical_order
>>>
>>> canon = canonical_order(hamil.normal_ordered().simplify(atol=1e-16))
>>> exit_code = group_terms_by_electronic_structure(canon, num_modes, two_body_physicist_order=False)
>>> assert exit_code is None
>>> print(canon.groups) # the groups attribute now contains some list of group indices
[0, ...]
QfFermionOperator* normal = qf_ferm_op_normal_ordered(hamil, NULL);
QfFermionOperator* simplified = qf_ferm_op_simplify(normal, 1e-16);
QfFermionOperator* canon = qf_ferm_op_canonical_order(simplified);
QfExitCode exit = qf_ferm_op_group_terms_by_electronic_structure(canon, num_modes, false);
assert(exit == QfExitCode_Success);
qf_ferm_op_free(normal);
qf_ferm_op_free(simplified);
Plný hamiltonián elektronové struktury obsahuje určité členy, jejichž zařazení do obvodu časového vývoje nemá vliv na pozorované bitové řetězce, a proto jen zvyšuje režii vzorkování. Doporučuje se proto tyto členy v tomto bodě z hamiltoniánu odfiltrovat, než v dalším kroku sestavíš hradlo Evolution.
Členy odpovídající tomuto popisu jsou ty, které jsou diagonální v bázi obsazovacích čísel, tedy součiny operátorů počtu (). Patří sem konstantní energetický posun, jehož časový vývoj do obvodu vnáší pouze globální fázi, jednotlivé operátory počtu, jejichž časový vývoj odpovídá jednoqubitovým rotacím Z, i součiny vyššího řádu, například . Žádný z nich nemůže změnit obsazení módu, takže jejich vynechání nestojí žádný excitační obsah. Na rozdíl od zamítání při každém tahu v kroku 5 se toto filtrování provede jednou, přímo na hamiltoniánu, takže rozdělení koeficientů, z něhož protokol vzorkuje, se konzistentně sestaví z filtrovaného operátoru, místo aby se uprostřed tahu renormalizovalo.
Funkce filter_diagonal_terms() takové členy z operátoru odstraní přímo na místě:
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.filtering import filter_diagonal_terms
>>>
>>> filter_diagonal_terms(canon)
qf_ferm_op_filter_diagonal_terms(canon);
Filtrovat zde, jednou, je výrazně levnější než filtrovat opakovaně. FermionicQDrift se spouští jednou pro každý transpilovaný obvod, takže filtrování hamiltoniánu na začátku, místo při každém volání, ušetří opakování práce pro každý z něj vygenerovaný obvod.
Ze stejného důvodu se vyplatí členy hamiltoniánu zde seřadit podle indexu skupiny. Indexy skupin jsou značka přiřazená každému členu, která nic neříká o tom, kde tyto členy leží, takže členy jedné skupiny jsou obecně roztroušeny po celém operátoru: výše použité seskupení přiděluje indexy v pořadí prvního výskytu, což druhý člen skupiny nechává daleko od prvního.
FermionicQDrift vzorkuje celé skupiny a každou vylosovanou skupinu vyhledává pomocí split_out_groups(). Nalezení skupiny mezi roztroušenými členy znamená projít je všechny, takže toto vyhledání stojí stejně, ať se losuje jedna skupina, nebo čtyřicet. group_order() shromáždí každou skupinu do jednoho souvislého úseku, po čemž je vyhledání binárním vyhledáváním přes hranice skupin a stojí tolik, co vylosované skupiny, nikoli tolik, co držené členy. Samotné vyhledání se dramaticky zlevní; kolik se tím u dané transpilace ušetří, závisí na tom, co dalšího pipeline dělá, protože po odstranění prohledávání dominuje mapování a syntéza:
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.ordering import group_order
>>>
>>> canon = group_order(canon)
>>> print(canon.groups[:4])
[0, 0, ...]
QfFermionOperator* grouped = qf_ferm_op_group_order(canon);
Tím se členy přeuspořádají beze změny hodnoty operátoru a stačí to provést jen jednou, bez ohledu na to, kolik randomizací obvodu se z něj vylosuje.
3. Příprava obvodu časového vývoje
Připrav obvod časového vývoje hamiltoniánu a základní obvod, ze kterého se budou losovat vzorky. qiskit_fermions.circuit.library obsahuje všechny potřebné součásti, v souladu s konvencemi Qiskit.
Protokol qDRIFT je syntézní metoda FermionicQDrift a hradlo Evolution ji nese jako svou synthesis. Metoda zavádí několik parametrů, kterými můžeš přizpůsobit generované obvody:
-
Délka každého obvodu v počtu excitačních skupin:
num_groups -
Volitelně počáteční hodnota (seed) generátoru náhodných čísel:
rng -
Volitelně předem vypočtené váhy vzorkování, pokud se úzkým místem stane samotná transpilace:
weights(viz (Volitelné) Předvýpočet vah vzorkování níže)
Protože metodu drží hradlo, cokoli popisuje operátor hradla, například tyto váhy, leží přímo vedle onoho operátoru.
- PYTHON
- C
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import FermionicQDrift
>>>
>>> time = 1.0 # you can choose a desired scaling factor here
>>> num_groups = 10
>>> qdrift = FermionicQDrift(num_groups, rng=19)
>>> evo_gate = Evolution(num_modes, canon, time, synthesis=qdrift)
>>>
>>> circ = FermionicCircuit(num_modes)
>>> circ.append(evo_gate, circ.modes)
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions.
Tento příklad neinicializuje fermionové módy částicemi ani neměří jejich konečný stav.
4. Transpilace obvodu
Modul qiskit_fermions.transpiler se přímo integruje s transpilačním pipeline Qiskit, takže výše sestavený FermionicCircuit lze přímo transpilovat na QuantumCircuit.
Použij mapování fermionů na qubity jordan_wigner(), abys hamiltonián vyjádřený pomocí fermionů převedl na vyjádření pomocí Pauliho řetězců. To lze provést přímo v rámci transpilace použitím transpilačního průchodu F2QSynthesis. Pomocí generate_preset_jw_pass_manager() sestav FermionicStagedPassManager, který zajistí, že se kódování Jordan-Wigner používá konzistentně pro všechny instrukce obvodu.
Především přidej průchod Qiskit Decompose do fáze optimization transpilačního pipeline. Rozloží hradlo Evolution pomocí syntézní metody, kterou hradlo nese, což je výše připravená metoda qDRIFT. Tím se zajistí, že obvod nepoužívá časový vývoj celého hamiltoniánu, jehož hloubka by překročila možnosti aktuálně dostupného hardwaru kvantových počítačů.
Místo toho při každé transpilaci obvodu vzorkuje pevný počet groups členů hamiltoniánu pro každý obvod. Díky tomu můžeš opakovaným spouštěním transpilačního pipeline generovat více randomizací obvodu, jak vyžaduje algoritmus SqDRIFT. Počet generovaných obvodů je num_sqdrift_randomizations.
- PYTHON
- C
>>> from qiskit.transpiler.passes import Decompose
>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> pm.optimization = FermionicPassManager([Decompose(["Evolution"])])
>>>
>>> num_sqdrift_randomizations = 10
>>> sqdrift_circuits = [
... pm.run(circ) for _ in range(num_sqdrift_randomizations)
... ]
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Předchozí příklady zafixují seed rng syntézní metody FermionicQDrift, aby se posloupnost randomizací dala reprodukovat.
Stejný pipeline přijímá metodu qDRIFT ještě na dvou dalších místech. Průchod F2FSynthesis přebírá jednu metodu pro každé hradlo Evolution podle názvu, F2FSynthesis({"Evolution": FermionicQDrift(num_groups)}), což stačí, když žádné hradlo nedrží nic specifického pro svůj operátor. Další krok používá třetí možnost, průchod QDriftTrotterization.
5. Filtrování vylosovaných excitací podle obsazení
Kromě diagonálních členů odfiltrovaných v předchozím kroku může vylosovaná excitace působit výhradně v množině módů, jejichž obsazení je již pevně dané (všechny obsazené nebo všechny neobsazené), kde nemůže přesunout částici z jedné strany na druhou. Taková excitace nic neříká o vzorkovaných bitových řetězcích, takže slot na ni vynaložený je slot zmařený. Průchod QDriftTrotterization, který obaluje stejnou metodu FermionicQDrift, takové excitace při losování zamítá a vylosuje náhradu, takže každý z num_groups slotů výsledného obvodu nese excitaci, která propojuje obě strany.
Toto filtrování není zadarmo, a proto je samostatným průchodem. Šetří rozpočet num_groups vzorkovaných slotů, nikoli hloubku obvodu: protože je tento rozpočet pevný, zamítnutá excitace se nahradí, nikoli vynechá, a levná ustoupí propojující excitaci, jejíž syntéza stojí víc. Počítej s tím, že filtrovaný obvod bude hlubší než nefiltrovaný vylosovaný ze stejného seedu.
Renormalizace losování přes přijaté excitace mění rozdělení, z něhož protokol qDRIFT vzorkuje, a na rozdíl od seskupení v kroku 2 také porušuje záruku konvergence protokolu: vzorkovaný součin už v průměru neodpovídá vývoji pod hamiltoniánem, který jsi zadal. Každá zachovaná excitace skončí s váhou převrácené hodnoty pravděpodobnosti přijetí, zamítnuté nepřispívají ničím a ani celková velikost koeficientů, ani čas vývoje na hradlo se nekorigují.
Pravidlo přijetí je také nadhodnocením. Porovnává pouze nosiče módů, takže nenese žádnou informaci o amplitudách a sledované množiny pouze rostou: excitace omezená na obsazenou množinu je na součinovém stavu skutečně netečná, ale stejná úvaha slábne u pozdějších tahů, jakmile dříve přijaté excitace stav zaplétly a většinu módů označily jako „nejisté“.
Filtrování použij pro postup vzorkování bitových řetězců, který tento návod popisuje, kde obvody vstupují do následného zpracování SQD a záleží jen na vzorkovaných bitových řetězcích. Nepoužívej je, pokud z těchto obvodů zamýšlíš odhadovat očekávanou hodnotu nebo se jinak spoléhat na chybovou mez qDRIFT: tyto výsledky nesou zkreslení, které průchod nekoriguje.
Toto filtrování potřebuje vědět, které módy jsou na začátku obsazené. Proto vyžaduje hradlo InitializeModes předcházející hradla Evolution v obvodu; InitializeModes.from_hartree_fock() je pohodlný způsob, jak ho sestavit. Přidej ho zde pro referenci Hartree-Fock molekuly N2 (sedm elektronů alfa a sedm elektronů beta ve 14 prostorových orbitalech) a porovnej vzorkované excitace s filtrováním a bez něj:
- PYTHON
- C
>>> from qiskit_fermions.circuit.library import InitializeModes
>>> from qiskit_fermions.transpiler.passes import F2FSynthesis, QDriftTrotterization
>>>
>>> init = InitializeModes.from_hartree_fock(fcidump.norb, (7, 7))
>>>
>>> hf_circ = FermionicCircuit(num_modes)
>>> hf_circ.append(init, hf_circ.modes)
>>> hf_circ.append(Evolution(num_modes, canon, time), hf_circ.modes)
>>>
>>> num_groups = 5
>>> qdrift_unfiltered = F2FSynthesis(
... {"Evolution": FermionicQDrift(num_groups, rng=3480)}
... )
>>> qdrift_trivial = QDriftTrotterization(
... FermionicQDrift(num_groups, rng=3480)
... )
>>>
>>> for instruction in FermionicPassManager(qdrift_unfiltered).run(hf_circ)._inner.data:
... if instruction.operation.name == "Evolution":
... print(sorted(instruction.operation.operator.get_support()))
[2, 4]
[41, 45, 52]
[15, 45, 55]
[41, 52, 53]
[10, 16, 37, 38]
>>>
>>> for instruction in FermionicPassManager(qdrift_trivial).run(hf_circ)._inner.data:
... if instruction.operation.name == "Evolution":
... print(sorted(instruction.operation.operator.get_support()))
[0, 1, 6, 7]
[0, 1, 28, 29]
[4, 13, 55]
[13, 20, 40, 41]
[0, 1, 28, 29]
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Žádná z excitací vylosovaných bez filtrování se obsazené množiny (0-6 a 28-34) vůbec nedotýká, takže žádná z nich nemůže přesunout částici mezi obsazeným a neobsazeným módem. S filtrováním je všech pět zamítnuto a nahrazeno excitacemi, které obsazený mód s neobsazeným skutečně propojují. Například první přijatá excitace [0, 1, 6, 7] přesouvá částici mezi obsazenými módy 0, 1 a 6 a neobsazeným módem 7.
Jakmile je excitace přijata, každý mód v jejím nosiči se stane „nejistým“ a tím pádem může hrát kteroukoli roli pro pozdější vzorky, takže množiny obsazených a neobsazených módů s dalšími přijatými excitacemi dál rostou. Právě proto je druhá excitace [0, 1, 28, 29] přijatelná. Všechny její čtyři módy patří mezi původně obsazené, takže se nepropojuje s žádným původně neobsazeným módem. Je přijata jen proto, že módy 0 a 1 se staly nejistými (a tedy vhodnými jako „neobsazená“ strana propojení), jakmile se jich dotkla první excitace.
Tento růst způsobuje, že pravidlo je spíše nadhodnocením než testem: s přibývajícími nejistými módy je zamítáno méně tahů a filtrování nejvíce ovlivňuje rozdělení vzorkování u nejranějších tahů.
Chceš-li posoudit, jak daleko filtrování rozdělení posunulo, přečti si počty zamítnutí, které průchod zaznamenává do metadat obvodu. Ukládá se jeden záznam na každé filtrované hradlo Evolution a poměr přijatých k celkovým odhaduje pravděpodobnost přijetí, tedy faktor, kterým byly zachované excitace nadvažovány:
- PYTHON
- C
>>> # a freshly seeded pass, so the counts do not depend on earlier draws
>>> qdrift_counted = QDriftTrotterization(
... FermionicQDrift(num_groups, rng=3480)
... )
>>> filtered = FermionicPassManager(qdrift_counted).run(hf_circ)
>>> discarded = filtered.metadata.get("filter_trivial.discarded")
>>> emitted = filtered.metadata.get("filter_trivial.emitted")
>>> print(f"{emitted[0]} kept, {discarded[0]} discarded")
5 kept, 7 discarded
>>> print(f"acceptance probability: {emitted[0] / (emitted[0] + discarded[0]):.2f}")
acceptance probability: 0.42
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Filtrování potřebuje informace o obsazení, podle kterých filtruje. Hradlo PrepareSlaterDeterminant je zadává stejně jako InitializeModes a každá OrbitalRotation označí každý mód, na který působí, jako „nejistý“. Bez kteréhokoli z nich, nebo když všechny zadané módy skončí na jedné straně (všechny obsazené nebo všechny neobsazené), průchod vyvolá UserWarning a vzorkování pro dané hradlo Evolution ponechá nefiltrované. U hamiltoniánu, jehož zbývající členy už nemohou propojit obě sledované množiny, průchod místo toho po QDriftTrotterization.MAX_SAMPLE_RETRIES po sobě jdoucích zamítnutích vyvolá RuntimeError.
(Volitelné) Předvýpočet vah vzorkování
Sáhni po tom, jen pokud je úzkým místem samotná transpilace. Na získaných obvodech to nic nemění.
FermionicQDrift losuje každou skupinu s pravděpodobností úměrnou velikosti jejího koeficientu a tyto velikosti při každém volání odvozuje z hamiltoniánu: jednu hodnotu na člen hamiltoniánu, zredukovanou na jednu na skupinu. Protože je metoda bezstavová, tuto redukci opakuje pro každou randomizaci, přestože je výsledek pokaždé stejný. Argument weights ti umožňuje vypočítat ji jednou pomocí group_coeff_means() a výsledek předat, čímž se náklady přesunou ze smyčky ven, přičemž metoda zůstane bez jakéhokoli uloženého stavu závislého na hamiltoniánu.
Je to protějšek uspořádání členů z kroku 2. To vynáší vyhledávání skupin ze smyčky, tohle redukci vah, a každé je znatelné jen tak, jak chybí to druhé, takže se vyplatí použít obě společně. Protože canon byl už výše přeuspořádán, oba kroky na sebe přímo navazují.
Váhy popisují skupiny jednoho konkrétního operátoru, takže patří k hradlu Evolution onoho operátoru: sestav metodu pro toto hradlo a předej ji jako synthesis hradla, přesně jako v kroku 3.
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.grouping import group_coeff_means
>>>
>>> weights = group_coeff_means(canon) # `canon` is group-ordered, from step 2
>>> qdrift_weighted = FermionicQDrift(10, rng=19, weights=weights)
>>> weighted_gate = Evolution(num_modes, canon, time, synthesis=qdrift_weighted)
>>>
>>> weighted_circ = FermionicCircuit(num_modes)
>>> weighted_circ.append(weighted_gate, weighted_circ.modes)
>>>
>>> weighted_circuits = [
... pm.run(weighted_circ) for _ in range(num_sqdrift_randomizations)
... ]
>>> len(weighted_circuits)
10
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Protože jde přesně o váhy, které by průchod vypočítal sám, jsou obvody stejné jako dříve; zmizela jen opakovaná redukce.
Očekává se jeden záznam na skupinu, nikoli na člen, kdykoli operátor nese skupiny, protože seskupený operátor se vzorkuje po skupinách. Záznamy musí být nezáporné: váha je velikost, protože protokol qDRIFT rozkládá s kladnými a znaménko koeficientu patří do , odkud ho průchod čte přímo z operátoru.
Váhy jsou absolutní velikosti, nikoli relativní preference. Jejich součet také určuje čas vývoje, takže vynásobení každého záznamu hodnotou vzorkuje stejně, ale vyvíjí se po dobu místo . Zadání čehokoli jiného než vlastních velikostí koeficientů hamiltoniánu proto mění, jaký vývoj soubor aproximuje, a proto je to nástroj pro výkon, nikoli způsob ladění vzorkování.
Protože váhy patří k hradlu, jehož operátor popisují, může obvod obsahovat několik hradel Evolution, každé s vahami svého vlastního operátoru. Průchod QDriftTrotterization z kroku 5 je respektuje stejným způsobem: hradlo losuje pomocí FermionicQDrift, které hradlo nese, a u hradel, která žádnou nenesou, se vrací k té, se kterou byl sestaven. Jiný druh syntézní metody na hradle není tvrzením o qDRIFT, takže ji průchod ignoruje. Předání metody průchodu podle názvu hradla, jako v F2FSynthesis({"Evolution": ...}), uplatní jednu metodu na každé hradlo Evolution, takže zde zadané váhy dávají smysl jen u obvodu s jediným takovým hradlem. Délka vah se kontroluje vůči každému hradlu, na které se použijí.
(Volitelné) Optimalizace indexování fermionových módů
Do transpilačního pipeline můžeš přidat další optimalizační krok, který minimalizuje vzdálenost nosičů fermionových excitací přeznačením indexů fermionových módů. Tato optimalizace byla zavedena v článku o SqDRIFT a je implementována funkcí build_excitation_span_minimization_model(). Do transpilačního pipeline ji lze snadno vložit pomocí průchodu RelabelModes:
- PYTHON
- C
>>> from pyomo.environ import SolverFactory
>>> from qiskit_fermions.transpiler.passes import RelabelModes
>>>
>>> solver = SolverFactory("appsi_highs")
>>> solver.options["time_limit"] = 10
>>>
>>> qdrift = F2FSynthesis({"Evolution": FermionicQDrift(5, rng=19)})
>>> relabel = RelabelModes(solver=solver)
>>>
>>> pm.optimization = FermionicPassManager([qdrift, relabel])
>>>
>>> relabeled_circ = pm.run(circ)
>>> # if the automatic mode relabeling was successful, the circuit's
>>> # metadata will contain the mode `permutation` information
// WARNING: This feature is not available via the C API.
Použití automatické optimalizace uvnitř RelabelModes (která využívá build_excitation_span_minimization_model()) vyžaduje volitelnou závislost spravovanou pomocí HAS_PYOMO.
Přeznačení mění, který mód daný qubit nese, takže diagonalizace podprostoru získá správné bitové řetězce, jen pokud se to zohlední. Kde se to zohledňuje, závisí na tom, kde měříš.
Přidej měření do FermionicCircuit pomocí FermionicCircuit.measure_all() před transpilací a není co dělat: každé měření pojmenovává mód, který čte, takže ho přeznačení přenese s sebou a vzorkované bitové řetězce jsou indexovány původním módem.
Pokud místo toho měříš transpilovaný QuantumCircuit, musí se bitové řetězce následně zpracovat pomocí permutation zaznamenané v metadatech obvodu. Postup najdeš v dokumentaci RelabelModes.
Další kroky
Nyní, když jsi úspěšně vygeneroval soubor obvodů, můžeš z nich vzorkovat bitové řetězce. K tomu se musí obvody spustit na hardwaru. Podrobné pokyny najdeš v dokumentaci Qiskit.
Jakmile získáš vzorky bitových řetězců, můžeš je v kombinaci s koeficienty hamiltoniánu použít k následnému zpracování SQD, jak je vysvětleno v tutoriálech doplňku SQD.