Primi passi con il circuit cutting tramite gate cut
Versioni dei pacchetti
Il codice in questa pagina è stato sviluppato utilizzando i seguenti requisiti. Si consiglia di usare queste versioni o versioni più recenti.
qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-aer~=0.17
qiskit-addon-cutting~=0.10.0
Questa guida illustra due esempi pratici di gate cut con il pacchetto qiskit-addon-cutting. Il primo esempio mostra come ridurre la profondità del circuito (il numero di istruzioni del circuito) tagliando i gate di entanglement su qubit non adiacenti, che altrimenti richiederebbero un overhead di SWAP in fase di transpilazione verso l'hardware. Il secondo esempio mostra come usare il gate cutting per ridurre la larghezza del circuito (il numero di qubit), suddividendo un circuito in più circuiti con un numero inferiore di qubit.
Entrambi gli esempi usano l'ansatz efficient_su2 e ricostruiscono lo stesso osservabile.
gate cutting per ridurre la profondità del circuito​
Il seguente flusso di lavoro riduce la profondità di un circuito tagliando i gate distanti, evitando così una lunga serie di gate SWAP che altrimenti verrebbero introdotti.
Inizia con l'ansatz efficient_su2, usando l'entanglement "circular" per introdurre gate distanti.
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
import numpy as np
from qiskit.circuit.library import efficient_su2
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
from qiskit_ibm_runtime import SamplerV2, Batch
from qiskit_aer.primitives import EstimatorV2
from qiskit_addon_cutting import (
cut_gates,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)
circuit = efficient_su2(num_qubits=4, entanglement="circular")
circuit.assign_parameters([0.4] * len(circuit.parameters), inplace=True)
observable = SparsePauliOp(["ZZII", "IZZI", "-IIZZ", "XIXI", "ZIZZ", "IXIX"])
print(f"Observable: {observable}")
circuit.draw("mpl", scale=0.8)
Observable: SparsePauliOp(['ZZII', 'IZZI', 'IIZZ', 'XIXI', 'ZIZZ', 'IXIX'],
coeffs=[ 1.+0.j, 1.+0.j, -1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])
Ciascuno dei gate CNOT tra i qubit e introduce due gate SWAP dopo la transpilazione (supponendo che i qubit siano collegati in linea retta). Per evitare questo aumento di profondità , puoi sostituire questi gate distanti con oggetti TwoQubitQPDGate usando il metodo cut_gates(). Questa funzione restituisce anche una lista di istanze QPDBasis — una per ogni decomposizione.
# Find the indices of the distant gates
cut_indices = [
i
for i, instruction in enumerate(circuit.data)
if {circuit.find_bit(q)[0] for q in instruction.qubits} == {0, 3}
]
# Decompose distant CNOTs into TwoQubitQPDGate instances
qpd_circuit, bases = cut_gates(circuit, cut_indices)
qpd_circuit.draw("mpl", scale=0.8)
Ora che le istruzioni di gate cut sono state aggiunte, i sotto-esperimenti avranno una profondità inferiore dopo la transpilazione rispetto al circuito originale. Lo snippet di codice qui sotto genera i sotto-esperimenti usando generate_cutting_experiments, che riceve in input il circuito e l'osservabile da ricostruire.
L'argomento num_samples specifica quanti campioni estrarre dalla distribuzione di quasi-probabilità e determina la precisione dei coefficienti usati per la ricostruzione. Passare infinito (np.inf) garantisce che tutti i coefficienti vengano calcolati esattamente. Consulta i documenti API su generazione dei pesi e generazione degli esperimenti di cutting per maggiori informazioni.
Una volta generati i sotto-esperimenti, puoi traspilarli e usare la primitiva Sampler per campionare la distribuzione e ricostruire i valori di aspettazione stimati. Il blocco di codice seguente genera, transpila ed esegue i sotto-esperimenti, poi ricostruisce i risultati e li confronta con il valore di aspettazione esatto.
# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observable.paulis, num_samples=np.inf
)
# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend
)
isa_subexperiments = pass_manager.run(subexperiments)
# Set up the Qiskit Runtime Sampler primitive, submit the subexperiments, and retrieve the results
sampler = SamplerV2(backend)
job = sampler.run(isa_subexperiments, shots=4096 * 3)
results = job.result()
# Reconstruct the results
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
observable.paulis,
)
# Apply the coefficients of the original observable
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
estimator = EstimatorV2()
exact_expval = (
estimator.run([(circuit, observable, [0.4] * len(circuit.parameters))])
.result()[0]
.data.evs
)
print(
f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}"
)
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(
f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}"
)
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 0.49812826
Exact expectation value: 0.50497603
Error in estimation: -0.00684778
Relative error in estimation: -0.0135606
Per ricostruire accuratamente il valore di aspettazione, i coefficienti dell'osservabile originale (che sono diversi dai coefficienti nell'output di generate_cutting_experiments()) devono essere applicati all'output della ricostruzione, poiché questa informazione viene persa durante la generazione degli esperimenti di cutting o quando l'osservabile viene espanso.
In genere questi coefficienti possono essere applicati tramite numpy.dot() come mostrato sopra.
gate cutting per ridurre la larghezza del circuito​
Questa sezione mostra come usare il gate cutting per ridurre la larghezza del circuito. Inizia con lo stesso ansatz efficient_su2, ma usando l'entanglement "linear".
qc = efficient_su2(4, entanglement="linear", reps=2)
qc.assign_parameters([0.4] * len(qc.parameters), inplace=True)
observable = SparsePauliOp(["ZZII", "IZZI", "-IIZZ", "XIXI", "ZIZZ", "IXIX"])
print(f"Observable: {observable}")
qc.draw("mpl", scale=0.8)
Observable: SparsePauliOp(['ZZII', 'IZZI', 'IIZZ', 'XIXI', 'ZIZZ', 'IXIX'],
coeffs=[ 1.+0.j, 1.+0.j, -1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])
Genera quindi i sottocircuiti e i sotto-osservabili da eseguire usando la funzione partition_problem(). Questa funzione riceve in input il circuito, l'osservabile e uno schema di partizionamento opzionale, e restituisce i circuiti tagliati e gli osservabili sotto forma di dizionario.
Il partizionamento è definito da una stringa di etichette nella forma "AABB", dove ogni etichetta corrisponde al qubit nello stesso indice dell'argomento circuit. I qubit che condividono la stessa etichetta di partizione vengono raggruppati insieme, e tutti i gate non locali che attraversano più di una partizione verranno tagliati.
Il kwarg observables di partition_problem è di tipo PauliList. I coefficienti e le fasi dei termini dell'osservabile vengono ignorati durante la decomposizione del problema e l'esecuzione dei sotto-esperimenti. Possono essere riapplicati durante la ricostruzione del valore di aspettazione.
partitioned_problem = partition_problem(
circuit=qc, partition_labels="AABB", observables=observable.paulis
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
print(f"Subobservables: {subobservables}")
subcircuits["A"].draw("mpl", scale=0.8)
Sampling overhead: 81.0
Subobservables: {'A': PauliList(['II', 'ZI', 'ZZ', 'XI', 'ZZ', 'IX']), 'B': PauliList(['ZZ', 'IZ', 'II', 'XI', 'ZI', 'IX'])}
subcircuits["B"].draw("mpl", scale=0.8)
Il passo successivo consiste nell'usare i sottocircuiti e i sotto-osservabili per generare i sotto-esperimenti da eseguire su un QPU tramite il metodo generate_cutting_experiments.
Per stimare il valore di aspettazione del circuito completo, molti sotto-esperimenti vengono generati dalla distribuzione di quasi-probabilità congiunta dei gate decomposti e poi eseguiti su uno o più QPU. Il numero di campioni da estrarre da questa distribuzione è controllato dall'argomento num_samples.
Il blocco di codice seguente genera i sotto-esperimenti e li esegue usando la primitiva Sampler su un simulatore locale. (Per eseguirli su un QPU, modifica il backend scegliendo la risorsa QPU desiderata.)
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)
# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend
)
isa_subexperiments = {
label: pass_manager.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
# Submit each partition's subexperiments to the Qiskit Runtime Sampler
# primitive, in a single batch so that the jobs will run back-to-back.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=4096 * 3)
for label, subsystem_subexpts in isa_subexperiments.items()
}
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
Infine, il valore di aspettazione del circuito completo viene ricostruito usando il metodo reconstruct_expectation_values.
Il blocco di codice seguente ricostruisce i risultati e li confronta con il valore di aspettazione esatto.
# Get expectation values for each observable term
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
# Reconstruct final expectation value
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
estimator = EstimatorV2()
exact_expval = (
estimator.run([(qc, observable, [0.4] * len(qc.parameters))])
.result()[0]
.data.evs
)
print(
f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}"
)
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(
f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}"
)
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 0.53571896
Exact expectation value: 0.56254612
Error in estimation: -0.02682716
Relative error in estimation: -0.04768882
Passi successivi​
- Leggi la guida Primi passi con il circuit cutting tramite wire cut.
- Leggi il paper arXiv sul circuit knitting con comunicazione classica.