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?

Praca z obwodami fermionowymi

Ważne

Koncepcje opisane w tym przewodniku są obecnie dostępne tylko w API Pythona. Równoważna funkcjonalność zostanie udostępniona w API C w przyszłym wydaniu.

Ten przewodnik wyjaśnia, jak używać FermionicCircuit do implementowania algorytmów kwantowych bezpośrednio w przestrzeni fermionowej, zamiast najpierw mapować na kubity.

Dlaczego warto używać obwodów fermionowych​

Rozważ tradycyjny przepływ pracy przy implementacji ewolucji czasowej hamiltonianu fermionowego w architekturze opartej na kubitach:

  1. Zdefiniuj hamiltonian fermionowy (na przykład H=∑pqhpqcp†cqH = \sum_{pq} h_{pq} c^\dagger_p c_q).

  2. Zmapuj go na operator kubitowy (na przykład za pomocą mapowania Jordana-Wignera).

  3. Zaimplementuj ewolucję czasową w przestrzeni kubitów (za pomocą trotteryzacji lub innych metod).

To podejście ma fundamentalne ograniczenie: wczesne mapowanie na kubity odrzuca wiedzę o problemie zawartą w strukturze fermionowej. Standardowe przebiegi optymalizacji nie potrafią wykorzystać symetrii fermionowych ani dozwolonych zmian kolejności wyrazów, co prowadzi do nieoptymalnych obwodów o zbędnej głębokości i liczbie bramek.

FermionicCircuit umożliwia lepszy przepływ pracy:

  1. Zdefiniuj hamiltonian fermionowy (na przykład H=∑pqhpqcp†cqH = \sum_{pq} h_{pq} c^\dagger_p c_q).

  2. Zaimplementuj ewolucję czasową bezpośrednio w przestrzeni fermionowej za pomocą bramek fermionowych.

  3. Zmapuj na przestrzeń kubitów w ramach procesu transpilacji.

To podejście rozwiązuje ten problem, zachowując wiedzę o problemie w przestrzeni fermionowej, gdzie przebiegi optymalizacji mogą wykorzystać strukturę fermionową i relacje komutacji, aby zmniejszyć głębokość obwodu. Obwód fermionowy opisuje, jakie obliczenie wykonać, a transpilacja zajmuje się tym, jak zmapować je na kubity, co zapewnia czystszy podział odpowiedzialności.

Ogólne indeksowanie modów​

Zarówno moduł operators, jak i FermionicCircuit używają ogólnego indeksowania opartego na modach, które nie czyni żadnych założeń co do natury modów. Mod to po prostu abstrakcyjny indeks oznaczający fermionowy stopień swobody, bez żadnej wewnętrznej semantyki. Ten wybór projektowy odzwierciedla moduł operators i zapewnia maksymalną elastyczność. Na przykład możesz zaimplementować ewolucję czasową dla dowolnego operatora implementującego protokół OperatorTrait, niezależnie od jego reprezentacji matematycznej.

Budowanie obwodu fermionowego​

Poniższy przykład implementuje ewolucję czasową opisaną w poprzedniej sekcji. Tworzy FermionicCircuit przez podanie liczby modów fermionowych, a następnie dodaje bramki fermionowe z qiskit_fermions.circuit.library, aby zaimplementować ewolucję czasową.

Przykład pokazuje także, jak włączyć wiedzę dziedzinową do operatora za pomocą grupowania wyrazów operatora. Przypisując indeksy grup do wyrazów hamiltonianu, zachowujesz informacje strukturalne, które przebiegi optymalizacji mogą wykorzystać w całym stosie transpilacji.

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Create a circuit with 4 fermionic modes
>>> circuit = FermionicCircuit(4)
>>>
>>> # Define a simple fermionic Hamiltonian
>>> hamiltonian = FermionOperator.from_terms([
... ([cre(0), ann(2)], 0.5),
... ([cre(2), ann(0)], 0.5),
... ([cre(1), ann(3)], 0.5),
... ([cre(3), ann(1)], 0.5),
... ])
>>> # Add some problem structure by grouping our Hamiltonian terms
>>> hamiltonian.groups = [0, 0, 1, 1]
>>>
>>> # Add an evolution gate to implement exp(-i * t * H)
>>> evolution = Evolution(4, hamiltonian, time=1.0)
>>> circuit.append(evolution, circuit.register)

A simple `FermionicCircuit` with a single time evolution gate.

A decomposed `FermionicCircuit` with several time evolution gates.

Zwróć uwagę, że grupowanie wyrazów operatora jest zachowane nawet w prostych operacjach, takich jak dekompozycja. Pokazuje to, jak informacje strukturalne przepływają przez stos obwodu. Aby poznać pełną transpilację do kubitów, zapoznaj się z przewodnikiem Transpilacja obwodów fermionowych.

Rozdzielanie bramek za pomocą bariery​

Przebiegi optymalizacji łączą sąsiednie bramki, gdy tylko mogą. Na przykład MergeOrbitalRotations łączy ciąg kolejnych bramek OrbitalRotation w jedną, a MergeSlaterDeterminantPreparation wtapia InitializeModes w następującą po nim rotację.

Użyj FermionicCircuit.barrier(), gdy chcesz temu zapobiec. Bariera nie ma efektu unitarnego, więc nie zmienia stanu przygotowywanego przez obwód; jedynie wyznacza punkt, przez który przebiegi nie łączą bramek. Wywołaj ją bez argumentów, aby objęła wszystkie mody, lub przekaż mody, do których ma się ograniczać:

>>> from qiskit_fermions.circuit.library import OrbitalRotation
>>> import numpy as np
>>>
>>> rotations = FermionicCircuit(4)
>>> rotations.append(OrbitalRotation(np.eye(4, dtype=complex)), rotations.register)
>>> rotations.barrier()
>>> rotations.append(OrbitalRotation(np.eye(4, dtype=complex)), rotations.register)

A `FermionicCircuit` with a barrier separating two orbital rotations.

Bariery przetrwają transpilację z fermionów do kubitów, więc ograniczają także następującą po niej optymalizację na poziomie kubitów. Ten etap omawia przewodnik Transpilacja obwodów fermionowych.

Powtarzanie obwodu​

Wiele algorytmów wielokrotnie stosuje ten sam blok bramek. Typowym przypadkiem jest ewolucja czasowa: krok Trottera buduje się raz, a następnie powtarza, aby osiągnąć czas docelowy. Użyj repeat(), aby zbudować powtórzony obwód, oraz compose(), aby dołączyć go do prefiksu przygotowującego stan.

Poniższy przykład buduje krok Trottera drugiego rzędu dla modelu Fermiego-Hubbarda. Krok jest symetryczny: dzieli wyraz przeskoku na dwie bramki OrbitalRotation o połowie czasu trwania, otaczające oddziaływanie na węźle.

>>> import scipy.linalg
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>>
>>> norb, reps, time = 2, 3, 1.0
>>> num_modes = 2 * norb
>>> dt = time / reps
>>>
>>> hopping = np.zeros((norb, norb))
>>> hopping[0, 1] = hopping[1, 0] = -1.0
>>> interaction = FermionOperator.from_dict(
... {(cre(p), ann(p), cre(p + norb), ann(p + norb)): 2.0 for p in range(norb)}
... )
>>>
>>> def rotation(duration):
... propagator = scipy.linalg.expm(-1j * duration * hopping)
... # the same rotation acts on the alpha and beta halves of the register
... return scipy.linalg.block_diag(propagator, propagator)
>>>
>>> trotter_step = FermionicCircuit(num_modes)
>>> trotter_step.append(OrbitalRotation(rotation(dt / 2)), trotter_step.modes)
>>> trotter_step.append(
... Evolution(num_modes, interaction, dt, atomic=True), trotter_step.modes
... )
>>> trotter_step.append(OrbitalRotation(rotation(dt / 2)), trotter_step.modes)

Powtórzenie kroku i złożenie go ze stanem odniesienia Hartree-Focka daje pełną ewolucję:

>>> evolution = FermionicCircuit(num_modes)
>>> evolution.append(InitializeModes([True, False, True, False]), evolution.modes)
>>> evolution.compose(trotter_step.repeat(reps), inplace=True)
>>> evolution.count_ops()["OrbitalRotation"]
6

A `FermionicCircuit` with repeated steps.

Powtórzenia są spłaszczane w zwracanym obwodzie, zamiast być opakowane w jedną bramkę, więc count_ops() zlicza dwie rotacje na krok. To spłaszczenie sprawia, że redundancja na granicach kroków staje się widoczna dla transpilatora: końcowa rotacja o połowie czasu jednego kroku i początkowa następnego sąsiadują ze sobą, więc MergeOrbitalRotations łączy je w jedną rotację.

>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import MergeOrbitalRotations
>>>
>>> merged = FermionicPassManager([MergeOrbitalRotations()]).run(evolution)
>>> merged.count_ops()["OrbitalRotation"]
4

A `FermionicCircuit` with repeated steps and merged orbital rotations.

Wynik kosztuje jedną rotację na krok plus jedną, czyli tyle, ile uzyskasz, łącząc te półkroki ręcznie. Zapisanie kroku w naturalnej formie symetrycznej i jego powtórzenie daje ten sam obwód, więc łączenie zostaje pozostawione etapowi optymalizacji.

Gdy potrzebujesz zachować powtórzenia oddzielnie, przekaż insert_barriers=True, aby umieścić między nimi barrier(). Przebiegi optymalizacji nie łączą bramek przez barierę, więc każda rotacja zostaje zachowana:

>>> separated = trotter_step.repeat(reps, insert_barriers=True)
>>> FermionicPassManager([MergeOrbitalRotations()]).run(separated).count_ops()[
... "OrbitalRotation"
... ]
6

A `FermionicCircuit` with repeated steps separated by barriers.

Uwaga

Argument reps w FermionicSuzukiTrotter to inna koncepcja. Dzieli on pojedynczą bramkę Evolution na więcej krótszych czynników, podczas gdy repeat() duplikuje całe obwody. Powtórzenie obwodu zawierającego jedną bramkę Evolution jedynie mnoży jej całkowity czas ewolucji.

Pomiar modów fermionowych​

measure_all() odczytuje każdy mod do osobnego bitu klasycznego, łącząc mod i z bitem i. Użyj measure() z własnym ClassicalRegister, aby odczytać tylko niektóre z nich.

>>> measured = trotter_step.measure_all(inplace=False)
>>> measured.count_ops()["measure"]
4

Przekazanie inplace=False zwraca zmierzoną kopię i pozostawia oryginał bez zmian, co ma znaczenie, ponieważ pomiar nie jest unitarny: obwodu zawierającego pomiar nie można już zastosować do wektora stanu, więc zachowaj niezmierzony obwód, jeśli chcesz go także symulować.

Pomiar w tym miejscu, a nie na transpilowanym QuantumCircuit, sprawia, że próbkowane ciągi bitów są łatwe do odczytania. Pomiar fermionowy wskazuje mod, który odczytuje, więc każdy etap przesuwający mody (RelabelModes na poziomie fermionowym, układ i routing na poziomie kubitów) przenosi razem z nimi pomiar, a ciąg bitów pozostaje indeksowany modem, o który pytasz.

Uwaga

W przeciwieństwie do measure_all(), przed pomiarami nie jest wstawiana żadna barrier(). Jeśli chcesz ją tam mieć, najpierw wywołaj barrier() samodzielnie.

To, czy zmierzony bit znaczy to, czego oczekujesz, zależy od kodowania fermion–kubit, które jest wybierane podczas transpilacji, a nie tutaj. W kodowaniu w bazie obsadzeń, takim jak Jordan-Wigner, każdy mod jest reprezentowany przez jeden kubit, a bit jest obsadzeniem tego modu; zobacz FermionicMeasure.

Transpilacja obwodów fermionowych​

Aby zaimplementować algorytm kwantowy reprezentowany przez Twój FermionicCircuit, należy go transpilować do QuantumCircuit. Jak to zrobić, dowiesz się z przewodnika Transpilacja obwodów fermionowych.

Kolejne kroki​