Wsteczna propagacja operatora (OBP) do estymacji wartości oczekiwanych
Szacowane zużycie zasobów: 4 minuty na procesorze Heron r3 (UWAGA: To tylko szacunek. Rzeczywisty czas wykonania może się różnić.)
Cele kształcenia
Po ukończeniu tego samouczka użytkownicy powinni rozumieć:
- Jak używać
qiskit-addon-obpdo redukcji głębokości obwodu kwantowego kosztem zwiększonej liczby wykonań obwodu - Jak używać
qiskit-addon-utilsdo konstruowania hamiltonianów XYZ i odpowiadających im obwodów ewolucji czasowej
Wymagania wstępne
Zalecamy, aby użytkownicy zapoznali się z następującymi tematami przed przystąpieniem do tego samouczka:
- Używanie prymitywu Estimator do obliczania wartości oczekiwanych obserwowalnej
Wprowadzenie
Wsteczna propagacja operatora to technika polegająca na wchłanianiu operacji z końca obwodu kwantowego do mierzonej obserwowalnej, co ogólnie zmniejsza głębokość obwodu kosztem dodatkowych składników w obserwowalnej. Celem jest wsteczne propagowanie jak największej części obwodu bez dopuszczenia do nadmiernego wzrostu obserwowalnej. Implementacja oparta na Qiskit jest dostępna w rozszerzeniu OBP Qiskit. Więcej informacji można znaleźć w odpowiedniej dokumentacji.
Rozważmy przykładowy obwód, dla którego mierzona jest obserwowalna , gdzie są Paulimi, a są współczynnikami. Oznaczmy obwód jako pojedynczą unitarną , którą można logicznie podzielić na , jak pokazano na rysunku poniżej.

Wsteczna propagacja operatora wchłania unitarną do obserwowalnej poprzez jej ewolucję jako . Innymi słowy, część obliczenia jest wykonywana klasycznie poprzez ewolucję obserwowalnej z do . Pierwotny problem można teraz sformułować na nowo jako pomiar obserwowalnej dla nowego obwodu o mniejszej głębokości, którego unitarna to .
Unitarna jest reprezentowana jako liczba przedziałów . Istnieje wiele sposobów definiowania przedziału. Na przykład, w powyższym przykładowym obwodzie każda warstwa bramek i każda warstwa bramek może być traktowana jako osobny przedział. Wsteczna propagacja polega na klasycznym obliczaniu . Każdy przedział można przedstawić jako , gdzie jest -qubitowym Paulim, a jest skalarem. Łatwo można sprawdzić, że
W powyższym przykładzie, jeśli , musimy wykonać dwa obwody kwantowe zamiast jednego, aby obliczyć wartość oczekiwaną. Dlatego wsteczna propagacja może zwiększyć liczbę składników w obserwowalnej, co prowadzi do większej liczby wykonań obwodu. Jednym ze sposobów umożliwienia głębszej wstecznej propagacji do obwodu przy jednoczesnym zapobieganiu nadmiernemu wzrostowi operatora jest obcinanie składników o małych współczynnikach zamiast dodawania ich do operatora. Na przykład w powyższym przykładzie można zdecydować się na obcięcie składnika zawierającego , pod warunkiem że jest wystarczająco mały. Obcinanie składników może skutkować mniejszą liczbą obwodów kwantowych do wykonania, jednak wiąże się z pewnym błędem w końcowym obliczeniu wartości oczekiwanej proporcjonalnym do wielkości współczynników obciętych składników.
Wymagania
Przed rozpoczęciem tego samouczka upewnij się, że masz zainstalowane następujące pakiety:
- Qiskit SDK v2.0 lub nowszy, z obsługą wizualizacji
- Qiskit Runtime v0.22 lub nowszy (
pip install qiskit-ibm-runtime) - Rozszerzenie OBP Qiskit 0.3 lub nowsze (
pip install qiskit-addon-obp) - Qiskit addon utils 0.3 lub nowsze (
pip install qiskit-addon-utils)
Konfiguracja
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-obp qiskit-addon-utils qiskit-ibm-runtime rustworkx
import numpy as np
import matplotlib.pyplot as plt
from qiskit.primitives import StatevectorEstimator
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
)
from qiskit_addon_utils.slicing import slice_by_depth, combine_slices
from qiskit_addon_obp.utils.simplify import OperatorBudget
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.truncating import setup_budget
from rustworkx.visualization import graphviz_draw
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2, EstimatorOptions
Przykład na małą skalę z symulatorem
Ten samouczek implementuje wzorzec Qiskit do symulacji dynamiki kwantowej łańcucha spinów Heisenberga przy użyciu rozszerzenia OBP Qiskit. Zauważ, że w symulatorze bez szumów wartość oczekiwana uzyskana z wsteczną propagacją i bez niej będzie taka sama.
Krok 1: Odwzorowanie klasycznych danych wejściowych na problem kwantowy
Odwzorowanie ewolucji czasowej kwantowego modelu Heisenberga na eksperyment kwantowy
Najpierw użyjemy funkcji generate_xyz_hamiltonian z qiskit-addon-utils do wygenerowania hamiltonianu podobnego do Heisenberga na danym grafie połączeń. Tym grafem może być rustworkx.PyGraph lub CouplingMap. Poniżej użyjemy liniowej mapy sprzężeń CouplingMap złożonej z 10 qubitów.
num_qubits = 10
layout = [(i - 1, i) for i in range(1, num_qubits)]
# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)
graphviz_draw(coupling_map.graph, method="circo")
Następnie generujemy operator Pauliego modelujący hamiltoniana Heisenberga XYZ:
gdzie jest grafem mapy sprzężeń. W tym samouczku użyliśmy równych odpowiednio , oraz równych odpowiednio .
# Get a qubit operator describing the Heisenberg XYZ model
hamiltonian = generate_xyz_hamiltonian(
coupling_map,
coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)
print(hamiltonian)
SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIYII', 'IIIIIIIZII', 'IIIIIIXIII', 'IIIIIIYIII', 'IIIIIIZIII', 'IIIIIXIIII', 'IIIIIYIIII', 'IIIIIZIIII', 'IIIIXIIIII', 'IIIIYIIIII', 'IIIIZIIIII', 'IIIXIIIIII', 'IIIYIIIIII', 'IIIZIIIIII', 'IIXIIIIIII', 'IIYIIIIIII', 'IIZIIIIIII', 'IXIIIIIIII', 'IYIIIIIIII', 'IZIIIIIIII', 'XIIIIIIIII', 'YIIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j])
Na podstawie operatora qubitowego możemy wygenerować obwód kwantowy modelujący jego ewolucję czasową. Użyliśmy generate_time_evolution_circuit z dekompozycją Lie Trottera do skonstruowania obwodu ewolucji czasowej.
circuit = generate_time_evolution_circuit(
hamiltonian,
time=0.2,
synthesis=LieTrotter(reps=2),
)
circuit.draw("mpl", style="iqp", fold=-1)

Krok 2: Optymalizacja problemu pod kątem wykonania na sprzęcie kwantowym
Tworzenie przedziałów obwodu do wstecznej propagacji
Funkcja backpropagate propaguje wstecz całe przedziały obwodu naraz. Dlatego wybór sposobu podziału na przedziały może mieć wpływ na efektywność wstecznej propagacji dla danego problemu. Tutaj pogrupujemy bramki tego samego typu w przedziały przy użyciu funkcji slice_by_depth.
Bardziej szczegółowe omówienie podziału obwodu na przedziały można znaleźć w tym przewodniku pakietu qiskit-addon-utils.
slices = slice_by_depth(circuit, max_slice_depth=1)
print(f"Separated the circuit into {len(slices)} slices.")
Separated the circuit into 18 slices.
Ograniczanie wzrostu operatora podczas wstecznej propagacji
Podczas wstecznej propagacji liczba składników w operatorze będzie generalnie szybko zbliżać się do , gdzie jest liczbą przedziałów. Gdy dwa składniki w operatorze nie komutują qubitowo, potrzebujemy osobnych obwodów do uzyskania odpowiadających im wartości oczekiwanych. Na przykład, jeśli mamy dwuqubitową obserwowalną , to ponieważ , pomiar w jednej bazie wystarcza do obliczenia wartości oczekiwanych dla tych dwóch składników. Jednak antykomutuje z pozostałymi dwoma składnikami, więc potrzebujemy osobnego pomiaru bazowego do obliczenia wartości oczekiwanej . Innymi słowy, potrzebujemy dwóch obwodów zamiast jednego, aby obliczyć . Wraz ze wzrostem liczby składników w operatorze istnieje możliwość, że wymagana liczba wykonań obwodu również wzrośnie.
Rozmiar operatora można ograniczyć, podając argument operator_budget funkcji backpropagate, który przyjmuje instancję OperatorBudget.
Aby kontrolować ilość dodatkowych zasobów (liczba wykonań obwodów, a co za tym idzie wymagany czas QPU) przydzielonych do obliczeń, ograniczamy maksymalną liczbę grup Paulich komutujących qubitowo, którą może mieć propagowana wstecznie obserwowalna. Tutaj określamy, że wsteczna propagacja powinna się zatrzymać, gdy liczba qubitowo komutujących grup Paulich w operatorze przekroczy osiem.
op_budget = OperatorBudget(max_qwc_groups=8)
Wsteczne propagowanie przedziałów z obwodu
Najpierw określamy obserwowalną jako , gdzie jest liczbą qubitów. Będziemy propagować wstecz przedziały z obwodu ewolucji czasowej do momentu, gdy składniki obserwowalnej nie będą mogły być już połączone w osiem lub mniej qubitowo komutujących grup Paulich.
observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
num_qubits=num_qubits,
)
observable
SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
0.1+0.j, 0.1+0.j])
Poniżej zobaczysz, że propagowaliśmy wstecz sześć przedziałów, a składniki zostały połączone w sześć, a nie osiem grup. Oznacza to, że propagowanie wstecz jeszcze jednego przedziału spowodowałoby przekroczenie ośmiu grup Paulich. Możemy to zweryfikować, analizując zwrócone metadane. Zauważ też, że w tej części transformacja obwodu jest dokładna. To znaczy, żaden ze składników nowej obserwowalnej nie został obcięty. Propagowany wstecznie obwód i propagowana wstecznie obserwowalna dają dokładnie taki sam wynik jak pierwotny obwód i obserwowalna.
# Backpropagate slices onto the observable
bp_obs, remaining_slices, metadata = backpropagate(
observable, slices, operator_budget=op_budget
)
# Recombine the slices remaining after backpropagation
bp_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
print(
f"New observable has {len(bp_obs.paulis)} terms, which can be combined into "
f"{len(bp_obs.group_commuting(qubit_wise=True))} groups."
)
print(
f"Note that backpropagating one more slice would result in "
f"{metadata.backpropagation_history[-1].num_paulis[0]} terms "
f"across {metadata.backpropagation_history[-1].num_qwc_groups} groups."
)
print("The remaining circuit after backpropagation looks as follows:")
bp_circuit.draw("mpl", fold=-1, scale=0.6)
Backpropagated 6 slices.
New observable has 60 terms, which can be combined into 6 groups.
Note that backpropagating one more slice would result in 114 terms across 12 groups.
The remaining circuit after backpropagation looks as follows:
W przypadku małoskalowego przykładu na symulatorze nie użyjemy obcinania. Wynika to z faktu, że przy braku szumów obwód z wsteczną propagacją i bez niej prowadzi do tego samego wyniku, a obcinanie pogarsza wynik ze względu na dodaną aproksymację.
Transpilacja obwodów do docelowego zestawu bramek
Teraz transpilujemy zarówno pierwotny, jak i propagowany wstecznie obwód do bramek bazowych backendu. Nie musimy transpilować na rzeczywistym backendzie, ponieważ będziemy uruchamiać na symulatorze dla małej instancji.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)
<IBMBackend('ibm_kingston')>
pm_basis = generate_preset_pass_manager(
optimization_level=3, basis_gates=backend.configuration().basis_gates
)
isa_circuit = pm_basis.run(circuit)
isa_bp_circuit = pm_basis.run(bp_circuit)
Krok 3: Wykonanie przy użyciu prymitywów Qiskit
Najpierw tworzymy dwa Primitive Unified Blocs (PUB) odpowiadające pierwotnemu obwodowi i propagowanemu wstecznie obwodowi. Następnie wykonujemy PUBy na idealnym Estimatorze, aby uzyskać wartości oczekiwane.
pubs = [(isa_circuit, observable), (isa_bp_circuit, bp_obs)]
rng = np.random.default_rng()
estimator = StatevectorEstimator(seed=rng)
job = estimator.run(pubs)
Krok 4: Post-processing i zwrócenie wyników w żądanym formacie klasycznym
Teraz uzyskujemy wartości oczekiwane pierwotnego i propagowanego wstecznie obwodu.
primitive_result = job.result()
circuit_expval = primitive_result[0].data.evs.item()
bp_circuit_expval = primitive_result[1].data.evs.item()
methods = [
"No backpropagation",
"Backpropagation",
]
values = [circuit_expval, bp_circuit_expval]
ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
ax.set_ylim([0.6, 0.92])
ax.set_ylabel(r"$M_Z$", fontsize=12)
Text(0, 0.5, '$M_Z$')
Zgodnie z oczekiwaniami obie wartości oczekiwane są zgodne. Ponieważ uruchamiamy na symulatorze stanu wektora bez szumów, wsteczna propagacja jest dokładną transformacją pary obwód-obserwowalna, więc pierwotny i propagowany wstecznie przepływ pracy muszą dawać tę samą wartość . Korzyść z wstecznej propagacji staje się widoczna dopiero na zaszumionym sprzęcie, gdzie krótszy propagowany wstecznie obwód akumuluje mniej błędów, co zilustrowano w poniższym przykładzie dużej skali na sprzęcie.
Przykład dużej skali na sprzęcie
Podczas opracowywania eksperymentu przydatne jest rozpoczęcie od małego obwodu, aby ułatwić wizualizacje i symulacje. Teraz przyjrzymy się wstecznej propagacji operatora dla 50-qubitowego hamiltonianu Heisenberga z tym samym zestawem wartości parametrów i oraz tą samą obserwowalną , ale dla czterech kroków Trottera. Idealnej wartości oczekiwanej w tej skali nie można obliczyć metodą brute force, dlatego używamy sieci tensorowej i uzyskujemy idealną wartość oczekiwaną równą .
Wraz z wsteczną propagacją, w tym przykładzie dużej skali wprowadzamy również wsteczną propagację z obcinaniem. Idealnie chcemy propagować wstecz jak najwięcej, aby zmniejszyć głębokość efektywnego obwodu. Jednak często prowadzi to do dużej liczby niekomutujących składników w zaktualizowanej obserwowalnej, co zwiększa narzut kwantowy. Dlatego możemy eliminować składniki obserwowalnej z małymi współczynnikami za pomocą praktyki zwanej obcinaniem. Podczas gdy obcinanie pozwala na głębszą propagację poprzez redukcję liczby składników w zaktualizowanej obserwowalnej, wprowadza również pewną aproksymację. Dlatego konieczne jest ograniczenie obcinania w pewnych granicach, aby błąd aproksymacji nie przeważył nad redukcją szumów uzyskaną dzięki głębszej wstecznej propagacji.
Aby ograniczyć ilość obcinania, przydzielamy budżet błędu dla każdego przedziału, jak również całkowity budżet błędu dla całego propagowanego wstecznie obwodu, używając funkcji setup_budget. Zapewnia to kontrolę nad obcinaniem zarówno dla każdego przedziału, jak i dla całego obwodu. Inne sposoby alokacji budżetu można znaleźć w tym przewodniku.
num_qubits = 50
layout = [(i - 1, i) for i in range(1, num_qubits)]
# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)
hamiltonian = generate_xyz_hamiltonian(
coupling_map,
coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)
# Generate a time evolution circuit for the Hamiltonian
circuit = generate_time_evolution_circuit(
hamiltonian,
time=0.2,
synthesis=LieTrotter(reps=4),
)
# Define the observable to measure
observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
num_qubits,
)
slices = slice_by_depth(circuit, max_slice_depth=1)
# Define the maximum number of qwc groups allowed in the
# backpropagated observable,
# and the truncation error budget
op_budget = OperatorBudget(max_qwc_groups=15)
truncation_error_budget = setup_budget(
max_error_total=0.03, max_error_per_slice=0.005
)
# First backpropagation without truncation
bp_obs, remaining_slices, metadata = backpropagate(
observable, slices, operator_budget=op_budget
)
bp_circuit = combine_slices(remaining_slices)
# Now backpropagate with truncation, using the same operator budget and
# the defined truncation error budget
bp_obs_trunc, remaining_slices_trunc, metadata = backpropagate(
observable,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
bp_circuit_trunc = combine_slices(
remaining_slices_trunc, include_barriers=False
)
# Now we transpile the original circuit and the two backpropagated circuits,
# and apply the layout to the corresponding observables
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
isa_circuit = pm.run(circuit)
isa_bp_circuit = pm.run(bp_circuit)
isa_bp_circuit_trunc = pm.run(bp_circuit_trunc)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_bp_observable = bp_obs.apply_layout(isa_bp_circuit.layout)
isa_bp_observable_trunc = bp_obs_trunc.apply_layout(
isa_bp_circuit_trunc.layout
)
# Compare the 2-qubit depth of each transpiled circuit to see how much
# depth backpropagation saved
print(
f"2-qubit depth without backpropagation: "
f"{isa_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation: "
f"{isa_bp_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation and truncation: "
f"{isa_bp_circuit_trunc.depth(lambda x: x.operation.num_qubits == 2)}"
)
pubs = [
(isa_circuit, isa_observable),
(isa_bp_circuit, isa_bp_observable),
(isa_bp_circuit_trunc, isa_bp_observable_trunc),
]
# Now we instantiate the Estimator primitive for the hardware with
# ZNE and measurement error
# mitigation and compute the three circuits and observables
options = EstimatorOptions()
options.default_precision = 0.01
options.resilience_level = 2
options.resilience.zne.noise_factors = [1, 1.2, 1.4]
options.resilience.zne.extrapolator = ["linear"]
estimator = EstimatorV2(mode=backend, options=options)
estimator.options.environment.job_tags = ["TUT_OBP"]
job = estimator.run(pubs)
# Retrieve the results and the standard deviations
result_no_bp = job.result()[0].data.evs.item()
result_bp = job.result()[1].data.evs.item()
result_bp_trunc = job.result()[2].data.evs.item()
std_no_bp = job.result()[0].data.stds.item()
std_bp = job.result()[1].data.stds.item()
std_bp_trunc = job.result()[2].data.stds.item()
2-qubit depth without backpropagation: 24
2-qubit depth with backpropagation: 20
2-qubit depth with backpropagation and truncation: 18
print(f"Expectation value without backpropagation: {result_no_bp}")
print(f"Backpropagated expectation value: {result_bp}")
print(f"Backpropagated expectation value with truncation: {result_bp_trunc}")
Expectation value without backpropagation: 0.9543907942381811
Backpropagated expectation value: 0.9445337385406468
Backpropagated expectation value with truncation: 0.934050286970965
# Plot the results
methods = [
"No backpropagation",
"Backpropagation",
"Backpropagation w/ truncation",
]
values = [result_no_bp, result_bp, result_bp_trunc]
error_bars = [std_no_bp, std_bp, std_bp_trunc]
ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.errorbar(methods, values, yerr=error_bars, fmt="o", color="r", capsize=5)
plt.axhline(0.89)
ax.set_ylim([0.8, 0.98])
plt.text(0.25, 0.895, "Exact result")
ax.set_ylabel(r"$M_Z$", fontsize=12)
Text(0, 0.5, '$M_Z$')
Kolejne kroki
Jeśli ta praca wydaje ci się interesująca, możesz zainteresować się następującymi materiałami:
- Przybliżona kompilacja kwantowa dla obwodów ewolucji czasowej
- Formuły wieloproduktowe do redukcji błędu Trottera
pauli-prop, pakiet przyspieszony przez Rust do propagacji Pauliego, z samouczkami obejmującymi OBP, klasyczne szacowanie wartości oczekiwanych i zaszumioną symulację