바닥 상태 추정을 위한 SqDRIFT 알고리즘
- Python
- C++
이 튜토리얼을 직접 실행하려면 Python 노트북을 다운로드하세요. 노트북은 FCIDump 입력 파일이 아직 없으면 다운로드합니다.
사용량 추정치: Heron r3 프로세서에서 180초 (참고: 이는 추정치일 뿐입니다. 실제 실행 시간은 다를 수 있습니다.)
학습 목표
-
Trotterization과 비교하여 더 작은 깊이의 회로를 만드는 방법 배우기
-
qDRIFT와 SQD를 사용한 바닥상태 추정을 위한 종단 간 워크플로 따라하기
-
이러한 워크플로를 구현하기 위해
qiskit-fermions를 다른 Qiskit 애드온과 함께 사용하는 방법 배우기
사전 요구 사항
-
샘플 기반 양자 대각화(SQD) 개요 읽기
배경
SqDRIFT는 비트스트링을 샘플링할 안자츠를 선택해야 하는 필요성을, 목표 해밀토니안으로부터 직접 구성된 시간 발전 회로들의 앙상블로 대체하는 SKQD의 변형입니다. 이는 해밀토니안의 계수를 기반으로 더 작은 시간 발전 연산자를 해밀토니안으로부터 서브샘플링함으로써 달성되며, 이는 qDRIFT 트로터화 방법으로 알려져 있습니다.
두 구현 모두 Qiskit Fermions를 사용하여 분자 해밀토니안을 표현하고 qDRIFT 알고리즘을 위한 회로를 구성합니다. Python 구현은 페르미온 Transpiler 패스를 사용하고, C++ 구현은 그룹을 명시적으로 샘플링하여 큐비트 연산자로 매핑합니다.
해밀토니안이 다음과 같은 형태라고 하겠습니다.
여기서, 일반성을 잃지 않고 이며 의 최대 고유값이 절댓값으로 과 같다고 요구합니다. 부호나 복소수 형태의 앞자리 인수는 모두 에 흡수되므로, 계수 는 엄격하게 양의 가중치이고 는 각 항의 방향을 담당합니다. 여기서 은 해밀토니안 안의 항의 개수(또는 그룹화 후에는 그룹의 개수)이며, 이는 해밀토니안의 속성으로서 하나의 회로로 샘플링되는 연산자 개수와는 구별됩니다. 후자는 아래에서 으로 표기됩니다.
그러면 qDRIFT 알고리즘은 목표 시간 에 대해 어떤 연산자 를 구현하며, 여기서 는 까지 진행하고 번째 SqDRIFT 회로를 나타내며, 다음과 같이 정의됩니다.
여기서 은 회로당 샘플링된 연산자의 개수이고 는 앙상블 내 회로의 개수입니다. 이 곱은 전체 개의 해밀토니안 항이 아니라 번의 추출에 대해 실행되며, 항들이 복원 추출되기 때문에 동일한 가 하나의 안에 한 번 이상 나타날 수 있습니다.
다음 양은:
계수들의 노름이며, 따라서 어떤 항이 추출되었는지와 무관하게 개의 각 단계는 동일한 시간 동안 진화합니다. 단계 각도의 균일성이 qDRIFT의 특징적인 성질입니다. 계수는 그 항이 얼마나 멀리 회전되는지가 아니라 그 항이 얼마나 자주 추출되는지를 통해 결과에 영향을 미칩니다. 인덱스는 다음 분포로부터 샘플링됩니다.
따라서 수열 은 이 분포에서 추출된 항 인덱스들의 무작위 수열입니다. 가 양수이고 합이 이므로, 이는 정규화된 확률 분포이며, 무작위 추출에 대한 결과 채널의 기댓값은 아래에서의 발전을 근사하며, 그 오차는 이 커질수록 감소합니다. 근사 오차는 항의 개수 이 아니라 에 의존한다는 점에 유의하세요.
(SqDRIFT 논문에서는 항의 개수를 으로, 수열 길이를 으로 표기합니다. 여기서는 두 가지를 명확히 구별하기 위해 과 을 사용합니다.)
이 튜토리얼은 이러한 무작위화된 회로들의 앙상블을 생성하는 방법을 보여줍니다. 이러한 회로를 만든 후, 서로 다른 연산자에 대해 Krylov 부분공간을 만드는 방식과 유사하게, 서로 다른 시간 매개변수를 가진 여러 연산자로부터 비트스트링을 샘플링합니다. 이렇게 하면 바닥상태 벡터와 샘플링된 비트스트링 사이의 겹침이 더 커집니다.
요구 사항
이 튜토리얼을 시작하기 전에 다음이 설치되어 있는지 확인하세요
- 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 파일은 실험적 평형 결합 길이인 1.09 의 원자 간 거리에서 최소 STO-3G 기저를 사용한 질소 분자()를 나타냅니다. 헤더에는 NORB=10, NELEC=14, MS2=0이 선언되어 있습니다. 즉 10개의 공간 오비탈(따라서 20개의 스핀 오비탈, Jordan-Wigner 변환 하에서 20개의 큐비트), 스핀 단일항 상태의 14개의 전자, 즉 일곱 개의 전자와 일곱 개의 전자를 의미합니다. 모든 오비탈에는 대칭 라벨 1이 부여되어 있으며, 이는 점군 대칭이 활용되지 않는다는 것을 의미합니다. 전체 공간 STO-3G 덤프이므로 동결된 오비탈이 없으며, 상관 공간이 충분히 작아서 다음 코드 블록에서 보여주듯이 비교를 위한 정확한 FCI 기준 에너지를 고전적으로 계산할 수 있습니다.
동일한 파일을 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")
적분값이 수렴된 SCF 오비탈에 의존하기 때문에, 다시 생성된 파일은 제공된 파일과 오비탈 위상이나 순서가 다를 수 있습니다. 그러나 총 에너지에는 영향을 미치지 않습니다.
파일 가져오기. FCIDump는 이 GitHub 저장소에서 찾을 수 있습니다. 아래 코드를 실행하여 튜토리얼의 나머지 부분이 예상하는 위치로 파일을 가져올 수 있습니다.
먼저 pyscf에서 제공하는 cisolver를 사용하여 기준 에너지를 구합니다. 이는 우리가 다루는 분자의 실제 바닥 상태 에너지입니다. 이를 위해 먼저 오비탈 수와 전자 수를 각각 나타내는 norb와 nelec을 선언합니다. 그런 다음 일전자 및 이전자 적분을 각각 나타내는 h1e와 h2e를 선언합니다. 이들은 모두 나중에 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
해밀토니안 불러오기
필요한 데이터가 준비되었으므로, qiskit-fermions와 호환되는 형식으로 FCI 파일에서 해밀토니안을 읽어옵니다
fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb
qiskit-fermions를 이용한 페르미온 워크플로
먼저 페르미온 회로에 특화된 Transpiler 패스와 게이트를 제공하는 qiskit-fermions를 사용하여 해밀토니안을 페르미온 회로 모델로 매핑합니다. 이들은 나중에 이 워크플로에서 Qiskit의 기존 Transpiler 패스보다 먼저 사용됩니다.
항 그룹화
결과의 재현성을 보장하기 위해 먼저 canonical_order를 사용하여 항을 오직 그 구조에 따라서만 정렬합니다. 따라서 canon 목록 내 연산자의 순서는 고정됩니다. 앞으로 사용할 QDriftTrotterization 패스가 qDRIFT 연산자를 생성하기 위해 무작위 인덱스를 샘플링하기 때문에, 이 과정은 생성된 연산자의 재현성을 보장합니다.
이 단계에서는 동일한 계수를 가진 관련 항들을 그룹화하여 전자 구조 해밀토니안에 존재하는 많은 대칭성을 활용합니다. 이렇게 하면 qDRIFT 프로토콜이 샘플링하는 연산자 계수 분포가 변경되지만, 이는 수렴 보장에는 영향을 미치지 않습니다. 결정적으로, 대칭성에 의해 관련된 항을 그룹화하면 파울리 항의 유리한 상쇄가 일어나고, 이러한 항의 작용 하에 상태를 시간 전개할 때 전체적으로 더 짧은 회로 깊이를 얻을 수 있습니다.
qiskit-fermions는 이러한 그룹화를 수행해 주는 group_terms_by_electronic_structure 함수를 제공합니다.
group_terms_by_electronic_structure는 정규 순서화된 항을 가정한다는 점에 유의하세요.
대각 항 필터링
회로를 생성하는 데 사용되는 해밀토니안에서 대각 항을 제거하여, 개의 qDRIFT 샘플링 슬롯이 구성 간 점유(분포)를 이동시키는 항에 사용되도록 합니다. 이러한 항은 다음 단계에서 Evolution 게이트가 구성되기 전, 바로 이 시점에서 해밀토니안으로부터 걸러내는 것이 가장 좋습니다.
여기서 말하는 항은 점유수 기저(occupation-number basis)에서 대각인 항, 즉 수 연산자 의 곱입니다. 이 범주에는 세 가지 종류의 항이 있습니다.
-
수 연산자가 하나도 없는 곱인 상수 에너지 오프셋으로, 그 시간 전개는 전역 위상만을 기여합니다.
-
개별 수 연산자 로, 그 시간 전개는 단일 큐비트 회전으로 귀결됩니다.
-
와 같은 고차 곱입니다.
이들 자체만으로는 점유수 구성 간에 인구를 이동시키지 않습니다. 이미 존재하는 구성의 위상에만 작용할 뿐입니다. 하지만 이들이 아무 영향도 없는 것은 아닙니다. 이러한 상대 위상은 회로 뒷부분의 여기(excitation) 항이 만들어내는 간섭에 영향을 주기 때문에, 이를 필터링하면 실제로 생성되는 전개가 달라지고 샘플링 분포도 바뀔 수 있습니다. 이는 샘플링된 분포를 그대로 유지하는 단계가 아니라, 샘플링을 여기 항에 집중시키기 위해 회로 생성 단계에서 의도적으로 도입한 근사입니다. qDRIFT 수렴 보장을 그대로 유지하는 위의 대칭성 그룹화와는 달리, 이 필터는 전개되는 연산자 자체를 변경합니다. 따라서 회로는 더 이상 전체 해밀토니안 하의 전개를 근사하지 않으며, qDRIFT 오차 범위는 원래 연산자가 아닌 필터링된 연산자에 적용됩니다. 이는 회로가 구성을 제안하는 데 사용되는 샘플링 휴리스틱에 불과하기 때문에 여기서는 허용됩니다. 필터는 회로를 구성하는 데 사용되는 해밀토니안에만 적용되고, 이후의 고전적 대각화는 대각 항을 포함한 전체 해밀토니안을 사용하므로 에너지 추정치 자체에서 어떤 항도 손실되지 않습니다. SQD의 정확도는 이 고전적 단계에 달려 있으며, 이 단계는 구성이 어떻게 제안되었는지와 무관하게 샘플링된 부분공간 내에서 변분적으로 남아 있습니다.
filter_diagonal_terms() 함수는 연산자에서 이러한 항을 제자리에서 제거합니다. 이 함수는 정규 순서화된 구조, 즉 생성 모드의 다중집합이 소멸 모드의 다중집합과 일치하는지를 통해 이러한 항을 식별하므로, 이미 정규 순서화된 연산자에만 유효합니다. 이 가정은 실행 시점에 검사되지 않습니다.
# 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
해밀토니안의 항을 그룹화했으므로, 이제 회로 앙상블을 생성하기 위해 다음 매개변수를 결정합니다.
- 생성할 회로 수:
num_circuits - 여기 그룹 기준으로 본 각 회로의 길이:
num_exc - 서로 다른 전개 시간에 대한 인자:
times
페르미온 회로 생성
이제 각 시간 단계에 대해 페르미온 회로를 생성합니다. 각 회로는 앞서 선언한 전개 시간을 갖는 단일 전개 게이트로 구성됩니다. 전개 연산자는 해밀토니안입니다. 이후 이 회로들에 Transpiler 패스를 실행하여 qDRIFT 회로를 생성합니다.
안자츠 준비
InitializeModes 클래스를 사용하여 Hartree-Fock 상태를 준비합니다. 질소의 경우, 이 과정은 단순히 처음 num_elec_a개의 큐비트에 X 게이트를 적용한 다음 num_elec_b개의 큐비트에 X 게이트를 적용하는 것으로, 질소에서는 두 값 모두 7입니다. 이 상태는 질소의 일곱 개 전자와 일곱 개 전자를 나타냅니다.
# 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단계: 양자 하드웨어 실행을 위한 문제 최적화
회로가 준비되었으므로, 먼저 qiskit-fermions에서 제공되는 패스를 사용하여 페르미온 수준의 최적화를 수행한 다음, 선택한 백엔드에 맞게 회로를 트랜스파일합니다. 이는 시뮬레이터 실험이므로 먼저 AerSimulator에 대해 이를 수행합니다.
각 그룹의 가중치 계산
이 단계에서는 해밀토니안 내 계수에 비례하는 확률로 항을 확률적으로 qDRIFT 샘플링합니다. qDRIFT Transpiler 패스가 이를 대신 수행합니다. 이제 해밀토니안이 장거리 결합 및 2차보다 높은 차수의 항을 포함하더라도, 제한된 큐비트 연결성에도 불구하고 하드웨어에서 더 효율적으로 실행할 수 있는 더 얕은 회로를 생성할 수 있습니다. 항 그룹화 후, 가중치를 기반으로 연산자를 샘플링합니다. 각 연산자 에 대해 가중치 는 다음과 같이 정의됩니다.
1단계에서 항들을 그룹화했으므로, 여기서 각 는 하나의 전체 그룹입니다. 는 그룹 에 속한 항들의 계수 절댓값의 평균이며, 그룹의 각 항은 계수를 부호만 남긴 채 시간 진화됩니다.
페르미온 및 하드웨어 네이티브 최적화
generate_preset_jw_pass_manager() 함수는 FermionicCircuit을 입력받아 하드웨어에서 실행하기 위해 트랜스파일할 수 있는 최적화된 최종 회로를 생성하는 MultiStagePassManager를 반환합니다. 기본 최적화 단계를 QDriftTrotterization 패스를 포함하는 FermionicPassManager로 대체합니다.
-
QDriftTrotterization패스는 샘플링에 사용할 회로를 생성하기 위해 내부적으로 가중치 계산 및 샘플링을 사용합니다 -
RelabelModes패스는 큐비트 간 연결성을 최적화하고 게이트 깊이를 줄이기 위해 페르미온 모드를 순열할 수 있는 또 다른 최적화 패스입니다. 자세한 내용은 API 참조를 참고하세요
MultiStagePassManager의 나머지 단계는 자동으로 실행되며 페르미온-큐비트 매핑 전체를 처리합니다.
-
F2QLayout: 프리셋 PassManager는 개의 페르미온 비트를 개의 큐비트에 단순하게 매핑하는
TrivialF2QLayout패스를 적용합니다. -
F2QSynth: 페르미온 기반 회로 명령어를 큐비트 기반 명령어로 매핑하는 Transpiler 패스입니다.
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
페르미온 수준 최적화가 완료되었으므로, 이제 시뮬레이터에서 실행하기 위해 회로를 트랜스파일할 수 있습니다.
simulator = AerSimulator()
shots = 100
transpiled_circuits = transpile(sqdrift_circuits, simulator)
3단계: Qiskit 프리미티브를 사용하여 실행
회로가 준비되었으므로, 이제 AerSimulator에서 Qiskit 프리미티브를 사용하여 회로를 실행할 수 있습니다. 서로 다른 회로들의 모든 카운트를 결합합니다. 이를 불리언 벡터로 변환한 다음 마지막으로 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단계: 후처리 및 원하는 고전적 형식으로 결과 반환
SQD에 비트스트링 사용하기
이제 선택된 비트스트링에 대해 대각화 방식을 실행하여 분자의 바닥 상태 에너지에 해당하는 최소 고유값을 찾을 수 있습니다. 콜백 함수를 생성하고, 초기 점유 상태를 선언하고, 매개변수를 설정한 다음 마지막으로 대각화 방식을 실행합니다. 콜백 함수는 각 반복에서 현재 반복 횟수와 현재 고유값 추정치를 출력하는 데 사용됩니다.
마지막으로, 바닥 상태 추정치를 얻기 위해 결과 에너지에 nuclear_repulsion_energy를 더합니다.
참고: 부분공간의 차원은 노이즈가 없는 시뮬레이터에서도 반복마다 고정되지 않습니다. 각 서브샘플은 서로 다른 구성 집합을 추출하고, 복구 단계가 반복 사이에 풀(pool)을 재구성하기 때문에, 보고되는 차원은 서브샘플마다 달라집니다. 노이즈 없는 샘플링 자체만으로는 선택된 부분공간의 차원을 고정하지 않습니다. 반면 하드웨어 실행은 체계적으로 더 큰 부분공간을 얻는 경향이 있는데, 이는 노이즈가 있는 샷이 입자 수 대칭성을 깨뜨리고 구성 복구가 이를 추가적인 기저 벡터로 바꾸기 때문입니다. 이러한 이유로, 하드웨어 섹션에서는 비트스트링을 가지치기(pruning)하는 또 다른 단계도 소개할 것입니다.
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개의 큐비트(10개의 공간 오비탈)를 사용합니다. 이 선택은 빠르게 실행되어야 하는 튜토리얼을 위한 편의상의 선택일 뿐, 이 방법에 대한 엄격한 상한선은 아닙니다.
고전적 단계의 비용은 큐비트 수에 의해 직접 결정되지 않습니다. SQD는 샘플링된 구성들이 생성하는 부분공간에 투영된 해밀토니안을 대각화하므로, 고전적 비용을 좌우하는 것은 이 선택된 부분공간의 차원입니다 — 이는 여기서 samples_per_batch, num_batches, 그리고 회로가 실제로 생성하는 서로 다른 구성의 수에 의해 결정되며, 투영된 해밀토니안을 적용하는 데 필요한 희소 선형대수 연산도 함께 작용합니다. 전체 CI 공간은 오비탈과 전자 수에 따라 조합적으로 증가하지만, 선택된 부분공간은 그중 작고 조정 가능한 일부이며, 우리는 그 크기를 직접 제어합니다. 결과적으로 큐비트 수와 고전적 난이도는 어느 정도 독립적으로 조절할 수 있습니다. 즉, 더 넓은 오비탈 공간을 적당한 부분공간으로 샘플링하는 것이 매우 큰 부분공간에 대해 대각화하는 더 작은 시스템보다 더 저렴할 수 있습니다.
따라서 실제로는 실행 가능한 시스템 크기가 원하는 정확도에 필요한 부분공간 차원과, 고유값 해석기가 사용할 수 있는 메모리 및 코어 수에 달려 있습니다. 일반적으로 더 넓은 오비탈 공간은 화학적 정확도에 도달하기 위해 더 큰 부분공간을 필요로 하며, 이것이 결국 분산 자원을 필요로 하게 만드는 이유입니다. 이 단계를 확장하는 방법에 대해서는 qiskit-addon-sqd-hpc를 참고하세요. 고정된 상한을 가정하기보다는, 반복에 걸쳐 보고되는 부분공간 차원과 에너지 수렴을 관찰하면서 에너지가 더 이상 개선되지 않거나 사용 가능한 메모리를 모두 소진할 때까지 부분공간 크기를 늘리는 것이 실용적인 접근 방식입니다.
참고: 하드웨어의 노이즈로 인한 샘플링 오차 때문에, 하드웨어 실행에서 대각화를 위해 생성되는 부분공간은 시뮬레이터를 사용할 때보다 더 커집니다. 이는 대각화하려는 부분공간의 차원을 늘리지만, SQD가 노이즈에 대해 강건하기 때문에 이 워크플로는 여전히 정확한 답을 제공합니다.
허위 문자열 가지치기
여기서는 추가 단계를 수행할지 선택할 수 있습니다. 회로 실행으로부터 모든 비트스트링을 얻은 후, SQD를 실행하기 전에 유효하지 않은 비트스트링을 걸러내거나, 가지치기 없이 진행할 수 있습니다. 하드웨어 실행에서는 일반적으로 가지치기를 건너뛰는 것이 더 낫습니다. 대칭성이 깨진 샷을 그대로 두면 구성 복구가 이를 유효한 구성으로 복원할 수 있어, 해당 샷을 완전히 버리는 대신 부분공간을 넓힐 수 있기 때문입니다.
질소는 일곱 개의 전자와 일곱 개의 전자만 가질 수 있으므로, 출력의 앞쪽 절반과 뒤쪽 절반에서 1의 개수가 7개보다 많거나 적은 비트스트링은 버릴 수 있습니다. 비트스트링이 유효한지 확인하고, 유효하지 않으면 버리는 함수를 정의합니다. 허위 비트스트링을 걸러낸 후, 나머지는 대각화 방식으로 전달됩니다. 아래의 PRUNE 플래그를 사용하여 두 동작을 전환할 수 있습니다.
가지치기는 회로 수, 진화 시간 집합, 대각 항 필터링과 더불어 최종 부분공간을 형성하는 여러 선택 사항 중 하나일 뿐이라는 점을 기억하세요. 가지치기를 수행한 실행과 수행하지 않은 실행을 비교하는 것은 다른 모든 요소가 고정되어 있을 때만 의미가 있습니다. C++ 탭은 이 점을 더 자세히 다루는데, 이는 복구가 아니라 후선택(postselect)을 수행하며 다른 매개변수들도 다르기 때문입니다.
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 프로젝트, 빌드 지침은 Qiskit 문서 저장소의 docs/tutorials/assets/sqdrift/cpp 디렉터리에 있습니다. FCIDump 입력은 이웃한 fcidump_files 디렉터리에 있습니다. 이 튜토리얼을 직접 빌드하고 실행하려면 저장소를 클론하거나 두 디렉터리를 모두 다운로드하세요.
사용량 추정치: Heron r3 프로세서에서 180초 (참고: 이는 추정치일 뿐입니다. 실제 실행 시간은 다를 수 있습니다.)
학습 목표 (C++)
-
Trotterization과 비교하여 더 작은 깊이의 회로를 만드는 방법 배우기
-
qDRIFT와 SQD를 사용한 바닥상태 추정을 위한 종단 간 워크플로 따라하기
-
이러한 워크플로를 구현하기 위해
qiskit-fermions를 다른 Qiskit 애드온과 함께 사용하는 방법 배우기
사전 요구 사항 (C++)
-
샘플 기반 양자 대각화(SQD) 개요 읽기
배경 (C++)
SqDRIFT는 비트스트링을 샘플링할 안자츠를 선택해야 하는 필요성을, 목표 해밀토니안으로부터 직접 구성된 시간 발전 회로들의 앙상블로 대체하는 SKQD의 변형입니다. 이는 해밀토니안의 계수를 기반으로 더 작은 시간 발전 연산자를 해밀토니안으로부터 서브샘플링함으로써 달성되며, 이는 qDRIFT 트로터화 방법으로 알려져 있습니다.
두 구현 모두 Qiskit Fermions를 사용하여 분자 해밀토니안을 표현하고 qDRIFT 알고리즘을 위한 회로를 구성합니다. Python 구현은 페르미온 Transpiler 패스를 사용하고, C++ 구현은 그룹을 명시적으로 샘플링하여 큐비트 연산자로 매핑합니다.
해밀토니안이 다음과 같은 형태라고 하겠습니다.
여기서, 일반성을 잃지 않고 이며 의 최대 고유값이 절댓값으로 과 같다고 요구합니다. 부호나 복소수 형태의 앞자리 인수는 모두 에 흡수되므로, 계수 는 엄격하게 양의 가중치이고 는 각 항의 방향을 담당합니다. 여기서 은 해밀토니안 안의 항의 개수(또는 그룹화 후에는 그룹의 개수)이며, 이는 해밀토니안의 속성으로서 하나의 회로로 샘플링되는 연산자 개수와는 구별됩니다. 후자는 아래에서 으로 표기됩니다.
그러면 qDRIFT 알고리즘은 목표 시간 에 대해 어떤 연산자 를 구현하며, 여기서 는 까지 진행하고 번째 SqDRIFT 회로를 나타내며, 다음과 같이 정의됩니다.
여기서 은 회로당 샘플링된 연산자의 개수이고 는 앙상블 내 회로의 개수입니다. 이 곱은 전체 개의 해밀토니안 항이 아니라 번의 추출에 대해 실행되며, 항들이 복원 추출되기 때문에 동일한 가 하나의 안에 한 번 이상 나타날 수 있습니다.
다음 양은:
계수들의 노름이며, 따라서 어떤 항이 추출되었는지와 무관하게 개의 각 단계는 동일한 시간 동안 진화합니다. 단계 각도의 균일성이 qDRIFT의 특징적인 성질입니다. 계수는 그 항이 얼마나 멀리 회전되는지가 아니라 그 항이 얼마나 자주 추출되는지를 통해 결과에 영향을 미칩니다. 인덱스는 다음 분포로부터 샘플링됩니다.
따라서 수열 은 이 분포에서 추출된 항 인덱스들의 무작위 수열입니다. 가 양수이고 합이 이므로, 이는 정규화된 확률 분포이며, 무작위 추출에 대한 결과 채널의 기댓값은 아래에서의 발전을 근사하며, 그 오차는 이 커질수록 감소합니다. 근사 오차는 항의 개수 이 아니라 에 의존한다는 점에 유의하세요.
(SqDRIFT 논문에서는 항의 개수를 으로, 수열 길이를 으로 표기합니다. 여기서는 두 가지를 명확히 구별하기 위해 과 을 사용합니다.)
이 튜토리얼은 이러한 무작위화된 회로들의 앙상블을 생성하는 방법을 보여줍니다. 이러한 회로를 만든 후, 서로 다른 연산자에 대해 Krylov 부분공간을 만드는 방식과 유사하게, 서로 다른 시간 매개변수를 가진 여러 연산자로부터 비트스트링을 샘플링합니다. 이렇게 하면 바닥상태 벡터와 샘플링된 비트스트링 사이의 겹침이 더 커집니다.
요구 사항 (C++)
이 구현은 IBM Quantum® 하드웨어에서 실행되며 고전적 대각화에 외부 SBD 실행 파일을 사용합니다. Python 시뮬레이터 예제는 포함하지 않습니다.
C++ 빌드 지침에 따라 컴파일러, CMake, Rust, Boost, 그리고 Qiskit 의존성을 설치하세요. Rust는 rustup으로 설치하세요. Qiskit 의존성은 자체 Rust 버전을 고정하며 rustup이 이를 자동으로 다운로드하지만, Homebrew나 Linux 패키지 관리자로 설치한 Rust는 너무 오래된 버전일 수 있습니다. 마지막 SBD 단계에는 추가로 MPI, OpenMP, BLAS/LAPACK이 필요합니다. 이 구현은 현재 Windows를 지원하지 않습니다.
저장소의 전체 C++ 소스와 CMake 프로젝트를 사용하세요. 예제를 다운로드할 때는 인접한 fcidump_files 디렉터리를 함께 유지하세요. 문서 저장소의 체크아웃에서 다음 명령으로 빌드합니다. 첫 번째 빌드는 모든 의존성을 다운로드하고 컴파일하므로 몇 분이 걸립니다.
cd docs/tutorials/assets/sqdrift/cpp
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
빌드는 SqDRIFT 실행 파일을 build/가 아니라 docs/tutorials/assets/sqdrift/cpp(프로젝트 루트)에 기록합니다. ../fcidump_files/N2_sto_3g를 찾을 수 있도록 프로젝트 루트에서 ./SqDRIFT를 실행하세요. 이 탭의 다른 모든 명령도 별도로 언급하지 않는 한 프로젝트 루트에서 실행합니다. ./SqDRIFT가 공유 라이브러리 누락 오류로 실패하면 빌드 가이드의 플랫폼별 라이브러리 경로 지침을 따르세요. macOS에서는 보통 그것 없이도 실행 파일이 동작합니다.
설정 (C++)
아래 코드 조각은 전체 SqDRIFT.cpp 프로그램에서 발췌한 것입니다. 해당 파일을 하나의 프로그램으로 빌드하고 실행하세요.
SqDRIFT를 실행하기 전에 IBM Quantum API 토큰이 IBM Cloud Platform용으로 구성되어 있는지 확인하세요.
qiskit-ibm-runtime이 설치된 Python이 있다면, 올바른 형식의 구성 파일을 만드는 가장 쉬운 방법은 다음과 같습니다.
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 키는 IBM Quantum Platform에서 받으세요.
참고: 구성 파일이 없거나 형식이 잘못되면 깔끔한 오류가 아니라 시작 시 도움이 되지 않는 Rust unwrap() 패닉이 발생합니다. 문제가 생기면 키 이름(default-ibm-quantum-platform)과 채널 값(ibm_quantum_platform)을 다시 확인하세요.
구현 (C++)
1단계: 고전 입력을 양자 문제로 매핑하기 (C++)
분자 해밀토니안 불러오기
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)
FCIDump 파일에서 분자 해밀토니안을 불러옵니다. 큐비트 수는 오비탈 수의 두 배입니다(스핀 업과 스핀 다운).
해밀토니안 생성 및 정규 순서화
// Create and normal order Hamiltonian
QfFermionOperator* hamiltonian = qf_ferm_op_from_fcidump(fcidump);
QfFermionOperator* normal_ordered = qf_ferm_op_normal_ordered(hamiltonian, nullptr);
FCIDump 데이터를 페르미온 연산자로 변환하고, 연산자 구조를 단순화하기 위해 정규 순서화를 적용합니다.
전자 구조에 따라 항 그룹화
// 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
진화 회로를 구성하기 전에 관련된 해밀토니안 항들을 전자 구조에 따라 그룹화합니다.
샘플링 가중치 계산 후 정규화하여 큐비트 연산자로 매핑
여기서는 순서가 중요합니다. 정규화는 모든 계수를 제자리에서 단위 크기로 재조정하므로 샘플링 가중치를 먼저 계산해야 합니다. 정규화를 먼저 실행하면 모든 가 정확히 1.0으로 읽히고 각 그룹의 가중치는 항의 개수로 붕괴하여, 샘플링 분포가 물리적 계수가 아니라 그룹 크기에 의해 결정됩니다.
각 그룹의 가중치는 해당 항들의 계수 절댓값의 평균입니다. Python 탭의 QDriftTrotterization 패스는 그룹화된 해밀토니안에 같은 가중치를 사용합니다. 합을 대신 사용하면 가 그룹 크기에 따라 커져, 결과적으로 진화 시간 이 왜곡됩니다.
// 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
가중치 는 원래 계수에서 읽은, 그룹 의 항들에 대한 계수 절댓값의 평균입니다. 이후 정규화는 Jordan-Wigner 변환 전에 각 페르미온 항을 단위 크기로 재조정하여, 진화된 연산자에서 위상/부호만 보존합니다. 이는 qDRIFT의 정규화된 항 진화와 일치합니다. 계수의 크기는 를 통해 샘플링 확률을 결정하고, 회로 진화는 정규화된 항을 사용하므로 큰 계수가 두 번 계산되지 않습니다.
연산자 집합 샘플링
// 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...
가중 분포에서 샘플링한 10개의 그룹을 각각 포함하는 100개의 무작위 회로에 대한 연산자 집합을 추출합니다. 다음 단계에서는 이 집합들을 qDRIFT 회로 앙상블로 변환합니다.
2단계: qDRIFT 회로 구성 (C++)
C++ 버전에서 이 단계는 샘플링된 연산자 집합으로부터 회로를 구성합니다. 대상 백엔드를 위한 트랜스파일은 각 회로를 제출하기 직전인 3단계에서 이루어집니다.
// 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
전체 프로그램은 이후 회로 통계와 회로 0의 연산자 인덱스를 출력하지만, 여기서는 생략했습니다.
샘플링된 각 연산자에 대해 시간 인 Suzuki-Trotter 진화 회로를 만듭니다. 여기서 은 회로당 샘플링된 연산자 수이고 는 시간 간격입니다. 샘플링된 각 그룹은 매핑 전에 항별로 정규화되었으므로, 이 진화는 회전 중에 각 항의 위상/부호만 적용하며 크기는 샘플링 분포를 통해서만 기여합니다. Hartree-Fock 초기 상태는 큐비트 0-6과 10-16에 X 게이트를 적용하여 준비합니다(각 스핀 섹터에 전자 7개). 각 진화 회로의 명령어는 C++ API를 사용해 메인 회로에 수동으로 덧붙입니다.
3단계: Qiskit 프리미티브를 사용하여 실행 (C++)
std::cout << "\n Connecting to IBM Quantum Cloud..." << std::endl;
// Initialize IBM Quantum Compute Service using qiskit-cpp
Qiskit::service::QiskitRuntimeService service;
std::cout << "✓ Connected to IBM Quantum" << std::endl;
// Get backend
const std::string backend_name = "ibm_fez";
auto backend = service.backend(backend_name);
if (backend.name().empty()) {
std::cerr << "Backend " << backend_name
<< " is not available to this account." << std::endl;
return 1;
}
std::cout << "✓ Selected backend: " << backend.name() << std::endl;
// Create sampler primitive
const int32_t shots = 100; // Number of shots per circuit
Qiskit::primitives::BackendSamplerV2 sampler(backend, shots);
std::cout << "\n Submitting " << num_circuits << " circuits ("
<< shots << " shots each)..." << std::endl;
// Submit one job per circuit.
std::vector<std::shared_ptr<Qiskit::primitives::BasePrimitiveJob>> jobs;
for (int i = 0; i < num_circuits; i++) {
for (uint32_t q = 0; q < num_qubits; q++) {
circuits[i].measure(q, q);
}
Qiskit::circuit::QuantumCircuit transpiled_qc =
Qiskit::compiler::transpile(circuits[i], backend, 2, 1.0, 42);
std::vector<Qiskit::primitives::SamplerPub> pubs;
pubs.push_back(Qiskit::primitives::SamplerPub(transpiled_qc, shots));
jobs.push_back(sampler.run(pubs));
}
std::cout << "\n⏳ Waiting for " << num_circuits << " jobs to complete..." << std::endl;
// Collect results — poll each job and extract bitstrings
std::vector<boost::dynamic_bitset<>> all_bitstrings;
for (int i = 0; i < num_circuits; i++) {
while (!jobs[i]->in_final_state()) {
sleep(5);
}
if (jobs[i]->done()) {
auto result = jobs[i]->result();
if (result.size() > 0) {
auto& pub_result = result[0];
auto& bit_array = pub_result.data();
for (size_t s = 0; s < bit_array.num_shots(); s++) {
auto sample = bit_array[s].to_string();
boost::dynamic_bitset<> bs(num_qubits);
for (uint32_t bit = 0; bit < num_qubits; bit++) {
bs[bit] = (sample[num_qubits - 1 - bit] == '1');
}
all_bitstrings.push_back(bs);
}
}
} else {
std::cout << " Circuit " << (i+1) << " did not complete successfully" << std::endl;
}
}
std::cout << "\n✓ Collected " << all_bitstrings.size()
<< " total bitstrings from all circuits" << std::endl;
IBM Quantum에 연결하여 이름으로 백엔드를 선택하고, 해당 백엔드에 맞게 각 회로를 트랜스파일한 뒤, 회로마다 작업 하나를 제출합니다. 20큐비트 레지스터에서 각각 100샷인 Sampler 작업 100개입니다. 이후 각 작업이 끝날 때까지 폴링하고 result[0]에서 비트열을 수집합니다. 모든 작업은 QPU 시간을 사용하며 각각 별도로 대기열에서 기다리므로, 이 단계는 일괄 처리된 단일 작업보다 훨씬 오래 걸릴 것으로 예상하세요.
이 페이지에 인용된 하드웨어 결과는 backend_name을 기본값인 ibm_fez에서 변경하여 ibm_pittsburgh에서 실행한 것입니다(100개 회로 × 100샷). 저장된 샘플은 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
백엔드 변경: 다른 장치를 대상으로 하려면 SqDRIFT.cpp의 backend_name을 변경하세요. service.backends()는 계정에서 접근할 수 있는 장치를 나열합니다.
4단계: 후처리 및 원하는 고전적 형식으로 결과 반환 (C++)
비트열 후선택 및 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!
올바른 해밍 가중치(FCIDump의 NELEC 필드에서 도출한 n_alpha개의 스핀 업, n_beta개의 스핀 다운 전자)를 가진 비트열을 후선택하고, 스핀 대칭화를 사용해 이를 Configuration Interaction(CI) 문자열로 변환한 뒤, 이후의 Selected Basis Diagonalization을 위해 alphadets_from_sqd.txt에 기록합니다.
리소스 정리
// 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;
할당된 모든 메모리와 리소스를 적절히 해제합니다. C++ std::vector<QuantumCircuit>는 RAII를 통해 자동으로 정리됩니다. 큐비트 연산자, 페르미온 연산자, FCIDump 데이터는 수동으로 해제합니다.
이제 해밀토니안을 투영할 기저를 만들었으므로, 대각화 과정을 진행하여 바닥 상태 추정값을 얻을 수 있습니다. 프로젝트 루트인 docs/tutorials/assets/sqdrift/cpp에서 다음 명령을 실행하세요.
SBD diag 바이너리 빌드
CMake는 SBD를 소스 전용으로 deps/sbd/에 가져옵니다(메인 빌드는 의도적으로 이를 컴파일하지 않습니다). SBD 앱은 일반 Makefile을 사용하며, SBD 자체는 MPI, OpenMP, BLAS/LAPACK이 필요합니다 — 따라서 이들이 설치되고 앱의 Configuration 파일이 툴체인과 일치하기 전까지는 단순한 make만으로는 충분하지 않습니다.
Makefile은 같은 디렉터리의 Configuration 파일에서 컴파일러와 링크 플래그를 읽습니다. 업스트림에 포함된 버전은 특정 macOS/Homebrew 구성에 맞춰져 있어 일반적인 머신에서는 그대로 빌드되지 않습니다 — 특히 -I/opt/homebrew/opt/llvm/include에는 omp.h가 없고(버전별 clang 리소스 디렉터리에 있습니다), Apple의 clang++는 -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 ../../../..
빌드가 성공하면 컴파일/링크 줄 두 개가 출력되고 Makefile 옆에 diag 바이너리가 생깁니다. ld: warning: ignoring duplicate libraries: '-lomp' 경고 하나는 무해합니다.
Intel macOS: Homebrew의 접두사는 /opt/homebrew가 아니라 /usr/local이므로, 앞의 두 경로 모두에서 이를 대체하세요.
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 문자열 생성
이미 1~4단계에서 ./SqDRIFT를 실행했다면 alphadets_from_sqd.txt가 프로젝트 루트에 이미 있으므로 이 단계를 건너뛸 수 있습니다. 그렇지 않으면 프로젝트 루트에서 다음을 실행하세요.
./SqDRIFT
# Writes alphadets_from_sqd.txt in the project root
대각화 실행
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 파일의 경로--adetfile: 알파 결정자 파일(SqDRIFT에서 나온 CI 문자열)의 경로--method 0: 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 줄은 샘플링된 부분공간에 대한 바닥 상태 추정값입니다.
Python 예제와의 차이점
두 예제는 대칭성이 잘못된 비트열을 다루는 방식뿐 아니라 여러 측면에서 동시에 다르므로, 동일한 에너지를 낼 것으로 기대하지는 않습니다.
| C++ 탭 | Python 탭 | |
|---|---|---|
| 대칭성이 잘못된 비트열 | 해밍 가중치에 대한 postselect_bitstrings로 폐기 | 반복적 구성 복구로 보정 |
| 루프 구조 | 단일 패스: 샘플링 → 후선택 → 대각화 한 번 | 외부 루프: 대각화, 오비탈 점유도 읽기, 구성 복구, 재대각화 |
| 회로 수 | 100개 회로 (num_circuits = 100) | 400개 회로 (진화 시간당 200개) |
| 진화 시간 | 단일 (time_step = 1) | 두 시간, times = [1.0, 10.0] |
| 대각 항 | 샘플링된 연산자에 유지 | filter_diagonal_terms로 제거되어 샘플링 분포와 회로가 바뀜 |
| 항 순서 | 그룹화만 수행 | 재현성을 위해 그룹화 전에 canonical_order 적용 |
| 대각화 솔버 | SBD (diag, 외부 MPI/OpenMP 바이너리) | qiskit-addon-sqd 프로세스 내 솔버 |
| 백엔드 | 이름으로 고정 (backend_name = "ibm_fez", 이 페이지의 실행은 ibm_pittsburgh 사용) | 큐비트가 충분한 least_busy 운영 백엔드 |
| 트랜스파일 | 각 회로를 개별적으로, 최적화 수준 2 | 모든 회로를 함께, 최적화 수준 3 |
| 작업 제출 | 작업 100개, 각각 회로 하나 | 400개 회로 전체를 담은 Sampler 작업 하나 |
| 시뮬레이터 실행 | 없음, 하드웨어만 | 하드웨어 실행 전 잡음 없는 시뮬레이터 예제 |
두 버전 모두 같은 해밀토니안을 사용하고, 항을 전자 구조에 따라 그룹화하며, 각 그룹에 항의 계수 절댓값 평균으로 가중치를 두고, Hartree-Fock 상태에서 시작하며, 회로당 10개의 그룹과 100샷을 샘플링합니다.
각 차이는 샘플링된 부분공간에, 따라서 에너지에 독립적으로 영향을 줍니다. 더 큰 앙상블과 두 번째의 더 긴 진화 시간은 모두 Python 예제가 탐색하는 구성을 넓힙니다. 둘 이상의 시간에서 샘플링하는 것이 SqDRIFT가 저에너지 공간을 Krylov와 유사하게 포괄하도록 합니다. 대각 항을 필터링하면 샘플러가 뽑을 수 있는 연산자와 그 가중치가 바뀌므로, 두 실행은 서로 다른 분포를 샘플링합니다.
후선택은 엄격한 필터이며, 입자 수가 잘못된 샷은 즉시 버려집니다. 위 실행에서는 이로 인해 데이터 대부분이 폐기되어, 10000샷 중 1137샷만 (7,7) 해밍 가중치 검사를 통과했고(약 11%) 49개의 서로 다른 -결정자로 붕괴했습니다. 반면 구성 복구는 이전 대각화에서 얻은 평균 오비탈 점유도를 사용해 비트를 뒤집어 그러한 샷을 보정하며, 후선택이 버렸을 것을 재활용합니다. 이러한 피드백 때문에 본질적으로 반복적인 방식이 됩니다. qiskit-addon-sqd-hpc의 recover_configurations는 대각화가 실행된 후에만 존재하는 avg_occupancies 인수를 필요로 합니다.
이 C++ 실행은 49개의 -결정자로 이루어진 부분공간과 -107.5109 Ha의 에너지를 산출합니다. 10개 오비탈에 전자 7개를 배치하는 전체 -결정자 공간은 개 문자열뿐이므로, 120개를 모두 같은 diag 바이너리에 입력하면 정확한 FCI 에너지 -107.6482 Ha를 얻습니다. 따라서 이 실행은 120개의 -결정자 중 49개에 걸쳐 있으면서 정확한 값보다 약 0.1373 Ha 높습니다. SQD는 선택된 부분공간에서 변분적이므로 더 작은 부분공간은 에너지를 높이기만 할 수 있고, 따라서 불완전한 부분공간이 이 차이에 대한 가장 직접적인 해석입니다.
이 정확한 FCI 검사는 예제가 작기 때문에 가능할 뿐이며 SqDRIFT 워크플로의 일부가 아닙니다. SQD가 실제로 필요한 시스템 크기에서는 그러한 기준값이 존재하지 않습니다. 여기서는 이 실행이 공간의 얼마만큼을 복구했는지 정량화하는 용도로만 사용됩니다.
Python 예제와의 비교는 통제된 것이 아니므로, 차이를 구성 복구가 없기 때문만으로 돌려서는 안 됩니다. 회로 수, 단일 진화 시간, 대각 항 필터링, 하드웨어 잡음이 모두 동시에 다르며 모두 부분공간을 형성한다는 점에 유의하세요.
이 차이를 줄이기 위해 이 튜토리얼을 마친 후 시도해 볼 수 있는 다음 단계는 다음과 같습니다. shots, num_circuits, ops_per_circuit을 늘리고, 둘 이상의 time_step에서 샘플링하며, Qiskit::addon::sqd::recover_configurations 위에 복구 루프를 추가하세요.
다음 단계
-
페르미온 격자 모형의 샘플 기반 Krylov 양자 대각화 — 변분 앤사츠 대신 시간 진화 회로를 사용하는 관련 튜토리얼입니다.
-
화학 해밀토니안의 샘플 기반 양자 대각화 — 양자 화학 시뮬레이션을 위한 국소 유니터리 클러스터 Jastrow(LUCJ) 회로를 구성하는 방법에 대한 튜토리얼입니다.
-
SqDRIFT 논문 — 이 튜토리얼의 기반이 된 문헌입니다. (이 논문에서 논의된 일부 최적화는 현재 진행 중이며, 이 튜토리얼은 사용된 라이브러리의 발전에 따라 향후 변경될 수 있습니다.)