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?

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 π\pi. Idealnym wyjściem jest wtedy ∣1⟩⊗n|1\rangle^{\otimes n}, z dokładnością do fazy globalnej.

Zmierz średnią jednokubitową wartość oczekiwaną Z i średnią wartość oczekiwaną sąsiednich ZZ, łącznie z parą okresową:

MZ=1n∑i=0n−1⟨Zi⟩,CZZ=1n∑i=0n−1⟨ZiZ(i+1) mod n⟩.M_Z = \frac{1}{n}\sum_{i=0}^{n-1}\langle Z_i\rangle, \qquad C_{ZZ} = \frac{1}{n}\sum_{i=0}^{n-1}\langle Z_i Z_{(i+1)\bmod n}\rangle.

Ich idealne wartości wynoszą odpowiednio −1-1 i +1+1. 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.]

Six-qubit periodic circuit with two boundary CX gates

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 62=366^2=36 eksperymentów. Narzut próbkowania to inna wielkość: kwadrat sumy wartości bezwzględnych współczynników dekompozycji wynosi 92=819^2=81. 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 nn wartości dla MZM_Z i pozostałe nn wartości dla CZZC_{ZZ}. 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.]

Ideal, cut, and uncut expectation values from noiseless simulation

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ł [−1,1][-1,1]. 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)

Ideal, cut, and uncut expectation values measured on ibm_boston

W zarejestrowanym przebiegu na ibm_boston estymaty z cięciem wyniosły w przybliżeniu MZ=−0.923M_Z=-0.923 i CZZ=0.896C_{ZZ}=0.896, w porównaniu z −0.611-0.611 i 0.5160.516 dla Circuitu bez cięć. Obie estymaty z cięciem były bliższe wartościom idealnym −1-1 i +1+1.

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​

Zalecenia