Vai al contenuto principale

Wire cutting per la stima dei valori di aspettazione

Stima di utilizzo: 22 secondi su un processore Heron (NOTA: questa è solo una stima. Il tempo di esecuzione effettivo può variare.)

Obiettivi di apprendimento

Dopo aver completato questo tutorial, gli utenti dovrebbero capire:

  • Come usare qiskit-addon-cutting per suddividere un circuito grande in sottocircuiti più piccoli, riducendo così l'effetto del rumore

Prerequisiti

Si suggerisce agli utenti di avere familiarità con il seguente argomento prima di procedere con questo tutorial:

  • Utilizzo della primitiva Sampler, che viene usata in questo flusso di lavoro

Contesto

Il circuit-knitting è un termine ombrello che racchiude vari metodi per suddividere un circuito in più sottocircuiti più piccoli che coinvolgono un numero inferiore di gate o qubit. Ciascuno dei sottocircuiti può essere eseguito in modo indipendente, e il risultato finale si ottiene tramite una post-elaborazione classica sull'esito di ciascun sottocircuito. Questa tecnica è accessibile nell'addon Qiskit per il circuit cutting; consulta la documentazione insieme ad altro materiale introduttivo per una spiegazione dettagliata della tecnica.

Questo tutorial si concentra su un metodo chiamato wire cutting, in cui il circuito viene suddiviso lungo il filo [1], [2]. Nota che la suddivisione è semplice nei circuiti classici poiché l'esito nel punto di suddivisione può essere determinato deterministicamente ed è 0 o 1. Tuttavia, lo stato del qubit nel punto di taglio è, in generale, uno stato misto. Pertanto, ciascun sottocircuito deve essere misurato più volte in basi diverse (di solito una base tomograficamente completa, come la base di Pauli [3], [4]) e preparato di conseguenza nel suo autostato. La figura seguente (per gentile concessione di: [7]) mostra un esempio di wire cutting per uno stato GHZ a quattro qubit in tre sottocircuiti. Qui MjM_j denota un insieme di basi (di solito Pauli X, Y e Z), e PiP_i denota un insieme di autostati (di solito 0|0\rangle, 1|1\rangle, +|+\rangle e +i|+i\rangle).

wc-1.png wc-2.png

Poiché ciascun sottocircuito ha meno qubit e gate, ci si aspetta che siano meno soggetti al rumore. Questo tutorial mostra un esempio in cui questo metodo può essere utilizzato per sopprimere efficacemente il rumore nel sistema.

Requisiti

Prima di iniziare questo tutorial, assicurati di aver 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 circuit cutting v0.10.0 o successivo (pip install qiskit-addon-cutting)
  • Qiskit addon utils 0.3 o successivo (pip install qiskit-addon-utils)
  • Qiskit Aer (pip install qiskit-aer )

Configurazione

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-cutting qiskit-aer 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_aer import AerSimulator
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

Esempio su simulatore su piccola scala

Questo tutorial implementa un pattern Qiskit per simulare un circuito di Many-Body Localization (MBL) unidimensionale (1D). Il circuito MBL è un circuito hardware-efficient ed è parametrizzato da due parametri θ\theta e ϕ\vec{\phi}. Quando θ\theta è impostato a 00 e lo stato iniziale è preparato in 0|0\rangle per tutti i qubit, il valore di aspettazione ideale di Zi\langle Z_i \rangle è +1+1 per ogni sito di qubit ii indipendentemente dai valori di ϕ\vec{\phi}. Ulteriori dettagli su questo circuito sono disponibili in questo articolo.

Nota che in un simulatore senza rumore, il valore di aspettazione ottenuto con e senza circuit cutting sarà lo stesso.

Passo 1: Mappa gli input classici in un problema quantistico

Costruisci il circuito MBL 1D

Per prima cosa, presentiamo una funzione per costruire il circuito MBL 1D.

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)
num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)

Output of the previous code cell

Calcoliamo il valore di aspettazione medio O=1niZiO = \frac{1}{n} \sum_i Z_i su tutti i qubit per θ=0\theta = 0. Poiché il valore di aspettazione ideale di Zi=1\langle Z_i \rangle = 1 \forall ii, anche il valore di aspettazione ideale di OO è 11. I parametri ϕ\phi sono selezionati in modo casuale.

np.random.seed(42)
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis

Il circuito deve essere annotato inserendo CutWire nelle posizioni desiderate per suddividerlo. Per questo tutorial, optiamo per una suddivisione uguale. Il circuito MBL è progettato in modo che impostare use_cut=True nella funzione inserisca correttamente l'annotazione dopo n2\frac{n}{2} qubit, dove nn è il numero di qubit nel circuito originale. Abbiamo anche assegnato i parametri generati casualmente al circuito.

mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)

Output of the previous code cell

Passo 2: Ottimizza il problema per l'esecuzione su hardware quantistico

Taglia il circuito in sottocircuiti più piccoli

Ora suddividiamo il circuito in due sottocircuiti più piccoli usando qiskit-addon-cutting. qiskit-addon-cutting aggiunge un gate Move virtuale per dividere la posizione del wire cut regolando opportunamente il numero di qubit. Ora creiamo il circuito con questo gate virtuale. Poiché c'è un solo wire cut, il numero di qubit associati verrà aumentato di 1.

mbl_move = cut_wires(mbl_cut)
mbl_move.draw("mpl", fold=-1)

Output of the previous code cell

Costruisci ed espandi l'osservabile

L'osservabile, come definito prima, sarà la media di ZZ su ciascun qubit. Tuttavia, dopo aver inserito il gate Move virtuale, il numero effettivo di qubit nel circuito aumenta. L'osservabile deve essere espanso di conseguenza per tenere conto di questa variazione nel numero di qubit. Nota che l'osservabile agisce sempre in modo banale (come II) sul qubit extra aggiunto per il gate Move virtuale.

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'])
new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])

Ora il circuito può essere suddiviso lungo il gate Move e si ottengono i sottocircuiti, nonché il sotto-osservabile, che è la porzione dell'osservabile originale associata a ciascun sottocircuito.

partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables

Qui visualizziamo i due sottocircuiti:

subcircuits[0].draw("mpl", fold=-1)

Output of the previous code cell

subcircuits[1].draw("mpl", fold=-1)

Output of the previous code cell

Per espandere l'osservabile usando l'operazione Move è necessaria una struttura dati PauliList. Per ricostruire il valore di aspettazione del circuito originale, abbiamo bisogno dell'osservabile nel formato SparsePauliOp.

M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)

Come discusso in precedenza, per ogni taglio il circuito a monte deve essere misurato in una base di Pauli, e il circuito a valle deve essere preparato nell'autostato della base. La funzione generate_cutting_experiments crea tutti questi circuiti necessari e i coefficienti associati a ciascun circuito necessari per la ricostruzione. Ulteriori dettagli in questo articolo.

subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)

Transpila i circuiti sul backend

Per il primo esempio che coinvolge solo la simulazione, transpiliamo il circuito nel set di gate di base del backend:

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)

print(backend)
<IBMBackend('ibm_fez')>

Passo 3: Esegui usando le primitive Qiskit

Ora, esegui ciascun sotto-esperimento:

pm_basis = generate_preset_pass_manager(
optimization_level=2, basis_gates=backend.configuration().basis_gates
)
basis_subexperiments = {
label: pm_basis.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
sampler = SamplerV2(mode=AerSimulator())
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in basis_subexperiments.items()
}

Passo 4: Post-elabora e restituisci il risultato nel formato classico desiderato

Ora recuperiamo il risultato di ciascun sotto-esperimento eseguito e ricostruiamo il valore di aspettazione del circuito non tagliato:

# 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
np.float64(0.9953821063041687)
methods = [
"Uncut",
"Wire cut",
]
values = [
1,
reconstructed_expval,
] # since the ideal expectation value in noiseless simulation is +1

ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
ax.set_ylabel(r"$M_Z$", fontsize=12)
Text(0, 0.5, '$M_Z$')

Output of the previous code cell

Esempio su hardware su larga scala

Ora dimostriamo il wire cutting per un circuito MBL a 60 qubit. Sia il circuito non tagliato che quello tagliato verranno eseguiti su hardware IBM Quantum®:

num_qubits = 60
depth = 2

# construct the circuit
mbl = MBLChainCircuit(num_qubits, depth)

# create parameters
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis

# construct the cut circuit
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_move = cut_wires(mbl_cut)

# Define observable and expand to account for the wire cut
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)

# Construct a SparsePauliOp version of the observable for later use in reconstruction
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)

# Partition the circuit and get subcircuits and subobservables
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables

# Obtain subexperiments and coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)

# Transpile the subexperiments to the backend
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}

# Execute the subexperiments and retrieve results
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
sampler.options.environment.job_tags = ["TUT_WC"]
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
results = {label: job.result() for label, job in jobs.items()}

# Reconstruct the expectation value of the original observable
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real

# Compute the uncut circuit to obtain the noisy expectation value for comparison
sampler = SamplerV2(mode=backend)
sampler.options.environment.job_tags = ["TUT_WC"]

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)

# visualize the results
ax = plt.gca()
methods = ["uncut", "cut"]
values = [uncut_expval, reconstructed_expval]

plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
plt.text(0.3, 0.95, "Exact result")
plt.show()

Output of the previous code cell

uncut_expval
0.9202473958333336

Passi successivi

Raccomandazioni

Se hai trovato questo lavoro interessante, potresti essere interessato al seguente materiale:

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.

[7] Majumdar, R. (2024). Efficient Reduction of Resources and Noise in Discrete Quantum Computing Circuits (Doctoral dissertation, Indian Statistical Institute - Kolkata). https://www.proquest.com/openview/b481def90b1cc80e6b58a77c99e8385c/1?pq-origsite=gscholar&cbl=2026366&diss=y