Sari la conținutul principal

Combină opțiunile de atenuare a erorilor cu primitiva Estimator

Estimare de utilizare: Șapte minute pe un procesor Heron r2 (NOTĂ: Aceasta este doar o estimare. Timpul tău de execuție poate varia.)

Background

Acest ghid explorează opțiunile de suprimare și atenuare a erorilor disponibile cu primitiva Estimator din Qiskit Runtime. Vei construi un Circuit și un observabil și vei trimite joburi folosind primitiva Estimator cu diferite combinații de setări de atenuare a erorilor. Apoi, vei reprezenta grafic rezultatele pentru a observa efectele diferitelor setări. Majoritatea exemplelor folosesc un circuit de 10 qubiți pentru a facilita vizualizările, iar la final poți scala fluxul de lucru la 50 de qubiți.

Acestea sunt opțiunile de suprimare și atenuare a erorilor pe care le vei folosi:

  • Decuplare dinamică
  • Atenuarea erorilor de măsurare
  • Twirling de Gate-uri
  • Extrapolarea la zgomot zero (ZNE)

Cerințe

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

  • Qiskit SDK v2.1 sau mai recent, cu suport pentru vizualizare
  • Qiskit Runtime v0.40 sau mai recent (pip install qiskit-ibm-runtime)

Configurare

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
import matplotlib.pyplot as plt
import numpy as np

from qiskit.circuit.library import efficient_su2, unitary_overlap
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Batch, EstimatorV2 as Estimator

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

Acest ghid presupune că problema clasică a fost deja mapată la cuantic. Începe prin a construi un Circuit și un observabil de măsurat. Deși tehnicile folosite aici se aplică la multe tipuri diferite de circuite, pentru simplitate acest ghid folosește circuitul efficient_su2 inclus în biblioteca de circuite Qiskit.

efficient_su2 este un Circuit cuantic parametrizat proiectat să fie executat eficient pe hardware cuantic cu conectivitate limitată între qubiți, fiind totuși suficient de expresiv pentru a rezolva probleme în domenii de aplicație precum optimizarea și chimia. Este construit prin alternarea straturilor de Gate-uri cu un singur Qubit parametrizate cu un strat ce conține un model fix de Gate-uri cu doi qubiți, pentru un număr ales de repetiții. Modelul de Gate-uri cu doi qubiți poate fi specificat de utilizator. Aici poți folosi modelul integrat pairwise deoarece minimizează adâncimea circuitului prin împachetarea cât mai densă a Gate-urilor cu doi qubiți. Acest model poate fi executat folosind doar conectivitate liniară între qubiți.

n_qubits = 10
reps = 1

circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)

circuit.decompose().draw("mpl", scale=0.7)

Output of the previous code cell

Output of the previous code cell

Pentru observabilul nostru, să luăm operatorul Pauli ZZ care acționează pe ultimul Qubit, ZIIZ I \cdots I.

# Z on the last qubit (index -1) with coefficient 1.0
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)

În acest moment, ai putea proceda la rularea circuitului tău și la măsurarea observabilului. Totuși, vrei și să compari rezultatul dispozitivului cuantic cu răspunsul corect — adică valoarea teoretică a observabilului, dacă circuitul ar fi fost executat fără erori. Pentru circuite cuantice mici poți calcula această valoare simulând circuitul pe un calculator clasic, dar acest lucru nu este posibil pentru circuite mai mari, la scară de utilitate. Poți ocoli această problemă cu tehnica „circuitului oglindă" (cunoscută și ca „compute-uncompute"), care este utilă pentru evaluarea performanței dispozitivelor cuantice.

Circuit oglindă

În tehnica circuitului oglindă, concatenezi circuitul cu circuitul său invers, care este format prin inversarea fiecărui Gate din circuit în ordine inversă. Circuitul rezultat implementează operatorul identitate, care poate fi simulat trivial. Deoarece structura circuitului original este păstrată în circuitul oglindă, executarea acestuia din urmă oferă totuși o idee despre cum s-ar descurca dispozitivul cuantic pe circuitul original.

Următoarea celulă de cod atribuie parametri aleatori circuitului tău, apoi construiește circuitul oglindă folosind clasa unitary_overlap. Înainte de oglindirea circuitului, adaugă-i o instrucțiune barrier pentru a împiedica Transpiler-ul să fuzioneze cele două părți ale circuitului de pe fiecare parte a barierei. Fără barieră, Transpiler-ul ar fuziona circuitul original cu inversul său, rezultând un circuit transpilat fără niciun Gate.

# Generate random parameters
rng = np.random.default_rng(1234)
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)

# Assign the parameters to the circuit
assigned_circuit = circuit.assign_parameters(params)

# Add a barrier to prevent circuit optimization of mirrored operators
assigned_circuit.barrier()

# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)

mirror_circuit.decompose().draw("mpl", scale=0.7)

Output of the previous code cell

Output of the previous code cell

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

Trebuie să optimizezi circuitul înainte de a-l rula pe hardware. Acest proces implică câțiva pași:

  • Alege un layout de qubiți care mapează qubiții virtuali ai circuitului la qubiți fizici pe hardware.
  • Inserează Gate-uri de swap după cum este necesar pentru a direcționa interacțiunile dintre qubiți care nu sunt conectați.
  • Traduce Gate-urile din circuit în instrucțiuni Instruction Set Architecture (ISA) care pot fi executate direct pe hardware.
  • Efectuează optimizări ale circuitului pentru a minimiza adâncimea circuitului și numărul de Gate-uri.

Transpiler-ul integrat în Qiskit poate efectua toți acești pași pentru tine. Deoarece acest exemplu folosește un circuit eficient pe hardware, Transpiler-ul ar trebui să poată alege un layout de qubiți care nu necesită inserarea de Gate-uri swap pentru direcționarea interacțiunilor.

Trebuie să alegi dispozitivul hardware de folosit înainte de a-ți optimiza circuitul. Următoarea celulă de cod solicită cel mai puțin ocupat dispozitiv cu cel puțin 127 de qubiți.

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

Poți transpila circuitul pentru Backend-ul ales creând un pass manager și rulând apoi pass manager-ul pe circuit. O modalitate ușoară de a crea un pass manager este să folosești funcția generate_preset_pass_manager. Consultă Transpilează cu pass managers pentru o explicație mai detaliată a transpilării cu pass managers.

pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=1234
)
isa_circuit = pass_manager.run(mirror_circuit)

isa_circuit.draw("mpl", idle_wires=False, scale=0.7, fold=-1)

Output of the previous code cell

Output of the previous code cell

Circuitul transpilat conține acum doar instrucțiuni ISA. Gate-urile cu un singur Qubit au fost descompuse în termeni de Gate-uri X\sqrt{X} și rotații RzR_z, iar Gate-urile CX au fost descompuse în Gate-uri ECR și rotații cu un singur Qubit.

Procesul de transpilare a mapat qubiții virtuali ai circuitului la qubiți fizici pe hardware. Informațiile despre layout-ul qubiților sunt stocate în atributul layout al circuitului transpilat. Observabilul a fost de asemenea definit în termeni de qubiți virtuali, deci trebuie să aplici acest layout observabilului, ceea ce poți face cu metoda apply_layout din SparsePauliOp.

isa_observable = observable.apply_layout(isa_circuit.layout)

print("Original observable:")
print(observable)
print()
print("Observable with layout applied:")
print(isa_observable)
Original observable:
SparsePauliOp(['ZIIIIIIIII'],
coeffs=[1.+0.j])

Observable with layout applied:
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])

Pasul 3: Execută folosind primitivele Qiskit

Acum ești gata să rulezi circuitul folosind primitiva Estimator.

Aici vei trimite cinci joburi separate, începând fără nicio suprimare sau atenuare a erorilor, și activând succesiv diverse opțiuni de suprimare și atenuare a erorilor disponibile în Qiskit Runtime. Pentru informații despre opțiuni, consultă următoarele pagini:

Deoarece aceste joburi pot rula independent unele de altele, poți folosi modul batch pentru a permite Qiskit Runtime să optimizeze momentul execuției lor.

pub = (isa_circuit, isa_observable)

jobs = []

with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0

# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)

# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)

# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)

# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)

# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)

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

În final, poți analiza datele. Aici vei prelua rezultatele joburilor, vei extrage valorile de așteptare măsurate din acestea și vei reprezenta grafic valorile, inclusiv barele de eroare de o deviație standard.

# Retrieve the job results
results = [job.result() for job in jobs]

# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]

# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)

# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")

plt.show()

Output of the previous code cell

La această scară mică, este greu de observat efectul majorității tehnicilor de atenuare a erorilor, dar extrapolarea la zgomot zero oferă o îmbunătățire notabilă. Totuși, reține că această îmbunătățire nu vine gratuit, deoarece rezultatul ZNE are și o bară de eroare mai mare.

Scalează experimentul

Când dezvolți un experiment, este util să începi cu un circuit mic pentru a facilita vizualizările și simulările. Acum că ai dezvoltat și testat fluxul de lucru pe un circuit de 10 qubiți, îl poți scala la 50 de qubiți. Următoarea celulă de cod repetă toți pașii din acest ghid, dar îi aplică acum unui circuit de 50 de qubiți.

n_qubits = 50
reps = 1

# Construct circuit and observable
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)

# Assign parameters to circuit
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
assigned_circuit = circuit.assign_parameters(params)
assigned_circuit.barrier()

# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)

# Transpile circuit and observable
isa_circuit = pass_manager.run(mirror_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)

# Run jobs
pub = (isa_circuit, isa_observable)

jobs = []

with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0

# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)

# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)

# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)

# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)

# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)

# Retrieve the job results
results = [job.result() for job in jobs]

# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]

# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)

# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")

plt.show()

Output of the previous code cell

Când compari rezultatele pentru 50 de qubiți cu rezultatele pentru 10 qubiți de mai devreme, s-ar putea să observi următoarele (rezultatele tale pot diferi de la o rulare la alta):

  • Rezultatele fără mitigarea erorilor sunt mai slabe. Rularea unui circuit mai mare implică executarea mai multor Gate-uri, deci există mai multe oportunități ca erorile să se acumuleze.
  • Adăugarea decuplării dinamice ar fi putut înrăutăți performanța. Acest lucru nu este surprinzător, deoarece circuitul este foarte dens. Decuplarea dinamică este utilă în principal atunci când există goluri mari în circuit, în care qubiții stau inactivi fără ca Gate-uri să li se aplice. Când aceste goluri nu există, decuplarea dinamică nu este eficientă și poate chiar înrăutăți performanța din cauza erorilor din pulsurile de decuplare dinamică în sine. Este posibil ca circuitul de 10 qubiți să fi fost prea mic pentru a observa acest efect.
  • Cu extrapolarea zero-zgomot, rezultatul este la fel de bun, sau aproape la fel de bun, ca rezultatul pentru 10 qubiți, deși bara de eroare este mult mai mare. Aceasta demonstrează puterea tehnicii ZNE!

Concluzie

În acest ghid, ai investigat diferitele opțiuni de mitigare a erorilor disponibile pentru primitiva Estimator din Qiskit Runtime. Ai dezvoltat un flux de lucru folosind un circuit de 10 qubiți, pe care l-ai scalat apoi la 50 de qubiți. Ai fi putut observa că activarea mai multor opțiuni de suprimare și mitigare a erorilor nu îmbunătățește întotdeauna performanța (mai exact, activarea decuplării dinamice în acest caz). Majoritatea opțiunilor acceptă configurații suplimentare, pe care le poți testa în propriile proiecte!