SqDRIFT algorithm para sa ground state estimation
- Python
- C++
I-download ang Python notebook para patakbuhin mo mismo ang tutorial na ito. Dina-download ng notebook ang FCIDump input kung wala pa ito.
Tinatayang paggamit: 180 seconds sa isang Heron r3 processor (TANDAAN: Isa lamang itong tantiya. Maaaring mag-iba ang runtime mo.)
Mga natutunan
-
Matutunan kung paano gumawa ng mas mababang depth na circuits kumpara sa Trotterization
-
Dumaan sa isang end-to-end workflow para sa ground state estimation gamit ang qDRIFT at SQD
-
Matutunan kung paano gamitin ang
qiskit-fermionskasabay ng ibang Qiskit addons para ipatupad ang ganitong workflow
Mga kinakailangan
-
Basahin ang overview ng Sample-based quantum diagonalization (SQD)
-
Basahin ang lesson na Sample-based Krylov Quantum Diagonalization (SKQD)
Kaligiran
Ang SqDRIFT ay isang variant ng SKQD na sa halip na pumili ng ansatz na pagkukunan ng sample ng bitstrings, gumagamit ito ng ensemble ng time-evolution circuits na direktang binuo mula sa target Hamiltonian. Nakakamit ito sa pamamagitan ng pag-subsample ng mas maliliit na time-evolution operators mula sa Hamiltonian batay sa mga coefficient nito, na kilala bilang ang qDRIFT Trotterization method.
Parehong gumagamit ang dalawang implementasyon ng Qiskit Fermions upang katawanin ang molecular Hamiltonian at buuin ang mga circuit para sa qDRIFT algorithm. Gumagamit ang Python implementation ng fermionic transpiler passes, habang tahasang nagsa-sample ang C++ implementation ng mga grupo at minamapa ang mga ito sa qubit operators.
Hayaang ang Hamiltonian ay sa anyong:
kung saan, nang hindi nawawalan ng generality, kinakailangan natin na at ang pinakamalaking eigenvalue ng ay pantay, sa absolute value, sa . Anumang may-sign o complex na prefactor ay isinasama (ina-absorb) sa , kaya ang mga coefficient na ay mahigpit na positive weights habang ang ay may dalang direksyon ng bawat term. Dito, ang ay ang bilang ng terms (o, pagkatapos ng grouping, ang bilang ng groups) sa Hamiltonian; ito ay property ng Hamiltonian at naiiba sa bilang ng operators na na-sample sa iisang circuit, na isinusulat bilang sa ibaba.
Para sa target time na , ipinapatupad ng qDRIFT algorithm ang isang operator na , kung saan ang ay mula at kumakatawan sa SqDRIFT circuit, na tinutukoy bilang:
Dito ang ay ang bilang ng sampled operators bawat circuit at ang ay ang bilang ng circuits sa ensemble. Ang produkto ay tumatakbo sa na pagkuha, hindi sa lahat ng Hamiltonian terms, at dahil kinukuha ang mga term nang may replacement, maaaring lumabas nang higit sa isang beses ang parehong sa iisang .
Ang dami:
ay ang norm ng mga coefficient, kaya bawat isa sa na hakbang ay nag-e-evolve sa parehong tagal na anuman ang term na nakuha. Ang pagkakapareho ng step angle ang katangiang tampok ng qDRIFT: ang isang coefficient ay nakaaapekto sa resulta sa pamamagitan ng gaano kadalas na nakukuha ang term nito, hindi sa pamamagitan ng kung gaano kalayo umiikot ang term na iyon. Ang mga index ay kinukuha mula sa distribution:
kaya ang series na ay isang random sequence ng term indices na kinuha mula sa distribution na ito. Dahil positive ang mga at ang kabuuan nila ay , isa itong normalized probability distribution, at ang expectation ng resultang channel sa mga random draws ay tinatantya ang evolution sa ilalim ng , na may error na bumababa habang lumalaki ang . Tandaan na ang approximation error ay nakadepende sa sa halip na sa bilang ng terms na .
(Isinusulat ng SqDRIFT paper ang bilang ng terms bilang at ang haba ng sequence bilang ; ginagamit natin ang at dito para panatilihing malinaw ang pagkakaiba ng dalawa.)
Ipinapakita ng tutorial na ito kung paano bumuo ng ensemble ng ganitong mga randomized circuits. Matapos nating gawin ang mga circuit na ito, katulad ng paggawa natin ng Krylov subspace para sa iba't ibang operators, kumukuha tayo ng sample ng bitstrings mula sa maraming ganitong operators na may iba't ibang time parameters. Sinisiguro nito ang mas mataas na overlap sa pagitan ng ground state vectors at ng sampled bitstrings.
Mga kailangan
Bago simulan ang tutorial na ito, siguraduhing na-install mo na ang
- Isang Python (>=3.10) virtual environment
- pip>=25.1
qiskit~= 2.5qiskit-fermions==0.1.0 (Tandaan na plural ang pangalan)- numpy
- pyscf
qiskit-aerqiskit-ibm-runtimeqiskit-addon-sqd
Puwede mong i-install ang lahat ng kinakailangang packages gamit ang:
pip install "qiskit~=2.5" "qiskit-fermions==0.1.0" qiskit-aer qiskit-ibm-runtime qiskit-addon-sqd pyscf numpy
Pag-setup
# 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,
)
Implementasyon
Hakbang 1: I-map ang classical inputs papunta sa quantum problem
Ginagamit ng mga Python step sa ibaba ang Aer simulator. Kasunod nito ang hardware example.
Pagbabasa at Paghahanda ng FCIDump
Para sa tutorial na ito, ilo-load natin ang electronic structure Hamiltonian para sa nitrogen (N2). May iba pang paraan din para gumawa ng fermionic operators. Sumangguni sa dokumentasyon sa qiskit_fermions.operators.library.
Tungkol sa FCIDump na ito. Inilalarawan ng file na N2_sto_3g ang isang molekula ng nitrogen () sa minimal STO-3G basis sa interatomic separation na 1.09 , ang experimental equilibrium bond length. Idineklara ng header nito ang NORB=10, NELEC=14, at MS2=0: 10 spatial orbitals (kaya 20 spin orbitals, at 20 qubits sa ilalim ng Jordan-Wigner), 14 electrons sa isang spin singlet, kaya pitong at pitong electrons. Lahat ng orbitals ay binigyan ng symmetry label 1, ibig sabihin, walang point-group symmetry na ginagamit. Dahil isa itong full-space STO-3G dump, walang orbitals na na-freeze at maliit lang ang correlation space kaya maaaring kalkulahin nang classical ang eksaktong FCI reference energy para sa paghahambing, tulad ng ipinapakita sa susunod na code block.
Maaaring i-regenerate ang isang katumbas na file gamit ang 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")
Dahil nakadepende ang mga integral sa converged SCF orbitals, maaaring magkaiba ang isang na-regenerate na file sa orihinal na ibinigay sa orbital phase o ordering; hindi apektado ang total energies.
Pagkuha ng file. Hanapin ang FCIDump sa GitHub repository na ito. Maaari mong patakbuhin ang code sa ibaba para kunin ito papunta sa lokasyong inaasahan ng natitirang bahagi ng tutorial.
Una, gagamitin natin ang cisolver na ibinibigay ng pyscf para makuha ang reference energy. Ito ang totoong ground state energy ng molekulang ginagamit natin. Para dito, ideklara muna natin ang norb at nelec, na siyang bilang ng mga orbitals at electrons, ayon sa pagkakasunod-sunod. Pagkatapos, ideklara natin ang h1e at h2e, na siyang one- at two-electron integrals ayon sa pagkakasunod-sunod. Gagamitin din ang lahat ng ito sa SQD sa ibang pagkakataon.
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
Pag-load ng Hamiltonian
Sa handa na ang kailangang data, babasahin natin ang Hamiltonian mula sa FCI file sa isang format na compatible sa qiskit-fermions
fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb
Mga fermionic workflow gamit ang qiskit-fermions
Una nating i-map ang Hamiltonian papunta sa isang fermionic circuit model gamit ang qiskit-fermions, na nagbibigay ng transpiler passes at gates na partikular sa fermionic circuits. Gagamitin ang mga ito bago ang mga tradisyunal na transpiler passes ng Qiskit para sa workflow na ito.
Pagpapangkat ng termino
Para matiyak ang reproducibility ng mga resulta, gagamitin muna natin ang canonical_order para i-sort ang mga termino batay lamang sa kanilang structure. Kaya naman nakapirmi ang order ng mga operator sa canon list. Tinitiyak nito ang reproducibility ng mga nagawang operator dahil ang QDriftTrotterization pass na gagamitin natin sa hinaharap ay nagsa-sample ng random indices para gumawa ng mga qDRIFT operator.
Sa hakbang na ito, sinasamantala natin ang maraming symmetry na naroroon sa electronic structure Hamiltonian sa pamamagitan ng pagpapangkat ng mga magkakaugnay na termino na may magkaparehong coefficient. Bagama't binabago nito ang distribution ng operator coefficient na pinagmumulan ng sample ng qDRIFT protocol, hindi ito nakakaapekto sa convergence guarantees nito. Mahalaga, ang pagpapangkat ng mga terminong magkaugnay dahil sa symmetry ay nagreresulta sa isang kanais-nais na pagkakansela ng mga Pauli term at sa mas maikling circuit depth kapag time-evolving ang isang state sa ilalim ng kanilang aksyon.
Ang qiskit-fermions ay nagbibigay ng group_terms_by_electronic_structure function na gumagawa ng pagpapangkat na ito para sa atin.
Tandaan na inaasahan ng group_terms_by_electronic_structure na ang mga termino ay normal ordering.
Pag-filter ng diagonal terms
Inaalis natin ang mga diagonal terms mula sa Hamiltonian na ginagamit para bumuo ng mga circuit, para magamit ang na qDRIFT sampling slots sa mga terminong naglilipat ng population sa pagitan ng mga configuration. Pinakamainam na i-filter out ang ganitong mga termino sa Hamiltonian sa puntong ito, bago buuin ang Evolution gate sa susunod na hakbang.
Ang mga terminong pinag-uusapan ay ang mga diagonal sa occupation-number basis, ibig sabihin, ang mga produkto ng number operators . May tatlong uri ng termino na kabilang sa deskripsyong ito:
-
ang constant energy offset, isang produkto ng zero number operators, na ang time evolution ay nag-aambag lamang ng global phase;
-
ang individual number operators , na ang time evolution ay nauuwi sa single-qubit rotations;
-
ang higher-order products tulad ng .
Sa kanilang sarili, wala sa mga ito ang naglilipat ng population sa pagitan ng mga occupation-number configuration; umaaksyon lamang sila sa mga phase ng mga configuration na naroroon na. Hindi sila inert, gayunpaman: ang mga relative phase na iyon ay nakakaapekto sa interference na nabubuo ng mga excitation term sa mga susunod na bahagi ng circuit, kaya nababago ng pag-filter sa kanila ang evolution na aktwal na nabubuo at maaaring baguhin ang sampling distribution. Ito ay isang sinadyang approximation sa hakbang ng paggawa ng circuit, na ginawa upang ituon ang sampling sa mga excitation term, sa halip na isang hakbang na hindi nakaaapekto sa sampled distribution. Hindi katulad ng symmetry grouping sa itaas, na hindi humihipo sa convergence guarantees ng qDRIFT, binabago ng filter na ito ang operator na ini-evolve. Kaya hindi na inaapproximate ng mga circuit ang evolution sa ilalim ng buong Hamiltonian, at ang qDRIFT error bounds ay nalalapat sa filtered operator sa halip na sa orihinal. Katanggap-tanggap ito dahil ang mga circuit ay heuristic lamang para sa sampling na ginagamit para magmungkahi ng mga configuration: walang terminong nawawala mula sa energy estimate mismo, dahil ang filter ay nalalapat lamang sa Hamiltonian na ginagamit para bumuo ng mga circuit, samantalang ginagamit ng classical diagonalization sa ibang pagkakataon ang buong Hamiltonian, kasama ang diagonal terms. Ang accuracy ng SQD ay nakadepende sa hakbang na iyon ng classical diagonalization, na nananatiling variational sa sampled subspace anuman ang paraan ng pagmumungkahi ng mga configuration.
Inaalis ng filter_diagonal_terms() function ang ganitong mga termino mula sa isang operator nang direkta (in place). Kinikilala nito ang mga ito mula sa kanilang normal-ordered structure — ang multiset ng creation modes na tumutugma sa multiset ng annihilation modes — kaya valid lamang ito sa isang operator na normal-ordered na. Hindi ito nasusuri sa 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
Ngayong napagpangkat na natin ang mga termino sa Hamiltonian, magdedesisyon tayo sa mga sumusunod na parameter para bumuo ng ensemble ng mga circuit:
- Ang bilang ng mga circuit na bubuuin:
num_circuits - Ang haba ng bawat circuit, sinusukat sa bilang ng excitation groups:
num_exc - Ang factor para sa iba't ibang evolution times:
times
Paggawa ng mga fermionic circuit
Gagawa na tayo ngayon ng mga fermionic circuit para sa bawat time-step. Ang bawat circuit ay binubuo ng iisang evolution gate, kasama ang evolution time na idineklara natin kanina. Ang evolution operator ay ang Hamiltonian. Sa ibang pagkakataon, magpapatakbo tayo ng transpiler passes sa mga circuit na ito para bumuo ng mga qDRIFT circuit.
Paghahanda ng ansatz
Inihahanda natin ang Hartree-Fock state gamit ang InitializeModes class. Para sa nitrogen, ang proseso ay simpleng pag-apply ng X gates sa unang num_elec_a qubits at pagkatapos sa num_elec_b qubits, na pareho, pitong-pito, para sa nitrogen. Ang state na ito ay kumakatawan sa pitong at pitong electrons ng 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)
Hakbang 2: I-optimize ang problema para sa pagsasagawa sa quantum hardware
Ngayong mayroon na tayong mga circuit, gagamitin muna natin ang mga pass na available sa qiskit-fermions para magsagawa ng fermionic level optimizations, susundan ng pag-transpile ng ating circuit para sa napiling backend. Dahil isa itong simulator experiment, gagawin muna natin ito para sa AerSimulator.
Pagkalkula ng weight para sa bawat grupo
Sa hakbang na ito, isinasagawa natin ang qDRIFT sampling ng mga termino nang stochastic na may mga probabilidad na proportional sa kanilang mga coefficient sa Hamiltonian. Ginagawa ito para sa atin ng qDRIFT transpiler pass. Makakagawa na tayo ngayon ng mas mabababaw na circuit na mas episyenteng maipapatakbo sa hardware sa kabila ng limitadong qubit connectivity, kahit na naglalaman ang Hamiltonian ng long-range couplings at mga terminong mas mataas pa sa quadratic. Matapos ang term grouping, nagsa-sample ito ng mga operator batay sa kanilang mga weight. Para sa bawat operator , ang weight ay nadedepine tulad ng sumusunod:
Dahil naka-group ang mga term sa Step 1, ang bawat dito ay isang buong grupo: ang ay ang mean absolute coefficient ng mga term sa grupong , at ang bawat term sa grupo ay ine-evolve na ang coefficient ay binawasan hanggang sa sign na lang nito.
Fermionic at hardware-native optimizations
Ang function na generate_preset_jw_pass_manager() ay nagbabalik ng isang MultiStagePassManager na kumukuha ng isang FermionicCircuit at gumagawa ng na-optimize na final circuit na maaari nating i-transpile para tumakbo sa ating hardware. Papalitan natin ang default nitong optimization stage ng isang FermionicPassManager na naglalaman ng ating QDriftTrotterization pass:
-
Ginagamit ng
QDriftTrotterizationpass ang weight-calculation at sampling nang internal para bumuo ng mga circuit na gagamitin natin para sa sampling -
Ang
RelabelModespass ay isa pang optimization pass na maaaring gamitin para i-permute ang mga fermionic modes para i-optimize ang connectivity sa mga qubit at bawasan ang gate depth; magbasa pa sa API reference
Awtomatikong tatakbo ang natitirang mga stage ng MultiStagePassManager at ito ang humahawak sa buong fermion-to-qubit mapping:
-
F2QLayout: Inilalapat ng preset pass manager ang
TrivialF2QLayoutpass, na simpleng nagma-map ng fermionic bits papunta sa qubits. -
F2QSynth: Isang transpilation pass para i-map ang mga fermion-based circuit instructions papunta sa mga qubit-based na instructions.
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
Ngayong tapos na tayo sa fermionic-level optimizations, maaari na nating i-transpile ang mga circuit para sa pagpapatakbo sa simulator.
simulator = AerSimulator()
shots = 100
transpiled_circuits = transpile(sqdrift_circuits, simulator)
Hakbang 3: Isagawa gamit ang Qiskit primitives
Ngayong mayroon na tayong mga circuit, maaari na natin itong patakbuhin gamit ang Qiskit primitives sa AerSimulator. Pagsasamahin natin ang lahat ng counts mula sa iba't ibang circuit. Iko-convert natin ang mga ito papunta sa boolean vectors bago tuluyang mag-post-process gamit ang 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
Hakbang 4: I-post-process at ibalik ang resulta sa nais na classical format
Paggamit ng bitstrings para sa SQD
Maaari na nating patakbuhin ang diagonalization scheme sa mga napiling bitstrings para hanapin ang pinakamababang eigenvalue na tutumbas sa ground state energy ng molekula. Gagawa tayo ng callback function, magdedeklara ng initial occupancies, at itatakda ang mga parameter bago tuluyang patakbuhin ang diagonalization scheme. Ginagamit ang callback function para i-print ang kasalukuyang iteration at ang kasalukuyang eigenvalue estimate sa bawat iteration.
Sa wakas, para makuha ang ground state estimate, idadagdag natin ang nuclear_repulsion_energy sa resultang energy.
Tandaan: Hindi nakapirmi ang subspace dimension sa iba't ibang iteration, kahit sa noiseless simulator — bawat subsample ay kumukuha ng ibang set ng mga configuration, at binabago ng recovery step ang shape ng pool sa pagitan ng mga iteration, kaya nag-iiba ang naiulat na dimension mula sa isang subsample papunta sa susunod. Hindi mismo pinipirmi ng noiseless sampling ang dimension ng napiling subspace. Gayunpaman, ang hardware run ay may tendensyang magbigay ng sistematikong mas malalaking subspace, dahil sinisira ng noisy shots ang particle-number symmetry at ginagawang karagdagang basis vectors ng configuration recovery ang mga ito. Dahil dito, magpapakilala din tayo ng isa pang hakbang para sa pag-prune ng bitstrings sa hardware section.
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
Halimbawa sa hardware
Gumagamit ang halimbawang ito ng 20 qubits (10 spatial orbitals). Ang pagpiling iyon ay para sa kaginhawaan ng isang tutorial na dapat tumakbo nang mabilis, hindi isang mahigpit na hangganan sa method.
Hindi direktang itinatakda ng bilang ng qubit ang gastos ng classical step. Dini-diagonalize ng SQD ang Hamiltonian na na-project papunta sa subspace na sinasaklaw ng mga na-sample na configuration, kaya ang nagtutulak sa classical cost ay ang dimension ng napiling subspace na iyon — pinamamahalaan dito ng samples_per_batch, num_batches, at kung gaano karaming natatanging configuration ang aktwal na nabubuo ng mga circuit — kasama ang sparse linear algebra na kailangan para i-apply ang projected Hamiltonian. Lumalaki nang combinatorial ang buong CI space ayon sa mga orbital at electron, pero ang napiling subspace ay isang maliit, natu-tune na bahagi lamang nito, at direkta nating kinokontrol ang laki nito. Dahil dito, ang bilang ng mga qubit at ang classical difficulty ay maaaring magbago nang medyo independiyente sa isa't isa: ang mas malawak na orbital space na na-sample papunta sa isang katamtamang subspace ay maaaring mas mura kaysa sa isang mas maliit na sistema na dini-diagonalize sa napakalaking subspace.
Sa praktika, kung gayon, nakadepende ang posibleng laki ng sistema sa subspace dimension na kailangan mo para sa accuracy na gusto mo at sa memory at cores na available sa eigensolver. Karaniwang nangangailangan nga ang mas malalaking orbital space ng mas malaking subspace para makarating sa chemical accuracy, at iyan ang eventually nagbibigay-motibo para sa distributed resources — tingnan ang qiskit-addon-sqd-hpc para sa pag-scale out ng hakbang na ito. Sa halip na mag-assume ng nakapirming cutoff, ang praktikal na paraan ay panoorin ang naiulat na subspace dimension at ang energy convergence sa iba't ibang iteration at dagdagan ang laki ng subspace hanggang tumigil sa pag-improve ang energy o maubos ang available na memory.
Tandaan: Dahil sa sampling error mula sa noise sa hardware, ang subspace na nagawa para sa diagonalization sa hardware run ay magiging mas malaki kaysa sa nakukuha natin kapag gumagamit ng simulator. Bagama't pinapataas nito ang dimension ng subspace na gusto nating i-diagonalize, nagbibigay pa rin ang workflow ng tumpak na sagot dahil sa robustness ng SQD laban sa noise.
Pag-prune ng mga spurious strings
Dito, maaari nating piliing magsagawa ng karagdagang hakbang. Kapag nasa atin na ang lahat ng bitstrings mula sa mga pagpapatakbo ng circuit, maaari nating i-filter out ang mga invalid na bitstrings bago patakbuhin ang SQD, o magpatuloy nang walang pruning. Sa pangkalahatan, mas mainam na laktawan ang pruning para sa hardware runs, dahil iniiwan nitong available ang symmetry-broken shots para sa configuration recovery, na maaaring ayusin ang mga ito papunta sa valid na configurations at sa gayon palawakin ang subspace sa halip na direktang itapon ang mga shots na iyon.
Dahil pitong at pitong electrons lang ang maaaring meron ang nitrogen, anumang bitstrings na may higit o kulang sa pitong 1s sa una at pangalawang kalahati ng output ay maaaring itapon. Magdedepine tayo ng isang function na sumusuri kung valid ang mga bitstrings, at kung hindi, itinatapon ang mga ito. Kapag na-filter out na natin ang mga spurious na bitstrings, ang natitira ay ipinapadala papunta sa diagonalization scheme. Gamitin ang PRUNE flag sa ibaba para lumipat sa pagitan ng dalawang behavior.
Tandaan na ang pruning ay isa lamang sa ilang mga pagpipilian na humuhubog sa final subspace, kasama ang bilang ng mga circuit, ang set ng evolution times, at diagonal-term filtering. Ang paghahambing ng isang pruned run laban sa isang unpruned run ay makabuluhan lamang kung nananatiling fixed ang lahat ng iba pa; tinatalakay ito nang mas detalyado ng C++ tab, dahil nagpo-postselect ito sa halip na mag-recover at naiiba rin sa ibang mga parameter na iyon.
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
Ang C++ source, CMake project, at mga tagubilin sa pag-build ay nasa directory na docs/tutorials/assets/sqdrift/cpp ng Qiskit documentation repository. Ang FCIDump input ay nasa katabing directory na fcidump_files. I-clone ang repository, o i-download ang parehong directory, para ma-build at mapatakbo mo mismo ang tutorial na ito.
Tinatayang paggamit: 180 seconds sa isang Heron r3 processor (TANDAAN: Isa lamang itong tantiya. Maaaring mag-iba ang runtime mo.)
Mga natutunan (C++)
-
Matutunan kung paano gumawa ng mas mababang depth na circuits kumpara sa Trotterization
-
Dumaan sa isang end-to-end workflow para sa ground state estimation gamit ang qDRIFT at SQD
-
Matutunan kung paano gamitin ang
qiskit-fermionskasabay ng ibang Qiskit addons para ipatupad ang ganitong workflow
Mga kinakailangan (C++)
-
Basahin ang overview ng Sample-based quantum diagonalization (SQD)
-
Basahin ang lesson na Sample-based Krylov Quantum Diagonalization (SKQD)
Kaligiran (C++)
Ang SqDRIFT ay isang variant ng SKQD na sa halip na pumili ng ansatz na pagkukunan ng sample ng bitstrings, gumagamit ito ng ensemble ng time-evolution circuits na direktang binuo mula sa target Hamiltonian. Nakakamit ito sa pamamagitan ng pag-subsample ng mas maliliit na time-evolution operators mula sa Hamiltonian batay sa mga coefficient nito, na kilala bilang ang qDRIFT Trotterization method.
Parehong gumagamit ang dalawang implementasyon ng Qiskit Fermions upang katawanin ang molecular Hamiltonian at buuin ang mga circuit para sa qDRIFT algorithm. Gumagamit ang Python implementation ng fermionic transpiler passes, habang tahasang nagsa-sample ang C++ implementation ng mga grupo at minamapa ang mga ito sa qubit operators.
Hayaang ang Hamiltonian ay sa anyong:
kung saan, nang hindi nawawalan ng generality, kinakailangan natin na at ang pinakamalaking eigenvalue ng ay pantay, sa absolute value, sa . Anumang may-sign o complex na prefactor ay isinasama (ina-absorb) sa , kaya ang mga coefficient na ay mahigpit na positive weights habang ang ay may dalang direksyon ng bawat term. Dito, ang ay ang bilang ng terms (o, pagkatapos ng grouping, ang bilang ng groups) sa Hamiltonian; ito ay property ng Hamiltonian at naiiba sa bilang ng operators na na-sample sa iisang circuit, na isinusulat bilang sa ibaba.
Para sa target time na , ipinapatupad ng qDRIFT algorithm ang isang operator na , kung saan ang ay mula at kumakatawan sa SqDRIFT circuit, na tinutukoy bilang:
Dito ang ay ang bilang ng sampled operators bawat circuit at ang ay ang bilang ng circuits sa ensemble. Ang produkto ay tumatakbo sa na pagkuha, hindi sa lahat ng Hamiltonian terms, at dahil kinukuha ang mga term nang may replacement, maaaring lumabas nang higit sa isang beses ang parehong sa iisang .
Ang dami:
ay ang norm ng mga coefficient, kaya bawat isa sa na hakbang ay nag-e-evolve sa parehong tagal na anuman ang term na nakuha. Ang pagkakapareho ng step angle ang katangiang tampok ng qDRIFT: ang isang coefficient ay nakaaapekto sa resulta sa pamamagitan ng gaano kadalas na nakukuha ang term nito, hindi sa pamamagitan ng kung gaano kalayo umiikot ang term na iyon. Ang mga index ay kinukuha mula sa distribution:
kaya ang series na ay isang random sequence ng term indices na kinuha mula sa distribution na ito. Dahil positive ang mga at ang kabuuan nila ay , isa itong normalized probability distribution, at ang expectation ng resultang channel sa mga random draws ay tinatantya ang evolution sa ilalim ng , na may error na bumababa habang lumalaki ang . Tandaan na ang approximation error ay nakadepende sa sa halip na sa bilang ng terms na .
(Isinusulat ng SqDRIFT paper ang bilang ng terms bilang at ang haba ng sequence bilang ; ginagamit natin ang at dito para panatilihing malinaw ang pagkakaiba ng dalawa.)
Ipinapakita ng tutorial na ito kung paano bumuo ng ensemble ng ganitong mga randomized circuits. Matapos nating gawin ang mga circuit na ito, katulad ng paggawa natin ng Krylov subspace para sa iba't ibang operators, kumukuha tayo ng sample ng bitstrings mula sa maraming ganitong operators na may iba't ibang time parameters. Sinisiguro nito ang mas mataas na overlap sa pagitan ng ground state vectors at ng sampled bitstrings.
Mga kailangan (C++)
Tumatakbo ang implementasyong ito sa IBM Quantum® hardware at gumagamit ng external na SBD executable para sa classical diagonalization. Hindi nito kasama ang Python simulator example.
Sundin ang mga tagubilin sa pag-build ng C++ para i-install ang compiler, CMake, Rust, Boost, at ang mga Qiskit dependency. I-install ang Rust gamit ang rustup: ang mga Qiskit dependency ay nagpi-pin ng sarili nilang bersyon ng Rust, na awtomatikong dina-download ng rustup, ngunit ang Rust na naka-install mula sa Homebrew o package manager ng Linux ay maaaring masyadong luma. Kailangan din ng huling SBD step ang MPI, OpenMP, at BLAS/LAPACK. Hindi pa sinusuportahan ng implementasyong ito ang Windows sa ngayon.
Gamitin ang kumpletong C++ source at CMake project mula sa repository. Panatilihin ang katabing directory na fcidump_files kapag dina-download ang example. Mula sa isang checkout ng documentation repository, mag-build gamit ang mga sumusunod na command. Dina-download at kino-compile ng unang build ang lahat ng dependency at inaabot ng ilang minuto.
cd docs/tutorials/assets/sqdrift/cpp
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
Isinusulat ng build ang executable na SqDRIFT sa docs/tutorials/assets/sqdrift/cpp (ang project root), hindi sa build/. Patakbuhin ang ./SqDRIFT mula sa project root para mahanap nito ang ../fcidump_files/N2_sto_3g. Lahat ng iba pang command sa tab na ito ay pinapatakbo rin mula sa project root maliban kung sinabi nang iba. Kung pumalya ang ./SqDRIFT na may error na nawawalang shared library, sundin ang mga tagubilin sa library-path na espesipiko sa platform sa build guide; sa macOS, karaniwang tumatakbo ang executable nang wala ang mga ito.
Setup (C++)
Ang mga snippet sa ibaba ay mga sipi mula sa kumpletong SqDRIFT.cpp program. I-build at patakbuhin ang file na iyon bilang iisang program.
Bago patakbuhin ang SqDRIFT, tiyaking naka-configure ang iyong IBM Quantum API token para sa IBM Cloud Platform.
Kung may Python ka na naka-install ang qiskit-ibm-runtime, ang pinakamadaling paraan para gumawa ng config file na tama ang format ay:
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(channel="ibm_quantum_platform", token="YOUR_IBM_QUANTUM_TOKEN_HERE")
Isinusulat nito ang ~/.qiskit/qiskit-ibm.json (Linux/macOS) o %USERPROFILE%\.qiskit\qiskit-ibm.json (Windows) na tama ang hugis. Kung mas gusto mong gawin ang file nang mano-mano:
{
"default-ibm-quantum-platform": {
"channel": "ibm_quantum_platform",
"token": "YOUR_IBM_QUANTUM_TOKEN_HERE"
}
}
Kunin ang iyong API key mula sa IBM Quantum Platform.
Tandaan: Ang nawawala o maling-format na config file ay nagdudulot ng hindi malinaw na Rust unwrap() panic sa startup sa halip na malinis na error. I-double-check ang pangalan ng key (default-ibm-quantum-platform) at ang value ng channel (ibm_quantum_platform) kung may problema ka.
Implementasyon (C++)
Hakbang 1: I-map ang classical inputs papunta sa quantum problem (C++)
I-load ang molecular Hamiltonian
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)
Nilo-load ang Hamiltonian ng molekulang mula sa isang FCIDump file. Ang bilang ng qubit ay katumbas ng dalawang beses ang bilang ng orbital (spin-up at spin-down).
Gumawa at i-normal-order ang Hamiltonian
// Create and normal order Hamiltonian
QfFermionOperator* hamiltonian = qf_ferm_op_from_fcidump(fcidump);
QfFermionOperator* normal_ordered = qf_ferm_op_normal_ordered(hamiltonian, nullptr);
Kino-convert ang FCIDump data sa isang fermionic operator at naglalapat ng normal ordering para pasimplehin ang istruktura ng operator.
I-group ang mga term ayon sa electronic structure
// 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
Ginagrupo ang magkakaugnay na Hamiltonian term ayon sa electronic structure bago buuin ang kanilang evolution circuit.
Kalkulahin ang sampling weights, pagkatapos ay i-normalize at i-map sa qubit operators
Mahalaga ang pagkakasunod-sunod dito. Nire-rescale ng normalization ang bawat coefficient sa unit magnitude in place, kaya dapat munang kalkulahin ang sampling weights. Kung mauuna ang normalization, ang bawat ay babalik bilang eksaktong 1.0 at ang weight ng bawat grupo ay magiging simpleng bilang na lang ng mga term nito — kaya ang sampling distribution ay itutulak ng laki ng grupo sa halip na ng mga pisikal na coefficient.
Ang weight ng bawat grupo ay ang mean absolute coefficient ng mga term nito. Ginagamit ng QDriftTrotterization pass ng Python tab ang parehong weight para sa isang grouped Hamiltonian. Kung sum ang gagamitin, magsa-scale ang ayon sa laki ng grupo, na siya namang magbabago sa evolution time na .
// 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
Ang weight na ay ang mean absolute coefficient sa mga term ng grupong , na binasa mula sa orihinal na mga coefficient. Pagkatapos, nire-rescale ng normalization ang bawat fermionic term sa unit magnitude bago ang Jordan-Wigner transformation, na pinapanatili lang ang phase/sign nito sa na-evolve na operator. Tugma ito sa normalized-term evolution ng qDRIFT: tinutukoy ng laki ng mga coefficient ang sampling probability sa pamamagitan ng , habang ang circuit evolution ay gumagamit ng normalized na term para hindi mabilang nang dalawang beses ang malalaking coefficient.
I-sample ang mga operator set
// 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...
Kumukuha ng mga operator set para sa 100 random na circuit, bawat isa ay may 10 grupong na-sample mula sa weighted distribution. Ginagawa ng susunod na step ang mga set na ito bilang ensemble ng mga qDRIFT circuit.
Hakbang 2: Buuin ang mga qDRIFT circuit (C++)
Sa C++ na bersyon, binubuo ng step na ito ang mga circuit mula sa mga na-sample na operator set. Ang transpilation para sa target na backend ay nangyayari sa Hakbang 3, kaagad bago isumite ang bawat circuit.
// 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
Pagkatapos, nagpi-print ang kumpletong program ng mga circuit statistic at ng mga operator index ng circuit 0, na hindi isinama rito.
Para sa bawat na-sample na operator, gumagawa ng Suzuki-Trotter evolution circuit na may time na , kung saan ang ay ang bilang ng operator na na-sample bawat circuit at ang ay ang time step. Dahil ang bawat na-sample na grupo ay na-normalize term-by-term bago ang mapping, ang evolution na ito ay naglalapat lang ng phase/sign ng bawat term sa rotation; ang mga magnitude ay nag-aambag lamang sa pamamagitan ng sampling distribution. Inihahanda ang Hartree-Fock initial state sa pamamagitan ng paglalapat ng X gate sa mga qubit 0-6 at 10-16 (pitong electron sa bawat spin sector). Ang mga instruction ng bawat evolution circuit ay mano-manong idinadagdag sa pangunahing circuit gamit ang C++ API.
Hakbang 3: Isagawa gamit ang Qiskit primitives (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;
Kumokonekta sa IBM Quantum, pinipili ang backend ayon sa pangalan, tina-transpile ang bawat circuit para sa backend na iyon, at nagsusumite ng isang job bawat circuit: 100 hiwalay na Sampler job na may tig-100 shot sa isang 20-qubit register. Pagkatapos, pino-poll nito ang bawat job hanggang matapos at kinokolekta ang mga bitstring mula sa result[0]. Gumagamit ng QPU time ang bawat job at hiwalay na naghihintay sa queue, kaya asahan na mas matagal ang step na ito kaysa sa iisang batched na job.
Ang mga resulta ng hardware na sinipi sa pahinang ito ay galing sa takbo sa ibm_pittsburgh (100 circuit × 100 shot), na ang backend_name ay pinalitan mula sa default na ibm_fez. Ang mga na-save na sample ay pinoproseso gamit ang naitamang determinant bit ordering na ipinakita sa Step 4. Nag-iiba ang ingay ng hardware sa bawat takbo, kaya iba ang iyong mga bilang at final energy.
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
Pagpapalit ng backend: Palitan ang backend_name sa SqDRIFT.cpp para i-target ang ibang device; inililista ng service.backends() ang maaabot ng account mo.
Hakbang 4: I-post-process at ibalik ang resulta sa nais na classical format (C++)
I-postselect ang mga bitstring at isulat ang mga CI string
// 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!
Nagpo-postselect ng mga bitstring na may tamang Hamming weight (n_alpha spin-up, n_beta spin-down na electron, hango sa field na NELEC ng FCIDump), kino-convert ang mga ito sa Configuration Interaction (CI) string gamit ang spin symmetrization, at isinusulat ang mga ito sa alphadets_from_sqd.txt para sa susunod na Selected Basis Diagonalization.
Linisin ang mga 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;
Maayos na pinapalaya ang lahat ng inilaang memory at resource. Awtomatikong nalilinis ang C++ std::vector<QuantumCircuit> sa pamamagitan ng RAII. Mano-manong pinapalaya ang mga qubit operator, fermionic operator, at ang FCIDump data.
Ngayong nabuo na natin ang basis na pagpoproyektuhan ng ating Hamiltonian, maaari na tayong magpatuloy sa proseso ng diagonalization para makuha ang ground state estimate. Patakbuhin ang mga sumusunod na command mula sa project root, docs/tutorials/assets/sqdrift/cpp.
I-build ang SBD diag binary
Kinukuha ng CMake ang SBD bilang source-only sa deps/sbd/ (sadyang hindi kino-compile ng pangunahing build ito). Gumagamit ang SBD app ng payak na Makefile, at ang SBD mismo ay nangangailangan ng MPI, OpenMP, at BLAS/LAPACK — kaya hindi sapat ang payak na make hangga't hindi naka-install ang mga iyon at hindi tumutugma ang Configuration file ng app sa toolchain mo.
Binabasa ng Makefile ang mga compiler at link flag nito mula sa Configuration file sa parehong directory. Ang bersyong ipinapadala ng upstream ay naka-tune para sa isang partikular na macOS/Homebrew layout at hindi mabubuo nang ganoon lang sa isang karaniwang makina — lalo na, ang -I/opt/homebrew/opt/llvm/include ay walang omp.h (nasa version-specific na clang resource directory ito), at tahasang tinatanggihan ng clang++ ng Apple ang -fopenmp:
clang++: error: unsupported option '-fopenmp'
Ang sumusunod na recipe ay napatunayang gumagana sa macOS (Apple Silicon, Darwin 25.6, Open MPI 5.x + Homebrew LLVM).
macOS (Apple Silicon) — nasubukan
# 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
Pagkatapos, palitan ang deps/sbd/apps/chemistry_tpb_selected_basis_diagonalization/Configuration ng:
# 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 ../../../..
Ang matagumpay na build ay nagpi-print ng dalawang compile/link na linya at nag-iiwan ng diag binary sa tabi ng Makefile. Hindi nakakasama ang isang ld: warning: ignoring duplicate libraries: '-lomp'.
Intel macOS: Ang prefix ng Homebrew ay /usr/local sa halip na /opt/homebrew, kaya palitan ito sa parehong naunang path.
Linux (Ubuntu/Debian) — hindi pa nasusubukan
Sakop ng mga sumusunod na prerequisite ang MPI, OpenMP, at BLAS/LAPACK. Natively sinusuportahan ng GCC ang -fopenmp, kaya hindi kailangan ng compiler override. Hindi pa ito napapatunayan sa 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
Patakbuhin ang SqDRIFT para gumawa ng mga CI string
Kung napatakbo mo na ang ./SqDRIFT sa Steps 1–4, nasa project root na ang alphadets_from_sqd.txt at maaari mong laktawan ang step na ito. Kung hindi, patakbuhin ito mula sa project root:
./SqDRIFT
# Writes alphadets_from_sqd.txt in the project root
Patakbuhin ang diagonalization
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 ../../../..
Mga parameter:
--fcidump: Path papunta sa FCIDump file na naglalaman ng Hamiltonian--adetfile: Path papunta sa alpha determinants file (mga CI string mula sa SqDRIFT)--method 0: Davidson diagonalization method--iteration 100: Pinakamataas na bilang ng Davidson iteration--block 10: Block size para sa Davidson algorithm--tolerance 1e-8: Convergence tolerance para sa energy
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
Ang linyang Energy = -107.5108722590187 ang ground-state estimate sa na-sample na subspace.
Paano ito naiiba sa Python example
Magkaiba ang dalawang example sa ilang aspeto nang sabay-sabay, hindi lang sa kung paano nila tinatrato ang mga bitstring na may maling symmetry, kaya hindi inaasahang magbibigay sila ng magkaparehong energy:
| C++ tab | Python tab | |
|---|---|---|
| Mga bitstring na may maling symmetry | Itinatapon ng postselect_bitstrings ayon sa Hamming weight | Inaayos ng iterative configuration recovery |
| Istruktura ng loop | Isang pass: sample → postselect → diagonalize nang isang beses | Outer loop: diagonalize, basahin ang orbital occupancies, i-recover ang mga configuration, i-diagonalize ulit |
| Bilang ng circuit | 100 circuit (num_circuits = 100) | 400 circuit (200 bawat evolution time) |
| Mga evolution time | Isang (time_step = 1) | Dalawang time, times = [1.0, 10.0] |
| Mga diagonal term | Pinapanatili sa na-sample na operator | Inaalis ng filter_diagonal_terms, na nagbabago sa sampling distribution at sa mga circuit |
| Pagkakasunod ng term | Naka-group lang | Inilalapat ang canonical_order bago mag-group para sa reproducibility |
| Diagonalization solver | SBD (diag, external na MPI/OpenMP binary) | In-process na solver ng qiskit-addon-sqd |
| Backend | Nakatakda ayon sa pangalan (backend_name = "ibm_fez"; ang takbo sa pahinang ito ay gumamit ng ibm_pittsburgh) | least_busy na operational backend na may sapat na qubit |
| Transpilation | Bawat circuit nang hiwalay, optimization level 2 | Lahat ng circuit nang sabay-sabay, optimization level 3 |
| Pagsusumite ng job | 100 job, isang circuit bawat isa | Isang Sampler job na naglalaman ng lahat ng 400 circuit |
| Simulator run | Wala; hardware lang | Noiseless na simulator example bago ang hardware run |
Gumagamit ang dalawang bersyon ng parehong Hamiltonian, ginagrupo ang mga term nito ayon sa electronic structure, tinitimbang ang bawat grupo ayon sa mean absolute coefficient ng mga term nito, nagsisimula sa Hartree-Fock state, at nagsa-sample ng 10 grupo at 100 shot bawat circuit.
Bawat pagkakaiba ay nakakaapekto sa na-sample na subspace, at kaya sa energy, nang nakapag-iisa. Parehong pinalalawak ng mas malaking ensemble at ng pangalawa, mas mahabang evolution time ang mga configuration na ine-explore ng Python example; ang pagsa-sample sa higit sa isang time ang nagbibigay sa SqDRIFT ng Krylov-like na saklaw ng low-energy space. Binabago ng pag-filter ng mga diagonal term kung aling mga operator ang maaaring kunin ng sampler at ang kanilang mga weight, kaya magkaibang distribution ang sina-sample ng dalawang takbo.
Mahigpit na filter ang postselection, at ang shot na may maling particle number ay tuluyang itinatapon. Sa takbo sa itaas, nagtapon ito ng karamihan sa data: 1137 sa 10000 shot ang pumasa sa (7,7) Hamming-weight check (mga 11%), na nauwi sa 49 lang na natatanging -determinant. Sa halip, ginagamit ng configuration recovery ang average orbital occupancies mula sa naunang diagonalization para i-flip ang mga bit at ayusin ang mga ganoong shot, na nire-recycle ang itatapon sana ng postselection. Dahil sa feedback na iyon, likas itong iterative: ang recover_configurations sa qiskit-addon-sqd-hpc ay nangangailangan ng argumentong avg_occupancies na umiiral lamang pagkatapos tumakbo ang isang diagonalization.
Ang C++ run na ito ay nagbibigay ng subspace na 49 -determinant at energy na -107.5109 Ha. Ang buong -determinant space para sa pitong electron sa 10 orbital ay string lang, kaya ang pagpapasok ng lahat ng 120 sa parehong diag binary ay nagbibigay ng eksaktong FCI energy na -107.6482 Ha. Kaya ang takbo ay nasa humigit-kumulang 0.1373 Ha sa itaas ng eksakto habang sumasaklaw sa 49 sa 120 -determinant. Dahil variational ang SQD sa napiling subspace, ang mas maliit na subspace ay maaari lang magpataas ng energy, kaya ang hindi kumpletong subspace ang pinakadirektang paliwanag sa agwat.
Posible lang ang exact-FCI check na ito dahil maliit ang example; hindi ito bahagi ng SqDRIFT workflow, at para sa mga laki ng sistema kung saan talagang kailangan ang SQD, walang ganoong reference. Nagsisilbi lang itong sukatin kung gaano karami ng space ang nabawi ng takbong ito.
Hindi kontrolado ang paghahambing sa Python example, kaya hindi dapat iugnay ang pagkakaiba sa kawalan lang ng configuration recovery; tandaan na magkakaiba nang sabay-sabay ang bilang ng circuit, ang iisang evolution time, ang diagonal-term filtering, at ang ingay ng hardware, at lahat ay humuhubog sa subspace.
Para mapaliit ang agwat, narito ang ilang posibleng susunod na hakbang na subukan pagkatapos makumpleto ang tutorial na ito: dagdagan ang shots, num_circuits, o ops_per_circuit; mag-sample sa higit sa isang time_step; at magdagdag ng recovery loop sa ibabaw ng Qiskit::addon::sqd::recover_configurations.
Mga susunod na hakbang
-
Sample-based Krylov quantum diagonalization ng isang fermionic lattice model — isang kaugnay na tutorial na gumagamit ng time-evolution circuits sa halip na variational ansatz.
-
Sample-based quantum diagonalization ng isang chemistry Hamiltonian — isang tutorial kung paano bumuo ng local unitary cluster Jastrow (LUCJ) circuit para sa quantum chemistry simulation.
-
Ang papel na SqDRIFT — ang literaturang pinagbatayan ng tutorial na ito. (Tandaan na ang ilan sa mga optimization na tinalakay sa papel na ito ay kasalukuyang work in progress, at maaaring magbago ang tutorial na ito sa hinaharap batay sa ebolusyon ng mga ginamit na library.)