Přeskočit na hlavní obsah
Tento projekt je v betě. Našel jsi chybu? Otevřít issue na GitHubu. Máš otázku nebo nápad? Zeptej se v Discussions — rádi si poslechneme tvou zpětnou vazbu!
Jaká je kvalita překladu na této stránce?

Circuit cutting pro periodické okrajové podmínky

Odhad spotřeby: 2 minuty na ibm_boston (POZNÁMKA: Jedná se pouze o odhad. Skutečná doba běhu se může lišit.)

Cíle učení​

  • Použij virtuální spojení k transpilaci periodického řetězce bez směrování jeho okrajových hradel.

  • Nahraď tato hradla experimenty dělení obvodu a rekonstruuj očekávané hodnoty z výsledků Sampler.

  • Porovnej dělené a nedělené obvody s ohledem na hloubku obvodu a náklady na vzorkování.

Předpoklady​

Měl bys být obeznámen s Qiskit patterns, transpilací a primitivou Sampler.

Pozadí​

Periodické okrajové podmínky propojují poslední qubit v řetězci s prvním. Pokud jsou tyto qubity na kvantovém procesoru daleko od sebe, implementace jejich interakce vyžaduje směrování, které může přidat dvouqubitová hradla a zvětšit hloubku obvodu.

Dělení hradel nahrazuje dvouqubitové hradlo váženou kombinací jednoqubitových operací a měření. Provedení těchto experimentů a kombinace jejich výsledků rekonstruuje očekávané hodnoty původního obvodu. To může snížit režii směrování za cenu dodatečného vzorkování. Základní kvazipravděpodobnostní dekompozici najdeš ve vysvětlení dělení obvodů.

Tento postup nejprve ověříš na šestiqubitovém bezšumovém simulátoru. Poté ho použiješ na řetězec nejméně 30 qubitů na hardwaru a porovnáš výsledky dělených a nedělených obvodů. Obvod používá parametry s analyticky známými očekávanými hodnotami, takže porovnání na hardwaru nevyžaduje rozsáhlou simulaci stavového vektoru.

Požadavky​

Před zahájením tohoto tutoriálu nainstaluj:

  • Qiskit SDK v2.3 nebo novější, s podporou vizualizace

  • Qiskit Runtime v0.46 nebo novější (pip install 'qiskit-ibm-runtime~=0.46.1')

  • Qiskit addon Circuit cutting verze v0.10.0 (pip install 'qiskit-addon-cutting~=0.10.0')

  • Qiskit Aer v0.17.2 nebo novější (pip install 'qiskit-aer~=0.17.2')

Příklad na hardwaru vyžaduje také účet IBM Quantum® Platform s přístupem k QPU s alespoň 120 qubity a uložené přihlašovací údaje. Příklad se simulátorem běží bez účtu.

Nastavení​

Importuj nástroje používané v obou příkladech. Aer podporuje průběžná měření zavedená dělením hradel.

# 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,
)

Příklad malého rozsahu na simulátoru​

Použij šestiqubitový kruh se dvěma vrstvami provazujících hradel. Simulované zařízení má lineární konektivitu, takže uzavírací hrana kruhu vyžaduje směrování, pokud není rozdělena.

Krok 1: Mapování klasických vstupů na kvantový problém​

Následující funkce vytváří vrstvy rotací RX proložené hradly CX na střídajících se hranách kruhu. Dvě opakování dávají dvě okrajová hradla k rozdělení. Nastav první dvě vrstvy rotací na nulu a poslední vrstvu na π\pi. Ideálním výstupem je pak ∣1⟩⊗n|1\rangle^{\otimes n}, až na globální fázi.

Změř průměrnou jednoqubitovou očekávanou hodnotu Z a průměrnou očekávanou hodnotu sousedních ZZ, včetně periodického páru:

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.

Jejich ideální hodnoty jsou −1-1, respektive +1+1. Tyto parametry poskytují měřítko pro postup; nedokazují klasicky obtížný výpočet.

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: Optimalizace problému pro provádění na kvantovém hardwaru​

Vytvoř model zařízení s lineární mapou propojení. Poskytuje cíl kompilace; provedení v kroku 3 bude bezšumové.

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))

Přidej virtuální spojení mezi fyzickými Qubity na koncích řetězce. Proveď jednu transpilaci s touto rozšířenou mapou propojení a bází CX. Virtuální spojení brání Transpileru směrovat hraniční Gate, ale výsledný Circuit zatím nelze spustit na skutečném zařízení.

Najdi a přestřihni dvě hraniční Gate CX, aplikuj virtuální rozložení na observabily a vygeneruj všechny členy dekompozice pomocí num_samples=np.inf. Tím jsou koeficienty dekompozice přesné; měření stále používají konečný počet shotů.

Nakonec přelož experimenty do nativních Gate zařízení a v případě potřeby oprav jejich směr. Tyto průchody zachovávají fyzické rozložení Qubitů a vyhýbají se opakování výběru rozložení a směrování pro každý experiment. Pomocná funkce vrací namapované observabily potřebné pro rekonstrukci.

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ždá dekompozice CX má šest členů. Se dvěma řezy a jednou sdílenou měřicí bází Z to dává 62=366^2=36 experimentů. Režie vzorkování je jiná veličina: druhá mocnina součtu absolutních hodnot koeficientů dekompozice je 92=819^2=81. Popisuje náklady na vzorkování potřebné k udržení přesnosti při kvazipravděpodobnostní rekonstrukci. Viz vysvětlení režie vzorkování.

Pro srovnání transpiluj měřenou kopii nerozřezaného obvodu s použitím skutečné mapy propojení. measure_all() zaznamenává původní logické Qubity do registru meas; transpilace zachovává toto mapování klasických bitů, takže její četnosti lze vyhodnotit s původními observabily.

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: Spuštění pomocí primitiv Qiskit​

Spusť obě verze pomocí primitiva Aer Sampler V2. Každý experiment s řezem dostane 10 000 shotů, stejně jako jediný nerozřezaný Circuit. Pracovní postup s řezy tedy v tomto srovnání spotřebuje 36krát více shotů. Stejný počet shotů na Circuit neznamená stejný celkový rozpočet na provedení.

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: Následné zpracování a vrácení výsledku v požadovaném klasickém formátu​

Rekonstruuj každou očekávanou hodnotu Pauliho operátoru pomocí stejných namapovaných observabilů a stejného pořadí experimentů, jaké se použilo při generování. Poté zprůměruj prvních nn hodnot pro MZM_Z a zbývajících nn hodnot pro CZZC_{ZZ}. U nerozřezaného obvodu spočítej očekávané hodnoty přímo z naměřených četností.

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

Výsledky bez šumu by měly být blízko ideálním hodnotám. Rekonstrukce s konečným počtem shotů může kolísat a rekonstruovaný odhad může ležet mimo [−1,1][-1,1]. Řezání nezlepšuje výsledek, který je už bez šumu; tento příklad ověřuje dekompozici, mapování rozložení a rekonstrukci před spuštěním na hardwaru.

Příklad velkého hardwaru​

Použij pracovní postup znovu na nejméně vytíženém provozním QPU s alespoň 120 Qubity. Vyber nejdelší nejkratší cestu v jeho neorientovaném grafu propojení: každá po sobě jdoucí dvojice je propojená, zatímco koncové body jsou od sebe daleko. Tím se řetězec přizpůsobí vybranému zařízení místo spoléhání na pevné rozložení Eagle. Kód kontroluje, že řetězec obsahuje alespoň 30 Qubitů a že jeho koncové body nejsou sousedé.

Následující blok spojuje čtyři kroky. Twirling Gate, twirling měření a dynamické oddělení (dynamical decoupling) jsou vypnuté pro obě verze. Obě úlohy běží ve stejné dávce a dostanou značku tutoriálu TUT_CCPBC. Experimenty s řezy stále používají celkově více shotů, proto porovnej přesnost i využití 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

V zaznamenaném běhu na ibm_boston byly odhady s řezy přibližně MZ=−0.923M_Z=-0.923 a CZZ=0.896C_{ZZ}=0.896, oproti −0.611-0.611 a 0.5160.516 u nerozřezaného obvodu. Oba odhady s řezy byly blíže ideálním hodnotám −1-1 a +1+1.

Porovnej každý výsledek s jeho ideální hodnotou a prozkoumej hloubky Circuitů a celkové využití QPU. Řezání odstraňuje nutnost směrovat periodické Gate, ale přináší mezilehlé operace a dodatečnou statistickou nejistotu. Šum hardwaru a kalibrace srovnání ovlivňují; zlepšení není zaručeno. K posouzení, zda je pozorovaný rozdíl statisticky významný, je potřeba opakovat běhy.

Další kroky​

Doporučení