Taglio di fili per la stima dei valori di aspettazione
Stima di utilizzo: un minuto su un processore Eagle (NOTA: Questa è solo una stima. Il vostro tempo di esecuzione potrebbe variare.)
Contesto
Il circuit-knitting è un termine ombrello che racchiude vari metodi di partizionamento di un circuito in più sottocircuiti più piccoli che coinvolgono meno porte e/o qubit. Ognuno dei sottocircuiti può essere eseguito indipendentemente e il risultato finale si ottiene mediante un post-processing classico sull'esito di ciascun sottocircuito. Questa tecnica è accessibile nell'addon Qiskit per il taglio di circuiti, una spiegazione dettagliata della tecnica è fornita nella documentazione insieme ad altro materiale introduttivo.
Questo notebook tratta un metodo chiamato taglio di fili in cui il circuito viene partizionato lungo il filo [1], [2]. Si noti che il partizionamento è semplice nei circuiti classici poiché l'esito nel punto di partizionamento può essere determinato in modo deterministico ed è 0 oppure 1. Tuttavia, lo stato del qubit nel punto del taglio è, in generale, uno stato misto. Pertanto, ogni sottocircuito deve essere misurato più volte in basi diverse (di solito un insieme tomograficamente completo di basi come la base di Pauli [3], [4] e di conseguenza preparato nel suo autostato. La figura seguente (per gentile concessione di: Tesi di Dottorato, Ritajit Majumdar) mostra un esempio di taglio di fili per uno stato GHZ a 4 qubit in tre sottocircuiti. Qui denota un insieme di basi (di solito Pauli X, Y e Z) e denota un insieme di autostati (di solito , , e ).
Poiché ogni sottocircuito ha meno qubit e/o porte, ci si aspetta che sia meno suscettibile al rumore. Questo notebook mostra un esempio in cui questo metodo può essere utilizzato per sopprimere efficacemente il rumore nel sistema.
Requisiti
Prima di iniziare questo tutorial, assicuratevi di avere installato quanto segue:
- Qiskit SDK v2.0 o successivo, con supporto per la visualizzazione
- Qiskit Runtime v0.22 o successivo (
pip install qiskit-ibm-runtime) - Addon Qiskit per il taglio di circuiti v0.9.0 o successivo (
pip install qiskit-addon-cutting)
Considereremo un circuito di Many Body Localization (MBL) per questo notebook. Il circuito MBL è un circuito hardware-efficient ed è parametrizzato da due parametri e . Quando è impostato a e lo stato iniziale è preparato in per tutti i qubit, il valore di aspettazione ideale di è per ogni sito di qubit indipendentemente dai valori di . Potete verificare maggiori dettagli sui circuiti MBL in questo articolo.
Configurazione
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-cutting qiskit-ibm-runtime
import numpy as np
import matplotlib.pyplot as plt
from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.quantum_info import PauliList, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.result import sampled_expectation_value
from qiskit_addon_cutting.instructions import CutWire
from qiskit_addon_cutting import (
cut_wires,
expand_observables,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2, Batch
class MBLChainCircuit(QuantumCircuit):
def __init__(
self, num_qubits: int, depth: int, use_cut: bool = False
) -> None:
super().__init__(
num_qubits, name=f"MBLChainCircuit<{num_qubits}, {depth}>"
)
evolution = MBLChainEvolution(num_qubits, depth, use_cut)
self.compose(evolution, inplace=True)
class MBLChainEvolution(QuantumCircuit):
def __init__(self, num_qubits: int, depth: int, use_cut) -> None:
super().__init__(
num_qubits, name=f"MBLChainEvolution<{num_qubits}, {depth}>"
)
theta = Parameter("θ")
phis = ParameterVector("φ", num_qubits)
for layer in range(depth):
layer_parity = layer % 2
# print("layer parity", layer_parity)
for qubit in range(layer_parity, num_qubits - 1, 2):
# print(qubit)
self.cz(qubit, qubit + 1)
self.u(theta, 0, np.pi, qubit)
self.u(theta, 0, np.pi, qubit + 1)
if (
use_cut
and layer_parity == 0
and (
qubit == num_qubits // 2 - 1
or qubit == num_qubits // 2
)
):
self.append(CutWire(), [num_qubits // 2])
if use_cut and layer < depth - 1 and layer_parity == 1:
if qubit == num_qubits // 2:
self.append(CutWire(), [qubit])
for qubit in range(num_qubits):
self.p(phis[qubit], qubit)
Parte I. Esempio su piccola scala
Passo 1: Mappare gli input classici a un problema quantistico
Inizialmente costruiamo un circuito template senza alcun valore di parametro specifico. Forniamo anche dei segnaposto, chiamati CutWire, per annotare la posizione dei tagli. Per l'esempio su piccola scala consideriamo un circuito MBL a 10 qubit.
num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)
Ricordiamo che il nostro obiettivo è trovare il valore di aspettazione dell'osservabile quando . Inseriremo alcuni valori casuali per il parametro .
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
params
[0,
0.2376615174332788,
0.28244289857682414,
0.019248960591717768,
0.46140600996102477,
0.31408025180068433,
0.718184005135733,
0.991153920182475,
0.09289485768301442,
0.8857848280067783,
0.6177529765767047]
Ora annotiamo il circuito per il taglio inserendo il CutWire appropriato per creare due tagli approssimativamente uguali. Impostiamo use_cut=True nella funzione e permettiamo che annoti dopo qubit, dove è il numero di qubit nel circuito originale.
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)
Passo 2: Ottimizzare il problema per l'esecuzione sull'hardware quantistico
Successivamente tagliamo il circuito in due sottocircuiti più piccoli. Per questo esempio, ci limitiamo a soli 2 sottocircuiti. Per questo, utilizziamo l'Addon Qiskit: Circuit Cutting.
Tagliare il circuito in sottocircuiti più piccoli
Tagliare il filo in un punto aumenta il conteggio dei qubit di uno. Oltre al qubit originale, c'è ora un qubit aggiuntivo come segnaposto per il circuito dopo il taglio. L'immagine seguente fornisce una rappresentazione:
Questo Addon utilizza la funzione cut_wires per tenere conto dei qubit aggiuntivi che emergono a causa del taglio.
mbl_move = cut_wires(mbl_cut)
Creare ed espandere le osservabili
Ora costruiamo l'osservabile . Poiché l'esito ideale di per ogni è , l'esito ideale di è anch'esso .
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
observable
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])
Tuttavia, si noti che il numero di qubit nel circuito è aumentato dopo l'inserimento delle operazioni virtuali Move a 2 qubit dopo il taglio. Pertanto, dobbiamo espandere anche le osservabili inserendo identità per adattarsi al circuito corrente.
new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])
Si noti che ogni osservabile si è ora espansa per ospitare sette qubit, come nel circuito con l'operazione Move, invece dei 6 qubit originali. Successivamente, partizioniamo il circuito in due sottocircuiti.
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
Visualizziamo i sottocircuiti
subcircuits = partitioned_problem.subcircuits
subcircuits[0].draw("mpl", fold=-1)
subcircuits[1].draw("mpl", fold=-1)
Anche le osservabili sono state partizionate per adattarsi ai sottocircuiti
subobservables = partitioned_problem.subobservables
subobservables
{0: PauliList(['IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IZIIII',
'IIZIII', 'IIIZII', 'IIIIZI', 'IIIIIZ']),
1: PauliList(['ZIIII', 'IZIII', 'IIZII', 'IIIZI', 'IIIIZ', 'IIIII', 'IIIII',
'IIIII', 'IIIII', 'IIIII'])}
Si noti che ogni sottocircuito porta a un numero di campioni. La ricostruzione tiene conto dell'esito di ciascuno di questi campioni. Ognuno di questi campioni è chiamato subexperiment.
L'estensione dell'osservabile utilizzando l'operazione Move richiede una struttura dati PauliList. Possiamo anche creare l'osservabile nella struttura dati più generica SparsePauliOp che sarà utile in seguito durante la ricostruzione dei subexperiment.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
M_z
SparsePauliOp(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII', 'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII', 'IIIIIIIIZI', 'IIIIIIIIIZ'],
coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
0.1+0.j, 0.1+0.j])
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
Vediamo due esempi in cui i qubit tagliati vengono misurati in due basi diverse. Per primo, viene misurato nella normale base Z, e successivamente viene misurato nella base X.
subexperiments[0][6].draw("mpl", fold=-1)
subexperiments[0][2].draw("mpl", fold=-1)
Traspilare ogni subexperiment
Attualmente dobbiamo traspilare i nostri circuiti prima di inviarli per l'esecuzione. Pertanto, traspilaremo prima ogni circuito nei subexperiment.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
Ora dobbiamo traspilare ciascuno dei circuiti nei subexperiment. Per fare ciò creiamo prima un pass manager, e poi lo usiamo per traspilare ciascuno dei circuiti.
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
isa_subexperiments[0][0].draw("mpl", fold=-1, idle_wires=False)
Passo 3: Eseguire utilizzando le primitive Qiskit
Ora eseguiremo ogni circuito nei subexperiment. Qiskit-addon-cutting utilizza SamplerV2 per eseguire i subexperiment.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
Passo 4: Post-processare e restituire il risultato nel formato classico desiderato
Una volta che i circuiti sono stati eseguiti, dobbiamo ora recuperare i risultati e ricostruire il valore di aspettazione per il circuito non tagliato e l'osservabile originale.
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9674376845359803
Verifica incrociata
Eseguiamo ora il circuito senza tagliare e verifichiamo l'esito. Si noti che per l'esecuzione del circuito non tagliato possiamo utilizzare direttamente EstimatorV2 per calcolare i valori di aspettazione. Ma utilizzeremo la stessa Primitive per tutto. Quindi useremo SamplerV2 per ottenere la distribuzione di probabilità e calcolare il valore di aspettazione utilizzando la funzione sampled_expectation_value.
Prima dobbiamo traspilare il circuito mbl non tagliato.
sampler = SamplerV2(mode=backend)
if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)
Successivamente costruiamo il pub ed eseguiamo il circuito non tagliato.
pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9498046875000001
Si noti che il valore di aspettazione ottenuto tramite il taglio di fili è più vicino al valore ideale di rispetto a quello non tagliato. Aumentiamo ora la dimensione del problema.
Part II. Aumentare la scala!
In precedenza, abbiamo mostrato i risultati per un circuito MBL a 10 qubit. Successivamente, dimostriamo che il miglioramento nel valore di aspettazione si ottiene anche per circuiti più grandi. Per dimostrarlo, ripetiamo il processo per un circuito MBL a 60 qubit.
Passo 1: Mappare gli input classici a un problema quantistico
num_qubits = 60
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
Creiamo un insieme casuale di valori per
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
Successivamente costruiamo il circuito tagliato
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)
Passo 2: Ottimizzare il problema per l'esecuzione su hardware quantistico
Come mostrato per l'esempio su piccola scala, partizioniamo il circuito e l'osservabile per gli esperimenti di taglio.
mbl_move = cut_wires(mbl_cut)
# Define observable
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)
# Partition the circuit into subcircuits
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
# Get subcircuits
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
Creiamo anche un oggetto SparsePauliOp per l'osservabile con i coefficienti appropriati.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
Successivamente generiamo i sottoesperimenti e compiliamo ogni circuito nel sottoesperimento.
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
Passo 3: Eseguire utilizzando le primitive di Qiskit
Utilizziamo la modalità Batch per eseguire tutti i circuiti nei sottoesperimenti.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
Passo 4: Post-elaborare e restituire il risultato nel formato classico desiderato
Recuperiamo ora i risultati per ogni circuito nel sottoesperimento e ricostruiamo il valore di aspettazione corrispondente al circuito non tagliato e all'osservabile originale.
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9631355921427409
Verifica incrociata
Come nell'esempio su piccola scala, otterremo ancora una volta il valore di aspettazione eseguendo il circuito non tagliato e confronteremo il risultato con il taglio del circuito. Utilizzeremo SamplerV2 per mantenere l'uniformità nell'uso delle Primitive.
sampler = SamplerV2(mode=backend)
if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)
pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9426757812499998
Visualizzare
Visualizziamo il miglioramento ottenuto nel valore di aspettazione utilizzando il taglio dei fili.
ax = plt.gca()
methods = ["cut", "uncut"]
values = [reconstructed_expval, uncut_expval]
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
ax.set_ylim([0.85, 1.02])
plt.text(0.3, 0.99, "Exact result")
plt.show()
Inferenza
Osserviamo che sia nei problemi su piccola che su larga scala il taglio dei fili porta a un risultato migliore rispetto a quello non tagliato. Si noti che non sono state utilizzate tecniche di mitigazione degli errori per questi esperimenti. Pertanto, il miglioramento nel risultato che è stato ottenuto è dovuto solo al taglio dei fili. Potrebbe essere possibile migliorare ulteriormente i risultati utilizzando diversi metodi di mitigazione insieme al taglio dei circuiti.
Inoltre, in questo notebook, abbiamo calcolato entrambi i sottocircuiti sullo stesso hardware. In [5], [6], gli autori mostrano un metodo per distribuire i sottocircuiti su hardware diversi utilizzando informazioni sul rumore al fine di massimizzare la soppressione del rumore e parallelizzare il processo.
Appendice: considerazioni sulla scalabilità delle risorse
Il numero di circuiti da eseguire aumenta con il numero di tagli. Pertanto, sebbene molti tagli possano produrre sottocircuiti piccoli, migliorando così ulteriormente le prestazioni, ciò porta anche a un numero significativamente elevato di esecuzioni di circuiti, che potrebbero non essere pratiche per la maggior parte dei casi. Di seguito, mostriamo un esempio del numero di sottocircuiti corrispondente al numero di tagli per un circuito a 50 qubit.
Si noti che anche per cinque tagli, il numero di sottoesperimenti è di circa 200k. Pertanto, il taglio dei circuiti dovrebbe essere utilizzato solo quando il numero di tagli è piccolo.
Un esempio di circuito adatto e non adatto al taglio
Circuito adatto al taglio
Come notato in precedenza, un circuito è adatto al taglio quando il circuito può essere partizionato in sottocircuiti disgiunti più piccoli con un numero ridotto di tagli. Qualsiasi circuito hardware-efficient, ovvero un circuito che richiede poche o nessuna porta SWAP quando mappato alla mappa di accoppiamento dell'hardware, è, in generale, adatto al taglio. Di seguito, mostriamo un esempio di ansatz che preserva l'eccitazione, utilizzato in Chimica Quantistica. Si noti che un tale circuito può essere partizionato in due sottocircuiti con un singolo taglio indipendentemente dal numero di qubit.

Circuito non adatto al taglio
Un circuito non è adatto al taglio se, in generale, il numero di tagli richiesti per formare partizioni disgiunte cresce significativamente con la profondità del numero di qubit. Ricordate che con ogni taglio è richiesto un qubit extra. Quindi, con il numero di tagli, anche il numero effettivo di qubit aumenta. Di seguito mostriamo un esempio di un circuito di Grover a 3 qubit con una possibile istanza di taglio.
Si noti che sono richiesti tre tagli, e il taglio è più verticale che orizzontale. Ciò significa che il numero di tagli dovrebbe scalare linearmente con il numero di qubit, il che non è adatto al taglio.
Riferimenti
[1] Peng, T., Harrow, A. W., Ozols, M., & Wu, X. (2020). Simulating large quantum circuits on a small quantum computer. Physical review letters, 125(15), 150504.
[2] Tang, W., Tomesh, T., Suchara, M., Larson, J., & Martonosi, M. (2021, April). Cutqc: using small quantum computers for large quantum circuit evaluations. In Proceedings of the 26th ACM International conference on architectural support for programming languages and operating systems (pp. 473-486).
[3] Perlin, M. A., Saleem, Z. H., Suchara, M., & Osborn, J. C. (2021). Quantum circuit cutting with maximum-likelihood tomography. npj Quantum Information, 7(1), 64.
[4] Majumdar, R., & Wood, C. J. (2022). Error mitigated quantum circuit cutting. arXiv preprint arXiv:2211.13431.
[5] Khare, T., Majumdar, R., Sangle, R., Ray, A., Seshadri, P. V., & Simmhan, Y. (2023). Parallelizing Quantum-Classical Workloads: Profiling the Impact of Splitting Techniques. In 2023 IEEE International Conference on Quantum Computing and Engineering (QCE) (Vol. 1, pp. 990-1000). IEEE.
[6] Bhoumik, D., Majumdar, R., Saha, A., & Sur-Kolay, S. (2023). Distributed Scheduling of Quantum Circuits with Noise and Time Optimization. arXiv preprint arXiv:2309.06005.
Sondaggio del tutorial
Vi preghiamo di rispondere a questo breve sondaggio per fornire un feedback su questo tutorial. Le vostre opinioni ci aiuteranno a migliorare la nostra offerta di contenuti e l'esperienza utente.