Funcții de cost
În această lecție vom învăța cum să evaluăm o funcție de cost:
- Mai întâi, vom afla despre primitivele Qiskit Runtime
- Vom defini o funcție de cost . Aceasta este o funcție specifică problemei care definește obiectivul problemei pentru ca optimizatorul să îl minimizeze (sau maximizeze)
- Vom defini o strategie de măsurare cu primitivele Qiskit Runtime pentru a optimiza viteza față de acuratețe
Primitive
Toate sistemele fizice, fie clasice, fie cuantice, pot exista în stări diferite. De exemplu, o mașină pe un drum poate avea o anumită masă, poziție, viteză sau accelerație care îi caracterizează starea. În mod similar, sistemele cuantice pot avea, de asemenea, configurații sau stări diferite, dar se deosebesc de sistemele clasice prin modul în care tratăm măsurătorile și evoluția stărilor. Aceasta conduce la proprietăți unice, cum ar fi superpunerea și entanglementul, exclusive mecanicii cuantice. Așa cum putem descrie starea unei mașini folosind proprietăți fizice precum viteza sau accelerația, putem descrie și starea unui sistem cuantic folosind observabile, care sunt obiecte matematice.
În mecanica cuantică, stările sunt reprezentate prin vectori coloană complecși normalizați, sau kets (), iar observabilele sunt operatori liniari Hermitian () care acționează asupra kets. Un vector propriu () al unui observabil este cunoscut sub numele de stare proprie. Măsurarea unui observabil pentru una dintre stările sale proprii () ne va oferi valoarea proprie corespunzătoare () ca rezultat.
Dacă te întrebi cum să măsori un sistem cuantic și ce poți măsura, Qiskit oferă două primitive care pot ajuta:
Sampler: Dată o stare cuantică , această primitivă obține probabilitatea fiecărei stări posibile din baza computațională.Estimator: Dat un observabil cuantic și o stare , această primitivă calculează valoarea așteptată a lui .
Primitiva Sampler
Primitiva Sampler calculează probabilitatea de a obține fiecare stare posibilă din baza computațională, dată un circuit cuantic care pregătește starea . Aceasta calculează
Unde este numărul de qubiți, iar este reprezentarea întreagă a oricărui șir binar de ieșire posibil (adică, numere întregi în baza ).
Qiskit Runtime Sampler rulează circuitul de mai multe ori pe un dispozitiv cuantic, efectuând măsurători la fiecare rulare, și reconstruind distribuția de probabilitate din șirurile de biți recuperate. Cu cât mai multe rulări (sau shots) efectuează, cu atât rezultatele vor fi mai precise, dar aceasta necesită mai mult timp și resurse cuantice.
Cu toate acestea, deoarece numărul de ieșiri posibile crește exponențial cu numărul de qubiți (adică, ), numărul de shots va trebui să crească exponențial și el pentru a capta o distribuție de probabilitate densă. Prin urmare, Sampler este eficient doar pentru distribuții de probabilitate rare (sparse); unde starea țintă trebuie să fie exprimabilă ca o combinație liniară a stărilor din baza computațională, cu numărul de termeni crescând cel mult polinomial cu numărul de qubiți:
Sampler poate fi, de asemenea, configurat pentru a recupera probabilitățile dintr-o subsecțiune a circuitului, reprezentând un subset din totalul stărilor posibile.
Primitiva Estimator
Primitiva Estimator calculează valoarea de așteptare a unui observabil pentru o stare cuantică ; unde probabilitățile observabilului pot fi exprimate ca , fiind stările proprii ale observabilului . Valoarea de așteptare este definită ca media tuturor rezultatelor posibile (adică, valorile proprii ale observabilului) ale unei măsurători a stării , ponderate de probabilitățile corespunzătoare:
Cu toate acestea, calcularea valorii de așteptare a unui observabil nu este întotdeauna posibilă, deoarece adesea nu cunoaștem eigenbasis-ul său. Qiskit Runtime Estimator utilizează un proces algebric complex pentru a estima valoarea de așteptare pe un dispozitiv cuantic real, descompunând observabilul într-o combinație de alți observabili al căror eigenbasis îl cunoaștem.
Mai simplu, Estimator descompune orice observabil pe care nu știe cum să îl măsoare în observabile mai simple, măsurabile, numite operatori Pauli.
Orice operator poate fi exprimat ca o combinație de operatori Pauli.
astfel că
unde este numărul de qubiți, pentru (adică, numere întregi în baza ), și .
După efectuarea acestei descompuneri, Estimator derivă un nou circuit pentru fiecare observabil (din circuitul original), pentru a diagonaliza efectiv observabilul Pauli în baza computațională și a-l măsura. Putem măsura cu ușurință observabilele Pauli deoarece cunoaștem în avans, ceea ce nu este cazul în general pentru alți observabili.
Pentru fiecare , Estimator rulează circuitul corespunzător pe un dispozitiv cuantic de mai multe ori, măsoară starea de ieșire în baza computațională și calculează probabilitatea de a obține fiecare ieșire posibilă . Apoi caută valoarea proprie a lui corespunzătoare fiecărei ieșiri , înmulțește cu și adaugă toate rezultatele împreună pentru a obține valoarea așteptată a observabilului pentru starea dată .
Deoarece calcularea valorii de așteptare a operatori Pauli este impractică (adică, creștere exponențială), Estimator poate fi eficient doar atunci când un număr mare de sunt zero (adică, descompunere Pauli rară în loc de densă). Formal spunem că, pentru ca această calculare să fie eficient rezolvabilă, numărul de termeni nenuli trebuie să crească cel mult polinomial cu numărul de qubiți :
Cititorul poate observa ipoteza implicită că eșantionarea probabilităților trebuie să fie, de asemenea, eficientă, așa cum s-a explicat pentru Sampler, ceea ce înseamnă
Exemplu ghidat pentru calculul valorilor de așteptare
Să presupunem starea cu un singur qubit , și observabilul
cu următoarea valoare de așteptare teoretică
Deoarece nu știm cum să măsurăm acest observabil, nu putem calcula direct valoarea sa de așteptare și trebuie să îl reexprimăm ca . Se poate arăta că evaluează același rezultat observând că și .
Să vedem cum să calculăm și direct. Deoarece și nu comută (adică, nu împart același eigenbasis), nu pot fi măsurate simultan, prin urmare avem nevoie de circuitele auxiliare:
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime rustworkx
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
# The following code will work for any other initial single-qubit state and observable
original_circuit = QuantumCircuit(1)
original_circuit.h(0)
H = SparsePauliOp(["X", "Z"], [2, -1])
aux_circuits = []
for pauli in H.paulis:
aux_circ = original_circuit.copy()
aux_circ.barrier()
if str(pauli) == "X":
aux_circ.h(0)
elif str(pauli) == "Y":
aux_circ.sdg(0)
aux_circ.h(0)
else:
aux_circ.id(0)
aux_circ.measure_all()
aux_circuits.append(aux_circ)
original_circuit.draw("mpl")
# Auxiliary circuit for X
aux_circuits[0].draw("mpl")
# Auxiliary circuit for Z
aux_circuits[1].draw("mpl")
Putem acum efectua calculul manual folosind Sampler și verifica rezultatele cu Estimator:
from qiskit.primitives import StatevectorSampler, StatevectorEstimator
from qiskit.result import QuasiDistribution
import numpy as np
## SAMPLER
shots = 10000
sampler = StatevectorSampler()
job = sampler.run(aux_circuits, shots=shots)
# Run the sampler job and step through results
expvals = []
for index, pauli in enumerate(H.paulis):
data_pub = job.result()[index].data
bitstrings = data_pub.meas.get_bitstrings()
counts = data_pub.meas.get_counts()
quasi_dist = QuasiDistribution(
{outcome: freq / shots for outcome, freq in counts.items()}
)
# Use the probabilities and known eigenvalues of Pauli operators to estimate the expectation value.
val = 0
if str(pauli) == "X":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Y":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Z":
val += 1 * quasi_dist.get(0, 0)
val += -1 * quasi_dist.get(1, 0)
expvals.append(val)
# Print expectation values
print("Sampler results:")
for pauli, expval in zip(H.paulis, expvals):
print(f" >> Expected value of {str(pauli)}: {expval:.5f}")
total_expval = np.sum(H.coeffs * expvals).real
print(f" >> Total expected value: {total_expval:.5f}")
# Use estimator for comparison
observables = [
*H.paulis,
H,
] # Note: run for individual Paulis as well as full observable H
estimator = StatevectorEstimator()
job = estimator.run([(original_circuit, observables)])
estimator_expvals = job.result()[0].data.evs
# Print results
print("Estimator results:")
for obs, expval in zip(observables, estimator_expvals):
if obs is not H:
print(f" >> Expected value of {str(obs)}: {expval:.5f}")
else:
print(f" >> Total expected value: {expval:.5f}")
Sampler results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00420
>> Total expected value: 1.99580
Estimator results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00000
>> Total expected value: 2.00000
Rigoare matematică (opțional)
Exprimând în raport cu baza stărilor proprii ale lui , , rezultă:
Deoarece nu cunoaștem valorile proprii sau stările proprii ale observabilului țintă , mai întâi trebuie să luăm în considerare diagonalizarea sa. Dat că este Hermitian, există o transformare unitară astfel că unde este matricea diagonală a valorilor proprii, astfel că dacă , și .
Aceasta implică că valoarea de așteptare poate fi rescrisă ca:
Dat că dacă un sistem se află în starea , probabilitatea de a măsura este , valoarea de așteptare de mai sus poate fi exprimată ca:
Este foarte important de reținut că probabilitățile sunt luate din starea în loc de . De aceea matricea este absolut necesară. S-ar putea să te întrebi cum să obții matricea și valorile proprii . Dacă ai deja valorile proprii, atunci nu ar mai fi nevoie să folosești un calculator cuantic, deoarece scopul algoritmilor variaționali este tocmai găsirea acestor valori proprii ale lui .
Din fericire, există o soluție: orice matrice poate fi scrisă ca o combinație liniară de produse tensoriale de matrici Pauli și identități, toate fiind atât Hermitian, cât și unitare, cu și cunoscute. Aceasta este ceea ce face Estimator din Runtime intern, descompunând orice obiect Operator într-un SparsePauliOp.
Iată operatorii care pot fi folosiți:
Deci să rescriem în raport cu operatorii Pauli și identități:
unde pentru (adică, baza ), și :
unde și , astfel că:
Funcții de cost
În general, funcțiile de cost sunt folosite pentru a descrie obiectivul unei probleme și cât de bine performează o stare de testare față de acel obiectiv. Această definiție poate fi aplicată la diverse exemple din chimie, învățare automată, finanțe, optimizare și altele.
Să considerăm un exemplu simplu de găsire a stării fundamentale a unui sistem. Obiectivul nostru este să minimizăm valoarea de așteptare a observabilului care reprezintă energia (Hamiltonian ):
Putem folosi Estimator pentru a evalua valoarea de așteptare și a transmite această valoare unui optimizator pentru a o minimiza. Dacă optimizarea reușește, va returna un set de valori optime ale parametrilor , din care vom putea construi starea soluție propusă și calcula valoarea de așteptare observată ca .
Observă că vom putea minimiza funcția de cost doar pentru setul limitat de stări pe care le luăm în considerare. Aceasta ne conduce la două posibilități separate:
- ansatz-ul nostru nu definește starea soluție în tot spațiul de căutare: Dacă acesta este cazul, optimizatorul nu va găsi niciodată soluția și trebuie să experimentăm cu alte ansatz-uri care ar putea reprezenta spațiul nostru de căutare mai precis.
- Optimizatorul nostru este incapabil să găsească această soluție validă: Optimizarea poate fi definită global și local. Vom explora ce înseamnă aceasta în secțiunea ulterioară.
În general, vom efectua o buclă de optimizare clasică, bazându-ne pe evaluarea funcției de cost pe un calculator cuantic. Din această perspectivă, s-ar putea considera optimizarea ca o sarcină pur clasică în care apelăm un oracol cuantic de tip cutie neagră de fiecare dată când optimizatorul trebuie să evalueze funcția de cost.
def cost_func_vqe(params, circuit, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (circuit, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
return cost
from qiskit.circuit.library import TwoLocal
observable = SparsePauliOp.from_list([("XX", 1), ("YY", -3)])
reference_circuit = QuantumCircuit(2)
reference_circuit.x(0)
variational_form = TwoLocal(
2,
rotation_blocks=["rz", "ry"],
entanglement_blocks="cx",
entanglement="linear",
reps=1,
)
ansatz = reference_circuit.compose(variational_form)
theta_list = (2 * np.pi * np.random.rand(1, 8)).tolist()
ansatz.decompose().draw("mpl")
Vom efectua mai întâi acest lucru folosind un simulator: StatevectorEstimator. Aceasta este de obicei recomandabilă pentru depanare, dar vom urma imediat rularea de depanare cu un calcul pe hardware cuantic real. Din ce în ce mai mult, problemele de interes nu mai sunt simulabile clasic fără facilități de supercalculare de ultimă generație.
estimator = StatevectorEstimator()
cost = cost_func_vqe(theta_list, ansatz, observable, estimator)
print(cost)
[-0.58744589]
Vom continua acum cu rularea pe un calculator cuantic real. Observă modificările de sintaxă. Pașii care implică pass_manager vor fi discutați mai departe în exemplul următor. Un pas de importanță deosebită în algoritmii variaționali este utilizarea unei sesiuni Qiskit Runtime. Deschiderea unei sesiuni îți permite să rulezi mai multe iterații ale unui algoritm variațional fără a aștepta într-o coadă nouă de fiecare dată când parametrii sunt actualizați. Aceasta este importantă dacă timpii de coadă sunt lungi și/sau sunt necesare multe iterații. Numai partenerii din IBM Quantum® Network pot folosi sesiunile Runtime. Dacă nu ai acces la sesiuni, poți reduce numărul de iterații pe care le trimiți la un moment dat și salva cei mai recenți parametri pentru utilizare în rulări viitoare. Dacă trimiți prea multe iterații sau întâlnești timpi de coadă prea lungi, poți întâlni codul de eroare 1217, care se referă la întârzieri mari între trimiteri de joburi.
# Estimated usage: < 1 min. Benchmarked at 7 seconds on an Eagle processor
# Load necessary packages:
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Select the least busy backend:
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_observable = observable.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Runtime session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(theta_list, isa_ansatz, isa_observable, estimator)
session.close()
print(cost)
Observă că valorile obținute din cele două calculări de mai sus sunt foarte similare. Tehnicile pentru îmbunătățirea rezultatelor vor fi discutate mai departe mai jos.
Exemplu de mapare la sisteme ne-fizice
Problema tăieturii maxime (Max-Cut) este o problemă de optimizare combinatorică care implică împărțirea vârfurilor unui graf în două seturi disjuncte astfel încât numărul de muchii dintre cele două seturi să fie maximizat. Mai formal, dat un graf neorientat , unde este setul de vârfuri și este setul de muchii, problema Max-Cut cere partiționarea vârfurilor în două subseturi disjuncte, și , astfel încât numărul de muchii cu un capăt în și celălalt în să fie maximizat.
Putem aplica Max-Cut pentru a rezolva diverse probleme, inclusiv: clasificare, proiectarea rețelelor, tranziții de fază și altele. Vom începe prin crearea unui graf al problemei:
import rustworkx as rx
from rustworkx.visualization import mpl_draw
n = 4
G = rx.PyGraph()
G.add_nodes_from(range(n))
# The edge syntax is (start, end, weight)
edges = [(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0), (1, 2, 1.0), (2, 3, 1.0)]
G.add_edges_from(edges)
mpl_draw(
G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color="#1192E8"
)
Această problemă poate fi exprimată ca o problemă de optimizare binară. Pentru fiecare nod , unde este numărul de noduri ale grafului (în acest caz ), vom considera variabila binară . Această variabilă va avea valoarea dacă nodul se află în unul din grupuri pe care îl vom eticheta și dacă se află în celălalt grup, pe care îl vom eticheta ca . De asemenea, vom nota cu (elementul al matricei de adiacență ) greutatea muchiei care merge de la nodul la nodul . Deoarece graful este neorientat, . Putem formula astfel problema noastră ca maximizarea următoarei funcții de cost:
Pentru a rezolva această problemă cu un calculator cuantic, vom exprima funcția de cost ca valoarea de așteptare a unui observabil. Cu toate acestea, observabilele pe care Qiskit le acceptă nativ constau din operatori Pauli, care au valorile proprii și în loc de și . De aceea vom face următoarea schimbare de variabilă:
Unde . Putem folosi matricea de adiacență pentru a accesa convenabil ponderile tuturor muchiilor. Aceasta va fi folosită pentru a obține funcția noastră de cost:
Aceasta implică că:
Deci noua funcție de cost pe care dorim să o maximizăm este:
Mai mult, tendința naturală a unui calculator cuantic este de a găsi minime (de obicei cea mai joasă energie) în loc de maxime, deci în loc să maximizăm vom minimiza:
Acum că avem o funcție de cost de minimizat ale cărei variabile pot lua valorile și , putem face următoarea analogie cu Pauli :
Cu alte cuvinte, variabila va fi echivalentă cu un Gate care acționează pe qubit . Mai mult:
Deci observabilul pe care îl vom lua în considerare este:
la care va trebui să adăugăm termenul independent ulterior:
Operatorul este o combinație liniară de termeni cu operatori Z pe nodurile conectate printr-o muchie (reamintește că qubit-ul 0 se află cel mai în dreapta): . Odată ce operatorul este construit, ansatz-ul pentru algoritmul QAOA poate fi ușor construit folosind circuitul QAOAAnsatz din biblioteca de circuite Qiskit.
from qiskit.circuit.library import QAOAAnsatz
from qiskit.quantum_info import SparsePauliOp
hamiltonian = SparsePauliOp.from_list(
[("IIZZ", 1), ("IZIZ", 1), ("IZZI", 1), ("ZIIZ", 1), ("ZZII", 1)]
)
ansatz = QAOAAnsatz(hamiltonian, reps=2)
# Draw
ansatz.decompose(reps=3).draw("mpl")
# Sum the weights, and divide by 2
offset = -sum(edge[2] for edge in edges) / 2
print(f"""Offset: {offset}""")
Offset: -2.5
Cu Estimator din Runtime care primește direct un Hamiltonian și un ansatz parametrizat, și returnează energia necesară, funcția de cost pentru o instanță QAOA este destul de simplă:
def cost_func(params, ansatz, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (ansatz, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
# cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]
return cost
import numpy as np
x0 = 2 * np.pi * np.random.rand(ansatz.num_parameters)
estimator = StatevectorEstimator()
cost = cost_func_vqe(x0, ansatz, hamiltonian, estimator)
print(cost)
1.473098768180865
# Estimated usage: < 1 min, benchmarked at 6 seconds on ibm_osaka, 5-23-24
# Load some necessary packages:
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Session, EstimatorV2 as Estimator
# Select the least busy backend:
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_hamiltonian = hamiltonian.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Runtime session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(x0, isa_ansatz, isa_hamiltonian, estimator)
# Close session after done
session.close()
print(cost)
1.1120776913677988
Vom reveni la acest exemplu în secțiunea Aplicații pentru a explora cum să utilizăm un optimizator pentru a parcurge spațiul de căutare. În general, aceasta include:
- Utilizarea unui optimizator pentru a găsi parametrii optimi
- Legarea parametrilor optimi la ansatz pentru a găsi valorile proprii
- Traducerea valorilor proprii la definiția problemei noastre
Strategia de măsurare: viteză versus acuratețe
Așa cum am menționat, folosim un calculator cuantic zgomotos ca oracol de tip cutie neagră, unde zgomotul poate face valorile recuperate nedeterministe, conducând la fluctuații aleatorii care, la rândul lor, vor afecta — sau chiar împiedica complet — convergența anumitor optimizatori la o soluție propusă. Aceasta este o problemă generală pe care trebuie să o abordăm pe măsură ce explorăm incremental utilitatea cuantică și progresăm spre avantajul cuantic:
Putem folosi opțiunile de suprimare a erorilor și de atenuare a erorilor din Qiskit Runtime Primitives pentru a aborda zgomotul și a maximiza utilitatea calculatoarelor cuantice de astăzi.
Suprimarea erorilor
Suprimarea erorilor se referă la tehnicile utilizate pentru a optimiza și transforma un circuit în timpul compilării pentru a minimiza erorile. Aceasta este o tehnică de bază de gestionare a erorilor care de obicei rezultă în ceva overhead de preprocesare clasică pentru durata totală de execuție. Overhead-ul include transpunerea circuitelor pentru a rula pe hardware cuantic prin:
- Exprimarea circuitului folosind porțile native disponibile pe un sistem cuantic
- Maparea qubiților virtuali la qubiți fizici
- Adăugarea de SWAP-uri pe baza cerințelor de conectivitate
- Optimizarea porților 1Q și 2Q
- Adăugarea de decuplare dinamică la qubiții inactivi pentru a preveni efectele decoherenței.
Primitivele permit utilizarea tehnicilor de suprimare a erorilor prin setarea opțiunii optimization_level și selectarea opțiunilor avansate de transpilare. Într-un curs ulterior, vom aprofunda diferite metode de construcție a circuitelor pentru a îmbunătăți rezultatele, dar pentru majoritatea cazurilor, recomandăm setarea optimization_level=3.
Vom vizualiza valoarea creșterii optimizării în procesul de transpilare prin examinarea unui exemplu de circuit cu un comportament ideal simplu.
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
theta = Parameter("theta")
qc = QuantumCircuit(2)
qc.x(1)
qc.h(0)
qc.cp(theta, 0, 1)
qc.h(0)
observables = SparsePauliOp.from_list([("ZZ", 1)])
qc.draw("mpl")
Circuitul de mai sus poate produce valori de așteptare sinusoidale ale observabilului dat, cu condiția să inserăm faze care acoperă un interval adecvat, cum ar fi .
## Setup phases
import numpy as np
phases = np.linspace(0, 2 * np.pi, 50)
# phases need to be expressed as a list of lists in order to work
individual_phases = [[phase] for phase in phases]
Putem folosi un simulator pentru a demonstra utilitatea unei transpilări optimizate. Vom reveni mai jos la utilizarea hardware-ului real pentru a demonstra utilitatea atenuării erorilor. Vom folosi QiskitRuntimeService pentru a obține un Backend real (în acest caz, ibm_brisbane), și AerSimulator pentru a simula acel Backend, inclusiv comportamentul său zgomotos.
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_aer import AerSimulator
# get a real backend from the runtime service
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
# generate a simulator that mimics the real quantum system with the latest calibration results
backend_sim = AerSimulator.from_backend(backend)
Putem acum folosi un pass manager pentru a transpune circuitul în „arhitectura setului de instrucțiuni" sau ISA a Backend-ului. Aceasta este o nouă cerință în Qiskit Runtime: toate circuitele trimise unui Backend trebuie să respecte constrângerile țintei Backend-ului, adică trebuie să fie scrise în termenii ISA a Backend-ului — adică setul de instrucțiuni pe care dispozitivul le poate înțelege și executa. Aceste constrângeri ale țintei sunt definite de factori precum porțile native ale dispozitivului, conectivitatea qubiților și — atunci când este relevant — specificațiile de sincronizare a pulsurilor și altor instrucțiuni.
Observă că în prezentul caz, vom face aceasta de două ori: o dată cu optimization_level = 0, și o dată cu acesta setat la 3. De fiecare dată vom folosi primitiva Estimator pentru a estima valorile de așteptare ale observabilului la diferite valori ale fazei.
# Import estimator and specify that we are using the simulated backend:
from qiskit_ibm_runtime import EstimatorV2 as Estimator
estimator = Estimator(mode=backend_sim)
circuit = qc
# Use a pass manager to transpile the circuit and observable for the backend being simulated.
# Start with no optimization:
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=0)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
noisy_exp_values = []
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
noisy_exp_values = cost[0]
# Repeat above steps, but now with optimization = 3:
exp_values_with_opt_es = []
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=3)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
exp_values_with_opt_es = cost[0]
În final, putem reprezenta grafic rezultatele și observăm că precizia calculului a fost destul de bună chiar și fără optimizare, dar s-a îmbunătățit definitiv prin creșterea optimizării la nivelul 3. Observă că în circuite mai profunde și mai complexe, diferența dintre nivelurile de optimizare 0 și 3 este probabil să fie mai semnificativă. Acesta este un circuit foarte simplu folosit ca model de jucărie.
import matplotlib.pyplot as plt
plt.plot(phases, noisy_exp_values, "o", label="opt=0")
plt.plot(phases, exp_values_with_opt_es, "o", label="opt=3")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()
Atenuarea erorilor
Atenuarea erorilor se referă la tehnicile care permit utilizatorilor să reducă erorile din circuit prin modelarea zgomotului dispozitivului la momentul execuției. De obicei, aceasta rezultă în overhead de preprocesare cuantică legat de antrenarea modelului și overhead de postprocesare clasică pentru a atenua erorile din rezultatele brute folosind modelul generat.
Opțiunea resilience_level a primitivei Qiskit Runtime specifică cantitatea de reziliență construită împotriva erorilor. Nivelurile mai ridicate generează rezultate mai precise cu prețul unor timpi de procesare mai lungi din cauza overhead-ului de eșantionare cuantică. Nivelurile de reziliență pot fi folosite pentru a configura compromisul dintre cost și acuratețe atunci când se aplică atenuarea erorilor la interogarea ta de primitivă.
Atunci când implementăm orice tehnică de atenuare a erorilor, ne așteptăm ca deviația din rezultatele noastre să fie redusă față de deviația anterioară, neatenuată. În unele cazuri, deviația poate chiar dispărea. Cu toate acestea, aceasta vine cu un cost. Pe măsură ce reducem deviația din cantitățile noastre estimate, variabilitatea statistică va crește (adică varianța), pe care o putem compensa prin creșterea ulterioară a numărului de shots per circuit în procesul nostru de eșantionare. Aceasta va introduce overhead dincolo de cel necesar pentru reducerea deviației, deci nu se face implicit. Putem opta ușor pentru acest comportament ajustând numărul de shots per circuit în options.executions.shots, după cum se arată în exemplul de mai jos.
Pentru acest curs, vom explora aceste modele de atenuare a erorilor la un nivel înalt pentru a ilustra atenuarea erorilor pe care primitivele Qiskit Runtime le pot efectua fără a necesita detalii complete de implementare.
Extinctia erorii de citire prin twirling (T-REx)
Extinctia erorii de citire prin twirling (T-REx) folosește o tehnică cunoscută sub numele de Pauli twirling pentru a reduce zgomotul introdus în timpul procesului de măsurare cuantică. Această tehnică nu presupune nicio formă specifică de zgomot, ceea ce o face foarte generală și eficientă.
Fluxul de lucru general:
- Achiziționarea datelor pentru starea zero cu flip-uri de biți aleatorii (Pauli X înainte de măsurare)
- Achiziționarea datelor pentru starea dorită (zgomotoasă) cu flip-uri de biți aleatorii (Pauli X înainte de măsurare)
- Calcularea funcției speciale pentru fiecare set de date și împărțirea.
Putem seta aceasta cu options.resilience_level = 1, demonstrat în exemplul de mai jos.
Extrapolarea la zero zgomot
Extrapolarea la zero zgomot (ZNE) funcționează prin amplificarea mai întâi a zgomotului din circuitul care pregătește starea cuantică dorită, obținerea măsurătorilor pentru mai multe niveluri diferite de zgomot, și utilizarea acelor măsurători pentru a deduce rezultatul fără zgomot.
Fluxul de lucru general:
- Amplificarea zgomotului circuitului pentru mai mulți factori de zgomot
- Rularea fiecărui circuit cu zgomot amplificat
- Extrapolarea înapoi la limita de zero zgomot
Putem seta aceasta cu options.resilience_level = 2. Putem optimiza aceasta în continuare prin explorarea unei varietăți de noise_factors, noise_amplifiers și extrapolators, dar aceasta este în afara domeniului acestui curs. Te încurajăm să experimentezi cu aceste opțiuni descrise aici.
Fiecare metodă vine cu propriul overhead asociat: un compromis între numărul de calcule cuantice necesare (timp) și acuratețea rezultatelor noastre:
Utilizarea opțiunilor de atenuare și suprimare din Qiskit Runtime
Iată cum să calculezi o valoare de așteptare folosind atenuarea și suprimarea erorilor în Qiskit Runtime. Putem folosi exact același circuit și observabil ca înainte, dar de această dată păstrând nivelul de optimizare fix la nivelul 2 și reglând acum reziliența sau tehnica(ile) de atenuare a erorilor utilizate. Acest proces de atenuare a erorilor are loc de mai multe ori pe parcursul unei bucle de optimizare.
Efectuăm această parte pe hardware real, deoarece atenuarea erorilor nu este disponibilă pe simulatoare.
# Estimated usage: 8 minutes, benchmarked on an Eagle processor, 5-23-24
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import (
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
# We select the least busy backend
# Select the least busy backend
# backend = service.least_busy(
# operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
# )
# Or use a specific backend
backend = service.backend("ibm_brisbane")
# Initialize some variables to save the results from different runs:
exp_values_with_em0_es = []
exp_values_with_em1_es = []
exp_values_with_em2_es = []
# Use a pass manager to optimize the circuit and observables for the backend chosen:
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
# Open a session and run with no error mitigation:
estimator_options = EstimatorOptions(resilience_level=0, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em0_es = cost[0]
# Open a session and run with resilience = 1:
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em1_es = cost[0]
# Open a session and run with resilience = 2:
estimator_options = EstimatorOptions(resilience_level=2, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em2_es = cost[0]
Ca și înainte, putem reprezenta grafic valorile de așteptare rezultate în funcție de unghiul de fază pentru cele trei niveluri de atenuare a erorilor utilizate. Cu mare dificultate, se poate observa că atenuarea erorilor îmbunătățește ușor rezultatele. Din nou, acest efect este mult mai pronunțat în circuite mai profunde și mai complexe.
import matplotlib.pyplot as plt
plt.plot(phases, exp_values_with_em0_es, "o", label="unmitigated")
plt.plot(phases, exp_values_with_em1_es, "o", label="resil = 1")
plt.plot(phases, exp_values_with_em2_es, "o", label="resil = 2")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()
Rezumat
Cu această lecție, ai învățat cum să creezi o funcție de cost:
- Crearea unei funcții de cost
- Cum să utilizezi primitivele Qiskit Runtime pentru a atenua și suprima zgomotul
- Cum să definești o strategie de măsurare pentru a optimiza viteza față de acuratețe
Iată sarcina de lucru variaționistă la nivel înalt:
Funcția noastră de cost rulează la fiecare iterație a buclei de optimizare. Lecția următoare va explora modul în care optimizatorul clasic utilizează evaluarea funcției noastre de cost pentru a selecta parametri noi.
import qiskit
import qiskit_ibm_runtime
print(qiskit.version.get_version_info())
print(qiskit_ibm_runtime.version.get_version_info())
1.1.0
0.23.0