Simulează dinamica Fermi-Hubbard 1D cu seturi de flux
Funcțiile descrise în acest ghid sunt disponibile în prezent doar în API-ul Python. Funcționalitatea echivalentă va fi disponibilă în API-ul C într-o versiune viitoare.
Acest ghid construiește un circuit pentru dinamica temporală a modelului Fermi-Hubbard unidimensional, de la scrierea hamiltonianului până la verificarea densităților de sit evoluate. Ceea ce face ruta interesantă este o singură idee, preluată din cadrul flow set al lui Gandon et al. [1], lucrarea care a motivat și TransferVertexOperator din acest pachet:
În loc să grupezi termenii hamiltonianului după maparea fermion-qubit, grupează-i înainte de ea, în seturi de flux, submulțimi unidimensionale ale grafului orientat al interacțiunilor fermionice ai căror operatori de transfer comută reciproc.
Această alegere determină tot ce urmează. Ea necesită următoarele:
-
O codificare fermion-qubit personalizată adaptată seturilor de flux, care aici consumă un qubit ancilă, astfel încât numărul de qubiți asociat implementării unui anumit termen hamiltonian nu mai crește odată cu numărul de moduri fermionice (cum este cazul la Jordan-Wigner (JW)).
-
O sinteză personalizată care exploatează comutativitatea din fiecare set. Câștigul este un circuit a cărui adâncime cu două qubiți este constantă în raport cu dimensiunea sistemului, în comparație cu creșterea liniară a unei trotterizări termen cu termen bazate pe JW.
Mecanismul care introduce o codificare proprie în transpiler (CustomF2QLayout și MapperFnEvolutionSynthesis) se aplică oricărui model.
Ghidul de transpilare pentru etapele transpiler la care se face referire pe parcurs și ghidul mappers pentru rețeta generală de scriere a unui mapper personalizat.
Operatori de transfer și seturi de flux
Un TransferVertexOperator este construit din operatori de vârf și operatori de transfer . În termeni de operatori fermionici de creare și anihilare,
deci măsoară ocuparea modului (este când este gol și când este ocupat), în timp ce transferă un fermion de-a lungul muchiei orientate . Reține că cei doi indici ai lui nu sunt interschimbabili. Primul indice poartă combinația cu minus, iar al doilea pe cea cu plus, ceea ce face distinct de și dă muchiei orientarea sa.
Aceștia satisfac relații de comutare mixte. Doi operatori de transfer care acționează pe situri disjuncte comută trivial, deoarece fiecare este un produs par de operatori fermionici. Cazul interesant este un sit comun. Acolo, cei doi comută dacă săgețile curg prin acel sit (una sosește, una pleacă) și anticomută dacă se ciocnesc (ambele sosesc sau ambele pleacă).
Regula se vede cel mai ușor pe o imagine. Desenarea vârfurilor ca noduri și a fiecărui ca o săgeată de la la dă graful de interacțiune orientat, aici pentru lanțul cu patru situri folosit în tot acest ghid, care conține ambele orientări ale fiecărei legături:
Urmărește regula pe figură. Uită-te, de exemplu, la și . O săgeată sosește la situl 2 și cealaltă îl părăsește, deci fermionul curge direct prin el, iar cei doi operatori comută. Uită-te în schimb la și . Ambele săgeți indică spre situl 2. Se ciocnesc, iar cei doi anticomută.
Aceasta face posibile seturile de flux. Alege un set de săgeți care formează o cale orientată și fiecare pereche fie se întâlnește cap la coadă, fie nu se întâlnește deloc, deci întregul set comută, chiar dacă operatorii individuali se suprapun pe situri comune. Evoluția sub un singur set de flux nu implică, așadar, nicio eroare Trotter și, după cum arată pasul 7, poate fi realizată și la adâncime constantă a circuitului.
1. Hamiltonianul Fermi-Hubbard
Modelul Fermi-Hubbard 1D pe situri (fără spin, condiții la limită deschise) este
Urmând Ec. (9) din Ref. [1], termenul de salt este . Interacțiunea este diagonală, deci este exprimată prin operatori de vârf folosind :
Ambele părți sunt construite direct ca un TransferVertexOperator. Reține că este stocat ca intrarea diagonală (j, j):
>>> from collections import defaultdict
>>> from qiskit_fermions.operators import TransferVertexOperator
>>>
>>> def fermi_hubbard_1d(num_sites, tunneling, interaction):
... data: defaultdict[tuple[tuple[int, int], ...], complex] = defaultdict(complex)
... for j in range(num_sites - 1):
... k = j + 1
... # hopping: -t (a^dag_j a_k + a^dag_k a_j) = t (T_jk + T_kj)
... data[((j, k),)] += tunneling
... data[((k, j),)] += tunneling
... # interaction: U n_j n_k with n_j = (1 - V_j) / 2
... data[()] += interaction / 4
... data[((j, j),)] -= interaction / 4
... data[((k, k),)] -= interaction / 4
... data[((j, j), (k, k))] += interaction / 4
... return TransferVertexOperator.from_dict(data)
>>>
>>> num_sites = 4
>>> hamiltonian = fermi_hubbard_1d(num_sites, tunneling=1.0, interaction=2.0)
>>> print(format(hamiltonian))
1.500000e0 +0.000000e0j * ()
-5.000000e-1 +0.000000e0j * (V(0))
5.000000e-1 +0.000000e0j * (V(0) V(1))
1.000000e0 +0.000000e0j * (T(0,1))
1.000000e0 +0.000000e0j * (T(1,0))
-1.000000e0 +0.000000e0j * (V(1))
5.000000e-1 +0.000000e0j * (V(1) V(2))
1.000000e0 +0.000000e0j * (T(1,2))
1.000000e0 +0.000000e0j * (T(2,1))
-1.000000e0 +0.000000e0j * (V(2))
5.000000e-1 +0.000000e0j * (V(2) V(3))
1.000000e0 +0.000000e0j * (T(2,3))
1.000000e0 +0.000000e0j * (T(3,2))
-5.000000e-1 +0.000000e0j * (V(3))
Siturile interioare primesc un coeficient de pe deoarece apar în două legături, în timp ce siturile de la margine apar doar într-una. Fiecare termen listat aici apare în graful desenat mai sus. Termenii T(j,k) sunt săgețile sale, iar termenii V(j) nodurile sale.
2. Partiționează în seturi de flux
În continuare, etichetează fiecare termen cu setul de flux căruia îi aparține. Atributul groups există în acest scop. O poartă Evolution peste un operator grupat se descompune într-un Evolution pentru fiecare grup (pasul 6 pune aceasta în practică), astfel încât gruparea decisă aici la nivel fermionic supraviețuiește în circuit.
Pentru lanțul 1D, cele două seturi de flux de salt sunt săgețile orientate spre est () și cele orientate spre vest (). Termenii diagonali de interacțiune comută cu tot ce este diagonal și formează un al treilea grup:
>>> def flow_set_groups(operator):
... groups = []
... for terms, _ in operator.iter_terms():
... match terms:
... case [(left, right)] if right == left + 1:
... groups.append(0) # east-oriented: T_{j,j+1}
... case [(left, right)] if right == left - 1:
... groups.append(1) # west-oriented: T_{j+1,j}
... case _:
... groups.append(2) # diagonal interaction terms
... return groups
>>>
>>> hamiltonian.groups = flow_set_groups(hamiltonian)
>>> hamiltonian.num_groups()
3
>>> for index, group in enumerate(hamiltonian.split_out_groups()):
... print(index, sorted(terms for terms, _ in group.iter_terms()))
0 [[(0, 1)], [(1, 2)], [(2, 3)]]
1 [[(1, 0)], [(2, 1)], [(3, 2)]]
2 [[], [(0, 0)], [(0, 0), (1, 1)], [(1, 1)], [(1, 1), (2, 2)], [(2, 2)], [(2, 2), (3, 3)], [(3, 3)]]
Grupurile 0 și 1 sunt cele două seturi de flux. Fiecare set este o cale orientată de-a lungul lanțului, deci, prin proprietatea de flux, toți termenii dintr-un grup comută.
Fiecare grup trebuie, de asemenea, să fie hermitian de sine stătător, deoarece o formulă de produs exponențiază grupurile separat și doar un factor hermitian are o evoluție unitară. groups_are_hermitian() confirmă aceasta:
>>> from qiskit_fermions.operators.terms.grouping import groups_are_hermitian
>>>
>>> groups_are_hermitian(hamiltonian)
[True, True, True]
Merită să ne oprim aici, deoarece cele două grupuri de salt sunt unidirecționale: grupul 0 conține doar termeni orientați spre est, iar grupul 1 doar termeni orientați spre vest. Împărțirea unui hamiltonian în acest mod ar rupe în mod normal hermiticitatea, deoarece are nevoie de partenerul său conjugat alături. Un TransferVertexOperator evită problema deoarece este deja hermitian de sine stătător. Este un produs de doi operatori Majorana (vezi definiția sa la pasul 1), nu un simplu , deci imaginea sa Jordan-Wigner poartă un coeficient real, nu unul imaginar. Alegerea acestei reprezentări a operatorului este, așadar, ceea ce face ca un set de flux unidirecțional să fie un factor Trotter legitim.
Etichetele est/vest folosite în acest ghid sunt geometrice, cu lanțul desenat de la stânga la dreapta și indicii modurilor crescând în acea direcție; indică spre est, iar spre vest. Ref. [1] este inconsistentă intern în privința acestei denumiri (textul și figurile sale nu concordă), așa că acest ghid nu urmează în mod deliberat textul, ci păstrează etichetele din imagine. Când citești în paralel cu lucrarea, potrivește seturile de flux după ponderile lor Pauli (1 și 3 mai jos), nu după cuvintele de orientare.
3. Scrie codificarea personalizată
Secțiunea IV B din Ref. [1] clasifică formele pe care le poate lua o codificare fermion-qubit locală când este restricționată la un set de flux. Jordan-Wigner simplu folosește qubiți și mapează fiecare termen de salt într-un Pauli de pondere 2. Clasificarea arată că folosirea unor qubiți suplimentari cumpără structură în operatorii codificați.
Codificarea implementată aici este construcția cu paritate delocalizată din Anexa B 2 (ecuațiile B4 și B5), care folosește un qubit ancilă, deci . Paritatea fermionică este delocalizată pe o pereche de qubiți:
Indicele din ultima expresie impune qubitul suplimentar. Ultima legătură a lanțului are , deci al ei ajunge pe qubitul , unul după ultimul mod fermionic . Acel qubit final este ancila și este motivul pentru care și nu . Aceeași extindere apare și în , al cărui se află pe ancilă pentru .
Câștigul este vizibil imediat; un întreg set de flux este mapat într-o sumă de Pauli de pondere 1.
Prefactorii sunt fixați de normalizarea , iar semnul minus relativ dintre cele două orientări este fixat de identitatea produsului , pe care o satisfac Paulii de mai sus. Dacă oricare dintre ele este greșită, rezultă un operator care respectă relațiile de comutare, dar nu mai reprezintă același hamiltonian, deci ar trebui să verifici codificarea numeric, așa cum se arată la pasul 4.
Scrierea codificării înseamnă scrierea unei funcții care mapează un singur operator de transfer generalizat într-un șir Pauli; map_transfer_vertex_generators() se ocupă de iterarea peste termeni și de compunerea lor:
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_transfer_vertex_generators
>>>
>>> def flow_set_action(action, num_qubits):
... match action:
... case (j, k) if j == k:
... # vertex operator: V_j = Z_j Z_{j+1}
... return SparseObservable.from_sparse_list(
... [("ZZ", [j, j + 1], 1.0)], num_qubits=num_qubits
... )
... case (j, k) if k == j + 1:
... # east-oriented transfer operator: weight 1
... return SparseObservable.from_sparse_list(
... [("X", [k], -0.5)], num_qubits=num_qubits
... )
... case (j, k) if k == j - 1:
... # west-oriented transfer operator: weight 3
... return SparseObservable.from_sparse_list(
... [("ZXZ", [k, k + 1, k + 2], 0.5)], num_qubits=num_qubits
... )
... case _:
... raise ValueError(f"not a nearest-neighbor transfer operator: {action}")
>>>
>>> def flow_set_encoding(operator, num_qubits):
... return map_transfer_vertex_generators(
... operator,
... lambda action: flow_set_action(action, num_qubits),
... identity=lambda: SparseObservable.identity(num_qubits),
... compose=SparseObservable.compose,
... ).simplify()
MapperFnEvolutionSynthesis.mapper_fn se așteaptă la semnătura (operator, num_qubits), iar tipul returnat trebuie să fie un SparseObservable, ceea ce face funcția direct utilizabilă ca plugin de transpiler. Vezi pasul 6.
Aplicarea ei hamiltonianului de la pasul 1 dă operatorul codificat pe qubiți:
>>> num_qubits = num_sites + 1
>>> encoded = flow_set_encoding(hamiltonian, num_qubits)
>>> for label, indices, coeff in sorted(encoded.to_sparse_list()):
... print(f"{label:3s} {str(indices):12s} {coeff.real:+.2f}")
[] +1.50
X [1] -0.50
X [2] -0.50
X [3] -0.50
ZXZ [0, 1, 2] +0.50
ZXZ [1, 2, 3] +0.50
ZXZ [2, 3, 4] +0.50
ZZ [0, 1] -0.50
ZZ [0, 2] +0.50
ZZ [1, 2] -1.00
ZZ [1, 3] +0.50
ZZ [2, 3] -1.00
ZZ [2, 4] +0.50
ZZ [3, 4] -0.50
Sunt 14 termeni, în trei forme: saltul a devenit X și ZXZ, interacțiunea ZZ, iar [] de la început este identitatea, care contribuie doar cu o fază globală. Reține că termenii ZZ ajung la vecinii apropiați și la cei următori; se întinde pe qubiții de la până la , ceea ce este prețul delocalizării parității.
Gruparea după setul de flux scoate la iveală asimetria pe care o cumpără construcția. Listează ponderile Pauli prezente în fiecare grup:
>>> for index, group in enumerate(hamiltonian.split_out_groups()):
... weights = {len(indices) for _, indices, _ in flow_set_encoding(group, num_qubits).to_sparse_list()}
... print(index, sorted(weights))
0 [1]
1 [3]
2 [0, 2]
Setul de flux orientat spre est (grupul 0) constă în întregime din Pauli de pondere 1. Evoluția sa în timp este, așadar, un strat de rotații cu un singur qubit și nu necesită nicio poartă de încâlcire, în timp ce sub Jordan-Wigner fiecare termen de salt are pondere 2 și le necesită. Acesta este compromisul spațiu-timp din Ref. [1] în forma sa cea mai simplă: un qubit suplimentar transformă jumătate din hamiltonianul de salt în rotații cu un singur qubit.
Setul de flux orientat spre vest (grupul 1) plătește pentru aceasta la pondere trei, iar interacțiunea (grupul 2) se află la pondere doi, unde intrarea de pondere zero este identitatea. Nimic din aceasta nu este încă o afirmație despre circuit; ceea ce face o sinteză din aceste ponderi este subiectul pașilor 6 și 7.
4. Verifică codificarea
O codificare personalizată este utilă doar dacă este fidelă, iar una scrisă de mână trebuie verificată. Deoarece această codificare folosește qubiți, spațiul Hilbert al qubiților este de două ori mai mare decât cel fermionic, deci hamiltonianul codificat nu poate fi egal cu cel Jordan-Wigner termen cu termen. Cele două sunt legate în schimb printr-o izometrie.
De unde provine izometria: Sub Jordan-Wigner o stare de bază este șirul de ocupare; qubitul deține . Această codificare stochează în schimb parități cumulative. Definește
astfel încât bitul înregistrează paritatea tuturor ocupărilor până la situl inclusiv. Există astfel de biți pentru ocupări, de unde provine ancila. Ocuparea este recuperată ca diferență dintre biții vecini, . Un sit ocupat apare, așadar, ca un perete de domeniu între doi biți adiacenți, motiv pentru care aceasta se numește codificare cu perete de domeniu (sau Kramers-Wannier). Tot de aceea : produsul a doi vecini citește acea diferență.
Scrierea explicită a acestei aplicații pentru câteva stări face structura concretă (observă că este fixat, deci doar jumătate din stările de qubit se află în imagine, și de aceea avem o izometrie, nu o transformare unitară):
>>> def domain_wall_bits(occupations):
... parity = 0
... bits = [0]
... for occupation in occupations:
... parity ^= occupation
... bits.append(parity)
... return bits
>>>
>>> for occupations in ([0, 0, 0, 0], [1, 0, 0, 0], [1, 1, 0, 0], [1, 0, 1, 0]):
... bits = domain_wall_bits(occupations)
... # little-endian: reversed() prints qubit 0 on the right, as Qiskit labels do
... print("".join(map(str, occupations)), "->", "".join(str(b) for b in reversed(bits)))
0000 -> 00000
1000 -> 11110
1100 -> 00010
1010 -> 00110
Un singur fermion pe situsul 0 inversează fiecare paritate cumulativă de deasupra lui, ceea ce reprezintă delocalizarea în acțiune. Adunând aceste coloane într-o matrice obținem izometria , iar codificarea este fidelă atunci când intercalează cei doi hamiltonieni:
Adică, aplicarea unei stări fermionice în spațiul qubiților și evoluția ei acolo trebuie să dea același rezultat ca evoluția ei mai întâi în spațiul fermionic și aplicarea ulterioară.
>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp
>>> from qiskit_fermions.mappers.library import transfer_vertex_jordan_wigner
>>>
>>> def to_matrix(observable):
... # SparseObservable carries no dense-matrix method, so route through
... # SparsePauliOp. This is only ever needed for the checks below --
... # the encoding itself never leaves SparseObservable.
... return SparsePauliOp.from_sparse_observable(observable).to_matrix()
>>>
>>> def domain_wall_isometry(num_sites):
... isometry = np.zeros((2 ** (num_sites + 1), 2**num_sites), dtype=complex)
... for index in range(2**num_sites):
... occupations = [(index >> j) & 1 for j in range(num_sites)]
... bits = domain_wall_bits(occupations)
... isometry[sum(b << j for j, b in enumerate(bits)), index] = 1.0
... return isometry
>>>
>>> isometry = domain_wall_isometry(num_sites)
>>> np.allclose(isometry.conj().T @ isometry, np.eye(2**num_sites))
True
>>>
>>> def intertwines(operator):
... """Whether both mappings of `operator` agree through the isometry."""
... flow = to_matrix(flow_set_encoding(operator, num_qubits))
... reference = to_matrix(transfer_vertex_jordan_wigner(operator, num_sites).simplify())
... return np.allclose(flow @ isometry, isometry @ reference)
>>>
>>> intertwines(hamiltonian)
True
>>> # and, more strongly, once per flow set: a product formula evolves the groups
>>> # separately, so each one has to be intertwined on its own
>>> all(intertwines(group) for group in hamiltonian.split_out_groups())
True
Relația de intercalare se verifică exact și separat pentru fiecare set de flux. Această a doua afirmație, mai puternică, este cea pe care se bazează pașii următori: o formulă de produs evoluează grupurile pe rând, iar un produs de factori intercalați este el însuși intercalat, deci orice aproximare construită din aceste grupuri concordă între cele două codificări, chiar dacă codificările diferă. Drept corolar, spectrele trebuie să coincidă, fiecare valoare proprie Jordan-Wigner apărând de două ori în spațiul mai mare, o dată pentru fiecare valoare a parității globale neconstrânse:
>>> jw_matrix = to_matrix(transfer_vertex_jordan_wigner(hamiltonian, num_sites).simplify())
>>> flow_matrix = to_matrix(flow_set_encoding(hamiltonian, num_qubits))
>>>
>>> jw_spectrum = np.linalg.eigvalsh(jw_matrix)
>>> flow_spectrum = np.linalg.eigvalsh(flow_matrix)
>>> np.allclose(
... np.sort(flow_spectrum),
... np.sort(np.concatenate([jw_spectrum, jw_spectrum])),
... )
True
>>> print(f"{jw_spectrum[0]:.10f} {flow_spectrum[0]:.10f}")
-1.8557725066 -1.8557725066
5. Construiește circuitul fermionic
Înainte de a implica vreun qubit, dinamica este exprimată la nivel fermionic. Un FermionicCircuit acționează asupra modurilor fermionice, nu asupra qubiților, iar o poartă Evolution peste hamiltonian este întregul circuit:
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import FermionicLieTrotter
>>>
>>> total_time = 1.0
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(
... Evolution(
... num_sites, hamiltonian, time=total_time, synthesis=FermionicLieTrotter()
... ),
... circuit.modes,
... )
>>> circuit.draw("mpl")
<Figure size ... with 1 Axes>
Există o singură cutie opacă peste toate cele patru moduri, iar partea interesantă este în ce se descompune. Argumentul synthesis spune cum: FermionicLieTrotter este formula de produs de ordinul întâi, care împarte operatorul după groups și evoluează fiecare grup pe rând. Așadar partiționarea pe seturi de flux din pasul 2 este trotterizarea, iar un singur decompose() o execută, dând o poartă Evolution pentru fiecare set de flux, în ordinea grupurilor: est, vest, apoi interacțiunea diagonală, cu 3, 3 și respectiv 8 termeni.
>>> flow_set_circuit = circuit.decompose()
>>> flow_set_circuit.draw("mpl")
<Figure size ... with 1 Axes>
Observă că totul se petrece integral în spațiul fermionic: niciun qubit nu a fost încă menționat, iar aproximarea este deja făcută. Numărul de pași Trotter aparține deci tot aici, sub forma reps, care împarte timpul și repetă întregul parcurs:
>>> def trotter_steps(reps):
... step = FermionicCircuit(num_sites)
... step.append(
... Evolution(
... num_sites,
... hamiltonian,
... time=total_time,
... synthesis=FermionicLieTrotter(reps=reps),
... ),
... step.modes,
... )
... return step.decompose()
>>>
>>> trotter_steps(3).count_ops()["Evolution"]
9
Nouă porți: cele trei seturi de flux, parcurse ciclic de trei ori, fiecare cu o treime din timp. Acest ciclu este ceea ce face din ea o trotterizare, și nu o simplă re-tăiere a unui singur grup. Repetarea unui singur grup nu ar realiza nimic, deoarece exponențialele consecutive ale aceluiași operator comută, deci A(t/3) A(t/3) A(t/3) este doar A(t). O formulă care citește grupurile le poate intercala; una aplicată după aplicare nu poate, pentru că gruparea a dispărut până atunci.
Acesta este pasul care face ca gruparea să conteze. Fiecare poartă emisă este aplicată și sintetizată independent, deci tot ce urmează acționează pe partiționarea stabilită la nivel fermionic. Omiterea descompunerii trimite întregul operator la etapa următoare, iar gruparea este ignorată.
6. Transpilează cu codificarea personalizată
Codificarea este acum gata de a fi folosită de transpiler. Mai sunt necesare două piese:
-
Un layout. Implicitul
TrivialF2QLayoutpresupune un qubit pe mod, ceea ce este greșit în această situație.CustomF2QLayoutasociază registrul fermionic cu unQuantumRegisterde altă dimensiune, așa intră qubiții ancilă în fluxul de procesare. -
Un plugin de sinteză.
MapperFnEvolutionSynthesisprimește direct funcția de mapare, aplică cu ea hamiltonianul porțiiEvolutionși emite oPauliEvolutionGate, păstrând convenția .product_formulaal acesteia decide cum este transformată poarta în rotații. Observă că aceasta este o a doua formulă de produs, independentă, care acționează asupra qubiților după mapare: aici este transmisăLieTrotterdin Qiskit, iar pasul 7 o înlocuiește cu una care cunoaște seturile de flux.
Deoarece fiecare comparație de mai jos reia acest flux, merită să îl încapsulăm o singură dată:
>>> from qiskit.circuit import QuantumRegister
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG, QuantumDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import (
... CustomF2QLayout,
... F2QSynthesis,
... MapperFnEvolutionSynthesis,
... )
>>>
>>> def flow_set_pass_manager(mode_register, product_formula=None):
... '''Build the pipeline mapping `mode_register` with the flow-set encoding.'''
... synthesis = F2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... flow_set_encoding, product_formula=product_formula
... )
... # this encoding spends one ancilla on top of the fermionic modes
... qubit_register = QuantumRegister(mode_register.size + 1, "q")
... return MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... layout=CustomF2QLayout({mode_register: qubit_register}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>>
>>> from qiskit.synthesis import LieTrotter
>>>
>>> pass_manager = flow_set_pass_manager(flow_set_circuit.register, LieTrotter())
>>> qubit_circuit = pass_manager.run(flow_set_circuit)
>>> qubit_circuit.num_qubits
5
Patru moduri fermionice au devenit un circuit cu cinci qubiți, ancila fiind furnizată de layout-ul personalizat.
Acum privește ce conține de fapt circuitul rezultat. Descompunerea repetată reduce porțile de evoluție până la U și CX, astfel încât costul cu doi qubiți al celor două strategii de sinteză comparate mai jos să fie măsurat în aceeași monedă: count_ops() raportează apoi numărul de CX, iar depth() primește o funcție de filtrare pentru a măsura adâncimea cu doi qubiți. Observă că acest reps este o adâncime de descompunere, fără legătură cu numărul de pași Trotter pe care îl face o formulă de produs, care se numește tot reps mai târziu în acest ghid:
>>> decomposed = qubit_circuit.decompose(reps=6)
>>> decomposed.count_ops()["cx"]
26
>>> decomposed.depth(lambda instruction: len(instruction.qubits) == 2)
23
Douăzeci și șase de porți CX în douăzeci și trei de straturi, deci aproape nimic nu rulează în paralel. Desenarea circuitului arată de ce:
>>> decomposed.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Fiecare termen a fost compilat izolat: o schimbare de bază, o scară de CX până la o singură rotație, apoi scara este desfăcută. Setul de flux de pondere 1 este gratuit; termenii lui sunt rotații singulare fără scară în jur, dar fiecare termen de pondere 3 costă patru porți CX, iar deoarece scările sunt emise secvențial, nimic nu se suprapune.
Așadar gruparea supraviețuiește întregului flux (trei seturi de flux, trei PauliEvolutionGate mapate) și totuși aduce aproape nimic. Trotterizarea fermionică din pasul 5 și-a făcut treaba; ceea ce o irosește este formula de pe partea qubiților. LieTrotter primește un set de termeni despre care nu i se spune nimic și compilează fiecare termen izolat. Nu poate ști că ei comută între ei, așa că serializează ceea ce ar putea împărți aceeași schimbare de bază.
Aceasta este lacuna pe care această codificare există pentru a o închide: două formule de produs sunt la lucru, iar până acum doar cea fermionică a primit structura. Pasul următor oferă o sinteză pe partea qubiților care o exploatează și pe ea.
7. O sinteză conștientă de seturile de flux
Recuperarea avantajului înseamnă a spune sintezei de pe partea qubiților despre structură. Aceasta este Secțiunea IV C din Ref. [1]: deoarece toți termenii unui set de flux comută, ei pot fi diagonalizați simultan de un singur circuit Clifford, rotiți într-o bază comună și rotiți înapoi, în loc să fie rotiți o dată pentru fiecare termen.
Pentru această codificare, Clifford-ul este simplu. Conjugarea printr-un lanț de porți CZ între vecini apropiați transformă
simultan pentru fiecare . Un lanț CZ transformă întregul set de flux de pondere 3 (vest) într-un strat de rotații independente pe un singur qubit. Interfața EvolutionSynthesis cere doar o metodă synthesize(), ceea ce este tot ce implementează această clasă. Ea nu poartă niciun număr de pași Trotter, deoarece primește câte un set de flux deodată și deci nu poate subdiviza evoluția între seturi. Această subdiviziune aparține unui nivel mai sus, în spațiul fermionic, unde un F2FSynthesisPlugin le vede pe toate deodată (vezi pasul 9):
>>> from qiskit.circuit import QuantumCircuit
>>> from qiskit.synthesis.evolution import EvolutionSynthesis
>>>
>>> def cz_brickwork(circuit):
... # emit even and odd bonds as two layers, so the CZs are scheduled in
... # parallel: a chain of overlapping CZs has depth 2, not num_qubits - 1
... #
... # NOTE: CZ gates are diagonal, so they all commute and each is its own inverse.
... # This whole layer is therefore self-inverse, which is why the conjugation below
... # calls it twice rather than calling it and then its inverse.
... for offset in (0, 1):
... for j in range(offset, circuit.num_qubits - 1, 2):
... circuit.cz(j, j + 1)
>>>
>>> # The interaction reaches nearest (distance 1) and next-nearest (distance 2)
>>> # neighbors, so every interior qubit carries four Rzz gates and can only do one at
>>> # a time: four layers is the best any schedule can do. These offsets achieve it.
>>> def rzz_layers(circuit, angles):
... """Emit the diagonal Rzz gates in four layers of disjoint qubit pairs."""
... near = [(j, k) for j, k in angles if k - j == 1]
... far = [(j, k) for j, k in angles if k - j == 2]
... schedule = [
... [(j, k) for j, k in near if j % 2 == 0],
... [(j, k) for j, k in far if j % 4 in (0, 1)],
... [(j, k) for j, k in near if j % 2 == 1],
... [(j, k) for j, k in far if j % 4 in (2, 3)],
... ]
... # emit layer by layer, since Qiskit's depth follows the order gates were added
... for layer in schedule:
... for j, k in layer:
... circuit.rzz(angles[j, k], j, k)
>>>
>>> class FlowSetSynthesis(EvolutionSynthesis):
... '''Synthesize one flow set of the encoded Hamiltonian at a time.'''
...
... preserve_order = True
...
... def synthesize(self, evolution):
... circuit = QuantumCircuit(evolution.operator.num_qubits)
... east, west, diagonal = [], [], {}
...
... for label, indices, coeff in evolution.operator.to_sparse_list():
... angle = 2 * evolution.time * coeff.real
... match label, indices:
... case "", _: # the identity contributes only a global phase
... circuit.global_phase -= angle / 2
... case "X", [qubit]: # east flow set: already weight-1
... east.append((qubit, angle))
... case "ZXZ", [j, k, l] if (k, l) == (j + 1, j + 2):
... west.append((k, angle)) # needs the CZ conjugation below
... case "ZZ", [j, k]: # interaction: diagonal, see rzz_layers above
... diagonal[min(j, k), max(j, k)] = angle
... case _:
... raise ValueError(f"unexpected Pauli term: {label} on {indices}")
...
... # east flow set: bare rotations, no entangling gates at all
... for qubit, angle in east:
... circuit.rx(angle, qubit)
...
... # west flow set: one CZ chain diagonalizes the WHOLE set at once
... if west:
... cz_brickwork(circuit)
... for qubit, angle in west:
... circuit.rx(angle, qubit)
... cz_brickwork(circuit)
...
... if diagonal:
... rzz_layers(circuit, diagonal)
...
... return circuit
Două detalii sunt ușor de ratat. Un EvolutionSynthesis personalizat trebuie să expună un atribut preserve_order, pe care îl citește pasul de sinteză de nivel înalt.
Ambele funcții auxiliare emit porțile lor în straturi explicite, nu în ordinea termenilor, deoarece depth() planifică porțile cât mai devreme posibil, în ordinea în care au fost adăugate. Cum toate porțile dintr-unul dintre aceste seturi comută, ordinea poate fi aleasă liber. O sinteză termen cu termen alege prost ordinea. Emiterea secvențială a lanțului CZ suprapus raportează o adâncime care crește cu numărul de qubiți, în loc de 2 constant pe care l-ar putea atinge hardware-ul.
De aceea sinteza de mai sus nu poartă niciun număr de pași Trotter. Ea primește câte un set de flux pe poartă, deci o repetiție în interiorul ei ar repeta în cadrul unui set, în loc să alterneze între seturi:
repeating within one set: A(t/2) A(t/2) B(t/2) B(t/2)
what a Trotterization needs: A(t/2) B(t/2) A(t/2) B(t/2)
Prima formă nu aduce nimic: exponențialele consecutive ale aceluiași operator comută, deci A(t/2) A(t/2) este doar A(t), iar eroarea rămâne la valoarea ei de un pas indiferent câți pași se cer.
Numărul de pași aparține deci lui FermionicLieTrotter fermionic din pasul 5, care citește groups și le poate intercala. Această diviziune a muncii este esența: formula fermionică decide câți pași și în ce ordine, iar această clasă decide cât de ieftin este realizat fiecare set rezultat.
Pentru a selecta ordinea, transmite-o lui MapperFnEvolutionSynthesis.product_formula, care este argumentul pe care îl primește flow_set_pass_manager. Nimic altceva din flux (sau din circuitul fermionic pe care rulează) nu se schimbă:
>>> pass_manager = flow_set_pass_manager(flow_set_circuit.register, FlowSetSynthesis())
>>>
>>> flow_circuit = pass_manager.run(flow_set_circuit).decompose(reps=6)
>>> flow_circuit.count_ops()["cx"]
22
>>> flow_circuit.depth(lambda instruction: len(instruction.qubits) == 2)
12
>>> flow_circuit.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
De la adâncime 23 cu 26 de porți CX la adâncime 12 cu 22 de porți CX. Observă că sunt mai puține porți de încâlcire și aproape jumătate din adâncime. Comparând cele două imagini, diferența nu este că porțile sunt mai ieftine, ci că se stivuiesc. Scările seriale au dispărut; porțile de încâlcire stau acum una deasupra alteia în straturi comune, deoarece întregul set de flux vest este rotit într-o singură bază comună, iar interacțiunea este emisă în straturi de perechi disjuncte.
FlowSetSynthesis potrivește tipare pe indici absoluți de qubiți, ceea ce este sigur aici doar pentru că fiecare set de flux al acestui hamiltonian acoperă întregul registru. O formulă de produs fermionică restrânge fiecare factor la modurile pe care le atinge efectiv, deci o grupare ale cărei seturi ar acoperi doar o parte din lanț ar sosi reetichetată, iar tiparele de mai sus nu s-ar potrivi.
Ambele desene arată circuitul după decompose(reps=6), care rescrie totul în baza U și CX, deci porțile CZ, Rx și Rzz emise de FlowSetSynthesis nu sunt vizibile ca atare. Aceasta este deliberat: pune ambele circuite în aceeași bază, singura modalitate prin care numărul de porți cu doi qubiți și adâncimile de mai sus sunt comparabile. Structura straturilor este ceea ce merită remarcat în aceste figuri, nu etichetele porților.
8. Adâncime constantă la scară mare
Întrebarea interesantă nu este numărul la patru moduri, ci cum scalează. Cele două straturi CZ în cărămidă nu cresc cu lungimea lanțului, deci adâncimea cu doi qubiți a unui pas Trotter ar trebui să fie constantă. Măsurând direct pe hamiltonianul Fermi-Hubbard complet, cu mult peste ce s-ar putea simula prin forță brută:
>>> def evolution_stats(num_sites, product_formula):
... operator = fermi_hubbard_1d(num_sites, tunneling=1.0, interaction=2.0)
... operator.groups = flow_set_groups(operator)
...
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(
... num_sites, operator, time=total_time, synthesis=FermionicLieTrotter()
... ),
... circuit.modes,
... )
... circuit = circuit.decompose() # one Evolution per flow set, as in step 5
...
... pass_manager = flow_set_pass_manager(circuit.register, product_formula)
... decomposed = pass_manager.run(circuit).decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return decomposed.count_ops()["cx"], decomposed.depth(two_qubit)
>>>
>>> sites = [4, 10, 20, 50, 100]
>>> flow = [evolution_stats(n, FlowSetSynthesis()) for n in sites]
>>> lie_trotter = [evolution_stats(n, LieTrotter()) for n in sites]
>>>
>>> [depth for _, depth in flow]
[12, 12, 12, 12, 12]
>>> [depth for _, depth in lie_trotter]
[23, 47, 87, 207, 407]
Adâncimea cu doi qubiți rămâne 12 de la patru moduri până la 100, în timp ce LieTrotter crește liniar până la 407. Seturile de flux îi permit să paralelizeze puțin în cadrul fluxului, dar fiecare set este totuși trotterizat termen cu termen, deci creșterea este doar încetinită, nu eliminată:
>>> import matplotlib.pyplot as plt
>>>
>>> figure, axes = plt.subplots(1, 2, figsize=(9, 3.5), layout="constrained")
>>> for panel, index, title in zip(axes, (1, 0), ("two-qubit depth", "CX count")):
... _ = panel.plot(sites, [stat[index] for stat in lie_trotter], "o-", label="qubit-level")
... _ = panel.plot(sites, [stat[index] for stat in flow], "s-", label="flow set")
... _ = panel.set(xlabel="modes", title=title)
... _ = panel.legend()
>>> figure
<Figure size ... with 2 Axes>
Numărul de porți crește liniar în ambele cazuri (există termeni de aplicat, iar asta este inevitabil), dar sinteza pe seturi de flux îi aplică într-un număr constant de straturi paralele. Acesta este rezultatul de adâncime constantă din Ref. [1].
Adâncimea 12 se împarte între cele trei seturi de flux ca 0 + 4 + 8, la orice lungime de lanț. Setul de flux est contribuie cu zero; sunt doar rotații Rx. Setul de flux vest costă 4, din cele două straturi CZ în cărămidă care intră și cele două care ies; eliminarea interacțiunii lasă o adâncime cu doi qubiți de patru la orice lungime de lanț. Cele opt rămase reprezintă interacțiunea, ai cărei termeni Rzz acționează atât asupra vecinilor apropiați, cât și a celor următori, și deci necesită patru straturi disjuncte, fiecare costând două porți CX. Patru este optim aici: fiecare qubit interior poartă patru porți Rzz (două către fiecare vecin și două către fiecare vecin următor) și poate participa la una singură deodată, deci nicio planificare nu poate face mai bine.
Două treimi din adâncime este interacțiunea, ceea ce arată că această codificare excelează la saltul (hopping), și deci la dinamica unui lanț de fermioni liberi. Totuși, observă că interacțiunea nu comută cu saltul. Omiterea ei nu este o aproximare a modelului Fermi-Hubbard, ci un alt model. Unde se așază echilibrul pentru o problemă dată merită explorat.
9. Verifică dinamica
Sub maparea domain-wall, un tipar de ocupare devine un șir de parități cumulative, deci starea inițială este pregătită diferit față de Jordan-Wigner. Aceasta este o consecință practică ușor de trecut cu vederea a delocalizării parității. Funcția auxiliară din pasul 4 dă eticheta corectă:
>>> occupations = [1, 0, 1, 0]
>>> initial_label = "".join(str(b) for b in reversed(domain_wall_bits(occupations)))
>>> initial_label
'00110'
Cu starea pregătită, rămân două întrebări, care se pun cel mai bine separat: calculează sinteza pe seturi de flux lucrul corect și îl calculează mai ieftin?
Mai întâi corectitudinea. FlowSetSynthesis și LieTrotter din Qiskit primesc aceleași seturi de flux codificate din aceeași trotterizare fermionică, deci trebuie să realizeze aceeași transformare unitară până la o fază globală. Cliffordurile personalizate reorganizează munca; nu schimbă rezultatul. Vectorii de stare aleatori detectează orice discrepanță mult mai ieftin decât construirea întregii matrice unitare:
>>> from qiskit.quantum_info import Statevector
>>>
>>> rng = np.random.default_rng(1234)
>>>
>>> def agrees_up_to_one_phase(reps, samples=3):
... """Compare both qubit-side syntheses of the same fermionic Trotterization."""
... step = trotter_steps(reps)
... trotter = flow_set_pass_manager(step.register, LieTrotter()).run(step)
... flow = flow_set_pass_manager(step.register, FlowSetSynthesis()).run(step)
... trotter, flow = trotter.decompose(reps=6), flow.decompose(reps=6)
... dimension = 2**trotter.num_qubits
... residuals, phases = [], []
... for _ in range(samples):
... vector = rng.normal(size=dimension) + 1j * rng.normal(size=dimension)
... state = Statevector(vector / np.linalg.norm(vector))
... a = state.evolve(trotter).data
... b = state.evolve(flow).data
... overlap = np.vdot(a, b)
... phases.append(overlap / abs(overlap))
... residuals.append(np.linalg.norm(b - a * overlap / abs(overlap)))
... # the SAME global phase must work for every input state
... spread = max(abs(phase - phases[0]) for phase in phases)
... return bool(max(residuals) < 1e-9), bool(spread < 1e-9)
>>>
>>> [agrees_up_to_one_phase(reps) for reps in (1, 3)]
[(True, True), (True, True)]
Ambele jumătăți ale acestei verificări contează. O suprapunere de modul unitar pe o singură stare este necesară, dar nu suficientă: o sinteză care ar greși o fază dependentă de stare ar mai apărea totuși. Cerința ca aceeași fază să apară la mai multe stări aleatorii este ceea ce închide lacuna. Verificarea mai multor valori reps contează de asemenea, deoarece confirmă că acordul supraviețuiește intercalării seturilor de flux, și nu este valabil doar pentru un singur parcurs.
Așadar alegerea sintezei de pe partea qubiților este lipsită de consecințe pentru acuratețe, ceea ce face semnificativă o comparație de cost. Întrebarea rămasă este ce câștigă codificarea în ansamblu față de alternativa evidentă: Jordan-Wigner simplu pe num_sites qubiți, fără ancilă și fără Cliffordurile personalizate. Atât codificarea, cât și sinteza de pe partea qubiților diferă între cele două coloane, dar verificarea de mai sus a arătat că a doua nu poate afecta rezultatul, deci ceea ce testează fidelitățile este codificarea. Ambele fluxuri pornesc de asemenea din aceeași trotterizare fermionică, deci orice diferență de fidelitate ar indica o eroare, nu un compromis. Cele două coloane sunt codificările, seturi de flux contra Jordan-Wigner:
>>> def jordan_wigner_pass_manager(mode_register, product_formula):
... """The same pipeline under plain Jordan-Wigner: one qubit per mode."""
... synthesis = F2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... transfer_vertex_jordan_wigner, product_formula=product_formula
... )
... qubit_register = QuantumRegister(mode_register.size, "q")
... return MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... layout=CustomF2QLayout({mode_register: qubit_register}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>>
>>> from scipy.linalg import expm
>>>
>>> jw_state = np.zeros(2**num_sites, dtype=complex)
>>> jw_state[sum(o << j for j, o in enumerate(occupations))] = 1.0
>>> reference_jw = expm(-1j * total_time * jw_matrix) @ jw_state
>>> reference_flow = expm(-1j * total_time * flow_matrix) @ (isometry @ jw_state)
>>>
>>> def cost_and_fidelity(reps):
... step = trotter_steps(reps)
... flow = flow_set_pass_manager(step.register, FlowSetSynthesis()).run(step)
... jw = jordan_wigner_pass_manager(step.register, LieTrotter()).run(step)
... flow, jw = flow.decompose(reps=6), jw.decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... evolved_flow = Statevector.from_label(initial_label).evolve(flow).data
... evolved_jw = Statevector(jw_state).evolve(jw).data
... return (
... abs(np.vdot(evolved_flow, reference_flow)) ** 2,
... flow.count_ops()["cx"],
... flow.depth(two_qubit),
... abs(np.vdot(evolved_jw, reference_jw)) ** 2,
... jw.count_ops()["cx"],
... jw.depth(two_qubit),
... )
>>>
>>> for reps in (1, 2, 4, 8):
... flow_f, flow_cx, flow_d, jw_f, jw_cx, jw_d = cost_and_fidelity(reps)
... print(
... f"{reps} step(s): flow set {flow_f:.6f}"
... f" ({flow_cx:3d} CX, depth {flow_d:3d})"
... f" JW {jw_f:.6f} ({jw_cx:3d} CX, depth {jw_d:3d})"
... )
1 step(s): flow set 0.590661 ( 22 CX, depth 12) JW 0.590661 ( 18 CX, depth 14)
2 step(s): flow set 0.926656 ( 44 CX, depth 24) JW 0.926656 ( 36 CX, depth 26)
4 step(s): flow set 0.982053 ( 88 CX, depth 48) JW 0.982053 ( 72 CX, depth 50)
8 step(s): flow set 0.995395 (176 CX, depth 96) JW 0.995395 (144 CX, depth 98)
Citește mai întâi coloana de fidelitate: identică, în fiecare cifră afișată, în două spații Hilbert diferite, sub două codificări diferite. Aceasta este trotterizarea fermionică care se vede. Aproximarea a fost fixată în pasul 5, înainte de alegerea oricărei codificări, deci ambele circuite poartă exact eroarea unei formule de ordinul întâi peste trei seturi de flux, iar aceasta scade de aproximativ patru ori la fiecare dublare a numărului de pași.
Prin ce diferă codificările este costul, iar compromisul merită spus pe șleau. Setul de flux cheltuiește mai multe porți de încâlcire: 22 față de 18 la un pas. Ancila nu este motivul, deoarece nu poartă nicio poartă proprie cu doi qubiți. Motivul este interacțiunea, exact cum a avertizat pasul 3: delocalizarea parității transformă trei termeni ZZ între vecini apropiați în șapte care acoperă vecinii apropiați și următori, iar fiecare costă două porți CX. Acesta este prețul plătit pentru a face saltul ieftin.
De asemenea, câștigă doar la limită la adâncime la această dimensiune (12 față de 14), deoarece patru moduri sunt prea puține pentru ca asimptotica să se vadă. Esențial este scalarea măsurată în pasul 8: adâncimea 12 este constantă, în timp ce adâncimea Jordan-Wigner crește odată cu lanțul. La 14 moduri este deja 12 față de 34.
Apelurile decompose() de mai sus sunt esențiale, din două motive independente.
evolve() și Operator folosesc definiția exactă a unei PauliEvolutionGate, ocolind o sinteză personalizată de pe partea qubiților. Verificarea față de un circuit nedescompus raportează fidelitate perfectă chiar și pentru o sinteză greșită în mod deliberat.
Același lucru se aplică un nivel mai sus, la sinteza fermionică. O poartă Evolution care nu este niciodată descompusă ajunge întreagă la etapa fermion-qubit, care o mapează fără a consulta deloc Evolution.synthesis, deci reps nu ar avea în tăcere niciun efect. trotter_steps din pasul 5 efectuează această descompunere; un flux care setează metoda cu F2FSynthesis obține aceeași expandare din pas.
Ca verificare încrucișată finală, densitățile de situs calculate în spațiul codificat trebuie să reproducă rezultatul Jordan-Wigner. Această verificare privește codificarea, nu circuitul, deci nicio aproximare Trotter nu intervine. Ambele evoluții în timp sunt calculate exact, prin exponențierea matricială a hamiltonienilor respectivi. Orice discrepanță ar indica deci codificarea, nu o formulă de produs:
>>> time = 0.8
>>> state_jw = np.zeros(2**num_sites, dtype=complex)
>>> state_jw[sum(o << j for j, o in enumerate(occupations))] = 1.0
>>> evolved_jw = expm(-1j * time * jw_matrix) @ state_jw
>>> evolved_flow = expm(-1j * time * flow_matrix) @ (isometry @ state_jw)
>>>
>>> for j in range(num_sites):
... v_flow = to_matrix(
... SparseObservable.from_sparse_list([("ZZ", [j, j + 1], 1.0)], num_qubits)
... )
... v_jw = to_matrix(SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_sites))
... density_flow = (1 - np.vdot(evolved_flow, v_flow @ evolved_flow).real) / 2
... density_jw = (1 - np.vdot(evolved_jw, v_jw @ evolved_jw).real) / 2
... print(f"site {j}: {density_flow:.10f} {density_jw:.10f}")
site 0: 0.6507935456 0.6507935456
site 1: 0.5963201678 0.5963201678
site 2: 0.3365016619 0.3365016619
site 3: 0.4163846247 0.4163846247
Densitățile concordă până la zece zecimale.
Ce să reții
-
Grupează înainte de mapare. Seturile de flux sunt definite pe graful de interacțiune fermionic orientat, deci gruparea este aleasă cât timp structura problemei este încă disponibilă. Atributul
groupso duce în circuit, unde un singurdecompose()o transformă într-o poartăEvolutionpentru fiecare set de flux. -
Ancilele cumpără structură. Cheltuirea unui qubit în plus transformă un întreg set de flux în Pauli de pondere 1, a căror evoluție nu necesită porți de încâlcire. Ref. [1] observă acest compromis în general. Rapoartele mai mari qubit-la-fermion admit circuite de evoluție mai puțin adânci.
-
O codificare personalizată este o singură funcție. Mapează un singur operator de transfer generalizat într-un șir Pauli;
map_transfer_vertex_generators()șiMapperFnEvolutionSynthesisfac restul. -
Maparea și gruparea singure nu sunt suficiente. Împărțirea evoluției pe seturi de flux a încetinit doar creșterea liniară a adâncimii; o sinteză termen cu termen a fiecărui set tot a irosit faptul că termenii lui comută. Doar furnizarea unui
EvolutionSynthesisconștient de seturile de flux a făcut adâncimea constantă. Codificările personalizate, gruparea și sinteza personalizată sunt complementare: toate trei sunt necesare. -
Două formule de produs, două sarcini.
FermionicLieTrotterfixează aproximarea în spațiul fermionic, unde gruparea este încă vizibilă, deci doar ea stabilește eroarea Trotter și numărul de pași.EvolutionSynthesisde pe partea qubiților decide apoi doar cât de ieftin este realizat fiecare set: pasul 9 confirmă că ambele alegeri dau aceeași transformare unitară. Separarea celor două este ceea ce permite reglarea independentă a acurateței și a costului. Vezi Sintetizează o poartă Evolution în spațiul fermionic. -
Verifică. O codificare care satisface relațiile de comutare ar putea totuși reprezenta un alt hamiltonian dacă un prefactor sau un semn este greșit. Verificarea unei relații de intercalare (sau, mai ieftin, a spectrului) prinde acest lucru.
FlowSetSynthesis de mai sus este scris deliberat pentru această codificare pe un lanț 1D. Potrivește tipare pe etichetele Pauli specifice pe care le produce codificarea și generează o eroare pentru orice altceva, în loc să revină în tăcere la o formulă de produs generică. O implementare generală are două jumătăți. Citirea structurii seturilor de flux din groups ale operatorului este ceea ce face deja un F2FSynthesisPlugin pentru o evoluție. Derivarea Clifford-ului de diagonalizare din grupul stabilizator, așa cum este descris în Secțiunea IV C din Ref. [1], este jumătatea care rămâne specifică codificării.
De asemenea, delocalizarea parității face ca termenul de interacțiune să aibă pondere 2, în loc să fie diagonal în qubiți singulari, ceea ce este costul corespunzător al termenilor de salt mai ieftini.
Următorul pas: două dimensiuni
Tot ce este mai sus este unidimensional, în mod deliberat. Pe un lanț, seturile de flux sunt doar cele două orientări ale săgeților, iar codificarea necesită o singură ancilă. Niciuna dintre simplificări nu supraviețuiește în două dimensiuni, unde sunt necesare codificări locale.
Pe o rețea plană, un set de flux este o cale orientată (sau o reuniune de căi orientate disjuncte ca vârfuri) prin graful de interacțiune, iar găsirea unui set mic de astfel de căi care acoperă fiecare muchie devine o problemă combinatorie de sine stătătoare, nu o clasificare în două rânduri. Codificările își schimbă și caracterul; în loc de o singură ancilă pentru întregul lanț, adaugă câte una pe situs sau celulă a rețelei. Verstraete-Cirac asociază fiecare situs fermionic cu propriul qubit auxiliar, un raport qubit-la-mod de , în timp ce Derby-Klassen plasează unul în centrul fiecărei plachete impare, pentru un raport de . Aranjamentul registrului are atunci o structură cu adevărat bidimensională, iar grupul stabilizator pe care trebuie să se proiecteze nu mai este trivial. Hamiltonianul codificat este fidel doar pe un subspațiu, ceea ce schimbă atât pregătirea stării, cât și rețeta de verificare din pasul 4.
Piesele pe care le construiește acest ghid sunt cele care se transferă: CustomF2QLayout pentru un registru a cărui dimensiune diferă de numărul de moduri, map_transfer_vertex_generators() pentru codificarea propriu-zisă, MapperFnEvolutionSynthesis pentru integrarea ei în transpiler și un EvolutionSynthesis personalizat pentru a face structura seturilor de flux profitabilă. Doar codificarea și descompunerea pe seturi de flux sunt diferite.
Ghidul seturilor de flux 2D continuă de aici, parcurgând codificarea Verstraete-Cirac pe o rețea pătrată: patru seturi de flux, un Clifford de adâncime 2 pentru fiecare și proiecția pe stabilizatori de care are nevoie apoi verificarea.
A. Gandon, S. Piccinelli, M. Rossmannek, F. Tacchino, A. Baiardi, J. Nys, and I. Tavernelli, Stabilizer-based quantum simulation of fermion dynamics with local qubit encodings (2026), arXiv:2512.11418v2.

