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țional | Sinteză Trotter → compresie AQC → execuție pe statevector, fake sau runtime, returnând | 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 KCuF. 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
-
Dezarhivează-o în directorul care conține acest notebook.
-
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ă.
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.
| Intrare | Implicit | Descriere |
|---|---|---|
hamiltonian | obligatoriu | Hamiltoniană 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_steps | obligatoriu | Numă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_segments | obligatoriu | Plan de compresie: o listă de {"n_steps": k, "ansatz_steps": m}. sum(n_steps) pași sunt comprimați; restul rulează ca Trotter simplu. |
dt | 0.2 | Timpul 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. |
observables | Z per sit | Orice acceptă EstimatorV2 ca argument observables. O observabilă pentru fiecare coloană de ieșire. |
trotter_options | Suzuki de ordinul 2 | {"method": ..., "synthesis_settings": {...}}. reps și time sunt gestionate de funcție. |
aqc_options | vezi descrierea | max_bond (32), cutoff (1e-8), autodiff_backend ("jax"), fidelity_target (None), optimizer_settings (L-BFGS-B, jac=True, maxiter=300). |
estimator_options | DD, twirling, TREX | EstimatorV2.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_name | cel mai puțin ocupat | Numele backend-ului IBM® pentru runtime, sau un backend fals numit. |
batches | 1 | Împarte circuitele pe N joburi runtime. Un singur lot trimite un singur job și nu creează nicio sesiune. |
parallel_sim | False | Distribuie căile simulatorului local pe toate nucleele disponibile folosind Ray. Nu are efect asupra runtime. |
return_circuits | False | Returnează 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.
backend | Ce este | Credențiale | Note |
|---|---|---|---|
"statevector" | StatevectorEstimator exact | Doar cont Serverless | Calea de referință exactă. Fără timp QPU. |
"fake" | Simulare locală zgomotoasă pe un backend fals Qiskit | Doar cont Serverless | O 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 real | Cont Serverless și o instanță cu acces QPU | backend_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 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_HARDWARE | pregă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_QPU | circuite în execuție (simulatoarele locale marchează acest lucru direct) |
RUNNING: POST_PROCESSING | asamblarea 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_optionsstabilește bugetul de shot-uri. Numărul total de shot-uri estenum_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. Setareabatches=4trimite 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
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:
-
Reconectare, necesară doar într-o nouă sesiune de kernel: rulează din nou celula Autentificare pentru a recrea
serverless, apoi reconstruiește handle-uljobdin ID-ul salvat. Sari peste această celulă dacă te afli încă în sesiunea în care ai trimis jobul, deoarece handle-ul este deja activ. -
Verifică starea: rulează din nou până când raportează
DONE. -
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
-
Parcurge Simulează împrăștierea de neutroni cu un flux de lucru Serverless AQC + Trotter, exemplul complementar care apelează această funcție implementată pentru a calcula factorul de structură dinamică al KCuF.
-
Citește șablonul AQC Dynamics pe GitHub pentru contractul complet de intrare și ieșire, exemple suplimentare și detalii de citare.
-
Răsfoiește depozitul de șabloane Qiskit Function pentru alte șabloane de aplicații construite în același mod.
-
Citește ghidul Qiskit Serverless pentru gestionarea funcțiilor implementate.
-
Aprofundează etapa de compresie AQC cu documentația Qiskit addon: AQC-Tensor.