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():
- PYTHON
- C
>>> 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)>
// The C API provides direct mapping functions for common transformations.
// Custom mapper prototyping as shown in Python is primarily a Python feature.
// For custom mappings in C, you will need to perform the iteration and
// conversion logic entirely by yourself.
Pașii esențiali pentru a implementa orice mapper personalizat sunt:
-
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.
-
Folosește una dintre funcțiile de mapare furnizate: modulul
mappersoferă 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 modululoperators. De exemplu,map_majorana_action_generators()funcționează cu operatori Majorana. -
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ă.
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:
- PYTHON
- C
>>> 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
#include <qiskit_fermions.h>
// Create a MajoranaOperator with the same terms as maj_op above
uint64_t num_terms = 2;
uint64_t num_modes = 3;
uint32_t modes[3] = {0, 1, 2};
QkComplex64 coeffs[2] = {{1.0, 0.0}, {0.5, 0.0}};
uint32_t boundaries[3] = {0, 2, 3};
QfMajoranaOperator *maj_op = qf_maj_op_new(num_terms, num_modes, coeffs, modes, boundaries);
// Convert MajoranaOperator to FermionOperator using library mapper
QfFermionOperator *ferm_op = qf_majorana_to_fermion(maj_op);
// Apply Jordan-Wigner mapper to get qubit operator
QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_jordan_wigner(ferm_op, 2, &qubit_op);
assert(exit == QfExitCode_Success);
// Clean up
qf_ferm_op_free(ferm_op);
qf_maj_op_free(maj_op);
qk_obs_free(qubit_op);
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 cresc odată cu indicele modului, așa că un operator Majorana pe ultimul dintre cele moduri se mapează pe un șir Pauli de pondere . 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 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 care coboară din nodul contribuie cu pe qubitul . 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.
- PYTHON
- C
>>> 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
#include <qiskit_fermions.h>
QfTernaryTree *chain;
qf_ternary_tree_chain(13, QfPauliLabel_Z, &chain);
QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *balanced;
qf_ternary_tree_breadth_first(13, 3, order, &balanced);
assert(qf_ternary_tree_max_weight(chain) == 13);
assert(qf_ternary_tree_max_weight(balanced) == 3);
qf_ternary_tree_free(chain);
qf_ternary_tree_free(balanced);
Această pondere maximă este optimă. Jiang et al. demonstrează că operatorii Majorana ai oricărei mapări fermion–qubit a moduri au o pondere Pauli medie de cel puțin , deci nicio mapare nu poate coborî cel mai mare dintre ei sub . Ambii arbori folosesc exact 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.
- PYTHON
- C
>>> 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)>
// Create the hopping Hamiltonian a^dagger_0 a_3 + a^dagger_3 a_0
QfFermionOperator *hamiltonian = qf_ferm_op_zero();
QkComplex64 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
bool actions[4] = {true, false, true, false};
uint32_t indices[4] = {0, 3, 3, 0};
for (int i = 0; i < 2; i++) {
qf_ferm_op_add_term(hamiltonian, 2, actions + 2 * i, indices + 2 * i, &coeffs[i]);
}
QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *tree;
qf_ternary_tree_breadth_first(4, 3, order, &tree);
// Compile the tree once, then reuse the encoding for every operator mapped through it.
QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);
QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_ternary_tree(hamiltonian, encoding, 4, &qubit_op);
assert(exit == QfExitCode_Success);
// Clean up
qf_ferm_op_free(hamiltonian);
qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(qubit_op);
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)>
Unele identități valabile în Jordan-Wigner sunt proprietăți ale lanțului, nu ale arborilor ternari. Operatorul de număr este doar pe lanț; pe un alt arbore, devine un șir peste o mulțime de qubiți. La fel, un operator de vârf este Paulii de pondere 1 doar pe lanț, unde lanțurile ale celor două șiruri Majorana se anulează.
Deoarece fiecare operator de număr se mapează în continuare pe un singur șir , o stare Fock rămâne o stare a bazei computaționale sub orice arbore ternar, dar bitul nu mai indică ocuparea . Ocuparea este în schimb paritatea biților măsurați de pe suportul imaginii lui . 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.
- PYTHON
- C
>>> # 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}
// Node 0, the root, branches three ways, node 1 twice, and node 4 carries a single child.
// The root has no parent and no incoming link, so its entries are never read: any value works.
#define IGNORED 0
uint32_t parents[7] = {IGNORED, 0, 0, 0, 1, 1, 4};
QfPauliLabel labels[7] = {
IGNORED, QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z,
QfPauliLabel_X, QfPauliLabel_Z, QfPauliLabel_Y,
};
QfTernaryTree *tree;
QfExitCode exit = qf_ternary_tree_new(7, 0, parents, labels, &tree);
assert(exit == QfExitCode_Success);
assert(qf_ternary_tree_num_nodes(tree) == 7);
assert(qf_ternary_tree_num_legs(tree) == 15);
assert(qf_ternary_tree_max_weight(tree) == 4);
qf_ternary_tree_free(tree);
Observă numărul de frunze: este întotdeauna , 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 șiruri, se împerechează în operatorii Majorana ai celor moduri, iar exact unul rămâne.
Paritate totală
Frunza rămasă este paritatea fermionică totală . Este o proprietate a codificării, nu o mapare a unui operator, așa că este o metodă a codificării însăși:
- PYTHON
- C
>>> 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)>
QfTernaryTree *tree;
qf_ternary_tree_chain(4, QfPauliLabel_Z, &tree);
QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);
// On the chain this is the full Z string; on the balanced tree it is not.
QkObs *parity;
qf_ternary_tree_encoding_total_parity(encoding, 4, &parity);
qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(parity);
Așa cum arată cazul echilibrat, aceasta nu este în general șirul format doar din : este frunza atinsă pe drumul format doar din , care acționează ca identitate pe fiecare nod aflat în afara acelui drum.
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 : o codificare cu arbore ternar folosește un qubit per mod și acoperă întregul spațiu de dimensiune , 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 , fie cea mai mare putere a lui doi care încape; nodul plasat pentru acel interval este , 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 î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 :
>>> 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 , iar aici acel drum se oprește la rădăcină, deci observabila este 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]