Sari la conținutul principal

Implementează și rulează un șablon de Qiskit Function pentru dinamica hamiltoniană AQC + Trotter

Prezentare generală

Acesta este un șablon de Qiskit Function independent de experiment pentru dinamica hamiltoniană. Dată fiind o hamiltoniană Pauli 1D pentru vecinii cei mai apropiați, o stare inițială pregătită (opțional) și un set de observabile, acesta rulează evoluția temporală Trotter, compresia circuitului prin compilare cuantică aproximativă (AQC) și execuția atenuată, apoi returnează seria temporală a fiecărei observabile. Schimbă configurarea (PRE) și analiza (POST), iar același nucleu conduce un experiment diferit:

PRE (configurarea ta)FUNCTION (implementată aici)POST (analiza ta)
Pregătește o stare, ca circuit sau ca stare produs, cu un impuls local opționalSinteză Trotter → compresie AQC → execuție pe statevector, fake sau runtime, returnând O(t)\langle O \rangle(t)S(q,ω)S(q, \omega) pentru împrăștiere de neutroni, sau magnetizare, transport, dinamică de quench și așa mai departe

Șablonul este publicat în depozitul de șabloane Qiskit Function, alături de celelalte șabloane de aplicații. Acest notebook îl implementează în propriul tău cont Qiskit Serverless. Rulează-l o singură dată, iar orice notebook poate apoi apela funcția cu serverless.load("aqc-dynamics-function").

Pentru un exemplu științific rezolvat, consultă Simulează împrăștierea de neutroni cu un flux de lucru Serverless AQC + Trotter, care apelează această funcție pentru a calcula factorul de structură dinamică al KCuF3_3. Acest notebook acoperă implementarea și contractul de intrare în schimb.

Cerințe

Înainte de a începe, asigură-te că ai următoarele în mediul kernel al acestui notebook:

  • Qiskit SDK v2.0 sau ulterior (pip install qiskit).

  • Clientul Qiskit IBM Catalog (pip install qiskit-ibm-catalog), care implementează și rulează sarcini de lucru pe Qiskit Serverless.

Dependențele științifice proprii ale funcției (qiskit-addon-aqc-tensor, cotengrust, qiskit-aer) nu trebuie instalate local.

Obține fișierele sursă ale șablonului

Funcția este un mic pachet Python pe care Qiskit Serverless îl rulează în cloud, așa că sursa sa trebuie să existe ca fișiere locale care sunt încărcate la momentul implementării. Pachetul este publicat în depozitul de șabloane Qiskit Function.

Descarcă source_files

Descărcarea este o singură arhivă zip, denumită după calea completă a directorului din depozit:

qiskit-community qiskit-function-templates main physics aqc_trotter source_files.zip

  1. Dezarhivează-o în directorul care conține acest notebook.

  2. Redenumește folderul extras din acel nume lung în source_files.

Directorul tău de lucru arată apoi astfel:

your-working-directory/
├── function-template-aqc-trotter.ipynb <- this notebook
└── source_files/ <- the renamed folder
├── __init__.py
├── program.py
└── source/
├── __init__.py
├── _serverless.py
├── app_function.py
├── aqc.py
├── build.py
├── execute.py
└── hamiltonian.py

Numele trebuie să fie exact source_files, deoarece acesta este working_dir pe care Pasul 3 îl încarcă.

program.py este punctul de intrare pe care gateway-ul îl invocă. Tot ce se află sub source/ reprezintă implementarea, împărțită pe etape: sinteza hamiltoniană și Trotter, compresia AQC și execuția. Niciuna dintre acestea nu necesită editare pentru a rula exemplele care urmează. Pasul 3 încarcă întregul director, așa că repetă acel pas de fiecare dată când modifici un fișier.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-ibm-catalog

1. Autentificare

Folosește qiskit-ibm-catalog pentru a te autentifica la QiskitServerless cu cheia ta API (token) și CRN (instanță), pe care le poți găsi în tabloul de bord IBM Quantum® Platform. Cu aceste credențiale poți instanția clientul serverless local pentru a încărca sau rula funcția selectată:

from qiskit_ibm_catalog import QiskitServerless
serverless = QiskitServerless(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")

Poți folosi opțional save_account() pentru a-ți salva credențialele în mediul local (vezi ghidul Configurează-ți contul IBM Cloud®). Reține că aceasta scrie credențialele tale în același fișier ca QiskitRuntimeService.save_account():

QiskitServerless.save_account(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")

Dacă contul este salvat, nu este nevoie să furnizezi token-ul pentru a te autentifica:

from qiskit_ibm_catalog import QiskitServerless

# Authenticate to the remote cluster
# In this case, loading a saved account
serverless = QiskitServerless()

# REPLACE WITH YOUR OWN CREDENTIALS or SAVED ACCOUNT
# serverless = QiskitServerless(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")

2. Declară dependențele

Pachete de care funcția are nevoie peste imaginea de bază serverless gestionată.

notă

Gateway-ul instalează doar numele aflate pe lista sa de permise (requirements-dynamic-dependencies.txt), potrivite după numele pachetului și fixate la versiunea permisă cu ==. Orice altceva trebuie să sosească tranzitiv (ca dependență a unui pachet aflat pe lista de permise). Sintaxa [extras] este respectată: qiskit-addon-aqc-tensor[quimb-jax] este cea care instalează quimb și jax. cotengrust este necesar pentru eficiența memoriei în timpul simulării rețelei tensoriale. qiskit-aer este listat separat pentru backend-ul fake (simulare zgomotoasă locală).

DEPENDENCIES = [
"qiskit-addon-aqc-tensor[quimb-jax]==0.3.1",
"qiskit-aer==0.17.2",
"cotengrust==0.2.0",
]

3. Definește și încarcă funcția

from qiskit_ibm_catalog import QiskitFunction

fn = QiskitFunction(
title="aqc-dynamics-function",
entrypoint="program.py",
working_dir="source_files/",
dependencies=DEPENDENCIES,
)
serverless.upload(fn)
QiskitFunction(aqc-dynamics-function)

4. Verifică dacă s-a înregistrat

next(p for p in serverless.list() if p.title == "aqc-dynamics-function")
QiskitFunction(aqc-dynamics-function)

Referință funcție

Aceasta este o introducere succintă. Fiecare câmp este documentat integral în README-ul șablonului AQC Dynamics: tabelul complet de intrări cu regulile sale de validare, câmpurile de ieșire, backend-urile de execuție și exemple suplimentare rezolvate. Ceea ce urmează este versiunea scurtă, suficientă pentru a citi exemplele care urmează.

Intrări

Fiecare rulare este un singur apel fn.run(...). Doar primele trei intrări din tabel sunt obligatorii: hamiltonian, t_steps și aqc_segments. Tot ce urmează după acestea este opțional și revine la valoarea implicită afișată, astfel încât un apel minim are trei argumente, iar restul tabelului reprezintă funcționalitatea la care poți opta. num_qubits al hamiltonianului stabilește lungimea lanțului, deci nu există o intrare separată pentru dimensiune.

IntrareImplicitDescriere
hamiltonianobligatoriuHamiltoniană Pauli 1D pentru vecinii cei mai apropiați, ca SparsePauliOp. Șirurile sunt operatori Pauli, deci nu există un factor implicit de o jumătate.
t_stepsobligatoriuNumărul total de pași Trotter. Evoluează până la T = t_steps * dt și raportează fiecare observabilă la fiecare t_k = k * dt.
aqc_segmentsobligatoriuPlan de compresie: o listă de {"n_steps": k, "ansatz_steps": m}. sum(n_steps) pași sunt comprimați; restul rulează ca Trotter simplu.
dt0.2Timpul fizic avansat de un pas Trotter.
initial_state|0...0>Un QuantumCircuit pregătit pentru evoluție. Include orice impuls local direct în acest circuit.
observablesZ per sitOrice acceptă EstimatorV2 ca argument observables. O observabilă pentru fiecare coloană de ieșire.
trotter_optionsSuzuki de ordinul 2{"method": ..., "synthesis_settings": {...}}. reps și time sunt gestionate de funcție.
aqc_optionsvezi descriereamax_bond (32), cutoff (1e-8), autodiff_backend ("jax"), fidelity_target (None), optimizer_settings (L-BFGS-B, jac=True, maxiter=300).
estimator_optionsDD, twirling, TREXEstimatorV2.options, transmis ca atare. Un dicționar furnizat înlocuiește valorile implicite în întregime, în loc să se combine cu acestea.
transpiler_options{"optimization_level": 3}Argumente cheie pentru generate_preset_pass_manager. backend și target sunt respinse, deoarece calea de execuție le gestionează.
backend"runtime""statevector", "fake" sau "runtime".
backend_namecel mai puțin ocupatNumele backend-ului IBM® pentru runtime, sau un backend fals numit.
batches1Împarte circuitele pe N joburi runtime. Un singur lot trimite un singur job și nu creează nicio sesiune.
parallel_simFalseDistribuie căile simulatorului local pe toate nucleele disponibile folosind Ray. Nu are efect asupra runtime.
return_circuitsFalseReturnează circuitele logice AQC + Trotter în rezultat, alături de seria de observabile.

Backend-uri de execuție

Toate cele trei căi partajează același cod și aceleași setări de atenuare. Ele diferă doar în ceea ce privește locul în care rulează circuitele.

backendCe esteCredențialeNote
"statevector"StatevectorEstimator exactDoar cont ServerlessCalea de referință exactă. Fără timp QPU.
"fake"Simulare locală zgomotoasă pe un backend fals QiskitDoar cont ServerlessO repetiție fidelă a căii atenuate runtime. Necesită qiskit-aer. Implicit folosește fake_sherbrooke de 127 de qubiți.
"runtime" (implicit)EstimatorV2 atenuat pe un QPU realCont Serverless și o instanță cu acces QPUbackend_name opțional; omiterea lui selectează dispozitivul cel mai puțin ocupat.

Ambele căi de simulator apelează totuși funcția implementată, așa că au nevoie de un cont Serverless salvat, chiar dacă nu folosesc timp QPU. Cele două exemple care urmează rulează aceeași sarcină de lucru mai întâi pe statevector, apoi pe runtime.

Ieșire

job.result() returnează un dicționar simplu:

{
"times": [...], # length t_steps + 1, t_k = k * dt (t=0 is the prepared state)
"expectation_values": [[...]], # shape (n_times, n_observables)
"observable_labels": [...], # for example: ["Z_0", "ZZ_0_1"]
"metadata": {
"n", "t_steps", "dt", "tier",
"aqc_compressed_steps": 5, # total compressed steps (= sum of segment n_steps)
"aqc_segments": [ # per segment: the plan plus its own results
{"n_steps": 3, "ansatz_steps": 1, "steps": [1, 2, 3], "n_params": 133,
"fidelities": {"1": ..., "2": ..., "3": ...}},
{"n_steps": 2, "ansatz_steps": 2, "steps": [4, 5], "n_params": 245,
"fidelities": {"4": ..., "5": ...}},
],
"execution_backend",
"aqc_fidelities": {"1": ..., "2": ...}, # flat per-step fidelity, all compressed steps
"circuit_stats": { # per-step 2q depth and gate count, full Trotter vs AQC
"1": {"full_trotter": {"depth_2q": ..., "num_2q_gates": ...},
"aqc_trotter": {"depth_2q": ..., "num_2q_gates": ...}},
"2": {...},
},
"warnings": [...], # non-fatal notices; for example, a cotengrust fallback
"resource_usage": { # per stage; QPU_TIME is the charged QPU time
"RUNNING: OPTIMIZING_FOR_HARDWARE": {"CPU_TIME": ...},
"RUNNING: WAITING_FOR_QPU": {"CPU_TIME": ...},
"RUNNING: EXECUTING_QPU": {"QPU_TIME": ...},
},
},
# present only when return_circuits=True
"circuits": [QuantumCircuit, ...], # one per evolved step; circuits[i] is at times[i + 1]
}

aqc_fidelities și circuit_stats sunt cele două de citit primele: împreună îți spun dacă compresia a rămas fidelă și dacă a economisit efectiv adâncime. Pe runtime, resource_usage raportează separat timpul de așteptare în coadă față de timpul QPU pentru care ești taxat. O intrare respinsă eșuează rapid ca ServerlessError structurat (cod 4615).

Exemplu de simulator

Rulează mai întâi funcția pe backend-ul exact statevector. Nu consumă timp QPU și validează implementarea de la un capăt la altul. Modelul de aici este un lanț Ising cu câmp transversal de opt qubiți, iar observables este omis, astfel încât funcția măsoară implicit ZZ per sit.

Planul de compresie este intrarea care merită înțeleasă. Fiecare segment {"n_steps": k, "ansatz_steps": m} comprimă k pași Trotter consecutivi într-un ansatz construit dintr-o țintă Trotter de m pași, iar orice pași dincolo de sum(n_steps) rulează ca Trotter simplu. Pașii timpurii, cu entanglement redus, se comprimă bine într-un ansatz superficial, cu un singur strat; pașii mai târzii, mai puternic entanglați, au nevoie de unul mai adânc.

from qiskit.quantum_info import SparsePauliOp

fn = serverless.load("aqc-dynamics-function")

n = 8
H = SparsePauliOp.from_sparse_list(
[("ZZ", [i, i + 1], 1.0) for i in range(n - 1)]
+ [("X", [i], 0.8) for i in range(n)],
num_qubits=n,
)

job = fn.run(
t_steps=8,
aqc_segments=[
{
"n_steps": 4,
"ansatz_steps": 1,
}, # early steps -> shallow 1-layer ansatz
{
"n_steps": 2,
"ansatz_steps": 2,
}, # later steps -> deeper 2-layer ansatz
],
hamiltonian=H,
aqc_options={"max_bond": 32},
backend="statevector",
)
print("job ID:", job.job_id)
job ID: ee1f3793-e995-427d-81d1-5924549beb38

Urmărește rularea și citește rezultatul

status() raportează atât ciclul de viață general al jobului, cât și sub-starea pe etape pe care funcția o publică pe măsură ce rulează. Aceleași etape se aplică și rulării pe hardware mai târziu în acest ghid:

QUEUED -> INITIALIZING -> RUNNING: OPTIMIZING_FOR_HARDWARE -> RUNNING: WAITING_FOR_QPU -> RUNNING: EXECUTING_QPU -> RUNNING: POST_PROCESSING -> DONE

Valoare status()Etapă
RUNNING: OPTIMIZING_FOR_HARDWAREpregătirea stării, construirea Trotter, compresia AQC
RUNNING: WAITING_FOR_QPUîn așteptare la coadă pe QPU (doar pentru backend-ul runtime)
RUNNING: EXECUTING_QPUcircuite în execuție (simulatoarele locale marchează acest lucru direct)
RUNNING: POST_PROCESSINGasamblarea dicționarului de rezultate

Stările finale sunt DONE, ERROR și CANCELED. Această rulare statevector nu are coadă QPU, deci sare peste RUNNING: WAITING_FOR_QPU. Folosește job.logs() în orice moment pentru a vedea jurnalele pe etape, inclusiv fidelitatea AQC atinsă la fiecare pas.

print(job.status()) # re-run until this reports DONE
DONE
import numpy as np

result = job.result()
ev = np.array(result["expectation_values"])

print("observables:", result["observable_labels"])
print("shape:", ev.shape, "-> (n_times, n_observables)")
print("first row (t = 0, the prepared state):", np.round(ev[0], 4))
print("last row (t = t_steps * dt):", np.round(ev[-1], 4))
print(
"AQC fidelities:",
{k: round(v, 4) for k, v in result["metadata"]["aqc_fidelities"].items()},
)

# What the compression bought: 2-qubit depth at the final time step.
stats = result["metadata"]["circuit_stats"][
str(result["metadata"]["t_steps"])
]
print(
"2q depth at the final step:",
stats["full_trotter"]["depth_2q"],
"(full Trotter) ->",
stats["aqc_trotter"]["depth_2q"],
"(AQC + Trotter)",
)
observables: ['Z_0', 'Z_1', 'Z_2', 'Z_3', 'Z_4', 'Z_5', 'Z_6', 'Z_7']
shape: (9, 8) -> (n_times, n_observables)
first row (t = 0, the prepared state): [1. 1. 1. 1. 1. 1. 1. 1.]
last row (t = t_steps * dt): [0.1442 0.2956 0.4686 0.4877 0.4869 0.4686 0.2963 0.1441]
AQC fidelities: {'1': 1.0, '2': 1.0, '3': 1.0, '4': 1.0, '5': 1.0, '6': 0.9999}
2q depth at the final step: 210 (full Trotter) -> 79 (AQC + Trotter)

Exemplu de hardware

Un apel de funcție cu backend="runtime" transpilează și execută pe un procesor IBM Quantum real, cu atenuarea de eroare încorporată a funcției: decuplare dinamică (XY4), twirling de gate-uri și eliminarea erorii de citire prin twirling (TREX). backend_name selectează dispozitivul; dacă îl omiți, funcția alege cel mai puțin ocupat.

Nimic din codul științific nu se schimbă. Ceea ce diferă față de exemplul de simulator este lungimea lanțului, numărul de pași Trotter, planul de compresie, backend-ul și setările explicite de atenuare acoperite în secțiunea următoare.

Dimensionarea jobului pentru hardware-ul de control

estimator_options este intrarea care merită setată în mod deliberat. Twirling-ul de gate-uri construiește num_randomizations circuite randomizate separate pentru fiecare PUB, iar întregul job, fiecare PUB cu toate randomizările sale, trebuie să încapă în memoria de instrucțiuni a sistemului de control clasic al QPU-ului. Funcția are implicit 1000 de randomizări, astfel încât o evoluție de 10 pași trimite 11 PUB-uri a câte 1000 de circuite fiecare: aproximativ 11.000 de instanțe de circuit într-un singur job.

Dacă depășești ceea ce poate reține sistemul de control, jobul eșuează cu eroarea 6073. Limitele jobului oferă pragurile și modul de a te raporta la ele, principalul fiind 26,8 milioane de instrucțiuni ale sistemului de control per qubit, aplicat per job, nu per PUB. Decuplarea dinamică adaugă gate-uri care contează în acest total.

Două intrări controlează dimensiunea:

  • estimator_options stabilește bugetul de shot-uri. Numărul total de shot-uri este num_randomizations * shots_per_randomization, așa că poți schimba randomizările contra shot-urilor per randomizare, păstrând statistica, și tot poți micșora programul. Următoarea celulă folosește 100 de randomizări la 200 de shot-uri fiecare, ceea ce înseamnă 20.000 de shot-uri per observabilă și aproximativ o zecime din instanțele de circuit pe care valorile implicite le-ar trimite. Vezi TwirlingOptions și Opțiuni Estimator pentru setul complet de câmpuri.

  • batches împarte PUB-urile pe atâtea joburi runtime separate, ceea ce este remediul sugerat chiar de eroarea 6073 și de ce contează încadrarea per job. Setarea batches=4 trimite aproximativ trei PUB-uri per job în loc de unsprezece deodată, iar joburile pleacă împreună într-un singur lot, astfel încât grupul intră o singură dată în coadă, în loc ca fiecare job să intre separat în coadă.

Reține că un estimator_options furnizat înlocuiește în întregime valorile implicite ale funcției, în loc să se combine cu acestea, astfel încât decuplarea dinamică și TREX sunt reafirmate în celula următoare pentru a le menține activate.

from qiskit.quantum_info import SparsePauliOp

fn = serverless.load("aqc-dynamics-function")

n = 10
H = SparsePauliOp.from_sparse_list(
[("ZZ", [i, i + 1], 1.0) for i in range(n - 1)]
+ [("X", [i], 0.8) for i in range(n)],
num_qubits=n,
)

job = fn.run(
t_steps=10,
aqc_segments=[
{
"n_steps": 3,
"ansatz_steps": 1,
}, # early steps -> shallow 1-layer ansatz
{
"n_steps": 3,
"ansatz_steps": 2,
}, # later steps -> deeper 2-layer ansatz
],
hamiltonian=H,
aqc_options={"max_bond": 32},
backend="runtime",
backend_name="ibm_marrakesh",
# The function defaults to 1000 twirling randomizations, which was too large
# for this device. Total shots is num_randomizations *
# shots_per_randomization, so this is 20,000 shots per observable.
estimator_options={
"dynamical_decoupling": {"enable": True, "sequence_type": "XY4"},
"twirling": {
"enable_gates": True,
"num_randomizations": 100,
"shots_per_randomization": 200,
},
"resilience": {"measure_mitigation": True},
},
)
print("job ID (save this to reconnect later):", job.job_id)
job ID (save this to reconnect later): 7229a8bf-9f83-4785-8dd4-489844abc2d9
Reconectarea la un job cu rulare îndelungată

O rulare pe hardware nu este rapidă, iar majoritatea timpului este clasic, nu pe QPU. Compresia AQC rulează în interiorul funcției înainte ca ceva să ajungă la QPU, iar coada QPU se adaugă peste asta. Nu trebuie să menții acest notebook sau kernel deschis în timp ce rulează.

Copiază ID-ul jobului afișat de celula precedentă și salvează-l. Următoarele trei celule îți permit să reiei rularea mai târziu:

  1. Reconectare, necesară doar într-o nouă sesiune de kernel: rulează din nou celula Autentificare pentru a recrea serverless, apoi reconstruiește handle-ul job din ID-ul salvat. Sari peste această celulă dacă te afli încă în sesiunea în care ai trimis jobul, deoarece handle-ul este deja activ.

  2. Verifică starea: rulează din nou până când raportează DONE.

  3. Obține rezultatul: rulează doar după ce starea este DONE.

Lipește ID-ul salvat peste substituentul din următoarea celulă de reconectare.

# Reconnect to a previously submitted job by its ID. Only needed in a NEW kernel
# session; if you are still in the session where you submitted, the `job` handle
# from the preceding cell is already live, so skip this cell. Replace the ID that follows with your own.
job = serverless.get_job_by_id("<your job ID>")
# Re-run this until it reports DONE, then fetch the result in the following cell.
print(job.status())
DONE
import numpy as np

# Run this only once the preceding status cell reports DONE. result() blocks until
# the job finishes, so calling it earlier just waits.
result = job.result()
ev = np.array(result["expectation_values"])

print("backend:", result["metadata"]["execution_backend"])
print("shape:", ev.shape, "-> (n_times, n_observables)")
print("last row (t = t_steps * dt):", np.round(ev[-1], 4))
print(
"AQC fidelities:",
{k: round(v, 4) for k, v in result["metadata"]["aqc_fidelities"].items()},
)

# What the compression bought: 2-qubit depth at the final time step.
stats = result["metadata"]["circuit_stats"][
str(result["metadata"]["t_steps"])
]
print(
"2q depth at the final step:",
stats["full_trotter"]["depth_2q"],
"(full Trotter) ->",
stats["aqc_trotter"]["depth_2q"],
"(AQC + Trotter)",
)
backend: runtime
shape: (11, 10) -> (n_times, n_observables)
last row (t = t_steps * dt): [0.1504 0.1361 0.218 0.2144 0.2275 0.1783 0.1749 0.1599 0.0915 0.0922]
AQC fidelities: {'1': 1.0, '2': 1.0, '3': 1.0, '4': 1.0, '5': 0.9999, '6': 0.9999}
2q depth at the final step: 342 (full Trotter) -> 171 (AQC + Trotter)

Pași următori

Recomandări