อัลกอริทึม SqDRIFT สำหรับการประมาณสถานะพื้น
- Python
- C++
ดาวน์โหลดโน้ตบุ๊ก Python เพื่อรันบทช่วยสอนนี้ด้วยตัวเอง โน้ตบุ๊กจะดาวน์โหลดอินพุต FCIDump หากยังไม่มีอยู่
ประมาณเวลาที่ใช้: 180 วินาทีบนโปรเซสเซอร์ Heron r3 (หมายเหตุ: นี่เป็นเพียงค่าประมาณเท่านั้น เวลารันจริงของคุณอาจแตกต่างออกไป)
ผลลัพธ์การเรียนรู้
-
เรียนรู้วิธีสร้างวงจรที่มีความลึกน้อยกว่าเมื่อเทียบกับ Trotterization
-
ทำความเข้าใจ workflow แบบ end-to-end สำหรับการประมาณสถานะพื้นโดยใช้ qDRIFT และ SQD
-
เรียนรู้วิธีใช้
qiskit-fermionsร่วมกับ Qiskit addon อื่นๆ เพื่อนำ workflow ดังกล่าวไปใช้งาน
ข้อกำหนดเบื้องต้น
-
อ่านภาพรวม Sample-based quantum diagonalization (SQD)
-
อ่านบทเรียน Sample-based Krylov Quantum Diagonalization (SKQD)
ความเป็นมา
SqDRIFT เป็นรูปแบบหนึ่งของ SKQD ที่แทนที่ความจำเป็นในการเลือก ansatz ที่ใช้สุ่มตัวอย่างบิตสตริงด้วยกลุ่ม (ensemble) ของวงจรวิวัฒนาการตามเวลาที่สร้างขึ้นโดยตรงจากแฮมิลโทเนียนเป้าหมาย สิ่งนี้ทำได้โดยการสุ่มตัวอย่างย่อยตัวดำเนินการวิวัฒนาการตามเวลาที่เล็กกว่าจากแฮมิลโทเนียนตามค่าสัมประสิทธิ์ของมัน ซึ่งเรียกว่าวิธี qDRIFT Trotterization
ทั้งสองการทำงานใช้ Qiskit Fermions เพื่อแสดง Hamiltonian ของโมเลกุลและสร้าง circuit สำหรับอัลกอริทึม qDRIFT การทำงานแบบ Python ใช้ transpiler pass แบบเฟอร์มิออน ส่วนการทำงานแบบ C++ สุ่มตัวอย่างกลุ่มอย่างชัดเจนและแมปไปยังตัวดำเนินการ qubit
ให้แฮมิลโทเนียนมีรูปแบบดังนี้
โดยที่ โดยไม่เสียนัยทั่วไป เรากำหนดให้ และค่าลักษณะเฉพาะ (eigenvalue) ที่ใหญ่ที่สุดของ มีค่าสัมบูรณ์เท่ากับ ตัวประกอบที่มีเครื่องหมายหรือเป็นจำนวนเชิงซ้อนใดๆ จะถูกดูดซับเข้าไปใน ดังนั้นสัมประสิทธิ์ จึงเป็นน้ำหนักบวกล้วนๆ ในขณะที่ ถือทิศทางของแต่ละพจน์ ในที่นี้ คือจำนวนพจน์ (หรือหลังจากการจัดกลุ่มแล้ว คือจำนวนกลุ่ม) ในแฮมิลโทเนียน มันเป็นคุณสมบัติของแฮมิลโทเนียนและแตกต่างจากจำนวนตัวดำเนินการที่ถูกสุ่มเข้าไปในวงจรเดียว ซึ่งเขียนแทนด้วย ด้านล่าง
จากนั้นอัลกอริทึม qDRIFT จะสร้างตัวดำเนินการ สำหรับเวลาเป้าหมาย โดยที่ มีค่าตั้งแต่ และหมายถึงวงจร SqDRIFT ลำดับที่ ซึ่งนิยามเป็น
ในที่นี้ คือจำนวนตัวดำเนินการที่สุ่มมาต่อวงจร และ คือจำนวนวงจรใน ensemble ผลคูณดำเนินไปตามการสุ่ม ครั้ง ไม่ใช่ตามพจน์ ทั้งหมดของแฮมิลโทเนียน และเนื่องจากพจน์เหล่านี้ถูกสุ่มแบบมีการแทนที่ (with replacement) ตัวเดียวกันอาจปรากฏมากกว่าหนึ่งครั้งใน เดียว
ปริมาณ
คือ norm แบบ ของสัมประสิทธิ์ ดังนั้นแต่ละขั้นตอนใน ขั้นตอนจะวิวัฒนาการเป็นระยะเวลาเท่ากันคือ ไม่ว่าจะสุ่มพจน์ใดมาก็ตาม ความสม่ำเสมอของมุมในแต่ละขั้นตอนคือลักษณะเฉพาะของ qDRIFT สัมประสิทธิ์มีอิทธิพลต่อผลลัพธ์ผ่านความถี่ที่พจน์ของมันถูกสุ่มมา ไม่ใช่ผ่านระยะที่พจน์นั้นถูกหมุน ดัชนีถูกสุ่มมาจากการแจกแจง
ดังนั้นลำดับ จึงเป็นลำดับสุ่มของดัชนีพจน์ที่สุ่มมาจากการแจกแจงนี้ เนื่องจาก เป็นบวกและรวมกันได้ นี่จึงเป็นการแจกแจงความน่าจะเป็นแบบปรับให้เป็นบรรทัดฐาน (normalized) และค่าคาดหมายของช่องสัญญาณ (channel) ที่ได้จากการสุ่มเหล่านี้ประมาณค่าวิวัฒนาการภายใต้ โดยมีข้อผิดพลาดที่ลดลงเมื่อ เพิ่มขึ้น โปรดสังเกตว่าข้อผิดพลาดในการประมาณขึ้นอยู่กับ ไม่ใช่จำนวนพจน์
(บทความ SqDRIFT เขียนจำนวนพจน์เป็น และความยาวของลำดับเป็น ในที่นี้เราใช้ และ เพื่อให้ทั้งสองแยกความแตกต่างกันได้ชัดเจน)
บทช่วยสอนนี้แสดงวิธีสร้างกลุ่มของวงจรแบบสุ่มเหล่านี้ หลังจากที่เราสร้างวงจรเหล่านี้แล้ว คล้ายกับวิธีที่เราสร้างซับสเปซ Krylov สำหรับตัวดำเนินการที่แตกต่างกัน เราสุ่มตัวอย่างบิตสตริงจากตัวดำเนินการหลายตัวเช่นนี้ด้วยพารามิเตอร์เวลาที่แตกต่างกัน สิ่งนี้ทำให้มั่นใจได้ว่ามีความซ้อนทับ (overlap) ที่สูงขึ้นระหว่างเวกเตอร์สถานะพื้นและบิตสตริงที่สุ่มมา
ข้อกำหนด
ก่อนเริ่มบทช่วยสอนนี้ ตรวจสอบให้แน่ใจว่าคุณได้ติดตั้ง
- สภาพแวดล้อมเสมือน Python (>=3.10)
- pip>=25.1
qiskit~= 2.5qiskit-fermions==0.1.0 (โปรดสังเกตว่าชื่อเป็นพหูพจน์)- numpy
- pyscf
qiskit-aerqiskit-ibm-runtimeqiskit-addon-sqd
คุณสามารถติดตั้งแพ็กเกจที่จำเป็นทั้งหมดได้ด้วยคำสั่ง
pip install "qiskit~=2.5" "qiskit-fermions==0.1.0" qiskit-aer qiskit-ibm-runtime qiskit-addon-sqd pyscf numpy
การตั้งค่า
# 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,
)
การนำไปใช้งาน
ขั้นตอนที่ 1: แมปอินพุตแบบคลาสสิกเป็นปัญหาควอนตัม
ขั้นตอน Python ด้านล่างใช้ตัวจำลอง Aer ตัวอย่างฮาร์ดแวร์ตามมาภายหลัง
การอ่านและเตรียม FCIDump
สำหรับบทช่วยสอนนี้ เราจะโหลดแฮมิลโทเนียนโครงสร้างอิเล็กทรอนิกส์สำหรับไนโตรเจน (N2) ยังมีวิธีอื่นในการสร้างตัวดำเนินการแบบเฟอร์มิออนอีกด้วย ดูเอกสารที่ qiskit_fermions.operators.library
เกี่ยวกับ FCIDump นี้ ไฟล์ N2_sto_3g อธิบายโมเลกุลไนโตรเจน () ในเบซิส STO-3G ขั้นต่ำ ที่ระยะห่างระหว่างอะตอม 1.09 ซึ่งเป็นความยาวพันธะสมดุลจากการทดลอง เฮดเดอร์ของไฟล์ประกาศ NORB=10, NELEC=14, และ MS2=0 นั่นคือ 10 spatial orbitals (จึงมี 20 spin orbitals และ 20 qubits ภายใต้ Jordan-Wigner) 14 electrons ในสถานะ spin singlet ดังนั้นจึงมี 7 และ 7 electrons ทุก orbitals ถูกกำหนด symmetry label เป็น 1 นั่นคือไม่มีการใช้ point-group symmetry เนื่องจากเป็น full-space STO-3G dump จึงไม่มี orbitals ใดถูกแช่แข็ง (frozen) และ correlation space มีขนาดเล็กพอที่จะคำนวณ exact FCI reference energy แบบคลาสสิกเพื่อเปรียบเทียบได้ ดังแสดงในช่วงโค้ดถัดไป
สามารถสร้างไฟล์ที่เทียบเท่ากันขึ้นใหม่ได้ด้วย 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")
เนื่องจาก integrals ขึ้นอยู่กับ orbitals ที่ converged จาก SCF ไฟล์ที่สร้างขึ้นใหม่อาจแตกต่างจากไฟล์ที่ให้มาในเรื่อง orbital phase หรือลำดับ แต่ total energies จะไม่ได้รับผลกระทบ
การได้ไฟล์มา ค้นหา FCIDump ได้ใน GitHub repository นี้ คุณสามารถรันโค้ดด้านล่างเพื่อดึงไฟล์มาไว้ในตำแหน่งที่ส่วนที่เหลือของทูโทเรียลนี้คาดหวังไว้
ก่อนอื่นเราใช้ cisolver ที่มาพร้อมกับ pyscf เพื่อหาค่า reference energy นี่คือค่าพลังงานสถานะพื้น (ground state energy) ที่แท้จริงของโมเลกุลที่เรากำลังทำงานด้วย สำหรับสิ่งนี้เราจะประกาศ norb และ nelec ก่อน ซึ่งคือจำนวน orbitals และจำนวน electrons ตามลำดับ จากนั้นเราจะประกาศ h1e และ h2e ซึ่งคือ one- และ two-electron integrals ตามลำดับ ทั้งหมดนี้จะถูกนำไปใช้สำหรับ SQD ในภายหลังด้วยเช่นกัน
import os
from urllib.request import urlopen
# The FCIDump is stored with this tutorial in the Qiskit documentation repository.
FCIDUMP_URL = "https://raw.githubusercontent.com/Qiskit/documentation/main/docs/tutorials/assets/sqdrift/fcidump_files/N2_sto_3g"
FCIDUMP_PATH = "fcidump_files/N2_sto_3g"
if not os.path.exists(FCIDUMP_PATH):
os.makedirs(os.path.dirname(FCIDUMP_PATH), exist_ok=True)
with urlopen(FCIDUMP_URL) as response:
contents = response.read()
with open(FCIDUMP_PATH, "wb") as f:
f.write(contents)
print(f"Downloaded FCIDump to {FCIDUMP_PATH}")
else:
print(f"Using existing FCIDump at {FCIDUMP_PATH}")
name = "fcidump_files/N2_sto_3g"
fcidump = tools.fcidump.read(name)
# Extract metadata from the FCIDump header
norb = fcidump["NORB"] # number of spatial orbitals
nelec = fcidump["NELEC"] # total number of electrons
e_nuc = fcidump["ECORE"] # nuclear repulsion / core energy
ms2 = fcidump["MS2"] # 2S (spin)
num_elec_a = (nelec + ms2) // 2 # alpha electrons
num_elec_b = (nelec - ms2) // 2 # beta electrons
# Reconstruct full 4-index ERIs from the FCIDump (stored in 8-fold symmetry)
h1e = fcidump["H1"] # shape (norb, norb)
h2e = ao2mo.restore( # shape (norb, norb, norb, norb)
1, fcidump["H2"], norb
)
cisolver = fci.direct_spin1.FCI()
cisolver.max_cycle = 200
cisolver.conv_tol = 1e-12
e_fci, _ = cisolver.kernel(
h1e,
h2e,
norb,
(num_elec_a, num_elec_b),
ecore=e_nuc, # adds nuclear repulsion to the final energy
)
reference_energy = e_fci
print(f"Reference FCI Energy = {reference_energy:.10f} Ha")
nuclear_repulsion_energy = fcidump["ECORE"]
print(f"Nuclear Repulsion Energy = {nuclear_repulsion_energy:.10f} Ha")
Reference FCI Energy = -107.6481842917 Ha
Nuclear Repulsion Energy = 23.7887003074 Ha
การโหลด Hamiltonian
เมื่อมีข้อมูลที่จำเป็นพร้อมแล้ว เราจะอ่าน Hamiltonian จากไฟล์ FCI ในรูปแบบที่เข้ากันได้กับ qiskit-fermions
fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb
Fermionic workflows ด้วย qiskit-fermions
ก่อนอื่นเราจะแมป Hamiltonian ลงใน fermionic circuit model โดยใช้ qiskit-fermions ซึ่งมี transpiler passes และ gates เฉพาะสำหรับ fermionic circuits สิ่งเหล่านี้จะถูกใช้ก่อน transpiler passes แบบดั้งเดิมของ Qiskit สำหรับ workflow นี้ในภายหลัง
การจัดกลุ่มเทอม (Term grouping)
เพื่อให้มั่นใจถึงความสามารถในการทำซ้ำผลลัพธ์ได้ (reproducibility) ก่อนอื่นเราใช้ canonical_order เพื่อเรียงลำดับเทอมโดยอิงจากโครงสร้างของมันเท่านั้น ลำดับของ operators ใน list canon จึงถูกกำหนดไว้แน่นอน สิ่งนี้ช่วยให้มั่นใจถึงความสามารถในการทำซ้ำได้ของ operators ที่สร้างขึ้น เนื่องจาก pass QDriftTrotterization ที่เราจะใช้ในภายหลังนั้นสุ่มเลือก indices เพื่อสร้าง qDRIFT operators
ในขั้นตอนนี้ เราใช้ประโยชน์จาก symmetries จำนวนมากที่มีอยู่ใน electronic structure Hamiltonian โดยการจัดกลุ่มเทอมที่เกี่ยวข้องกันซึ่งมี coefficients เหมือนกัน แม้การทำเช่นนี้จะเปลี่ยนการกระจายของ operator coefficient ที่ qDRIFT protocol สุ่มเลือกจาก แต่จะไม่ส่งผลต่อ convergence guarantees ของมัน สิ่งสำคัญคือ การจัดกลุ่มเทอมที่เกี่ยวข้องกันด้วย symmetry จะส่งผลให้เกิดการหักล้างกันของ Pauli terms ที่ดี และทำให้ circuit depth โดยรวมสั้นลงเมื่อทำการ time-evolving สถานะภายใต้การกระทำของมัน
qiskit-fermions มีฟังก์ชัน group_terms_by_electronic_structure ที่ทำการจัดกลุ่มนี้ให้เรา
โปรดทราบว่า group_terms_by_electronic_structure สันนิษฐานว่าเทอมต่างๆ อยู่ใน normal ordering
การกรองเทอมแนวทแยง (diagonal terms)
เราจะลบเทอมแนวทแยงออกจาก Hamiltonian ที่ใช้สร้าง circuits เพื่อให้ช่อง qDRIFT sampling จำนวน ช่องถูกใช้ไปกับเทอมที่เคลื่อนย้าย population ระหว่าง configurations เทอมประเภทนี้ควรถูกกรองออกจาก Hamiltonian ในจุดนี้ ก่อนที่ gate Evolution จะถูกสร้างขึ้นในขั้นตอนถัดไป
เทอมที่เป็นประเด็นคือเทอมที่เป็นแนวทแยงใน occupation-number basis นั่นคือผลคูณของ number operators มีเทอมสามประเภทที่เข้าข่ายลักษณะนี้:
-
ค่าออฟเซ็ตพลังงานคงที่ (constant energy offset) ซึ่งเป็นผลคูณของ number operators ศูนย์ตัว โดยการ time evolution ของมันส่งผลเพียงแค่ global phase
-
number operators แต่ละตัว ซึ่ง time evolution ของมันลดรูปเหลือเพียง single-qubit rotations
-
ผลคูณลำดับสูงกว่า (higher-order products) เช่น
เมื่อพิจารณาเพียงลำพัง ไม่มีเทอมใดในนี้ที่เคลื่อนย้าย population ระหว่าง occupation-number configurations เทอมเหล่านี้ทำหน้าที่เพียงแค่กับ phases ของ configurations ที่มีอยู่แล้วเท่านั้น อย่างไรก็ตาม เทอมเหล่านี้ไม่ได้นิ่งเฉยโดยสิ้นเชิง เพราะ relative phases เหล่านั้นป้อนเข้าสู่การแทรกสอด (interference) ที่เกิดจากเทอม excitation ในภายหลังของ circuit ดังนั้นการกรองเทอมเหล่านี้ออกจึงเปลี่ยนแปลง evolution ที่ถูกสร้างขึ้นจริง และสามารถเปลี่ยนแปลง sampling distribution ได้ นี่คือการประมาณค่าโดยเจตนาในขั้นตอนการสร้าง circuit ซึ่งทำขึ้นเพื่อมุ่งเน้น sampling ไปที่เทอม excitation มากกว่าที่จะเป็นขั้นตอนที่ไม่ส่งผลต่อ distribution ที่ถูกสุ่มออกมา ต่างจากการจัดกลุ่มด้วย symmetry ข้างต้นซึ่งไม่กระทบ qDRIFT convergence guarantees การกรองนี้เปลี่ยนแปลง operator ที่กำลังถูก evolve circuits จึงไม่ประมาณค่า evolution ภายใต้ Hamiltonian เต็มรูปแบบอีกต่อไป และขอบเขตความผิดพลาดของ qDRIFT จะใช้กับ operator ที่ถูกกรองแล้ว ไม่ใช่ operator เดิม สิ่งนี้ยอมรับได้ในที่นี้เพราะ circuits เป็นเพียง sampling heuristic ที่ใช้เสนอ configurations เท่านั้น ไม่มีเทอมใดสูญหายไปจากการประมาณค่าพลังงานเอง เนื่องจากการกรองนี้ใช้เฉพาะกับ Hamiltonian ที่ใช้สร้าง circuits ในขณะที่ classical diagonalization ในภายหลังใช้ Hamiltonian เต็มรูปแบบ รวมถึงเทอมแนวทแยงด้วย ความแม่นยำของ SQD ขึ้นอยู่กับขั้นตอน classical นั้น ซึ่งยังคง variational อยู่ใน sampled subspace ไม่ว่า configurations จะถูกเสนอมาอย่างไร
ฟังก์ชัน filter_diagonal_terms() ลบเทอมประเภทนี้ออกจาก operator แบบ in place มันระบุเทอมเหล่านี้จากโครงสร้าง normal-ordered ของมัน — multiset ของ creation modes ที่ตรงกับ multiset ของ annihilation modes — ดังนั้นจึงใช้ได้เฉพาะกับ operator ที่เป็น normal-ordered อยู่แล้วเท่านั้น สมมติฐานนี้ไม่ได้ถูกตรวจสอบขณะรันไทม์
# 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
เมื่อเราจัดกลุ่มเทอมใน Hamiltonian แล้ว เราจะตัดสินใจเลือกพารามิเตอร์ต่อไปนี้เพื่อสร้าง ensemble ของ circuits:
- จำนวน circuits ที่จะสร้าง:
num_circuits - ความยาวของแต่ละ circuit ในแง่ของกลุ่ม excitation:
num_exc - ตัวประกอบสำหรับเวลา evolution ที่แตกต่างกัน:
times
การสร้าง fermionic circuits
ตอนนี้เราจะสร้าง fermionic circuits สำหรับแต่ละ time-step แต่ละ circuit จะประกอบด้วย evolution gate เพียงตัวเดียว โดยใช้ evolution time ที่เราประกาศไว้ก่อนหน้านี้ evolution operator คือ Hamiltonian ภายหลังเราจะรัน transpiler passes บน circuits เหล่านี้เพื่อสร้าง qDRIFT circuits
การเตรียม Ansatz
เราเตรียมสถานะ Hartree-Fock โดยใช้คลาส InitializeModes สำหรับไนโตรเจน กระบวนการนี้เป็นเพียงการใช้ X gates กับ num_elec_a qubits แรก แล้วตามด้วย num_elec_b qubits ซึ่งทั้งสองค่าเท่ากับเจ็ดสำหรับไนโตรเจน สถานะนี้แทน 7 และ 7 electrons ของไนโตรเจน
# 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)
ขั้นตอนที่ 2: ปรับปัญหาให้เหมาะสมสำหรับการทำงานบนฮาร์ดแวร์ควอนตัม
เมื่อเรามี circuits ของเราแล้ว เราจะใช้ passes ที่มีอยู่ใน qiskit-fermions ก่อนเพื่อทำ fermionic level optimizations ตามด้วยการ transpile circuit ของเราสำหรับ backend ที่เลือก เนื่องจากนี่คือการทดลองแบบ simulator เราจะทำสิ่งนี้ให้กับ AerSimulator ก่อน
การคำนวณน้ำหนักสำหรับแต่ละกลุ่ม
ในขั้นตอนนี้ เราทำการ qDRIFT sampling ของเทอมแบบสุ่มโดยมีความน่าจะเป็นแปรผันตาม coefficients ของเทอมนั้นใน Hamiltonian pass transpiler ของ qDRIFT ทำสิ่งนี้ให้เรา ตอนนี้เราสามารถสร้าง circuits ที่ตื้นกว่าซึ่งสามารถรันบนฮาร์ดแวร์ได้อย่างมีประสิทธิภาพมากขึ้น แม้จะมีการเชื่อมต่อ qubit ที่จำกัด แม้ในกรณีที่ Hamiltonian มี long-range couplings และเทอมที่มีลำดับสูงกว่ากำลังสอง หลังจากการจัดกลุ่มเทอมแล้ว จะทำการสุ่มเลือก operators ตามน้ำหนักของมัน สำหรับแต่ละ operator น้ำหนัก ถูกนิยามดังนี้:
เนื่องจากเทอมต่างๆ ถูกจัดกลุ่มไว้ใน Step 1 แล้ว แต่ละ ตรงนี้จึงเป็นทั้งกลุ่ม: คือค่าเฉลี่ยของสัมประสิทธิ์สัมบูรณ์ของเทอมในกลุ่ม และแต่ละเทอมในกลุ่มจะถูก evolve โดยลดสัมประสิทธิ์ของมันเหลือเพียงเครื่องหมาย
การปรับให้เหมาะกับ fermionic และฮาร์ดแวร์แบบ native
ฟังก์ชัน generate_preset_jw_pass_manager() คืนค่า MultiStagePassManager ที่รับ FermionicCircuit และสร้าง circuit สุดท้ายที่ถูกปรับให้เหมาะสมซึ่งเราสามารถ transpile เพื่อรันบนฮาร์ดแวร์ของเราได้ เราแทนที่ optimization stage เริ่มต้นด้วย FermionicPassManager ที่มี pass QDriftTrotterization ของเรา:
-
pass
QDriftTrotterizationใช้การคำนวณน้ำหนักและการสุ่มภายในเพื่อสร้าง circuits ที่เราจะใช้สำหรับ sampling -
pass
RelabelModesเป็น optimization pass อีกตัวที่สามารถใช้เพื่อสลับตำแหน่ง fermionic modes เพื่อปรับการเชื่อมต่อระหว่าง qubits ให้เหมาะสมและลด gate depth อ่านเพิ่มเติมได้ใน API reference
ขั้นตอนที่เหลือของ MultiStagePassManager จะรันโดยอัตโนมัติและจัดการการแมป fermion-to-qubit แบบเต็มรูปแบบ:
-
F2QLayout: preset pass manager จะใช้ pass
TrivialF2QLayoutซึ่งแมป fermionic bits จำนวน บิตไปยัง qubits จำนวน ตัวแบบตรงตัว (trivial) -
F2QSynth: transpilation pass สำหรับแมปคำสั่งของ circuit แบบ fermion-based ไปเป็นแบบ qubit-based
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
เมื่อเราทำ fermionic-level optimizations เสร็จแล้ว เราสามารถ transpile circuits สำหรับการรันบน simulator ได้
simulator = AerSimulator()
shots = 100
transpiled_circuits = transpile(sqdrift_circuits, simulator)
ขั้นตอนที่ 3: รันโดยใช้ Qiskit primitives
เมื่อเรามี circuits ของเราแล้ว เราสามารถรันมันโดยใช้ Qiskit primitives บน AerSimulator ได้ เราจะรวม counts จาก circuits ที่แตกต่างกันทั้งหมดเข้าด้วยกัน เราแปลงมันเป็น boolean vectors ก่อนที่จะทำการ post-processing ด้วย 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
ขั้นตอนที่ 4: ประมวลผลภายหลังและส่งคืนผลลัพธ์ในรูปแบบคลาสสิกที่ต้องการ
การใช้ bitstrings สำหรับ SQD
ตอนนี้เราสามารถรัน diagonalization scheme บน bitstrings ที่เลือกไว้เพื่อหาค่า eigenvalue ที่ต่ำที่สุดซึ่งจะสอดคล้องกับพลังงานสถานะพื้น (ground state energy) ของโมเลกุลได้ เราสร้างฟังก์ชัน callback ประกาศ occupancies เริ่มต้น และตั้งค่าพารามิเตอร์ก่อนที่จะรัน diagonalization scheme ในที่สุด ฟังก์ชัน callback ใช้เพื่อพิมพ์ iteration ปัจจุบันและค่าประมาณ eigenvalue ปัจจุบันในแต่ละ iteration
สุดท้าย เพื่อให้ได้ค่าประมาณพลังงานสถานะพื้น เราบวก nuclear_repulsion_energy เข้ากับพลังงานที่ได้
หมายเหตุ: มิติของ subspace ไม่คงที่ระหว่าง iterations แม้บน simulator ที่ไม่มี noise ก็ตาม — แต่ละ subsample ดึง configurations ชุดที่แตกต่างกัน และขั้นตอน recovery ปรับรูปร่าง pool ระหว่าง iterations ดังนั้นมิติที่รายงานจึงแตกต่างกันไปในแต่ละ subsample การสุ่มแบบไม่มี noise เองไม่ได้กำหนดมิติของ selected subspace ให้คงที่ อย่างไรก็ตาม การรันบนฮาร์ดแวร์มักให้ subspaces ที่ใหญ่กว่าอย่างเป็นระบบ เนื่องจาก shots ที่มี noise ทำลาย particle-number symmetry และ configuration recovery เปลี่ยนสิ่งเหล่านั้นให้เป็น basis vectors เพิ่มเติม ด้วยเหตุนี้ เราจะแนะนำอีกขั้นตอนหนึ่งสำหรับการตัดทอน (pruning) bitstrings ในส่วนของฮาร์ดแวร์ด้วย
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
ตัวอย่างฮาร์ดแวร์
ตัวอย่างนี้ใช้ 20 qubits (10 spatial orbitals) ตัวเลือกนี้เป็นไปเพื่อความสะดวกสำหรับทูโทเรียลที่ควรรันได้อย่างรวดเร็ว ไม่ใช่เพดานตายตัวของวิธีการนี้
ต้นทุนของขั้นตอน classical ไม่ได้ถูกกำหนดโดยจำนวน qubit โดยตรง SQD ทำ diagonalize Hamiltonian ที่ถูกฉายลงบน subspace ที่ครอบคลุมโดย configurations ที่ ถูกสุ่มมา ดังนั้นสิ่งที่ขับเคลื่อนต้นทุนของ classical คือมิติของ selected subspace นั้น — ซึ่งถูกควบคุมในที่นี้โดย samples_per_batch, num_batches และจำนวน configurations ที่แตกต่างกันที่ circuits สร้างขึ้นจริง — ร่วมกับ sparse linear algebra ที่จำเป็นในการใช้ projected Hamiltonian full CI space เติบโตแบบ combinatorial ตาม orbitals และ electrons แต่ selected subspace เป็นส่วนเล็กๆ ที่ปรับแต่งได้ของมัน และเราควบคุมขนาดของมันได้โดยตรง ดังนั้น จำนวน qubits และความยากของ classical จึงสามารถแปรผันได้ในระดับหนึ่งโดยอิสระจากกัน orbital space ที่กว้างขึ้นซึ่งถูกสุ่มลงใน subspace ขนาดพอประมาณอาจมีต้นทุนถูกกว่าระบบที่เล็กกว่าซึ่งถูก diagonalize บน subspace ที่ใหญ่มาก
ดังนั้นในทางปฏิบัติ ขนาดของระบบที่เป็นไปได้จึงขึ้นอยู่กับมิติของ subspace ที่คุณต้องการเพื่อความแม่นยำที่คุณต้องการ และขึ้นอยู่กับหน่วยความจำและจำนวนคอร์ที่มีให้กับ eigensolver โดยทั่วไป orbital spaces ที่ใหญ่กว่ามักต้องการ subspace ที่ใหญ่กว่าเพื่อให้ถึง chemical accuracy และนั่นคือสิ่งที่ในที่สุดผลักดันให้ต้องใช้ทรัพยากรแบบกระจาย (distributed) — ดู qiskit-addon-sqd-hpc สำหรับการขยายขั้นตอนนี้ แทนที่จะสมมติค่าตัดตอนคงที่ วิธีปฏิบัติจริงคือการเฝ้าดูมิติของ subspace ที่รายงานและการลู่เข้าของพลังงานตลอด iterations และเพิ่มขนาดของ subspace จนกว่าพลังงานจะหยุดดีขึ้นหรือคุณใช้หน่วยความจำที่มีอยู่จนหมด
หมายเหตุ: เนื่องจากความผิดพลาดจากการสุ่ม (sampling error) ที่เกิดจาก noise ในฮาร์ดแวร์ subspace ที่สร้างขึ้นสำหรับ diagonalization ในการรันบนฮาร์ดแวร์จะใหญ่กว่าสิ่งที่เราได้เมื่อใช้ simulator แม้จะเพิ่มมิติของ subspace ที่เราต้องการ diagonalize แต่ workflow ก็ยังคงให้คำตอบที่แม่นยำแก่เรา เนื่องจากความทนทานของ SQD ต่อ noise
การตัดทอนสตริงที่ผิดปกติ (spurious strings)
ในที่นี้เราสามารถเลือกที่จะทำขั้นตอนเพิ่มเติมได้ เมื่อเรามี bitstrings ทั้งหมดจากการรัน circuit เราสามารถกรอง bitstrings ที่ไม่ถูกต้องออกก่อนรัน SQD หรือดำเนินการต่อไปโดยไม่ตัดทอนก็ได้ การข้ามการตัดทอนโดยทั่วไปเป็นทางเลือกที่ดีกว่าสำหรับการรันบนฮาร์ดแวร์ เพราะมันปล่อยให้ shots ที่ symmetry ถูกทำลายยังคงพร้อมใช้งานสำหรับ configuration recovery ซึ่งสามารถซ่อมแซมให้กลายเป็น configurations ที่ถูกต้องได้ และด้วยเหตุนี้จึงขยาย subspace แทนที่จะทิ้ง shots เหล่านั้นไปโดยตรง
เนื่องจากไนโตรเจนสามารถมีได้เพียง 7 และ 7 electrons เท่านั้น bitstrings ใดๆ ที่มีเลข 1 มากกว่าหรือน้อยกว่าเจ็ดตัวในครึ่งแรกและครึ่งหลังของ output สามารถถูกทิ้งได้ เรานิยามฟังก์ชันที่ตรวจสอบว่า bitstrings นั้นถูกต้องหรือไม่ และหากไม่ถูกต้องก็จะทิ้งมันไป เมื่อเรากรอง bitstrings ที่ผิดปกติออกแล้ว ที่เหลือจะถูกส่งเข้าสู่ diagonalization scheme ใช้แฟล็ก PRUNE ด้านล่างเพื่อสลับระหว่างพฤติกรรมทั้งสองแบบ
โปรดจำไว้ว่าการตัดทอนเป็นเพียงหนึ่งในหลายทางเลือกที่กำหนดรูปร่างของ subspace สุดท้าย เคียงคู่ไปกับจำนวน circuit ชุดของ evolution time และการกรองเทอมแนวทแยง การเปรียบเทียบการรันแบบตัดทอนกับแบบไม่ตัดทอนจะมีประโยชน์ก็ต่อเมื่อทุกอย่างอื่นถูกตรึงไว้คงที่เท่านั้น แท็บ C++ อภิปรายเรื่องนี้ในรายละเอียดมากขึ้น เนื่องจากมันทำ postselect แทนที่จะ recover และยังแตกต่างกันในพารามิเตอร์อื่นๆ เหล่านั้นด้วย
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
ซอร์สโค้ด C++ โปรเจกต์ CMake และคำแนะนำการสร้างอยู่ในไดเรกทอรี docs/tutorials/assets/sqdrift/cpp ของที่เก็บเอกสาร Qiskit อินพุต FCIDump อยู่ในไดเรกทอรีข้างเคียง fcidump_files โคลนที่เก็บ หรือดาวน์โหลดทั้งสองไดเรกทอรี เพื่อสร้างและรันบทช่วยสอนนี้ด้วยตัวเอง
ประมาณเวลาที่ใช้: 180 วินาทีบนโปรเซสเซอร์ Heron r3 (หมายเหตุ: นี่เป็นเพียงค่าประมาณเท่านั้น เวลารันจริงของคุณอาจแตกต่างออกไป)
ผลลัพธ์การเรียนรู้ (C++)
-
เรียนรู้วิธีสร้างวงจรที่มีความลึกน้อยกว่าเมื่อเทียบกับ Trotterization
-
ทำความเข้าใจ workflow แบบ end-to-end สำหรับการประมาณสถานะพื้นโดยใช้ qDRIFT และ SQD
-
เรียนรู้วิธีใช้
qiskit-fermionsร่วมกับ Qiskit addon อื่นๆ เพื่อนำ workflow ดังกล่าวไปใช้งาน
ข้อกำหนดเบื้องต้น (C++)
-
อ่านภาพรวม Sample-based quantum diagonalization (SQD)
-
อ่านบทเรียน Sample-based Krylov Quantum Diagonalization (SKQD)
ความเป็นมา (C++)
SqDRIFT เป็นรูปแบบหนึ่งของ SKQD ที่แทนที่ความจำเป็นในการเลือก ansatz ที่ใช้สุ่มตัวอย่างบิตสตริงด้วยกลุ่ม (ensemble) ของวงจรวิวัฒนาการตามเวลาที่สร้างขึ้นโดยตรงจากแฮมิลโทเนียนเป้าหมาย สิ่งนี้ทำได้โดยการสุ่มตัวอย่างย่อยตัวดำเนินการวิวัฒนาการตามเวลาที่เล็กกว่าจากแฮมิลโทเนียนตามค่าสัมประสิทธิ์ของมัน ซึ่งเรียกว่าวิธี qDRIFT Trotterization
ทั้งสองการทำงานใช้ Qiskit Fermions เพื่อแสดง Hamiltonian ของโมเลกุลและสร้าง circuit สำหรับอัลกอริทึม qDRIFT การทำงานแบบ Python ใช้ transpiler pass แบบเฟอร์มิออน ส่วนการทำงานแบบ C++ สุ่มตัวอย่างกลุ่มอย่างชัดเจนและแมปไปยังตัวดำเนินการ qubit
ให้แฮมิลโทเนียนมีรูปแบบดังนี้
โดยที่ โดยไม่เสียนัยทั่วไป เรากำหนดให้ และค่าลักษณะเฉพาะ (eigenvalue) ที่ใหญ่ที่สุดของ มีค่าสัมบูรณ์เท่ากับ ตัวประกอบที่มีเครื่องหมายหรือเป็นจำนวนเชิงซ้อนใดๆ จะถูกดูดซับเข้าไปใน ดังนั้นสัมประสิทธิ์ จึงเป็นน้ำหนักบวกล้วนๆ ในขณะที่ ถือทิศทางของแต่ละพจน์ ในที่นี้ คือจำนวนพจน์ (หรือหลังจากการจัดกลุ่มแล้ว คือจำนวนกลุ่ม) ในแฮมิลโทเนียน มันเป็นคุณสมบัติของแฮมิลโทเนียนและแตกต่างจากจำนวนตัวดำเนินการที่ถูกสุ่มเข้าไปในวงจรเดียว ซึ่งเขียนแทนด้วย ด้านล่าง
จากนั้นอัลกอริทึม qDRIFT จะสร้างตัวดำเนินการ สำหรับเวลาเป้าหมาย โดยที่ มีค่าตั้งแต่ และหมายถึงวงจร SqDRIFT ลำดับที่ ซึ่งนิยามเป็น
ในที่นี้ คือจำนวนตัวดำเนินการที่สุ่มมาต่อวงจร และ คือจำนวนวงจรใน ensemble ผลคูณดำเนินไปตามการสุ่ม ครั้ง ไม่ใช่ตามพจน์ ทั้งหมดของแฮมิลโทเนียน และเนื่องจากพจน์เหล่านี้ถูกสุ่มแบบมีการแทนที่ (with replacement) ตัวเดียวกันอาจปรากฏมากกว่าหนึ่งครั้งใน เดียว
ปริมาณ
คือ norm แบบ ของสัมประสิทธิ์ ดังนั้นแต่ละขั้นตอนใน ขั้นตอนจะวิวัฒนาการเป็นระยะเวลาเท่ากันคือ ไม่ว่าจะสุ่มพจน์ใดมาก็ตาม ความสม่ำเสมอของมุมในแต่ละขั้นตอนคือลักษณะเฉพาะของ qDRIFT สัมประสิทธิ์มีอิทธิพลต่อผลลัพธ์ผ่านความถี่ที่พจน์ของมันถูกสุ่มมา ไม่ใช่ผ่านระยะที่พจน์นั้นถูกหมุน ดัชนีถูกสุ่มมาจากการแจกแจง
ดังนั้นลำดับ จึงเป็นลำดับสุ่มของดัชนีพจน์ที่สุ่มมาจากการแจกแจงนี้ เนื่องจาก เป็นบวกและรวมกันได้ นี่จึงเป็นการแจกแจงความน่าจะเป็นแบบปรับให้เป็นบรรทัดฐาน (normalized) และค่าคาดหมายของช่องสัญญาณ (channel) ที่ได้จากการสุ่มเหล่านี้ประมาณค่าวิวัฒนาการภายใต้ โดยมีข้อผิดพลาดที่ลดลงเมื่อ เพิ่มขึ้น โปรดสังเกตว่าข้อผิดพลาดในการประมาณขึ้นอยู่กับ ไม่ใช่จำนวนพจน์
(บทความ SqDRIFT เขียนจำนวนพจน์เป็น และความยาวของลำดับเป็น ในที่นี้เราใช้ และ เพื่อให้ทั้งสองแยกความแตกต่างกันได้ชัดเจน)
บทช่วยสอนนี้แสดงวิธีสร้างกลุ่มของวงจรแบบสุ่มเหล่านี้ หลังจากที่เราสร้างวงจรเหล่านี้แล้ว คล้ายกับวิธีที่เราสร้างซับสเปซ Krylov สำหรับตัวดำเนินการที่แตกต่างกัน เราสุ่มตัวอย่างบิตสตริงจากตัวดำเนินการหลายตัวเช่นนี้ด้วยพารามิเตอร์เวลาที่แตกต่างกัน สิ่งนี้ทำให้มั่นใจได้ว่ามีความซ้อนทับ (overlap) ที่สูงขึ้นระหว่างเวกเตอร์สถานะพื้นและบิตสตริงที่สุ่มมา
ข้อกำหนด (C++)
การทำงานนี้รันบนฮาร์ดแวร์ IBM Quantum® และใช้ไฟล์ปฏิบัติการ SBD ภายนอกสำหรับ diagonalization แบบคลาสสิก ไม่รวมตัวอย่างตัวจำลอง Python
ทำตามคำแนะนำการสร้าง C++ เพื่อติดตั้งคอมไพเลอร์ CMake Rust Boost และ dependency ของ Qiskit ติดตั้ง Rust ด้วย rustup: dependency ของ Qiskit ตรึงเวอร์ชัน Rust ของตัวเอง ซึ่ง rustup จะดาวน์โหลดให้อัตโนมัติ แต่ Rust ที่ติดตั้งจาก Homebrew หรือตัวจัดการแพ็กเกจของ Linux อาจเก่าเกินไป ขั้นตอน SBD สุดท้ายยังต้องใช้ MPI, OpenMP และ BLAS/LAPACK เพิ่มเติม ขณะนี้การทำงานนี้ยังไม่รองรับ Windows
ใช้ซอร์สโค้ด C++และโปรเจกต์ CMakeฉบับสมบูรณ์จากที่เก็บ เก็บไดเรกทอรี fcidump_files ที่อยู่ติดกันไว้เมื่อดาวน์โหลดตัวอย่าง จาก checkout ของที่เก็บเอกสาร ให้สร้างด้วยคำสั่งต่อไปนี้ การสร้างครั้งแรกจะดาวน์โหลดและคอมไพล์ dependency ทั้งหมดและใช้เวลาหลายนาที
cd docs/tutorials/assets/sqdrift/cpp
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
การสร้างจะเขียนไฟล์ปฏิบัติการ SqDRIFT ไปที่ docs/tutorials/assets/sqdrift/cpp (รากของโปรเจกต์) ไม่ใช่ที่ build/ รัน ./SqDRIFT จากรากของโปรเจกต์เพื่อให้หา ../fcidump_files/N2_sto_3g พบ คำสั่งอื่นทั้งหมดในแท็บนี้ก็รันจากรากของโปรเจกต์เช่นกัน เว้นแต่จะระบุไว้เป็นอย่างอื่น หาก ./SqDRIFT ล้มเหลวด้วยข้อผิดพลาดไลบรารีที่ใช้ร่วมกันหายไป ให้ทำตามคำแนะนำเรื่อง library-path เฉพาะแพลตฟอร์มในคู่มือการสร้าง บน macOS ไฟล์ปฏิบัติการมักรันได้โดยไม่ต้องทำ
การตั้งค่า (C++)
ส่วนโค้ดด้านล่างเป็นข้อความตัดตอนจากโปรแกรม SqDRIFT.cppฉบับสมบูรณ์ ให้สร้างและรันไฟล์นั้นเป็นโปรแกรมเดียว
ก่อนรัน SqDRIFT ตรวจสอบให้แน่ใจว่าได้กำหนดค่า IBM Quantum API token ของคุณสำหรับ IBM Cloud Platform แล้ว
หากคุณมี Python พร้อมติดตั้ง qiskit-ibm-runtime วิธีที่ง่ายที่สุดในการสร้างไฟล์กำหนดค่าที่มีรูปแบบถูกต้องคือ:
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(channel="ibm_quantum_platform", token="YOUR_IBM_QUANTUM_TOKEN_HERE")
คำสั่งนี้เขียน ~/.qiskit/qiskit-ibm.json (Linux/macOS) หรือ %USERPROFILE%\.qiskit\qiskit-ibm.json (Windows) ด้วยรูปแบบที่ถูกต้อง หากคุณต้องการสร้างไฟล์ด้วยตัวเอง:
{
"default-ibm-quantum-platform": {
"channel": "ibm_quantum_platform",
"token": "YOUR_IBM_QUANTUM_TOKEN_HERE"
}
}
รับ API key ของคุณจาก IBM Quantum Platform
หมายเหตุ: ไฟล์กำหนดค่าที่หายไปหรือรูปแบบไม่ถูกต้องจะทำให้เกิด Rust unwrap() panic ที่ไม่ช่วยอะไรตอนเริ่มต้น แทนที่จะเป็นข้อผิดพลาดที่ชัดเจน หากพบปัญหา ให้ตรวจสอบชื่อคีย์ (default-ibm-quantum-platform) และค่า channel (ibm_quantum_platform) อีกครั้ง
การนำไปใช้งาน (C++)
ขั้นตอนที่ 1: แมปอินพุตแบบคลาสสิกเป็นปัญหาควอนตัม (C++)
โหลด 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)
โหลด Hamiltonian ของโมเลกุล จากไฟล์ FCIDump จำนวน qubit เท่ากับสองเท่าของจำนวนออร์บิทัล (สปินขึ้นและสปินลง)
สร้าง Hamiltonian และทำ normal-order
// Create and normal order Hamiltonian
QfFermionOperator* hamiltonian = qf_ferm_op_from_fcidump(fcidump);
QfFermionOperator* normal_ordered = qf_ferm_op_normal_ordered(hamiltonian, nullptr);
แปลงข้อมูล FCIDump เป็นตัวดำเนินการเฟอร์มิออนและใช้ normal ordering เพื่อทำให้โครงสร้างของตัวดำเนินการง่ายขึ้น
จัดกลุ่มเทอมตามโครงสร้างอิเล็กทรอนิกส์
// 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
จัดกลุ่มเทอมของ Hamiltonian ที่เกี่ยวข้องกันตามโครงสร้างอิเล็กทรอนิกส์ ก่อนสร้าง circuit วิวัฒนาการของเทอมเหล่านั้น
คำนวณน้ำหนักการสุ่มตัวอย่าง แล้ว normalize และแมปไปยังตัวดำเนินการ qubit
ลำดับมีความสำคัญตรงนี้ การ normalize ปรับสัมประสิทธิ์ทุกตัวให้มีขนาดเป็นหนึ่ง ในที่เดิม ดังนั้นต้องคำนวณน้ำหนักการสุ่มตัวอย่างก่อน หากทำ normalize ก่อน ทุก จะอ่านกลับมาได้เป็น 1.0 พอดี และน้ำหนักของแต่ละกลุ่มจะยุบเหลือเพียงจำนวนเทอมของมัน — การแจกแจงการสุ่มตัวอย่างจะถูกขับเคลื่อนด้วยขนาดของกลุ่มแทนที่จะเป็นสัมประสิทธิ์ทางฟิสิกส์
น้ำหนักของแต่ละกลุ่มคือสัมประสิทธิ์สัมบูรณ์ เฉลี่ย ของเทอมในกลุ่มนั้น pass QDriftTrotterization ของแท็บ Python ใช้น้ำหนักเดียวกันสำหรับ Hamiltonian ที่จัดกลุ่มแล้ว หากใช้ผลรวมแทน จะขยายตามขนาดของกลุ่ม ซึ่งส่งผลให้เวลาวิวัฒนาการ เบี่ยงเบนไป
// 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
น้ำหนัก คือสัมประสิทธิ์สัมบูรณ์เฉลี่ยของเทอมในกลุ่ม อ่านจากสัมประสิทธิ์ดั้งเดิม จากนั้นการ normalize จะปรับแต่ละเทอมเฟอร์มิออนให้มีขนาดเป็นหนึ่งก่อนการแปลง Jordan-Wigner โดยคงไว้เพียงเฟส/เครื่องหมายของมันในตัวดำเนินการที่ evolve แล้ว ซึ่งตรงกับการวิวัฒนาการแบบเทอมที่ normalize แล้วของ qDRIFT: ขนาดของสัมประสิทธิ์กำหนดความน่าจะเป็นในการสุ่มตัวอย่างผ่าน ขณะที่วิวัฒนาการของ circuit ใช้เทอมที่ normalize แล้ว สัมประสิทธิ์ขนาดใหญ่จึงไม่ถูกนับซ้ำสองครั้ง
สุ่มตัวอย่างชุดตัวดำเนินการ
// 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...
สุ่มชุดตัวดำเนินการสำหรับ circuit สุ่ม 100 ชุด แต่ละชุดมี 10 กลุ่มที่สุ่มจากการแจกแจงแบบถ่วงน้ำหนัก ขั้นตอนถัดไปจะเปลี่ยนชุดเหล่านี้ให้เป็น ensemble ของ circuit qDRIFT
ขั้นตอนที่ 2: สร้าง circuit qDRIFT (C++)
ในเวอร์ชัน C++ ขั้นตอนนี้สร้าง circuit จากชุดตัวดำเนินการที่สุ่มมา การทรานสไพล์สำหรับ backend เป้าหมายเกิดขึ้นใน Step 3 ทันทีก่อนส่งแต่ละ 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
จากนั้นโปรแกรมฉบับสมบูรณ์จะพิมพ์สถิติของ circuit และดัชนีตัวดำเนินการของ circuit 0 ซึ่งละไว้ในที่นี้
สำหรับตัวดำเนินการที่สุ่มมาแต่ละตัว จะสร้าง circuit วิวัฒนาการแบบ Suzuki-Trotter ด้วยเวลา โดย คือจำนวนตัวดำเนินการที่สุ่มต่อ circuit และ คือ time step เนื่องจากแต่ละกลุ่มที่สุ่มมาถูก normalize ทีละเทอมก่อนแมป การวิวัฒนาการนี้จึงใช้เฉพาะเฟส/เครื่องหมายของแต่ละเทอมระหว่างการหมุน ส่วนขนาดมีผลผ่านการแจกแจงการสุ่มตัวอย่างเท่านั้น สถานะเริ่มต้น Hartree-Fock เตรียมโดยใช้ X gate กับ qubit 0-6 และ 10-16 (อิเล็กตรอนเจ็ดตัวในแต่ละเซกเตอร์สปิน) คำสั่งของ circuit วิวัฒนาการแต่ละตัวถูกต่อท้าย circuit หลักด้วยตนเองโดยใช้ C++ API
ขั้นตอนที่ 3: รันโดยใช้ 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;
เชื่อมต่อกับ IBM Quantum เลือก backend ตามชื่อ ทรานสไพล์แต่ละ circuit สำหรับ backend นั้น และส่ง หนึ่งงานต่อหนึ่ง circuit: งาน Sampler แยกกัน 100 งาน งานละ 100 shot บน register 20 qubit จากนั้นสำรวจแต่ละงานจนเสร็จและรวบรวม bitstring จาก result[0] ทุกงานใช้เวลา QPU และรอคิวแยกกัน ดังนั้นคาดว่าขั้นตอนนี้จะใช้เวลานานกว่างานแบบ batch เดียวมาก
ผลลัพธ์จากฮาร์ดแวร์ที่อ้างถึงในหน้านี้มาจากการรันบน ibm_pittsburgh (100 circuit × 100 shot) โดยเปลี่ยน backend_name จากค่าเริ่มต้น ibm_fez ตัวอย่างที่บันทึกไว้ถูกประมวลผลภายหลังด้วยลำดับบิตของ determinant ที่แก้ไขแล้วซึ่งแสดงใน Step 4 สัญญาณรบกวนของฮาร์ดแวร์แตกต่างกันในแต่ละการรัน ดังนั้นจำนวนนับและพลังงานสุดท้ายของคุณจะต่างออกไป
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
การเปลี่ยน backend: เปลี่ยน backend_name ใน SqDRIFT.cpp เพื่อใช้อุปกรณ์อื่น service.backends() จะแสดงรายการที่บัญชีของคุณเข้าถึงได้
ขั้นตอนที่ 4: ประมวลผลภายหลังและส่งคืนผลลัพธ์ในรูปแบบคลาสสิกที่ต้องการ (C++)
Postselect bitstring และเขียนสตริง CI
// Postselect bitstrings by Hamming weight (derived from FCIDump NELEC)
std::vector<double> bitstring_weights(all_bitstrings.size(), 1.0);
auto [filtered_bitstrings, filtered_weights] = Qiskit::addon::sqd::postselect_bitstrings(
all_bitstrings,
bitstring_weights,
Qiskit::addon::sqd::MatchesRightLeftHamming<uint32_t>(n_alpha, n_beta)
);
std::ignore = filtered_weights;
std::cout << "✓ Postselected " << filtered_bitstrings.size()
<< " bitstrings with Hamming weight ("
<< n_alpha << "," << n_beta << ")" << std::endl;
// Convert bitstrings to CI strings using SQD addon
auto ci_strings = Qiskit::addon::sqd::bitstrings_to_ci_strings_symmetrize_spin(
filtered_bitstrings,
std::nullopt // No dimension limit
);
std::cout << "✓ Generated " << ci_strings.size() << " CI strings" << std::endl;
// Write CI strings to file for SBD
std::ofstream alpha_file("alphadets_from_sqd.txt");
for (const auto& ci_string : ci_strings) {
std::string bitstr;
// SBD expects the highest orbital index first (bit zero on the right).
boost::to_string(ci_string, bitstr);
alpha_file << bitstr << "\n";
}
alpha_file.close();
std::cout << "✓ Wrote " << ci_strings.size()
<< " CI strings to alphadets_from_sqd.txt" << std::endl;
std::cout << " Ready for SBD diagonalization!" << std::endl;
Output:
✓ Postselected 1137 bitstrings with Hamming weight (7,7)
✓ Generated 49 CI strings
✓ Wrote 49 CI strings to alphadets_from_sqd.txt
Ready for SBD diagonalization!
Postselect bitstring ที่มี Hamming weight ถูกต้อง (n_alpha อิเล็กตรอนสปินขึ้น, n_beta อิเล็กตรอนสปินลง ได้มาจากฟิลด์ NELEC ของ FCIDump) แปลงเป็นสตริง Configuration Interaction (CI) โดยใช้การทำสมมาตรสปิน และเขียนลงใน alphadets_from_sqd.txt สำหรับ Selected Basis Diagonalization ในลำดับถัดไป
คืนทรัพยากร
// 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;
คืนหน่วยความจำและทรัพยากรที่จัดสรรไว้ทั้งหมดอย่างถูกต้อง std::vector<QuantumCircuit> ของ C++ ถูกล้างอัตโนมัติผ่าน RAII ส่วนตัวดำเนินการ qubit ตัวดำเนินการเฟอร์มิออน และข้อมูล FCIDump ต้องคืนด้วยตนเอง
เมื่อเราสร้าง basis สำหรับการโปรเจกต์ Hamiltonian แล้ว เราก็ดำเนินการ diagonalization เพื่อให้ได้ค่าประมาณสถานะพื้นได้ รันคำสั่งต่อไปนี้จากรากของโปรเจกต์ docs/tutorials/assets/sqdrift/cpp
สร้างไบนารี SBD diag
CMake ดึง SBD มาเป็นซอร์สเท่านั้นไว้ใน deps/sbd/ (การสร้างหลักตั้งใจข้ามการคอมไพล์ส่วนนี้) แอป SBD ใช้ Makefile ธรรมดา และ SBD เองต้องใช้ MPI, OpenMP และ BLAS/LAPACK — ดังนั้นการรัน make เฉยๆ จึงไม่เพียงพอจนกว่าจะติดตั้งสิ่งเหล่านั้นและไฟล์ Configuration ของแอปตรงกับ toolchain ของคุณ
Makefile อ่านแฟลกของคอมไพเลอร์และลิงก์จากไฟล์ Configuration ในไดเรกทอรีเดียวกัน เวอร์ชันที่ส่งมากับต้นทางถูกปรับแต่งให้เข้ากับโครงสร้าง macOS/Homebrew แบบเฉพาะเจาะจงและ จะสร้างไม่ผ่านหากใช้ตามเดิม บนเครื่องปกติ โดยเฉพาะ -I/opt/homebrew/opt/llvm/include ไม่มี omp.h (อยู่ในไดเรกทอรี resource ของ clang เฉพาะเวอร์ชัน) และ clang++ ของ Apple ปฏิเสธ -fopenmp โดยสิ้นเชิง:
clang++: error: unsupported option '-fopenmp'
สูตรต่อไปนี้ ยืนยันแล้วว่าใช้งานได้บน macOS (Apple Silicon, Darwin 25.6, Open MPI 5.x + Homebrew LLVM)
macOS (Apple Silicon) — ทดสอบแล้ว
# 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
จากนั้นแทนที่ deps/sbd/apps/chemistry_tpb_selected_basis_diagonalization/Configuration ด้วย:
# 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 ../../../..
การสร้างที่สำเร็จจะพิมพ์บรรทัดคอมไพล์/ลิงก์สองบรรทัดและทิ้งไบนารี diag ไว้ข้าง Makefile คำเตือน ld: warning: ignoring duplicate libraries: '-lomp' หนึ่งรายการนั้นไม่เป็นอันตราย
Intel macOS: prefix ของ Homebrew คือ /usr/local แทนที่จะเป็น /opt/homebrew ให้แทนที่ในทั้งสองพาธก่อนหน้า
Linux (Ubuntu/Debian) — ยังไม่ได้ทดสอบ
ข้อกำหนดเบื้องต้นต่อไปนี้ครอบคลุม MPI, OpenMP และ BLAS/LAPACK GCC รองรับ -fopenmp โดยตรง จึงไม่ต้องแทนที่คอมไพเลอร์ ส่วนนี้ ยังไม่ได้ ยืนยันบน 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
รัน SqDRIFT เพื่อสร้างสตริง CI
หากคุณรัน ./SqDRIFT ผ่าน Step 1–4 แล้ว alphadets_from_sqd.txt จะมีอยู่ในรากของโปรเจกต์แล้วและคุณข้ามขั้นตอนนี้ได้ หากไม่เช่นนั้น ให้รันจากรากของโปรเจกต์:
./SqDRIFT
# Writes alphadets_from_sqd.txt in the project root
รัน 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 ../../../..
พารามิเตอร์:
--fcidump: พาธไปยังไฟล์ FCIDump ที่มี Hamiltonian--adetfile: พาธไปยังไฟล์ alpha determinant (สตริง CI จาก SqDRIFT)--method 0: วิธี diagonalization แบบ Davidson--iteration 100: จำนวนการวนซ้ำ Davidson สูงสุด--block 10: ขนาดบล็อกสำหรับอัลกอริทึม Davidson--tolerance 1e-8: ค่าความคลาดเคลื่อนการลู่เข้าของพลังงาน
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
บรรทัด Energy = -107.5108722590187 คือค่าประมาณสถานะพื้นเหนือ subspace ที่สุ่มตัวอย่างได้
สิ่งนี้แตกต่างจากตัวอย่าง Python อย่างไร
ตัวอย่างทั้งสองต่างกันในหลายแง่พร้อมกัน ไม่ใช่แค่วิธีจัดการ bitstring ที่สมมาตรไม่ถูกต้อง จึงไม่คาดว่าจะให้พลังงานเหมือนกัน:
| แท็บ C++ | แท็บ Python | |
|---|---|---|
| Bitstring ที่สมมาตรไม่ถูกต้อง | ถูกทิ้ง โดย postselect_bitstrings ตาม Hamming weight | ถูกซ่อมแซม ด้วยการกู้คืน configuration แบบวนซ้ำ |
| โครงสร้างลูป | รอบเดียว: สุ่มตัวอย่าง → postselect → diagonalize ครั้งเดียว | ลูปนอก: diagonalize อ่านการครอบครองออร์บิทัล กู้คืน configuration แล้ว diagonalize ใหม่ |
| จำนวน circuit | 100 circuit (num_circuits = 100) | 400 circuit (200 ต่อ evolution time) |
| Evolution time | เดียว (time_step = 1) | สองค่า times = [1.0, 10.0] |
| เทอมแนวทแยง | เก็บไว้ในตัวดำเนินการที่สุ่มมา | ถูกลบด้วย filter_diagonal_terms ทำให้การแจกแจงการสุ่มตัวอย่างและ circuit เปลี่ยนไป |
| ลำดับของเทอม | จัดกลุ่มเท่านั้น | ใช้ canonical_order ก่อนจัดกลุ่มเพื่อให้ทำซ้ำได้ |
| ตัวแก้ diagonalization | SBD (diag ไบนารี MPI/OpenMP ภายนอก) | ตัวแก้ในกระบวนการของ qiskit-addon-sqd |
| Backend | กำหนดตามชื่อ (backend_name = "ibm_fez"; การรันในหน้านี้ใช้ ibm_pittsburgh) | backend least_busy ที่ใช้งานได้และมี qubit เพียงพอ |
| การทรานสไพล์ | แต่ละ circuit แยกกัน ระดับการเพิ่มประสิทธิภาพ 2 | ทุก circuit พร้อมกัน ระดับการเพิ่มประสิทธิภาพ 3 |
| การส่งงาน | 100 งาน งานละหนึ่ง circuit | หนึ่งงาน Sampler ที่มีทั้ง 400 circuit |
| การรันตัวจำลอง | ไม่มี ใช้ฮาร์ดแวร์เท่านั้น | มีตัวอย่างตัวจำลองไร้สัญญาณรบกวนก่อนการรันบนฮาร์ดแวร์ |
ทั้งสองเวอร์ชันใช้ Hamiltonian เดียวกัน จัดกลุ่มเทอมตามโครงสร้างอิเล็กทรอนิกส์ ถ่วงน้ำหนักแต่ละกลุ่มด้วยสัมประสิทธิ์สัมบูรณ์เฉลี่ยของเทอมในกลุ่ม เริ่มจากสถานะ Hartree-Fock และสุ่มตัวอย่าง 10 กลุ่มและ 100 shot ต่อ circuit
แต่ละความแตกต่างส่งผลต่อ subspace ที่สุ่มตัวอย่างได้ และจึงส่งผลต่อพลังงาน โดยอิสระต่อกัน ensemble ที่ใหญ่กว่าและ evolution time ที่สองซึ่งยาวกว่า ทำให้ configuration ที่ตัวอย่าง Python สำรวจกว้างขึ้นทั้งคู่ การสุ่มตัวอย่างที่มากกว่าหนึ่งเวลาทำให้ SqDRIFT ครอบคลุมปริภูมิพลังงานต่ำแบบ Krylov การกรองเทอมแนวทแยงเปลี่ยนตัวดำเนินการที่ sampler สุ่มได้และน้ำหนักของมัน การรันทั้งสองจึงสุ่มจากการแจกแจงที่ต่างกัน
Postselection เป็นตัวกรองที่เข้มงวด และ shot ที่มีจำนวนอนุภาคผิดจะถูกทิ้งทันที ในการรันข้างต้นนั้นทิ้งข้อมูลส่วนใหญ่ไป มีเพียง 1137 จาก 10000 shot ที่ผ่านการตรวจสอบ Hamming weight (7,7) (ประมาณ 11%) ซึ่งยุบเหลือ -determinant ที่ไม่ซ้ำกันเพียง 49 ตัว ส่วนการกู้คืน configuration ใช้การครอบครองออร์บิทัลเฉลี่ยจาก diagonalization ครั้งก่อนเพื่อพลิกบิตและ ซ่อมแซม shot เหล่านั้น นำสิ่งที่ postselection จะทิ้งกลับมาใช้ใหม่ ฟีดแบ็กนั้นทำให้มันเป็นกระบวนการวนซ้ำโดยธรรมชาติ: recover_configurations ใน qiskit-addon-sqd-hpc ต้องการอาร์กิวเมนต์ avg_occupancies ซึ่งมีอยู่ได้ก็ต่อเมื่อรัน diagonalization แล้วเท่านั้น
การรัน C++ นี้ให้ subspace ของ -determinant 49 ตัวและพลังงาน -107.5109 Ha ปริภูมิ -determinant ทั้งหมดสำหรับอิเล็กตรอนเจ็ดตัวใน 10 ออร์บิทัลมีเพียง สตริง ดังนั้นการป้อนทั้ง 120 สตริงให้ไบนารี diag ตัวเดียวกันจึงได้พลังงาน FCI ที่แน่นอนเท่ากับ -107.6482 Ha การรันนี้จึงอยู่สูงกว่าค่าที่แน่นอนประมาณ 0.1373 Ha ขณะครอบคลุม -determinant 49 จาก 120 ตัว เนื่องจาก SQD เป็นแบบแปรผันใน subspace ที่เลือก subspace ที่เล็กกว่าจึงทำได้เพียงเพิ่มพลังงาน ดังนั้น subspace ที่ไม่สมบูรณ์จึงเป็นคำอธิบายที่ตรงที่สุดของช่องว่างนี้
การตรวจสอบ FCI ที่แน่นอนนี้ทำได้เพราะตัวอย่างมีขนาดเล็กเท่านั้น ไม่ได้เป็นส่วนหนึ่งของเวิร์กโฟลว์ SqDRIFT และสำหรับขนาดระบบที่ต้องใช้ SQD จริงๆ ก็ไม่มีค่าอ้างอิงเช่นนั้น ในที่นี้ใช้เพียงเพื่อวัดว่าการรันนี้กู้คืนปริภูมิได้มากเพียงใด
การเปรียบเทียบกับตัวอย่าง Python ไม่ได้ควบคุมตัวแปร ดังนั้นไม่ควรอ้างว่าความแตกต่างเกิดจากการไม่มีการกู้คืน configuration เพียงอย่างเดียว สังเกตว่าจำนวน circuit evolution time เดียว การกรองเทอมแนวทแยง และสัญญาณรบกวนของฮาร์ดแวร์ต่างกันพร้อมกันทั้งหมดและล้วนกำหนดรูปร่างของ subspace
เพื่อลดช่องว่างนี้ ต่อไปนี้เป็นขั้นตอนถัดไปที่อาจลองหลังจากทำบทช่วยสอนนี้เสร็จ: เพิ่ม shots, num_circuits หรือ ops_per_circuit สุ่มตัวอย่างที่มากกว่าหนึ่ง time_step และเพิ่มลูปการกู้คืนไว้บน Qiskit::addon::sqd::recover_configurations
ขั้นตอนถัดไป
-
Sample-based Krylov quantum diagonalization ของแบบจำลองแลตทิซเฟอร์มิออน — บทช่วยสอนที่เกี่ยวข้องซึ่งใช้ circuit วิวัฒนาการตามเวลาแทน ansatz เชิงแปรผัน
-
Sample-based quantum diagonalization ของ Hamiltonian ทางเคมี — บทช่วยสอนเกี่ยวกับวิธีสร้าง circuit local unitary cluster Jastrow (LUCJ) สำหรับการจำลองเคมีควอนตัม
-
งานวิจัย SqDRIFT — เอกสารที่บทช่วยสอนนี้อ้างอิง (โปรดทราบว่าการเพิ่มประสิทธิภาพบางอย่างที่กล่าวถึงในงานวิจัยนี้อยู่ระหว่างการพัฒนา และบทช่วยสอนนี้อาจเปลี่ยนแปลงในอนาคตตามวิวัฒนาการของไลบรารีที่ใช้)