ข้ามไปยังเนื้อหาหลัก

อัลกอริทึม SqDRIFT สำหรับการประมาณสถานะพื้น

ประมาณเวลาที่ใช้: 180 วินาทีบนโปรเซสเซอร์ Heron r3 (หมายเหตุ: นี่เป็นเพียงค่าประมาณเท่านั้น เวลารันจริงของคุณอาจแตกต่างออกไป)

กำลังมองหาเวอร์ชัน C++ อยู่หรือไม่?

บทช่วยสอนนี้ใช้ Python สำหรับการนำไปใช้งานด้วย C++ รวมถึงซอร์สโค้ดและคำแนะนำการ build ดูที่ บทช่วยสอน SqDRIFT เวอร์ชัน C++

ผลลัพธ์การเรียนรู้​

  • เรียนรู้วิธีสร้างวงจรที่มีความลึกน้อยกว่าเมื่อเทียบกับ Trotterization

  • ทำความเข้าใจ workflow แบบ end-to-end สำหรับการประมาณสถานะพื้นโดยใช้ qDRIFT และ SQD

  • เรียนรู้วิธีใช้ qiskit-fermions ร่วมกับ Qiskit addon อื่นๆ เพื่อนำ workflow ดังกล่าวไปใช้งาน

บทช่วยสอนนี้นำเสนอในรูปแบบโน้ตบุ๊ก Python เพื่อจุดประสงค์ในการสอน

ข้อกำหนดเบื้องต้น​

พื้นฐาน​

SqDRIFT เป็นตัวแปรหนึ่งของ SKQD ที่แทนที่ความจำเป็นในการเลือก ansatz ที่ใช้สุ่มตัวอย่างบิตสตริงด้วยกลุ่ม (ensemble) ของวงจรวิวัฒนาการตามเวลาที่สร้างขึ้นโดยตรงจากแฮมิลโทเนียนเป้าหมาย สิ่งนี้ทำได้โดยการสุ่มตัวอย่างย่อยตัวดำเนินการวิวัฒนาการตามเวลาที่เล็กกว่าจากแฮมิลโทเนียนตามค่าสัมประสิทธิ์ของมัน ซึ่งเรียกว่าวิธี qDRIFT Trotterization

บทช่วยสอนนี้ใช้ Qiskit Fermions เพื่อสร้างวงจรแบบเฟอร์มิออนที่เป็นธรรมชาติมากขึ้นสำหรับอัลกอริทึม qDRIFT ตามด้วยการใช้ layout แบบเฟอร์มิออนและ synthesis pass ก่อนที่จะนำวงจรเข้าสู่ pipeline ของ Qiskit แบบดั้งเดิมสำหรับการรันบนฮาร์ดแวร์

ให้แฮมิลโทเนียนมีรูปแบบดังนี้

H=∑i=1NcihiH = \sum_{i=1}^{N} c_i h_i

โดยที่ เพื่อความเป็นทั่วไป เรากำหนดให้ ci>0c_i > 0 และไอเกนค่าที่ใหญ่ที่สุดของ hih_i มีค่าเท่ากัน ในค่าสัมบูรณ์ กับ 11 ตัวประกอบที่มีเครื่องหมายหรือเป็นจำนวนเชิงซ้อนใดๆ จะถูกดูดซับเข้าไปใน hih_i ดังนั้นสัมประสิทธิ์ cic_i จึงเป็นน้ำหนักบวกล้วนๆ ในขณะที่ hih_i ถือทิศทางของแต่ละพจน์ ในที่นี้ NN คือจำนวนพจน์ (หรือหลังจากการจัดกลุ่มแล้ว คือจำนวนกลุ่ม) ในแฮมิลโทเนียน มันเป็นคุณสมบัติของแฮมิลโทเนียนและแตกต่างจากจำนวนตัวดำเนินการที่ถูกสุ่มเข้าไปในวงจรเดียว ซึ่งเขียนแทนด้วย nn ด้านล่าง

จากนั้นอัลกอริทึม qDRIFT จะสร้างตัวดำเนินการ VkV_k สำหรับเวลาเป้าหมาย tt โดยที่ kk มีค่าตั้งแต่ 1⋯K1 \cdots K และหมายถึงวงจร SqDRIFT ลำดับที่ kk ซึ่งนิยามเป็น

Vk=∏j=1ne−ihkjλt/nV_k = \prod_{j=1}^{n} e^{-i h_{k_j} \lambda t / n }

ในที่นี้ nn คือจำนวนตัวดำเนินการที่สุ่มมาต่อวงจร และ KK คือจำนวนวงจรใน ensemble ผลคูณดำเนินไปตามการสุ่ม nn ครั้ง ไม่ใช่ตามพจน์ NN ทั้งหมดของแฮมิลโทเนียน และเนื่องจากพจน์เหล่านี้ถูกสุ่มแบบมีการแทนที่ (with replacement) hih_i ตัวเดียวกันอาจปรากฏมากกว่าหนึ่งครั้งใน VkV_k เดียว

ปริมาณ

λ=∑i=1Nci\lambda = \sum_{i=1}^{N} c_i

คือ norm แบบ L1L_1 ของสัมประสิทธิ์ ดังนั้นแต่ละขั้นตอนใน nn ขั้นตอนจะวิวัฒนาการเป็นระยะเวลาเท่ากันคือ λt/n\lambda t / n ไม่ว่าจะสุ่มพจน์ใดมาก็ตาม ความสม่ำเสมอของมุมในแต่ละขั้นตอนคือลักษณะเฉพาะของ qDRIFT สัมประสิทธิ์มีอิทธิพลต่อผลลัพธ์ผ่านความถี่ที่พจน์ของมันถูกสุ่มมา ไม่ใช่ผ่านระยะที่พจน์นั้นถูกหมุน ดัชนีถูกสุ่มมาจากการแจกแจง

P[ki]=ciλP[k_i] = \frac{c_i}{\lambda}

ดังนั้นลำดับ (k1,…,kn)(k_1, \ldots, k_n) จึงเป็นลำดับสุ่มของดัชนีพจน์ที่สุ่มมาจากการแจกแจงนี้ เนื่องจาก cic_i เป็นบวกและรวมกันได้ λ\lambda นี่จึงเป็นการแจกแจงความน่าจะเป็นแบบปรับให้เป็นบรรทัดฐาน (normalized) และค่าคาดหมายของช่องสัญญาณ (channel) ที่ได้จากการสุ่มเหล่านี้ประมาณค่าวิวัฒนาการภายใต้ HH โดยมีข้อผิดพลาดที่ลดลงเมื่อ nn เพิ่มขึ้น โปรดสังเกตว่าข้อผิดพลาดในการประมาณขึ้นอยู่กับ λ\lambda ไม่ใช่จำนวนพจน์ NN

(บทความ SqDRIFT เขียนจำนวนพจน์เป็น N\mathcal{N} และความยาวของลำดับเป็น NN ในที่นี้เราใช้ NN และ nn เพื่อให้ทั้งสองแยกความแตกต่างกันได้ชัดเจน)

บทช่วยสอนนี้แสดงวิธีสร้างกลุ่มของวงจรแบบสุ่มเหล่านี้ หลังจากที่เราสร้างวงจรเหล่านี้แล้ว คล้ายกับวิธีที่เราสร้างซับสเปซ Krylov สำหรับตัวดำเนินการที่แตกต่างกัน เราสุ่มตัวอย่างบิตสตริงจากตัวดำเนินการหลายตัวเช่นนี้ด้วยพารามิเตอร์เวลาที่แตกต่างกัน สิ่งนี้ทำให้มั่นใจได้ว่ามีความซ้อนทับ (overlap) ที่สูงขึ้นระหว่างเวกเตอร์สถานะพื้นและบิตสตริงที่สุ่มมา

ข้อกำหนด​

ก่อนเริ่มบทช่วยสอนนี้ ตรวจสอบให้แน่ใจว่าคุณได้ติดตั้ง

  • สภาพแวดล้อมเสมือน Python (>=3.10)
  • pip>=25.1
  • qiskit ~= 2.5
  • qiskit-fermions==0.1.0 (โปรดสังเกตว่าชื่อเป็นพหูพจน์)
  • numpy
  • pyscf
  • qiskit-aer
  • qiskit-ibm-runtime
  • qiskit-addon-sqd

คุณสามารถติดตั้งแพ็กเกจที่จำเป็นทั้งหมดได้ด้วยคำสั่ง

pip install "qiskit~=2.5" "qiskit-fermions==0.1.0" qiskit-aer qiskit-ibm-runtime qiskit-addon-sqd pyscf numpy

การตั้งค่า​

# Added by doQumentation — required packages for this notebook
!pip install -q numpy pyscf qiskit qiskit-addon-sqd qiskit-aer qiskit-fermions qiskit-ibm-runtime
# Third-party scientific computing
import numpy as np

# PySCF
from pyscf import tools, ao2mo, fci

# Qiskit core
from qiskit import transpile
from qiskit.primitives import BitArray

# Qiskit Aer
from qiskit_aer import AerSimulator

# IBM Quantum Compute Service
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler

# Qiskit Fermions
from qiskit_fermions.operators.library import FCIDump
from qiskit_fermions.operators import FermionOperator
from qiskit_fermions.operators.terms.filtering import filter_diagonal_terms
from qiskit_fermions.operators.terms.grouping import (
group_terms_by_electronic_structure,
)
from qiskit_fermions.operators.terms.ordering import canonical_order
from qiskit_fermions.circuit import FermionicCircuit
from qiskit_fermions.circuit.library import Evolution
from qiskit_fermions.transpiler import FermionicPassManager
from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
from qiskit_fermions.transpiler.passes import QDriftTrotterization
from qiskit_fermions.circuit.library import InitializeModes

# Qiskit addon SQD
from qiskit_addon_sqd.fermion import (
diagonalize_fermionic_hamiltonian,
SCIResult,
)

ตัวอย่างตัวจำลอง (simulator)​

ขั้นตอนที่ 1: แมปอินพุตแบบคลาสสิกเป็นปัญหาควอนตัม​

การอ่านและเตรียม FCIDump

สำหรับบทช่วยสอนนี้ เราจะโหลดแฮมิลโทเนียนโครงสร้างอิเล็กทรอนิกส์สำหรับไนโตรเจน (N2) ยังมีวิธีอื่นในการสร้างตัวดำเนินการแบบเฟอร์มิออนอีกด้วย ดูเอกสารที่ qiskit_fermions.operators.library

เกี่ยวกับ FCIDump นี้ ไฟล์ N2_sto_3g อธิบายโมเลกุลไนโตรเจน (N2N_2) ในเบซิส STO-3G ขั้นต่ำ ที่ระยะห่างระหว่างอะตอม 1.09 A˚\AA ซึ่งเป็นความยาวพันธะสมดุลจากการทดลอง เฮดเดอร์ของไฟล์ประกาศ NORB=10, NELEC=14, และ MS2=0 นั่นคือ 10 spatial orbitals (จึงมี 20 spin orbitals และ 20 qubits ภายใต้ Jordan-Wigner) 14 electrons ในสถานะ spin singlet ดังนั้นจึงมี 7 α\alpha และ 7 β\beta 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 = "assets/sqdrift/fcidump_files/N2_sto_3g"

if not os.path.exists(FCIDUMP_PATH):
os.makedirs(os.path.dirname(FCIDUMP_PATH), exist_ok=True)
with urlopen(FCIDUMP_URL) as response:
contents = response.read()
with open(FCIDUMP_PATH, "wb") as f:
f.write(contents)
print(f"Downloaded FCIDump to {FCIDUMP_PATH}")
else:
print(f"Using existing FCIDump at {FCIDUMP_PATH}")
Using existing FCIDump at assets/sqdrift/fcidump_files/N2_sto_3g
name = "assets/sqdrift/fcidump_files/N2_sto_3g"

fcidump = tools.fcidump.read(name)

# Extract metadata from the FCIDump header
norb = fcidump["NORB"] # number of spatial orbitals
nelec = fcidump["NELEC"] # total number of electrons
e_nuc = fcidump["ECORE"] # nuclear repulsion / core energy
ms2 = fcidump["MS2"] # 2S (spin)

num_elec_a = (nelec + ms2) // 2 # alpha electrons
num_elec_b = (nelec - ms2) // 2 # beta electrons

# Reconstruct full 4-index ERIs from the FCIDump (stored in 8-fold symmetry)
h1e = fcidump["H1"] # shape (norb, norb)
h2e = ao2mo.restore( # shape (norb, norb, norb, norb)
1, fcidump["H2"], norb
)

cisolver = fci.direct_spin1.FCI()
cisolver.max_cycle = 200
cisolver.conv_tol = 1e-12

e_fci, _ = cisolver.kernel(
h1e,
h2e,
norb,
(num_elec_a, num_elec_b),
ecore=e_nuc, # adds nuclear repulsion to the final energy
)

reference_energy = e_fci

print(f"Reference FCI Energy = {reference_energy:.10f} Ha")

nuclear_repulsion_energy = fcidump["ECORE"]
print(f"Nuclear Repulsion Energy = {nuclear_repulsion_energy:.10f} Ha")
Parsing assets/sqdrift/fcidump_files/N2_sto_3g
Reference FCI Energy = -107.6481842917 Ha
Nuclear Repulsion Energy = 23.7887003074 Ha

การโหลด 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 จำนวน nn ช่องถูกใช้ไปกับเทอมที่เคลื่อนย้าย population ระหว่าง configurations เทอมประเภทนี้ควรถูกกรองออกจาก Hamiltonian ในจุดนี้ ก่อนที่ gate Evolution จะถูกสร้างขึ้นในขั้นตอนถัดไป

เทอมที่เป็นประเด็นคือเทอมที่เป็นแนวทแยงใน occupation-number basis นั่นคือผลคูณของ number operators ai†aia^\dagger_i a_i มีเทอมสามประเภทที่เข้าข่ายลักษณะนี้:

  • ค่าออฟเซ็ตพลังงานคงที่ (constant energy offset) ซึ่งเป็นผลคูณของ number operators ศูนย์ตัว โดยการ time evolution ของมันส่งผลเพียงแค่ global phase

  • number operators แต่ละตัว nin_i ซึ่ง time evolution ของมันลดรูปเหลือเพียง single-qubit ZZ rotations

  • ผลคูณลำดับสูงกว่า (higher-order products) เช่น ninjn_i n_j

เมื่อพิจารณาเพียงลำพัง ไม่มีเทอมใดในนี้ที่เคลื่อนย้าย 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 α\alpha และ 7 β\beta 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 hih_i น้ำหนัก WhiW_{h_i} ถูกนิยามดังนี้:

Whi=∣ci∣/λW_{h_i} = |c_i| / \lambda

การปรับให้เหมาะกับ 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 จำนวน nn บิตไปยัง qubits จำนวน nn ตัวอย่างตรงไปตรงมา

  • 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 α\alpha และ 7 β\beta electrons เท่านั้น bitstrings ใดๆ ที่มีเลข 1 มากกว่าหรือน้อยกว่าเจ็ดตัวในครึ่งแรกและครึ่งหลังของ output สามารถถูกทิ้งได้ เรานิยามฟังก์ชันที่ตรวจสอบว่า bitstrings นั้นถูกต้องหรือไม่ และหากไม่ถูกต้องก็จะทิ้งมันไป เมื่อเรากรอง bitstrings ที่ผิดปกติออกแล้ว ที่เหลือจะถูกส่งเข้าสู่ diagonalization scheme ใช้แฟล็ก PRUNE ด้านล่างเพื่อสลับระหว่างพฤติกรรมทั้งสองแบบ

โปรดจำไว้ว่าการตัดทอนเป็นเพียงหนึ่งในหลายทางเลือกที่กำหนดรูปร่างของ subspace สุดท้าย เคียงคู่ไปกับจำนวน circuits ชุดของ evolution times และการกรองเทอมแนวทแยง การเปรียบเทียบการรันแบบตัดทอนกับแบบไม่ตัดทอนจะมีประโยชน์ก็ต่อเมื่อทุกอย่างอื่นถูกตรึงไว้คงที่เท่านั้น C++ companion อภิปรายเรื่องนี้ในรายละเอียดมากขึ้น เนื่องจากมันทำ postselect แทนที่จะ recover และยังแตกต่างกันในพารามิเตอร์อื่นๆ เหล่านั้นด้วย

name = "assets/sqdrift/fcidump_files/N2_sto_3g"

fcidump = tools.fcidump.read(name)

# Extract metadata from the FCIDump header
norb = fcidump["NORB"] # number of spatial orbitals
nelec = fcidump["NELEC"] # total number of electrons
e_nuc = fcidump["ECORE"] # nuclear repulsion / core energy
ms2 = fcidump["MS2"] # 2S (spin)

num_elec_a = (nelec + ms2) // 2 # alpha electrons
num_elec_b = (nelec - ms2) // 2 # beta electrons

# Reconstruct full 4-index ERIs from the FCIDump (stored in 8-fold symmetry)
h1e = fcidump["H1"] # shape (norb, norb)
h2e = ao2mo.restore( # shape (norb, norb, norb, norb)
1, fcidump["H2"], norb
)

cisolver = fci.direct_spin1.FCI()
cisolver.max_cycle = 200
cisolver.conv_tol = 1e-12

e_fci, _ = cisolver.kernel(
h1e,
h2e,
norb,
(num_elec_a, num_elec_b),
ecore=e_nuc, # adds nuclear repulsion to the final energy
)

reference_energy = e_fci

print(f"Reference FCI Energy = {reference_energy:.10f} Ha")

nuclear_repulsion_energy = fcidump["ECORE"]
print(f"Nuclear Repulsion Energy = {nuclear_repulsion_energy:.10f} Ha")

fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb

# Apply automatic grouping
canon = canonical_order(hamiltonian.normal_ordered().simplify(atol=1e-16))
exit_code = group_terms_by_electronic_structure(
canon, num_modes, two_body_physicist_order=False
)
filter_diagonal_terms(canon)

print(len(canon.groups))

# SqDRIFT parameters
times = [1.0, 10.0] # Total evolution times used for the subspace creation
num_exc = 10 # Number of excitation groups per circuit
num_circuits = 200 # Number of circuits to generate

init_circuits = []
hf_gate = InitializeModes.from_hartree_fock(norb, (num_elec_a, num_elec_b))

for time in times:
evo_gate = Evolution(num_modes, canon, time)
circ = FermionicCircuit(num_modes)
circ.append(hf_gate, circ.modes)
circ.append(evo_gate, circ.modes)
init_circuits.append(circ)

# Calculate weights for sampling (one per group)
qdrift = QDriftTrotterization(num_exc, rng=19)

pm = generate_preset_jw_pass_manager()
pm.optimization = FermionicPassManager([qdrift])

sqdrift_circuits = []
for circ in init_circuits:
sqdrift_circuits += (pm.run(circ) for _ in range(num_circuits))

for circ in sqdrift_circuits:
circ.measure_all()

print(len(sqdrift_circuits))

# This example assumes you have saved your IBM Quantum Platform account locally.
service = QiskitRuntimeService(channel="ibm_quantum_platform")

# Select backend (choose based on qubit requirements)
backend = service.least_busy(
operational=True,
simulator=False,
min_num_qubits=2 * norb,
)

print(f"Selected backend: {backend.name} ({backend.num_qubits} qubits)")

# Transpile for hardware
transpiled_circuits = transpile(
sqdrift_circuits,
backend=backend,
optimization_level=3,
seed_transpiler=42,
)

shots = 100

sampler = Sampler(mode=backend)

sampler.options.environment.job_tags = ["TUT-SqDRIFT"]

job = sampler.run(transpiled_circuits, shots=shots)
result = job.result()

# Extract counts from SamplerV2 results
all_counts = [pub_result.data.meas.get_counts() for pub_result in result]

# Set to True to filter out bitstrings that violate electron-number conservation
PRUNE = False

def is_valid_bitstring(
bitstring: str, norb: int, nelec: tuple[int, int]
) -> bool:
n_alpha, n_beta = nelec
return (
len(bitstring) == 2 * norb
and bitstring[norb:].count("1") == n_alpha
and bitstring[:norb].count("1") == n_beta
)

if PRUNE:
all_counts_filtered = []
for counts in all_counts:
filtered_count = {}
for key in counts:
if not is_valid_bitstring(key, norb, (num_elec_a, num_elec_b)):
continue
elif key not in filtered_count.keys():
filtered_count[key] = counts[key]
else:
filtered_count[key] += counts[key]
all_counts_filtered.append(filtered_count)
all_counts = all_counts_filtered

combined_counts = {}
for counts in all_counts:
for bitstring, count in counts.items():
combined_counts[bitstring] = combined_counts.get(bitstring, 0) + count

bit_array = BitArray.from_counts(combined_counts)
print(bit_array.num_shots)

print("Electron configuration:")
print(f" Total electrons: {nelec}")
print(f" Alpha electrons: {num_elec_a}")
print(f" Beta electrons: {num_elec_b}")
print(f" Number of orbitals: {norb}")
print(f" Number of spin orbitals (qubits): {2*norb}")
print(f"Integral shapes: h1e={h1e.shape}, h2e={h2e.shape}")

# SQD parameters
samples_per_batch = 300
num_batches = 3
max_iterations = 5

initial_occupancies = (
np.array([1] * num_elec_a + [0] * (norb - num_elec_a)), # alpha
np.array([1] * num_elec_b + [0] * (norb - num_elec_b)), # beta
)

result_history = []

def callback(results: list[SCIResult]):
result_history.append(results)
iteration = len(result_history)
print(f"Iteration {iteration}")
for i, result in enumerate(results):
print(f"\tSubsample {i}")
print(f"\t\tEnergy: {result.energy + nuclear_repulsion_energy}")
print(
f"\t\tSubspace dimension: {np.prod(result.sci_state.amplitudes.shape)}"
)

# Run SQD with configuration recovery
print("\nRunning SQD with configuration recovery...")
result = diagonalize_fermionic_hamiltonian(
h1e,
h2e,
bit_array,
samples_per_batch=samples_per_batch,
norb=norb,
nelec=(num_elec_a, num_elec_b),
num_batches=num_batches,
energy_tol=1e-3,
occupancies_tol=1e-3,
max_iterations=max_iterations,
initial_occupancies=initial_occupancies,
seed=42,
callback=callback,
)

computed_energy = result.energy + nuclear_repulsion_energy

print("FINAL SQD RESULTS")
print(f"Orbital occupancies (alpha): {result.orbital_occupancies[0]}")
print(f"Orbital occupancies (beta): {result.orbital_occupancies[1]}")

energy_error = abs(computed_energy - reference_energy)
print(f"Reference Energy: {reference_energy:.10f} Ha")
print(f"Computed Energy: {computed_energy:.10f} Ha")
print(f"Error: {energy_error:.10e} Ha")
Parsing assets/sqdrift/fcidump_files/N2_sto_3g
Reference FCI Energy = -107.6481842917 Ha
Nuclear Repulsion Energy = 23.7887003074 Ha
5060
400
Selected backend: ibm_aachen (156 qubits)
40000
Electron configuration:
Total electrons: 14
Alpha electrons: 7
Beta electrons: 7
Number of orbitals: 10
Number of spin orbitals (qubits): 20
Integral shapes: h1e=(10, 10), h2e=(10, 10, 10, 10)

Running SQD with configuration recovery...
Iteration 1
Subsample 0
Energy: -107.64593072647523
Subspace dimension: 7221
Subsample 1
Energy: -107.6458270048177
Subspace dimension: 7209
Subsample 2
Energy: -107.64007673117075
Subspace dimension: 7138
Iteration 2
Subsample 0
Energy: -107.64757372124944
Subspace dimension: 9009
Subsample 1
Energy: -107.64674060104392
Subspace dimension: 8245
Subsample 2
Energy: -107.64731360491942
Subspace dimension: 8178
Iteration 3
Subsample 0
Energy: -107.64765518770588
Subspace dimension: 8835
Subsample 1
Energy: -107.64767975712016
Subspace dimension: 8649
Subsample 2
Energy: -107.64761634415606
Subspace dimension: 8648
FINAL SQD RESULTS
Orbital occupancies (alpha): [0.99999504 0.9999964 0.99590318 0.9932359 0.96697158 0.96696295
0.99298797 0.03728154 0.03728186 0.00938359]
Orbital occupancies (beta): [0.9999946 0.99999641 0.99590413 0.99323077 0.96697361 0.96696174
0.99298424 0.03728121 0.03728169 0.00939159]
Reference Energy: -107.6481842917 Ha
Computed Energy: -107.6476797571 Ha
Error: 5.0453460619e-04 Ha

ขั้นตอนถัดไป​

คำแนะนำ

หากคุณพบว่างานนี้น่าสนใจ คุณอาจสนใจเนื้อหาต่อไปนี้:

  • Sample-based Krylov quantum diagonalization of a fermionic lattice model - ทูโทเรียลที่เกี่ยวข้องซึ่งใช้ time-evolution circuits แทน variational ansatz
  • Sample-based quantum diagonalization of a chemistry Hamiltonian - ทูโทเรียลเกี่ยวกับวิธีสร้าง local unitary cluster Jastrow (LUCJ) circuit สำหรับการจำลองทางเคมีควอนตัม
  • เปเปอร์ SqDRIFT - งานวิจัยที่ทูโทเรียลนี้อ้างอิงมา (โปรดทราบว่าการปรับให้เหมาะสมบางส่วนที่กล่าวถึงในเปเปอร์นี้กำลังอยู่ระหว่างดำเนินการอยู่ในปัจจุบัน และทูโทเรียลนี้อาจมีการเปลี่ยนแปลงในอนาคตตามวิวัฒนาการของไลบรารีที่ใช้)