Algoritma SqDRIFT untuk anggaran keadaan dasar
Anggaran penggunaan: 180 saat pada pemproses Heron r3 (NOTA: Ini hanyalah anggaran. Masa larian anda mungkin berbeza.)
Tutorial ini menggunakan Python. Untuk pelaksanaan C++, termasuk kod sumber dan arahan binaan, lihat tutorial SqDRIFT C++.
Hasil pembelajaran
-
Pelajari cara mencipta litar dengan kedalaman lebih kecil berbanding dengan Trotterisasi
-
Ikuti aliran kerja hujung-ke-hujung untuk anggaran keadaan dasar menggunakan qDRIFT dan SQD
-
Pelajari cara menggunakan
qiskit-fermionsbersama-sama dengan addon Qiskit lain untuk melaksanakan aliran kerja sedemikian
Tutorial ini dipersembahkan sebagai notebook Python untuk tujuan pengajaran.
Prasyarat
-
Baca gambaran keseluruhan Diagonalisasi kuantum berasaskan sampel (SQD)
-
Baca pelajaran Diagonalisasi Kuantum Krylov Berasaskan Sampel (SKQD)
Latar Belakang
SqDRIFT ialah varian SKQD yang menggantikan keperluan untuk memilih ansatz yang darinya bitstring disampel dengan ensembel litar evolusi-masa yang dibina terus daripada Hamiltonian sasaran. Ini dicapai dengan subpensampelan operator evolusi-masa yang lebih kecil daripada Hamiltonian berdasarkan pekalinya, yang dikenali sebagai kaedah Trotterisasi qDRIFT.
Tutorial ini menggunakan Qiskit Fermions untuk mencipta litar fermionik yang lebih semula jadi bagi algoritma qDRIFT, diikuti dengan penggunaan pass susun atur dan sintesis fermionik sebelum memasukkan litar ke dalam saluran paip Qiskit tradisional untuk pelaksanaan perkakasan.
Biarkan Hamiltonian dalam bentuk:
di mana, tanpa kehilangan keumuman, kita memerlukan dan nilai eigen terbesar bagi adalah sama, dalam nilai mutlak, dengan . Sebarang faktor-awal bertanda atau kompleks diserap ke dalam , jadi pekali ialah berat positif secara ketat manakala membawa arah setiap terma. Di sini ialah bilangan terma (atau, selepas pengumpulan, bilangan kumpulan) dalam Hamiltonian; ia ialah ciri Hamiltonian dan berbeza daripada bilangan operator yang disampel ke dalam satu litar, ditulis di bawah.
Algoritma qDRIFT kemudian merealisasikan, bagi masa sasaran , sesuatu operator , di mana berjulat dari dan menandakan litar SqDRIFT ke-, ditakrifkan sebagai:
Di sini ialah bilangan operator yang disampel bagi setiap litar dan ialah bilangan litar dalam ensembel. Hasil darab berjalan merentas cabutan, bukan merentas kesemua terma Hamiltonian, dan kerana terma dicabut dengan penggantian, yang sama boleh muncul lebih daripada sekali dalam satu .
Kuantiti:
ialah norma bagi pekali, jadi setiap satu daripada langkah berevolusi untuk tempoh yang sama tanpa mengira terma mana yang dicabut. Keseragaman sudut langkah ialah ciri khusus qDRIFT: sesuatu pekali mempengaruhi hasil melalui berapa kerap termanya dicabut, bukan melalui berapa jauh terma itu diputar. Indeks disampel daripada taburan:
jadi siri ialah jujukan rawak indeks terma yang dicabut daripada taburan ini. Kerana adalah positif dan berjumlah , ini ialah taburan kebarangkalian ternormal, dan jangkaan saluran yang terhasil merentas cabutan rawak menghampiri evolusi di bawah , dengan ralat yang berkurangan apabila bertambah. Perhatikan bahawa ralat penghampiran bergantung pada dan bukan pada bilangan terma .
(Kertas SqDRIFT menulis bilangan terma sebagai dan panjang jujukan sebagai ; kami menggunakan dan di sini untuk mengekalkan kedua-duanya jelas berbeza.)
Tutorial ini menunjukkan cara menjana ensembel litar rawak sedemikian. Selepas kita mencipta litar ini, sama seperti kita mencipta subruang Krylov bagi operator yang berbeza, kita menyampel bitstring daripada beberapa operator sedemikian dengan parameter masa yang berbeza. Ini memastikan pertindihan yang lebih tinggi antara vektor keadaan dasar dan bitstring yang disampel.
Keperluan
Sebelum memulakan tutorial ini, pastikan anda telah memasang
- Persekitaran maya Python (>=3.10)
- pip>=25.1
- qiskit ~= 2.5
- qiskit-fermions==0.1.0 (Perhatikan bahawa namanya jamak)
- numpy
- pyscf
- qiskit-aer
- qiskit-ibm-runtime
- qiskit-addon-sqd
Anda boleh memasang semua pakej yang diperlukan dengan:
pip install "qiskit~=2.5" "qiskit-fermions==0.1.0" qiskit-aer qiskit-ibm-runtime qiskit-addon-sqd pyscf numpy
Persediaan
# Added by doQumentation — required packages for this notebook
!pip install -q numpy pyscf qiskit qiskit-addon-sqd qiskit-aer qiskit-fermions qiskit-ibm-runtime
# 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,
)
Contoh simulator
Langkah 1: Petakan input klasik kepada masalah kuantum
Membaca dan Menyediakan FCIDump
Untuk tutorial ini, kita akan memuatkan Hamiltonian struktur elektronik bagi nitrogen (N2). Terdapat cara lain untuk mencipta operator fermionik juga. Rujuk dokumentasi di qiskit_fermions.operators.library.
Tentang FCIDump ini. Fail N2_sto_3g menerangkan molekul nitrogen () dalam asas minimum STO-3G pada jarak antara atom 1.09 , iaitu panjang ikatan keseimbangan eksperimen. Pengepalanya mengisytiharkan NORB=10, NELEC=14, dan MS2=0: 10 orbital ruang (maka 20 orbital spin, dan 20 qubit di bawah Jordan-Wigner), 14 elektron dalam singlet spin, jadi tujuh elektron dan tujuh elektron . Semua orbital diberikan label simetri 1, iaitu tiada simetri kumpulan titik yang dieksploitasi. Kerana ini adalah dump STO-3G ruang penuh, tiada orbital yang dibekukan dan ruang korelasi cukup kecil supaya tenaga rujukan FCI yang tepat boleh dikira secara klasik untuk perbandingan, seperti yang ditunjukkan dalam sel seterusnya.
Fail yang setara boleh dijana semula 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")
Oleh kerana kamiran bergantung pada orbital SCF yang telah menumpu, fail yang dijana semula mungkin berbeza daripada fail yang disertakan dari segi fasa atau susunan orbital; jumlah tenaga tidak terjejas.
Mendapatkan fail. Cari FCIDump dalam repositori GitHub ini. Anda boleh jalankan sel di bawah untuk mengambilnya ke lokasi yang dijangkakan oleh selebihnya tutorial ini.
Mula-mula kita guna cisolver yang disediakan oleh pyscf untuk mendapatkan tenaga rujukan. Ini ialah tenaga keadaan asas sebenar bagi molekul yang kita sedang kerjakan. Untuk ini, kita akan mengisytiharkan norb dan nelec dahulu, iaitu bilangan orbital dan bilangan elektron masing-masing. Kemudian kita mengisytiharkan h1e dan h2e, iaitu kamiran satu- dan dua-elektron masing-masing. Semua ini akan digunakan kemudian untuk SQD juga.
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 = "assets/sqdrift/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}")
Using existing FCIDump at assets/sqdrift/fcidump_files/N2_sto_3g
name = "assets/sqdrift/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")
Parsing assets/sqdrift/fcidump_files/N2_sto_3g
Reference FCI Energy = -107.6481842917 Ha
Nuclear Repulsion Energy = 23.7887003074 Ha
Memuatkan Hamiltonian
Dengan data yang diperlukan sudah sedia, kita membaca Hamiltonian daripada fail FCI dalam format yang serasi dengan qiskit-fermions
fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb
Aliran kerja fermionik dengan qiskit-fermions
Kita akan memetakan Hamiltonian terlebih dahulu ke dalam model litar fermionik menggunakan qiskit-fermions, yang menyediakan laluan transpiler dan Gate khusus untuk litar fermionik. Ini akan digunakan kemudian sebelum laluan transpiler tradisional Qiskit untuk aliran kerja ini.
Pengumpulan sebutan
Untuk memastikan kebolehulangan hasil, kita mula-mula menggunakan canonical_order untuk menyusun sebutan berdasarkan strukturnya sahaja. Susunan operator dalam senarai canon dengan itu adalah tetap. Ini memastikan kebolehulangan operator yang dicipta kerana laluan QDriftTrotterization yang akan kita gunakan kelak mensampel indeks rawak untuk mencipta operator qDRIFT.
Dalam langkah ini, kita mengeksploitasi pelbagai simetri yang wujud dalam Hamiltonian struktur elektronik dengan mengumpulkan sebutan berkaitan yang mempunyai pekali yang sama. Walaupun langkah ini mengubah taburan pekali operator yang disampel oleh protokol qDRIFT, ini tidak menjejaskan jaminan penumpuannya. Yang penting, pengumpulan sebutan yang berkaitan melalui simetri menghasilkan pembatalan sebutan Pauli yang baik dan kedalaman litar yang lebih pendek secara keseluruhan apabila mengevolusikan-masa suatu keadaan di bawah tindakannya.
qiskit-fermions menyediakan fungsi group_terms_by_electronic_structure yang melakukan pengumpulan ini untuk kita.
Ambil perhatian bahawa group_terms_by_electronic_structure mengandaikan sebutan susunan normal.
Menapis sebutan pepenjuru
Kita mengeluarkan sebutan pepenjuru daripada Hamiltonian yang digunakan untuk menjana litar, supaya slot pensampelan qDRIFT sebanyak digunakan untuk sebutan yang menggerakkan populasi antara konfigurasi. Sebutan sedemikian sebaiknya ditapis keluar daripada Hamiltonian pada peringkat ini, sebelum Gate Evolution dibina dalam langkah seterusnya.
Sebutan yang dimaksudkan ialah sebutan yang bersifat pepenjuru dalam asas nombor-penghunian, iaitu hasil darab operator nombor . Tiga jenis sebutan tergolong dalam huraian ini:
-
ofset tenaga malar, hasil darab sifar operator nombor, yang evolusi masanya hanya menyumbang fasa global;
-
operator nombor individu , yang evolusi masanya berkurang kepada putaran satu-qubit;
-
hasil darab peringkat lebih tinggi seperti .
Dengan sendirinya, tiada satu pun daripada sebutan ini menggerakkan populasi antara konfigurasi nombor-penghunian; ia hanya bertindak ke atas fasa konfigurasi yang sudah wujud. Namun, ia bukan lengai: fasa relatif tersebut menyumbang kepada interferens yang dihasilkan oleh sebutan pengujaan kemudian dalam litar, jadi menapisnya mengubah evolusi yang sebenarnya dihasilkan dan boleh mengubah taburan pensampelan. Ini adalah penghampiran yang disengajakan dalam langkah penjanaan litar, dibuat untuk memfokuskan pensampelan pada sebutan pengujaan, bukannya langkah yang membiarkan taburan yang disampel tidak terjejas. Berbeza daripada pengumpulan simetri di atas, yang mengekalkan jaminan penumpuan qDRIFT, penapis ini mengubah operator yang dievolusikan. Oleh itu litar tidak lagi menghampiri evolusi di bawah Hamiltonian penuh, dan sempadan ralat qDRIFT terpakai kepada operator yang ditapis dan bukannya operator asal. Ini boleh diterima di sini kerana litar hanyalah heuristik pensampelan yang digunakan untuk mencadangkan konfigurasi: tiada sebutan yang hilang daripada anggaran tenaga itu sendiri, kerana penapis hanya terpakai kepada Hamiltonian yang digunakan untuk membina litar, manakala pepenjuruan klasik kemudiannya menggunakan Hamiltonian penuh, termasuk sebutan pepenjuru. Ketepatan SQD bergantung pada langkah klasik tersebut, yang kekal variasional dalam subruang yang disampel tanpa mengira bagaimana konfigurasi itu dicadangkan.
Fungsi filter_diagonal_terms() mengeluarkan sebutan sedemikian daripada operator secara in-place. Ia mengenal pasti sebutan tersebut daripada struktur susunan normalnya — multiset mod penciptaan yang sepadan dengan multiset mod pemusnahan — jadi ia hanya sah pada operator yang sudah tersusun secara normal. Andaian ini tidak diperiksa semasa masa jalan.
# 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 mengumpulkan sebutan dalam Hamiltonian, kita akan menentukan parameter berikut untuk menjana ensembel litar:
- Bilangan litar yang akan dijana:
num_circuits - Panjang setiap litar dari segi kumpulan pengujaan:
num_exc - Faktor untuk masa evolusi yang berbeza-beza:
times
Mencipta litar fermionik
Kita akan mencipta litar fermionik untuk setiap langkah masa. Setiap litar akan terdiri daripada satu Gate evolusi sahaja, dengan masa evolusi yang telah kita isytiharkan sebelum ini. Operator evolusi ialah Hamiltonian. Kemudian kita menjalankan laluan transpiler pada litar-litar ini untuk mencipta litar qDRIFT.
Penyediaan ansatz
Kita menyediakan keadaan Hartree-Fock menggunakan kelas InitializeModes. Bagi nitrogen, prosesnya hanyalah mengenakan Gate X pada num_elec_a qubit pertama dan kemudian pada num_elec_b qubit, kedua-duanya bersamaan dengan tujuh bagi nitrogen. Keadaan ini mewakili tujuh elektron dan tujuh elektron 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: Optimumkan masalah untuk pelaksanaan perkakasan kuantum
Sekarang setelah kita mempunyai litar kita, kita akan mula-mula menggunakan laluan yang tersedia dalam qiskit-fermions untuk melakukan pengoptimuman pada peringkat fermionik, diikuti dengan mentranspil litar kita untuk Backend pilihan. Oleh kerana ini adalah eksperimen simulator, kita akan melakukannya terlebih dahulu untuk AerSimulator.
Pengiraan berat untuk setiap kumpulan
Dalam langkah ini, kita melakukan pensampelan qDRIFT bagi sebutan secara stokastik dengan kebarangkalian berkadar dengan pekalinya dalam Hamiltonian. Laluan transpiler qDRIFT melakukan ini untuk kita. Kita kini boleh mencipta litar yang lebih cetek yang boleh dilaksanakan pada perkakasan dengan lebih cekap walaupun konektiviti qubit terhad, malah apabila Hamiltonian mengandungi gandingan jarak jauh dan sebutan lebih tinggi daripada kuadratik. Selepas pengumpulan sebutan, ia mensampel operator berdasarkan beratnya. Bagi setiap operator , berat ditakrifkan seperti berikut:
Pengoptimuman fermionik dan asli-perkakasan
Fungsi generate_preset_jw_pass_manager() mengembalikan MultiStagePassManager yang menerima FermionicCircuit dan menghasilkan litar akhir yang dioptimumkan yang boleh kita transpil untuk dijalankan pada perkakasan kita. Kita menggantikan peringkat pengoptimuman lalainya dengan FermionicPassManager yang mengandungi laluan QDriftTrotterization kita:
-
Laluan
QDriftTrotterizationmenggunakan pengiraan berat dan pensampelan secara dalaman untuk menjana litar yang akan kita gunakan untuk pensampelan -
Laluan
RelabelModesialah laluan pengoptimuman lain yang boleh digunakan untuk memermutasikan mod fermionik bagi mengoptimumkan konektiviti merentas qubit dan mengurangkan kedalaman Gate; baca lebih lanjut dalam rujukan API
Peringkat-peringkat lain bagi MultiStagePassManager berjalan secara automatik dan mengendalikan pemetaan fermion-ke-qubit yang lengkap:
-
F2QLayout: Pengurus laluan pratetap mengenakan laluan
TrivialF2QLayout, yang memetakan secara remeh bit fermionik kepada qubit. -
F2QSynth: Laluan transpilasi untuk memetakan arahan litar berasaskan fermion kepada arahan berasaskan 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 pengoptimuman pada peringkat fermionik, kita boleh mentranspil litar untuk pelaksanaan pada simulator.
simulator = AerSimulator()
shots = 100
transpiled_circuits = transpile(sqdrift_circuits, simulator)
Langkah 3: Laksanakan menggunakan primitif Qiskit
Sekarang setelah kita mempunyai litar kita, kita boleh menjalankannya menggunakan primitif Qiskit pada AerSimulator. Kita akan menggabungkan semua kiraan daripada litar yang berbeza. Kita menukarkannya kepada vektor Boolean sebelum akhirnya melakukan pasca-pemprosesan 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: Pasca-proses dan kembalikan hasil dalam format klasik yang dikehendaki
Menggunakan string bit untuk SQD
Kita kini boleh menjalankan skema pepenjuruan pada string bit yang dipilih untuk mencari nilai eigen terendah yang akan sepadan dengan tenaga keadaan asas molekul. Kita mencipta fungsi panggil balik, mengisytiharkan penghunian awal, dan menetapkan parameter sebelum akhirnya menjalankan skema pepenjuruan. Fungsi panggil balik digunakan untuk mencetak lelaran semasa dan anggaran nilai eigen semasa pada setiap lelaran.
Akhirnya, untuk mendapatkan anggaran keadaan asas, kita menambah nuclear_repulsion_energy kepada tenaga yang terhasil.
Nota: Dimensi subruang tidak tetap merentas lelaran, walaupun pada simulator tanpa hingar — setiap subsampel mengambil set konfigurasi yang berbeza, dan langkah pemulihan membentuk semula kolam antara lelaran, jadi dimensi yang dilaporkan berbeza-beza daripada satu subsampel ke subsampel seterusnya. Pensampelan tanpa hingar tidak dengan sendirinya menetapkan dimensi subruang yang dipilih. Namun, larian perkakasan cenderung memberikan subruang yang secara sistematik lebih besar, kerana tembakan berhingar memecahkan simetri bilangan zarah dan pemulihan konfigurasi menukarkannya kepada vektor asas tambahan. Oleh sebab itu, kita juga akan memperkenalkan satu lagi langkah untuk memangkas string bit dalam bahagian perkakasan.
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 perkakasan
Contoh ini menggunakan 20 qubit (10 orbital ruang). Pilihan itu adalah untuk kemudahan tutorial yang sepatutnya berjalan pantas, bukan had mutlak pada kaedah ini.
Kos langkah klasik tidak ditentukan secara langsung oleh bilangan qubit. SQD mempepenjurukan Hamiltonian yang diunjurkan ke atas subruang yang direntangi oleh konfigurasi yang disampel, jadi apa yang menentukan kos klasik ialah dimensi subruang terpilih itu — dikawal di sini oleh samples_per_batch, num_batches, dan berapa banyak konfigurasi berbeza yang sebenarnya dihasilkan oleh litar — bersama-sama dengan aljabar linear jarang yang diperlukan untuk mengenakan Hamiltonian yang diunjurkan itu. Ruang CI penuh berkembang secara kombinatorik mengikut orbital dan elektron, tetapi subruang terpilih ialah sekeping kecil yang boleh dilaraskan daripadanya, dan kita mengawal saiznya secara langsung. Akibatnya, bilangan qubit dan kesukaran klasik boleh diubah secara agak bebas antara satu sama lain: ruang orbital yang lebih luas yang disampel ke dalam subruang sederhana boleh lebih murah daripada sistem yang lebih kecil tetapi dipepenjurukan merentas subruang yang sangat besar.
Dalam praktiknya, saiz sistem yang boleh dilaksanakan bergantung pada dimensi subruang yang anda perlukan untuk ketepatan yang anda inginkan dan pada memori serta teras yang tersedia untuk penyelesai eigen. Ruang orbital yang lebih besar biasanya memang memerlukan subruang yang lebih besar untuk mencapai ketepatan kimia, dan itulah yang akhirnya mendorong penggunaan sumber teragih — lihat qiskit-addon-sqd-hpc untuk menskalakan langkah ini. Daripada mengandaikan had tetap, pendekatan praktikal ialah memerhatikan dimensi subruang yang dilaporkan dan penumpuan tenaga merentas lelaran, dan meningkatkan saiz subruang sehingga tenaga berhenti bertambah baik atau anda kehabisan memori yang tersedia.
Nota: Disebabkan ralat pensampelan daripada hingar pada perkakasan, subruang yang dicipta untuk pepenjuruan dalam larian perkakasan akan lebih besar daripada yang kita perolehi apabila menggunakan simulator. Walaupun ini meningkatkan dimensi subruang yang kita ingin pepenjurukan, aliran kerja masih memberikan kita jawapan yang tepat kerana keteguhan SQD terhadap hingar.
Pemangkasan string palsu
Di sini kita boleh memilih untuk melakukan satu langkah tambahan. Apabila kita mempunyai semua string bit daripada pelaksanaan litar, kita boleh sama ada menapis keluar string bit yang tidak sah sebelum menjalankan SQD, atau meneruskan tanpa pemangkasan. Melangkau pemangkasan secara amnya lebih baik untuk larian perkakasan, kerana ia membiarkan tembakan yang simetrinya terpecah tersedia untuk pemulihan konfigurasi, yang boleh membaikinya menjadi konfigurasi yang sah dan dengan itu meluaskan subruang berbanding membuang tembakan tersebut terus.
Oleh kerana nitrogen hanya boleh mempunyai tujuh elektron dan tujuh elektron , mana-mana string bit yang mempunyai lebih atau kurang daripada tujuh 1 dalam separuh pertama dan separuh kedua output boleh dibuang. Kita mentakrifkan fungsi yang memeriksa sama ada string bit itu sah, dan jika tidak, membuangnya. Setelah kita menapis keluar string bit palsu, selebihnya dihantar ke dalam skema pepenjuruan. Gunakan bendera PRUNE di bawah untuk bertukar antara kedua-dua tingkah laku.
Ingatlah bahawa pemangkasan hanyalah salah satu daripada beberapa pilihan yang membentuk subruang akhir, di samping bilangan litar, set masa evolusi, dan penapisan sebutan pepenjuru. Membandingkan larian yang dipangkas dengan larian yang tidak dipangkas hanya bermaklumat jika segala-galanya yang lain dikekalkan tetap; pendamping C++ membincangkan ini dengan lebih terperinci, memandangkan ia melakukan pasca-pemilihan dan bukannya pemulihan dan juga berbeza dari segi parameter lain tersebut.
name = "assets/sqdrift/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")
Parsing assets/sqdrift/fcidump_files/N2_sto_3g
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
Langkah seterusnya
Jika anda mendapati kerja ini menarik, anda mungkin berminat dengan bahan berikut:
- Pepenjuruan kuantum Krylov berasaskan sampel bagi model kekisi fermionik - tutorial berkaitan yang menggunakan litar evolusi-masa dan bukannya ansatz variasional.
- Pepenjuruan kuantum berasaskan sampel bagi Hamiltonian kimia - tutorial tentang cara membina litar cluster Jastrow unitari setempat (LUCJ) untuk simulasi kimia kuantum.
- Kertas SqDRIFT - literatur yang menjadi asas tutorial ini. (Ambil perhatian bahawa sebahagian daripada pengoptimuman yang dibincangkan dalam kertas ini kini masih dalam pembangunan, dan tutorial ini tertakluk kepada perubahan pada masa hadapan berdasarkan evolusi perpustakaan yang digunakan.)