Sari la conținutul principal

Tăierea firelor pentru estimarea valorilor de așteptare

Estimare de utilizare: un minut pe un procesor Eagle (NOTĂ: Aceasta este doar o estimare. Timpul tău de execuție poate varia.)

Context

Circuit-knitting este un termen general care înglobează diverse metode de partiționare a unui circuit în mai multe subcircuite mai mici, implicând mai puține porți și/sau qubiți. Fiecare dintre subcircuite poate fi executat independent, iar rezultatul final se obține prin procesare clasică ulterioară a rezultatelor fiecărui subcircuit. Această tehnică este accesibilă prin addon-ul Qiskit pentru tăierea circuitelor; o explicație detaliată a tehnicii se găsește în documentație, alături de alt material introductiv.

Acest notebook se ocupă de o metodă numită tăierea firelor, în care circuitul este partiționat de-a lungul firului [1], [2]. De remarcat că partiționarea este simplă în circuitele clasice, deoarece rezultatul la punctul de partiționare poate fi determinat determinist și este fie 0, fie 1. Cu toate acestea, starea qubitului la punctul de tăiere este, în general, o stare mixtă. Prin urmare, fiecare subcircuit trebuie măsurat de mai multe ori în baze diferite (de obicei un set tomografic complet de baze, cum ar fi baza Pauli [3], [4]), și pregătit corespunzător în propria sa stare proprie. Figura de mai jos (cu amabilitatea: Teză de doctorat, Ritajit Majumdar) arată un exemplu de tăiere a firelor pentru o stare GHZ cu 4 qubiți în trei subcircuite. Aici MjM_j denotă un set de baze (de obicei Pauli X, Y și Z), iar PiP_i denotă un set de stări proprii (de obicei 0|0\rangle, 1|1\rangle, +|+\rangle și +i|+i\rangle).

wc-1.png wc-2.png

Deoarece fiecare subcircuit are mai puțini qubiți și/sau porți, este de așteptat ca acestea să fie mai puțin susceptibile la zgomot. Acest notebook arată un exemplu în care această metodă poate fi utilizată pentru a suprima eficient zgomotul din sistem.

Cerințe

Înainte de a începe acest tutorial, asigură-te că ai instalate următoarele:

  • Qiskit SDK v2.0 sau mai recent, cu suport pentru vizualizare
  • Qiskit Runtime v0.22 sau mai recent ( pip install qiskit-ibm-runtime )
  • Addon-ul Qiskit pentru tăierea circuitelor v0.9.0 sau mai recent (pip install qiskit-addon-cutting)

Vom considera un circuit Many Body Localization (MBL) pentru acest notebook. Circuitul MBL este un circuit eficient pentru hardware și este parametrizat de doi parametri θ\theta și ϕ\vec{\phi}. Când θ\theta este setat la 00 și starea inițială este pregătită în 0|0\rangle pentru toți qubiții, valoarea de așteptare ideală a Zi\langle Z_i \rangle este +1+1 pentru fiecare site de qubit ii, indiferent de valorile lui ϕ\vec{\phi}. Poți găsi mai multe detalii despre circuitele MBL în această lucrare.

Configurare

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

Partea I. Exemplu la scară mică

Pasul 1: Maparea intrărilor clasice la o problemă cuantică

Inițial construim un circuit șablon fără valori specifice ale parametrilor. Furnizăm, de asemenea, substituenți, numiți CutWire, pentru a adnota pozițiile tăieturilor. Pentru exemplul la scară mică considerăm un circuit MBL cu 10 qubiți.

num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)

Output of the previous code cell

Reamintim că scopul nostru este să găsim valoarea de așteptare a observabilului 1ni=1nZi\frac{1}{n}\sum_{i=1} ^n Z_i când θ=0\theta=0. Vom pune valori aleatorii pentru parametrul ϕ\vec{\phi}.

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]

Acum adnotăm circuitul pentru tăiere inserând CutWire-uri corespunzătoare pentru a crea două tăieturi aproximativ egale. Setăm use_cut=True în funcție și permitem adnotarea după n2\frac{n}{2} qubiți, unde nn este numărul de qubiți din circuitul original.

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

Pasul 2: Optimizarea problemei pentru execuția pe hardware cuantic

În continuare tăiem circuitul în două subcircuite mai mici. Pentru acest exemplu, ne limităm la doar 2 subcircuite. Pentru aceasta, folosim Addon-ul Qiskit: Tăierea Circuitelor.

Tăierea circuitului în subcircuite mai mici

Tăierea firului la un punct crește numărul de qubiți cu unu. Pe lângă qubitul original, există acum un qubit suplimentar ca substituent în circuitul de după tăiere. Imaginea următoare oferă o reprezentare:

wc-4.png

Acest Addon folosește funcția cut_wires pentru a ține cont de qubiții suplimentari apăruți în urma tăierii.

mbl_move = cut_wires(mbl_cut)

Crearea și extinderea observabililor

Acum construim observabilul Mz=1ni=1nZiM_z = \frac{1}{n}\sum_{i=1}^n \langle Z_i \rangle. Deoarece rezultatul ideal al Zi\langle Z_i \rangle pentru fiecare ii este +1+1, rezultatul ideal al lui MzM_z este, de asemenea, +1+1.

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

Cu toate acestea, de remarcat că numărul de qubiți din circuit a crescut după inserarea operațiilor virtuale Move cu 2 qubiți în urma tăierii. Prin urmare, trebuie să extindem și observabilii prin inserarea de identități pentru a corespunde circuitului curent.

new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])

De remarcat că fiecare observabil s-a extins acum pentru a acomoda șapte qubiți, ca în circuitul cu operația Move, în loc de cei 6 qubiți originali. În continuare, partiționăm circuitul în două subcircuite.

partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)

Să vizualizăm subcircuitele

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

Output of the previous code cell

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

Output of the previous code cell

Observabilii au fost și ei partiționați pentru a se potrivi subcircuitelor

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

De remarcat că fiecare subcircuit generează un număr de eșantioane. Reconstrucția ia în considerare rezultatul fiecăruia dintre aceste eșantioane. Fiecare dintre aceste eșantioane este denumit un subexperiment. Extinderea observabilului folosind operația Move necesită o structură de date PauliList. Putem crea de asemenea observabilul MzM_z în structura de date mai generică SparsePauliOp, care va fi utilă ulterior la reconstrucția subexperimentelor.

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

Să vedem două exemple în care qubiții tăiați sunt măsurați în două baze diferite. Mai întâi, este măsurat în baza normală Z, iar apoi în baza X.

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

Output of the previous code cell

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

Output of the previous code cell

Transpilarea fiecărui subexperiment

În prezent trebuie să transpilăm circuitele noastre înainte de a le trimite spre execuție. Prin urmare, vom transpila mai întâi fiecare circuit din subexperimente.

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

Acum trebuie să transpilăm fiecare dintre circuitele din subexperimente. Pentru aceasta, creăm mai întâi un manager de pași, și îl folosim apoi pentru a transpila fiecare circuit.

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)

Output of the previous code cell

Pasul 3: Execuția folosind primitivele Qiskit

Acum vom executa fiecare circuit din subexperiment. Qiskit-addon-cutting folosește SamplerV2 pentru a executa subexperimentele.

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

Pasul 4: Post-procesarea și returnarea rezultatului în formatul clasic dorit

După ce circuitele au fost executate, trebuie acum să recuperăm rezultatele și să reconstruim valoarea de așteptare pentru circuitul netăiat și observabilul original.

# 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

Verificare încrucișată

Să executăm acum circuitul fără tăiere și să verificăm rezultatul obținut. De remarcat că pentru executarea circuitului netăiat putem folosi direct EstimatorV2 pentru calcularea valorilor de așteptare. Dar vom folosi același Primitive pe tot parcursul. Deci vom folosi SamplerV2 pentru a obține distribuția de probabilitate și vom calcula valoarea de așteptare folosind funcția sampled_expectation_value.

Mai întâi trebuie să transpilăm circuitul mbl netăiat.

sampler = SamplerV2(mode=backend)

if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)

Apoi construim pub-ul și rulăm circuitul netăiat.

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

Observăm că valoarea de așteptare obținută prin tăierea firelor este mai aproape de valoarea ideală de +1+1 decât cea a circuitului netăiat. Să extindem acum dimensiunea problemei.

Partea a II-a. Mărește scara!

Anterior, am arătat rezultatele pentru un circuit MBL de 10 qubiți. În continuare, demonstrăm că îmbunătățirea valorii de așteptare se obține și pentru circuite mai mari. Pentru aceasta, repetăm procesul pentru un circuit MBL de 60 de qubiți.

Pasul 1: Mapează intrările clasice la o problemă cuantică

num_qubits = 60
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)

Creăm un set aleatoriu de valori pentru ϕ\vec{\phi}

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

Apoi construim circuitul tăiat

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

Pasul 2: Optimizează problema pentru execuția pe hardware cuantic

Așa cum am arătat în exemplul la scară mică, partitionăm circuitul și observabilul pentru experimentele de tăiere.

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

De asemenea, creăm un obiect SparsePauliOp pentru observabil cu coeficienții corespunzători.

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

Apoi generăm subexperimentele și transpilăm fiecare circuit din subexperiment.

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

Pasul 3: Execută folosind primitivele Qiskit

Folosim modul Batch pentru a executa toate circuitele din subexperimente.

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

Pasul 4: Post-procesează și returnează rezultatul în formatul clasic dorit

Să preluăm acum rezultatele pentru fiecare circuit din subexperiment și să reconstruim valoarea de așteptare corespunzătoare circuitului netăiat și observabilului original.

# 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

Verificare încrucișată

Ca și în exemplul la scară mică, vom obține din nou valoarea de așteptare prin executarea circuitului netăiat și vom compara rezultatul cu tăierea de circuite. Vom folosi SamplerV2 pentru a menține uniformitatea în utilizarea primitivelor.

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

Vizualizare

Să vizualizăm îmbunătățirea obținută în valoarea de așteptare prin utilizarea tăierii de fire.

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

Output of the previous code cell

Concluzie

Observăm că atât în problemele la scară mică, cât și la scară mare, tăierea de fire conduce la un rezultat mai bun decât circuitul netăiat. Rețineți că nu au fost utilizate tehnici de atenuare a erorilor pentru aceste experimente. Prin urmare, îmbunătățirea rezultatelor obținute se datorează exclusiv tăierii de fire. Este posibil ca rezultatele să poată fi îmbunătățite și mai mult prin combinarea diferitelor metode de atenuare cu tăierea de circuite.

Mai mult, în acest notebook, am calculat ambele subcircuite pe același hardware. În [5], [6], autorii prezintă o metodă de distribuire a subcircuitelor pe hardware diferit, folosind informații despre zgomot, pentru a maximiza suprimarea zgomotului și a paraleliza procesul.

Anexă: considerații privind scalarea resurselor

Numărul de circuite care trebuie executate crește odată cu numărul de tăieri. Prin urmare, deși mai multe tăieri pot produce subcircuite mai mici, îmbunătățind astfel performanța, ele duc și la un număr semnificativ mai mare de execuții de circuite, ceea ce nu este practic în majoritatea cazurilor. Mai jos, prezentăm un exemplu al numărului de subcircuite corespunzător numărului de tăieri pentru un circuit de 50 de qubiți.

wc-5.png

Rețineți că și pentru cinci tăieri, numărul de subexperimente este de aproximativ 200k. Prin urmare, tăierea de circuite ar trebui utilizată doar când numărul de tăieri este mic.

Un exemplu de circuit favorabil tăierii și unul nefavorabil tăierii

Circuit favorabil tăierii

Așa cum am menționat anterior, un circuit este favorabil tăierii atunci când poate fi partitionat în subcircuite disjuncte mai mici cu un număr mic de tăieri. Orice circuit eficient din punct de vedere hardware, adică un circuit care necesită puține sau deloc porți SWAP la maparea pe harta de cuplare a hardware-ului, este, în general, favorabil tăierii. Mai jos, prezentăm un exemplu de ansatz cu păstrarea excitației, utilizat în chimia cuantică. Rețineți că un astfel de circuit poate fi partitionat în două subcircuite cu o singură tăiere, indiferent de numărul de qubiți.

wc-6.png

Circuit nefavorabil tăierii

Un circuit este nefavorabil tăierii dacă, în general, numărul de tăieri necesare pentru a forma partiții disjuncte crește semnificativ odată cu adâncimea sau numărul de qubiți. Rețineți că la fiecare tăiere este necesar un qubit suplimentar. Prin urmare, odată cu numărul de tăieri, crește și numărul efectiv de qubiți. Mai jos, prezentăm un exemplu de circuit Grover cu 3 qubiți și o posibilă instanță de tăiere.

wc-7.png

Observăm că sunt necesare trei tăieri, iar tăierea este mai degrabă verticală decât orizontală. Aceasta înseamnă că numărul de tăieri se așteaptă să crească liniar cu numărul de qubiți, ceea ce nu este favorabil pentru tăiere.

Referințe

[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.

Sondaj tutorial

Te rog să completezi acest scurt sondaj pentru a oferi feedback despre acest tutorial. Ideile tale ne vor ajuta să îmbunătățim conținutul și experiența utilizatorilor.

Link către sondaj

Note: This survey is provided by IBM Quantum and relates to the original English content. To give feedback on doQumentation's website, translations, or code execution, please open a GitHub issue.