Zasady projektowe reprezentacji operatorów
Ten przewodnik wyjaśnia, jak zorganizowany jest BosonOperator i jak z nim pracować: jak go zbudować, jak sprawdzać i łączyć operatory oraz jak doprowadzić dwa operatory do postaci, w której można je porównać. Zacznij tutaj, gdy zapisujesz hamiltonian.
BosonOperator jest dokładną sumą iloczynów bozonowych operatorów kreacji i anihilacji o zespolonych współczynnikach. Nie niesie żadnego odcięcia obsadzeń. Mod bozonowy może zawierać dowolnie wiele bozonów, więc jego przestrzeń Focka jest nieskończenie wymiarowa, a obcięcie pojawia się dopiero przy odwzorowaniu operatora na kubity (zobacz Obcinanie przestrzeni Focka). Wszystko w tym przewodniku odbywa się zatem w dokładnej algebrze kanonicznych relacji komutacji,
Dwie konsekwencje kształtują interfejs. Indeks modu może powtarzać się w obrębie jednego członu, ponieważ jest poprawnym operatorem, który tworzy dwa bozony. Ponadto zmiana kolejności dwóch czynników nigdy nie wprowadza znaku, choć przesunięcie operatora anihilacji za operator kreacji na tym samym modzie dodaje człon, .
Przechowywanie
Operator przechowuje swoje człony w czterech płaskich tablicach, podobnie do skompresowanego formatu wierszowego (CSR) macierzy rzadkiej:
coeffs | Jeden zespolony współczynnik na człon. |
actions | Jedna wartość logiczna na czynnik: True dla operatora kreacji i False dla anihilacji. |
modes | Jeden indeks modu na czynnik. |
boundaries | Pozycje w actions i modes, w których każdy człon się zaczyna i kończy. |
Konstruktor przyjmuje te cztery tablice bezpośrednio. Poniższy operator ma trzy człony, mianowicie , oraz stałą , a tablice możesz odczytać metodami get_*:
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> op = BosonOperator(
... coeffs=[2.0, -1.0j, 0.5],
... actions=[True, False, True, True],
... modes=[0, 1, 1, 1],
... boundaries=[0, 2, 4, 4],
... )
>>> op.get_coeffs()
[(2+0j), (-0-1j), (0.5+0j)]
>>> op.get_actions()
[True, False, True, True]
>>> op.get_modes()
[0, 1, 1, 1]
>>> op.get_boundaries()
[0, 2, 4, 4]
>>> len(op)
3
Pierwszy człon obejmuje pozycje od 0 do 2 w actions i modes, drugi od 2 do 4, a trzeci od 4 do 4, w ten sposób przechowywany jest człon stały, czyli człon bez czynników. Powtórzony mod 1 w drugim członie zostaje zachowany tak, jak zapisano; człon nigdy nie jest odrzucany ani scalany dlatego, że indeks modu pojawia się dwa razy.
Operator zbudowany za pomocą from_dict() nie przechowuje swoich członów w kolejności słownika, więc jego tablice mogą wymieniać człony w innej kolejności przy każdym uruchomieniu. Sam operator jest taki sam, a nic z poniższych rzeczy, co porównuje lub wypisuje operatory, nie zależy od przechowywanej kolejności.
Taki układ sprawia, że konstrukcja i arytmetyka są tanie, z jedną konsekwencją: operator może zawierać ten sam człon kilka razy jednocześnie. Sumy i iloczyny dołączają człony, a nie scalają ich, więc wywołaj simplify(), gdy potrzebujesz unikalnych członów (zobacz Arytmetyka).
Budowanie operatorów
Najbardziej bezpośrednim sposobem zbudowania operatora jest from_dict(), który przyjmuje słownik odwzorowujący krotkę czynników na współczynnik. Funkcje cre() i ann() tworzą czynniki, a pusta krotka to człon stały:
>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dag = BosonOperator.from_dict({(cre(0),): 1.0})
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> print(format(number))
1.000000e0 +0.000000e0j * (+0 -0)
W postaci wypisanej +0 oznacza , a -0 oznacza . Konstruktory zero() i one() zwracają element neutralny dodawania i mnożenia, a from_terms() odtwarza operator z iteratora iter_terms().
Biblioteka operatorów udostępnia bezwymiarowe kwadratury położenia i pędu,
jako position() i momentum(). Razem z funkcjami komutatorów z tego samego modułu możesz bezpośrednio sprawdzić algebrę:
>>> from qiskit_bosons.operators.library import commutator, momentum, position
>>> q, p = position(0), momentum(0)
>>> q.is_hermitian() and p.is_hermitian()
True
>>> commutator(q, p).equiv(1j * BosonOperator.one())
True
>>> commutator(b, b_dag).equiv(BosonOperator.one())
True
Moduł udostępnia również anti_commutator() i double_commutator(). Wszystkie trzy przyjmują operatory tego samego typu i zwracają nowy operator.
Uważaj na kolejność iloczynu
Dwa operatory można pomnożyć na dwa sposoby i łatwo je pomylić. Operator @ jest iloczynem matematycznym, więc a @ b to . Operator & to złożenie, które podąża za konwencją Qiskit odczytu od lewej do prawej: a & b najpierw stosuje a, a potem b, więc to . Funkcje komutatorów używają @, więc commutator(a, b) to . Zbudowanie nawiasu samodzielnie z użyciem & po cichu zmienia jego znak:
>>> (b @ b_dag).equiv(b_dag @ b + BosonOperator.one())
True
>>> (b & b_dag).equiv(b_dag @ b)
True
>>> ((b @ b_dag) - (b_dag @ b)).equiv(BosonOperator.one())
True
>>> ((b & b_dag) - (b_dag & b)).equiv(-BosonOperator.one())
True
Sprawdzanie operatora
Operator nie jest bezpośrednio iterowalny, ponieważ jego człony znajdują się w płaskich tablicach opisanych wyżej. Użyj zamiast tego iter_terms(), który dla każdego członu zwraca listę par (is_creation, mode) oraz współczynnik. Iterator zwraca kopie, więc ich zmiana nie zmienia operatora.
>>> hopping = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(2), cre(2), ann(2), ann(2)): 0.5,
... }
... )
>>> len(hopping)
3
>>> sorted(hopping.iter_terms())
[([(True, 0), (False, 1)], (-1+0j)), ([(True, 1), (False, 0)], (-1+0j)), ([(True, 2), (True, 2), (False, 2), (False, 2)], (0.5+0j))]
Kilka metod podsumowuje strukturę operatora:
>>> hopping.get_support()
{0, 1, 2}
>>> hopping.min_num_modes()
3
>>> hopping.max_rank()
4
get_support() to zbiór modów, na które działa operator, min_num_modes() to najmniejszy rejestr, który może go pomieścić, a max_rank() to długość jego najdłuższego członu.
Mod to abstrakcyjny indeks oznaczający jeden bozonowy stopień swobody. Sam w sobie nie ma znaczenia: to ty określasz, któremu węzłowi sieci, której współrzędnej drgań lub któremu portowi optycznemu odpowiada dany mod. BosonicCircuit używa tych samych indeksów (zobacz Praca z obwodami bozonowymi), więc operator i niosące go bramki zgadzają się co do tego, czym jest mod 2, bez wiedzy o tym, co on reprezentuje. Nic w operatorze nie ustala, ile poziomów ma mod, dlatego rozmiar rejestru jest tylko dolnym ograniczeniem; min_num_modes() to najmniejszy rejestr, który mieści operator, a nie jego właściwość.
Metody obsadzeń podają, ile bozonów pojedynczy człon może umieścić w jednym modzie. To dolne ograniczenie odcięcia, którego potrzebuje odwzorowanie. Nie jest to odcięcie wystarczające, ponieważ wielokrotne stosowanie operatora, jak w ewolucji czasowej, może zapełniać wyższe poziomy (zobacz Obcinanie przestrzeni Focka):
>>> hopping.mode_occupations()
[1, 1, 2]
>>> hopping.mode_occupations([2, 0])
[2, 1]
>>> hopping.max_occupation()
2
Dwa dalsze sprawdzenia dotyczą wielkości zachowanych. Operator zachowuje liczbę cząstek, gdy każdy człon ma tyle samo operatorów kreacji co anihilacji. Przekazanie listy rozmiarów bloków sprawdza ten sam warunek w obrębie każdego bloku kolejnych modów:
>>> hopping.conserves_particle_number()
True
>>> move = BosonOperator.from_dict({(cre(0), ann(2)): 1.0})
>>> move.conserves_sector([3])
True
>>> move.conserves_sector([2, 1])
False
Na koniec relabel_modes() zmienia nazwy modów: mod i staje się permutation[i]. Lista musi być wystarczająco długa, aby indeksować każdy mod operatora, i nie może powtarzać żadnego wpisu:
>>> print(format(move.relabel_modes([5, 6, 7])))
1.000000e0 +0.000000e0j * (+5 -7)
Arytmetyka
Operatory obsługują dodawanie, odejmowanie, negację oraz mnożenie i dzielenie przez liczbę, w zwykłej postaci operatorowej lub w miejscu:
>>> total = 2 * b_dag + b_dag - 3j * b
>>> len(total)
3
>>> len(total.simplify())
2
>>> total.simplify().equiv(3 * b_dag - 3j * b)
True
Trzy człony total nigdy nie zostały scalone, więc operator zawierał b_dag dwukrotnie, dopóki simplify() ich nie połączył. Metoda sumuje współczynniki identycznych członów i odrzuca te, których łączna wielkość jest poniżej tolerancji. Jest to operacja strukturalna: porównuje zapisane czynniki i nie stosuje relacji komutacji. Pokrewna metoda ichop() usuwa małe współczynniki w miejscu bez wcześniejszego scalania, więc wywołaj simplify() przed ichop(), gdy operator może zawierać zduplikowane człony.
Iloczyny używają @ lub &, jak opisano wyżej, a ** podnosi operator do potęgi całkowitej:
>>> x = b_dag + b
>>> print(format((x ** 2).simplify()))
1.000000e0 +0.000000e0j * (-0 -0)
1.000000e0 +0.000000e0j * (-0 +0)
1.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0)
Wynik wymienia cztery uporządkowania tak, jak zapisano. Sprowadzenie ich do jednej postaci wymaga relacji komutacji, które stosuje następna sekcja.
Sprzężenie hermitowskie odwraca kolejność czynników w każdym członie, zamienia operatory kreacji i anihilacji oraz sprzęga współczynniki. Metoda is_hermitian() sprawdza, czy operator jest równy swojemu sprzężeniu:
>>> hop = BosonOperator.from_dict({(cre(0), ann(1)): 1j})
>>> print(format(hop.adjoint()))
0.000000e0 -1.000000e0j * (+1 -0)
>>> hop.is_hermitian()
False
>>> (hop + hop.adjoint()).is_hermitian()
True
Pojedynczy człon przeskoku nie jest hermitowski, co ma znaczenie przy grupowaniu członów do ewolucji czasowej: każda grupa musi być sama w sobie hermitowska (zobacz Grupowanie członów operatora: wykorzystanie struktury operatora).
Porządek normalny i porównywanie operatorów
Dwa iloczyny operatorów drabinkowych mogą być tym samym operatorem i mimo to wyglądać inaczej, ponieważ i są równe w algebrze. Operatory równe w tym sensie nie muszą być przechowywane jednakowo, więc ich porównanie wymaga relacji komutacji. Stosuje je metoda normal_ordered(): przesuwa każdy operator kreacji na lewo od każdego operatora anihilacji i dodaje człon za każdym razem, gdy zamieniana jest para na tym samym modzie.
>>> product = BosonOperator.from_dict({(ann(0), cre(0), ann(0), cre(0)): 1.0})
>>> ordered = product.normal_ordered()
>>> len(ordered)
5
>>> print(format(ordered.simplify()))
1.000000e0 +0.000000e0j * ()
3.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0 -0 -0)
Uporządkowany operator jest zwracany jako niescalona suma, z pięcioma przechowywanymi członami, które redukują się do trzech po scaleniu przez simplify(). Porządek normalny nigdy nie powoduje zniknięcia członu: i każdy inny powtórzony czynnik pozostają.
Argument sandwich ustala, jak rozmieszczone są mody wewnątrz dwóch grup czynników. Domyślnie obie grupy są sortowane malejąco. Przy sandwich=True operatory kreacji są rosnąco, a operatory anihilacji malejąco, co umieszcza większe indeksy ku środkowi:
>>> pair = BosonOperator.from_dict({(cre(0), cre(1), ann(0), ann(1)): 1.0})
>>> list(pair.normal_ordered().iter_terms())
[([(True, 1), (True, 0), (False, 1), (False, 0)], (1+0j))]
>>> list(pair.normal_ordered(sandwich=True).iter_terms())
[([(True, 0), (True, 1), (False, 1), (False, 0)], (1+0j))]
Operator == sprawdza dokładną równość przechowywanych członów, bez tolerancji i bez uproszczania, więc może zgłosić różnicę między dwoma operatorami, które są matematycznie równe. Metoda equiv() najpierw sprowadza różnicę dwóch operatorów do porządku normalnego, a następnie sprawdza, czy każdy współczynnik jest poniżej tolerancji. To porównanie, którego prawie zawsze potrzebujesz:
>>> reordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0, (): 1.0})
>>> contracted = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> contracted == reordered
False
>>> contracted.simplify() == reordered.simplify()
False
>>> contracted.equiv(reordered)
True
Sama metoda simplify pozostawia obie postaci rozdzielone, ponieważ scala tylko identyczne zapisane człony. Podobnie is_hermitian() porównuje operator z jego sprzężeniem w porządku normalnym, więc rozpoznaje jako hermitowski bez dodatkowej pracy.
Porządek normalny jest dokładną tożsamością w nieobciętej algebrze i na tym opiera się equiv. Nie jest tożsamością po obcięciu. Odwzorowany operator zależy od postaci, którą odwzorowujesz, więc odwzorowane obrazy contracted i reordered powyżej różnią się, tylko na najwyższym zachowanym poziomie. Rozmiar różnicy i sposób wyboru postaci opisuje Obcinanie przestrzeni Focka.
Porządkowanie i filtrowanie członów
Zmiana kolejności członów operatora, w odróżnieniu od czynników wewnątrz członu, nie zmienia operatora, ale może zmienić zachowanie algorytmów czytających człony po kolei, takich jak losowa formuła produktowa. Funkcje z qiskit_bosons.operators.terms zwracają nowy operator i zachowują indeksy grup przypisane do członów.
canonical_order() sortuje człony w stałej kolejności zależnej tylko od zapisanych czynników, więc operator wygląda tak samo niezależnie od sposobu złożenia. order_terms() sortuje według klucza, który podajesz i który otrzymuje każdy człon jako krotkę zwracaną przez iter_terms():
>>> from qiskit_bosons.operators.terms import canonical_order, order_terms
>>> mixed = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> list(canonical_order(mixed).iter_terms())
[([], (2+0j)), ([(True, 0), (False, 1)], (3+0j)), ([(True, 1), (False, 0)], (1+0j))]
>>> by_weight = order_terms(mixed, key=lambda term: abs(term[1]), reverse=True)
>>> [coeff for _, coeff in by_weight.iter_terms()]
[(3+0j), (2+0j), (1+0j)]
Jeśli operator niesie indeksy grup (zobacz Grupowanie członów operatora: wykorzystanie struktury operatora), funkcja group_order() sprawia, że każda grupa tworzy jeden ciągły przedział członów:
>>> from qiskit_bosons.operators.terms import group_order
>>> grouped = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> grouped.groups = [1, 0, 1]
>>> group_order(grouped).groups
[0, 1, 1]
Ostatni pomocnik usuwa człony zamiast je przestawiać. Człon będący iloczynem operatorów liczby, , tylko mnoży każdy stan Focka przez fazę, więc filter_diagonal_terms() może odrzucić stałą, operatory liczby i ich iloczyny, gdy ta faza nie ma znaczenia dla tego, co obliczasz. Zmienia operator w miejscu i wymaga operatora w porządku normalnym, czego nie sprawdza:
>>> from qiskit_bosons.operators.terms import filter_diagonal_terms
>>> model = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(0), cre(0), ann(0), ann(0)): 0.5,
... (cre(0), ann(0)): 0.2,
... (): 0.1,
... }
... )
>>> filter_diagonal_terms(model)
>>> len(model)
2
>>> model.is_hermitian()
True
Pozostają tylko dwa człony przeskoku. Oddziaływanie lokalne i operator liczby są diagonalne w bazie obsadzeń, więc zostają usunięte razem ze stałą. Powtórzony mod w oddziaływaniu jest obsłużony poprawnie: jest diagonalny, podczas gdy nie byłby.
Kolejne kroki
-
Grupowanie członów operatora: wykorzystanie struktury operatora pokazuje, jak oznaczać człony indeksami grup.
-
Odwzorowywanie reprezentacji operatorów zamienia operator na operator kubitowy, w tym wybór kodowania i skutki obcięcia.
-
Praca z obwodami bozonowymi umieszcza operator w obwodzie jako bramkę
Evolution. -
Symulacja dynamiki Bosego-Hubbarda z wyborem kodowania buduje hamiltonian łańcucha Bosego-Hubbarda i symuluje go od początku do końca.
-
Moduły
qiskit_bosons.operatorsiqiskit_bosons.operators.termsdokumentują pełny interfejs.