Sari la conținutul principal
Acest proiect este în versiune beta. Ai găsit o eroare? Deschide un issue pe GitHub. Ai o întrebare sau o idee? Întreabă în Discussions — ne-ar plăcea să aflăm părerea ta!
Cum este calitatea traducerii pe această pagină?

Mapează reprezentările operatorilor

Mapperele sunt rutine care transformă operatorii dintr-o reprezentare în alta păstrând echivalența matematică. Folosește-le pentru a lucra flexibil între diferite algebre de operatori și pentru a pregăti operatorii pentru execuția pe circuite cuantice.

Implementează mappere personalizate​

Modulul mappers face simplă implementarea unor mapări personalizate. Mapperele funcționează transformând acțiunile fundamentale (creare, anihilare, Majorana și așa mai departe) care compun un operator, apoi combinând aceste acțiuni transformate conform regulilor reprezentării țintă.

Ca exemplu concret, ia în considerare o transformare Jordan-Wigner care mapează un MajoranaOperator la un operator de qubiți folosind map_majorana_action_generators():

>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_majorana_action_generators
>>> from qiskit_fermions.operators import MajoranaOperator
>>>
>>> # Create a Majorana operator
>>> maj_op = MajoranaOperator.from_dict({(0, 1): 1.0, (2,): 0.5})
>>>
>>> # Define how each Majorana action maps to Pauli strings
>>> num_qubits = 2
>>> def map_action(mode: int) -> SparseObservable:
... idx = mode // 2
... qubits = list(range(idx + 1))
... pauli = "Y" if mode % 2 else "X"
... return SparseObservable.from_sparse_list(
... [("Z" * idx + pauli, qubits, 1.0)],
... num_qubits=num_qubits,
... )
>>>
>>> # Apply the mapping to transform the operator
>>> custom_observable = map_majorana_action_generators(
... maj_op,
... map_action,
... identity=lambda: SparseObservable.identity(num_qubits),
... compose=SparseObservable.compose,
... )
>>> print(custom_observable.simplify())
<SparseObservable with 2 terms on 2 qubits: (0+1j)(Z_0) + (0.5+0j)(X_1 Z_0)>

Pașii esențiali pentru a implementa orice mapper personalizat sunt:

  1. Definește transformarea acțiunilor: specifică modul în care fiecare acțiune (indexată după mod) se mapează la reprezentarea țintă. În exemplul de mai sus, acțiunile Majorana se mapează la șiruri Pauli conform regulilor Jordan-Wigner.

  2. Folosește una dintre funcțiile de mapare furnizate: modulul mappers oferă funcții utilitare care se ocupă de iterarea termenilor operatorului și de aplicarea ulterioară a hărții de acțiuni personalizate pentru reprezentările de operatori furnizate de modulul operators. De exemplu, map_majorana_action_generators() funcționează cu operatori Majorana.

  3. Obține rezultatul: funcția de mapare returnează un operator în reprezentarea țintă, cu transformarea aplicată consecvent tuturor termenilor.

Acest design face ușoară prototiparea unor mapări personalizate fără o cunoaștere aprofundată a structurilor interne ale operatorilor. Transformarea se aplică automat fiecărui termen al operatorului, păstrând echivalența matematică.

Indiciu

Poți itera și manual termenii unui operator, în loc să folosești una dintre funcțiile iterator furnizate. Consultă secțiunea despre iterarea termenilor din ghidul operatorilor pentru detalii.

Implementări din bibliotecă​

Modulul qiskit_fermions.mappers.library oferă implementări eficiente și testate temeinic ale mapărilor uzuale. Acestea urmează același tipar de bază ca mapperele personalizate, dar sunt optimizate pentru utilizare în producție și sunt disponibile atât în API-ul Python, cât și în cel C.

Exemplul de mai jos aplică un mapper din bibliotecă aceluiași MajoranaOperator din secțiunea despre mapperul personalizat, demonstrând rezultate echivalente:

>>> from qiskit_fermions.mappers.library import majorana_to_fermion, jordan_wigner
>>>
>>> # Convert MajoranaOperator to FermionOperator using library mapper
>>> ferm_op = majorana_to_fermion(maj_op)
>>>
>>> # Apply Jordan-Wigner mapper to get qubit operator
>>> sparse_observable = jordan_wigner(ferm_op, num_qubits=2)
>>> difference = sparse_observable - custom_observable
>>> print(difference.simplify() == SparseObservable.zero(num_qubits))
True
Notă

Cei doi pași de mai sus sunt scriși explicit pentru a arăta cum se compun două mappere din bibliotecă. În practică ai mapa un MajoranaOperator într-un singur pas, cu majorana_jordan_wigner() (qf_maj_op_jordan_wigner() în API-ul C), și la fel pentru celelalte tipuri de operatori – edge_vertex_jordan_wigner() și transfer_vertex_jordan_wigner() (qf_edge_op_jordan_wigner(), qf_transfer_op_jordan_wigner()). Trecerea printr-un FermionOperator umflă, de asemenea, rezultatul intermediar: fiecare generator al acestor algebre se mapează pe un singur șir Pauli, în timp ce fiecare acțiune fermionică se mapează pe o sumă de doi termeni, astfel că un termen construit din mai mulți generatori se extinde doar pentru a fi apoi comprimat la loc. Această economie crește odată cu lungimea termenilor; pentru termenii cu un singur generator ai unui hamiltonian de salt (hopping), cele două căi costă aproximativ la fel.

În API-ul Python, unele dintre aceste mappere din bibliotecă pot fi accesate și prin mappere de conveniență agnostice față de tip. fermion_operator() și majorana_operator() stau în fața lui majorana_to_fermion() și fermion_to_majorana() (printre altele), delegând intern către implementarea potrivită din bibliotecă în funcție de tipul obiectului primit, în loc să te oblige să alegi tu funcția corectă. Același lucru se aplică lui jordan_wigner() și lui ternary_tree() (prezentat mai jos), fiecare dintre ele trimițând către implementarea directă care corespunde operatorului primit. Consultă qiskit_fermions.protocols pentru a vedea cum este implementată intern această trimitere.

Alege o codificare cu arbori ternari​

Jordan-Wigner nu este singura codificare fermion–qubit și nici cea mai ieftină. Șirurile sale ZZ cresc odată cu indicele modului, așa că un operator Majorana pe ultimul dintre cele nn moduri se mapează pe un șir Pauli de pondere nn. Codificările care distribuie informația de paritate într-un arbore, în loc de un lanț, se comportă asimptotic mai bine.

Un arbore ternar cu nn noduri cuprinde toată această familie. Fiecare nod este un qubit cu trei legături descendente etichetate X, Y și Z; o legătură fie coboară către un nod copil (o muchie), fie se termină (o frunză). Drumul fiecărei frunze înapoi la rădăcină scrie un șir Pauli: traversarea legăturii etichetate PP care coboară din nodul uu contribuie cu PP pe qubitul uu. Fiecare astfel de arbore produce o codificare validă, iar cele cunoscute sunt pur și simplu forme particulare – un lanț de-a lungul lui Z este Jordan-Wigner, în timp ce un arbore în care fiecare nod are trei copii, în măsura în care permite numărul de noduri, este echilibrat.

>>> from qiskit_fermions.mappers import TernaryTree, TernaryTreeEncoding
>>>
>>> # Jordan-Wigner is a chain descending along Z, with X and Y as legs at every node.
>>> TernaryTree.chain(13, "Z").max_weight
13
>>> # The balanced tree spreads the same information over depth log_3(2n+1).
>>> TernaryTree.breadth_first(13, 3).max_weight
3

Această pondere maximă este optimă. Jiang et al. demonstrează că operatorii Majorana ai oricărei mapări fermion–qubit a nn moduri au o pondere Pauli medie de cel puțin log⁡3(2n)\log_3(2n), deci nicio mapare nu poate coborî cel mai mare dintre ei sub ⌈log⁡3(2n+1)⌉\lceil \log_3(2n+1) \rceil. Ambii arbori folosesc exact nn qubiți, așa că economia este în ponderea Pauli, nu în numărul de qubiți.

Pentru a mapa un operator, compilează un arbore într-o codificare și transmite-o unui mapper împreună cu numărul de qubiți pentru rezultat. Compilarea împerechează frunzele arborelui în operatori Majorana și construiește o singură dată șirurile lor Pauli, așa că aceeași codificare ar trebui reutilizată pentru mulți operatori. Numărul de qubiți rămâne la apelul de mapare, nu la codificare, astfel încât o codificare poate servi registre de lățimi diferite; un număr mai mare decât numărul de noduri ale arborelui completează cu identitatea.

>>> from qiskit_fermions.mappers.library import fermion_ternary_tree
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> hamiltonian = FermionOperator.from_dict(
... {((True, 0), (False, 3)): 1.0, ((True, 3), (False, 0)): 1.0}
... )
>>> encoding = TernaryTreeEncoding(TernaryTree.breadth_first(4, 3))
>>> fermion_ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>

Maparea printr-un lanț de-a lungul lui Z reproduce exact fermion_jordan_wigner() (qf_ferm_op_jordan_wigner() în API-ul C), în acest sens fiind o generalizare a acesteia:

>>> from qiskit_fermions.mappers.library import fermion_jordan_wigner
>>> chain = TernaryTreeEncoding(TernaryTree.chain(4, "Z"))
>>> direct = fermion_jordan_wigner(hamiltonian, 4)
>>> (fermion_ternary_tree(hamiltonian, chain, 4) - direct).simplify().num_terms
0

Fiecare reprezentare de operator are un mapper cu arbore ternar, oglindind setul Jordan-Wigner; consultă qiskit_fermions.mappers.library pentru lista completă, și Mapper Library în API-ul C. În API-ul Python, ele pot fi accesate și prin ternary_tree(), wrapperul agnostic față de tip care îl alege pe cel potrivit pentru operatorul primit, la fel ca jordan_wigner():

>>> from qiskit_fermions.mappers.library import ternary_tree
>>> ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>
Notă

Unele identități valabile în Jordan-Wigner sunt proprietăți ale lanțului, nu ale arborilor ternari. Operatorul de număr este nj=(1−Zj)/2n_j = (1 - Z_j)/2 doar pe lanț; pe un alt arbore, ZjZ_j devine un șir ZZ peste o mulțime de qubiți. La fel, un operator de vârf VjV_j este Paulii de pondere 1 ZjZ_j doar pe lanț, unde lanțurile ZZ ale celor două șiruri Majorana se anulează.

Deoarece fiecare operator de număr se mapează în continuare pe un singur șir ZZ, o stare Fock rămâne o stare a bazei computaționale sub orice arbore ternar, dar bitul qq nu mai indică ocuparea qq. Ocuparea jj este în schimb paritatea biților măsurați de pe suportul imaginii lui njn_j. Citește aceste suporturi direct din mapper, în loc să le deduci din arbore, astfel încât decodarea să rămână corectă pentru orice formă sau permutare de moduri:

>>> for mode in range(4):
... number = FermionOperator.from_dict({((True, mode), (False, mode)): 1.0})
... print(mode, ternary_tree(number, encoding, 4).simplify())
0 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2 Z_1 Z_0)>
1 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_1)>
2 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2)>
3 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_3)>

Pe acest arbore echilibrat, modul 0 este ocupat când qubiții 0, 1 și 2 conțin un număr impar de unități, așa că pregătirea lui înseamnă inversarea doar a qubitului 0, în timp ce decodarea unui șir de biți măsurat necesită paritatea peste toți trei. O singură eroare de inversare de bit pe qubitul 1 alterează atunci ocupările atât ale modului 0, cât și ale modului 1, ceea ce contează pentru orice flux de lucru care citește ocupările din eșantioane zgomotoase.

Când un Evolution este coborât printr-un astfel de mapper, învelește mapperul în group_wise() (vezi Sintetizarea unei porți Evolution în spațiul fermionic). Un termen de salt (hopping) se mapează pe două șiruri Pauli care comută, dintre care niciunul nu conservă singur numărul de particule; doar produsul lor în evoluție o face. În Jordan-Wigner ambele șiruri acționează pe aceiași qubiți, așa că o ordine simplificată a termenilor le ține adiacente. Pe alți arbori suporturile lor diferă (ca în Y_3 Z_1 Y_0 și X_3 Z_2 X_0 mai devreme), atât ordinea brută, cât și cea simplificată le intercalează cu imaginile altor termeni, iar o formulă de produs la nivel de qubit pierde atunci numărul de particule.

Construiește un arbore arbitrar​

Constructorii pentru lanț și pentru parcurgerea în lățime acoperă familiile uniforme, dar interfața generală primește câte o pereche (parent, label) pentru fiecare nod, cu None pentru rădăcină (API-ul C primește în schimb indicele rădăcinii). Acest lucru contează deoarece arborii care merită căutați nu sunt de obicei nici lanțuri, nici uniformi – algoritmul Bonsai, de exemplu, crește un arbore de acoperire al grafului de cuplare al unui dispozitiv, deci ramificarea lui urmează acea conectivitate.

>>> # Node 0 branches three ways, node 1 twice, and node 4 carries a single child.
>>> tree = TernaryTree([None, (0, "X"), (0, "Y"), (0, "Z"), (1, "X"), (1, "Z"), (4, "Y")])
>>> tree.num_nodes, tree.num_legs, tree.max_weight
(7, 15, 4)
>>> tree.children(0)
{'X': 1, 'Y': 2, 'Z': 3}

Observă numărul de frunze: este întotdeauna 2n+12n+1, indiferent de formă. Un nod cu mai puțini copii nu generează mai puține șiruri Pauli – legăturile sale nefolosite sunt frunze, iar o frunză scrie un șir exact ca descendenții unei muchii. Din cele 2n+12n+1 șiruri, 2n2n se împerechează în operatorii Majorana ai celor nn moduri, iar exact unul rămâne.

Paritate totală​

Frunza rămasă este paritatea fermionică totală ∏j(1−2nj)\prod_j (1 - 2 n_j). Este o proprietate a codificării, nu o mapare a unui operator, așa că este o metodă a codificării însăși:

>>> TernaryTreeEncoding(TernaryTree.chain(4, "Z")).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_2 Z_1 Z_0)>
>>> TernaryTreeEncoding(TernaryTree.breadth_first(4, 3)).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_0)>

Așa cum arată cazul echilibrat, aceasta nu este în general șirul format doar din ZZ: este frunza atinsă pe drumul format doar din ZZ, care acționează ca identitate pe fiecare nod aflat în afara acelui drum.

Important

Paritatea totală este aici o observabilă de măsurat, nu o constrângere de impus. Ambele sale valori proprii sunt fizice, așa că nu proiecta o stare pe sectorul +1+1: o codificare cu arbore ternar folosește un qubit per mod și acoperă întregul spațiu de dimensiune 2n2^n, fără a lăsa vreun subspațiu nefizic de eliminat prin proiecție.

Exemplu lucrat: un arbore Bravyi-Kitaev​

Specificațiile sunt mai des calculate decât scrise de mână, deoarece arborele unei codificări reale este rareori suficient de mic pentru a fi scris manual. Bravyi-Kitaev este un exemplu bun: aparține acestei familii, dar arborele său nu este uniform, deci niciun constructor de conveniență nu îl acoperă.

Arborele său este construit printr-o împărțire recursivă a nodurilor, câte unul pentru fiecare mod. Într-un interval de noduri [l,r][l, r], fie hh cea mai mare putere a lui doi care încape; nodul plasat pentru acel interval este l+h−1l + h - 1, ultimul din acel bloc. Legătura sa X coboară către arborele construit din blocul dinaintea lui, iar legătura Z către arborele construit din restul de după el, lăsând legătura Y ca frunză:

>>> def bravyi_kitaev_tree(num_modes: int) -> TernaryTree:
... """Builds the ternary tree of the Bravyi-Kitaev encoding on ``num_modes`` modes."""
... spec: list[tuple[int, str] | None] = [None] * num_modes
... def build(left: int, right: int) -> int | None:
... if left > right:
... return None # an empty range: the parent's link stays a leg
... block = 1
... while 2 * block <= right - left + 1:
... block *= 2
... node = left + block - 1
... for child, label in (
... (build(left, node - 1), "X"), # the block before this node
... (build(node + 1, right), "Z"), # the remainder after it
... ):
... if child is not None:
... spec[child] = (node, label)
... return node
... build(0, num_modes - 1)
... return TernaryTree(spec)

La patru moduri, întregul interval este un singur bloc, deci nodul plasat primul este 3, cu [0,2][0, 2] înaintea lui și nimic după; acel subinterval dă nodul 1 deasupra nodurilor singulare 0 și 2. Compilarea arborelui confirmă că este vorba despre codificarea Bravyi-Kitaev și nu doar despre un arbore de aceeași clasă de forme – prima sa imagine Majorana este X0X1X3X_0 X_1 X_3:

>>> tree = bravyi_kitaev_tree(4)
>>> tree.children(1)
{'X': 0, 'Z': 2}
>>> encoding = TernaryTreeEncoding(tree)
>>> encoding.majorana_image(0)
[(0, 'X'), (1, 'X'), (3, 'X')]

Paritatea sa totală face concretă ideea secțiunii anterioare: frunza rămasă este cea atinsă pe drumul format doar din ZZ, iar aici acel drum se oprește la rădăcină, deci observabila este Z3Z_3 pe un singur qubit, nu un șir peste toți cei patru qubiți:

>>> encoding.total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3)>

Scalarea nu necesită decât un argument mai mare. Deoarece niciun nod nu își folosește vreodată legătura Y pentru o muchie, fiecare se ramifică în cel mult două direcții, deci ponderea Pauli crește logaritmic, nu liniar:

>>> [bravyi_kitaev_tree(n).max_weight for n in (4, 8, 16, 32, 64)]
[3, 4, 5, 6, 7]