Przejdź do głównej treści

Wdrażanie i uruchamianie szablonu Qiskit Function dla dynamiki hamiltonianu AQC + Trotter

Przegląd

To jest niezależny od eksperymentu szablon Qiskit Function dla dynamiki hamiltonianu. Mając 1D hamiltonian Pauliego z oddziaływaniem najbliższych sąsiadów, przygotowany stan początkowy (opcjonalnie) oraz zestaw obserwabli, uruchamia on ewolucję czasową Trottera, kompresję obwodu za pomocą przybliżonej kompilacji kwantowej (AQC) oraz wykonanie z łagodzeniem błędów, a następnie zwraca szereg czasowy dla każdej obserwabli. Zamień konfigurację (PRE) i analizę (POST), a ten sam rdzeń napędza inny eksperyment:

PRE (twoja konfiguracja)FUNKCJA (wdrożona tutaj)POST (twoja analiza)
Przygotuj stan, jako obwód lub stan produktowy, z opcjonalnym lokalnym kopnięciemSynteza Trottera → kompresja AQC → wykonanie na statevector, fake lub runtime, zwracające O(t)\langle O \rangle(t)S(q,ω)S(q, \omega) dla rozpraszania neutronów, lub magnetyzacja, transport, dynamika quench i tak dalej

Szablon jest opublikowany w repozytorium szablonów Qiskit Function, obok innych szablonów aplikacji. Ten notatnik wdraża go na twoim własnym koncie Qiskit Serverless. Uruchom go raz, a każdy notatnik będzie mógł potem wywołać funkcję za pomocą serverless.load("aqc-dynamics-function").

Aby zobaczyć przepracowany przykład naukowy, zobacz Symulacja rozpraszania neutronów za pomocą przepływu pracy Serverless AQC + Trotter dynamics, który wywołuje tę funkcję w celu obliczenia czynnika struktury dynamicznej KCuF3_3. Ten notatnik dotyczy zamiast tego wdrożenia i kontraktu wejściowego.

Wymagania

Zanim zaczniesz, upewnij się, że masz następujące elementy w środowisku kernela tego notatnika:

  • Qiskit SDK w wersji 2.0 lub nowszej (pip install qiskit).

  • Klient Qiskit IBM Catalog (pip install qiskit-ibm-catalog), który wdraża i uruchamia zadania w Qiskit Serverless.

Własne zależności naukowe funkcji (qiskit-addon-aqc-tensor, cotengrust, qiskit-aer) nie muszą być instalowane lokalnie.

Pobierz pliki źródłowe szablonu

Funkcja jest małym pakietem Python, który Qiskit Serverless uruchamia w chmurze, więc jej źródło musi istnieć jako lokalne pliki, które są przesyłane w momencie wdrożenia. Pakiet jest opublikowany w repozytorium szablonów Qiskit Function.

Pobierz source_files

Pobrany plik to pojedyncze archiwum zip, nazwane zgodnie z pełną ścieżką katalogu w repozytorium:

qiskit-community qiskit-function-templates main physics aqc_trotter source_files.zip

  1. Rozpakuj go do katalogu, w którym znajduje się ten notatnik.

  2. Zmień nazwę wypakowanego folderu z tej długiej nazwy na source_files.

Twój katalog roboczy będzie wtedy wyglądał tak:

your-working-directory/
├── function-template-aqc-trotter.ipynb <- this notebook
└── source_files/ <- the renamed folder
├── __init__.py
├── program.py
└── source/
├── __init__.py
├── _serverless.py
├── app_function.py
├── aqc.py
├── build.py
├── execute.py
└── hamiltonian.py

Nazwa musi brzmieć dokładnie source_files, ponieważ to jest working_dir, który przesyła Krok 3.

program.py jest punktem wejścia wywoływanym przez bramę. Wszystko pod source/ jest implementacją, podzieloną na etapy: synteza hamiltonianu i Trottera, kompresja AQC oraz wykonanie. Żaden z tych plików nie wymaga edycji, aby uruchomić poniższe przykłady. Krok 3 przesyła cały katalog, więc powtarzaj ten krok za każdym razem, gdy zmienisz plik.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-ibm-catalog

1. Uwierzytelnianie

Użyj qiskit-ibm-catalog, aby uwierzytelnić się w QiskitServerless za pomocą klucza API (tokenu) i CRN (instancji), które znajdziesz na pulpicie IBM Quantum® Platform. Dzięki tym danym uwierzytelniającym możesz utworzyć lokalną instancję klienta serverless, aby przesyłać lub uruchamiać wybraną funkcję:

from qiskit_ibm_catalog import QiskitServerless
serverless = QiskitServerless(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")

Możesz opcjonalnie użyć save_account(), aby zapisać swoje dane uwierzytelniające w lokalnym środowisku (zobacz przewodnik Konfiguracja konta IBM Cloud®). Zauważ, że zapisuje to twoje dane uwierzytelniające do tego samego pliku, co QiskitRuntimeService.save_account():

QiskitServerless.save_account(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")

Jeśli konto zostało zapisane, nie ma potrzeby podawania tokenu w celu uwierzytelnienia:

from qiskit_ibm_catalog import QiskitServerless

# Authenticate to the remote cluster
# In this case, loading a saved account
serverless = QiskitServerless()

# REPLACE WITH YOUR OWN CREDENTIALS or SAVED ACCOUNT
# serverless = QiskitServerless(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")

2. Deklarowanie zależności

Pakiety, których funkcja potrzebuje oprócz zarządzanego bazowego obrazu serverless.

uwaga

Brama instaluje jedynie nazwy z jej listy dozwolonych (requirements-dynamic-dependencies.txt), dopasowywane po nazwie pakietu i przypięte do dozwolonej wersji za pomocą ==. Wszystko inne musi dotrzeć tranzytywnie (jako zależność pakietu z listy dozwolonych). Składnia [extras] jest respektowana: qiskit-addon-aqc-tensor[quimb-jax] instaluje quimb i jax. cotengrust jest potrzebny dla wydajności pamięciowej podczas symulacji sieci tensorowej. qiskit-aer jest wymieniony osobno dla backendu fake (lokalna symulacja z szumem).

DEPENDENCIES = [
"qiskit-addon-aqc-tensor[quimb-jax]==0.3.1",
"qiskit-aer==0.17.2",
"cotengrust==0.2.0",
]

3. Definiowanie i przesyłanie funkcji

from qiskit_ibm_catalog import QiskitFunction

fn = QiskitFunction(
title="aqc-dynamics-function",
entrypoint="program.py",
working_dir="source_files/",
dependencies=DEPENDENCIES,
)
serverless.upload(fn)
QiskitFunction(aqc-dynamics-function)

4. Weryfikacja rejestracji

next(p for p in serverless.list() if p.title == "aqc-dynamics-function")
QiskitFunction(aqc-dynamics-function)

Dokumentacja funkcji

To jest krótkie wprowadzenie. Każde pole jest w pełni udokumentowane w README szablonu AQC Dynamics: pełna tabela wejść z regułami walidacji, pola wyjściowe, backendy wykonania oraz dalsze przepracowane przykłady. Poniżej znajduje się skrócona wersja, wystarczająca do zapoznania się z kolejnymi przykładami.

Wejścia

Każde uruchomienie to pojedyncze wywołanie fn.run(...). Wymagane są tylko pierwsze trzy wejścia w tabeli: hamiltonian, t_steps i aqc_segments. Wszystko po nich jest opcjonalne i przyjmuje pokazaną wartość domyślną, więc minimalne wywołanie to trzy argumenty, a reszta tabeli to funkcjonalność, w którą możesz się opcjonalnie włączyć. num_qubits hamiltonianu ustala długość łańcucha, więc nie ma osobnego wejścia rozmiaru.

WejścieDomyślnieOpis
hamiltonianwymagane1D hamiltonian Pauliego z oddziaływaniem najbliższych sąsiadów jako SparsePauliOp. Ciągi to operatory Pauliego, więc nie ma niejawnego współczynnika jednej drugiej.
t_stepswymaganeCałkowita liczba kroków Trottera. Ewoluuje do T = t_steps * dt i raportuje każdą obserwablę przy każdym t_k = k * dt.
aqc_segmentswymaganePlan kompresji: lista {"n_steps": k, "ansatz_steps": m}. Kroki sum(n_steps) są kompresowane; reszta działa jako zwykły Trotter.
dt0.2Czas fizyczny, o jaki przesuwa jeden krok Trottera.
initial_state|0...0>Przygotowany QuantumCircuit do ewolucji. Wpiecz dowolne lokalne kopnięcie w ten obwód.
observablesZ dla każdego miejscaWszystko, co EstimatorV2 akceptuje jako swój argument observables. Jedna obserwabla na kolumnę wyjściową.
trotter_optionsSuzuki 2. rzędu{"method": ..., "synthesis_settings": {...}}. reps i time są własnością funkcji.
aqc_optionspatrz opismax_bond (32), cutoff (1e-8), autodiff_backend ("jax"), fidelity_target (None), optimizer_settings (L-BFGS-B, jac=True, maxiter=300).
estimator_optionsDD, twirling, TREXEstimatorV2.options, przekazywane bez zmian. Podany słownik zastępuje wartości domyślne w całości, zamiast się z nimi łączyć.
transpiler_options{"optimization_level": 3}Argumenty słowa kluczowego generate_preset_pass_manager. backend i target są odrzucane, ponieważ są własnością ścieżki wykonania.
backend"runtime""statevector", "fake" lub "runtime".
backend_namenajmniej obciążonyNazwa backendu IBM® dla runtime lub nazwanego fałszywego backendu.
batches1Podziel obwody na N zadań runtime. Jedna partia przesyła pojedyncze zadanie i nie tworzy sesji.
parallel_simFalseRozłóż ścieżki lokalnego symulatora na wszystkie dostępne rdzenie za pomocą Ray. Bez wpływu na runtime.
return_circuitsFalseZwróć logiczne obwody AQC + Trotter w wyniku obok szeregu obserwabli.

Backendy wykonania

Wszystkie trzy ścieżki współdzielą ten sam kod i te same ustawienia łagodzenia błędów. Różnią się tylko miejscem, w którym uruchamiane są obwody.

backendCo to jestDane uwierzytelniająceUwagi
"statevector"Dokładny StatevectorEstimatorTylko konto ServerlessDokładna ścieżka referencyjna. Bez czasu QPU.
"fake"Symulacja lokalna z szumem na fałszywym backendzie QiskitTylko konto ServerlessWierna próba generalna ścieżki runtime z łagodzeniem błędów. Wymaga qiskit-aer. Domyślnie 127-kubitowy fake_sherbrooke.
"runtime" (domyślnie)Złagodzony EstimatorV2 na prawdziwym QPUKonto Serverless i instancja z dostępem do QPUbackend_name opcjonalne; pominięcie go wybiera najmniej obciążone urządzenie.

Obie ścieżki symulatora nadal wywołują wdrożoną funkcję, więc wymagają zapisanego konta Serverless, mimo że nie korzystają z czasu QPU. Dwa poniższe przykłady uruchamiają to samo zadanie najpierw na statevector, a następnie na runtime.

Wynik

job.result() zwraca zwykły słownik:

{
"times": [...], # length t_steps + 1, t_k = k * dt (t=0 is the prepared state)
"expectation_values": [[...]], # shape (n_times, n_observables)
"observable_labels": [...], # for example: ["Z_0", "ZZ_0_1"]
"metadata": {
"n", "t_steps", "dt", "tier",
"aqc_compressed_steps": 5, # total compressed steps (= sum of segment n_steps)
"aqc_segments": [ # per segment: the plan plus its own results
{"n_steps": 3, "ansatz_steps": 1, "steps": [1, 2, 3], "n_params": 133,
"fidelities": {"1": ..., "2": ..., "3": ...}},
{"n_steps": 2, "ansatz_steps": 2, "steps": [4, 5], "n_params": 245,
"fidelities": {"4": ..., "5": ...}},
],
"execution_backend",
"aqc_fidelities": {"1": ..., "2": ...}, # flat per-step fidelity, all compressed steps
"circuit_stats": { # per-step 2q depth and gate count, full Trotter vs AQC
"1": {"full_trotter": {"depth_2q": ..., "num_2q_gates": ...},
"aqc_trotter": {"depth_2q": ..., "num_2q_gates": ...}},
"2": {...},
},
"warnings": [...], # non-fatal notices; for example, a cotengrust fallback
"resource_usage": { # per stage; QPU_TIME is the charged QPU time
"RUNNING: OPTIMIZING_FOR_HARDWARE": {"CPU_TIME": ...},
"RUNNING: WAITING_FOR_QPU": {"CPU_TIME": ...},
"RUNNING: EXECUTING_QPU": {"QPU_TIME": ...},
},
},
# present only when return_circuits=True
"circuits": [QuantumCircuit, ...], # one per evolved step; circuits[i] is at times[i + 1]
}

aqc_fidelities i circuit_stats to dwa pola, które warto przeczytać najpierw: razem mówią ci, czy kompresja pozostała wierna i czy rzeczywiście zaoszczędziła głębokość. W runtime resource_usage raportuje czas oczekiwania w kolejce oddzielnie od czasu QPU, za który jesteś obciążany. Odrzucone wejście kończy się szybko strukturalnym błędem ServerlessError (kod 4615).

Przykład symulatora

Najpierw uruchom funkcję na dokładnym backendzie statevector. Nie zużywa on czasu QPU i weryfikuje wdrożenie od początku do końca. Modelem jest tutaj ośmiokubitowy łańcuch Isinga z polem poprzecznym, a observables jest pominięte, więc funkcja mierzy domyślne ZZ dla każdego miejsca.

Plan kompresji to wejście warte zrozumienia. Każdy segment {"n_steps": k, "ansatz_steps": m} kompresuje k kolejnych kroków Trottera do ansatzu zbudowanego z celu Trottera o m krokach, a wszelkie kroki poza sum(n_steps) działają jako zwykły Trotter. Wczesne kroki o niskim splątaniu kompresują się dobrze do płytkiego jednowarstwowego ansatzu; późniejsze, bardziej splątane kroki wymagają głębszego.

from qiskit.quantum_info import SparsePauliOp

fn = serverless.load("aqc-dynamics-function")

n = 8
H = SparsePauliOp.from_sparse_list(
[("ZZ", [i, i + 1], 1.0) for i in range(n - 1)]
+ [("X", [i], 0.8) for i in range(n)],
num_qubits=n,
)

job = fn.run(
t_steps=8,
aqc_segments=[
{
"n_steps": 4,
"ansatz_steps": 1,
}, # early steps -> shallow 1-layer ansatz
{
"n_steps": 2,
"ansatz_steps": 2,
}, # later steps -> deeper 2-layer ansatz
],
hamiltonian=H,
aqc_options={"max_bond": 32},
backend="statevector",
)
print("job ID:", job.job_id)
job ID: ee1f3793-e995-427d-81d1-5924549beb38

Śledzenie uruchomienia i odczyt wyniku

status() raportuje zarówno ogólny cykl życia zadania, jak i podstatus dla poszczególnych etapów, publikowany przez funkcję w trakcie działania. Te same etapy dotyczą uruchomienia na sprzęcie później w tym przewodniku:

QUEUED -> INITIALIZING -> RUNNING: OPTIMIZING_FOR_HARDWARE -> RUNNING: WAITING_FOR_QPU -> RUNNING: EXECUTING_QPU -> RUNNING: POST_PROCESSING -> DONE

wartość status()Etap
RUNNING: OPTIMIZING_FOR_HARDWAREprzygotowanie stanu, budowa Trottera, kompresja AQC
RUNNING: WAITING_FOR_QPUw kolejce na QPU (tylko backend runtime)
RUNNING: EXECUTING_QPUobwody są wykonywane (lokalne symulatory oznaczają to bezpośrednio)
RUNNING: POST_PROCESSINGbudowanie słownika wyniku

Stanami końcowymi są DONE, ERROR i CANCELED. To uruchomienie statevector nie ma kolejki QPU, więc pomija RUNNING: WAITING_FOR_QPU. Użyj job.logs() w dowolnym momencie, aby zobaczyć logi poszczególnych etapów, w tym wierność AQC osiągniętą przy każdym kroku.

print(job.status()) # re-run until this reports DONE
DONE
import numpy as np

result = job.result()
ev = np.array(result["expectation_values"])

print("observables:", result["observable_labels"])
print("shape:", ev.shape, "-> (n_times, n_observables)")
print("first row (t = 0, the prepared state):", np.round(ev[0], 4))
print("last row (t = t_steps * dt):", np.round(ev[-1], 4))
print(
"AQC fidelities:",
{k: round(v, 4) for k, v in result["metadata"]["aqc_fidelities"].items()},
)

# What the compression bought: 2-qubit depth at the final time step.
stats = result["metadata"]["circuit_stats"][
str(result["metadata"]["t_steps"])
]
print(
"2q depth at the final step:",
stats["full_trotter"]["depth_2q"],
"(full Trotter) ->",
stats["aqc_trotter"]["depth_2q"],
"(AQC + Trotter)",
)
observables: ['Z_0', 'Z_1', 'Z_2', 'Z_3', 'Z_4', 'Z_5', 'Z_6', 'Z_7']
shape: (9, 8) -> (n_times, n_observables)
first row (t = 0, the prepared state): [1. 1. 1. 1. 1. 1. 1. 1.]
last row (t = t_steps * dt): [0.1442 0.2956 0.4686 0.4877 0.4869 0.4686 0.2963 0.1441]
AQC fidelities: {'1': 1.0, '2': 1.0, '3': 1.0, '4': 1.0, '5': 1.0, '6': 0.9999}
2q depth at the final step: 210 (full Trotter) -> 79 (AQC + Trotter)

Przykład na sprzęcie

Wywołanie funkcji z backend="runtime" transpiluje i wykonuje na prawdziwym procesorze IBM Quantum, z wbudowanym łagodzeniem błędów funkcji: dynamicznym odsprzęganiem (XY4), twirlingiem bramek oraz twirlowaną eliminacją błędów odczytu (TREX). backend_name wybiera urządzenie; pomiń go, a funkcja weźmie najmniej obciążone.

Nic w kodzie naukowym się nie zmienia. To, co różni się od przykładu symulatora, to długość łańcucha, liczba kroków Trottera, plan kompresji, backend oraz jawne ustawienia łagodzenia błędów omówione w następnej sekcji.

Dobór rozmiaru zadania dla sprzętu sterującego

estimator_options to wejście warte świadomego ustawienia. Twirling bramek buduje num_randomizations oddzielnych zrandomizowanych obwodów dla każdego PUB, a całe zadanie, każdy PUB ze wszystkimi swoimi randomizacjami, musi zmieścić się w pamięci instrukcji klasycznego systemu sterowania QPU. Funkcja domyślnie ustawia 1000 randomizacji, więc 10-krokowa ewolucja przesyła 11 PUB-ów po 1000 obwodów każdy: około 11 000 instancji obwodów w pojedynczym zadaniu.

Przekroczenie tego, co pomieści system sterowania, powoduje niepowodzenie zadania z błędem 6073. Limity zadań podają progi i sposób ich obliczania, przy czym głównym z nich jest 26,8 miliona instrukcji systemu sterowania na kubit, stosowane na zadanie, a nie na PUB. Dynamiczne odsprzęganie dodaje bramki, które wliczają się do tego limitu.

Dwa wejścia kontrolują rozmiar:

  • estimator_options ustawia budżet strzałów. Całkowita liczba strzałów to num_randomizations * shots_per_randomization, więc możesz wymieniać randomizacje na strzały na randomizację, zachowując statystyki, a jednocześnie zmniejszając program. Poniższa komórka używa 100 randomizacji po 200 strzałów każda, co daje 20 000 strzałów na obserwablę i około jedną dziesiątą liczby instancji obwodów, które przesłałyby wartości domyślne. Zobacz TwirlingOptions oraz Opcje Estimatora, aby poznać pełny zestaw pól.

  • batches dzieli PUB-y na tyle oddzielnych zadań runtime, co jest środkiem zaradczym sugerowanym przez sam błąd 6073 i dlatego liczy się podejście na zadanie. Ustawienie batches=4 wysyła około trzech PUB-ów na zadanie zamiast jedenastu naraz, a zadania wychodzą razem w jednej partii, więc grupa staje w kolejce raz, a nie każde zadanie osobno.

Pamiętaj, że podane estimator_options zastępuje wartości domyślne funkcji w całości, zamiast się z nimi łączyć, więc dynamiczne odsprzęganie i TREX są ponownie ustawione w poniższej komórce, aby pozostały włączone.

from qiskit.quantum_info import SparsePauliOp

fn = serverless.load("aqc-dynamics-function")

n = 10
H = SparsePauliOp.from_sparse_list(
[("ZZ", [i, i + 1], 1.0) for i in range(n - 1)]
+ [("X", [i], 0.8) for i in range(n)],
num_qubits=n,
)

job = fn.run(
t_steps=10,
aqc_segments=[
{
"n_steps": 3,
"ansatz_steps": 1,
}, # early steps -> shallow 1-layer ansatz
{
"n_steps": 3,
"ansatz_steps": 2,
}, # later steps -> deeper 2-layer ansatz
],
hamiltonian=H,
aqc_options={"max_bond": 32},
backend="runtime",
backend_name="ibm_marrakesh",
# The function defaults to 1000 twirling randomizations, which was too large
# for this device. Total shots is num_randomizations *
# shots_per_randomization, so this is 20,000 shots per observable.
estimator_options={
"dynamical_decoupling": {"enable": True, "sequence_type": "XY4"},
"twirling": {
"enable_gates": True,
"num_randomizations": 100,
"shots_per_randomization": 200,
},
"resilience": {"measure_mitigation": True},
},
)
print("job ID (save this to reconnect later):", job.job_id)
job ID (save this to reconnect later): 7229a8bf-9f83-4785-8dd4-489844abc2d9
Ponowne łączenie się z długo działającym zadaniem

Uruchomienie na sprzęcie nie jest szybkie, a większość czasu jest klasyczna, a nie na QPU. Kompresja AQC działa wewnątrz funkcji, zanim cokolwiek dotrze do QPU, a kolejka QPU dochodzi do tego dodatkowo. Nie musisz utrzymywać otwartego tego notatnika ani kernela podczas jego działania.

Skopiuj ID zadania wydrukowany przez poprzednią komórkę i zapisz go. Kolejne trzy komórki pozwalają ci wrócić do zadania później:

  1. Ponowne połączenie, potrzebne tylko w nowej sesji kernela: uruchom ponownie komórkę Uwierzytelnianie, aby odtworzyć serverless, a następnie odbuduj uchwyt job na podstawie zapisanego ID. Pomiń tę komórkę, jeśli nadal jesteś w sesji, w której przesłałeś zadanie, ponieważ uchwyt jest już aktywny.

  2. Sprawdź status: uruchamiaj ponownie, aż zgłosi DONE.

  3. Pobierz wynik: uruchom tylko wtedy, gdy status to DONE.

Wklej zapisany identyfikator w miejsce symbolu zastępczego w poniższej komórce ponownego łączenia.

# Reconnect to a previously submitted job by its ID. Only needed in a NEW kernel
# session; if you are still in the session where you submitted, the `job` handle
# from the preceding cell is already live, so skip this cell. Replace the ID that follows with your own.
job = serverless.get_job_by_id("<your job ID>")
# Re-run this until it reports DONE, then fetch the result in the following cell.
print(job.status())
DONE
import numpy as np

# Run this only once the preceding status cell reports DONE. result() blocks until
# the job finishes, so calling it earlier just waits.
result = job.result()
ev = np.array(result["expectation_values"])

print("backend:", result["metadata"]["execution_backend"])
print("shape:", ev.shape, "-> (n_times, n_observables)")
print("last row (t = t_steps * dt):", np.round(ev[-1], 4))
print(
"AQC fidelities:",
{k: round(v, 4) for k, v in result["metadata"]["aqc_fidelities"].items()},
)

# What the compression bought: 2-qubit depth at the final time step.
stats = result["metadata"]["circuit_stats"][
str(result["metadata"]["t_steps"])
]
print(
"2q depth at the final step:",
stats["full_trotter"]["depth_2q"],
"(full Trotter) ->",
stats["aqc_trotter"]["depth_2q"],
"(AQC + Trotter)",
)
backend: runtime
shape: (11, 10) -> (n_times, n_observables)
last row (t = t_steps * dt): [0.1504 0.1361 0.218 0.2144 0.2275 0.1783 0.1749 0.1599 0.0915 0.0922]
AQC fidelities: {'1': 1.0, '2': 1.0, '3': 1.0, '4': 1.0, '5': 0.9999, '6': 0.9999}
2q depth at the final step: 342 (full Trotter) -> 171 (AQC + Trotter)

Kolejne kroki

Zalecenia