Cięcie obwodów dla periodycznych warunków brzegowych
Szacowane użycie: 2 minuty na ibm_boston (UWAGA: To jest jedynie szacunek. Twój czas wykonania może się różnić.)
Cele nauki
-
Użyj wirtualnego połączenia, aby transpilować okresowy łańcuch bez routingu jego bramek brzegowych.
-
Zastąp te bramki eksperymentami cięcia obwodu i zrekonstruuj wartości oczekiwane z wyników Sampler.
-
Porównaj obwody cięte i nieciête, uwzględniając głębokość obwodu i koszt próbkowania.
Wymagania wstępne
Powinieneś znać wzorce Qiskit, transpilację i prymityw Sampler.
Tło
Okresowe warunki brzegowe łączą ostatni kubit w łańcuchu z pierwszym. Jeśli te kubity są od siebie daleko na procesorze kwantowym, realizacja ich oddziaływania wymaga routingu, który może dodać bramki dwukubitowe i zwiększyć głębokość obwodu.
Cięcie bramek zastępuje bramkę dwukubitową ważoną kombinacją operacji jednokubitowych i pomiarów. Wykonanie tych eksperymentów i połączenie ich wyników rekonstruuje wartości oczekiwane dla oryginalnego obwodu. Może to zmniejszyć narzut routingu kosztem dodatkowego próbkowania. Zobacz wyjaśnienie cięcia obwodów, aby poznać leżący u podstaw rozkład kwazi-prawdopodobieństwa.
Najpierw zwalidujesz ten przepływ pracy na sześciokubitowym symulatorze bez szumu. Następnie zastosujesz go do łańcucha co najmniej 30 kubitów na sprzęcie i porównasz wyniki cięte i nieciête. Obwód używa parametrów o analitycznie znanych wartościach oczekiwanych, więc porównanie na sprzęcie nie wymaga symulacji dużego wektora stanu.
Wymagania
Przed rozpoczęciem tego samouczka zainstaluj:
-
Qiskit SDK v2.3 lub nowszy, z obsługą wizualizacji
-
Qiskit Runtime v0.46 lub nowszy (
pip install 'qiskit-ibm-runtime~=0.46.1') -
Dodatek Qiskit Circuit cutting w wersji v0.10.0 (
pip install 'qiskit-addon-cutting~=0.10.0') -
Qiskit Aer v0.17.2 lub nowszy (
pip install 'qiskit-aer~=0.17.2')
Przykład sprzętowy wymaga również konta IBM Quantum® Platform z dostępem do QPU z co najmniej 120 kubitami oraz zapisanych poświadczeń. Przykład z symulatorem działa bez konta.
Konfiguracja
Zaimportuj narzędzia używane w obu przykładach. Aer obsługuje pomiary pośrednie wprowadzane przez cięcie bramek.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_cutting": "qiskit-addon-cutting", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "rustworkx": "rustworkx"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import matplotlib.pyplot as plt
import numpy as np
import rustworkx as rx
from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary
from qiskit.circuit.library import n_local
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.quantum_info import SparsePauliOp, Statevector
from qiskit.result import sampled_expectation_value
from qiskit.transpiler import CouplingMap, PassManager
from qiskit.transpiler.passes import (
BasisTranslator,
GateDirection,
Optimize1qGatesDecomposition,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_aer.primitives import SamplerV2 as AerSampler
from qiskit_addon_cutting import (
cut_gates,
generate_cutting_experiments,
reconstruct_expectation_values,
)
from qiskit_ibm_runtime import (
Batch,
QiskitRuntimeService,
SamplerOptions,
SamplerV2,
)
Przykład symulatora w małej skali
Użyj sześciokubitowego pierścienia z dwiema warstwami bramek splątujących. Symulowane urządzenie ma łączność liniową, więc zamykająca krawędź pierścienia wymaga routingu, chyba że zostanie przecięta.
Krok 1: Odwzorowanie klasycznych danych wejściowych na problem kwantowy
Poniższa funkcja tworzy warstwy rotacji RX przeplatane bramkami CX na przemiennych krawędziach pierścienia. Dwa powtórzenia dają dwie bramki brzegowe do przecięcia. Ustaw pierwsze dwie warstwy rotacji na zero, a ostatnią warstwę na . Idealnym wyjściem jest wtedy , z dokładnością do fazy globalnej.
Zmierz średnią jednokubitową wartość oczekiwaną Z i średnią wartość oczekiwaną sąsiednich ZZ, łącznie z parą okresową:
Ich idealne wartości wynoszą odpowiednio i . Te parametry stanowią punkt odniesienia dla przepływu pracy; nie demonstrują obliczenia trudnego klasycznie.
def build_periodic_problem(num_qubits, num_reps=2):
"""Build a periodic benchmark circuit and its Z and ZZ observables."""
edges = [(i, (i + 1) % num_qubits) for i in range(num_qubits)]
circuit = n_local(
num_qubits,
rotation_blocks="rx",
entanglement_blocks="cx",
entanglement=edges[::2] + edges[1::2],
reps=num_reps,
)
parameters = np.zeros(circuit.num_parameters)
parameters[-num_qubits:] = np.pi
circuit = circuit.assign_parameters(parameters)
terms = [("Z", [i], 1) for i in range(num_qubits)]
terms += [("ZZ", [i, (i + 1) % num_qubits], 1) for i in range(num_qubits)]
observables = SparsePauliOp.from_sparse_list(terms, num_qubits)
return circuit, observables
num_qubits = 6
circuit, observables = build_periodic_problem(num_qubits)
ideal_expvals = np.array(
[
Statevector(circuit).expectation_value(p).real
for p in observables.paulis
]
)
print("Ideal averages:", ideal_expvals.reshape(2, num_qubits).mean(axis=1))
circuit.draw("mpl", style="iqp", fold=60)
Ideal averages: [-1. 1.]

Krok 2: Optymalizacja problemu pod kątem wykonania na sprzęcie kwantowym
Utwórz model urządzenia z liniową mapą sprzężeń. Dostarcza on cel kompilacji; wykonanie w Kroku 3 będzie bez szumu.
simulation_backend = GenericBackendV2(
num_qubits=num_qubits,
basis_gates=["id", "rz", "sx", "x", "cx"],
coupling_map=CouplingMap.from_line(num_qubits),
seed=1234,
)
initial_layout = list(range(num_qubits))
Dodaj połączenie wirtualne między fizycznymi kubitami na końcach łańcucha. Dokonaj transpilacji jednokrotnie, używając tej rozszerzonej mapy sprzężeń i bazy CX. Połączenie wirtualne uniemożliwia Transpilerowi routowanie bramek granicznych, ale otrzymany Circuit nie może jeszcze zostać uruchomiony na prawdziwym urządzeniu.
Znajdź i potnij dwie graniczne bramki CX, zastosuj układ wirtualny do obserwabli i wygeneruj wszystkie składniki dekompozycji z num_samples=np.inf. Dzięki temu współczynniki dekompozycji są dokładne; pomiary nadal używają skończonej liczby shotów.
Na koniec przetłumacz eksperymenty na natywne bramki urządzenia i popraw ich kierunek tam, gdzie jest to potrzebne. Te przebiegi zachowują fizyczny układ kubitów i pozwalają uniknąć powtarzania wyboru układu i routingu dla każdego eksperymentu. Funkcja pomocnicza zwraca zmapowane obserwable potrzebne do rekonstrukcji.
def prepare_cut_experiments(circuit, observables, backend, initial_layout):
"""Compile with a virtual edge, cut it, and translate to native gates."""
boundary = {initial_layout[0], initial_layout[-1]}
virtual_map = CouplingMap(list(backend.coupling_map.get_edges()))
virtual_map.make_symmetric()
virtual_map.add_edge(initial_layout[-1], initial_layout[0])
virtual_map.add_edge(initial_layout[0], initial_layout[-1])
virtual_pm = generate_preset_pass_manager(
optimization_level=1,
coupling_map=virtual_map,
basis_gates=["rz", "sx", "x", "cx"],
initial_layout=initial_layout,
seed_transpiler=1234,
)
virtual_circuit = virtual_pm.run(circuit)
cut_indices = [
i
for i, instruction in enumerate(virtual_circuit.data)
if instruction.operation.name == "cx"
and {virtual_circuit.find_bit(q).index for q in instruction.qubits}
== boundary
]
mapped_observables = observables.apply_layout(virtual_circuit.layout)
qpd_circuit, bases = cut_gates(virtual_circuit, cut_indices)
experiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit,
observables=mapped_observables.paulis,
num_samples=np.inf,
)
native_pm = PassManager(
[
BasisTranslator(
SessionEquivalenceLibrary,
target_basis=backend.operation_names,
target=backend.target,
),
GateDirection(backend.coupling_map, target=backend.target),
BasisTranslator(
SessionEquivalenceLibrary,
target_basis=backend.operation_names,
target=backend.target,
),
Optimize1qGatesDecomposition(target=backend.target),
]
)
isa_experiments = native_pm.run(experiments)
print(f"Boundary gates cut: {len(cut_indices)}")
print(f"Number of experiments: {len(isa_experiments)}")
print(
f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}"
)
return isa_experiments, coefficients, mapped_observables
isa_experiments, coefficients, mapped_observables = prepare_cut_experiments(
circuit, observables, simulation_backend, initial_layout
)
Boundary gates cut: 2
Number of experiments: 36
Sampling overhead: 81.0
Każda dekompozycja CX ma sześć składników. Przy dwóch cięciach i jednej wspólnej bazie pomiarowej Z daje to eksperymentów. Narzut próbkowania to inna wielkość: kwadrat sumy wartości bezwzględnych współczynników dekompozycji wynosi . Opisuje on koszt próbkowania potrzebny do utrzymania precyzji przy rekonstrukcji kwaziprawdopodobieństw. Zobacz wyjaśnienie narzutu próbkowania.
Dla porównania dokonaj transpilacji skopiowanego Circuitu bez cięć, z dodanymi pomiarami, używając rzeczywistej mapy sprzężeń. measure_all() zapisuje oryginalne kubity logiczne w rejestrze meas; transpilacja zachowuje to odwzorowanie bitów klasycznych, więc jego zliczenia można oceniać za pomocą oryginalnych obserwabli.
uncut_pm = generate_preset_pass_manager(
optimization_level=1,
backend=simulation_backend,
initial_layout=initial_layout,
seed_transpiler=1234,
)
isa_uncut = uncut_pm.run(circuit.measure_all(inplace=False))
print("Uncut circuit depth:", isa_uncut.depth())
print("Cut circuit depths:", sorted({qc.depth() for qc in isa_experiments}))
Uncut circuit depth: 15
Cut circuit depths: [6, 7, 8, 9, 10, 11, 12, 15]
Krok 3: Wykonanie przy użyciu prymitywów Qiskit
Uruchom obie wersje za pomocą prymitywu Aer Sampler V2. Każdy eksperyment z cięciem otrzymuje 10 000 shotów, tak samo jak pojedynczy Circuit bez cięć. Przepływ pracy z cięciem zużywa więc w tym porównaniu 36 razy więcej shotów. Równa liczba shotów na Circuit nie oznacza równego całkowitego budżetu wykonania.
shots = 10_000
sampler = AerSampler(seed=1234)
cut_result = sampler.run(isa_experiments, shots=shots).result()
uncut_result = sampler.run([isa_uncut], shots=shots).result()
Krok 4: Post-processing i zwrócenie wyniku w pożądanym formacie klasycznym
Zrekonstruuj każdą wartość oczekiwaną Pauliego, używając tych samych zmapowanych obserwabli i kolejności eksperymentów, które zastosowano podczas generowania. Następnie uśrednij pierwsze wartości dla i pozostałe wartości dla . Dla Circuitu bez cięć oblicz wartości oczekiwane bezpośrednio ze zmierzonych zliczeń.
cut_expvals = reconstruct_expectation_values(
cut_result, coefficients, mapped_observables.paulis
)
uncut_counts = uncut_result[0].data.meas.get_counts()
uncut_expvals = [
sampled_expectation_value(uncut_counts, p) for p in observables.paulis
]
def compare_results(cut_expvals, uncut_expvals, num_qubits):
"""Display average Z and periodic ZZ values against the ideal values."""
cut_averages = np.asarray(cut_expvals).reshape(2, num_qubits).mean(axis=1)
uncut_averages = (
np.asarray(uncut_expvals).reshape(2, num_qubits).mean(axis=1)
)
print("Cut averages (Z, ZZ):", cut_averages)
print("Uncut averages (Z, ZZ):", uncut_averages)
print("Absolute cut errors:", np.abs(cut_averages - [-1, 1]))
print("Absolute uncut errors:", np.abs(uncut_averages - [-1, 1]))
fig, ax = plt.subplots(figsize=(7, 4))
positions = np.arange(2)
for offset, values, label, color in [
(-0.25, [-1, 1], "Ideal", "#4589ff"),
(0, cut_averages, "Cut", "#a56eff"),
(0.25, uncut_averages, "Uncut", "#009d9a"),
]:
ax.bar(
positions + offset, values, width=0.25, label=label, color=color
)
ax.set_xticks(positions, ["Average Z", "Average periodic ZZ"])
ax.set_ylabel("Expectation value")
ax.axhline(0, color="black", linewidth=0.8)
ax.legend()
fig.tight_layout()
plt.show()
compare_results(cut_expvals, uncut_expvals, num_qubits)
Cut averages (Z, ZZ): [-1.00100833 0.99778333]
Uncut averages (Z, ZZ): [-1. 1.]
Absolute cut errors: [0.00100833 0.00221667]
Absolute uncut errors: [0. 0.]
Wyniki bez szumu powinny być bliskie wartościom idealnym. Rekonstrukcja przy skończonej liczbie shotów może fluktuować, a zrekonstruowany estymator może wypaść poza przedział . Cięcie nie poprawia wyniku, który jest już wolny od szumu; ten przykład weryfikuje dekompozycję, odwzorowanie układu i rekonstrukcję przed wykonaniem na sprzęcie.
Przykład sprzętowy na dużą skalę
Użyj ponownie tego przepływu pracy na najmniej obciążonym działającym QPU z co najmniej 120 kubitami. Wybierz najdłuższą spośród najkrótszych ścieżek w jego nieskierowanym grafie sprzężeń: każda kolejna para jest połączona, a końce są od siebie daleko. Dzięki temu łańcuch jest dopasowany do wybranego urządzenia, zamiast opierać się na stałym układzie Eagle. Kod sprawdza, czy łańcuch zawiera co najmniej 30 kubitów i czy jego końce nie są sąsiadami.
Poniższy blok łączy cztery kroki. Twirling bramek, twirling pomiarów i dynamiczne rozprzęganie są wyłączone w obu wersjach. Oba zadania działają w tym samym batchu i otrzymują tag samouczka TUT_CCPBC. Eksperymenty z cięciem nadal używają łącznie więcej shotów, więc porównaj zarówno dokładność, jak i zużycie QPU.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=120
)
# ------------------------- Step 1 -------------------------
graph = backend.coupling_map.graph.to_undirected(multigraph=False)
paths = rx.all_pairs_dijkstra_shortest_paths(graph, lambda _: 1)
initial_layout = list(
max(
(
path
for source_paths in paths.values()
for path in source_paths.values()
),
key=len,
)
)
num_qubits = len(initial_layout)
if num_qubits < 30:
raise ValueError(
"Select a backend with a shortest path of at least 30 qubits."
)
if graph.has_edge(initial_layout[0], initial_layout[-1]):
raise ValueError("The chain endpoints must not be directly connected.")
print(f"Backend: {backend.name}; chain length: {num_qubits}")
print("Initial layout:", initial_layout)
circuit, observables = build_periodic_problem(num_qubits)
# ------------------------- Step 2 -------------------------
isa_experiments, coefficients, mapped_observables = prepare_cut_experiments(
circuit, observables, backend, initial_layout
)
uncut_pm = generate_preset_pass_manager(
optimization_level=1,
backend=backend,
initial_layout=initial_layout,
seed_transpiler=1234,
)
isa_uncut = uncut_pm.run(circuit.measure_all(inplace=False))
print("Uncut circuit depth:", isa_uncut.depth())
print("Cut circuit depths:", sorted({qc.depth() for qc in isa_experiments}))
# ------------------------- Step 3 -------------------------
options = SamplerOptions()
options.dynamical_decoupling.enable = False
options.twirling.enable_gates = False
options.twirling.enable_measure = False
options.environment.job_tags = ["TUT_CCPBC"]
shots = 10_000
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch, options=options)
cut_job = sampler.run(isa_experiments, shots=shots)
uncut_job = sampler.run([isa_uncut], shots=shots)
print("Cut job ID:", cut_job.job_id())
print("Uncut job ID:", uncut_job.job_id())
cut_result = cut_job.result()
uncut_result = uncut_job.result()
# ------------------------- Step 4 -------------------------
cut_expvals = reconstruct_expectation_values(
cut_result, coefficients, mapped_observables.paulis
)
uncut_counts = uncut_result[0].data.meas.get_counts()
uncut_expvals = [
sampled_expectation_value(uncut_counts, p) for p in observables.paulis
]
compare_results(cut_expvals, uncut_expvals, num_qubits)
qpu_seconds = sum(
job.metrics()["usage"]["quantum_seconds"] for job in [cut_job, uncut_job]
)
print(
f"Total QPU usage: {qpu_seconds:.1f} seconds ({qpu_seconds / 60:.2f} minutes)"
)
Backend: ibm_boston; chain length: 33
Initial layout: [0, 1, 2, 3, 16, 23, 24, 25, 37, 45, 44, 43, 56, 63, 64, 65, 77, 85, 86, 87, 97, 107, 106, 105, 117, 125, 124, 123, 136, 143, 142, 141, 140]
Boundary gates cut: 2
Number of experiments: 36
Sampling overhead: 81.0
Uncut circuit depth: 375
Cut circuit depths: [20]
Cut job ID: dagp7fj9k43c73admp40
Uncut job ID: dagp7fj9k43c73admp5g
Cut averages (Z, ZZ): [-0.92305152 0.89586667]
Uncut averages (Z, ZZ): [-0.61106061 0.51606061]
Absolute cut errors: [0.07694848 0.10413333]
Absolute uncut errors: [0.38893939 0.48393939]
Total QPU usage: 102.0 seconds (1.70 minutes)
W zarejestrowanym przebiegu na ibm_boston estymaty z cięciem wyniosły w przybliżeniu i , w porównaniu z i dla Circuitu bez cięć. Obie estymaty z cięciem były bliższe wartościom idealnym i .
Porównaj każdy wynik z jego wartością idealną oraz sprawdź głębokości Circuitów i całkowite zużycie QPU. Cięcie eliminuje potrzebę routowania bramek periodycznych, ale wprowadza operacje pośrednie i dodatkową niepewność statystyczną. Szum sprzętowy i kalibracja wpływają na porównanie; poprawa nie jest gwarantowana. Aby ocenić, czy zaobserwowana różnica jest istotna statystycznie, potrzebne są powtórne uruchomienia.
Kolejne kroki
-
Poznaj cięcie Circuitów w celu redukcji głębokości dla innych oddziaływań nielokalnych.
-
Wypróbuj cięcie przewodów do estymacji wartości oczekiwanych, aby podzielić Circuit na mniejsze podsystemy.
-
Przeczytaj materiały wyjaśniające dotyczące cięcia Circuitów, aby zrozumieć kompromis między cięciem a narzutem próbkowania.