Praca z obwodami bozonowymi
BosonicCircuit wyraża obliczenie bezpośrednio na trybach bozonowych. Nie niesie ze sobą obcięcia, kodowania ani liczby kubitów: te pojawiają się później, podczas transpilacji, dzięki czemu jeden obwód można zredukować przy kilku kodowaniach i kilku obcięciach bez jego przebudowy.
Celowo udostępnia zredukowane API w porównaniu z QuantumCircuit z Qiskit, tak aby bramek kubitowych nie dało się zastosować do trybu bozonowego, co nie byłoby dobrze zdefiniowaną operacją. Ten przewodnik omawia operacje, które są udostępnione, do składania większych obwodów z mniejszych.
Dlaczego warto używać obwodów bozonowych
Rozważ tradycyjny przepływ pracy przy przenoszeniu ewolucji czasowej hamiltonianu bozonowego na architekturę opartą na kubitach:
-
Zdefiniuj hamiltonian bozonowy, na przykład .
-
Wybierz obcięcie i kodowanie oraz zmapuj za ich pomocą hamiltonian na operator kubitowy.
-
Zaimplementuj ewolucję czasową w przestrzeni kubitów, przez trotteryzację lub inną metodę.
Drugi krok następuje wcześnie i podejmuje dwie decyzje, które trudno cofnąć. Obcięcie ustala, ile poziomów zachowuje każdy tryb, a kodowanie ustala, ile kubitów to kosztuje, więc oba są zafiksowane w operatorze kubitowym, zanim cokolwiek innego zrobiono z obwodem. Aby spojrzeć na tę samą fizykę przy innym odcięciu lub innym kodowaniu, mapujesz hamiltonian ponownie i budujesz obwód ponownie. Mapowanie odrzuca też strukturę bozonową: które człony należą do siebie i to, że człony pary przeskoku są swoimi wzajemnymi sprzężeniami.
BosonicCircuit przenosi te decyzje na koniec:
-
Zdefiniuj hamiltonian bozonowy.
-
Wyraź ewolucję czasową bezpośrednio na trybach bozonowych, za pomocą bramek bozonowych.
-
Wybierz obcięcie i kodowanie w ramach transpilacji.
Obwód opisuje, jakie obliczenie wykonać, a transpilacja decyduje, jak umieścić je na kubitach. Ten sam obwód jest następnie redukowany przy każdym odcięciu i każdym kodowaniu, a grupowanie operatora wciąż istnieje dla przebiegów, które mogą z niego skorzystać. Kolejne sekcje pokazują, jak taki obwód się składa; Transpilacja obwodów bozonowych pokazuje jego redukcję.
Ogólne indeksowanie trybów
Moduł operators i obwód mają wspólną konwencję dla trybów. Tryb to tylko indeks oznaczający bozonowy stopień swobody i sam w sobie nie niesie żadnego znaczenia. Możesz więc ewoluować dowolny operator implementujący protokół OperatorTrait, cokolwiek reprezentuje, a obwód nigdy nie musi wiedzieć, co oznacza tryb 2. To samo dotyczy rozmiaru trybu: obwód nie ma odcięcia obsadzeń, więc nic tu nie mówi, ile poziomów ma tryb.
Dołączanie bramek
Bramki są dołączane do trybów. Akceptowane są tylko instancje BosonicGate:
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> hopping = BosonOperator.from_dict({(cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0})
>>> hopping.groups = [0] * len(hopping) # the pair is Hermitian, its terms alone are not
>>>
>>> prepare = BosonicCircuit(2)
>>> prepare.append(InitializeModes([2, 0]), prepare.modes)
>>> step = BosonicCircuit(2)
>>> step.append(Evolution(2, hopping, time=0.1), step.modes)
Wszystko inne jest odrzucane, a błąd wskazuje nazwę niewłaściwego typu:
>>> from qiskit.circuit.library import HGate
>>> try:
... prepare.append(HGate(), [0])
... except ValueError as error:
... print(error)
Unsupported instruction type: <class '...HGate'>
Składanie obwodów
compose() umieszcza instrukcje jednego obwodu w drugim. Są one wstawiane w miejscu, a nie opakowywane w jedną bramkę, więc wynik jest zwykłym obwodem, który możesz dalej dekomponować i transpilować.
>>> program = prepare.compose(step)
>>> program.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Domyślnie zwracany jest nowy obwód, a oba operandy pozostają nietknięte. Przekaż inplace=True, aby zamiast tego zmodyfikować pierwszy, oraz front=True, aby umieścić operand przed istniejącymi instrukcjami:
>>> step.compose(prepare, front=True)._inner.data == program._inner.data
True
Węższy obwód można umieścić na wybranym podzbiorze trybów:
>>> single = BosonicCircuit(1)
>>> single.append(Evolution(1, BosonOperator.from_dict({(cre(0), ann(0)): 1.0}), 0.1), single.modes)
>>> wide = BosonicCircuit(3).compose(single, [2])
>>> wide.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Jako operand akceptowany jest tylko inny BosonicCircuit, ponieważ własny compose z Qiskit wstawiłby bramki kubitowe i ominął kontrolę, którą wymusza append.
Powtarzanie obwodu
repeat() powtarza obwód od początku do końca. Powtórzenia są spłaszczone: wynik zawiera instrukcje reps razy po kolei, a nie jedną bramkę opakowującą obwód. W związku z tym liczba operacji skaluje się z reps, a decompose działa na powtórzonym ciele zamiast usuwać jedno powtórzenie.
>>> len(step.repeat(0)._inner.data) # zero repetitions is the identity
0
>>> step.repeat(5).draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
To sposób trotteryzacji na poziomie obwodu: ewoluuj przez krótki czas i powtórz. W Synteza bramki Evolution w przestrzeni bozonowej znajdziesz alternatywę polegającą na wyborze wzoru produktowego na samej bramce oraz wyjaśnienie, dlaczego to dwie różne rzeczy, które tylko przypadkiem dzielą słowo „powtórzenia”.
Rozdzielanie kroków barierami
Bariera oznacza punkt synchronizacji, ponad którym przebiegi optymalizacji nie scalają bramek. barrier() umieszcza ją na wszystkich trybach lub na trybach, które wskażesz:
>>> program = BosonicCircuit(2)
>>> program.append(InitializeModes([2, 0]), program.modes)
>>> program.barrier()
>>> program.barrier(program.modes[0], label="zero")
>>> program.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
repeat(..., insert_barriers=True) wstawia jedną między kolejnymi powtórzeniami, a po ostatnim żadnej:
>>> step.repeat(3, insert_barriers=True).draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Bariery a kodowanie
Kodowanie potrzebuje kilku kubitów na tryb, więc rejestr trybów i rejestr kubitów różnią się rozmiarem, a układ (layout) wiąże całe rejestry. Tryb nie ma więc własnych kubitów, do których można by zawęzić barierę. Podczas syntezy bozon–kubit bariera jest mapowana na każdy kubit każdego rejestru, którego dotykała, co jest poprawne przy dowolnym układzie:
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>> lowered = generate_preset_occupation_pass_manager(LevelSpec(2, "unary_projected")).run(program)
>>> lowered.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Odczyt obwodu bozonowego
BosonicCircuit nie ma measure ani measure_all. To, co oznacza zmierzony bit, zależy od kodowania. W kodowaniu unarnym obsadzenie trybu jest rozłożone na kilka kubitów i trzeba je odczytać ze wzorca bitów, a w zwartym jest liczbą binarną, więc pomiar wskazujący tryb musiałby znać kodowanie, czego obwód bozonowy celowo nie robi. Zamiast tego zmierz transpilowany QuantumCircuit i zdekoduj ciągi bitów za pomocą LevelEncoding.level_to_bits() i przesunięć LevelSpec, przy którym redukowano obwód. Symulacja dynamiki Bosego-Hubbarda z wyborem kodowania robi to dla kompletnego modelu.
>>> hasattr(BosonicCircuit, "measure_all")
False
Dalsze kroki
-
Transpilacja obwodów bozonowych wyjaśnia etap po etapie, jak obwód bozonowy staje się obwodem kubitowym.
-
Obracanie trybów bozonowych omawia bramkę, która miesza tryby i zachowuje liczbę bozonów.
-
Synteza bramki Evolution w przestrzeni bozonowej pokazuje, jak trotteryzować ewolucję w przestrzeni bozonowej.
-
Zasady projektowe reprezentacji operatorów wyjaśniają operatory, które niosą bramki.