Algoritma SqDRIFT untuk estimasi ground state
- Python
- C++
Unduh notebook Python untuk menjalankan tutorial ini sendiri. Notebook ini mengunduh input FCIDump jika belum ada.
Estimasi penggunaan: 180 detik pada prosesor Heron r3 (CATATAN: Ini hanya perkiraan. Runtime Anda mungkin berbeda.)
Hasil pembelajaran
-
Pelajari cara membuat circuit dengan kedalaman lebih kecil dibandingkan Trotterization
-
Telusuri workflow end-to-end untuk estimasi ground state menggunakan qDRIFT dan SQD
-
Pelajari cara menggunakan
qiskit-fermionsbersama dengan addon Qiskit lainnya untuk mengimplementasikan workflow semacam itu
Prasyarat
-
Baca ringkasan Sample-based quantum diagonalization (SQD)
-
Baca pelajaran Sample-based Krylov Quantum Diagonalization (SKQD)
Latar Belakang
SqDRIFT adalah varian dari SKQD yang menggantikan kebutuhan untuk memilih ansatz tempat men-sampling bitstring dengan sebuah ensemble circuit evolusi-waktu yang dibangun langsung dari Hamiltonian target. Ini dicapai dengan men-subsample operator evolusi-waktu yang lebih kecil dari Hamiltonian berdasarkan koefisiennya, yang dikenal sebagai metode Trotterization qDRIFT.
Kedua implementasi memakai Qiskit Fermions untuk merepresentasikan Hamiltonian molekul dan membangun circuit untuk algoritma qDRIFT. Implementasi Python memakai pass transpiler fermionik, sedangkan implementasi C++ secara eksplisit men-sampling grup dan memetakannya ke operator qubit.
Misalkan Hamiltonian berbentuk:
dengan, tanpa mengurangi keumuman, kita mensyaratkan dan eigenvalue terbesar dari sama dengan dalam nilai absolut. Prefaktor bertanda atau kompleks apa pun diserap ke dalam , sehingga koefisien adalah bobot yang benar-benar positif sementara membawa arah dari setiap suku. Di sini adalah jumlah suku (atau, setelah pengelompokan, jumlah grup) dalam Hamiltonian; ini adalah properti dari Hamiltonian dan berbeda dari jumlah operator yang di-sampling ke dalam satu circuit, yang ditulis di bawah ini.
Algoritma qDRIFT kemudian merealisasikan, untuk waktu target , sebuah operator , di mana berjalan dari dan menandakan circuit SqDRIFT ke-, didefinisikan sebagai:
Di sini adalah jumlah operator yang di-sampling per circuit dan adalah jumlah circuit dalam ensemble. Produk tersebut berjalan pada pengambilan, bukan pada seluruh suku Hamiltonian, dan karena suku-suku diambil dengan pengembalian (with replacement), yang sama dapat muncul lebih dari sekali dalam satu .
Besaran:
adalah norma dari koefisien-koefisien tersebut, sehingga masing-masing dari langkah berevolusi selama durasi yang sama terlepas dari suku mana yang diambil. Keseragaman sudut langkah adalah fitur khas dari qDRIFT: sebuah koefisien memengaruhi hasil melalui seberapa sering sukunya diambil, bukan melalui seberapa jauh suku tersebut dirotasi. Indeks-indeks tersebut di-sampling dari distribusi:
sehingga deret adalah barisan acak indeks suku yang diambil dari distribusi ini. Karena positif dan berjumlah , ini adalah distribusi probabilitas yang ternormalisasi, dan ekspektasi channel yang dihasilkan pada pengambilan acak ini mengaproksimasi evolusi di bawah , dengan error yang menurun seiring bertambahnya . Perhatikan bahwa error aproksimasi bergantung pada , bukan pada jumlah suku .
(Paper SqDRIFT menulis jumlah suku sebagai dan panjang deret sebagai ; di sini kita menggunakan dan agar keduanya tetap jelas berbeda.)
Tutorial ini menunjukkan cara menghasilkan ensemble dari circuit yang diacak semacam itu. Setelah kita membuat circuit-circuit ini, mirip dengan cara kita membuat subspace Krylov untuk operator yang berbeda-beda, kita men-sampling bitstring dari beberapa operator semacam itu dengan parameter waktu yang berbeda-beda. Ini memastikan overlap yang lebih tinggi antara vektor ground state dan bitstring yang di-sampling.
Persyaratan
Sebelum memulai tutorial ini, pastikan Anda sudah menginstal
- Virtual environment Python (>=3.10)
- pip>=25.1
qiskit~= 2.5qiskit-fermions==0.1.0 (Perhatikan bahwa namanya berbentuk jamak)- numpy
- pyscf
qiskit-aerqiskit-ibm-runtimeqiskit-addon-sqd
Anda dapat menginstal semua paket yang diperlukan dengan:
pip install "qiskit~=2.5" "qiskit-fermions==0.1.0" qiskit-aer qiskit-ibm-runtime qiskit-addon-sqd pyscf numpy
Penyiapan
# Third-party scientific computing
import numpy as np
# PySCF
from pyscf import tools, ao2mo, fci
# Qiskit core
from qiskit import transpile
from qiskit.primitives import BitArray
# Qiskit Aer
from qiskit_aer import AerSimulator
# IBM Quantum Compute Service
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
# Qiskit Fermions
from qiskit_fermions.operators.library import FCIDump
from qiskit_fermions.operators import FermionOperator
from qiskit_fermions.operators.terms.filtering import filter_diagonal_terms
from qiskit_fermions.operators.terms.grouping import (
group_terms_by_electronic_structure,
)
from qiskit_fermions.operators.terms.ordering import canonical_order
from qiskit_fermions.circuit import FermionicCircuit
from qiskit_fermions.circuit.library import Evolution
from qiskit_fermions.transpiler import FermionicPassManager
from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
from qiskit_fermions.transpiler.passes import QDriftTrotterization
from qiskit_fermions.circuit.library import InitializeModes
# Qiskit addon SQD
from qiskit_addon_sqd.fermion import (
diagonalize_fermionic_hamiltonian,
SCIResult,
)
Implementasi
Langkah 1: Memetakan input klasik ke masalah kuantum
Langkah-langkah Python di bawah ini memakai simulator Aer. Contoh hardware menyusul setelahnya.
Membaca dan Menyiapkan FCIDump
Untuk tutorial ini, kita akan memuat Hamiltonian struktur elektronik untuk nitrogen (N2). Ada cara lain untuk membuat operator fermionik juga. Lihat dokumentasi di qiskit_fermions.operators.library.
Tentang FCIDump ini. File N2_sto_3g mendeskripsikan molekul nitrogen () dalam basis minimal STO-3G pada jarak antar-atom 1.09 , panjang ikatan kesetimbangan eksperimental. Headernya mendeklarasikan NORB=10, NELEC=14, dan MS2=0: 10 orbital spasial (sehingga 20 orbital spin, dan 20 qubit di bawah Jordan-Wigner), 14 elektron dalam singlet spin, jadi tujuh elektron dan tujuh elektron . Semua orbital diberi label simetri 1, artinya tidak ada simetri gugus titik yang dimanfaatkan. Karena ini adalah dump STO-3G ruang penuh, tidak ada orbital yang dibekukan dan ruang korelasinya cukup kecil sehingga energi referensi FCI eksak dapat dihitung secara klasik untuk perbandingan, seperti ditunjukkan pada blok kode berikutnya.
File yang setara dapat dibuat ulang dengan PySCF:
from pyscf import gto, scf, tools
mol = gto.M(atom="N 0 0 0; N 0 0 1.09", basis="sto-3g", symmetry=False)
mf = scf.RHF(mol).run()
tools.fcidump.from_scf(mf, "N2_sto_3g")
Karena integral bergantung pada orbital SCF yang telah konvergen, file yang dibuat ulang mungkin berbeda dari file bawaan dalam hal fase atau urutan orbital; total energinya tidak terpengaruh.
Mendapatkan file. Temukan FCIDump di repositori GitHub ini. Anda dapat menjalankan kode di bawah untuk mengambilnya ke lokasi yang diharapkan oleh sisa tutorial ini.
Pertama kita menggunakan cisolver yang disediakan oleh pyscf untuk mendapatkan energi referensi. Ini adalah energi ground state sebenarnya dari molekul yang kita kerjakan. Untuk ini kita akan mendeklarasikan norb dan nelec terlebih dahulu, yang masing-masing adalah jumlah orbital dan jumlah elektron. Kemudian kita mendeklarasikan h1e dan h2e, yang masing-masing adalah integral satu- dan dua-elektron. Semua ini nantinya juga akan digunakan untuk SQD.
import os
from urllib.request import urlopen
# The FCIDump is stored with this tutorial in the Qiskit documentation repository.
FCIDUMP_URL = "https://raw.githubusercontent.com/Qiskit/documentation/main/docs/tutorials/assets/sqdrift/fcidump_files/N2_sto_3g"
FCIDUMP_PATH = "fcidump_files/N2_sto_3g"
if not os.path.exists(FCIDUMP_PATH):
os.makedirs(os.path.dirname(FCIDUMP_PATH), exist_ok=True)
with urlopen(FCIDUMP_URL) as response:
contents = response.read()
with open(FCIDUMP_PATH, "wb") as f:
f.write(contents)
print(f"Downloaded FCIDump to {FCIDUMP_PATH}")
else:
print(f"Using existing FCIDump at {FCIDUMP_PATH}")
name = "fcidump_files/N2_sto_3g"
fcidump = tools.fcidump.read(name)
# Extract metadata from the FCIDump header
norb = fcidump["NORB"] # number of spatial orbitals
nelec = fcidump["NELEC"] # total number of electrons
e_nuc = fcidump["ECORE"] # nuclear repulsion / core energy
ms2 = fcidump["MS2"] # 2S (spin)
num_elec_a = (nelec + ms2) // 2 # alpha electrons
num_elec_b = (nelec - ms2) // 2 # beta electrons
# Reconstruct full 4-index ERIs from the FCIDump (stored in 8-fold symmetry)
h1e = fcidump["H1"] # shape (norb, norb)
h2e = ao2mo.restore( # shape (norb, norb, norb, norb)
1, fcidump["H2"], norb
)
cisolver = fci.direct_spin1.FCI()
cisolver.max_cycle = 200
cisolver.conv_tol = 1e-12
e_fci, _ = cisolver.kernel(
h1e,
h2e,
norb,
(num_elec_a, num_elec_b),
ecore=e_nuc, # adds nuclear repulsion to the final energy
)
reference_energy = e_fci
print(f"Reference FCI Energy = {reference_energy:.10f} Ha")
nuclear_repulsion_energy = fcidump["ECORE"]
print(f"Nuclear Repulsion Energy = {nuclear_repulsion_energy:.10f} Ha")
Reference FCI Energy = -107.6481842917 Ha
Nuclear Repulsion Energy = 23.7887003074 Ha
Memuat Hamiltonian
Dengan data yang diperlukan sudah siap, kita membaca Hamiltonian dari file FCI dalam format yang kompatibel dengan qiskit-fermions
fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb
Workflow fermionik dengan qiskit-fermions
Pertama kita akan memetakan Hamiltonian ke dalam model circuit fermionik menggunakan qiskit-fermions, yang menyediakan pass transpiler dan gate khusus untuk circuit fermionik. Pass ini nantinya akan digunakan sebelum pass transpiler Qiskit yang biasa untuk workflow ini.
Pengelompokan suku
Untuk memastikan reproduktibilitas hasil, pertama kita menggunakan canonical_order untuk mengurutkan suku hanya berdasarkan strukturnya. Urutan operator dalam daftar canon dengan demikian bersifat tetap. Ini memastikan reproduktibilitas operator yang dibuat karena pass QDriftTrotterization yang akan kita gunakan nanti mengambil sampel indeks acak untuk membuat operator qDRIFT.
Pada langkah ini, kita memanfaatkan banyak simetri yang ada dalam Hamiltonian struktur elektronik dengan mengelompokkan suku-suku terkait yang memiliki koefisien identik. Meskipun melakukan hal ini mengubah distribusi koefisien operator yang menjadi sumber sampling protokol qDRIFT, hal ini tidak memengaruhi jaminan konvergensinya. Yang penting, pengelompokan suku yang terkait melalui simetri menghasilkan pembatalan suku Pauli yang menguntungkan dan kedalaman circuit yang lebih pendek secara keseluruhan saat mengevolusikan keadaan di bawah aksinya.
qiskit-fermions menyediakan fungsi group_terms_by_electronic_structure yang melakukan pengelompokan ini untuk kita.
Perhatikan bahwa group_terms_by_electronic_structure mengasumsikan suku dengan urutan normal.
Penyaringan suku diagonal
Kita menghapus suku diagonal dari Hamiltonian yang digunakan untuk membuat circuit, sehingga slot sampling qDRIFT digunakan untuk suku yang memindahkan populasi antar konfigurasi. Suku semacam ini paling baik disaring dari Hamiltonian pada titik ini, sebelum gate Evolution dibuat pada langkah berikutnya.
Suku yang dimaksud adalah suku yang diagonal dalam basis bilangan okupasi, yaitu hasil kali operator bilangan . Ada tiga jenis suku yang termasuk dalam deskripsi ini:
-
offset energi konstan, hasil kali dari nol operator bilangan, yang evolusi waktunya hanya menyumbang fase global;
-
operator bilangan individual , yang evolusi waktunya tereduksi menjadi rotasi satu-qubit;
-
hasil kali orde lebih tinggi seperti .
Dengan sendirinya, tidak satu pun dari suku ini memindahkan populasi antar konfigurasi bilangan okupasi; mereka hanya bekerja pada fase konfigurasi yang sudah ada. Namun, mereka bukan tidak berpengaruh: fase relatif tersebut memberi masukan pada interferensi yang dihasilkan oleh suku eksitasi belakangan dalam circuit, sehingga menyaringnya mengubah evolusi yang sebenarnya dihasilkan dan dapat mengubah distribusi sampling. Ini adalah aproksimasi yang disengaja dalam langkah pembuatan circuit, dibuat untuk memfokuskan sampling pada suku eksitasi, bukan langkah yang membiarkan distribusi yang disampel tidak berubah. Berbeda dengan pengelompokan simetri di atas, yang membiarkan jaminan konvergensi qDRIFT tetap utuh, filter ini mengubah operator yang dievolusikan. Circuit oleh karena itu tidak lagi mengaproksimasi evolusi di bawah Hamiltonian penuh, dan batas error qDRIFT berlaku untuk operator yang telah difilter, bukan operator aslinya. Hal ini dapat diterima di sini karena circuit hanyalah heuristik sampling yang digunakan untuk mengusulkan konfigurasi: tidak ada suku yang hilang dari estimasi energi itu sendiri, karena filter hanya berlaku pada Hamiltonian yang digunakan untuk membangun circuit, sementara diagonalisasi klasik nantinya menggunakan Hamiltonian penuh, termasuk suku diagonal. Akurasi SQD bergantung pada langkah klasik tersebut, yang tetap variasional dalam subruang yang disampel terlepas dari bagaimana konfigurasi diusulkan.
Fungsi filter_diagonal_terms() menghapus suku semacam itu dari sebuah operator secara in-place. Fungsi ini mengidentifikasinya dari struktur urutan normalnya — multiset mode kreasi yang cocok dengan multiset mode anihilasi — sehingga hanya valid pada operator yang sudah berurutan normal. Asumsi ini tidak diperiksa saat runtime.
# Apply automatic grouping
canon = canonical_order(hamiltonian.normal_ordered().simplify(atol=1e-16))
exit_code = group_terms_by_electronic_structure(
canon, num_modes, two_body_physicist_order=False
)
filter_diagonal_terms(canon)
print(len(canon.groups))
5060
Sekarang setelah kita mengelompokkan suku-suku dalam Hamiltonian, kita akan menentukan parameter berikut untuk membuat ensemble circuit:
- Jumlah circuit yang dibuat:
num_circuits - Panjang setiap circuit, dinyatakan dalam jumlah kelompok eksitasi:
num_exc - Faktor untuk waktu evolusi yang berbeda:
times
Membuat circuit fermionik
Sekarang kita akan membuat circuit fermionik untuk setiap langkah waktu. Setiap circuit akan terdiri dari satu gate evolusi, dengan waktu evolusi yang telah kita deklarasikan sebelumnya. Operator evolusinya adalah Hamiltonian. Selanjutnya kita menjalankan pass transpiler pada circuit ini untuk membuat circuit qDRIFT.
Persiapan ansatz
Kita menyiapkan keadaan Hartree-Fock menggunakan kelas InitializeModes. Untuk nitrogen, prosesnya cukup dengan menerapkan gate X pada num_elec_a qubit pertama lalu pada num_elec_b qubit, yang keduanya sama dengan tujuh untuk nitrogen. Keadaan ini merepresentasikan tujuh elektron dan tujuh elektron dari nitrogen.
# SqDRIFT parameters
times = [1.0, 10.0] # Total evolution times used for the subspace creation
num_exc = 10 # Number of excitation groups per circuit
num_circuits = 200 # Number of circuits to generate
init_circuits = []
hf_gate = InitializeModes.from_hartree_fock(norb, (num_elec_a, num_elec_b))
for time in times:
evo_gate = Evolution(num_modes, canon, time)
circ = FermionicCircuit(num_modes)
circ.append(hf_gate, circ.modes)
circ.append(evo_gate, circ.modes)
init_circuits.append(circ)
Langkah 2: Optimalkan masalah untuk eksekusi perangkat keras kuantum
Sekarang setelah kita memiliki circuit kita, pertama kita akan menggunakan pass yang tersedia di qiskit-fermions untuk melakukan optimalisasi tingkat fermionik, diikuti dengan mentranspile circuit kita untuk backend pilihan. Karena ini adalah eksperimen simulator, kita akan melakukan ini terlebih dahulu untuk AerSimulator.
Perhitungan bobot untuk setiap kelompok
Pada langkah ini, kita melakukan sampling qDRIFT dari suku-suku secara stokastik dengan probabilitas sebanding dengan koefisiennya dalam Hamiltonian. Pass transpiler qDRIFT melakukan ini untuk kita. Sekarang kita dapat membuat circuit yang lebih dangkal yang dapat dieksekusi di perangkat keras dengan lebih efisien meskipun konektivitas qubit terbatas, bahkan ketika Hamiltonian mengandung kopling jarak jauh dan suku yang lebih dari kuadratik. Setelah pengelompokan suku, pass ini menyampel operator berdasarkan bobotnya. Untuk setiap operator , bobot didefinisikan sebagai berikut:
Karena suku-suku sudah dikelompokkan di Langkah 1, setiap di sini adalah satu grup utuh: adalah rata-rata koefisien absolut dari suku-suku dalam grup , dan tiap suku dalam grup dievolusikan dengan koefisien yang direduksi menjadi tandanya saja.
Optimalisasi fermionik dan native perangkat keras
Fungsi generate_preset_jw_pass_manager() mengembalikan MultiStagePassManager yang mengambil FermionicCircuit dan menghasilkan sirkuit akhir yang teroptimasi yang dapat kita transpile untuk dijalankan di perangkat keras kita. Kita mengganti tahap optimalisasi bawaannya dengan FermionicPassManager yang berisi pass QDriftTrotterization kita:
-
Pass
QDriftTrotterizationmenggunakan perhitungan bobot dan sampling secara internal untuk membuat sirkuit yang akan kita gunakan untuk sampling -
Pass
RelabelModesadalah pass optimalisasi lain yang dapat digunakan untuk mempermutasi mode fermionik guna mengoptimalkan konektivitas antar qubit dan mengurangi kedalaman gate; baca selengkapnya di referensi API
Tahap-tahap lain dari MultiStagePassManager berjalan secara otomatis dan menangani pemetaan fermion-ke-qubit secara penuh:
-
F2QLayout: Preset pass manager menerapkan pass
TrivialF2QLayout, yang secara trivial memetakan bit fermionik ke qubit. -
F2QSynth: Pass transpilasi untuk memetakan instruksi circuit berbasis fermion ke instruksi berbasis qubit.
qdrift = QDriftTrotterization(num_exc, rng=19)
pm = generate_preset_jw_pass_manager()
pm.optimization = FermionicPassManager([qdrift])
sqdrift_circuits = []
for circ in init_circuits:
sqdrift_circuits += (pm.run(circ) for _ in range(num_circuits))
for circ in sqdrift_circuits:
circ.measure_all()
print(len(sqdrift_circuits))
400
Sekarang setelah kita selesai dengan optimalisasi tingkat fermionik, kita dapat mentranspile circuit untuk eksekusi di simulator.
simulator = AerSimulator()
shots = 100
transpiled_circuits = transpile(sqdrift_circuits, simulator)
Langkah 3: Eksekusi menggunakan primitif Qiskit
Sekarang setelah kita memiliki circuit kita, kita dapat menjalankannya menggunakan primitif Qiskit di AerSimulator. Kita akan menggabungkan semua count dari berbagai circuit. Kita mengubahnya menjadi vektor boolean sebelum akhirnya melakukan post-processing dengan SQD.
print(
f"Executing {len(transpiled_circuits)} circuits with {shots} shots each..."
)
job = simulator.run(transpiled_circuits, shots=shots)
result = job.result()
all_counts = [result.get_counts(i) for i in range(len(transpiled_circuits))]
print(len(all_counts), "length before post processing")
Executing 400 circuits with 100 shots each...
400 length before post processing
Langkah 4: Post-process dan kembalikan hasil dalam format klasik yang diinginkan
Menggunakan bitstring untuk SQD
Sekarang kita dapat menjalankan skema diagonalisasi pada bitstring yang dipilih untuk menemukan eigenvalue terendah yang akan sesuai dengan energi ground state molekul. Kita membuat fungsi callback, mendeklarasikan okupansi awal, dan mengatur parameter sebelum akhirnya menjalankan skema diagonalisasi. Fungsi callback digunakan untuk mencetak iterasi saat ini dan estimasi eigenvalue saat ini pada setiap iterasi.
Akhirnya, untuk mendapatkan estimasi ground state, kita menambahkan nuclear_repulsion_energy ke energi hasilnya.
Catatan: Dimensi subruang tidak tetap di seluruh iterasi, bahkan pada simulator tanpa noise — setiap subsampel mengambil sekumpulan konfigurasi yang berbeda, dan langkah recovery membentuk ulang pool di antara iterasi, sehingga dimensi yang dilaporkan bervariasi dari satu subsampel ke subsampel berikutnya. Sampling tanpa noise tidak dengan sendirinya mematok dimensi subruang yang dipilih. Namun, eksekusi di perangkat keras cenderung memberikan subruang yang secara sistematis lebih besar, karena shot yang mengandung noise merusak simetri jumlah partikel dan recovery konfigurasi mengubahnya menjadi vektor basis tambahan. Karena itu, kita juga akan memperkenalkan langkah tambahan untuk pruning bitstring pada bagian perangkat keras.
combined_counts = {}
for counts in all_counts:
for bitstring, count in counts.items():
combined_counts[bitstring] = combined_counts.get(bitstring, 0) + count
bit_array = BitArray.from_counts(combined_counts)
print(bit_array.num_shots)
print(f" Alpha electrons: {num_elec_a}")
print(f" Beta electrons: {num_elec_b}")
print(f" Number of orbitals: {norb}")
print(f" Number of spin orbitals (qubits): {2*norb}")
print(f"Integral shapes: h1e={h1e.shape}, h2e={h2e.shape}")
# SQD parameters
samples_per_batch = 300
num_batches = 3
max_iterations = 5
initial_occupancies = (
np.array([1] * num_elec_a + [0] * (norb - num_elec_a)), # alpha
np.array([1] * num_elec_b + [0] * (norb - num_elec_b)), # beta
)
result_history = []
def callback(results: list[SCIResult]):
result_history.append(results)
iteration = len(result_history)
print(f"Iteration {iteration}")
for i, result in enumerate(results):
print(f"\tSubsample {i}")
print(f"\t\tEnergy: {result.energy + nuclear_repulsion_energy}")
print(
f"\t\tSubspace dimension: {np.prod(result.sci_state.amplitudes.shape)}"
)
# Run SQD with configuration recovery
print("\nRunning SQD with configuration recovery...")
result = diagonalize_fermionic_hamiltonian(
h1e,
h2e,
bit_array,
samples_per_batch=samples_per_batch,
norb=norb,
nelec=(num_elec_a, num_elec_b),
num_batches=num_batches,
energy_tol=1e-3,
occupancies_tol=1e-3,
max_iterations=max_iterations,
initial_occupancies=initial_occupancies,
seed=42,
callback=callback,
)
computed_energy = result.energy + nuclear_repulsion_energy
print("FINAL SQD RESULTS")
print(f"Orbital occupancies (alpha): {result.orbital_occupancies[0]}")
print(f"Orbital occupancies (beta): {result.orbital_occupancies[1]}")
energy_error = abs(computed_energy - reference_energy)
print(f"Reference Energy: {reference_energy:.10f} Ha")
print(f"Computed Energy: {computed_energy:.10f} Ha")
print(f"Error: {energy_error:.10e} Ha")
40000
Alpha electrons: 7
Beta electrons: 7
Number of orbitals: 10
Number of spin orbitals (qubits): 20
Integral shapes: h1e=(10, 10), h2e=(10, 10, 10, 10)
Running SQD with configuration recovery...
Iteration 1
Subsample 0
Energy: -107.64767025226178
Subspace dimension: 5538
Subsample 1
Energy: -107.64772799119115
Subspace dimension: 5670
Subsample 2
Energy: -107.64765512281548
Subspace dimension: 5767
Iteration 2
Subsample 0
Energy: -107.64795948524682
Subspace dimension: 6080
Subsample 1
Energy: -107.64806617355072
Subspace dimension: 6300
Subsample 2
Energy: -107.64802260640258
Subspace dimension: 6308
FINAL SQD RESULTS
Orbital occupancies (alpha): [0.99999464 0.99999643 0.99584631 0.99332984 0.96684652 0.96686712
0.99301927 0.0373282 0.0373266 0.00944508]
Orbital occupancies (beta): [0.99999462 0.99999643 0.9958261 0.99332349 0.96684268 0.96686737
0.99302145 0.03733536 0.03733399 0.0094585 ]
Reference Energy: -107.6481842917 Ha
Computed Energy: -107.6480661736 Ha
Error: 1.1811817564e-04 Ha
Contoh perangkat keras
Contoh ini menggunakan 20 qubit (10 orbital spasial). Pilihan itu sekadar demi kepraktisan agar tutorial berjalan cepat, bukan batas atas yang mutlak untuk metode ini.
Biaya langkah klasik tidak ditentukan langsung oleh jumlah qubit. SQD mendiagonalisasi Hamiltonian yang diproyeksikan ke subruang yang dibentang oleh konfigurasi yang disampel, sehingga yang menentukan biaya klasik adalah dimensi subruang terpilih tersebut — di sini ditentukan oleh samples_per_batch, num_batches, dan berapa banyak konfigurasi berbeda yang benar-benar dihasilkan oleh circuit — bersama dengan aljabar linear jarang yang diperlukan untuk menerapkan Hamiltonian terproyeksi. Ruang CI penuh tumbuh secara kombinatorial dengan orbital dan elektron, tetapi subruang terpilih adalah irisan kecil dan dapat disesuaikan darinya, dan kita mengendalikan ukurannya secara langsung. Akibatnya, jumlah qubit dan kesulitan klasik dapat bervariasi secara agak independen: ruang orbital yang lebih luas yang disampel ke dalam subruang sedang bisa lebih murah daripada sistem yang lebih kecil yang didiagonalisasi pada subruang yang sangat besar.
Dalam praktiknya, ukuran sistem yang layak bergantung pada dimensi subruang yang Anda butuhkan untuk akurasi yang diinginkan dan pada memori serta core yang tersedia untuk eigensolver. Ruang orbital yang lebih besar biasanya memang memerlukan subruang yang lebih besar untuk mencapai akurasi kimia, dan itulah yang pada akhirnya mendorong penggunaan sumber daya terdistribusi — lihat qiskit-addon-sqd-hpc untuk menskalakan langkah ini. Alih-alih mengasumsikan batas tetap, pendekatan praktisnya adalah mengamati dimensi subruang yang dilaporkan dan konvergensi energi di seluruh iterasi lalu meningkatkan ukuran subruang hingga energi berhenti membaik atau Anda kehabisan memori yang tersedia.
Catatan: Karena error sampling akibat noise pada perangkat keras, subruang yang dibuat untuk diagonalisasi pada eksekusi perangkat keras akan lebih besar daripada yang kita dapatkan saat menggunakan simulator. Meskipun ini meningkatkan dimensi subruang yang ingin kita diagonalisasi, workflow tetap memberi kita jawaban yang akurat berkat ketahanan SQD terhadap noise.
Pruning string yang keliru
Di sini kita dapat memilih untuk melakukan langkah tambahan. Ketika kita memiliki semua bitstring dari eksekusi circuit, kita bisa menyaring bitstring yang tidak valid sebelum menjalankan SQD, atau melanjutkan tanpa pruning. Melewati pruning umumnya lebih disukai untuk eksekusi perangkat keras, karena hal ini membiarkan shot yang simetrinya rusak tetap tersedia untuk recovery konfigurasi, yang dapat memperbaikinya menjadi konfigurasi valid dan dengan demikian memperluas subruang alih-alih membuang shot tersebut begitu saja.
Karena nitrogen hanya dapat memiliki tujuh elektron dan tujuh elektron , bitstring apa pun yang memiliki lebih atau kurang dari tujuh angka 1 pada paruh pertama dan paruh kedua output dapat dibuang. Kita mendefinisikan fungsi yang memeriksa apakah bitstring valid, dan jika tidak, membuangnya. Setelah kita menyaring bitstring yang keliru, sisanya dikirim ke skema diagonalisasi. Gunakan flag PRUNE di bawah untuk beralih antara dua perilaku ini.
Perlu diingat bahwa pruning hanyalah salah satu dari beberapa pilihan yang membentuk subruang akhir, selain jumlah circuit, kumpulan waktu evolusi, dan penyaringan suku diagonal. Membandingkan eksekusi yang di-pruning dengan yang tidak di-pruning hanya informatif jika semua hal lain tetap sama; tab C++ membahas ini lebih rinci, karena ia melakukan postseleksi alih-alih recovery dan juga berbeda dalam parameter lainnya.
name = "fcidump_files/N2_sto_3g"
fcidump = tools.fcidump.read(name)
# Extract metadata from the FCIDump header
norb = fcidump["NORB"] # number of spatial orbitals
nelec = fcidump["NELEC"] # total number of electrons
e_nuc = fcidump["ECORE"] # nuclear repulsion / core energy
ms2 = fcidump["MS2"] # 2S (spin)
num_elec_a = (nelec + ms2) // 2 # alpha electrons
num_elec_b = (nelec - ms2) // 2 # beta electrons
# Reconstruct full 4-index ERIs from the FCIDump (stored in 8-fold symmetry)
h1e = fcidump["H1"] # shape (norb, norb)
h2e = ao2mo.restore( # shape (norb, norb, norb, norb)
1, fcidump["H2"], norb
)
cisolver = fci.direct_spin1.FCI()
cisolver.max_cycle = 200
cisolver.conv_tol = 1e-12
e_fci, _ = cisolver.kernel(
h1e,
h2e,
norb,
(num_elec_a, num_elec_b),
ecore=e_nuc, # adds nuclear repulsion to the final energy
)
reference_energy = e_fci
print(f"Reference FCI Energy = {reference_energy:.10f} Ha")
nuclear_repulsion_energy = fcidump["ECORE"]
print(f"Nuclear Repulsion Energy = {nuclear_repulsion_energy:.10f} Ha")
fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb
# Apply automatic grouping
canon = canonical_order(hamiltonian.normal_ordered().simplify(atol=1e-16))
exit_code = group_terms_by_electronic_structure(
canon, num_modes, two_body_physicist_order=False
)
filter_diagonal_terms(canon)
print(len(canon.groups))
# SqDRIFT parameters
times = [1.0, 10.0] # Total evolution times used for the subspace creation
num_exc = 10 # Number of excitation groups per circuit
num_circuits = 200 # Number of circuits to generate
init_circuits = []
hf_gate = InitializeModes.from_hartree_fock(norb, (num_elec_a, num_elec_b))
for time in times:
evo_gate = Evolution(num_modes, canon, time)
circ = FermionicCircuit(num_modes)
circ.append(hf_gate, circ.modes)
circ.append(evo_gate, circ.modes)
init_circuits.append(circ)
# Calculate weights for sampling (one per group)
qdrift = QDriftTrotterization(num_exc, rng=19)
pm = generate_preset_jw_pass_manager()
pm.optimization = FermionicPassManager([qdrift])
sqdrift_circuits = []
for circ in init_circuits:
sqdrift_circuits += (pm.run(circ) for _ in range(num_circuits))
for circ in sqdrift_circuits:
circ.measure_all()
print(len(sqdrift_circuits))
# This example assumes you have saved your IBM Quantum Platform account locally.
service = QiskitRuntimeService(channel="ibm_quantum_platform")
# Select backend (choose based on qubit requirements)
backend = service.least_busy(
operational=True,
simulator=False,
min_num_qubits=2 * norb,
)
print(f"Selected backend: {backend.name} ({backend.num_qubits} qubits)")
# Transpile for hardware
transpiled_circuits = transpile(
sqdrift_circuits,
backend=backend,
optimization_level=3,
seed_transpiler=42,
)
shots = 100
sampler = Sampler(mode=backend)
sampler.options.environment.job_tags = ["TUT_SQDRIFT"]
job = sampler.run(transpiled_circuits, shots=shots)
result = job.result()
# Extract counts from SamplerV2 results
all_counts = [pub_result.data.meas.get_counts() for pub_result in result]
# Set to True to filter out bitstrings that violate electron-number conservation
PRUNE = False
def is_valid_bitstring(
bitstring: str, norb: int, nelec: tuple[int, int]
) -> bool:
n_alpha, n_beta = nelec
return (
len(bitstring) == 2 * norb
and bitstring[norb:].count("1") == n_alpha
and bitstring[:norb].count("1") == n_beta
)
if PRUNE:
all_counts_filtered = []
for counts in all_counts:
filtered_count = {}
for key in counts:
if not is_valid_bitstring(key, norb, (num_elec_a, num_elec_b)):
continue
elif key not in filtered_count.keys():
filtered_count[key] = counts[key]
else:
filtered_count[key] += counts[key]
all_counts_filtered.append(filtered_count)
all_counts = all_counts_filtered
combined_counts = {}
for counts in all_counts:
for bitstring, count in counts.items():
combined_counts[bitstring] = combined_counts.get(bitstring, 0) + count
bit_array = BitArray.from_counts(combined_counts)
print(bit_array.num_shots)
print("Electron configuration:")
print(f" Total electrons: {nelec}")
print(f" Alpha electrons: {num_elec_a}")
print(f" Beta electrons: {num_elec_b}")
print(f" Number of orbitals: {norb}")
print(f" Number of spin orbitals (qubits): {2*norb}")
print(f"Integral shapes: h1e={h1e.shape}, h2e={h2e.shape}")
# SQD parameters
samples_per_batch = 300
num_batches = 3
max_iterations = 5
initial_occupancies = (
np.array([1] * num_elec_a + [0] * (norb - num_elec_a)), # alpha
np.array([1] * num_elec_b + [0] * (norb - num_elec_b)), # beta
)
result_history = []
def callback(results: list[SCIResult]):
result_history.append(results)
iteration = len(result_history)
print(f"Iteration {iteration}")
for i, result in enumerate(results):
print(f"\tSubsample {i}")
print(f"\t\tEnergy: {result.energy + nuclear_repulsion_energy}")
print(
f"\t\tSubspace dimension: {np.prod(result.sci_state.amplitudes.shape)}"
)
# Run SQD with configuration recovery
print("\nRunning SQD with configuration recovery...")
result = diagonalize_fermionic_hamiltonian(
h1e,
h2e,
bit_array,
samples_per_batch=samples_per_batch,
norb=norb,
nelec=(num_elec_a, num_elec_b),
num_batches=num_batches,
energy_tol=1e-3,
occupancies_tol=1e-3,
max_iterations=max_iterations,
initial_occupancies=initial_occupancies,
seed=42,
callback=callback,
)
computed_energy = result.energy + nuclear_repulsion_energy
print("FINAL SQD RESULTS")
print(f"Orbital occupancies (alpha): {result.orbital_occupancies[0]}")
print(f"Orbital occupancies (beta): {result.orbital_occupancies[1]}")
energy_error = abs(computed_energy - reference_energy)
print(f"Reference Energy: {reference_energy:.10f} Ha")
print(f"Computed Energy: {computed_energy:.10f} Ha")
print(f"Error: {energy_error:.10e} Ha")
Reference FCI Energy = -107.6481842917 Ha
Nuclear Repulsion Energy = 23.7887003074 Ha
5060
400
Selected backend: ibm_aachen (156 qubits)
40000
Electron configuration:
Total electrons: 14
Alpha electrons: 7
Beta electrons: 7
Number of orbitals: 10
Number of spin orbitals (qubits): 20
Integral shapes: h1e=(10, 10), h2e=(10, 10, 10, 10)
Running SQD with configuration recovery...
Iteration 1
Subsample 0
Energy: -107.64593072647523
Subspace dimension: 7221
Subsample 1
Energy: -107.6458270048177
Subspace dimension: 7209
Subsample 2
Energy: -107.64007673117075
Subspace dimension: 7138
Iteration 2
Subsample 0
Energy: -107.64757372124944
Subspace dimension: 9009
Subsample 1
Energy: -107.64674060104392
Subspace dimension: 8245
Subsample 2
Energy: -107.64731360491942
Subspace dimension: 8178
Iteration 3
Subsample 0
Energy: -107.64765518770588
Subspace dimension: 8835
Subsample 1
Energy: -107.64767975712016
Subspace dimension: 8649
Subsample 2
Energy: -107.64761634415606
Subspace dimension: 8648
FINAL SQD RESULTS
Orbital occupancies (alpha): [0.99999504 0.9999964 0.99590318 0.9932359 0.96697158 0.96696295
0.99298797 0.03728154 0.03728186 0.00938359]
Orbital occupancies (beta): [0.9999946 0.99999641 0.99590413 0.99323077 0.96697361 0.96696174
0.99298424 0.03728121 0.03728169 0.00939159]
Reference Energy: -107.6481842917 Ha
Computed Energy: -107.6476797571 Ha
Error: 5.0453460619e-04 Ha
Source C++, proyek CMake, dan petunjuk build ada di direktori docs/tutorials/assets/sqdrift/cpp pada repositori dokumentasi Qiskit. Input FCIDump ada di direktori tetangganya, fcidump_files. Clone repositorinya, atau unduh kedua direktori, untuk membangun dan menjalankan tutorial ini sendiri.
Estimasi penggunaan: 180 detik pada prosesor Heron r3 (CATATAN: Ini hanya perkiraan. Runtime Anda mungkin berbeda.)
Hasil pembelajaran (C++)
-
Pelajari cara membuat circuit dengan kedalaman lebih kecil dibandingkan Trotterization
-
Telusuri workflow end-to-end untuk estimasi ground state menggunakan qDRIFT dan SQD
-
Pelajari cara menggunakan
qiskit-fermionsbersama dengan addon Qiskit lainnya untuk mengimplementasikan workflow semacam itu
Prasyarat (C++)
-
Baca ringkasan Sample-based quantum diagonalization (SQD)
-
Baca pelajaran Sample-based Krylov Quantum Diagonalization (SKQD)
Latar Belakang (C++)
SqDRIFT adalah varian dari SKQD yang menggantikan kebutuhan untuk memilih ansatz tempat men-sampling bitstring dengan sebuah ensemble circuit evolusi-waktu yang dibangun langsung dari Hamiltonian target. Ini dicapai dengan men-subsample operator evolusi-waktu yang lebih kecil dari Hamiltonian berdasarkan koefisiennya, yang dikenal sebagai metode Trotterization qDRIFT.
Kedua implementasi memakai Qiskit Fermions untuk merepresentasikan Hamiltonian molekul dan membangun circuit untuk algoritma qDRIFT. Implementasi Python memakai pass transpiler fermionik, sedangkan implementasi C++ secara eksplisit men-sampling grup dan memetakannya ke operator qubit.
Misalkan Hamiltonian berbentuk:
dengan, tanpa mengurangi keumuman, kita mensyaratkan dan eigenvalue terbesar dari sama dengan dalam nilai absolut. Prefaktor bertanda atau kompleks apa pun diserap ke dalam , sehingga koefisien adalah bobot yang benar-benar positif sementara membawa arah dari setiap suku. Di sini adalah jumlah suku (atau, setelah pengelompokan, jumlah grup) dalam Hamiltonian; ini adalah properti dari Hamiltonian dan berbeda dari jumlah operator yang di-sampling ke dalam satu circuit, yang ditulis di bawah ini.
Algoritma qDRIFT kemudian merealisasikan, untuk waktu target , sebuah operator , di mana berjalan dari dan menandakan circuit SqDRIFT ke-, didefinisikan sebagai:
Di sini adalah jumlah operator yang di-sampling per circuit dan adalah jumlah circuit dalam ensemble. Produk tersebut berjalan pada pengambilan, bukan pada seluruh suku Hamiltonian, dan karena suku-suku diambil dengan pengembalian (with replacement), yang sama dapat muncul lebih dari sekali dalam satu .
Besaran:
adalah norma dari koefisien-koefisien tersebut, sehingga masing-masing dari langkah berevolusi selama durasi yang sama terlepas dari suku mana yang diambil. Keseragaman sudut langkah adalah fitur khas dari qDRIFT: sebuah koefisien memengaruhi hasil melalui seberapa sering sukunya diambil, bukan melalui seberapa jauh suku tersebut dirotasi. Indeks-indeks tersebut di-sampling dari distribusi:
sehingga deret adalah barisan acak indeks suku yang diambil dari distribusi ini. Karena positif dan berjumlah , ini adalah distribusi probabilitas yang ternormalisasi, dan ekspektasi channel yang dihasilkan pada pengambilan acak ini mengaproksimasi evolusi di bawah , dengan error yang menurun seiring bertambahnya . Perhatikan bahwa error aproksimasi bergantung pada , bukan pada jumlah suku .
(Paper SqDRIFT menulis jumlah suku sebagai dan panjang deret sebagai ; di sini kita menggunakan dan agar keduanya tetap jelas berbeda.)
Tutorial ini menunjukkan cara menghasilkan ensemble dari circuit yang diacak semacam itu. Setelah kita membuat circuit-circuit ini, mirip dengan cara kita membuat subspace Krylov untuk operator yang berbeda-beda, kita men-sampling bitstring dari beberapa operator semacam itu dengan parameter waktu yang berbeda-beda. Ini memastikan overlap yang lebih tinggi antara vektor ground state dan bitstring yang di-sampling.
Persyaratan (C++)
Implementasi ini berjalan di hardware IBM Quantum® dan memakai executable SBD eksternal untuk diagonalisasi klasik. Implementasi ini tidak menyertakan contoh simulator Python.
Ikuti petunjuk build C++ untuk menginstal compiler, CMake, Rust, Boost, dan dependensi Qiskit. Instal Rust dengan rustup: dependensi Qiskit mem-pin versi Rust-nya sendiri, yang diunduh rustup secara otomatis, tetapi Rust yang diinstal dari Homebrew atau package manager Linux mungkin terlalu lama. Langkah SBD terakhir juga membutuhkan MPI, OpenMP, dan BLAS/LAPACK. Windows saat ini tidak didukung oleh implementasi ini.
Pakai source C++ lengkap dan proyek CMake dari repositori. Pertahankan direktori fcidump_files yang bersebelahan saat mengunduh contohnya. Dari hasil checkout repositori dokumentasi, build dengan perintah berikut. Build pertama mengunduh dan mengompilasi semua dependensi dan memakan waktu beberapa menit.
cd docs/tutorials/assets/sqdrift/cpp
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
Build menulis executable SqDRIFT ke docs/tutorials/assets/sqdrift/cpp (root proyek), bukan ke build/. Jalankan ./SqDRIFT dari root proyek agar ia bisa menemukan ../fcidump_files/N2_sto_3g. Semua perintah lain di tab ini juga dijalankan dari root proyek kecuali dinyatakan lain. Jika ./SqDRIFT gagal dengan error shared library hilang, ikuti petunjuk library-path khusus platform di panduan build; di macOS, executable biasanya jalan tanpa itu.
Penyiapan (C++)
Potongan kode di bawah ini adalah kutipan dari program SqDRIFT.cpp yang lengkap. Build dan jalankan file itu sebagai satu program.
Sebelum menjalankan SqDRIFT, pastikan token API IBM Quantum Anda sudah dikonfigurasi untuk IBM Cloud Platform.
Jika Anda punya Python dengan qiskit-ibm-runtime terinstal, cara termudah untuk membuat file konfigurasi dengan format yang benar adalah:
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(channel="ibm_quantum_platform", token="YOUR_IBM_QUANTUM_TOKEN_HERE")
Ini menulis ~/.qiskit/qiskit-ibm.json (Linux/macOS) atau %USERPROFILE%\.qiskit\qiskit-ibm.json (Windows) dengan bentuk yang tepat. Kalau Anda lebih suka membuat file secara manual:
{
"default-ibm-quantum-platform": {
"channel": "ibm_quantum_platform",
"token": "YOUR_IBM_QUANTUM_TOKEN_HERE"
}
}
Dapatkan API key Anda dari IBM Quantum Platform.
Catatan: File konfigurasi yang hilang atau salah format menyebabkan panic Rust unwrap() yang tidak informatif saat startup, bukan error yang jelas. Periksa lagi nama key (default-ibm-quantum-platform) dan nilai channel (ibm_quantum_platform) jika Anda menemui masalah.
Implementasi (C++)
Langkah 1: Memetakan input klasik ke masalah kuantum (C++)
Muat Hamiltonian molekul
char filename[] = "../fcidump_files/N2_sto_3g";
QfFCIDump* fcidump = qf_fcidump_from_file(filename);
if (fcidump == nullptr) {
std::cerr << "Failed to load FCIDump!" << std::endl;
return 1;
}
uint32_t norb = qf_fcidump_norb(fcidump);
uint32_t nelec = qf_fcidump_nelec(fcidump);
uint32_t n_alpha = nelec / 2;
uint32_t n_beta = nelec - n_alpha;
uint32_t num_modes = 2 * norb;
uint32_t num_qubits = num_modes;
std::cout << "✓ Loaded N2 molecule (" << norb << " orbitals, "
<< num_qubits << " qubits, " << nelec << " electrons)" << std::endl;
Output:
✓ Loaded N2 molecule (10 orbitals, 20 qubits, 14 electrons)
Memuat Hamiltonian molekul dari file FCIDump. Jumlah qubit sama dengan dua kali jumlah orbital (spin-up dan spin-down).
Buat Hamiltonian dan lakukan normal-ordering
// Create and normal order Hamiltonian
QfFermionOperator* hamiltonian = qf_ferm_op_from_fcidump(fcidump);
QfFermionOperator* normal_ordered = qf_ferm_op_normal_ordered(hamiltonian, nullptr);
Mengonversi data FCIDump menjadi operator fermionik dan menerapkan normal ordering untuk menyederhanakan struktur operator.
Kelompokkan suku berdasarkan struktur elektronik
// Group terms by electronic structure
QfExitCode exit_code = qf_ferm_op_group_terms_by_electronic_structure(
normal_ordered, num_modes, false
);
if (exit_code != QfExitCode_Success) {
std::cerr << "Failed to group terms!" << std::endl;
return 1;
}
uint32_t num_groups = qf_ferm_op_num_groups(normal_ordered);
std::cout << "Grouped into " << num_groups << " groups" << std::endl;
// Split into group operators
QfFermionOperator** group_ops = new QfFermionOperator*[num_groups];
qf_ferm_op_split_out_groups(normal_ordered, nullptr, 0, group_ops);
Output:
Grouped into 1902 groups
Mengelompokkan suku-suku Hamiltonian yang terkait berdasarkan struktur elektronik sebelum membangun circuit evolusinya.
Hitung bobot sampling, lalu normalisasi dan petakan ke operator qubit
Urutan penting di sini. Normalisasi menskalakan ulang setiap koefisien menjadi magnitudo satu di tempat, sehingga bobot sampling harus dihitung lebih dulu. Kalau normalisasi dijalankan duluan, setiap terbaca persis 1.0 dan bobot tiap grup runtuh menjadi sekadar hitungan jumlah sukunya — distribusi sampling lalu akan ditentukan oleh ukuran grup, bukan oleh koefisien fisiknya.
Bobot tiap grup adalah koefisien absolut rata-rata dari suku-sukunya. Pass QDriftTrotterization di tab Python memakai bobot yang sama untuk Hamiltonian yang dikelompokkan. Kalau memakai jumlah, akan ikut membesar seiring ukuran grup, yang pada gilirannya membuat waktu evolusi condong.
// Calculate sampling weights BEFORE normalization.
std::vector<double> weights(num_groups);
double total_weight = 0.0;
for (uint32_t i = 0; i < num_groups; i++) {
QkComplex64* coeffs;
uint64_t num_terms;
qf_ferm_op_get_coeffs(group_ops[i], &coeffs, &num_terms);
double abs_sum = 0.0;
for (uint64_t j = 0; j < num_terms; j++) {
abs_sum += std::sqrt(coeffs[j].re * coeffs[j].re +
coeffs[j].im * coeffs[j].im);
}
weights[i] = (num_terms > 0) ? (abs_sum / static_cast<double>(num_terms)) : 0.0;
total_weight += weights[i];
}
std::cout << "Total weight (λ): " << total_weight << std::endl;
// Normalize the evolution operators term-by-term, now that the sampling
// weights above have already captured the original magnitudes.
for (uint32_t i = 0; i < num_groups; i++) {
QkComplex64* coeffs;
uint64_t num_terms;
qf_ferm_op_get_coeffs(group_ops[i], &coeffs, &num_terms);
for (uint64_t j = 0; j < num_terms; j++) {
const double magnitude = std::sqrt(coeffs[j].re * coeffs[j].re +
coeffs[j].im * coeffs[j].im);
if (magnitude > 0.0) {
coeffs[j].re /= magnitude;
coeffs[j].im /= magnitude;
}
}
}
QkObs** qubit_ops = new QkObs*[num_groups];
for (uint32_t i = 0; i < num_groups; i++) {
QfExitCode jw_exit = qf_ferm_op_jordan_wigner(group_ops[i], num_qubits, &qubit_ops[i]);
if (jw_exit != QfExitCode_Success) {
std::cerr << "Failed to map group " << i << std::endl;
return 1;
}
}
std::cout << "Mapped all " << num_groups << " normalized groups to qubit operators" << std::endl;
Output:
Total weight (λ): 337.261
Mapped all 1902 normalized groups to qubit operators
Bobot adalah koefisien absolut rata-rata atas suku-suku grup , dibaca dari koefisien aslinya. Normalisasi kemudian menskalakan ulang tiap suku fermionik menjadi magnitudo satu sebelum transformasi Jordan-Wigner, sehingga hanya fase/tandanya yang tersisa pada operator yang dievolusikan. Ini sesuai dengan evolusi suku ternormalisasi pada qDRIFT: magnitudo koefisien menentukan probabilitas sampling lewat , sedangkan evolusi circuit memakai suku yang sudah dinormalisasi sehingga koefisien besar tidak dihitung dua kali.
Sampling set operator
// SqDRIFT Sampling: Create operator sets
const int num_circuits = 100; // Number of circuits to create
const int ops_per_circuit = 10; // Operators per circuit
const double time_step = 1; // Time step for evolution
std::cout << "\n Generating " << num_circuits << " circuits with "
<< ops_per_circuit << " operators each..." << std::endl;
std::mt19937 gen(42);
std::discrete_distribution<> dist(weights.begin(), weights.end());
// Store sampled operator indices
std::vector<std::vector<int>> operator_sets(num_circuits);
for (int i = 0; i < num_circuits; i++) {
operator_sets[i].resize(ops_per_circuit);
for (int j = 0; j < ops_per_circuit; j++) {
operator_sets[i][j] = dist(gen);
}
}
Output:
Generating 100 circuits with 10 operators each...
Mengambil set operator untuk 100 circuit acak, masing-masing berisi 10 grup yang disampel dari distribusi berbobot. Langkah berikutnya mengubah set-set ini menjadi ensemble circuit qDRIFT.
Langkah 2: Bangun circuit qDRIFT (C++)
Pada versi C++, langkah ini membangun circuit dari set operator yang disampel. Transpilasi untuk backend target terjadi di Langkah 3, tepat sebelum tiap circuit dikirim.
// Create Suzuki-Trotter circuits by composing all operators
std::vector<Qiskit::circuit::QuantumCircuit> circuits;
circuits.reserve(num_circuits);
const uint32_t trotter_order = 1;
const uint32_t trotter_reps = 1;
const bool preserve_order = false;
const bool insert_barriers = false;
// Gate name mapping is static — compute once before all loops
auto name_map = Qiskit::circuit::get_standard_gate_name_mapping();
for (int circuit_idx = 0; circuit_idx < num_circuits; circuit_idx++) {
// Create initial circuit with state preparation using C++ API
Qiskit::circuit::QuantumCircuit qc(num_qubits, num_qubits);
// State preparation: apply X gates to spin-up qubits 0..n_alpha-1
// and spin-down qubits norb..norb+n_beta-1 (derived from FCIDump NELEC)
for (uint32_t q = 0; q < n_alpha && q < num_qubits; q++) {
qc.x(q);
}
for (uint32_t q = norb; q < norb + n_beta && q < num_qubits; q++) {
qc.x(q);
}
// For each operator in this circuit's set
for (int op_idx = 0; op_idx < ops_per_circuit; op_idx++) {
int group_idx = operator_sets[circuit_idx][op_idx];
double evolution_time = (total_weight * time_step) / ops_per_circuit;
// Create Suzuki-Trotter evolution circuit
QkCircuit* evolution_circuit = qk_circuit_library_suzuki_trotter(
qubit_ops[group_idx], trotter_order, trotter_reps,
evolution_time, preserve_order, insert_barriers
);
if (evolution_circuit != nullptr) {
// Get number of instructions
uint32_t num_ops = qk_circuit_num_instructions(evolution_circuit);
// Manually append each instruction
for (uint32_t i = 0; i < num_ops; i++) {
QkCircuitInstruction inst;
qk_circuit_get_instruction(evolution_circuit, i, &inst);
// Prepare qubit vector
std::vector<uint32_t> qubits(inst.num_qubits);
for (uint32_t j = 0; j < inst.num_qubits; j++) {
qubits[j] = inst.qubits[j];
}
// Get operation kind
QkOperationKind kind = qk_circuit_instruction_kind(evolution_circuit, i);
// Get mutable pointer to circuit
QkCircuit* mutable_circuit = qc.get_rust_circuit().get();
// Append based on operation type
if (kind == QkOperationKind_Gate) {
// Convert gate name string to QkGate enum
std::string gate_name(inst.name);
auto gate_it = name_map.find(gate_name);
if (gate_it != name_map.end()) {
QkGate gate_enum = gate_it->second.gate_map();
qk_circuit_parameterized_gate(
mutable_circuit,
gate_enum, // Use QkGate enum, not string
qubits.data(),
inst.params
);
} else {
std::cerr << "Unknown gate in evolution circuit: " << gate_name << std::endl;
qk_circuit_instruction_clear(&inst);
qk_circuit_free(evolution_circuit);
return 1;
}
} else if (kind == QkOperationKind_Barrier) {
qk_circuit_barrier(
mutable_circuit,
qubits.data(),
inst.num_qubits
);
} else if (kind == QkOperationKind_Reset) {
qk_circuit_reset(
mutable_circuit,
qubits[0]
);
}
qk_circuit_instruction_clear(&inst);
}
qk_circuit_free(evolution_circuit);
}
}
circuits.push_back(qc);
// Display progress
if ((circuit_idx + 1) % 10 == 0) {
std::cout << " Created " << (circuit_idx + 1) << "/"
<< num_circuits << " circuits" << std::endl;
}
}
std::cout << "✓ Created all " << num_circuits << " Suzuki-Trotter circuits" << std::endl;
Output:
Created 10/100 circuits
Created 20/100 circuits
Created 30/100 circuits
Created 40/100 circuits
Created 50/100 circuits
Created 60/100 circuits
Created 70/100 circuits
Created 80/100 circuits
Created 90/100 circuits
Created 100/100 circuits
✓ Created all 100 Suzuki-Trotter circuits
Program lengkap kemudian mencetak statistik circuit dan indeks operator dari circuit 0, yang dihilangkan di sini.
Untuk setiap operator yang disampel, dibuat circuit evolusi Suzuki-Trotter dengan waktu , di mana adalah jumlah operator yang disampel per circuit dan adalah langkah waktu. Karena setiap grup yang disampel dinormalisasi suku demi suku sebelum pemetaan, evolusi ini hanya menerapkan fase/tanda dari tiap suku selama rotasi; magnitudonya berkontribusi hanya lewat distribusi sampling. State awal Hartree-Fock disiapkan dengan menerapkan gate X pada qubit 0-6 dan 10-16 (tujuh elektron di setiap sektor spin). Instruksi tiap circuit evolusi ditambahkan secara manual ke circuit utama memakai API C++.
Langkah 3: Eksekusi menggunakan primitif Qiskit (C++)
std::cout << "\n Connecting to IBM Quantum Cloud..." << std::endl;
// Initialize IBM Quantum Compute Service using qiskit-cpp
Qiskit::service::QiskitRuntimeService service;
std::cout << "✓ Connected to IBM Quantum" << std::endl;
// Get backend
const std::string backend_name = "ibm_fez";
auto backend = service.backend(backend_name);
if (backend.name().empty()) {
std::cerr << "Backend " << backend_name
<< " is not available to this account." << std::endl;
return 1;
}
std::cout << "✓ Selected backend: " << backend.name() << std::endl;
// Create sampler primitive
const int32_t shots = 100; // Number of shots per circuit
Qiskit::primitives::BackendSamplerV2 sampler(backend, shots);
std::cout << "\n Submitting " << num_circuits << " circuits ("
<< shots << " shots each)..." << std::endl;
// Submit one job per circuit.
std::vector<std::shared_ptr<Qiskit::primitives::BasePrimitiveJob>> jobs;
for (int i = 0; i < num_circuits; i++) {
for (uint32_t q = 0; q < num_qubits; q++) {
circuits[i].measure(q, q);
}
Qiskit::circuit::QuantumCircuit transpiled_qc =
Qiskit::compiler::transpile(circuits[i], backend, 2, 1.0, 42);
std::vector<Qiskit::primitives::SamplerPub> pubs;
pubs.push_back(Qiskit::primitives::SamplerPub(transpiled_qc, shots));
jobs.push_back(sampler.run(pubs));
}
std::cout << "\n⏳ Waiting for " << num_circuits << " jobs to complete..." << std::endl;
// Collect results — poll each job and extract bitstrings
std::vector<boost::dynamic_bitset<>> all_bitstrings;
for (int i = 0; i < num_circuits; i++) {
while (!jobs[i]->in_final_state()) {
sleep(5);
}
if (jobs[i]->done()) {
auto result = jobs[i]->result();
if (result.size() > 0) {
auto& pub_result = result[0];
auto& bit_array = pub_result.data();
for (size_t s = 0; s < bit_array.num_shots(); s++) {
auto sample = bit_array[s].to_string();
boost::dynamic_bitset<> bs(num_qubits);
for (uint32_t bit = 0; bit < num_qubits; bit++) {
bs[bit] = (sample[num_qubits - 1 - bit] == '1');
}
all_bitstrings.push_back(bs);
}
}
} else {
std::cout << " Circuit " << (i+1) << " did not complete successfully" << std::endl;
}
}
std::cout << "\n✓ Collected " << all_bitstrings.size()
<< " total bitstrings from all circuits" << std::endl;
Terhubung ke IBM Quantum, memilih backend berdasarkan nama, men-transpile tiap circuit untuk backend itu, dan mengirim satu job per circuit: 100 job Sampler terpisah masing-masing 100 shot pada register 20 qubit. Lalu ia mem-polling tiap job sampai selesai dan mengumpulkan bitstring dari result[0]. Setiap job memakai waktu QPU dan mengantre sendiri-sendiri, jadi langkah ini akan memakan waktu jauh lebih lama daripada satu job batch.
Hasil perangkat keras yang dikutip di halaman ini berasal dari eksekusi di ibm_pittsburgh (100 circuit × 100 shot), dengan backend_name diubah dari default ibm_fez. Sampel yang tersimpan diproses ulang dengan urutan bit determinan yang sudah dikoreksi seperti ditunjukkan di Langkah 4. Noise perangkat keras bervariasi antar eksekusi, jadi hitungan dan energi akhir Anda akan berbeda.
Output:
Connecting to IBM Quantum Cloud...
✓ Connected to IBM Quantum
✓ Selected backend: ibm_pittsburgh
Submitting 100 circuits (100 shots each)...
⏳ Waiting for 100 jobs to complete...
✓ Collected 10000 total bitstrings from all circuits
Mengganti backend: Ubah backend_name di SqDRIFT.cpp untuk menargetkan perangkat lain; service.backends() menampilkan daftar yang bisa dijangkau akunmu.
Langkah 4: Post-process dan kembalikan hasil dalam format klasik yang diinginkan (C++)
Postseleksi bitstring dan tulis string CI
// Postselect bitstrings by Hamming weight (derived from FCIDump NELEC)
std::vector<double> bitstring_weights(all_bitstrings.size(), 1.0);
auto [filtered_bitstrings, filtered_weights] = Qiskit::addon::sqd::postselect_bitstrings(
all_bitstrings,
bitstring_weights,
Qiskit::addon::sqd::MatchesRightLeftHamming<uint32_t>(n_alpha, n_beta)
);
std::ignore = filtered_weights;
std::cout << "✓ Postselected " << filtered_bitstrings.size()
<< " bitstrings with Hamming weight ("
<< n_alpha << "," << n_beta << ")" << std::endl;
// Convert bitstrings to CI strings using SQD addon
auto ci_strings = Qiskit::addon::sqd::bitstrings_to_ci_strings_symmetrize_spin(
filtered_bitstrings,
std::nullopt // No dimension limit
);
std::cout << "✓ Generated " << ci_strings.size() << " CI strings" << std::endl;
// Write CI strings to file for SBD
std::ofstream alpha_file("alphadets_from_sqd.txt");
for (const auto& ci_string : ci_strings) {
std::string bitstr;
// SBD expects the highest orbital index first (bit zero on the right).
boost::to_string(ci_string, bitstr);
alpha_file << bitstr << "\n";
}
alpha_file.close();
std::cout << "✓ Wrote " << ci_strings.size()
<< " CI strings to alphadets_from_sqd.txt" << std::endl;
std::cout << " Ready for SBD diagonalization!" << std::endl;
Output:
✓ Postselected 1137 bitstrings with Hamming weight (7,7)
✓ Generated 49 CI strings
✓ Wrote 49 CI strings to alphadets_from_sqd.txt
Ready for SBD diagonalization!
Mempostseleksi bitstring dengan Hamming weight yang benar (n_alpha elektron spin-up, n_beta elektron spin-down, diturunkan dari field NELEC pada FCIDump), mengonversinya menjadi string Configuration Interaction (CI) memakai simetrisasi spin, dan menuliskannya ke alphadets_from_sqd.txt untuk Selected Basis Diagonalization berikutnya.
Bersihkan resource
// Cleanup
// circuits vector will be automatically cleaned up (RAII)
// Free qubit operators
for (uint32_t i = 0; i < num_groups; i++) {
qk_obs_free(qubit_ops[i]);
}
delete[] qubit_ops;
// Free fermionic operators
for (uint32_t i = 0; i < num_groups; i++) {
qf_ferm_op_free(group_ops[i]);
}
delete[] group_ops;
qf_ferm_op_free(normal_ordered);
qf_ferm_op_free(hamiltonian);
qf_fcidump_free(fcidump);
return 0;
Membebaskan dengan benar semua memori dan resource yang dialokasikan. std::vector<QuantumCircuit> di C++ dibersihkan otomatis lewat RAII. Operator qubit, operator fermionik, dan data FCIDump dibebaskan secara manual.
Sekarang setelah kita membuat basis untuk memproyeksikan Hamiltonian, kita bisa melanjutkan ke proses diagonalisasi untuk mendapatkan estimasi ground state. Jalankan perintah-perintah berikut dari root proyek, docs/tutorials/assets/sqdrift/cpp.
Build binary SBD diag
CMake mengambil SBD sebagai source saja ke deps/sbd/ (build utama sengaja melewati kompilasinya). Aplikasi SBD memakai Makefile biasa, dan SBD sendiri membutuhkan MPI, OpenMP, dan BLAS/LAPACK — jadi make biasa belum cukup sampai semuanya terinstal dan file Configuration aplikasi cocok dengan toolchain Anda.
Makefile membaca flag compiler dan link-nya dari file Configuration di direktori yang sama. Versi yang dikirim upstream disetel untuk satu layout macOS/Homebrew tertentu dan tidak akan ter-build apa adanya di mesin standar — terutama -I/opt/homebrew/opt/llvm/include tidak berisi omp.h (letaknya di direktori resource clang yang spesifik versi), dan clang++ milik Apple langsung menolak -fopenmp:
clang++: error: unsupported option '-fopenmp'
Resep berikut terverifikasi berjalan di macOS (Apple Silicon, Darwin 25.6, Open MPI 5.x + Homebrew LLVM).
macOS (Apple Silicon) — teruji
# 1. Install MPI and an OpenMP-capable compiler.
# Apple clang cannot do OpenMP, so Homebrew's LLVM provides both
# clang++ and libomp. BLAS/LAPACK come from Apple's Accelerate
# framework, which is already part of macOS — no openblas needed.
brew install open-mpi llvm
Lalu ganti deps/sbd/apps/chemistry_tpb_selected_basis_diagonalization/Configuration dengan:
# Path to the SBD library
SBD_PATH=../..
# MPI C++ compiler. OMPI_CXX points Open MPI's wrapper at Homebrew's clang++,
# which (unlike Apple clang) supports -fopenmp.
CCCOM=OMPI_CXX=/opt/homebrew/opt/llvm/bin/clang++ mpicxx
# Build flags. omp.h lives in a version-specific clang resource dir, so let
# Homebrew's clang++ find it itself rather than hardcoding an -I path.
CCFLAGS= -std=c++17 -fopenmp -O3
# Link flags: OpenMP runtime from Homebrew LLVM, BLAS/LAPACK from the
# Accelerate framework that ships with macOS (no openblas install needed).
SYSLIB= -L/opt/homebrew/opt/llvm/lib -lomp -framework Accelerate
# 2. Build
cd deps/sbd/apps/chemistry_tpb_selected_basis_diagonalization
make
cd ../../../..
Build yang berhasil mencetak dua baris compile/link dan menghasilkan binary diag di sebelah Makefile. Satu ld: warning: ignoring duplicate libraries: '-lomp' tidak berbahaya.
macOS Intel: Prefix Homebrew adalah /usr/local, bukan /opt/homebrew, jadi gantilah di kedua path sebelumnya.
Linux (Ubuntu/Debian) — belum teruji
Prasyarat berikut mencakup MPI, OpenMP, dan BLAS/LAPACK. GCC mendukung -fopenmp secara native, jadi tidak perlu override compiler. Ini belum diverifikasi di Linux:
sudo apt install -y libopenmpi-dev libomp-dev libblas-dev liblapack-dev
SBD_PATH=../..
CCCOM=mpicxx
CCFLAGS= -std=c++17 -fopenmp -O3
SYSLIB= -llapack -lblas
Jalankan SqDRIFT untuk membuat string CI
Jika Anda sudah menjalankan ./SqDRIFT sampai Langkah 1–4, alphadets_from_sqd.txt sudah ada di root proyek dan Anda bisa melewati langkah ini. Kalau belum, jalankan dari root proyek:
./SqDRIFT
# Writes alphadets_from_sqd.txt in the project root
Jalankan diagonalisasi
cd deps/sbd/apps/chemistry_tpb_selected_basis_diagonalization
# The FCIDump lives in the shared sqdrift/fcidump_files/ directory, one level
# above this tutorial's project root, hence five levels up from here.
ln -sf ../../../../../fcidump_files/N2_sto_3g fcidump.txt
# alphadets_from_sqd.txt is written by ./SqDRIFT into the project root itself,
# which is four levels up.
ln -sf ../../../../alphadets_from_sqd.txt alphadets.txt
./diag \
--fcidump fcidump.txt \
--adetfile alphadets.txt \
--method 0 \
--iteration 100 \
--block 10 \
--tolerance 1e-8 \
| tee ../../../../sbd_output.txt
cd ../../../..
Parameter:
--fcidump: Path ke file FCIDump yang berisi Hamiltonian--adetfile: Path ke file determinan alpha (string CI dari SqDRIFT)--method 0: Metode diagonalisasi Davidson--iteration 100: Jumlah maksimum iterasi Davidson--block 10: Ukuran blok untuk algoritma Davidson--tolerance 1e-8: Toleransi konvergensi untuk energi
Elapsed time for helper construction 0.001512 (sec)
Elapsed time for init 1e-06 (sec)
Davidson iteration 0.0 (tol=0.1857493937033264): -107.493531425221
Davidson iteration 0.1 (tol=0.0455699976495391): -107.5098150493379 -105.93368397386
Davidson iteration 0.2 (tol=0.009291923190816917): -107.5108356028703 -106.1245977311231 -105.6823366883346
Davidson iteration 0.3 (tol=0.001488674426419356): -107.5108711084406 -106.3219246016818 -105.7376912083761 -105.4392974334491
Davidson iteration 0.4 (tol=0.0003188697654737439): -107.5108722237947 -106.5935152393201 -106.1108017566682 -105.4932621297472
Davidson iteration 0.5 (tol=3.080273116687244e-05): -107.5108722587463 -106.7245930484005 -106.2313070085243 -105.6423237790271
Davidson iteration 0.6 (tol=2.981303155550973e-06): -107.5108722590151 -106.8008116251279 -106.330411722324 -105.69489630447
Davidson iteration 0.7 (tol=4.565075388900609e-07): -107.5108722590186 -106.8090569201597 -106.3345525819528 -106.0578325555555
Davidson iteration 0.8 (tol=4.870689101320012e-08): -107.5108722590186 -106.8098939189322 -106.3973756614146 -106.1617813840531
Davidson iteration 0.9 (tol=6.224330513865847e-09): -107.5108722590187 -106.8159869231416 -106.4315891597535 -106.2222021964013
Elapsed time for davidson 0.031276 (sec)
Elapsed time for diagonalization 0.031286 (sec)
Elapsed time for mult 0.002511 (sec)
Energy = -107.5108722590187
Elapsed time for measurement 4.6e-05 (sec)
Sample-based diagonalization: Energy = -107.5108722590187
Sample-based diagonalization: density = [1.999989340109303,1.99999662423008,1.999199121034164,1.981735389885314,1.999825046204946,1.999847567448985,1.995347419300126,0.01136512984757364,0.009844780833945199,0.002849581105572152
Sample-based diagonalization: carryover bitstrings = [], size = 0
Baris Energy = -107.5108722590187 adalah estimasi ground state di subruang yang disampel.
Apa bedanya dengan contoh Python
Kedua contoh berbeda dalam beberapa hal sekaligus, bukan hanya cara menangani bitstring dengan simetri yang salah, jadi keduanya tidak diharapkan menghasilkan energi yang identik:
| Tab C++ | Tab Python | |
|---|---|---|
| Bitstring dengan simetri salah | Dibuang oleh postselect_bitstrings berdasarkan Hamming weight | Diperbaiki oleh pemulihan konfigurasi iteratif |
| Struktur loop | Sekali jalan: sampling → postseleksi → diagonalisasi sekali | Loop luar: diagonalisasi, baca okupansi orbital, pulihkan konfigurasi, diagonalisasi ulang |
| Jumlah circuit | 100 circuit (num_circuits = 100) | 400 circuit (200 per waktu evolusi) |
| Waktu evolusi | Satu saja (time_step = 1) | Dua waktu, times = [1.0, 10.0] |
| Suku diagonal | Dipertahankan di operator yang disampel | Dihapus oleh filter_diagonal_terms, mengubah distribusi sampling dan circuit |
| Pengurutan suku | Hanya dikelompokkan | canonical_order diterapkan sebelum pengelompokan demi reproduksibilitas |
| Solver diagonalisasi | SBD (diag, binary MPI/OpenMP eksternal) | Solver in-process qiskit-addon-sqd |
| Backend | Tetap berdasarkan nama (backend_name = "ibm_fez"; eksekusi di halaman ini memakai ibm_pittsburgh) | Backend operasional least_busy dengan qubit yang cukup |
| Transpilasi | Tiap circuit terpisah, optimization level 2 | Semua circuit sekaligus, optimization level 3 |
| Pengiriman job | 100 job, satu circuit per job | Satu job Sampler berisi semua 400 circuit |
| Eksekusi simulator | Tidak ada; hanya hardware | Contoh simulator noiseless sebelum eksekusi hardware |
Kedua versi memakai Hamiltonian yang sama, mengelompokkan sukunya berdasarkan struktur elektronik, membobot tiap grup dengan koefisien absolut rata-rata dari sukunya, mulai dari state Hartree-Fock, dan men-sampling 10 grup serta 100 shot per circuit.
Tiap perbedaan memengaruhi subruang yang disampel, dan karena itu energinya, secara independen. Ensemble yang lebih besar dan waktu evolusi kedua yang lebih panjang sama-sama memperluas konfigurasi yang dijelajahi contoh Python; sampling pada lebih dari satu waktu memberi SqDRIFT cakupan ala Krylov atas ruang energi rendah. Memfilter suku diagonal mengubah operator mana yang bisa diambil sampler beserta bobotnya, jadi kedua eksekusi men-sampling distribusi yang berbeda.
Postseleksi adalah filter yang ketat, dan shot dengan jumlah partikel yang salah langsung dibuang. Pada eksekusi di atas, itu membuang sebagian besar data: 1137 dari 10000 shot lolos pengecekan Hamming-weight (7,7) (sekitar 11%), yang menciut menjadi hanya 49 -determinan yang berbeda. Sebaliknya, pemulihan konfigurasi memakai okupansi orbital rata-rata dari diagonalisasi sebelumnya untuk membalik bit dan memperbaiki shot semacam itu, mendaur ulang apa yang akan dibuang postseleksi. Umpan balik itu membuatnya inheren iteratif: recover_configurations di qiskit-addon-sqd-hpc membutuhkan argumen avg_occupancies yang baru ada setelah diagonalisasi dijalankan.
Eksekusi C++ ini menghasilkan subruang berisi 49 -determinan dan energi -107.5109 Ha. Ruang -determinan penuh untuk tujuh elektron di 10 orbital hanya string, jadi memasukkan semua 120 ke binary diag yang sama menghasilkan energi FCI eksak -107.6482 Ha. Jadi eksekusi ini berada sekitar 0.1373 Ha di atas nilai eksak sambil mencakup 49 dari 120 -determinan. Karena SQD bersifat variasional dalam subruang terpilih, subruang yang lebih kecil hanya bisa menaikkan energi, sehingga subruang yang tidak lengkap adalah penjelasan paling langsung untuk selisih ini.
Pengecekan FCI eksak ini hanya mungkin karena contohnya kecil; ini bukan bagian dari workflow SqDRIFT, dan untuk ukuran sistem yang benar-benar membutuhkan SQD, referensi seperti itu tidak ada. Di sini ia hanya berfungsi untuk mengukur seberapa banyak ruang yang berhasil dipulihkan eksekusi ini.
Perbandingan dengan contoh Python tidak terkontrol, jadi perbedaan tidak boleh dikaitkan hanya dengan tidak adanya pemulihan konfigurasi; perhatikan bahwa jumlah circuit, waktu evolusi tunggal, penyaringan suku diagonal, dan noise hardware semuanya berbeda sekaligus dan semuanya membentuk subruang.
Untuk menutup selisih itu, berikut beberapa langkah lanjutan yang bisa dicoba setelah menyelesaikan tutorial ini: naikkan shots, num_circuits, atau ops_per_circuit; lakukan sampling pada lebih dari satu time_step; dan tambahkan loop pemulihan di atas Qiskit::addon::sqd::recover_configurations.
Langkah selanjutnya
-
Sample-based Krylov quantum diagonalization dari model kisi fermionik — tutorial terkait yang memakai circuit evolusi waktu alih-alih ansatz variasional.
-
Sample-based quantum diagonalization dari Hamiltonian kimia — tutorial tentang cara membangun circuit local unitary cluster Jastrow (LUCJ) untuk simulasi kimia kuantum.
-
Paper SqDRIFT — literatur yang menjadi dasar tutorial ini. (Perlu dicatat bahwa beberapa optimisasi yang dibahas di paper ini masih dalam pengerjaan, dan tutorial ini dapat berubah di masa depan mengikuti perkembangan library yang digunakan.)