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?

Zasady projektowe reprezentacji operatorów

Ten przewodnik wyjaśnia wspólne zasady projektowe i podstawowe pojęcia wspólne dla wszystkich reprezentacji operatorów w module operators.

Przegląd​

Reprezentacje operatorów udostępniane przez ten moduł mają kilka wspólnych podstawowych zasad projektowych:

  • Rzadka struktura danych: Operatory kodują tylko operacje nietożsamościowe. Wewnętrzny układ danych jest zasadniczo inspirowany formatami rzadkich macierzy, co umożliwia wydajne przechowywanie i obliczenia dla układów z wieloma trybami, ale względnie niewielką liczbą istotnych wkładów.

  • Iteracja po wyrazach i rekonstrukcja: Niezależnie od wewnętrznego rzadkiego przechowywania operatory udostępniają spójny interfejs iteracji, więc możesz sprawdzać, filtrować i przekształcać wyrazy bez znajomości struktury danych, a następnie rekonstruować nowe operatory ze zmodyfikowanych wyrazów.

  • Indeksowanie oparte na trybach: Operatory używają abstrakcyjnych indeksów trybów do oznaczania fermionowych stopni swobody, co umożliwia elastyczne odwzorowanie układów fizycznych na reprezentację operatora.

  • Grupowanie wyrazów i relacje komutacji: Operatory natywnie obsługują informację o grupowaniu, która wiąże wyrazy z indeksami grup. Umożliwia to optymalizacje i zachowanie struktury fizycznej bez osobnych struktur danych. Praktyczne użycie opisuje przewodnik o grupowaniu.

  • Operacje arytmetyczne i matematyczne: Wszystkie operatory implementują spójny zestaw operacji arytmetycznych (takich jak dodawanie, mnożenie i składanie) oraz funkcji matematycznych za pomocą protokołu OperatorTrait, co umożliwia jednolity kod dla różnych typów operatorów.

  • Kolejność wyrazów operatora i postacie normalne: Matematycznie równoważne operatory mogą mieć bardzo różne reprezentacje i zachowanie w algorytmach kwantowych. Wszystkie reprezentacje operatorów obsługują różne postacie normalne (oparte na relacjach komutacji właściwych dla danej algebry), aby uzyskać kanoniczne, przewidywalne i optymalizowalne reprezentacje operatorów.

Rzadka struktura danych​

Wszystkie operatory używają rzadkiej reprezentacji, w której śledzone są tylko operacje nietożsamościowe. Każda operacja nietożsamościowa składa się ze współczynnika (liczba zespolona) i ciągu akcji na określonych trybach.

Takie podejście radykalnie zmniejsza zużycie pamięci i czas obliczeń, zwłaszcza dla układów z wieloma trybami, ale względnie niewielką liczbą istotnych wkładów. Dzięki kodowaniu tylko operacji nietożsamościowych operacje skupiają się wyłącznie na tym, co istotne, co umożliwia pracę z dużymi układami, które byłyby niewykonalne przy reprezentacjach gęstych. Ponadto operatory naturalnie skalują się do dowolnej liczby trybów; operator działający na trybach {0, 1} działa bez zmian w układach z wieloma dodatkowymi trybami, ponieważ niezaangażowane tryby są domyślnie tożsamością.

Identyczne wyrazy są zachowywane osobno podczas operacji arytmetycznych i w razie potrzeby trzeba je jawnie połączyć.

Wewnętrzny format przechowywania​

Wewnętrznie operatory są przechowywane w tablicach inspirowanych formatami rzadkich macierzy:

  • Tablica współczynników: Zespolony współczynnik każdego wyrazu

  • Tablica indeksów trybów: Tryby fermionowe, na które działa każda akcja

  • Tablica granic: Indeksy wskazujące, gdzie w tablicy trybów zaczynają się i kończą tryby każdego wyrazu

Ważne

W zależności od typu operatora mogą występować dodatkowe tablice. Na przykład instancje FermionOperator zawierają tablicę akcji wartości logicznych, która określa typ akcji fermionowej działającej na dany indeks trybu. Natomiast klasa MajoranaOperator nie wymaga tego rozróżnienia, ponieważ koduje tę informację w parzystości indeksu trybu. Pełny format przechowywania znajdziesz w dokumentacji API dla swojego typu operatora.

Poniższe przykłady pokazują, jak te tablice są zorganizowane. Najpierw bezpośrednia konstrukcja z użyciem rzadkich tablic:

>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> # Construct operators directly using sparse arrays
>>> # First operator: 1.0 * +0 -1
>>> op1 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[0, 1],
... boundaries=[0, 2],
... )
>>>
>>> # Second operator: 1.0 * +2 -3
>>> op2 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[2, 3],
... boundaries=[0, 2],
... )
>>>
>>> # Combine the sparse operators
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)

Wygodne metody konstrukcji​

Dla programistów Pythona dostępnych jest kilka wygodnych metod konstrukcji, które ukrywają szczegóły rzadkiego przechowywania danych. Dzięki nim łatwiej budować operatory bez zajmowania się zarządzaniem tablicami współczynników, modów i granic:

>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct operators using operator algebra notation
>>> op1 = FermionOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> op2 = FermionOperator.from_dict({(cre(2), ann(3)): 1.0})
>>>
>>> # The result is sparse even when combining them
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)
Wskazówka

Poszczególne implementacje operatorów mogą wspierać dodatkowe metody konstrukcji dopasowane do ich konkretnego zastosowania. Zajrzyj do dokumentacji API swojego typu operatora, aby zobaczyć wszystkie dostępne opcje konstrukcji.

Iteracja po składnikach i rekonstrukcja​

Operatory udostępniają spójny interfejs iteracji za pomocą OperatorTrait.iter_terms(), niezależnie od ich wewnętrznej rzadkiej reprezentacji. Dzięki temu możesz przeglądać, filtrować lub przekształcać składniki bez konieczności rozumienia leżącej u podstaw struktury danych. Następnie możesz odtworzyć nowy operator z przekształconych składników za pomocą OperatorTrait.from_terms().

>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct an operator with terms of different orders
>>> op = FermionOperator.from_dict({
... (): 0.5, # constant term (order 0)
... (cre(0), ann(1)): 1.0, # two-body term (order 2)
... (cre(0), cre(1), ann(1), ann(0)): 0.25 # four-body term (order 4)
... })
>>>
>>> # Filter to keep only terms of order 2
>>> order_two_terms = [
... (term, coeff) for term, coeff in op.iter_terms()
... if len(term) == 2
... ]
>>>
>>> # Reconstruct operator from filtered terms
>>> filtered_op = FermionOperator.from_terms(order_two_terms)
>>> print(f"Original operator has {len(op)} terms")
Original operator has 3 terms
>>> print(f"Filtered operator has {len(filtered_op)} term")
Filtered operator has 1 term

Indeksowanie oparte na modach​

Wszystkie reprezentacje operatorów odnoszą się do indeksów, na które działają ich składniki, jako do modów. Mod to po prostu indeks identyfikujący fermionowy stopień swobody w Twoim układzie. Odwzorowanie fizycznych stopni swobody (takich jak orbitale przestrzenne, stany spinowe lub inne liczby kwantowe) na indeksy modów pozostawiono użytkownikowi, co zapewnia maksymalną elastyczność.

Ta abstrakcja występuje również w module qiskit_fermions.circuit, w którym FermionicCircuit działa na rejestrze modów fermionowych. Zarówno w reprezentacji operatorowej, jak i obwodowej mody zapewniają spójny, niezależny od algebry sposób określania, które stopnie swobody uczestniczą w danej operacji.

Ważne

Obecne implementacje używają modów bezspinowych: wszystkie reprezentacje operatorów udostępniane obecnie przez ten moduł traktują mody jako bezspinowe fermionowe stopnie swobody. Oznacza to, że jeśli Twój układ zawiera zarówno elektrony lub fermiony o spinie w górę, jak i w dół, musisz jawnie odwzorować je na odrębne mody (na przykład mody 0-3 dla spinu w górę czterech orbitali przestrzennych, mody 4-7 dla spinu w dół lub dowolna inna przyjęta przez Ciebie konwencja).

Ten projekt utrzymuje podstawowe reprezentacje w prostej i ogólnej formie, a jednocześnie nie narzuca konkretnej konwencji uporządkowania spinów. Moduły narzędziowe, takie jak qiskit_fermions.operators.library, udostępniają funkcje pomocnicze (na przykład FCIDump.from_file()), które obsługują takie odwzorowania za Ciebie podczas wczytywania danych struktury elektronowej.

Wskazówka

W miarę rozwoju pakietu mogą zostać dodane reprezentacje operatorów ze spinem, które natywnie wspierają stopnie swobody spinu w swoim modelu danych. Będą one wyraźnie odróżnione od obecnych implementacji bezspinowych i będą z nimi współistnieć w module.

Grupowanie składników i relacje komutacji​

Podobnie jak współczynniki i indeksy modów, operatory mogą opcjonalnie przechowywać tablicę grup, która przypisuje każdemu składnikowi indeks grupy. Dzięki wbudowaniu grupowania bezpośrednio w reprezentację operatora jako części rzadkiej struktury danych informacje o grupowaniu naturalnie podróżują razem z operatorem przez kolejne transformacje. Umożliwia to systematyczne wykorzystanie struktury, czy to wynikającej z właściwości fizycznych, relacji algebraicznych, czy symetrii specyficznych dla problemu. Uzyskane w ten sposób informacje o strukturze można następnie wykorzystać w dalszych operacjach, takich jak synteza i dekompozycja obwodów, za pomocą metod takich jak OperatorTrait.split_out_groups().

Szczegółowe wskazówki dotyczące grupowania składników operatorów w Twoich przepływach pracy znajdziesz w przewodniku grupowanie.

>>> from qiskit_fermions.operators import MajoranaOperator, gamma
>>> op = MajoranaOperator.from_dict({
... (gamma(0, False),): 1.0,
... (gamma(1, False),): 1.0,
... (gamma(2, False), gamma(3, False)): 1.0
... })
>>> # Assign group indices to terms
>>> op.groups = [0, 0, 1]
>>> # Partition operator by groups
>>> grouped_ops = op.split_out_groups()

Operacje arytmetyczne i matematyczne​

Wszystkie operatory implementują protokół OperatorTrait, który zapewnia ujednolicony zestaw operacji dla różnych typów operatorów. Dzięki temu kod napisany dla jednej reprezentacji operatora działa jednolicie z pozostałymi. Zobacz qiskit_fermions.protocols, aby dowiedzieć się, jak ten protokół odnosi się do pozostałych definiowanych przez ten pakiet.

Protokół obejmuje operacje arytmetyczne (takie jak dodawanie, mnożenie i składanie), operacje strukturalne (iteracja po składnikach, analiza wsparcia modów, zmiana etykiet), funkcje matematyczne (uporządkowanie normalne, upraszczanie, sprawdzanie równoważności) i inne. Pełny opis wszystkich dostępnych operacji znajdziesz w dokumentacji OperatorTrait.

>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct a Hermitian operator: H = +0 -1 + +1 -0
>>> op = FermionOperator.from_dict({
... (cre(0), ann(1)): 1.0,
... (cre(1), ann(0)): 1.0
... })
>>>
>>> # Check if the operator is Hermitian
>>> print(f"Operator is Hermitian: {op.is_hermitian(atol=1e-10)}")
Operator is Hermitian: True
Uwaga

is_hermitian() sprawdza, że H−H†=0H - H^\dagger = 0, redukując tę różnicę do postaci normalnej i porównując ją z zero(). Rozpisane za pomocą innych operacji protokołu, wykonuje ono następujące kroki:

>>> difference = (op - op.adjoint()).normal_ordered().simplify(atol=1e-10)
>>> difference.equiv(FermionOperator.zero(), atol=1e-10)
True

Siła tego sprawdzenia zależy od typu operatora, ponieważ zależy od tego, jak dokładnie postać normalna danego typu kontraktuje sąsiednie generatory. Wynik True jest zawsze wiarygodny, ale dla niektórych typów wynik False jest zachowawczy: szczegóły znajdziesz w is_hermitian().

Ważne

Przykład używa atol=1e-10. Parametr atol (tolerancja bezwzględna) określa próg. Współczynniki o module mniejszym niż atol są traktowane jako zero i odrzucane. Jest to niezbędne dla stabilności numerycznej przy porównywaniu operatorów, ponieważ arytmetyka zmiennoprzecinkowa może wprowadzać drobne błędy zaokrągleń, które w przeciwnym razie uniemożliwiłyby rozpoznanie równoważnych operatorów jako takich.

Wskazówka

Choć protokół OperatorTrait zapewnia wspólny interfejs, poszczególne implementacje operatorów mogą oferować dodatkowe wygodne metody, które nie są częścią protokołu. Na przykład FermionOperator i MajoranaOperator udostępniają metodę max_rank(), która zwraca długość ich najdłuższego składnika. Zawsze zajrzyj do dokumentacji API swojego typu operatora, aby poznać wszystkie dostępne funkcje.

Uporządkowanie składników operatora i postacie normalne​

Fundamentalnym wyzwaniem w algebrze operatorów kwantowych jest to, że matematycznie równoważne operatory można przedstawiać na wiele różnych sposobów, z których każdy ma inne konsekwencje dla algorytmów kwantowych. Ten sam operator można zapisać w algebraicznie równoważnych postaciach (na przykład a†ba^\dagger b można wyrazić jako ba†+[a†,b]ba^\dagger + [a^\dagger,b]), a mimo to te reprezentacje prowadzą do odmiennego zachowania w syntezie obwodów, upraszczaniu i algorytmach numerycznych.

Reprezentacje operatorów wspierają operacje uporządkowania normalnego, które przekształcają operatory do specyficznych dla danej algebry postaci kanonicznych z użyciem relacji komutacji. Umożliwia to wiarygodne porównania (dwa równoważne operatory mają identyczne postacie uporządkowane normalnie), ujawnia uproszczenia (relacje komutacji powodują znoszenie się lub łączenie składników) i wspiera algorytmy wymagające określonych postaci operatorów dla poprawności i wydajności.

Ważne

Różne reprezentacje operatorów mogą implementować uporządkowanie normalne w oparciu o różne relacje komutacji odpowiednie dla ich algebry. Na przykład fermionowe uporządkowanie normalne wykorzystuje relacje antykomutacji ({ci,cj†}=δij\{c_i, c_j^\dagger\} = \delta_{ij}), podczas gdy uporządkowanie normalne Majorany stosuje inne konwencje algebry ({γi,γj}=2δij\{\gamma_i, \gamma_j\} = 2\delta_{ij}). Zawsze zajrzyj do dokumentacji swojego typu operatora, aby zrozumieć, jak zaimplementowano uporządkowanie normalne.

>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Two different representations of the same operator
>>> op1 = FermionOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> op2 = FermionOperator.from_dict({(): 1.0, (cre(0), ann(0)): -1.0})
>>>
>>> # Direct comparison fails due to different forms
>>> op1.equiv(op2, atol=1e-10)
False
>>>
>>> # Normal-order both and compare again
>>> op1_normal = op1.normal_ordered()
>>> op2_normal = op2.normal_ordered()
>>> op1_normal.equiv(op2_normal, atol=1e-10)
True
Wskazówka

Metoda protokołu OperatorTrait.normal_ordered() celowo pozostawia swoje argumenty pozycyjne i nazwane nieokreślone, co pozwala konkretnym implementacjom operatorów definiować parametry sterujące dokładną wytwarzaną postacią kanoniczną. Umożliwia to optymalizacje specyficzne dla operatora oraz warianty postaci normalnej dopasowane do jego algebry lub zastosowania. Zajrzyj do dokumentacji API swojego typu operatora, aby zobaczyć, jakie parametry są dostępne.