Sintetizează o poartă Evolution în spațiul fermionic
Conceptele din acest ghid sunt disponibile momentan doar în API-ul Python. Funcționalitatea echivalentă va fi pusă la dispoziție în API-ul C într-o versiune viitoare.
O poartă Evolution poartă evoluția în timp a unui operator întreg. O sinteză fermion-fermion rescrie acea poartă într-un FermionicCircuit format din porți fermionice mai mici. Ambele părți ale rescrierii rămân în spațiul fermionic, ceea ce o deosebește de sinteza fermion-qubit care urmează: groups ale operatorului (vezi Grupează termenii operatorului: folosește structura operatorului) supraviețuiesc acesteia, în timp ce o mapare fermion-qubit returnează un SparseObservable, care nu are noțiunea de grupare.
Două comenzi selectează metoda. Evolution.synthesis o stabilește pentru o singură poartă, iar trecerea de transpiler F2FSynthesis o stabilește pentru un circuit întreg. Ambele primesc un F2FSynthesisPlugin, interfața pe care o implementează orice astfel de rescriere. Metodele pe care acest pachet le oferă momentan pentru o evoluție sunt formule de produs, pe care le folosește restul acestui ghid: FermionicLieTrotter pentru o formulă de ordinul întâi (cea implicită) și FermionicSuzukiTrotter pentru un ordin par mai mare. Orice altceva care implementează interfața se selectează în același mod, iar secțiunile despre cele două comenzi i se aplică neschimbate.
Acest pas fermion-fermion este opțional. O poartă Evolution poate merge direct la etapa fermion-qubit, care o mapează și o sintetizează întreagă, indiferent câți termeni conține. Împărțirea ei mai întâi este o alegere, făcută fie pentru că porțile rezultate sunt individual mai ieftine, fie pentru că împărțirea expune structură pe care o etapă ulterioară o poate folosi.
Cei doi pași se compun, deci o evoluție poate ajunge aproximată la un nivel, la ambele sau la niciunul. O formulă de produs aproximează, cu excepția cazului în care factorii ei comută; o metodă de sinteză nu trebuie neapărat să aproximeze deloc.
Omiterea acestui pas nu înseamnă neapărat pierderea completă a grupării. Învelirea mapper-ului în group_wise() mapează operatorul grup cu grup și însumează rezultatele, astfel încât termenii Pauli mapați ajung grupați, nu intercalați. O formulă de produs de pe partea qubiților îi sintetizează în ordinea în care îi primește, deci termenii care comută și ajung adiacenți pot fi programați împreună.
Pe Hamiltonianul de mai jos, aceasta recuperează întregul beneficiu de adâncime pe doi qubiți al împărțirii: 18 în loc de 30, la un număr neschimbat de porți. Diferența este unde este exprimată structura. Împărțirea în spațiul fermionic face din fiecare grup o poartă separată, pe care o vede și o poate folosi orice etapă ulterioară; group_wise lasă o singură poartă și doar reordonează termenii din interiorul ei, ceea ce este suficient pentru programare, dar nu lasă nimic cu care o trecere fermionică să lucreze. Folosește-l când evoluția trebuie să rămână o singură poartă, iar în rest apelează la o formulă de produs fermionică.
Construiește un Hamiltonian grupat
Ambele formule de produs împart un operator în factori, iar pentru un operator grupat acești factori sunt grupurile lui. Gruparea determină deci atât costul, cât și acuratețea rezultatului. Să luăm un lanț Fermi-Hubbard cu șase situri, grupat după felul în care comută termenii lui: termenii de salt pe legăturile pare, termenii de salt pe legăturile impare și interacțiunea diagonală.
>>> 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
Cincisprezece termeni în trei grupuri. Fiecare grup de salt asociază fiecare termen cu partenerul său conjugat, ceea ce contează: un factor trebuie să fie hermitian pentru ca exponențiala lui să fie unitară, iar aceasta nu rezultă din faptul că suma este hermitiană. groups_are_hermitian() verifică această convenție, în loc să o lase la inspecție:
>>> from qiskit_fermions.operators.terms.grouping import groups_are_hermitian
>>>
>>> groups_are_hermitian(hamiltonian)
[True, True, True]
Separarea lui de partenerul său ar strica acest lucru, iar lăsarea operatorului negrupat are același efect, deoarece ambele formule îl împart atunci termen cu termen, iar un singur nu este hermitian:
>>> 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
Un astfel de factor este un operator fermionic valid, iar acest pachet va construi poarta pentru el, așa că nimic nu se plânge până când operatorul mapat ajunge la PauliEvolutionGate, care cere coeficienți reali și ridică ValueError: Operator contains complex coefficients, which are not supported. Verificarea grupării dinainte transformă acea eroare tardivă într-un răspuns disponibil înainte de construirea oricărui circuit.
Stabilește metoda pentru o singură poartă
Transmite o metodă de sinteză porții, apoi descompune-o. Fiecare poartă pe care o emite revine ca propria poartă 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
Ordinul întâi emite un factor pe grup. Ordinul doi emite palindromul simetric, deci cele două grupuri exterioare apar de două ori, cu jumătate din timp fiecare, ceea ce dă cinci. Patru repetări ale acestuia ar da douăzeci, dar fiecare repetare se termină cu același grup cu care începe următoarea. Aceste porți vecine evoluează sub același operator, deci sunt fuzionate într-o singură poartă cu timpul însumat, ceea ce economisește o poartă la fiecare îmbinare. Transmite merge_adjacent=False pentru a păstra secvența din manual. Fiecare ordin împarte operatorul în același mod; ceea ce se schimbă este ordinea factorilor și timpii lor de evoluție, nu partiția.
Factorii sunt marcați atomic, deci o nouă descompunere îi lasă pe loc, în loc să îi împartă în continuare. Descompunerea are un punct fix, ceea ce face sigură apelarea repetată a decompose().
Stabilește metoda pentru un întreg pipeline
Stabilirea synthesis pe fiecare poartă înseamnă să transmiți alegerea prin tot ce construiește una, inclusiv UCC și UCJ, care își construiesc porțile intern. Trecerea F2FSynthesis aplică în schimb o singură metodă fiecărei Evolution dintr-un circuit, deci alegerea se face o singură dată pentru pipeline. Ea se configurează după numele porții, la fel ca F2QSynthesis, iar o poartă care nu este numită rămâne așa cum este:
>>> 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
Acesta este același număr pe care decompose() îl dă pentru acea metodă mai sus. Trecerea MergeEvolutions aplică aceeași fuziune porților vecine care nu provin dintr-o singură sinteză, cum sunt îmbinările dintre circuite consecutive. Trecerea execută singură formula, deci nu e nevoie de un pas separat de expandare, iar rularea ei de două ori nu schimbă nimic, deoarece factorii pe care îi emite sunt atomic și deci nu au o definiție proprie.
Transmite apply=False pentru a doar selecta metoda și a lăsa expandarea în seama altcuiva, cum ar fi Decompose din Qiskit (numirea porții ca Decompose("Evolution") îl restrânge la evoluții, lăsând celelalte porți fermionice pe seama propriilor plugin-uri de sinteză).
Acest lucru este util pentru a alege dinainte metoda pentru un circuit întreg, amânând expandarea până în momentul în care e dorită, de exemplu pentru a inspecta mai întâi circuitul:
>>> 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
Fii atent însă la ce face o poartă neexpandată: ajunge întreagă la etapa fermion-qubit, unde este mapată fără ca synthesis să fie consultat. Aceasta duce ușor la o greșeală când compari metode, deoarece fiecare dintre ele produce atunci un rezultat identic, iar comparația nu măsoară în tăcere nimic. Pe Hamiltonianul de mai sus, lăsarea porții de ordinul doi neexpandate prăbușește circuitul mult mai mare de mai jos la costul unei singure evoluții mapate, indiferent ce metodă a fost selectată.
Măsoară câștigul
Un ordin mai mare costă adâncime, deci întrebarea nu este dacă e mai precis, ci dacă e mai precis pe poartă. Sunt necesare două numere pentru fiecare metodă: eroarea stării pe care o produce și costul pe doi qubiți al circuitului care a produs-o.
Eroarea se măsoară cu ffsim, care simulează un FermionicCircuit direct (vezi Cum se raportează acest pachet la ffsim). Nu este implicată nicio mapare fermion-qubit, deci comparația izolează formula de produs fermionică de orice ar putea aduce maparea sau sinteza de pe partea qubiților. Lucrează și în sectorul cu număr fix de particule, care este atât mai mic decât spațiul complet , cât și spațiul în care evoluția rămâne de fapt.
>>> 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
Costul pe doi qubiți este lăsat intenționat în afara acelui tabel. Depinde de sinteza de pe partea qubiților pe care o selectează Qiskit, care se îmbunătățește între versiuni, deci numerele afișate aici ar fi o instantanee, nu o proprietate a formulei de produs. Figura de mai jos le reprezintă în schimb grafic, iar ceea ce se sprijină comparația este afirmat ca o relație:
Citește ultimele două metode împreună, deoarece ele sunt comparația care contează. Ordinul doi cu patru repetări este de aproximativ zece ori mai precis decât ordinul unu cu zece, și ajunge acolo cu mai puține porți de încâlcire, nu mai multe, așa cum arată figura. Aici, cheltuirea unui buget de porți pe un ordin mai mare bate cheltuirea lui pe mai mulți pași de ordinul întâi.
Acest rezultat nu este universal. Creșterea lui reps și creșterea lui order reduc eroarea în ritmuri diferite pentru același tip de cost, iar care câștigă depinde de operator și de timpul evoluat. Cele două rânduri de mai sus sunt modul de a afla pentru o problemă dată.
Formula fermionică se compune cu cea aleasă pentru etapa fermion-qubit (vezi MapperFnEvolutionSynthesis.product_formula). Acolo unde ambele aproximează, acuratețea rezultatului este dictată de cea mai slabă dintre ele, deci creșterea ordinului aici, în timp ce formula de pe partea qubiților rămâne de ordinul întâi, aduce puțin.
Restrânge trecerea la porțile care beneficiază
Un ordin mai mare ajută doar când factorii nu comută. Dacă toți comută, formula de ordinul întâi este deja exactă, iar factorii suplimentari sunt overhead pur. Un operator pur diagonal face acest lucru concret, deoarece operatorii de număr comută între ei:
>>> 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
Ambele circuite sunt exacte, deci al doilea cheltuiește cu 80% mai multe porți de încâlcire degeaba. Operatorii Coulomb diagonali ai unui UCJ au această formă, motiv pentru care există F2FSynthesis.filter: primește un predicat peste DAGOpNode și lasă neatinse porțile respinse, astfel încât o singură trecere poate crește ordinul pentru porțile care beneficiază și poate lăsa restul la ordinul întâi.
Un circuit care conține ambele tipuri de evoluție arată ce se economisește astfel. Predicatul de mai jos acceptă o poartă doar atunci când operatorul ei mută particule între moduri, adică exact atunci când grupurile nu comută și un ordin mai mare are ceva de câștigat:
>>> 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
Porțile pe care filtrul le economisește sunt exact cele pe care ordinul doi le-a cheltuit pe evoluția diagonală, pe care comparația diagonală de mai sus le evaluează la opt (18 față de 10). Acolo nu au adus nimic, deoarece acea evoluție este deja exactă la ordinul întâi, deci eliminarea lor nu costă acuratețe.
Compară cele două circuite de ordinul doi între ele, nu cu cel neexpandat. Lăsarea ambelor porți neexpandate trimite fiecare poartă întreagă la etapa fermion-qubit, ceea ce este greșeala descrisă de nota despre apply=False de mai sus: poate ajunge la același număr de porți de încâlcire ca circuitul filtrat, fiind totuși un circuit diferit, la o adâncime diferită, deci citirea beneficiului filtrului din acel număr nu măsoară nimic.
Niciuna dintre comenzi nu verifică dacă factorii sunt hermitieni, iar un operator grupat incorect poate arăta mai bine la un ordin par decât la ordinul întâi, fiind totuși greșit, deoarece simetrizarea anulează parțial eroarea. Grupează împreună partenerii conjugați, așa cum face fermi_hubbard_chain de mai sus.
Pașii următori
-
Citește Grupează termenii operatorului: folosește structura operatorului pentru a afla cum sunt atribuite grupurile după care se împart aceste formule de produs.
-
Citește Transpilează circuite fermionice pentru etapele prin care trece un
FermionicCircuit. -
Vezi Simulează dinamica Fermi-Hubbard 1D cu seturi de flux pentru un caz lucrat în care formula fermionică furnizează numărul de pași pe care o sinteză personalizată de pe partea qubiților nu îl poate oferi.
-
Consultă
synthesispentru metodele de sinteză în sine șiFermionicQDriftpentru o alternativă randomizată.