Tăierea circuitelor pentru condiții periodice la margine
Estimare de utilizare: 2 minute pe ibm_boston (NOTĂ: Aceasta este doar o estimare. Timpul tău de rulare poate varia.)
Rezultate de învățare
-
Folosește o conexiune virtuală pentru a transpila un lanț periodic fără a rula rutarea porților sale de graniță.
-
Înlocuiește acele porți cu experimente de tăiere a circuitului și reconstruiește valorile de așteptare din rezultatele Sampler.
-
Compară circuitele tăiate și netăiate, ținând cont de adâncimea circuitului și de costul de eșantionare.
Cerințe preliminare
Ar trebui să fii familiarizat cu modelele Qiskit, transpilarea și primitiva Sampler.
Fundal
Condițiile la limită periodice conectează ultimul qubit dintr-un lanț cu primul. Dacă acești qubiți sunt depărtați pe un procesor cuantic, implementarea interacțiunii lor necesită rutare, ceea ce poate adăuga porți cu doi qubiți și poate crește adâncimea circuitului.
Tăierea porților înlocuiește o poartă cu doi qubiți cu o combinație ponderată de operații cu un singur qubit și măsurători. Executarea acestor experimente și combinarea rezultatelor lor reconstruiește valorile de așteptare pentru circuitul original. Aceasta poate reduce overhead-ul de rutare, cu prețul unei eșantionări suplimentare. Vezi explicația tăierii circuitelor pentru descompunerea cvasiprobabilistică de bază.
Mai întâi vei valida acest flux de lucru pe un simulator fără zgomot cu șase qubiți. Apoi îl vei aplica unui lanț de cel puțin 30 de qubiți pe hardware și vei compara rezultatele tăiate și netăiate. Circuitul folosește parametri cu valori de așteptare cunoscute analitic, deci compararea pe hardware nu necesită o simulare mare a vectorului de stare.
Cerințe
Înainte de a începe acest tutorial, instalează:
-
Qiskit SDK v2.3 sau mai nou, cu suport pentru vizualizare
-
Qiskit Runtime v0.46 sau o versiune mai recentă (
pip install 'qiskit-ibm-runtime~=0.46.1') -
Addon-ul Qiskit Circuit cutting v0.10.0 (
pip install 'qiskit-addon-cutting~=0.10.0') -
Qiskit Aer v0.17.2 sau o versiune mai recentă (
pip install 'qiskit-aer~=0.17.2')
Exemplul hardware necesită și un cont IBM Quantum® Platform cu acces la un QPU cu cel puțin 120 de qubiți și credențiale salvate. Exemplul cu simulator rulează fără cont.
Configurare
Importă instrumentele folosite în ambele exemple. Aer acceptă măsurătorile intermediare introduse de tăierea porților.
# 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,
)
Exemplu la scară mică cu simulator
Folosește un inel de șase qubiți cu două straturi de porți de încâlcire. Dispozitivul simulat are conectivitate liniară, deci muchia de închidere a inelului necesită rutare dacă nu este tăiată.
Pasul 1: Maparea intrărilor clasice la o problemă cuantică
Funcția următoare creează straturi de rotații RX intercalate cu porți CX pe muchii alternante ale inelului. Două repetiții dau două porți de graniță de tăiat. Setează primele două straturi de rotație la zero și stratul final la . Ieșirea ideală este atunci , până la o fază globală.
Măsoară valoarea de așteptare medie Z pe un singur qubit și valoarea de așteptare medie ZZ pe perechi adiacente, inclusiv perechea periodică:
Valorile lor ideale sunt , respectiv . Acești parametri oferă un reper pentru fluxul de lucru; ei nu demonstrează un calcul dificil clasic.
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.]

Pasul 2: Optimizarea problemei pentru execuția pe hardware cuantic
Creează un model de dispozitiv cu o hartă de cuplare liniară. Acesta furnizează ținta de compilare; execuția din Pasul 3 va fi fără zgomot.
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))
Adaugă o conexiune virtuală între qubiții fizici de la capetele lanțului. Transpilează o dată folosind această hartă de cuplare augmentată și o bază CX. Conexiunea virtuală împiedică transpiler-ul să direcționeze porțile de la limită, dar circuitul rezultat nu poate fi încă rulat pe dispozitivul real.
Găsește și taie cele două porți CX de la limită, aplică layout-ul virtual asupra observabilelor și generează toți termenii descompunerii cu num_samples=np.inf. Astfel coeficienții descompunerii sunt exacți; măsurătorile folosesc totuși un număr finit de shot-uri.
În final, tradu experimentele în porțile native ale dispozitivului și corectează direcția acestora acolo unde este necesar. Aceste pase păstrează layout-ul qubiților fizici și evită repetarea selecției layout-ului și a rutării pentru fiecare experiment. Funcția ajutătoare returnează observabilele mapate necesare pentru reconstrucție.
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
Fiecare descompunere CX are șase termeni. Cu două tăieturi și o singură bază de măsurare Z comună, rezultă experimente. Overhead-ul de eșantionare este o cantitate diferită: suma pătratelor coeficienților absoluți ai descompunerii este . Acesta descrie costul de eșantionare pentru menținerea preciziei în reconstrucția cvasiprobabilistică. Vezi explicația overhead-ului de eșantionare.
Pentru comparație, transpilează o copie cu măsurători a circuitului netăiat, folosind harta de cuplare reală. measure_all() înregistrează qubiții logici originali în registrul meas; transpilarea păstrează această mapare a biților clasici, astfel încât numărătorile sale pot fi evaluate cu observabilele originale.
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]
Pasul 3: Executarea cu primitive Qiskit
Rulează ambele versiuni cu primitiva Aer Sampler V2. Fiecare experiment tăiat primește 10.000 de shot-uri, la fel ca singurul circuit netăiat. Prin urmare, fluxul cu tăiere folosește de 36 de ori mai multe shot-uri în această comparație. Un număr egal de shot-uri per circuit nu înseamnă un buget total de execuție egal.
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()
Pasul 4: Post-procesare și returnarea rezultatului în formatul clasic dorit
Reconstruiește fiecare valoare de așteptare Pauli folosind aceleași observabile mapate și aceeași ordine a experimentelor ca la generare. Apoi calculează media primelor valori pentru și a celor valori rămase pentru . Pentru circuitul netăiat, calculează valorile de așteptare direct din numărătorile măsurate.
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.]
Rezultatele fără zgomot ar trebui să fie apropiate de valorile ideale. Reconstrucția cu un număr finit de shot-uri poate fluctua, iar o estimare reconstruită poate ieși din intervalul . Tăierea nu îmbunătățește un rezultat deja fără zgomot; acest exemplu verifică descompunerea, maparea layout-ului și reconstrucția înainte de execuția pe hardware.
Exemplu hardware la scară mare
Reutilizează fluxul de lucru pe cel mai puțin ocupat QPU operațional cu cel puțin 120 de qubiți. Selectează un cel mai lung drum cel mai scurt din graful său de cuplare neorientat: fiecare pereche consecutivă este conectată, în timp ce capetele sunt departe unul de altul. Astfel lanțul se adaptează dispozitivului ales, în loc să se bazeze pe un layout Eagle fix. Codul verifică dacă lanțul conține cel puțin 30 de qubiți și dacă capetele sale nu sunt vecine.
Blocul următor combină cei patru pași. Gate twirling, measurement twirling și dynamical decoupling sunt dezactivate pentru ambele versiuni. Ambele joburi rulează în același batch și primesc eticheta de tutorial TUT_CCPBC. Experimentele tăiate folosesc în continuare mai multe shot-uri în total, așa că compară atât acuratețea, cât și utilizarea 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)
În rularea înregistrată pe ibm_boston, estimările cu tăiere au fost de aproximativ și , comparativ cu și pentru circuitul netăiat. Ambele estimări cu tăiere au fost mai apropiate de valorile ideale și .
Compară fiecare rezultat cu valoarea sa ideală și inspectează adâncimile circuitelor și utilizarea totală a QPU. Tăierea elimină nevoia de a direcționa porțile periodice, dar introduce operații intermediare și incertitudine statistică suplimentară. Zgomotul hardware și calibrarea influențează comparația; o îmbunătățire nu este garantată. Sunt necesare rulări repetate pentru a evalua dacă o diferență observată este semnificativă statistic.
Pașii următori
-
Explorează tăierea circuitelor pentru reducerea adâncimii pentru alte interacțiuni nelocale.
-
Încearcă tăierea firelor pentru estimarea valorilor de așteptare pentru a partiționa un circuit în subsisteme mai mici.
-
Citește materialul explicativ despre tăierea circuitelor pentru a înțelege compromisul dintre tăiere și overhead-ul de eșantionare.