Syntéza hradla Evolution ve fermionickém prostoru
Koncepty v tomto průvodci jsou aktuálně dostupné pouze v Python API. Ekvivalentní funkcionalita bude v C API zpřístupněna v budoucím vydání.
Hradlo Evolution nese časový vývoj celého operátoru. Syntéza fermion-na-fermion přepíše toto hradlo na FermionicCircuit menších fermionických hradel. Obě strany přepisu zůstávají ve fermionickém prostoru, čímž se liší od následné syntézy fermion-na-qubit: groups operátoru (viz Seskupování členů operátoru: využití struktury operátoru) ji přežijí, zatímco mapování fermion-na-qubit vrací SparseObservable, který pojem seskupení nezná.
Metodu vybírají dva ovladače. Evolution.synthesis ji nastaví pro jedno hradlo a průchod transpilátoru F2FSynthesis ji nastaví pro celý obvod. Oba přijímají F2FSynthesisPlugin, rozhraní, které implementuje každý takový přepis. Metody, které tento balíček pro evoluci aktuálně poskytuje, jsou součinové formule, což je to, co používá zbytek tohoto průvodce: FermionicLieTrotter pro formuli prvního řádu (výchozí) a FermionicSuzukiTrotter pro vyšší sudý řád. Cokoli jiného, co rozhraní implementuje, se vybírá stejným způsobem a části o obou ovladačích platí beze změny i pro to.
Tento krok fermion-na-fermion je volitelný. Hradlo Evolution může jít rovnou do fáze fermion-na-qubit, která ho namapuje a syntetizuje vcelku, bez ohledu na to, kolik členů obsahuje. Jeho předchozí rozdělení je volba, kterou uděláš, protože výsledná hradla jsou jednotlivě levnější, nebo protože rozdělení odhalí strukturu, kterou může využít pozdější fáze.
Tyto dva kroky se skládají, takže evoluce může skončit aproximovaná na kterékoli úrovni, na obou, nebo na žádné. Součinová formule aproximuje, pokud její činitelé nekomutují; syntézní metoda nemusí aproximovat vůbec.
Vynechání tohoto kroku nemusí znamenat úplnou ztrátu seskupení. Obalení mapperu pomocí group_wise() mapuje operátor po jedné skupině a výsledky sčítá, takže namapované Pauliho členy přicházejí seskupené, nikoli promíchané. Součinová formule na straně qubitů je syntetizuje v pořadí, v jakém je dostane, takže komutující členy, které skončí vedle sebe, lze naplánovat společně.
Na níže uvedeném hamiltoniánu tak získáš celý přínos rozdělení z hlediska dvouqubitové hloubky: 18 místo 30 při nezměněném počtu hradel. Rozdíl je v tom, kde se struktura vyjádří. Rozdělení ve fermionickém prostoru udělá z každé skupiny samostatné hradlo, které vidí a může na něj působit každá další fáze; group_wise ponechá jedno hradlo a pouze přeuspořádá členy uvnitř něj, což stačí pro plánování, ale nezbývá tak nic, s čím by mohl pracovat fermionický průchod. Použij ho, když evoluce musí zůstat jediným hradlem, jinak sáhni po fermionické součinové formuli.
Sestavení seskupeného hamiltoniánu
Obě součinové formule rozdělují operátor na činitele a u seskupeného operátoru jsou těmito činiteli jeho skupiny. Seskupení tedy určuje jak cenu, tak přesnost výsledku. Vezmi šestimístný Fermiho-Hubbardův řetězec, seskupený podle toho, jak jeho členy komutují: členy přeskoku na sudých vazbách, členy přeskoku na lichých vazbách a diagonální interakce.
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> def fermi_hubbard_chain(num_sites, tunneling, interaction):
... """A 1D Fermi-Hubbard chain grouped into even bonds, odd bonds, and the interaction."""
... terms, groups = [], []
... for offset in (0, 1):
... for j in range(offset, num_sites - 1, 2):
... terms += [
... ([cre(j), ann(j + 1)], -tunneling),
... ([cre(j + 1), ann(j)], -tunneling),
... ]
... groups += [offset, offset]
... for j in range(num_sites - 1):
... terms.append(([cre(j), ann(j), cre(j + 1), ann(j + 1)], interaction))
... groups.append(2)
... operator = FermionOperator.from_terms(terms)
... operator.groups = groups
... return operator
>>>
>>> num_sites = 6
>>> hamiltonian = fermi_hubbard_chain(num_sites, tunneling=1.0, interaction=2.0)
>>> hamiltonian.num_groups()
3
Patnáct členů ve třech skupinách. Každá skupina přeskoku páruje každý člen s jeho sdruženým partnerem, na čemž záleží: činitel musí být hermitovský, aby jeho exponenciála byla unitární, a to neplyne z toho, že součet je hermitovský. groups_are_hermitian() tuto konvenci kontroluje, místo aby ji nechávala na posouzení okem:
>>> from qiskit_fermions.operators.terms.grouping import groups_are_hermitian
>>>
>>> groups_are_hermitian(hamiltonian)
[True, True, True]
Oddělení od jeho partnera by ji porušilo a ponechání operátoru neseskupeného má stejný účinek, protože obě formule ho pak rozdělují člen po členu a osamocený není hermitovský:
>>> ungrouped = fermi_hubbard_chain(num_sites, tunneling=1.0, interaction=2.0)
>>> ungrouped.groups = range(len(list(ungrouped.iter_terms()))) # one group per term
>>> all(groups_are_hermitian(ungrouped))
False
Takový činitel je platný fermionický operátor a tento balíček pro něj hradlo sestaví, takže si nic nestěžuje, dokud namapovaný operátor nedorazí k PauliEvolutionGate, který vyžaduje reálné koeficienty a vyvolá ValueError: Operator contains complex coefficients, which are not supported. Kontrola seskupení předem změní toto pozdní selhání na odpověď dostupnou ještě před sestavením jakéhokoli obvodu.
Nastavení metody pro jedno hradlo
Předej hradlu syntézní metodu a poté ho dekomponuj. Každé hradlo, které vydá, se vrátí jako vlastní hradlo Evolution:
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import (
... FermionicLieTrotter,
... FermionicSuzukiTrotter,
... )
>>>
>>> total_time = 1.0
>>>
>>> def factors(synthesis):
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(num_sites, hamiltonian, time=total_time, synthesis=synthesis),
... circuit.modes,
... )
... return circuit.decompose().count_ops()["Evolution"]
>>>
>>> factors(FermionicLieTrotter())
3
>>> factors(FermionicSuzukiTrotter(order=2))
5
>>> factors(FermionicSuzukiTrotter(order=2, reps=4))
17
>>> factors(FermionicSuzukiTrotter(order=2, reps=4, merge_adjacent=False))
20
První řád vydá jednoho činitele na skupinu. Druhý řád vydá symetrický palindrom, takže dvě vnější skupiny se objeví dvakrát, každá s poloviční dobou, což dává pět. Čtyři opakování by daly dvacet, ale každé opakování končí stejnou skupinou, jakou následující začíná. Tato sousední hradla se vyvíjejí podle stejného operátoru, takže se sloučí do jednoho hradla se sečteným časem, což ušetří jedno hradlo na každém švu. Předej merge_adjacent=False, chceš-li zachovat učebnicovou posloupnost. Každý řád rozděluje operátor stejným způsobem; mění se pořadí činitelů a jejich doby evoluce, nikoli rozdělení.
Činitelé jsou označeni atomic, takže další dekompozice je ponechá na místě a nerozdělí je dále. Dekompozice má pevný bod, a proto je bezpečné volat decompose() opakovaně.
Nastavení metody pro celý pipeline
Nastavování synthesis pro každé hradlo zvlášť znamená protlačit volbu vším, co hradla vytváří, včetně UCC a UCJ, které svá hradla konstruují interně. Průchod F2FSynthesis místo toho aplikuje jednu metodu na každé Evolution v obvodu, takže se volba provede jednou pro celý pipeline. Konfiguruje se podle názvu hradla, stejně jako F2QSynthesis, a hradlo, které není uvedeno, zůstane tak, jak je:
>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import F2FSynthesis
>>>
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
>>>
>>> expanded = FermionicPassManager(
... F2FSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2, "reps": 4})})
... ).run(circuit)
>>> expanded.count_ops()["Evolution"]
17
Toto je stejný počet, jaký pro tuto metodu výše dává decompose(). Průchod MergeEvolutions aplikuje stejné sloučení na sousední hradla, která nevznikla jednou syntézou, například na švy mezi po sobě jdoucími obvody. Průchod provede formuli sám, takže není potřeba žádný samostatný krok rozbalení, a jeho dvojí spuštění nic nezmění, protože činitelé, které vydává, jsou atomic a nenesou tedy žádnou vlastní definici.
Předej apply=False, abys metodu pouze vybral a rozbalení přenechal něčemu jinému, například Decompose z Qiskit (pojmenování hradla jako Decompose("Evolution") ho omezí na evoluce a ostatní fermionická hradla ponechá jejich vlastním syntézním pluginům).
To se hodí, když chceš metodu pro celý obvod zvolit předem a rozbalení odložit na okamžik, kdy je potřeba, například abys obvod nejdříve prozkoumal:
>>> selected = FermionicPassManager(
... F2FSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}, apply=False)
... ).run(circuit)
>>> selected.count_ops()["Evolution"] # selected, not yet expanded
1
>>> selected.decompose().count_ops()["Evolution"] # expanded on demand
5
Mysli však na to, co dělá nerozbalené hradlo: dorazí do fáze fermion-na-qubit celé, kde se namapuje bez ohledu na synthesis. To vede k snadné chybě při porovnávání metod, protože každá z nich pak vytvoří identický výstup a srovnání tiše neměří nic. Na výše uvedeném hamiltoniánu nerozbalení hradla druhého řádu sbalí mnohem větší obvod níže na cenu jediné namapované evoluce, ať už byla zvolena jakákoli metoda.
Změření přínosu
Vyšší řád stojí hloubku, takže otázka nezní, zda je přesnější, ale zda je přesnější na jedno hradlo. Pro každou metodu jsou potřeba dvě čísla: chyba stavu, který vytváří, a dvouqubitová cena obvodu, který ho vytvořil.
Chyba se měří pomocí ffsim, který simuluje FermionicCircuit přímo (viz Jak tento balíček souvisí s ffsim). Nepoužívá se žádné mapování fermion-na-qubit, takže srovnání izoluje fermionickou součinovou formuli od čehokoli, co by mohlo přispět mapování nebo syntéza na straně qubitů. Pracuje také v sektoru s pevným počtem částic, který je menší než celý prostor a je to prostor, ve kterém evoluce skutečně zůstává.
>>> import ffsim
>>> import numpy as np
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> norb, nelec = num_sites, (3, 0) # three spinless fermions on six modes
>>> initial = ffsim.hartree_fock_state(norb, nelec)
>>>
>>> hamiltonian_operator = ffsim.linear_operator(hamiltonian, norb=norb, nelec=nelec)
>>> exact = expm_multiply(
... -1j * total_time * hamiltonian_operator,
... initial,
... traceA=-1j * total_time * ffsim.trace(hamiltonian, norb=norb, nelec=nelec),
... )
>>>
>>> def cost_and_error(synthesis):
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(num_sites, hamiltonian, time=total_time, synthesis=synthesis),
... circuit.modes,
... )
... # the method is set on the gate, so decompose() is what expands it here; a pipeline
... # would reach for F2FSynthesis instead (see the note above)
... circuit = circuit.decompose()
... evolved = ffsim.apply_unitary(initial, circuit, norb=norb, nelec=nelec)
... error = 1 - abs(np.vdot(evolved, exact)) ** 2
... qubit_circuit = generate_preset_jw_pass_manager().run(circuit).decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return error, qubit_circuit.count_ops()["cx"], qubit_circuit.depth(two_qubit)
>>>
>>> methods = [
... ("order 1, reps 1 ", FermionicLieTrotter()),
... ("order 2, reps 1 ", FermionicSuzukiTrotter(order=2)),
... ("order 1, reps 10", FermionicLieTrotter(reps=10)),
... ("order 2, reps 4 ", FermionicSuzukiTrotter(order=2, reps=4)),
... ]
>>> costs = {label: cost_and_error(synthesis) for label, synthesis in methods}
>>> labels, counts, depths = [], [], []
>>> for label, (error, gates, depth) in costs.items():
... labels.append(label.strip().replace(", ", "\n"))
... counts.append(gates)
... depths.append(depth)
... print(f"{label} error {error:.6f}")
order 1, reps 1 error 0.515997
order 2, reps 1 error 0.180958
order 1, reps 10 error 0.004026
order 2, reps 4 error 0.000421
Dvouqubitová cena je z této tabulky záměrně vynechána. Závisí na syntéze na straně qubitů, kterou Qiskit vybere a která se mezi vydáními zlepšuje, takže zde vypsaná čísla by byla momentkou, nikoli vlastností součinové formule. Místo toho je vykresluje obrázek níže a to, o co se srovnání opírá, je ověřeno jako vztah:
Přečti si poslední dvě metody společně, protože jsou tím srovnáním, na kterém záleží. Řád dva se čtyřmi opakováními je zhruba desetkrát přesnější než řád jedna s deseti a dosahuje toho s menším, nikoli větším počtem provazujících hradel, jak ukazuje obrázek. Utratit rozpočet hradel za vyšší řád je tu výhodnější než za více kroků prvního řádu.
Tento výsledek není univerzální. Zvyšování reps a zvyšování order snižují chybu různou rychlostí za stejný druh ceny a to, co vyhraje, závisí na operátoru a na vyvíjeném čase. Dva řádky výše ukazují, jak to zjistit pro daný problém.
Fermionická formule se skládá s tou, která je zvolena pro fázi fermion-na-qubit (viz MapperFnEvolutionSynthesis.product_formula). Kde aproximují obě, přesnost výsledku určuje slabší z nich, takže zvýšení řádu zde, zatímco formule na straně qubitů zůstává prvního řádu, přinese málo.
Omezení průchodu na hradla, kterým prospěje
Vyšší řád pomáhá jen tehdy, když činitelé nekomutují. Pokud všichni komutují, formule prvního řádu je už přesná a další činitelé jsou čistá režie. Čistě diagonální operátor to dobře ukazuje, protože operátory počtu částic spolu komutují:
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> diagonal = FermionOperator.from_terms(
... [([cre(j), ann(j), cre(j + 1), ann(j + 1)], 1.0 + 0.1 * j) for j in range(num_sites - 1)]
... )
>>> diagonal.groups = list(range(num_sites - 1))
>>>
>>> def diagonal_cost(synthesis):
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(num_sites, diagonal, time=total_time, synthesis=synthesis), circuit.modes
... )
... transpiled = generate_preset_jw_pass_manager().run(circuit.decompose())
... return transpiled.decompose(reps=6).count_ops()["cx"]
>>>
>>> diagonal_cost(FermionicLieTrotter())
10
>>> diagonal_cost(FermionicSuzukiTrotter(order=2))
18
Oba obvody jsou přesné, takže druhý utratí o 80 % více provazujících hradel zbytečně. Diagonální Coulombovy operátory UCJ mají tento tvar, a proto existuje F2FSynthesis.filter: přijímá predikát nad DAGOpNode a odmítnutá hradla ponechá nedotčená, takže jeden průchod může zvýšit řád u hradel, kterým to prospěje, a zbytek nechat na prvním řádu.
Obvod obsahující oba druhy evoluce ukazuje, kolik se tím ušetří. Predikát níže přijme hradlo jen tehdy, když jeho operátor přesouvá částice mezi módy, což je přesně případ, kdy skupiny nekomutují a vyšší řád má co přinést:
>>> def is_diagonal(operator):
... """Whether every term only counts particles, rather than moving them between modes."""
... return all(
... sorted(mode for created, mode in actions if created)
... == sorted(mode for created, mode in actions if not created)
... for actions, _ in operator.iter_terms()
... )
>>>
>>> def mixed_circuit():
... circuit = FermionicCircuit(num_sites)
... circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
... circuit.append(Evolution(num_sites, diagonal, time=total_time), circuit.modes)
... return circuit
>>>
>>> def two_qubit_cost(circuit):
... transpiled = generate_preset_jw_pass_manager().run(circuit).decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return transpiled.count_ops()["cx"], transpiled.depth(two_qubit)
>>>
>>> order_2 = FermionicSuzukiTrotter(order=2)
>>>
>>> everywhere = two_qubit_cost(
... FermionicPassManager(F2FSynthesis({"Evolution": order_2})).run(mixed_circuit())
... )
>>> restricted = two_qubit_cost(
... FermionicPassManager(
... F2FSynthesis(
... {"Evolution": order_2}, filter=lambda node: not is_diagonal(node.op.operator)
... )
... ).run(mixed_circuit())
... )
>>> everywhere[0] - restricted[0] # the gates the filter saved
8
>>> restricted[0] < everywhere[0] and restricted[1] < everywhere[1]
True
Hradla, která filtr ušetří, jsou přesně ta, která řád dva utratil na diagonální evoluci, což diagonální srovnání výše ocenilo na osm (18 oproti 10). Nic tam nepřinesla, protože ta evoluce je už na prvním řádu přesná, takže jejich vynechání nestojí žádnou přesnost.
Porovnávej dva obvody druhého řádu mezi sebou, nikoli s nerozbaleným. Ponechání obou hradel nerozbalených pošle každé do fáze fermion-na-qubit celé, což je chyba popsaná v poznámce k apply=False výše: může skončit na stejném počtu provazujících hradel jako filtrovaný obvod, přestože jde o jiný obvod s jinou hloubkou, takže odečítat přínos filtru z tohoto čísla neměří nic.
Ani jeden z ovladačů neověřuje, že činitelé jsou hermitovští, a nesprávně seskupený operátor může při sudém řádu vypadat lépe než při prvním, a přitom být stále chybný, protože symetrizace chybu částečně vyruší. Seskup sdružené partnery dohromady, jak to dělá fermi_hubbard_chain výše.
Další kroky
-
Přečti si Seskupování členů operátoru: využití struktury operátoru, kde se dozvíš, jak se přiřazují skupiny, podle kterých se tyto součinové formule dělí.
-
Přečti si Transpilace fermionických obvodů o fázích, kterými
FermionicCircuitprochází. -
Podívej se na Simulace 1D Fermiho-Hubbardovy dynamiky pomocí flow sets, kde je rozpracovaný případ, v němž fermionická formule dodá počet kroků, který vlastní syntéza na straně qubitů dodat nedokáže.
-
Nahlédni do
synthesispro samotné syntézní metody a doFermionicQDriftpro randomizovanou alternativu.