Mapowanie reprezentacji operatorów
Mappery to procedury, które przekształcają operatory z jednej reprezentacji w inną, zachowując równoważność matematyczną. Używaj ich, aby elastycznie pracować w różnych algebrach operatorów i przygotowywać operatory do wykonania na obwodach kwantowych.
Implementacja własnych mapperów
Moduł mapperów ułatwia implementację własnych odwzorowań. Mappery działają, przekształcając podstawowe akcje (kreacji, anihilacji, Majorany itd.), z których składa się operator, a następnie łącząc te przekształcone akcje zgodnie z regułami reprezentacji docelowej.
Jako konkretny przykład rozważ transformację Jordana-Wignera, która odwzorowuje MajoranaOperator na operator kubitowy za pomocą 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.
Kluczowe kroki implementacji dowolnego niestandardowego mappera to:
-
Zdefiniuj transformację akcji: Określ, jak każda akcja (indeksowana modem) odwzorowuje się na reprezentację docelową. W powyższym przykładzie akcje Majorany odwzorowują się na ciągi Pauliego zgodnie z regułami Jordana-Wignera.
-
Użyj jednej z dostarczonych funkcji odwzorowujących: Moduł
mappersudostępnia funkcje pomocnicze, które obsługują iterowanie po wyrazach operatora i kolejne zastosowania niestandardowej mapy akcji dla reprezentacji operatorów dostarczanych przez modułoperators. Na przykładmap_majorana_action_generators()działa z operatorami Majorany. -
Pobierz wynik: Funkcja odwzorowująca zwraca operator w reprezentacji docelowej, z transformacją zastosowaną spójnie do wszystkich wyrazów.
Taka konstrukcja ułatwia prototypowanie niestandardowych odwzorowań bez głębokiej znajomości wewnętrznych struktur operatorów. Transformacja jest stosowana automatycznie do każdego wyrazu operatora, z zachowaniem równoważności matematycznej.
Możesz też iterować po wyrazach operatora ręcznie, zamiast używać jednej z dostarczonych funkcji iteratorów. Szczegóły znajdziesz w sekcji o iteracji po wyrazach w przewodniku o operatorach.
Implementacje biblioteczne
Moduł qiskit_fermions.mappers.library udostępnia wydajne, dokładnie przetestowane implementacje popularnych odwzorowań. Opierają się na tym samym schemacie co niestandardowe mappery, ale są zoptymalizowane pod kątem zastosowań produkcyjnych i dostępne zarówno w API Pythona, jak i C.
Poniższy przykład stosuje biblioteczny mapper do tego samego MajoranaOperator z sekcji o niestandardowym mapperze i pokazuje równoważne wyniki:
- 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);
Powyższe dwa kroki zapisano jawnie, aby pokazać składanie dwóch bibliotecznych mapperów. W praktyce MajoranaOperator odwzorowuje się w jednym kroku, za pomocą majorana_jordan_wigner() (qf_maj_op_jordan_wigner() w API C), i podobnie dla pozostałych typów operatorów – edge_vertex_jordan_wigner() i transfer_vertex_jordan_wigner() (qf_edge_op_jordan_wigner(), qf_transfer_op_jordan_wigner()). Przejście przez FermionOperator także powiększa wynik pośredni: każdy generator tych algebr odwzorowuje się na pojedynczy ciąg Pauliego, podczas gdy każda akcja fermionowa odwzorowuje się na sumę dwóch wyrazów, więc wyraz zbudowany z kilku generatorów rozrasta się tylko po to, by zostać z powrotem scalonym. Ta oszczędność rośnie wraz z długością wyrazów; dla wyrazów z pojedynczym generatorem w hamiltonianie przeskoków obie drogi kosztują mniej więcej tyle samo.
W API Pythona niektóre z tych bibliotecznych mapperów są dostępne także przez uniwersalne (niezależne od typu) mappery pomocnicze. fermion_operator() i majorana_operator() stoją przed majorana_to_fermion() i fermion_to_majorana() (między innymi) i wewnętrznie delegują do odpowiedniej implementacji bibliotecznej na podstawie typu otrzymanego obiektu, zamiast wymagać, byś sam wybierał właściwą funkcję. To samo dotyczy jordan_wigner() oraz ternary_tree() (opisanego niżej): każda z nich kieruje wywołanie do bezpośredniej implementacji pasującej do otrzymanego operatora. Zobacz qiskit_fermions.protocols, aby dowiedzieć się, jak to kierowanie jest zaimplementowane wewnętrznie.
Wybierz kodowanie za pomocą drzew trójkowych
Jordan-Wigner nie jest jedynym kodowaniem fermion–kubit i nie jest najtańszym. Jego ciągi rosną wraz z indeksem modu, więc operator Majorany na ostatnim z modów odwzorowuje się na ciąg Pauliego o wadze . Kodowania, które rozprowadzają informację o parzystości po drzewie zamiast po łańcuchu, są asymptotycznie lepsze.
Drzewo trójkowe o węzłach obejmuje całą tę rodzinę. Każdy węzeł to kubit z trzema dowiązaniami w dół, oznaczonymi X, Y i Z; dowiązanie albo prowadzi do węzła potomnego (krawędź), albo się kończy (liść). Ścieżka każdego liścia z powrotem do korzenia zapisuje ciąg Pauliego: przejście dowiązaniem oznaczonym , które schodzi z węzła , wnosi na kubit . Każde takie drzewo daje poprawne kodowanie, a znane kodowania to po prostu szczególne kształty – łańcuch wzdłuż Z to Jordan-Wigner, a drzewo, w którym każdy węzeł ma trzech potomków (na tyle, na ile pozwala liczba węzłów), jest zrównoważone.
- 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);
Ta maksymalna waga jest optymalna. Jiang i in. dowodzą, że operatory Majorany dowolnego odwzorowania fermion–kubit dla modów mają średnią wagę Pauliego co najmniej , więc żadne odwzorowanie nie może sprowadzić największej z nich poniżej . Oba drzewa używają dokładnie kubitów, więc oszczędność dotyczy wagi Pauliego, a nie liczby kubitów.
Aby odwzorować operator, skompiluj drzewo do kodowania i przekaż je mapperowi wraz z liczbą kubitów wyniku. Kompilacja łączy liście drzewa w pary operatorów Majorany i jednorazowo buduje ich ciągi Pauliego, więc to samo kodowanie warto wykorzystać dla wielu operatorów. Liczba kubitów jest częścią wywołania odwzorowania, a nie kodowania, dzięki czemu jedno kodowanie może obsługiwać rejestry o różnej szerokości; liczba większa niż liczba węzłów drzewa jest dopełniana identycznością.
- 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);
Odwzorowanie przez łańcuch wzdłuż Z dokładnie odtwarza fermion_jordan_wigner() (qf_ferm_op_jordan_wigner() w API C), i właśnie w tym sensie jest to jego uogólnienie:
>>> 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
Każda reprezentacja operatora ma mapper oparty na drzewie trójkowym, odpowiadający zestawowi Jordana-Wignera; pełną listę znajdziesz w qiskit_fermions.mappers.library, a dla API C w Mapper Library. W API Pythona są one dostępne także przez ternary_tree(), uniwersalną nakładkę, która wybiera właściwy mapper dla otrzymanego operatora, tak jak robi to 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)>
Niektóre tożsamości obowiązujące w kodowaniu Jordana-Wignera są właściwościami łańcucha, a nie drzew trójkowych. Operator liczby cząstek ma postać tylko na łańcuchu; na innym drzewie staje się ciągiem na zbiorze kubitów. Podobnie operator wierzchołka jest operatorem Pauliego o wadze 1 tylko na łańcuchu, gdzie łańcuchy obu ciągów Majorany się znoszą.
Ponieważ każdy operator liczby cząstek nadal odwzorowuje się na pojedynczy ciąg , stan Focka nadal jest stanem bazy obliczeniowej w dowolnym drzewie trójkowym, ale bit nie odpowiada już obsadzeniu . Obsadzenie jest natomiast parzystością zmierzonych bitów na nośniku obrazu . Odczytaj te nośniki z mappera, zamiast wyprowadzać je z drzewa, aby dekodowanie pozostało poprawne dla dowolnego kształtu lub permutacji modów:
>>> 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)>
Na tym zrównoważonym drzewie mod 0 jest obsadzony, gdy kubity 0, 1 i 2 zawierają nieparzystą liczbę jedynek, więc jego przygotowanie oznacza odwrócenie samego kubitu 0, natomiast dekodowanie zmierzonego ciągu bitów wymaga parzystości po wszystkich trzech. Pojedynczy błąd odwrócenia bitu na kubicie 1 psuje wtedy obsadzenia zarówno modu 0, jak i modu 1, co ma znaczenie w każdym przepływie pracy, który odczytuje obsadzenia z zaszumionych próbek.
Gdy Evolution jest obniżana przez taki mapper, opakuj mapper w group_wise() (zobacz Synteza bramki Evolution w przestrzeni fermionowej). Wyraz przeskoku odwzorowuje się na dwa komutujące ciągi Pauliego, z których żaden sam nie zachowuje liczby cząstek; zachowuje ją dopiero ich iloczyn w ewolucji. W kodowaniu Jordana-Wignera oba ciągi działają na tych samych kubitach, więc uproszczona kolejność wyrazów trzyma je obok siebie. Na innych drzewach ich nośniki się różnią (jak w przypadku Y_3 Z_1 Y_0 i X_3 Z_2 X_0 wcześniej), zarówno surowa, jak i uproszczona kolejność przeplata je z obrazami innych wyrazów, a wtedy formuła produktowa po stronie kubitów powoduje wyciek liczby cząstek.
Zbuduj dowolne drzewo
Konstruktory łańcucha i przeszukiwania wszerz obejmują jednorodne rodziny, ale ogólny interfejs przyjmuje jedną parę (parent, label) na węzeł, z None dla korzenia (API C przyjmuje zamiast tego indeks korzenia). Ma to znaczenie, ponieważ drzewa warte poszukiwania zwykle nie są ani łańcuchem, ani jednorodne – algorytm Bonsai na przykład hoduje drzewo rozpinające grafu połączeń urządzenia, więc jego rozgałęzienia podążają za tą łącznością.
- 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);
Zwróć uwagę na liczbę liści: zawsze wynosi , niezależnie od kształtu. Węzeł z mniejszą liczbą potomków nie generuje mniej ciągów Pauliego – jego niewykorzystane dowiązania są liśćmi, a liść zapisuje ciąg tak samo jak potomkowie krawędzi. Spośród tych ciągów łączy się w pary w operatory Majorany modów i dokładnie jeden zostaje.
Parzystość całkowita
Ten pozostały liść to całkowita parzystość fermionowa . Jest ona własnością kodowania, a nie odwzorowaniem jakiegokolwiek operatora, więc jest metodą samego kodowania:
- 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);
Jak pokazuje przypadek zrównoważony, nie jest to w ogólności ciąg złożony z samych : jest to liść osiągany ścieżką złożoną z samych , który działa jako identyczność na każdym węźle spoza tej ścieżki.
Parzystość całkowita jest tu obserwablą do zmierzenia, a nie ograniczeniem do narzucenia. Obie jej wartości własne są fizyczne, więc nie rzutuj stanu na sektor : kodowanie drzewem trójkowym zużywa jeden kubit na mod i jest na całą -wymiarową przestrzeń, więc nie ma niefizycznej podprzestrzeni do odrzucenia.
Przykład praktyczny: drzewo Bravyi-Kitaeva
Specyfikacje częściej się oblicza, niż wpisuje, ponieważ drzewo prawdziwego kodowania rzadko jest dość małe, by zapisać je ręcznie. Bravyi-Kitaev jest dobrym przykładem: należy do tej rodziny, ale jego drzewo nie jest jednorodne, więc żaden wygodny konstruktor go nie obejmuje.
Jego drzewo powstaje przez rekurencyjny podział węzłów, po jednym na mod. W zakresie węzłów niech będzie największą potęgą dwójki, która się mieści; węzłem umieszczonym dla tego zakresu jest , ostatni w tym bloku. Jego dowiązanie X schodzi do drzewa zbudowanego z bloku przed nim, a dowiązanie Z do drzewa zbudowanego z reszty po nim, pozostawiając dowiązanie Y jako liść:
>>> 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)
Dla czterech modów cały zakres jest jednym blokiem, więc pierwszym umieszczonym węzłem jest 3, z przed nim i niczym po nim; ten podzakres daje węzeł 1 nad pojedynczymi węzłami 0 i 2. Kompilacja drzewa potwierdza, że kodowanie to Bravyi-Kitaev, a nie tylko drzewo tej samej klasy kształtu – jego pierwszy obraz Majorany to :
>>> 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')]
Jego parzystość całkowita uwidacznia sens poprzedniej sekcji: pozostały liść to ten osiągany ścieżką złożoną z samych , a tutaj ścieżka ta kończy się w korzeniu, więc obserwabla to jednokubitowe , a nie ciąg po wszystkich czterech kubitach:
>>> encoding.total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3)>
Skalowanie w górę wymaga tylko większego argumentu. Ponieważ żaden węzeł nigdy nie zużywa dowiązania Y na krawędź, każdy rozgałęzia się najwyżej na dwie strony, więc waga Pauliego rośnie logarytmicznie, a nie liniowo:
>>> [bravyi_kitaev_tree(n).max_weight for n in (4, 8, 16, 32, 64)]
[3, 4, 5, 6, 7]