Skip to main content
IBM Quantum Platform

Taglio dei circuiti per condizioni al contorno periodiche

Stima della durata di utilizzo: 2 minuti ( ibm_boston NOTA: si tratta solo di una stima. (La durata potrebbe variare.)


Risultati di apprendimento

  • Utilizza una connessione virtuale per traspilare una catena periodica senza instradare i suoi gate di confine.
  • Sostituisci quei cancelli con esperimenti di interruzione del circuito e ricostruisci i valori attesi dai risultati del Sampler.
  • Confrontare i circuiti con e senza taglio, tenendo conto della profondità del circuito e dei costi di campionamento.

Prerequisiti

Dovresti avere familiarità con i modelli di Qiskit, la transpilazione e la primitiva Sampler.


Sfondo

Le condizioni al contorno periodiche collegano l'ultimo qubit di una catena al primo. Se questi qubit sono distanti tra loro all’interno di un processore quantistico, l’implementazione della loro interazione richiede un instradamento che può comportare l’aggiunta di porte a due qubit e aumentare la profondità del circuito.

Il taglio dei gate sostituisce un gate a due qubit con una combinazione ponderata di operazioni su un singolo qubit e misurazioni. L'esecuzione di questi esperimenti e la combinazione dei relativi risultati consentono di ricostruire i valori attesi per il circuito originale. Ciò può ridurre il sovraccarico di instradamento, a scapito di un campionamento aggiuntivo. Si veda la spiegazione relativa al taglio del circuito per la scomposizione in quasi-probabilità sottostante.

Per prima cosa verificherai questo flusso di lavoro su un simulatore privo di rumore a sei qubit. Successivamente lo applicherai a una catena di almeno 30 qubit su hardware e confronterai i risultati con e senza taglio. Il circuito utilizza parametri con valori attesi noti analiticamente, pertanto il confronto a livello hardware non richiede una simulazione con vettori di stato di grandi dimensioni.


Requisiti

Prima di iniziare questo tutorial, installa:

  • Qiskit SDK v2.3 o versioni successive, con supporto alla visualizzazione
  • Qiskit Runtime v0.46 o versioni successive (pip install 'qiskit-ibm-runtime~=0.46.1')
  • Componente aggiuntivo Qiskit per il disegno di circuiti, versio v0.10.0 (pip install 'qiskit-addon-cutting~=0.10.0')
  • Qiskit Aer versio v0.17.2 o successive (pip install 'qiskit-aer~=0.17.2')

L'esempio relativo all'hardware richiede inoltre un account IBM Quantum® Platform con accesso a una QPU con almeno 120 qubit e credenziali salvate. L'esempio del simulatore funziona senza account.


Configura

Importa gli strumenti utilizzati in entrambi gli esempi. Aer supporta le misurazioni intermedie introdotte dal taglio a gate.

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

Esempio di simulatore su piccola scala

Utilizzare un anello a sei qubit con due livelli di porte di entanglement. Il dispositivo simulato presenta una connettività lineare, pertanto il bordo di chiusura dell'anello richiede un instradamento, a meno che non venga tagliato.

Fase 1: mappare gli input classici su un problema quantistico

La seguente funzione crea livelli di rotazione RX intercalati con porte CX sui fronti alternati dell'anello. Due ripetizioni danno come risultato due porte di confine da tagliare. Imposta i primi due livelli di rotazione a zero e l'ultimo livello a π\pi. Il risultato ideale è quindi ∣1⟩⊗n|1\rangle^{\otimes n}, a parte una fase globale.

Calcolare il valore atteso medio della componente Z di un singolo qubit e il valore atteso medio della componente ZZ tra qubit adiacenti, inclusa la coppia periodica :

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.

I loro valori ideali sono, rispettivamente, −1-1 e +1+1. Questi parametri costituiscono un punto di riferimento per il flusso di lavoro; non rappresentano un calcolo classicamente complesso.

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)

Output:

Ideal averages: [-1.  1.]
Six-qubit periodic circuit with two boundary CX gates

Fase 2: Ottimizzazione del problema per l'esecuzione su hardware quantistico

Creare un modello di dispositivo con una mappa di accoppiamento lineare. Fornisce la destinazione della compilazione; l'esecuzione nella Fase 3 avverrà senza intoppi.

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

Aggiungere una connessione virtuale tra i qubit fisici alle estremità della catena. Eseguire una volta la transpilazione utilizzando questa mappa di accoppiamento estesa e una base CX. La connessione virtuale impedisce al transpiler di instradare i gate di confine, ma il circuito risultante non è ancora in grado di funzionare sul dispositivo reale.

Individua ed elimina i due gate CX di confine, applica il layout virtuale alle grandezze osservabili e genera tutti i termini di decomposizione con num_samples=np.inf. In questo modo i coefficienti di decomposizione risultano esatti; le misurazioni continuano comunque a basarsi su un numero finito di impulsi.

Infine, traduci gli esperimenti nei gate nativi del dispositivo e correggi la loro direzione dove necessario. Questi passaggi mantengono la disposizione fisica dei qubit ed evitano di dover ripetere la selezione della disposizione e l'instradamento per ogni esperimento. L'helper restituisce gli osservabili mappati necessari per la ricostruzione.

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
)

Output:

Boundary gates cut: 2
Number of experiments: 36
Sampling overhead: 81.0

Ogni scomposizione CX è composta da sei termini. Con due tagli e una base di misura Z condivisa, si ottengono esperimenti di tipo 62=366^2=36. Il sovraccarico di campionamento è una grandezza diversa: la somma al quadrato dei coefficienti di decomposizione assoluti è 92=819^2=81. Descrive il costo di campionamento necessario per mantenere la precisione nella ricostruzione per quasi-probabilità. Si veda la spiegazione relativa al sovraccarico di campionamento.

A titolo di confronto, si esegua la transpilazione di una copia misurata del circuito non tagliato utilizzando la mappa di accoppiamento effettiva. measure_all() registra i qubit logici originali nel meas registro; la transpilazione preserva tale mappatura sui bit classici, pertanto i suoi conteggi possono essere valutati utilizzando gli osservabili originali.

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

Output:

Uncut circuit depth: 15
Cut circuit depths: [6, 7, 8, 9, 10, 11, 12, 15]

Passaggio 3: eseguire utilizzando Qiskit primitives

Esegui entrambe le versioni con la primitiva Aer Sampler V2. Ogni esperimento con taglio prevede 10.000 colpi, così come il singolo circuito senza taglio. In questo confronto, il flusso di lavoro di montaggio utilizza quindi 36 volte più riprese. Il numero di scatti corrispondenti per ogni circuito non corrisponde al budget totale di esecuzione.

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

Fase 4: Post-elaborazione e restituzione del risultato nel formato classico desiderato

Ricostruire ciascun valore atteso di Pauli utilizzando le stesse grandezze osservabili mappate e lo stesso ordine degli esperimenti utilizzati durante la generazione. Quindi calcolare la media dei primi valori nn per MZM_Z e dei restanti valori nn per CZZC_{ZZ}. Per il circuito non tagliato, calcolare i valori attesi direttamente dai conteggi misurati.

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)

Output:

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

I risultati senza rumore dovrebbero essere vicini ai valori ideali. La ricostruzione a numero finito di campioni può presentare fluttuazioni e una stima ricostruita può trovarsi al di fuori dell'intervallo [−1,1][-1,1]. Il taglio non migliora un risultato già privo di rumore; questo esempio verifica la scomposizione, la mappatura del layout e la ricostruzione prima dell'esecuzione sull'hardware.


Esempio di hardware su larga scala

Riutilizzare il flusso di lavoro sulla QPU operativa meno sollecitata con almeno 120 qubit. Scegliere il percorso più lungo e quello più breve nel relativo grafo di accoppiamento non orientato: ogni coppia consecutiva è collegata, mentre i vertici estremi sono distanti tra loro. In questo modo la catena viene adattata al dispositivo selezionato, anziché basarsi su un layout Eagle fisso. Il codice verifica che la catena contenga almeno 30 qubit e che i suoi estremi non siano adiacenti.

Il blocco seguente riunisce i quattro passaggi. La rotazione dei gate, la rotazione delle misure e il disaccoppiamento dinamico sono disabilitati in entrambe le versioni. Entrambi i lavori vengono eseguiti nello stesso batch e ricevono il tag “tutorial TUT_CCPBC”. Gli esperimenti di taglio richiedono comunque un numero maggiore di fotogrammi complessivi, quindi confronta sia la precisione che l'utilizzo della 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)"
)

Output:

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

Nella simulazione registrata su ibm_boston, le stime di taglio erano approssimativamente MZ=−0.923M_Z=-0.923 e CZZ=0.896C_{ZZ}=0.896, rispetto a −0.611-0.611 e 0.5160.516 per il circuito non tagliato. Entrambe le stime di taglio erano più vicine ai valori ideali di −1-1 e +1+1.

Confronta ciascun risultato con il relativo valore ideale e verifica le profondità del circuito e l'utilizzo totale della QPU. Il taglio elimina la necessità di instradare i gate periodici, ma introduce operazioni intermedie e un’ulteriore incertezza statistica. Il rumore dell'hardware e la calibrazione influenzano il confronto; non è garantito alcun miglioramento. Sono necessarie ripetute analisi per valutare se una differenza osservata sia statisticamente significativa.


Passi successivi

Suggerimenti
Questa pagina è stata utile?
Segnala un bug, un errore di battitura o richiedi contenuti su GitHub.