Przejdź do głównej treści
Ten projekt jest w wersji beta. Znalazłeś błąd? Otwórz zgłoszenie na GitHub. Masz pytanie lub pomysł? Zapytaj w Discussions — chętnie poznamy Twoją opinię!
Jak oceniasz jakość tłumaczenia tej strony?

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():

>>> 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)>

Kluczowe kroki implementacji dowolnego niestandardowego mappera to:

  1. 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.

  2. Użyj jednej z dostarczonych funkcji odwzorowujących: Moduł mappers udostę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ład map_majorana_action_generators() działa z operatorami Majorany.

  3. 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.

Wskazówka

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:

>>> 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
Uwaga

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 ZZ rosną wraz z indeksem modu, więc operator Majorany na ostatnim z nn modów odwzorowuje się na ciąg Pauliego o wadze nn. Kodowania, które rozprowadzają informację o parzystości po drzewie zamiast po łańcuchu, są asymptotycznie lepsze.

Drzewo trójkowe o nn 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 PP, które schodzi z węzła uu, wnosi PP na kubit uu. 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.

>>> 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

Ta maksymalna waga jest optymalna. Jiang i in. dowodzą, że operatory Majorany dowolnego odwzorowania fermion–kubit dla nn modów mają średnią wagę Pauliego co najmniej log⁡3(2n)\log_3(2n), więc żadne odwzorowanie nie może sprowadzić największej z nich poniżej ⌈log⁡3(2n+1)⌉\lceil \log_3(2n+1) \rceil. Oba drzewa używają dokładnie nn 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ą.

>>> 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)>

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)>
Uwaga

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ć nj=(1−Zj)/2n_j = (1 - Z_j)/2 tylko na łańcuchu; na innym drzewie ZjZ_j staje się ciągiem ZZ na zbiorze kubitów. Podobnie operator wierzchołka VjV_j jest operatorem Pauliego ZjZ_j o wadze 1 tylko na łańcuchu, gdzie łańcuchy ZZ obu ciągów Majorany się znoszą.

Ponieważ każdy operator liczby cząstek nadal odwzorowuje się na pojedynczy ciąg ZZ, stan Focka nadal jest stanem bazy obliczeniowej w dowolnym drzewie trójkowym, ale bit qq nie odpowiada już obsadzeniu qq. Obsadzenie jj jest natomiast parzystością zmierzonych bitów na nośniku obrazu njn_j. 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ą.

>>> # 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}

Zwróć uwagę na liczbę liści: zawsze wynosi 2n+12n+1, 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 2n+12n+1 ciągów 2n2n łączy się w pary w operatory Majorany nn modów i dokładnie jeden zostaje.

Parzystość całkowita​

Ten pozostały liść to całkowita parzystość fermionowa ∏j(1−2nj)\prod_j (1 - 2 n_j). Jest ona własnością kodowania, a nie odwzorowaniem jakiegokolwiek operatora, więc jest metodą samego kodowania:

>>> 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)>

Jak pokazuje przypadek zrównoważony, nie jest to w ogólności ciąg złożony z samych ZZ: jest to liść osiągany ścieżką złożoną z samych ZZ, który działa jako identyczność na każdym węźle spoza tej ścieżki.

Ważne

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 +1+1: kodowanie drzewem trójkowym zużywa jeden kubit na mod i jest na całą 2n2^n-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 [l,r][l, r] niech hh będzie największą potęgą dwójki, która się mieści; węzłem umieszczonym dla tego zakresu jest l+h−1l + h - 1, 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 [0,2][0, 2] 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 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')]

Jego parzystość całkowita uwidacznia sens poprzedniej sekcji: pozostały liść to ten osiągany ścieżką złożoną z samych ZZ, a tutaj ścieżka ta kończy się w korzeniu, więc obserwabla to jednokubitowe Z3Z_3, 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]